Wrap in libmspack4n and LessIO as external code

This commit is contained in:
Matt Nadareski
2021-03-02 12:14:14 -08:00
parent b3671a430e
commit 73aae8118f
14 changed files with 1894 additions and 11 deletions

View File

@@ -27,8 +27,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LessIO" Version="1.0.34" Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'" />
<PackageReference Include="libmspack4n" Version="0.9.10" Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="UnshieldSharp" Version="1.4.2.4" />
<PackageReference Include="WiseUnpacker" Version="1.0.1" />

View File

@@ -0,0 +1,55 @@
using System;
namespace LessIO
{
//TODO: These are the samea s Win32. Consider whether we should expose all of these?
/// <summary>
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/gg258117%28v=vs.85%29.aspx
/// </summary>
/// <remarks>
/// These have the same values as <see cref="System.IO.FileAttributes"/> 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
/// </remarks>
[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
*/
}
}

View File

@@ -0,0 +1,160 @@
using System;
using LessIO.Strategies;
using LessIO.Strategies.Win32;
using BadPath = System.IO.Path;
using System.Collections.Generic;
namespace LessIO
{
/// <summary>
/// Provides various file system operations for the current platform.
/// </summary>
public static class FileSystem
{
private static readonly Lazy<FileSystemStrategy> LazyStrategy = new Lazy<FileSystemStrategy>(() => new Win32FileSystemStrategy());
private static FileSystemStrategy Strategy
{
get { return LazyStrategy.Value; }
}
/// <summary>
/// Sets the date and time that the specified file was last written to.
/// </summary>
/// <param name="path">The path of the file to set the file time on.</param>
/// <param name="lastWriteTime">
/// The date to set for the last write date and time of specified file.
/// Expressed in local time.
/// </param>
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);
}
/// <summary>
/// Returns true if a file or directory exists at the specified path.
/// </summary>
public static bool Exists(Path path)
{
return Strategy.Exists(path);
}
/// <summary>
/// Creates the specified directory.
/// </summary>
/// <remarks>
/// Creates parent directories as needed.
/// </remarks>
public static void CreateDirectory(Path path)
{
Strategy.CreateDirectory(path);
}
/// <summary>
/// Removes/deletes an existing empty directory.
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365488%28v=vs.85%29.aspx
/// </summary>
/// <param name="path"></param>
public static void RemoveDirectory(Path path)
{
RemoveDirectory(path, false);
}
/// <summary>
/// Removes/deletes an existing directory.
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365488%28v=vs.85%29.aspx
/// </summary>
/// <param name="path">The path to the directory to remove.</param>
/// <param name="recursively">
/// True to remove the directory and all of its contents recursively.
/// False will remove the directory only if it is empty.
/// </param>
/// <remarks>Recursively implies removing contained files forcefully.</remarks>
public static void RemoveDirectory(Path path, bool recursively)
{
Strategy.RemoveDirectory(path, recursively);
}
/// <summary>
/// Removes/deletes an existing file.
/// To remove a directory see <see cref="RemoveDirectory"/>.
/// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363915%28v=vs.85%29.aspx
/// </summary>
/// <param name="path">The file to remove.</param>
/// <param name="forcefully">True to remove the file even if it is read-only.</param>
public static void RemoveFile(Path path, bool forcefully)
{
Strategy.RemoveFile(path, forcefully);
}
/// <summary>
/// Removes/deletes an existing file.
/// To remove a directory see <see cref="RemoveDirectory"/>.
/// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363915%28v=vs.85%29.aspx
/// </summary>
/// <param name="path">The file to remove.</param>
public static void RemoveFile(Path path)
{
Strategy.RemoveFile(path, false);
}
/// <summary>
/// Copies the specified existing file to a new location.
/// Will throw an exception if the destination file already exists.
/// </summary>
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);
}
/// <summary>
/// Returns true if the specified path is a directory.
/// </summary>
internal static bool IsDirectory(Path path)
{
return Strategy.IsDirectory(path);
}
/// <summary>
/// Creates or overwrites the file at the specified path.
/// </summary>
/// <param name="path">The path and name of the file to create. Supports long file paths.</param>
/// <returns>A <see cref="System.IO.Stream"/> that provides read/write access to the file specified in path.</returns>
public static System.IO.Stream CreateFile(Path path)
{
return Strategy.CreateFile(path);
}
/// <summary>
/// Returns the contents of the specified directory.
/// </summary>
/// <param name="directory">The path to the directory to get the contents of.</param>
public static IEnumerable<Path> ListContents(Path directory)
{
return Strategy.ListContents(directory);
}
/// <summary>
/// Returns the contents of the specified directory.
/// </summary>
/// <param name="directory">The path to the directory to get the contents of.</param>
/// <param name="recursive">True to list the contents of any child directories.</param>
/// <remarks>If the specified directory is not actually a directory then an empty set is returned.</remarks>
public static IEnumerable<Path> ListContents(Path directory, bool recursive)
{
return Strategy.ListContents(directory, recursive);
}
}
}

