From 9edb6e2676d9303239f10876606ac97a75aaaa5b Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 21 Feb 2022 01:56:02 -0800 Subject: [PATCH 01/50] refactor: add FSCrompression, uses PInvoke no clue how this works --- src/Common/FSCompression.cs | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/Common/FSCompression.cs diff --git a/src/Common/FSCompression.cs b/src/Common/FSCompression.cs new file mode 100644 index 00000000..2d7ab15a --- /dev/null +++ b/src/Common/FSCompression.cs @@ -0,0 +1,38 @@ +/// Compress a folder using NTFS compression in .NET +/// https://stackoverflow.com/a/624446 +/// - Zack Elan https://stackoverflow.com/users/2461/zack-elan +/// - Goz https://stackoverflow.com/users/131140/goz + +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace HXE.Common +{ + internal class FSCompression + { + private const int FSCTL_SET_COMPRESSION = 0x9C040; + private const short COMPRESSION_FORMAT_DEFAULT = 1; + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern int DeviceIoControl( + SafeFileHandle hDevice, + int dwIoControlCode, + ref short lpInBuffer, + int nInBufferSize, + IntPtr lpOutBuffer, + int nOutBufferSize, + ref int lpBytesReturned, + IntPtr lpOverlapped); + + public static bool EnableCompression(SafeFileHandle handle) + { + int lpBytesReturned = 0; + short lpInBuffer = COMPRESSION_FORMAT_DEFAULT; + + return DeviceIoControl(handle, FSCTL_SET_COMPRESSION, + ref lpInBuffer, sizeof(short), IntPtr.Zero, 0, + ref lpBytesReturned, IntPtr.Zero) != 0; + } + } +} From a56dd6300e2ab062452f53fe926b5cc899175fb1 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 21 Feb 2022 01:56:23 -0800 Subject: [PATCH 02/50] refactor: make FSCompression static --- src/Common/FSCompression.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/FSCompression.cs b/src/Common/FSCompression.cs index 2d7ab15a..9cd8bc8a 100644 --- a/src/Common/FSCompression.cs +++ b/src/Common/FSCompression.cs @@ -9,7 +9,7 @@ namespace HXE.Common { - internal class FSCompression + internal static class FSCompression { private const int FSCTL_SET_COMPRESSION = 0x9C040; private const short COMPRESSION_FORMAT_DEFAULT = 1; From 3c89746969fc7b52efc7a72437e50617eaa174ba Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 21 Feb 2022 02:01:52 -0800 Subject: [PATCH 03/50] refactor: rename FileSystemCompression --- src/Common/{FSCompression.cs => FileSystemCompression.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/Common/{FSCompression.cs => FileSystemCompression.cs} (96%) diff --git a/src/Common/FSCompression.cs b/src/Common/FileSystemCompression.cs similarity index 96% rename from src/Common/FSCompression.cs rename to src/Common/FileSystemCompression.cs index 9cd8bc8a..76094a7e 100644 --- a/src/Common/FSCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -9,7 +9,7 @@ namespace HXE.Common { - internal static class FSCompression + internal static class FileSystemCompression { private const int FSCTL_SET_COMPRESSION = 0x9C040; private const short COMPRESSION_FORMAT_DEFAULT = 1; From 8684200a23ca82f062229fc7a794fb8a88e5dec4 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 14 Mar 2022 00:55:56 -0700 Subject: [PATCH 04/50] build: AllowUnsafeBlocks --- src/HXE.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/HXE.csproj b/src/HXE.csproj index 694d6537..91195e42 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -50,6 +50,7 @@ true en true + true DEBUG;TRACE From ada6deef3aa00c5198328fa05f6afb47b0f3f56e Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 14 Mar 2022 00:56:32 -0700 Subject: [PATCH 05/50] build: add PackageReference pinvoke.kernel32 --- src/HXE.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/HXE.csproj b/src/HXE.csproj index 91195e42..5540829a 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -119,6 +119,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + From 07028274fb80591b4928661644f5df5e7665164b Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 14 Mar 2022 03:11:28 -0700 Subject: [PATCH 06/50] refactor: overhaul FileSystemCompression class Added extension methods: DirectoryInfo.Compress() FileInfo.Compress() Reworked methods: SetCompression() (formerly EnableCompression()) --- src/Common/FileSystemCompression.cs | 162 ++++++++++++++++++++++++---- 1 file changed, 141 insertions(+), 21 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index 76094a7e..24c76f52 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -4,35 +4,155 @@ /// - Goz https://stackoverflow.com/users/131140/goz using System; +using System.IO; using System.Runtime.InteropServices; -using Microsoft.Win32.SafeHandles; +using PInvoke; namespace HXE.Common { internal static class FileSystemCompression { - private const int FSCTL_SET_COMPRESSION = 0x9C040; - private const short COMPRESSION_FORMAT_DEFAULT = 1; - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern int DeviceIoControl( - SafeFileHandle hDevice, - int dwIoControlCode, - ref short lpInBuffer, - int nInBufferSize, - IntPtr lpOutBuffer, - int nOutBufferSize, - ref int lpBytesReturned, - IntPtr lpOverlapped); - - public static bool EnableCompression(SafeFileHandle handle) + private static class Constants { - int lpBytesReturned = 0; - short lpInBuffer = COMPRESSION_FORMAT_DEFAULT; + public const int FSCTL_SET_COMPRESSION = 0x9C040; + public const short COMPRESSION_FORMAT_DEFAULT = 1; + } + + // TODO: Duplicate as non-extension "CompressDirectory()" + /// + /// Compress the directory represented by the DirectoryInfo object. + /// + /// The directory to enable compression for. + /// // TODO + /// // TODO + /// // TODO + /// + /// The path encapsulated in the System.IO.DirectoryInfo object is invalid, such
+ /// as being on an unmapped drive. + ///
+ /// The caller does not have the required permission. + /// The file is not found. + /// file path is read-only. + /// DeviceIoControl operation failed. See for exception data. + public static void Compress(this DirectoryInfo directoryInfo, bool compressFiles = true, bool recurse = false, IProgress progress = null) + { + /* Progress */ + bool withProgress = progress != null; + int itemsCompleted = 0; + int itemsTotal = 1; + + void UpdateProgress(int n) + { + if (withProgress) return; // not necessary, but it's here just in case. + itemsCompleted += n; + progress.Report(itemsCompleted * 100 / itemsTotal); + } + + /* Get files, subdirectories */ + SearchOption searchOption = recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + DirectoryInfo[] directories = recurse ? directoryInfo.GetDirectories( + searchPattern: "*", + searchOption: searchOption + ) : null; + FileInfo[] files = compressFiles ? directoryInfo.GetFiles( + searchPattern: "*", + searchOption: searchOption + ) : null; + + /* Add files, directories count to itemsTotal; Update progress */ + if (withProgress) + { + if (files != null) + { + itemsTotal += files.Length; + } + if (directories != null) + { + itemsTotal += directories.Length; + } + + UpdateProgress(0); + } + + /* Compress root directory */ + Kernel32.SafeObjectHandle hDirectory = Kernel32.CreateFile( + filename: directoryInfo.FullName, + access: Kernel32.ACCESS_MASK.GenericRight.GENERIC_READ | Kernel32.ACCESS_MASK.GenericRight.GENERIC_WRITE, + share: Kernel32.FileShare.None, + securityAttributes: Kernel32.SECURITY_ATTRIBUTES.Create(), + creationDisposition: Kernel32.CreationDisposition.OPEN_EXISTING, + flagsAndAttributes: Kernel32.CreateFileFlags.FILE_FLAG_BACKUP_SEMANTICS, + templateFile: null + ); + + SetCompression(hDirectory); + hDirectory.Close(); + if (withProgress) + UpdateProgress(itemsCompleted++); + + /* Compress sub-directories */ + if (recurse) + { + foreach (DirectoryInfo directory in directories) + { + directory.Compress(); + if (withProgress) + UpdateProgress(itemsCompleted++); + } + } + + /* Compress files*/ + if (compressFiles) + { + foreach (FileInfo file in files) + { + file.Compress(); + if (withProgress) + UpdateProgress(itemsCompleted++); + } + } + } + + // TODO: Duplicate as non-extension "CompressFile()" + /// + /// Set filesystem compression for the file + /// + /// A FileInfo object indicating the file to compress. + /// The caller does not have the required permission. + /// The file is not found. + /// path is read-only or is a directory. + /// The specified path is invalid, such as being on an unmapped drive. + /// DeviceIoControl operation failed. See for exception data. + public static void Compress(this FileInfo fileInfo) + { + FileStream fileStream = fileInfo.Open(mode: FileMode.Open, access: FileAccess.ReadWrite, share: FileShare.None); + SetCompression((Kernel32.SafeObjectHandle)(SafeHandle)fileStream.SafeFileHandle); + fileStream.Dispose(); + } + + /// + /// P/Invoke DeviceIoControl with the FSCTL_SET_COMPRESSION + /// + /// //TODO + /// DeviceIoControl operation failed. See for exception data. + public static unsafe void SetCompression(Kernel32.SafeObjectHandle handle) + { + short lpInBuffer = Constants.COMPRESSION_FORMAT_DEFAULT; + bool success = Kernel32.DeviceIoControl( + hDevice: handle, + dwIoControlCode: Constants.FSCTL_SET_COMPRESSION, + inBuffer: &lpInBuffer, + nInBufferSize: sizeof(short), // sizeof(typeof(lpInBuffer.GetType())) + outBuffer: (void*)IntPtr.Zero, + nOutBufferSize: 0, + pBytesReturned: out _, + lpOverlapped: (Kernel32.OVERLAPPED*)IntPtr.Zero + ); - return DeviceIoControl(handle, FSCTL_SET_COMPRESSION, - ref lpInBuffer, sizeof(short), IntPtr.Zero, 0, - ref lpBytesReturned, IntPtr.Zero) != 0; + if (!success) + { + throw new Win32Exception(Kernel32.GetLastError()); + } } } } From a40015a0c408fc9856d9293dd88a91ac99885658 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 21 Mar 2022 00:47:37 -0700 Subject: [PATCH 07/50] build: re-add Fody, Costura.Fody --- src/FodyWeavers.xml | 6 ++++++ src/HXE.csproj | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/FodyWeavers.xml diff --git a/src/FodyWeavers.xml b/src/FodyWeavers.xml new file mode 100644 index 00000000..250af971 --- /dev/null +++ b/src/FodyWeavers.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/src/HXE.csproj b/src/HXE.csproj index 5540829a..84e5bf7a 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -6,7 +6,6 @@ false Exe net462 - net462;net480;net5.0-windows;net6.0-windows HXE.Program HXE HXE @@ -110,6 +109,13 @@
+ + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + all runtime; build; native; contentfiles; analyzers; buildtransitive From 836e92f53ea59b2bdf367f97ef1de851ea17a788 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 21 Mar 2022 01:16:50 -0700 Subject: [PATCH 08/50] build(deps): fix name of PInvoke.Kernel32 --- src/HXE.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index 84e5bf7a..6969ece9 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -125,7 +125,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + From 743e5584ea7aadba51a741f5d9747bbcd7d6d6a1 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 21 Mar 2022 01:35:07 -0700 Subject: [PATCH 09/50] refactor: change DirectoyInfo.Compress()'s progress from Int to HXE.Status --- src/Common/FileSystemCompression.cs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index 24c76f52..e9a7bcea 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -34,18 +34,22 @@ private static class Constants /// The file is not found. /// file path is read-only. /// DeviceIoControl operation failed. See for exception data. - public static void Compress(this DirectoryInfo directoryInfo, bool compressFiles = true, bool recurse = false, IProgress progress = null) + public static void Compress(this DirectoryInfo directoryInfo, bool compressFiles = true, bool recurse = false, IProgress progress = null) { /* Progress */ bool withProgress = progress != null; - int itemsCompleted = 0; - int itemsTotal = 1; + var status = withProgress ? new Status + { + Description = $"Compressing '{directoryInfo.FullName}' and its descendents...", + Current = 0, + Total = 1 + } : null; void UpdateProgress(int n) { - if (withProgress) return; // not necessary, but it's here just in case. - itemsCompleted += n; - progress.Report(itemsCompleted * 100 / itemsTotal); + if (!withProgress) return; // not necessary, but it's here just in case. + status.Current += n; + progress.Report(status); } /* Get files, subdirectories */ @@ -64,11 +68,11 @@ void UpdateProgress(int n) { if (files != null) { - itemsTotal += files.Length; + status.Total += files.Length; } if (directories != null) { - itemsTotal += directories.Length; + status.Total += directories.Length; } UpdateProgress(0); From 292435366ffa0a23eb1e19fbe485093097e821e4 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 21 Mar 2022 01:38:59 -0700 Subject: [PATCH 10/50] fix: increment Compress()'s status properly --- src/Common/FileSystemCompression.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index e9a7bcea..eb02ed95 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -92,7 +92,7 @@ void UpdateProgress(int n) SetCompression(hDirectory); hDirectory.Close(); if (withProgress) - UpdateProgress(itemsCompleted++); + UpdateProgress(1); /* Compress sub-directories */ if (recurse) @@ -101,7 +101,7 @@ void UpdateProgress(int n) { directory.Compress(); if (withProgress) - UpdateProgress(itemsCompleted++); + UpdateProgress(1); } } @@ -112,7 +112,7 @@ void UpdateProgress(int n) { file.Compress(); if (withProgress) - UpdateProgress(itemsCompleted++); + UpdateProgress(1); } } } From 4fffd4d9e4ac544f737049c11abac0080fc14d37 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 21 Mar 2022 01:43:55 -0700 Subject: [PATCH 11/50] fix: replace enableLZNT1 procedure --- src/Installer.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Installer.cs b/src/Installer.cs index 651ed81d..9719772b 100644 --- a/src/Installer.cs +++ b/src/Installer.cs @@ -24,6 +24,7 @@ using System.IO.Compression; using System.Threading; using System.Threading.Tasks; +using HXE.Common; using HXE.Properties; using static System.IO.File; using static System.IO.Path; @@ -71,16 +72,10 @@ public static void Install(string source, string target, IProgress progr Directory.CreateDirectory(target); if (enableLZNT1) /// TODO: refactor to new Method for use from other Classes. { - string[] directories = Directory.GetDirectories(target); - string[] files = Directory.GetFiles(target); - foreach (string directoryPath in directories) - { - new DirectoryInfo(directoryPath).Attributes |= FileAttributes.Compressed; - } - foreach (string filePath in files) - { - new FileInfo(filePath).Attributes |= FileAttributes.Compressed; - } + /// Because there will probably be no files in the target directory, + /// this will be rather fast. + var directoryInfo = new DirectoryInfo(target); + directoryInfo.Compress(compressFiles:true, recurse:true, progress: progress); /// TODO: Introduce and display progress of changes using Events } From 97592cdc5463705f8fa79a4934f431ca738e316b Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Mon, 21 Mar 2022 02:46:03 -0700 Subject: [PATCH 12/50] feat: add --applyLznt1= CLI parameter --- src/Program.cs | 59 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/src/Program.cs b/src/Program.cs index 5932e821..e0a3b696 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -25,6 +25,7 @@ using System.Linq; using System.Threading.Tasks; using System.Windows; +using HXE.Common; using HXE.HCE; using static System.Console; using static System.Environment; @@ -57,6 +58,7 @@ _ ___ ________ --help Displays commands list --test Start a dry run of HXE to self-test --config Opens configuration GUI + --applyLznt1=VALUE Enables LZNT1 compression for a directory --positions Opens positions GUI --cli Opens CLI instead of GUI where available --install=VALUE Installs HCE/SPV3 to destination @@ -97,6 +99,7 @@ public static void Main(string[] args) /// --test Start a dry run of HXE to self-test
/// --config Opens configuration GUI
/// --positions Opens first-person model positions GUI
+ /// --applyLznt1=VALUE Enables LZNT1 compression for a directory
/// --cli Opens CLI instead of GUI where available
/// --install=VALUE Installs HCE/SPV3 to destination
/// --compile=VALUE Compiles HCE/SPV3 to destination
@@ -119,32 +122,34 @@ private static void InvokeProgram(string[] args) { Directory.CreateDirectory(Paths.Directory); - var help = false; /* Displays commands list */ - var test = false; /* Start a dry run of HXE to self-test */ - var config = false; /* Opens configuration GUI */ - var positions = false; /* Opens positions GUI */ - var cli = false; /* Opens CLI instead of GUI where available */ - var install = string.Empty; /* Installs HCE/SPV3 to destination */ - var compile = string.Empty; /* Compiles HCE/SPV3 to destination */ - var update = string.Empty; /* Updates directory using manifest */ - var registry = string.Empty; /* Write to Windows Registry */ - var infer = false; /* Infer the running Halo executable */ - var console = false; /* Loads HCE with console mode */ - var devmode = false; /* Loads HCE with developer mode */ - var screenshot = false; /* Loads HCE with screenshot ability */ - var window = false; /* Loads HCE in window mode */ - var nogamma = false; /* Loads HCE without gamma overriding */ - var adapter = string.Empty; /* Loads HCE on monitor X */ - var path = string.Empty; /* Loads HCE with custom profile path */ - var exec = string.Empty; /* Loads HCE with custom init file */ - var vidmode = string.Empty; /* Loads HCE with custom res. and Hz */ - var refresh = string.Empty; /* Loads HCE with custom refresh rate */ + var help = false; /* Displays commands list */ + var test = false; /* Start a dry run of HXE to self-test */ + var config = false; /* Opens configuration GUI */ + var positions = false; /* Opens positions GUI */ + var applyLznt1 = string.Empty; /* Enables LZNT1 compression for a directory */ + var cli = false; /* Opens CLI instead of GUI where available */ + var install = string.Empty; /* Installs HCE/SPV3 to destination */ + var compile = string.Empty; /* Compiles HCE/SPV3 to destination */ + var update = string.Empty; /* Updates directory using manifest */ + var registry = string.Empty; /* Write to Windows Registry */ + var infer = false; /* Infer the running Halo executable */ + var console = false; /* Loads HCE with console mode */ + var devmode = false; /* Loads HCE with developer mode */ + var screenshot = false; /* Loads HCE with screenshot ability */ + var window = false; /* Loads HCE in window mode */ + var nogamma = false; /* Loads HCE without gamma overriding */ + var adapter = string.Empty; /* Loads HCE on monitor X */ + var path = string.Empty; /* Loads HCE with custom profile path */ + var exec = string.Empty; /* Loads HCE with custom init file */ + var vidmode = string.Empty; /* Loads HCE with custom res. and Hz */ + var refresh = string.Empty; /* Loads HCE with custom refresh rate */ var options = new OptionSet() .Add("help", "Displays commands list", s => help = s != null) /* hxe command */ - .Add("test", "Start a dry run of HXE to self-test", s => test =s != null) /* hxe command */ + .Add("test", "Start a dry run of HXE to self-test", s => test = s != null) /* hxe command */ .Add("config", "Opens configuration GUI", s => config = s != null) /* hxe command */ .Add("positions", "Opens positions GUI", s => positions = s != null) /* hxe command */ + .Add("applyLznt1=", "Enables LZNT1 compression for a directory", s => applyLznt1 = s) /* hxe command */ .Add("cli", "Enable CLI of Positions or Config", s => cli = s != null) /* hxe parameter */ .Add("install=", "Installs HCE/SPV3 to destination", s => install = s) /* hxe parameter */ .Add("compile=", "Compiles HCE/SPV3 to destination", s => compile = s) /* hxe parameter */ @@ -167,6 +172,18 @@ private static void InvokeProgram(string[] args) foreach (var i in input) Info("Discovered CLI command: " + i); + if (!string.IsNullOrWhiteSpace(applyLznt1)) + { + var directoryInfo = new DirectoryInfo(applyLznt1); + IProgress p = new Progress(progress => + { + Wait(progress.Description); + Wait(progress.Current + " of " + progress.Total + " items complete."); + }); + directoryInfo.Compress(compressFiles: true, recurse: true, progress: p); + WithCode(Code.Success); + } + var hce = new Executable(); if (help) From a52c8c183c804ef4f4be3a35c87f0acbe5016137 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 10 Apr 2022 03:12:44 -0700 Subject: [PATCH 13/50] build(deps): add CsWin32 and PInvoke.AdvApi32 --- src/HXE.csproj | 7 +++++++ src/NativeMethods.json | 4 ++++ src/NativeMethods.txt | 14 ++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 src/NativeMethods.json create mode 100644 src/NativeMethods.txt diff --git a/src/HXE.csproj b/src/HXE.csproj index 6969ece9..d0b78ab7 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -6,6 +6,7 @@ false Exe net462 + 8 HXE.Program HXE HXE @@ -50,6 +51,7 @@ en true true + 7.0
DEBUG;TRACE @@ -125,6 +127,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + diff --git a/src/NativeMethods.json b/src/NativeMethods.json new file mode 100644 index 00000000..2dc1665d --- /dev/null +++ b/src/NativeMethods.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "emitSingleFile": true +} diff --git a/src/NativeMethods.txt b/src/NativeMethods.txt new file mode 100644 index 00000000..fade8d1a --- /dev/null +++ b/src/NativeMethods.txt @@ -0,0 +1,14 @@ +COMPRESSION_FORMAT_DEFAULT +FSCTL_SET_COMPRESSION +SE_BACKUP_NAME +TOKEN_ELEVATION_TYPE +TOKEN_INFORMATION_CLASS +TOKEN_PRIVILEGES +TOKEN_PRIVILEGES_ATTRIBUTES +AdjustTokenPrivileges +CreateFile +DeviceIoControl +GetTokenInformation +LookupPrivilegeValue +OpenProcessToken +PrivilegeCheck From e9beeabd39ec93d0a31e56bcf4a467c7ee729b95 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 10 Apr 2022 03:09:58 -0700 Subject: [PATCH 14/50] feat: add Process extensions + IsCurrentProcessElevated() + IsElevated() + HasSeBackupPrivilege() + SetSeBackupPrivilege() --- src/Extensions/Process.cs | 202 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 src/Extensions/Process.cs diff --git a/src/Extensions/Process.cs b/src/Extensions/Process.cs new file mode 100644 index 00000000..36de0f48 --- /dev/null +++ b/src/Extensions/Process.cs @@ -0,0 +1,202 @@ +using System; +using System.Security.Principal; +using HXE.Common; +using Microsoft.Win32.SafeHandles; +using PInvoke; +using Windows.Win32.Foundation; +using Windows.Win32.Security; +using static Windows.Win32.PInvoke; +using static Windows.Win32.Security.TOKEN_ACCESS_MASK; +using static Windows.Win32.Security.TOKEN_PRIVILEGES_ATTRIBUTES; + +namespace HXE.Extensions +{ + public static class ExtProcess + { + /// + /// See Well-known SIDs. + /// + internal const int DOMAIN_GROUP_RID_ADMINS = 0x200; + public static bool IsCurrentProcessElevated() + { + // WindowsPrincipal.IsInRole() secretly checks process security tokens + + WindowsIdentity identity = WindowsIdentity.GetCurrent(ifImpersonating: false); + WindowsPrincipal principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator) + || principal.IsInRole(DOMAIN_GROUP_RID_ADMINS); + } + public static bool IsElevated(this System.Diagnostics.Process process) + { + if (process == null) + throw new ArgumentNullException(nameof(process)); + + if (process.SafeHandle != null && process.Id == 4) + { + // TODO: better exception Type + throw new AccessViolationException("System (PID 4) token can't be opened"); + } + else + { + if (!(bool)OpenProcessToken( + ProcessHandle: process.SafeHandle, + DesiredAccess: TOKEN_QUERY, + TokenHandle: out SafeFileHandle tokenHandle)) + { + FileSystemCompression.ThrowWin32Exception(Kernel32.GetLastError()); + } + + TOKEN_ELEVATION_TYPE elevationType; + + using (tokenHandle) + using (Kernel32.SafeObjectHandle objectHandle = new Kernel32.SafeObjectHandle(tokenHandle.DangerousGetHandle())) + { + elevationType = (TOKEN_ELEVATION_TYPE)AdvApi32.GetTokenElevationType(objectHandle); + } + + return elevationType == TOKEN_ELEVATION_TYPE.TokenElevationTypeFull; + } + } + + /// + /// + /// + /// + /// + /// TODO: NOT TESTED! + public static bool HasSeBackupPrivilege(this System.Diagnostics.Process process) + { + /// + /// Initialize new TOKEN_PRIVILEGES instance for SE_BACKUP_NAME + /// + + TOKEN_PRIVILEGES tokenPrivilege = new TOKEN_PRIVILEGES + { + PrivilegeCount = 1 + }; + tokenPrivilege.Privileges._0.Luid.LowPart = 0u; + tokenPrivilege.Privileges._0.Luid.HighPart = 0; + + /// + /// Get Process Token + /// + + if (!OpenProcessToken( + process.SafeHandle, + TOKEN_QUERY, + out SafeFileHandle processToken + )) + { + Win32ErrorCode error = Kernel32.GetLastError(); + throw new Win32Exception(error, error.GetMessage()); + } + + /// + /// Get Locally Unique Idenifier (LUID) of SE_BACKUP_NAME + /// + + if (!LookupPrivilegeValue( + lpSystemName: null, + lpName: SE_BACKUP_NAME, + lpLuid: out tokenPrivilege.Privileges._0.Luid)) + { + Win32ErrorCode error = Kernel32.GetLastError(); + throw new Win32Exception(error, error.GetMessage()); + } + + PRIVILEGE_SET requiredPrivileges = new PRIVILEGE_SET + { + PrivilegeCount = 1 + }; + requiredPrivileges.Privilege._0.Luid.LowPart = 0u; + requiredPrivileges.Privilege._0.Luid.HighPart = 0; + + /// + /// Check for process token for SE_BACKUP_NAME + /// + + if (!PrivilegeCheck( + processToken, + ref requiredPrivileges, + out int pfResult + )) + { + FileSystemCompression.ThrowWin32Exception(Kernel32.GetLastError()); + } + return pfResult != 0; + } + + /// + /// + /// + /// + /// + /// TODO: Works, but I don't know how it works. If the privilege isn't already present, how does one ADD it? + public static void SetSeBackupPrivilege(this System.Diagnostics.Process process, bool enable = true) + { + + if (!OpenProcessToken(process.SafeHandle, + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + out SafeFileHandle accessTokenHandle)) + { + FileSystemCompression.ThrowWin32Exception(Kernel32.GetLastError()); + } + + if (!LookupPrivilegeValue(null, SE_BACKUP_NAME, out LUID luidPrivilege)) + { + FileSystemCompression.ThrowWin32Exception(Kernel32.GetLastError()); + } + + TOKEN_PRIVILEGES privileges; + privileges.PrivilegeCount = 1; + privileges.Privileges._0.Luid = luidPrivilege; + privileges.Privileges._0.Attributes = SE_PRIVILEGE_ENABLED; + + unsafe + { + if (!AdjustTokenPrivileges( + accessTokenHandle, + false, + privileges, + 0, + null, + null + )) + { + FileSystemCompression.ThrowWin32Exception(Kernel32.GetLastError()); + } + } + } + /* Get TokenInformation as defined Type + internal static TokenInformation GetTokenInformation(this System.Diagnostics.Process process) + { + if (process == null) + throw new ArgumentNullException(nameof(process)); + + if (process.SafeHandle != null && process.Id == 4) + { + // TODO: better exception Type + throw new AccessViolationException("System (PID 4) token can't be opened"); + } + else + { + bool optSuccess = AdvApi32.OpenProcessToken( + processHandle: process.SafeHandle.DangerousGetHandle(), + desiredAccess: AdvApi32.TokenAccessRights.TOKEN_QUERY | AdvApi32.TokenAccessRights.TOKEN_DUPLICATE, + tokenHandle: out Kernel32.SafeObjectHandle tokenHandle); + if (!optSuccess) + { + Win32ErrorCode error = Kernel32.GetLastError(); + throw new PInvoke.Win32Exception(error, message: error.GetMessage()); + } + AdvApi32.GetTokenInformation( + TokenHandle: tokenHandle, + TokenInformationClass: AdvApi32.TOKEN_INFORMATION_CLASS., + TokenInformation: null, + TokenInformationLength: null, + ReturnLength: out int length + ); + } + }*/ + } +} From 4518e397fac8f32cee0249dce51042db67d9e5e9 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 10 Apr 2022 03:13:48 -0700 Subject: [PATCH 15/50] build: fix condition of EnableCompressionInSingleFile --- src/HXE.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index d0b78ab7..f7db28f2 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -63,7 +63,7 @@ true true - true + true From 9848513654a2cbb84a76b1ae0b56c35c1f711349 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 10 Apr 2022 03:14:16 -0700 Subject: [PATCH 16/50] build: update condition of RuntimeIdentifier --- src/HXE.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index f7db28f2..3c5e51bc 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -36,7 +36,7 @@ true true - win7-x86 + win7-x86 true $([MSBuild]::VersionGreaterThanOrEquals($(NETCoreSdkVersion), '6.0.300')) $(Win7SF) From 37c556a3e02915edec6726103ead2c622f316b40 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 10 Apr 2022 03:34:15 -0700 Subject: [PATCH 17/50] chore(vscode): update VSCode/IDE launch configs --- .vscode/launch.json | 15 ++++++++++++++- src/Properties/launchSettings.json | 8 ++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/Properties/launchSettings.json diff --git a/.vscode/launch.json b/.vscode/launch.json index ad85a9e0..d255ab13 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,13 +10,26 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder/bin/Debug/net462/HXE.dll", + "program": "${workspaceFolder}/bin/Debug/net462/HXE.exe", "args": [], "cwd": "${workspaceFolder}/src", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", "stopAtEntry": false }, + { + "name": "Debug LZNT1 - .NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/net462/HXE.exe", + "args": ["--applyLznt1=${workspaceFolder}"], + "cwd": "${workspaceFolder}/src", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, { "name": ".NET Core Attach", "type": "coreclr", diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json new file mode 100644 index 00000000..0f7c5c5e --- /dev/null +++ b/src/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "HXE": { + "commandName": "Project", + "commandLineArgs": "--applyLznt1=$(pwd)" + } + } +} From fc5c4d73f635ffbe528131c373ae0f634550b2d0 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 10 Apr 2022 03:36:59 -0700 Subject: [PATCH 18/50] docs: add copyright header to FileSystemCompression --- src/Common/FileSystemCompression.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index eb02ed95..48ad2a8c 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -1,3 +1,22 @@ +/** + * Copyright (c) 2022 Noah Sherwin + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ /// Compress a folder using NTFS compression in .NET /// https://stackoverflow.com/a/624446 /// - Zack Elan https://stackoverflow.com/users/2461/zack-elan From a7ec42f046cdf572db5351334cf6f266225f3db2 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 10 Apr 2022 03:41:31 -0700 Subject: [PATCH 19/50] fix: get FileSystemCompression working - replace Constants class with CsWin32 constants - update UpdateProgress - set SeBackupPrivilege - get Directory handles properly - add FileInfo.GetHandle() - add DirectoryInfo.GetHandle() - rewrite SetCompression() to use CsWin32 - Add ThrowWin32Exception() --- src/Common/FileSystemCompression.cs | 130 ++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 38 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index 48ad2a8c..63ed7b89 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -21,22 +21,18 @@ /// https://stackoverflow.com/a/624446 /// - Zack Elan https://stackoverflow.com/users/2461/zack-elan /// - Goz https://stackoverflow.com/users/131140/goz - using System; using System.IO; -using System.Runtime.InteropServices; +using HXE.Extensions; +using Microsoft.Win32.SafeHandles; using PInvoke; +using Windows.Win32.Storage.FileSystem; +using static Windows.Win32.PInvoke; namespace HXE.Common { internal static class FileSystemCompression { - private static class Constants - { - public const int FSCTL_SET_COMPRESSION = 0x9C040; - public const short COMPRESSION_FORMAT_DEFAULT = 1; - } - // TODO: Duplicate as non-extension "CompressDirectory()" /// /// Compress the directory represented by the DirectoryInfo object. @@ -57,14 +53,14 @@ public static void Compress(this DirectoryInfo directoryInfo, bool compressFiles { /* Progress */ bool withProgress = progress != null; - var status = withProgress ? new Status + Status status = withProgress ? new Status { Description = $"Compressing '{directoryInfo.FullName}' and its descendents...", Current = 0, Total = 1 } : null; - void UpdateProgress(int n) + void UpdateProgress(int n = 1) { if (!withProgress) return; // not necessary, but it's here just in case. status.Current += n; @@ -97,21 +93,17 @@ void UpdateProgress(int n) UpdateProgress(0); } + /* Adjust current process permissions */ + System.Diagnostics.Process.GetCurrentProcess().SetSeBackupPrivilege(); + /* Compress root directory */ - Kernel32.SafeObjectHandle hDirectory = Kernel32.CreateFile( - filename: directoryInfo.FullName, - access: Kernel32.ACCESS_MASK.GenericRight.GENERIC_READ | Kernel32.ACCESS_MASK.GenericRight.GENERIC_WRITE, - share: Kernel32.FileShare.None, - securityAttributes: Kernel32.SECURITY_ATTRIBUTES.Create(), - creationDisposition: Kernel32.CreationDisposition.OPEN_EXISTING, - flagsAndAttributes: Kernel32.CreateFileFlags.FILE_FLAG_BACKUP_SEMANTICS, - templateFile: null - ); - SetCompression(hDirectory); - hDirectory.Close(); + SafeFileHandle directoryHandle = directoryInfo.GetHandle(); + + SetCompression(directoryHandle); + directoryHandle.Dispose(); if (withProgress) - UpdateProgress(1); + UpdateProgress(); /* Compress sub-directories */ if (recurse) @@ -120,7 +112,7 @@ void UpdateProgress(int n) { directory.Compress(); if (withProgress) - UpdateProgress(1); + UpdateProgress(); } } @@ -131,7 +123,7 @@ void UpdateProgress(int n) { file.Compress(); if (withProgress) - UpdateProgress(1); + UpdateProgress(); } } } @@ -149,33 +141,95 @@ void UpdateProgress(int n) public static void Compress(this FileInfo fileInfo) { FileStream fileStream = fileInfo.Open(mode: FileMode.Open, access: FileAccess.ReadWrite, share: FileShare.None); - SetCompression((Kernel32.SafeObjectHandle)(SafeHandle)fileStream.SafeFileHandle); + SetCompression(fileStream.SafeFileHandle); fileStream.Dispose(); } + + public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) + { + System.Diagnostics.Process.GetCurrentProcess().SetSeBackupPrivilege(); + + SafeFileHandle directoryHandle = CreateFile( + lpFileName: directoryInfo.FullName, + dwDesiredAccess: FILE_ACCESS_FLAGS.FILE_GENERIC_READ | FILE_ACCESS_FLAGS.FILE_GENERIC_WRITE, + dwShareMode: FILE_SHARE_MODE.FILE_SHARE_NONE, + lpSecurityAttributes: null, + dwCreationDisposition: FILE_CREATION_DISPOSITION.OPEN_EXISTING, + dwFlagsAndAttributes: FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS, + hTemplateFile: null + ); + + if (directoryHandle.IsInvalid) + { + var error = Kernel32.GetLastError(); + switch (error) + { + case Win32ErrorCode.ERROR_SHARING_VIOLATION: break; + } + /// TODO: Handle the following exceptions: + /// PInvoke.Win32ErrorCode.ERROR_SHARING_VIOLATION + /// The process cannot access the file because it is being used by another process + /// Win32ErrorCode.ERROR_CANT_ACCESS_FILE ???? maybe ACLs + /// Win32ErrorCode.ERROR_FILE_CHECKED_OUT ???? + /// Win32ErrorCode.ERROR_FILE_ENCRYPTED ???? just skip the entry + /// Win32ErrorCode.ERROR_FILE_INVALID ???? + /// Win32ErrorCode.ERROR_FILE_NOT_FOUND ???? After DirectoryInfo instance is created, the fs item may be deleted + /// Win32ErrorCode.ERROR_FILE_OFFLINE ???? for network drives, offline files + /// Win32ErrorCode.ERROR_FILE_READ_ONLY ???? try temporary disable + /// Win32ErrorCode.ERROR_FILE_SYSTEM_LIMITATION ???? check for NTFS! + /// Win32ErrorCode.ERROR_FILE_TOO_LARGE ???? Only files smaller than 30 GiB can be compressed! + /// Win32ErrorCode.ERROR_OPEN_FILES ???? + /// Win32ErrorCode.ERROR_TOO_MANY_OPEN_FILES ???? Very rare, but possible + /// Win32ErrorCode.ERROR_USER_MAPPED_FILE ???? part of the file is open. not sure if problem + ThrowWin32Exception(error); + return new SafeFileHandle(IntPtr.Zero, true); + } + else + { + return directoryHandle; + } + } + + public static SafeFileHandle GetHandle(this FileInfo fileInfo) + { + FileStream fileStream = fileInfo.Open( + FileMode.Open, + FileAccess.ReadWrite, + FileShare.None); + return fileStream.SafeFileHandle; + } + /// /// P/Invoke DeviceIoControl with the FSCTL_SET_COMPRESSION /// /// //TODO /// DeviceIoControl operation failed. See for exception data. - public static unsafe void SetCompression(Kernel32.SafeObjectHandle handle) + internal static unsafe void SetCompression(SafeFileHandle handle) { - short lpInBuffer = Constants.COMPRESSION_FORMAT_DEFAULT; - bool success = Kernel32.DeviceIoControl( + uint defaultFormat = COMPRESSION_FORMAT_DEFAULT; + + if (!DeviceIoControl( hDevice: handle, - dwIoControlCode: Constants.FSCTL_SET_COMPRESSION, - inBuffer: &lpInBuffer, - nInBufferSize: sizeof(short), // sizeof(typeof(lpInBuffer.GetType())) - outBuffer: (void*)IntPtr.Zero, + dwIoControlCode: FSCTL_SET_COMPRESSION, + lpInBuffer: &defaultFormat, + nInBufferSize: sizeof(uint), // sizeof(typeof(lpInBuffer.GetType())) + lpOutBuffer: (void*)IntPtr.Zero, nOutBufferSize: 0, - pBytesReturned: out _, - lpOverlapped: (Kernel32.OVERLAPPED*)IntPtr.Zero - ); - - if (!success) + lpBytesReturned: (uint*)IntPtr.Zero, + lpOverlapped: (Windows.Win32.System.IO.OVERLAPPED*)IntPtr.Zero + )) { - throw new Win32Exception(Kernel32.GetLastError()); + ThrowWin32Exception(Kernel32.GetLastError()); } } + + internal static void ThrowWin32Exception(Win32ErrorCode errorCode, string messageAppendix = null) + { + throw new Win32Exception( + error: errorCode, + message: errorCode.GetMessage() + " : " + messageAppendix + ); + } } } From 4e017546764f1892252b1b2c895b463feb4bd928 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 17 Apr 2022 04:24:57 -0700 Subject: [PATCH 20/50] refactor: move equivalent of Get-ChildItems to new method and struct chore: improve inline docs --- src/Common/FileSystemCompression.cs | 78 ++++++++++++++++++----------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index 63ed7b89..13a5e1f1 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -33,6 +33,22 @@ namespace HXE.Common { internal static class FileSystemCompression { + internal struct SubItems + { + public DirectoryInfo[] Directories; + public FileInfo[] Files; + } + + internal static SubItems GetSubItems(this DirectoryInfo rootDir, bool compressFiles, bool recurse = false) => new SubItems + { + Files = compressFiles ? + rootDir.GetFiles(searchPattern: "*", searchOption: recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) : + Array.Empty(), + Directories = recurse ? + rootDir.GetDirectories(searchPattern: "*", searchOption: SearchOption.AllDirectories) : + Array.Empty() + }; + // TODO: Duplicate as non-extension "CompressDirectory()" /// /// Compress the directory represented by the DirectoryInfo object. @@ -51,7 +67,9 @@ internal static class FileSystemCompression /// DeviceIoControl operation failed. See for exception data. public static void Compress(this DirectoryInfo directoryInfo, bool compressFiles = true, bool recurse = false, IProgress progress = null) { - /* Progress */ + /// + /// Set up Progress + /// bool withProgress = progress != null; Status status = withProgress ? new Status { @@ -67,37 +85,37 @@ void UpdateProgress(int n = 1) progress.Report(status); } - /* Get files, subdirectories */ - SearchOption searchOption = recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - DirectoryInfo[] directories = recurse ? directoryInfo.GetDirectories( - searchPattern: "*", - searchOption: searchOption - ) : null; - FileInfo[] files = compressFiles ? directoryInfo.GetFiles( - searchPattern: "*", - searchOption: searchOption - ) : null; - - /* Add files, directories count to itemsTotal; Update progress */ + /// + /// Get files, subdirectories + /// + SubItems? subItems = null; + if (compressFiles || recurse) + { + directoryInfo.GetSubItems(compressFiles: compressFiles, recurse: recurse); + } + + /// + /// Add files, directories count to itemsTotal; Update progress + /// if (withProgress) { - if (files != null) + if (subItems != null) { - status.Total += files.Length; - } - if (directories != null) - { - status.Total += directories.Length; + // if (subItems != null), then (Length is always >= 0). + status.Total += subItems.Value.Files.Length + subItems.Value.Directories.Length; } - UpdateProgress(0); + UpdateProgress(0); // Initial update. } - /* Adjust current process permissions */ + /// + /// Adjust current process permissions + /// System.Diagnostics.Process.GetCurrentProcess().SetSeBackupPrivilege(); - /* Compress root directory */ - + /// + /// Compress root directory + /// SafeFileHandle directoryHandle = directoryInfo.GetHandle(); SetCompression(directoryHandle); @@ -105,21 +123,25 @@ void UpdateProgress(int n = 1) if (withProgress) UpdateProgress(); - /* Compress sub-directories */ + /// + /// Compress sub-directories + /// if (recurse) { - foreach (DirectoryInfo directory in directories) + foreach (DirectoryInfo directory in subItems.Value.Directories) { - directory.Compress(); + directory.Compress(compressFiles: false); if (withProgress) UpdateProgress(); } } - /* Compress files*/ + /// + /// Compress files + /// if (compressFiles) { - foreach (FileInfo file in files) + foreach (FileInfo file in subItems.Value.Files) { file.Compress(); if (withProgress) From 42dfde97593f2a89fa5ed358e46683f32ead74f6 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 17 Apr 2022 04:37:37 -0700 Subject: [PATCH 21/50] refactor: move ThrowWin32Exception to new InfoWin32Exception class feat: add Process.GetProcessToken() extension --- src/Common/FileSystemCompression.cs | 94 +++++++++++++++++++---------- src/Common/InfoWin32Exception.cs | 55 +++++++++++++++++ src/Extensions/Process.cs | 86 ++++++++++++++++++-------- 3 files changed, 180 insertions(+), 55 deletions(-) create mode 100644 src/Common/InfoWin32Exception.cs diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index 13a5e1f1..edc7b6db 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -159,7 +159,7 @@ void UpdateProgress(int n = 1) /// The file is not found. /// path is read-only or is a directory. /// The specified path is invalid, such as being on an unmapped drive. - /// DeviceIoControl operation failed. See for exception data. + /// DeviceIoControl operation failed. See for exception data. public static void Compress(this FileInfo fileInfo) { FileStream fileStream = fileInfo.Open(mode: FileMode.Open, access: FileAccess.ReadWrite, share: FileShare.None); @@ -167,7 +167,12 @@ public static void Compress(this FileInfo fileInfo) fileStream.Dispose(); } - + /// + /// Get a Win32 SafeFileHandle for the specified directory + /// + /// An existing directory the current process can access + /// A ReadWrite, NoShare SafeFileHandle representing + /// A Win32Exception with its Message prefixed with the error code's associated string. public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) { System.Diagnostics.Process.GetCurrentProcess().SetSeBackupPrivilege(); @@ -184,28 +189,63 @@ public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) if (directoryHandle.IsInvalid) { - var error = Kernel32.GetLastError(); + Win32ErrorCode error = Kernel32.GetLastError(); + /// TODO: Handle the following exceptions: + /// ERROR_SHARING_VIOLATION + /// The process cannot access the file because it is being used by another process + /// ERROR_ACCESS_DENIED + /// Access is denied. + /// ERROR_CANT_ACCESS_FILE + /// The file cannot be accessed by the system. Maybe ACLs? + /// ERROR_FILE_CHECKED_OUT + /// This file is checked out or locked for editing by another user. + /// ERROR_FILE_ENCRYPTED + /// SKIP + /// ERROR_FILE_INVALID ???? + /// ERROR_FILE_NOT_FOUND + /// After DirectoryInfo instance is created, the fs item may be deleted + /// SKIP + /// ERROR_FILE_OFFLINE + /// for network drives, offline files + /// SKIP + /// ERROR_FILE_READ_ONLY + /// try temporary disable + /// ERROR_FILE_SYSTEM_LIMITATION + /// check for NTFS! + /// ERROR_FILE_TOO_LARGE + /// The file size exceeds the limit allowed and cannot be saved. + /// Only files smaller than 30 GiB can be compressed! + /// ERROR_TOO_MANY_OPEN_FILES + /// The system cannot open the file. + /// Very rare, but possible. + /// SKIP + /// ERROR_USER_MAPPED_FILE + /// SKIP + /// ERROR_VIRUS_INFECTED + /// Operation did not complete successfully because the file contains a virus or potentially unwanted software. + /// SKIP + /// ERROR_VIRUS_DELETED + /// This file contains a virus or potentially unwanted software and cannot be opened. Due to the nature of this virus or potentially unwanted software, the file has been removed from this location. + /// SKIP switch (error) { - case Win32ErrorCode.ERROR_SHARING_VIOLATION: break; + case Win32ErrorCode.ERROR_SHARING_VIOLATION: // The process cannot access the file because it is being used by another process + /// A: Wait and try again + /// B: Schedule the task to execute after reboot + /// C: Steal the file from the other process + + // return directoryHandle; + break; + case Win32ErrorCode.ERROR_FILE_SYSTEM_LIMITATION: + throw new InfoWin32Exception( + error, + ( + new DriveInfo(Path.GetPathRoot(directoryInfo.FullName)).DriveFormat != "NTFS" ? + "Unknown reason. " : "LZNT1 compression can be applied only on NTFS-formatted drives." + ) + directoryInfo.FullName + ); } - /// TODO: Handle the following exceptions: - /// PInvoke.Win32ErrorCode.ERROR_SHARING_VIOLATION - /// The process cannot access the file because it is being used by another process - /// Win32ErrorCode.ERROR_CANT_ACCESS_FILE ???? maybe ACLs - /// Win32ErrorCode.ERROR_FILE_CHECKED_OUT ???? - /// Win32ErrorCode.ERROR_FILE_ENCRYPTED ???? just skip the entry - /// Win32ErrorCode.ERROR_FILE_INVALID ???? - /// Win32ErrorCode.ERROR_FILE_NOT_FOUND ???? After DirectoryInfo instance is created, the fs item may be deleted - /// Win32ErrorCode.ERROR_FILE_OFFLINE ???? for network drives, offline files - /// Win32ErrorCode.ERROR_FILE_READ_ONLY ???? try temporary disable - /// Win32ErrorCode.ERROR_FILE_SYSTEM_LIMITATION ???? check for NTFS! - /// Win32ErrorCode.ERROR_FILE_TOO_LARGE ???? Only files smaller than 30 GiB can be compressed! - /// Win32ErrorCode.ERROR_OPEN_FILES ???? - /// Win32ErrorCode.ERROR_TOO_MANY_OPEN_FILES ???? Very rare, but possible - /// Win32ErrorCode.ERROR_USER_MAPPED_FILE ???? part of the file is open. not sure if problem - ThrowWin32Exception(error); - return new SafeFileHandle(IntPtr.Zero, true); + throw new InfoWin32Exception(error, directoryInfo.FullName); } else { @@ -226,7 +266,7 @@ public static SafeFileHandle GetHandle(this FileInfo fileInfo) /// P/Invoke DeviceIoControl with the FSCTL_SET_COMPRESSION /// /// //TODO - /// DeviceIoControl operation failed. See for exception data. + /// DeviceIoControl operation failed. See for reason. internal static unsafe void SetCompression(SafeFileHandle handle) { uint defaultFormat = COMPRESSION_FORMAT_DEFAULT; @@ -242,16 +282,8 @@ internal static unsafe void SetCompression(SafeFileHandle handle) lpOverlapped: (Windows.Win32.System.IO.OVERLAPPED*)IntPtr.Zero )) { - ThrowWin32Exception(Kernel32.GetLastError()); + throw new InfoWin32Exception(Kernel32.GetLastError()); } } - - internal static void ThrowWin32Exception(Win32ErrorCode errorCode, string messageAppendix = null) - { - throw new Win32Exception( - error: errorCode, - message: errorCode.GetMessage() + " : " + messageAppendix - ); - } } } diff --git a/src/Common/InfoWin32Exception.cs b/src/Common/InfoWin32Exception.cs new file mode 100644 index 00000000..5a30265e --- /dev/null +++ b/src/Common/InfoWin32Exception.cs @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2022 Noah Sherwin + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ +/// Compress a folder using NTFS compression in .NET +/// https://stackoverflow.com/a/624446 +/// - Zack Elan https://stackoverflow.com/users/2461/zack-elan +/// - Goz https://stackoverflow.com/users/131140/goz +using PInvoke; + +namespace HXE.Common +{ + internal class InfoWin32Exception : Win32Exception + { + /// + /// Initializes a new instance of the HXE.Common.InfoWin32Exception class. + /// + /// The Win32 error code associated with this exception. The Message of the exception is set to the error code's associate string if available. + public InfoWin32Exception(Win32ErrorCode error) : base(error, error.GetMessage()) + { + } + + /// + /// Initializes a new instance of the HXE.Common.InfoWin32Exception class. + /// + /// The Win32 error code associated with this exception. + public InfoWin32Exception(int error) : this((Win32ErrorCode) error) + { + } + + /// + /// Initializes a new instance of the HXE.Common.InfoWin32Exception class. + /// + /// The Win32 error code associated with this exception. The Message of the exception is set to the error code's associate string if available. + /// The message for this exception. This is appended to the system message associated with the error code. + public InfoWin32Exception(Win32ErrorCode error, string message) : base(error, error.GetMessage() + " : " + message) + { + } + } +} diff --git a/src/Extensions/Process.cs b/src/Extensions/Process.cs index 36de0f48..921fcc45 100644 --- a/src/Extensions/Process.cs +++ b/src/Extensions/Process.cs @@ -6,7 +6,6 @@ using Windows.Win32.Foundation; using Windows.Win32.Security; using static Windows.Win32.PInvoke; -using static Windows.Win32.Security.TOKEN_ACCESS_MASK; using static Windows.Win32.Security.TOKEN_PRIVILEGES_ATTRIBUTES; namespace HXE.Extensions @@ -40,10 +39,10 @@ public static bool IsElevated(this System.Diagnostics.Process process) { if (!(bool)OpenProcessToken( ProcessHandle: process.SafeHandle, - DesiredAccess: TOKEN_QUERY, + DesiredAccess: TOKEN_ACCESS_MASK.TOKEN_QUERY, TokenHandle: out SafeFileHandle tokenHandle)) { - FileSystemCompression.ThrowWin32Exception(Kernel32.GetLastError()); + throw new InfoWin32Exception(Kernel32.GetLastError()); } TOKEN_ELEVATION_TYPE elevationType; @@ -80,16 +79,7 @@ public static bool HasSeBackupPrivilege(this System.Diagnostics.Process process) /// /// Get Process Token /// - - if (!OpenProcessToken( - process.SafeHandle, - TOKEN_QUERY, - out SafeFileHandle processToken - )) - { - Win32ErrorCode error = Kernel32.GetLastError(); - throw new Win32Exception(error, error.GetMessage()); - } + SafeFileHandle processToken = GetProcessToken(process, TokenAccessMask.Query); /// /// Get Locally Unique Idenifier (LUID) of SE_BACKUP_NAME @@ -121,11 +111,65 @@ out SafeFileHandle processToken out int pfResult )) { - FileSystemCompression.ThrowWin32Exception(Kernel32.GetLastError()); + throw new InfoWin32Exception(Kernel32.GetLastError()); } return pfResult != 0; } + + + [Flags] + public enum TokenAccessMask : uint + { + None = 0, + AssignPrimary = TokenAccessLevels.AssignPrimary, + Duplicate = TokenAccessLevels.Duplicate, + Impersonate = TokenAccessLevels.Impersonate, + Query = TokenAccessLevels.Query, + QuerySource = TokenAccessLevels.QuerySource, + AdjustPrivileges = TokenAccessLevels.AdjustPrivileges, + AdjustGroups = TokenAccessLevels.AdjustGroups, + AdjustDefault = TokenAccessLevels.AdjustDefault, + AdjustSessionId = TokenAccessLevels.AdjustSessionId, + Delete = TOKEN_ACCESS_MASK.TOKEN_DELETE, + ReadControl = TOKEN_ACCESS_MASK.TOKEN_READ_CONTROL, // STANDARD_RIGHTS_READ + Read = Query | ReadControl, + Write = ReadControl | AdjustPrivileges | AdjustGroups | AdjustDefault, + WriteDac = TOKEN_ACCESS_MASK.TOKEN_WRITE_DAC, + WriteOwner = TOKEN_ACCESS_MASK.TOKEN_WRITE_OWNER, + // STANDARD_RIGHTS_REQUIRED = 0xF0000 // 983040U + AllAccess = TokenAccessLevels.AllAccess, + AccessSystemSecurity = TOKEN_ACCESS_MASK.TOKEN_ACCESS_SYSTEM_SECURITY, + MaximumAllowed = TokenAccessLevels.MaximumAllowed + } + + /// + /// Get a Win32 Process security token + /// + /// The process to get a token from. + /// A SafeFileHandle representing the process's security token with the given access privileges. + public static SafeFileHandle GetProcessToken(this System.Diagnostics.Process process, TokenAccessMask access) + { + if (!OpenProcessToken(process.SafeHandle, + (TOKEN_ACCESS_MASK)access, + out SafeFileHandle processToken)) + { + //TODO: improve exception handling. Create/Use new exception types + throw new InfoWin32Exception(Kernel32.GetLastError()); + } + + return processToken; + } + + /// AdjustTokenPrivileges( + /// System.Runtime.InteropServices.SafeHandle TokenHandle, + /// BOOL DisableAllPrivileges, + /// TOKEN_PRIVILEGES? NewState, + /// uint BufferLength, + /// TOKEN_PRIVILEGES* PreviousState, + /// uint* ReturnLength) + //public static void AdjustTokenPrivileges(this SafeFileHandle processToken){} + /// /// /// @@ -134,17 +178,11 @@ out int pfResult /// TODO: Works, but I don't know how it works. If the privilege isn't already present, how does one ADD it? public static void SetSeBackupPrivilege(this System.Diagnostics.Process process, bool enable = true) { - - if (!OpenProcessToken(process.SafeHandle, - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - out SafeFileHandle accessTokenHandle)) - { - FileSystemCompression.ThrowWin32Exception(Kernel32.GetLastError()); - } + SafeFileHandle processToken = process.GetProcessToken(TokenAccessMask.AdjustPrivileges | TokenQuery); if (!LookupPrivilegeValue(null, SE_BACKUP_NAME, out LUID luidPrivilege)) { - FileSystemCompression.ThrowWin32Exception(Kernel32.GetLastError()); + throw new InfoWin32Exception(Kernel32.GetLastError()); } TOKEN_PRIVILEGES privileges; @@ -155,7 +193,7 @@ public static void SetSeBackupPrivilege(this System.Diagnostics.Process process, unsafe { if (!AdjustTokenPrivileges( - accessTokenHandle, + processToken, false, privileges, 0, @@ -163,7 +201,7 @@ public static void SetSeBackupPrivilege(this System.Diagnostics.Process process, null )) { - FileSystemCompression.ThrowWin32Exception(Kernel32.GetLastError()); + throw new InfoWin32Exception(Kernel32.GetLastError()); } } } From 1e96335a848ad1c1920b200c101fca35da4be318 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 17 Apr 2022 04:39:16 -0700 Subject: [PATCH 22/50] refactor: make Process.IsElevated() throw NotSupportedException for SYSTEM (PID 4) --- src/Extensions/Process.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Extensions/Process.cs b/src/Extensions/Process.cs index 921fcc45..e1584a66 100644 --- a/src/Extensions/Process.cs +++ b/src/Extensions/Process.cs @@ -25,6 +25,15 @@ public static bool IsCurrentProcessElevated() return principal.IsInRole(WindowsBuiltInRole.Administrator) || principal.IsInRole(DOMAIN_GROUP_RID_ADMINS); } + + /// + /// + /// + /// + /// + /// + /// is null. + /// public static bool IsElevated(this System.Diagnostics.Process process) { if (process == null) @@ -33,7 +42,7 @@ public static bool IsElevated(this System.Diagnostics.Process process) if (process.SafeHandle != null && process.Id == 4) { // TODO: better exception Type - throw new AccessViolationException("System (PID 4) token can't be opened"); + throw new NotSupportedException("System (PID 4) token can't be opened"); } else { From 1d1f6d2fc84d369fcc3cecfeaef88a9b1f4c934b Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 17 Apr 2022 04:40:02 -0700 Subject: [PATCH 23/50] chore: add TODO for Process.Security member --- src/Extensions/Process.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Extensions/Process.cs b/src/Extensions/Process.cs index e1584a66..632be4f6 100644 --- a/src/Extensions/Process.cs +++ b/src/Extensions/Process.cs @@ -10,6 +10,7 @@ namespace HXE.Extensions { + // TODO: Process.Security member. See https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights public static class ExtProcess { /// From 6defbbfc8fbe211212e528f139848a3f800e4520 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 17 Apr 2022 04:40:48 -0700 Subject: [PATCH 24/50] chore: add comments and inline docs --- src/Common/FileSystemCompression.cs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index edc7b6db..69fdd044 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -58,14 +58,19 @@ internal struct SubItems /// // TODO /// // TODO /// - /// The path encapsulated in the System.IO.DirectoryInfo object is invalid, such
- /// as being on an unmapped drive. + /// The path encapsulated in the System.IO.DirectoryInfo object is invalid, such
+ /// as being on an unmapped drive. ///
+ /// The file system is not NTFS. + /// The current operating system is not Microsoft Windows NT or later. /// The caller does not have the required permission. /// The file is not found. - /// file path is read-only. - /// DeviceIoControl operation failed. See for exception data. - public static void Compress(this DirectoryInfo directoryInfo, bool compressFiles = true, bool recurse = false, IProgress progress = null) + /// + /// file path is read-only.-or-
+ /// This operation is not supported on the current platform.-or-
+ /// The caller does not have the required permission. + ///
+ public static void Compress(this DirectoryInfo directoryInfo, bool compressFiles, bool recurse = false, IProgress progress = null) { /// /// Set up Progress @@ -259,6 +264,11 @@ public static SafeFileHandle GetHandle(this FileInfo fileInfo) FileMode.Open, FileAccess.ReadWrite, FileShare.None); + /// System.Security.SecurityException: The caller does not have the required permission. + /// System.IO.FileNotFoundException: The file is not found. + /// System.UnauthorizedAccessException: path is read-only or is a directory. + /// System.IO.DirectoryNotFoundException: The specified path is invalid, such as being on an unmapped drive. + /// System.IO.IOException: The file is already open. return fileStream.SafeFileHandle; } From bd722128026fc51a379b8f89fc94824e2b340e7d Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 17 Apr 2022 04:41:12 -0700 Subject: [PATCH 25/50] refactor: initialize Status with Total 0 --- src/Common/FileSystemCompression.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index 69fdd044..4eec6fa4 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -80,7 +80,7 @@ public static void Compress(this DirectoryInfo directoryInfo, bool compressFiles { Description = $"Compressing '{directoryInfo.FullName}' and its descendents...", Current = 0, - Total = 1 + Total = 0 } : null; void UpdateProgress(int n = 1) From b6e5f2b8d53c38fd540169c32717d72e14c711c6 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sun, 17 Apr 2022 04:42:08 -0700 Subject: [PATCH 26/50] refactor: add exception-throwing wrapper of CreateFile --- src/Common/FileSystemCompression.cs | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index 4eec6fa4..cc7e0331 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -295,5 +295,41 @@ internal static unsafe void SetCompression(SafeFileHandle handle) throw new InfoWin32Exception(Kernel32.GetLastError()); } } + + /// + /// A Win32Exception with its Message prefixed with the error code's associated string. + internal static unsafe SafeFileHandle CreateFile(string lpFileName, FILE_ACCESS_FLAGS dwDesiredAccess, FILE_SHARE_MODE dwShareMode, Windows.Win32.Security.SECURITY_ATTRIBUTES? lpSecurityAttributes, FILE_CREATION_DISPOSITION dwCreationDisposition, FILE_FLAGS_AND_ATTRIBUTES dwFlagsAndAttributes, SafeHandleZeroOrMinusOneIsInvalid hTemplateFile) + { + bool hTemplateFileAddRef = false; + try + { + fixed (char* lpFileNameLocal = lpFileName) + { + Windows.Win32.Security.SECURITY_ATTRIBUTES lpSecurityAttributesLocal = lpSecurityAttributes ?? default; + Windows.Win32.Foundation.HANDLE hTemplateFileLocal; + if (hTemplateFile is object) + { + hTemplateFile.DangerousAddRef(ref hTemplateFileAddRef); + hTemplateFileLocal = (Windows.Win32.Foundation.HANDLE)hTemplateFile.DangerousGetHandle(); + } + else + { + hTemplateFileLocal = default; + } + + Windows.Win32.Foundation.HANDLE __result = Windows.Win32.PInvoke.CreateFile(lpFileNameLocal, dwDesiredAccess, dwShareMode, lpSecurityAttributes.HasValue ? &lpSecurityAttributesLocal : null, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFileLocal); + var returnHandle = new SafeFileHandle(__result, ownsHandle: true); + + if (returnHandle.IsInvalid) + throw new InfoWin32Exception(error: Kernel32.GetLastError()); + return returnHandle; + } + } + finally + { + if (hTemplateFileAddRef) + hTemplateFile.DangerousRelease(); + } + } } } From 5cf890406abf7a64c24f1e6978ee1c0e87f6cc72 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Fri, 26 May 2023 18:34:46 -0700 Subject: [PATCH 27/50] refactor: replace IsElevated's NotSupportedException with InvalidOperationException docs: fill IsElevated doc elements --- src/Extensions/Process.cs | 43 +++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/src/Extensions/Process.cs b/src/Extensions/Process.cs index 632be4f6..85842b01 100644 --- a/src/Extensions/Process.cs +++ b/src/Extensions/Process.cs @@ -28,43 +28,38 @@ public static bool IsCurrentProcessElevated() } /// - /// + /// Check if the given process is running with elevated permissions, typically due to being run as Administrator. /// - /// - /// - /// + /// The process to inspect for elevated permissions. + /// if the given process is running with elevated permissions. Else, . + /// This method cannot operate on fake processes such as System (PID 4). /// is null. - /// + /// The native, Win32 error encountered when calling the native function OpenProcessToken. More often than not, this is typically ERROR_ACCESS_DENIED due to the current process having insufficient permission to inspect the target process. public static bool IsElevated(this System.Diagnostics.Process process) { if (process == null) throw new ArgumentNullException(nameof(process)); if (process.SafeHandle != null && process.Id == 4) + throw new InvalidOperationException("System (PID 4) token can't be opened"); + + if (!(bool)OpenProcessToken( + ProcessHandle: process.SafeHandle, + DesiredAccess: TOKEN_ACCESS_MASK.TOKEN_QUERY, + TokenHandle: out SafeFileHandle tokenHandle)) { - // TODO: better exception Type - throw new NotSupportedException("System (PID 4) token can't be opened"); + throw new InfoWin32Exception(Kernel32.GetLastError()); } - else - { - if (!(bool)OpenProcessToken( - ProcessHandle: process.SafeHandle, - DesiredAccess: TOKEN_ACCESS_MASK.TOKEN_QUERY, - TokenHandle: out SafeFileHandle tokenHandle)) - { - throw new InfoWin32Exception(Kernel32.GetLastError()); - } - - TOKEN_ELEVATION_TYPE elevationType; - using (tokenHandle) - using (Kernel32.SafeObjectHandle objectHandle = new Kernel32.SafeObjectHandle(tokenHandle.DangerousGetHandle())) - { - elevationType = (TOKEN_ELEVATION_TYPE)AdvApi32.GetTokenElevationType(objectHandle); - } + TOKEN_ELEVATION_TYPE elevationType; - return elevationType == TOKEN_ELEVATION_TYPE.TokenElevationTypeFull; + using (tokenHandle) + using (Kernel32.SafeObjectHandle objectHandle = new Kernel32.SafeObjectHandle(tokenHandle.DangerousGetHandle())) + { + elevationType = (TOKEN_ELEVATION_TYPE)AdvApi32.GetTokenElevationType(objectHandle); } + + return elevationType == TOKEN_ELEVATION_TYPE.TokenElevationTypeFull; } /// From 7fadb341c3946a46758ce0fd29838739bd3ec8e1 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Fri, 26 May 2023 19:08:21 -0700 Subject: [PATCH 28/50] fix: call GetLastPInvokeError instead of GetLastWin32Error refactor: remove redundant InfoWin32Exception class --- src/Common/FileSystemCompression.cs | 19 +++++----- src/Common/InfoWin32Exception.cs | 55 ----------------------------- src/Extensions/Process.cs | 17 +++++---- 3 files changed, 18 insertions(+), 73 deletions(-) delete mode 100644 src/Common/InfoWin32Exception.cs diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index cc7e0331..05342c93 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -23,6 +23,7 @@ /// - Goz https://stackoverflow.com/users/131140/goz using System; using System.IO; +using System.Runtime.InteropServices; using HXE.Extensions; using Microsoft.Win32.SafeHandles; using PInvoke; @@ -164,7 +165,7 @@ void UpdateProgress(int n = 1) /// The file is not found. /// path is read-only or is a directory. /// The specified path is invalid, such as being on an unmapped drive. - /// DeviceIoControl operation failed. See for exception data. + /// DeviceIoControl operation failed. See for exception data. public static void Compress(this FileInfo fileInfo) { FileStream fileStream = fileInfo.Open(mode: FileMode.Open, access: FileAccess.ReadWrite, share: FileShare.None); @@ -177,7 +178,7 @@ public static void Compress(this FileInfo fileInfo) /// /// An existing directory the current process can access /// A ReadWrite, NoShare SafeFileHandle representing - /// A Win32Exception with its Message prefixed with the error code's associated string. + /// A Win32Exception with its Message prefixed with the error code's associated string. public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) { System.Diagnostics.Process.GetCurrentProcess().SetSeBackupPrivilege(); @@ -194,7 +195,7 @@ public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) if (directoryHandle.IsInvalid) { - Win32ErrorCode error = Kernel32.GetLastError(); + Win32ErrorCode error = (Win32ErrorCode)Marshal.GetLastPInvokeError(); /// TODO: Handle the following exceptions: /// ERROR_SHARING_VIOLATION /// The process cannot access the file because it is being used by another process @@ -242,7 +243,7 @@ public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) // return directoryHandle; break; case Win32ErrorCode.ERROR_FILE_SYSTEM_LIMITATION: - throw new InfoWin32Exception( + throw new Win32Exception( error, ( new DriveInfo(Path.GetPathRoot(directoryInfo.FullName)).DriveFormat != "NTFS" ? @@ -250,7 +251,7 @@ public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) ) + directoryInfo.FullName ); } - throw new InfoWin32Exception(error, directoryInfo.FullName); + throw new Win32Exception(error, directoryInfo.FullName); } else { @@ -276,7 +277,7 @@ public static SafeFileHandle GetHandle(this FileInfo fileInfo) /// P/Invoke DeviceIoControl with the FSCTL_SET_COMPRESSION ///
/// //TODO - /// DeviceIoControl operation failed. See for reason. + /// DeviceIoControl operation failed. See for reason. internal static unsafe void SetCompression(SafeFileHandle handle) { uint defaultFormat = COMPRESSION_FORMAT_DEFAULT; @@ -292,12 +293,12 @@ internal static unsafe void SetCompression(SafeFileHandle handle) lpOverlapped: (Windows.Win32.System.IO.OVERLAPPED*)IntPtr.Zero )) { - throw new InfoWin32Exception(Kernel32.GetLastError()); + throw new Win32Exception(); } } /// - /// A Win32Exception with its Message prefixed with the error code's associated string. + /// A Win32Exception with its Message prefixed with the error code's associated string. internal static unsafe SafeFileHandle CreateFile(string lpFileName, FILE_ACCESS_FLAGS dwDesiredAccess, FILE_SHARE_MODE dwShareMode, Windows.Win32.Security.SECURITY_ATTRIBUTES? lpSecurityAttributes, FILE_CREATION_DISPOSITION dwCreationDisposition, FILE_FLAGS_AND_ATTRIBUTES dwFlagsAndAttributes, SafeHandleZeroOrMinusOneIsInvalid hTemplateFile) { bool hTemplateFileAddRef = false; @@ -321,7 +322,7 @@ internal static unsafe SafeFileHandle CreateFile(string lpFileName, FILE_ACCESS_ var returnHandle = new SafeFileHandle(__result, ownsHandle: true); if (returnHandle.IsInvalid) - throw new InfoWin32Exception(error: Kernel32.GetLastError()); + throw new Win32Exception(); return returnHandle; } } diff --git a/src/Common/InfoWin32Exception.cs b/src/Common/InfoWin32Exception.cs deleted file mode 100644 index 5a30265e..00000000 --- a/src/Common/InfoWin32Exception.cs +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2022 Noah Sherwin - * - * This software is provided 'as-is', without any express or implied - * warranty. In no event will the authors be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, subject to the following restrictions: - * - * 1. The origin of this software must not be misrepresented; you must not - * claim that you wrote the original software. If you use this software - * in a product, an acknowledgment in the product documentation would be - * appreciated but is not required. - * 2. Altered source versions must be plainly marked as such, and must not be - * misrepresented as being the original software. - * 3. This notice may not be removed or altered from any source distribution. - */ -/// Compress a folder using NTFS compression in .NET -/// https://stackoverflow.com/a/624446 -/// - Zack Elan https://stackoverflow.com/users/2461/zack-elan -/// - Goz https://stackoverflow.com/users/131140/goz -using PInvoke; - -namespace HXE.Common -{ - internal class InfoWin32Exception : Win32Exception - { - /// - /// Initializes a new instance of the HXE.Common.InfoWin32Exception class. - /// - /// The Win32 error code associated with this exception. The Message of the exception is set to the error code's associate string if available. - public InfoWin32Exception(Win32ErrorCode error) : base(error, error.GetMessage()) - { - } - - /// - /// Initializes a new instance of the HXE.Common.InfoWin32Exception class. - /// - /// The Win32 error code associated with this exception. - public InfoWin32Exception(int error) : this((Win32ErrorCode) error) - { - } - - /// - /// Initializes a new instance of the HXE.Common.InfoWin32Exception class. - /// - /// The Win32 error code associated with this exception. The Message of the exception is set to the error code's associate string if available. - /// The message for this exception. This is appended to the system message associated with the error code. - public InfoWin32Exception(Win32ErrorCode error, string message) : base(error, error.GetMessage() + " : " + message) - { - } - } -} diff --git a/src/Extensions/Process.cs b/src/Extensions/Process.cs index 85842b01..dd43bbf0 100644 --- a/src/Extensions/Process.cs +++ b/src/Extensions/Process.cs @@ -1,12 +1,12 @@ using System; using System.Security.Principal; -using HXE.Common; using Microsoft.Win32.SafeHandles; using PInvoke; using Windows.Win32.Foundation; using Windows.Win32.Security; using static Windows.Win32.PInvoke; using static Windows.Win32.Security.TOKEN_PRIVILEGES_ATTRIBUTES; +using Win32Exception = System.ComponentModel.Win32Exception; namespace HXE.Extensions { @@ -34,7 +34,7 @@ public static bool IsCurrentProcessElevated() /// if the given process is running with elevated permissions. Else, . /// This method cannot operate on fake processes such as System (PID 4). /// is null. - /// The native, Win32 error encountered when calling the native function OpenProcessToken. More often than not, this is typically ERROR_ACCESS_DENIED due to the current process having insufficient permission to inspect the target process. + /// The native, Win32 error encountered when calling the native function OpenProcessToken. More often than not, this is typically ERROR_ACCESS_DENIED due to the current process having insufficient permission to inspect the target process. public static bool IsElevated(this System.Diagnostics.Process process) { if (process == null) @@ -48,7 +48,7 @@ public static bool IsElevated(this System.Diagnostics.Process process) DesiredAccess: TOKEN_ACCESS_MASK.TOKEN_QUERY, TokenHandle: out SafeFileHandle tokenHandle)) { - throw new InfoWin32Exception(Kernel32.GetLastError()); + throw new Win32Exception(); } TOKEN_ELEVATION_TYPE elevationType; @@ -95,8 +95,7 @@ public static bool HasSeBackupPrivilege(this System.Diagnostics.Process process) lpName: SE_BACKUP_NAME, lpLuid: out tokenPrivilege.Privileges._0.Luid)) { - Win32ErrorCode error = Kernel32.GetLastError(); - throw new Win32Exception(error, error.GetMessage()); + throw new Win32Exception(); } PRIVILEGE_SET requiredPrivileges = new PRIVILEGE_SET @@ -116,7 +115,7 @@ public static bool HasSeBackupPrivilege(this System.Diagnostics.Process process) out int pfResult )) { - throw new InfoWin32Exception(Kernel32.GetLastError()); + throw new Win32Exception(); } return pfResult != 0; } @@ -160,7 +159,7 @@ public static SafeFileHandle GetProcessToken(this System.Diagnostics.Process pro out SafeFileHandle processToken)) { //TODO: improve exception handling. Create/Use new exception types - throw new InfoWin32Exception(Kernel32.GetLastError()); + throw new Win32Exception(); } return processToken; @@ -187,7 +186,7 @@ public static void SetSeBackupPrivilege(this System.Diagnostics.Process process, if (!LookupPrivilegeValue(null, SE_BACKUP_NAME, out LUID luidPrivilege)) { - throw new InfoWin32Exception(Kernel32.GetLastError()); + throw new Win32Exception(); } TOKEN_PRIVILEGES privileges; @@ -206,7 +205,7 @@ public static void SetSeBackupPrivilege(this System.Diagnostics.Process process, null )) { - throw new InfoWin32Exception(Kernel32.GetLastError()); + throw new Win32Exception(); } } } From 27260eca1033060449bb5bd28c04422a4591765f Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Fri, 26 May 2023 19:12:37 -0700 Subject: [PATCH 29/50] build: set TargetFramework back to net6.0 Why did I change this to .NET Framework 4.6.2? --- src/HXE.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index 3c5e51bc..968591f8 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -5,7 +5,7 @@ AnyCPU false Exe - net462 + net6.0 8 HXE.Program HXE From 6c538534f81efecec36e79c4eb7a652971fb2e81 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Fri, 26 May 2023 19:28:21 -0700 Subject: [PATCH 30/50] docs: make custom 'TokenAccessMask' inheritdoc as much as possible --- src/Extensions/Process.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Extensions/Process.cs b/src/Extensions/Process.cs index dd43bbf0..2411d405 100644 --- a/src/Extensions/Process.cs +++ b/src/Extensions/Process.cs @@ -126,24 +126,38 @@ out int pfResult public enum TokenAccessMask : uint { None = 0, + /// AssignPrimary = TokenAccessLevels.AssignPrimary, + /// Duplicate = TokenAccessLevels.Duplicate, + /// Impersonate = TokenAccessLevels.Impersonate, + /// Query = TokenAccessLevels.Query, + /// QuerySource = TokenAccessLevels.QuerySource, + /// AdjustPrivileges = TokenAccessLevels.AdjustPrivileges, + /// AdjustGroups = TokenAccessLevels.AdjustGroups, + /// AdjustDefault = TokenAccessLevels.AdjustDefault, + /// AdjustSessionId = TokenAccessLevels.AdjustSessionId, Delete = TOKEN_ACCESS_MASK.TOKEN_DELETE, - ReadControl = TOKEN_ACCESS_MASK.TOKEN_READ_CONTROL, // STANDARD_RIGHTS_READ + /// STANDARD_RIGHTS_READ + ReadControl = TOKEN_ACCESS_MASK.TOKEN_READ_CONTROL, + /// Read = Query | ReadControl, + /// Write = ReadControl | AdjustPrivileges | AdjustGroups | AdjustDefault, WriteDac = TOKEN_ACCESS_MASK.TOKEN_WRITE_DAC, WriteOwner = TOKEN_ACCESS_MASK.TOKEN_WRITE_OWNER, // STANDARD_RIGHTS_REQUIRED = 0xF0000 // 983040U + /// AllAccess = TokenAccessLevels.AllAccess, AccessSystemSecurity = TOKEN_ACCESS_MASK.TOKEN_ACCESS_SYSTEM_SECURITY, + /// MaximumAllowed = TokenAccessLevels.MaximumAllowed } From 58334c8ae77d684cc141665a519ae71b5218c45d Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Fri, 26 May 2023 19:38:55 -0700 Subject: [PATCH 31/50] fix: fix reference to TokenAccessMask.Query --- src/Extensions/Process.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Extensions/Process.cs b/src/Extensions/Process.cs index 2411d405..d22e8f6b 100644 --- a/src/Extensions/Process.cs +++ b/src/Extensions/Process.cs @@ -120,8 +120,6 @@ out int pfResult return pfResult != 0; } - - [Flags] public enum TokenAccessMask : uint { @@ -193,10 +191,9 @@ public static SafeFileHandle GetProcessToken(this System.Diagnostics.Process pro ///
/// /// - /// TODO: Works, but I don't know how it works. If the privilege isn't already present, how does one ADD it? public static void SetSeBackupPrivilege(this System.Diagnostics.Process process, bool enable = true) { - SafeFileHandle processToken = process.GetProcessToken(TokenAccessMask.AdjustPrivileges | TokenQuery); + SafeFileHandle processToken = process.GetProcessToken(TokenAccessMask.AdjustPrivileges | TokenAccessMask.Query); if (!LookupPrivilegeValue(null, SE_BACKUP_NAME, out LUID luidPrivilege)) { From aae7666bcee1af6ad2d94db76b64d3b8741bd1f4 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Fri, 26 May 2023 19:12:37 -0700 Subject: [PATCH 32/50] build: set TargetFramework back to net6.0 Why did I change this to .NET Framework 4.6.2? --- src/HXE.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index 3c5e51bc..968591f8 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -5,7 +5,7 @@ AnyCPU false Exe - net462 + net6.0 8 HXE.Program HXE From 471e74d52660f1f6612bed2395a900473acecb8f Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Fri, 26 May 2023 22:35:19 -0700 Subject: [PATCH 33/50] build: switch to net6.0-windows until we can get rid of WPF --- src/HXE.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index 968591f8..9789b188 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -5,7 +5,7 @@ AnyCPU false Exe - net6.0 + net6.0-windows 8 HXE.Program HXE From 2e9b309d709da384463bcb196f4ea59fe3ac8426 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Fri, 26 May 2023 22:36:53 -0700 Subject: [PATCH 34/50] build: use latest C# language version --- src/HXE.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index 9789b188..c8630944 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -6,7 +6,6 @@ false Exe net6.0-windows - 8 HXE.Program HXE HXE From 40f48e2ff350fceb03d0f8b0000db4624bef94b7 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Fri, 26 May 2023 22:37:38 -0700 Subject: [PATCH 35/50] build: remove redundant property 'Deterministic' --- src/HXE.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index c8630944..d3125cd5 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -11,8 +11,6 @@ HXE Assets\icon.ico - - true OnBuildSuccess From 946bbfffea92eb20bd1fcfd22cc2624767e30d79 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 01:11:30 -0700 Subject: [PATCH 36/50] revert(deps): "build: re-add Fody, Costura.Fody" This reverts commit a40015a0c408fc9856d9293dd88a91ac99885658. --- src/FodyWeavers.xml | 6 ------ src/HXE.csproj | 8 +------- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 src/FodyWeavers.xml diff --git a/src/FodyWeavers.xml b/src/FodyWeavers.xml deleted file mode 100644 index 250af971..00000000 --- a/src/FodyWeavers.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/src/HXE.csproj b/src/HXE.csproj index d3125cd5..c66e9f55 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -6,6 +6,7 @@ false Exe net6.0-windows + net6.0;net6.0-windows HXE.Program HXE HXE @@ -108,13 +109,6 @@
- - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - all runtime; build; native; contentfiles; analyzers; buildtransitive From 4c74ded943eb390a423561c30f92c6e7956a27f5 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 03:39:03 -0700 Subject: [PATCH 37/50] build(deps): update dependency microsoft.windows.cswin32 to v0.2.252-beta --- src/HXE.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index c66e9f55..346ec7e0 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -118,10 +118,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + From 52126a8901d7ca909e2085c0e5bc50f79797ea61 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 03:55:25 -0700 Subject: [PATCH 38/50] build(deps): update PInvoke monorepo to v0.7.124 --- src/HXE.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index 346ec7e0..e3c202fe 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -119,8 +119,8 @@ all
- - + + From 1cfa07ae7a5fefac2619225f94fc0aaa762c4c2f Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 03:27:46 -0700 Subject: [PATCH 39/50] chore(vscode): update launch configs to net6.0-windows/win7-x86 --- .vscode/launch.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d255ab13..61effa42 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/bin/Debug/net462/HXE.exe", + "program": "${workspaceFolder}/bin/Debug/net6.0-windows/win7-x86/HXE.dll", "args": [], "cwd": "${workspaceFolder}/src", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console @@ -23,8 +23,10 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/bin/Debug/net462/HXE.exe", - "args": ["--applyLznt1=${workspaceFolder}"], + "program": "${workspaceFolder}/bin/Debug/net6.0-windows/win7-x86/HXE.exe", + "args": [ + "--applyLznt1=${workspaceFolder}" + ], "cwd": "${workspaceFolder}/src", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", From 4b331d84b36f4cdd17559b22c361a323fb6de6eb Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 03:30:00 -0700 Subject: [PATCH 40/50] chore(vscode): restrict launch config 'Debug LZNT1' to operate only on README.md --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 61effa42..f557c7e0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -25,7 +25,7 @@ // If you have changed target frameworks, make sure to update the program path. "program": "${workspaceFolder}/bin/Debug/net6.0-windows/win7-x86/HXE.exe", "args": [ - "--applyLznt1=${workspaceFolder}" + "--applyLznt1='${workspaceFolder}/README.md'" ], "cwd": "${workspaceFolder}/src", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console From b7695b1d70f135b400ef58b931930923414146cf Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 02:53:58 -0700 Subject: [PATCH 41/50] refactor: wrap some specific SET_COMPRESSION errors docs: update SetCompression's docs --- src/Common/FileSystemCompression.cs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index 05342c93..b955f2b6 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -276,24 +276,39 @@ public static SafeFileHandle GetHandle(this FileInfo fileInfo) /// /// P/Invoke DeviceIoControl with the FSCTL_SET_COMPRESSION /// - /// //TODO - /// DeviceIoControl operation failed. See for reason. + /// If a file, then the handle must have READ/WRITE access and NO sharing. The latter prevents data loss by race conditions. If a Directory, then the same ACCESS and SHARE values should be used with the addition of . + /// The input buffer length is less than 2, or the handle is not to a file or directory, or the requested CompressionState is not one of the values listed in the table for 'CompressionState' in 'FSCTL_SET_COMPRESSION Request (section 2.3.67)' (see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/77f650a3-e3a2-4a25-baac-4bf9b36bcc46). + /// The device, driver, or file system does not implement functionality for or support the request ''. + /// The file could not be compressed because the disk or volume is full. + /// DeviceIoControl operation failed. See exception's NativeErrorCode (as Win32ErrorCode) and Message for details. + /// TODO: UNIX/POSIX-based OSs and capable file systems. https://unix.stackexchange.com/questions/635016/how-to-get-transparent-drive-or-folder-compression-for-ext4-partition-used-by-de internal static unsafe void SetCompression(SafeFileHandle handle) { uint defaultFormat = COMPRESSION_FORMAT_DEFAULT; + //TODO: use Nt version instead. Its NTSTATUS responses are documented (as part of "Windows Protocols"), but the win32 API variant's errors are barely documented and sometimes misleading. if (!DeviceIoControl( hDevice: handle, dwIoControlCode: FSCTL_SET_COMPRESSION, lpInBuffer: &defaultFormat, nInBufferSize: sizeof(uint), // sizeof(typeof(lpInBuffer.GetType())) - lpOutBuffer: (void*)IntPtr.Zero, + lpOutBuffer: null, nOutBufferSize: 0, - lpBytesReturned: (uint*)IntPtr.Zero, - lpOverlapped: (Windows.Win32.System.IO.OVERLAPPED*)IntPtr.Zero + lpBytesReturned: null, + lpOverlapped: null )) { - throw new Win32Exception(); + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ff42e806-0125-4fc2-adf7-e9db53bea161 + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fsa/8e2a2e1e-5a90-4251-8b4d-18f1a4c0be43 + Win32ErrorCode err = (Win32ErrorCode)Marshal.GetLastPInvokeError(); + if (err is Win32ErrorCode.ERROR_INVALID_PARAMETER) + throw new ArgumentException("The input buffer length is less than 2, or the handle is not to a file or directory, or the requested CompressionState is not one of the values listed in the table for 'CompressionState' in 'FSCTL_SET_COMPRESSION Request (section 2.3.67)' (see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/77f650a3-e3a2-4a25-baac-4bf9b36bcc46).", new Win32Exception(err)); + else if (err is Win32ErrorCode.ERROR_INVALID_FUNCTION) // STATUS_INVALID_DEVICE_REQUEST + throw new NotSupportedException($"The device, driver, or file system does not implement functionality for or support the request '{nameof(FSCTL_SET_COMPRESSION)}'.", new Win32Exception(err)); + else if (err is Win32ErrorCode.ERROR_DISK_FULL) + throw new IOException("The file could not be compressed because the disk or volume is full.", new Win32Exception(err)); + else + throw new Win32Exception(err); } } From d0cb3578433d01d8f5320cb2cd281d1105736595 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 03:13:05 -0700 Subject: [PATCH 42/50] fix: return GetHandle's FileStream to dispose it later --- src/Common/FileSystemCompression.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index b955f2b6..66c08a17 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -259,18 +259,23 @@ public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) } } - public static SafeFileHandle GetHandle(this FileInfo fileInfo) + /// + /// Open a FileStream (preferably with a using statement) to the selected file with the necessary access and share for (de)compression. Pass the FileStream's SafeFileHandle when necessary. If a using statement was not use and the FileStream is no longer needed, call fileStream.Dipose(). + /// + /// An existing file the user (or this code) can access. + /// A FileStream wrapping the SafeFileHandle. Assign in a using statement or call fileStream.Dispose() when no longer needed. + /// + public static FileStream GetHandle(this FileInfo fileInfo) { - FileStream fileStream = fileInfo.Open( - FileMode.Open, - FileAccess.ReadWrite, - FileShare.None); /// System.Security.SecurityException: The caller does not have the required permission. /// System.IO.FileNotFoundException: The file is not found. /// System.UnauthorizedAccessException: path is read-only or is a directory. /// System.IO.DirectoryNotFoundException: The specified path is invalid, such as being on an unmapped drive. /// System.IO.IOException: The file is already open. - return fileStream.SafeFileHandle; + return fileInfo.Open( + FileMode.Open, + FileAccess.ReadWrite, + FileShare.None); } /// From f856cc98a1b18b5d478a7a8d919bd0bad208b9db Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 03:24:37 -0700 Subject: [PATCH 43/50] docs: update SetSeBackupPrivilege documentation --- src/Extensions/Process.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Extensions/Process.cs b/src/Extensions/Process.cs index d22e8f6b..f014bd48 100644 --- a/src/Extensions/Process.cs +++ b/src/Extensions/Process.cs @@ -177,20 +177,21 @@ public static SafeFileHandle GetProcessToken(this System.Diagnostics.Process pro return processToken; } - /// AdjustTokenPrivileges( - /// System.Runtime.InteropServices.SafeHandle TokenHandle, - /// BOOL DisableAllPrivileges, - /// TOKEN_PRIVILEGES? NewState, - /// uint BufferLength, - /// TOKEN_PRIVILEGES* PreviousState, - /// uint* ReturnLength) + // AdjustTokenPrivileges( + // System.Runtime.InteropServices.SafeHandle TokenHandle, + // BOOL DisableAllPrivileges, + // TOKEN_PRIVILEGES? NewState, + // uint BufferLength, + // TOKEN_PRIVILEGES* PreviousState, + // uint* ReturnLength) //public static void AdjustTokenPrivileges(this SafeFileHandle processToken){} /// - /// + /// Enable or disable SE_BACKUP_PRIVILEGE for the given process. This privilege must be enabled to toggle directories' COMPRESSION attributes and flags. /// - /// - /// + /// The process for which SE_BACKUP_PRIVILEGE will be enabled, if the caller has permission to do so. + /// If (default), SE_BACKUP_PRIVILEGE will be enabled for the given process. Else, the privilege will be disabled for the given process. + /// LookupPrivilegeValue or AdjustTokenPrivileges failed. public static void SetSeBackupPrivilege(this System.Diagnostics.Process process, bool enable = true) { SafeFileHandle processToken = process.GetProcessToken(TokenAccessMask.AdjustPrivileges | TokenAccessMask.Query); From 489a11d60f690a06dc374de40a139655bac86944 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 16:16:44 -0700 Subject: [PATCH 44/50] build(deps): re-add CsWin32's PrivateAssets property --- src/HXE.csproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index e3c202fe..4fe1541a 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -118,7 +118,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + + all + From 04e19535c666fa9d8da75361e93d8c14420d66e7 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 16:17:18 -0700 Subject: [PATCH 45/50] build: enable prop EmitCompilerGeneratedFiles --- src/HXE.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index 4fe1541a..e7d8487e 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -49,7 +49,8 @@ en true true - 7.0 + 7.0 + true DEBUG;TRACE From 4045c341c97f580efbf88d870b959e15cce13707 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 16:18:41 -0700 Subject: [PATCH 46/50] build: comment out prop TargetFrameworks CsWin32's code gen was not running with this property present. --- src/HXE.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HXE.csproj b/src/HXE.csproj index e7d8487e..146a15f6 100644 --- a/src/HXE.csproj +++ b/src/HXE.csproj @@ -6,7 +6,7 @@ false Exe net6.0-windows - net6.0;net6.0-windows + HXE.Program HXE HXE From 07c58c6921915597059fb283712028fcab1aa883 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 16:59:48 -0700 Subject: [PATCH 47/50] fix: update PInvoke, CsWin32 references --- src/Common/FileSystemCompression.cs | 42 +++++------------------------ src/NativeMethods.txt | 18 ++++++++----- 2 files changed, 18 insertions(+), 42 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index 66c08a17..b768cd49 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -185,7 +185,7 @@ public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) SafeFileHandle directoryHandle = CreateFile( lpFileName: directoryInfo.FullName, - dwDesiredAccess: FILE_ACCESS_FLAGS.FILE_GENERIC_READ | FILE_ACCESS_FLAGS.FILE_GENERIC_WRITE, + dwDesiredAccess: FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE, dwShareMode: FILE_SHARE_MODE.FILE_SHARE_NONE, lpSecurityAttributes: null, dwCreationDisposition: FILE_CREATION_DISPOSITION.OPEN_EXISTING, @@ -289,14 +289,14 @@ public static FileStream GetHandle(this FileInfo fileInfo) /// TODO: UNIX/POSIX-based OSs and capable file systems. https://unix.stackexchange.com/questions/635016/how-to-get-transparent-drive-or-folder-compression-for-ext4-partition-used-by-de internal static unsafe void SetCompression(SafeFileHandle handle) { - uint defaultFormat = COMPRESSION_FORMAT_DEFAULT; + COMPRESSION_FORMAT defaultFormat = COMPRESSION_FORMAT.COMPRESSION_FORMAT_DEFAULT; //TODO: use Nt version instead. Its NTSTATUS responses are documented (as part of "Windows Protocols"), but the win32 API variant's errors are barely documented and sometimes misleading. if (!DeviceIoControl( hDevice: handle, dwIoControlCode: FSCTL_SET_COMPRESSION, lpInBuffer: &defaultFormat, - nInBufferSize: sizeof(uint), // sizeof(typeof(lpInBuffer.GetType())) + nInBufferSize: sizeof(COMPRESSION_FORMAT), // sizeof(typeof(lpInBuffer.GetType())) lpOutBuffer: null, nOutBufferSize: 0, lpBytesReturned: null, @@ -317,40 +317,12 @@ internal static unsafe void SetCompression(SafeFileHandle handle) } } - /// + /// /// A Win32Exception with its Message prefixed with the error code's associated string. - internal static unsafe SafeFileHandle CreateFile(string lpFileName, FILE_ACCESS_FLAGS dwDesiredAccess, FILE_SHARE_MODE dwShareMode, Windows.Win32.Security.SECURITY_ATTRIBUTES? lpSecurityAttributes, FILE_CREATION_DISPOSITION dwCreationDisposition, FILE_FLAGS_AND_ATTRIBUTES dwFlagsAndAttributes, SafeHandleZeroOrMinusOneIsInvalid hTemplateFile) + internal static unsafe SafeFileHandle CreateFile(string lpFileName, FILE_ACCESS_RIGHTS dwDesiredAccess, FILE_SHARE_MODE dwShareMode, Windows.Win32.Security.SECURITY_ATTRIBUTES? lpSecurityAttributes, FILE_CREATION_DISPOSITION dwCreationDisposition, FILE_FLAGS_AND_ATTRIBUTES dwFlagsAndAttributes, SafeHandleZeroOrMinusOneIsInvalid hTemplateFile) { - bool hTemplateFileAddRef = false; - try - { - fixed (char* lpFileNameLocal = lpFileName) - { - Windows.Win32.Security.SECURITY_ATTRIBUTES lpSecurityAttributesLocal = lpSecurityAttributes ?? default; - Windows.Win32.Foundation.HANDLE hTemplateFileLocal; - if (hTemplateFile is object) - { - hTemplateFile.DangerousAddRef(ref hTemplateFileAddRef); - hTemplateFileLocal = (Windows.Win32.Foundation.HANDLE)hTemplateFile.DangerousGetHandle(); - } - else - { - hTemplateFileLocal = default; - } - - Windows.Win32.Foundation.HANDLE __result = Windows.Win32.PInvoke.CreateFile(lpFileNameLocal, dwDesiredAccess, dwShareMode, lpSecurityAttributes.HasValue ? &lpSecurityAttributesLocal : null, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFileLocal); - var returnHandle = new SafeFileHandle(__result, ownsHandle: true); - - if (returnHandle.IsInvalid) - throw new Win32Exception(); - return returnHandle; - } - } - finally - { - if (hTemplateFileAddRef) - hTemplateFile.DangerousRelease(); - } + SafeFileHandle __result = Windows.Win32.PInvoke.CreateFile(lpFileName, (uint)dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + return __result.IsInvalid ? throw new Win32Exception() : __result; } } } diff --git a/src/NativeMethods.txt b/src/NativeMethods.txt index fade8d1a..f3f16281 100644 --- a/src/NativeMethods.txt +++ b/src/NativeMethods.txt @@ -1,14 +1,18 @@ -COMPRESSION_FORMAT_DEFAULT -FSCTL_SET_COMPRESSION -SE_BACKUP_NAME -TOKEN_ELEVATION_TYPE -TOKEN_INFORMATION_CLASS -TOKEN_PRIVILEGES -TOKEN_PRIVILEGES_ATTRIBUTES AdjustTokenPrivileges +COMPRESSION_FORMAT CreateFile DeviceIoControl +FILE_ACCESS_RIGHTS +FILE_CREATION_DISPOSITION +FILE_FLAGS_AND_ATTRIBUTES +FSCTL_SET_COMPRESSION GetTokenInformation LookupPrivilegeValue OpenProcessToken PrivilegeCheck +SE_BACKUP_NAME +TOKEN_ACCESS_MASK +TOKEN_ELEVATION_TYPE +TOKEN_INFORMATION_CLASS +TOKEN_PRIVILEGES +TOKEN_PRIVILEGES_ATTRIBUTES From abca804ba76e7c21fb1f90ab360757729488c587 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 17:35:24 -0700 Subject: [PATCH 48/50] feat: add SharingViolationException --- src/Common/FileSystemCompression.cs | 7 ++- src/Exceptions/SharingViolationException.cs | 62 +++++++++++++++++++++ src/NativeMethods.txt | 1 + 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/Exceptions/SharingViolationException.cs diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index b768cd49..2688f01c 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -24,6 +24,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using HXE.Exceptions; using HXE.Extensions; using Microsoft.Win32.SafeHandles; using PInvoke; @@ -179,6 +180,7 @@ public static void Compress(this FileInfo fileInfo) /// An existing directory the current process can access /// A ReadWrite, NoShare SafeFileHandle representing /// A Win32Exception with its Message prefixed with the error code's associated string. + /// The process cannot access the file because it is being used by another process public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) { System.Diagnostics.Process.GetCurrentProcess().SetSeBackupPrivilege(); @@ -239,9 +241,8 @@ public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) /// A: Wait and try again /// B: Schedule the task to execute after reboot /// C: Steal the file from the other process - - // return directoryHandle; - break; + //return directoryHandle; + throw new SharingViolationException($"The process cannot access '{directoryInfo.FullName}' because it is being used by another process.", new Win32Exception(error)); case Win32ErrorCode.ERROR_FILE_SYSTEM_LIMITATION: throw new Win32Exception( error, diff --git a/src/Exceptions/SharingViolationException.cs b/src/Exceptions/SharingViolationException.cs new file mode 100644 index 00000000..cd671c37 --- /dev/null +++ b/src/Exceptions/SharingViolationException.cs @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2023 Noah Sherwin + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ +using System; +using System.Runtime.Serialization; +using PInvoke; +using Windows.Win32.Foundation; + +namespace HXE.Exceptions; + +[Serializable] +internal class SharingViolationException : System.IO.IOException +{ + public SharingViolationException() + { } + + public SharingViolationException(string message) : base(message) + { } + + public SharingViolationException(string message, Exception innerException) : base(message, innerException) + { } + + protected SharingViolationException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + + public SharingViolationException(string message, int hresult) : base(message, hresult) + { } + + public static SharingViolationException FromWin32ErrorCode(Win32ErrorCode win32ErrorCode) + { + if (win32ErrorCode is not Win32ErrorCode.ERROR_SHARING_VIOLATION) + throw new ArgumentException($"This method can only be used with {Win32ErrorCode.ERROR_SHARING_VIOLATION}.", paramName: nameof(win32ErrorCode)); + + var win32Exception = new Win32Exception(win32ErrorCode); + return new SharingViolationException(win32Exception.Message, win32Exception); + } + + public static SharingViolationException FromWin32ErrorCode(WIN32_ERROR win32ErrorCode) + { + if (win32ErrorCode is not WIN32_ERROR.ERROR_SHARING_VIOLATION) + throw new ArgumentException($"This method can only be used with {Win32ErrorCode.ERROR_SHARING_VIOLATION}.", paramName: nameof(win32ErrorCode)); + + var win32Exception = new Win32Exception((Win32ErrorCode)win32ErrorCode); + return new SharingViolationException(win32Exception.Message, win32Exception); + } +} diff --git a/src/NativeMethods.txt b/src/NativeMethods.txt index f3f16281..b91f748a 100644 --- a/src/NativeMethods.txt +++ b/src/NativeMethods.txt @@ -16,3 +16,4 @@ TOKEN_ELEVATION_TYPE TOKEN_INFORMATION_CLASS TOKEN_PRIVILEGES TOKEN_PRIVILEGES_ATTRIBUTES +WIN32_ERROR From 9486b41bfd594e3ffc83b0ccaf006e4229ed22c8 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 17:48:09 -0700 Subject: [PATCH 49/50] refactor: improve GetHandle exception messages --- src/Common/FileSystemCompression.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index 2688f01c..b57bf7c5 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -247,12 +247,12 @@ public static SafeFileHandle GetHandle(this DirectoryInfo directoryInfo) throw new Win32Exception( error, ( - new DriveInfo(Path.GetPathRoot(directoryInfo.FullName)).DriveFormat != "NTFS" ? - "Unknown reason. " : "LZNT1 compression can be applied only on NTFS-formatted drives." + new DriveInfo(Path.GetPathRoot(directoryInfo.FullName)).DriveFormat is "NTFS" ? + "LZNT1 compression can be applied only on NTFS-formatted drives." : "Unknown reason. " ) + directoryInfo.FullName ); } - throw new Win32Exception(error, directoryInfo.FullName); + throw new Win32Exception(error, $"[{directoryInfo.FullName}] : {error.GetMessage()}"); } else { From 4f07f2f644803a3109e2550c7ff232b4f6137815 Mon Sep 17 00:00:00 2001 From: Noah Sherwin Date: Sat, 27 May 2023 17:47:27 -0700 Subject: [PATCH 50/50] refactor: change SubItems to class --- src/Common/FileSystemCompression.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Common/FileSystemCompression.cs b/src/Common/FileSystemCompression.cs index b57bf7c5..e594365f 100644 --- a/src/Common/FileSystemCompression.cs +++ b/src/Common/FileSystemCompression.cs @@ -35,7 +35,7 @@ namespace HXE.Common { internal static class FileSystemCompression { - internal struct SubItems + internal class SubItems { public DirectoryInfo[] Directories; public FileInfo[] Files; @@ -95,7 +95,7 @@ void UpdateProgress(int n = 1) /// /// Get files, subdirectories /// - SubItems? subItems = null; + SubItems subItems = null; if (compressFiles || recurse) { directoryInfo.GetSubItems(compressFiles: compressFiles, recurse: recurse); @@ -109,7 +109,7 @@ void UpdateProgress(int n = 1) if (subItems != null) { // if (subItems != null), then (Length is always >= 0). - status.Total += subItems.Value.Files.Length + subItems.Value.Directories.Length; + status.Total += subItems.Files.Length + subItems.Directories.Length; } UpdateProgress(0); // Initial update. @@ -135,7 +135,7 @@ void UpdateProgress(int n = 1) /// if (recurse) { - foreach (DirectoryInfo directory in subItems.Value.Directories) + foreach (DirectoryInfo directory in subItems.Directories) { directory.Compress(compressFiles: false); if (withProgress) @@ -148,7 +148,7 @@ void UpdateProgress(int n = 1) /// if (compressFiles) { - foreach (FileInfo file in subItems.Value.Files) + foreach (FileInfo file in subItems.Files) { file.Compress(); if (withProgress)