Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 20 additions & 40 deletions src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,183 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

namespace System.Formats.Tar.Tests;

public abstract class TarFile_ExtractToDirectory_Tests : TarTestsBase
{
protected abstract Task ExtractArchive(MemoryStream archive, string destinationDirectoryName, bool overwriteFiles, bool useOptions, bool async, CancellationToken cancellationToken = default);

public static IEnumerable<object[]> DestinationDirectory_OptionsAndBooleanData() => GetDataAndBooleanData(GetTwoBooleansData());

public static IEnumerable<object[]> DestinationDirectory_TestDataAndBooleanData() => GetDataAndBooleanData(DestinationDirectory_TestData());

public static IEnumerable<object[]> DestinationDirectory_TestData()
{
foreach (bool overwriteFiles in new[] { false, true })
{
foreach (bool useOptions in new[] { false, true })
{
foreach (int missingDirectoryCount in new[] { 0, 1, 3 })
{
foreach (string entryName in new[] { null, "", "file.txt", "nested/child/file.txt", "nested/empty/", "./" })
{
yield return new object[] { overwriteFiles, useOptions, missingDirectoryCount, entryName };
}
}
}
}
}

[Theory]
[MemberData(nameof(DestinationDirectory_TestDataAndBooleanData))]
public async Task DestinationDirectory_CreatedAsNeeded(bool overwriteFiles, bool useOptions, int missingDirectoryCount, string entryName, bool async)
{
using TempDirectory root = new TempDirectory();
string destination = root.Path;
for (int i = 0; i < missingDirectoryCount; i++)
{
destination = Path.Join(destination, "missing");
}

using MemoryStream archive = CreateArchive(entryName);
await ExtractArchive(archive, destination, overwriteFiles, useOptions, async);

Assert.True(archive.CanRead);
if (string.IsNullOrEmpty(entryName))
{
Assert.Equal(missingDirectoryCount == 0, Directory.Exists(destination));
Assert.Empty(Directory.GetFileSystemEntries(root.Path));
}
else if (entryName.EndsWith('/'))
{
Assert.True(Directory.Exists(Path.Join(destination, entryName)));
}
else
{
Assert.Equal("archive contents", File.ReadAllText(Path.Join(destination, entryName)));
}
}

[Theory]
[MemberData(nameof(DestinationDirectory_OptionsAndBooleanData))]
public async Task DestinationDirectory_FileCollision(bool overwriteFiles, bool useOptions, bool async)
{
using TempDirectory root = new TempDirectory();
string filePath = Path.Join(root.Path, "file.txt");
File.WriteAllText(filePath, "original contents");
using MemoryStream archive = CreateArchive("file.txt");

if (overwriteFiles)
{
await ExtractArchive(archive, root.Path, overwriteFiles, useOptions, async);
Assert.Equal("archive contents", File.ReadAllText(filePath));
}
else
{
await Assert.ThrowsAsync<IOException>(() => ExtractArchive(archive, root.Path, overwriteFiles, useOptions, async));
Assert.Equal("original contents", File.ReadAllText(filePath));
}

Assert.True(archive.CanRead);
}

[Theory]
[MemberData(nameof(DestinationDirectory_OptionsAndBooleanData))]
public async Task DestinationDirectory_FileInRequiredPath_Throws(bool overwriteFiles, bool useOptions, bool async)
{
using TempDirectory root = new TempDirectory();
string filePath = Path.Join(root.Path, "file");
File.WriteAllText(filePath, "original contents");

foreach (string destination in new[] { filePath, Path.Join(filePath, "missing") })
{
using MemoryStream archive = CreateArchive("file.txt");
await Assert.ThrowsAnyAsync<IOException>(() => ExtractArchive(archive, destination, overwriteFiles, useOptions, async));
Assert.True(archive.CanRead);
Assert.Equal("original contents", File.ReadAllText(filePath));
}
}

[Theory]
[MemberData(nameof(DestinationDirectory_OptionsAndBooleanData))]
public async Task DestinationDirectory_Traversal_Throws(bool overwriteFiles, bool useOptions, bool async)
{
using TempDirectory root = new TempDirectory();
string destination = Path.Join(root.Path, "missing");

foreach (TarEntryType entryType in new[] { TarEntryType.RegularFile, TarEntryType.SymbolicLink, TarEntryType.HardLink })
{
using MemoryStream archive = new MemoryStream();
using (TarWriter writer = new TarWriter(archive, leaveOpen: true))
{
PaxTarEntry entry = new PaxTarEntry(entryType, entryType == TarEntryType.RegularFile ? "../outside.txt" : "link");
if (entryType is TarEntryType.SymbolicLink or TarEntryType.HardLink)
{
entry.LinkName = "../outside.txt";
}
writer.WriteEntry(entry);
}
archive.Position = 0;

await Assert.ThrowsAsync<IOException>(() => ExtractArchive(archive, destination, overwriteFiles, useOptions, async));
Assert.True(archive.CanRead);
Assert.Empty(Directory.GetFileSystemEntries(root.Path));
}
}

[Theory]
[MemberData(nameof(GetTwoBooleansData))]
public async Task DestinationDirectory_InvalidPath_Throws(bool useOptions, bool async)
{
using MemoryStream archive = CreateArchive(entryName: null);
await Assert.ThrowsAsync<ArgumentException>(() => ExtractArchive(archive, "\0", overwriteFiles: false, useOptions, async));
Assert.True(archive.CanRead);
}

[Theory]
[MemberData(nameof(GetBooleanData))]
public async Task ExtractToDirectoryAsync_Cancel(bool useOptions)
{
using TempDirectory root = new TempDirectory();
using CancellationTokenSource cs = new CancellationTokenSource();
cs.Cancel();
using MemoryStream archive = new MemoryStream();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => ExtractArchive(archive, Path.Join(root.Path, "missing", "directory"), overwriteFiles: true, useOptions, async: true, cs.Token));
Assert.True(archive.CanRead);
Assert.Equal(0, archive.Position);
Assert.Empty(Directory.GetFileSystemEntries(root.Path));
}