399
BurnOutSharp/External/LessIO/Path.cs vendored Normal file
View File

@@ -0,0 +1,399 @@
using System;
using System.Linq;
using BadPath = System.IO.Path;
namespace LessIO
{
/// <summary>
/// Represents a file system path.
/// </summary>
public struct Path : IEquatable<Path>
{
private readonly string _path;
private static readonly string _pathEmpty = string.Empty;
public static readonly Path Empty = new Path();
/// <summary>
/// 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
/// </summary>
private static readonly string Win32LongPathPrefix = @"\\?\";
/// <summary>
/// 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
/// </summary>
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;
}
/// <summary>
/// Returns the directory seperator characers.
/// </summary>
internal static char[] DirectorySeperatorChars
{
get
{
return new char[] { BadPath.DirectorySeparatorChar, BadPath.AltDirectorySeparatorChar };
}
}
internal static bool IsDirectorySeparator(char ch)
{
return Array.Exists<char>(DirectorySeperatorChars, c => c == ch);
}
/// <summary>
/// Gets the normalized path string. May be rooted or may be relative.
/// For a rooted/qualified path use <see cref="FullPathString"/>
/// </summary>
public string PathString
{
get
{
return _path != null ? _path : _pathEmpty;
}
}
/// <summary>
/// Returns the absolute path for the current path.
/// Compatible with <see cref="System.IO.Path.GetFullPath(string)"/>.
/// </summary>
public Path FullPath
{
get
{
return new Path(this.FullPathString);
}
}
/// <summary>
/// Returns the absolute path for the current path.
/// Compatible with <see cref="System.IO.Path.GetFullPath(string)"/>.
/// </summary>
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); }
}
/// <summary>
/// Indicates if the two paths are equivelent and point to the same file or directory.
/// </summary>
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);
}
/// <summary>
/// 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
/// </summary>
/// <remarks>
/// The <see cref="Path"/> 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).
/// </remarks>
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();
}
/// <summary>
/// Gets the root directory information of the specified path.
/// </summary>
public string PathRoot
{
get
{
return GetPathRoot(this.PathString);
}
}
/// <summary>
/// Modeled after <see cref="System.IO.Path.GetPathRoot(string)"/> but supports long path names.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
/// <remarks>
/// 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).
/// </remarks>
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;
}
}
/// <summary>
/// Indicates if the file or directory at the specified path exists.
/// For code compatibility with <see cref="System.IO.FileSystemInfo.Exists"/>.
/// </summary>
public bool Exists
{
get { return FileSystem.Exists(this); }
}
/// <summary>
/// True if the path is a rooted/fully qualified path. Otherwise returns false if it is a relative path.
/// Compatible with <see cref="System.IO.Path.IsPathRooted(string)"/>.
/// </summary>
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;
}
}
/// <summary>
/// For code compatibility with <see cref="System.IO.FileInfo.CreateText()"/>
/// </summary>
public System.IO.StreamWriter CreateText()
{
var stream = FileSystem.CreateFile(this);
return new System.IO.StreamWriter(stream, System.Text.Encoding.UTF8);
}
/// <summary>
/// For code compatibility with <see cref="System.IO.Path.GetFileName(string)"/>
/// </summary>
public static string GetFileName(string path)
{
return BadPath.GetFileName(path);
}
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace LessIO.Strategies
{
/// <summary>
/// See <see cref="FileSystem"/> for documentation of each method of this class.
/// </summary>
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<Path> ListContents(Path directory);
public virtual IEnumerable<Path> ListContents(Path directory, bool recursive)
{
IEnumerable<Path> children = ListContents(directory);
if (recursive)
{
IEnumerable<Path> 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);
}
}
}

