diff --git a/SabreTools.Printing.sln b/SabreTools.Printing.sln
index 479fc7b..3bf45a5 100644
--- a/SabreTools.Printing.sln
+++ b/SabreTools.Printing.sln
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Printing", "SabreTools.Printing\SabreTools.Printing.csproj", "{4274AA02-00A0-48F1-98C4-B3286E547246}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{F7DE19C8-9E62-443D-BBD0-A846405E3903}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
{4274AA02-00A0-48F1-98C4-B3286E547246}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4274AA02-00A0-48F1-98C4-B3286E547246}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4274AA02-00A0-48F1-98C4-B3286E547246}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F7DE19C8-9E62-443D-BBD0-A846405E3903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F7DE19C8-9E62-443D-BBD0-A846405E3903}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F7DE19C8-9E62-443D-BBD0-A846405E3903}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F7DE19C8-9E62-443D-BBD0-A846405E3903}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/Test/FileTypes.cs b/Test/FileTypes.cs
new file mode 100644
index 0000000..66cb8aa
--- /dev/null
+++ b/Test/FileTypes.cs
@@ -0,0 +1,812 @@
+using System;
+using SabreTools.Matching;
+
+namespace Test
+{
+ internal static class FileTypes
+ {
+ ///
+ /// Get the supported file type for a magic string
+ ///
+ /// Recommend sending in 16 bytes to check
+ public static SupportedFileType GetFileType(byte[] magic)
+ {
+ // If we have an invalid magic byte array
+ if (magic == null || magic.Length == 0)
+ return SupportedFileType.UNKNOWN;
+
+ // TODO: For all modelled types, use the constants instead of hardcoded values here
+ #region AACSMediaKeyBlock
+
+ // Block starting with verify media key record
+ if (magic.StartsWith(new byte?[] { 0x81, 0x00, 0x00, 0x14 }))
+ return SupportedFileType.AACSMediaKeyBlock;
+
+ // Block starting with type and version record
+ if (magic.StartsWith(new byte?[] { 0x10, 0x00, 0x00, 0x0C }))
+ return SupportedFileType.AACSMediaKeyBlock;
+
+ #endregion
+
+ #region BDPlusSVM
+
+ if (magic.StartsWith(new byte?[] { 0x42, 0x44, 0x53, 0x56, 0x4D, 0x5F, 0x43, 0x43 }))
+ return SupportedFileType.BDPlusSVM;
+
+ #endregion
+
+ #region BFPK
+
+ if (magic.StartsWith(new byte?[] { 0x42, 0x46, 0x50, 0x4b }))
+ return SupportedFileType.BFPK;
+
+ #endregion
+
+ #region BSP
+
+ if (magic.StartsWith(new byte?[] { 0x1e, 0x00, 0x00, 0x00 }))
+ return SupportedFileType.BSP;
+
+ #endregion
+
+ #region BZip2
+
+ if (magic.StartsWith(new byte?[] { 0x42, 0x52, 0x68 }))
+ return SupportedFileType.BZip2;
+
+ #endregion
+
+ #region CFB
+
+ if (magic.StartsWith(new byte?[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 }))
+ return SupportedFileType.CFB;
+
+ #endregion
+
+ #region CIA
+
+ // No magic checks for CIA
+
+ #endregion
+
+ #region Executable
+
+ // DOS MZ executable file format (and descendants)
+ if (magic.StartsWith(new byte?[] { 0x4d, 0x5a }))
+ return SupportedFileType.Executable;
+
+ /*
+ // None of the following are supported in scans yet
+
+ // Executable and Linkable Format
+ if (magic.StartsWith(new byte?[] { 0x7f, 0x45, 0x4c, 0x46 }))
+ return FileTypes.Executable;
+
+ // Mach-O binary (32-bit)
+ if (magic.StartsWith(new byte?[] { 0xfe, 0xed, 0xfa, 0xce }))
+ return FileTypes.Executable;
+
+ // Mach-O binary (32-bit, reverse byte ordering scheme)
+ if (magic.StartsWith(new byte?[] { 0xce, 0xfa, 0xed, 0xfe }))
+ return FileTypes.Executable;
+
+ // Mach-O binary (64-bit)
+ if (magic.StartsWith(new byte?[] { 0xfe, 0xed, 0xfa, 0xcf }))
+ return FileTypes.Executable;
+
+ // Mach-O binary (64-bit, reverse byte ordering scheme)
+ if (magic.StartsWith(new byte?[] { 0xcf, 0xfa, 0xed, 0xfe }))
+ return FileTypes.Executable;
+
+ // Prefrred Executable File Format
+ if (magic.StartsWith(new byte?[] { 0x4a, 0x6f, 0x79, 0x21, 0x70, 0x65, 0x66, 0x66 }))
+ return FileTypes.Executable;
+ */
+
+ #endregion
+
+ #region GCF
+
+ if (magic.StartsWith(new byte?[] { 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00 }))
+ return SupportedFileType.GCF;
+
+ #endregion
+
+ #region GZIP
+
+ if (magic.StartsWith(new byte?[] { 0x1f, 0x8b }))
+ return SupportedFileType.GZIP;
+
+ #endregion
+
+ #region IniFile
+
+ // No magic checks for IniFile
+
+ #endregion
+
+ #region InstallShieldArchiveV3
+
+ if (magic.StartsWith(new byte?[] { 0x13, 0x5D, 0x65, 0x8C }))
+ return SupportedFileType.InstallShieldArchiveV3;
+
+ #endregion
+
+ #region InstallShieldCAB
+
+ if (magic.StartsWith(new byte?[] { 0x49, 0x53, 0x63 }))
+ return SupportedFileType.InstallShieldCAB;
+
+ #endregion
+
+ #region LDSCRYPT
+
+ if (magic.StartsWith(new byte?[] { 0x4C, 0x44, 0x53, 0x43, 0x52, 0x59, 0x50, 0x54 }))
+ return SupportedFileType.LDSCRYPT;
+
+ #endregion
+
+ #region MicrosoftCAB
+
+ if (magic.StartsWith(new byte?[] { 0x4d, 0x53, 0x43, 0x46 }))
+ return SupportedFileType.MicrosoftCAB;
+
+ #endregion
+
+ #region MicrosoftLZ
+
+ if (magic.StartsWith(new byte?[] { 0x53, 0x5a, 0x44, 0x44, 0x88, 0xf0, 0x27, 0x33 }))
+ return SupportedFileType.MicrosoftLZ;
+
+ #endregion
+
+ #region MPQ
+
+ if (magic.StartsWith(new byte?[] { 0x4d, 0x50, 0x51, 0x1a }))
+ return SupportedFileType.MPQ;
+
+ if (magic.StartsWith(new byte?[] { 0x4d, 0x50, 0x51, 0x1b }))
+ return SupportedFileType.MPQ;
+
+ #endregion
+
+ #region N3DS
+
+ // No magic checks for N3DS
+
+ #endregion
+
+ #region NCF
+
+ if (magic.StartsWith(new byte?[] { 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }))
+ return SupportedFileType.NCF;
+
+ #endregion
+
+ #region Nitro
+
+ // No magic checks for Nitro
+
+ #endregion
+
+ #region PAK
+
+ if (magic.StartsWith(new byte?[] { 0x50, 0x41, 0x43, 0x4B }))
+ return SupportedFileType.PAK;
+
+ #endregion
+
+ #region PFF
+
+ // Version 2
+ if (magic.StartsWith(new byte?[] { 0x14, 0x00, 0x00, 0x00, 0x50, 0x46, 0x46, 0x32 }))
+ return SupportedFileType.PFF;
+
+ // Version 3
+ if (magic.StartsWith(new byte?[] { 0x14, 0x00, 0x00, 0x00, 0x50, 0x46, 0x46, 0x33 }))
+ return SupportedFileType.PFF;
+
+ // Version 4
+ if (magic.StartsWith(new byte?[] { 0x14, 0x00, 0x00, 0x00, 0x50, 0x46, 0x46, 0x34 }))
+ return SupportedFileType.PFF;
+
+ #endregion
+
+ #region PKZIP
+
+ // PKZIP (Unknown)
+ if (magic.StartsWith(new byte?[] { 0x50, 0x4b, 0x00, 0x00 }))
+ return SupportedFileType.PKZIP;
+
+ // PKZIP
+ if (magic.StartsWith(new byte?[] { 0x50, 0x4b, 0x03, 0x04 }))
+ return SupportedFileType.PKZIP;
+
+ // PKZIP (Empty Archive)
+ if (magic.StartsWith(new byte?[] { 0x50, 0x4b, 0x05, 0x06 }))
+ return SupportedFileType.PKZIP;
+
+ // PKZIP (Spanned Archive)
+ if (magic.StartsWith(new byte?[] { 0x50, 0x4b, 0x07, 0x08 }))
+ return SupportedFileType.PKZIP;
+
+ #endregion
+
+ #region PLJ
+
+ // https://www.iana.org/assignments/media-types/audio/vnd.everad.plj
+ if (magic.StartsWith(new byte?[] { 0xFF, 0x9D, 0x53, 0x4B }))
+ return SupportedFileType.PLJ;
+
+ #endregion
+
+ #region Quantum
+
+ if (magic.StartsWith(new byte?[] { 0x44, 0x53 }))
+ return SupportedFileType.Quantum;
+
+ #endregion
+
+ #region RAR
+
+ // RAR archive version 1.50 onwards
+ if (magic.StartsWith(new byte?[] { 0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00 }))
+ return SupportedFileType.RAR;
+
+ // RAR archive version 5.0 onwards
+ if (magic.StartsWith(new byte?[] { 0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00 }))
+ return SupportedFileType.RAR;
+
+ #endregion
+
+ #region RealArcade
+
+ // RASGI2.0
+ // Found in the ".rgs files in IA item "Nova_RealArcadeCD_USA".
+ if (magic.StartsWith(new byte?[] { 0x52, 0x41, 0x53, 0x47, 0x49, 0x32, 0x2E, 0x30 }))
+ return SupportedFileType.RealArcadeInstaller;
+
+ // XZip2.0
+ // Found in the ".mez" files in IA item "Nova_RealArcadeCD_USA".
+ if (magic.StartsWith(new byte?[] { 0x58, 0x5A, 0x69, 0x70, 0x32, 0x2E, 0x30 }))
+ return SupportedFileType.RealArcadeMezzanine;
+
+ #endregion
+
+ #region SevenZip
+
+ if (magic.StartsWith(new byte?[] { 0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c }))
+ return SupportedFileType.SevenZip;
+
+ #endregion
+
+ #region SFFS
+
+ // Found in Redump entry 81756, confirmed to be "StarForce Filesystem" by PiD.
+ if (magic.StartsWith(new byte?[] { 0x53, 0x46, 0x46, 0x53 }))
+ return SupportedFileType.SFFS;
+
+ #endregion
+
+ #region SGA
+
+ if (magic.StartsWith(new byte?[] { 0x5F, 0x41, 0x52, 0x43, 0x48, 0x49, 0x56, 0x45 }))
+ return SupportedFileType.SGA;
+
+ #endregion
+
+ #region TapeArchive
+
+ if (magic.StartsWith(new byte?[] { 0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30 }))
+ return SupportedFileType.TapeArchive;
+
+ if (magic.StartsWith(new byte?[] { 0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00 }))
+ return SupportedFileType.TapeArchive;
+
+ #endregion
+
+ #region Textfile
+
+ // Not all textfiles can be determined through magic number
+
+ // HTML
+ if (magic.StartsWith(new byte?[] { 0x3c, 0x68, 0x74, 0x6d, 0x6c }))
+ return SupportedFileType.Textfile;
+
+ // HTML and XML
+ if (magic.StartsWith(new byte?[] { 0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x45 }))
+ return SupportedFileType.Textfile;
+
+ // InstallShield Compiled Rules
+ if (magic.StartsWith(new byte?[] { 0x61, 0x4C, 0x75, 0x5A }))
+ return SupportedFileType.Textfile;
+
+ // Microsoft Office File (old)
+ if (magic.StartsWith(new byte?[] { 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1 }))
+ return SupportedFileType.Textfile;
+
+ // Rich Text File
+ if (magic.StartsWith(new byte?[] { 0x7b, 0x5c, 0x72, 0x74, 0x66, 0x31 }))
+ return SupportedFileType.Textfile;
+
+ // Windows Help File
+ if (magic.StartsWith(new byte?[] { 0x3F, 0x5F, 0x03, 0x00 }))
+ return SupportedFileType.Textfile;
+
+ // XML
+ // "
+ /// Get the supported file type for an extension
+ ///
+ /// This is less accurate than a magic string match
+ public static SupportedFileType GetFileType(string extension)
+ {
+ // If we have an invalid extension
+ if (string.IsNullOrEmpty(extension))
+ return SupportedFileType.UNKNOWN;
+
+ // Normalize the extension
+ extension = extension.TrimStart('.').Trim();
+
+ #region AACSMediaKeyBlock
+
+ // Shares an extension with INF setup information so it can't be used accurately
+ // Blu-ray
+ // if (extension.Equals("inf", StringComparison.OrdinalIgnoreCase))
+ // return SupportedFileType.AACSMediaKeyBlock;
+
+ // HD-DVD
+ if (extension.Equals("aacs", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.AACSMediaKeyBlock;
+
+ #endregion
+
+ #region BDPlusSVM
+
+ if (extension.Equals("svm", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.BDPlusSVM;
+
+ #endregion
+
+ #region BFPK
+
+ // No extensions registered for BFPK
+
+ #endregion
+
+ #region BSP
+
+ // Shares an extension with VBSP so it can't be used accurately
+ // if (extension.Equals("bsp", StringComparison.OrdinalIgnoreCase))
+ // return SupportedFileType.BSP;
+
+ #endregion
+
+ #region BZip2
+
+ if (extension.Equals("bz2", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.BZip2;
+
+ #endregion
+
+ #region CFB
+
+ // Installer package
+ if (extension.Equals("msi", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.CFB;
+
+ // Merge module
+ else if (extension.Equals("msm", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.CFB;
+
+ // Patch Package
+ else if (extension.Equals("msp", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.CFB;
+
+ // Transform
+ else if (extension.Equals("mst", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.CFB;
+
+ // Patch Creation Properties
+ else if (extension.Equals("pcp", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.CFB;
+
+ #endregion
+
+ #region CIA
+
+ if (extension.Equals("cia", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.CIA;
+
+ #endregion
+
+ #region Executable
+
+ // DOS MZ executable file format (and descendants)
+ if (extension.Equals("exe", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Executable;
+
+ // DOS MZ library file format (and descendants)
+ if (extension.Equals("dll", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Executable;
+
+ #endregion
+
+ #region GCF
+
+ if (extension.Equals("gcf", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.GCF;
+
+ #endregion
+
+ #region GZIP
+
+ if (extension.Equals("gz", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.GZIP;
+
+ #endregion
+
+ #region IniFile
+
+ if (extension.Equals("ini", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.IniFile;
+
+ #endregion
+
+ #region InstallShieldArchiveV3
+
+ if (extension.Equals("z", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.InstallShieldArchiveV3;
+
+ #endregion
+
+ #region InstallShieldCAB
+
+ // No extensions registered for InstallShieldCAB
+ // Both InstallShieldCAB and MicrosoftCAB share the same extension
+
+ #endregion
+
+ #region MicrosoftCAB
+
+ // No extensions registered for InstallShieldCAB
+ // Both InstallShieldCAB and MicrosoftCAB share the same extension
+
+ #endregion
+
+ #region MPQ
+
+ if (extension.Equals("mpq", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.MPQ;
+
+ #endregion
+
+ #region N3DS
+
+ // 3DS cart image
+ if (extension.Equals("3ds", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.N3DS;
+
+ // CIA package -- Not currently supported
+ // else if (extension.Equals("cia", StringComparison.OrdinalIgnoreCase))
+ // return SupportedFileType.N3DS;
+
+ #endregion
+
+ #region NCF
+
+ if (extension.Equals("ncf", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.NCF;
+
+ #endregion
+
+ #region Nitro
+
+ // DS cart image
+ if (extension.Equals("nds", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Nitro;
+
+ // DS development cart image
+ else if (extension.Equals("srl", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Nitro;
+
+ // DSi cart image
+ else if (extension.Equals("dsi", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Nitro;
+
+ // iQue DS cart image
+ else if (extension.Equals("ids", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Nitro;
+
+ #endregion
+
+ #region PAK
+
+ // No extensions registered for PAK
+ // Both PAK and Quantum share one extension
+ // if (extension.Equals("pak", StringComparison.OrdinalIgnoreCase))
+ // return SupportedFileType.PAK;
+
+ #endregion
+
+ #region PFF
+
+ if (extension.Equals("pff", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PFF;
+
+ #endregion
+
+ #region PKZIP
+
+ // PKZIP
+ if (extension.Equals("zip", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // Android package
+ if (extension.Equals("apk", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // Java archive
+ if (extension.Equals("jar", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // Google Earth saved working session file
+ if (extension.Equals("kmz", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // KWord document
+ if (extension.Equals("kwd", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // Microsoft Office Open XML Format (OOXML) Document
+ if (extension.Equals("docx", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // Microsoft Office Open XML Format (OOXML) Presentation
+ if (extension.Equals("pptx", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // Microsoft Office Open XML Format (OOXML) Spreadsheet
+ if (extension.Equals("xlsx", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // OpenDocument text document
+ if (extension.Equals("odt", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // OpenDocument presentation
+ if (extension.Equals("odp", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // OpenDocument text document template
+ if (extension.Equals("ott", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // Microsoft Open XML paper specification file
+ if (extension.Equals("oxps", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // OpenOffice spreadsheet
+ if (extension.Equals("sxc", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // OpenOffice drawing
+ if (extension.Equals("sxd", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // OpenOffice presentation
+ if (extension.Equals("sxi", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // OpenOffice word processing
+ if (extension.Equals("sxw", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // StarOffice spreadsheet
+ if (extension.Equals("sxc", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // Windows Media compressed skin file
+ if (extension.Equals("wmz", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // Mozilla Browser Archive
+ if (extension.Equals("xpi", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // XML paper specification file
+ if (extension.Equals("xps", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ // eXact Packager Models
+ if (extension.Equals("xpt", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PKZIP;
+
+ #endregion
+
+ #region PLJ
+
+ // https://www.iana.org/assignments/media-types/audio/vnd.everad.plj
+ if (extension.Equals("plj", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.PLJ;
+
+ #endregion
+
+ #region Quantum
+
+ if (extension.Equals("q", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Quantum;
+
+ // Both PAK and Quantum share one extension
+ // if (extension.Equals("pak", StringComparison.OrdinalIgnoreCase))
+ // return SupportedFileType.Quantum;
+
+ #endregion
+
+ #region RAR
+
+ if (extension.Equals("rar", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.RAR;
+
+ #endregion
+
+ #region SevenZip
+
+ if (extension.Equals("7z", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.SevenZip;
+
+ #endregion
+
+ #region SGA
+
+ if (extension.Equals("sga", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.SGA;
+
+ #endregion
+
+ #region TapeArchive
+
+ if (extension.Equals("tar", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.SevenZip;
+
+ #endregion
+
+ #region Textfile
+
+ // "Description in Zip"
+ if (extension.Equals("diz", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+ // Generic textfile (no header)
+ if (extension.Equals("txt", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+ // HTML
+ if (extension.Equals("htm", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+ if (extension.Equals("html", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+ // InstallShield Script
+ if (extension.Equals("ins", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+ // Microsoft Office File (old)
+ if (extension.Equals("doc", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+ // Property list
+ if (extension.Equals("plist", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+ // Rich Text File
+ if (extension.Equals("rtf", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+ // Setup information
+ if (extension.Equals("inf", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+ // Windows Help File
+ if (extension.Equals("hlp", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+ // WZC
+ if (extension.Equals("wzc", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+ // XML
+ if (extension.Equals("xml", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.Textfile;
+
+
+
+ #endregion
+
+ #region VBSP
+
+ // Shares an extension with BSP so it can't be used accurately
+ // if (extension.Equals("bsp", StringComparison.OrdinalIgnoreCase))
+ // return SupportedFileType.VBSP;
+
+ #endregion
+
+ #region VPK
+
+ // Common extension so this cannot be used accurately
+ // if (extension.Equals("vpk", StringComparison.OrdinalIgnoreCase))
+ // return SupportedFileType.VPK;
+
+ #endregion
+
+ #region WAD
+
+ // Common extension so this cannot be used accurately
+ // if (extension.Equals("wad", StringComparison.OrdinalIgnoreCase))
+ // return SupportedFileType.WAD;
+
+ #endregion
+
+ #region XZ
+
+ if (extension.Equals("xz", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.XZ;
+
+ #endregion
+
+ #region XZP
+
+ if (extension.Equals("xzp", StringComparison.OrdinalIgnoreCase))
+ return SupportedFileType.XZP;
+
+ #endregion
+
+ // We couldn't find a supported match
+ return SupportedFileType.UNKNOWN;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Test/Options.cs b/Test/Options.cs
new file mode 100644
index 0000000..1678ca3
--- /dev/null
+++ b/Test/Options.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+
+namespace Test
+{
+ ///
+ /// Set of options for the test executable
+ ///
+ internal sealed class Options
+ {
+ #region Properties
+
+ ///
+ /// Enable debug output for relevant operations
+ ///
+ public bool Debug { get; private set; } = false;
+
+ ///
+ /// Set of input paths to use for operations
+ ///
+ public List InputPaths { get; private set; } = [];
+
+#if NETCOREAPP3_1_OR_GREATER
+ ///
+ /// Enable JSON output
+ ///
+ public bool Json { get; private set; } = false;
+#endif
+
+ #endregion
+
+ ///
+ /// Parse commandline arguments into an Options object
+ ///
+ public static Options? ParseOptions(string[] args)
+ {
+ // If we have invalid arguments
+ if (args == null || args.Length == 0)
+ return null;
+
+ // Create an Options object
+ var options = new Options();
+
+ // Parse the features
+ int index = 0;
+ for (; index < args.Length; index++)
+ {
+ string arg = args[index];
+ bool featureFound = false;
+ switch (arg)
+ {
+ case "-?":
+ case "-h":
+ case "--help":
+ return null;
+
+ default:
+ break;
+ }
+
+ // If the flag wasn't a feature
+ if (!featureFound)
+ break;
+ }
+
+ // Parse the options and paths
+ for (; index < args.Length; index++)
+ {
+ string arg = args[index];
+ switch (arg)
+ {
+ case "-d":
+ case "--debug":
+ options.Debug = true;
+ break;
+
+ case "-j":
+ case "--json":
+#if NET6_0_OR_GREATER
+ options.Json = true;
+#else
+ Console.WriteLine("JSON output not available in .NET Framework");
+#endif
+ break;
+
+ default:
+ options.InputPaths.Add(arg);
+ break;
+ }
+ }
+
+ // Validate we have any input paths to work on
+ if (options.InputPaths.Count == 0)
+ {
+ Console.WriteLine("At least one path is required!");
+ return null;
+ }
+
+ return options;
+ }
+
+ ///
+ /// Display help text
+ ///
+ public static void DisplayHelp()
+ {
+ Console.WriteLine("SabreTools.Printing Test Program");
+ Console.WriteLine();
+ Console.WriteLine("test.exe file|directory ...");
+ Console.WriteLine();
+ Console.WriteLine("Options:");
+ Console.WriteLine("-?, -h, --help Display this help text and quit");
+ Console.WriteLine("-d, --debug Enable debug mode");
+#if NET6_0_OR_GREATER
+ Console.WriteLine("-j, --json Print executable info as JSON");
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/Test/Program.cs b/Test/Program.cs
new file mode 100644
index 0000000..d479d94
--- /dev/null
+++ b/Test/Program.cs
@@ -0,0 +1,141 @@
+using System;
+using System.IO;
+using SabreTools.IO.Extensions;
+using SabreTools.Printing;
+
+namespace Test
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ // Get the options from the arguments
+ var options = Options.ParseOptions(args);
+
+ // If we have an invalid state
+ if (options == null)
+ {
+ Options.DisplayHelp();
+ Console.WriteLine("Press enter to close the program...");
+ Console.ReadLine();
+ return;
+ }
+
+ // Loop through the input paths
+ foreach (string inputPath in args)
+ {
+#if NETFRAMEWORK
+ PrintPathInfo(inputPath, false, options.Debug);
+#else
+ PrintPathInfo(inputPath, options.Json, options.Debug);
+#endif
+ }
+ }
+
+ ///
+ /// Wrapper to print information for a single path
+ ///
+ /// File or directory path
+ /// Enable JSON output, if supported
+ /// Enable debug output
+ private static void PrintPathInfo(string path, bool json, bool debug)
+ {
+ Console.WriteLine($"Checking possible path: {path}");
+
+ // Check if the file or directory exists
+ if (File.Exists(path))
+ {
+ PrintFileInfo(path, json, debug);
+ }
+ else if (Directory.Exists(path))
+ {
+#if NET20 || NET35
+ foreach (string file in Directory.GetFiles(path, "*", SearchOption.AllDirectories))
+#else
+ foreach (string file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
+#endif
+ {
+ PrintFileInfo(file, json, debug);
+ }
+ }
+ else
+ {
+ Console.WriteLine($"{path} does not exist, skipping...");
+ }
+ }
+
+ ///
+ /// Print information for a single file, if possible
+ ///
+ private static void PrintFileInfo(string file, bool json, bool debug)
+ {
+ Console.WriteLine($"Attempting to print info for {file}");
+
+ try
+ {
+ using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ // Read the first 8 bytes
+ byte[]? magic = stream.ReadBytes(8);
+ stream.Seek(0, SeekOrigin.Begin);
+
+ // Get the file type
+ SupportedFileType ft = FileTypes.GetFileType(magic ?? []);
+ if (ft == SupportedFileType.UNKNOWN)
+ {
+ string extension = Path.GetExtension(file).TrimStart('.');
+ ft = FileTypes.GetFileType(extension);
+ }
+
+ // Print out the file format
+ Console.WriteLine($"File format found: {ft}");
+
+ // Setup the wrapper to print
+ var wrapper = WrapperFactory.CreateWrapper(ft, stream);
+
+ // If we don't have a wrapper
+ if (wrapper == null)
+ {
+ Console.WriteLine($"Either {ft} is not supported or something went wrong during parsing!");
+ Console.WriteLine();
+ return;
+ }
+
+ // Get the base info output name
+ string filenameBase = $"info-{DateTime.Now:yyyy-MM-dd_HHmmss.ffff}";
+
+#if NET6_0_OR_GREATER
+ // If we have the JSON flag
+ if (json)
+ {
+ // Create the output data
+ string serializedData = wrapper.ExportJSON();
+ Console.WriteLine(serializedData);
+
+ // Write the output data
+ using var jsw = new StreamWriter(File.OpenWrite($"{filenameBase}.json"));
+ jsw.WriteLine(serializedData);
+ }
+#endif
+
+ // Create the output data
+ var builder = wrapper.ExportStringBuilder();
+ if (builder == null)
+ {
+ Console.WriteLine("No item information could be generated");
+ return;
+ }
+
+ // Write the output data
+ Console.WriteLine(builder);
+ using var sw = new StreamWriter(File.OpenWrite($"{filenameBase}.txt"));
+ sw.WriteLine(builder.ToString());
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(debug ? ex : "[Exception opening file, please try again]");
+ Console.WriteLine();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Test/SupportedFileType.cs b/Test/SupportedFileType.cs
new file mode 100644
index 0000000..038e09a
--- /dev/null
+++ b/Test/SupportedFileType.cs
@@ -0,0 +1,203 @@
+namespace Test
+{
+ ///
+ /// Subset of file types that are supported by the library
+ ///
+ internal enum SupportedFileType
+ {
+ ///
+ /// Unknown or unsupported
+ ///
+ UNKNOWN,
+
+ ///
+ /// AACS media key block
+ ///
+ AACSMediaKeyBlock,
+
+ ///
+ /// BD+ SVM
+ ///
+ BDPlusSVM,
+
+ ///
+ /// BFPK custom archive
+ ///
+ BFPK,
+
+ ///
+ /// Half-Life Level
+ ///
+ BSP,
+
+ ///
+ /// bzip2 archive
+ ///
+ BZip2,
+
+ ///
+ /// Compound File Binary
+ ///
+ CFB,
+
+ ///
+ /// CTR Importable Archive
+ ///
+ CIA,
+
+ ///
+ /// Executable or library
+ ///
+ Executable,
+
+ ///
+ /// Half-Life Game Cache File
+ ///
+ GCF,
+
+ ///
+ /// gzip archive
+ ///
+ GZIP,
+
+ ///
+ /// Key-value pair INI file
+ ///
+ IniFile,
+
+ ///
+ /// InstallShield archive v3
+ ///
+ InstallShieldArchiveV3,
+
+ ///
+ /// InstallShield cabinet file
+ ///
+ InstallShieldCAB,
+
+ ///
+ /// Link Data Security encrypted file
+ ///
+ LDSCRYPT,
+
+ ///
+ /// Microsoft cabinet file
+ ///
+ MicrosoftCAB,
+
+ ///
+ /// Microsoft LZ-compressed file
+ ///
+ MicrosoftLZ,
+
+ ///
+ /// MPQ game data archive
+ ///
+ MPQ,
+
+ ///
+ /// Nintendo 3DS cart image
+ ///
+ N3DS,
+
+ ///
+ /// Half-Life No Cache File
+ ///
+ NCF,
+
+ ///
+ /// Nintendo DS/DSi cart image
+ ///
+ Nitro,
+
+ ///
+ /// Half-Life Package File
+ ///
+ PAK,
+
+ ///
+ /// NovaLogic Game Archive Format
+ ///
+ PFF,
+
+ ///
+ /// PKWARE ZIP archive and derivatives
+ ///
+ PKZIP,
+
+ ///
+ /// PlayJ audio file
+ ///
+ PLJ,
+
+ ///
+ /// Quantum archive
+ ///
+ Quantum,
+
+ ///
+ /// RAR archive
+ ///
+ RAR,
+
+ ///
+ /// RealArcade Installer
+ ///
+ RealArcadeInstaller,
+
+ ///
+ /// RealArcade Mezzanine
+ ///
+ RealArcadeMezzanine,
+
+ ///
+ /// 7-zip archive
+ ///
+ SevenZip,
+
+ ///
+ /// StarForce FileSystem file
+ ///
+ SFFS,
+
+ ///
+ /// SGA
+ ///
+ SGA,
+
+ ///
+ /// Tape archive
+ ///
+ TapeArchive,
+
+ ///
+ /// Various generic textfile formats
+ ///
+ Textfile,
+
+ ///
+ /// Half-Life 2 Level
+ ///
+ VBSP,
+
+ ///
+ /// Valve Package File
+ ///
+ VPK,
+
+ ///
+ /// Half-Life Texture Package File
+ ///
+ WAD,
+
+ ///
+ /// xz archive
+ ///
+ XZ,
+
+ ///
+ /// Xbox Package File
+ ///
+ XZP,
+ }
+}
\ No newline at end of file
diff --git a/Test/Test.csproj b/Test/Test.csproj
new file mode 100644
index 0000000..fb85686
--- /dev/null
+++ b/Test/Test.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0
+ win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64
+ Exe
+ false
+ false
+ latest
+ enable
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Test/WrapperFactory.cs b/Test/WrapperFactory.cs
new file mode 100644
index 0000000..7b4855e
--- /dev/null
+++ b/Test/WrapperFactory.cs
@@ -0,0 +1,116 @@
+using System.IO;
+using SabreTools.IO.Extensions;
+using SabreTools.Matching;
+using SabreTools.Serialization.Interfaces;
+using SabreTools.Serialization.Wrappers;
+
+namespace Test
+{
+ internal static class WrapperFactory
+ {
+ ///
+ /// Create an instance of a wrapper based on file type
+ ///
+ public static IWrapper? CreateWrapper(SupportedFileType fileType, Stream? data)
+ {
+ switch (fileType)
+ {
+ case SupportedFileType.AACSMediaKeyBlock: return AACSMediaKeyBlock.Create(data);
+ case SupportedFileType.BDPlusSVM: return BDPlusSVM.Create(data);
+ case SupportedFileType.BFPK: return BFPK.Create(data);
+ case SupportedFileType.BSP: return BSP.Create(data);
+ //case SupportedFileType.BZip2: return BZip2.Create(data);
+ case SupportedFileType.CFB: return CFB.Create(data);
+ case SupportedFileType.CIA: return CIA.Create(data);
+ case SupportedFileType.Executable: return CreateExecutableWrapper(data);
+ case SupportedFileType.GCF: return GCF.Create(data);
+ //case SupportedFileType.GZIP: return GZIP.Create(data);
+ //case SupportedFileType.IniFile: return IniFile.Create(data);
+ //case SupportedFileType.InstallShieldArchiveV3: return InstallShieldArchiveV3.Create(data);
+ case SupportedFileType.InstallShieldCAB: return InstallShieldCabinet.Create(data);
+ //case SupportedFileType.LDSCRYPT: return LDSCRYPT.Create(data);
+ case SupportedFileType.MicrosoftCAB: return MicrosoftCabinet.Create(data);
+ //case SupportedFileType.MicrosoftLZ: return MicrosoftLZ.Create(data);
+ case SupportedFileType.MPQ: return MoPaQ.Create(data);
+ case SupportedFileType.N3DS: return N3DS.Create(data);
+ case SupportedFileType.NCF: return NCF.Create(data);
+ case SupportedFileType.Nitro: return Nitro.Create(data);
+ case SupportedFileType.PAK: return PAK.Create(data);
+ case SupportedFileType.PFF: return PFF.Create(data);
+ //case SupportedFileType.PIC: return PIC.Create(data);
+ //case SupportedFileType.PKZIP: return PKZIP.Create(data);
+ case SupportedFileType.PLJ: return PlayJAudioFile.Create(data);
+ //case SupportedFileType.PLJPlaylist: return PlayJPlaylist.Create(data);
+ case SupportedFileType.Quantum: return Quantum.Create(data);
+ //case SupportedFileType.RAR: return RAR.Create(data);
+ //case SupportedFileType.SevenZip: return SevenZip.Create(data);
+ //case SupportedFileType.SFFS: return SFFS.Create(data);
+ case SupportedFileType.SGA: return SGA.Create(data);
+ //case SupportedFileType.TapeArchive: return TapeArchive.Create(data);
+ //case SupportedFileType.Textfile: return Textfile.Create(data);
+ case SupportedFileType.VBSP: return VBSP.Create(data);
+ case SupportedFileType.VPK: return VPK.Create(data);
+ case SupportedFileType.WAD: return WAD.Create(data);
+ //case SupportedFileType.XZ: return XZ.Create(data);
+ case SupportedFileType.XZP: return XZP.Create(data);
+ default: return null;
+ }
+ }
+
+ ///
+ /// Create an instance of a wrapper based on the executable type
+ ///
+ /// Stream data to parse
+ /// IWrapper representing the executable, null on error
+ public static IWrapper? CreateExecutableWrapper(Stream? stream)
+ {
+ // If we have no stream
+ if (stream == null)
+ return null;
+
+ // Try to get an MS-DOS wrapper first
+ var wrapper = MSDOS.Create(stream);
+ if (wrapper == null || !(wrapper is MSDOS msdos))
+ return null;
+
+ // Check for a valid new executable address
+ if (msdos.Model.Header?.NewExeHeaderAddr == null || msdos.Model.Header.NewExeHeaderAddr >= stream.Length)
+ return wrapper;
+
+ // Try to read the executable info
+ stream.Seek(msdos.Model.Header.NewExeHeaderAddr, SeekOrigin.Begin);
+ var magic = stream.ReadBytes(4);
+
+ // If we didn't get valid data at the offset
+ if (magic == null)
+ {
+ return wrapper;
+ }
+
+ // New Executable
+ else if (magic.StartsWith(SabreTools.Models.NewExecutable.Constants.SignatureBytes))
+ {
+ stream.Seek(0, SeekOrigin.Begin);
+ return NewExecutable.Create(stream);
+ }
+
+ // Linear Executable
+ else if (magic.StartsWith(SabreTools.Models.LinearExecutable.Constants.LESignatureBytes)
+ || magic.StartsWith(SabreTools.Models.LinearExecutable.Constants.LXSignatureBytes))
+ {
+ stream.Seek(0, SeekOrigin.Begin);
+ return LinearExecutable.Create(stream);
+ }
+
+ // Portable Executable
+ else if (magic.StartsWith(SabreTools.Models.PortableExecutable.Constants.SignatureBytes))
+ {
+ stream.Seek(0, SeekOrigin.Begin);
+ return PortableExecutable.Create(stream);
+ }
+
+ // Everything else fails
+ return null;
+ }
+ }
+}