├── .editorconfig ├── .gitattributes ├── .gitignore ├── AuroraLib.Compression.sln ├── Benchmarks.md ├── Benchmarks ├── Benchmarks.csproj ├── Benchmarks │ ├── TestAlgorithm.cs │ └── TestAllAlgorithms.cs ├── Program.cs └── Test.bmp ├── CompressionTest ├── CompressionAlgorithmTest.cs ├── CompressionUnitTest.csproj ├── Test.bmp └── Test.lz ├── LICENSE ├── README.md ├── icon-ex.png ├── icon.png └── src ├── AuroraLib.Compression-Extended ├── Algorithms │ ├── AKLZ.cs │ ├── AsuraZlb.cs │ ├── CLZ0.cs │ ├── CNS.cs │ ├── CNX2.cs │ ├── COMP.cs │ ├── CXLZ.cs │ ├── ECD.cs │ ├── FCMP.cs │ ├── GCLZ.cs │ ├── HWGZ.cs │ ├── IECP.cs │ ├── LZ00.cs │ ├── LZ01.cs │ ├── LZ02.cs │ ├── LZ4.Frame.cs │ ├── LZ4.FrameDescriptor.cs │ ├── LZ4.cs │ ├── LZ40.cs │ ├── LZ4Legacy.cs │ ├── LZ60.cs │ ├── LZHudson.cs │ ├── LZSega.cs │ ├── LZShrek.cs │ ├── Level5.cs │ ├── MDF0.cs │ ├── RLHudson.cs │ ├── SSZL.cs │ └── ZLB.cs ├── AuroraLib.Compression-Extended.csproj └── Helper.cs └── AuroraLib.Compression ├── Algorithms ├── ALLZ.cs ├── CompressionExtension.cs ├── GZip.cs ├── HUF20.cs ├── LZ10.cs ├── LZ11.cs ├── LZ77.cs ├── LZO.cs ├── LZOn.cs ├── LZSS.cs ├── MIO0.cs ├── PRS.cs ├── RLE30.cs ├── RefPack.cs ├── YAY0.cs ├── YAZ0.cs ├── YAZ1.cs └── ZLib.cs ├── AuroraLib.Compression.csproj ├── Exceptions └── DecompressedSizeException.cs ├── Helper.cs ├── Huffman ├── HuffmanNode.cs └── HuffmanTree.cs ├── IO ├── FlagReader.cs ├── FlagWriter.cs └── LzWindows.cs ├── Interfaces ├── ICompressionAlgorithm.cs ├── ICompressionDecoder.cs ├── ICompressionEncoder.cs ├── IEndianDependentFormat.cs ├── ILzSettings.cs └── IProvidesDecompressedSize.cs ├── LzProperties.cs └── MatchFinder ├── LzMatch.cs ├── LzMatchFinder.cs └── RleMatchFinder.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{sln,csproj}] 11 | end_of_line = crlf 12 | charset = utf-8-bom 13 | 14 | [*.sln] 15 | indent_style = tab 16 | 17 | [*.csproj] 18 | indent_size = 2 19 | 20 | [*.cs] 21 | indent_size = 4 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto eol=lf 3 | 4 | # Source files 5 | *.cs text diff=csharp 6 | *.cshtml text diff=html 7 | *.csx text diff=csharp 8 | 9 | # Project and solution files 10 | *.sln text eol=crlf 11 | *.csproj text eol=crlf 12 | *.vbproj text eol=crlf 13 | *.vcxproj text eol=crlf 14 | *.vcproj text eol=crlf 15 | *.dbproj text eol=crlf 16 | *.fsproj text eol=crlf 17 | *.lsproj text eol=crlf 18 | *.wixproj text eol=crlf 19 | *.modelproj text eol=crlf 20 | *.sqlproj text eol=crlf 21 | *.wwaproj text eol=crlf 22 | *.xproj text eol=crlf 23 | *.props text eol=crlf 24 | *.filters text eol=crlf 25 | *.vcxitems text eol=crlf 26 | -------------------------------------------------------------------------------- /AuroraLib.Compression.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.34024.191 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuroraLib.Compression", "src\AuroraLib.Compression\AuroraLib.Compression.csproj", "{90CFBCD7-EE97-46B1-A4E1-21521BACFC77}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompressionUnitTest", "CompressionTest\CompressionUnitTest.csproj", "{D9D66680-45F4-4DBD-9965-350E98160329}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{AEF632EF-EF3F-4314-B9DB-A70354AF214F}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuroraLib.Compression-Extended", "src\AuroraLib.Compression-Extended\AuroraLib.Compression-Extended.csproj", "{D0CE9878-C8E9-4E5F-9EEC-E20A504083C5}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Optimized|Any CPU = Optimized|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {90CFBCD7-EE97-46B1-A4E1-21521BACFC77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {90CFBCD7-EE97-46B1-A4E1-21521BACFC77}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {90CFBCD7-EE97-46B1-A4E1-21521BACFC77}.Optimized|Any CPU.ActiveCfg = Release|Any CPU 24 | {90CFBCD7-EE97-46B1-A4E1-21521BACFC77}.Optimized|Any CPU.Build.0 = Release|Any CPU 25 | {90CFBCD7-EE97-46B1-A4E1-21521BACFC77}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {90CFBCD7-EE97-46B1-A4E1-21521BACFC77}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {D9D66680-45F4-4DBD-9965-350E98160329}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {D9D66680-45F4-4DBD-9965-350E98160329}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {D9D66680-45F4-4DBD-9965-350E98160329}.Optimized|Any CPU.ActiveCfg = Release|Any CPU 30 | {D9D66680-45F4-4DBD-9965-350E98160329}.Optimized|Any CPU.Build.0 = Release|Any CPU 31 | {D9D66680-45F4-4DBD-9965-350E98160329}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {D9D66680-45F4-4DBD-9965-350E98160329}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {AEF632EF-EF3F-4314-B9DB-A70354AF214F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {AEF632EF-EF3F-4314-B9DB-A70354AF214F}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {AEF632EF-EF3F-4314-B9DB-A70354AF214F}.Optimized|Any CPU.ActiveCfg = Release|Any CPU 36 | {AEF632EF-EF3F-4314-B9DB-A70354AF214F}.Optimized|Any CPU.Build.0 = Release|Any CPU 37 | {AEF632EF-EF3F-4314-B9DB-A70354AF214F}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {AEF632EF-EF3F-4314-B9DB-A70354AF214F}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {D0CE9878-C8E9-4E5F-9EEC-E20A504083C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {D0CE9878-C8E9-4E5F-9EEC-E20A504083C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {D0CE9878-C8E9-4E5F-9EEC-E20A504083C5}.Optimized|Any CPU.ActiveCfg = Optimized|Any CPU 42 | {D0CE9878-C8E9-4E5F-9EEC-E20A504083C5}.Optimized|Any CPU.Build.0 = Optimized|Any CPU 43 | {D0CE9878-C8E9-4E5F-9EEC-E20A504083C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {D0CE9878-C8E9-4E5F-9EEC-E20A504083C5}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | GlobalSection(ExtensibilityGlobals) = postSolution 50 | SolutionGuid = {3E284A3C-F433-424B-A6F4-97DE84B9DCBD} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /Benchmarks.md: -------------------------------------------------------------------------------- 1 | BenchmarkDotNet v0.14.0, Windows 10, AMD Ryzen 7 3800X, 1 CPU, 16 logical and 8 physical cores. NET SDK 8.0.400 2 | 3 | | Method | Algorithm | MB | Mean | Error | StdDev | Gen0 | Allocated | 4 | |----------- |---------- |--- |-------------:|------------:|------------:|--------:|----------:| 5 | | Compress | ALLZ | 1 | 72,736.5 us | 1,434.81 us | 2,147.56 us | - | 7087 B | 6 | | Decompress | ALLZ | 1 | 3,455.3 us | 48.01 us | 42.56 us | - | 141 B | 7 | | Compress | CLZ0 | 1 | 21,654.8 us | 424.41 us | 471.74 us | - | 6856 B | 8 | | Decompress | CLZ0 | 1 | 3,901.3 us | 46.34 us | 43.34 us | - | 213 B | 9 | | Compress | CNS | 1 | 5,601.4 us | 77.30 us | 68.52 us | - | 6805 B | 10 | | Decompress | CNS | 1 | 3,598.0 us | 42.41 us | 39.67 us | - | 197 B | 11 | | Compress | CNX2 | 1 | 19,876.6 us | 348.84 us | 309.24 us | - | 6976 B | 12 | | Decompress | CNX2 | 1 | 3,653.0 us | 43.65 us | 40.83 us | - | 245 B | 13 | | Compress | HUF20 | 1 | 17,689.6 us | 201.15 us | 188.16 us | 31.2500 | 398903 B | 14 | | Decompress | HUF20 | 1 | 9,574.7 us | 185.77 us | 241.56 us | - | 106 B | 15 | | Compress | HWGZ | 1 | 7,394.5 us | 92.70 us | 86.71 us | - | 4182 B | 16 | | Decompress | HWGZ | 1 | 1,174.0 us | 14.27 us | 12.65 us | - | 6105 B | 17 | | Compress | LZ00 | 1 | 23,128.5 us | 434.20 us | 445.90 us | - | 6968 B | 18 | | Decompress | LZ00 | 1 | 4,205.2 us | 50.96 us | 47.66 us | - | 299 B | 19 | | Compress | LZ02 | 1 | 44,243.8 us | 881.28 us | 1,114.54 us | - | 6889 B | 20 | | Decompress | LZ02 | 1 | 2,986.9 us | 34.56 us | 32.33 us | - | 215 B | 21 | | Compress | LZ10 | 1 | 37,801.3 us | 669.52 us | 771.02 us | - | 6864 B | 22 | | Decompress | LZ10 | 1 | 3,828.9 us | 6.12 us | 5.73 us | - | 189 B | 23 | | Compress | LZ11 | 1 | 35,927.7 us | 697.26 us | 618.10 us | - | 6891 B | 24 | | Decompress | LZ11 | 1 | 3,500.1 us | 3.03 us | 2.68 us | - | 189 B | 25 | | Compress | LZ40 | 1 | 20,452.9 us | 319.00 us | 298.39 us | - | 6950 B | 26 | | Decompress | LZ40 | 1 | 3,532.6 us | 6.55 us | 6.13 us | - | 141 B | 27 | | Compress | LZ4Legacy | 1 | 131,799.8 us | 2,470.23 us | 4,578.74 us | - | 7218 B | 28 | | Decompress | LZ4Legacy | 1 | 2,649.3 us | 32.52 us | 30.41 us | - | 141 B | 29 | | Compress | LZO | 1 | 129,463.7 us | 2,415.20 us | 5,832.97 us | - | 7156 B | 30 | | Decompress | LZO | 1 | 2,902.7 us | 39.59 us | 37.03 us | - | 142 B | 31 | | Compress | LZSS | 1 | 21,672.2 us | 391.68 us | 347.22 us | - | 6922 B | 32 | | Decompress | LZSS | 1 | 3,741.9 us | 39.14 us | 36.61 us | - | 213 B | 33 | | Compress | LZShrek | 1 | 39,688.8 us | 792.59 us | 778.43 us | - | 7010 B | 34 | | Decompress | LZShrek | 1 | 2,603.3 us | 29.79 us | 27.86 us | - | 141 B | 35 | | Compress | MIO0 | 1 | 40,157.3 us | 779.19 us | 985.42 us | - | 7103 B | 36 | | Decompress | MIO0 | 1 | 2,284.3 us | 19.84 us | 18.56 us | - | 165 B | 37 | | Compress | PRS | 1 | 34,021.8 us | 580.83 us | 543.30 us | - | 6879 B | 38 | | Decompress | PRS | 1 | 2,131.5 us | 28.23 us | 26.40 us | - | 237 B | 39 | | Compress | RLE30 | 1 | 3,810.5 us | 38.89 us | 36.38 us | - | 91 B | 40 | | Decompress | RLE30 | 1 | 662.6 us | 6.05 us | 5.66 us | - | 65 B | 41 | | Compress | RefPack | 1 | 69,832.0 us | 1,364.97 us | 2,164.98 us | - | 6967 B | 42 | | Decompress | RefPack | 1 | 1,254.7 us | 14.06 us | 13.15 us | - | 139 B | 43 | | Compress | Yay0 | 1 | 23,130.2 us | 439.65 us | 431.79 us | - | 7050 B | 44 | | Decompress | Yay0 | 1 | 3,241.4 us | 31.60 us | 29.56 us | - | 478 B | 45 | | Compress | Yaz0 | 1 | 23,597.2 us | 470.32 us | 461.92 us | - | 6872 B | 46 | | Decompress | Yaz0 | 1 | 2,653.0 us | 33.51 us | 31.34 us | - | 213 B | -------------------------------------------------------------------------------- /Benchmarks/Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | true 9 | Debug;Release;Optimized 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Always 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Benchmarks/Benchmarks/TestAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Algorithms; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Core.IO; 4 | using BenchmarkDotNet.Attributes; 5 | 6 | namespace Benchmarks.Benchmarks 7 | { 8 | [MemoryDiagnoser] 9 | public class TestAlgorithm where T : ICompressionAlgorithm, new() 10 | { 11 | public const string TestFile = "Test.bmp"; 12 | public Stream TestRawData = Stream.Null; 13 | public Stream TestComData = Stream.Null; 14 | public T Instance = new(); 15 | 16 | [Params(1)] 17 | public int MB; 18 | 19 | [GlobalSetup] 20 | public void GlobalSetup() 21 | { 22 | using FileStream input = new(TestFile, FileMode.Open, FileAccess.Read); 23 | TestRawData = new MemoryPoolStream(input, 1024 * 1024); //read 1mb 24 | TestComData = Instance.Compress(TestRawData); 25 | } 26 | 27 | [GlobalCleanup] 28 | public void GlobalCleanup() 29 | { 30 | TestRawData.Dispose(); 31 | TestComData.Dispose(); 32 | } 33 | 34 | [Benchmark] 35 | public void Compress() 36 | { 37 | using MemoryPoolStream output = new(); 38 | for (int i = 0; i < MB; i++) 39 | { 40 | TestRawData.Position = 0; 41 | output.Position = 0; 42 | Instance.Compress(TestRawData, output); 43 | } 44 | } 45 | 46 | [Benchmark] 47 | public void Decompress() 48 | { 49 | using MemoryPoolStream output = new(); 50 | for (int i = 0; i < MB; i++) 51 | { 52 | TestComData.Position = 0; 53 | output.Position = 0; 54 | Instance.Decompress(TestComData, output); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Benchmarks/Benchmarks/TestAllAlgorithms.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Algorithms; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Core.IO; 4 | using BenchmarkDotNet.Attributes; 5 | 6 | namespace Benchmarks.Benchmarks 7 | { 8 | [MemoryDiagnoser] 9 | public class TestAllAlgorithms 10 | { 11 | public const string TestFile = "Test.bmp"; 12 | public Stream TestRawData = Stream.Null; 13 | public Stream TestComData = Stream.Null; 14 | 15 | // Test all unique algorithm. 16 | [Params(typeof(ALLZ), typeof(CLZ0), typeof(CNS), typeof(CNX2), typeof(LZ00), typeof(LZ02), typeof(LZ10), typeof(LZ11), typeof(LZ40), typeof(LZO), typeof(LZShrek), typeof(LZSS), typeof(MIO0), typeof(PRS), typeof(RefPack), typeof(RLE30), typeof(Yay0), typeof(Yaz0), typeof(LZ4Legacy), typeof(HWGZ), typeof(HUF20))] 17 | public Type Algorithm = null!; 18 | 19 | public ICompressionAlgorithm Instance = null!; 20 | 21 | [Params(1)] 22 | public int MB; 23 | 24 | [GlobalSetup] 25 | public void GlobalSetup() 26 | { 27 | Instance = (ICompressionAlgorithm)Activator.CreateInstance(Algorithm)!; 28 | using FileStream input = new(TestFile, FileMode.Open, FileAccess.Read); 29 | TestRawData = new MemoryPoolStream(input, 1024 * 1024); //read 1mb 30 | TestComData = Instance.Compress(TestRawData); 31 | } 32 | 33 | [GlobalCleanup] 34 | public void GlobalCleanup() 35 | { 36 | TestRawData.Dispose(); 37 | TestComData.Dispose(); 38 | } 39 | 40 | [Benchmark] 41 | public void Compress() 42 | { 43 | using MemoryPoolStream output = new(); 44 | for (int i = 0; i < MB; i++) 45 | { 46 | TestRawData.Position = 0; 47 | output.Position = 0; 48 | Instance.Compress(TestRawData, output); 49 | } 50 | } 51 | 52 | [Benchmark] 53 | public void Decompress() 54 | { 55 | using MemoryPoolStream output = new(); 56 | for (int i = 0; i < MB; i++) 57 | { 58 | TestComData.Position = 0; 59 | output.Position = 0; 60 | Instance.Decompress(TestComData, output); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using Benchmarks.Benchmarks; 3 | 4 | //BenchmarkRunner.Run>(); 5 | 6 | BenchmarkRunner.Run(); -------------------------------------------------------------------------------- /Benchmarks/Test.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Venomalia/AuroraLib.Compression/1a2c6607500ade4705b5bf48f66514380d258732/Benchmarks/Test.bmp -------------------------------------------------------------------------------- /CompressionTest/CompressionAlgorithmTest.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression; 2 | using AuroraLib.Compression.Algorithms; 3 | using AuroraLib.Compression.Interfaces; 4 | using AuroraLib.Core.Buffers; 5 | using AuroraLib.Core.Format; 6 | using AuroraLib.Core.IO; 7 | using HashDepot; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | using System; 10 | using System.Buffers; 11 | using System.Collections.Generic; 12 | using System.IO; 13 | using System.IO.Compression; 14 | using System.Linq; 15 | 16 | namespace CompressionTest 17 | { 18 | [TestClass] 19 | public class CompressionAlgorithmTest 20 | { 21 | static CompressionAlgorithmTest() 22 | { 23 | LZ4.HashAlgorithm = b => XXHash.Hash32(b); 24 | } 25 | 26 | [TestMethod] 27 | public void LzssStaticDecodingTest() 28 | { 29 | using (FileStream compressData = new FileStream("Test.lz", FileMode.Open, FileAccess.Read)) 30 | { 31 | LZSS lz = new LZSS(new LzProperties((byte)10, 6, 2)); 32 | int decompressedSize = (int)lz.GetDecompressedSize(compressData); 33 | byte[] data = ArrayPool.Shared.Rent(decompressedSize); 34 | try 35 | { 36 | Span buffer = data.AsSpan(0, decompressedSize); 37 | lz.Decompress(compressData, buffer); 38 | ulong decompressDataHash = XXHash.Hash64(buffer); 39 | Assert.AreEqual(11520079745250749767, decompressDataHash); 40 | } 41 | finally 42 | { 43 | ArrayPool.Shared.Return(data); 44 | } 45 | } 46 | } 47 | 48 | public static IEnumerable GetAvailableAlgorithms() 49 | { 50 | IEnumerable availableAlgorithmTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes().Where(s => typeof(ICompressionAlgorithm).IsAssignableFrom(s) && !s.IsInterface && !s.IsAbstract)); 51 | return availableAlgorithmTypes.Select(x => new object[] { (ICompressionAlgorithm)Activator.CreateInstance(x)! }); 52 | } 53 | 54 | private static FormatDictionary Formats = new FormatDictionary(AppDomain.CurrentDomain.GetAssemblies()); 55 | 56 | [TestMethod] 57 | [DynamicData(nameof(GetAvailableAlgorithms), DynamicDataSourceType.Method)] 58 | public void DataRecognitionTest(ICompressionAlgorithm algorithm) 59 | { 60 | using (FileStream testData = new FileStream("Test.bmp", FileMode.Open, FileAccess.Read)) 61 | using (MemoryPoolStream compressData = algorithm.Compress(testData, CompressionLevel.NoCompression)) 62 | { 63 | ReadOnlySpan fileNameAndExtension = $"Test.bmp.{algorithm.GetType().Name}".AsSpan(); 64 | if (Formats.Identify(compressData, fileNameAndExtension, out IFormatInfo? format) && format!.Class != null && typeof(ICompressionAlgorithm).IsAssignableFrom(format.Class)) 65 | { 66 | Assert.AreEqual(format.Class, algorithm.GetType()); 67 | } 68 | else 69 | { 70 | Assert.Fail(); 71 | } 72 | if (algorithm is IProvidesDecompressedSize providesDecompressedSize) 73 | { 74 | Assert.AreEqual(providesDecompressedSize.GetDecompressedSize(compressData), testData.Length); 75 | } 76 | } 77 | } 78 | 79 | [TestMethod] 80 | [DynamicData(nameof(GetAvailableAlgorithms), DynamicDataSourceType.Method)] 81 | public void EncodingAndDecodingMatchTest(ICompressionAlgorithm algorithm) 82 | { 83 | using (FileStream testData = new FileStream("Test.bmp", FileMode.Open, FileAccess.Read)) 84 | using (SpanBuffer testDataBytes = new SpanBuffer((int)testData.Length)) 85 | { 86 | testData.Read(testDataBytes); 87 | ulong expectedHash = XXHash.Hash64(testDataBytes); 88 | 89 | using (Stream compressData = algorithm.Compress(testDataBytes)) 90 | using (MemoryPoolStream decompressData = algorithm.Decompress(compressData)) 91 | { 92 | ulong decompressDataHash = XXHash.Hash64(decompressData.UnsafeAsSpan()); 93 | Assert.AreEqual(expectedHash, decompressDataHash); 94 | } 95 | } 96 | } 97 | 98 | [TestMethod] 99 | public void EncodingAndDecodingMatchTest_LZ4Frame() 100 | { 101 | LZ4 LZ4Frame = new LZ4() { FrameType = LZ4.FrameTypes.LZ4FrameHeader, Flags = LZ4.FrameDescriptorFlags.IsVersion1 | LZ4.FrameDescriptorFlags.HasContentSize | LZ4.FrameDescriptorFlags.HasContentChecksum | LZ4.FrameDescriptorFlags.HasBlockChecksum }; 102 | EncodingAndDecodingMatchTest(LZ4Frame); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /CompressionTest/CompressionUnitTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;net6.0;net472; 5 | disable 6 | enable 7 | false 8 | true 9 | 10 | Library 11 | Debug;Release;Optimized 12 | 13 | 14 | 15 | 8.0 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Always 37 | 38 | 39 | Always 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /CompressionTest/Test.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Venomalia/AuroraLib.Compression/1a2c6607500ade4705b5bf48f66514380d258732/CompressionTest/Test.bmp -------------------------------------------------------------------------------- /CompressionTest/Test.lz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Venomalia/AuroraLib.Compression/1a2c6607500ade4705b5bf48f66514380d258732/CompressionTest/Test.lz -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Venomalia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /icon-ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Venomalia/AuroraLib.Compression/1a2c6607500ade4705b5bf48f66514380d258732/icon-ex.png -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Venomalia/AuroraLib.Compression/1a2c6607500ade4705b5bf48f66514380d258732/icon.png -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/AKLZ.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core; 3 | using AuroraLib.Core.Format; 4 | using AuroraLib.Core.Format.Identifier; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace AuroraLib.Compression.Algorithms 11 | { 12 | /// 13 | /// AKLZ implementation based on LZSS algorithm used in Skies of Arcadia Legends. 14 | /// 15 | public sealed class AKLZ : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IProvidesDecompressedSize 16 | { 17 | /// 18 | public IIdentifier Identifier => _identifier; 19 | 20 | private static readonly Identifier _identifier = new Identifier("AKLZ~?Qd=ÌÌÍ"); 21 | 22 | /// 23 | public IFormatInfo Info => _info; 24 | 25 | private static readonly IFormatInfo _info = new FormatInfo("Arcadia LZ", new MediaType(MIMEType.Application, "x-lzss+arcadia"), string.Empty, _identifier); 26 | 27 | /// 28 | public bool LookAhead { get; set; } = true; 29 | 30 | private static readonly LzProperties _lz = LZSS.DefaultProperties; 31 | 32 | /// 33 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 34 | => IsMatchStatic(stream, fileNameAndExtension); 35 | 36 | /// 37 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 38 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 39 | 40 | /// 41 | public uint GetDecompressedSize(Stream source) 42 | => source.Peek(s => 43 | { 44 | s.MatchThrow(_identifier); 45 | return s.ReadUInt32(Endian.Big); 46 | }); 47 | 48 | /// 49 | public void Decompress(Stream source, Stream destination) 50 | { 51 | source.MatchThrow(_identifier); 52 | uint decompressedSize = source.ReadUInt32(Endian.Big); 53 | LZSS.DecompressHeaderless(source, destination, decompressedSize, _lz); 54 | } 55 | 56 | /// 57 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 58 | { 59 | destination.Write(_identifier.AsSpan()); 60 | destination.Write(source.Length, Endian.Big); 61 | LZSS.CompressHeaderless(source, destination, _lz, LookAhead, level); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/AsuraZlb.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Core; 4 | using AuroraLib.Core.Format; 5 | using AuroraLib.Core.Format.Identifier; 6 | using AuroraLib.Core.IO; 7 | using System; 8 | using System.IO; 9 | using System.IO.Compression; 10 | 11 | namespace AuroraLib.Compression.Algorithms 12 | { 13 | /// 14 | /// AsuraZlb based on ZLib compression algorithm used in The Simpsons Game. 15 | /// 16 | public sealed class AsuraZlb : ICompressionAlgorithm, IHasIdentifier, IProvidesDecompressedSize 17 | { 18 | /// 19 | public IIdentifier Identifier => _identifier; 20 | 21 | private static readonly Identifier64 _identifier = new Identifier64("AsuraZlb".AsSpan()); 22 | 23 | /// 24 | public IFormatInfo Info => _info; 25 | 26 | private static readonly IFormatInfo _info = new FormatInfo("Asura zlip", new MediaType(MIMEType.Application, "zlip+asura"), string.Empty, _identifier); 27 | 28 | private static readonly ZLib zLib = new ZLib(); 29 | 30 | /// 31 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 32 | => IsMatchStatic(stream, fileNameAndExtension); 33 | 34 | /// 35 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 36 | => stream.Position + 0x14 < stream.Length && stream.Peek(s => s.Match(_identifier)); 37 | 38 | /// 39 | public uint GetDecompressedSize(Stream source) 40 | => source.Peek(s => 41 | { 42 | s.MatchThrow(_identifier); 43 | s.Position += 8; 44 | return s.ReadUInt32(Endian.Big); 45 | }); 46 | 47 | /// 48 | public void Decompress(Stream source, Stream destination) 49 | { 50 | long start = destination.Position; 51 | source.MatchThrow(_identifier); 52 | source.Skip(4); 53 | uint compressedSize = source.ReadUInt32(Endian.Big); 54 | uint decompressedSize = source.ReadUInt32(Endian.Big); 55 | 56 | // Mark the initial positions of the streams 57 | long compressedStartPosition = source.Position; 58 | long destinationStartPosition = destination.Position; 59 | 60 | // Perform the decompression 61 | zLib.Decompress(source, destination, (int)compressedSize); 62 | 63 | // Verify decompressed size 64 | DecompressedSizeException.ThrowIfMismatch(destination.Position - destinationStartPosition, decompressedSize); 65 | 66 | // Verify compressed size and handle mismatches 67 | Helper.TraceIfCompressedSizeMismatch(source.Position - compressedStartPosition, compressedSize); 68 | } 69 | 70 | /// 71 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 72 | { 73 | long start = destination.Position; 74 | destination.Write(_identifier); 75 | destination.Write(1); 76 | destination.Write(0, Endian.Big); // Placeholder 77 | destination.Write(source.Length, Endian.Big); 78 | zLib.Compress(source, destination, level); 79 | destination.At(start + 12, s => s.Write((uint)(destination.Length - start - 0x14), Endian.Big)); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/CLZ0.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Compression.IO; 4 | using AuroraLib.Compression.MatchFinder; 5 | using AuroraLib.Core; 6 | using AuroraLib.Core.Collections; 7 | using AuroraLib.Core.Format; 8 | using AuroraLib.Core.Format.Identifier; 9 | using AuroraLib.Core.IO; 10 | using System; 11 | using System.IO; 12 | using System.IO.Compression; 13 | 14 | namespace AuroraLib.Compression.Algorithms 15 | { 16 | /// 17 | /// CLZ0 compression algorithm, used in Games from Victor Interactive Software. 18 | /// 19 | public sealed class CLZ0 : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IProvidesDecompressedSize 20 | { 21 | /// 22 | public IIdentifier Identifier => _identifier; 23 | 24 | private static readonly Identifier32 _identifier = new Identifier32((byte)'C', (byte)'L', (byte)'Z', 0); 25 | 26 | /// 27 | public IFormatInfo Info => _info; 28 | 29 | private static readonly IFormatInfo _info = new FormatInfo("Victor Interactive CLZ0", new MediaType(MIMEType.Application, "x-clz0"), string.Empty, _identifier); 30 | 31 | internal static readonly LzProperties _lz = new LzProperties(0x1000, 18, 3); 32 | 33 | /// 34 | public bool LookAhead { get; set; } = true; 35 | 36 | /// 37 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 38 | => IsMatchStatic(stream, fileNameAndExtension); 39 | 40 | /// 41 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 42 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 43 | 44 | /// 45 | public uint GetDecompressedSize(Stream source) 46 | => source.Peek(s => 47 | { 48 | s.MatchThrow(_identifier); 49 | s.Position += 8; 50 | return s.ReadUInt32(Endian.Big); 51 | }); 52 | 53 | /// 54 | public void Decompress(Stream source, Stream destination) 55 | { 56 | // Read Header 57 | source.MatchThrow(_identifier); 58 | _ = source.ReadUInt32(Endian.Big); 59 | _ = source.ReadUInt32(Endian.Big); 60 | uint decompressedSize = source.ReadUInt32(Endian.Big); 61 | 62 | // Perform the decompression 63 | DecompressHeaderless(source, destination, (int)decompressedSize); 64 | } 65 | 66 | /// 67 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 68 | { 69 | // Write Header 70 | destination.Write(_identifier); 71 | destination.Write(source.Length, Endian.Big); 72 | destination.Write(0, Endian.Big); 73 | destination.Write(source.Length, Endian.Big); 74 | 75 | // Perform the compression 76 | CompressHeaderless(source, destination, LookAhead, level); 77 | } 78 | 79 | public static void DecompressHeaderless(Stream source, Stream destination, int decomLength) 80 | { 81 | long endPosition = destination.Position + decomLength; 82 | destination.SetLength(endPosition); 83 | FlagReader flag = new FlagReader(source, Endian.Little); 84 | using (LzWindows buffer = new LzWindows(destination, _lz.WindowsSize)) 85 | { 86 | while (destination.Position + buffer.Position < endPosition) 87 | { 88 | if (flag.Readbit()) 89 | { 90 | int distance = source.ReadByte(); 91 | int length = source.ReadByte(); 92 | 93 | distance |= length >> 4 << 8; 94 | distance = 0x1000 - distance; // window delta to distance 95 | length = (length & 0x0f) + 3; 96 | 97 | buffer.BackCopy(distance, length); 98 | } 99 | else 100 | { 101 | buffer.WriteByte(source.ReadUInt8()); 102 | } 103 | } 104 | } 105 | 106 | // Verify decompressed size 107 | if (destination.Position != endPosition) 108 | { 109 | throw new DecompressedSizeException(decomLength, destination.Position - (endPosition - decomLength)); 110 | } 111 | } 112 | 113 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, bool lookAhead = true, CompressionLevel level = CompressionLevel.Optimal) 114 | { 115 | int sourcePointer = 0x0, matchPointer = 0x0; 116 | using PoolList matches = LZMatchFinder.FindMatchesParallel(source, _lz, lookAhead, level); 117 | using FlagWriter flag = new FlagWriter(destination, Endian.Little); 118 | while (sourcePointer < source.Length) 119 | { 120 | if (matchPointer < matches.Count && matches[matchPointer].Offset == sourcePointer) 121 | { 122 | LzMatch match = matches[matchPointer++]; 123 | 124 | int delta = 0x1000 - match.Distance; 125 | flag.Buffer.WriteByte((byte)delta); 126 | flag.Buffer.WriteByte((byte)((match.Length - 3) | (delta >> 8 << 4))); 127 | sourcePointer += match.Length; 128 | flag.WriteBit(true); 129 | 130 | } 131 | else 132 | { 133 | flag.Buffer.WriteByte(source[sourcePointer++]); 134 | flag.WriteBit(false); 135 | } 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/CNS.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Compression.IO; 4 | using AuroraLib.Compression.MatchFinder; 5 | using AuroraLib.Core; 6 | using AuroraLib.Core.Collections; 7 | using AuroraLib.Core.Format; 8 | using AuroraLib.Core.Format.Identifier; 9 | using AuroraLib.Core.IO; 10 | using System; 11 | using System.IO; 12 | using System.IO.Compression; 13 | 14 | namespace AuroraLib.Compression.Algorithms 15 | { 16 | /// 17 | /// CNS compression algorithm, used in Games from Red Entertainment. 18 | /// 19 | public sealed class CNS : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IProvidesDecompressedSize 20 | { 21 | /// 22 | public IIdentifier Identifier => _identifier; 23 | 24 | private static readonly Identifier32 _identifier = new Identifier32("@CNS".AsSpan()); 25 | 26 | /// 27 | public IFormatInfo Info => _info; 28 | 29 | private static readonly IFormatInfo _info = new FormatInfo("Red Entertainmen CNS", new MediaType(MIMEType.Application, "x-red-cns"), string.Empty, _identifier); 30 | 31 | internal static readonly LzProperties _lz = new LzProperties(0x100, 130, 3); 32 | 33 | /// 34 | public bool LookAhead { get; set; } = true; 35 | 36 | /// 37 | /// The extension string that is set when writing and reading. 38 | /// 39 | public string Extension = "PAK"; 40 | 41 | /// 42 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 43 | => IsMatchStatic(stream, fileNameAndExtension); 44 | 45 | /// 46 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 47 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 48 | 49 | /// 50 | public uint GetDecompressedSize(Stream source) 51 | => source.Peek(s => 52 | { 53 | s.MatchThrow(_identifier); 54 | s.Position += 4; 55 | return s.ReadUInt32(Endian.Little); 56 | }); 57 | 58 | /// 59 | public void Decompress(Stream source, Stream destination) 60 | { 61 | // Read Header 62 | source.MatchThrow(_identifier); 63 | Extension = source.ReadString(4); 64 | uint decompressedSize = source.ReadUInt32(Endian.Little); 65 | source.Position += 4; // 0 66 | 67 | // Perform the decompression 68 | DecompressHeaderless(source, destination, (int)decompressedSize); 69 | } 70 | 71 | /// 72 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 73 | { 74 | // Write Header 75 | destination.Write(_identifier); 76 | if (source[0] == 0x0 && source[1] == 0x20 && source[2] == 0xAF && source[3] == 0x30) 77 | { 78 | destination.WriteString("TPL".AsSpan(), 4); 79 | } 80 | else 81 | { 82 | destination.WriteString(Extension.AsSpan(), 4); 83 | } 84 | destination.Write(source.Length, Endian.Little); 85 | destination.Write(0); 86 | 87 | // Perform the compression 88 | CompressHeaderless(source, destination, LookAhead, level); 89 | } 90 | 91 | public static void DecompressHeaderless(Stream source, Stream destination, int decomLength) 92 | { 93 | long endPosition = destination.Position + decomLength; 94 | destination.SetLength(endPosition); 95 | using (LzWindows buffer = new LzWindows(destination, _lz.WindowsSize)) 96 | { 97 | 98 | Span bytes = stackalloc byte[128]; 99 | 100 | while (destination.Position + buffer.Position < endPosition) 101 | { 102 | int length = source.ReadUInt8(); 103 | 104 | // The first bit is the flag. 105 | if ((length & 0x80) == 0) // Uncompressed 1-127 106 | { 107 | if (source.Read(bytes.Slice(0, length)) != length) 108 | { 109 | throw new EndOfStreamException(); 110 | } 111 | buffer.Write(bytes.Slice(0, length)); 112 | } 113 | else // Compressed 3-130 114 | { 115 | int distance = source.ReadUInt8() + 1; 116 | length = (length & 0x7F) + 3; 117 | 118 | buffer.BackCopy(distance, length); 119 | } 120 | } 121 | } 122 | 123 | // Verify decompressed size 124 | if (destination.Position != endPosition) 125 | { 126 | throw new DecompressedSizeException(decomLength, destination.Position - (endPosition - decomLength)); 127 | } 128 | } 129 | 130 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, bool lookAhead = true, CompressionLevel level = CompressionLevel.Optimal) 131 | { 132 | int sourcePointer = 0x0, plainSize; 133 | using (PoolList matches = LZMatchFinder.FindMatchesParallel(source, _lz, lookAhead, level)) 134 | { 135 | matches.Add(new LzMatch(source.Length, 0, 0)); // Dummy-Match 136 | 137 | foreach (LzMatch match in matches) 138 | { 139 | plainSize = match.Offset - sourcePointer; 140 | 141 | while (plainSize != 0) 142 | { 143 | byte length = (byte)Math.Min(127, plainSize); 144 | destination.WriteByte(length); 145 | destination.Write(source.Slice(sourcePointer, length)); 146 | sourcePointer += length; 147 | plainSize -= length; 148 | } 149 | 150 | // Match has data that still needs to be processed? 151 | if (match.Length != 0) 152 | { 153 | destination.WriteByte((byte)(0x80 | (match.Length - 3))); 154 | destination.WriteByte((byte)(match.Distance - 1)); 155 | sourcePointer += match.Length; 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/COMP.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core; 3 | using AuroraLib.Core.Format; 4 | using AuroraLib.Core.Format.Identifier; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace AuroraLib.Compression.Algorithms 11 | { 12 | /// 13 | /// COMP extension header based on LZ11 algorithm used in Puyo Puyo Chronicle. 14 | /// 15 | public sealed class COMP : LZ11, ICompressionAlgorithm, IHasIdentifier 16 | { 17 | /// 18 | public IIdentifier Identifier => _identifier; 19 | 20 | private static readonly Identifier32 _identifier = new Identifier32("COMP".AsSpan()); 21 | 22 | /// 23 | public override IFormatInfo Info => _info; 24 | 25 | private static readonly IFormatInfo _info = new FormatInfo("COMP", new MediaType(MIMEType.Application, "x-nintendo-lz11+comp"), string.Empty, _identifier); 26 | 27 | /// 28 | public override bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 29 | => IsMatchStatic(stream, fileNameAndExtension); 30 | 31 | /// 32 | public new static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 33 | => stream.Position + 0x8 < stream.Length && stream.Peek(s => s.Match(_identifier) && LZ11.IsMatchStatic(s)); 34 | 35 | /// 36 | public override uint GetDecompressedSize(Stream source) 37 | => source.Peek(s => 38 | { 39 | s.MatchThrow(_identifier); 40 | return InternalGetDecompressedSize(s); 41 | }); 42 | 43 | /// 44 | public override void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 45 | { 46 | destination.Write(_identifier); 47 | base.Compress(source, destination, level); 48 | } 49 | 50 | /// 51 | public override void Decompress(Stream source, Stream destination) 52 | { 53 | source.MatchThrow(_identifier); 54 | base.Decompress(source, destination); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/CXLZ.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core.Format; 3 | using AuroraLib.Core.Format.Identifier; 4 | using AuroraLib.Core.IO; 5 | using System; 6 | using System.IO; 7 | using System.IO.Compression; 8 | 9 | namespace AuroraLib.Compression.Algorithms 10 | { 11 | /// 12 | /// CXLZ extension header based on LZ10 algorithm used in Puyo Puyo. 13 | /// 14 | public sealed class CXLZ : LZ10, ICompressionAlgorithm, IHasIdentifier 15 | { 16 | /// 17 | public IIdentifier Identifier => _identifier; 18 | 19 | private static readonly Identifier32 _identifier = new Identifier32("CXLZ".AsSpan()); 20 | 21 | /// 22 | public override IFormatInfo Info => _info; 23 | 24 | private static readonly IFormatInfo _info = new FormatInfo("CXLZ", new MediaType(MIMEType.Application, "x-nintendo-lz10+cxlz"), string.Empty, _identifier); 25 | 26 | /// 27 | public override bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 28 | => IsMatchStatic(stream, fileNameAndExtension); 29 | 30 | /// 31 | public new static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 32 | => stream.Position + 0x8 < stream.Length && stream.Peek(s => s.Match(_identifier) && LZ10.IsMatchStatic(s)); 33 | 34 | /// 35 | public override uint GetDecompressedSize(Stream source) 36 | => source.Peek(s => 37 | { 38 | s.MatchThrow(_identifier); 39 | return InternalGetDecompressedSize(s); 40 | }); 41 | 42 | /// 43 | public override void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 44 | { 45 | destination.Write(_identifier); 46 | base.Compress(source, destination, level); 47 | } 48 | 49 | /// 50 | public override void Decompress(Stream source, Stream destination) 51 | { 52 | source.MatchThrow(_identifier); 53 | base.Decompress(source, destination); 54 | } 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/ECD.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core; 3 | using AuroraLib.Core.Format; 4 | using AuroraLib.Core.Format.Identifier; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace AuroraLib.Compression.Algorithms 11 | { 12 | /// 13 | /// Rocket Company Ltd ECD algorithm base on LZSS, used in Kanken Training 2. 14 | /// 15 | // https://github.com/FanTranslatorsInternational/Kuriimu2/blob/8cc3c310a597fdf78209d693c7333009d772c15f/src/Kompression/Implementations/Decoders/LzEcdDecoder.cs 16 | public sealed class ECD : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IProvidesDecompressedSize 17 | { 18 | private static readonly LzProperties LZPropertie = new LzProperties(0x400, 0x42, 3, 0x3BE); 19 | 20 | /// 21 | public IIdentifier Identifier => _identifier; 22 | 23 | private static readonly Identifier _identifier = new Identifier("ECD"); 24 | 25 | /// 26 | public IFormatInfo Info => _info; 27 | 28 | private static readonly IFormatInfo _info = new FormatInfo("ECD lzss", new MediaType(MIMEType.Application, "x-lzss+ecd"), string.Empty, _identifier); 29 | 30 | /// 31 | public bool LookAhead { get; set; } = true; 32 | 33 | /// 34 | /// 35 | /// 36 | public byte PlainSize { get; set; } = 4; 37 | 38 | /// 39 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 40 | => IsMatchStatic(stream, fileNameAndExtension); 41 | 42 | /// 43 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 44 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)) && GetDecompressedSizeStatic(stream) != 0; 45 | 46 | /// 47 | public uint GetDecompressedSize(Stream source) => GetDecompressedSizeStatic(source); 48 | 49 | private static uint GetDecompressedSizeStatic(Stream source) 50 | => source.Peek(s => 51 | { 52 | s.MatchThrow(_identifier); 53 | s.Position += 5; 54 | if (s.ReadUInt32(Endian.Big) + 0x10 > source.Length) 55 | return 0u; 56 | return s.ReadUInt32(Endian.Big); 57 | }); 58 | 59 | /// 60 | public void Decompress(Stream source, Stream destination) 61 | { 62 | source.MatchThrow(_identifier); 63 | bool isCompressed = source.ReadByte() == 1; 64 | uint plainSize = source.ReadUInt32(Endian.Big); 65 | uint compressedSize = source.ReadUInt32(Endian.Big); 66 | uint decompressedSize = source.ReadUInt32(Endian.Big); 67 | 68 | // Mark the initial positions of the streams 69 | long compressedStartPosition = source.Position; 70 | 71 | if (isCompressed) 72 | { 73 | // Copy plain bytes 74 | for (var i = 0; i < plainSize; i++) 75 | destination.WriteByte((byte)source.ReadByte()); 76 | 77 | // Perform the decompression 78 | LZSS.DecompressHeaderless(source, destination, decompressedSize - plainSize, LZPropertie); 79 | } 80 | else 81 | { 82 | source.CopyTo(destination); 83 | } 84 | 85 | // Verify compressed size and handle mismatches 86 | Helper.TraceIfCompressedSizeMismatch(source.Position - compressedStartPosition, compressedSize); 87 | } 88 | 89 | /// 90 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 91 | { 92 | // Determine whether compression should be applied 93 | bool isCompressed = level != CompressionLevel.NoCompression; 94 | int plainSize = isCompressed ? PlainSize : 0; 95 | 96 | // Store the current position in the destination stream (assumes the stream supports seeking) 97 | long startpos = destination.Length; 98 | destination.Write(_identifier.AsSpan()); 99 | destination.WriteByte(isCompressed ? (byte)1 : (byte)0); 100 | destination.Write(plainSize, Endian.Big); 101 | destination.Write(isCompressed ? 0 : source.Length, Endian.Big); // Placeholder 102 | destination.Write(source.Length, Endian.Big); 103 | 104 | if (isCompressed) 105 | { 106 | // Copy plain bytes 107 | destination.Write(source.Slice(0, plainSize)); 108 | 109 | LZSS.CompressHeaderless(source.Slice(plainSize), destination, LZPropertie, LookAhead, level); 110 | 111 | uint compressedSize = (uint)(destination.Length - startpos - 0x10); 112 | destination.At(startpos + 0x8, s => s.Write(compressedSize, Endian.Big)); 113 | 114 | // If compression was ineffective (data grew in size), fall back to uncompressed storage 115 | if (compressedSize > source.Length) 116 | { 117 | destination.Position = startpos; 118 | destination.SetLength(startpos); 119 | Compress(source, destination, CompressionLevel.NoCompression); 120 | } 121 | } 122 | else 123 | { 124 | destination.Write(source); 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/FCMP.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core.Format; 3 | using AuroraLib.Core.Format.Identifier; 4 | using AuroraLib.Core.IO; 5 | using System; 6 | using System.IO; 7 | using System.IO.Compression; 8 | 9 | namespace AuroraLib.Compression.Algorithms 10 | { 11 | 12 | /// 13 | /// FCMP extension header based on LZSS algorithm used in Muramasa The Demon Blade. 14 | /// 15 | public sealed class FCMP : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IProvidesDecompressedSize 16 | { 17 | /// 18 | public IIdentifier Identifier => _identifier; 19 | 20 | private static readonly Identifier32 _identifier = new Identifier32("FCMP".AsSpan()); 21 | 22 | /// 23 | public IFormatInfo Info => _info; 24 | 25 | private static readonly IFormatInfo _info = new FormatInfo("FCMP", new MediaType(MIMEType.Application, "x-lzss+fcmp"), string.Empty, _identifier); 26 | 27 | /// 28 | public bool LookAhead { get; set; } = true; 29 | 30 | private static readonly LzProperties _lz = LZSS.Lzss0Properties; 31 | 32 | /// 33 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 34 | => IsMatchStatic(stream, fileNameAndExtension); 35 | 36 | /// 37 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 38 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 39 | 40 | /// 41 | public uint GetDecompressedSize(Stream source) 42 | => source.Peek(s => 43 | { 44 | s.MatchThrow(_identifier); 45 | return s.ReadUInt32(); 46 | }); 47 | 48 | /// 49 | public void Decompress(Stream source, Stream destination) 50 | { 51 | source.MatchThrow(_identifier); 52 | uint decompressedSize = source.ReadUInt32(); 53 | _ = source.ReadInt32(); //always 305397760? 54 | LZSS.DecompressHeaderless(source, destination, decompressedSize, _lz); 55 | } 56 | 57 | /// 58 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 59 | { 60 | // Write out the header 61 | destination.Write(_identifier); 62 | destination.Write(source.Length); // Decompressed length 63 | destination.Write(305397760); 64 | LZSS.CompressHeaderless(source, destination, _lz, LookAhead, level); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/GCLZ.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core.Format; 3 | using AuroraLib.Core.Format.Identifier; 4 | using AuroraLib.Core.IO; 5 | using System; 6 | using System.IO; 7 | using System.IO.Compression; 8 | 9 | namespace AuroraLib.Compression.Algorithms 10 | { 11 | /// 12 | /// GCLZ extension header based on LZ10 algorithm used in Pandora's Tower. 13 | /// 14 | public sealed class GCLZ : LZ10, ICompressionAlgorithm, IHasIdentifier 15 | { 16 | /// 17 | public IIdentifier Identifier => _identifier; 18 | 19 | private static readonly Identifier32 _identifier = new Identifier32("GCLZ".AsSpan()); 20 | 21 | /// 22 | public override IFormatInfo Info => _info; 23 | 24 | private static readonly IFormatInfo _info = new FormatInfo("GCLZ", new MediaType(MIMEType.Application, "x-nintendo-lz10+gclz"), string.Empty); 25 | /// 26 | public override bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 27 | => IsMatchStatic(stream, fileNameAndExtension); 28 | 29 | /// 30 | public new static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 31 | => stream.Position + 0x8 < stream.Length && stream.Peek(s => s.Match(_identifier) && LZ10.IsMatchStatic(s)); 32 | 33 | /// 34 | public override uint GetDecompressedSize(Stream source) 35 | => source.Peek(s => 36 | { 37 | s.MatchThrow(_identifier); 38 | return InternalGetDecompressedSize(s); 39 | }); 40 | 41 | /// 42 | public override void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 43 | { 44 | destination.Write(_identifier); 45 | base.Compress(source, destination, level); 46 | } 47 | 48 | /// 49 | public override void Decompress(Stream source, Stream destination) 50 | { 51 | source.MatchThrow(_identifier); 52 | base.Decompress(source, destination); 53 | } 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/HWGZ.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Core; 4 | using AuroraLib.Core.Format; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.Buffers.Binary; 8 | using System.IO; 9 | using System.IO.Compression; 10 | 11 | namespace AuroraLib.Compression.Algorithms 12 | { 13 | /// 14 | /// Hyrule Warriors GZ compression format based on ZLib. 15 | /// 16 | public sealed class HWGZ : ICompressionAlgorithm, IEndianDependentFormat, IProvidesDecompressedSize 17 | { 18 | /// 19 | public IFormatInfo Info => _info; 20 | 21 | private static readonly IFormatInfo _info = new FormatInfo("Hyrule Warriors GZ", new MediaType(MIMEType.Application, "zlib+hwgz"), ".gz"); 22 | 23 | /// 24 | /// Defines the size of the chunks. 25 | /// 26 | public int ChunkSize = 0x10000; 27 | 28 | /// 29 | public Endian FormatByteOrder { get; set; } = Endian.Big; 30 | 31 | /// 32 | public bool IsMatch(Stream source, ReadOnlySpan fileNameAndExtension = default) 33 | => IsMatchStatic(source, fileNameAndExtension); 34 | 35 | /// 36 | public static bool IsMatchStatic(Stream source, ReadOnlySpan fileNameAndExtension = default) 37 | { 38 | if (source.Length < 0x80) 39 | return false; 40 | 41 | long pos = source.Position; 42 | if (!Header.TryRead(source, out Header header, out Endian order)) 43 | return false; 44 | 45 | source.Align(4 * header.ChunkCount, SeekOrigin.Current, 128); 46 | bool result = source.At(4, SeekOrigin.Current, s => s.Read().Validate()); 47 | source.Position = pos; 48 | return result; 49 | } 50 | 51 | /// 52 | public uint GetDecompressedSize(Stream source) 53 | => source.Peek(s => 54 | { 55 | if (!Header.TryRead(s, out Header header, out Endian order)) 56 | throw new InvalidOperationException("Header a Invalidate"); 57 | return header.DecompressedSize; 58 | }); 59 | 60 | /// 61 | public void Decompress(Stream source, Stream destination) 62 | { 63 | // Read Header 64 | if (!Header.TryRead(source, out Header header, out Endian order)) 65 | throw new InvalidOperationException("Header a Invalidate"); 66 | 67 | source.Align(4 * header.ChunkCount, SeekOrigin.Current, 128); 68 | 69 | // Mark the initial positions of the destination 70 | long destinationStartPosition = destination.Position; 71 | 72 | ZLib zLib = new ZLib(); 73 | // Decompress each chunk 74 | for (int i = 0; i < header.ChunkCount; i++) 75 | { 76 | int chunkDataSize = source.ReadInt32(FormatByteOrder); 77 | zLib.Decompress(source, destination, chunkDataSize); 78 | source.Align(128); 79 | } 80 | 81 | // Verify decompressed size 82 | DecompressedSizeException.ThrowIfMismatch(destination.Position - destinationStartPosition, header.DecompressedSize); 83 | } 84 | 85 | /// 86 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 87 | { 88 | // Mark the initial positions of the destination 89 | long destStart = destination.Position; 90 | 91 | int chunkCount = (source.Length + ChunkSize - 1) / ChunkSize; 92 | uint[] chunkSizes = new uint[chunkCount]; 93 | 94 | // Write Header 95 | destination.Write(ChunkSize, FormatByteOrder); 96 | destination.Write(chunkCount, FormatByteOrder); 97 | destination.Write(source.Length, FormatByteOrder); 98 | destination.Write(chunkSizes, FormatByteOrder); // Placeholder 99 | 100 | destination.WriteAlign(128); 101 | 102 | ZLib zLib = new ZLib(); 103 | // Compress each chunk 104 | using (MemoryPoolStream buffer = new MemoryPoolStream(ChunkSize)) 105 | { 106 | for (int i = 0; i < chunkCount; i++) 107 | { 108 | int segmentStart = i * ChunkSize; 109 | int segmentSize = Math.Min(ChunkSize, source.Length - segmentStart); 110 | 111 | buffer.SetLength(0); 112 | zLib.Compress(source.Slice(segmentStart, segmentSize), buffer, level); 113 | 114 | ReadOnlySpan segmentData = buffer.UnsafeAsSpan(); 115 | chunkSizes[i] = (uint)(segmentData.Length + 4); 116 | destination.Write(segmentData.Length, FormatByteOrder); 117 | destination.Write(segmentData); 118 | destination.WriteAlign(128); 119 | } 120 | } 121 | destination.At(destStart + 12, s => s.Write(chunkSizes, FormatByteOrder)); 122 | } 123 | 124 | private readonly struct Header : IReversibleEndianness
125 | { 126 | public readonly uint ChunkSize; 127 | public readonly uint ChunkCount; 128 | public readonly uint DecompressedSize; 129 | 130 | public bool Validate() => ChunkCount != 0 && ChunkCount == (DecompressedSize + ChunkSize - 1) / ChunkSize; 131 | 132 | public Header(uint chunkSize, uint chunkCount, uint decompressedSize) 133 | { 134 | ChunkSize = chunkSize; 135 | ChunkCount = chunkCount; 136 | DecompressedSize = decompressedSize; 137 | } 138 | 139 | public static bool TryRead(Stream source, out Header header, out Endian endian) 140 | { 141 | endian = Endian.Little; 142 | header = source.Read
(); 143 | if (header.ChunkSize == 0 || header.ChunkCount == 0 || header.DecompressedSize == 0) 144 | return false; 145 | 146 | if (!header.Validate()) 147 | { 148 | endian = Endian.Big; 149 | header = header.ReverseEndianness(); 150 | if (!header.Validate()) 151 | return false; 152 | } 153 | return true; 154 | } 155 | 156 | public Header ReverseEndianness() 157 | => new Header( 158 | BinaryPrimitives.ReverseEndianness(ChunkSize), 159 | BinaryPrimitives.ReverseEndianness(ChunkCount), 160 | BinaryPrimitives.ReverseEndianness(DecompressedSize)); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/IECP.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core; 3 | using AuroraLib.Core.Format; 4 | using AuroraLib.Core.Format.Identifier; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace AuroraLib.Compression.Algorithms 11 | { 12 | /// 13 | /// IECP algorithm base on LZSS, used in Fate/Extra. 14 | /// 15 | public class IECP : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IProvidesDecompressedSize 16 | { 17 | /// 18 | public IIdentifier Identifier => _identifier; 19 | 20 | private static readonly Identifier32 _identifier = new Identifier32("IECP".AsSpan()); 21 | 22 | /// 23 | public IFormatInfo Info => _info; 24 | 25 | private static readonly IFormatInfo _info = new FormatInfo("Fate/Extra IECP", new MediaType(MIMEType.Application, "x-lzss-iecp"), string.Empty, _identifier); 26 | 27 | /// 28 | public bool LookAhead { get; set; } = true; 29 | 30 | private static readonly LzProperties _lz = LZSS.Lzss0Properties; 31 | 32 | /// 33 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 34 | => IsMatchStatic(stream, fileNameAndExtension); 35 | 36 | /// 37 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 38 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 39 | 40 | /// 41 | public uint GetDecompressedSize(Stream source) 42 | => source.Peek(s => 43 | { 44 | s.MatchThrow(_identifier); 45 | return s.ReadUInt32(); 46 | }); 47 | 48 | /// 49 | public void Decompress(Stream source, Stream destination) 50 | { 51 | source.MatchThrow(_identifier); 52 | uint decompressedSize = source.ReadUInt32(); 53 | LZSS.DecompressHeaderless(source, destination, decompressedSize, _lz); 54 | } 55 | 56 | /// 57 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 58 | { 59 | destination.Write(_identifier); 60 | destination.Write(source.Length); 61 | LZSS.CompressHeaderless(source, destination, _lz, LookAhead, level); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/LZ01.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core; 3 | using AuroraLib.Core.Format; 4 | using AuroraLib.Core.Format.Identifier; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace AuroraLib.Compression.Algorithms 11 | { 12 | /// 13 | /// LZ01 implementation based on LZSS algorithm used in Skies of Arcadia Legends. 14 | /// 15 | public sealed class LZ01 : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IProvidesDecompressedSize 16 | { 17 | /// 18 | public IIdentifier Identifier => _identifier; 19 | 20 | private static readonly Identifier32 _identifier = new Identifier32("LZ01".AsSpan()); 21 | 22 | /// 23 | public IFormatInfo Info => _info; 24 | 25 | private static readonly IFormatInfo _info = new FormatInfo("Sega LZ01", new MediaType(MIMEType.Application, "x-lzss+lz01"), string.Empty, _identifier); 26 | 27 | private static readonly LzProperties _lz = LZSS.Lzss0Properties; 28 | 29 | /// 30 | public bool LookAhead { get; set; } = true; 31 | 32 | /// 33 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 34 | => IsMatchStatic(stream, fileNameAndExtension); 35 | 36 | /// 37 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 38 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 39 | 40 | /// 41 | public uint GetDecompressedSize(Stream source) 42 | => source.Peek(s => 43 | { 44 | s.MatchThrow(_identifier); 45 | s.Position += 4; 46 | return s.ReadUInt32(); 47 | }); 48 | 49 | /// 50 | public void Decompress(Stream source, Stream destination) 51 | { 52 | // Mark the initial positions of the streams 53 | long compressedStartPosition = source.Position; 54 | 55 | // Read Header 56 | source.MatchThrow(_identifier); 57 | uint sourceLength = source.ReadUInt32(); 58 | uint decompressedSize = source.ReadUInt32(); 59 | _ = source.ReadUInt32(); 60 | 61 | // Perform the decompression 62 | LZSS.DecompressHeaderless(source, destination, decompressedSize, _lz); 63 | 64 | // Verify compressed size and handle mismatches 65 | Helper.TraceIfCompressedSizeMismatch(source.Position - compressedStartPosition, sourceLength); 66 | } 67 | 68 | /// 69 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 70 | { 71 | // Mark the initial positions of the destination 72 | long destinationStartPosition = destination.Position; 73 | 74 | // Write Header 75 | destination.Write(_identifier); 76 | destination.Write(0); // Compressed length (will be filled in later) 77 | destination.Write(source.Length); 78 | destination.Write(0); 79 | 80 | // Perform the compression 81 | LZSS.CompressHeaderless(source, destination, _lz, LookAhead, level); 82 | 83 | // Go back to the beginning of the file and write out the compressed length 84 | int destinationLength = (int)(destination.Position - destinationStartPosition); 85 | destination.At(destinationStartPosition + 4, x => x.Write(destinationLength)); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/LZ02.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Compression.IO; 4 | using AuroraLib.Compression.MatchFinder; 5 | using AuroraLib.Core; 6 | using AuroraLib.Core.Collections; 7 | using AuroraLib.Core.Exceptions; 8 | using AuroraLib.Core.Format; 9 | using AuroraLib.Core.IO; 10 | using System; 11 | using System.IO; 12 | using System.IO.Compression; 13 | 14 | namespace AuroraLib.Compression.Algorithms 15 | { 16 | /// 17 | /// LZ02 compression algorithm used in Mario Golf: Toadstool Tour. 18 | /// 19 | public sealed class LZ02 : ICompressionAlgorithm, ILzSettings, IProvidesDecompressedSize 20 | { 21 | private static readonly string[] _extensions = new string[] { ".lz02", string.Empty }; 22 | 23 | /// 24 | public IFormatInfo Info => _info; 25 | 26 | private static readonly IFormatInfo _info = new FormatInfo("LZ02", new MediaType(MIMEType.Application, "x-lz02"), _extensions); 27 | 28 | internal static readonly LzProperties _lz = new LzProperties(0xFFF, 272, 3); 29 | 30 | /// 31 | public bool LookAhead { get; set; } = true; 32 | 33 | /// 34 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 35 | => IsMatchStatic(stream, fileNameAndExtension); 36 | 37 | /// 38 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 39 | // Has no distinct header, recognition is inaccurate! 40 | => (fileNameAndExtension.IsEmpty || PathX.GetExtension(fileNameAndExtension).Contains(_extensions[0].AsSpan(), StringComparison.InvariantCultureIgnoreCase)) 41 | #if NET5_0_OR_GREATER 42 | && stream.Position + 0x8 < stream.Length && stream.Peek(s => Enum.IsDefined(s.Read()) && s.Read(Endian.Big) != 0 && (s.ReadByte() & 0xE0) == 0); 43 | #else 44 | && stream.Position + 0x8 < stream.Length && stream.Peek(s => Enum.IsDefined(typeof(DataType), s.Read()) && s.Read(Endian.Big) != 0 && (s.ReadByte() & 0xE0) == 0); 45 | #endif 46 | 47 | private enum DataType : byte 48 | { 49 | Default = 0x01, 50 | Extended = 0x02, 51 | } 52 | 53 | /// 54 | public uint GetDecompressedSize(Stream source) 55 | => source.Peek(s => InternalGetDecompressedSize(s, out _)); 56 | 57 | private static uint InternalGetDecompressedSize(Stream source, out DataType type) 58 | { 59 | type = source!.Read(); 60 | if (!Enum.IsDefined(typeof(DataType), type)) 61 | throw new InvalidIdentifierException(type.ToString("X"), "1 or 2"); 62 | return source!.ReadUInt24(Endian.Big); 63 | } 64 | 65 | /// 66 | public void Decompress(Stream source, Stream destination) 67 | { 68 | uint uncompressedSize = InternalGetDecompressedSize(source, out DataType type); 69 | DecompressHeaderless(source, destination, uncompressedSize); 70 | } 71 | 72 | /// 73 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 74 | => Compress(source, ReadOnlySpan.Empty, destination, level); 75 | 76 | /// 77 | public void Compress(ReadOnlySpan source, ReadOnlySpan extendData, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 78 | { 79 | destination.Write(extendData.IsEmpty ? DataType.Default : DataType.Extended); 80 | destination.Write((UInt24)source.Length, Endian.Big); 81 | CompressHeaderless(source, destination, LookAhead, level); 82 | destination.Write(extendData); 83 | } 84 | 85 | public static void DecompressHeaderless(Stream source, Stream destination, uint decomLength = 0) 86 | { 87 | long endPosition = destination.Position + decomLength; 88 | destination.SetLength(endPosition); 89 | FlagReader flag = new FlagReader(source, Endian.Big); 90 | using LzWindows buffer = new LzWindows(destination, _lz.WindowsSize); 91 | while (source.Position < source.Length) 92 | { 93 | if (flag.Readbit()) // Compressed 94 | { 95 | // DDDDLLLL DDDDDDDD 96 | byte b1 = source.ReadUInt8(); 97 | byte b2 = source.ReadUInt8(); 98 | int distance = (b1 & 0xF0) << 4 | b2; 99 | int length = (b1 & 0xF) + 1; // 1-16 length 100 | 101 | if (length == 1) 102 | { 103 | if (distance == 0) 104 | { 105 | if (destination.Position + buffer.Position > endPosition) 106 | { 107 | throw new DecompressedSizeException(decomLength, destination.Position + buffer.Position - (endPosition - decomLength)); 108 | } 109 | return; 110 | } 111 | length = source.ReadUInt8() + 17; // 17-272 length 112 | } 113 | 114 | buffer.BackCopy(distance, length); 115 | } 116 | else // Not compressed 117 | { 118 | buffer.WriteByte(source.ReadUInt8()); 119 | } 120 | } 121 | throw new EndOfStreamException(); 122 | } 123 | 124 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, bool lookAhead = true, CompressionLevel level = CompressionLevel.Optimal) 125 | { 126 | int sourcePointer = 0x0, matchPointer = 0x0; 127 | 128 | using PoolList matches = LZMatchFinder.FindMatchesParallel(source, _lz, lookAhead, level); 129 | using FlagWriter flag = new FlagWriter(destination, Endian.Big); 130 | while (sourcePointer < source.Length) 131 | { 132 | if (matchPointer < matches.Count && matches[matchPointer].Offset == sourcePointer) 133 | { 134 | LzMatch match = matches[matchPointer++]; 135 | 136 | int length = match.Length > 16 ? 0 : match.Length - 1; 137 | flag.Buffer.WriteByte((byte)((match.Distance >> 8 << 4) | length)); 138 | flag.Buffer.WriteByte((byte)(match.Distance & 0xFF)); 139 | if (length == 0) 140 | flag.Buffer.WriteByte((byte)(match.Length - 17)); 141 | 142 | sourcePointer += match.Length; 143 | flag.WriteBit(true); 144 | 145 | } 146 | else 147 | { 148 | flag.Buffer.WriteByte(source[sourcePointer++]); 149 | flag.WriteBit(false); 150 | } 151 | } 152 | flag.Buffer.Write(0); 153 | flag.Buffer.Write(0); 154 | flag.WriteBit(true); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/LZ4.FrameDescriptor.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Core.IO; 2 | using System; 3 | using System.Buffers.Binary; 4 | using System.IO; 5 | 6 | namespace AuroraLib.Compression.Algorithms 7 | { 8 | public sealed partial class LZ4 9 | { 10 | private sealed class FrameDescriptor : IBinaryObject 11 | { 12 | public FrameDescriptorFlags Flags; 13 | public BlockMaxSizes BlockMaxSize; 14 | public long ContentSize; 15 | public uint DictionaryID; 16 | public byte HeaderChecksum; 17 | 18 | public void BinaryDeserialize(Stream source) 19 | { 20 | Flags = source.Read(); 21 | byte BD = source.ReadUInt8(); 22 | BlockMaxSize = GetBlockMaxSize(BD); 23 | ContentSize = Flags.HasFlag(FrameDescriptorFlags.HasContentSize) ? source.ReadInt64() : 0; 24 | DictionaryID = Flags.HasFlag(FrameDescriptorFlags.HasDictID) ? source.ReadUInt32() : 0; 25 | HeaderChecksum = source.ReadUInt8(); 26 | } 27 | 28 | public void BinarySerialize(Stream dest) 29 | { 30 | Span bytes = stackalloc byte[GetDescriptorSize(Flags)]; 31 | BinarySerialize(bytes); 32 | dest.Write(bytes); 33 | } 34 | 35 | private void BinarySerialize(Span bytes) 36 | { 37 | bytes[0] = (byte)(Flags | FrameDescriptorFlags.IsVersion1); 38 | bytes[1] = BuildBDByte(); 39 | if (Flags.HasFlag(FrameDescriptorFlags.HasContentSize)) 40 | BinaryPrimitives.WriteInt64LittleEndian(bytes.Slice(2), ContentSize); 41 | if (Flags.HasFlag(FrameDescriptorFlags.HasDictID)) 42 | BinaryPrimitives.WriteUInt32LittleEndian(bytes.Slice(bytes.Length - 5), DictionaryID); 43 | 44 | bytes[bytes.Length - 1] = (byte)((HashAlgorithm!.Invoke(bytes.Slice(0, bytes.Length - 1)) >> 8) & 0xFF); 45 | } 46 | 47 | private BlockMaxSizes GetBlockMaxSize(byte BD) 48 | { 49 | // Extract block size bits (bits 4-6 of BD byte) 50 | switch ((BD & 0x70) >> 4) 51 | { 52 | case 4: 53 | return BlockMaxSizes.Block64KB; 54 | case 5: 55 | return BlockMaxSizes.Block256KB; 56 | case 6: 57 | return BlockMaxSizes.Block1MB; 58 | case 7: 59 | return BlockMaxSizes.Block4MB; 60 | default: 61 | throw new ArgumentOutOfRangeException(nameof(BD), "Invalid BlockMaxSize in BD byte"); 62 | } 63 | } 64 | 65 | private byte BuildBDByte() 66 | { 67 | switch (BlockMaxSize) 68 | { 69 | case BlockMaxSizes.Block64KB: 70 | return 0x40; 71 | case BlockMaxSizes.Block256KB: 72 | return 0x50; 73 | case BlockMaxSizes.Block1MB: 74 | return 0x60; 75 | case BlockMaxSizes.Block4MB: 76 | return 0x70; 77 | default: 78 | throw new ArgumentOutOfRangeException(nameof(BlockMaxSize), "Invalid BlockMaxSize value"); 79 | } 80 | } 81 | 82 | private static int GetDescriptorSize(FrameDescriptorFlags flag) 83 | { 84 | int size = 3; 85 | if ((flag & FrameDescriptorFlags.HasContentSize) != 0) 86 | size += 8; 87 | if ((flag & FrameDescriptorFlags.HasDictID) != 0) 88 | size += 4; 89 | return size; 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/LZ40.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Compression.IO; 4 | using AuroraLib.Compression.MatchFinder; 5 | using AuroraLib.Core; 6 | using AuroraLib.Core.Collections; 7 | using AuroraLib.Core.Exceptions; 8 | using AuroraLib.Core.Format; 9 | using AuroraLib.Core.IO; 10 | using System; 11 | using System.IO; 12 | using System.IO.Compression; 13 | 14 | namespace AuroraLib.Compression.Algorithms 15 | { 16 | /// 17 | /// Nintendo LZ40 compression algorithm similar to , mainly used in DS games. 18 | /// 19 | public sealed class LZ40 : ICompressionAlgorithm, ILzSettings, IProvidesDecompressedSize 20 | { 21 | private const byte Identifier = 0x40; 22 | 23 | /// 24 | public IFormatInfo Info => _info; 25 | 26 | private static readonly IFormatInfo _info = new FormatInfo("Nintendo LZ40", new MediaType(MIMEType.Application, "x-nintendo-lz40"), ".lz"); 27 | 28 | private static readonly LzProperties _lz = new LzProperties(0x1000, 0x4000, 3); 29 | 30 | /// 31 | public bool LookAhead { get; set; } = true; 32 | 33 | /// 34 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 35 | => IsMatchStatic(stream, fileNameAndExtension); 36 | 37 | /// 38 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 39 | // Has no distinct header, recognition is inaccurate! 40 | => stream.Peek(s => s.Position + 0x8 < s.Length && s.ReadByte() == Identifier && (s.ReadUInt24() != 0 || s.ReadUInt32() != 0)); 41 | 42 | /// 43 | public uint GetDecompressedSize(Stream source) 44 | => source.Peek(InternalGetDecompressedSize); 45 | 46 | protected static uint InternalGetDecompressedSize(Stream source) 47 | { 48 | byte identifier = source.ReadUInt8(); 49 | if (identifier != Identifier) 50 | throw new InvalidIdentifierException(identifier.ToString("X"), Identifier.ToString("X")); 51 | uint decompressedSize = source.ReadUInt24(); 52 | if (decompressedSize == 0) 53 | decompressedSize = source.ReadUInt32(); 54 | 55 | return decompressedSize; 56 | } 57 | 58 | /// 59 | public void Decompress(Stream source, Stream destination) 60 | { 61 | uint uncompressedSize = InternalGetDecompressedSize(source); 62 | DecompressHeaderless(source, destination, uncompressedSize); 63 | } 64 | 65 | /// 66 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 67 | { 68 | if (source.Length <= 0xFFFFFF) 69 | { 70 | destination.Write(Identifier | (source.Length << 8)); 71 | } 72 | else 73 | { 74 | destination.Write(Identifier | 0); 75 | destination.Write(source.Length); 76 | } 77 | 78 | CompressHeaderless(source, destination, LookAhead, level); 79 | } 80 | 81 | public static void DecompressHeaderless(Stream source, Stream destination, uint decomLength) 82 | { 83 | long endPosition = destination.Position + decomLength; 84 | destination.SetLength(endPosition); 85 | using (LzWindows buffer = new LzWindows(destination, _lz.WindowsSize)) 86 | { 87 | int flag = 0, flagbits = 0; 88 | 89 | while (destination.Position + buffer.Position < endPosition) 90 | { 91 | if (flagbits == 0) 92 | { 93 | // The flag value must be reversed. 94 | flag = (byte)-source.ReadByte(); 95 | flagbits = 8; 96 | } 97 | if ((flag & 0x80) != 0) // Compressed 98 | { 99 | //DDDDDDDD DDDDLLLL 100 | int distance = source.ReadUInt16(); 101 | int length = distance & 0xF; 102 | distance >>= 4; 103 | if (length <= 1) 104 | { 105 | if (length == 0) // match.Length 16-271 106 | { 107 | //DDDDDDDD DDDD0000 LLLLLLLL 108 | length = source.ReadUInt8() + 16; 109 | } 110 | else // match.Length 272-65808 111 | { 112 | //DDDDDDDD DDDD0001 LLLLLLLL LLLLLLLL 113 | length = source.ReadUInt16() + 272; 114 | } 115 | } 116 | buffer.BackCopy(distance, length); 117 | } 118 | else // Not compressed 119 | { 120 | buffer.WriteByte(source.ReadUInt8()); 121 | } 122 | flag <<= 1; 123 | flagbits--; 124 | } 125 | } 126 | if (destination.Position > endPosition) 127 | { 128 | throw new DecompressedSizeException(decomLength, destination.Position - (endPosition - decomLength)); 129 | } 130 | } 131 | 132 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, bool lookAhead = true, CompressionLevel level = CompressionLevel.Optimal) 133 | { 134 | int sourcePointer = 0x0, matchPointer = 0x0; 135 | 136 | using PoolList matches = LZMatchFinder.FindMatchesParallel(source, _lz, lookAhead, level); 137 | using FlagWriter flag = new FlagWriter(destination, Endian.Big, i => destination.WriteByte((byte)-i), 1); 138 | while (sourcePointer < source.Length) 139 | { 140 | if (matchPointer < matches.Count && matches[matchPointer].Offset == sourcePointer) 141 | { 142 | LzMatch match = matches[matchPointer++]; 143 | 144 | if (match.Length < 16) // match.Length 3-15 145 | { 146 | flag.Buffer.Write((ushort)(match.Distance << 4 | match.Length)); 147 | } 148 | else if (match.Length < 272) // match.Length 16-271 149 | { 150 | flag.Buffer.Write((ushort)(match.Distance << 4)); 151 | flag.Buffer.Write((byte)(match.Length - 16)); 152 | } 153 | else // match.Length 272-65808 154 | { 155 | flag.Buffer.Write((ushort)(match.Distance << 4 | 1)); 156 | flag.Buffer.Write((ushort)(match.Length - 272)); 157 | } 158 | 159 | sourcePointer += match.Length; 160 | flag.WriteBit(true); 161 | } 162 | else 163 | { 164 | flag.Buffer.WriteByte(source[sourcePointer++]); 165 | flag.WriteBit(false); 166 | } 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/LZ4Legacy.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core.Format; 3 | using AuroraLib.Core.Format.Identifier; 4 | using AuroraLib.Core.IO; 5 | using System; 6 | using System.IO; 7 | using System.IO.Compression; 8 | 9 | namespace AuroraLib.Compression.Algorithms 10 | { 11 | /// 12 | /// initial versions of “LZ4Demo”, known as LZ4Legacy. 13 | /// 14 | public sealed class LZ4Legacy : ICompressionAlgorithm, ILzSettings, IHasIdentifier 15 | { 16 | /// 17 | public IIdentifier Identifier => _identifier; 18 | 19 | private static readonly Identifier32 _identifier = new Identifier32((uint)LZ4.FrameTypes.Legacy); 20 | 21 | /// 22 | public IFormatInfo Info => _info; 23 | 24 | private static readonly IFormatInfo _info = new FormatInfo("LZ4 Legacy Compression", new MediaType(MIMEType.Application, "x-lz4demo"), ".lz4", _identifier); 25 | 26 | /// 27 | public bool LookAhead { get; set; } = true; 28 | 29 | private LZ4 algorithmlZ4 = new LZ4(); 30 | 31 | /// 32 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 33 | => IsMatchStatic(stream, fileNameAndExtension); 34 | 35 | /// 36 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 37 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 38 | 39 | /// 40 | public void Decompress(Stream source, Stream destination) 41 | => algorithmlZ4.Decompress(source, destination); 42 | 43 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 44 | { 45 | algorithmlZ4.FrameType = LZ4.FrameTypes.Legacy; 46 | algorithmlZ4.LookAhead = LookAhead; 47 | algorithmlZ4.Compress(source, destination, level); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/LZ60.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core.Exceptions; 3 | using AuroraLib.Core.Format; 4 | using AuroraLib.Core.IO; 5 | using System; 6 | using System.IO; 7 | using System.IO.Compression; 8 | 9 | namespace AuroraLib.Compression.Algorithms 10 | { 11 | /// 12 | /// Nintendo LZ60 compression algorithm same as . 13 | /// 14 | public sealed class LZ60 : ICompressionAlgorithm, ILzSettings, IProvidesDecompressedSize 15 | { 16 | private const byte Identifier = 0x60; 17 | 18 | /// 19 | public IFormatInfo Info => _info; 20 | 21 | private static readonly IFormatInfo _info = new FormatInfo("Nintendo LZ60", new MediaType(MIMEType.Application, "x-nintendo-lz60"), ".lz"); 22 | 23 | /// 24 | public bool LookAhead { get; set; } = true; 25 | 26 | /// 27 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 28 | => IsMatchStatic(stream, fileNameAndExtension); 29 | 30 | /// 31 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 32 | // Has no distinct header, recognition is inaccurate! 33 | => stream.Position + 0x8 < stream.Length && stream.Peek(s => s.ReadByte() == Identifier && (s.ReadUInt24() != 0 || s.ReadUInt32() != 0)); 34 | 35 | /// 36 | public uint GetDecompressedSize(Stream source) 37 | => source.Peek(InternalGetDecompressedSize); 38 | 39 | protected static uint InternalGetDecompressedSize(Stream source) 40 | { 41 | byte identifier = source.ReadUInt8(); 42 | if (identifier != Identifier) 43 | throw new InvalidIdentifierException(identifier.ToString("X"), Identifier.ToString("X")); 44 | uint decompressedSize = source.ReadUInt24(); 45 | if (decompressedSize == 0) 46 | decompressedSize = source.ReadUInt32(); 47 | 48 | return decompressedSize; 49 | } 50 | 51 | /// 52 | public void Decompress(Stream source, Stream destination) 53 | { 54 | uint uncompressedSize = InternalGetDecompressedSize(source); 55 | LZ40.DecompressHeaderless(source, destination, uncompressedSize); 56 | } 57 | 58 | /// 59 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 60 | { 61 | if (source.Length <= 0xFFFFFF) 62 | { 63 | destination.Write(Identifier | (source.Length << 8)); 64 | } 65 | else 66 | { 67 | destination.Write(Identifier | 0); 68 | destination.Write(source.Length); 69 | } 70 | 71 | LZ40.CompressHeaderless(source, destination, LookAhead, level); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/LZHudson.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Compression.IO; 3 | using AuroraLib.Core; 4 | using AuroraLib.Core.Format; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace AuroraLib.Compression.Algorithms 11 | { 12 | /// 13 | /// LZHudson is an LZ based compression algorithm used in Mario Party 4. 14 | /// 15 | public sealed class LZHudson : ICompressionAlgorithm, ILzSettings, IProvidesDecompressedSize 16 | { 17 | private static readonly string[] _extensions = new string[] { ".lzHudson" }; 18 | 19 | /// 20 | public IFormatInfo Info => _info; 21 | 22 | private static readonly IFormatInfo _info = new FormatInfo("LZHudson", new MediaType(MIMEType.Application, "x-lzhudson"), _extensions); 23 | 24 | private static readonly LzProperties _lz = new LzProperties(0x1000, 0xFF + 18, 3); 25 | 26 | /// 27 | public bool LookAhead { get; set; } = true; 28 | 29 | /// 30 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 31 | => IsMatchStatic(stream, fileNameAndExtension); 32 | 33 | /// 34 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 35 | => (fileNameAndExtension.IsEmpty || PathX.GetExtension(fileNameAndExtension).Contains(_extensions[0].AsSpan(), StringComparison.InvariantCultureIgnoreCase)) && stream.Position + 0x8 < stream.Length && stream.Peek(Endian.Big) != 0; 36 | 37 | /// 38 | public uint GetDecompressedSize(Stream source) 39 | => source.Peek(Endian.Big); 40 | 41 | /// 42 | public void Decompress(Stream source, Stream destination) 43 | { 44 | uint decompressedSize = source.ReadUInt32(Endian.Big); 45 | DecompressHeaderless(source, destination, decompressedSize); 46 | } 47 | 48 | /// 49 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 50 | { 51 | destination.Write(source.Length, Endian.Big); 52 | CompressHeaderless(source, destination, LookAhead, level); 53 | } 54 | 55 | public static void DecompressHeaderless(Stream source, Stream destination, uint decomLength) 56 | => Yay0.DecompressHeaderless(new FlagReader(source, Endian.Big, 4, Endian.Big), source, source, destination, decomLength); 57 | 58 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, bool lookAhead = true, CompressionLevel level = CompressionLevel.Optimal) 59 | { 60 | using FlagWriter flag = new FlagWriter(destination, Endian.Big, 4, Endian.Big); 61 | Yay0.CompressHeaderless(source, flag.Buffer, flag.Buffer, flag, lookAhead, level); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/LZSega.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core; 3 | using AuroraLib.Core.Format; 4 | using AuroraLib.Core.IO; 5 | using System; 6 | using System.IO; 7 | using System.IO.Compression; 8 | 9 | namespace AuroraLib.Compression.Algorithms 10 | { 11 | /// 12 | /// This LZSS header was used by Sega in early GameCube games like F-zero GX or Super Monkey Ball. 13 | /// 14 | public sealed class LZSega : ICompressionAlgorithm, ILzSettings, IProvidesDecompressedSize 15 | { 16 | /// 17 | public IFormatInfo Info => _info; 18 | 19 | private static readonly IFormatInfo _info = new FormatInfo("LZ Sega", new MediaType(MIMEType.Application, "x-lzss+sega"), ".lz"); 20 | 21 | /// 22 | public bool LookAhead { get; set; } = true; 23 | 24 | private static readonly LzProperties _lz = LZSS.DefaultProperties; 25 | 26 | /// 27 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 28 | => IsMatchStatic(stream, fileNameAndExtension); 29 | 30 | /// 31 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 32 | { 33 | if (stream.Length < 0x12) 34 | return false; 35 | 36 | long pos = stream.Position; 37 | uint compressedSize = stream.ReadUInt32(); 38 | uint decompressedSize = stream.ReadUInt32(); 39 | stream.Position = pos; 40 | return (compressedSize == stream.Length - 8 || compressedSize == stream.Length) && decompressedSize != compressedSize && decompressedSize >= 0x20; 41 | } 42 | 43 | /// 44 | public uint GetDecompressedSize(Stream source) 45 | => source.Peek(s => 46 | { 47 | s.Position =+ 4; 48 | return s.ReadUInt32(); 49 | }); 50 | 51 | /// 52 | public void Decompress(Stream source, Stream destination) 53 | { 54 | uint compressedSize = source.ReadUInt32(); 55 | uint decompressedSize = source.ReadUInt32(); 56 | LZSS.DecompressHeaderless(source, destination, decompressedSize, _lz); 57 | } 58 | 59 | /// 60 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 61 | { 62 | long destinationStartPosition = destination.Position; 63 | destination.Write(0); // Compressed length (will be filled in later) 64 | destination.Write(source.Length); 65 | LZSS.CompressHeaderless(source, destination, _lz, LookAhead, level); 66 | 67 | // Go back to the beginning of the file and write out the compressed length 68 | int destinationLength = (int)(destination.Position - destinationStartPosition - 0x8); 69 | destination.At(destinationStartPosition, x => x.Write(destinationLength)); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/Level5.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core; 3 | using AuroraLib.Core.Buffers; 4 | using AuroraLib.Core.Format; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace AuroraLib.Compression.Algorithms 11 | { 12 | /// 13 | /// Level5 compression algorithm, mainly used in Level5 3ds games. 14 | /// 15 | public class Level5 : ICompressionAlgorithm, ILzSettings, IProvidesDecompressedSize 16 | { 17 | private static readonly string[] _extensions = new string[] { ".Level5" }; 18 | 19 | /// 20 | public IFormatInfo Info => _info; 21 | 22 | private static readonly IFormatInfo _info = new FormatInfo("Level5 compression", new MediaType(MIMEType.Application, "x-level5-compressed"), _extensions); 23 | 24 | /// 25 | public bool LookAhead { get; set; } = true; 26 | 27 | /// 28 | /// Specifies the type of compression used. 29 | /// 30 | public CompressionType Type = CompressionType.LZ10; 31 | 32 | /// 33 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 34 | => IsMatchStatic(stream, fileNameAndExtension); 35 | 36 | /// 37 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 38 | { 39 | if (stream.Length < 0x10) 40 | return false; 41 | 42 | uint typeAndSize = stream.ReadUInt32(); 43 | int decompressedSize = (int)(typeAndSize >> 3); 44 | bool isZLib = typeAndSize != 0 && stream.PeekByte() == 0x78 && stream.Peek().Validate(); 45 | stream.Position -= 4; 46 | return isZLib || (fileNameAndExtension.IsEmpty || PathX.GetExtension(fileNameAndExtension).Contains(_extensions[0].AsSpan(), StringComparison.InvariantCultureIgnoreCase)) && Enum.IsDefined(typeof(CompressionType), (CompressionType)(typeAndSize & 0x7)) && decompressedSize != 0; 47 | } 48 | 49 | /// 50 | public uint GetDecompressedSize(Stream source) 51 | => source.Peek(s => 52 | { 53 | uint typeAndSize = s.ReadUInt32(); 54 | return s.Peek() == 0x78 ? typeAndSize : typeAndSize >> 3; 55 | }); 56 | 57 | /// 58 | public void Decompress(Stream source, Stream destination) 59 | { 60 | uint typeAndSize = source.ReadUInt32(); 61 | if (source.Peek() == 0x78) // Zlib Type 62 | { 63 | new ZLib().Decompress(source, destination); 64 | return; 65 | } 66 | CompressionType type = (CompressionType)(typeAndSize & 0x7); 67 | uint decompressedSize = typeAndSize >> 3; 68 | 69 | switch (type) 70 | { 71 | case CompressionType.OnlySave: 72 | using (SpanBuffer buffer = new SpanBuffer(decompressedSize)) 73 | { 74 | source.Read(buffer); 75 | destination.Write(buffer); 76 | } 77 | break; 78 | case CompressionType.LZ10: 79 | LZ10.DecompressHeaderless(source, destination, decompressedSize); 80 | break; 81 | case CompressionType.RLE: 82 | RLE30.DecompressHeaderless(source, destination, decompressedSize); 83 | break; 84 | case CompressionType.Huffman4Bit: 85 | HUF20.DecompressHeaderless(source, destination, (int)decompressedSize, 4, Endian.Big); 86 | break; 87 | case CompressionType.Huffman8Bit: 88 | HUF20.DecompressHeaderless(source, destination, (int)decompressedSize, 8, Endian.Big); 89 | break; 90 | default: 91 | throw new NotSupportedException(); 92 | } 93 | } 94 | 95 | /// 96 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 97 | { 98 | if (Type == CompressionType.ZLib) // Zlib Type 99 | { 100 | destination.Write(source.Length); 101 | new ZLib().Compress(source, destination, level); 102 | } 103 | 104 | if (level == CompressionLevel.NoCompression) 105 | Type = CompressionType.OnlySave; 106 | 107 | if (source.Length > 0x1fffffff) 108 | { 109 | new ArgumentOutOfRangeException($"{nameof(Level5)} does not support files larger than 0x1fffffff."); 110 | } 111 | destination.Write((int)Type | (source.Length << 3)); 112 | 113 | switch (Type) 114 | { 115 | case CompressionType.OnlySave: 116 | destination.Write(source); 117 | break; 118 | case CompressionType.LZ10: 119 | LZ10.CompressHeaderless(source, destination, LookAhead, level); 120 | break; 121 | case CompressionType.RLE: 122 | RLE30.CompressHeaderless(source, destination); 123 | break; 124 | case CompressionType.Huffman4Bit: 125 | HUF20.CompressHeaderless(source, destination, 4, Endian.Big); 126 | break; 127 | case CompressionType.Huffman8Bit: 128 | HUF20.CompressHeaderless(source, destination, 8, Endian.Big); 129 | break; 130 | default: 131 | throw new NotSupportedException(); 132 | } 133 | } 134 | 135 | public enum CompressionType 136 | { 137 | OnlySave = 0, 138 | LZ10 = 1, 139 | Huffman4Bit = 2, 140 | Huffman8Bit = 3, 141 | RLE = 4, 142 | ZLib = 8 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/MDF0.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Core.Format; 4 | using AuroraLib.Core.Format.Identifier; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace AuroraLib.Compression.Algorithms 11 | { 12 | /// 13 | /// Konami MDF0 based on ZLib compression algorithm used in Castlevania: The Adventure ReBirth. 14 | /// 15 | public sealed class MDF0 : ICompressionAlgorithm, IHasIdentifier, IProvidesDecompressedSize 16 | { 17 | /// 18 | public IIdentifier Identifier => _identifier; 19 | 20 | private static readonly Identifier32 _identifier = new Identifier32((byte)'m', (byte)'d', (byte)'f', 0x0); 21 | 22 | /// 23 | public IFormatInfo Info => _info; 24 | 25 | private static readonly IFormatInfo _info = new FormatInfo("Konami MDF0", new MediaType(MIMEType.Application, "zlib+mdf0"), string.Empty, _identifier); 26 | 27 | private static readonly ZLib zLib = new ZLib(); 28 | 29 | /// 30 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 31 | => IsMatchStatic(stream, fileNameAndExtension); 32 | 33 | /// 34 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 35 | => stream.Position + 0x14 < stream.Length && stream.Peek(s => s.Match(_identifier) && s.ReadUInt32() != 0 && s.Read().Validate()); 36 | 37 | /// 38 | public uint GetDecompressedSize(Stream source) 39 | => source.Peek(s => 40 | { 41 | s.MatchThrow(_identifier); 42 | return s.ReadUInt32(); 43 | }); 44 | 45 | /// 46 | public void Decompress(Stream source, Stream destination) 47 | { 48 | long start = destination.Position; 49 | source.MatchThrow(_identifier); 50 | uint decompressedSize = source.ReadUInt32(); 51 | zLib.Decompress(source, destination); 52 | 53 | if (destination.Position - start != decompressedSize) 54 | { 55 | throw new DecompressedSizeException(decompressedSize, destination.Position - start); 56 | } 57 | } 58 | 59 | /// 60 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 61 | { 62 | destination.Write(_identifier); 63 | destination.Write(source.Length); // decompressedSize 64 | zLib.Compress(source, destination, level); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/RLHudson.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Core; 4 | using AuroraLib.Core.Format; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.IO; 8 | using System.IO.Compression; 9 | using System.Runtime.CompilerServices; 10 | 11 | namespace AuroraLib.Compression.Algorithms 12 | { 13 | /// 14 | /// Run-Length Encoding algorithm used by Hudson Soft. 15 | /// 16 | public sealed class RLHudson : ICompressionAlgorithm, IProvidesDecompressedSize 17 | { 18 | private const int Identifier = 0x5; 19 | private const int FlagMask = 0x80; 20 | private const int MaxLength = 0x7F; 21 | 22 | /// 23 | public IFormatInfo Info => _info; 24 | 25 | private static readonly IFormatInfo _info = new FormatInfo("Hudson RLE", new MediaType(MIMEType.Application, "x-hudson-rle"), string.Empty); 26 | 27 | /// 28 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 29 | => IsMatchStatic(stream, fileNameAndExtension); 30 | 31 | /// 32 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 33 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.ReadInt32(Endian.Big) != 0 && s.ReadInt32(Endian.Big) == Identifier); 34 | 35 | /// 36 | public uint GetDecompressedSize(Stream source) 37 | => source.Peek(Endian.Big); 38 | 39 | /// 40 | public void Decompress(Stream source, Stream destination) 41 | { 42 | uint uncompressedSize = source.ReadUInt32(Endian.Big); 43 | _ = source.ReadUInt32(Endian.Big); 44 | 45 | DecompressHeaderless(source, destination, (int)uncompressedSize); 46 | } 47 | 48 | /// 49 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 50 | { 51 | destination.Write(source.Length, Endian.Big); 52 | destination.Write(Identifier, Endian.Big); 53 | 54 | CompressHeaderless(source, destination); 55 | } 56 | 57 | public static void DecompressHeaderless(Stream source, Stream destination, int decomLength) 58 | { 59 | long endPosition = destination.Position + decomLength; 60 | destination.SetLength(endPosition); 61 | Span bytes = stackalloc byte[MaxLength]; 62 | 63 | while (destination.Position < endPosition) 64 | { 65 | int flag = source.ReadByte(); 66 | int length = flag & MaxLength; 67 | Span section = bytes.Slice(0, length); 68 | if (flag < FlagMask) 69 | { 70 | section.Fill(source.ReadUInt8()); 71 | } 72 | else 73 | { 74 | if (source.Read(section) != length) 75 | throw new EndOfStreamException(); 76 | } 77 | destination.Write(section); 78 | } 79 | 80 | if (destination.Position > endPosition) 81 | { 82 | throw new DecompressedSizeException(decomLength, destination.Position - (endPosition - decomLength)); 83 | } 84 | } 85 | 86 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination) 87 | { 88 | int sourcePointer = 0x0; 89 | RleMatchFinder matchFinder = new RleMatchFinder(3, MaxLength); 90 | 91 | while (sourcePointer < source.Length) 92 | { 93 | if (matchFinder.TryToFindMatch(source, sourcePointer, out int duration)) 94 | { 95 | destination.WriteByte((byte)duration); 96 | destination.Write(source[sourcePointer]); 97 | } 98 | else 99 | { 100 | destination.WriteByte((byte)(duration | FlagMask)); 101 | destination.Write(source.Slice(sourcePointer, duration)); 102 | } 103 | sourcePointer += duration; 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/SSZL.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core; 3 | using AuroraLib.Core.Format; 4 | using AuroraLib.Core.Format.Identifier; 5 | using AuroraLib.Core.IO; 6 | using System; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace AuroraLib.Compression.Algorithms 11 | { 12 | /// 13 | /// Level5 SSZL algorithm base on LZSS, first used in Inazuma Eleven 3. 14 | /// 15 | public class SSZL : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IProvidesDecompressedSize 16 | { 17 | /// 18 | public IIdentifier Identifier => _identifier; 19 | 20 | private static readonly Identifier32 _identifier = new Identifier32("SSZL".AsSpan()); 21 | 22 | /// 23 | public IFormatInfo Info => _info; 24 | 25 | private static readonly IFormatInfo _info = new FormatInfo("Level5 lzss", new MediaType(MIMEType.Application, "x-lzss+level5"), string.Empty, _identifier); 26 | 27 | /// 28 | public bool LookAhead { get; set; } = true; 29 | 30 | /// 31 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 32 | => IsMatchStatic(stream, fileNameAndExtension); 33 | 34 | /// 35 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 36 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier) && s.ReadUInt32() == 0); 37 | 38 | /// 39 | public uint GetDecompressedSize(Stream source) 40 | => source.Peek(s => 41 | { 42 | s.MatchThrow(_identifier); 43 | s.Position += 8; 44 | return s.ReadUInt32(); 45 | }); 46 | 47 | /// 48 | public void Decompress(Stream source, Stream destination) 49 | { 50 | // Read Header 51 | source.MatchThrow(_identifier); 52 | _ = source.ReadUInt32(); 53 | uint compressedSize = source.ReadUInt32(); 54 | uint decompressedSize = source.ReadUInt32(); 55 | 56 | // Mark the initial positions of the streams 57 | long compressedStartPosition = source.Position; 58 | 59 | // Perform the decompression 60 | LZSS.DecompressHeaderless(source, destination, decompressedSize, LZSS.Lzss0Properties); 61 | 62 | // Verify compressed size and handle mismatches 63 | Helper.TraceIfCompressedSizeMismatch(source.Position - compressedStartPosition, compressedSize); 64 | } 65 | 66 | /// 67 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 68 | { 69 | long startpos = destination.Length; 70 | 71 | destination.Write(_identifier); 72 | destination.Write(0); 73 | destination.Write(0); // Placeholder 74 | destination.Write(source.Length); 75 | 76 | LZSS.CompressHeaderless(source, destination, LZSS.Lzss0Properties, LookAhead, level); 77 | destination.At(startpos + 0x8, s => s.Write((uint)(destination.Length - startpos - 0x10))); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Algorithms/ZLB.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Core; 4 | using AuroraLib.Core.Format; 5 | using AuroraLib.Core.Format.Identifier; 6 | using AuroraLib.Core.IO; 7 | using System; 8 | using System.Buffers.Binary; 9 | using System.Diagnostics; 10 | using System.IO; 11 | using System.IO.Compression; 12 | 13 | namespace AuroraLib.Compression.Algorithms 14 | { 15 | /// 16 | /// ZLB based on ZLib compression algorithm used in Star Fox Adventures. 17 | /// 18 | public sealed class ZLB : ICompressionAlgorithm, IHasIdentifier, IProvidesDecompressedSize 19 | { 20 | /// 21 | public IIdentifier Identifier => _identifier; 22 | 23 | private static readonly Identifier32 _identifier = new Identifier32((byte)'Z', (byte)'L', (byte)'B', 0x0); 24 | 25 | /// 26 | public IFormatInfo Info => _info; 27 | 28 | private static readonly IFormatInfo _info = new FormatInfo("ZLB", new MediaType(MIMEType.Application, "zlib+zlb"), ".zlb", _identifier); 29 | 30 | private static readonly ZLib zLib = new ZLib(); 31 | 32 | /// 33 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 34 | => IsMatchStatic(stream, fileNameAndExtension); 35 | 36 | /// 37 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 38 | => stream.Position + 0x14 < stream.Length && stream.Peek(s => s.Match(_identifier)); 39 | 40 | /// 41 | public uint GetDecompressedSize(Stream source) 42 | => source.Peek(s => 43 | { 44 | s.MatchThrow(_identifier); 45 | return s.Read
(Endian.Big).DecompressedSize; 46 | }); 47 | 48 | /// 49 | public void Decompress(Stream source, Stream destination) 50 | { 51 | // Read Header 52 | source.MatchThrow(_identifier); 53 | Header header = source.Read
(Endian.Big); 54 | 55 | // Validate header version 56 | if (header.Version != 1) 57 | { 58 | Trace.WriteLine($"Unexpected header version: {header.Version}"); 59 | } 60 | 61 | // Mark the initial positions of the streams 62 | long compressedStartPosition = source.Position; 63 | long destinationStartPosition = destination.Position; 64 | 65 | // Perform the decompression 66 | zLib.Decompress(source, destination, (int)header.CompressedSize); 67 | 68 | // Verify decompressed size 69 | DecompressedSizeException.ThrowIfMismatch(destination.Position - destinationStartPosition, header.DecompressedSize); 70 | 71 | // Verify compressed size and handle mismatches 72 | Helper.TraceIfCompressedSizeMismatch(source.Position - compressedStartPosition, header.CompressedSize); 73 | } 74 | 75 | /// 76 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 77 | { 78 | // Mark the initial positions of the destination 79 | long start = destination.Position; 80 | 81 | // Write Header 82 | destination.Write(_identifier); 83 | destination.Write(1, Endian.Big); 84 | destination.Write(source.Length, Endian.Big); 85 | destination.Write(0, Endian.Big); // Placeholder 86 | 87 | // Perform the compression 88 | zLib.Compress(source, destination, level); 89 | 90 | // Go back to the beginning of the file and write out the compressed length 91 | destination.At(start + 12, s => s.Write((uint)(destination.Length - start - 0x14), Endian.Big)); 92 | } 93 | 94 | private readonly struct Header : IReversibleEndianness
95 | { 96 | public readonly uint Version; // 1 97 | public readonly uint DecompressedSize; 98 | public readonly uint CompressedSize; 99 | 100 | public Header(uint version, uint decompressedSize, uint compressedSize) 101 | { 102 | Version = version; 103 | DecompressedSize = decompressedSize; 104 | CompressedSize = compressedSize; 105 | } 106 | 107 | public Header ReverseEndianness() 108 | => new Header( 109 | BinaryPrimitives.ReverseEndianness(Version), 110 | BinaryPrimitives.ReverseEndianness(DecompressedSize), 111 | BinaryPrimitives.ReverseEndianness(CompressedSize)); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/AuroraLib.Compression-Extended.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;net6.0;netstandard2.0;netstandard2.1;net472; 5 | AuroraLib.Compression 6 | disable 7 | enable 8 | True 9 | AuroraLib.Compression-Extended 10 | 1.0.1.0 11 | $(Version) 12 | MIT 13 | false 14 | icon-ex.png 15 | README.md 16 | https://github.com/Venomalia/AuroraLib.Compression 17 | https://github.com/Venomalia/AuroraLib.Compression 18 | Venomalia 19 | 20 | Has additional specific compression algorithms that are rarely used. 21 | 22 | true 23 | |netstandard2.0|net481|net48|net472|NET471|NET47|NET462|NET461| 24 | Debug;Release;Optimized 25 | 26 | 27 | 28 | 8.0 29 | 30 | 31 | 32 | 33 | True 34 | \ 35 | 36 | 37 | True 38 | \ 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 1.5.0 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression-Extended/Helper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace AuroraLib.Compression 4 | { 5 | internal static class Helper 6 | { 7 | internal static void TraceIfCompressedSizeMismatch(long actual, long expected) 8 | { 9 | if (actual != expected) 10 | { 11 | Trace.WriteLine($"Warning: Expected {expected} bytes of compressed data, but read {actual} bytes."); 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/ALLZ.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Compression.IO; 3 | using AuroraLib.Compression.MatchFinder; 4 | using AuroraLib.Core; 5 | using AuroraLib.Core.Buffers; 6 | using AuroraLib.Core.Collections; 7 | using AuroraLib.Core.Format; 8 | using AuroraLib.Core.Format.Identifier; 9 | using AuroraLib.Core.IO; 10 | using System; 11 | using System.IO; 12 | using System.IO.Compression; 13 | 14 | namespace AuroraLib.Compression.Algorithms 15 | { 16 | /// 17 | /// Aqualead LZ compression algorithm, used in games that utilize the Aqualead framework. 18 | /// 19 | public sealed class ALLZ : ICompressionAlgorithm, IHasIdentifier, IProvidesDecompressedSize 20 | { 21 | /// 22 | public IIdentifier Identifier => _identifier; 23 | 24 | private static readonly Identifier32 _identifier = new Identifier32("ALLZ".AsSpan()); 25 | 26 | /// 27 | public IFormatInfo Info => _info; 28 | 29 | private static readonly IFormatInfo _info = new FormatInfo("Aqualead LZ", new MediaType(MIMEType.Application, "x-aqualead-lz"), string.Empty, _identifier); 30 | 31 | internal static readonly LzProperties _lz = new LzProperties(0x20000, 0x40000, 3); //A larger window is possible but will take a lot longer. 32 | 33 | public byte LzCopyBits = 0; 34 | public byte LzDistanceBits = 14; 35 | public byte LzLengthBits = 1; 36 | 37 | /// 38 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 39 | => IsMatchStatic(stream, fileNameAndExtension); 40 | 41 | /// 42 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 43 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 44 | 45 | /// 46 | public uint GetDecompressedSize(Stream source) 47 | => source.Peek(s => 48 | { 49 | s.MatchThrow(_identifier); 50 | s.Position += 4; 51 | return s.ReadUInt32(); 52 | }); 53 | 54 | /// 55 | public void Decompress(Stream source, Stream destination) 56 | { 57 | // Read Header 58 | source.MatchThrow(_identifier); 59 | Span flags = stackalloc byte[4]; 60 | source.Read(flags); 61 | uint decompressedSize = source.ReadUInt32(); 62 | 63 | // Perform the decompression 64 | using SpanBuffer buffer = new SpanBuffer(decompressedSize); 65 | DecompressHeaderless(source, buffer, flags); 66 | destination.Write(buffer); 67 | } 68 | 69 | /// 70 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 71 | { 72 | // Write Header 73 | destination.Write(_identifier); 74 | Span flags = stackalloc byte[4] { 0, LzCopyBits, LzDistanceBits, LzLengthBits }; 75 | destination.Write(flags); 76 | destination.Write(source.Length); 77 | 78 | // Perform the compression 79 | CompressHeaderless(source, destination, flags, true, level); 80 | } 81 | 82 | public static void DecompressHeaderless(Stream source, Span destination, ReadOnlySpan flags) 83 | { 84 | int length, distance, destinationPointer = 0; 85 | FlagReader flag = new FlagReader(source, Endian.Little); 86 | 87 | while (destinationPointer < destination.Length) 88 | { 89 | if (!flag.Readbit()) 90 | { 91 | length = ReadALFlag(flags[3]) + 1; 92 | source.Read(destination.Slice(destinationPointer, length)); 93 | destinationPointer += length; 94 | } 95 | 96 | if (destinationPointer < destination.Length) 97 | { 98 | distance = ReadALFlag(flags[2]) + 1; 99 | length = ReadALFlag(flags[1]) + 3; 100 | 101 | while (length-- > 0) 102 | { 103 | destination[destinationPointer] = destination[destinationPointer - distance]; 104 | destinationPointer++; 105 | } 106 | } 107 | } 108 | return; 109 | 110 | int ReadALFlag(int startBits) 111 | { 112 | int bits = startBits; 113 | while (flag.Readbit()) 114 | bits++; 115 | int result = flag.ReadInt(bits); 116 | result += ((1 << (bits - startBits)) - 1) << startBits; 117 | return result; 118 | } 119 | } 120 | 121 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, ReadOnlySpan flags, bool lookAhead = true, CompressionLevel level = CompressionLevel.Optimal) 122 | { 123 | int sourcePointer = 0x0, plainSize = 0; 124 | using PoolList matches = LZMatchFinder.FindMatchesParallel(source, _lz, lookAhead, level); 125 | using FlagWriter flag = new FlagWriter(destination, Endian.Little); 126 | matches.Add(new LzMatch(source.Length, 0, 0)); // Dummy-Match 127 | foreach (LzMatch match in matches) 128 | { 129 | plainSize = match.Offset - sourcePointer; 130 | if (plainSize > 0) 131 | { 132 | flag.WriteBit(false); 133 | WriteALFlag(flags[3], plainSize - 1); 134 | flag.Buffer.Write(source.Slice(sourcePointer, plainSize)); 135 | sourcePointer += plainSize; 136 | flag.FlushIfNecessary(); 137 | } 138 | else 139 | { 140 | flag.WriteBit(true); 141 | } 142 | 143 | WriteALFlag(flags[2], match.Distance - 1); 144 | WriteALFlag(flags[1], match.Length - 3); 145 | sourcePointer += match.Length; 146 | } 147 | 148 | return; 149 | 150 | void WriteALFlag(int startBits, int value) 151 | { 152 | int bitMask = 0, bits = startBits; 153 | 154 | while (bitMask + ((1 << bits) - 1) < value) 155 | { 156 | bits++; 157 | bitMask = ((1 << (bits - startBits)) - 1) << startBits; 158 | flag.WriteBit(true); 159 | } 160 | flag.WriteBit(false); 161 | value -= bitMask; 162 | flag.WriteInt(value, bits); 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/GZip.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core.Format; 3 | using AuroraLib.Core.IO; 4 | using System; 5 | using System.IO; 6 | using System.IO.Compression; 7 | 8 | namespace AuroraLib.Compression.Algorithms 9 | { 10 | /// 11 | /// GZip open-source compression algorithm, which is based on the DEFLATE compression format. 12 | /// 13 | public sealed class GZip : ICompressionAlgorithm 14 | { 15 | /// 16 | public IFormatInfo Info => _info; 17 | 18 | private static readonly IFormatInfo _info = new FormatInfo("gzip", new MediaType(MIMEType.Application, "gzip"), ".gz"); 19 | 20 | /// 21 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 22 | => IsMatchStatic(stream, fileNameAndExtension); 23 | 24 | /// 25 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 26 | => stream.Position + 0x8 < stream.Length && stream.Peek() == 35615; 27 | 28 | /// 29 | public void Decompress(Stream source, Stream destination) 30 | { 31 | using (GZipStream algo = new GZipStream(source, CompressionMode.Decompress, true)) 32 | algo.CopyTo(destination); 33 | source.Position = source.Length; 34 | } 35 | 36 | /// 37 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 38 | { 39 | using (GZipStream algo = new GZipStream(destination, level, true)) 40 | algo.Write(source); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/LZ10.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Compression.IO; 4 | using AuroraLib.Compression.MatchFinder; 5 | using AuroraLib.Core; 6 | using AuroraLib.Core.Collections; 7 | using AuroraLib.Core.Exceptions; 8 | using AuroraLib.Core.Format; 9 | using AuroraLib.Core.IO; 10 | using System; 11 | using System.IO; 12 | using System.IO.Compression; 13 | 14 | namespace AuroraLib.Compression.Algorithms 15 | { 16 | /// 17 | /// Nintendo LZ10 compression algorithm based on LZ77, mainly used in GBA, DS and WII games. 18 | /// 19 | public class LZ10 : ICompressionAlgorithm, ILzSettings, IProvidesDecompressedSize 20 | { 21 | private const byte Identifier = 0x10; 22 | 23 | /// 24 | public virtual IFormatInfo Info => _info; 25 | 26 | private static readonly IFormatInfo _info = new FormatInfo("Nintendo LZ10", new MediaType(MIMEType.Application, "x-nintendo-lz10"), ".lz"); 27 | 28 | internal static readonly LzProperties _lz = new LzProperties(0x1000, 18, 3); 29 | 30 | /// 31 | public bool LookAhead { get; set; } = true; 32 | 33 | /// 34 | public virtual bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 35 | => IsMatchStatic(stream, fileNameAndExtension); 36 | 37 | /// 38 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 39 | // Has no distinct header, recognition is inaccurate! 40 | => stream.Position + 0x8 < stream.Length && stream.Peek(s => s.ReadByte() == Identifier && (s.ReadUInt24() != 0 || s.ReadUInt32() != 0) && (s.ReadUInt8() & 0xC0) == 0); 41 | 42 | /// 43 | public virtual uint GetDecompressedSize(Stream source) 44 | => source.Peek(InternalGetDecompressedSize); 45 | 46 | protected static uint InternalGetDecompressedSize(Stream source) 47 | { 48 | byte identifier = source.ReadUInt8(); 49 | if (identifier != Identifier) 50 | throw new InvalidIdentifierException(identifier.ToString("X"), Identifier.ToString("X")); 51 | uint decompressedSize = source.ReadUInt24(); 52 | if (decompressedSize == 0) 53 | decompressedSize = source.ReadUInt32(); 54 | 55 | return decompressedSize; 56 | } 57 | 58 | /// 59 | public virtual void Decompress(Stream source, Stream destination) 60 | { 61 | uint decompressedSize = InternalGetDecompressedSize(source); 62 | DecompressHeaderless(source, destination, decompressedSize); 63 | } 64 | 65 | /// 66 | public virtual void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 67 | { 68 | if (source.Length <= 0xFFFFFF) 69 | { 70 | destination.Write(Identifier | (source.Length << 8)); 71 | } 72 | else 73 | { 74 | destination.Write(Identifier | 0); 75 | destination.Write(source.Length); 76 | } 77 | 78 | CompressHeaderless(source, destination, LookAhead, level); 79 | } 80 | 81 | public static void DecompressHeaderless(Stream source, Stream destination, uint decomLength) 82 | { 83 | long endPosition = destination.Position + decomLength; 84 | destination.SetLength(endPosition); 85 | using (LzWindows buffer = new LzWindows(destination, _lz.WindowsSize)) 86 | { 87 | FlagReader flag = new FlagReader(source, Endian.Big); 88 | 89 | while (destination.Position + buffer.Position < endPosition) 90 | { 91 | if (flag.Readbit()) // Compressed 92 | { 93 | byte b1 = source.ReadUInt8(); 94 | byte b2 = source.ReadUInt8(); 95 | int distance = ((b1 & 0xf) << 8 | b2) + 1; 96 | int length = (b1 >> 4) + 3; 97 | buffer.BackCopy(distance, length); 98 | } 99 | else // Not compressed 100 | { 101 | buffer.WriteByte(source.ReadUInt8()); 102 | } 103 | } 104 | } 105 | 106 | if (destination.Position > endPosition) 107 | { 108 | throw new DecompressedSizeException(decomLength, destination.Position - (endPosition - decomLength)); 109 | } 110 | } 111 | 112 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, bool lookAhead = true, CompressionLevel level = CompressionLevel.Optimal) 113 | { 114 | int sourcePointer = 0x0, matchPointer = 0x0; 115 | 116 | using PoolList matches = LZMatchFinder.FindMatchesParallel(source, _lz, lookAhead, level); 117 | using FlagWriter flag = new FlagWriter(destination, Endian.Big); 118 | while (sourcePointer < source.Length) 119 | { 120 | if (matchPointer < matches.Count && matches[matchPointer].Offset == sourcePointer) 121 | { 122 | LzMatch match = matches[matchPointer++]; 123 | 124 | flag.Buffer.Write((ushort)((match.Length - 3) << 12 | ((match.Distance - 1) & 0xFFF)), Endian.Big); 125 | sourcePointer += match.Length; 126 | flag.WriteBit(true); 127 | 128 | } 129 | else 130 | { 131 | flag.Buffer.WriteByte(source[sourcePointer++]); 132 | flag.WriteBit(false); 133 | } 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/LZ11.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Compression.IO; 4 | using AuroraLib.Compression.MatchFinder; 5 | using AuroraLib.Core; 6 | using AuroraLib.Core.Collections; 7 | using AuroraLib.Core.Exceptions; 8 | using AuroraLib.Core.Format; 9 | using AuroraLib.Core.IO; 10 | using System; 11 | using System.IO; 12 | using System.IO.Compression; 13 | 14 | namespace AuroraLib.Compression.Algorithms 15 | { 16 | /// 17 | /// Nintendo LZ11 compression algorithm extension of the algorithm, mainly used in DS and WII games. 18 | /// 19 | public class LZ11 : ICompressionAlgorithm, ILzSettings, IProvidesDecompressedSize 20 | { 21 | private const byte Identifier = 0x11; 22 | 23 | /// 24 | public virtual IFormatInfo Info => _info; 25 | 26 | private static readonly IFormatInfo _info = new FormatInfo("Nintendo LZ11", new MediaType(MIMEType.Application, "x-nintendo-lz11"), ".lz"); 27 | 28 | private static readonly LzProperties _lz = new LzProperties(0x1000, 0x4000, 3); 29 | 30 | /// 31 | public bool LookAhead { get; set; } = true; 32 | 33 | /// 34 | public virtual bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 35 | => IsMatchStatic(stream, fileNameAndExtension); 36 | 37 | /// 38 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 39 | // Has no distinct header, recognition is inaccurate! 40 | => stream.Position + 0x8 < stream.Length && stream.Peek(s => s.ReadByte() == Identifier && (s.ReadUInt24() != 0 || s.ReadUInt32() != 0) && (s.ReadUInt8() & 0x80) == 0); 41 | 42 | /// 43 | public virtual uint GetDecompressedSize(Stream source) 44 | => source.Peek(InternalGetDecompressedSize); 45 | 46 | protected static uint InternalGetDecompressedSize(Stream source) 47 | { 48 | byte identifier = source.ReadUInt8(); 49 | if (identifier != Identifier) 50 | throw new InvalidIdentifierException(identifier.ToString("X"), Identifier.ToString("X")); 51 | uint decompressedSize = source.ReadUInt24(); 52 | if (decompressedSize == 0) 53 | decompressedSize = source.ReadUInt32(); 54 | 55 | return decompressedSize; 56 | } 57 | 58 | /// 59 | public virtual void Decompress(Stream source, Stream destination) 60 | { 61 | // Read Header 62 | uint decompressedSize = InternalGetDecompressedSize(source); 63 | 64 | // Perform the decompression 65 | DecompressHeaderless(source, destination, decompressedSize); 66 | } 67 | 68 | /// 69 | public virtual void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 70 | { 71 | // Write Header 72 | if (source.Length <= 0xFFFFFF) 73 | { 74 | destination.Write(Identifier | (source.Length << 8)); 75 | } 76 | else 77 | { 78 | destination.Write(Identifier | 0); 79 | destination.Write(source.Length); 80 | } 81 | 82 | // Perform the compression 83 | CompressHeaderless(source, destination, LookAhead, level); 84 | } 85 | 86 | public static void DecompressHeaderless(Stream source, Stream destination, uint decomLength) 87 | { 88 | long endPosition = destination.Position + decomLength; 89 | destination.SetLength(endPosition); 90 | using (LzWindows buffer = new LzWindows(destination, _lz.WindowsSize)) 91 | { 92 | FlagReader Flag = new FlagReader(source, Endian.Big); 93 | 94 | while (destination.Position + buffer.Position < endPosition) 95 | { 96 | if (Flag.Readbit()) 97 | { 98 | int distance, length; 99 | byte b1 = source.ReadUInt8(); 100 | byte b2 = source.ReadUInt8(); 101 | if (b1 >> 4 == 0) // match.Length 17-272 102 | { 103 | //0000LLLL LLLLDDDD DDDDDDDD 104 | byte b3 = source.ReadUInt8(); 105 | distance = ((b2 & 0xf) << 8 | b3) + 1; 106 | length = ((b1 & 0xf) << 4 | b2 >> 4) + 17; 107 | } 108 | else if (b1 >> 4 == 1) // match.Length 273-65808 109 | { 110 | //0001LLLL LLLLLLLL LLLLDDDD DDDDDDDD 111 | byte b3 = source.ReadUInt8(); 112 | byte b4 = source.ReadUInt8(); 113 | distance = ((b3 & 0xf) << 8 | b4) + 1; 114 | length = ((b1 & 0xf) << 12 | b2 << 4 | b3 >> 4) + 273; 115 | } 116 | else // match.Length 3-16 117 | { 118 | //LLLLDDDD DDDDDDDD 119 | distance = ((b1 & 0xf) << 8 | b2) + 1; 120 | length = (b1 >> 4) + 1; 121 | } 122 | 123 | buffer.BackCopy(distance, length); 124 | } 125 | else // Not compressed 126 | { 127 | buffer.WriteByte(source.ReadUInt8()); 128 | } 129 | } 130 | } 131 | 132 | if (destination.Position > endPosition) 133 | { 134 | throw new DecompressedSizeException(decomLength, destination.Position - (endPosition - decomLength)); 135 | } 136 | } 137 | 138 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, bool lookAhead = true, CompressionLevel level = CompressionLevel.Optimal) 139 | { 140 | int sourcePointer = 0x0, matchPointer = 0x0; 141 | 142 | using PoolList matches = LZMatchFinder.FindMatchesParallel(source, _lz, lookAhead, level); 143 | using FlagWriter flag = new FlagWriter(destination, Endian.Big); 144 | while (sourcePointer < source.Length) 145 | { 146 | if (matchPointer < matches.Count && matches[matchPointer].Offset == sourcePointer) 147 | { 148 | LzMatch match = matches[matchPointer++]; 149 | 150 | if (match.Length <= 16) // match.Length 3-16 151 | { 152 | flag.Buffer.Write((ushort)((match.Length - 1) << 12 | ((match.Distance - 1) & 0xFFF)), Endian.Big); 153 | } 154 | else if (match.Length <= 272) // match.Length 17-272 155 | { 156 | flag.Buffer.WriteByte((byte)(((match.Length - 17) & 0xFF) >> 4)); 157 | flag.Buffer.Write((ushort)((match.Length - 17) << 12 | ((match.Distance - 1) & 0xFFF)), Endian.Big); 158 | } 159 | else // match.Length 273-65808 160 | { 161 | flag.Buffer.Write((uint)(0x10000000 | ((match.Length - 273) & 0xFFFF) << 12 | ((match.Distance - 1) & 0xFFF)), Endian.Big); 162 | } 163 | sourcePointer += match.Length; 164 | flag.WriteBit(true); 165 | } 166 | else 167 | { 168 | flag.Buffer.WriteByte(source[sourcePointer++]); 169 | flag.WriteBit(false); 170 | } 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/LZ77.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Core; 4 | using AuroraLib.Core.Format; 5 | using AuroraLib.Core.Format.Identifier; 6 | using AuroraLib.Core.IO; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.IO.Compression; 11 | using System.Linq; 12 | 13 | namespace AuroraLib.Compression.Algorithms 14 | { 15 | /// 16 | /// Nintendo LZ77 extension Header from LZ10 algorithm 17 | /// 18 | public sealed class LZ77 : LZ10, ICompressionAlgorithm, IHasIdentifier 19 | { 20 | /// 21 | public IIdentifier Identifier => _identifier; 22 | 23 | private static readonly Identifier32 _identifier = new Identifier32("LZ77".AsSpan()); 24 | 25 | /// 26 | public override IFormatInfo Info => _info; 27 | 28 | private static readonly IFormatInfo _info = new FormatInfo("Nintendo LZ77", new MediaType(MIMEType.Application, "x-nintendo-lz10+lz77"), string.Empty, _identifier); 29 | 30 | /// 31 | /// Specifies the type of compression used. 32 | /// 33 | public CompressionType Type = CompressionType.LZ10; 34 | 35 | /// 36 | /// Defines the size of the chunks when is used. (4 KB by default). 37 | /// 38 | public UInt24 ChunkSize = 0x1000; 39 | 40 | /// 41 | public override bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 42 | => IsMatchStatic(stream, fileNameAndExtension); 43 | 44 | /// 45 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 46 | => stream.Position + 0x8 < stream.Length && stream.Peek(s => s.Match(_identifier) && Enum.IsDefined(typeof(CompressionType), s.Read())); 47 | 48 | /// 49 | public override uint GetDecompressedSize(Stream source) 50 | => source.Peek(s => 51 | { 52 | s.MatchThrow(_identifier); 53 | s.Skip(1); 54 | uint decompressedSize = s.ReadUInt24(); 55 | if (decompressedSize == 0) 56 | decompressedSize = s.ReadUInt32(); 57 | return decompressedSize; 58 | }); 59 | 60 | /// 61 | public override void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 62 | { 63 | destination.Write(_identifier); 64 | 65 | if (Type == CompressionType.ChunkLZ10 && ChunkSize < source.Length) 66 | { 67 | if (source.Length > 0xffffff) 68 | { 69 | new ArgumentOutOfRangeException($"{nameof(LZ77)} compression type {nameof(CompressionType.ChunkLZ10)} does not support files larger than 0xffffff."); 70 | } 71 | 72 | destination.Write((byte)Type | (source.Length << 8)); 73 | int segments = (source.Length + ChunkSize - 1) / ChunkSize; 74 | ushort[] segmentEndOffsets = new ushort[segments]; 75 | destination.Write(segmentEndOffsets); // Placeholder 76 | 77 | long headerEndOffset = destination.Position; 78 | for (int i = 0; i < segmentEndOffsets.Length; i++) 79 | { 80 | int segmentStart = i * ChunkSize; 81 | int segmentSize = Math.Min(ChunkSize, source.Length - segmentStart); 82 | base.Compress(source.Slice(segmentStart, segmentSize), destination, level); 83 | 84 | long segmentEndOffset = destination.Position - headerEndOffset; 85 | if (segmentEndOffset > 0xffff) 86 | { 87 | throw new ArgumentOutOfRangeException($"{nameof(LZ77)} chunks too large to process."); 88 | } 89 | segmentEndOffsets[i] = (ushort)segmentEndOffset; 90 | } 91 | 92 | destination.At(headerEndOffset - (segments * 2), s => s.Write(segmentEndOffsets)); 93 | } 94 | else 95 | { 96 | base.Compress(source, destination, level); 97 | } 98 | } 99 | 100 | /// 101 | public override void Decompress(Stream source, Stream destination) 102 | { 103 | source.MatchThrow(_identifier); 104 | CompressionType type = source.Read(); 105 | 106 | uint decompressedSize = source.ReadUInt24(); 107 | if (decompressedSize == 0) decompressedSize = source.ReadUInt32(); 108 | 109 | switch (type) 110 | { 111 | case CompressionType.LZ10: 112 | DecompressHeaderless(source, destination, decompressedSize); 113 | break; 114 | case CompressionType.ChunkLZ10: 115 | long destinationEndPosition = destination.Position + decompressedSize; 116 | 117 | List segmentEndOffsets = new List(); 118 | do 119 | { 120 | segmentEndOffsets.Add(source.ReadUInt16()); 121 | } while (segmentEndOffsets.Last() + source.Position != source.Length); 122 | long headerEndOffset = source.Position; 123 | 124 | //Unpack the individual chunks 125 | for (int i = 0; i < segmentEndOffsets.Count; i++) 126 | { 127 | base.Decompress(source, destination); 128 | source.Seek(segmentEndOffsets[i] + headerEndOffset, SeekOrigin.Begin); 129 | } 130 | 131 | if (destination.Position > destinationEndPosition) 132 | { 133 | throw new DecompressedSizeException(decompressedSize, destination.Position - (destinationEndPosition - decompressedSize)); 134 | } 135 | break; 136 | default: 137 | throw new NotSupportedException($"{nameof(LZ77)} compression type {type} not supported."); 138 | } 139 | } 140 | 141 | public enum CompressionType : byte 142 | { 143 | LZ10 = 0x10, 144 | ChunkLZ10 = 0xf7 145 | } 146 | 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/LZOn.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Core; 4 | using AuroraLib.Core.Format; 5 | using AuroraLib.Core.Format.Identifier; 6 | using AuroraLib.Core.IO; 7 | using System; 8 | using System.Diagnostics; 9 | using System.IO; 10 | using System.IO.Compression; 11 | 12 | namespace AuroraLib.Compression.Algorithms 13 | { 14 | /// 15 | /// LZO Nintendo mainly used in DS Download Games. 16 | /// 17 | public sealed class LZOn : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IProvidesDecompressedSize 18 | { 19 | /// 20 | public IIdentifier Identifier => _identifier; 21 | 22 | private static readonly Identifier64 _identifier = new Identifier64(new Identifier32("LZOn".AsSpan()), new Identifier32(0x00, 0x2F, 0xF1, 0x71)); 23 | 24 | /// 25 | public IFormatInfo Info => _info; 26 | 27 | private static readonly IFormatInfo _info = new FormatInfo("LZO Nintendo", new MediaType(MIMEType.Application, "x-lzo+nintendo"), string.Empty, _identifier); 28 | 29 | /// 30 | public bool LookAhead { get; set; } = true; 31 | 32 | /// 33 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 34 | => IsMatchStatic(stream, fileNameAndExtension); 35 | 36 | /// 37 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 38 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 39 | 40 | /// 41 | public uint GetDecompressedSize(Stream source) 42 | => source.Peek(s => 43 | { 44 | s.MatchThrow(_identifier); 45 | return s.ReadUInt32(Endian.Big); 46 | }); 47 | 48 | /// 49 | public void Decompress(Stream source, Stream destination) 50 | { 51 | // Read Header 52 | source.MatchThrow(_identifier); 53 | uint decompressedSize = source.ReadUInt32(Endian.Big); 54 | uint compressedSize = source.ReadUInt32(Endian.Big); 55 | 56 | // Mark the initial positions of the streams 57 | long compressedStartPosition = source.Position; 58 | long destinationStartPosition = destination.Position; 59 | 60 | // Perform the decompression 61 | LZO.DecompressHeaderless(source, destination); 62 | 63 | // Verify decompressed size 64 | DecompressedSizeException.ThrowIfMismatch(destination.Position - destinationStartPosition, decompressedSize); 65 | 66 | // Verify compressed size and handle mismatches 67 | Helper.TraceIfCompressedSizeMismatch(source.Position - compressedStartPosition, compressedSize); 68 | } 69 | 70 | /// 71 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 72 | { 73 | // Mark the initial positions of the destination 74 | long destinationStartPosition = destination.Position; 75 | 76 | // Write Header 77 | destination.Write(_identifier); 78 | destination.Write(source.Length, Endian.Big); 79 | destination.Write(0); // Compressed length (will be filled in later) 80 | 81 | // Perform the compression 82 | LZO.CompressHeaderless(source, destination, LookAhead, level); 83 | 84 | // Go back to the beginning of the file and write out the compressed length 85 | int destinationLength = (int)(destination.Position - destinationStartPosition - 0x10); 86 | destination.At(destinationStartPosition + 12, x => x.Write(destinationLength, Endian.Big)); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/LZSS.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Compression.IO; 4 | using AuroraLib.Compression.MatchFinder; 5 | using AuroraLib.Core; 6 | using AuroraLib.Core.Collections; 7 | using AuroraLib.Core.Format; 8 | using AuroraLib.Core.Format.Identifier; 9 | using AuroraLib.Core.IO; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Diagnostics; 13 | using System.IO; 14 | using System.IO.Compression; 15 | 16 | namespace AuroraLib.Compression.Algorithms 17 | { 18 | /// 19 | /// Lempel–Ziv–Storer–Szymanski algorithm, a derivative of LZ77 from Haruhiko Okumura. 20 | /// 21 | public sealed class LZSS : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IProvidesDecompressedSize 22 | { 23 | /// 24 | public IIdentifier Identifier => _identifier; 25 | 26 | private static readonly Identifier32 _identifier = new Identifier32("LZSS".AsSpan()); 27 | 28 | /// 29 | public IFormatInfo Info => _info; 30 | 31 | private static readonly IFormatInfo _info = new FormatInfo("Lempel–Ziv–Storer–Szymanski", new MediaType(MIMEType.Application, "x-lzss"), string.Empty, _identifier); 32 | 33 | protected readonly LzProperties LZ; 34 | 35 | /// 36 | public bool LookAhead { get; set; } = true; 37 | 38 | public LZSS() : this(DefaultProperties) 39 | { } 40 | 41 | public LZSS(LzProperties lz) 42 | => LZ = lz; 43 | 44 | public static LzProperties DefaultProperties => new LzProperties((byte)12, 4, 2); 45 | public static LzProperties Lzss0Properties => new LzProperties(0x1000, 0xF + 3, 3, 0xFEE); 46 | 47 | /// 48 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 49 | => IsMatchStatic(stream, fileNameAndExtension); 50 | 51 | /// 52 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 53 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 54 | 55 | /// 56 | public uint GetDecompressedSize(Stream source) 57 | => source.Peek(s => 58 | { 59 | s.MatchThrow(_identifier); 60 | return s.ReadUInt32(Endian.Big); 61 | }); 62 | 63 | /// 64 | public void Decompress(Stream source, Stream destination) 65 | { 66 | // Read Header 67 | source.MatchThrow(_identifier); 68 | uint decompressedSize = source.ReadUInt32(Endian.Big); 69 | uint compressedSize = source.ReadUInt32(Endian.Big); 70 | uint unk = source.ReadUInt32(Endian.Big); 71 | 72 | // Mark the initial positions of the streams 73 | long compressedStartPosition = source.Position; 74 | 75 | // Perform the decompression 76 | DecompressHeaderless(source, destination, decompressedSize, LZ); 77 | 78 | // Verify compressed size and handle mismatches 79 | Helper.TraceIfCompressedSizeMismatch(source.Position - compressedStartPosition, compressedSize); 80 | } 81 | 82 | /// 83 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 84 | { 85 | // Mark the initial positions of the destination 86 | long destinationStartPosition = destination.Position; 87 | 88 | // Write Header 89 | destination.Write(_identifier); 90 | destination.Write(source.Length, Endian.Big); 91 | destination.Write(0); // Compressed length (will be filled in later) 92 | destination.Write(0); 93 | 94 | // Perform the compression 95 | CompressHeaderless(source, destination, LZ, LookAhead, level); 96 | 97 | // Go back to the beginning of the file and write out the compressed length 98 | int destinationLength = (int)(destination.Position - destinationStartPosition - 0x10); 99 | destination.At(destinationStartPosition + 8, x => x.Write(destinationLength, Endian.Big)); 100 | } 101 | 102 | public static void DecompressHeaderless(Stream source, Stream destination, uint decomLength, LzProperties lz, byte initialFill = 0x0) 103 | { 104 | long endPosition = destination.Position + decomLength; 105 | destination.SetLength(endPosition); 106 | FlagReader flag = new FlagReader(source, Endian.Little); 107 | using (LzWindows buffer = new LzWindows(destination, lz.WindowsSize)) 108 | { 109 | if (initialFill != 0) 110 | buffer.UnsafeAsSpan().Fill(initialFill); 111 | 112 | int f = lz.GetLengthBitsFlag(); 113 | 114 | while (destination.Position + buffer.Position < endPosition) 115 | { 116 | if (flag.Readbit()) 117 | { 118 | buffer.WriteByte(source.ReadUInt8()); 119 | } 120 | else 121 | { 122 | byte b1 = source.ReadUInt8(); 123 | byte b2 = source.ReadUInt8(); 124 | 125 | int offset = (b2 >> lz.LengthBits << 8) | b1; 126 | int length = (b2 & f) + lz.MinLength; 127 | offset = lz.WindowsSize + offset - lz.WindowsStart; 128 | 129 | buffer.OffsetCopy(offset, length); 130 | } 131 | } 132 | 133 | } 134 | 135 | // Verify decompressed size 136 | if (destination.Position != endPosition) 137 | { 138 | throw new DecompressedSizeException(decomLength, destination.Position - (endPosition - decomLength)); 139 | } 140 | } 141 | 142 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, LzProperties lz, bool lookAhead = true, CompressionLevel level = CompressionLevel.Optimal) 143 | { 144 | using PoolList matches = LZMatchFinder.FindMatchesParallel(source, lz, lookAhead, level); 145 | CompressHeaderless(source, destination, matches, lz); 146 | } 147 | 148 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, IReadOnlyList matches, LzProperties lz) 149 | { 150 | int sourcePointer = 0x0, matchPointer = 0x0; 151 | 152 | using FlagWriter flag = new FlagWriter(destination, Endian.Little); 153 | int n = lz.GetWindowsFlag(); 154 | int f = lz.GetLengthBitsFlag(); 155 | 156 | while (sourcePointer < source.Length) 157 | { 158 | if (matchPointer < matches.Count && matches[matchPointer].Offset == sourcePointer) 159 | { 160 | LzMatch lzMatch = matches[matchPointer++]; 161 | 162 | // Distance to offset 163 | int offset = ((lz.WindowsStart + sourcePointer - lzMatch.Distance) & n); 164 | flag.Buffer.Write((ushort)((offset & 0xFF) | (offset & 0xFF00) << lz.LengthBits | ((lzMatch.Length - lz.MinLength) & f) << 8)); 165 | flag.WriteBit(false); 166 | sourcePointer += lzMatch.Length; 167 | 168 | } 169 | else 170 | { 171 | flag.Buffer.WriteByte(source[sourcePointer++]); 172 | flag.WriteBit(true); 173 | } 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/RLE30.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Compression.Interfaces; 3 | using AuroraLib.Core; 4 | using AuroraLib.Core.Exceptions; 5 | using AuroraLib.Core.Format; 6 | using AuroraLib.Core.IO; 7 | using System; 8 | using System.IO; 9 | using System.IO.Compression; 10 | using System.Runtime.CompilerServices; 11 | 12 | namespace AuroraLib.Compression.Algorithms 13 | { 14 | /// 15 | /// Nintendos Run-Length Encoding algorithm mainly used in GBA, DS games. 16 | /// 17 | public sealed class RLE30 : ICompressionAlgorithm, IProvidesDecompressedSize 18 | { 19 | private const byte Identifier = 0x30; 20 | private const int FlagMask = 0x80; 21 | private const int MinLength = 1; 22 | private const int MaxLength = 0x7F; 23 | 24 | /// 25 | public IFormatInfo Info => _info; 26 | 27 | private static readonly IFormatInfo _info = new FormatInfo("Nintendo RLE30", new MediaType(MIMEType.Application, "x-nintendo-rle30"), string.Empty); 28 | 29 | /// 30 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 31 | => IsMatchStatic(stream, fileNameAndExtension); 32 | 33 | /// 34 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 35 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.ReadByte() == Identifier && (s.ReadUInt24() != 0 || s.ReadUInt32() != 0) && s.ReadUInt8() != 0); 36 | 37 | /// 38 | public uint GetDecompressedSize(Stream source) 39 | => source.Peek(InternalGetDecompressedSize); 40 | 41 | private static uint InternalGetDecompressedSize(Stream source) 42 | { 43 | byte identifier = source.ReadUInt8(); 44 | if (identifier != Identifier) 45 | throw new InvalidIdentifierException(identifier.ToString("X"), Identifier.ToString("X")); 46 | uint decompressedSize = source.ReadUInt24(); 47 | if (decompressedSize == 0) 48 | decompressedSize = source.ReadUInt32(); 49 | 50 | return decompressedSize; 51 | } 52 | 53 | /// 54 | public void Decompress(Stream source, Stream destination) 55 | { 56 | uint uncompressedSize = InternalGetDecompressedSize(source); 57 | DecompressHeaderless(source, destination, uncompressedSize); 58 | } 59 | 60 | /// 61 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 62 | { 63 | destination.Write(Identifier); 64 | if (source.Length <= 0xFFFFFF) 65 | { 66 | destination.Write((UInt24)source.Length); 67 | } 68 | else 69 | { 70 | destination.Write(new UInt24(0)); 71 | destination.Write(source.Length); 72 | } 73 | 74 | CompressHeaderless(source, destination); 75 | } 76 | 77 | public static void DecompressHeaderless(Stream source, Stream destination, uint decomLength) 78 | { 79 | long endPosition = destination.Position + decomLength; 80 | destination.SetLength(endPosition); 81 | Span bytes = stackalloc byte[0xFF]; 82 | 83 | while (destination.Position < endPosition) 84 | { 85 | int flag = source.ReadByte(); 86 | int length = (flag & MaxLength) + MinLength; 87 | Span section = bytes; 88 | if (flag >= FlagMask) 89 | { 90 | section = bytes.Slice(0, length + 2); 91 | section.Fill(source.ReadUInt8()); 92 | } 93 | else 94 | { 95 | section = bytes.Slice(0, length); 96 | if (source.Read(section) != length) 97 | throw new EndOfStreamException(); 98 | } 99 | destination.Write(section); 100 | } 101 | 102 | if (destination.Position > endPosition) 103 | { 104 | throw new DecompressedSizeException(decomLength, destination.Position - (endPosition - decomLength)); 105 | } 106 | } 107 | 108 | #if !(NETSTANDARD || NET20_OR_GREATER) 109 | [MethodImpl(MethodImplOptions.AggressiveOptimization)] 110 | #endif 111 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination) 112 | { 113 | int sourcePointer = 0x0; 114 | RleMatchFinder matchFinder = new RleMatchFinder(3, MaxLength); 115 | 116 | while (sourcePointer < source.Length) 117 | { 118 | if (matchFinder.TryToFindMatch(source, sourcePointer, out int duration)) 119 | { 120 | destination.WriteByte((byte)(duration - 3 | FlagMask)); 121 | destination.Write(source[sourcePointer]); 122 | } 123 | else 124 | { 125 | destination.WriteByte((byte)(duration - MinLength)); 126 | destination.Write(source.Slice(sourcePointer, duration)); 127 | } 128 | sourcePointer += duration; 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/YAZ0.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Compression.IO; 3 | using AuroraLib.Core; 4 | using AuroraLib.Core.Format; 5 | using AuroraLib.Core.Format.Identifier; 6 | using AuroraLib.Core.IO; 7 | using System; 8 | using System.IO; 9 | using System.IO.Compression; 10 | 11 | namespace AuroraLib.Compression.Algorithms 12 | { 13 | /// 14 | /// Nintendo Yaz0 compression algorithm successor to the algorithm, used in numerous Nintendo titles from the N64 era to Switch. 15 | /// 16 | public class Yaz0 : ICompressionAlgorithm, ILzSettings, IHasIdentifier, IEndianDependentFormat, IProvidesDecompressedSize 17 | { 18 | 19 | /// 20 | public virtual IIdentifier Identifier => _identifier; 21 | 22 | private static readonly Identifier32 _identifier = new Identifier32("Yaz0".AsSpan()); 23 | 24 | /// 25 | public virtual IFormatInfo Info => _info; 26 | 27 | private static readonly IFormatInfo _info = new FormatInfo("Nintendo Yaz0", new MediaType(MIMEType.Application, "x-nintendo-yaz0"), string.Empty, _identifier); 28 | 29 | /// 30 | public bool LookAhead { get; set; } = true; 31 | 32 | /// 33 | public Endian FormatByteOrder { get; set; } = Endian.Big; 34 | 35 | /// 36 | /// Gets or sets the memory alignment for the initialized buffer.
37 | /// Must be 0 or a power of two. 38 | /// 39 | /// This setting is only supported in select titles starting with the Wii U generation. 40 | /// It has no effect on compression. 41 | ///
42 | public uint MemoryAlignment { get; set; } = 0; 43 | 44 | /// 45 | public virtual bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 46 | => IsMatchStatic(stream, fileNameAndExtension); 47 | 48 | /// 49 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 50 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 51 | 52 | /// 53 | public uint GetDecompressedSize(Stream source) 54 | => source.Peek(s => 55 | { 56 | s.MatchThrow(Identifier); 57 | return s.ReadUInt32(FormatByteOrder); 58 | }); 59 | 60 | /// 61 | public virtual void Decompress(Stream source, Stream destination) 62 | { 63 | source.MatchThrow(Identifier); 64 | uint decompressedSize = source.ReadUInt32(FormatByteOrder); 65 | MemoryAlignment = source.ReadUInt32(FormatByteOrder); 66 | _ = source.ReadUInt32(FormatByteOrder); 67 | 68 | long sourceDataStartPosition = source.Position; 69 | long destinationStartPosition = destination.Position; 70 | try 71 | { 72 | DecompressHeaderless(source, destination, decompressedSize); 73 | } 74 | catch (Exception) // try other order 75 | { 76 | source.Seek(sourceDataStartPosition, SeekOrigin.Begin); 77 | destination.Seek(destinationStartPosition, SeekOrigin.Begin); 78 | decompressedSize = BitConverterX.ReverseEndianness(decompressedSize); 79 | MemoryAlignment = BitConverterX.ReverseEndianness(MemoryAlignment); 80 | DecompressHeaderless(source, destination, decompressedSize); 81 | } 82 | } 83 | 84 | /// 85 | public virtual void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 86 | { 87 | destination.Write(Identifier.AsSpan()); 88 | destination.Write(source.Length, FormatByteOrder); 89 | destination.Write(MemoryAlignment); 90 | destination.Write(0); 91 | CompressHeaderless(source, destination, LookAhead, level); 92 | } 93 | 94 | public static void DecompressHeaderless(Stream source, Stream destination, uint decomLength) 95 | => Yay0.DecompressHeaderless(new FlagReader(source, Endian.Big), source, source, destination, decomLength); 96 | 97 | public static void CompressHeaderless(ReadOnlySpan source, Stream destination, bool lookAhead = true, CompressionLevel level = CompressionLevel.Optimal) 98 | { 99 | using FlagWriter flag = new FlagWriter(destination, Endian.Big); 100 | Yay0.CompressHeaderless(source, flag.Buffer, flag.Buffer, flag, lookAhead, level); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/YAZ1.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core.Format; 3 | using AuroraLib.Core.Format.Identifier; 4 | using AuroraLib.Core.IO; 5 | using System; 6 | using System.IO; 7 | 8 | namespace AuroraLib.Compression.Algorithms 9 | { 10 | /// 11 | /// Identical to only with different identifier, in N64DD games the 0 was replaced by 1 if the files were on the disk instead of the cartridge. 12 | /// 13 | public sealed class Yaz1 : Yaz0, ICompressionAlgorithm 14 | { 15 | /// 16 | public override IIdentifier Identifier => _identifier; 17 | 18 | private static readonly Identifier32 _identifier = new Identifier32("Yaz1".AsSpan()); 19 | 20 | /// 21 | public override IFormatInfo Info => _info; 22 | 23 | private static readonly IFormatInfo _info = new FormatInfo("Nintendo Yaz0 N64DD", new MediaType(MIMEType.Application, "x-nintendo-yaz0+dd"), string.Empty, _identifier); 24 | 25 | /// 26 | public override bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 27 | => IsMatchStatic(stream, fileNameAndExtension); 28 | 29 | /// 30 | public new static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 31 | => stream.Position + 0x10 < stream.Length && stream.Peek(s => s.Match(_identifier)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Algorithms/ZLib.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Interfaces; 2 | using AuroraLib.Core; 3 | using AuroraLib.Core.Format; 4 | using AuroraLib.Core.IO; 5 | using System; 6 | using System.Buffers; 7 | using System.IO; 8 | using System.IO.Compression; 9 | 10 | namespace AuroraLib.Compression.Algorithms 11 | { 12 | /// 13 | /// ZLib open-source compression algorithm, which is based on the DEFLATE compression format. 14 | /// 15 | public sealed class ZLib : ICompressionAlgorithm 16 | { 17 | private static readonly string[] _extensions = new string[] { ".zz", ".zlib", string.Empty }; 18 | /// 19 | public IFormatInfo Info => _info; 20 | 21 | private static readonly IFormatInfo _info = new FormatInfo("zlib", new MediaType(MIMEType.Application, "zlib"), _extensions); 22 | /// 23 | public bool IsMatch(Stream stream, ReadOnlySpan fileNameAndExtension = default) 24 | => IsMatchStatic(stream, fileNameAndExtension); 25 | 26 | /// 27 | public static bool IsMatchStatic(Stream stream, ReadOnlySpan fileNameAndExtension = default) 28 | => stream.Position + 0x8 < stream.Length && stream.Peek
().Validate(); 29 | 30 | /// 31 | public void Decompress(Stream source, Stream destination) 32 | { 33 | #if NET6_0_OR_GREATER 34 | using ZLibStream algo = new(source, CompressionMode.Decompress, true); 35 | algo.CopyTo(destination); 36 | #else 37 | Header header = source.Read
(); 38 | using (DeflateStream dflStream = new DeflateStream(source, CompressionMode.Decompress, true)) 39 | dflStream.CopyTo(destination); 40 | #endif 41 | source.Position = source.Length; 42 | } 43 | 44 | 45 | public void Decompress(Stream source, Stream destination, int sourceSize) 46 | => Decompress(new SubStream(source, sourceSize), destination); 47 | 48 | /// 49 | public void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal) 50 | { 51 | #if NET6_0_OR_GREATER 52 | using ZLibStream algo = new(destination, level, true); 53 | algo.Write(source); 54 | } 55 | #else 56 | destination.Write(new Header(level)); 57 | using (DeflateStream dflStream = new DeflateStream(destination, level, true)) 58 | dflStream.Write(source); 59 | 60 | destination.Write(ComputeAdler32(source), Endian.Big); 61 | } 62 | 63 | private static uint ComputeAdler32(ReadOnlySpan source) 64 | { 65 | const uint MOD_ADLER = 65521; 66 | uint a = 1, b = 0; 67 | 68 | foreach (byte byteValue in source) 69 | { 70 | a = (a + byteValue) % MOD_ADLER; 71 | b = (b + a) % MOD_ADLER; 72 | } 73 | 74 | return (b << 16) | a; 75 | } 76 | #endif 77 | 78 | public readonly struct Header 79 | { 80 | private readonly byte cmf; 81 | private readonly byte flg; 82 | 83 | public Header(CompressionLevel level) 84 | { 85 | cmf = 0x78; 86 | switch (level) 87 | { 88 | case System.IO.Compression.CompressionLevel.NoCompression: 89 | case System.IO.Compression.CompressionLevel.Fastest: 90 | flg = 0x01; 91 | break; 92 | case System.IO.Compression.CompressionLevel.Optimal: 93 | flg = 0xDA; 94 | break; 95 | default: 96 | flg = 0x9C; 97 | break; 98 | } 99 | } 100 | 101 | public enum CompressionMethod : byte 102 | { 103 | Deflate = 8 104 | } 105 | 106 | public CompressionMethod Method => (CompressionMethod)(cmf & 0x0F); 107 | 108 | public byte CompressionInfo => (byte)((cmf >> 4) & 0x0F); 109 | 110 | public ushort FletcherChecksum => (ushort)(((flg & 0xFF) << 8) | cmf); 111 | 112 | public bool HasDictionary => ((flg >> 5) & 0x01) != 0; 113 | 114 | public byte CompressionLevel => (byte)((flg >> 6) & 0x03); 115 | 116 | public bool Validate() 117 | { 118 | ushort checksum = FletcherChecksum; 119 | 120 | if (Method != CompressionMethod.Deflate || CompressionInfo > 7) 121 | return false; 122 | 123 | return checksum % 31 != 0 || checksum % 255 != 0; 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/AuroraLib.Compression.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;net6.0;netstandard2.0;netstandard2.1;net472; 5 | disable 6 | enable 7 | True 8 | AuroraLib.Compression 9 | 1.5.1.0 10 | $(Version) 11 | MIT 12 | True 13 | icon.png 14 | README.md 15 | https://github.com/Venomalia/AuroraLib.Compression 16 | https://github.com/Venomalia/AuroraLib.Compression 17 | Venomalia 18 | Supports a wide range of compression algorithms mainly used in video games, 19 | like LZSS, LZ10, LZ11, MIO0, YAZ0, YAY0, PRS, LZ0, ZLib and more. 20 | true 21 | true 22 | |netstandard2.0|net481|net48|net472|NET471|NET47|NET462|NET461| 23 | Debug;Release 24 | 25 | 26 | 27 | 8.0 28 | 29 | 30 | 31 | 32 | True 33 | \ 34 | 35 | 36 | True 37 | \ 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Exceptions/DecompressedSizeException.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Core; 2 | using System; 3 | using System.IO; 4 | 5 | namespace AuroraLib.Compression.Exceptions 6 | { 7 | /// 8 | /// Exception thrown when the actual decompressed size differs from the expected size. 9 | /// 10 | public class DecompressedSizeException : Exception 11 | { 12 | public DecompressedSizeException() : base() { } 13 | 14 | public DecompressedSizeException(long expected, long actual) : base(CreateMessage(expected, actual)) 15 | { } 16 | 17 | private static string CreateMessage(long expected, long actual) 18 | => $"Expected {expected} bytes, but write {actual}bytes."; 19 | 20 | public static void ThrowIfMismatch(long actual, long expected) 21 | { 22 | if (actual != expected) 23 | { 24 | throw new DecompressedSizeException(expected, actual); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Helper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace AuroraLib.Compression 4 | { 5 | internal static class Helper 6 | { 7 | internal static void TraceIfCompressedSizeMismatch(long actual, long expected) 8 | { 9 | if (actual != expected) 10 | { 11 | Trace.WriteLine($"Warning: Expected {expected} bytes of compressed data, but read {actual} bytes."); 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Huffman/HuffmanNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace AuroraLib.Compression.Huffman 5 | { 6 | /// 7 | /// Represents a node in a Huffman tree, used in the Huffman algorithm. 8 | /// 9 | /// The type of the value contained in the Huffman node. 10 | public sealed class HuffmanNode : IComparable> where T : notnull 11 | { 12 | /// 13 | /// The value stored in the node. 14 | /// 15 | public T Value; 16 | 17 | /// 18 | /// The frequency of the value. 19 | /// 20 | public uint Frequency; 21 | 22 | /// 23 | /// The left and right children of the node. 24 | /// 25 | public (HuffmanNode? Left, HuffmanNode? Right) Children { get; set; } 26 | 27 | /// 28 | /// Gets a value indicating whether the node is a leaf node. 29 | /// 30 | public bool IsLeaf => Children.Left == null; 31 | 32 | public HuffmanNode(T vaule, uint frequency) 33 | { 34 | Value = vaule; 35 | Frequency = frequency; 36 | } 37 | 38 | public HuffmanNode(uint frequency, HuffmanNode childrenLeft, HuffmanNode childrenRight) 39 | { 40 | Frequency = frequency; 41 | Children = (childrenLeft, childrenRight); 42 | } 43 | 44 | public Dictionary GetCodeDictionary() 45 | { 46 | var result = new Dictionary(); 47 | BuildCodeDictionary(0, 0, result); 48 | return result; 49 | } 50 | 51 | private void BuildCodeDictionary(int code, short len, Dictionary result) 52 | { 53 | if (IsLeaf) 54 | { 55 | result.Add(Value, (code, len)); 56 | } 57 | else 58 | { 59 | len++; 60 | Children.Left!.BuildCodeDictionary((code << 1), len, result); 61 | Children.Right!.BuildCodeDictionary((code << 1) | 1, len, result); 62 | } 63 | } 64 | 65 | /// 66 | public int CompareTo(HuffmanNode? other) 67 | => other == null ? 1 : Frequency.CompareTo(other.Frequency); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Huffman/HuffmanTree.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace AuroraLib.Compression.Huffman 5 | { 6 | public static class HuffmanTree 7 | { 8 | /// 9 | /// Builds a Huffman tree from the given input, where each symbol is represented by a specified number of bits (default 8 bits). 10 | /// 11 | /// A read-only span of bytes representing the data to be compressed using Huffman coding. 12 | /// The number of bits per symbol (default is 8, which means each byte is a symbol). 13 | /// The root node of the generated Huffman tree. 14 | public static HuffmanNode Build(ReadOnlySpan input, int bitDepth = 8) 15 | => CreateTree(GetFrequencies(input, bitDepth)); 16 | 17 | /// 18 | /// Builds a Huffman tree from the given input, where each element in the input span is treated as a symbol. 19 | /// 20 | /// The type of the elements in the input span, which will be treated as symbols in the Huffman tree. 21 | /// A read-only span of elements representing the data to be compressed using Huffman coding. 22 | /// The root node of the generated Huffman tree. 23 | public static HuffmanNode Build(ReadOnlySpan input) where T : notnull 24 | => CreateTree(GetFrequencies(input)); 25 | 26 | private static List> GetFrequencies(ReadOnlySpan input, int bitDepth) 27 | { 28 | if (bitDepth != 4 && bitDepth != 8) 29 | throw new ArgumentException($"bitDepth {bitDepth}"); 30 | 31 | // Iterate through the input span, counting the frequency of each unique symbol. 32 | Span frequency = stackalloc uint[1 << bitDepth]; 33 | if (bitDepth == 4) 34 | { 35 | foreach (byte item in input) 36 | { 37 | frequency[item >> 4]++; 38 | frequency[item & 0xF]++; 39 | } 40 | } 41 | else 42 | { 43 | foreach (byte item in input) 44 | frequency[item]++; 45 | } 46 | 47 | // Convert each frequency into a Huffman node and add it to the list. 48 | List> tree = new List>(frequency.Length); 49 | for (int i = 0; i < frequency.Length; i++) 50 | { 51 | if (frequency[i] != 0) 52 | tree.Add(new HuffmanNode((byte)i, frequency[i])); 53 | } 54 | return tree; 55 | } 56 | 57 | private static List> GetFrequencies(ReadOnlySpan input) where T : notnull 58 | { 59 | // Iterate through the input span, counting the frequency of each unique symbol. 60 | Dictionary frequency = new Dictionary(); 61 | foreach (T item in input) 62 | { 63 | if (frequency.TryGetValue(item, out uint value)) 64 | frequency[item] = ++value; 65 | else 66 | frequency[item] = 1; 67 | } 68 | 69 | // Convert each dictionary entry (symbol and its frequency) into a Huffman node and add it to the list. 70 | List> tree = new List>(); 71 | foreach (var entry in frequency) 72 | { 73 | tree.Add(new HuffmanNode(entry.Key, entry.Value)); 74 | } 75 | 76 | return tree; 77 | } 78 | 79 | /// 80 | /// Creates a Huffman tree from a collection of Huffman nodes. 81 | /// 82 | /// The type of the value contained in the Huffman node. 83 | /// A collection of Huffman nodes. 84 | /// The root node of the resulting Huffman tree. 85 | public static HuffmanNode CreateTree(IEnumerable> frequencies) where T : notnull 86 | { 87 | var frequenciesList = new List>(frequencies); 88 | 89 | while (frequenciesList.Count > 1) 90 | { 91 | frequenciesList.Sort(); 92 | 93 | var leastFrequencyNode1 = frequenciesList[0]; 94 | frequenciesList.RemoveAt(0); 95 | 96 | var leastFrequencyNode2 = frequenciesList[0]; 97 | frequenciesList.RemoveAt(0); 98 | 99 | var combinedNode = new HuffmanNode(leastFrequencyNode1.Frequency + leastFrequencyNode2.Frequency, leastFrequencyNode1, leastFrequencyNode2); 100 | frequenciesList.Add(combinedNode); 101 | } 102 | return frequenciesList[0]; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/IO/FlagReader.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Core; 2 | using AuroraLib.Core.IO; 3 | using System; 4 | using System.IO; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AuroraLib.Compression.IO 8 | { 9 | /// 10 | /// Reads individual bits from a stream and provides methods for interpreting the flag values. 11 | /// 12 | public sealed class FlagReader 13 | { 14 | public int BitsLeft { get; private set; } 15 | 16 | private int CurrentFlag; 17 | private readonly int _FlagSize; 18 | private readonly bool _BitOrderIsBe; 19 | private readonly Endian _ByteOrder; 20 | private readonly Stream _Base; 21 | private readonly Func? ReadFlagDelegate; 22 | 23 | public FlagReader(Stream source, Endian bitOrder, byte flagSize = 1, Endian byteOrder = Endian.Little) 24 | { 25 | ThrowIf.Null(source, nameof(source)); 26 | ThrowIf.NegativeOrZero(flagSize, nameof(flagSize)); 27 | 28 | _Base = source; 29 | _BitOrderIsBe = bitOrder == Endian.Big; 30 | 31 | _FlagSize = flagSize * 8; 32 | _ByteOrder = byteOrder; 33 | } 34 | 35 | public FlagReader(Stream source, Endian bitOrder, Func readFlagDelegate, byte flagSize) : this(source, bitOrder, flagSize) 36 | => ReadFlagDelegate = readFlagDelegate; 37 | 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | private int ReadNextFlag() => _FlagSize switch 40 | { 41 | 8 => _Base.ReadInt8(), 42 | 16 => _Base.ReadUInt16(_ByteOrder), 43 | 24 => _Base.ReadUInt24(_ByteOrder), 44 | 32 => _Base.ReadInt32(_ByteOrder), 45 | _ => throw new NotImplementedException($"Unsupported flag size: {_FlagSize}") 46 | }; 47 | 48 | /// 49 | /// Reads a single bit from the stream. 50 | /// 51 | /// The value of the read bit. 52 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 53 | public bool Readbit() 54 | { 55 | if (BitsLeft == 0) 56 | { 57 | CurrentFlag = ReadFlagDelegate is null ? ReadNextFlag() : ReadFlagDelegate(); 58 | BitsLeft = _FlagSize; 59 | } 60 | 61 | int shiftAmount = _BitOrderIsBe ? BitsLeft - 1 : _FlagSize - BitsLeft; 62 | BitsLeft--; 63 | 64 | return (CurrentFlag & (1 << shiftAmount)) != 0; 65 | } 66 | 67 | /// 68 | /// Reads an integer value with the specified number of bits from the stream. 69 | /// 70 | /// The number of bits to read. 71 | /// The integer value read from the stream. 72 | #if !(NETSTANDARD || NET20_OR_GREATER) 73 | [MethodImpl(MethodImplOptions.AggressiveOptimization)] 74 | #endif 75 | public int ReadInt(int bits, bool reverseOrder = false) 76 | { 77 | int vaule = 0; 78 | if (!reverseOrder) 79 | { 80 | for (int i = 0; i < bits; i++) 81 | { 82 | if (Readbit()) 83 | { 84 | vaule |= 1 << i; 85 | } 86 | } 87 | } 88 | else 89 | { 90 | for (int i = 0; i < bits; i++) 91 | { 92 | vaule <<= 1; 93 | if (Readbit()) 94 | { 95 | vaule |= 1; 96 | } 97 | } 98 | } 99 | return vaule; 100 | } 101 | 102 | public void Reset() => BitsLeft = 0; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/IO/FlagWriter.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Core; 2 | using AuroraLib.Core.IO; 3 | using System; 4 | using System.IO; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AuroraLib.Compression.IO 8 | { 9 | /// 10 | /// Represents a flag writer used for compressing data. It provides methods to write individual bits. 11 | /// 12 | public sealed class FlagWriter : IDisposable 13 | { 14 | public int BitsLeft { get; private set; } 15 | 16 | private int CurrentFlag; 17 | private readonly int _FlagSize; 18 | private readonly bool _BitOrderIsBe; 19 | private readonly Endian _ByteOrder; 20 | 21 | private readonly Stream _Base; 22 | public readonly MemoryPoolStream Buffer; 23 | private readonly Action? WriteFlagDelegate; 24 | 25 | public FlagWriter(Stream destination, Endian bitOrder, byte flagSize = 1, Endian byteOrder = Endian.Little, int bufferCapacity = 0x100) 26 | { 27 | ThrowIf.Null(destination, nameof(destination)); 28 | ThrowIf.NegativeOrZero(flagSize, nameof(flagSize)); 29 | 30 | _FlagSize = BitsLeft = 8 * flagSize; 31 | _BitOrderIsBe = bitOrder == Endian.Big; 32 | _ByteOrder = byteOrder; 33 | 34 | _Base = destination; 35 | Buffer = new MemoryPoolStream(bufferCapacity); 36 | } 37 | 38 | public FlagWriter(Stream destination, Endian bitOrder, Action writeFlagDelegate, byte flagSize, int bufferCapacity = 0x100) : this(destination, bitOrder, flagSize, default, bufferCapacity) 39 | => WriteFlagDelegate = writeFlagDelegate; 40 | 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | private void WriteFlag(int value) 43 | { 44 | switch (_FlagSize) 45 | { 46 | case 8: 47 | _Base.WriteByte((byte)value); 48 | return; 49 | case 16: 50 | _Base.Write((ushort)value, _ByteOrder); 51 | return; 52 | case 24: 53 | _Base.Write((UInt24)value, _ByteOrder); 54 | return; 55 | case 32: 56 | _Base.Write(value, _ByteOrder); 57 | return; 58 | default: 59 | throw new NotImplementedException($"Unsupported flag size: {_FlagSize}"); 60 | }; 61 | } 62 | 63 | /// 64 | /// Writes a single bit as a flag. The bits are accumulated in a byte and flushed to the destination stream when necessary. 65 | /// 66 | /// The bit value to write (true for 1, false for 0). 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | public void WriteBit(bool bit) 69 | { 70 | if (bit) 71 | { 72 | int shiftAmount = _BitOrderIsBe ? BitsLeft - 1 : _FlagSize - BitsLeft; 73 | CurrentFlag |= (1 << shiftAmount); 74 | } 75 | BitsLeft--; 76 | if (BitsLeft == 0) 77 | Flush(); 78 | } 79 | 80 | /// 81 | /// Writes an integer value as a sequence of bits with the specified number of bits. The bits are written from the most significant bit to the least significant bit. 82 | /// 83 | /// The integer value to write. 84 | /// The number of bits to write (default is 1). 85 | #if !(NETSTANDARD || NET20_OR_GREATER) 86 | [MethodImpl(MethodImplOptions.AggressiveOptimization)] 87 | #endif 88 | public void WriteInt(int value, int bits, bool reverseOrder = false) 89 | { 90 | if (!reverseOrder) 91 | { 92 | for (int i = 0; i < bits; i++) 93 | { 94 | WriteBit(((value >> i) & 0x1) == 1); 95 | } 96 | } 97 | else 98 | { 99 | for (int i = bits - 1; i >= 0; i--) 100 | { 101 | WriteBit(((value >> i) & 0x1) == 1); 102 | } 103 | } 104 | } 105 | 106 | /// 107 | /// Flushes any remaining bits and the buffer to the underlying stream. 108 | /// 109 | public void Flush() 110 | { 111 | if (BitsLeft != _FlagSize) 112 | { 113 | if (WriteFlagDelegate is null) 114 | WriteFlag(CurrentFlag); 115 | else 116 | WriteFlagDelegate(CurrentFlag); 117 | BitsLeft = _FlagSize; 118 | CurrentFlag = 0; 119 | } 120 | if (Buffer.Length != 0) 121 | { 122 | Buffer.WriteTo(_Base); 123 | Buffer.SetLength(0); 124 | } 125 | } 126 | 127 | /// 128 | /// Flushes the buffer to the underlying stream if necessary. 129 | /// 130 | public void FlushIfNecessary() 131 | { 132 | if (BitsLeft == _FlagSize && Buffer.Length != 0) 133 | { 134 | Buffer.WriteTo(_Base); 135 | Buffer.SetLength(0); 136 | } 137 | } 138 | 139 | public void Dispose() 140 | { 141 | Flush(); 142 | Buffer.Dispose(); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/IO/LzWindows.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Core.IO; 2 | using System; 3 | using System.Buffers; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace AuroraLib.Compression.IO 9 | { 10 | /// 11 | /// Represents a circular window buffer used in LZ compression. 12 | /// 13 | public sealed class LzWindows : PoolStream 14 | { 15 | private readonly Stream destination; 16 | 17 | private long _Position; 18 | 19 | /// 20 | public override long Position 21 | { 22 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 23 | [DebuggerStepThrough] 24 | get 25 | { 26 | return _Position; 27 | } 28 | [DebuggerStepThrough] 29 | set 30 | { 31 | if (value < 0) 32 | { 33 | _Position = Length + value; 34 | } 35 | else if (value >= Length) 36 | { 37 | _Position = value % Length; 38 | } 39 | else 40 | { 41 | _Position = value; 42 | } 43 | } 44 | } 45 | 46 | public LzWindows(Stream destination, int capacity) : this(destination, ArrayPool.Shared, capacity) 47 | { } 48 | 49 | 50 | public LzWindows(Stream destination, ArrayPool aPool, int capacity) : base(aPool, aPool.Rent(capacity), capacity) 51 | => this.destination = destination; 52 | 53 | /// 54 | /// Copies data from a specific position in the circular buffer to the current position. 55 | /// 56 | /// The distance from the current position to the source data. 57 | /// The number of bytes to copy. 58 | [DebuggerStepThrough] 59 | public void BackCopy(int distance, int length) 60 | { 61 | // Optimization: Ensure distance and position are valid for the operation. 62 | if (distance >= length && distance <= Position) 63 | Write(_Buffer.AsSpan((int)(Position - distance), length)); 64 | else 65 | OffsetCopy((int)(Length + Position - distance), length); 66 | } 67 | 68 | /// 69 | /// Copies data from an offset position within the circular buffer to the current position. 70 | /// 71 | /// The offset position from which data will be copied. 72 | /// The number of bytes to copy. 73 | [DebuggerStepThrough] 74 | public void OffsetCopy(int Offset, int length) 75 | { 76 | for (int i = 0; i < length; i++) 77 | { 78 | WriteByte(_Buffer[(Offset + i) % Length]); 79 | } 80 | } 81 | 82 | /// 83 | /// Copies data from a to this . 84 | /// 85 | /// The source stream containing data to copy. 86 | /// The number of bytes to copy. 87 | [DebuggerStepThrough] 88 | public void CopyFrom(Stream source, int length) 89 | { 90 | while (length != 0) 91 | { 92 | int l = Math.Min(length, (int)(Length - Position)); 93 | source.Read(_Buffer, (int)Position, l); 94 | Position += l; 95 | length -= l; 96 | if (Position == 0) 97 | FlushToDestination((int)Length); 98 | } 99 | } 100 | 101 | /// 102 | [DebuggerStepThrough] 103 | public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count)); 104 | 105 | /// 106 | [DebuggerStepThrough] 107 | public override int Read(Span buffer) 108 | { 109 | int num; 110 | for (int i = 0; buffer.Length > i; i += num) 111 | { 112 | num = (int)Math.Min(Length - Position, buffer.Length); 113 | _Buffer.AsSpan((int)Position, num).CopyTo(buffer.Slice(i, num)); 114 | Position += num; 115 | } 116 | 117 | return buffer.Length; 118 | } 119 | 120 | /// 121 | [DebuggerStepThrough] 122 | public override void Write(byte[] buffer, int offset, int count) => Write(buffer.AsSpan(offset, count)); 123 | 124 | /// 125 | [DebuggerStepThrough] 126 | public unsafe override void Write(ReadOnlySpan buffer) 127 | { 128 | int position = (int)_Position; 129 | if (Length > position + buffer.Length) 130 | { 131 | // The entire buffer fits without wrapping around. 132 | buffer.CopyTo(_Buffer.AsSpan(position)); 133 | _Position += buffer.Length; 134 | } 135 | else 136 | { 137 | // Partially write and wrap around. 138 | int left = (int)(Length - position); 139 | int copy = (int)Math.Min(Length, buffer.Length); 140 | 141 | destination.Write(_Buffer, 0, position); 142 | if (left != 0) 143 | { 144 | destination.Write(buffer.Slice(0, left)); 145 | buffer.Slice(buffer.Length - copy, left).CopyTo(_Buffer.AsSpan(position, left)); 146 | } 147 | 148 | if (copy > left) 149 | { 150 | int remaining = copy - left; 151 | buffer.Slice(buffer.Length - remaining, remaining).CopyTo(_Buffer.AsSpan(0, remaining)); 152 | } 153 | 154 | Position += copy; 155 | } 156 | } 157 | 158 | /// 159 | [DebuggerStepThrough] 160 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 161 | public override void WriteByte(byte value) 162 | { 163 | _Buffer[Position++] = value; 164 | if (Position == 0) 165 | FlushToDestination((int)Length); 166 | } 167 | 168 | /// 169 | public override void Flush() 170 | { 171 | if (Position != 0) 172 | { 173 | FlushToDestination((int)Position); 174 | Position = 0; 175 | } 176 | } 177 | 178 | private void FlushToDestination(int length) 179 | => destination.Write(_Buffer, 0, length); 180 | 181 | /// 182 | protected override Span InternalBufferAsSpan(int start, int length) 183 | => _Buffer.AsSpan(start, length); 184 | 185 | /// 186 | protected override void ExpandBuffer(int minimumLength) 187 | => throw new NotSupportedException(); 188 | 189 | /// 190 | [DebuggerStepThrough] 191 | protected override void Dispose(bool disposing) 192 | { 193 | if (_Buffer.Length != 0 && Position != 0) 194 | FlushToDestination((int)Position); 195 | base.Dispose(disposing); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Interfaces/ICompressionAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Core.Format; 2 | 3 | namespace AuroraLib.Compression.Interfaces 4 | { 5 | /// 6 | /// Interface for compressing and decompressing data. 7 | /// 8 | public interface ICompressionAlgorithm : ICompressionDecoder, ICompressionEncoder, IFormatInfoProvider 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Interfaces/ICompressionDecoder.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Compression.Exceptions; 2 | using AuroraLib.Core.Exceptions; 3 | using AuroraLib.Core.Format; 4 | using System; 5 | using System.IO; 6 | 7 | namespace AuroraLib.Compression.Interfaces 8 | { 9 | /// 10 | /// Defines an interface for file decompression. 11 | /// 12 | #if NET8_0_OR_GREATER 13 | public interface ICompressionDecoder : IStaticFormatRecognition 14 | #else 15 | public interface ICompressionDecoder : IFormatRecognition 16 | #endif 17 | { 18 | /// 19 | /// Decompresses data from the and writes the decompressed data to the . 20 | /// 21 | /// The containing compressed data to be decompressed. 22 | /// The to write the decompressed data to. 23 | /// Thrown if the end of the stream is reached unexpectedly. 24 | /// Thrown if the or is null. 25 | /// Thrown if the stream does not contain valid compressed data. 26 | /// Thrown if the stream does not contain valid compressed data. 27 | /// Thrown if the stream does not support writing. 28 | void Decompress(Stream source, Stream destination); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Interfaces/ICompressionEncoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | 5 | namespace AuroraLib.Compression.Interfaces 6 | { 7 | /// 8 | /// Defines an interface for file compression. 9 | /// 10 | public interface ICompressionEncoder 11 | { 12 | /// 13 | /// Compresses data from the span and writes the compressed data to the . 14 | /// 15 | /// The ReadOnlySpan containing the data to be compressed. 16 | /// The Stream to write the compressed data to. 17 | /// The CompressionLevel to use for compression (default is ). 18 | /// Thrown if the or is null. 19 | /// Thrown if the stream does not support writing. 20 | void Compress(ReadOnlySpan source, Stream destination, CompressionLevel level = CompressionLevel.Optimal); 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Interfaces/IEndianDependentFormat.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Core; 2 | 3 | namespace AuroraLib.Compression.Interfaces 4 | { 5 | /// 6 | /// Represents a file format whose endianness (byte order) depends on the target architecture. 7 | /// 8 | public interface IEndianDependentFormat 9 | { 10 | /// 11 | /// Gets or sets the byte order (endianness) for this format, depending on the target architecture. 12 | /// 13 | Endian FormatByteOrder { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Interfaces/ILzSettings.cs: -------------------------------------------------------------------------------- 1 | namespace AuroraLib.Compression.Interfaces 2 | { 3 | /// 4 | /// Extends the interface to provide functionality more settings for LZ-based compression algorithms, allowing for customization of compression behavior. 5 | /// 6 | public interface ILzSettings : ICompressionEncoder 7 | { 8 | /// 9 | /// Enables the algorithm to analyze repeating patterns more effectively, resulting in smaller compressed files. 10 | /// Note: Activating this setting may cause issues if the specific implementation of a game does not support LookAhead optimization. 11 | /// 12 | bool LookAhead { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/Interfaces/IProvidesDecompressedSize.cs: -------------------------------------------------------------------------------- 1 | using AuroraLib.Core.Exceptions; 2 | using System; 3 | using System.IO; 4 | 5 | namespace AuroraLib.Compression.Interfaces 6 | { 7 | /// 8 | /// Extends the interface to provide functionality for determining the size of decompressed data. 9 | /// 10 | public interface IProvidesDecompressedSize : ICompressionDecoder 11 | { 12 | /// 13 | /// Reads the provided to determine the size of the decompressed data. 14 | /// 15 | /// The stream containing the compressed data. 16 | /// The size of the decompressed data, in bytes. 17 | /// 18 | /// Thrown if the is null. 19 | /// Thrown if the stream does not contain valid compressed data. 20 | uint GetDecompressedSize(Stream source); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/LzProperties.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Compression; 3 | 4 | namespace AuroraLib.Compression 5 | { 6 | /// 7 | /// Represents properties and settings for LZ compression. 8 | /// 9 | public class LzProperties 10 | { 11 | /// 12 | /// Gets the number of bits used to represent the distance. 13 | /// 14 | public readonly byte DistanceBits; 15 | 16 | /// 17 | /// Gets the number of bits used to represent the length. 18 | /// 19 | public readonly byte LengthBits; 20 | 21 | /// 22 | /// Gets the minimum match length. 23 | /// 24 | public readonly byte MinLength; 25 | 26 | /// 27 | /// Gets the maximum match length. 28 | /// 29 | public readonly int MaxLength; 30 | 31 | /// 32 | /// Gets the window size. 33 | /// 34 | public readonly int WindowsSize; 35 | 36 | /// 37 | /// Gets the window start position. 38 | /// 39 | public readonly int WindowsStart; 40 | 41 | public LzProperties(int windowsSize, int maxLength, byte minLength = 3, int windowsStart = 0) 42 | { 43 | DistanceBits = (byte)Math.Ceiling(Math.Log(windowsSize, 2)); 44 | LengthBits = (byte)Math.Ceiling(Math.Log(maxLength - minLength, 2)); 45 | MinLength = minLength; 46 | WindowsSize = windowsSize; 47 | MaxLength = maxLength; 48 | WindowsStart = windowsStart; 49 | } 50 | 51 | public LzProperties(byte distanceBits, byte lengthBits, byte threshold = 2) 52 | { 53 | DistanceBits = distanceBits; 54 | LengthBits = lengthBits; 55 | MinLength = (byte)(threshold + 1); 56 | WindowsSize = 1 << distanceBits; 57 | MaxLength = (1 << lengthBits) + threshold; 58 | WindowsStart = WindowsSize - (1 << lengthBits) - threshold; 59 | } 60 | 61 | public int GetWindowsLevel(CompressionLevel level) 62 | #if NET6_0_OR_GREATER 63 | => level switch 64 | { 65 | CompressionLevel.Optimal => WindowsSize > 0x8000 ? 0x8000 : WindowsSize, 66 | CompressionLevel.Fastest => WindowsSize > 0x4000 ? 0x4000 : WindowsSize >> 1, 67 | CompressionLevel.SmallestSize => WindowsSize, 68 | CompressionLevel.NoCompression => 0, 69 | _ => throw new NotImplementedException(), 70 | }; 71 | #else 72 | { 73 | switch (level) 74 | { 75 | case CompressionLevel.Optimal: 76 | return WindowsSize > 0x10000 ? 0x10000 : WindowsSize; 77 | case CompressionLevel.Fastest: 78 | return WindowsSize > 0x4000 ? 0x4000 : WindowsSize >> 1; 79 | case (CompressionLevel)3: 80 | return WindowsSize; 81 | case CompressionLevel.NoCompression: 82 | return 0; 83 | default: 84 | throw new NotImplementedException(); 85 | } 86 | } 87 | #endif 88 | 89 | public int GetWindowsFlag() 90 | => WindowsSize - 1; 91 | 92 | public int GetLengthBitsFlag() 93 | => (1 << LengthBits) - 1; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/MatchFinder/LzMatch.cs: -------------------------------------------------------------------------------- 1 | namespace AuroraLib.Compression.MatchFinder 2 | { 3 | /// 4 | /// A match in LZ compression, storing information about the distance and length of the match. 5 | /// 6 | public readonly struct LzMatch 7 | { 8 | public readonly int Offset; 9 | /// 10 | /// Gets the distance of the match, indicating the offset from the current position. 11 | /// 12 | public readonly int Distance; 13 | 14 | /// 15 | /// Gets the length of the match, indicating the number of bytes in the match. 16 | /// 17 | public readonly int Length; 18 | 19 | public LzMatch(int offset, int distance, int length) 20 | { 21 | Offset = offset; 22 | Distance = distance; 23 | Length = length; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AuroraLib.Compression/MatchFinder/RleMatchFinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace AuroraLib.Compression 5 | { 6 | /// 7 | /// Provides functionality for finding matches in Run-Length compression algorithm. 8 | /// 9 | public sealed class RleMatchFinder 10 | { 11 | private readonly int minMatch; 12 | private readonly int maxMatch; 13 | 14 | public RleMatchFinder(int minMatch = 3, int maxMatch = 127) 15 | { 16 | this.minMatch = minMatch; 17 | this.maxMatch = maxMatch; 18 | } 19 | 20 | /// 21 | /// Attempts to find the best match in the provided source data at the given offset and returns the match information. 22 | /// 23 | /// The source data to search for matches. 24 | /// The offset in the source data to start searching for a match. 25 | /// True if a match is found; otherwise, false. 26 | #if !(NETSTANDARD || NET20_OR_GREATER) 27 | [MethodImpl(MethodImplOptions.AggressiveOptimization)] 28 | #endif 29 | public bool TryToFindMatch(ReadOnlySpan source, int offset, out int duration) 30 | { 31 | ReadOnlySpan dataToMatch = source.Slice(offset, Math.Min(maxMatch, source.Length - offset)); 32 | duration = GetRelMatchLength(dataToMatch); 33 | 34 | if (duration < minMatch) 35 | { 36 | duration = 0; 37 | do 38 | { 39 | duration++; 40 | 41 | if (source.Length - offset - duration < minMatch) 42 | { 43 | duration = source.Length - offset; 44 | break; 45 | } 46 | dataToMatch = source.Slice(offset + duration, minMatch); 47 | } while (duration != maxMatch && minMatch != GetRelMatchLength(dataToMatch)); 48 | return false; 49 | } 50 | return true; 51 | } 52 | 53 | private static int GetRelMatchLength(ReadOnlySpan data) 54 | { 55 | byte matchByte = data[0]; 56 | for (int i = 1; i < data.Length; i++) 57 | { 58 | if (data[i] != matchByte) 59 | { 60 | return i; 61 | } 62 | } 63 | return data.Length; 64 | } 65 | } 66 | } 67 | --------------------------------------------------------------------------------