diff --git a/BurnOutSharp/BurnOutSharp.csproj b/BurnOutSharp/BurnOutSharp.csproj index 3ab7310d..ff308869 100644 --- a/BurnOutSharp/BurnOutSharp.csproj +++ b/BurnOutSharp/BurnOutSharp.csproj @@ -27,8 +27,6 @@ - - diff --git a/BurnOutSharp/External/LessIO/FileAttributes.cs b/BurnOutSharp/External/LessIO/FileAttributes.cs new file mode 100644 index 00000000..b9cdaa11 --- /dev/null +++ b/BurnOutSharp/External/LessIO/FileAttributes.cs @@ -0,0 +1,55 @@ +using System; + +namespace LessIO +{ + //TODO: These are the samea s Win32. Consider whether we should expose all of these? + /// + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/gg258117%28v=vs.85%29.aspx + /// + /// + /// These have the same values as they can generally be casted between these enums (this Enum has more values than System.IO though). + /// Defined in C:\Program Files (x86)\Windows Kits\8.1\Include\um\winnt.h + /// + [Flags] + public enum FileAttributes + { + //#define FILE_ATTRIBUTE_READONLY 0x00000001 + ReadOnly = 0x00000001, + //#define FILE_ATTRIBUTE_HIDDEN 0x00000002 + Hidden = 0x00000002, + //#define FILE_ATTRIBUTE_SYSTEM 0x00000004 + System = 0x00000004, + //#define FILE_ATTRIBUTE_DIRECTORY 0x00000010 + Directory = 0x00000010, + //#define FILE_ATTRIBUTE_ARCHIVE 0x00000020 + Archive = 0x00000020, + //#define FILE_ATTRIBUTE_DEVICE 0x00000040 + Device = 0x00000040, + //#define FILE_ATTRIBUTE_NORMAL 0x00000080 + Normal = 0x00000080, + //#define FILE_ATTRIBUTE_TEMPORARY 0x00000100 + Temporary = 0x00000100, + //#define FILE_ATTRIBUTE_SPARSE_FILE 0x00000200 + SparseFile = 0x00000200, + //#define FILE_ATTRIBUTE_REPARSE_POINT 0x00000400 + ReparsePoint = 0x00000400, + //#define FILE_ATTRIBUTE_COMPRESSED 0x00000800 + Compressed = 0x00000800, + //#define FILE_ATTRIBUTE_OFFLINE 0x00001000 + Offline = 0x00001000, + //#define FILE_ATTRIBUTE_NOT_CONTENT_INDEXED 0x00002000 + NotContentIndexed = 0x00002000, + //#define FILE_ATTRIBUTE_ENCRYPTED 0x00004000 + Encrypted = 0x00004000, + //#define FILE_ATTRIBUTE_INTEGRITY_STREAM 0x00008000 + IntegrityStream = 0x00008000, + //#define FILE_ATTRIBUTE_VIRTUAL 0x00010000 + Virtual = 0x00010000, + //#define FILE_ATTRIBUTE_NO_SCRUB_DATA 0x00020000 + NoScrubData = 0x00020000, + /* EA is not documented + #define FILE_ATTRIBUTE_EA 0x00040000 + EA = 0x00040000 + */ + } +} diff --git a/BurnOutSharp/External/LessIO/FileSystem.cs b/BurnOutSharp/External/LessIO/FileSystem.cs new file mode 100644 index 00000000..86089c60 --- /dev/null +++ b/BurnOutSharp/External/LessIO/FileSystem.cs @@ -0,0 +1,160 @@ +using System; +using LessIO.Strategies; +using LessIO.Strategies.Win32; +using BadPath = System.IO.Path; +using System.Collections.Generic; + +namespace LessIO +{ + /// + /// Provides various file system operations for the current platform. + /// + public static class FileSystem + { + private static readonly Lazy LazyStrategy = new Lazy(() => new Win32FileSystemStrategy()); + + private static FileSystemStrategy Strategy + { + get { return LazyStrategy.Value; } + } + + /// + /// Sets the date and time that the specified file was last written to. + /// + /// The path of the file to set the file time on. + /// + /// The date to set for the last write date and time of specified file. + /// Expressed in local time. + /// + public static void SetLastWriteTime(Path path, DateTime lastWriteTime) + { + Strategy.SetLastWriteTime(path, lastWriteTime); + } + + public static void SetAttributes(Path path, LessIO.FileAttributes fileAttributes) + { + Strategy.SetAttributes(path, fileAttributes); + } + + public static LessIO.FileAttributes GetAttributes(Path path) + { + return Strategy.GetAttributes(path); + } + + /// + /// Returns true if a file or directory exists at the specified path. + /// + public static bool Exists(Path path) + { + return Strategy.Exists(path); + } + + /// + /// Creates the specified directory. + /// + /// + /// Creates parent directories as needed. + /// + public static void CreateDirectory(Path path) + { + Strategy.CreateDirectory(path); + } + + /// + /// Removes/deletes an existing empty directory. + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365488%28v=vs.85%29.aspx + /// + /// + public static void RemoveDirectory(Path path) + { + RemoveDirectory(path, false); + } + + /// + /// Removes/deletes an existing directory. + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365488%28v=vs.85%29.aspx + /// + /// The path to the directory to remove. + /// + /// True to remove the directory and all of its contents recursively. + /// False will remove the directory only if it is empty. + /// + /// Recursively implies removing contained files forcefully. + public static void RemoveDirectory(Path path, bool recursively) + { + Strategy.RemoveDirectory(path, recursively); + } + + /// + /// Removes/deletes an existing file. + /// To remove a directory see . + /// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363915%28v=vs.85%29.aspx + /// + /// The file to remove. + /// True to remove the file even if it is read-only. + public static void RemoveFile(Path path, bool forcefully) + { + Strategy.RemoveFile(path, forcefully); + } + + /// + /// Removes/deletes an existing file. + /// To remove a directory see . + /// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363915%28v=vs.85%29.aspx + /// + /// The file to remove. + public static void RemoveFile(Path path) + { + Strategy.RemoveFile(path, false); + } + + /// + /// Copies the specified existing file to a new location. + /// Will throw an exception if the destination file already exists. + /// + public static void Copy(Path source, Path dest) + { + if (!Strategy.Exists(source)) + throw new Exception(string.Format("The file \"{0}\" does not exist.", source)); + Strategy.Copy(source, dest); + } + + /// + /// Returns true if the specified path is a directory. + /// + internal static bool IsDirectory(Path path) + { + return Strategy.IsDirectory(path); + } + + /// + /// Creates or overwrites the file at the specified path. + /// + /// The path and name of the file to create. Supports long file paths. + /// A that provides read/write access to the file specified in path. + public static System.IO.Stream CreateFile(Path path) + { + return Strategy.CreateFile(path); + } + + /// + /// Returns the contents of the specified directory. + /// + /// The path to the directory to get the contents of. + public static IEnumerable ListContents(Path directory) + { + return Strategy.ListContents(directory); + } + + /// + /// Returns the contents of the specified directory. + /// + /// The path to the directory to get the contents of. + /// True to list the contents of any child directories. + /// If the specified directory is not actually a directory then an empty set is returned. + public static IEnumerable ListContents(Path directory, bool recursive) + { + return Strategy.ListContents(directory, recursive); + } + } +} diff --git a/BurnOutSharp/External/LessIO/Path.cs b/BurnOutSharp/External/LessIO/Path.cs new file mode 100644 index 00000000..e7ac1f1a --- /dev/null +++ b/BurnOutSharp/External/LessIO/Path.cs @@ -0,0 +1,399 @@ +using System; +using System.Linq; +using BadPath = System.IO.Path; + +namespace LessIO +{ + /// + /// Represents a file system path. + /// + public struct Path : IEquatable + { + private readonly string _path; + private static readonly string _pathEmpty = string.Empty; + public static readonly Path Empty = new Path(); + + /// + /// This is the special prefix to prepend to paths to support up to 32,767 character paths. + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath + /// + private static readonly string Win32LongPathPrefix = @"\\?\"; + /// + /// This is the special prefix to prepend to paths to support long paths for UNC paths. + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath + /// + private static readonly string Win32LongPathPrefixUNC = @"\\?\UNC\"; + + private static readonly string UNCPrefix = @"\\"; + + // TODO: Add validation using a strategy? Or just use it as a strongly typed path to force caller to be explicit? + + public Path(string path) + { + //TODO: Consider doing a FileSystem.Normalize and FileSystem.Validate to allow the strategy to Normalize & Validate path + //To maintain sanity NEVER let the Path object store the long path prefixes. That is a hack for Win32 that should only ever be used just before calling the Win32 API and stripped out of any paths coming out of the Win32 API. + path = StripWin32PathPrefix(path); + path = StripDirectorySeperatorPostfix(path); + path = RemoveDoubleSeperators(path); + _path = path; + } + + private static string RemoveDoubleSeperators(string path) + { + /* + "\\" is legit for UNC paths and as a prefix. + So don't remove "\\" if it is in the root. + */ + string root = GetPathRoot(path); + string remainder = path.Length > root.Length ? path.Substring(root.Length) : ""; + + Array.ForEach(DirectorySeperatorChars, sep => remainder = remainder.Replace(new string(new char[] { sep, sep }), new string(new char[] { sep }))); + return root + remainder; + } + + private static string StripDirectorySeperatorPostfix(string path) + { + /* Here we want to trim any trailing directory seperator charactars EXCEPT + in one case: When the path is a fully qualified root dir such as "x:\". See GetPathRoot and System.IO.Path.GetPathRoot + */ + + // "X:/"(path specified an absolute path on a given drive). + if (path.Length == 3 && path[1] == ':' && IsDirectorySeparator(path[2])) + return path; + else + return path.TrimEnd(DirectorySeperatorChars); + } + + private static string StripWin32PathPrefix(string pathString) + { + if (pathString.StartsWith(Win32LongPathPrefixUNC)) + return UNCPrefix + pathString.Substring(Win32LongPathPrefixUNC.Length); + + if (pathString.StartsWith(Win32LongPathPrefix)) + return pathString.Substring(Win32LongPathPrefix.Length); + + return pathString; + } + + /// + /// Returns the directory seperator characers. + /// + internal static char[] DirectorySeperatorChars + { + get + { + return new char[] { BadPath.DirectorySeparatorChar, BadPath.AltDirectorySeparatorChar }; + } + } + + internal static bool IsDirectorySeparator(char ch) + { + return Array.Exists(DirectorySeperatorChars, c => c == ch); + } + + /// + /// Gets the normalized path string. May be rooted or may be relative. + /// For a rooted/qualified path use + /// + public string PathString + { + get + { + return _path != null ? _path : _pathEmpty; + } + } + + /// + /// Returns the absolute path for the current path. + /// Compatible with . + /// + public Path FullPath + { + get + { + return new Path(this.FullPathString); + } + } + + /// + /// Returns the absolute path for the current path. + /// Compatible with . + /// + public string FullPathString + { + get + { + var pathString = this.PathString; + var pathRoot = this.PathRoot; + if (pathRoot == "") + { // relative + return Combine(WorkingDirectory, pathString).PathString; + } + else if (pathRoot == @"\" || pathRoot == @"/") + { // use the working directory's drive/root only. + pathString = pathString.TrimStart(DirectorySeperatorChars);//otherwise Combine will ignore the root + string workingRoot = new Path(WorkingDirectory).PathRoot; + return Combine(workingRoot, pathString).PathString; + } + else + { + return pathString; + } + } + } + + private static string WorkingDirectory + { + get { + //TODO: There is a Win32 native equivelent for this: + return System.IO.Directory.GetCurrentDirectory(); + } + } + + public bool IsEmpty + { + get { return Equals(Path.Empty); } + } + + /// + /// Indicates if the two paths are equivelent and point to the same file or directory. + /// + private static bool PathEquals(string pathA, string pathB) + { + /* Now we never let the Win32 long path prefix get into a Path instance: + pathA = StripWin32PathPrefix(pathA); + pathB = StripWin32PathPrefix(pathB); + */ + pathA = pathA.TrimEnd(DirectorySeperatorChars); + pathB = pathB.TrimEnd(DirectorySeperatorChars); + var partsA = pathA.Split(DirectorySeperatorChars); + var partsB = pathB.Split(DirectorySeperatorChars); + if (partsA.Length != partsB.Length) + return false; + + for (var i = 0; i < partsA.Length; i++) + { + var areEqual = string.Equals(partsA[i], partsB[i], StringComparison.InvariantCultureIgnoreCase); + if (!areEqual) + return false; + } + return true; + } + + public static bool operator ==(Path a, Path b) + { + return Path.Equals(a, b); + } + + public static bool operator !=(Path a, Path b) + { + return !Path.Equals(a, b); + } + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) + return false; + return Equals((Path)obj); + } + + public bool Equals(Path other) + { + return Path.PathEquals(this.PathString, other.PathString); + } + + internal static bool Equals(Path a, Path b) + { + return PathEquals(a.PathString, b.PathString); + } + + /// + /// Long-form filenames are not supported by the .NET system libraries, so we do win32 calls. + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx#maxpath + /// + /// + /// The object will never store the Win32 long path prefix. Instead use this method to add it back when necessary (i.e. when making direct calls into Win32 APIs). + /// + public string WithWin32LongPathPrefix() + { + if (!PathString.StartsWith(Win32LongPathPrefix)) // More consistent to deal with if we just add it to all of them: if (!path.StartsWith(LongPathPrefix) && path.Length >= MAX_PATH) + { + if (PathString.StartsWith(UNCPrefix)) + return Win32LongPathPrefixUNC + this.PathString.Substring(UNCPrefix.Length); + else + return Win32LongPathPrefix + this.PathString; + } + else + { + //NOTE that Win32LongPathPrefixUNC is a superset of Win32LongPathPrefix we just assume the right pathprefix is already there. + return this.PathString; + } + } + + public override int GetHashCode() + { + return PathString.GetHashCode(); + } + + public override string ToString() + { + return PathString.ToString(); + } + + /// + /// Gets the root directory information of the specified path. + /// + public string PathRoot + { + get + { + return GetPathRoot(this.PathString); + } + } + + /// + /// Modeled after but supports long path names. + /// + /// + /// + /// + /// See https://msdn.microsoft.com/en-us/library/system.io.path.getpathroot%28v=vs.110%29.aspx + /// Possible patterns for the string returned by this method are as follows: + /// An empty string (path specified a relative path on the current drive or volume). + /// "/"(path specified an absolute path on the current drive). + /// "X:"(path specified a relative path on a drive, where X represents a drive or volume letter). + /// "X:/"(path specified an absolute path on a given drive). + /// "\\ComputerName\SharedFolder"(a UNC path). + /// + internal static string GetPathRoot(string path) + { + // "X:/"(path specified an absolute path on a given drive). + if (path.Length >= 3 && path[1] == ':' && IsDirectorySeparator(path[2])) + return path.Substring(0, 3); + // "X:"(path specified a relative path on a drive, where X represents a drive or volume letter). + if (path.Length >= 2 && path[1] == ':') + { + return path.Substring(0, 2); + } + // "\\ComputerName\SharedFolder"(a UNC path). + // NOTE: UNC Path "root" includes the server/host AND have the root share folder too. + if (path.Length > 2 + && IsDirectorySeparator(path[0]) + && IsDirectorySeparator(path[1]) + && path.IndexOfAny(DirectorySeperatorChars, 2) > 2) + { + var beginShareName = path.IndexOfAny(DirectorySeperatorChars, 2); + var endShareName = path.IndexOfAny(DirectorySeperatorChars, beginShareName + 1); + if (endShareName < 0) + endShareName = path.Length; + if (beginShareName > 2 && endShareName > beginShareName) + return path.Substring(0, endShareName); + } + // "/"(path specified an absolute path on the current drive). + if (path.Length >= 1 && IsDirectorySeparator(path[0])) + { + return path.Substring(0, 1); + } + // path specified a relative path on the current drive or volume? + return ""; + } + + public static Path Combine(Path path1, params string[] pathParts) + { + if (path1.IsEmpty) + throw new ArgumentNullException("path1"); + if (pathParts == null || pathParts.Length == 0) + throw new ArgumentNullException("pathParts"); + + string[] allStrings = new string[pathParts.Length + 1]; + allStrings[0] = path1.PathString; + Array.Copy(pathParts, 0, allStrings, 1, pathParts.Length); + return Combine(allStrings); + } + + public static Path Combine(params Path[] pathParts) + { + if (pathParts == null) + throw new ArgumentNullException(); + var strs = pathParts.Select(p => p.PathString); + return Combine(strs.ToArray()); + } + + public static Path Combine(params string[] pathParts) + { + if (pathParts == null) + throw new ArgumentNullException(); + if (pathParts.Length < 2) + throw new ArgumentException("Expected at least two parts to combine."); + var output = BadPath.Combine(pathParts[0], pathParts[1]); + for (var i = 2; i < pathParts.Length; i++) + { + output = BadPath.Combine(output, pathParts[i]); + } + return new Path(output); + } + + public Path Parent + { + get + { + var path = this.PathString; + path = path.TrimEnd(Path.DirectorySeperatorChars); + var parentEnd = path.LastIndexOfAny(Path.DirectorySeperatorChars); + if (parentEnd >= 0 && parentEnd > GetPathRoot(path).Length) + { + var result = path.Substring(0, parentEnd); + return new Path(result); + } + else + return Path.Empty; + } + } + + /// + /// Indicates if the file or directory at the specified path exists. + /// For code compatibility with . + /// + public bool Exists + { + get { return FileSystem.Exists(this); } + } + + /// + /// True if the path is a rooted/fully qualified path. Otherwise returns false if it is a relative path. + /// Compatible with . + /// + public bool IsPathRooted + { + get + { + /* The IsPathRooted method returns true if the first character is a directory separator character such as "\", or if the path starts with a drive letter and colon (:). + * For example, it returns true for path strings such as "\\MyDir\\MyFile.txt", "C:\\MyDir", or "C: MyDir". It returns false for path strings such as "MyDir". + * - https://msdn.microsoft.com/en-us/library/system.io.path.ispathrooted%28v=vs.110%29.aspx + */ + var pathString = this.PathString; + bool rooted = + DirectorySeperatorChars.Any(c => c == pathString[0]) + || pathString.Length >= 2 && pathString[1] == ':'; + return rooted; + } + } + + /// + /// For code compatibility with + /// + public System.IO.StreamWriter CreateText() + { + var stream = FileSystem.CreateFile(this); + return new System.IO.StreamWriter(stream, System.Text.Encoding.UTF8); + } + + /// + /// For code compatibility with + /// + public static string GetFileName(string path) + { + return BadPath.GetFileName(path); + } + } +} diff --git a/BurnOutSharp/External/LessIO/Strategies/FileSystemStrategy.cs b/BurnOutSharp/External/LessIO/Strategies/FileSystemStrategy.cs new file mode 100644 index 00000000..2f36677e --- /dev/null +++ b/BurnOutSharp/External/LessIO/Strategies/FileSystemStrategy.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace LessIO.Strategies +{ + /// + /// See for documentation of each method of this class. + /// + internal abstract class FileSystemStrategy + { + public abstract void SetLastWriteTime(Path path, DateTime lastWriteTime); + public abstract void SetAttributes(Path path, FileAttributes fileAttributes); + public abstract FileAttributes GetAttributes(Path path); + public abstract bool Exists(Path path); + public abstract void CreateDirectory(Path path); + public abstract void Copy(Path source, Path dest); + public abstract void RemoveDirectory(Path path, bool recursively); + public abstract void RemoveFile(Path path, bool force); + public abstract System.IO.Stream CreateFile(Path path); + public abstract IEnumerable ListContents(Path directory); + + public virtual IEnumerable ListContents(Path directory, bool recursive) + { + IEnumerable children = ListContents(directory); + if (recursive) + { + IEnumerable grandChildren = children.SelectMany( + p => ListContents(p, recursive) + ); + return Enumerable.Concat(children, grandChildren); + } + else + { + return children; + } + } + + public virtual bool IsDirectory(Path path) + { + FileAttributes attributes = GetAttributes(path); + return (attributes & FileAttributes.Directory) == FileAttributes.Directory; + } + + public virtual bool IsReadOnly(Path path) + { + FileAttributes attributes = GetAttributes(path); + return (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; + } + + public virtual void SetReadOnly(Path path, bool readOnly) + { + FileAttributes attributes = GetAttributes(path); + if (readOnly) + attributes = attributes | FileAttributes.ReadOnly; + else + attributes = attributes & ~FileAttributes.ReadOnly; + SetAttributes(path, attributes); + } + } +} diff --git a/BurnOutSharp/External/LessIO/Strategies/Win32/NativeMethods.cs b/BurnOutSharp/External/LessIO/Strategies/Win32/NativeMethods.cs new file mode 100644 index 00000000..933c640c --- /dev/null +++ b/BurnOutSharp/External/LessIO/Strategies/Win32/NativeMethods.cs @@ -0,0 +1,211 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.InteropServices; +using FileAttributes = System.IO.FileAttributes; + +namespace LessIO.Strategies.Win32 +{ + /// + /// Native/Win32 methods for accessing file system. Primarily to get around . + /// Good references: + /// * https://blogs.msdn.microsoft.com/bclteam/2007/03/26/long-paths-in-net-part-2-of-3-long-path-workarounds-kim-hamilton/ + /// + internal class NativeMethods + { + /// + /// Specified in Windows Headers for default maximum path. To go beyond this length you must prepend to the path. + /// + internal const int MAX_PATH = 260; + /// + /// This is the special prefix to prepend to paths to support up to 32,767 character paths. + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath + /// + internal static readonly string LongPathPrefix = @"\\?\"; + + /// + /// This is the special prefix to prepend to UNC paths to support up to 32,767 character paths. + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath + /// + internal static readonly string LongPathPrefixUNC = @"\\?\UNC\"; + + internal static IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); + + [StructLayout(LayoutKind.Sequential)] + internal class SECURITY_ATTRIBUTES + { + //internal unsafe byte* pSecurityDescriptor = (byte*)null; + internal IntPtr pSecurityDescriptor; + internal int nLength; + internal int bInheritHandle; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct FILETIME + { + internal uint dwLowDateTime; + internal uint dwHighDateTime; + }; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct WIN32_FIND_DATA + { + internal EFileAttributes dwFileAttributes; + internal FILETIME ftCreationTime; + internal FILETIME ftLastAccessTime; + internal FILETIME ftLastWriteTime; + internal int nFileSizeHigh; + internal int nFileSizeLow; + internal int dwReserved0; + internal int dwReserved1; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)] + internal string cFileName; + // not using this + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] + internal string cAlternate; + } + + [Flags] + internal enum EFileAccess : uint + { + FILE_READ_ATTRIBUTES = 0x00000080, + FILE_WRITE_ATTRIBUTES = 0x00000100, + GenericRead = 0x80000000, + GenericWrite = 0x40000000, + GenericExecute = 0x20000000, + GenericAll = 0x10000000 + } + + [Flags] + internal enum EFileShare : uint + { + None = 0x00000000, + Read = 0x00000001, + Write = 0x00000002, + Delete = 0x00000004 + } + + internal enum ECreationDisposition : uint + { + New = 1, + CreateAlways = 2, + OpenExisting = 3, + OpenAlways = 4, + TruncateExisting = 5 + } + + [Flags] + internal enum EFileAttributes : uint + { + None = 0x0, + Readonly = 0x00000001, + Hidden = 0x00000002, + System = 0x00000004, + Directory = 0x00000010, + Archive = 0x00000020, + Device = 0x00000040, + Normal = 0x00000080, + Temporary = 0x00000100, + SparseFile = 0x00000200, + ReparsePoint = 0x00000400, + Compressed = 0x00000800, + Offline = 0x00001000, + NotContentIndexed = 0x00002000, + Encrypted = 0x00004000, + Write_Through = 0x80000000, + Overlapped = 0x40000000, + NoBuffering = 0x20000000, + RandomAccess = 0x10000000, + SequentialScan = 0x08000000, + DeleteOnClose = 0x04000000, + BackupSemantics = 0x02000000, + PosixSemantics = 0x01000000, + OpenReparsePoint = 0x00200000, + OpenNoRecall = 0x00100000, + FirstPipeInstance = 0x00080000 + } + + /// + /// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363855%28v=vs.85%29.aspx + /// + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false)] + internal static extern bool CreateDirectory(string path, SECURITY_ATTRIBUTES lpSecurityAttributes); + + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr FindFirstFile(string lpFileName, out + WIN32_FIND_DATA lpFindFileData); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + internal static extern bool FindNextFile(IntPtr hFindFile, out + WIN32_FIND_DATA lpFindFileData); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool FindClose(IntPtr hFindFile); + + + + + /// + /// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365535%28v=vs.85%29.aspx + /// + [DllImport("kernel32.dll", EntryPoint = "SetFileAttributes", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false)] + internal static extern bool SetFileAttributes(string lpFileName, uint dwFileAttributes); + + /// + /// https://msdn.microsoft.com/en-us/library/windows/desktop/aa364944%28v=vs.85%29.aspx + /// + /// + /// + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern uint GetFileAttributes(string lpFileName); + + // Invalid is from C:\Program Files (x86)\Windows Kits\8.1\Include\um\fileapi.h + //#define INVALID_FILE_ATTRIBUTES ((DWORD)-1) + internal static readonly uint INVALID_FILE_ATTRIBUTES = 0xffffffff; + + + [DllImport("kernel32.dll", EntryPoint = "RemoveDirectory", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false)] + internal static extern bool RemoveDirectory(string lpPathName); + + [DllImport("kernel32.dll", EntryPoint = "DeleteFile", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false)] + internal static extern bool DeleteFile(string path); + + /// + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858%28v=vs.85%29.aspx + /// + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern SafeFileHandle CreateFile(string lpFileName, + EFileAccess dwDesiredAccess, + EFileShare dwShareMode, + IntPtr lpSecurityAttributes, + ECreationDisposition dwCreationDisposition, + EFileAttributes dwFlagsAndAttributes, + IntPtr hTemplateFile); + + // This binding only allows setting creation and last write times. + // The last access time parameter must be zero; that time is not + // modified. + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool SetFileTime( + IntPtr hFile, + ref long lpCreationTime, + IntPtr lpLastAccessTime, + ref long lpLastWriteTime); + + /// + /// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363851%28v=vs.85%29.aspx + /// + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + internal static extern bool CopyFile(string lpExistingFileName, string lpNewFileName, bool bFailIfExists); + + + internal const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; + + /// + /// https://msdn.microsoft.com/en-us/library/windows/desktop/ms679351%28v=vs.85%29.aspx + /// + [DllImport("kernel32.dll")] + internal static extern uint FormatMessage(uint dwFlags, IntPtr lpSource, uint dwMessageId, uint dwLanguageId, [Out] System.Text.StringBuilder lpBuffer, uint nSize, IntPtr Arguments); + } +} diff --git a/BurnOutSharp/External/LessIO/Strategies/Win32/Win32FileSystemStrategy.cs b/BurnOutSharp/External/LessIO/Strategies/Win32/Win32FileSystemStrategy.cs new file mode 100644 index 00000000..5c8c8eb9 --- /dev/null +++ b/BurnOutSharp/External/LessIO/Strategies/Win32/Win32FileSystemStrategy.cs @@ -0,0 +1,230 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using BadPath = System.IO.Path; + +namespace LessIO.Strategies.Win32 +{ + internal sealed class Win32FileSystemStrategy : FileSystemStrategy + { + public Win32FileSystemStrategy() + { + } + + private static Exception CreateWin32LastErrorException(string userMessage, params object[] args) + { + uint lastError = (uint)Marshal.GetLastWin32Error(); + + const int bufferByteCapacity = 1024 * 32; + StringBuilder buffer = new StringBuilder(bufferByteCapacity);// NOTE: capacity here will be interpreted by StringBuilder as chars not bytes, but that's ok since it is definitely bigger in byte terms. + uint bufferCharLength = NativeMethods.FormatMessage(NativeMethods.FORMAT_MESSAGE_FROM_SYSTEM, IntPtr.Zero, lastError, 0, buffer, bufferByteCapacity, IntPtr.Zero); + + string systemErrorMessage; + if (bufferCharLength == 0) + { //FormatMessage failed: + systemErrorMessage = string.Format("Error code=0x{1:x8}", lastError); + } + else + { + Debug.Assert(bufferCharLength < buffer.Capacity, "unexpected result capacity"); + char[] systemChars = new char[bufferCharLength]; + buffer.CopyTo(0, systemChars, 0, (int)bufferCharLength); + systemErrorMessage = new string(systemChars); + } + + var formattedUserMessage = string.Format(userMessage, args); + return new Exception(formattedUserMessage + " System error information:'" + systemErrorMessage + "'"); + } + + public override void SetLastWriteTime(Path path, DateTime lastWriteTime) + { + SafeFileHandle h = NativeMethods.CreateFile( + path.WithWin32LongPathPrefix(), + NativeMethods.EFileAccess.FILE_READ_ATTRIBUTES | NativeMethods.EFileAccess.FILE_WRITE_ATTRIBUTES, + NativeMethods.EFileShare.Read | NativeMethods.EFileShare.Write| NativeMethods.EFileShare.Delete, + IntPtr.Zero,// See https://github.com/activescott/LessIO/issues/4 before changing these flags. + NativeMethods.ECreationDisposition.OpenExisting, + NativeMethods.EFileAttributes.None, + IntPtr.Zero); + using (h) + { + if (h.IsInvalid) + throw CreateWin32LastErrorException("Error opening file {0}.", path); + var modifiedTime = lastWriteTime.ToFileTime(); + if (!NativeMethods.SetFileTime( + h.DangerousGetHandle(), + ref modifiedTime, IntPtr.Zero, ref modifiedTime)) + throw CreateWin32LastErrorException("Error setting times for {0}.", path); + } + } + + public override void SetAttributes(Path path, FileAttributes fileAttributes) + { + var result = NativeMethods.SetFileAttributes(path.WithWin32LongPathPrefix(), (uint)fileAttributes); + if (!result) + { + throw CreateWin32LastErrorException("Error setting file attributes on '{0}'.", path); + } + } + + public override FileAttributes GetAttributes(Path path) + { + var result = NativeMethods.GetFileAttributes(path.WithWin32LongPathPrefix()); + if (result == NativeMethods.INVALID_FILE_ATTRIBUTES) + { + throw CreateWin32LastErrorException("Error getting file attributes for '{0}'.", path); + } + return (FileAttributes)result; + } + + public override void CreateDirectory(Path path) + { + // Since System.IO.Directory.Create() creates all neecessary directories, we emulate here: + string pathString = path.PathString; + var dirsToCreate = new List(); + int lengthRoot = path.PathRoot.Length; + + var firstNonRootPathIndex = pathString.IndexOfAny(Path.DirectorySeperatorChars, lengthRoot); + if (firstNonRootPathIndex == -1) + { + // this is a directory directly off of the root (because no, non-root directory seperators exist) + firstNonRootPathIndex = pathString.Length - 1; // set it to the whole path so that the loop below will create the root dir + } + var i = firstNonRootPathIndex; + while (i < pathString.Length) + { + if (Path.IsDirectorySeparator(pathString[i]) || i == pathString.Length - 1) + { + Path currentPath = new Path(pathString.Substring(0, i + 1)); + if (!Exists(currentPath)) + { + bool succeeded = NativeMethods.CreateDirectory(currentPath.WithWin32LongPathPrefix(), null); + if (!succeeded) + throw CreateWin32LastErrorException("Error creating directory '{0}'.", currentPath); + } + Debug.Assert(Exists(currentPath), "path should always exists at this point!"); + } + i++; + } + } + + public override bool Exists(Path path) + { + var result = NativeMethods.GetFileAttributes(path.WithWin32LongPathPrefix()); + return (result != NativeMethods.INVALID_FILE_ATTRIBUTES); + } + + public override void RemoveDirectory(Path path, bool recursively) + { + if (recursively) + { // first gather contents and remove all content + RemoveDirectoryRecursively(path); + } + else + { + var succeeded = NativeMethods.RemoveDirectory(path.WithWin32LongPathPrefix()); + if (!succeeded) + throw CreateWin32LastErrorException("Error removing directory '{0}'.", path); + } + } + + private void RemoveDirectoryRecursively(Path dirName) + { + var list = ListContents(dirName, true).ToArray(); + /* + We need to delete leaf nodes in the file hierarchy first to make sure all directories are empty. + We identify leaf nodes by their depth (greatest number of path parts) + */ + Func pathPartCount = (Path p) => p.PathString.Split(Path.DirectorySeperatorChars).Length; + Array.Sort(list, (a,b) => pathPartCount(b) - pathPartCount(a)); + Array.ForEach(list + , p => { + if (FileSystem.IsDirectory(p)) + FileSystem.RemoveDirectory(p); + else + FileSystem.RemoveFile(p, true); + Debug.Assert(FileSystem.Exists(p) == false, string.Format("Files/directory still exits:'{0}'", p)); + } + ); + Debug.Assert(list.All(p => FileSystem.Exists(p) == false), "Some files/directories still exits?"); + FileSystem.RemoveDirectory(dirName, false); + } + + public override void RemoveFile(Path path, bool force) + { + if (IsReadOnly(path) && force) + SetReadOnly(path, false); + + var succeeded = NativeMethods.DeleteFile(path.WithWin32LongPathPrefix()); + if (!succeeded) + throw CreateWin32LastErrorException("Error deleting file '{0}'.", path); + } + + public override void Copy(Path source, Path dest) + { + var succeeded = NativeMethods.CopyFile(source.WithWin32LongPathPrefix(), dest.WithWin32LongPathPrefix(), true); + if (!succeeded) + throw CreateWin32LastErrorException("Error copying file '{0}' to '{1}'.", source, dest); + } + + /// + /// Creates or overwrites the file at the specified path. + /// + /// The path and name of the file to create. Supports long file paths. + /// A that provides read/write access to the file specified in path. + public override System.IO.Stream CreateFile(Path path) + { + if (path.IsEmpty) + throw new ArgumentNullException("path"); + + NativeMethods.EFileAccess fileAccess = NativeMethods.EFileAccess.GenericWrite | NativeMethods.EFileAccess.GenericRead; + NativeMethods.EFileShare fileShareMode = NativeMethods.EFileShare.None;//exclusive + NativeMethods.ECreationDisposition creationDisposition = NativeMethods.ECreationDisposition.CreateAlways; + SafeFileHandle hFile = NativeMethods.CreateFile(path.WithWin32LongPathPrefix(), fileAccess, fileShareMode, IntPtr.Zero, creationDisposition, NativeMethods.EFileAttributes.Normal, IntPtr.Zero); + if (hFile.IsInvalid) + throw CreateWin32LastErrorException("Error creating file at path '{0}'.", path); + return new System.IO.FileStream(hFile, System.IO.FileAccess.ReadWrite); + } + + public override IEnumerable ListContents(Path directory) + { + //NOTE: An important part of our contract is that if directory is not a directory, we return an empty set: + if (!Exists(directory) || !IsDirectory(directory)) + yield break; + + // normalize dirName so we can assume it doesn't have a slash on the end: + string dirName = directory.PathString; + dirName = dirName.TrimEnd(Path.DirectorySeperatorChars); + dirName = new Path(dirName).WithWin32LongPathPrefix(); + + NativeMethods.WIN32_FIND_DATA findData; + IntPtr findHandle = NativeMethods.FindFirstFile(dirName + @"\*", out findData); + if (findHandle != NativeMethods.INVALID_HANDLE_VALUE) + { + try + { + bool found; + do + { + string currentFileName = findData.cFileName; + if (currentFileName != "." && currentFileName != "..") + { + //NOTE: Instantiating the Path here will strip off the Win32 prefix that is added here as needed: + yield return Path.Combine(dirName, currentFileName); + } + // find next + found = NativeMethods.FindNextFile(findHandle, out findData); + } while (found); + } + finally + { + NativeMethods.FindClose(findHandle); + } + } + } + } +} diff --git a/BurnOutSharp/External/libmspack4n/MSCabDecompressor.cs b/BurnOutSharp/External/libmspack4n/MSCabDecompressor.cs new file mode 100644 index 00000000..48e50e11 --- /dev/null +++ b/BurnOutSharp/External/libmspack4n/MSCabDecompressor.cs @@ -0,0 +1,92 @@ +using System; + +namespace LibMSPackN +{ + /// + /// Used internally to manage the decompressor. Technically you need one of these per thread (see code comments in libmspack, but I'm assuming single threaded or safe multithreaded access here). + /// + internal sealed class MSCabDecompressor : IDisposable + { + private IntPtr _pDecompressor; + private static volatile MSCabDecompressor _default; + private static readonly object SyncRoot = new object(); + + private MSCabDecompressor() + { + _pDecompressor = CreateInstance(); + } + + ~MSCabDecompressor() + { + Dispose(); + } + + public void Dispose() + { + if (_pDecompressor != IntPtr.Zero) + { + DestroyInstance(_pDecompressor); + _pDecompressor = IntPtr.Zero; + } + } + + /// + /// Creates a native instance of the decompressor and returns the pointer. Must be destroyed via . + /// + internal static IntPtr CreateInstance() + { + var ptr = NativeMethods.mspack_create_cab_decompressor(IntPtr.Zero); + if (ptr == IntPtr.Zero) + throw new Exception("Failed to create cab_decompressor."); + return ptr; + } + + /// + /// Destroys the specified instance that was created with . + /// + /// + internal static void DestroyInstance(IntPtr pDecompressor) + { + NativeMethods.mspack_destroy_cab_decompressor(pDecompressor); + } + + internal IntPtr Pointer + { + get + { + ThrowOnInvalidState(); + return _pDecompressor; + } + } + + /// + /// Returns the defalut instance of a decompressor. DO NOT DISPOSE IT! + /// + public static MSCabDecompressor Default + { + get + { + if (_default == null) + { + lock (SyncRoot) + { + if (_default == null) + _default = new MSCabDecompressor(); + } + } + return _default; + } + } + + public bool IsInvalidState + { + get { return _pDecompressor == IntPtr.Zero; } + } + + private void ThrowOnInvalidState() + { + if (IsInvalidState) + throw new ObjectDisposedException(typeof(MSCabDecompressor).Name); + } + } +} diff --git a/BurnOutSharp/External/libmspack4n/MSCabinet.cs b/BurnOutSharp/External/libmspack4n/MSCabinet.cs new file mode 100644 index 00000000..7b44dde1 --- /dev/null +++ b/BurnOutSharp/External/libmspack4n/MSCabinet.cs @@ -0,0 +1,184 @@ +using System; +using LessIO; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace LibMSPackN +{ + /// + /// Represents a .cab file. + /// + /// + /// Example usage: + /// + /// const string cabinetFilename = @"c:\somecab.cab"; + /// using (var cabinet = new MSCabinet(cabinetFilename)) + /// { + /// var outputDir = Path.Combine(Assembly.GetExecutingAssembly().Location, Path.GetFileNameWithoutExtension(cabinetFilename)); + /// foreach (MSCabFile containedFile in cabinet.GetFiles()) + /// { + /// Debug.Print(containedFile.Filename); + /// Debug.Print(containedFile.LengthInBytes.ToString(CultureInfo.InvariantCulture)); + /// containedFile.ExtractTo(Path.Combine(outputDir, containedFile.Filename)); + /// } + /// } + /// + /// + public sealed class MSCabinet : IDisposable + { + private NativeMethods.mscabd_cabinet _nativeCabinet = new NativeMethods.mscabd_cabinet(); + private IntPtr _pNativeCabinet; + private readonly string _cabinetFilename; + private IntPtr _pCabinetFilenamePinned; + private IntPtr _pDecompressor; + + public MSCabinet(string cabinetFilename) + { + cabinetFilename = new Path(cabinetFilename).WithWin32LongPathPrefix(); + _cabinetFilename = cabinetFilename; + _pCabinetFilenamePinned = Marshal.StringToCoTaskMemAnsi(_cabinetFilename);// needs to be pinned as we use the address in unmanaged code. + _pDecompressor = MSCabDecompressor.CreateInstance(); + + // open cabinet: + _pNativeCabinet = NativeMethods.mspack_invoke_mscab_decompressor_open(_pDecompressor, _pCabinetFilenamePinned); + if (_pNativeCabinet == IntPtr.Zero) + { + var lasterror = NativeMethods.mspack_invoke_mscab_decompressor_last_error(_pDecompressor); + throw new Exception("Failed to open cabinet. Last error:" + lasterror); + } + //Marshal.PtrToStructure(_pNativeCabinet, _nativeCabinet); + _nativeCabinet = (NativeMethods.mscabd_cabinet) Marshal.PtrToStructure(_pNativeCabinet, typeof (NativeMethods.mscabd_cabinet)); + } + + public string LocalFilePath + { + get { return _cabinetFilename; } + } + + ~MSCabinet() + { + Close(false); + } + + public void Close(bool isDisposing) + { + Debug.Print("Disposing MSCabinet for {0}. isDisposing:{1}", _cabinetFilename, isDisposing); + if (_pNativeCabinet != IntPtr.Zero) + { + NativeMethods.mspack_invoke_mscab_decompressor_close(_pDecompressor, _pNativeCabinet); + _pNativeCabinet = IntPtr.Zero; + } + + if (_pDecompressor != IntPtr.Zero) + { + MSCabDecompressor.DestroyInstance(_pDecompressor); + _pDecompressor = IntPtr.Zero; + } + if (_pCabinetFilenamePinned!= IntPtr.Zero) + { + Marshal.FreeCoTaskMem(_pCabinetFilenamePinned); + _pCabinetFilenamePinned = IntPtr.Zero; + } + GC.SuppressFinalize(this); + } + + void IDisposable.Dispose() + { + Close(true); + } + + [DebuggerStepThrough, DebuggerHidden] + private void ThrowOnInvalidState() + { + if (_pNativeCabinet == IntPtr.Zero) + throw new InvalidOperationException("Cabinet not initialized."); + if (_pDecompressor == IntPtr.Zero) + throw new InvalidOperationException("Decompressor not initialized."); + } + + internal bool IsInvalidState + { + get { return _pNativeCabinet == IntPtr.Zero || _pDecompressor == IntPtr.Zero; } + } + + public MSCabinetFlags Flags + { + get + { + ThrowOnInvalidState(); + return (MSCabinetFlags)_nativeCabinet.flags; + } + } + + public string PrevName + { + get + { + ThrowOnInvalidState(); + return _nativeCabinet.prevname; + } + } + + public string NextName + { + get + { + ThrowOnInvalidState(); + return _nativeCabinet.nextname; + } + } + + internal IntPtr Decompressor + { + get { return _pDecompressor; } + } + + public IEnumerable GetFiles() + { + ThrowOnInvalidState(); + + IntPtr pNextFile = _nativeCabinet.files; + MSCompressedFile containedFile; + if (pNextFile != IntPtr.Zero) + containedFile = new MSCompressedFile(this, pNextFile); + else + containedFile = null; + + while (containedFile != null) + { + yield return containedFile; + containedFile = containedFile.Next; + } + } + + /// + /// Appends specified cabinet to this one, forming or extending a cabinet set. + /// + /// The cab to append to this one. + public void Append(MSCabinet nextCabinet) + { + var result = NativeMethods.mspack_invoke_mscab_decompressor_append(_pDecompressor, _pNativeCabinet, nextCabinet._pNativeCabinet); + if (result != NativeMethods.MSPACK_ERR.MSPACK_ERR_OK) + throw new Exception(string.Format("Error '{0}' appending cab '{1}' to {2}.", result, nextCabinet._cabinetFilename, _cabinetFilename)); + + // after a successul append remarshal over the nativeCabinet struct5ure as it now represents the combined state. + _nativeCabinet = (NativeMethods.mscabd_cabinet)Marshal.PtrToStructure(_pNativeCabinet, typeof(NativeMethods.mscabd_cabinet)); + nextCabinet._nativeCabinet = (NativeMethods.mscabd_cabinet)Marshal.PtrToStructure(nextCabinet._pNativeCabinet, typeof(NativeMethods.mscabd_cabinet)); + } + } + + /// + /// Used with + /// + [Flags] + public enum MSCabinetFlags + { + /** Cabinet header flag: cabinet has a predecessor */ + MSCAB_HDR_PREVCAB = 0x01, + /** Cabinet header flag: cabinet has a successor */ + MSCAB_HDR_NEXTCAB = 0x02, + /** Cabinet header flag: cabinet has reserved header space */ + MSCAB_HDR_RESV = 0x04 + } +} \ No newline at end of file diff --git a/BurnOutSharp/External/libmspack4n/MSCompressedFile.cs b/BurnOutSharp/External/libmspack4n/MSCompressedFile.cs new file mode 100644 index 00000000..ee07af3d --- /dev/null +++ b/BurnOutSharp/External/libmspack4n/MSCompressedFile.cs @@ -0,0 +1,103 @@ +using System; +using LessIO; +using System.Runtime.InteropServices; + +namespace LibMSPackN +{ + /// + /// Represents a file contained inside of a cab. Returned from . + /// + public sealed class MSCompressedFile + { + private readonly MSCabinet _parentCabinet; + private readonly NativeMethods.mscabd_file _nativeFile; + private readonly IntPtr _pNativeFile; + + internal MSCompressedFile(MSCabinet parentCabinet, IntPtr pNativeFile) + { + //NOTE: we don't need to explicitly clean the nativeFile up. It is cleaned up by the parent cabinet. + _parentCabinet = parentCabinet; + _pNativeFile = pNativeFile; + _nativeFile = (NativeMethods.mscabd_file)Marshal.PtrToStructure(_pNativeFile, typeof (NativeMethods.mscabd_file)); + } + + private void ThrowOnInvalidState() + { + if (_parentCabinet.IsInvalidState) + throw new InvalidOperationException("Parent cabinet is no longer in a valid state. Ensure it was not disposed."); + if (_pNativeFile == IntPtr.Zero) + throw new InvalidOperationException("Native file is not initialized."); + } + + public string Filename + { + get + { + ThrowOnInvalidState(); + return _nativeFile.filename; + } + } + + public uint LengthInBytes + { + get + { + ThrowOnInvalidState(); + return _nativeFile.length; + } + } + + public MSCompressedFile Next + { + get + { + MSCompressedFile next; + if (_nativeFile.next != IntPtr.Zero) + next = new MSCompressedFile(_parentCabinet, _nativeFile.next); + else + next = null; + return next; + } + } + + public void ExtractTo(string destinationFilename) + { + ThrowOnInvalidState(); + IntPtr pDestinationFilename = IntPtr.Zero; + //TODO: Delete the file if it exists. If there are any issues ovewriting the dest file (e.g. it's readonly) MSPACK gives essentialy no error information. + string longDestinationFilename = new Path(destinationFilename).WithWin32LongPathPrefix(); + try + { + pDestinationFilename = Marshal.StringToCoTaskMemAnsi(longDestinationFilename); + var result = NativeMethods.mspack_invoke_mscab_decompressor_extract(_parentCabinet.Decompressor, _pNativeFile, pDestinationFilename); + if (result != NativeMethods.MSPACK_ERR.MSPACK_ERR_OK) + throw new Exception(string.Format("Error '{0}' extracting file to {1}.", result, longDestinationFilename)); + + FileSystem.SetLastWriteTime(new LessIO.Path(longDestinationFilename), GetModifiedTime()); + + var theAttributes = FileSystem.GetAttributes(new LessIO.Path(longDestinationFilename)); + + if ((_nativeFile.attribs & NativeMethods.mscabd_file_attribs.MSCAB_ATTRIB_ARCH) == NativeMethods.mscabd_file_attribs.MSCAB_ATTRIB_ARCH) + theAttributes |= FileAttributes.Archive; + if ((_nativeFile.attribs & NativeMethods.mscabd_file_attribs.MSCAB_ATTRIB_HIDDEN) == NativeMethods.mscabd_file_attribs.MSCAB_ATTRIB_HIDDEN) + theAttributes |= FileAttributes.Hidden; + if ((_nativeFile.attribs & NativeMethods.mscabd_file_attribs.MSCAB_ATTRIB_RDONLY) == NativeMethods.mscabd_file_attribs.MSCAB_ATTRIB_RDONLY) + theAttributes |= FileAttributes.ReadOnly; + if ((_nativeFile.attribs & NativeMethods.mscabd_file_attribs.MSCAB_ATTRIB_SYSTEM) == NativeMethods.mscabd_file_attribs.MSCAB_ATTRIB_SYSTEM) + theAttributes |= FileAttributes.System; + + FileSystem.SetAttributes(new LessIO.Path(longDestinationFilename), theAttributes); + } + finally + { + if (pDestinationFilename != IntPtr.Zero) + Marshal.FreeCoTaskMem(pDestinationFilename); + } + } + + private DateTime GetModifiedTime() + { + return new DateTime(_nativeFile.date_y, _nativeFile.date_m, _nativeFile.date_d, _nativeFile.time_h, _nativeFile.time_m, _nativeFile.time_s); + } + } +} \ No newline at end of file diff --git a/BurnOutSharp/External/libmspack4n/NativeMethods.cs b/BurnOutSharp/External/libmspack4n/NativeMethods.cs new file mode 100644 index 00000000..1ad0880c --- /dev/null +++ b/BurnOutSharp/External/libmspack4n/NativeMethods.cs @@ -0,0 +1,390 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace LibMSPackN +{ + internal static class NativeMethods + { + // ReSharper disable InconsistentNaming + // ReSharper disable EnumUnderlyingTypeIsInt + /// + /// A very simple test/usage example of the raw native mspack API. + /// + public static void DoTest() + { + IntPtr pDecompressor = mspack_create_cab_decompressor(IntPtr.Zero); + const string cabFilename = @"C:\projects\LibMSPackDotNet\LibMSPackDotNetTest\bin\Debug\huge-lzx15.cab"; + const string outFilename = @"C:\projects\LibMSPackDotNet\LibMSPackDotNetTest\bin\Debug\huge-lzx15.out"; + + IntPtr pCabFilename = Marshal.StringToCoTaskMemAnsi(cabFilename); + IntPtr pOutFilename = Marshal.StringToCoTaskMemAnsi(outFilename); + + IntPtr pCabinet = mspack_invoke_mscab_decompressor_open(pDecompressor, pCabFilename); + mscabd_cabinet cabStruct = (mscabd_cabinet) Marshal.PtrToStructure(pCabinet, typeof (mscabd_cabinet)); + IntPtr pFirstFile = cabStruct.files; + Debug.Print("pFirstFile: 0x" + pFirstFile.ToInt32().ToString("x")); + MSPACK_ERR result = mspack_invoke_mscab_decompressor_extract(pDecompressor, pFirstFile, pOutFilename); + Debug.Print("extract result: {0}", result); + mspack_invoke_mscab_decompressor_close(pDecompressor, pCabinet); + mspack_destroy_cab_decompressor(pDecompressor); + + Marshal.FreeCoTaskMem(pOutFilename); + Marshal.FreeCoTaskMem(pCabFilename); + } + + /// + /// A structure which represents a single file in a cabinet or cabinet set. + /// * All fields are READ ONLY. + /// + [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Ansi, Size = 36)] + internal struct mscabd_file + { + /** + * The next file in the cabinet or cabinet set, or NULL if this is the + * final file. + */ + [FieldOffset(0)] public IntPtr /*mscabd_file*/ next; + + /** + * The filename of the file. + * + * A null terminated string of up to 255 bytes in length, it may be in + * either ISO-8859-1 or UTF8 format, depending on the file attributes. + * + * @see attribs + */ + [MarshalAs(UnmanagedType.LPStr)] [FieldOffset(4)] public string filename; + + /** The uncompressed length of the file, in bytes. */ + [FieldOffset(8)] public uint length; + + /** + * File attributes. + * + * The following attributes are defined: + * - #MSCAB_ATTRIB_RDONLY indicates the file is write protected. + * - #MSCAB_ATTRIB_HIDDEN indicates the file is hidden. + * - #MSCAB_ATTRIB_SYSTEM indicates the file is a operating system file. + * - #MSCAB_ATTRIB_ARCH indicates the file is "archived". + * - #MSCAB_ATTRIB_EXEC indicates the file is an executable program. + * - #MSCAB_ATTRIB_UTF_NAME indicates the filename is in UTF8 format rather + * than ISO-8859-1. + */ + [FieldOffset(12)] public mscabd_file_attribs attribs; + + /** File's last modified time, hour field. */ + [FieldOffset(16)] public byte time_h; + + /** File's last modified time, minute field. */ + [FieldOffset(17)] public byte time_m; + + /** File's last modified time, second field. */ + [FieldOffset(18)] public byte time_s; + + /** File's last modified date, day field. */ + [FieldOffset(19)] public byte date_d; + + /** File's last modified date, month field. */ + [FieldOffset(20)] public byte date_m; + + /** File's last modified date, year field. */ + [FieldOffset(24)] public int date_y; + + /** A pointer to the folder that contains this file. */ + [FieldOffset(28)] public IntPtr /*mscabd_folder*/ folder; + + /** The uncompressed offset of this file in its folder. */ + [FieldOffset(32)] public uint offset; + } + + [Flags] + internal enum mscabd_file_attribs : int + { + /** mscabd_file::attribs attribute: file is read-only. */ + MSCAB_ATTRIB_RDONLY = 0x01, + /** mscabd_file::attribs attribute: file is hidden. */ + MSCAB_ATTRIB_HIDDEN = 0x02, + /** mscabd_file::attribs attribute: file is an operating system file. */ + MSCAB_ATTRIB_SYSTEM = 0x04, + /** mscabd_file::attribs attribute: file is "archived". */ + MSCAB_ATTRIB_ARCH = 0x20, + /** mscabd_file::attribs attribute: file is an executable program. */ + MSCAB_ATTRIB_EXEC = 0x40, + /** mscabd_file::attribs attribute: filename is UTF8, not ISO-8859-1. */ + MSCAB_ATTRIB_UTF_NAME = 0x80 + } + + /// + /// A structure which represents a single cabinet file. + /// + /// All fields are READ ONLY. + /// + /// If this cabinet is part of a merged cabinet set, the #files and #folders + /// fields are common to all cabinets in the set, and will be identical. + /// + /// @see mscab_decompressor::open(), mscab_decompressor::close(), + /// mscab_decompressor::search() + /// + [StructLayout(LayoutKind.Explicit, Size = 60, CharSet = CharSet.Ansi)] + internal class mscabd_cabinet + { + /** + * The next cabinet in a chained list, if this cabinet was opened with + * mscab_decompressor::search(). May be NULL to mark the end of the + * list. + */ + [FieldOffset(0)] public IntPtr /*mscabd_cabinet*/ next; + + /** + * The filename of the cabinet. More correctly, the filename of the + * physical file that the cabinet resides in. This is given by the + * library user and may be in any format. + */ + [MarshalAs(UnmanagedType.LPStr)] [FieldOffset(4)] public string filename; + + /** The file offset of cabinet within the physical file it resides in. */ + [FieldOffset(8)] public off_t base_offset; + + /** The length of the cabinet file in bytes. */ + [FieldOffset(12)] public UInt32 length; + + /** The previous cabinet in a cabinet set, or NULL. */ + [FieldOffset(16)] public IntPtr /*mscabd_cabinet*/ prevcab; + + /** The next cabinet in a cabinet set, or NULL. */ + [FieldOffset(20)] public IntPtr /*mscabd_cabinet*/ nextcab; + + /** The filename of the previous cabinet in a cabinet set, or NULL. */ + [MarshalAs(UnmanagedType.LPStr)] [FieldOffset(24)] public string prevname; + + /** The filename of the next cabinet in a cabinet set, or NULL. */ + [MarshalAs(UnmanagedType.LPStr)] [FieldOffset(28)] public string nextname; + + /** The name of the disk containing the previous cabinet in a cabinet + * set, or NULL. + */ + [MarshalAs(UnmanagedType.LPStr)] [FieldOffset(32)] public string previnfo; + + /** The name of the disk containing the next cabinet in a cabinet set, + * or NULL. + */ + [MarshalAs(UnmanagedType.LPStr)] [FieldOffset(36)] public string nextinfo; + + /** A list of all files in the cabinet or cabinet set. */ + [FieldOffset(40)] public IntPtr /*mscabd_file*/ files; + + /** A list of all folders in the cabinet or cabinet set. */ + [FieldOffset(44)] public IntPtr /*mscabd_folder*/ folders; + + /** + * The set ID of the cabinet. All cabinets in the same set should have + * the same set ID. + */ + [FieldOffset(48)] public ushort set_id; + + /** + * The index number of the cabinet within the set. Numbering should + * start from 0 for the first cabinet in the set, and increment by 1 for + * each following cabinet. + */ + [FieldOffset(50)] public ushort set_index; + + /** + * The number of bytes reserved in the header area of the cabinet. + * + * If this is non-zero and flags has MSCAB_HDR_RESV set, this data can + * be read by the calling application. It is of the given length, + * located at offset (base_offset + MSCAB_HDR_RESV_OFFSET) in the + * cabinet file. + * + * @see flags + */ + [FieldOffset(52)] public ushort header_resv; + + /** + * Header flags. + * + * - MSCAB_HDR_PREVCAB indicates the cabinet is part of a cabinet set, and + * has a predecessor cabinet. + * - MSCAB_HDR_NEXTCAB indicates the cabinet is part of a cabinet set, and + * has a successor cabinet. + * - MSCAB_HDR_RESV indicates the cabinet has reserved header space. + * + * @see prevname, previnfo, nextname, nextinfo, header_resv + */ + + [FieldOffset(56)] //NOTE the unexpected pack here. But this is the way it is in C! + public int flags; + } + + /// + /// Creates a new CAB decompressor. + /// + /// a custom mspack_system structure, or NULL to use the default. PASS NULL! + /// A pointer to mscab_decompressor or null. + [DllImport("mspack.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr mspack_create_cab_decompressor(IntPtr sys); + + /// + /// Destroys an existing CAB decompressor. + /// + /// the #mscab_decompressor to destroy + [DllImport("mspack.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + internal static extern void mspack_destroy_cab_decompressor(IntPtr mscab_decompressor_self); + + /// + /// Opens a cabinet file and reads its contents. + /// + /// If the file opened is a valid cabinet file, all headers will be read + /// and a mscabd_cabinet structure will be returned, with a full list of + /// folders and files. + /// + /// In the case of an error occuring, NULL is returned and the error code + /// is available from last_error(). + /// + /// The filename pointer should be considered "in use" until close() is + /// called on the cabinet. + /// + /// a self-referential pointer to the mscab_decompressor instance (returned from ) being called + /// the filename of the cabinet file. + /// *NOTE*: Since this string is allocated in the "managed world" (probably with ) it must be pinned lifetime of this cabinet as libmspack uses this string during future operations on the cabinet. Otherwise the GC will move it or free it. Be sure to clean it up later! + /// + /// a pointer to a mscabd_cabinet structure, or NULL on failure + [DllImport("mspack.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr /*mscabd_cabinet*/ mspack_invoke_mscab_decompressor_open(IntPtr mscab_decompressor_self, IntPtr pinnedFilename); + + /// + /// Closes a previously opened cabinet or cabinet set. + /// + /// This closes a cabinet, all cabinets associated with it via the + /// mscabd_cabinet::next, mscabd_cabinet::prevcab and + /// mscabd_cabinet::nextcab pointers, and all folders and files. All + /// memory used by these entities is freed. + /// + /// The cabinet pointer is now invalid and cannot be used again. All + /// mscabd_folder and mscabd_file pointers from that cabinet or cabinet + /// set are also now invalid, and cannot be used again. + /// + /// If the cabinet pointer given was created using search(), it MUST be + /// the cabinet pointer returned by search() and not one of the later + /// cabinet pointers further along the mscabd_cabinet::next chain. + /// + /// If extra cabinets have been added using append() or prepend(), these + /// will all be freed, even if the cabinet pointer given is not the first + /// cabinet in the set. Do NOT close() more than one cabinet in the set. + /// + /// The mscabd_cabinet::filename is not freed by the library, as it is + /// not allocated by the library. The caller should free this itself if + /// necessary, before it is lost forever. + /// + /// a self-referential pointer to the mscab_decompressor instance (returned from ) being called + /// the cabinet to close + [DllImport("mspack.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + internal static extern void mspack_invoke_mscab_decompressor_close(IntPtr mscab_decompressor_self, IntPtr /*mscabd_cabinet*/ cab); + + /// + /// Extracts a file from a cabinet or cabinet set. + /// + /// This extracts a compressed file in a cabinet and writes it to the given + /// filename. + /// + /// The MS-DOS filename of the file, mscabd_file::filename, is NOT USED + /// by extract(). The caller must examine this MS-DOS filename, copy and + /// change it as necessary, create directories as necessary, and provide + /// the correct filename as a parameter, which will be passed unchanged + /// to the decompressor's mspack_system::open() + /// + /// If the file belongs to a split folder in a multi-part cabinet set, + /// and not enough parts of the cabinet set have been loaded and appended + /// or prepended, an error will be returned immediately. + /// + /// a self-referential pointer to the mscab_decompressor instance (returned from ) being called + /// the file to be decompressed + /// + /// the filename of the file being written to. + /// Allocate this file with (more expaination in ). + /// + /// an error code, or MSPACK_ERR_OK if successful + [DllImport("mspack.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + internal static extern MSPACK_ERR mspack_invoke_mscab_decompressor_extract(IntPtr mscab_decompressor_self, IntPtr file, IntPtr filename); + + [DllImport("mspack.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + internal static extern MSPACK_ERR mspack_invoke_mscab_decompressor_last_error(IntPtr mscab_decompressor_self); + + [DllImport("mspack.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + internal static extern MSPACK_ERR mspack_invoke_mscab_decompressor_append(IntPtr mscab_decompressor_self, IntPtr /*mscabd_cabinet*/ cab, IntPtr /*mscabd_cabinet*/ nextcab); + + /// + /// All compressors and decompressors use the same set of error codes. Most + /// methods return an error code directly. For methods which do not + /// return error codes directly, the error code can be obtained with the + /// method. + /// + internal enum MSPACK_ERR : int + { + /// Error code: no error + MSPACK_ERR_OK = 0, + + /// Error code: bad arguments to method + MSPACK_ERR_ARGS = 1, + + /// Error code: error opening file + MSPACK_ERR_OPEN = 2, + + /// Error code: error reading file + MSPACK_ERR_READ = 3, + + /// Error code: error writing file + MSPACK_ERR_WRITE = 4, + + /// Error code: seek error + MSPACK_ERR_SEEK = 5, + + /// Error code: out of memory + MSPACK_ERR_NOMEMORY = 6, + + /// Error code: bad "magic id" in file + MSPACK_ERR_SIGNATURE = 7, + + /// Error code: bad or corrupt file format + MSPACK_ERR_DATAFORMAT = 8, + + /// Error code: bad checksum or CRC + MSPACK_ERR_CHECKSUM = 9, + + /// Error code: error during compression + MSPACK_ERR_CRUNCH = 10, + + /// Error code: error during decompression + MSPACK_ERR_DECRUNCH = 11 + } + + /// + /// Used by . + /// + [StructLayout(LayoutKind.Sequential, Size = 4)] + internal struct off_t + { + private Int32 value; + + public off_t(Int32 x) + { + value = x; + } + + public static implicit operator off_t(Int32 x) + { + return new off_t(x); + } + + public static implicit operator Int32(off_t x) + { + return x.value; + } + } + // ReSharper restore EnumUnderlyingTypeIsInt + // ReSharper restore InconsistentNaming + } +} diff --git a/BurnOutSharp/PackerType/PECompact.cs b/BurnOutSharp/PackerType/PECompact.cs index dbb56978..db7b6950 100644 --- a/BurnOutSharp/PackerType/PECompact.cs +++ b/BurnOutSharp/PackerType/PECompact.cs @@ -7,7 +7,7 @@ { // "PEC2" byte[] check = new byte[] { 0x50, 0x45, 0x43, 0x32 }; - if (fileContent.Contains(check, out int position)) + if (fileContent.Contains(check, out int position, end: 2048)) return "PE Compact 2" + (includePosition ? $" (Index {position})" : string.Empty); return null; diff --git a/BurnOutSharp/PackerType/UPX.cs b/BurnOutSharp/PackerType/UPX.cs index 86b53bba..9f8dc142 100644 --- a/BurnOutSharp/PackerType/UPX.cs +++ b/BurnOutSharp/PackerType/UPX.cs @@ -25,11 +25,11 @@ namespace BurnOutSharp.PackerType // UPX0 check = new byte[] { 0x55, 0x50, 0x58, 0x30 }; - if (fileContent.Contains(check, out position)) + if (fileContent.Contains(check, out position, end: 2048)) { // UPX1 byte[] check2 = new byte[] { 0x55, 0x50, 0x58, 0x31 }; - if (fileContent.Contains(check2, out int position2)) + if (fileContent.Contains(check2, out int position2, end: 2048)) { return $"UPX (Unknown Version)" + (includePosition ? $" (Index {position}, {position2})" : string.Empty); } @@ -37,11 +37,11 @@ namespace BurnOutSharp.PackerType // NOS0 check = new byte[] { 0x4E, 0x4F, 0x53, 0x30 }; - if (fileContent.Contains(check, out position)) + if (fileContent.Contains(check, out position, end: 2048)) { // NOS1 byte[] check2 = new byte[] { 0x4E, 0x4F, 0x53, 0x31 }; - if (fileContent.Contains(check2, out int position2)) + if (fileContent.Contains(check2, out int position2, end: 2048)) { return $"UPX (NOS Variant) (Unknown Version)" + (includePosition ? $" (Index {position}, {position2})" : string.Empty); } diff --git a/README.md b/README.md index 757de9af..cb875671 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ Below is a list of the protections that can be detected using this code: - Hexalock Autolock - Impulse Reactor - IndyVCD -- Inno Setup - JoWooD X-Prot (v1/v2) - Key2Audio XS - Key-Lock (Dongle) @@ -76,7 +75,6 @@ Below is a list of the protections that can be detected using this code: - Uplay (partial) - VOB ProtectCD/DVD - Winlock -- WISE Installer - WTM CD Protect - WTM Copy Protection - XCP @@ -90,9 +88,11 @@ Below is a list of the executable packers that can be detected using this code: - Armadillo - dotFuscator - EXE Stealth +- Inno Setup - NSIS - PECompact - UPX +- WISE Installer ## Archive Formats @@ -103,7 +103,7 @@ Below is a list of archive or archive-like formats that can be extracted and hav - BZIP2 - GZIP - InstallShield CAB -- Microsoft CAB (.NET Framework 4.7.2 and 4.8 only) +- Microsoft CAB - MPQ - Microsoft Installer (MSI) (.NET Framework 4.7.2 and 4.8 only) - PKZIP and derived files @@ -114,5 +114,5 @@ Below is a list of archive or archive-like formats that can be extracted and hav ## Contributions -Contributions to the project are welcome. Please follow the current coding styles and please do not add any keys or legally dubious things to the code. Thank you to all of the testers, particularly from the DICUI project who helped get this rolling. +Contributions to the project are welcome. Please follow the current coding styles and please do not add any keys or legally dubious things to the code. Thank you to all of the testers, particularly from the MPF project who helped get this rolling.