diff --git a/fxl.codes.kisekae.data.test/Examples/2RANMAS.lzh b/fxl.codes.kisekae.data.test/Examples/2RANMAS.lzh new file mode 100644 index 0000000..0c6c879 Binary files /dev/null and b/fxl.codes.kisekae.data.test/Examples/2RANMAS.lzh differ diff --git a/fxl.codes.kisekae.data.test/LhContainerTests.cs b/fxl.codes.kisekae.data.test/LhContainerTests.cs new file mode 100644 index 0000000..4e474ef --- /dev/null +++ b/fxl.codes.kisekae.data.test/LhContainerTests.cs @@ -0,0 +1,50 @@ +using System.Reflection; +using fxl.codes.kisekae.data.Archives; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace fxl.codes.kisekae.data.test; + +public class LhContainerTests +{ + private readonly Assembly _assembly = typeof(LhContainerTests).Assembly; + private readonly List _resourceNames = new(); + private LhCodecService _service; + + [SetUp] + public void Setup() + { + foreach (var name in _assembly.GetManifestResourceNames()) _resourceNames.Add(name); + var logger = new Logger(NullLoggerFactory.Instance); + _service = new LhCodecService(logger); + } + + [Test] + public void TestHeader() + { + foreach (var resource in _resourceNames) + { + using var stream = _assembly.GetManifestResourceStream(resource); + if (stream == null) continue; + + var files = LhCodecService.GetFiles(in stream); + + Assert.Multiple(() => + { + Assert.That(files, Is.Not.Empty); + + foreach (var file in files) + { + Assert.That(file.FileName, Is.Not.Null, "Filename is {0}", file.FileName); + Assert.That(file.CompressedSize, Is.GreaterThan(0), "Compressed size is {0}", file.CompressedSize); + Assert.That(file.UncompressedSize, Is.GreaterThan(0), "Uncompressed size is {0}", file.UncompressedSize); + Assert.That(file.UncompressedSize, Is.GreaterThan(file.CompressedSize), "Uncompressed is larger than compressed"); + Assert.That(file.MethodId, Is.Not.Null, "Method id is {0}", file.MethodId); + Assert.That(file.MethodId, Does.StartWith("-l")); + Assert.That(file.MethodId, Does.EndWith("-")); + Console.WriteLine($"Archive name: {file.FileName}, compression method: {file.MethodId}"); + } + }); + } + } +} \ No newline at end of file diff --git a/fxl.codes.kisekae.data.test/LhMethodTests.cs b/fxl.codes.kisekae.data.test/LhMethodTests.cs deleted file mode 100644 index 564e950..0000000 --- a/fxl.codes.kisekae.data.test/LhMethodTests.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace fxl.codes.kisekae.data.test; - -public class LhMethodTests -{ - [SetUp] - public void Setup() - { - } - - [Test] - public void Test1() - { - Assert.Pass(); - } -} \ No newline at end of file diff --git a/fxl.codes.kisekae.data.test/fxl.codes.kisekae.data.test.csproj b/fxl.codes.kisekae.data.test/fxl.codes.kisekae.data.test.csproj index 366a6ff..fc11ded 100644 --- a/fxl.codes.kisekae.data.test/fxl.codes.kisekae.data.test.csproj +++ b/fxl.codes.kisekae.data.test/fxl.codes.kisekae.data.test.csproj @@ -23,4 +23,19 @@ + + + + + + + + PreserveNewest + + + + + + + diff --git a/fxl.codes.kisekae.data/Archives/Algorithms/HuffmanNode.cs b/fxl.codes.kisekae.data/Archives/Algorithms/HuffmanNode.cs new file mode 100644 index 0000000..efecaad --- /dev/null +++ b/fxl.codes.kisekae.data/Archives/Algorithms/HuffmanNode.cs @@ -0,0 +1,35 @@ +namespace fxl.codes.kisekae.data.Archives.Algorithms; + +public class HuffmanNode +{ + public HuffmanNode(char value) + { + } + + public HuffmanNode(HuffmanNode left, HuffmanNode right) + { + Left = left; + Left.Side = HuffmanNodeSide.Left; + Left.Parent = this; + + Right = right; + Right.Side = HuffmanNodeSide.Right; + Right.Parent = this; + + Weight = left.Weight + right.Weight; + } + + public char? Value { get; set; } + public int Weight { get; set; } = 1; + + public HuffmanNode? Parent { get; set; } + public HuffmanNode? Left { get; set; } + public HuffmanNode? Right { get; set; } + public HuffmanNodeSide? Side { get; set; } +} + +public enum HuffmanNodeSide +{ + Left, + Right +} \ No newline at end of file diff --git a/fxl.codes.kisekae.data/Archives/EndianUtility.cs b/fxl.codes.kisekae.data/Archives/EndianUtility.cs index 55b43e7..de2db30 100644 --- a/fxl.codes.kisekae.data/Archives/EndianUtility.cs +++ b/fxl.codes.kisekae.data/Archives/EndianUtility.cs @@ -6,7 +6,7 @@ internal static class EndianUtility { public static int ToLittleEndian(this ReadOnlySpan bytes) { - var x = BitConverter.ToInt32(bytes); + var x = bytes.Length == 2 ? BitConverter.ToInt16(bytes) : BitConverter.ToInt32(bytes); return BitConverter.IsLittleEndian ? x : BinaryPrimitives.ReverseEndianness(x); } diff --git a/fxl.codes.kisekae.data/Archives/LhCodecService.cs b/fxl.codes.kisekae.data/Archives/LhCodecService.cs index f27e5a4..f4a090c 100644 --- a/fxl.codes.kisekae.data/Archives/LhCodecService.cs +++ b/fxl.codes.kisekae.data/Archives/LhCodecService.cs @@ -1,4 +1,3 @@ -using fxl.codes.kisekae.data.Archives.LhHeaders; using fxl.codes.kisekae.data.Entities; using Microsoft.Extensions.Logging; @@ -8,29 +7,13 @@ public class LhCodecService(ILogger logger) { public async Task ReadArchive(Stream stream) { - try - { - var header = new LhHeader(in stream); - switch (header.MethodId) - { - case "-lh0-": break; - case "-lzs-": break; - case "-lz4-": break; - case "-lh1-": break; - case "-lh2-": break; - case "-lh3-": break; - case "-lh4-": break; - case "-lh5-": break; - case "-lh6-": break; - case "-lh7-": break; - default: throw new Exception("Unknown method"); - } - } - catch (Exception e) - { - logger.LogError(e, "Unable to open archive"); - } + throw new NotImplementedException(); + } - return null; + internal static IEnumerable GetFiles(ref readonly Stream stream) + { + var files = new List(); + while (stream.Position + 21 < stream.Length) files.Add(new LhContainer(in stream)); + return files; } } \ No newline at end of file diff --git a/fxl.codes.kisekae.data/Archives/LhHeaders/LhHeader.cs b/fxl.codes.kisekae.data/Archives/LhContainer.cs similarity index 88% rename from fxl.codes.kisekae.data/Archives/LhHeaders/LhHeader.cs rename to fxl.codes.kisekae.data/Archives/LhContainer.cs index 25d6799..3203a7d 100644 --- a/fxl.codes.kisekae.data/Archives/LhHeaders/LhHeader.cs +++ b/fxl.codes.kisekae.data/Archives/LhContainer.cs @@ -1,19 +1,20 @@ using System.Text; -namespace fxl.codes.kisekae.data.Archives.LhHeaders; +namespace fxl.codes.kisekae.data.Archives; -internal class LhHeader +internal class LhContainer { - public LhHeader(ref readonly Stream stream) + public LhContainer(ref readonly Stream stream) { + HeaderDataPosition = stream.Position; var bytes = new byte[21]; - stream.Position = 0; stream.ReadExactly(bytes); var span = bytes.AsSpan(); Level = span[20]; MethodId = Encoding.ASCII.GetString(span[2..7]); CompressedSize = EndianUtility.ToLittleEndian(span[7..11]); + CompressedFile = new byte[CompressedSize]; UncompressedSize = EndianUtility.ToLittleEndian(span[11..15]); Created = EndianUtility.ToDateTime(span[15..19]); FileOrDirectory = span[19]; @@ -36,7 +37,7 @@ internal class LhHeader public int HeaderSize { get; private set; } public int HeaderCheckSum { get; private set; } public string MethodId { get; private set; } - public int CompressedSize { get; private set; } + public int CompressedSize { get; } public int UncompressedSize { get; private set; } public DateTime Created { get; private set; } public int FileOrDirectory { get; private set; } @@ -45,25 +46,33 @@ internal class LhHeader public string? FileName { get; private set; } + public long HeaderDataPosition { get; } public long FileDataPosition { get; private set; } + public byte[] CompressedFile { get; } private void Level0(ReadOnlySpan span, ref readonly Stream stream) { HeaderSize = span[0]; HeaderCheckSum = span[1]; - stream.Position = span.Length; FileName = GetFilename(in stream); Crc16 = (short)stream.ReadLittleEndian(2); FileDataPosition = stream.Position; + stream.ReadExactly(CompressedFile); } private void Level1(ReadOnlySpan span, ref readonly Stream stream) { - Level0(span, in stream); + HeaderSize = span[0]; + HeaderCheckSum = span[1]; + FileName = GetFilename(in stream); + Crc16 = (short)stream.ReadLittleEndian(2); OperatingSystem = (char)stream.ReadByte(); + SetExtendedHeaders(in stream); + FileDataPosition = stream.Position; + stream.ReadExactly(CompressedFile); } private void Level2(ReadOnlySpan span, ref readonly Stream stream) @@ -71,8 +80,11 @@ internal class LhHeader HeaderSize = span[..2].ToLittleEndian(); Crc16 = (short)stream.ReadLittleEndian(2); OperatingSystem = (char)stream.ReadByte(); + SetExtendedHeaders(in stream); + FileDataPosition = stream.Position; + stream.ReadExactly(CompressedFile); } private static string GetFilename(ref readonly Stream stream) diff --git a/fxl.codes.kisekae.data/Archives/LhMethods/BaseLhMethod.cs b/fxl.codes.kisekae.data/Archives/LhMethods/BaseLhMethod.cs index 6950e5a..700e01d 100644 --- a/fxl.codes.kisekae.data/Archives/LhMethods/BaseLhMethod.cs +++ b/fxl.codes.kisekae.data/Archives/LhMethods/BaseLhMethod.cs @@ -1,29 +1,32 @@ using fxl.codes.kisekae.data.Archives.Algorithms; -using fxl.codes.kisekae.data.Archives.LhHeaders; namespace fxl.codes.kisekae.data.Archives.LhMethods; -internal abstract class BaseLhMethod(ref readonly Stream stream, LhHeader header, int slidingWindowBytes, int matchingWindowBytes, IAlgorithm? algorithm) +internal abstract class BaseLhMethod(LhContainer container, int slidingWindowBytes, int matchingWindowBytes, IAlgorithm? algorithm) { public int SlidingWindow => slidingWindowBytes * 1024; + + public void InitializeAlgorithm() + { + } } -internal class Lzs(ref readonly Stream stream, LhHeader header) : BaseLhMethod(in stream, header, 2, 17, null); +internal class Lzs(LhContainer container) : BaseLhMethod(container, 2, 17, null); -internal class Lz4(ref readonly Stream stream, LhHeader header) : BaseLhMethod(in stream, header, 0, 0, null); +internal class Lz4(LhContainer container) : BaseLhMethod(container, 0, 0, null); -internal class Lh0(ref readonly Stream stream, LhHeader header) : BaseLhMethod(in stream, header, 0, 0, null); +internal class Lh0(LhContainer container) : BaseLhMethod(container, 0, 0, null); -internal class Lh1(ref readonly Stream stream, LhHeader header) : BaseLhMethod(in stream, header, 4, 60, new HuffmanDynamic()); +internal class Lh1(LhContainer container) : BaseLhMethod(container, 4, 60, new HuffmanDynamic()); -internal class Lh2(ref readonly Stream stream, LhHeader header) : BaseLhMethod(in stream, header, 8, 256, new HuffmanDynamic()); +internal class Lh2(LhContainer container) : BaseLhMethod(container, 8, 256, new HuffmanDynamic()); -internal class Lh3(ref readonly Stream stream, LhHeader header) : BaseLhMethod(in stream, header, 8, 256, new HuffmanStatic()); +internal class Lh3(LhContainer container) : BaseLhMethod(container, 8, 256, new HuffmanStatic()); -internal class Lh4(ref readonly Stream stream, LhHeader header) : BaseLhMethod(in stream, header, 4, 256, new HuffmanStatic()); +internal class Lh4(LhContainer container) : BaseLhMethod(container, 4, 256, new HuffmanStatic()); -internal class Lh5(ref readonly Stream stream, LhHeader header) : BaseLhMethod(in stream, header, 8, 256, new HuffmanStatic()); +internal class Lh5(LhContainer container) : BaseLhMethod(container, 8, 256, new HuffmanStatic()); -internal class Lh6(ref readonly Stream stream, LhHeader header) : BaseLhMethod(in stream, header, 32, 256, new HuffmanStatic()); +internal class Lh6(LhContainer container) : BaseLhMethod(container, 32, 256, new HuffmanStatic()); -internal class Lh7(ref readonly Stream stream, LhHeader header) : BaseLhMethod(in stream, header, 64, 256, new HuffmanStatic()); \ No newline at end of file +internal class Lh7(LhContainer container) : BaseLhMethod(container, 64, 256, new HuffmanStatic()); \ No newline at end of file diff --git a/fxl.codes.kisekae.data/fxl.codes.kisekae.data.csproj b/fxl.codes.kisekae.data/fxl.codes.kisekae.data.csproj index 383954d..c4c15a5 100644 --- a/fxl.codes.kisekae.data/fxl.codes.kisekae.data.csproj +++ b/fxl.codes.kisekae.data/fxl.codes.kisekae.data.csproj @@ -19,4 +19,8 @@ + + + +