From ea3d18595a610ee07b03f07af4f03cf75b5ab420 Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Wed, 29 Nov 2017 14:08:08 -0800 Subject: Improved cabinet handling --- src/WixToolset.Core.Native/Cabinet.cs | 199 +++++++++++++++++++++ src/WixToolset.Core.Native/CabinetCompressFile.cs | 65 +++++++ .../CabinetCompressionLevel.cs | 25 +++ src/WixToolset.Core.Native/CabinetFileInfo.cs | 67 +++++++ src/WixToolset.Core.Native/WixNativeExe.cs | 115 ++++++++++++ .../WixToolset.Core.Native.csproj | 24 ++- .../WixToolset.Core.Native.nuspec | 7 +- 7 files changed, 498 insertions(+), 4 deletions(-) create mode 100644 src/WixToolset.Core.Native/Cabinet.cs create mode 100644 src/WixToolset.Core.Native/CabinetCompressFile.cs create mode 100644 src/WixToolset.Core.Native/CabinetCompressionLevel.cs create mode 100644 src/WixToolset.Core.Native/CabinetFileInfo.cs create mode 100644 src/WixToolset.Core.Native/WixNativeExe.cs (limited to 'src/WixToolset.Core.Native') diff --git a/src/WixToolset.Core.Native/Cabinet.cs b/src/WixToolset.Core.Native/Cabinet.cs new file mode 100644 index 00000000..27b0ec74 --- /dev/null +++ b/src/WixToolset.Core.Native/Cabinet.cs @@ -0,0 +1,199 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.Native +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// Wrapper class around interop with wixcab.dll to compress files into a cabinet. + /// + public sealed class Cabinet + { + private const string CompressionLevelVariable = "WIX_COMPRESSION_LEVEL"; + private static readonly char[] TextLineSplitter = new[] { '\t' }; + + public Cabinet(string path) + { + this.Path = path; + } + + public string Path { get; } + + /// + /// Creates a cabinet. + /// + /// Path of cabinet to create. + /// Level of compression to apply. + /// Maximum number of files that will be added to cabinet. + /// Maximum size of cabinet. + /// Maximum threshold for each cabinet. + public void Compress(IEnumerable files, CabinetCompressionLevel compressionLevel, int maxSize = 0, int maxThresh = 0) + { + var compressionLevelVariable = Environment.GetEnvironmentVariable(CompressionLevelVariable); + + // Override authored compression level if environment variable is present. + if (!String.IsNullOrEmpty(compressionLevelVariable)) + { + if (!Enum.TryParse(compressionLevelVariable, true, out compressionLevel)) + { + //throw new WixException(WixErrors.IllegalEnvironmentVariable(CompressionLevelVariable, compressionLevelVariable)); + throw new ArgumentException(); + } + } + + var wixnative = new WixNativeExe("smartcab", this.Path, Convert.ToInt32(compressionLevel), files.Count(), maxSize, maxThresh); + + foreach (var file in files) + { + wixnative.AddStdinLine(file.ToWixNativeStdinLine()); + } + + wixnative.Run(); + +#if TOOD_ERROR_HANDLING + catch (COMException ce) + { + // If we get a "the file exists" error, we must have a full temp directory - so report the issue + if (0x80070050 == unchecked((uint)ce.ErrorCode)) + { + throw new WixException(WixErrors.FullTempDirectory("WSC", Path.GetTempPath())); + } + + throw; + } +#endif + } + + /// + /// Enumerates all files in a cabinet. + /// + /// >List of CabinetFileInfo + public List Enumerate() + { + var wixnative = new WixNativeExe("enumcab", this.Path); + var lines = wixnative.Run(); + + var fileInfoList = new List(); + + foreach (var line in lines) + { + if (String.IsNullOrEmpty(line)) + { + continue; + } + + var data = line.Split(TextLineSplitter, StringSplitOptions.None); + + var size = Convert.ToInt32(data[1]); + var date = Convert.ToInt32(data[2]); + var time = Convert.ToInt32(data[3]); + + fileInfoList.Add(new CabinetFileInfo(data[0], size, date, time)); + } + + return fileInfoList; + } + + /// + /// Extracts all the files from a cabinet to a directory. + /// + /// Directory to extract files to. + public void Extract(string outputFolder) + { + if (!outputFolder.EndsWith("\\", StringComparison.Ordinal)) + { + outputFolder += "\\"; + } + + var wixnative = new WixNativeExe("extractcab", this.Path, outputFolder); + wixnative.Run(); + } + +#if TOOD_ERROR_HANDLING + /// + /// Adds a file to the cabinet with an optional MSI file hash. + /// + /// The file to add. + /// The token for the file. + /// The MSI file hash of the file. + //private void AddFile(string file, string token, MsiInterop.MSIFILEHASHINFO fileHash) + //{ + // try + // { + // NativeMethods.CreateCabAddFile(file, token, fileHash, this.handle); + // } + // catch (COMException ce) + // { + // if (0x80004005 == unchecked((uint)ce.ErrorCode)) // E_FAIL + // { + // throw new WixException(WixErrors.CreateCabAddFileFailed()); + // } + // else if (0x80070070 == unchecked((uint)ce.ErrorCode)) // ERROR_DISK_FULL + // { + // throw new WixException(WixErrors.CreateCabInsufficientDiskSpace()); + // } + // else + // { + // throw; + // } + // } + // catch (DirectoryNotFoundException) + // { + // throw new WixFileNotFoundException(file); + // } + // catch (FileNotFoundException) + // { + // throw new WixFileNotFoundException(file); + // } + //} + + /// + /// Complete/commit the cabinet - this must be called before Dispose so that errors will be + /// reported on the same thread. + /// + /// Address of Binder's callback function for Cabinet Splitting + public void Complete(IntPtr newCabNamesCallBackAddress) + { + if (IntPtr.Zero != this.handle) + { + try + { + if (newCabNamesCallBackAddress != IntPtr.Zero && this.maxSize != 0) + { + NativeMethods.CreateCabFinish(this.handle, newCabNamesCallBackAddress); + } + else + { + NativeMethods.CreateCabFinish(this.handle, IntPtr.Zero); + } + + GC.SuppressFinalize(this); + this.disposed = true; + } + catch (COMException ce) + { + //if (0x80004005 == unchecked((uint)ce.ErrorCode)) // E_FAIL + //{ + // // This error seems to happen, among other situations, when cabbing more than 0xFFFF files + // throw new WixException(WixErrors.FinishCabFailed()); + //} + //else if (0x80070070 == unchecked((uint)ce.ErrorCode)) // ERROR_DISK_FULL + //{ + // throw new WixException(WixErrors.CreateCabInsufficientDiskSpace()); + //} + //else + //{ + // throw; + //} + } + finally + { + this.handle = IntPtr.Zero; + } + } + } +#endif + } +} diff --git a/src/WixToolset.Core.Native/CabinetCompressFile.cs b/src/WixToolset.Core.Native/CabinetCompressFile.cs new file mode 100644 index 00000000..6778f4a1 --- /dev/null +++ b/src/WixToolset.Core.Native/CabinetCompressFile.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.Native +{ + /// + /// Information to compress file into a cabinet. + /// + public sealed class CabinetCompressFile + { + /// + /// Cabinet compress file. + /// + /// Path to file to add. + /// The token for the file. + public CabinetCompressFile(string path, string token) + { + this.Path = path; + this.Token = token; + this.Hash = null; + } + + /// + /// Cabinet compress file. + /// + /// Path to file to add. + /// The token for the file. + /// Hash 1 + /// Hash 2 + /// Hash 3 + /// Hash 4 + public CabinetCompressFile(string path, string token, int hash1, int hash2, int hash3, int hash4) + { + this.Path = path; + this.Token = token; + this.Hash = new[] { hash1, hash2, hash3, hash4 }; + } + + /// + /// Gets the path to the file to compress. + /// + public string Path { get; } + + /// + /// Gets the token for the file to compress. + /// + public string Token { get; } + + /// + /// Gets the hash of the file to compress. + /// + public int[] Hash { get; } + + internal string ToWixNativeStdinLine() + { + if (this.Hash == null) + { + return $"{this.Path}\t{this.Token}"; + } + else + { + return $"{this.Path}\t{this.Token}\t{this.Hash[0]}\t{this.Hash[1]}\t{this.Hash[2]}\t{this.Hash[3]}"; + } + } + } +} diff --git a/src/WixToolset.Core.Native/CabinetCompressionLevel.cs b/src/WixToolset.Core.Native/CabinetCompressionLevel.cs new file mode 100644 index 00000000..fce1ff41 --- /dev/null +++ b/src/WixToolset.Core.Native/CabinetCompressionLevel.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.Native +{ + /// + /// Compression level to use when creating cabinet. + /// + public enum CabinetCompressionLevel + { + /// Use no compression. + None, + + /// Use low compression. + Low, + + /// Use medium compression. + Medium, + + /// Use high compression. + High, + + /// Use ms-zip compression. + Mszip + } +} \ No newline at end of file diff --git a/src/WixToolset.Core.Native/CabinetFileInfo.cs b/src/WixToolset.Core.Native/CabinetFileInfo.cs new file mode 100644 index 00000000..ea229121 --- /dev/null +++ b/src/WixToolset.Core.Native/CabinetFileInfo.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.Native +{ + using System; + + /// + /// Properties of a file in a cabinet. + /// + public sealed class CabinetFileInfo + { + /// + /// Constructs CabinetFileInfo + /// + /// File Id + /// Size of file + /// Last modified date + /// Last modified time + public CabinetFileInfo(string fileId, int size, int date, int time) + { + this.FileId = fileId; + this.Size = size; + this.Date = date; + this.Time = time; + } + + /// + /// Gets the file Id of the file. + /// + /// file Id + public string FileId { get; } + + /// + /// Gets modified date (DOS format). + /// + public int Date { get; } + + /// + /// Gets modified time (DOS format). + /// + public int Time { get; } + + /// + /// Gets the size of the file in bytes. + /// + public int Size { get; } + + /// + /// Compares this file info's date and time with another datetime. + /// + /// Date and time to compare with/ + /// + /// For some reason DateTime.ToLocalTime() does not match kernel32.dll FileTimeToLocalFileTime(). + /// Since cabinets store date and time with the kernel32.dll functions, we need to convert DateTime + /// to local file time using the kernel32.dll functions. + /// + public bool SameAsDateTime(DateTime dateTime) + { + long filetime = dateTime.ToFileTime(); + long localTime = 0; + NativeMethods.FileTimeToLocalFileTime(ref filetime, ref localTime); + NativeMethods.FileTimeToDosDateTime(ref localTime, out var cabDate, out var cabTime); + + return this.Date == cabDate && this.Time == cabTime; + } + } +} diff --git a/src/WixToolset.Core.Native/WixNativeExe.cs b/src/WixToolset.Core.Native/WixNativeExe.cs new file mode 100644 index 00000000..8626bea3 --- /dev/null +++ b/src/WixToolset.Core.Native/WixNativeExe.cs @@ -0,0 +1,115 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.Native +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics; + using System.IO; + using System.Reflection; + + internal class WixNativeExe + { + private const int FiveMinutesInMilliseconds = 300000; + private static readonly string PathToWixNativeExe; + + private readonly string commandLine; + private readonly List stdinLines = new List(); + + static WixNativeExe() + { + PathToWixNativeExe = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath), "x86\\wixnative.exe"); + } + + public WixNativeExe(params object[] args) + { + this.commandLine = String.Join(" ", QuoteArgumentsAsNecesary(args)); + } + + public void AddStdinLine(string line) + { + this.stdinLines.Add(line); + } + + public void AddStdinLines(IEnumerable lines) + { + this.stdinLines.AddRange(lines); + } + + public IEnumerable Run() + { + var wixNativeInfo = new ProcessStartInfo(PathToWixNativeExe, this.commandLine) + { + RedirectStandardInput = true, + RedirectStandardOutput = true, + CreateNoWindow = true, + ErrorDialog = false, + UseShellExecute = false + }; + + var stdoutLines = new List(); + + using (var process = Process.Start(wixNativeInfo)) + { + process.OutputDataReceived += (s, a) => stdoutLines.Add(a.Data); + process.BeginOutputReadLine(); + + if (this.stdinLines.Count > 0) + { + foreach (var line in this.stdinLines) + { + process.StandardInput.WriteLine(line); + } + + // Trailing blank line indicates stdin complete. + process.StandardInput.WriteLine(); + } + + if (process.WaitForExit(FiveMinutesInMilliseconds)) + { + // If the process successfully exits documentation says we need to wait again + // without a timeout to ensure that all of the redirected output is captured. + // + process.WaitForExit(); + } + + if (process.ExitCode != 0) + { + throw new Win32Exception(process.ExitCode); + } + } + + return stdoutLines; + } + + private static IEnumerable QuoteArgumentsAsNecesary(object[] args) + { + foreach (var arg in args) + { + if (arg is string str) + { + if (String.IsNullOrEmpty(str)) + { + } + else if (str.Contains(" ") && !str.StartsWith("\"")) + { + yield return $"\"{str}\""; + } + else + { + yield return str; + } + } + else if (arg is int i) + { + yield return i.ToString(); + } + else + { + throw new ArgumentException(nameof(args)); + } + } + } + } +} diff --git a/src/WixToolset.Core.Native/WixToolset.Core.Native.csproj b/src/WixToolset.Core.Native/WixToolset.Core.Native.csproj index 3e66d84e..aa87186b 100644 --- a/src/WixToolset.Core.Native/WixToolset.Core.Native.csproj +++ b/src/WixToolset.Core.Native/WixToolset.Core.Native.csproj @@ -3,6 +3,7 @@ netstandard2.0 $(MSBuildThisFileName).nuspec + Core Native @@ -10,9 +11,26 @@ - + + + + + + + x86\ + + + x64\ + + + + PreserveNewest + %(TargetRelativeFolder)%(Filename)%(Extension) + + + + diff --git a/src/WixToolset.Core.Native/WixToolset.Core.Native.nuspec b/src/WixToolset.Core.Native/WixToolset.Core.Native.nuspec index 68d154c8..6a96167e 100644 --- a/src/WixToolset.Core.Native/WixToolset.Core.Native.nuspec +++ b/src/WixToolset.Core.Native/WixToolset.Core.Native.nuspec @@ -10,15 +10,20 @@ false $description$ $copyright$ - + + + + + -- cgit v1.2.3-55-g6feb