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 +- .../WixToolsetTest.Core.Native/CabinetFixture.cs | 115 ++++++++++++ .../WixToolsetTest.Core.Native/TestData/test.cab | Bin 0 -> 115 bytes .../WixToolsetTest.Core.Native/TestData/test.txt | 1 + .../Utility/DisposableFileSystem.cs | 86 +++++++++ .../WixToolsetTest.Core.Native/Utility/Pushd.cs | 46 +++++ .../WixToolsetTest.Core.Native/Utility/TestData.cs | 17 ++ .../WixToolsetTest.Core.Native.csproj | 24 +++ src/wixnative/enumcab.cpp | 47 +++++ src/wixnative/extractcab.cpp | 50 ++++++ src/wixnative/packages.config | 5 + src/wixnative/precomp.cpp | 3 + src/wixnative/precomp.h | 19 ++ src/wixnative/resetacls.cpp | 51 ++++++ src/wixnative/smartcab.cpp | 157 ++++++++++++++++ src/wixnative/wixnative.cpp | 38 ++++ src/wixnative/wixnative.vcxproj | 80 +++++++++ 23 files changed, 1237 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 create mode 100644 src/test/WixToolsetTest.Core.Native/CabinetFixture.cs create mode 100644 src/test/WixToolsetTest.Core.Native/TestData/test.cab create mode 100644 src/test/WixToolsetTest.Core.Native/TestData/test.txt create mode 100644 src/test/WixToolsetTest.Core.Native/Utility/DisposableFileSystem.cs create mode 100644 src/test/WixToolsetTest.Core.Native/Utility/Pushd.cs create mode 100644 src/test/WixToolsetTest.Core.Native/Utility/TestData.cs create mode 100644 src/test/WixToolsetTest.Core.Native/WixToolsetTest.Core.Native.csproj create mode 100644 src/wixnative/enumcab.cpp create mode 100644 src/wixnative/extractcab.cpp create mode 100644 src/wixnative/packages.config create mode 100644 src/wixnative/precomp.cpp create mode 100644 src/wixnative/precomp.h create mode 100644 src/wixnative/resetacls.cpp create mode 100644 src/wixnative/smartcab.cpp create mode 100644 src/wixnative/wixnative.cpp create mode 100644 src/wixnative/wixnative.vcxproj (limited to 'src') 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$ - + + + + + diff --git a/src/test/WixToolsetTest.Core.Native/CabinetFixture.cs b/src/test/WixToolsetTest.Core.Native/CabinetFixture.cs new file mode 100644 index 00000000..baab3bee --- /dev/null +++ b/src/test/WixToolsetTest.Core.Native/CabinetFixture.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 WixToolsetTest.CoreNative +{ + using System; + using System.IO; + using System.Linq; + using WixToolset.Core.Native; + using WixToolsetTest.CoreNative.Utility; + using Xunit; + + public class CabinetFixture + { + [Fact] + public void CanCreateSingleFileCabinet() + { + using (var fs = new DisposableFileSystem()) + { + var intermediateFolder = fs.GetFolder(true); + var cabPath = Path.Combine(intermediateFolder, "testout.cab"); + + var files = new[] { new CabinetCompressFile(TestData.Get(@"TestData\test.txt"), "test.txt") }; + + var cabinet = new Cabinet(cabPath); + cabinet.Compress(files, CabinetCompressionLevel.Low); + + Assert.True(File.Exists(cabPath)); + } + } + + [Fact] + public void CanEnumerateSingleFileCabinet() + { + var cabinetPath = TestData.Get(@"TestData\test.cab"); + + var cabinet = new Cabinet(cabinetPath); + var files = cabinet.Enumerate(); + + var file = files.Single(); + Assert.Equal("test.txt", file.FileId); + Assert.Equal(17, file.Size); + + Assert.Equal(19259, file.Date); + Assert.Equal(47731, file.Time); + Assert.True(file.SameAsDateTime(new DateTime(2017, 9, 28, 0, 19, 38))); + } + + [Fact] + public void CanExtractSingleFileCabinet() + { + var cabinetPath = TestData.Get(@"TestData\test.cab"); + + using (var fs = new DisposableFileSystem()) + { + var extractFolder = fs.GetFolder(true); + + var cabinet = new Cabinet(cabinetPath); + cabinet.Extract(extractFolder); + + var files = Directory.EnumerateFiles(extractFolder); + + var file = new FileInfo(files.Single()); + CabInterop.DateTimeToCabDateAndTime(file.CreationTime, out var date, out var time); + + Assert.Equal("test.txt", file.Name); + Assert.Equal(17, file.Length); + Assert.Equal(19259, date); + Assert.Equal(47731, time); + } + } + + [Fact] + public void IntegrationTest() + { + using (var fs = new DisposableFileSystem()) + { + var intermediateFolder = fs.GetFolder(true); + var cabinetPath = Path.Combine(intermediateFolder, "testout.cab"); + var extractFolder = fs.GetFolder(true); + + // Compress. + { + var files = new[] { new CabinetCompressFile(TestData.Get(@"TestData\test.txt"), "test.txt") }; + + var cabinet = new Cabinet(cabinetPath); + cabinet.Compress(files, CabinetCompressionLevel.Low); + } + + // Extract. + { + var cabinet = new Cabinet(cabinetPath); + cabinet.Extract(extractFolder); + } + + // Enumerate to compare cabinet to extracted files. + { + var cabinet = new Cabinet(cabinetPath); + var enumerated = cabinet.Enumerate().OrderBy(f => f.FileId).ToArray(); + + var files = Directory.EnumerateFiles(extractFolder).OrderBy(f => f).ToArray(); + + for (var i =0; i < enumerated.Length; ++i) + { + var cabFileInfo = enumerated[i]; + var fileInfo = new FileInfo(files[i]); + + Assert.Equal(cabFileInfo.FileId, fileInfo.Name); + Assert.Equal(cabFileInfo.Size, fileInfo.Length); + Assert.True(cabFileInfo.SameAsDateTime(fileInfo.CreationTime)); + } + } + } + } + } +} diff --git a/src/test/WixToolsetTest.Core.Native/TestData/test.cab b/src/test/WixToolsetTest.Core.Native/TestData/test.cab new file mode 100644 index 00000000..ca78f632 Binary files /dev/null and b/src/test/WixToolsetTest.Core.Native/TestData/test.cab differ diff --git a/src/test/WixToolsetTest.Core.Native/TestData/test.txt b/src/test/WixToolsetTest.Core.Native/TestData/test.txt new file mode 100644 index 00000000..cd0db0e1 --- /dev/null +++ b/src/test/WixToolsetTest.Core.Native/TestData/test.txt @@ -0,0 +1 @@ +This is test.txt. \ No newline at end of file diff --git a/src/test/WixToolsetTest.Core.Native/Utility/DisposableFileSystem.cs b/src/test/WixToolsetTest.Core.Native/Utility/DisposableFileSystem.cs new file mode 100644 index 00000000..c9957247 --- /dev/null +++ b/src/test/WixToolsetTest.Core.Native/Utility/DisposableFileSystem.cs @@ -0,0 +1,86 @@ +// 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 WixToolsetTest.CoreNative.Utility +{ + using System; + using System.Collections.Generic; + using System.IO; + + public class DisposableFileSystem : IDisposable + { + protected bool Disposed { get; private set; } + + private List CleanupPaths { get; } = new List(); + + public string GetFile(bool create = false) + { + var path = Path.GetTempFileName(); + + if (!create) + { + File.Delete(path); + } + + this.CleanupPaths.Add(path); + + return path; + } + + public string GetFolder(bool create = false) + { + var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + if (create) + { + Directory.CreateDirectory(path); + } + + this.CleanupPaths.Add(path); + + return path; + } + + + #region // IDisposable + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (this.Disposed) + { + return; + } + + if (disposing) + { + foreach (var path in this.CleanupPaths) + { + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + else if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + catch + { + // Best effort delete, so ignore any failures. + } + } + } + + this.Disposed = true; + } + + #endregion + } +} diff --git a/src/test/WixToolsetTest.Core.Native/Utility/Pushd.cs b/src/test/WixToolsetTest.Core.Native/Utility/Pushd.cs new file mode 100644 index 00000000..91700c2f --- /dev/null +++ b/src/test/WixToolsetTest.Core.Native/Utility/Pushd.cs @@ -0,0 +1,46 @@ +// 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 WixToolsetTest.CoreNative.Utility +{ + using System; + using System.IO; + + public class Pushd : IDisposable + { + protected bool Disposed { get; private set; } + + public Pushd(string path) + { + this.PreviousDirectory = Directory.GetCurrentDirectory(); + + Directory.SetCurrentDirectory(path); + } + + public string PreviousDirectory { get; } + + #region // IDisposable + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (this.Disposed) + { + return; + } + + if (disposing) + { + Directory.SetCurrentDirectory(this.PreviousDirectory); + } + + this.Disposed = true; + } + + #endregion + } +} diff --git a/src/test/WixToolsetTest.Core.Native/Utility/TestData.cs b/src/test/WixToolsetTest.Core.Native/Utility/TestData.cs new file mode 100644 index 00000000..cd9c6318 --- /dev/null +++ b/src/test/WixToolsetTest.Core.Native/Utility/TestData.cs @@ -0,0 +1,17 @@ +// 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 WixToolsetTest.CoreNative.Utility +{ + using System; + using System.IO; + + public class TestData + { + public static string LocalPath => Path.GetDirectoryName(new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath); + + public static string Get(params string[] paths) + { + return Path.Combine(LocalPath, Path.Combine(paths)); + } + } +} diff --git a/src/test/WixToolsetTest.Core.Native/WixToolsetTest.Core.Native.csproj b/src/test/WixToolsetTest.Core.Native/WixToolsetTest.Core.Native.csproj new file mode 100644 index 00000000..c7fd89ea --- /dev/null +++ b/src/test/WixToolsetTest.Core.Native/WixToolsetTest.Core.Native.csproj @@ -0,0 +1,24 @@ + + + + + + netcoreapp2.0 + false + + + + + + + + + + + + + + + + + diff --git a/src/wixnative/enumcab.cpp b/src/wixnative/enumcab.cpp new file mode 100644 index 00000000..e7717bac --- /dev/null +++ b/src/wixnative/enumcab.cpp @@ -0,0 +1,47 @@ +// 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. + +#include "precomp.h" + +static INT_PTR __stdcall EnumCallback(FDINOTIFICATIONTYPE fdint, PFDINOTIFICATION pfdin); + + +HRESULT EnumCabCommand( + __in int argc, + __in LPWSTR argv[] +) +{ + HRESULT hr = E_INVALIDARG; + LPCWSTR wzCabPath = NULL; + + if (argc < 1) + { + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "Must specify: cabPath outputFolder"); + } + + wzCabPath = argv[0]; + + hr = CabInitialize(FALSE); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to initialize cabinet: %ls", wzCabPath); + + hr = CabEnumerate(wzCabPath, L"*", EnumCallback, 0); + ExitOnFailure(hr, "failed to compress files into cabinet: %ls", wzCabPath); + +LExit: + CabUninitialize(); + + return hr; +} + + +static INT_PTR __stdcall EnumCallback( + __in FDINOTIFICATIONTYPE fdint, + __in PFDINOTIFICATION pfdin +) +{ + if (fdint == fdintCOPY_FILE) + { + ConsoleWriteLine(CONSOLE_COLOR_NORMAL, "%s\t%d\t%u\t%u", pfdin->psz1, pfdin->cb, pfdin->date, pfdin->time); + } + + return 0; +} diff --git a/src/wixnative/extractcab.cpp b/src/wixnative/extractcab.cpp new file mode 100644 index 00000000..53f53266 --- /dev/null +++ b/src/wixnative/extractcab.cpp @@ -0,0 +1,50 @@ +// 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. + +#include "precomp.h" + +static HRESULT ProgressCallback(BOOL fBeginFile, LPCWSTR wzFileId, LPVOID pvContext); + + +HRESULT ExtractCabCommand( + __in int argc, + __in LPWSTR argv[] +) +{ + HRESULT hr = E_INVALIDARG; + LPCWSTR wzCabPath = NULL; + LPCWSTR wzOutputFolder = NULL; + + if (argc < 2) + { + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "Must specify: cabPath outputFolder"); + } + + wzCabPath = argv[0]; + wzOutputFolder = argv[1]; + + hr = CabInitialize(FALSE); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to initialize cabinet: %ls", wzCabPath); + + hr = CabExtract(wzCabPath, L"*", wzOutputFolder, ProgressCallback, NULL, 0); + ExitOnFailure(hr, "failed to compress files into cabinet: %ls", wzCabPath); + +LExit: + CabUninitialize(); + + return hr; +} + + +static HRESULT ProgressCallback( + __in BOOL fBeginFile, + __in LPCWSTR wzFileId, + __in LPVOID /*pvContext*/ +) +{ + if (fBeginFile) + { + ConsoleWriteLine(CONSOLE_COLOR_NORMAL, "%ls", wzFileId); + } + + return S_OK; +} diff --git a/src/wixnative/packages.config b/src/wixnative/packages.config new file mode 100644 index 00000000..02ee2250 --- /dev/null +++ b/src/wixnative/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/wixnative/precomp.cpp b/src/wixnative/precomp.cpp new file mode 100644 index 00000000..37664a1c --- /dev/null +++ b/src/wixnative/precomp.cpp @@ -0,0 +1,3 @@ +// 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. + +#include "precomp.h" diff --git a/src/wixnative/precomp.h b/src/wixnative/precomp.h new file mode 100644 index 00000000..5bd617e5 --- /dev/null +++ b/src/wixnative/precomp.h @@ -0,0 +1,19 @@ +#pragma once +// 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. + +#include +#include +#include + +#include "dutil.h" +#include "conutil.h" +#include "memutil.h" +#include "pathutil.h" +#include "strutil.h" +#include "cabcutil.h" +#include "cabutil.h" + +HRESULT SmartCabCommand(int argc, LPWSTR argv[]); +HRESULT ResetAclsCommand(int argc, LPWSTR argv[]); +HRESULT EnumCabCommand(int argc, LPWSTR argv[]); +HRESULT ExtractCabCommand(int argc, LPWSTR argv[]); diff --git a/src/wixnative/resetacls.cpp b/src/wixnative/resetacls.cpp new file mode 100644 index 00000000..8c5bdc56 --- /dev/null +++ b/src/wixnative/resetacls.cpp @@ -0,0 +1,51 @@ +// 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. + +#include "precomp.h" + +HRESULT ResetAclsCommand(int argc, LPWSTR argv[]) +{ + Unused(argc); + Unused(argv); + + HRESULT hr = S_OK; + ACL* pacl = NULL; + DWORD cbAcl = sizeof(ACL); + LPWSTR sczFilePath = NULL; + + // create an empty (not NULL!) ACL to use on all the files + pacl = static_cast(MemAlloc(cbAcl, FALSE)); + ConsoleExitOnNull(pacl, hr, E_OUTOFMEMORY, CONSOLE_COLOR_RED, "failed to allocate ACL"); + +#pragma prefast(push) +#pragma prefast(disable:25029) + if (!::InitializeAcl(pacl, cbAcl, ACL_REVISION)) +#pragma prefast(op) + { + ConsoleExitOnLastError(hr, CONSOLE_COLOR_RED, "failed to initialize ACL"); + } + + // Reset the existing security permissions on each provided file. + for (;;) + { + hr = ConsoleReadW(&sczFilePath); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to read file path from stdin"); + + if (!*sczFilePath) + { + break; + } + + hr = ::SetNamedSecurityInfoW(sczFilePath, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION, NULL, NULL, pacl, NULL); + if (ERROR_FILE_NOT_FOUND != hr && ERROR_PATH_NOT_FOUND != hr) + { + ConsoleExitOnFailure(hr = HRESULT_FROM_WIN32(hr), CONSOLE_COLOR_RED, "failed to set security descriptor for file: %ls", sczFilePath); + } + } + + AssertSz(::IsValidAcl(pacl), "ResetAcls() - created invalid ACL"); + +LExit: + ReleaseStr(sczFilePath); + ReleaseMem(pacl); + return hr; +} diff --git a/src/wixnative/smartcab.cpp b/src/wixnative/smartcab.cpp new file mode 100644 index 00000000..da9087a3 --- /dev/null +++ b/src/wixnative/smartcab.cpp @@ -0,0 +1,157 @@ +// 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. + +#include "precomp.h" + +static HRESULT CompressFiles(HANDLE hCab); +static void __stdcall CabNamesCallback(LPWSTR wzFirstCabName, LPWSTR wzNewCabName, LPWSTR wzFileToken); + + +HRESULT SmartCabCommand( + __in int argc, + __in LPWSTR argv[] +) +{ + HRESULT hr = E_INVALIDARG; + LPCWSTR wzCabPath = NULL; + LPCWSTR wzCabName = NULL; + LPWSTR sczCabDir = NULL; + UINT uiFileCount = 0; + UINT uiMaxSize = 0; + UINT uiMaxThresh = 0; + COMPRESSION_TYPE ct = COMPRESSION_TYPE_NONE; + HANDLE hCab = NULL; + + if (argc < 1) + { + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "Must specify: outCabPath [compressionType] [fileCount] [maxSizePerCabInMB [maxThreshold]]"); + } + else + { + wzCabPath = argv[0]; + wzCabName = PathFile(wzCabPath); + + hr = PathGetDirectory(wzCabPath, &sczCabDir); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "Could not parse directory from path: %ls", wzCabPath); + + if (argc > 1) + { + UINT uiCompressionType; + hr = StrStringToUInt32(argv[1], 0, &uiCompressionType); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "Could not parse compression type as number: %ls", argv[1]); + + ct = (uiCompressionType > 4) ? COMPRESSION_TYPE_HIGH : static_cast(uiCompressionType); + } + + if (argc > 2) + { + hr = StrStringToUInt32(argv[2], 0, &uiFileCount); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "Could not parse file count as number: %ls", argv[2]); + } + + if (argc > 3) + { + hr = StrStringToUInt32(argv[3], 0, &uiMaxSize); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "Could not parse max size as number: %ls", argv[3]); + } + + if (argc > 4) + { + hr = StrStringToUInt32(argv[4], 0, &uiMaxThresh); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "Could not parse max threshold as number: %ls", argv[4]); + } + } + + hr = CabCBegin(wzCabName, sczCabDir, uiFileCount, uiMaxSize, uiMaxThresh, ct, &hCab); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to initialize cabinet: %ls", wzCabPath); + + hr = CompressFiles(hCab); + ExitOnFailure(hr, "failed to compress files into cabinet: %ls", wzCabPath); + + hr = CabCFinish(hCab, CabNamesCallback); + hCab = NULL; // once finish is called, the handle is invalid. + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to compress cabinet: %ls", wzCabPath); + + +LExit: + if (hCab) + { + CabCCancel(hCab); + } + ReleaseStr(sczCabDir); + + return hr; +} + + +static HRESULT CompressFiles( + __in HANDLE hCab +) +{ + HRESULT hr = S_OK; + LPWSTR sczLine = NULL; + LPWSTR* rgsczSplit = NULL; + UINT cSplit = 0; + MSIFILEHASHINFO hashInfo = { sizeof(MSIFILEHASHINFO) }; + + for (;;) + { + hr = ConsoleReadW(&sczLine); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to read smartcab line from stdin"); + + if (!*sczLine) + { + break; + } + + hr = StrSplitAllocArray(&rgsczSplit, &cSplit, sczLine, L"\t"); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to split smartcab line from stdin: %ls", sczLine); + + if (cSplit != 2 && cSplit != 6) + { + hr = E_INVALIDARG; + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to split smartcab line into hash x 4, token, source file: %ls", sczLine); + } + + LPCWSTR wzFilePath = rgsczSplit[0]; + LPCWSTR wzToken = rgsczSplit[1]; + PMSIFILEHASHINFO pHashInfo = NULL; + + if (cSplit == 6) + { + for (int i = 0; i < 4; ++i) + { + LPCWSTR wzHash = rgsczSplit[i + 2]; + + hr = StrStringToInt32(wzHash, 0, reinterpret_cast(hashInfo.dwData + i)); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to parse hash: %ls for file: %ls", wzHash, wzFilePath); + } + + pHashInfo = &hashInfo; + } + + hr = CabCAddFile(wzFilePath, wzToken, pHashInfo, hCab); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "failed to add file: %ls", wzFilePath); + + ReleaseNullStrArray(rgsczSplit, cSplit); + } + +LExit: + ReleaseNullStrArray(rgsczSplit, cSplit); + ReleaseStr(sczLine); + + return hr; +} + + +// Callback from PFNFCIGETNEXTCABINET CabCGetNextCabinet method +// First argument is the name of splitting cabinet without extension e.g. "cab1" +// Second argument is name of the new cabinet that would be formed by splitting e.g. "cab1b.cab" +// Third argument is the file token of the first file present in the splitting cabinet +static void __stdcall CabNamesCallback( + __in LPWSTR wzFirstCabName, + __in LPWSTR wzNewCabName, + __in LPWSTR wzFileToken +) +{ + ConsoleWriteLine(CONSOLE_COLOR_NORMAL, "%ls\t%ls\t%ls", wzFirstCabName, wzNewCabName, wzFileToken); +} diff --git a/src/wixnative/wixnative.cpp b/src/wixnative/wixnative.cpp new file mode 100644 index 00000000..7bd8dbca --- /dev/null +++ b/src/wixnative/wixnative.cpp @@ -0,0 +1,38 @@ +// 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. + +#include "precomp.h" + +int __cdecl wmain(int argc, LPWSTR argv[]) +{ + HRESULT hr = E_INVALIDARG; + + ConsoleInitialize(); + + if (argc < 2) + { + ConsoleWriteError(hr, CONSOLE_COLOR_RED, "Must specify a command"); + } + else if (CSTR_EQUAL == ::CompareString(LOCALE_INVARIANT, NORM_IGNORECASE, argv[1], -1, L"smartcab", -1)) + { + hr = SmartCabCommand(argc - 2, argv + 2); + } + else if (CSTR_EQUAL == ::CompareString(LOCALE_INVARIANT, NORM_IGNORECASE, argv[1], -1, L"extractcab", -1)) + { + hr = ExtractCabCommand(argc - 2, argv + 2); + } + else if (CSTR_EQUAL == ::CompareString(LOCALE_INVARIANT, NORM_IGNORECASE, argv[1], -1, L"enumcab", -1)) + { + hr = EnumCabCommand(argc - 2, argv + 2); + } + else if (CSTR_EQUAL == ::CompareString(LOCALE_INVARIANT, NORM_IGNORECASE, argv[1], -1, L"resetacls", -1)) + { + hr = ResetAclsCommand(argc - 2, argv + 2); + } + else + { + ConsoleWriteError(hr, CONSOLE_COLOR_RED, "Unknown command: %ls", argv[1]); + } + + ConsoleUninitialize(); + return HRESULT_CODE(hr); +} diff --git a/src/wixnative/wixnative.vcxproj b/src/wixnative/wixnative.vcxproj new file mode 100644 index 00000000..2a4ce3d5 --- /dev/null +++ b/src/wixnative/wixnative.vcxproj @@ -0,0 +1,80 @@ + + + + + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + {8497EC72-B8D0-4272-A9D0-7E9D871CEFBF} + Application + Console + wixnative + v141 + Unicode + Native component of WixToolset.Core + + + + + + + + + + + + + crypt32.lib;cabinet.lib;msi.lib + + + + + + 4996 + + + Create + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + -- cgit v1.2.3-55-g6feb