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 |