Skip to content
Draft
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
2 changes: 1 addition & 1 deletion external/Xamarin.MacDev
214 changes: 153 additions & 61 deletions msbuild/Xamarin.MacDev.Tasks/Decompress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,30 @@ public static bool IsCompressed (string path)
/// <returns>True if successfully decompressed, false otherwise.</returns>
public static bool TryDecompress (TaskLoggingHelper log, string zip, string resource, string decompressionDir, List<string> createdFiles, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? decompressedResource)
{
decompressedResource = Path.Combine (decompressionDir, resource);
return TryDecompress (log, zip, resource, decompressionDir, null, null, createdFiles, cancellationToken, out decompressedResource);
}

/// <summary>
/// Extracts the specified resource (may be either a file or a directory) from the given zip file.
/// A stamp file will be created to avoid re-extracting unnecessarily.
///
/// Fails if:
/// * The resource is or contains a symlink and we're executing on Windows.
/// * The resource isn't found inside the zip file.
/// </summary>
/// <param name="log"></param>
/// <param name="zip">The zip to search in</param>
/// <param name="resource">The relative path inside the zip to extract (may be a file or a directory).</param>
/// <param name="decompressionDir">The location on disk to store the extracted results</param>
/// <param name="decompressionName">The name of the extracted resource (will be combined with <see paramref="decompressionDir"/>). The default is <see paramref="resource" />.</param>
/// <param name="cancellationToken">The cancellation token (if any=</param>
/// <param name="decompressedResource">The location on disk to the extracted resource</param>
/// <returns>True if successfully decompressed, false otherwise.</returns>
public static bool TryDecompress (TaskLoggingHelper log, string zip, string resource, string decompressionDir, string? decompressionName, UnzipFilter? filter, List<string> createdFiles, CancellationToken? cancellationToken, [NotNullWhen (true)] out string? decompressedResource)
{
if (string.IsNullOrEmpty (decompressionName))
decompressionName = resource;
decompressedResource = Path.Combine (decompressionDir, decompressionName);

var stampFile = decompressedResource.TrimEnd ('\\', '/') + ".stamp";

Expand All @@ -105,11 +128,11 @@ public static bool TryDecompress (TaskLoggingHelper log, string zip, string reso

bool rv;
if (Environment.OSVersion.Platform == PlatformID.Win32NT) {
rv = TryDecompressUsingSystemIOCompression (log, zip, resource, decompressionDir, cancellationToken);
rv = TryDecompressUsingSystemIOCompression (log, zip, resource, decompressionDir, filter, cancellationToken);
} else if (!string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("XAMARIN_USE_SYSTEM_IO_COMPRESSION"))) {
rv = TryDecompressUsingSystemIOCompression (log, zip, resource, decompressionDir, cancellationToken);
rv = TryDecompressUsingSystemIOCompression (log, zip, resource, decompressionDir, filter, cancellationToken);
} else {
rv = TryDecompressUsingUnzip (log, zip, resource, decompressionDir, cancellationToken);
rv = TryDecompressUsingUnzip (log, zip, resource, decompressionDir, filter, cancellationToken);
}

if (rv) {
Expand All @@ -132,41 +155,21 @@ public static bool TryDecompress (TaskLoggingHelper log, string zip, string reso
// The dir separator character in zip files is always "/", even on Windows
const char zipDirectorySeparator = '/';

static bool TryDecompressUsingUnzip (TaskLoggingHelper log, string zip, string resource, string decompressionDir, CancellationToken? cancellationToken)
{
Directory.CreateDirectory (decompressionDir);
var args = new List<string> {
"-u", "-o",
"-d", decompressionDir,
zip,
};

if (!string.IsNullOrEmpty (resource)) {
using var archive = ZipFile.OpenRead (zip);
resource = resource.Replace ('\\', zipDirectorySeparator);
var entry = archive.GetEntry (resource);
if (entry is null) {
entry = archive.GetEntry (resource + zipDirectorySeparator);
if (entry is null) {
log.LogError (MSBStrings.E7112 /* Could not find the file or directory '{0}' in the zip file '{1}'. */, resource, zip);
return false;
}
}

var zipPattern = entry.FullName;
if (zipPattern.Length > 0 && zipPattern [zipPattern.Length - 1] == zipDirectorySeparator) {
zipPattern += "*";
}

args.Add (zipPattern);
}
/// <summary>
/// A filter to determine whether an entry in a zip file should be extracted or not.
/// Returns the relative target path for the entry (relative to the target directory).
/// </summary>
/// <param name="entryPath">The name of the entry inside the zip file. The path separator will always be '/'.</param>
/// <param name="isDirectory">Whether the entry is a directory.</param>
/// <returns></returns>
public delegate string? UnzipFilter (string entryPath, bool isDirectory);

var rv = XamarinTask.ExecuteAsync (log, "unzip", args, cancellationToken: cancellationToken).Result;
return rv.ExitCode == 0;
}
delegate bool DecompressImplementation (TaskLoggingHelper log, string zip, ZipArchiveEntry entry, string targetPath, CancellationToken? cancellationToken);

static bool TryDecompressUsingSystemIOCompression (TaskLoggingHelper log, string zip, string resource, string decompressionDir, CancellationToken? cancellationToken)
static bool TryDecompressFiltered (TaskLoggingHelper log, string zip, string resource, string decompressionDir, UnzipFilter? filter, DecompressImplementation decompress, CancellationToken? cancellationToken)
{
log.LogMessage (MessageImportance.Low, $"TryDecompressFiltered (zip={zip}, resource={resource}, decompressionDir={decompressionDir})\n{Environment.StackTrace}");

var rv = true;

// canonicalize input
Expand All @@ -182,38 +185,35 @@ static bool TryDecompressUsingSystemIOCompression (TaskLoggingHelper log, string
if (entryPath.Length == 0)
continue;

if (string.IsNullOrEmpty (resource)) {
// an empty resource means extract everything, so we want this
} else if (entryPath.StartsWith (resourceAsDir, StringComparison.Ordinal)) {
var isDir = entryPath [entryPath.Length - 1] == zipDirectorySeparator;
var canonicalizedEntryPath = entryPath.Replace (zipDirectorySeparator, Path.DirectorySeparatorChar);

if (string.IsNullOrEmpty (resource) || canonicalizedEntryPath == resource || canonicalizedEntryPath.StartsWith (resourceAsDir, StringComparison.Ordinal)) {
// yep, we want this entry
} else if (entryPath == resource) {
// we want this one too
} else {
log.LogMessage (MessageImportance.Low, "Did not extract {0} because it didn't match the resource {1}", entryPath, resource);
log.LogMessage (MessageImportance.Low, "Did not extract {0} because it didn't match the resource {1}", canonicalizedEntryPath, resource);
// but otherwise nope
continue;
}

// Check if the file or directory is a symlink, and show an error if so. Symlinks are only supported
// on non-Windows platforms.
var entryAttributes = ((uint) GetExternalAttributes (entry)) >> 16;
const uint S_IFLNK = 0xa000; // #define S_IFLNK 0120000 /* symbolic link */
var isSymlink = (entryAttributes & S_IFLNK) == S_IFLNK;
if (isSymlink) {
log.LogError (MSBStrings.E7113 /* Can't process the zip file '{0}' on this platform: the file '{1}' is a symlink. */, zip, entryPath);
rv = false;
var relativeTargetPath = filter is null ? canonicalizedEntryPath : filter (canonicalizedEntryPath, isDir);
if (string.IsNullOrEmpty (relativeTargetPath)) {
log.LogMessage (MessageImportance.Low, "Did not extract {0} because the filter filtered it out.", entryPath);
// but otherwise nope
continue;
}

var isDir = entryPath [entryPath.Length - 1] == zipDirectorySeparator;
var targetPath = Path.Combine (decompressionDir, entryPath.Replace (zipDirectorySeparator, Path.DirectorySeparatorChar));
// canonicalize the target path
var targetPath = Path.GetFullPath (Path.Combine (decompressionDir, relativeTargetPath));

log.LogMessage (MessageImportance.Low, "Extracting '{0}' to '{1}' => '{2}'.", entryPath, relativeTargetPath, targetPath);

// canonicalize the path
targetPath = Path.GetFullPath (targetPath);

// validate that the unzipped file is inside the target directory
var decompressionDirectoryPath = decompressionDir.Trim (Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
var decompressionDirectoryPath = decompressionDir.TrimEnd (Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (!targetPath.StartsWith (decompressionDirectoryPath)) {
log.LogMessage (MessageImportance.Low, $"targetPath: {targetPath}");
log.LogMessage (MessageImportance.Low, $"decompressionDirectoryPath: {decompressionDirectoryPath}");
log.LogWarning (7144, null, MSBStrings.W7144 /* Did not extract {0} because it would write outside the target directory. */, entryPath);
continue;
}
Expand All @@ -222,18 +222,110 @@ static bool TryDecompressUsingSystemIOCompression (TaskLoggingHelper log, string
Directory.CreateDirectory (targetPath);
} else {
Directory.CreateDirectory (Path.GetDirectoryName (targetPath));
using var streamWrite = File.OpenWrite (targetPath);
using var streamRead = entry.Open ();
File.Delete (targetPath);
if (!decompress (log, zip, entry, targetPath, cancellationToken)) {
rv = false;
continue;
}
log.LogMessage (MessageImportance.Low, "Extracted {0} into {1}", entryPath, targetPath);
}
}

return rv;
}

static bool DecompressFileEntryWithStream (TaskLoggingHelper log, string zip, ZipArchiveEntry entry, string targetPath, CancellationToken? cancellationToken)
{
// Check if the file or directory is a symlink, and show an error if so. Symlinks are only supported
// on non-Windows platforms.
var entryPath = entry.FullName;
var entryAttributes = ((uint) GetExternalAttributes (entry)) >> 16;
const uint S_IFLNK = 0xa000; // #define S_IFLNK 0120000 /* symbolic link */
var isSymlink = (entryAttributes & S_IFLNK) == S_IFLNK;
if (isSymlink) {
log.LogError (MSBStrings.E7113 /* Can't process the zip file '{0}' on this platform: the file '{1}' is a symlink. */, zip, entryPath);
return false;
}

using var streamWrite = File.OpenWrite (targetPath);
using var streamRead = entry.Open ();
#if NET
streamRead.CopyToAsync (streamWrite, cancellationToken ?? CancellationToken.None).Wait ();
streamRead.CopyToAsync (streamWrite, cancellationToken ?? CancellationToken.None).Wait ();
#else
streamRead.CopyToAsync (streamWrite, 81920 /* default buffer size according to docs */, cancellationToken ?? CancellationToken.None).Wait ();
streamRead.CopyToAsync (streamWrite, 81920 /* default buffer size according to docs */, cancellationToken ?? CancellationToken.None).Wait ();
#endif
log.LogMessage (MessageImportance.Low, "Extracted {0} into {1}", entryPath, targetPath);
return true;
}

static bool DecompressFileEntryWithUnzip (TaskLoggingHelper log, string zip, ZipArchiveEntry entry, string targetPath, CancellationToken? cancellationToken)
{
// Check if the file or directory is a symlink, and show an error if so. Symlinks are only supported
// on non-Windows platforms.
var entryPath = entry.FullName;
var targetDirectory = Path.GetDirectoryName (targetPath);

var args = new List<string> {
"-u", "-o", "-j",
"-d", targetDirectory,
zip,
entryPath,
};

var rv = XamarinTask.ExecuteAsync (log, "unzip", args, cancellationToken: cancellationToken).Result;
if (rv.ExitCode != 0)
return false;

if (entry.Name != Path.GetFileName (targetPath))
File.Move (Path.Combine (targetDirectory, entry.Name), targetPath);

return true;
}

static bool TryDecompressUsingUnzip (TaskLoggingHelper log, string zip, string resource, string decompressionDir, UnzipFilter? filter, CancellationToken? cancellationToken)
{
if (filter is null)
return TryDecompressUsingUnzip (log, zip, resource, decompressionDir, cancellationToken);

return TryDecompressFiltered (log, zip, resource, decompressionDir, filter, DecompressFileEntryWithUnzip, cancellationToken);
}

// Does not support filtering nor extracting partial contents into a custom directory hierarchy.
static bool TryDecompressUsingUnzip (TaskLoggingHelper log, string zip, string resource, string decompressionDir, CancellationToken? cancellationToken)
{
Directory.CreateDirectory (decompressionDir);
var args = new List<string> {
"-u", "-o",
"-d", decompressionDir,
zip,
};

if (!string.IsNullOrEmpty (resource)) {
using var archive = ZipFile.OpenRead (zip);
resource = resource.Replace ('\\', zipDirectorySeparator);
var entry = archive.GetEntry (resource);
if (entry is null) {
entry = archive.GetEntry (resource + zipDirectorySeparator);
if (entry is null) {
log.LogError (MSBStrings.E7112 /* Could not find the file or directory '{0}' in the zip file '{1}'. */, resource, zip);
return false;
}
}

var zipPattern = entry.FullName;
if (zipPattern.Length > 0 && zipPattern [zipPattern.Length - 1] == zipDirectorySeparator) {
zipPattern += "*";
}

args.Add (zipPattern);
}

return rv;
var rv = XamarinTask.ExecuteAsync (log, "unzip", args, cancellationToken: cancellationToken).Result;
return rv.ExitCode == 0;
}

static bool TryDecompressUsingSystemIOCompression (TaskLoggingHelper log, string zip, string resource, string decompressionDir, UnzipFilter? filter, CancellationToken? cancellationToken)
{
return TryDecompressFiltered (log, zip, resource, decompressionDir, filter, DecompressFileEntryWithStream, cancellationToken);
}

/// <summary>
Expand Down
Loading
Loading