private static MemoryStream CreateArchive(string entryName)
{
MemoryStream archive = new MemoryStream();
if (entryName == "")
{
// An empty archive may contain just the two end-of-archive records.
archive.SetLength(1024);
}
else if (entryName is not null)
{
using TarWriter writer = new TarWriter(archive, leaveOpen: true);
bool isDirectory = entryName.EndsWith('/');
PaxTarEntry entry = new PaxTarEntry(isDirectory ? TarEntryType.Directory : TarEntryType.RegularFile, entryName);
using MemoryStream contents = new MemoryStream(Encoding.UTF8.GetBytes("archive contents"));
if (!isDirectory)
{
entry.DataStream = contents;
}
writer.WriteEntry(entry);
}
archive.Position = 0;
return archive;
}

// TarEntryFormat, TarEntryType, string fileName
public static IEnumerable<object[]> GetExactRootDirMatchCases()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace System.Formats.Tar.Tests
{
public partial class TarFile_ExtractToDirectory_File_Tests : TarTestsBase
public partial class TarFile_ExtractToDirectory_File_Tests : TarFile_ExtractToDirectory_Tests
{
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotPrivilegedProcess))]
[MemberData(nameof(GetBooleanData))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace System.Formats.Tar.Tests
{
public partial class TarFile_ExtractToDirectory_File_Tests : TarTestsBase
public partial class TarFile_ExtractToDirectory_File_Tests : TarFile_ExtractToDirectory_Tests
{
[Theory]
[MemberData(nameof(GetBooleanData))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,28 @@

namespace System.Formats.Tar.Tests
{
public partial class TarFile_ExtractToDirectory_File_Tests : TarTestsBase
public partial class TarFile_ExtractToDirectory_File_Tests : TarFile_ExtractToDirectory_Tests
{
[Fact]
public Task ExtractToDirectoryAsync_Cancel()
protected override Task ExtractArchive(MemoryStream archive, string destinationDirectoryName, bool overwriteFiles, bool useOptions, bool async, CancellationToken cancellationToken = default)
{
CancellationTokenSource cs = new CancellationTokenSource();
cs.Cancel();
return Assert.ThrowsAsync<TaskCanceledException>(() => TarFile.ExtractToDirectoryAsync("file.tar", "directory", overwriteFiles: true, cs.Token));
string archivePath = GetTestFilePath();
File.WriteAllBytes(archivePath, archive.ToArray());
if (async)
{
return useOptions
? TarFile.ExtractToDirectoryAsync(archivePath, destinationDirectoryName, new TarExtractOptions { OverwriteFiles = overwriteFiles }, cancellationToken)
: TarFile.ExtractToDirectoryAsync(archivePath, destinationDirectoryName, overwriteFiles, cancellationToken);
}

if (useOptions)
{
TarFile.ExtractToDirectory(archivePath, destinationDirectoryName, new TarExtractOptions { OverwriteFiles = overwriteFiles });
}
else
{
TarFile.ExtractToDirectory(archivePath, destinationDirectoryName, overwriteFiles);
}
return Task.CompletedTask;
}

[Theory]
Expand All @@ -44,20 +58,6 @@ public async Task NonExistentFile_Throws(bool async)
await Assert.ThrowsAsync<FileNotFoundException>(() => ExtractToDirectory(filePath, dirPath, overwriteFiles: false, async));
}

[Theory]
[MemberData(nameof(GetBooleanData))]
public async Task NonExistentDirectory_Throws(bool async)
{
using TempDirectory root = new TempDirectory();

string filePath = Path.Join(root.Path, "file.tar");
string dirPath = Path.Join(root.Path, "dir");

File.Create(filePath).Dispose();

await Assert.ThrowsAsync<DirectoryNotFoundException>(() => ExtractToDirectory(filePath, dirPath, overwriteFiles: false, async));
}

[Theory]
[MemberData(nameof(GetBooleanData))]
public async Task SetsLastModifiedTimeOnExtractedFiles(bool async)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ namespace System.Formats.Tar.Tests
{
public class TarFile_ExtractToDirectory_Stream_Tests : TarFile_ExtractToDirectory_Tests
{
protected override Task ExtractArchive(MemoryStream archive, string destinationDirectoryName, bool overwriteFiles, bool useOptions, bool async, CancellationToken cancellationToken = default)
{
if (async)
{
return useOptions
? TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName, new TarExtractOptions { OverwriteFiles = overwriteFiles }, cancellationToken)
: TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName, overwriteFiles, cancellationToken);
}

if (useOptions)
{
TarFile.ExtractToDirectory(archive, destinationDirectoryName, new TarExtractOptions { OverwriteFiles = overwriteFiles });
}
else
{
TarFile.ExtractToDirectory(archive, destinationDirectoryName, overwriteFiles);
}
return Task.CompletedTask;
}

public static IEnumerable<object[]> GetLinkEntryTypesAndBooleanData() => GetDataAndBooleanData(new[]
{
new object[] { TarEntryType.SymbolicLink },
Expand All @@ -35,15 +55,6 @@ public static IEnumerable<object[]> GetNormalizedPathAndBooleanData() => GetData
new object[] { "subdir/./readme.txt", Path.Join("subdir", "readme.txt") }
});

[Fact]
public async Task ExtractToDirectoryAsync_Cancel()
{
CancellationTokenSource cs = new CancellationTokenSource();
cs.Cancel();
using MemoryStream archiveStream = new MemoryStream();
await Assert.ThrowsAsync<TaskCanceledException>(() => TarFile.ExtractToDirectoryAsync(archiveStream, "directory", overwriteFiles: true, cs.Token));
}

[Theory]
[MemberData(nameof(GetBooleanData))]
public Task NullStream_Throws(bool async) =>
Expand All @@ -67,17 +78,6 @@ public async Task UnreadableStream_Throws(bool async)
await Assert.ThrowsAsync<ArgumentException>(() => ExtractToDirectory(unreadable, destinationDirectoryName: "path", overwriteFiles: false, async));
}

[Theory]
[MemberData(nameof(GetBooleanData))]
public async Task NonExistentDirectory_Throws(bool async)
{
using TempDirectory root = new TempDirectory();
string dirPath = Path.Join(root.Path, "dir");

using MemoryStream archive = new MemoryStream();
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => ExtractToDirectory(archive, destinationDirectoryName: dirPath, overwriteFiles: false, async));
}

[Theory]
[MemberData(nameof(GetBooleanData))]
public async Task ExtractEntry_ManySubfolderSegments_NoPrecedingDirectoryEntries(bool async)
Expand Down
Loading