diff --git a/BurnOutSharp.Compression/LZ.cs b/BurnOutSharp.Compression/LZ.cs index fe0dfd2f..8ab40db9 100644 --- a/BurnOutSharp.Compression/LZ.cs +++ b/BurnOutSharp.Compression/LZ.cs @@ -85,6 +85,44 @@ namespace BurnOutSharp.Compression return decompressed; } + /// + /// Reconstructs the full filename of the compressed file + /// + public static string GetExpandedName(string input, out LZERROR error) + { + // Try to open the file as a compressed stream + var fileStream = File.Open(input, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + var state = new LZ().Open(fileStream, out error); + if (state?.Window == null) + return null; + + // Get the extension for modification + string inputExtension = Path.GetExtension(input).TrimStart('.'); + + // If we have no extension + if (string.IsNullOrWhiteSpace(inputExtension)) + return Path.GetFileNameWithoutExtension(input); + + // If we have an extension of length 1 + if (inputExtension.Length == 1) + { + if (inputExtension == "_") + return $"{Path.GetFileNameWithoutExtension(input)}.{char.ToLower(state.LastChar)}"; + else + return Path.GetFileNameWithoutExtension(input); + } + + // If we have an extension that doesn't end in an underscore + if (!inputExtension.EndsWith("_")) + return Path.GetFileNameWithoutExtension(input); + + // Build the new filename + bool isLowerCase = char.IsUpper(input[0]); + char replacementChar = isLowerCase ? char.ToLower(state.LastChar) : char.ToUpper(state.LastChar); + string outputExtension = inputExtension.Substring(0, inputExtension.Length - 1) + replacementChar; + return $"{Path.GetFileNameWithoutExtension(input)}.{outputExtension}"; + } + #endregion #region State Management @@ -175,48 +213,6 @@ namespace BurnOutSharp.Compression #endregion - #region File Name Handling - - /// - /// Reconstructs the full filename of the compressed file - /// - public string GetExpandedName(string input, out LZERROR error) - { - // Try to open the file as a compressed stream - var fileStream = File.Open(input, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - var state = Open(fileStream, out error); - if (state?.Window == null) - return null; - - // Get the extension for modification - string inputExtension = Path.GetExtension(input).TrimStart('.'); - - // If we have no extension - if (string.IsNullOrWhiteSpace(inputExtension)) - return Path.GetFileNameWithoutExtension(input); - - // If we have an extension of length 1 - if (inputExtension.Length == 1) - { - if (inputExtension == "_") - return $"{Path.GetFileNameWithoutExtension(input)}.{char.ToLower(state.LastChar)}"; - else - return Path.GetFileNameWithoutExtension(input); - } - - // If we have an extension that doesn't end in an underscore - if (!inputExtension.EndsWith("_")) - return Path.GetFileNameWithoutExtension(input); - - // Build the new filename - bool isLowerCase = char.IsUpper(input[0]); - char replacementChar = isLowerCase ? char.ToLower(state.LastChar) : char.ToUpper(state.LastChar); - string outputExtension = inputExtension.Substring(0, inputExtension.Length - 1) + replacementChar; - return $"{Path.GetFileNameWithoutExtension(input)}.{outputExtension}"; - } - - #endregion - #region Stream Functionality /// diff --git a/BurnOutSharp/Enums.cs b/BurnOutSharp/Enums.cs index 313542f9..f7551075 100644 --- a/BurnOutSharp/Enums.cs +++ b/BurnOutSharp/Enums.cs @@ -60,6 +60,11 @@ /// MicrosoftCAB, + /// + /// Microsoft LZ-compressed file + /// + MicrosoftLZ, + /// /// MPQ game data archive /// diff --git a/BurnOutSharp/FileType/MicrosoftLZ.cs b/BurnOutSharp/FileType/MicrosoftLZ.cs new file mode 100644 index 00000000..bb7b2dbc --- /dev/null +++ b/BurnOutSharp/FileType/MicrosoftLZ.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using BurnOutSharp.Compression; +using BurnOutSharp.Interfaces; +using static BurnOutSharp.Utilities.Dictionary; + +namespace BurnOutSharp.FileType +{ + /// + /// Microsoft LZ-compressed Files (LZ32) + /// + /// This is treated like an archive type due to the packing style + public class MicrosoftLZ : IScannable + { + /// + public ConcurrentDictionary> Scan(Scanner scanner, string file) + { + if (!File.Exists(file)) + return null; + + using (var fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + return Scan(scanner, fs, file); + } + } + + /// + public ConcurrentDictionary> Scan(Scanner scanner, Stream stream, string file) + { + // If the LZ file itself fails + try + { + string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempPath); + + byte[] data = LZ.Decompress(stream); + + // Create the temp filename + string tempFile = "temp.bin"; + if (!string.IsNullOrEmpty(file)) + { + string expandedFilePath = LZ.GetExpandedName(file, out _); + tempFile = Path.GetFileName(expandedFilePath).TrimEnd('\0'); + if (tempFile.EndsWith(".ex")) + tempFile += "e"; + else if (tempFile.EndsWith(".dl")) + tempFile += "l"; + } + + tempFile = Path.Combine(tempPath, tempFile); + + // Write the file data to a temp file + using (Stream tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) + { + tempStream.Write(data, 0, data.Length); + } + + // Collect and format all found protections + var protections = scanner.GetProtections(tempPath); + + // If temp directory cleanup fails + try + { + Directory.Delete(tempPath, true); + } + catch (Exception ex) + { + if (scanner.IncludeDebug) Console.WriteLine(ex); + } + + // Remove temporary path references + StripFromKeys(protections, tempPath); + + return protections; + } + catch (Exception ex) + { + if (scanner.IncludeDebug) Console.WriteLine(ex); + } + + return null; + } + } +} diff --git a/BurnOutSharp/Scanner.cs b/BurnOutSharp/Scanner.cs index eb4709ad..5489d9a6 100644 --- a/BurnOutSharp/Scanner.cs +++ b/BurnOutSharp/Scanner.cs @@ -433,6 +433,14 @@ namespace BurnOutSharp AppendToDictionary(protections, subProtections); } + // Microsoft LZ + if (fileName != null && scannable is MicrosoftLZ) + { + var subProtections = scannable.Scan(this, fileName); + PrependToKeys(subProtections, fileName); + AppendToDictionary(protections, subProtections); + } + // MSI if (fileName != null && scannable is MSI) { diff --git a/BurnOutSharp/Tools/Utilities.cs b/BurnOutSharp/Tools/Utilities.cs index 06e6d767..f53ee454 100644 --- a/BurnOutSharp/Tools/Utilities.cs +++ b/BurnOutSharp/Tools/Utilities.cs @@ -120,6 +120,13 @@ namespace BurnOutSharp.Tools #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 })) @@ -633,6 +640,7 @@ namespace BurnOutSharp.Tools case SupportedFileType.InstallShieldArchiveV3: return new FileType.InstallShieldArchiveV3(); case SupportedFileType.InstallShieldCAB: return new FileType.InstallShieldCAB(); case SupportedFileType.MicrosoftCAB: return new FileType.MicrosoftCAB(); + case SupportedFileType.MicrosoftLZ: return new FileType.MicrosoftLZ(); case SupportedFileType.MPQ: return new FileType.MPQ(); case SupportedFileType.MSI: return new FileType.MSI(); case SupportedFileType.PAK: return new FileType.PAK(); diff --git a/README.md b/README.md index 97b73077..4b81b12f 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ Below is a list of container formats that are supported in some way: | InstallShield CAB | No | Yes | Yes | Via `UnshieldSharp` | | Linear Executable | No | No | No | Skeleton only | | Microsoft cabinet file | Yes | Yes | Yes | Via `WixToolset.Dtf` / ~~`LibMSPackSharp`~~ (Currently disabled) | +| Microsoft LZ-compressed files | No | Yes | Yes | | | MoPaQ game data archive (MPQ) | No | Yes | Yes | Via `StormLibSharp` | | Microsoft installation package (MSI) | No | Yes | Yes | Via `OpenMcdf` | | MS-DOS Executable | Yes | Yes | No | Incomplete |