View File

@@ -0,0 +1,211 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;
using FileAttributes = System.IO.FileAttributes;
namespace LessIO.Strategies.Win32
{
/// <summary>
/// Native/Win32 methods for accessing file system. Primarily to get around <see cref="System.IO.PathTooLongException"/>.
/// Good references:
/// * https://blogs.msdn.microsoft.com/bclteam/2007/03/26/long-paths-in-net-part-2-of-3-long-path-workarounds-kim-hamilton/
/// </summary>
internal class NativeMethods
{
/// <summary>
/// Specified in Windows Headers for default maximum path. To go beyond this length you must prepend <see cref="LongPathPrefix"/> to the path.
/// </summary>
internal const int MAX_PATH = 260;
/// <summary>
/// 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
/// </summary>
internal static readonly string LongPathPrefix = @"\\?\";
/// <summary>
/// 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
/// </summary>
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
}
/// <summary>
/// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363855%28v=vs.85%29.aspx
/// </summary>
[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);
/// <summary>
/// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365535%28v=vs.85%29.aspx
/// </summary>
[DllImport("kernel32.dll", EntryPoint = "SetFileAttributes", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false)]
internal static extern bool SetFileAttributes(string lpFileName, uint dwFileAttributes);
/// <summary>
/// https://msdn.microsoft.com/en-us/library/windows/desktop/aa364944%28v=vs.85%29.aspx
/// </summary>
/// <param name="lpFileName"></param>
/// <returns></returns>
[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);
/// <summary>
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858%28v=vs.85%29.aspx
/// </summary>
[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);
/// <summary>
/// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363851%28v=vs.85%29.aspx
/// </summary>
[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;
/// <summary>
/// https://msdn.microsoft.com/en-us/library/windows/desktop/ms679351%28v=vs.85%29.aspx
/// </summary>
[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);
}
}

View File

@@ -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<String>();
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<Path,int> pathPartCount = (Path p) => p.PathString.Split(Path.DirectorySeperatorChars).Length;
Array.Sort<Path>(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);
}
/// <summary>
/// Creates or overwrites the file at the specified path.
/// </summary>
/// <param name="path">The path and name of the file to create. Supports long file paths.</param>
/// <returns>A <see cref="System.IO.Stream"/> that provides read/write access to the file specified in path.</returns>
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<Path> 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);
}
}
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
namespace LibMSPackN
{
/// <summary>
/// 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).
/// </summary>
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;
}
}
/// <summary>
/// Creates a native instance of the decompressor and returns the pointer. Must be destroyed via <see cref="DestroyInstance"/>.
/// </summary>
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;
}
/// <summary>
/// Destroys the specified instance that was created with <see cref="CreateInstance"/>.
/// </summary>
/// <param name="pDecompressor"></param>
internal static void DestroyInstance(IntPtr pDecompressor)
{
NativeMethods.mspack_destroy_cab_decompressor(pDecompressor);
}
internal IntPtr Pointer
{
get
{
ThrowOnInvalidState();
return _pDecompressor;
}
}
/// <summary>
/// Returns the defalut instance of a decompressor. DO NOT DISPOSE IT!
/// </summary>
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);
}
}
}

View File

@@ -0,0 +1,184 @@
using System;
using LessIO;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace LibMSPackN
{
/// <summary>
/// Represents a .cab file.
/// </summary>
/// <remarks>
/// Example usage:
/// <code>
/// 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));
/// }
/// }
/// </code>
/// </remarks>
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<MSCompressedFile> 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;
}
}
/// <summary>
/// Appends specified cabinet to this one, forming or extending a cabinet set.
/// </summary>
/// <param name="nextCabinet">The cab to append to this one.</param>
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));
}
}
/// <summary>
/// Used with <see cref="MSCabinet.Flags"/>
/// </summary>
[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
}
}

View File

@@ -0,0 +1,103 @@
using System;
using LessIO;
using System.Runtime.InteropServices;
namespace LibMSPackN
{
/// <summary>
/// Represents a file contained inside of a cab. Returned from <see cref="MSCabinet.GetFiles()"/>.
/// </summary>
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);
}
}
}

View File

@@ -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
/// <summary>
/// A very simple test/usage example of the raw native mspack API.
/// </summary>
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);
}
/// <summary>
/// A structure which represents a single file in a cabinet or cabinet set.
/// * All fields are READ ONLY.
/// </summary>
[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
}
/// <summary>
/// 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()
/// </summary>
[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;
}
/// <summary>
/// Creates a new CAB decompressor.
/// </summary>
/// <param name="sys">a custom mspack_system structure, or NULL to use the default. PASS NULL!</param>
/// <returns>A pointer to mscab_decompressor or null.</returns>
[DllImport("mspack.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr mspack_create_cab_decompressor(IntPtr sys);
/// <summary>
/// Destroys an existing CAB decompressor.
/// </summary>
/// <param name="mscab_decompressor_self">the #mscab_decompressor to destroy</param>
[DllImport("mspack.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
internal static extern void mspack_destroy_cab_decompressor(IntPtr mscab_decompressor_self);
/// <summary>
/// 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.
/// </summary>
/// <param name="mscab_decompressor_self">a self-referential pointer to the mscab_decompressor instance (returned from <see cref="mspack_create_cab_decompressor"/>) being called</param>
/// <param name="pinnedFilename">the filename of the cabinet file.
/// *NOTE*: Since this string is allocated in the "managed world" (probably with <see cref="Marshal.StringToCoTaskMemAnsi"/>) 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!
/// </param>
/// <returns>a pointer to a mscabd_cabinet structure, or NULL on failure</returns>
[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);
/// <summary>
/// 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.
/// </summary>
/// <param name="mscab_decompressor_self">a self-referential pointer to the mscab_decompressor instance (returned from <see cref="mspack_create_cab_decompressor"/>) being called</param>
/// <param name="cab">the cabinet to close</param>
[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);
/// <summary>
/// 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.
/// </summary>
/// <param name="mscab_decompressor_self">a self-referential pointer to the mscab_decompressor instance (returned from <see cref="mspack_create_cab_decompressor"/>) being called</param>
/// <param name="file">the file to be decompressed</param>
/// <param name="filename">
/// the filename of the file being written to.
/// Allocate this file with <see cref="Marshal.StringToCoTaskMemAnsi"/> (more expaination in <see cref="mspack_invoke_mscab_decompressor_open"/>).
/// </param>
/// <returns>an error code, or MSPACK_ERR_OK if successful</returns>
[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);
/// <summary>
/// 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
/// <see cref="mspack_invoke_mscab_decompressor_last_error"/> method.
/// </summary>
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
}
/// <summary>
/// Used by <see cref="mscabd_cabinet"/>.
/// </summary>
[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
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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.