From f9b27ead006e9b7408abec42455ed4556b068b4e Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Thu, 26 Mar 2026 18:16:15 +0100 Subject: [PATCH 01/17] ted impl skeleton --- ModVerify.slnx | 1 + .../Binary/Reader/ITedFileReader.cs | 6 +++ .../Binary/Reader/ITedFileReaderFactory.cs | 8 ++++ .../Binary/Reader/TedFileReader.cs | 13 ++++++ .../Binary/Reader/TedFileReaderFactory.cs | 12 ++++++ .../Data/IMapData.cs | 8 ++++ .../Files/ITedFile.cs | 6 +++ .../Files/TedFile.cs | 7 +++ .../Files/TedFileInformation.cs | 19 ++++++++ .../PG.StarWarsGame.Files.TED.csproj | 27 ++++++++++++ .../Services/ITedFileService.cs | 14 ++++++ .../Services/TedFileService.cs | 43 +++++++++++++++++++ .../TedServiceContribution.cs | 14 ++++++ 13 files changed, 178 insertions(+) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReader.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReaderFactory.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/ITedFile.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFile.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFileInformation.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/PG.StarWarsGame.Files.TED.csproj create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/ITedFileService.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/TedFileService.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/TedServiceContribution.cs diff --git a/ModVerify.slnx b/ModVerify.slnx index 3527ff4e..35980abe 100644 --- a/ModVerify.slnx +++ b/ModVerify.slnx @@ -20,6 +20,7 @@ + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReader.cs new file mode 100644 index 00000000..619e29cb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReader.cs @@ -0,0 +1,6 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +using PG.StarWarsGame.Files.TED.Data; + +namespace PG.StarWarsGame.Files.TED.Binary.Reader; + +internal interface ITedFileReader : IChunkFileReader; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReaderFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReaderFactory.cs new file mode 100644 index 00000000..a0cc0824 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReaderFactory.cs @@ -0,0 +1,8 @@ +using System.IO; + +namespace PG.StarWarsGame.Files.TED.Binary.Reader; + +internal interface ITedFileReaderFactory +{ + ITedFileReader GetReader(Stream dataStream); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs new file mode 100644 index 00000000..92e0c5f2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs @@ -0,0 +1,13 @@ +using System.IO; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +using PG.StarWarsGame.Files.TED.Data; + +namespace PG.StarWarsGame.Files.TED.Binary.Reader; + +internal sealed class TedFileReader(Stream stream) : ChunkFileReaderBase(stream), ITedFileReader +{ + public override IMapData Read() + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs new file mode 100644 index 00000000..d9be9eb8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs @@ -0,0 +1,12 @@ +using System; +using System.IO; + +namespace PG.StarWarsGame.Files.TED.Binary.Reader; + +internal class TedFileReaderFactory(IServiceProvider serviceProvider) : ITedFileReaderFactory +{ + public ITedFileReader GetReader(Stream dataStream) + { + return new TedFileReader(dataStream); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs new file mode 100644 index 00000000..bd335930 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs @@ -0,0 +1,8 @@ +using AnakinRaW.CommonUtilities; +using PG.StarWarsGame.Files.ChunkFiles.Data; + +namespace PG.StarWarsGame.Files.TED.Data; + +public class IMapData : DisposableObject, IChunkData +{ +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/ITedFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/ITedFile.cs new file mode 100644 index 00000000..76f610d9 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/ITedFile.cs @@ -0,0 +1,6 @@ +using PG.StarWarsGame.Files.ChunkFiles.Files; +using PG.StarWarsGame.Files.TED.Data; + +namespace PG.StarWarsGame.Files.TED.Files; + +public interface ITedFile : IChunkFile; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFile.cs new file mode 100644 index 00000000..5b075d66 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFile.cs @@ -0,0 +1,7 @@ +using System; +using PG.StarWarsGame.Files.TED.Data; + +namespace PG.StarWarsGame.Files.TED.Files; + +public sealed class TedFile(IMapData data, TedFileInformation fileInformation, IServiceProvider serviceProvider) + : PetroglyphFileHolder(data, fileInformation, serviceProvider), ITedFile; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFileInformation.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFileInformation.cs new file mode 100644 index 00000000..22a532e8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFileInformation.cs @@ -0,0 +1,19 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace PG.StarWarsGame.Files.TED.Files; + +public sealed record TedFileInformation : PetroglyphMegPackableFileInformation +{ + /// + /// Initializes a new instance of the class. + /// + /// The file path of the alo file. + /// Information whether this file info is created from a meg data entry. + /// is null. + /// is empty. + [SetsRequiredMembers] + public TedFileInformation(string path, bool isInMeg) : base(path, isInMeg) + { + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/PG.StarWarsGame.Files.TED.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/PG.StarWarsGame.Files.TED.csproj new file mode 100644 index 00000000..0071f913 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/PG.StarWarsGame.Files.TED.csproj @@ -0,0 +1,27 @@ + + + netstandard2.0;netstandard2.1;net10.0 + PG.StarWarsGame.Files.TED + PG.StarWarsGame.Files.TED + AlamoEngineTools.PG.StarWarsGame.Files.TED + alamo,petroglyph,glyphx + + + + true + + + true + snupkg + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/ITedFileService.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/ITedFileService.cs new file mode 100644 index 00000000..72b96d40 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/ITedFileService.cs @@ -0,0 +1,14 @@ +using System.IO; +using System.IO.Abstractions; +using PG.StarWarsGame.Files.TED.Files; + +namespace PG.StarWarsGame.Files.TED.Services; + +public interface ITedFileService +{ + void RemoveMapPreview(Stream tedStream, FileSystemStream destination, bool extract, out byte[]? previewImageBytes); + + ITedFile Load(string path); + + ITedFile Load(Stream stream); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/TedFileService.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/TedFileService.cs new file mode 100644 index 00000000..9e97c4f8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/TedFileService.cs @@ -0,0 +1,43 @@ +using PG.Commons.Services; +using PG.Commons.Utilities; +using PG.StarWarsGame.Files.TED.Files; +using System; +using System.IO; +using System.IO.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.TED.Binary.Reader; + +namespace PG.StarWarsGame.Files.TED.Services; + +internal class TedFileService(IServiceProvider serviceProvider) : ServiceBase(serviceProvider), ITedFileService +{ + public void RemoveMapPreview(Stream tedStream, FileSystemStream destination, bool extract, out byte[]? previewImageBytes) + { + if (tedStream == null) + throw new ArgumentNullException(nameof(tedStream)); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + + throw new NotImplementedException(); + } + + public ITedFile Load(string path) + { + using var fileStream = FileSystem.FileStream.New(path, FileMode.Open, FileAccess.Read, FileShare.Read); + return Load(fileStream); + } + + public ITedFile Load(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + using var reader = Services.GetRequiredService().GetReader(stream); + var map = reader.Read(); + + var filePath = stream.GetFilePath(out var isInMeg); + var fileInfo = new TedFileInformation(filePath, isInMeg); + + return new TedFile(map, fileInfo, Services); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/TedServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/TedServiceContribution.cs new file mode 100644 index 00000000..d2ba314c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/TedServiceContribution.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.TED.Binary.Reader; +using PG.StarWarsGame.Files.TED.Services; + +namespace PG.StarWarsGame.Files.TED; + +public static class TedServiceContribution +{ + public static void SupportTED(this IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(sp => new TedFileReaderFactory(sp)); + serviceCollection.AddSingleton(sp => new TedFileService(sp)); + } +} \ No newline at end of file From 8272e1e7ac94a0de3fc48e781a4e87d6313f80c8 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 28 Mar 2026 10:13:44 +0100 Subject: [PATCH 02/17] start implementing TED support --- ModVerify.slnx | 1 + .../Identifier/AloContentInfoIdentifier.cs | 10 +- .../Reader/Animations/AnimationReaderBase.cs | 28 +-- .../Binary/Reader/Models/ModelFileReader.cs | 44 ++-- .../Reader/Particles/ParticleReaderV1.cs | 26 +-- .../Binary/Metadata/Chunk.cs | 189 ++++++++++++++++++ .../Binary/Metadata/ChunkFile.cs | 48 +++++ .../Binary/Metadata/ChunkMetadata.cs | 50 +++-- .../Binary/Reader/ChunkFileReaderBase.cs | 9 +- .../Binary/Reader/ChunkReader.cs | 38 +++- .../CommonDatTestBase.cs | 13 ++ .../PG.StarWarsGame.Files.TED.Test.csproj | 37 ++++ .../Services/TedFileServiceTest.cs | 52 +++++ .../Binary/Reader/TedFileReader.cs | 5 +- .../Binary/Writer/MapPreviewExtractor.cs | 57 ++++++ .../Properties/AssemblyAttributes.cs | 3 + .../Services/ITedFileService.cs | 4 +- .../Services/TedFileService.cs | 14 +- 18 files changed, 544 insertions(+), 84 deletions(-) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/CommonDatTestBase.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/Services/TedFileServiceTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Writer/MapPreviewExtractor.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Properties/AssemblyAttributes.cs diff --git a/ModVerify.slnx b/ModVerify.slnx index 35980abe..dd535739 100644 --- a/ModVerify.slnx +++ b/ModVerify.slnx @@ -20,6 +20,7 @@ + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs index 6fce5c07..3024efd7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs @@ -19,7 +19,7 @@ public AloContentInfo GetContentInfo(Stream stream) case ChunkType.Skeleton: case ChunkType.Mesh: case ChunkType.Light: - return FromModel(chunk.Size, chunkReader); + return FromModel(chunk.BodySize, chunkReader); case ChunkType.Connections: return FromConnection(chunkReader); case ChunkType.Particle: @@ -41,14 +41,14 @@ private static AloContentInfo FromAnimation(ChunkReader chunkReader) switch ((ChunkType)chunk.Value.Type) { case ChunkType.AnimationInformation: - return chunk.Value.Size switch + return chunk.Value.BodySize switch { 36 => new AloContentInfo(AloType.Animation, AloVersion.V2), 18 => new AloContentInfo(AloType.Animation, AloVersion.V1), _ => throw new BinaryCorruptedException("Invalid ALA animation.") }; default: - chunkReader.Skip(chunk.Value.Size); + chunkReader.Skip(chunk.Value.BodySize); break; } chunk = chunkReader.TryReadChunk(); @@ -66,7 +66,7 @@ private static AloContentInfo FromConnection(ChunkReader chunkReader) case ChunkType.ProxyConnection: case ChunkType.ObjectConnection: case ChunkType.ConnectionCounts: - chunkReader.Skip(chunk.Value.Size); + chunkReader.Skip(chunk.Value.BodySize); break; case ChunkType.Dazzle: return new AloContentInfo(AloType.Model, AloVersion.V2); @@ -92,7 +92,7 @@ private static AloContentInfo FromModel(int size, ChunkReader chunkReader) case ChunkType.Skeleton: case ChunkType.Mesh: case ChunkType.Light: - return FromModel(chunk.Value.Size, chunkReader); + return FromModel(chunk.Value.BodySize, chunkReader); default: throw new BinaryCorruptedException("Invalid ALO model."); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs index 632caf37..dbcfc4d1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs @@ -27,11 +27,11 @@ public sealed override AlamoAnimation Read() { var chunk = ChunkReader.ReadChunk(ref actualSize); ReadAnimation(chunk, ref info, bones); - actualSize += chunk.Size; + actualSize += chunk.BodySize; - } while (actualSize < rootChunk.Size); + } while (actualSize < rootChunk.BodySize); - if (actualSize != rootChunk.Size) + if (actualSize != rootChunk.BodySize) throw new BinaryCorruptedException(); if (info.NumberBones != bones.Count) @@ -54,13 +54,15 @@ protected virtual void ReadAnimation( switch (chunk.Type) { case (int)AnimationChunkTypes.AnimationInfo: - animationInformation = ReadAnimationInfo(chunk.Size); + if (chunk.RawSize < 0) + ThrowChunkSizeTooLargeException(); + animationInformation = ReadAnimationInfo(chunk.BodySize); break; case (int)AnimationChunkTypes.BoneData: - ReadBonesData(chunk.Size, bones); + ReadBonesData(chunk.BodySize, bones); break; default: - ChunkReader.Skip(chunk.Size); + ChunkReader.Skip(chunk.BodySize); break; } } @@ -70,10 +72,12 @@ protected virtual void ReadBoneDataCore(ChunkMetadata chunk, List bones) { var chunk = ChunkReader.ReadChunk(ref actualSize); ReadBoneDataCore(chunk, bones); - actualSize += chunk.Size; + actualSize += chunk.BodySize; } while (actualSize < chunkSize); @@ -108,13 +112,13 @@ private void ReadBoneInfo(int chunkSize, List bones) switch (chunk.Type) { case (int)AnimationChunkTypes.BoneName: - name = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true, ref actualSize); + name = ChunkReader.ReadString(chunk.BodySize, Encoding.ASCII, true, ref actualSize); break; case (int)AnimationChunkTypes.BoneIndex: index = ChunkReader.ReadDword(ref actualSize); break; default: - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); break; } @@ -156,7 +160,7 @@ private AnimationInformationData ReadAnimationInfo(int chunkSize) info.ScaleBlockSize = ChunkReader.ReadDword(ref actualSize); break; default: - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); break; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Models/ModelFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Models/ModelFileReader.cs index 13da425c..6aee4b60 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Models/ModelFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Models/ModelFileReader.cs @@ -25,16 +25,16 @@ public override AlamoModel Read() switch (chunk.Value.Type) { case (int)ModelChunkTypes.Skeleton: - ReadSkeleton(chunk.Value.Size, bones); + ReadSkeleton(chunk.Value.BodySize, bones); break; case (int)ModelChunkTypes.Mesh: - ReadMesh(chunk.Value.Size, textures, shaders); + ReadMesh(chunk.Value.BodySize, textures, shaders); break; case (int)ModelChunkTypes.Connections: - ReadConnections(chunk.Value.Size, proxies); + ReadConnections(chunk.Value.BodySize, proxies); break; default: - ChunkReader.Skip(chunk.Value.Size); + ChunkReader.Skip(chunk.Value.BodySize); break; } @@ -59,14 +59,16 @@ private void ReadConnections(int size, HashSet proxies) do { var chunk = ChunkReader.ReadChunk(ref actualSize); + if (chunk.RawSize < 0) + ThrowChunkSizeTooLargeException(); if (chunk.Type == (int)ModelChunkTypes.ProxyConnection) { - ReadProxy(chunk.Size, out var proxy, ref actualSize); + ReadProxy(chunk.BodySize, out var proxy, ref actualSize); proxies.Add(proxy); } else - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); } while (actualSize < size); @@ -84,9 +86,9 @@ private void ReadProxy(int size, out string proxy, ref int readSize) var chunk = ChunkReader.ReadMiniChunk(ref actualSize); if (chunk.Type == 5) - proxy = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true, ref actualSize); + proxy = ChunkReader.ReadString(chunk.BodySize, Encoding.ASCII, true, ref actualSize); else - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); } while (actualSize < size); @@ -110,9 +112,9 @@ private void ReadMesh(int size, ISet textures, ISet shaders) var chunk = ChunkReader.ReadChunk(ref actualSize); if (chunk.Type == (int)ModelChunkTypes.SubMeshMaterialInformation) - ReadSubMeshMaterialInformation(chunk.Size, textures, shaders, ref actualSize); + ReadSubMeshMaterialInformation(chunk.BodySize, textures, shaders, ref actualSize); else - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); } while (actualSize < size); @@ -132,15 +134,17 @@ private void ReadSubMeshMaterialInformation(int size, ISet textures, ISe { case (int)ModelChunkTypes.ShaderFileName: { - var shader = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true, ref actualSize); + var shader = ChunkReader.ReadString(chunk.BodySize, Encoding.ASCII, true, ref actualSize); shaders.Add(shader); break; } case (int)ModelChunkTypes.ShaderTexture: - ReadShaderTexture(chunk.Size, textures, ref actualSize); + if (chunk.RawSize < 0) + ThrowChunkSizeTooLargeException(); + ReadShaderTexture(chunk.BodySize, textures, ref actualSize); break; default: - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); break; } @@ -162,11 +166,11 @@ private void ReadShaderTexture(int size, ISet textures, ref int readSize if (mini.Type == 2) { - var texture = ChunkReader.ReadString(mini.Size, Encoding.ASCII, true, ref actualTextureChunkSize); + var texture = ChunkReader.ReadString(mini.BodySize, Encoding.ASCII, true, ref actualTextureChunkSize); textures.Add(texture); } else - ChunkReader.Skip(mini.Size, ref actualTextureChunkSize); + ChunkReader.Skip(mini.BodySize, ref actualTextureChunkSize); } while (actualTextureChunkSize != size); @@ -191,7 +195,7 @@ private void ReadSkeleton(int size, IList bones) var boneCountChunk = ChunkReader.ReadChunk(ref actualSize); - Debug.Assert(boneCountChunk is { Size: 128, Type: (int)ModelChunkTypes.BoneCount }); + Debug.Assert(boneCountChunk is { BodySize: 128, Type: (int)ModelChunkTypes.BoneCount }); var boneCount = ChunkReader.ReadDword(ref actualSize); @@ -201,24 +205,24 @@ private void ReadSkeleton(int size, IList bones) { var bone = ChunkReader.ReadChunk(ref actualSize); - Debug.Assert(bone is { Type: (int)ModelChunkTypes.Bone, IsContainer: true }); + Debug.Assert(bone is { Type: (int)ModelChunkTypes.Bone, HasChildrenHint: true }); var boneReadSize = 0; - while (boneReadSize < bone.Size) + while (boneReadSize < bone.BodySize) { var innerBoneChunk = ChunkReader.ReadChunk(ref boneReadSize); if (innerBoneChunk.Type == (int)ModelChunkTypes.BoneName) { - var nameSize = innerBoneChunk.Size; + var nameSize = innerBoneChunk.BodySize; var name = ChunkReader.ReadString(nameSize, Encoding.ASCII, true, ref boneReadSize); bones.Add(name); } else { - ChunkReader.Skip(innerBoneChunk.Size, ref boneReadSize); + ChunkReader.Skip(innerBoneChunk.BodySize, ref boneReadSize); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Particles/ParticleReaderV1.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Particles/ParticleReaderV1.cs index ed6de4b6..bbca3813 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Particles/ParticleReaderV1.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Particles/ParticleReaderV1.cs @@ -30,23 +30,23 @@ public override AlamoParticle Read() switch (chunk.Type) { case (int)ParticleChunkType.Name: - ReadName(chunk.Size, out name); + ReadName(chunk.BodySize, out name); break; case (int)ParticleChunkType.Emitters: - ReadEmitters(chunk.Size, textures); + ReadEmitters(chunk.BodySize, textures); break; default: - ChunkReader.Skip(chunk.Size); + ChunkReader.Skip(chunk.BodySize); break; } - actualSize += chunk.Size; + actualSize += chunk.BodySize; - } while (actualSize < rootChunk.Size); + } while (actualSize < rootChunk.BodySize); - if (actualSize != rootChunk.Size) + if (actualSize != rootChunk.BodySize) throw new BinaryCorruptedException(); if (string.IsNullOrEmpty(name)) @@ -70,9 +70,9 @@ private void ReadEmitters(int size, HashSet textures) if (chunk.Type != (int)ParticleChunkType.Emitter) throw new BinaryCorruptedException("Unable to read particle"); - ReadEmitter(chunk.Size, textures); + ReadEmitter(chunk.BodySize, textures); - actualSize += chunk.Size; + actualSize += chunk.BodySize; } while (actualSize < size); @@ -92,24 +92,24 @@ private void ReadEmitter(int chunkSize, HashSet textures) if (chunk.Type == (int)ParticleChunkType.Properties) { var shader = ChunkReader.ReadDword(); - ChunkReader.Skip(chunk.Size - sizeof(uint)); + ChunkReader.Skip(chunk.BodySize - sizeof(uint)); } else if (chunk.Type == (int)ParticleChunkType.ColorTextureName) { - var texture = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true); + var texture = ChunkReader.ReadString(chunk.BodySize, Encoding.ASCII, true); textures.Add(texture); } else if (chunk.Type == (int)ParticleChunkType.BumpTextureName) { - var bump = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true); + var bump = ChunkReader.ReadString(chunk.BodySize, Encoding.ASCII, true); textures.Add(bump); } else { - ChunkReader.Skip(chunk.Size); + ChunkReader.Skip(chunk.BodySize); } - actualSize += chunk.Size; + actualSize += chunk.BodySize; } while (actualSize < chunkSize); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs new file mode 100644 index 00000000..c708838d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs @@ -0,0 +1,189 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; +using PG.StarWarsGame.Files.Binary; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; + +public sealed class Chunk : IBinary +{ + public ChunkMetadata Info { get; } + + public byte[]? Data { get; } + + public IReadOnlyList Children { get; } + + public byte[] Bytes + { + get + { + var bytes = new byte[Size]; + GetBytes(bytes); + return bytes; + } + } + + public int Size => Info.IsMiniChunk + ? 2 + Data!.Length + : Data is not null + ? 8 + Data.Length + : 8 + Children.Sum(c => c.Size); + + public Chunk(ChunkMetadata info, byte[] data) + { + Info = info; + Data = data ?? throw new ArgumentNullException(nameof(data)); + Children = []; + } + + public Chunk(ChunkMetadata info, IReadOnlyList children) + { + if (info.IsMiniChunk) + throw new ArgumentException("MiniChunks cannot have child chunks", nameof(info)); + Info = info; + Data = null; + Children = children ?? throw new ArgumentNullException(nameof(children)); + } + + public void GetBytes(Span bytes) + { + if (Info.IsMiniChunk) + { + bytes[0] = (byte)Info.Type; + bytes[1] = (byte)Data!.Length; + Data.AsSpan().CopyTo(bytes[2..]); + return; + } + + BinaryPrimitives.WriteUInt32LittleEndian(bytes, Info.Type); + + if (Data is not null) + { + BinaryPrimitives.WriteInt32LittleEndian(bytes[4..], Data.Length); + Data.AsSpan().CopyTo(bytes[8..]); + } + else + { + // .Sum is a checked operation and will already throw an overflow exception + var bodySize = Children.Sum(c => c.Size); + var hasMiniChunkChildren = Children.Count > 0 && Children[0].Info.IsMiniChunk; + + var sizeField = hasMiniChunkChildren + ? bodySize + : (int)(bodySize | 0x8000_0000u); + + BinaryPrimitives.WriteInt32LittleEndian(bytes[4..], sizeField); + + var offset = 8; + foreach (var child in Children) + { + child.GetBytes(bytes[offset..]); + offset += child.Size; + } + } + } +} + +/// +/// Provides factory methods for creating chunks and chunk files. +/// +/// +/// +/// This class provides static methods for creating hierarchical chunk structures. +/// Three chunk types are supported: +/// +/// +/// Data chunks - Regular chunks containing binary data +/// Mini-chunks - Chunks with 2-byte headers for data up to 255 bytes +/// Node chunks - Container chunks that hold child chunks +/// +/// +public static class ChunkFactory +{ + /// + /// Creates a data chunk with the specified type and binary data. + /// + /// The chunk type identifier. + /// The binary data to store in the chunk. + /// A containing the specified data. + /// is . + public static Chunk Data(uint type, byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var metadata = new ChunkMetadata(type, (uint)data.Length, false); + return new Chunk(metadata, data); + } + + /// + /// Creates a mini-chunk with the specified type and binary data. + /// + /// The mini-chunk type identifier. + /// The binary data to store in the mini-chunk. Maximum length is 255 bytes. + /// A representing a mini-chunk with a 2-byte header. + /// is . + /// The length of exceeds 255 bytes. + public static Chunk Mini(byte type, byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + if (data.Length > byte.MaxValue) + throw new ArgumentException( + $"Mini-chunk data cannot exceed {byte.MaxValue} bytes. Provided data length: {data.Length}.", + nameof(data)); + + var metadata = new ChunkMetadata(type, (uint)data.Length, true); + return new Chunk(metadata, data); + } + + /// + /// Creates a chunk node that contains child chunks. + /// + /// The chunk type identifier. + /// The child chunks to include in this node. + /// A containing the specified children. + /// is . + public static Chunk Node(uint type, params Chunk[] children) + { + if (children == null) + throw new ArgumentNullException(nameof(children)); + + var size = (uint)children.Sum(c => c.Size); + var metadata = new ChunkMetadata(type, size, false); + return new Chunk(metadata, children); + } + + /// + /// Creates a chunk node that contains child chunks built using a configuration action. + /// + /// The chunk type identifier. + /// An action that populates a list with child chunks. + /// A containing the configured children. + /// is . + public static Chunk Node(uint type, Action> configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + var children = new List(); + configure(children); + return Node(type, children.ToArray()); + } + + /// + /// Creates a chunk file containing the specified root chunks. + /// + /// The top-level chunks to include in the file. + /// A containing the specified root chunks. + /// is . + public static ChunkFile File(params Chunk[] rootChunks) + { + if (rootChunks == null) + throw new ArgumentNullException(nameof(rootChunks)); + + return new ChunkFile(rootChunks); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs new file mode 100644 index 00000000..1c38ddbb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using PG.StarWarsGame.Files.Binary.File; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; + +public sealed class ChunkFile : IBinaryFile +{ + public IReadOnlyList RootChunks { get; } + + public int Size => RootChunks.Sum(c => c.Size); + + public byte[] Bytes + { + get + { + var bytes = new byte[Size]; + GetBytes(bytes); + return bytes; + } + } + + public ChunkFile(IReadOnlyList rootChunks) + { + if (rootChunks == null) + throw new ArgumentNullException(nameof(rootChunks)); + if (rootChunks.Count == 0) + throw new ArgumentOutOfRangeException(nameof(rootChunks), "A chunk file must contain at least one chunk"); + RootChunks = rootChunks; + } + + public void GetBytes(Span bytes) + { + var offset = 0; + foreach (var chunk in RootChunks) + { + chunk.GetBytes(bytes[offset..]); + offset += chunk.Size; + } + } + + public void WriteTo(Stream stream) + { + stream.Write(Bytes, 0, Size); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs index df47dc23..3a767498 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs @@ -1,29 +1,39 @@ -namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +using System; +using System.Diagnostics; +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; + +[DebuggerDisplay("Type: {Type}, Size: {BodySize}, Mini:{IsMiniChunk}")] public readonly struct ChunkMetadata { - public readonly int Type; - public readonly int Size; - - private ChunkMetadata(int type, int size, bool isContainer, bool isMiniChunk) - { - Type = type; - Size = size; - IsMiniChunk = isMiniChunk; - IsContainer = isContainer; - } + public readonly uint Type; + public readonly uint RawSize; + public readonly bool IsMiniChunk; - public bool IsContainer { get; } + /// + /// Indicates that bit 31 of RawSize is set. + /// This is a hint that the body contains child chunks, not a guarantee. + /// + public bool HasChildrenHint => !IsMiniChunk && (int)RawSize < 0; - public bool IsMiniChunk { get; } + /// + /// Gets the size of the chunk's data in bytes. + /// + /// + /// This value has bit 31 masked off compared to . + /// Per spec, bit 31 is set only for chunks containing regular child chunks. + /// Chunks containing mini-chunks (treated as data) do NOT set bit 31. + /// Since this library doesn't support sizes > , masking bit 31 + /// has no practical impact on the usable size range. + /// + public int BodySize => (int)(RawSize & 0x7FFF_FFFF); - public static ChunkMetadata FromContainer(int type, int size) + public ChunkMetadata(uint type, uint rawSize, bool isMiniChunk) { - return new ChunkMetadata(type, size, true, false); - } - - public static ChunkMetadata FromData(int type, int size, bool isMini = false) - { - return new ChunkMetadata(type, size, false, isMini); + if (isMiniChunk && rawSize > byte.MaxValue) + throw new ArgumentOutOfRangeException(nameof(rawSize), "Mini chunk size must fit in a byte (0-255)."); + Type = type; + RawSize = rawSize; + IsMiniChunk = isMiniChunk; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs index 01d085ac..eb664447 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using AnakinRaW.CommonUtilities; using PG.StarWarsGame.Files.ChunkFiles.Data; @@ -20,4 +21,10 @@ protected override void DisposeResources() base.DisposeResources(); ChunkReader.Dispose(); } + + //[DoesNotReturn] + protected void ThrowChunkSizeTooLargeException() + { + throw new NotSupportedException("Chunk sizes larger than int.MaxValue are not supported."); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs index 742924dd..024d6eee 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs @@ -20,13 +20,9 @@ public ChunkReader(Stream stream, bool leaveOpen = false) public ChunkMetadata ReadChunk() { - var type = _binaryReader.ReadInt32(); - var rawSize = _binaryReader.ReadInt32(); - - var isContainer = (rawSize & 0x80000000) != 0; - var size = rawSize & 0x7FFFFFFF; - - return isContainer ? ChunkMetadata.FromContainer(type, size) : ChunkMetadata.FromData(type, size); + var type = _binaryReader.ReadUInt32(); + var rawSize = _binaryReader.ReadUInt32(); + return new ChunkMetadata(type, rawSize, false); } public ChunkMetadata ReadChunk(ref int readBytes) @@ -43,9 +39,35 @@ public ChunkMetadata ReadMiniChunk(ref int readBytes) readBytes += 2; - return ChunkMetadata.FromData(type, size, true); + return new ChunkMetadata(type, size, true); + } + + public byte[] ReadData(ChunkMetadata chunk) + { + if (chunk.HasChildrenHint) + throw new InvalidOperationException("Unable to read data from container chunk."); + + return _binaryReader.ReadBytes(chunk.BodySize); + } + + public byte[] ReadData(int size) + { + return size < 0 ? + throw new ArgumentOutOfRangeException(nameof(size), "size cannot be negative") : + _binaryReader.ReadBytes(size); } + public byte[] ReadData(ChunkMetadata chunk, ref int readSize) + { + if (chunk.HasChildrenHint) + throw new InvalidOperationException("Unable to read data from container chunk."); + + var data = _binaryReader.ReadBytes(chunk.BodySize); + readSize += chunk.BodySize; + return data; + } + + public uint ReadDword(ref int readSize) { var value = _binaryReader.ReadUInt32(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/CommonDatTestBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/CommonDatTestBase.cs new file mode 100644 index 00000000..85589e41 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/CommonDatTestBase.cs @@ -0,0 +1,13 @@ +using AnakinRaW.CommonUtilities.Testing; +using Microsoft.Extensions.DependencyInjection; + +namespace PG.StarWarsGame.Files.TED.Test; + +public class CommonDatTestBase : TestBaseWithFileSystem +{ + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + serviceCollection.SupportTED(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj new file mode 100644 index 00000000..04f7ff7c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj @@ -0,0 +1,37 @@ + + + + net8.0;net10.0 + $(TargetFrameworks);net481 + + + false + true + Exe + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/Services/TedFileServiceTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/Services/TedFileServiceTest.cs new file mode 100644 index 00000000..49871310 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/Services/TedFileServiceTest.cs @@ -0,0 +1,52 @@ +using System.IO; +using System.IO.Abstractions; +using AnakinRaW.CommonUtilities.Hashing; +using AnakinRaW.CommonUtilities.Testing; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons; +using PG.StarWarsGame.Files.TED.Services; +using Testably.Abstractions; +using Xunit; + +namespace PG.StarWarsGame.Files.TED.Test.Services; + +public class TedFileServiceTest : TestBaseWithFileSystem +{ + private readonly TedFileService _service; + + protected override IFileSystem CreateFileSystem() + { + return new RealFileSystem(); + } + + public TedFileServiceTest() + { + _service = new TedFileService(ServiceProvider); + } + + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(sp => new HashingService(sp)); + PetroglyphCommons.ContributeServices(serviceCollection); + serviceCollection.SupportTED(); + } + + [Fact] + public void Foo() + { + const string path = @"C:\Program Files (x86)\Steam\steamapps\workshop\content\32470\1129810972\Data\Art\Maps\_land_planet_felucia_00.ted"; + using var tedFs = FileSystem.FileStream.New(path, FileMode.Open); + using var dest = FileSystem.FileStream.New("c:/test/map.ted", FileMode.Create); + _service.RemoveMapPreview(tedFs, dest, true, out var bytes); + + if (bytes is not null) + { + using var img = FileSystem.FileStream.New("c:/test/img.dds", FileMode.Create); + using var streamWriter = new BinaryWriter(img); + streamWriter.Write(bytes); + } + + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs index 92e0c5f2..0556a80b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; using PG.StarWarsGame.Files.TED.Data; @@ -8,6 +9,6 @@ internal sealed class TedFileReader(Stream stream) : ChunkFileReaderBase Date: Sat, 28 Mar 2026 10:25:58 +0100 Subject: [PATCH 03/17] add testing project for chunk file project --- ModVerify.slnx | 1 + ....StarWarsGame.Files.ChunkFiles.Test.csproj | 37 +++++++++++++++++++ .../Properties/AssemblyAttributes.cs | 3 ++ 3 files changed, 41 insertions(+) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Properties/AssemblyAttributes.cs diff --git a/ModVerify.slnx b/ModVerify.slnx index dd535739..caae34b0 100644 --- a/ModVerify.slnx +++ b/ModVerify.slnx @@ -19,6 +19,7 @@ + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj new file mode 100644 index 00000000..cb0168a3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj @@ -0,0 +1,37 @@ + + + + net8.0;net10.0 + $(TargetFrameworks);net481 + + + false + true + Exe + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Properties/AssemblyAttributes.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Properties/AssemblyAttributes.cs new file mode 100644 index 00000000..6e1a5430 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Properties/AssemblyAttributes.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("PG.StarWarsGame.Files.ChunkFiles.Test")] \ No newline at end of file From 49243b9c765894dcd68a706198540e03dd764f1a Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 30 Mar 2026 13:51:57 +0200 Subject: [PATCH 04/17] new chunk implementation --- ModVerify.slnx | 4 +- .../Binary/AloChunkType.cs} | 4 +- .../Identifier/AloContentInfoIdentifier.cs | 42 +-- .../Reader/Animations/AnimationReaderBase.cs | 8 +- .../Binary/Reader/Models/ModelFileReader.cs | 7 +- .../Binary/ChunkFactory.cs | 128 +++++++++ .../Binary/Metadata/Chunk.cs | 189 ------------- .../Binary/Metadata/ChunkFile.cs | 48 ---- .../Binary/Metadata/ChunkMetadata.cs | 39 --- .../Binary/Model/Chunk.cs | 30 ++ .../Binary/Model/ChunkFile.cs | 90 ++++++ .../Binary/Model/DataChunk.cs | 60 ++++ .../Binary/Model/Metadata/ChunkMetadata.cs | 49 ++++ .../Model/Metadata/MiniChunkMetadata.cs | 35 +++ .../Binary/Model/MiniChunk.cs | 63 +++++ .../Binary/Model/MiniNodeChunk.cs | 29 ++ .../Binary/Model/NodeChunk.cs | 27 ++ .../Binary/Model/NodeChunkBase.cs | 70 +++++ .../Binary/Model/RawChunk.cs | 66 +++++ .../Binary/Model/RootChunk.cs | 7 + .../Binary/Reader/ChunkFileReaderBase.cs | 45 ++- .../Binary/Reader/ChunkReader.cs | 261 +++++++++++++++--- .../Binary/Reader/IChunkFileReader.cs | 18 ++ .../PG.StarWarsGame.Files.ChunkFiles.csproj | 1 + .../Binary/Writer/MapPreviewExtractor.cs | 5 +- 25 files changed, 969 insertions(+), 356 deletions(-) rename src/PetroglyphTools/{PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs => PG.StarWarsGame.Files.ALO/Binary/AloChunkType.cs} (90%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Chunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/MiniChunkMetadata.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RootChunk.cs diff --git a/ModVerify.slnx b/ModVerify.slnx index caae34b0..2d271f09 100644 --- a/ModVerify.slnx +++ b/ModVerify.slnx @@ -19,7 +19,9 @@ - + + + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/AloChunkType.cs similarity index 90% rename from src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs rename to src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/AloChunkType.cs index 70af55dd..2a88e557 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/AloChunkType.cs @@ -1,6 +1,6 @@ -namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +namespace PG.StarWarsGame.Files.ALO.Binary; -public enum ChunkType +public enum AloChunkType { Unknown, Name = 0x0, diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs index 3024efd7..e2da2859 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs @@ -1,7 +1,7 @@ using System.IO; using PG.StarWarsGame.Files.ALO.Files; using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; namespace PG.StarWarsGame.Files.ALO.Binary.Identifier; @@ -14,19 +14,19 @@ public AloContentInfo GetContentInfo(Stream stream) var chunk = chunkReader.ReadChunk(); - switch ((ChunkType)chunk.Type) + switch ((AloChunkType)chunk.Type) { - case ChunkType.Skeleton: - case ChunkType.Mesh: - case ChunkType.Light: + case AloChunkType.Skeleton: + case AloChunkType.Mesh: + case AloChunkType.Light: return FromModel(chunk.BodySize, chunkReader); - case ChunkType.Connections: + case AloChunkType.Connections: return FromConnection(chunkReader); - case ChunkType.Particle: + case AloChunkType.Particle: return new AloContentInfo(AloType.Particle, AloVersion.V1); - case ChunkType.ParticleUaW: + case AloChunkType.ParticleUaW: return new AloContentInfo(AloType.Particle, AloVersion.V2); - case ChunkType.Animation: + case AloChunkType.Animation: return FromAnimation(chunkReader); default: throw new BinaryCorruptedException("Unable to get ALO content information."); @@ -38,9 +38,9 @@ private static AloContentInfo FromAnimation(ChunkReader chunkReader) var chunk = chunkReader.TryReadChunk(); while (chunk.HasValue) { - switch ((ChunkType)chunk.Value.Type) + switch ((AloChunkType)chunk.Value.Type) { - case ChunkType.AnimationInformation: + case AloChunkType.AnimationInformation: return chunk.Value.BodySize switch { 36 => new AloContentInfo(AloType.Animation, AloVersion.V2), @@ -61,14 +61,14 @@ private static AloContentInfo FromConnection(ChunkReader chunkReader) var chunk = chunkReader.TryReadChunk(); while (chunk.HasValue) { - switch ((ChunkType)chunk.Value.Type) + switch ((AloChunkType)chunk.Value.Type) { - case ChunkType.ProxyConnection: - case ChunkType.ObjectConnection: - case ChunkType.ConnectionCounts: + case AloChunkType.ProxyConnection: + case AloChunkType.ObjectConnection: + case AloChunkType.ConnectionCounts: chunkReader.Skip(chunk.Value.BodySize); break; - case ChunkType.Dazzle: + case AloChunkType.Dazzle: return new AloContentInfo(AloType.Model, AloVersion.V2); default: throw new BinaryCorruptedException("Invalid ALO model."); @@ -85,13 +85,13 @@ private static AloContentInfo FromModel(int size, ChunkReader chunkReader) var chunk = chunkReader.TryReadChunk(); if (chunk is null) throw new BinaryCorruptedException("Unable to get ALO content information."); - switch ((ChunkType)chunk.Value.Type) + switch ((AloChunkType)chunk.Value.Type) { - case ChunkType.Connections: + case AloChunkType.Connections: return FromConnection(chunkReader); - case ChunkType.Skeleton: - case ChunkType.Mesh: - case ChunkType.Light: + case AloChunkType.Skeleton: + case AloChunkType.Mesh: + case AloChunkType.Light: return FromModel(chunk.Value.BodySize, chunkReader); default: throw new BinaryCorruptedException("Invalid ALO model."); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs index dbcfc4d1..d42e0ba5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs @@ -4,7 +4,7 @@ using PG.StarWarsGame.Files.ALO.Data; using PG.StarWarsGame.Files.ALO.Services; using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; namespace PG.StarWarsGame.Files.ALO.Binary.Reader.Animations; @@ -54,8 +54,7 @@ protected virtual void ReadAnimation( switch (chunk.Type) { case (int)AnimationChunkTypes.AnimationInfo: - if (chunk.RawSize < 0) - ThrowChunkSizeTooLargeException(); + ThrowIfChunkSizeTooLargeException(chunk); animationInformation = ReadAnimationInfo(chunk.BodySize); break; case (int)AnimationChunkTypes.BoneData: @@ -72,8 +71,7 @@ protected virtual void ReadBoneDataCore(ChunkMetadata chunk, List proxies) do { var chunk = ChunkReader.ReadChunk(ref actualSize); - if (chunk.RawSize < 0) - ThrowChunkSizeTooLargeException(); - + ThrowIfChunkSizeTooLargeException(chunk); if (chunk.Type == (int)ModelChunkTypes.ProxyConnection) { ReadProxy(chunk.BodySize, out var proxy, ref actualSize); @@ -139,8 +137,7 @@ private void ReadSubMeshMaterialInformation(int size, ISet textures, ISe break; } case (int)ModelChunkTypes.ShaderTexture: - if (chunk.RawSize < 0) - ThrowChunkSizeTooLargeException(); + ThrowIfChunkSizeTooLargeException(chunk); ReadShaderTexture(chunk.BodySize, textures, ref actualSize); break; default: diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs new file mode 100644 index 00000000..29f61617 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs @@ -0,0 +1,128 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Diagnostics; +using System.Linq; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary; + +/// +/// Provides factory methods for creating chunks and chunk files. +/// +/// +/// Five chunk types are supported: +/// +/// — Regular chunk containing binary data. +/// — Chunk with a smaller, 2-byte header for data up to 255 bytes. +/// — Container chunk holding regular child chunks. +/// — Container chunk holding mini-chunk children. +/// — Chunk holding raw binary data. +/// +/// +public static class ChunkFactory +{ + /// + /// Creates a data chunk with the specified type and binary data. + /// + /// The chunk type identifier. + /// The binary data to store in the chunk. + /// A new . + /// is . + public static DataChunk Data(uint type, byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var metadata = new ChunkMetadata(type, (uint)data.Length); + return new DataChunk(metadata, data); + } + + /// + /// Creates a raw chunk that stores its body as an uninterpreted byte blob. + /// + /// The chunk type identifier. + /// The raw size value written to the header. The high bit 31, set or unset, is not interpreted. + /// The raw body data. + /// A new . + /// is . + public static RawChunk Raw(uint type, uint rawSize, byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var metadata = new ChunkMetadata(type, rawSize); + return new RawChunk(metadata, data); + } + + /// + /// Creates a mini-chunk with the specified type and binary data. + /// + /// The mini-chunk type identifier. + /// The binary data to store. Maximum length is 255 bytes. + /// A new . + /// is . + /// The length of exceeds 255 bytes. + public static MiniChunk Mini(byte type, byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + if (data.Length > byte.MaxValue) + throw new ArgumentException($"Mini-chunk data cannot exceed {byte.MaxValue} bytes. Provided: {data.Length}.", + nameof(data)); + + var metadata = new MiniChunkMetadata(type, (byte)data.Length); + return new MiniChunk(metadata, data); + } + + /// + /// Creates a node chunk containing regular child chunks. + /// + /// The chunk type identifier. + /// The child chunks. + /// A new with bit 31 set in the size field. + /// is . + /// The total size of the child chunks exceeds 2GB. + public static NodeChunk Node(uint type, params RootChunk[] children) + { + if (children == null) + throw new ArgumentNullException(nameof(children)); + + var size = (uint)children.Sum(c => c.Size); + if (size > int.MaxValue) + throw new InvalidOperationException("Chunk nodes cannot contain chunks with a total content size larger than 2GB."); + var metadata = new ChunkMetadata(type, size | 0x8000_0000u); + Debug.Assert((int)metadata.RawSize < 0); + return new NodeChunk(metadata, children); + } + + /// + /// Creates a node chunk containing mini-chunk children. + /// + /// The chunk type identifier. + /// The mini-chunk children. + /// A new with bit 31 cleared in the size field. + /// is . + public static MiniNodeChunk Node(uint type, params MiniChunk[] children) + { + if (children == null) + throw new ArgumentNullException(nameof(children)); + + var size = (uint)children.Sum(c => c.Size); + var metadata = new ChunkMetadata(type, size); + return new MiniNodeChunk(metadata, children); + } + + /// + /// Creates a chunk file containing the specified root chunks. + /// + /// The top-level chunks. + /// A new . + /// is . + public static ChunkFile File(params RootChunk[] rootChunks) + { + if (rootChunks == null) + throw new ArgumentNullException(nameof(rootChunks)); + return new ChunkFile(rootChunks); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs deleted file mode 100644 index c708838d..00000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.Linq; -using PG.StarWarsGame.Files.Binary; - -namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; - -public sealed class Chunk : IBinary -{ - public ChunkMetadata Info { get; } - - public byte[]? Data { get; } - - public IReadOnlyList Children { get; } - - public byte[] Bytes - { - get - { - var bytes = new byte[Size]; - GetBytes(bytes); - return bytes; - } - } - - public int Size => Info.IsMiniChunk - ? 2 + Data!.Length - : Data is not null - ? 8 + Data.Length - : 8 + Children.Sum(c => c.Size); - - public Chunk(ChunkMetadata info, byte[] data) - { - Info = info; - Data = data ?? throw new ArgumentNullException(nameof(data)); - Children = []; - } - - public Chunk(ChunkMetadata info, IReadOnlyList children) - { - if (info.IsMiniChunk) - throw new ArgumentException("MiniChunks cannot have child chunks", nameof(info)); - Info = info; - Data = null; - Children = children ?? throw new ArgumentNullException(nameof(children)); - } - - public void GetBytes(Span bytes) - { - if (Info.IsMiniChunk) - { - bytes[0] = (byte)Info.Type; - bytes[1] = (byte)Data!.Length; - Data.AsSpan().CopyTo(bytes[2..]); - return; - } - - BinaryPrimitives.WriteUInt32LittleEndian(bytes, Info.Type); - - if (Data is not null) - { - BinaryPrimitives.WriteInt32LittleEndian(bytes[4..], Data.Length); - Data.AsSpan().CopyTo(bytes[8..]); - } - else - { - // .Sum is a checked operation and will already throw an overflow exception - var bodySize = Children.Sum(c => c.Size); - var hasMiniChunkChildren = Children.Count > 0 && Children[0].Info.IsMiniChunk; - - var sizeField = hasMiniChunkChildren - ? bodySize - : (int)(bodySize | 0x8000_0000u); - - BinaryPrimitives.WriteInt32LittleEndian(bytes[4..], sizeField); - - var offset = 8; - foreach (var child in Children) - { - child.GetBytes(bytes[offset..]); - offset += child.Size; - } - } - } -} - -/// -/// Provides factory methods for creating chunks and chunk files. -/// -/// -/// -/// This class provides static methods for creating hierarchical chunk structures. -/// Three chunk types are supported: -/// -/// -/// Data chunks - Regular chunks containing binary data -/// Mini-chunks - Chunks with 2-byte headers for data up to 255 bytes -/// Node chunks - Container chunks that hold child chunks -/// -/// -public static class ChunkFactory -{ - /// - /// Creates a data chunk with the specified type and binary data. - /// - /// The chunk type identifier. - /// The binary data to store in the chunk. - /// A containing the specified data. - /// is . - public static Chunk Data(uint type, byte[] data) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var metadata = new ChunkMetadata(type, (uint)data.Length, false); - return new Chunk(metadata, data); - } - - /// - /// Creates a mini-chunk with the specified type and binary data. - /// - /// The mini-chunk type identifier. - /// The binary data to store in the mini-chunk. Maximum length is 255 bytes. - /// A representing a mini-chunk with a 2-byte header. - /// is . - /// The length of exceeds 255 bytes. - public static Chunk Mini(byte type, byte[] data) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - if (data.Length > byte.MaxValue) - throw new ArgumentException( - $"Mini-chunk data cannot exceed {byte.MaxValue} bytes. Provided data length: {data.Length}.", - nameof(data)); - - var metadata = new ChunkMetadata(type, (uint)data.Length, true); - return new Chunk(metadata, data); - } - - /// - /// Creates a chunk node that contains child chunks. - /// - /// The chunk type identifier. - /// The child chunks to include in this node. - /// A containing the specified children. - /// is . - public static Chunk Node(uint type, params Chunk[] children) - { - if (children == null) - throw new ArgumentNullException(nameof(children)); - - var size = (uint)children.Sum(c => c.Size); - var metadata = new ChunkMetadata(type, size, false); - return new Chunk(metadata, children); - } - - /// - /// Creates a chunk node that contains child chunks built using a configuration action. - /// - /// The chunk type identifier. - /// An action that populates a list with child chunks. - /// A containing the configured children. - /// is . - public static Chunk Node(uint type, Action> configure) - { - if (configure == null) - throw new ArgumentNullException(nameof(configure)); - - var children = new List(); - configure(children); - return Node(type, children.ToArray()); - } - - /// - /// Creates a chunk file containing the specified root chunks. - /// - /// The top-level chunks to include in the file. - /// A containing the specified root chunks. - /// is . - public static ChunkFile File(params Chunk[] rootChunks) - { - if (rootChunks == null) - throw new ArgumentNullException(nameof(rootChunks)); - - return new ChunkFile(rootChunks); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs deleted file mode 100644 index 1c38ddbb..00000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using PG.StarWarsGame.Files.Binary.File; - -namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; - -public sealed class ChunkFile : IBinaryFile -{ - public IReadOnlyList RootChunks { get; } - - public int Size => RootChunks.Sum(c => c.Size); - - public byte[] Bytes - { - get - { - var bytes = new byte[Size]; - GetBytes(bytes); - return bytes; - } - } - - public ChunkFile(IReadOnlyList rootChunks) - { - if (rootChunks == null) - throw new ArgumentNullException(nameof(rootChunks)); - if (rootChunks.Count == 0) - throw new ArgumentOutOfRangeException(nameof(rootChunks), "A chunk file must contain at least one chunk"); - RootChunks = rootChunks; - } - - public void GetBytes(Span bytes) - { - var offset = 0; - foreach (var chunk in RootChunks) - { - chunk.GetBytes(bytes[offset..]); - offset += chunk.Size; - } - } - - public void WriteTo(Stream stream) - { - stream.Write(Bytes, 0, Size); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs deleted file mode 100644 index 3a767498..00000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Diagnostics; - -namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; - -[DebuggerDisplay("Type: {Type}, Size: {BodySize}, Mini:{IsMiniChunk}")] -public readonly struct ChunkMetadata -{ - public readonly uint Type; - public readonly uint RawSize; - public readonly bool IsMiniChunk; - - /// - /// Indicates that bit 31 of RawSize is set. - /// This is a hint that the body contains child chunks, not a guarantee. - /// - public bool HasChildrenHint => !IsMiniChunk && (int)RawSize < 0; - - /// - /// Gets the size of the chunk's data in bytes. - /// - /// - /// This value has bit 31 masked off compared to . - /// Per spec, bit 31 is set only for chunks containing regular child chunks. - /// Chunks containing mini-chunks (treated as data) do NOT set bit 31. - /// Since this library doesn't support sizes > , masking bit 31 - /// has no practical impact on the usable size range. - /// - public int BodySize => (int)(RawSize & 0x7FFF_FFFF); - - public ChunkMetadata(uint type, uint rawSize, bool isMiniChunk) - { - if (isMiniChunk && rawSize > byte.MaxValue) - throw new ArgumentOutOfRangeException(nameof(rawSize), "Mini chunk size must fit in a byte (0-255)."); - Type = type; - RawSize = rawSize; - IsMiniChunk = isMiniChunk; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Chunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Chunk.cs new file mode 100644 index 00000000..10296133 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Chunk.cs @@ -0,0 +1,30 @@ +using System; +using PG.StarWarsGame.Files.Binary; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + + +/// +/// Base class for all chunk types in a chunked file. +/// +public abstract class Chunk : IBinary +{ + /// + /// Gets the total size of this chunk in bytes, including the header. + /// + public abstract int Size { get; } + + /// + public byte[] Bytes + { + get + { + var bytes = new byte[Size]; + GetBytes(bytes); + return bytes; + } + } + + /// + public abstract void GetBytes(Span bytes); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs new file mode 100644 index 00000000..852ebb8c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using PG.StarWarsGame.Files.Binary.File; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// Represents a chunked file containing one or more root-level chunks. +/// +public sealed class ChunkFile : IBinaryFile +{ + /// + /// Gets the root-level chunks in this file. + /// + public IReadOnlyList RootChunks { get; } + + /// + /// Gets the total file size in bytes. + /// + public int Size => RootChunks.Sum(c => c.Size); + + /// + /// Initializes a new instance of the class. + /// + /// + /// The root-level chunks. Must contain at least one element. + /// + /// + /// is . + /// + /// + /// is empty. + /// + public ChunkFile(IReadOnlyList rootChunks) + { + if (rootChunks == null) + throw new ArgumentNullException(nameof(rootChunks)); + + if (rootChunks.Count == 0) + throw new ArgumentException("ChunkFile must have at least one root chunk.", nameof(rootChunks)); + + RootChunks = rootChunks; + } + + /// + /// Gets the file's binary representation as a byte array. + /// + public byte[] Bytes + { + get + { + var bytes = new byte[Size]; + GetBytes(bytes); + return bytes; + } + } + + /// + /// Writes the file's binary representation into the specified span. + /// + /// + /// The destination span. Must be at least bytes long. + /// + public void GetBytes(Span bytes) + { + var offset = 0; + foreach (var chunk in RootChunks) + { + chunk.GetBytes(bytes[offset..]); + offset += chunk.Size; + } + } + + /// + /// Writes the file's binary representation to a stream. + /// + /// The destination stream. + /// + /// is . + /// + public void WriteTo(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + stream.Write(Bytes, 0, Size); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs new file mode 100644 index 00000000..0b5b820d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs @@ -0,0 +1,60 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Buffers.Binary; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// A chunk containing binary data. +/// +public sealed class DataChunk : Chunk +{ + /// + /// Gets the chunk metadata. + /// + public ChunkMetadata Info { get; } + + /// + /// Gets the chunk's binary data payload. + /// + public ReadOnlyMemory Data { get; } + + /// + public override unsafe int Size => sizeof(ChunkMetadata) + Data.Length; + + /// + /// Initializes a new instance of the class. + /// + /// The chunk metadata. Must not have bit 31 set. + /// The binary data payload. + /// + /// is empty, or + /// has bit 31 set, or + /// body size does not match the data length. + /// + public DataChunk(ChunkMetadata info, ReadOnlyMemory data) + { + if (data is { IsEmpty: true, Length: 0 }) + throw new ArgumentException("Data cannot be empty.", nameof(data)); + + if (info.HasChildrenHint) + throw new ArgumentException( + "DataChunk metadata must not have bit 31 set.", nameof(info)); + + if (info.BodySize != data.Length) + throw new ArgumentException( + $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", + nameof(info)); + + Info = info; + Data = data; + } + + /// + public override unsafe void GetBytes(Span bytes) + { + BinaryPrimitives.WriteUInt32LittleEndian(bytes, Info.Type); + BinaryPrimitives.WriteInt32LittleEndian(bytes[sizeof(uint)..], Data.Length); + Data.Span.CopyTo(bytes[sizeof(ChunkMetadata)..]); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs new file mode 100644 index 00000000..6dbb9a96 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs @@ -0,0 +1,49 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; + +/// +/// Describes the header of a regular chunk. +/// +/// +/// A regular chunk header is 8 bytes: 4 bytes for the type and 4 bytes for the size. +/// Bit 31 of the size field indicates whether the chunk body contains child chunks. +/// +[DebuggerDisplay("Type: 0x{Type:X8}, Size: {BodySize}, HasChildrenHint: {HasChildrenHint}")] +public readonly struct ChunkMetadata +{ + /// + /// The chunk type identifier. + /// + public readonly uint Type; + + /// + /// The raw size value as stored in the chunk header, including bit 31. + /// + public readonly uint RawSize; + + /// + /// Gets a value indicating whether bit 31 of is set. + /// This is a hint that the body contains child chunks, not a guarantee. + /// + public bool HasChildrenHint => (int)RawSize < 0; + + /// + /// Gets the size of the chunk body in bytes with bit 31 masked off. + /// + public int BodySize => (int)(RawSize & 0x7FFF_FFFF); + + /// + /// Initializes a new instance of the struct. + /// + /// The chunk type identifier. + /// + /// The raw size value. Bit 31 should be set if the chunk contains child chunks. + /// + public ChunkMetadata(uint type, uint rawSize) + { + Type = type; + RawSize = rawSize; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/MiniChunkMetadata.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/MiniChunkMetadata.cs new file mode 100644 index 00000000..9b702219 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/MiniChunkMetadata.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; + +/// +/// Describes the header of a mini-chunk. +/// +/// +/// A mini-chunk header is 2 bytes: 1 byte for the type and 1 byte for the size. +/// Mini-chunks cannot contain children. +/// +[DebuggerDisplay("Type: 0x{Type:X2}, Size: {BodySize}")] +public readonly struct MiniChunkMetadata +{ + /// + /// The mini-chunk type identifier. + /// + public readonly byte Type; + + /// + /// The size of the mini-chunk body in bytes. + /// + public readonly byte BodySize; + + /// + /// Initializes a new instance of the struct. + /// + /// The mini-chunk type identifier. + /// The size of the mini-chunk body in bytes. + public MiniChunkMetadata(byte type, byte size) + { + Type = type; + BodySize = size; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs new file mode 100644 index 00000000..de72659c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs @@ -0,0 +1,63 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// A mini-chunk containing binary data with a compact 2-byte header. +/// +/// +/// +/// Mini-chunks use a 2-byte header (1 byte type, 1 byte size) instead of the standard 8-byte header. +/// They can only appear as children of a . +/// +/// +/// A mini chunk cannot be used as a root chunk or as a child of a . +/// +/// +public sealed class MiniChunk : Chunk +{ + /// + /// Gets the mini-chunk metadata. + /// + public MiniChunkMetadata Info { get; } + + /// + /// Gets the mini-chunk's binary data payload. + /// + public ReadOnlyMemory Data { get; } + + /// + public override unsafe int Size => sizeof(MiniChunkMetadata) + Data.Length; + + /// + /// Initializes a new instance of the class. + /// + /// The mini-chunk metadata. + /// The binary data payload. Maximum length is 255 bytes. + /// + /// is empty, or + /// size does not match the data length. + /// + public MiniChunk(MiniChunkMetadata info, ReadOnlyMemory data) + { + if (data is { IsEmpty: true, Length: 0 }) + throw new ArgumentException("Data cannot be empty.", nameof(data)); + + if (info.BodySize!= data.Length) + throw new ArgumentException( + $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", + nameof(info)); + + Info = info; + Data = data; + } + + /// + public override unsafe void GetBytes(Span bytes) + { + bytes[0] = Info.Type; + bytes[1] = Info.BodySize; + Data.Span.CopyTo(bytes[sizeof(MiniChunkMetadata)..]); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs new file mode 100644 index 00000000..0db79f73 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs @@ -0,0 +1,29 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// A chunk containing mini-chunk children. +/// +/// +/// +/// This chunk type holds exclusively children. +/// Bit 31 of the size field is not used as a container flag. +/// +/// +public sealed class MiniNodeChunk : NodeChunkBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The chunk metadata. Must not have bit 31 set. + /// The mini-chunk children. Must contain at least one element. + /// Chunks larger than 2GB are not supported. + public MiniNodeChunk(ChunkMetadata info, IReadOnlyList children) : base(info, children) + { + if (info.RawSize > int.MaxValue) + throw new NotSupportedException("Chunks larger than int32.MaxValue bytes are not supported."); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs new file mode 100644 index 00000000..05a17e6d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs @@ -0,0 +1,27 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// A chunk containing regular child chunks. +/// +/// +/// Bit 31 of the size field must be set. +/// +public sealed class NodeChunk : NodeChunkBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The chunk metadata. Must have bit 31 set. + /// The child chunks. Must contain at least one element. + /// does not have bit 31 set. + public NodeChunk(ChunkMetadata info, IReadOnlyList children) : base(info, children) + { + if (!info.HasChildrenHint) + throw new ArgumentException( + "NodeChunk metadata must have bit 31 set.", nameof(info)); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs new file mode 100644 index 00000000..3d900e8e --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs @@ -0,0 +1,70 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// Base class for chunks that contain child chunks of type . +/// +/// The type of child chunk this node contains. +public abstract class NodeChunkBase : RootChunk where TChild : Chunk +{ + /// + /// Gets the chunk metadata. + /// + public ChunkMetadata Info { get; } + + /// + /// Gets the child chunks. + /// + public IReadOnlyList Children { get; } + + /// + public override unsafe int Size => sizeof(ChunkMetadata) + Children.Sum(c => c.Size); + + /// + /// Initializes a new instance of the class. + /// + /// The chunk metadata. + /// The child chunks. Must contain at least one element. + /// is . + /// + /// is empty, or + /// body size does not match the sum of children sizes. + /// + protected NodeChunkBase(ChunkMetadata info, IReadOnlyList children) + { + if (children == null) + throw new ArgumentNullException(nameof(children)); + + if (children.Count == 0) + throw new ArgumentException( + $"{GetType().Name} must have at least one child.", nameof(children)); + + var actualSize = children.Sum(c => c.Size); + if (info.BodySize != actualSize) + throw new ArgumentException( + $"Metadata size ({info.BodySize}) does not match sum of children sizes ({actualSize}).", + nameof(info)); + + Info = info; + Children = children; + } + + /// + public override unsafe void GetBytes(Span bytes) + { + BinaryPrimitives.WriteUInt32LittleEndian(bytes, Info.Type); + BinaryPrimitives.WriteUInt32LittleEndian(bytes[sizeof(uint)..], Info.RawSize); + + var offset = sizeof(ChunkMetadata); + foreach (var child in Children) + { + child.GetBytes(bytes[offset..]); + offset += child.Size; + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs new file mode 100644 index 00000000..bd336b05 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs @@ -0,0 +1,66 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Buffers.Binary; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// A chunk that stores raw binary data without interpreting its contents. +/// +/// +/// +/// This chunk stores its body as a raw byte blob. The body may contain +/// any content: raw data, child chunks, mini-chunks, or any combination. +/// No structural interpretation or validation is performed on the body. +/// +/// +/// This is useful for: +/// +/// +/// Round-tripping unknown or unparsed chunks. +/// Creating chunks from pre-serialized data. +/// +/// +/// The metadata is written as-is, including bit 31 of the size field. +/// +/// +public sealed class RawChunk : RootChunk +{ + /// + /// Gets the chunk metadata. + /// + public ChunkMetadata Info { get; } + + /// + /// Gets the raw body data. + /// + public ReadOnlyMemory Data { get; } + + /// + public override unsafe int Size => sizeof(ChunkMetadata) + Data.Length; + + /// + /// Initializes a new instance of the class. + /// + /// The chunk metadata, written as-is. No validation is performed on bit 31. + /// The raw body data. + /// body size does not match the data length. + public RawChunk(ChunkMetadata info, ReadOnlyMemory data) + { + if (info.BodySize != data.Length) + throw new ArgumentException( + $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", + nameof(info)); + + Info = info; + Data = data; + } + + /// + public override unsafe void GetBytes(Span bytes) + { + BinaryPrimitives.WriteUInt32LittleEndian(bytes, Info.Type); + BinaryPrimitives.WriteUInt32LittleEndian(bytes[sizeof(uint)..], Info.RawSize); + Data.Span.CopyTo(bytes[sizeof(ChunkMetadata)..]); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RootChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RootChunk.cs new file mode 100644 index 00000000..e079bf06 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RootChunk.cs @@ -0,0 +1,7 @@ +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// Base class for chunks that can appear as root-level elements in a +/// or as children in a . +/// +public abstract class RootChunk : Chunk; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs index eb664447..5a98ea03 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs @@ -1,30 +1,65 @@ using System; using System.IO; using AnakinRaW.CommonUtilities; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; using PG.StarWarsGame.Files.ChunkFiles.Data; namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; -public abstract class ChunkFileReaderBase(Stream stream) : DisposableObject, IChunkFileReader where T : IChunkData +/// +/// Represents the base class for reading chunk files in a binary format. +/// +/// The type of chunk data being read. +/// The input stream from which the chunk file is read. +/// +/// A boolean value indicating whether the input stream should remain open after the reader is disposed. +/// Default value is . +/// +public abstract class ChunkFileReaderBase(Stream stream, bool leaveStreamOpen = false) + : DisposableObject, IChunkFileReader where T : IChunkData { - protected readonly ChunkReader ChunkReader = new(stream); + /// + /// The instance used to read chunk data from the input stream. + /// + /// + /// This reader is initialized with the provided stream and the option to leave the stream open after disposal. + /// + protected readonly ChunkReader ChunkReader = new(stream, leaveStreamOpen); + /// public abstract T Read(); + /// IChunkData IChunkFileReader.Read() { return Read(); } + /// + /// Releases the resources used by the instance, including + /// the associated . + /// protected override void DisposeResources() { base.DisposeResources(); ChunkReader.Dispose(); } - //[DoesNotReturn] - protected void ThrowChunkSizeTooLargeException() + /// + /// Validates the raw binary size of the specified chunk and throws an exception if the size exceeds the maximum allowed value. + /// + /// + /// This method should not be used for , as their raw size always exceeds + /// due to the presence of the high bit flag in the size field, which indicates that the chunk has child chunks. + /// + /// The metadata of the chunk to validate. + /// + /// Thrown when the raw binary size of the chunk exceeds , as such sizes are not supported. + /// + protected void ThrowIfChunkSizeTooLargeException(ChunkMetadata chunk) { - throw new NotSupportedException("Chunk sizes larger than int.MaxValue are not supported."); + if (chunk.RawSize > int.MaxValue) + throw new NotSupportedException("Chunk sizes larger than int.MaxValue are not supported."); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs index 024d6eee..5b67f1da 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs @@ -3,14 +3,37 @@ using System.Text; using AnakinRaW.CommonUtilities; using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +/// +/// Reads chunks and their data from a stream. +/// +/// +/// +/// The reader operates sequentially on the underlying stream. It reads chunk headers, +/// mini-chunk headers, and raw data. It does not build a tree structure — callers are +/// responsible for interpreting chunk relationships based on the file format specification. +/// +/// +/// Several methods accept a ref int readBytes parameter that tracks the number +/// of bytes read. This is useful for parsing chunk bodies where the caller needs to know +/// when the body has been fully consumed. +/// +/// public class ChunkReader : DisposableObject { private readonly PetroglyphBinaryReader _binaryReader; + /// + /// Initializes a new instance of the class. + /// + /// The stream to read from. + /// + /// to leave the stream open after this reader is disposed; otherwise, . + /// + /// is . public ChunkReader(Stream stream, bool leaveOpen = false) { if (stream == null) @@ -18,130 +41,290 @@ public ChunkReader(Stream stream, bool leaveOpen = false) _binaryReader = new PetroglyphBinaryReader(stream, leaveOpen); } + /// + /// Reads an 8-byte chunk header from the stream. + /// + /// The chunk metadata. + /// The end of the stream is reached before the header could be read. public ChunkMetadata ReadChunk() { var type = _binaryReader.ReadUInt32(); var rawSize = _binaryReader.ReadUInt32(); - return new ChunkMetadata(type, rawSize, false); + return new ChunkMetadata(type, rawSize); } + /// + /// Reads an 8-byte chunk header from the stream and advances the byte counter. + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by 8. + /// + /// The chunk metadata. + /// The end of the stream is reached before the header could be read. public ChunkMetadata ReadChunk(ref int readBytes) { var chunk = ReadChunk(); - readBytes += 8; + IncrementReadBytesByChunkSize(ref readBytes); return chunk; } - public ChunkMetadata ReadMiniChunk(ref int readBytes) + /// + /// Reads a 2-byte mini-chunk header from the stream and advances the byte counter. + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by 2. + /// + /// The mini-chunk metadata. + /// The end of the stream is reached before the header could be read. + public MiniChunkMetadata ReadMiniChunk(ref int readBytes) { var type = _binaryReader.ReadByte(); var size = _binaryReader.ReadByte(); - - readBytes += 2; - - return new ChunkMetadata(type, size, true); + IncrementReadBytesByMiniChunkSize(ref readBytes); + return new MiniChunkMetadata(type, size); } + /// + /// Reads the body data of a chunk. + /// + /// The chunk whose body to read. + /// The chunk body as a byte array. + /// Attempt to read data from a node chunk. + /// The end of the stream is reached before the body could be read. public byte[] ReadData(ChunkMetadata chunk) { - if (chunk.HasChildrenHint) - throw new InvalidOperationException("Unable to read data from container chunk."); + if (chunk.RawSize > int.MaxValue) + throw new InvalidOperationException("Cannot to read data from container chunk."); + return _binaryReader.ReadBytes(chunk.BodySize); + } + /// + /// Reads the body data of a mini chunk. + /// + /// The mini chunk whose body to read. + /// The chunk body as a byte array. + /// The end of the stream is reached before the body could be read. + public byte[] ReadData(MiniChunkMetadata chunk) + { return _binaryReader.ReadBytes(chunk.BodySize); } + /// + /// Reads the specified number of bytes from the stream. + /// + /// The number of bytes to read. + /// The data as a byte array. + /// is negative. + /// The end of the stream is reached before all bytes could be read. public byte[] ReadData(int size) { - return size < 0 ? - throw new ArgumentOutOfRangeException(nameof(size), "size cannot be negative") : - _binaryReader.ReadBytes(size); + return size < 0 + ? throw new ArgumentOutOfRangeException(nameof(size), "size cannot be negative") + : _binaryReader.ReadBytes(size); } - public byte[] ReadData(ChunkMetadata chunk, ref int readSize) + /// + /// Reads the body data of a chunk and advances the byte counter. + /// + /// The chunk whose body to read. + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by the body size of . + /// + /// The chunk body as a byte array. + /// Attempt to read data from a node chunk. + /// The end of the stream is reached before the body could be read. + public byte[] ReadData(ChunkMetadata chunk, ref int readBytes) { - if (chunk.HasChildrenHint) - throw new InvalidOperationException("Unable to read data from container chunk."); + if (chunk.RawSize >= int.MaxValue) + throw new InvalidOperationException("Cannot to read data from container chunk."); var data = _binaryReader.ReadBytes(chunk.BodySize); - readSize += chunk.BodySize; + readBytes += chunk.BodySize; return data; } + /// + /// Reads the body data of a mini chunk and advances the byte counter. + /// + /// The mini chunk whose body to read. + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by the body size of . + /// + /// The chunk body as a byte array. + /// The end of the stream is reached before the body could be read. + public byte[] ReadData(MiniChunkMetadata chunk, ref int readBytes) + { + var data = _binaryReader.ReadBytes(chunk.BodySize); + readBytes += chunk.BodySize; + return data; + } - public uint ReadDword(ref int readSize) + /// + /// Reads a 4-byte unsigned integer from the stream and advances the byte counter. + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by 4. + /// + /// The value read. + /// The end of the stream is reached before the value could be read. + public uint ReadDword(ref int readBytes) { var value = _binaryReader.ReadUInt32(); - readSize += sizeof(uint); + readBytes += sizeof(uint); return value; } - public float ReadFloat(ref int readSize) + /// + /// Reads a 4-byte floating-point value from the stream and advances the byte counter. + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by 4. + /// + /// The value read. + /// The end of the stream is reached before the value could be read. + public float ReadFloat(ref int readBytes) { var value = _binaryReader.ReadSingle(); - readSize += sizeof(float); + readBytes += sizeof(float); return value; } + /// + /// Reads a 4-byte unsigned integer from the stream. + /// + /// The value read. + /// The end of the stream is reached before the value could be read. public uint ReadDword() - { + { return _binaryReader.ReadUInt32(); } + /// + /// Advances the stream position by the specified number of bytes + /// and advances the byte counter. + /// + /// The number of bytes to skip. + /// Incremented by . public void Skip(int bytesToSkip, ref int readBytes) { _binaryReader.BaseStream.Seek(bytesToSkip, SeekOrigin.Current); readBytes += bytesToSkip; } + /// + /// Advances the stream position by the specified number of bytes. + /// + /// The number of bytes to skip. public void Skip(int bytesToSkip) { _binaryReader.BaseStream.Seek(bytesToSkip, SeekOrigin.Current); } - public string ReadString(int size, Encoding encoding, bool zeroTerminated, ref int readSize) + /// + /// Reads a string from the stream and advances the byte counter. + /// + /// The number of bytes to read for the string. + /// The character encoding to use. + /// + /// to trim the string at the first null character;otherwise, . + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by . + /// + /// The decoded string. + /// The string data could not be decoded. + /// The end of the stream is reached before all bytes could be read. + public string ReadString(int size, Encoding encoding, bool zeroTerminated, ref int readBytes) { var value = ReadString(encoding, size, zeroTerminated); - readSize += size; + readBytes += size; return value; } + /// + /// Reads a string from the stream. + /// + /// The number of bytes to read for the string. + /// The character encoding to use. + /// + /// to trim the string at the first null character;otherwise, . + /// + /// The decoded string.The string data could not be decoded. + /// The end of the stream is reached before all bytes could be read. public string ReadString(int size, Encoding encoding, bool zeroTerminated) { var value = ReadString(encoding, size, zeroTerminated); return value; } - private string ReadString(Encoding encoding, int size, bool zeroTerminated) - { - try - { - return _binaryReader.ReadString(encoding, size, zeroTerminated); - } - catch (Exception e) - { - throw new BinaryCorruptedException($"Unable to read string: {e.Message}", e); - } - } - + /// + /// Reads a chunk header from the stream if data is available. + /// + /// + /// The chunk metadata, or if the stream is at the end. + /// public ChunkMetadata? TryReadChunk() { var _ = 0; return TryReadChunk(ref _); } - public ChunkMetadata? TryReadChunk(ref int size) + /// + /// Reads a chunk header from the stream if data is available + /// and advances the byte counter. + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns and a chunk was read, it is incremented by 8. + /// + /// + /// The chunk metadata, or if the stream is at the end. + /// + public ChunkMetadata? TryReadChunk(ref int readBytes) { if (_binaryReader.BaseStream.Position == _binaryReader.BaseStream.Length) return null; - + var chunk = ReadChunk(); - size += 8; + IncrementReadBytesByChunkSize(ref readBytes); return chunk; } + /// + /// Releases the resources used by the . + /// protected override void DisposeResources() { base.DisposeResources(); _binaryReader.Dispose(); } + + private static unsafe void IncrementReadBytesByChunkSize(ref int readBytes) + { + readBytes += sizeof(ChunkMetadata); + } + + private static unsafe void IncrementReadBytesByMiniChunkSize(ref int readBytes) + { + readBytes += sizeof(MiniChunkMetadata); + } + + private string ReadString(Encoding encoding, int size, bool zeroTerminated) + { + try + { + return _binaryReader.ReadString(encoding, size, zeroTerminated); + } + catch (Exception e) + { + throw new BinaryCorruptedException($"Unable to read string: {e.Message}", e); + } + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/IChunkFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/IChunkFileReader.cs index 49d525a1..cdc3379f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/IChunkFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/IChunkFileReader.cs @@ -1,14 +1,32 @@ using System; +using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.ChunkFiles.Data; namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +/// +/// Reads a chunked file from a stream and produces a parsed representation. +/// public interface IChunkFileReader : IDisposable { + /// + /// Reads the chunked file and returns its parsed data. + /// + /// The parsed chunk data. + /// The stream contains invalid or unexpected data. IChunkData Read(); } +/// +/// Reads a chunked file from a stream and produces a strongly-typed parsed representation. +/// +/// The type of chunk data produced by this reader. public interface IChunkFileReader : IChunkFileReader where T : IChunkData { + /// + /// Reads the chunked file and returns its parsed data. + /// + /// The parsed chunk data of type . + /// The stream contains invalid or unexpected data. new T Read(); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj index 000b3a60..db01bf5b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj @@ -15,6 +15,7 @@ true snupkg preview + true diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Writer/MapPreviewExtractor.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Writer/MapPreviewExtractor.cs index bd6a4f61..3a873ba5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Writer/MapPreviewExtractor.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Writer/MapPreviewExtractor.cs @@ -1,6 +1,7 @@ using System; using System.IO; -using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; namespace PG.StarWarsGame.Files.TED.Binary.Writer; @@ -47,7 +48,7 @@ public bool ExtractPreview(Stream tedStream, Stream destination, bool extract, o } else { - var chunk = new Chunk(chunkInfo.Value, reader.ReadData(chunkInfo.Value.BodySize)); + var chunk = new RawChunk(chunkInfo.Value, reader.ReadData(chunkInfo.Value.BodySize)); destination.Write(chunk.Bytes, 0, chunk.Bytes.Length); } } From 634a98c764a736226abde660d43b4af2abac2895 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 30 Mar 2026 14:57:09 +0200 Subject: [PATCH 05/17] start testing --- .../Binary/ChunkFactoryTest.cs | 284 +++++++++++++++ .../Binary/ChunkFileBinaryEquivalenceTest.cs | 45 +++ .../Binary/Model/ChunkFileTest.cs | 96 +++++ .../Binary/Model/DataChunkTest.cs | 81 +++++ .../Model/Metadata/ChunkMetadataTest.cs | 90 +++++ .../Model/Metadata/MiniChunkMetadataTest.cs | 38 ++ .../Binary/Model/MiniChunkTest.cs | 71 ++++ .../Binary/Model/MiniNodeChunkTest.cs | 89 +++++ .../Binary/Model/NodeChunkTest.cs | 103 ++++++ .../Binary/Model/RawChunkTest.cs | 88 +++++ .../Binary/Reader/ChunkFileReaderBaseTest.cs | 84 +++++ .../Binary/Reader/ChunkReaderTest.cs | 337 ++++++++++++++++++ .../Binary/Reader/TestChunkFileReader.cs | 21 ++ .../Binary/Reader/TestChunkFileReaderTest.cs | 45 +++ .../Binary/TestChunkFileData.cs | 103 ++++++ .../Binary/ChunkFactory.cs | 2 - .../Binary/Model/DataChunk.cs | 2 +- .../Binary/Model/RawChunk.cs | 8 +- 18 files changed, 1583 insertions(+), 4 deletions(-) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/ChunkMetadataTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/MiniChunkMetadataTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkFileReaderBaseTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs new file mode 100644 index 00000000..0b08f9fc --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs @@ -0,0 +1,284 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; + +public class ChunkFactoryTest +{ + #region Data + + [Fact] + public void Data_CreatesDataChunk() + { + var chunk = ChunkFactory.Data(0x01, [0xAA, 0xBB]); + Assert.IsType(chunk); + Assert.Equal(0x01u, chunk.Info.Type); + Assert.Equal(2, chunk.Info.BodySize); + Assert.False(chunk.Info.HasChildrenHint); + Assert.Equal(new byte[] { 0xAA, 0xBB }, chunk.Data.ToArray()); + } + + [Fact] + public void Data_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.Data(1, null!)); + } + + [Fact] + public void Data_ThrowsOnEmptyArray() + { + Assert.Throws(() => ChunkFactory.Data(1, [])); + } + + [Fact] + public void Data_SingleByte() + { + var chunk = ChunkFactory.Data(0xFF, [0x42]); + Assert.Equal(1, chunk.Info.BodySize); + Assert.Equal(new byte[] { 0x42 }, chunk.Data.ToArray()); + } + + #endregion + + #region Raw + + [Fact] + public void Raw_CreatesRawChunk() + { + var chunk = ChunkFactory.Raw(0x02, 0x8000_0003u, [1, 2, 3]); + Assert.IsType(chunk); + Assert.Equal(0x02u, chunk.Info.Type); + Assert.Equal(0x8000_0003u, chunk.Info.RawSize); + Assert.Equal(new byte[] { 1, 2, 3 }, chunk.Data.ToArray()); + } + + [Fact] + public void Raw_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.Raw(1, 0, null!)); + } + + [Fact] + public void Raw_ThrowsOnEmptyData() + { + Assert.Throws(() => ChunkFactory.Raw(0x01, 0, [])); + } + + [Fact] + public void Raw_ThrowsOnSizeMismatch() + { + Assert.Throws(() => ChunkFactory.Raw(1, 5, [0xAA])); + } + + [Fact] + public void Raw_WithBit31Set_PreservesBit31() + { + var chunk = ChunkFactory.Raw(1, 0x8000_0002u, [0xAA, 0xBB]); + Assert.True(chunk.Info.HasChildrenHint); + Assert.Equal(2, chunk.Info.BodySize); + } + + [Fact] + public void Raw_WithBit31Clear_NoBit31() + { + var chunk = ChunkFactory.Raw(1, 2, [0xAA, 0xBB]); + Assert.False(chunk.Info.HasChildrenHint); + } + + #endregion + + #region Mini + + [Fact] + public void Mini_CreatesMiniChunk() + { + var chunk = ChunkFactory.Mini(0x05, [0xCC]); + Assert.IsType(chunk); + Assert.Equal(0x05, chunk.Info.Type); + Assert.Equal(1, chunk.Info.BodySize); + Assert.Equal(new byte[] { 0xCC }, chunk.Data.ToArray()); + } + + [Fact] + public void Mini_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.Mini(1, null!)); + } + + [Fact] + public void Mini_ThrowsWhenDataExceeds255() + { + var data = new byte[256]; + Assert.Throws(() => ChunkFactory.Mini(1, data)); + } + + [Fact] + public void Mini_AllowsMaxLength255() + { + var data = new byte[255]; + var chunk = ChunkFactory.Mini(1, data); + Assert.Equal(255, chunk.Info.BodySize); + } + + [Fact] + public void Mini_ThrowsOnEmptyArray() + { + Assert.Throws(() => ChunkFactory.Mini(1, [])); + } + + [Fact] + public void Mini_SingleByte() + { + var chunk = ChunkFactory.Mini(0, [0x01]); + Assert.Equal(1, chunk.Info.BodySize); + } + + #endregion + + #region Node (RootChunk) + + [Fact] + public void Node_RootChunk_CreatesNodeChunk() + { + var child = ChunkFactory.Raw(1, 2, [0xAA, 0xBB]); + var node = ChunkFactory.Node(0x10, child); + + Assert.IsType(node); + Assert.Equal(0x10u, node.Info.Type); + Assert.True(node.Info.HasChildrenHint); + Assert.Single(node.Children); + } + + [Fact] + public void Node_RootChunk_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.Node(1, (RootChunk[])null!)); + } + + [Fact] + public void Node_RootChunk_ThrowsOnEmpty() + { + Assert.Throws(() => ChunkFactory.Node(1, Array.Empty())); + } + + [Fact] + public void Node_RootChunk_MultipleChildren() + { + var c1 = ChunkFactory.Raw(1, 1, [0xAA]); + var c2 = ChunkFactory.Raw(2, 2, [0xBB, 0xCC]); + var node = ChunkFactory.Node(0x10, c1, c2); + + Assert.Equal(2, node.Children.Count); + Assert.True(node.Info.HasChildrenHint); + Assert.Equal(c1.Size + c2.Size, node.Info.BodySize); + } + + [Fact] + public void Node_RootChunk_NestedNodes() + { + var leaf = ChunkFactory.Raw(1, 1, [0xAA]); + var inner = ChunkFactory.Node(2u, leaf); + var outer = ChunkFactory.Node(3u, inner); + + Assert.Single(outer.Children); + Assert.IsType(outer.Children[0]); + } + + [Fact] + public void Node_RootChunk_SetsCorrectMetadataSize() + { + var child = ChunkFactory.Raw(1, 2, [0xAA, 0xBB]); + var node = ChunkFactory.Node(0x10, child); + + // BodySize should equal child's total Size (header + data) + Assert.Equal(child.Size, node.Info.BodySize); + } + + #endregion + + #region Node (MiniChunk) + + [Fact] + public void Node_MiniChunk_CreatesMiniNodeChunk() + { + var child = ChunkFactory.Mini(1, [0xAA]); + var node = ChunkFactory.Node(0x20, child); + + Assert.IsType(node); + Assert.Equal(0x20u, node.Info.Type); + Assert.False(node.Info.HasChildrenHint); + Assert.Single(node.Children); + } + + [Fact] + public void Node_MiniChunk_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.Node(1, (MiniChunk[])null!)); + } + + [Fact] + public void Node_MiniChunk_ThrowsOnEmpty() + { + Assert.Throws(() => ChunkFactory.Node(1, Array.Empty())); + } + + [Fact] + public void Node_MiniChunk_MultipleChildren() + { + var c1 = ChunkFactory.Mini(1, [0xAA]); + var c2 = ChunkFactory.Mini(2, [0xBB]); + var node = ChunkFactory.Node(0x20, c1, c2); + + Assert.Equal(2, node.Children.Count); + Assert.False(node.Info.HasChildrenHint); + } + + [Fact] + public void Node_MiniChunk_SetsCorrectMetadataSize() + { + var child = ChunkFactory.Mini(1, [0xAA]); + var node = ChunkFactory.Node(0x20, child); + + Assert.Equal(child.Size, node.Info.BodySize); + } + + #endregion + + #region File + + [Fact] + public void File_CreatesChunkFile() + { + var root = ChunkFactory.Raw(1, 1, [0xAA]); + var file = ChunkFactory.File(root); + + Assert.IsType(file); + Assert.Single(file.RootChunks); + } + + [Fact] + public void File_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.File(null!)); + } + + [Fact] + public void File_ThrowsOnEmpty() + { + Assert.Throws(() => ChunkFactory.File()); + } + + [Fact] + public void File_MultipleRoots() + { + var r1 = ChunkFactory.Raw(1, 1, [0xAA]); + var r2 = ChunkFactory.Raw(2, 2, [0xBB, 0xCC]); + var file = ChunkFactory.File(r1, r2); + + Assert.Equal(2, file.RootChunks.Count); + } + + #endregion +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs new file mode 100644 index 00000000..3445ebc2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs @@ -0,0 +1,45 @@ +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; + +public class ChunkFileBinaryEquivalenceTest +{ + [Fact] + public void StructuredFile_And_RawFile_ProduceSameBinary() + { + var structuredBytes = TestChunkFileData.StructuredFile.Bytes; + var rawBytes = TestChunkFileData.RawFile.Bytes; + + Assert.Equal(structuredBytes, rawBytes); + } + + [Fact] + public void StructuredFile_MatchesExpectedBytes() + { + Assert.Equal(TestChunkFileData.ExpectedBytes, TestChunkFileData.StructuredFile.Bytes); + } + + [Fact] + public void RawFile_MatchesExpectedBytes() + { + Assert.Equal(TestChunkFileData.ExpectedBytes, TestChunkFileData.RawFile.Bytes); + } + + [Fact] + public void StructuredFile_HasExpectedRootCount() + { + Assert.Equal(3, TestChunkFileData.StructuredFile.RootChunks.Count); + } + + [Fact] + public void RawFile_HasExpectedRootCount() + { + Assert.Equal(3, TestChunkFileData.RawFile.RootChunks.Count); + } + + [Fact] + public void Files_HaveSameSize() + { + Assert.Equal(TestChunkFileData.StructuredFile.Size, TestChunkFileData.RawFile.Size); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs new file mode 100644 index 00000000..b3a7e702 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class ChunkFileTest +{ + private static RawChunk CreateRoot(uint type, byte[] data) + { + return new RawChunk(new ChunkMetadata(type, (uint)data.Length), data); + } + + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var root = CreateRoot(1, [0xAA]); + var file = new ChunkFile([root]); + + Assert.Single(file.RootChunks); + Assert.Same(root, file.RootChunks[0]); + } + + [Fact] + public void Ctor_ThrowsOnNull() + { + Assert.Throws(() => new ChunkFile(null!)); + } + + [Fact] + public void Ctor_ThrowsOnEmpty() + { + Assert.Throws(() => new ChunkFile([])); + } + + [Fact] + public void Size_SumsRootChunkSizes() + { + var r1 = CreateRoot(1, [0xAA]); + var r2 = CreateRoot(2, [0xBB, 0xCC]); + var file = new ChunkFile([r1, r2]); + + Assert.Equal(r1.Size + r2.Size, file.Size); + } + + [Fact] + public void Bytes_ReturnsCorrectBinary() + { + var r1 = CreateRoot(1, [0xAA]); + var file = new ChunkFile([r1]); + + var bytes = file.Bytes; + Assert.Equal(file.Size, bytes.Length); + Assert.Equal(r1.Bytes, bytes); + } + + [Fact] + public void GetBytes_WritesMultipleRootChunks() + { + var r1 = CreateRoot(1, [0xAA]); + var r2 = CreateRoot(2, [0xBB]); + var file = new ChunkFile([r1, r2]); + + var bytes = file.Bytes; + var r1Bytes = r1.Bytes; + var r2Bytes = r2.Bytes; + + for (var i = 0; i < r1Bytes.Length; i++) + Assert.Equal(r1Bytes[i], bytes[i]); + for (var i = 0; i < r2Bytes.Length; i++) + Assert.Equal(r2Bytes[i], bytes[r1.Size + i]); + } + + [Fact] + public void WriteTo_WritesToStream() + { + var r1 = CreateRoot(1, [0xAA]); + var file = new ChunkFile([r1]); + + using var ms = new MemoryStream(); + file.WriteTo(ms); + + Assert.Equal(file.Bytes, ms.ToArray()); + } + + [Fact] + public void WriteTo_ThrowsOnNullStream() + { + var r1 = CreateRoot(1, [0xAA]); + var file = new ChunkFile([r1]); + + Assert.Throws(() => file.WriteTo(null!)); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs new file mode 100644 index 00000000..de47ce76 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs @@ -0,0 +1,81 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class DataChunkTest +{ + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 3); + var chunk = new DataChunk(info, data); + + Assert.Equal(info, chunk.Info); + Assert.Equal(data, chunk.Data.ToArray()); + } + + [Fact] + public void Ctor_ThrowsOnEmptyData() + { + var info = new ChunkMetadata(0x10, 0); + Assert.Throws(() => new DataChunk(info, ReadOnlyMemory.Empty)); + } + + [Fact] + public void Ctor_ThrowsWhenBit31Set() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 0x8000_0003u); + Assert.Throws(() => new DataChunk(info, data)); + } + + [Fact] + public void Ctor_ThrowsWhenSizeMismatch() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 5); + Assert.Throws(() => new DataChunk(info, data)); + } + + [Fact] + public void Size_IncludesHeaderAndData() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 3); + var chunk = new DataChunk(info, data); + + // Header is 8 bytes (sizeof ChunkMetadata) + 3 bytes data + Assert.Equal(8 + 3, chunk.Size); + } + + [Fact] + public void GetBytes_WritesCorrectBinary() + { + var data = new byte[] { 0xAA, 0xBB }; + var info = new ChunkMetadata(0x01, 2); + var chunk = new DataChunk(info, data); + + var bytes = chunk.Bytes; + Assert.Equal(10, bytes.Length); + + // Type (LE) + Assert.Equal(0x01, bytes[0]); + Assert.Equal(0x00, bytes[1]); + Assert.Equal(0x00, bytes[2]); + Assert.Equal(0x00, bytes[3]); + + // Size (LE) - no bit 31 + Assert.Equal(0x02, bytes[4]); + Assert.Equal(0x00, bytes[5]); + Assert.Equal(0x00, bytes[6]); + Assert.Equal(0x00, bytes[7]); + + // Data + Assert.Equal(0xAA, bytes[8]); + Assert.Equal(0xBB, bytes[9]); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/ChunkMetadataTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/ChunkMetadataTest.cs new file mode 100644 index 00000000..6429bbec --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/ChunkMetadataTest.cs @@ -0,0 +1,90 @@ +using System.Runtime.CompilerServices; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model.Metadata; + +public class ChunkMetadataTest +{ + [Fact] + public void SizeOf_Is8Bytes() + { + Assert.Equal(8, Unsafe.SizeOf()); + } + + [Fact] + public void Ctor_SetsTypeAndRawSize() + { + var meta = new ChunkMetadata(0x1234u, 0x5678u); + Assert.Equal(0x1234u, meta.Type); + Assert.Equal(0x5678u, meta.RawSize); + } + + [Fact] + public void Default_HasZeroValues() + { + var meta = default(ChunkMetadata); + Assert.Equal(0u, meta.Type); + Assert.Equal(0u, meta.RawSize); + Assert.False(meta.HasChildrenHint); + Assert.Equal(0, meta.BodySize); + Assert.Equal((uint)meta.BodySize, meta.RawSize); + } + + [Fact] + public void HasChildrenHint_ReturnsFalse_WhenBit31NotSet() + { + var meta = new ChunkMetadata(1, 0x7FFF_FFFFu); + Assert.Equal(0x7FFF_FFFFu, meta.RawSize); + Assert.False(meta.HasChildrenHint); + } + + [Fact] + public void HasChildrenHint_ReturnsTrue_WhenBit31Set() + { + var meta = new ChunkMetadata(1, 0x8000_0000u); + Assert.Equal(0x8000_0000u, meta.RawSize); + Assert.True(meta.HasChildrenHint); + } + + [Fact] + public void HasChildrenHint_ReturnsTrue_WhenAllBitsSet() + { + var meta = new ChunkMetadata(1, 0xFFFF_FFFFu); + Assert.Equal(0xFFFF_FFFFu, meta.RawSize); + Assert.True(meta.HasChildrenHint); + } + + [Fact] + public void BodySize_MasksBit31Off() + { + var meta = new ChunkMetadata(1, 0x8000_0005u); + Assert.Equal(0x8000_0005u, meta.RawSize); + Assert.Equal(5, meta.BodySize); + } + + [Fact] + public void BodySize_ReturnsRawSize_WhenBit31NotSet() + { + var meta = new ChunkMetadata(1, 100u); + Assert.Equal(100u, meta.RawSize); + Assert.Equal(100, meta.BodySize); + Assert.Equal((uint)meta.BodySize, meta.RawSize); + } + + [Fact] + public void BodySize_ReturnsMaxValue_WhenAllLower31BitsSet() + { + var meta = new ChunkMetadata(1, 0xFFFF_FFFFu); + Assert.Equal(0xFFFF_FFFFu, meta.RawSize); + Assert.Equal(int.MaxValue, meta.BodySize); + } + + [Fact] + public void BodySize_ReturnsZero_WhenOnlyBit31Set() + { + var meta = new ChunkMetadata(1, 0x8000_0000u); + Assert.Equal(0x8000_0000u, meta.RawSize); + Assert.Equal(0, meta.BodySize); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/MiniChunkMetadataTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/MiniChunkMetadataTest.cs new file mode 100644 index 00000000..20b25107 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/MiniChunkMetadataTest.cs @@ -0,0 +1,38 @@ +using System.Runtime.CompilerServices; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model.Metadata; + +public class MiniChunkMetadataTest +{ + [Fact] + public void SizeOf_Is2Bytes() + { + Assert.Equal(2, Unsafe.SizeOf()); + } + + [Fact] + public void Ctor_SetsTypeAndBodySize() + { + var meta = new MiniChunkMetadata(0x0A, 0xFF); + Assert.Equal(0x0A, meta.Type); + Assert.Equal(0xFF, meta.BodySize); + } + + [Fact] + public void Default_HasZeroValues() + { + var meta = default(MiniChunkMetadata); + Assert.Equal(0, meta.Type); + Assert.Equal(0, meta.BodySize); + } + + [Fact] + public void Ctor_MaxValues() + { + var meta = new MiniChunkMetadata(byte.MaxValue, byte.MaxValue); + Assert.Equal(byte.MaxValue, meta.Type); + Assert.Equal(byte.MaxValue, meta.BodySize); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs new file mode 100644 index 00000000..bbb3562b --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs @@ -0,0 +1,71 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class MiniChunkTest +{ + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var data = new byte[] { 0xAA, 0xBB }; + var info = new MiniChunkMetadata(0x05, 2); + var chunk = new MiniChunk(info, data); + + Assert.Equal(info, chunk.Info); + Assert.Equal(data, chunk.Data.ToArray()); + } + + [Fact] + public void Ctor_ThrowsOnEmptyData() + { + var info = new MiniChunkMetadata(0x05, 0); + Assert.Throws(() => new MiniChunk(info, ReadOnlyMemory.Empty)); + } + + [Fact] + public void Ctor_ThrowsWhenSizeMismatch() + { + var data = new byte[] { 1, 2, 3 }; + var info = new MiniChunkMetadata(0x05, 5); + Assert.Throws(() => new MiniChunk(info, data)); + } + + [Fact] + public void Size_IncludesHeaderAndData() + { + var data = new byte[] { 1, 2, 3 }; + var info = new MiniChunkMetadata(0x05, 3); + var chunk = new MiniChunk(info, data); + + // Header is 2 bytes (sizeof MiniChunkMetadata) + 3 bytes data + Assert.Equal(2 + 3, chunk.Size); + } + + [Fact] + public void GetBytes_WritesCorrectBinary() + { + var data = new byte[] { 0xDD }; + var info = new MiniChunkMetadata(0x0A, 1); + var chunk = new MiniChunk(info, data); + + var bytes = chunk.Bytes; + Assert.Equal(3, bytes.Length); + + Assert.Equal(0x0A, bytes[0]); // Type + Assert.Equal(0x01, bytes[1]); // Size + Assert.Equal(0xDD, bytes[2]); // Data + } + + [Fact] + public void IsChunk_ButNotRootChunk() + { + var data = new byte[] { 1 }; + var info = new MiniChunkMetadata(1, 1); + var chunk = new MiniChunk(info, data); + Assert.IsAssignableFrom(chunk); + Assert.IsNotType(chunk); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs new file mode 100644 index 00000000..a4ac8068 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs @@ -0,0 +1,89 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class MiniNodeChunkTest +{ + private static MiniChunk CreateMiniChild(byte type, byte[] data) + { + return new MiniChunk(new MiniChunkMetadata(type, (byte)data.Length), data); + } + + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var child = CreateMiniChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x30, (uint)child.Size); + var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); + + Assert.Equal(info, chunk.Info); + Assert.Single(chunk.Children); + Assert.Same(child, chunk.Children[0]); + } + + [Fact] + public void Ctor_ThrowsOnNullChildren() + { + var info = new ChunkMetadata(0x30, 0); + Assert.Throws(() => new MiniNodeChunk(info, null!)); + } + + [Fact] + public void Ctor_ThrowsOnEmptyChildren() + { + var info = new ChunkMetadata(0x30, 0); + Assert.Throws(() => new MiniNodeChunk(info, Array.Empty())); + } + + [Fact] + public void Ctor_ThrowsWhenSizeMismatch() + { + var child = CreateMiniChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x30, 999); + Assert.Throws(() => new MiniNodeChunk(info, new MiniChunk[] { child })); + } + + [Fact] + public void Ctor_ThrowsWhenRawSizeExceedsIntMax() + { + var child = CreateMiniChild(1, new byte[] { 0xAA }); + // RawSize with bit 31 set exceeds int.MaxValue + var info = new ChunkMetadata(0x30, 0x8000_0000u | (uint)child.Size); + Assert.Throws(() => new MiniNodeChunk(info, new MiniChunk[] { child })); + } + + [Fact] + public void Size_IncludesHeaderAndChildren() + { + var child = CreateMiniChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x30, (uint)child.Size); + var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); + + Assert.Equal(8 + child.Size, chunk.Size); + } + + [Fact] + public void IsRootChunk() + { + var child = CreateMiniChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x30, (uint)child.Size); + var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); + Assert.IsAssignableFrom(chunk); + } + + [Fact] + public void MultipleChildren() + { + var c1 = CreateMiniChild(1, new byte[] { 0xAA }); + var c2 = CreateMiniChild(2, new byte[] { 0xBB, 0xCC }); + var totalChildSize = c1.Size + c2.Size; + var info = new ChunkMetadata(0x30, (uint)totalChildSize); + var chunk = new MiniNodeChunk(info, new MiniChunk[] { c1, c2 }); + + Assert.Equal(2, chunk.Children.Count); + Assert.Equal(8 + totalChildSize, chunk.Size); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs new file mode 100644 index 00000000..55bf0e77 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs @@ -0,0 +1,103 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class NodeChunkTest +{ + private static RawChunk CreateChild(uint type, byte[] data) + { + return new RawChunk(new ChunkMetadata(type, (uint)data.Length), data); + } + + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var child = CreateChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); + var chunk = new NodeChunk(info, new RootChunk[] { child }); + + Assert.Equal(info, chunk.Info); + Assert.Single(chunk.Children); + Assert.Same(child, chunk.Children[0]); + } + + [Fact] + public void Ctor_ThrowsWhenBit31NotSet() + { + var child = CreateChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x20, (uint)child.Size); + Assert.Throws(() => new NodeChunk(info, new RootChunk[] { child })); + } + + [Fact] + public void Ctor_ThrowsOnNullChildren() + { + var info = new ChunkMetadata(0x20, 0x8000_0000u); + Assert.Throws(() => new NodeChunk(info, null!)); + } + + [Fact] + public void Ctor_ThrowsOnEmptyChildren() + { + var info = new ChunkMetadata(0x20, 0x8000_0000u); + Assert.Throws(() => new NodeChunk(info, Array.Empty())); + } + + [Fact] + public void Ctor_ThrowsWhenSizeMismatch() + { + var child = CreateChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x20, 0x8000_0000u | 999u); + Assert.Throws(() => new NodeChunk(info, new RootChunk[] { child })); + } + + [Fact] + public void Size_IncludesHeaderAndChildren() + { + var child = CreateChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); + var chunk = new NodeChunk(info, new RootChunk[] { child }); + + Assert.Equal(8 + child.Size, chunk.Size); + } + + [Fact] + public void GetBytes_WritesHeaderAndChildBytes() + { + var child = CreateChild(1, new byte[] { 0xFF }); + var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); + var chunk = new NodeChunk(info, new RootChunk[] { child }); + + var bytes = chunk.Bytes; + Assert.Equal(chunk.Size, bytes.Length); + + var childBytes = child.Bytes; + for (var i = 0; i < childBytes.Length; i++) + Assert.Equal(childBytes[i], bytes[8 + i]); + } + + [Fact] + public void MultipleChildren() + { + var c1 = CreateChild(1, new byte[] { 0xAA }); + var c2 = CreateChild(2, new byte[] { 0xBB, 0xCC }); + var totalChildSize = c1.Size + c2.Size; + var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)totalChildSize); + var chunk = new NodeChunk(info, new RootChunk[] { c1, c2 }); + + Assert.Equal(2, chunk.Children.Count); + Assert.Equal(8 + totalChildSize, chunk.Size); + } + + [Fact] + public void IsRootChunk() + { + var child = CreateChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); + var chunk = new NodeChunk(info, new RootChunk[] { child }); + Assert.IsAssignableFrom(chunk); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs new file mode 100644 index 00000000..8ef2edda --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs @@ -0,0 +1,88 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class RawChunkTest +{ + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 3); + var chunk = new RawChunk(info, data); + + Assert.Equal(info, chunk.Info); + Assert.Equal(data, chunk.Data.ToArray()); + } + + [Fact] + public void Ctor_AllowsBit31Set() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 0x8000_0003u); + var chunk = new RawChunk(info, data); + Assert.True(chunk.Info.HasChildrenHint); + } + + [Fact] + public void Ctor_ThrowsOnEmptyData() + { + var info = new ChunkMetadata(0x10, 0); + Assert.Throws(() => new RawChunk(info, ReadOnlyMemory.Empty)); + } + + [Fact] + public void Ctor_ThrowsWhenSizeMismatch() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 5); + Assert.Throws(() => new RawChunk(info, data)); + } + + [Fact] + public void Size_IncludesHeaderAndData() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 3); + var chunk = new RawChunk(info, data); + Assert.Equal(8 + 3, chunk.Size); + } + + [Fact] + public void GetBytes_WritesRawSizeIncludingBit31() + { + var data = new byte[] { 0xCC }; + var info = new ChunkMetadata(0x02, 0x8000_0001u); + var chunk = new RawChunk(info, data); + + var bytes = chunk.Bytes; + Assert.Equal(9, bytes.Length); + + // Type (LE) + Assert.Equal(0x02, bytes[0]); + Assert.Equal(0x00, bytes[1]); + Assert.Equal(0x00, bytes[2]); + Assert.Equal(0x00, bytes[3]); + + // RawSize (LE) - bit 31 preserved + Assert.Equal(0x01, bytes[4]); + Assert.Equal(0x00, bytes[5]); + Assert.Equal(0x00, bytes[6]); + Assert.Equal(0x80, bytes[7]); + + // Data + Assert.Equal(0xCC, bytes[8]); + } + + [Fact] + public void IsRootChunk() + { + var data = new byte[] { 1 }; + var info = new ChunkMetadata(1, 1); + var chunk = new RawChunk(info, data); + Assert.IsAssignableFrom(chunk); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkFileReaderBaseTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkFileReaderBaseTest.cs new file mode 100644 index 00000000..ca2d4acb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkFileReaderBaseTest.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +using PG.StarWarsGame.Files.ChunkFiles.Data; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader; + +public abstract class ChunkFileReaderBaseTest + where TReader : ChunkFileReaderBase + where TData : IChunkData +{ + protected abstract TReader CreateReader(Stream stream, bool leaveStreamOpen = false); + + protected abstract byte[] CreateValidStreamContent(); + + protected abstract void AssertReadResult(TData result); + + protected abstract void CallThrowIfChunkSizeTooLarge(TReader reader, ChunkMetadata chunk); + + [Fact] + public void Read_ReturnsChunkData() + { + var content = CreateValidStreamContent(); + using var reader = CreateReader(new MemoryStream(content)); + var result = reader.Read(); + AssertReadResult(result); + } + + [Fact] + public void Read_IChunkFileReader_ReturnsIChunkData() + { + var content = CreateValidStreamContent(); + using var reader = CreateReader(new MemoryStream(content)); + IChunkFileReader interfaceReader = reader; + var result = interfaceReader.Read(); + Assert.IsType(result); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Dispose_StreamBehaviorDependsOnLeaveOpen(bool leaveOpen) + { + var ms = new MemoryStream([1, 2, 3]); + var reader = CreateReader(ms, leaveStreamOpen: leaveOpen); + reader.Dispose(); + + if (leaveOpen) + { + ms.Position = 0; + Assert.Equal(1, ms.ReadByte()); + } + else + { + Assert.Throws(() => ms.ReadByte()); + } + } + + [Fact] + public void ThrowIfChunkSizeTooLarge_DoesNotThrow_ForNormalSize() + { + using var reader = CreateReader(new MemoryStream(new byte[8])); + var meta = new ChunkMetadata(1, 100); + CallThrowIfChunkSizeTooLarge(reader, meta); + } + + [Fact] + public void ThrowIfChunkSizeTooLarge_Throws_WhenRawSizeExceedsIntMax() + { + using var reader = CreateReader(new MemoryStream(new byte[8])); + var meta = new ChunkMetadata(1, 0x8000_0001u); + Assert.Throws(() => CallThrowIfChunkSizeTooLarge(reader, meta)); + } + + [Fact] + public void ThrowIfChunkSizeTooLarge_DoesNotThrow_AtExactIntMax() + { + using var reader = CreateReader(new MemoryStream(new byte[8])); + var meta = new ChunkMetadata(1, int.MaxValue); + CallThrowIfChunkSizeTooLarge(reader, meta); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs new file mode 100644 index 00000000..e671a595 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs @@ -0,0 +1,337 @@ +using System; +using System.IO; +using System.Text; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader; + +public class ChunkReaderTest +{ + private static MemoryStream CreateStream(byte[] data) => new(data); + + private static byte[] WriteChunkHeader(uint type, uint rawSize) + { + var bytes = new byte[8]; + BitConverter.GetBytes(type).CopyTo(bytes, 0); + BitConverter.GetBytes(rawSize).CopyTo(bytes, 4); + return bytes; + } + + [Fact] + public void Ctor_ThrowsOnNullStream() + { + Assert.Throws(() => new ChunkReader(null!)); + } + + [Fact] + public void ReadChunk_ReadsHeaderCorrectly() + { + var header = WriteChunkHeader(0x01, 0x0A); + using var reader = new ChunkReader(CreateStream(header)); + + var meta = reader.ReadChunk(); + Assert.Equal(0x01u, meta.Type); + Assert.Equal(0x0Au, meta.RawSize); + } + + [Fact] + public void ReadChunk_WithRefBytes_IncrementsBy8() + { + var header = WriteChunkHeader(0x01, 0x0A); + using var reader = new ChunkReader(CreateStream(header)); + + var readBytes = 0; + var meta = reader.ReadChunk(ref readBytes); + Assert.Equal(8, readBytes); + Assert.Equal(0x01u, meta.Type); + } + + [Fact] + public void ReadChunk_ThrowsAtEndOfStream() + { + using var reader = new ChunkReader(CreateStream(Array.Empty())); + Assert.Throws(() => reader.ReadChunk()); + } + + [Fact] + public void ReadMiniChunk_ReadsHeaderCorrectly() + { + var data = new byte[] { 0x05, 0x03 }; + using var reader = new ChunkReader(CreateStream(data)); + + var readBytes = 0; + var meta = reader.ReadMiniChunk(ref readBytes); + Assert.Equal(0x05, meta.Type); + Assert.Equal(0x03, meta.BodySize); + Assert.Equal(2, readBytes); + } + + [Fact] + public void ReadData_ChunkMetadata_ReadsBody() + { + var header = WriteChunkHeader(0x01, 3); + var body = new byte[] { 0xAA, 0xBB, 0xCC }; + var stream = new byte[header.Length + body.Length]; + header.CopyTo(stream, 0); + body.CopyTo(stream, header.Length); + + using var reader = new ChunkReader(CreateStream(stream)); + var meta = reader.ReadChunk(); + var data = reader.ReadData(meta); + + Assert.Equal(body, data); + } + + [Fact] + public void ReadData_ChunkMetadata_ThrowsForContainerChunk() + { + var header = WriteChunkHeader(0x01, 0x8000_0003u); + using var reader = new ChunkReader(CreateStream(header)); + var meta = reader.ReadChunk(); + + Assert.Throws(() => reader.ReadData(meta)); + } + + [Fact] + public void ReadData_ChunkMetadata_WithRefBytes_IncrementsCorrectly() + { + var header = WriteChunkHeader(0x01, 3); + var body = new byte[] { 0xAA, 0xBB, 0xCC }; + var stream = new byte[header.Length + body.Length]; + header.CopyTo(stream, 0); + body.CopyTo(stream, header.Length); + + using var reader = new ChunkReader(CreateStream(stream)); + var meta = reader.ReadChunk(); + var readBytes = 0; + var data = reader.ReadData(meta, ref readBytes); + + Assert.Equal(body, data); + Assert.Equal(3, readBytes); + } + + [Fact] + public void ReadData_MiniChunkMetadata_ReadsBody() + { + var miniHeader = new byte[] { 0x05, 0x02 }; + var body = new byte[] { 0xDD, 0xEE }; + var stream = new byte[miniHeader.Length + body.Length]; + miniHeader.CopyTo(stream, 0); + body.CopyTo(stream, miniHeader.Length); + + using var reader = new ChunkReader(CreateStream(stream)); + var rb = 0; + var meta = reader.ReadMiniChunk(ref rb); + var data = reader.ReadData(meta); + + Assert.Equal(body, data); + } + + [Fact] + public void ReadData_MiniChunkMetadata_WithRefBytes_IncrementsCorrectly() + { + var miniHeader = new byte[] { 0x05, 0x02 }; + var body = new byte[] { 0xDD, 0xEE }; + var stream = new byte[miniHeader.Length + body.Length]; + miniHeader.CopyTo(stream, 0); + body.CopyTo(stream, miniHeader.Length); + + using var reader = new ChunkReader(CreateStream(stream)); + var rb = 0; + var meta = reader.ReadMiniChunk(ref rb); + rb = 0; + var data = reader.ReadData(meta, ref rb); + + Assert.Equal(body, data); + Assert.Equal(2, rb); + } + + [Fact] + public void ReadData_BySize_ReadsCorrectBytes() + { + var data = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + using var reader = new ChunkReader(CreateStream(data)); + + var result = reader.ReadData(3); + Assert.Equal(new byte[] { 0x01, 0x02, 0x03 }, result); + } + + [Fact] + public void ReadData_BySize_ThrowsOnNegative() + { + using var reader = new ChunkReader(CreateStream(new byte[] { 0x01 })); + Assert.Throws(() => reader.ReadData(-1)); + } + + [Fact] + public void ReadDword_ReadsUInt32() + { + var data = BitConverter.GetBytes(0x12345678u); + using var reader = new ChunkReader(CreateStream(data)); + + var value = reader.ReadDword(); + Assert.Equal(0x12345678u, value); + } + + [Fact] + public void ReadDword_WithRefBytes_IncrementsBy4() + { + var data = BitConverter.GetBytes(42u); + using var reader = new ChunkReader(CreateStream(data)); + + var readBytes = 0; + var value = reader.ReadDword(ref readBytes); + Assert.Equal(42u, value); + Assert.Equal(4, readBytes); + } + + [Fact] + public void ReadFloat_WithRefBytes_IncrementsBy4() + { + var data = BitConverter.GetBytes(3.14f); + using var reader = new ChunkReader(CreateStream(data)); + + var readBytes = 0; + var value = reader.ReadFloat(ref readBytes); + Assert.Equal(3.14f, value); + Assert.Equal(4, readBytes); + } + + [Fact] + public void Skip_AdvancesStreamPosition() + { + var data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + using var reader = new ChunkReader(CreateStream(data)); + + reader.Skip(3); + var result = reader.ReadData(2); + Assert.Equal(new byte[] { 0x04, 0x05 }, result); + } + + [Fact] + public void Skip_WithRefBytes_IncrementsCounter() + { + var data = new byte[] { 0x01, 0x02, 0x03 }; + using var reader = new ChunkReader(CreateStream(data)); + + var readBytes = 0; + reader.Skip(2, ref readBytes); + Assert.Equal(2, readBytes); + } + + [Fact] + public void TryReadChunk_ReturnsNull_AtEndOfStream() + { + using var reader = new ChunkReader(CreateStream(Array.Empty())); + var result = reader.TryReadChunk(); + Assert.Null(result); + } + + [Fact] + public void TryReadChunk_ReturnsMetadata_WhenDataAvailable() + { + var header = WriteChunkHeader(0x42, 0x10); + using var reader = new ChunkReader(CreateStream(header)); + + var result = reader.TryReadChunk(); + Assert.NotNull(result); + Assert.Equal(0x42u, result.Value.Type); + Assert.Equal(0x10u, result.Value.RawSize); + } + + [Fact] + public void TryReadChunk_WithRefBytes_IncrementsBy8() + { + var header = WriteChunkHeader(0x42, 0x10); + using var reader = new ChunkReader(CreateStream(header)); + + var readBytes = 0; + var result = reader.TryReadChunk(ref readBytes); + Assert.NotNull(result); + Assert.Equal(8, readBytes); + } + + [Fact] + public void TryReadChunk_WithRefBytes_DoesNotIncrement_AtEnd() + { + using var reader = new ChunkReader(CreateStream(Array.Empty())); + + var readBytes = 5; + var result = reader.TryReadChunk(ref readBytes); + Assert.Null(result); + Assert.Equal(5, readBytes); + } + + [Fact] + public void ReadString_ReadsCorrectly() + { + var text = "Hello"; + var encoded = Encoding.ASCII.GetBytes(text); + using var reader = new ChunkReader(CreateStream(encoded)); + + var readBytes = 0; + var result = reader.ReadString(encoded.Length, Encoding.ASCII, false, ref readBytes); + Assert.Equal(text, result); + Assert.Equal(encoded.Length, readBytes); + } + + [Fact] + public void ReadString_ZeroTerminated_TrimsAtNull() + { + var encoded = new byte[] { (byte)'H', (byte)'i', 0, (byte)'X' }; + using var reader = new ChunkReader(CreateStream(encoded)); + + var result = reader.ReadString(encoded.Length, Encoding.ASCII, true); + Assert.Equal("Hi", result); + } + + [Fact] + public void ReadString_WithoutRefBytes_Works() + { + var text = "AB"; + var encoded = Encoding.ASCII.GetBytes(text); + using var reader = new ChunkReader(CreateStream(encoded)); + + var result = reader.ReadString(encoded.Length, Encoding.ASCII, false); + Assert.Equal(text, result); + } + + [Fact] + public void Dispose_DisposesUnderlyingStream() + { + var ms = CreateStream(new byte[] { 1, 2, 3 }); + var reader = new ChunkReader(ms); + reader.Dispose(); + + Assert.Throws(() => ms.ReadByte()); + } + + [Fact] + public void Dispose_LeaveOpen_KeepsStreamOpen() + { + var ms = CreateStream(new byte[] { 1, 2, 3 }); + var reader = new ChunkReader(ms, leaveOpen: true); + reader.Dispose(); + + // Stream should still be usable + ms.Position = 0; + Assert.Equal(1, ms.ReadByte()); + } + + [Fact] + public void ReadChunk_Roundtrip_WithChunkFactory() + { + var original = PG.StarWarsGame.Files.ChunkFiles.Binary.ChunkFactory.Data(0x42, new byte[] { 1, 2, 3 }); + var bytes = original.Bytes; + + using var reader = new ChunkReader(CreateStream(bytes)); + var meta = reader.ReadChunk(); + var data = reader.ReadData(meta); + + Assert.Equal(0x42u, meta.Type); + Assert.Equal(3, meta.BodySize); + Assert.False(meta.HasChildrenHint); + Assert.Equal(new byte[] { 1, 2, 3 }, data); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs new file mode 100644 index 00000000..c0ffa408 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs @@ -0,0 +1,21 @@ +using System.IO; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader; + +public sealed class TestChunkFileReader(Stream stream, bool leaveStreamOpen = false) + : ChunkFileReaderBase(stream, leaveStreamOpen) +{ + public override TestChunkFileReaderTest.TestChunkData Read() + { + var meta = ChunkReader.ReadChunk(); + var data = ChunkReader.ReadData(meta); + return new TestChunkFileReaderTest.TestChunkData(data); + } + + public void CallThrowIfChunkSizeTooLarge(ChunkMetadata chunk) + { + ThrowIfChunkSizeTooLargeException(chunk); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs new file mode 100644 index 00000000..0f4ebecc --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs @@ -0,0 +1,45 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Data; +using System; +using System.IO; +using Xunit; +using static PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader.TestChunkFileReaderTest; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader; + +public sealed class TestChunkFileReaderTest : ChunkFileReaderBaseTest +{ + public sealed class TestChunkData(byte[] data) : IChunkData + { + public byte[] Data { get; } = data; + + public void Dispose() { } + } + + protected override TestChunkFileReader CreateReader(Stream stream, bool leaveStreamOpen = false) + { + return new TestChunkFileReader(stream, leaveStreamOpen); + } + + protected override byte[] CreateValidStreamContent() + { + var header = new byte[8]; + BitConverter.GetBytes(0x01u).CopyTo(header, 0); + BitConverter.GetBytes(3u).CopyTo(header, 4); + var body = new byte[] { 0xAA, 0xBB, 0xCC }; + var stream = new byte[header.Length + body.Length]; + header.CopyTo(stream, 0); + body.CopyTo(stream, header.Length); + return stream; + } + + protected override void AssertReadResult(TestChunkData result) + { + Assert.Equal(new byte[] { 0xAA, 0xBB, 0xCC }, result.Data); + } + + protected override void CallThrowIfChunkSizeTooLarge(TestChunkFileReader reader, ChunkMetadata chunk) + { + reader.CallThrowIfChunkSizeTooLarge(chunk); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs new file mode 100644 index 00000000..5dd8627a --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs @@ -0,0 +1,103 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; + +/// +/// Provides a reusable, complex test chunk file built using . +/// The file structure is: +/// +/// Root[0]: DataChunk(type=0x01, data=[0xAA, 0xBB, 0xCC]) +/// Root[1]: NodeChunk(type=0x10) +/// Child[0]: DataChunk(type=0x02, data=[0x11, 0x22]) +/// Child[1]: NodeChunk(type=0x20) +/// Child[0]: DataChunk(type=0x03, data=[0x33]) +/// Child[2]: MiniNodeChunk(type=0x30) +/// Mini[0]: MiniChunk(type=0x04, data=[0x44, 0x55]) +/// Mini[1]: MiniChunk(type=0x05, data=[0x66]) +/// Root[2]: DataChunk(type=0x06, data=[0x77, 0x88, 0x99, 0xDD]) +/// +/// +public static class TestChunkFileData +{ + /// + /// Gets a complex chunk file built using structured chunk types (NodeChunk, MiniNodeChunk, and DataChunk leaves). + /// + public static ChunkFile StructuredFile { get; } = BuildStructuredFile(); + + /// + /// Gets a chunk file built entirely from RawChunk instances that produce the same binary as . + /// + public static ChunkFile RawFile { get; } = BuildRawFile(); + + /// + /// Gets the expected binary representation of the test chunk file. + /// + public static byte[] ExpectedBytes { get; } = StructuredFile.Bytes; + + private static ChunkFile BuildStructuredFile() + { + // Root[0]: leaf data chunk + var root0 = ChunkFactory.Data(0x01, [0xAA, 0xBB, 0xCC]); + + // Build children for Root[1] NodeChunk + var child0 = ChunkFactory.Data(0x02, [0x11, 0x22]); + + var grandchild = ChunkFactory.Data(0x03, [0x33]); + var child1 = ChunkFactory.Node(0x20u, grandchild); + + // MiniNodeChunk as a child - serialize mini-chunks into raw body + var miniChild0 = ChunkFactory.Mini(0x04, [0x44, 0x55]); + var miniChild1 = ChunkFactory.Mini(0x05, [0x66]); + var child2 = ChunkFactory.Node(0x30u, miniChild0, miniChild1); + + var root1 = ChunkFactory.Node(0x10u, child0, child1, child2); + + // Root[2]: leaf data chunk + var root2 = ChunkFactory.Data(0x06, [0x77, 0x88, 0x99, 0xDD]); + + return ChunkFactory.File(root0, root1, root2); + } + + private static ChunkFile BuildRawFile() + { + // Build the exact same binary using only RawChunk instances. + + // Root[0] + var root0 = ChunkFactory.Raw(0x01, 3, [0xAA, 0xBB, 0xCC]); + + // child0: RawChunk(type=0x02, size=2, data=[0x11, 0x22]) -> 10 bytes total + var rawChild0 = ChunkFactory.Raw(0x02, 2, [0x11, 0x22]); + + // grandchild: RawChunk(type=0x03, size=1, data=[0x33]) -> 9 bytes total + // child1: NodeChunk(type=0x20) wrapping grandchild -> body = grandchild.Bytes (9 bytes) + var grandchildBytes = ChunkFactory.Raw(0x03, 1, [0x33]).Bytes; + var rawChild1 = ChunkFactory.Raw(0x20, 0x8000_0000u | (uint)grandchildBytes.Length, grandchildBytes); + + // child2: MiniNodeChunk(type=0x30) with 2 mini-chunks + // Mini(0x04,[0x44,0x55]) = 4 bytes, Mini(0x05,[0x66]) = 3 bytes -> body = 7 bytes + var mc0 = ChunkFactory.Mini(0x04, [0x44, 0x55]); + var mc1 = ChunkFactory.Mini(0x05, [0x66]); + var miniBody = new byte[mc0.Size + mc1.Size]; + mc0.GetBytes(miniBody); + mc1.GetBytes(((Span)miniBody).Slice(mc0.Size)); + var rawChild2 = ChunkFactory.Raw(0x30, (uint)miniBody.Length, miniBody); + + // Root[1]: NodeChunk(type=0x10) body = rawChild0 + rawChild1 + rawChild2 + var root1BodySize = rawChild0.Size + rawChild1.Size + rawChild2.Size; + var root1Body = new byte[root1BodySize]; + var offset = 0; + rawChild0.GetBytes(root1Body); + offset += rawChild0.Size; + rawChild1.GetBytes(((Span)root1Body).Slice(offset)); + offset += rawChild1.Size; + rawChild2.GetBytes(((Span)root1Body).Slice(offset)); + var root1 = ChunkFactory.Raw(0x10, 0x8000_0000u | (uint)root1BodySize, root1Body); + + // Root[2] + var root2 = ChunkFactory.Raw(0x06, 4, [0x77, 0x88, 0x99, 0xDD]); + + return ChunkFactory.File(root0, root1, root2); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs index 29f61617..d384d069 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs @@ -32,7 +32,6 @@ public static DataChunk Data(uint type, byte[] data) { if (data == null) throw new ArgumentNullException(nameof(data)); - var metadata = new ChunkMetadata(type, (uint)data.Length); return new DataChunk(metadata, data); } @@ -49,7 +48,6 @@ public static RawChunk Raw(uint type, uint rawSize, byte[] data) { if (data == null) throw new ArgumentNullException(nameof(data)); - var metadata = new ChunkMetadata(type, rawSize); return new RawChunk(metadata, data); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs index 0b5b820d..f4d03188 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs @@ -7,7 +7,7 @@ namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; /// /// A chunk containing binary data. /// -public sealed class DataChunk : Chunk +public sealed class DataChunk : RootChunk { /// /// Gets the chunk metadata. diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs index bd336b05..22940ba6 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs @@ -44,9 +44,15 @@ public sealed class RawChunk : RootChunk /// /// The chunk metadata, written as-is. No validation is performed on bit 31. /// The raw body data. - /// body size does not match the data length. + /// + /// is empty, or + /// body size does not match the data length. + /// public RawChunk(ChunkMetadata info, ReadOnlyMemory data) { + if (data is { IsEmpty: true, Length: 0 }) + throw new ArgumentException("Data cannot be empty.", nameof(data)); + if (info.BodySize != data.Length) throw new ArgumentException( $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", From 3a3eb5e2042baa2744cadcbfd19b0160c74e0890 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 30 Mar 2026 16:08:51 +0200 Subject: [PATCH 06/17] support empty data and childs --- .../Binary/ChunkFactoryTest.cs | 30 +-- .../Binary/ChunkFileBinaryEquivalenceTest.cs | 45 ----- .../Binary/ChunkFileIntegrationTest.cs | 140 ++++++++++++++ .../Binary/Model/ChunkFileTest.cs | 24 +-- .../Binary/Model/DataChunkTest.cs | 27 ++- .../Binary/Model/MiniChunkTest.cs | 22 ++- .../Binary/Model/MiniNodeChunkTest.cs | 74 ++++++-- .../Binary/Model/NodeChunkTest.cs | 74 +++++--- .../Binary/Model/RawChunkTest.cs | 27 ++- .../Binary/RawChunkEquivalenceTest.cs | 173 ++++++++++++++++++ .../Binary/Reader/ChunkReaderTest.cs | 85 ++++++++- .../Binary/TestChunkFileData.cs | 122 ++++++------ ....StarWarsGame.Files.ChunkFiles.Test.csproj | 6 +- .../Binary/Model/ChunkFile.cs | 24 +-- .../Binary/Model/DataChunk.cs | 4 - .../Binary/Model/MiniChunk.cs | 6 +- .../Binary/Model/MiniNodeChunk.cs | 2 +- .../Binary/Model/NodeChunk.cs | 2 +- .../Binary/Model/NodeChunkBase.cs | 7 +- .../Binary/Model/RawChunk.cs | 4 - .../PG.StarWarsGame.Files.TED.Test.csproj | 1 + 21 files changed, 661 insertions(+), 238 deletions(-) delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileIntegrationTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/RawChunkEquivalenceTest.cs diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs index 0b08f9fc..a5228afb 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs @@ -27,9 +27,10 @@ public void Data_ThrowsOnNull() } [Fact] - public void Data_ThrowsOnEmptyArray() + public void Data_AllowsEmptyArray() { - Assert.Throws(() => ChunkFactory.Data(1, [])); + var chunk = ChunkFactory.Data(1, []); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -61,9 +62,10 @@ public void Raw_ThrowsOnNull() } [Fact] - public void Raw_ThrowsOnEmptyData() + public void Raw_AllowsEmptyData() { - Assert.Throws(() => ChunkFactory.Raw(0x01, 0, [])); + var chunk = ChunkFactory.Raw(0x01, 0, []); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -123,9 +125,10 @@ public void Mini_AllowsMaxLength255() } [Fact] - public void Mini_ThrowsOnEmptyArray() + public void Mini_AllowsEmptyArray() { - Assert.Throws(() => ChunkFactory.Mini(1, [])); + var chunk = ChunkFactory.Mini(1, []); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -158,9 +161,10 @@ public void Node_RootChunk_ThrowsOnNull() } [Fact] - public void Node_RootChunk_ThrowsOnEmpty() + public void Node_RootChunk_AllowsEmpty() { - Assert.Throws(() => ChunkFactory.Node(1, Array.Empty())); + var node = ChunkFactory.Node(1, Array.Empty()); + Assert.Empty(node.Children); } [Fact] @@ -219,9 +223,10 @@ public void Node_MiniChunk_ThrowsOnNull() } [Fact] - public void Node_MiniChunk_ThrowsOnEmpty() + public void Node_MiniChunk_AllowsEmpty() { - Assert.Throws(() => ChunkFactory.Node(1, Array.Empty())); + var node = ChunkFactory.Node(1, Array.Empty()); + Assert.Empty(node.Children); } [Fact] @@ -265,9 +270,10 @@ public void File_ThrowsOnNull() } [Fact] - public void File_ThrowsOnEmpty() + public void File_AllowsEmpty() { - Assert.Throws(() => ChunkFactory.File()); + var file = ChunkFactory.File(); + Assert.Empty(file.RootChunks); } [Fact] diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs deleted file mode 100644 index 3445ebc2..00000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Xunit; - -namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; - -public class ChunkFileBinaryEquivalenceTest -{ - [Fact] - public void StructuredFile_And_RawFile_ProduceSameBinary() - { - var structuredBytes = TestChunkFileData.StructuredFile.Bytes; - var rawBytes = TestChunkFileData.RawFile.Bytes; - - Assert.Equal(structuredBytes, rawBytes); - } - - [Fact] - public void StructuredFile_MatchesExpectedBytes() - { - Assert.Equal(TestChunkFileData.ExpectedBytes, TestChunkFileData.StructuredFile.Bytes); - } - - [Fact] - public void RawFile_MatchesExpectedBytes() - { - Assert.Equal(TestChunkFileData.ExpectedBytes, TestChunkFileData.RawFile.Bytes); - } - - [Fact] - public void StructuredFile_HasExpectedRootCount() - { - Assert.Equal(3, TestChunkFileData.StructuredFile.RootChunks.Count); - } - - [Fact] - public void RawFile_HasExpectedRootCount() - { - Assert.Equal(3, TestChunkFileData.RawFile.RootChunks.Count); - } - - [Fact] - public void Files_HaveSameSize() - { - Assert.Equal(TestChunkFileData.StructuredFile.Size, TestChunkFileData.RawFile.Size); - } -} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileIntegrationTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileIntegrationTest.cs new file mode 100644 index 00000000..e571243c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileIntegrationTest.cs @@ -0,0 +1,140 @@ +using System.IO; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; + +/// +/// Integration test that reads the binary representation of +/// using and reconstructs the original chunk tree structure, +/// asserting that every chunk type, size, and data value matches the expected layout. +/// +public class ChunkFileIntegrationTest +{ + /// + /// Reads the full test chunk file from and + /// verifies that the reconstructed chunk tree matches the documented structure exactly. + /// + /// Root[0]: DataChunk(type=0x01, data=[0xAA, 0xBB, 0xCC]) + /// Root[1]: NodeChunk(type=0x10) + /// Child[0]: DataChunk(type=0x02, data=[0x11, 0x22]) + /// Child[1]: NodeChunk(type=0x20) + /// Child[0]: DataChunk(type=0x03, data=[0x33]) + /// Child[2]: MiniNodeChunk(type=0x30) + /// Mini[0]: MiniChunk(type=0x04, data=[0x44, 0x55]) + /// Mini[1]: MiniChunk(type=0x05, data=[0x66]) + /// Root[2]: DataChunk(type=0x06, data=[0x77, 0x88, 0x99, 0xDD]) + /// Root[3]: DataChunk(type=0x07, data=[]) + /// Root[4]: NodeChunk(type=0x11, children=[]) + /// Root[5]: MiniNodeChunk(type=0x31) + /// Mini[0]: MiniChunk(type=0x08, data=[]) + /// Root[6]: MiniNodeChunk(type=0x32, children=[]) + /// + /// + [Fact] + public void Read_ReconstructsFullChunkTree() + { + var bytes = TestChunkFileData.ExpectedBytes; + using var stream = new MemoryStream(bytes.ToArray()); + using var reader = new ChunkReader(stream, leaveOpen: true); + + // Root[0]: DataChunk(type=0x01, data=[0xAA, 0xBB, 0xCC]) + var root0 = reader.ReadChunk(); + Assert.Equal(0x01u, root0.Type); + Assert.False(root0.HasChildrenHint); + Assert.Equal(3, root0.BodySize); + Assert.Equal([0xAA, 0xBB, 0xCC], reader.ReadData(root0)); + + // Root[1]: NodeChunk(type=0x10) with 3 children + var root1 = reader.ReadChunk(); + Assert.Equal(0x10u, root1.Type); + Assert.True(root1.HasChildrenHint); + var root1Read = 0; + + // Root[1] Child[0]: DataChunk(type=0x02, data=[0x11, 0x22]) + var r1c0 = reader.ReadChunk(ref root1Read); + Assert.Equal(0x02u, r1c0.Type); + Assert.False(r1c0.HasChildrenHint); + Assert.Equal([0x11, 0x22], reader.ReadData(r1c0)); + root1Read += r1c0.BodySize; + + // Root[1] Child[1]: NodeChunk(type=0x20) with 1 child + var r1c1 = reader.ReadChunk(ref root1Read); + Assert.Equal(0x20u, r1c1.Type); + Assert.True(r1c1.HasChildrenHint); + var r1c1Read = 0; + + // Root[1] Child[1] Child[0]: DataChunk(type=0x03, data=[0x33]) + var r1c1c0 = reader.ReadChunk(ref r1c1Read); + Assert.Equal(0x03u, r1c1c0.Type); + Assert.False(r1c1c0.HasChildrenHint); + Assert.Equal([0x33], reader.ReadData(r1c1c0)); + r1c1Read += r1c1c0.BodySize; + Assert.Equal(r1c1.BodySize, r1c1Read); + root1Read += r1c1.BodySize; + + // Root[1] Child[2]: MiniNodeChunk(type=0x30) with 2 mini-chunks + var r1c2 = reader.ReadChunk(ref root1Read); + Assert.Equal(0x30u, r1c2.Type); + Assert.False(r1c2.HasChildrenHint); + var r1c2Read = 0; + + // Root[1] Child[2] Mini[0]: MiniChunk(type=0x04, data=[0x44, 0x55]) + var r1c2m0 = reader.ReadMiniChunk(ref r1c2Read); + Assert.Equal(0x04, r1c2m0.Type); + Assert.Equal([0x44, 0x55], reader.ReadData(r1c2m0)); + r1c2Read += r1c2m0.BodySize; + + // Root[1] Child[2] Mini[1]: MiniChunk(type=0x05, data=[0x66]) + var r1c2m1 = reader.ReadMiniChunk(ref r1c2Read); + Assert.Equal(0x05, r1c2m1.Type); + Assert.Equal([0x66], reader.ReadData(r1c2m1)); + r1c2Read += r1c2m1.BodySize; + + Assert.Equal(r1c2.BodySize, r1c2Read); + root1Read += r1c2.BodySize; + Assert.Equal(root1.BodySize, root1Read); + + // Root[2]: DataChunk(type=0x06, data=[0x77, 0x88, 0x99, 0xDD]) + var root2 = reader.ReadChunk(); + Assert.Equal(0x06u, root2.Type); + Assert.False(root2.HasChildrenHint); + Assert.Equal([0x77, 0x88, 0x99, 0xDD], reader.ReadData(root2)); + + // Root[3]: DataChunk(type=0x07, data=[]) + var root3 = reader.ReadChunk(); + Assert.Equal(0x07u, root3.Type); + Assert.False(root3.HasChildrenHint); + Assert.Equal(0, root3.BodySize); + Assert.Equal([], reader.ReadData(root3)); + + // Root[4]: NodeChunk(type=0x11, children=[]) + var root4 = reader.ReadChunk(); + Assert.Equal(0x11u, root4.Type); + Assert.True(root4.HasChildrenHint); + Assert.Equal(0, root4.BodySize); + + // Root[5]: MiniNodeChunk(type=0x31) with 1 empty mini-chunk + var root5 = reader.ReadChunk(); + Assert.Equal(0x31u, root5.Type); + Assert.False(root5.HasChildrenHint); + var root5Read = 0; + + // Root[5] Mini[0]: MiniChunk(type=0x08, data=[]) + var r5m0 = reader.ReadMiniChunk(ref root5Read); + Assert.Equal(0x08, r5m0.Type); + Assert.Equal(0, r5m0.BodySize); + Assert.Equal([], reader.ReadData(r5m0)); + root5Read += r5m0.BodySize; + Assert.Equal(root5.BodySize, root5Read); + + // Root[6]: MiniNodeChunk(type=0x32, children=[]) + var root6 = reader.ReadChunk(); + Assert.Equal(0x32u, root6.Type); + Assert.False(root6.HasChildrenHint); + Assert.Equal(0, root6.BodySize); + + // Verify entire stream was consumed + Assert.Equal(stream.Length, stream.Position); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs index b3a7e702..6fcbb5f1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs @@ -30,9 +30,10 @@ public void Ctor_ThrowsOnNull() } [Fact] - public void Ctor_ThrowsOnEmpty() + public void Ctor_AllowsEmpty() { - Assert.Throws(() => new ChunkFile([])); + var file = new ChunkFile([]); + Assert.Empty(file.RootChunks); } [Fact] @@ -57,20 +58,19 @@ public void Bytes_ReturnsCorrectBinary() } [Fact] - public void GetBytes_WritesMultipleRootChunks() + public void GetBytes_WritesExactByteSequence() { + // type=0x01, size=1: [0x01,0x00,0x00,0x00, 0x01,0x00,0x00,0x00, 0xAA] + // type=0x02, size=2: [0x02,0x00,0x00,0x00, 0x02,0x00,0x00,0x00, 0xBB,0xCC] var r1 = CreateRoot(1, [0xAA]); - var r2 = CreateRoot(2, [0xBB]); + var r2 = CreateRoot(2, [0xBB, 0xCC]); var file = new ChunkFile([r1, r2]); - var bytes = file.Bytes; - var r1Bytes = r1.Bytes; - var r2Bytes = r2.Bytes; - - for (var i = 0; i < r1Bytes.Length; i++) - Assert.Equal(r1Bytes[i], bytes[i]); - for (var i = 0; i < r2Bytes.Length; i++) - Assert.Equal(r2Bytes[i], bytes[r1.Size + i]); + Assert.Equal(new byte[] + { + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xAA, + 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xBB, 0xCC, + }, file.Bytes); } [Fact] diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs index de47ce76..815053b0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs @@ -19,10 +19,11 @@ public void Ctor_ValidArgs_SetsProperties() } [Fact] - public void Ctor_ThrowsOnEmptyData() + public void Ctor_AllowsEmptyData() { var info = new ChunkMetadata(0x10, 0); - Assert.Throws(() => new DataChunk(info, ReadOnlyMemory.Empty)); + var chunk = new DataChunk(info, ReadOnlyMemory.Empty); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -52,6 +53,28 @@ public void Size_IncludesHeaderAndData() Assert.Equal(8 + 3, chunk.Size); } + [Fact] + public void GetBytes_EmptyData_WritesHeaderOnly() + { + var info = new ChunkMetadata(0x05, 0); + var chunk = new DataChunk(info, ReadOnlyMemory.Empty); + + var bytes = chunk.Bytes; + Assert.Equal(8, bytes.Length); + + // Type (LE) + Assert.Equal(0x05, bytes[0]); + Assert.Equal(0x00, bytes[1]); + Assert.Equal(0x00, bytes[2]); + Assert.Equal(0x00, bytes[3]); + + // Size = 0 (LE) + Assert.Equal(0x00, bytes[4]); + Assert.Equal(0x00, bytes[5]); + Assert.Equal(0x00, bytes[6]); + Assert.Equal(0x00, bytes[7]); + } + [Fact] public void GetBytes_WritesCorrectBinary() { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs index bbb3562b..e9ef0c8b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs @@ -19,10 +19,11 @@ public void Ctor_ValidArgs_SetsProperties() } [Fact] - public void Ctor_ThrowsOnEmptyData() + public void Ctor_AllowsEmptyData() { var info = new MiniChunkMetadata(0x05, 0); - Assert.Throws(() => new MiniChunk(info, ReadOnlyMemory.Empty)); + var chunk = new MiniChunk(info, ReadOnlyMemory.Empty); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -44,6 +45,19 @@ public void Size_IncludesHeaderAndData() Assert.Equal(2 + 3, chunk.Size); } + [Fact] + public void GetBytes_EmptyData_WritesHeaderOnly() + { + var info = new MiniChunkMetadata(0x0B, 0); + var chunk = new MiniChunk(info, ReadOnlyMemory.Empty); + + var bytes = chunk.Bytes; + Assert.Equal(2, bytes.Length); + + Assert.Equal(0x0B, bytes[0]); // Type + Assert.Equal(0x00, bytes[1]); // Size = 0 + } + [Fact] public void GetBytes_WritesCorrectBinary() { @@ -65,7 +79,7 @@ public void IsChunk_ButNotRootChunk() var data = new byte[] { 1 }; var info = new MiniChunkMetadata(1, 1); var chunk = new MiniChunk(info, data); - Assert.IsAssignableFrom(chunk); - Assert.IsNotType(chunk); + Assert.IsType(chunk, false); + Assert.IsNotType(chunk, exactMatch: false); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs index a4ac8068..20055bd7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs @@ -15,9 +15,9 @@ private static MiniChunk CreateMiniChild(byte type, byte[] data) [Fact] public void Ctor_ValidArgs_SetsProperties() { - var child = CreateMiniChild(1, new byte[] { 0xAA }); + var child = CreateMiniChild(1, [0xAA]); var info = new ChunkMetadata(0x30, (uint)child.Size); - var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); + var chunk = new MiniNodeChunk(info, [child]); Assert.Equal(info, chunk.Info); Assert.Single(chunk.Children); @@ -32,56 +32,98 @@ public void Ctor_ThrowsOnNullChildren() } [Fact] - public void Ctor_ThrowsOnEmptyChildren() + public void Ctor_AllowsEmptyChildren() { var info = new ChunkMetadata(0x30, 0); - Assert.Throws(() => new MiniNodeChunk(info, Array.Empty())); + var chunk = new MiniNodeChunk(info, []); + Assert.Empty(chunk.Children); } [Fact] public void Ctor_ThrowsWhenSizeMismatch() { - var child = CreateMiniChild(1, new byte[] { 0xAA }); + var child = CreateMiniChild(1, [0xAA]); var info = new ChunkMetadata(0x30, 999); - Assert.Throws(() => new MiniNodeChunk(info, new MiniChunk[] { child })); + Assert.Throws(() => new MiniNodeChunk(info, [child])); } [Fact] public void Ctor_ThrowsWhenRawSizeExceedsIntMax() { - var child = CreateMiniChild(1, new byte[] { 0xAA }); + var child = CreateMiniChild(1, [0xAA]); // RawSize with bit 31 set exceeds int.MaxValue var info = new ChunkMetadata(0x30, 0x8000_0000u | (uint)child.Size); - Assert.Throws(() => new MiniNodeChunk(info, new MiniChunk[] { child })); + Assert.Throws(() => new MiniNodeChunk(info, [child])); } [Fact] public void Size_IncludesHeaderAndChildren() { - var child = CreateMiniChild(1, new byte[] { 0xAA }); + var child = CreateMiniChild(1, [0xAA]); var info = new ChunkMetadata(0x30, (uint)child.Size); - var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); + var chunk = new MiniNodeChunk(info, [child]); Assert.Equal(8 + child.Size, chunk.Size); } + [Fact] + public void GetBytes_WritesExactByteSequence() + { + // child: type=0x01, data=[0xAA] => MiniChunk header [01 01] + [AA] = 3 bytes + var child = CreateMiniChild(1, [0xAA]); + // MiniNodeChunk header: type=0x30, RawSize=3 + var info = new ChunkMetadata(0x30, (uint)child.Size); + var chunk = new MiniNodeChunk(info, [child]); + + var bytes = chunk.Bytes; + + byte[] expected = + [ + // MiniNodeChunk header (8 bytes): type=0x00000030, RawSize=0x00000003 + 0x30, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, + // child header: type=0x01, size=0x01 + 0x01, 0x01, + // child data + 0xAA + ]; + Assert.Equal(expected, bytes); + } + + [Fact] + public void GetBytes_EmptyChildren_WritesHeaderOnly() + { + var info = new ChunkMetadata(0x30, 0); + var chunk = new MiniNodeChunk(info, []); + + var bytes = chunk.Bytes; + + byte[] expected = + [ + // MiniNodeChunk header: type=0x00000030, RawSize=0x00000000 + 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ]; + Assert.Equal(expected, bytes); + } + [Fact] public void IsRootChunk() { - var child = CreateMiniChild(1, new byte[] { 0xAA }); + var child = CreateMiniChild(1, [0xAA]); var info = new ChunkMetadata(0x30, (uint)child.Size); - var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); - Assert.IsAssignableFrom(chunk); + var chunk = new MiniNodeChunk(info, [child]); + Assert.IsType(chunk, false); } [Fact] public void MultipleChildren() { - var c1 = CreateMiniChild(1, new byte[] { 0xAA }); - var c2 = CreateMiniChild(2, new byte[] { 0xBB, 0xCC }); + var c1 = CreateMiniChild(1, [0xAA]); + var c2 = CreateMiniChild(2, [0xBB, 0xCC]); var totalChildSize = c1.Size + c2.Size; var info = new ChunkMetadata(0x30, (uint)totalChildSize); - var chunk = new MiniNodeChunk(info, new MiniChunk[] { c1, c2 }); + var chunk = new MiniNodeChunk(info, [c1, c2]); Assert.Equal(2, chunk.Children.Count); Assert.Equal(8 + totalChildSize, chunk.Size); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs index 55bf0e77..d334b5d3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs @@ -15,9 +15,9 @@ private static RawChunk CreateChild(uint type, byte[] data) [Fact] public void Ctor_ValidArgs_SetsProperties() { - var child = CreateChild(1, new byte[] { 0xAA }); + var child = CreateChild(1, [0xAA]); var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); - var chunk = new NodeChunk(info, new RootChunk[] { child }); + var chunk = new NodeChunk(info, [child]); Assert.Equal(info, chunk.Info); Assert.Single(chunk.Children); @@ -27,9 +27,9 @@ public void Ctor_ValidArgs_SetsProperties() [Fact] public void Ctor_ThrowsWhenBit31NotSet() { - var child = CreateChild(1, new byte[] { 0xAA }); + var child = CreateChild(1, [0xAA]); var info = new ChunkMetadata(0x20, (uint)child.Size); - Assert.Throws(() => new NodeChunk(info, new RootChunk[] { child })); + Assert.Throws(() => new NodeChunk(info, [child])); } [Fact] @@ -40,53 +40,81 @@ public void Ctor_ThrowsOnNullChildren() } [Fact] - public void Ctor_ThrowsOnEmptyChildren() + public void Ctor_AllowsEmptyChildren() { var info = new ChunkMetadata(0x20, 0x8000_0000u); - Assert.Throws(() => new NodeChunk(info, Array.Empty())); + var chunk = new NodeChunk(info, []); + Assert.Empty(chunk.Children); } [Fact] public void Ctor_ThrowsWhenSizeMismatch() { - var child = CreateChild(1, new byte[] { 0xAA }); + var child = CreateChild(1, [0xAA]); var info = new ChunkMetadata(0x20, 0x8000_0000u | 999u); - Assert.Throws(() => new NodeChunk(info, new RootChunk[] { child })); + Assert.Throws(() => new NodeChunk(info, [child])); } [Fact] public void Size_IncludesHeaderAndChildren() { - var child = CreateChild(1, new byte[] { 0xAA }); + var child = CreateChild(1, [0xAA]); var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); - var chunk = new NodeChunk(info, new RootChunk[] { child }); + var chunk = new NodeChunk(info, [child]); Assert.Equal(8 + child.Size, chunk.Size); } [Fact] - public void GetBytes_WritesHeaderAndChildBytes() + public void GetBytes_WritesExactByteSequence() { - var child = CreateChild(1, new byte[] { 0xFF }); + // child: type=0x01, data=[0xFF] => RawChunk header [01 00 00 00 01 00 00 00] + [FF] = 9 bytes + var child = CreateChild(1, [0xFF]); + // NodeChunk header: type=0x20, RawSize = 0x8000_0000 | 9 = 0x80000009 var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); - var chunk = new NodeChunk(info, new RootChunk[] { child }); + var chunk = new NodeChunk(info, [child]); var bytes = chunk.Bytes; - Assert.Equal(chunk.Size, bytes.Length); - var childBytes = child.Bytes; - for (var i = 0; i < childBytes.Length; i++) - Assert.Equal(childBytes[i], bytes[8 + i]); + byte[] expected = + [ + // NodeChunk header (8 bytes): type=0x00000020, RawSize=0x80000009 + 0x20, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x80, + // child header: type=0x00000001, RawSize=0x00000001 + 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + // child data + 0xFF + ]; + Assert.Equal(expected, bytes); + } + + [Fact] + public void GetBytes_EmptyChildren_WritesHeaderOnly() + { + var info = new ChunkMetadata(0x20, 0x8000_0000u); + var chunk = new NodeChunk(info, []); + + var bytes = chunk.Bytes; + + byte[] expected = + [ + // NodeChunk header: type=0x00000020, RawSize=0x80000000 + 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x80 + ]; + Assert.Equal(expected, bytes); } [Fact] public void MultipleChildren() { - var c1 = CreateChild(1, new byte[] { 0xAA }); - var c2 = CreateChild(2, new byte[] { 0xBB, 0xCC }); + var c1 = CreateChild(1, [0xAA]); + var c2 = CreateChild(2, [0xBB, 0xCC]); var totalChildSize = c1.Size + c2.Size; var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)totalChildSize); - var chunk = new NodeChunk(info, new RootChunk[] { c1, c2 }); + var chunk = new NodeChunk(info, [c1, c2]); Assert.Equal(2, chunk.Children.Count); Assert.Equal(8 + totalChildSize, chunk.Size); @@ -95,9 +123,9 @@ public void MultipleChildren() [Fact] public void IsRootChunk() { - var child = CreateChild(1, new byte[] { 0xAA }); + var child = CreateChild(1, [0xAA]); var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); - var chunk = new NodeChunk(info, new RootChunk[] { child }); - Assert.IsAssignableFrom(chunk); + var chunk = new NodeChunk(info, [child]); + Assert.IsType(chunk, false); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs index 8ef2edda..806bda13 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs @@ -28,10 +28,11 @@ public void Ctor_AllowsBit31Set() } [Fact] - public void Ctor_ThrowsOnEmptyData() + public void Ctor_AllowsEmptyData() { var info = new ChunkMetadata(0x10, 0); - Assert.Throws(() => new RawChunk(info, ReadOnlyMemory.Empty)); + var chunk = new RawChunk(info, ReadOnlyMemory.Empty); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -51,6 +52,28 @@ public void Size_IncludesHeaderAndData() Assert.Equal(8 + 3, chunk.Size); } + [Fact] + public void GetBytes_EmptyData_WritesHeaderOnly() + { + var info = new ChunkMetadata(0x03, 0); + var chunk = new RawChunk(info, ReadOnlyMemory.Empty); + + var bytes = chunk.Bytes; + Assert.Equal(8, bytes.Length); + + // Type (LE) + Assert.Equal(0x03, bytes[0]); + Assert.Equal(0x00, bytes[1]); + Assert.Equal(0x00, bytes[2]); + Assert.Equal(0x00, bytes[3]); + + // Size = 0 (LE) + Assert.Equal(0x00, bytes[4]); + Assert.Equal(0x00, bytes[5]); + Assert.Equal(0x00, bytes[6]); + Assert.Equal(0x00, bytes[7]); + } + [Fact] public void GetBytes_WritesRawSizeIncludingBit31() { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/RawChunkEquivalenceTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/RawChunkEquivalenceTest.cs new file mode 100644 index 00000000..62d939b8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/RawChunkEquivalenceTest.cs @@ -0,0 +1,173 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; + +/// +/// Systematically verifies that every structured chunk type produces the exact same binary +/// as a constructed +/// from the equivalent header and body bytes. +/// +public class RawChunkEquivalenceTest +{ + #region DataChunk + + [Fact] + public void DataChunk_WithData_MatchesRaw() + { + var structured = ChunkFactory.Data(0x01, [0xAA, 0xBB, 0xCC]); + var raw = ChunkFactory.Raw(0x01, 3, [0xAA, 0xBB, 0xCC]); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void DataChunk_SingleByte_MatchesRaw() + { + var structured = ChunkFactory.Data(0xFF, [0x42]); + var raw = ChunkFactory.Raw(0xFF, 1, [0x42]); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void DataChunk_EmptyData_MatchesRaw() + { + var structured = ChunkFactory.Data(0x07, []); + var raw = ChunkFactory.Raw(0x07, 0, []); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + #endregion + + #region MiniNodeChunk + + [Fact] + public void MiniNodeChunk_WithChildren_MatchesRaw() + { + var structured = ChunkFactory.Node(0x30u, + ChunkFactory.Mini(0x04, [0x44, 0x55]), + ChunkFactory.Mini(0x05, [0x66])); + + // Body = mini0.Bytes + mini1.Bytes = [0x04,0x02,0x44,0x55] + [0x05,0x01,0x66] + var body = new byte[] { 0x04, 0x02, 0x44, 0x55, 0x05, 0x01, 0x66 }; + var raw = ChunkFactory.Raw(0x30, (uint)body.Length, body); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void MiniNodeChunk_SingleChild_MatchesRaw() + { + var structured = ChunkFactory.Node(0x31u, ChunkFactory.Mini(0x08, [])); + + var body = new byte[] { 0x08, 0x00 }; + var raw = ChunkFactory.Raw(0x31, (uint)body.Length, body); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void MiniNodeChunk_EmptyChildren_MatchesRaw() + { + var structured = ChunkFactory.Node(0x32u, (MiniChunk[])[]); + var raw = ChunkFactory.Raw(0x32, 0u, []); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + #endregion + + #region NodeChunk + + [Fact] + public void NodeChunk_WithDataChildren_MatchesRaw() + { + var structured = ChunkFactory.Node(0x20u, + ChunkFactory.Data(0x03, [0x33])); + + // Body = DataChunk(0x03, [0x33]).Bytes = 8-byte header + [0x33] + var childBytes = ChunkFactory.Data(0x03, [0x33]).Bytes; + var raw = ChunkFactory.Raw(0x20, 0x8000_0000u | (uint)childBytes.Length, childBytes); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void NodeChunk_WithMultipleDataChildren_MatchesRaw() + { + var child0 = ChunkFactory.Data(0x02, [0x11, 0x22]); + var child1 = ChunkFactory.Data(0x03, [0x33]); + var structured = ChunkFactory.Node(0x10u, child0, child1); + + var body = new byte[child0.Size + child1.Size]; + child0.GetBytes(body); + child1.GetBytes(((Span)body).Slice(child0.Size)); + var raw = ChunkFactory.Raw(0x10, 0x8000_0000u | (uint)body.Length, body); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void NodeChunk_EmptyChildren_MatchesRaw() + { + var structured = ChunkFactory.Node(0x11u, (RootChunk[])[]); + var raw = ChunkFactory.Raw(0x11, 0x8000_0000u, []); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void NodeChunk_WithNestedNodeChild_MatchesRaw() + { + var grandchild = ChunkFactory.Data(0x03, [0x33]); + var innerNode = ChunkFactory.Node(0x20u, grandchild); + var structured = ChunkFactory.Node(0x10u, innerNode); + + var innerBytes = innerNode.Bytes; + var raw = ChunkFactory.Raw(0x10, 0x8000_0000u | (uint)innerBytes.Length, innerBytes); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void NodeChunk_WithMiniNodeChild_MatchesRaw() + { + var miniNode = ChunkFactory.Node(0x30u, + ChunkFactory.Mini(0x04, [0x44, 0x55]), + ChunkFactory.Mini(0x05, [0x66])); + var structured = ChunkFactory.Node(0x10u, miniNode); + + var miniNodeBytes = miniNode.Bytes; + var raw = ChunkFactory.Raw(0x10, 0x8000_0000u | (uint)miniNodeBytes.Length, miniNodeBytes); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + #endregion + + #region ChunkFile + + [Fact] + public void ChunkFile_StructuredFile_MatchesExpectedBytes() + { + Assert.Equal(TestChunkFileData.ExpectedBytes.ToArray(), TestChunkFileData.StructuredFile.Bytes); + } + + [Fact] + public void ChunkFile_StructuredFile_HasExpectedSize() + { + Assert.Equal(TestChunkFileData.ExpectedBytes.Length, TestChunkFileData.StructuredFile.Size); + } + + [Fact] + public void ChunkFile_StructuredFile_HasExpectedRootCount() + { + Assert.Equal(7, TestChunkFileData.StructuredFile.RootChunks.Count); + } + + #endregion +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs index e671a595..0e2be661 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Text; +using PG.StarWarsGame.Files.Binary; +using PG.StarWarsGame.Files.ChunkFiles.Binary; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; using Xunit; @@ -50,10 +52,33 @@ public void ReadChunk_WithRefBytes_IncrementsBy8() [Fact] public void ReadChunk_ThrowsAtEndOfStream() { - using var reader = new ChunkReader(CreateStream(Array.Empty())); + using var reader = new ChunkReader(CreateStream([])); Assert.Throws(() => reader.ReadChunk()); } + [Fact] + public void ReadChunk_ThrowsOnPartialHeader() + { + // Only 4 bytes instead of the required 8 + using var reader = new ChunkReader(CreateStream([0x01, 0x00, 0x00, 0x00])); + Assert.Throws(() => reader.ReadChunk()); + } + + [Fact] + public void ReadMiniChunk_ThrowsAtEndOfStream() + { + using var reader = new ChunkReader(CreateStream([])); + Assert.Throws(() => { var rb = 0; reader.ReadMiniChunk(ref rb); }); + } + + [Fact] + public void ReadMiniChunk_ThrowsOnPartialHeader() + { + // Only 1 byte instead of the required 2 + using var reader = new ChunkReader(CreateStream([0x05])); + Assert.Throws(() => { var rb = 0; reader.ReadMiniChunk(ref rb); }); + } + [Fact] public void ReadMiniChunk_ReadsHeaderCorrectly() { @@ -157,10 +182,19 @@ public void ReadData_BySize_ReadsCorrectBytes() Assert.Equal(new byte[] { 0x01, 0x02, 0x03 }, result); } + [Fact] + public void ReadData_BySize_ReturnsAvailableBytes_WhenSizeExceedsStream() + { + var data = new byte[] { 0x01, 0x02 }; + using var reader = new ChunkReader(CreateStream(data)); + var result = reader.ReadData(10); + Assert.Equal(data, result); + } + [Fact] public void ReadData_BySize_ThrowsOnNegative() { - using var reader = new ChunkReader(CreateStream(new byte[] { 0x01 })); + using var reader = new ChunkReader(CreateStream([0x01])); Assert.Throws(() => reader.ReadData(-1)); } @@ -223,7 +257,7 @@ public void Skip_WithRefBytes_IncrementsCounter() [Fact] public void TryReadChunk_ReturnsNull_AtEndOfStream() { - using var reader = new ChunkReader(CreateStream(Array.Empty())); + using var reader = new ChunkReader(CreateStream([])); var result = reader.TryReadChunk(); Assert.Null(result); } @@ -255,7 +289,7 @@ public void TryReadChunk_WithRefBytes_IncrementsBy8() [Fact] public void TryReadChunk_WithRefBytes_DoesNotIncrement_AtEnd() { - using var reader = new ChunkReader(CreateStream(Array.Empty())); + using var reader = new ChunkReader(CreateStream([])); var readBytes = 5; var result = reader.TryReadChunk(ref readBytes); @@ -263,10 +297,12 @@ public void TryReadChunk_WithRefBytes_DoesNotIncrement_AtEnd() Assert.Equal(5, readBytes); } + #region ReadString + [Fact] public void ReadString_ReadsCorrectly() { - var text = "Hello"; + const string text = "Hello"; var encoded = Encoding.ASCII.GetBytes(text); using var reader = new ChunkReader(CreateStream(encoded)); @@ -279,7 +315,7 @@ public void ReadString_ReadsCorrectly() [Fact] public void ReadString_ZeroTerminated_TrimsAtNull() { - var encoded = new byte[] { (byte)'H', (byte)'i', 0, (byte)'X' }; + var encoded = "Hi\0X"u8.ToArray(); using var reader = new ChunkReader(CreateStream(encoded)); var result = reader.ReadString(encoded.Length, Encoding.ASCII, true); @@ -289,7 +325,7 @@ public void ReadString_ZeroTerminated_TrimsAtNull() [Fact] public void ReadString_WithoutRefBytes_Works() { - var text = "AB"; + const string text = "AB"; var encoded = Encoding.ASCII.GetBytes(text); using var reader = new ChunkReader(CreateStream(encoded)); @@ -297,10 +333,39 @@ public void ReadString_WithoutRefBytes_Works() Assert.Equal(text, result); } + [Fact] + public void ReadString_ThrowsBinaryCorrupted_WhenSizeExceedsStream() + { + var encoded = "Hi"u8.ToArray(); + using var reader = new ChunkReader(CreateStream(encoded)); + Assert.Throws(() => reader.ReadString(10, Encoding.ASCII, false)); + } + + [Fact] + public void ReadString_ZeroTerminated_ThrowsBinaryCorrupted_WhenNoNullTerminator() + { + // No null terminator present — the reader cannot find the terminator and throws + var encoded = "Hello"u8.ToArray(); + using var reader = new ChunkReader(CreateStream(encoded)); + + Assert.Throws(() => + reader.ReadString(encoded.Length, Encoding.ASCII, zeroTerminated: true)); + } + + [Fact] + public void ReadString_ZeroTerminated_ThrowsBinaryCorrupted_WhenSizeExceedsStream() + { + var encoded = "Hi"u8.ToArray(); + using var reader = new ChunkReader(CreateStream(encoded)); + Assert.Throws(() => reader.ReadString(10, Encoding.ASCII, zeroTerminated: true)); + } + + #endregion + [Fact] public void Dispose_DisposesUnderlyingStream() { - var ms = CreateStream(new byte[] { 1, 2, 3 }); + var ms = CreateStream([1, 2, 3]); var reader = new ChunkReader(ms); reader.Dispose(); @@ -310,7 +375,7 @@ public void Dispose_DisposesUnderlyingStream() [Fact] public void Dispose_LeaveOpen_KeepsStreamOpen() { - var ms = CreateStream(new byte[] { 1, 2, 3 }); + var ms = CreateStream([1, 2, 3]); var reader = new ChunkReader(ms, leaveOpen: true); reader.Dispose(); @@ -322,7 +387,7 @@ public void Dispose_LeaveOpen_KeepsStreamOpen() [Fact] public void ReadChunk_Roundtrip_WithChunkFactory() { - var original = PG.StarWarsGame.Files.ChunkFiles.Binary.ChunkFactory.Data(0x42, new byte[] { 1, 2, 3 }); + var original = ChunkFactory.Data(0x42, [1, 2, 3]); var bytes = original.Bytes; using var reader = new ChunkReader(CreateStream(bytes)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs index 5dd8627a..972e9417 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs @@ -17,87 +17,73 @@ namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; /// Mini[0]: MiniChunk(type=0x04, data=[0x44, 0x55]) /// Mini[1]: MiniChunk(type=0x05, data=[0x66]) /// Root[2]: DataChunk(type=0x06, data=[0x77, 0x88, 0x99, 0xDD]) +/// Root[3]: DataChunk(type=0x07, data=[]) -- empty data chunk +/// Root[4]: NodeChunk(type=0x11, children=[]) -- empty node chunk +/// Root[5]: MiniNodeChunk(type=0x31) -- mini-node chunk with one empty mini-chunk +/// Mini[0]: MiniChunk(type=0x08, data=[]) -- empty mini-chunk +/// Root[6]: MiniNodeChunk(type=0x32, children=[]) -- empty mini-node chunk /// /// public static class TestChunkFileData { /// - /// Gets a complex chunk file built using structured chunk types (NodeChunk, MiniNodeChunk, and DataChunk leaves). + /// Gets a complex chunk file built using structured chunk types (NodeChunk, MiniNodeChunk, and DataChunk leaves), + /// including chunks with empty data, a MiniNodeChunk with one empty MiniChunk, and an empty MiniNodeChunk to exercise those allowed edge cases. /// public static ChunkFile StructuredFile { get; } = BuildStructuredFile(); /// - /// Gets a chunk file built entirely from RawChunk instances that produce the same binary as . + /// Gets the expected binary representation of the test chunk file, + /// matching the binary output of . /// - public static ChunkFile RawFile { get; } = BuildRawFile(); - - /// - /// Gets the expected binary representation of the test chunk file. - /// - public static byte[] ExpectedBytes { get; } = StructuredFile.Bytes; + public static ReadOnlyMemory ExpectedBytes { get; } = new byte[] + { + // Root[0]: DataChunk(type=0x01, data=[0xAA, 0xBB, 0xCC]) + 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC, + // Root[1]: NodeChunk(type=0x10, bodySize=0x2A | bit31) + 0x10, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x80, + // Child[0]: DataChunk(type=0x02, data=[0x11, 0x22]) + 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x11, 0x22, + // Child[1]: NodeChunk(type=0x20, bodySize=0x09 | bit31) + 0x20, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x80, + // Grandchild: DataChunk(type=0x03, data=[0x33]) + 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x33, + // Child[2]: MiniNodeChunk(type=0x30, bodySize=7) + 0x30, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + // Mini[0]: MiniChunk(type=0x04, data=[0x44, 0x55]) + 0x04, 0x02, 0x44, 0x55, + // Mini[1]: MiniChunk(type=0x05, data=[0x66]) + 0x05, 0x01, 0x66, + // Root[2]: DataChunk(type=0x06, data=[0x77, 0x88, 0x99, 0xDD]) + 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x77, 0x88, 0x99, 0xDD, + // Root[3]: DataChunk(type=0x07, data=[]) + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Root[4]: NodeChunk(type=0x11, children=[]) + 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + // Root[5]: MiniNodeChunk(type=0x31) with Mini[0]: MiniChunk(type=0x08, data=[]) + 0x31, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x08, 0x00, + // Root[6]: MiniNodeChunk(type=0x32, children=[]) + 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; private static ChunkFile BuildStructuredFile() { - // Root[0]: leaf data chunk - var root0 = ChunkFactory.Data(0x01, [0xAA, 0xBB, 0xCC]); - - // Build children for Root[1] NodeChunk - var child0 = ChunkFactory.Data(0x02, [0x11, 0x22]); - - var grandchild = ChunkFactory.Data(0x03, [0x33]); - var child1 = ChunkFactory.Node(0x20u, grandchild); - - // MiniNodeChunk as a child - serialize mini-chunks into raw body - var miniChild0 = ChunkFactory.Mini(0x04, [0x44, 0x55]); - var miniChild1 = ChunkFactory.Mini(0x05, [0x66]); - var child2 = ChunkFactory.Node(0x30u, miniChild0, miniChild1); - - var root1 = ChunkFactory.Node(0x10u, child0, child1, child2); - - // Root[2]: leaf data chunk - var root2 = ChunkFactory.Data(0x06, [0x77, 0x88, 0x99, 0xDD]); - - return ChunkFactory.File(root0, root1, root2); + return ChunkFactory.File( + ChunkFactory.Data(0x01, [0xAA, 0xBB, 0xCC]), + ChunkFactory.Node(0x10u, + ChunkFactory.Data(0x02, [0x11, 0x22]), + ChunkFactory.Node(0x20u, + ChunkFactory.Data(0x03, [0x33])), + ChunkFactory.Node(0x30u, + ChunkFactory.Mini(0x04, [0x44, 0x55]), + ChunkFactory.Mini(0x05, [0x66]))), + ChunkFactory.Data(0x06, [0x77, 0x88, 0x99, 0xDD]), + ChunkFactory.Data(0x07, []), + ChunkFactory.Node(0x11u, (RootChunk[])[]), + ChunkFactory.Node(0x31u, ChunkFactory.Mini(0x08, [])), + ChunkFactory.Node(0x32u, (MiniChunk[])[]) + ); } - private static ChunkFile BuildRawFile() - { - // Build the exact same binary using only RawChunk instances. - - // Root[0] - var root0 = ChunkFactory.Raw(0x01, 3, [0xAA, 0xBB, 0xCC]); - - // child0: RawChunk(type=0x02, size=2, data=[0x11, 0x22]) -> 10 bytes total - var rawChild0 = ChunkFactory.Raw(0x02, 2, [0x11, 0x22]); - - // grandchild: RawChunk(type=0x03, size=1, data=[0x33]) -> 9 bytes total - // child1: NodeChunk(type=0x20) wrapping grandchild -> body = grandchild.Bytes (9 bytes) - var grandchildBytes = ChunkFactory.Raw(0x03, 1, [0x33]).Bytes; - var rawChild1 = ChunkFactory.Raw(0x20, 0x8000_0000u | (uint)grandchildBytes.Length, grandchildBytes); - - // child2: MiniNodeChunk(type=0x30) with 2 mini-chunks - // Mini(0x04,[0x44,0x55]) = 4 bytes, Mini(0x05,[0x66]) = 3 bytes -> body = 7 bytes - var mc0 = ChunkFactory.Mini(0x04, [0x44, 0x55]); - var mc1 = ChunkFactory.Mini(0x05, [0x66]); - var miniBody = new byte[mc0.Size + mc1.Size]; - mc0.GetBytes(miniBody); - mc1.GetBytes(((Span)miniBody).Slice(mc0.Size)); - var rawChild2 = ChunkFactory.Raw(0x30, (uint)miniBody.Length, miniBody); - - // Root[1]: NodeChunk(type=0x10) body = rawChild0 + rawChild1 + rawChild2 - var root1BodySize = rawChild0.Size + rawChild1.Size + rawChild2.Size; - var root1Body = new byte[root1BodySize]; - var offset = 0; - rawChild0.GetBytes(root1Body); - offset += rawChild0.Size; - rawChild1.GetBytes(((Span)root1Body).Slice(offset)); - offset += rawChild1.Size; - rawChild2.GetBytes(((Span)root1Body).Slice(offset)); - var root1 = ChunkFactory.Raw(0x10, 0x8000_0000u | (uint)root1BodySize, root1Body); - - // Root[2] - var root2 = ChunkFactory.Raw(0x06, 4, [0x77, 0x88, 0x99, 0xDD]); - - return ChunkFactory.File(root0, root1, root2); - } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj index cb0168a3..e1181964 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj @@ -1,8 +1,8 @@  - net8.0;net10.0 $(TargetFrameworks);net481 + true false @@ -31,7 +31,7 @@ - + - + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs index 852ebb8c..d9346444 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs @@ -24,24 +24,11 @@ public sealed class ChunkFile : IBinaryFile /// /// Initializes a new instance of the class. /// - /// - /// The root-level chunks. Must contain at least one element. - /// - /// - /// is . - /// - /// - /// is empty. - /// + /// The root-level chunks. + /// is . public ChunkFile(IReadOnlyList rootChunks) { - if (rootChunks == null) - throw new ArgumentNullException(nameof(rootChunks)); - - if (rootChunks.Count == 0) - throw new ArgumentException("ChunkFile must have at least one root chunk.", nameof(rootChunks)); - - RootChunks = rootChunks; + RootChunks = rootChunks ?? throw new ArgumentNullException(nameof(rootChunks)); } /// @@ -77,14 +64,11 @@ public void GetBytes(Span bytes) /// Writes the file's binary representation to a stream. /// /// The destination stream. - /// - /// is . - /// + /// is . public void WriteTo(Stream stream) { if (stream == null) throw new ArgumentNullException(nameof(stream)); - stream.Write(Bytes, 0, Size); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs index f4d03188..89afff91 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs @@ -28,15 +28,11 @@ public sealed class DataChunk : RootChunk /// The chunk metadata. Must not have bit 31 set. /// The binary data payload. /// - /// is empty, or /// has bit 31 set, or /// body size does not match the data length. /// public DataChunk(ChunkMetadata info, ReadOnlyMemory data) { - if (data is { IsEmpty: true, Length: 0 }) - throw new ArgumentException("Data cannot be empty.", nameof(data)); - if (info.HasChildrenHint) throw new ArgumentException( "DataChunk metadata must not have bit 31 set.", nameof(info)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs index de72659c..cde78a5f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs @@ -36,15 +36,11 @@ public sealed class MiniChunk : Chunk /// The mini-chunk metadata. /// The binary data payload. Maximum length is 255 bytes. /// - /// is empty, or /// size does not match the data length. /// public MiniChunk(MiniChunkMetadata info, ReadOnlyMemory data) { - if (data is { IsEmpty: true, Length: 0 }) - throw new ArgumentException("Data cannot be empty.", nameof(data)); - - if (info.BodySize!= data.Length) + if (info.BodySize != data.Length) throw new ArgumentException( $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", nameof(info)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs index 0db79f73..8e5b50f5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs @@ -19,7 +19,7 @@ public sealed class MiniNodeChunk : NodeChunkBase /// Initializes a new instance of the class. /// /// The chunk metadata. Must not have bit 31 set. - /// The mini-chunk children. Must contain at least one element. + /// The mini-chunk children. /// Chunks larger than 2GB are not supported. public MiniNodeChunk(ChunkMetadata info, IReadOnlyList children) : base(info, children) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs index 05a17e6d..eabc9e18 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs @@ -16,7 +16,7 @@ public sealed class NodeChunk : NodeChunkBase /// Initializes a new instance of the class. /// /// The chunk metadata. Must have bit 31 set. - /// The child chunks. Must contain at least one element. + /// The child chunks. /// does not have bit 31 set. public NodeChunk(ChunkMetadata info, IReadOnlyList children) : base(info, children) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs index 3d900e8e..e654025b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs @@ -29,10 +29,9 @@ public abstract class NodeChunkBase : RootChunk where TChild : Chunk /// Initializes a new instance of the class. /// /// The chunk metadata. - /// The child chunks. Must contain at least one element. + /// The child chunks. /// is . /// - /// is empty, or /// body size does not match the sum of children sizes. /// protected NodeChunkBase(ChunkMetadata info, IReadOnlyList children) @@ -40,10 +39,6 @@ protected NodeChunkBase(ChunkMetadata info, IReadOnlyList children) if (children == null) throw new ArgumentNullException(nameof(children)); - if (children.Count == 0) - throw new ArgumentException( - $"{GetType().Name} must have at least one child.", nameof(children)); - var actualSize = children.Sum(c => c.Size); if (info.BodySize != actualSize) throw new ArgumentException( diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs index 22940ba6..e4ec33fc 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs @@ -45,14 +45,10 @@ public sealed class RawChunk : RootChunk /// The chunk metadata, written as-is. No validation is performed on bit 31. /// The raw body data. /// - /// is empty, or /// body size does not match the data length. /// public RawChunk(ChunkMetadata info, ReadOnlyMemory data) { - if (data is { IsEmpty: true, Length: 0 }) - throw new ArgumentException("Data cannot be empty.", nameof(data)); - if (info.BodySize != data.Length) throw new ArgumentException( $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj index 04f7ff7c..acb477d9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj @@ -3,6 +3,7 @@ net8.0;net10.0 $(TargetFrameworks);net481 + true false From a020d6065dd0f38ccbdaba02843f80eb5dfaa55d Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 3 Apr 2026 10:50:51 +0200 Subject: [PATCH 07/17] add FileName property to TestChunkFileReader and corresponding tests --- .../Binary/Reader/TestChunkFileReader.cs | 2 ++ .../Binary/Reader/TestChunkFileReaderTest.cs | 23 +++++++++++++++++++ .../Binary/Model/Metadata/ChunkMetadata.cs | 1 - .../Binary/Reader/ChunkFileReaderBase.cs | 4 ++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs index c0ffa408..38f9465a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs @@ -7,6 +7,8 @@ namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader; public sealed class TestChunkFileReader(Stream stream, bool leaveStreamOpen = false) : ChunkFileReaderBase(stream, leaveStreamOpen) { + public new string? FileName => base.FileName; + public override TestChunkFileReaderTest.TestChunkData Read() { var meta = ChunkReader.ReadChunk(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs index 0f4ebecc..0c957c73 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs @@ -42,4 +42,27 @@ protected override void CallThrowIfChunkSizeTooLarge(TestChunkFileReader reader, { reader.CallThrowIfChunkSizeTooLarge(chunk); } + + [Fact] + public void FileName_IsNullForMemoryStream() + { + using var reader = CreateReader(new MemoryStream()); + Assert.Null(reader.FileName); + } + + [Fact] + public void FileName_IsPopulatedFromFileStream() + { + var tempFile = Path.GetTempFileName(); + try + { + using var fs = File.OpenRead(tempFile); + using var reader = CreateReader(fs); + Assert.Equal(tempFile, reader.FileName); + } + finally + { + File.Delete(tempFile); + } + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs index 6dbb9a96..a415e595 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Runtime.InteropServices; namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs index 5a98ea03..f001d112 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs @@ -1,6 +1,7 @@ using System; using System.IO; using AnakinRaW.CommonUtilities; +using PG.Commons.Utilities; using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; using PG.StarWarsGame.Files.ChunkFiles.Data; @@ -27,6 +28,9 @@ public abstract class ChunkFileReaderBase(Stream stream, bool leaveStreamOpen /// protected readonly ChunkReader ChunkReader = new(stream, leaveStreamOpen); + /// + public string? FileName { get; } = stream.TryGetFilePath(); + /// public abstract T Read(); From 338f165aa5eb115819c3f60f918fa9104a51cf64 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 3 Apr 2026 10:52:14 +0200 Subject: [PATCH 08/17] start implementing TED reader --- .../Binary/Reader/TedFileReader.cs | 163 +++++++++++++++++- .../Binary/Reader/TedFileReaderFactory.cs | 2 +- .../Binary/TedChunkType.cs | 27 +++ .../Data/IMapData.cs | 135 ++++++++++++++- 4 files changed, 320 insertions(+), 7 deletions(-) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/TedChunkType.cs diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs index 0556a80b..42e1a936 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs @@ -1,14 +1,169 @@ -using System; -using System.IO; +using PG.StarWarsGame.Files.Binary; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; using PG.StarWarsGame.Files.TED.Data; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Numerics; namespace PG.StarWarsGame.Files.TED.Binary.Reader; -internal sealed class TedFileReader(Stream stream) : ChunkFileReaderBase(stream), ITedFileReader +internal sealed class TedFileReader(TedLoadOptions loadOptions, Stream stream) + : ChunkFileReaderBase(stream), ITedFileReader { + protected TedLoadOptions LoadOptions { get; } = loadOptions; + public override IMapData Read() { - throw new NotImplementedException(); + ChunkMetadata? chunk; + MapInfo? mapInfo = null; + + byte[]? previewImageData = null; + int? previewImageDataSize = null; + List startPositionMarkers = []; + + var chunkCount = 0; + while ((chunk = ChunkReader.TryReadChunk()) is not null) + { + if (chunkCount++ == 0 && chunk.Value.Type != (uint)TedChunkType.MapInfo) + throw new BinaryCorruptedException($"The first chunk of a TED file must be of type {TedChunkType.MapInfo}."); + + switch ((TedChunkType)chunk.Value.Type) + { + case TedChunkType.MapInfo: + mapInfo = ReadMapInformation(chunk.Value); + break; + case TedChunkType.StartPositions: + ReadStartPositionMarkers(chunk.Value, startPositionMarkers); + break; + case TedChunkType.MapPreviewImageData: + ReadMapPreviewImageData(chunk.Value, out previewImageDataSize, out previewImageData); + break; + default: + throw new BinaryCorruptedException($"Unknown chunk type: {chunk.Value.Type}."); + } + } + + if (mapInfo is null) + throw new BinaryCorruptedException("The TED file does not have map information data."); + + mapInfo = mapInfo with + { + EmbeddedPreviewTextureFile = previewImageData, + EmbeddedPreviewTextureFileSize = previewImageDataSize, + StartPositionMarkers = startPositionMarkers + }; + + return new MapData(mapInfo); + } + + private void ReadStartPositionMarkers(ChunkMetadata chunk, List startPositionMarkers) + { + var readBytes = 0; + var positionsCount = (int)ChunkReader.ReadDword(ref readBytes); + + startPositionMarkers.Clear(); + + for (var i = 0; i < positionsCount; i++) + { + var x = ChunkReader.ReadFloat(ref readBytes); + var y = ChunkReader.ReadFloat(ref readBytes); + startPositionMarkers.Add(new Vector2(x, y)); + } + + if (readBytes != chunk.BodySize) + throw new BinaryCorruptedException( + $"Unable to read Start Position Markers chunk. Expected {chunk.BodySize} bytes, but read {readBytes} bytes."); } + + private void ReadMapPreviewImageData(ChunkMetadata chunk, out int? previewImageDataSize, out byte[]? previewImageData) + { + Debug.Assert(!chunk.HasChildrenHint); + + previewImageDataSize = chunk.BodySize; + + if (LoadOptions.HasFlag(TedLoadOptions.PreviewImageData)) + { + previewImageData = ChunkReader.ReadData(chunk); + return; + } + + ChunkReader.Skip(chunk.BodySize); + previewImageData = null; + } + + private MapInfo ReadMapInformation(ChunkMetadata chunk) + { + var readBytes = 0; + + var mapFileName = FileName ?? "[NO FILE NAME]"; + int version = 0; + var type = -1; + var playerCount = 1; + var mapLevels = 1; + var supportedWeather = 1; + var factionOwner = 0; + var mapType = 0; + var supportedSystems = 0; + var mapName = string.Empty; + var planetName = string.Empty; + var gameTypes = string.Empty; + var customMap = false; + var width = 0.0f; + var height = 0.0f; + var newMultiplayerMarkerSystem = false; + + MiniChunkMetadata? miniChunk; + while ((miniChunk = ChunkReader.ReadMiniChunk(ref readBytes)) is not null) + { + switch ((MapInfoChunkType)miniChunk.Value.Type) + { + case MapInfoChunkType.Version: + version = (int)ChunkReader.ReadDword(ref readBytes); + Debug.Assert(version == 513); // 01 02 00 00 (LE) + break; + } + } + + if (version == 0) + throw new BinaryCorruptedException("Map Information chunk is missing the version mini-chunk."); + + if (readBytes != chunk.BodySize) + throw new BinaryCorruptedException( + $"Unable to read Map Information chunk. Expected {chunk.BodySize} bytes, but read {readBytes} bytes."); + + if (string.IsNullOrEmpty(mapName)) + mapName = mapFileName; + + return new MapInfo + { + MapFileName = mapFileName, + Version = version, + Type = (MapMode)type, + MaxPlayers = playerCount, + Levels = mapLevels, + Terrain = (MapEnvironmentType)mapType, + Owner = (MapOwnerType)factionOwner, + MapName = mapName, + PlanetName = planetName, + GameTypes = gameTypes.ToUpper(), // The game actually uses the current locale + CustomMap = customMap, + MapSize = new Vector2(width, height), + NewMultiplayerMarkerSystem = newMultiplayerMarkerSystem + }; + } +} + + +[Flags] +public enum TedLoadOptions +{ + /// + /// Loads the entire file. + /// + Full = 0, + PreviewImageData = 1, + XRefs = 2 } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs index d9be9eb8..9bde79cb 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs @@ -7,6 +7,6 @@ internal class TedFileReaderFactory(IServiceProvider serviceProvider) : ITedFile { public ITedFileReader GetReader(Stream dataStream) { - return new TedFileReader(dataStream); + return new TedFileReader(TedLoadOptions.Full, dataStream); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/TedChunkType.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/TedChunkType.cs new file mode 100644 index 00000000..a8308562 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/TedChunkType.cs @@ -0,0 +1,27 @@ +namespace PG.StarWarsGame.Files.TED.Binary; + +internal enum TedChunkType : uint +{ + MapInfo = 0x0, + StartPositions = 0x3, + MapPreviewImageData = 0x13, +} + +internal enum MapInfoChunkType : uint +{ + Version = 0, + Type = 1, + PlayerCount = 2, + MapLevels = 3, + SupportedWeather = 4, + FactionOwner = 5, + MapType = 6, + SupportedSystems = 7, + MapName = 8, + PlanetName = 9, + GameTypes = 10, + CustomMap = 11, + Width = 16, + Height = 17, + NewMultiplayerMarkerSystem = 18, +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs index bd335930..1bafc19e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs @@ -1,8 +1,139 @@ -using AnakinRaW.CommonUtilities; +using System; +using System.Collections.Generic; +using System.Numerics; +using AnakinRaW.CommonUtilities; using PG.StarWarsGame.Files.ChunkFiles.Data; namespace PG.StarWarsGame.Files.TED.Data; -public class IMapData : DisposableObject, IChunkData +public interface IMapData : IChunkData { + public MapInfo MapInfo { get; } +} + +public class MapData : DisposableObject, IMapData +{ + public MapInfo MapInfo { get; } + + public MapData(MapInfo mapInfo) + { + MapInfo = mapInfo ?? throw new ArgumentNullException(nameof(mapInfo)); + } + + protected override void DisposeResources() + { + base.DisposeResources(); + MapInfo.Dispose(); + } +} + +public sealed record MapInfo : IDisposable +{ + private byte[]? _previewImage; + + public int Version { get; init; } + + public string MapFileName + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } = string.Empty; + + public string MapName + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } = string.Empty; + + public string DisplayName => $"({MaxPlayers}) {MapName}"; + + public string PlanetName + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } = string.Empty; + + public string GameTypes + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } = string.Empty; + + public MapMode Type { get; init; } + + public MapEnvironmentType Terrain { get; init; } + + public MapOwnerType Owner { get; init; } + + public int MaxPlayers { get; init; } + + public int Levels { get; init; } + + public bool CustomMap { get; init; } + + public bool NewMultiplayerMarkerSystem { get; init; } + + public Vector2 MapSize { get; init; } + + public IReadOnlyList StartPositionMarkers + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } = []; + + public byte[]? EmbeddedPreviewTextureFile + { + get + { + if (_previewImage is null) + return null; + return (byte[])_previewImage.Clone(); + } + init => _previewImage = value; + } + + public int? EmbeddedPreviewTextureFileSize { get; init; } + + // We use the presence of the file size to determine if a preview image exists, + // as the file data itself may not have been loaded. + public bool PreviewImageExists => EmbeddedPreviewTextureFileSize.HasValue; + + public void Dispose() + { + if (_previewImage is not null) + { + Array.Clear(_previewImage, 0, _previewImage.Length); + _previewImage = null; + } + } +} + + +// TODO: What happens if a map has 0 (Galactic) set??? +public enum MapMode +{ + Land = 1, + Space = 2 +} + +public enum MapEnvironmentType +{ + Temperate = 0x0, + Arctic = 0x1, + Desert = 0x2, + Forest = 0x3, + Swamp = 0x4, + Volcanic = 0x5, + Urban = 0x6, + Space = 0x7, +} + +public enum MapOwnerType +{ + Rebel = 0x0, + Empire = 0x1, + Pirate = 0x2, + Branched = 0x3, + Underworld = 0x4, + Hutts = 0x5, } \ No newline at end of file From f3a175796d86328ae736e3d0bc181e2f0e73e8ec Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Thu, 26 Mar 2026 18:16:15 +0100 Subject: [PATCH 09/17] ted impl skeleton --- ModVerify.slnx | 1 + .../Binary/Reader/ITedFileReader.cs | 6 +++ .../Binary/Reader/ITedFileReaderFactory.cs | 8 ++++ .../Binary/Reader/TedFileReader.cs | 13 ++++++ .../Binary/Reader/TedFileReaderFactory.cs | 12 ++++++ .../Data/IMapData.cs | 8 ++++ .../Files/ITedFile.cs | 6 +++ .../Files/TedFile.cs | 7 +++ .../Files/TedFileInformation.cs | 19 ++++++++ .../PG.StarWarsGame.Files.TED.csproj | 27 ++++++++++++ .../Services/ITedFileService.cs | 14 ++++++ .../Services/TedFileService.cs | 43 +++++++++++++++++++ .../TedServiceContribution.cs | 14 ++++++ 13 files changed, 178 insertions(+) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReader.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReaderFactory.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/ITedFile.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFile.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFileInformation.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/PG.StarWarsGame.Files.TED.csproj create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/ITedFileService.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/TedFileService.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/TedServiceContribution.cs diff --git a/ModVerify.slnx b/ModVerify.slnx index 3527ff4e..35980abe 100644 --- a/ModVerify.slnx +++ b/ModVerify.slnx @@ -20,6 +20,7 @@ + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReader.cs new file mode 100644 index 00000000..619e29cb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReader.cs @@ -0,0 +1,6 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +using PG.StarWarsGame.Files.TED.Data; + +namespace PG.StarWarsGame.Files.TED.Binary.Reader; + +internal interface ITedFileReader : IChunkFileReader; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReaderFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReaderFactory.cs new file mode 100644 index 00000000..a0cc0824 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/ITedFileReaderFactory.cs @@ -0,0 +1,8 @@ +using System.IO; + +namespace PG.StarWarsGame.Files.TED.Binary.Reader; + +internal interface ITedFileReaderFactory +{ + ITedFileReader GetReader(Stream dataStream); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs new file mode 100644 index 00000000..92e0c5f2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs @@ -0,0 +1,13 @@ +using System.IO; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +using PG.StarWarsGame.Files.TED.Data; + +namespace PG.StarWarsGame.Files.TED.Binary.Reader; + +internal sealed class TedFileReader(Stream stream) : ChunkFileReaderBase(stream), ITedFileReader +{ + public override IMapData Read() + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs new file mode 100644 index 00000000..d9be9eb8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs @@ -0,0 +1,12 @@ +using System; +using System.IO; + +namespace PG.StarWarsGame.Files.TED.Binary.Reader; + +internal class TedFileReaderFactory(IServiceProvider serviceProvider) : ITedFileReaderFactory +{ + public ITedFileReader GetReader(Stream dataStream) + { + return new TedFileReader(dataStream); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs new file mode 100644 index 00000000..bd335930 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs @@ -0,0 +1,8 @@ +using AnakinRaW.CommonUtilities; +using PG.StarWarsGame.Files.ChunkFiles.Data; + +namespace PG.StarWarsGame.Files.TED.Data; + +public class IMapData : DisposableObject, IChunkData +{ +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/ITedFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/ITedFile.cs new file mode 100644 index 00000000..76f610d9 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/ITedFile.cs @@ -0,0 +1,6 @@ +using PG.StarWarsGame.Files.ChunkFiles.Files; +using PG.StarWarsGame.Files.TED.Data; + +namespace PG.StarWarsGame.Files.TED.Files; + +public interface ITedFile : IChunkFile; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFile.cs new file mode 100644 index 00000000..5b075d66 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFile.cs @@ -0,0 +1,7 @@ +using System; +using PG.StarWarsGame.Files.TED.Data; + +namespace PG.StarWarsGame.Files.TED.Files; + +public sealed class TedFile(IMapData data, TedFileInformation fileInformation, IServiceProvider serviceProvider) + : PetroglyphFileHolder(data, fileInformation, serviceProvider), ITedFile; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFileInformation.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFileInformation.cs new file mode 100644 index 00000000..22a532e8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Files/TedFileInformation.cs @@ -0,0 +1,19 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace PG.StarWarsGame.Files.TED.Files; + +public sealed record TedFileInformation : PetroglyphMegPackableFileInformation +{ + /// + /// Initializes a new instance of the class. + /// + /// The file path of the alo file. + /// Information whether this file info is created from a meg data entry. + /// is null. + /// is empty. + [SetsRequiredMembers] + public TedFileInformation(string path, bool isInMeg) : base(path, isInMeg) + { + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/PG.StarWarsGame.Files.TED.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/PG.StarWarsGame.Files.TED.csproj new file mode 100644 index 00000000..0071f913 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/PG.StarWarsGame.Files.TED.csproj @@ -0,0 +1,27 @@ + + + netstandard2.0;netstandard2.1;net10.0 + PG.StarWarsGame.Files.TED + PG.StarWarsGame.Files.TED + AlamoEngineTools.PG.StarWarsGame.Files.TED + alamo,petroglyph,glyphx + + + + true + + + true + snupkg + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/ITedFileService.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/ITedFileService.cs new file mode 100644 index 00000000..72b96d40 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/ITedFileService.cs @@ -0,0 +1,14 @@ +using System.IO; +using System.IO.Abstractions; +using PG.StarWarsGame.Files.TED.Files; + +namespace PG.StarWarsGame.Files.TED.Services; + +public interface ITedFileService +{ + void RemoveMapPreview(Stream tedStream, FileSystemStream destination, bool extract, out byte[]? previewImageBytes); + + ITedFile Load(string path); + + ITedFile Load(Stream stream); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/TedFileService.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/TedFileService.cs new file mode 100644 index 00000000..9e97c4f8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Services/TedFileService.cs @@ -0,0 +1,43 @@ +using PG.Commons.Services; +using PG.Commons.Utilities; +using PG.StarWarsGame.Files.TED.Files; +using System; +using System.IO; +using System.IO.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.TED.Binary.Reader; + +namespace PG.StarWarsGame.Files.TED.Services; + +internal class TedFileService(IServiceProvider serviceProvider) : ServiceBase(serviceProvider), ITedFileService +{ + public void RemoveMapPreview(Stream tedStream, FileSystemStream destination, bool extract, out byte[]? previewImageBytes) + { + if (tedStream == null) + throw new ArgumentNullException(nameof(tedStream)); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + + throw new NotImplementedException(); + } + + public ITedFile Load(string path) + { + using var fileStream = FileSystem.FileStream.New(path, FileMode.Open, FileAccess.Read, FileShare.Read); + return Load(fileStream); + } + + public ITedFile Load(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + using var reader = Services.GetRequiredService().GetReader(stream); + var map = reader.Read(); + + var filePath = stream.GetFilePath(out var isInMeg); + var fileInfo = new TedFileInformation(filePath, isInMeg); + + return new TedFile(map, fileInfo, Services); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/TedServiceContribution.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/TedServiceContribution.cs new file mode 100644 index 00000000..d2ba314c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/TedServiceContribution.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.TED.Binary.Reader; +using PG.StarWarsGame.Files.TED.Services; + +namespace PG.StarWarsGame.Files.TED; + +public static class TedServiceContribution +{ + public static void SupportTED(this IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(sp => new TedFileReaderFactory(sp)); + serviceCollection.AddSingleton(sp => new TedFileService(sp)); + } +} \ No newline at end of file From c7170cf4722932909e83dc172c4f39312294e9c3 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 28 Mar 2026 10:13:44 +0100 Subject: [PATCH 10/17] start implementing TED support --- ModVerify.slnx | 1 + .../Identifier/AloContentInfoIdentifier.cs | 10 +- .../Reader/Animations/AnimationReaderBase.cs | 28 +-- .../Binary/Reader/Models/ModelFileReader.cs | 44 ++-- .../Reader/Particles/ParticleReaderV1.cs | 26 +-- .../Binary/Metadata/Chunk.cs | 189 ++++++++++++++++++ .../Binary/Metadata/ChunkFile.cs | 48 +++++ .../Binary/Metadata/ChunkMetadata.cs | 50 +++-- .../Binary/Reader/ChunkFileReaderBase.cs | 9 +- .../Binary/Reader/ChunkReader.cs | 38 +++- .../CommonDatTestBase.cs | 13 ++ .../PG.StarWarsGame.Files.TED.Test.csproj | 37 ++++ .../Services/TedFileServiceTest.cs | 52 +++++ .../Binary/Reader/TedFileReader.cs | 5 +- .../Binary/Writer/MapPreviewExtractor.cs | 57 ++++++ .../Properties/AssemblyAttributes.cs | 3 + .../Services/ITedFileService.cs | 4 +- .../Services/TedFileService.cs | 14 +- 18 files changed, 544 insertions(+), 84 deletions(-) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/CommonDatTestBase.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/Services/TedFileServiceTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Writer/MapPreviewExtractor.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Properties/AssemblyAttributes.cs diff --git a/ModVerify.slnx b/ModVerify.slnx index 35980abe..dd535739 100644 --- a/ModVerify.slnx +++ b/ModVerify.slnx @@ -20,6 +20,7 @@ + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs index 6fce5c07..3024efd7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs @@ -19,7 +19,7 @@ public AloContentInfo GetContentInfo(Stream stream) case ChunkType.Skeleton: case ChunkType.Mesh: case ChunkType.Light: - return FromModel(chunk.Size, chunkReader); + return FromModel(chunk.BodySize, chunkReader); case ChunkType.Connections: return FromConnection(chunkReader); case ChunkType.Particle: @@ -41,14 +41,14 @@ private static AloContentInfo FromAnimation(ChunkReader chunkReader) switch ((ChunkType)chunk.Value.Type) { case ChunkType.AnimationInformation: - return chunk.Value.Size switch + return chunk.Value.BodySize switch { 36 => new AloContentInfo(AloType.Animation, AloVersion.V2), 18 => new AloContentInfo(AloType.Animation, AloVersion.V1), _ => throw new BinaryCorruptedException("Invalid ALA animation.") }; default: - chunkReader.Skip(chunk.Value.Size); + chunkReader.Skip(chunk.Value.BodySize); break; } chunk = chunkReader.TryReadChunk(); @@ -66,7 +66,7 @@ private static AloContentInfo FromConnection(ChunkReader chunkReader) case ChunkType.ProxyConnection: case ChunkType.ObjectConnection: case ChunkType.ConnectionCounts: - chunkReader.Skip(chunk.Value.Size); + chunkReader.Skip(chunk.Value.BodySize); break; case ChunkType.Dazzle: return new AloContentInfo(AloType.Model, AloVersion.V2); @@ -92,7 +92,7 @@ private static AloContentInfo FromModel(int size, ChunkReader chunkReader) case ChunkType.Skeleton: case ChunkType.Mesh: case ChunkType.Light: - return FromModel(chunk.Value.Size, chunkReader); + return FromModel(chunk.Value.BodySize, chunkReader); default: throw new BinaryCorruptedException("Invalid ALO model."); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs index 632caf37..dbcfc4d1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs @@ -27,11 +27,11 @@ public sealed override AlamoAnimation Read() { var chunk = ChunkReader.ReadChunk(ref actualSize); ReadAnimation(chunk, ref info, bones); - actualSize += chunk.Size; + actualSize += chunk.BodySize; - } while (actualSize < rootChunk.Size); + } while (actualSize < rootChunk.BodySize); - if (actualSize != rootChunk.Size) + if (actualSize != rootChunk.BodySize) throw new BinaryCorruptedException(); if (info.NumberBones != bones.Count) @@ -54,13 +54,15 @@ protected virtual void ReadAnimation( switch (chunk.Type) { case (int)AnimationChunkTypes.AnimationInfo: - animationInformation = ReadAnimationInfo(chunk.Size); + if (chunk.RawSize < 0) + ThrowChunkSizeTooLargeException(); + animationInformation = ReadAnimationInfo(chunk.BodySize); break; case (int)AnimationChunkTypes.BoneData: - ReadBonesData(chunk.Size, bones); + ReadBonesData(chunk.BodySize, bones); break; default: - ChunkReader.Skip(chunk.Size); + ChunkReader.Skip(chunk.BodySize); break; } } @@ -70,10 +72,12 @@ protected virtual void ReadBoneDataCore(ChunkMetadata chunk, List bones) { var chunk = ChunkReader.ReadChunk(ref actualSize); ReadBoneDataCore(chunk, bones); - actualSize += chunk.Size; + actualSize += chunk.BodySize; } while (actualSize < chunkSize); @@ -108,13 +112,13 @@ private void ReadBoneInfo(int chunkSize, List bones) switch (chunk.Type) { case (int)AnimationChunkTypes.BoneName: - name = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true, ref actualSize); + name = ChunkReader.ReadString(chunk.BodySize, Encoding.ASCII, true, ref actualSize); break; case (int)AnimationChunkTypes.BoneIndex: index = ChunkReader.ReadDword(ref actualSize); break; default: - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); break; } @@ -156,7 +160,7 @@ private AnimationInformationData ReadAnimationInfo(int chunkSize) info.ScaleBlockSize = ChunkReader.ReadDword(ref actualSize); break; default: - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); break; } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Models/ModelFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Models/ModelFileReader.cs index 13da425c..6aee4b60 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Models/ModelFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Models/ModelFileReader.cs @@ -25,16 +25,16 @@ public override AlamoModel Read() switch (chunk.Value.Type) { case (int)ModelChunkTypes.Skeleton: - ReadSkeleton(chunk.Value.Size, bones); + ReadSkeleton(chunk.Value.BodySize, bones); break; case (int)ModelChunkTypes.Mesh: - ReadMesh(chunk.Value.Size, textures, shaders); + ReadMesh(chunk.Value.BodySize, textures, shaders); break; case (int)ModelChunkTypes.Connections: - ReadConnections(chunk.Value.Size, proxies); + ReadConnections(chunk.Value.BodySize, proxies); break; default: - ChunkReader.Skip(chunk.Value.Size); + ChunkReader.Skip(chunk.Value.BodySize); break; } @@ -59,14 +59,16 @@ private void ReadConnections(int size, HashSet proxies) do { var chunk = ChunkReader.ReadChunk(ref actualSize); + if (chunk.RawSize < 0) + ThrowChunkSizeTooLargeException(); if (chunk.Type == (int)ModelChunkTypes.ProxyConnection) { - ReadProxy(chunk.Size, out var proxy, ref actualSize); + ReadProxy(chunk.BodySize, out var proxy, ref actualSize); proxies.Add(proxy); } else - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); } while (actualSize < size); @@ -84,9 +86,9 @@ private void ReadProxy(int size, out string proxy, ref int readSize) var chunk = ChunkReader.ReadMiniChunk(ref actualSize); if (chunk.Type == 5) - proxy = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true, ref actualSize); + proxy = ChunkReader.ReadString(chunk.BodySize, Encoding.ASCII, true, ref actualSize); else - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); } while (actualSize < size); @@ -110,9 +112,9 @@ private void ReadMesh(int size, ISet textures, ISet shaders) var chunk = ChunkReader.ReadChunk(ref actualSize); if (chunk.Type == (int)ModelChunkTypes.SubMeshMaterialInformation) - ReadSubMeshMaterialInformation(chunk.Size, textures, shaders, ref actualSize); + ReadSubMeshMaterialInformation(chunk.BodySize, textures, shaders, ref actualSize); else - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); } while (actualSize < size); @@ -132,15 +134,17 @@ private void ReadSubMeshMaterialInformation(int size, ISet textures, ISe { case (int)ModelChunkTypes.ShaderFileName: { - var shader = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true, ref actualSize); + var shader = ChunkReader.ReadString(chunk.BodySize, Encoding.ASCII, true, ref actualSize); shaders.Add(shader); break; } case (int)ModelChunkTypes.ShaderTexture: - ReadShaderTexture(chunk.Size, textures, ref actualSize); + if (chunk.RawSize < 0) + ThrowChunkSizeTooLargeException(); + ReadShaderTexture(chunk.BodySize, textures, ref actualSize); break; default: - ChunkReader.Skip(chunk.Size, ref actualSize); + ChunkReader.Skip(chunk.BodySize, ref actualSize); break; } @@ -162,11 +166,11 @@ private void ReadShaderTexture(int size, ISet textures, ref int readSize if (mini.Type == 2) { - var texture = ChunkReader.ReadString(mini.Size, Encoding.ASCII, true, ref actualTextureChunkSize); + var texture = ChunkReader.ReadString(mini.BodySize, Encoding.ASCII, true, ref actualTextureChunkSize); textures.Add(texture); } else - ChunkReader.Skip(mini.Size, ref actualTextureChunkSize); + ChunkReader.Skip(mini.BodySize, ref actualTextureChunkSize); } while (actualTextureChunkSize != size); @@ -191,7 +195,7 @@ private void ReadSkeleton(int size, IList bones) var boneCountChunk = ChunkReader.ReadChunk(ref actualSize); - Debug.Assert(boneCountChunk is { Size: 128, Type: (int)ModelChunkTypes.BoneCount }); + Debug.Assert(boneCountChunk is { BodySize: 128, Type: (int)ModelChunkTypes.BoneCount }); var boneCount = ChunkReader.ReadDword(ref actualSize); @@ -201,24 +205,24 @@ private void ReadSkeleton(int size, IList bones) { var bone = ChunkReader.ReadChunk(ref actualSize); - Debug.Assert(bone is { Type: (int)ModelChunkTypes.Bone, IsContainer: true }); + Debug.Assert(bone is { Type: (int)ModelChunkTypes.Bone, HasChildrenHint: true }); var boneReadSize = 0; - while (boneReadSize < bone.Size) + while (boneReadSize < bone.BodySize) { var innerBoneChunk = ChunkReader.ReadChunk(ref boneReadSize); if (innerBoneChunk.Type == (int)ModelChunkTypes.BoneName) { - var nameSize = innerBoneChunk.Size; + var nameSize = innerBoneChunk.BodySize; var name = ChunkReader.ReadString(nameSize, Encoding.ASCII, true, ref boneReadSize); bones.Add(name); } else { - ChunkReader.Skip(innerBoneChunk.Size, ref boneReadSize); + ChunkReader.Skip(innerBoneChunk.BodySize, ref boneReadSize); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Particles/ParticleReaderV1.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Particles/ParticleReaderV1.cs index ed6de4b6..bbca3813 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Particles/ParticleReaderV1.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Particles/ParticleReaderV1.cs @@ -30,23 +30,23 @@ public override AlamoParticle Read() switch (chunk.Type) { case (int)ParticleChunkType.Name: - ReadName(chunk.Size, out name); + ReadName(chunk.BodySize, out name); break; case (int)ParticleChunkType.Emitters: - ReadEmitters(chunk.Size, textures); + ReadEmitters(chunk.BodySize, textures); break; default: - ChunkReader.Skip(chunk.Size); + ChunkReader.Skip(chunk.BodySize); break; } - actualSize += chunk.Size; + actualSize += chunk.BodySize; - } while (actualSize < rootChunk.Size); + } while (actualSize < rootChunk.BodySize); - if (actualSize != rootChunk.Size) + if (actualSize != rootChunk.BodySize) throw new BinaryCorruptedException(); if (string.IsNullOrEmpty(name)) @@ -70,9 +70,9 @@ private void ReadEmitters(int size, HashSet textures) if (chunk.Type != (int)ParticleChunkType.Emitter) throw new BinaryCorruptedException("Unable to read particle"); - ReadEmitter(chunk.Size, textures); + ReadEmitter(chunk.BodySize, textures); - actualSize += chunk.Size; + actualSize += chunk.BodySize; } while (actualSize < size); @@ -92,24 +92,24 @@ private void ReadEmitter(int chunkSize, HashSet textures) if (chunk.Type == (int)ParticleChunkType.Properties) { var shader = ChunkReader.ReadDword(); - ChunkReader.Skip(chunk.Size - sizeof(uint)); + ChunkReader.Skip(chunk.BodySize - sizeof(uint)); } else if (chunk.Type == (int)ParticleChunkType.ColorTextureName) { - var texture = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true); + var texture = ChunkReader.ReadString(chunk.BodySize, Encoding.ASCII, true); textures.Add(texture); } else if (chunk.Type == (int)ParticleChunkType.BumpTextureName) { - var bump = ChunkReader.ReadString(chunk.Size, Encoding.ASCII, true); + var bump = ChunkReader.ReadString(chunk.BodySize, Encoding.ASCII, true); textures.Add(bump); } else { - ChunkReader.Skip(chunk.Size); + ChunkReader.Skip(chunk.BodySize); } - actualSize += chunk.Size; + actualSize += chunk.BodySize; } while (actualSize < chunkSize); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs new file mode 100644 index 00000000..c708838d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs @@ -0,0 +1,189 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; +using PG.StarWarsGame.Files.Binary; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; + +public sealed class Chunk : IBinary +{ + public ChunkMetadata Info { get; } + + public byte[]? Data { get; } + + public IReadOnlyList Children { get; } + + public byte[] Bytes + { + get + { + var bytes = new byte[Size]; + GetBytes(bytes); + return bytes; + } + } + + public int Size => Info.IsMiniChunk + ? 2 + Data!.Length + : Data is not null + ? 8 + Data.Length + : 8 + Children.Sum(c => c.Size); + + public Chunk(ChunkMetadata info, byte[] data) + { + Info = info; + Data = data ?? throw new ArgumentNullException(nameof(data)); + Children = []; + } + + public Chunk(ChunkMetadata info, IReadOnlyList children) + { + if (info.IsMiniChunk) + throw new ArgumentException("MiniChunks cannot have child chunks", nameof(info)); + Info = info; + Data = null; + Children = children ?? throw new ArgumentNullException(nameof(children)); + } + + public void GetBytes(Span bytes) + { + if (Info.IsMiniChunk) + { + bytes[0] = (byte)Info.Type; + bytes[1] = (byte)Data!.Length; + Data.AsSpan().CopyTo(bytes[2..]); + return; + } + + BinaryPrimitives.WriteUInt32LittleEndian(bytes, Info.Type); + + if (Data is not null) + { + BinaryPrimitives.WriteInt32LittleEndian(bytes[4..], Data.Length); + Data.AsSpan().CopyTo(bytes[8..]); + } + else + { + // .Sum is a checked operation and will already throw an overflow exception + var bodySize = Children.Sum(c => c.Size); + var hasMiniChunkChildren = Children.Count > 0 && Children[0].Info.IsMiniChunk; + + var sizeField = hasMiniChunkChildren + ? bodySize + : (int)(bodySize | 0x8000_0000u); + + BinaryPrimitives.WriteInt32LittleEndian(bytes[4..], sizeField); + + var offset = 8; + foreach (var child in Children) + { + child.GetBytes(bytes[offset..]); + offset += child.Size; + } + } + } +} + +/// +/// Provides factory methods for creating chunks and chunk files. +/// +/// +/// +/// This class provides static methods for creating hierarchical chunk structures. +/// Three chunk types are supported: +/// +/// +/// Data chunks - Regular chunks containing binary data +/// Mini-chunks - Chunks with 2-byte headers for data up to 255 bytes +/// Node chunks - Container chunks that hold child chunks +/// +/// +public static class ChunkFactory +{ + /// + /// Creates a data chunk with the specified type and binary data. + /// + /// The chunk type identifier. + /// The binary data to store in the chunk. + /// A containing the specified data. + /// is . + public static Chunk Data(uint type, byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var metadata = new ChunkMetadata(type, (uint)data.Length, false); + return new Chunk(metadata, data); + } + + /// + /// Creates a mini-chunk with the specified type and binary data. + /// + /// The mini-chunk type identifier. + /// The binary data to store in the mini-chunk. Maximum length is 255 bytes. + /// A representing a mini-chunk with a 2-byte header. + /// is . + /// The length of exceeds 255 bytes. + public static Chunk Mini(byte type, byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + if (data.Length > byte.MaxValue) + throw new ArgumentException( + $"Mini-chunk data cannot exceed {byte.MaxValue} bytes. Provided data length: {data.Length}.", + nameof(data)); + + var metadata = new ChunkMetadata(type, (uint)data.Length, true); + return new Chunk(metadata, data); + } + + /// + /// Creates a chunk node that contains child chunks. + /// + /// The chunk type identifier. + /// The child chunks to include in this node. + /// A containing the specified children. + /// is . + public static Chunk Node(uint type, params Chunk[] children) + { + if (children == null) + throw new ArgumentNullException(nameof(children)); + + var size = (uint)children.Sum(c => c.Size); + var metadata = new ChunkMetadata(type, size, false); + return new Chunk(metadata, children); + } + + /// + /// Creates a chunk node that contains child chunks built using a configuration action. + /// + /// The chunk type identifier. + /// An action that populates a list with child chunks. + /// A containing the configured children. + /// is . + public static Chunk Node(uint type, Action> configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + var children = new List(); + configure(children); + return Node(type, children.ToArray()); + } + + /// + /// Creates a chunk file containing the specified root chunks. + /// + /// The top-level chunks to include in the file. + /// A containing the specified root chunks. + /// is . + public static ChunkFile File(params Chunk[] rootChunks) + { + if (rootChunks == null) + throw new ArgumentNullException(nameof(rootChunks)); + + return new ChunkFile(rootChunks); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs new file mode 100644 index 00000000..1c38ddbb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using PG.StarWarsGame.Files.Binary.File; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; + +public sealed class ChunkFile : IBinaryFile +{ + public IReadOnlyList RootChunks { get; } + + public int Size => RootChunks.Sum(c => c.Size); + + public byte[] Bytes + { + get + { + var bytes = new byte[Size]; + GetBytes(bytes); + return bytes; + } + } + + public ChunkFile(IReadOnlyList rootChunks) + { + if (rootChunks == null) + throw new ArgumentNullException(nameof(rootChunks)); + if (rootChunks.Count == 0) + throw new ArgumentOutOfRangeException(nameof(rootChunks), "A chunk file must contain at least one chunk"); + RootChunks = rootChunks; + } + + public void GetBytes(Span bytes) + { + var offset = 0; + foreach (var chunk in RootChunks) + { + chunk.GetBytes(bytes[offset..]); + offset += chunk.Size; + } + } + + public void WriteTo(Stream stream) + { + stream.Write(Bytes, 0, Size); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs index df47dc23..3a767498 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs @@ -1,29 +1,39 @@ -namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +using System; +using System.Diagnostics; +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; + +[DebuggerDisplay("Type: {Type}, Size: {BodySize}, Mini:{IsMiniChunk}")] public readonly struct ChunkMetadata { - public readonly int Type; - public readonly int Size; - - private ChunkMetadata(int type, int size, bool isContainer, bool isMiniChunk) - { - Type = type; - Size = size; - IsMiniChunk = isMiniChunk; - IsContainer = isContainer; - } + public readonly uint Type; + public readonly uint RawSize; + public readonly bool IsMiniChunk; - public bool IsContainer { get; } + /// + /// Indicates that bit 31 of RawSize is set. + /// This is a hint that the body contains child chunks, not a guarantee. + /// + public bool HasChildrenHint => !IsMiniChunk && (int)RawSize < 0; - public bool IsMiniChunk { get; } + /// + /// Gets the size of the chunk's data in bytes. + /// + /// + /// This value has bit 31 masked off compared to . + /// Per spec, bit 31 is set only for chunks containing regular child chunks. + /// Chunks containing mini-chunks (treated as data) do NOT set bit 31. + /// Since this library doesn't support sizes > , masking bit 31 + /// has no practical impact on the usable size range. + /// + public int BodySize => (int)(RawSize & 0x7FFF_FFFF); - public static ChunkMetadata FromContainer(int type, int size) + public ChunkMetadata(uint type, uint rawSize, bool isMiniChunk) { - return new ChunkMetadata(type, size, true, false); - } - - public static ChunkMetadata FromData(int type, int size, bool isMini = false) - { - return new ChunkMetadata(type, size, false, isMini); + if (isMiniChunk && rawSize > byte.MaxValue) + throw new ArgumentOutOfRangeException(nameof(rawSize), "Mini chunk size must fit in a byte (0-255)."); + Type = type; + RawSize = rawSize; + IsMiniChunk = isMiniChunk; } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs index 01d085ac..eb664447 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using AnakinRaW.CommonUtilities; using PG.StarWarsGame.Files.ChunkFiles.Data; @@ -20,4 +21,10 @@ protected override void DisposeResources() base.DisposeResources(); ChunkReader.Dispose(); } + + //[DoesNotReturn] + protected void ThrowChunkSizeTooLargeException() + { + throw new NotSupportedException("Chunk sizes larger than int.MaxValue are not supported."); + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs index 742924dd..024d6eee 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs @@ -20,13 +20,9 @@ public ChunkReader(Stream stream, bool leaveOpen = false) public ChunkMetadata ReadChunk() { - var type = _binaryReader.ReadInt32(); - var rawSize = _binaryReader.ReadInt32(); - - var isContainer = (rawSize & 0x80000000) != 0; - var size = rawSize & 0x7FFFFFFF; - - return isContainer ? ChunkMetadata.FromContainer(type, size) : ChunkMetadata.FromData(type, size); + var type = _binaryReader.ReadUInt32(); + var rawSize = _binaryReader.ReadUInt32(); + return new ChunkMetadata(type, rawSize, false); } public ChunkMetadata ReadChunk(ref int readBytes) @@ -43,9 +39,35 @@ public ChunkMetadata ReadMiniChunk(ref int readBytes) readBytes += 2; - return ChunkMetadata.FromData(type, size, true); + return new ChunkMetadata(type, size, true); + } + + public byte[] ReadData(ChunkMetadata chunk) + { + if (chunk.HasChildrenHint) + throw new InvalidOperationException("Unable to read data from container chunk."); + + return _binaryReader.ReadBytes(chunk.BodySize); + } + + public byte[] ReadData(int size) + { + return size < 0 ? + throw new ArgumentOutOfRangeException(nameof(size), "size cannot be negative") : + _binaryReader.ReadBytes(size); } + public byte[] ReadData(ChunkMetadata chunk, ref int readSize) + { + if (chunk.HasChildrenHint) + throw new InvalidOperationException("Unable to read data from container chunk."); + + var data = _binaryReader.ReadBytes(chunk.BodySize); + readSize += chunk.BodySize; + return data; + } + + public uint ReadDword(ref int readSize) { var value = _binaryReader.ReadUInt32(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/CommonDatTestBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/CommonDatTestBase.cs new file mode 100644 index 00000000..85589e41 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/CommonDatTestBase.cs @@ -0,0 +1,13 @@ +using AnakinRaW.CommonUtilities.Testing; +using Microsoft.Extensions.DependencyInjection; + +namespace PG.StarWarsGame.Files.TED.Test; + +public class CommonDatTestBase : TestBaseWithFileSystem +{ + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + serviceCollection.SupportTED(); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj new file mode 100644 index 00000000..04f7ff7c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj @@ -0,0 +1,37 @@ + + + + net8.0;net10.0 + $(TargetFrameworks);net481 + + + false + true + Exe + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/Services/TedFileServiceTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/Services/TedFileServiceTest.cs new file mode 100644 index 00000000..49871310 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/Services/TedFileServiceTest.cs @@ -0,0 +1,52 @@ +using System.IO; +using System.IO.Abstractions; +using AnakinRaW.CommonUtilities.Hashing; +using AnakinRaW.CommonUtilities.Testing; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons; +using PG.StarWarsGame.Files.TED.Services; +using Testably.Abstractions; +using Xunit; + +namespace PG.StarWarsGame.Files.TED.Test.Services; + +public class TedFileServiceTest : TestBaseWithFileSystem +{ + private readonly TedFileService _service; + + protected override IFileSystem CreateFileSystem() + { + return new RealFileSystem(); + } + + public TedFileServiceTest() + { + _service = new TedFileService(ServiceProvider); + } + + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(sp => new HashingService(sp)); + PetroglyphCommons.ContributeServices(serviceCollection); + serviceCollection.SupportTED(); + } + + [Fact] + public void Foo() + { + const string path = @"C:\Program Files (x86)\Steam\steamapps\workshop\content\32470\1129810972\Data\Art\Maps\_land_planet_felucia_00.ted"; + using var tedFs = FileSystem.FileStream.New(path, FileMode.Open); + using var dest = FileSystem.FileStream.New("c:/test/map.ted", FileMode.Create); + _service.RemoveMapPreview(tedFs, dest, true, out var bytes); + + if (bytes is not null) + { + using var img = FileSystem.FileStream.New("c:/test/img.dds", FileMode.Create); + using var streamWriter = new BinaryWriter(img); + streamWriter.Write(bytes); + } + + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs index 92e0c5f2..0556a80b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; using PG.StarWarsGame.Files.TED.Data; @@ -8,6 +9,6 @@ internal sealed class TedFileReader(Stream stream) : ChunkFileReaderBase Date: Sat, 28 Mar 2026 10:25:58 +0100 Subject: [PATCH 11/17] add testing project for chunk file project --- ModVerify.slnx | 1 + ....StarWarsGame.Files.ChunkFiles.Test.csproj | 37 +++++++++++++++++++ .../Properties/AssemblyAttributes.cs | 3 ++ 3 files changed, 41 insertions(+) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Properties/AssemblyAttributes.cs diff --git a/ModVerify.slnx b/ModVerify.slnx index dd535739..caae34b0 100644 --- a/ModVerify.slnx +++ b/ModVerify.slnx @@ -19,6 +19,7 @@ + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj new file mode 100644 index 00000000..cb0168a3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj @@ -0,0 +1,37 @@ + + + + net8.0;net10.0 + $(TargetFrameworks);net481 + + + false + true + Exe + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Properties/AssemblyAttributes.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Properties/AssemblyAttributes.cs new file mode 100644 index 00000000..6e1a5430 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Properties/AssemblyAttributes.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("PG.StarWarsGame.Files.ChunkFiles.Test")] \ No newline at end of file From 8dc31d303f2de42b689a4f1e6a49a560772ae99b Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 30 Mar 2026 13:51:57 +0200 Subject: [PATCH 12/17] new chunk implementation --- ModVerify.slnx | 4 +- .../Binary/AloChunkType.cs} | 4 +- .../Identifier/AloContentInfoIdentifier.cs | 42 +-- .../Reader/Animations/AnimationReaderBase.cs | 8 +- .../Binary/Reader/Models/ModelFileReader.cs | 7 +- .../Binary/ChunkFactory.cs | 128 +++++++++ .../Binary/Metadata/Chunk.cs | 189 ------------- .../Binary/Metadata/ChunkFile.cs | 48 ---- .../Binary/Metadata/ChunkMetadata.cs | 39 --- .../Binary/Model/Chunk.cs | 30 ++ .../Binary/Model/ChunkFile.cs | 90 ++++++ .../Binary/Model/DataChunk.cs | 60 ++++ .../Binary/Model/Metadata/ChunkMetadata.cs | 49 ++++ .../Model/Metadata/MiniChunkMetadata.cs | 35 +++ .../Binary/Model/MiniChunk.cs | 63 +++++ .../Binary/Model/MiniNodeChunk.cs | 29 ++ .../Binary/Model/NodeChunk.cs | 27 ++ .../Binary/Model/NodeChunkBase.cs | 70 +++++ .../Binary/Model/RawChunk.cs | 66 +++++ .../Binary/Model/RootChunk.cs | 7 + .../Binary/Reader/ChunkFileReaderBase.cs | 45 ++- .../Binary/Reader/ChunkReader.cs | 261 +++++++++++++++--- .../Binary/Reader/IChunkFileReader.cs | 18 ++ .../PG.StarWarsGame.Files.ChunkFiles.csproj | 1 + .../Binary/Writer/MapPreviewExtractor.cs | 5 +- 25 files changed, 969 insertions(+), 356 deletions(-) rename src/PetroglyphTools/{PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs => PG.StarWarsGame.Files.ALO/Binary/AloChunkType.cs} (90%) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Chunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/MiniChunkMetadata.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RootChunk.cs diff --git a/ModVerify.slnx b/ModVerify.slnx index caae34b0..2d271f09 100644 --- a/ModVerify.slnx +++ b/ModVerify.slnx @@ -19,7 +19,9 @@ - + + + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/AloChunkType.cs similarity index 90% rename from src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs rename to src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/AloChunkType.cs index 70af55dd..2a88e557 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkType.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/AloChunkType.cs @@ -1,6 +1,6 @@ -namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +namespace PG.StarWarsGame.Files.ALO.Binary; -public enum ChunkType +public enum AloChunkType { Unknown, Name = 0x0, diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs index 3024efd7..e2da2859 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Identifier/AloContentInfoIdentifier.cs @@ -1,7 +1,7 @@ using System.IO; using PG.StarWarsGame.Files.ALO.Files; using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; namespace PG.StarWarsGame.Files.ALO.Binary.Identifier; @@ -14,19 +14,19 @@ public AloContentInfo GetContentInfo(Stream stream) var chunk = chunkReader.ReadChunk(); - switch ((ChunkType)chunk.Type) + switch ((AloChunkType)chunk.Type) { - case ChunkType.Skeleton: - case ChunkType.Mesh: - case ChunkType.Light: + case AloChunkType.Skeleton: + case AloChunkType.Mesh: + case AloChunkType.Light: return FromModel(chunk.BodySize, chunkReader); - case ChunkType.Connections: + case AloChunkType.Connections: return FromConnection(chunkReader); - case ChunkType.Particle: + case AloChunkType.Particle: return new AloContentInfo(AloType.Particle, AloVersion.V1); - case ChunkType.ParticleUaW: + case AloChunkType.ParticleUaW: return new AloContentInfo(AloType.Particle, AloVersion.V2); - case ChunkType.Animation: + case AloChunkType.Animation: return FromAnimation(chunkReader); default: throw new BinaryCorruptedException("Unable to get ALO content information."); @@ -38,9 +38,9 @@ private static AloContentInfo FromAnimation(ChunkReader chunkReader) var chunk = chunkReader.TryReadChunk(); while (chunk.HasValue) { - switch ((ChunkType)chunk.Value.Type) + switch ((AloChunkType)chunk.Value.Type) { - case ChunkType.AnimationInformation: + case AloChunkType.AnimationInformation: return chunk.Value.BodySize switch { 36 => new AloContentInfo(AloType.Animation, AloVersion.V2), @@ -61,14 +61,14 @@ private static AloContentInfo FromConnection(ChunkReader chunkReader) var chunk = chunkReader.TryReadChunk(); while (chunk.HasValue) { - switch ((ChunkType)chunk.Value.Type) + switch ((AloChunkType)chunk.Value.Type) { - case ChunkType.ProxyConnection: - case ChunkType.ObjectConnection: - case ChunkType.ConnectionCounts: + case AloChunkType.ProxyConnection: + case AloChunkType.ObjectConnection: + case AloChunkType.ConnectionCounts: chunkReader.Skip(chunk.Value.BodySize); break; - case ChunkType.Dazzle: + case AloChunkType.Dazzle: return new AloContentInfo(AloType.Model, AloVersion.V2); default: throw new BinaryCorruptedException("Invalid ALO model."); @@ -85,13 +85,13 @@ private static AloContentInfo FromModel(int size, ChunkReader chunkReader) var chunk = chunkReader.TryReadChunk(); if (chunk is null) throw new BinaryCorruptedException("Unable to get ALO content information."); - switch ((ChunkType)chunk.Value.Type) + switch ((AloChunkType)chunk.Value.Type) { - case ChunkType.Connections: + case AloChunkType.Connections: return FromConnection(chunkReader); - case ChunkType.Skeleton: - case ChunkType.Mesh: - case ChunkType.Light: + case AloChunkType.Skeleton: + case AloChunkType.Mesh: + case AloChunkType.Light: return FromModel(chunk.Value.BodySize, chunkReader); default: throw new BinaryCorruptedException("Invalid ALO model."); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs index dbcfc4d1..d42e0ba5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ALO/Binary/Reader/Animations/AnimationReaderBase.cs @@ -4,7 +4,7 @@ using PG.StarWarsGame.Files.ALO.Data; using PG.StarWarsGame.Files.ALO.Services; using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; namespace PG.StarWarsGame.Files.ALO.Binary.Reader.Animations; @@ -54,8 +54,7 @@ protected virtual void ReadAnimation( switch (chunk.Type) { case (int)AnimationChunkTypes.AnimationInfo: - if (chunk.RawSize < 0) - ThrowChunkSizeTooLargeException(); + ThrowIfChunkSizeTooLargeException(chunk); animationInformation = ReadAnimationInfo(chunk.BodySize); break; case (int)AnimationChunkTypes.BoneData: @@ -72,8 +71,7 @@ protected virtual void ReadBoneDataCore(ChunkMetadata chunk, List proxies) do { var chunk = ChunkReader.ReadChunk(ref actualSize); - if (chunk.RawSize < 0) - ThrowChunkSizeTooLargeException(); - + ThrowIfChunkSizeTooLargeException(chunk); if (chunk.Type == (int)ModelChunkTypes.ProxyConnection) { ReadProxy(chunk.BodySize, out var proxy, ref actualSize); @@ -139,8 +137,7 @@ private void ReadSubMeshMaterialInformation(int size, ISet textures, ISe break; } case (int)ModelChunkTypes.ShaderTexture: - if (chunk.RawSize < 0) - ThrowChunkSizeTooLargeException(); + ThrowIfChunkSizeTooLargeException(chunk); ReadShaderTexture(chunk.BodySize, textures, ref actualSize); break; default: diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs new file mode 100644 index 00000000..29f61617 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs @@ -0,0 +1,128 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Diagnostics; +using System.Linq; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary; + +/// +/// Provides factory methods for creating chunks and chunk files. +/// +/// +/// Five chunk types are supported: +/// +/// — Regular chunk containing binary data. +/// — Chunk with a smaller, 2-byte header for data up to 255 bytes. +/// — Container chunk holding regular child chunks. +/// — Container chunk holding mini-chunk children. +/// — Chunk holding raw binary data. +/// +/// +public static class ChunkFactory +{ + /// + /// Creates a data chunk with the specified type and binary data. + /// + /// The chunk type identifier. + /// The binary data to store in the chunk. + /// A new . + /// is . + public static DataChunk Data(uint type, byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var metadata = new ChunkMetadata(type, (uint)data.Length); + return new DataChunk(metadata, data); + } + + /// + /// Creates a raw chunk that stores its body as an uninterpreted byte blob. + /// + /// The chunk type identifier. + /// The raw size value written to the header. The high bit 31, set or unset, is not interpreted. + /// The raw body data. + /// A new . + /// is . + public static RawChunk Raw(uint type, uint rawSize, byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var metadata = new ChunkMetadata(type, rawSize); + return new RawChunk(metadata, data); + } + + /// + /// Creates a mini-chunk with the specified type and binary data. + /// + /// The mini-chunk type identifier. + /// The binary data to store. Maximum length is 255 bytes. + /// A new . + /// is . + /// The length of exceeds 255 bytes. + public static MiniChunk Mini(byte type, byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + if (data.Length > byte.MaxValue) + throw new ArgumentException($"Mini-chunk data cannot exceed {byte.MaxValue} bytes. Provided: {data.Length}.", + nameof(data)); + + var metadata = new MiniChunkMetadata(type, (byte)data.Length); + return new MiniChunk(metadata, data); + } + + /// + /// Creates a node chunk containing regular child chunks. + /// + /// The chunk type identifier. + /// The child chunks. + /// A new with bit 31 set in the size field. + /// is . + /// The total size of the child chunks exceeds 2GB. + public static NodeChunk Node(uint type, params RootChunk[] children) + { + if (children == null) + throw new ArgumentNullException(nameof(children)); + + var size = (uint)children.Sum(c => c.Size); + if (size > int.MaxValue) + throw new InvalidOperationException("Chunk nodes cannot contain chunks with a total content size larger than 2GB."); + var metadata = new ChunkMetadata(type, size | 0x8000_0000u); + Debug.Assert((int)metadata.RawSize < 0); + return new NodeChunk(metadata, children); + } + + /// + /// Creates a node chunk containing mini-chunk children. + /// + /// The chunk type identifier. + /// The mini-chunk children. + /// A new with bit 31 cleared in the size field. + /// is . + public static MiniNodeChunk Node(uint type, params MiniChunk[] children) + { + if (children == null) + throw new ArgumentNullException(nameof(children)); + + var size = (uint)children.Sum(c => c.Size); + var metadata = new ChunkMetadata(type, size); + return new MiniNodeChunk(metadata, children); + } + + /// + /// Creates a chunk file containing the specified root chunks. + /// + /// The top-level chunks. + /// A new . + /// is . + public static ChunkFile File(params RootChunk[] rootChunks) + { + if (rootChunks == null) + throw new ArgumentNullException(nameof(rootChunks)); + return new ChunkFile(rootChunks); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs deleted file mode 100644 index c708838d..00000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/Chunk.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.Linq; -using PG.StarWarsGame.Files.Binary; - -namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; - -public sealed class Chunk : IBinary -{ - public ChunkMetadata Info { get; } - - public byte[]? Data { get; } - - public IReadOnlyList Children { get; } - - public byte[] Bytes - { - get - { - var bytes = new byte[Size]; - GetBytes(bytes); - return bytes; - } - } - - public int Size => Info.IsMiniChunk - ? 2 + Data!.Length - : Data is not null - ? 8 + Data.Length - : 8 + Children.Sum(c => c.Size); - - public Chunk(ChunkMetadata info, byte[] data) - { - Info = info; - Data = data ?? throw new ArgumentNullException(nameof(data)); - Children = []; - } - - public Chunk(ChunkMetadata info, IReadOnlyList children) - { - if (info.IsMiniChunk) - throw new ArgumentException("MiniChunks cannot have child chunks", nameof(info)); - Info = info; - Data = null; - Children = children ?? throw new ArgumentNullException(nameof(children)); - } - - public void GetBytes(Span bytes) - { - if (Info.IsMiniChunk) - { - bytes[0] = (byte)Info.Type; - bytes[1] = (byte)Data!.Length; - Data.AsSpan().CopyTo(bytes[2..]); - return; - } - - BinaryPrimitives.WriteUInt32LittleEndian(bytes, Info.Type); - - if (Data is not null) - { - BinaryPrimitives.WriteInt32LittleEndian(bytes[4..], Data.Length); - Data.AsSpan().CopyTo(bytes[8..]); - } - else - { - // .Sum is a checked operation and will already throw an overflow exception - var bodySize = Children.Sum(c => c.Size); - var hasMiniChunkChildren = Children.Count > 0 && Children[0].Info.IsMiniChunk; - - var sizeField = hasMiniChunkChildren - ? bodySize - : (int)(bodySize | 0x8000_0000u); - - BinaryPrimitives.WriteInt32LittleEndian(bytes[4..], sizeField); - - var offset = 8; - foreach (var child in Children) - { - child.GetBytes(bytes[offset..]); - offset += child.Size; - } - } - } -} - -/// -/// Provides factory methods for creating chunks and chunk files. -/// -/// -/// -/// This class provides static methods for creating hierarchical chunk structures. -/// Three chunk types are supported: -/// -/// -/// Data chunks - Regular chunks containing binary data -/// Mini-chunks - Chunks with 2-byte headers for data up to 255 bytes -/// Node chunks - Container chunks that hold child chunks -/// -/// -public static class ChunkFactory -{ - /// - /// Creates a data chunk with the specified type and binary data. - /// - /// The chunk type identifier. - /// The binary data to store in the chunk. - /// A containing the specified data. - /// is . - public static Chunk Data(uint type, byte[] data) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var metadata = new ChunkMetadata(type, (uint)data.Length, false); - return new Chunk(metadata, data); - } - - /// - /// Creates a mini-chunk with the specified type and binary data. - /// - /// The mini-chunk type identifier. - /// The binary data to store in the mini-chunk. Maximum length is 255 bytes. - /// A representing a mini-chunk with a 2-byte header. - /// is . - /// The length of exceeds 255 bytes. - public static Chunk Mini(byte type, byte[] data) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - if (data.Length > byte.MaxValue) - throw new ArgumentException( - $"Mini-chunk data cannot exceed {byte.MaxValue} bytes. Provided data length: {data.Length}.", - nameof(data)); - - var metadata = new ChunkMetadata(type, (uint)data.Length, true); - return new Chunk(metadata, data); - } - - /// - /// Creates a chunk node that contains child chunks. - /// - /// The chunk type identifier. - /// The child chunks to include in this node. - /// A containing the specified children. - /// is . - public static Chunk Node(uint type, params Chunk[] children) - { - if (children == null) - throw new ArgumentNullException(nameof(children)); - - var size = (uint)children.Sum(c => c.Size); - var metadata = new ChunkMetadata(type, size, false); - return new Chunk(metadata, children); - } - - /// - /// Creates a chunk node that contains child chunks built using a configuration action. - /// - /// The chunk type identifier. - /// An action that populates a list with child chunks. - /// A containing the configured children. - /// is . - public static Chunk Node(uint type, Action> configure) - { - if (configure == null) - throw new ArgumentNullException(nameof(configure)); - - var children = new List(); - configure(children); - return Node(type, children.ToArray()); - } - - /// - /// Creates a chunk file containing the specified root chunks. - /// - /// The top-level chunks to include in the file. - /// A containing the specified root chunks. - /// is . - public static ChunkFile File(params Chunk[] rootChunks) - { - if (rootChunks == null) - throw new ArgumentNullException(nameof(rootChunks)); - - return new ChunkFile(rootChunks); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs deleted file mode 100644 index 1c38ddbb..00000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkFile.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using PG.StarWarsGame.Files.Binary.File; - -namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; - -public sealed class ChunkFile : IBinaryFile -{ - public IReadOnlyList RootChunks { get; } - - public int Size => RootChunks.Sum(c => c.Size); - - public byte[] Bytes - { - get - { - var bytes = new byte[Size]; - GetBytes(bytes); - return bytes; - } - } - - public ChunkFile(IReadOnlyList rootChunks) - { - if (rootChunks == null) - throw new ArgumentNullException(nameof(rootChunks)); - if (rootChunks.Count == 0) - throw new ArgumentOutOfRangeException(nameof(rootChunks), "A chunk file must contain at least one chunk"); - RootChunks = rootChunks; - } - - public void GetBytes(Span bytes) - { - var offset = 0; - foreach (var chunk in RootChunks) - { - chunk.GetBytes(bytes[offset..]); - offset += chunk.Size; - } - } - - public void WriteTo(Stream stream) - { - stream.Write(Bytes, 0, Size); - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs deleted file mode 100644 index 3a767498..00000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Metadata/ChunkMetadata.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Diagnostics; - -namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; - -[DebuggerDisplay("Type: {Type}, Size: {BodySize}, Mini:{IsMiniChunk}")] -public readonly struct ChunkMetadata -{ - public readonly uint Type; - public readonly uint RawSize; - public readonly bool IsMiniChunk; - - /// - /// Indicates that bit 31 of RawSize is set. - /// This is a hint that the body contains child chunks, not a guarantee. - /// - public bool HasChildrenHint => !IsMiniChunk && (int)RawSize < 0; - - /// - /// Gets the size of the chunk's data in bytes. - /// - /// - /// This value has bit 31 masked off compared to . - /// Per spec, bit 31 is set only for chunks containing regular child chunks. - /// Chunks containing mini-chunks (treated as data) do NOT set bit 31. - /// Since this library doesn't support sizes > , masking bit 31 - /// has no practical impact on the usable size range. - /// - public int BodySize => (int)(RawSize & 0x7FFF_FFFF); - - public ChunkMetadata(uint type, uint rawSize, bool isMiniChunk) - { - if (isMiniChunk && rawSize > byte.MaxValue) - throw new ArgumentOutOfRangeException(nameof(rawSize), "Mini chunk size must fit in a byte (0-255)."); - Type = type; - RawSize = rawSize; - IsMiniChunk = isMiniChunk; - } -} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Chunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Chunk.cs new file mode 100644 index 00000000..10296133 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Chunk.cs @@ -0,0 +1,30 @@ +using System; +using PG.StarWarsGame.Files.Binary; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + + +/// +/// Base class for all chunk types in a chunked file. +/// +public abstract class Chunk : IBinary +{ + /// + /// Gets the total size of this chunk in bytes, including the header. + /// + public abstract int Size { get; } + + /// + public byte[] Bytes + { + get + { + var bytes = new byte[Size]; + GetBytes(bytes); + return bytes; + } + } + + /// + public abstract void GetBytes(Span bytes); +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs new file mode 100644 index 00000000..852ebb8c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using PG.StarWarsGame.Files.Binary.File; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// Represents a chunked file containing one or more root-level chunks. +/// +public sealed class ChunkFile : IBinaryFile +{ + /// + /// Gets the root-level chunks in this file. + /// + public IReadOnlyList RootChunks { get; } + + /// + /// Gets the total file size in bytes. + /// + public int Size => RootChunks.Sum(c => c.Size); + + /// + /// Initializes a new instance of the class. + /// + /// + /// The root-level chunks. Must contain at least one element. + /// + /// + /// is . + /// + /// + /// is empty. + /// + public ChunkFile(IReadOnlyList rootChunks) + { + if (rootChunks == null) + throw new ArgumentNullException(nameof(rootChunks)); + + if (rootChunks.Count == 0) + throw new ArgumentException("ChunkFile must have at least one root chunk.", nameof(rootChunks)); + + RootChunks = rootChunks; + } + + /// + /// Gets the file's binary representation as a byte array. + /// + public byte[] Bytes + { + get + { + var bytes = new byte[Size]; + GetBytes(bytes); + return bytes; + } + } + + /// + /// Writes the file's binary representation into the specified span. + /// + /// + /// The destination span. Must be at least bytes long. + /// + public void GetBytes(Span bytes) + { + var offset = 0; + foreach (var chunk in RootChunks) + { + chunk.GetBytes(bytes[offset..]); + offset += chunk.Size; + } + } + + /// + /// Writes the file's binary representation to a stream. + /// + /// The destination stream. + /// + /// is . + /// + public void WriteTo(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + stream.Write(Bytes, 0, Size); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs new file mode 100644 index 00000000..0b5b820d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs @@ -0,0 +1,60 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Buffers.Binary; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// A chunk containing binary data. +/// +public sealed class DataChunk : Chunk +{ + /// + /// Gets the chunk metadata. + /// + public ChunkMetadata Info { get; } + + /// + /// Gets the chunk's binary data payload. + /// + public ReadOnlyMemory Data { get; } + + /// + public override unsafe int Size => sizeof(ChunkMetadata) + Data.Length; + + /// + /// Initializes a new instance of the class. + /// + /// The chunk metadata. Must not have bit 31 set. + /// The binary data payload. + /// + /// is empty, or + /// has bit 31 set, or + /// body size does not match the data length. + /// + public DataChunk(ChunkMetadata info, ReadOnlyMemory data) + { + if (data is { IsEmpty: true, Length: 0 }) + throw new ArgumentException("Data cannot be empty.", nameof(data)); + + if (info.HasChildrenHint) + throw new ArgumentException( + "DataChunk metadata must not have bit 31 set.", nameof(info)); + + if (info.BodySize != data.Length) + throw new ArgumentException( + $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", + nameof(info)); + + Info = info; + Data = data; + } + + /// + public override unsafe void GetBytes(Span bytes) + { + BinaryPrimitives.WriteUInt32LittleEndian(bytes, Info.Type); + BinaryPrimitives.WriteInt32LittleEndian(bytes[sizeof(uint)..], Data.Length); + Data.Span.CopyTo(bytes[sizeof(ChunkMetadata)..]); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs new file mode 100644 index 00000000..6dbb9a96 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs @@ -0,0 +1,49 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; + +/// +/// Describes the header of a regular chunk. +/// +/// +/// A regular chunk header is 8 bytes: 4 bytes for the type and 4 bytes for the size. +/// Bit 31 of the size field indicates whether the chunk body contains child chunks. +/// +[DebuggerDisplay("Type: 0x{Type:X8}, Size: {BodySize}, HasChildrenHint: {HasChildrenHint}")] +public readonly struct ChunkMetadata +{ + /// + /// The chunk type identifier. + /// + public readonly uint Type; + + /// + /// The raw size value as stored in the chunk header, including bit 31. + /// + public readonly uint RawSize; + + /// + /// Gets a value indicating whether bit 31 of is set. + /// This is a hint that the body contains child chunks, not a guarantee. + /// + public bool HasChildrenHint => (int)RawSize < 0; + + /// + /// Gets the size of the chunk body in bytes with bit 31 masked off. + /// + public int BodySize => (int)(RawSize & 0x7FFF_FFFF); + + /// + /// Initializes a new instance of the struct. + /// + /// The chunk type identifier. + /// + /// The raw size value. Bit 31 should be set if the chunk contains child chunks. + /// + public ChunkMetadata(uint type, uint rawSize) + { + Type = type; + RawSize = rawSize; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/MiniChunkMetadata.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/MiniChunkMetadata.cs new file mode 100644 index 00000000..9b702219 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/MiniChunkMetadata.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; + +/// +/// Describes the header of a mini-chunk. +/// +/// +/// A mini-chunk header is 2 bytes: 1 byte for the type and 1 byte for the size. +/// Mini-chunks cannot contain children. +/// +[DebuggerDisplay("Type: 0x{Type:X2}, Size: {BodySize}")] +public readonly struct MiniChunkMetadata +{ + /// + /// The mini-chunk type identifier. + /// + public readonly byte Type; + + /// + /// The size of the mini-chunk body in bytes. + /// + public readonly byte BodySize; + + /// + /// Initializes a new instance of the struct. + /// + /// The mini-chunk type identifier. + /// The size of the mini-chunk body in bytes. + public MiniChunkMetadata(byte type, byte size) + { + Type = type; + BodySize = size; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs new file mode 100644 index 00000000..de72659c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs @@ -0,0 +1,63 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// A mini-chunk containing binary data with a compact 2-byte header. +/// +/// +/// +/// Mini-chunks use a 2-byte header (1 byte type, 1 byte size) instead of the standard 8-byte header. +/// They can only appear as children of a . +/// +/// +/// A mini chunk cannot be used as a root chunk or as a child of a . +/// +/// +public sealed class MiniChunk : Chunk +{ + /// + /// Gets the mini-chunk metadata. + /// + public MiniChunkMetadata Info { get; } + + /// + /// Gets the mini-chunk's binary data payload. + /// + public ReadOnlyMemory Data { get; } + + /// + public override unsafe int Size => sizeof(MiniChunkMetadata) + Data.Length; + + /// + /// Initializes a new instance of the class. + /// + /// The mini-chunk metadata. + /// The binary data payload. Maximum length is 255 bytes. + /// + /// is empty, or + /// size does not match the data length. + /// + public MiniChunk(MiniChunkMetadata info, ReadOnlyMemory data) + { + if (data is { IsEmpty: true, Length: 0 }) + throw new ArgumentException("Data cannot be empty.", nameof(data)); + + if (info.BodySize!= data.Length) + throw new ArgumentException( + $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", + nameof(info)); + + Info = info; + Data = data; + } + + /// + public override unsafe void GetBytes(Span bytes) + { + bytes[0] = Info.Type; + bytes[1] = Info.BodySize; + Data.Span.CopyTo(bytes[sizeof(MiniChunkMetadata)..]); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs new file mode 100644 index 00000000..0db79f73 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs @@ -0,0 +1,29 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// A chunk containing mini-chunk children. +/// +/// +/// +/// This chunk type holds exclusively children. +/// Bit 31 of the size field is not used as a container flag. +/// +/// +public sealed class MiniNodeChunk : NodeChunkBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The chunk metadata. Must not have bit 31 set. + /// The mini-chunk children. Must contain at least one element. + /// Chunks larger than 2GB are not supported. + public MiniNodeChunk(ChunkMetadata info, IReadOnlyList children) : base(info, children) + { + if (info.RawSize > int.MaxValue) + throw new NotSupportedException("Chunks larger than int32.MaxValue bytes are not supported."); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs new file mode 100644 index 00000000..05a17e6d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs @@ -0,0 +1,27 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Collections.Generic; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// A chunk containing regular child chunks. +/// +/// +/// Bit 31 of the size field must be set. +/// +public sealed class NodeChunk : NodeChunkBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The chunk metadata. Must have bit 31 set. + /// The child chunks. Must contain at least one element. + /// does not have bit 31 set. + public NodeChunk(ChunkMetadata info, IReadOnlyList children) : base(info, children) + { + if (!info.HasChildrenHint) + throw new ArgumentException( + "NodeChunk metadata must have bit 31 set.", nameof(info)); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs new file mode 100644 index 00000000..3d900e8e --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs @@ -0,0 +1,70 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// Base class for chunks that contain child chunks of type . +/// +/// The type of child chunk this node contains. +public abstract class NodeChunkBase : RootChunk where TChild : Chunk +{ + /// + /// Gets the chunk metadata. + /// + public ChunkMetadata Info { get; } + + /// + /// Gets the child chunks. + /// + public IReadOnlyList Children { get; } + + /// + public override unsafe int Size => sizeof(ChunkMetadata) + Children.Sum(c => c.Size); + + /// + /// Initializes a new instance of the class. + /// + /// The chunk metadata. + /// The child chunks. Must contain at least one element. + /// is . + /// + /// is empty, or + /// body size does not match the sum of children sizes. + /// + protected NodeChunkBase(ChunkMetadata info, IReadOnlyList children) + { + if (children == null) + throw new ArgumentNullException(nameof(children)); + + if (children.Count == 0) + throw new ArgumentException( + $"{GetType().Name} must have at least one child.", nameof(children)); + + var actualSize = children.Sum(c => c.Size); + if (info.BodySize != actualSize) + throw new ArgumentException( + $"Metadata size ({info.BodySize}) does not match sum of children sizes ({actualSize}).", + nameof(info)); + + Info = info; + Children = children; + } + + /// + public override unsafe void GetBytes(Span bytes) + { + BinaryPrimitives.WriteUInt32LittleEndian(bytes, Info.Type); + BinaryPrimitives.WriteUInt32LittleEndian(bytes[sizeof(uint)..], Info.RawSize); + + var offset = sizeof(ChunkMetadata); + foreach (var child in Children) + { + child.GetBytes(bytes[offset..]); + offset += child.Size; + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs new file mode 100644 index 00000000..bd336b05 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs @@ -0,0 +1,66 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using System; +using System.Buffers.Binary; + +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// A chunk that stores raw binary data without interpreting its contents. +/// +/// +/// +/// This chunk stores its body as a raw byte blob. The body may contain +/// any content: raw data, child chunks, mini-chunks, or any combination. +/// No structural interpretation or validation is performed on the body. +/// +/// +/// This is useful for: +/// +/// +/// Round-tripping unknown or unparsed chunks. +/// Creating chunks from pre-serialized data. +/// +/// +/// The metadata is written as-is, including bit 31 of the size field. +/// +/// +public sealed class RawChunk : RootChunk +{ + /// + /// Gets the chunk metadata. + /// + public ChunkMetadata Info { get; } + + /// + /// Gets the raw body data. + /// + public ReadOnlyMemory Data { get; } + + /// + public override unsafe int Size => sizeof(ChunkMetadata) + Data.Length; + + /// + /// Initializes a new instance of the class. + /// + /// The chunk metadata, written as-is. No validation is performed on bit 31. + /// The raw body data. + /// body size does not match the data length. + public RawChunk(ChunkMetadata info, ReadOnlyMemory data) + { + if (info.BodySize != data.Length) + throw new ArgumentException( + $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", + nameof(info)); + + Info = info; + Data = data; + } + + /// + public override unsafe void GetBytes(Span bytes) + { + BinaryPrimitives.WriteUInt32LittleEndian(bytes, Info.Type); + BinaryPrimitives.WriteUInt32LittleEndian(bytes[sizeof(uint)..], Info.RawSize); + Data.Span.CopyTo(bytes[sizeof(ChunkMetadata)..]); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RootChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RootChunk.cs new file mode 100644 index 00000000..e079bf06 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RootChunk.cs @@ -0,0 +1,7 @@ +namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +/// +/// Base class for chunks that can appear as root-level elements in a +/// or as children in a . +/// +public abstract class RootChunk : Chunk; \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs index eb664447..5a98ea03 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs @@ -1,30 +1,65 @@ using System; using System.IO; using AnakinRaW.CommonUtilities; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; using PG.StarWarsGame.Files.ChunkFiles.Data; namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; -public abstract class ChunkFileReaderBase(Stream stream) : DisposableObject, IChunkFileReader where T : IChunkData +/// +/// Represents the base class for reading chunk files in a binary format. +/// +/// The type of chunk data being read. +/// The input stream from which the chunk file is read. +/// +/// A boolean value indicating whether the input stream should remain open after the reader is disposed. +/// Default value is . +/// +public abstract class ChunkFileReaderBase(Stream stream, bool leaveStreamOpen = false) + : DisposableObject, IChunkFileReader where T : IChunkData { - protected readonly ChunkReader ChunkReader = new(stream); + /// + /// The instance used to read chunk data from the input stream. + /// + /// + /// This reader is initialized with the provided stream and the option to leave the stream open after disposal. + /// + protected readonly ChunkReader ChunkReader = new(stream, leaveStreamOpen); + /// public abstract T Read(); + /// IChunkData IChunkFileReader.Read() { return Read(); } + /// + /// Releases the resources used by the instance, including + /// the associated . + /// protected override void DisposeResources() { base.DisposeResources(); ChunkReader.Dispose(); } - //[DoesNotReturn] - protected void ThrowChunkSizeTooLargeException() + /// + /// Validates the raw binary size of the specified chunk and throws an exception if the size exceeds the maximum allowed value. + /// + /// + /// This method should not be used for , as their raw size always exceeds + /// due to the presence of the high bit flag in the size field, which indicates that the chunk has child chunks. + /// + /// The metadata of the chunk to validate. + /// + /// Thrown when the raw binary size of the chunk exceeds , as such sizes are not supported. + /// + protected void ThrowIfChunkSizeTooLargeException(ChunkMetadata chunk) { - throw new NotSupportedException("Chunk sizes larger than int.MaxValue are not supported."); + if (chunk.RawSize > int.MaxValue) + throw new NotSupportedException("Chunk sizes larger than int.MaxValue are not supported."); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs index 024d6eee..5b67f1da 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkReader.cs @@ -3,14 +3,37 @@ using System.Text; using AnakinRaW.CommonUtilities; using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +/// +/// Reads chunks and their data from a stream. +/// +/// +/// +/// The reader operates sequentially on the underlying stream. It reads chunk headers, +/// mini-chunk headers, and raw data. It does not build a tree structure — callers are +/// responsible for interpreting chunk relationships based on the file format specification. +/// +/// +/// Several methods accept a ref int readBytes parameter that tracks the number +/// of bytes read. This is useful for parsing chunk bodies where the caller needs to know +/// when the body has been fully consumed. +/// +/// public class ChunkReader : DisposableObject { private readonly PetroglyphBinaryReader _binaryReader; + /// + /// Initializes a new instance of the class. + /// + /// The stream to read from. + /// + /// to leave the stream open after this reader is disposed; otherwise, . + /// + /// is . public ChunkReader(Stream stream, bool leaveOpen = false) { if (stream == null) @@ -18,130 +41,290 @@ public ChunkReader(Stream stream, bool leaveOpen = false) _binaryReader = new PetroglyphBinaryReader(stream, leaveOpen); } + /// + /// Reads an 8-byte chunk header from the stream. + /// + /// The chunk metadata. + /// The end of the stream is reached before the header could be read. public ChunkMetadata ReadChunk() { var type = _binaryReader.ReadUInt32(); var rawSize = _binaryReader.ReadUInt32(); - return new ChunkMetadata(type, rawSize, false); + return new ChunkMetadata(type, rawSize); } + /// + /// Reads an 8-byte chunk header from the stream and advances the byte counter. + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by 8. + /// + /// The chunk metadata. + /// The end of the stream is reached before the header could be read. public ChunkMetadata ReadChunk(ref int readBytes) { var chunk = ReadChunk(); - readBytes += 8; + IncrementReadBytesByChunkSize(ref readBytes); return chunk; } - public ChunkMetadata ReadMiniChunk(ref int readBytes) + /// + /// Reads a 2-byte mini-chunk header from the stream and advances the byte counter. + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by 2. + /// + /// The mini-chunk metadata. + /// The end of the stream is reached before the header could be read. + public MiniChunkMetadata ReadMiniChunk(ref int readBytes) { var type = _binaryReader.ReadByte(); var size = _binaryReader.ReadByte(); - - readBytes += 2; - - return new ChunkMetadata(type, size, true); + IncrementReadBytesByMiniChunkSize(ref readBytes); + return new MiniChunkMetadata(type, size); } + /// + /// Reads the body data of a chunk. + /// + /// The chunk whose body to read. + /// The chunk body as a byte array. + /// Attempt to read data from a node chunk. + /// The end of the stream is reached before the body could be read. public byte[] ReadData(ChunkMetadata chunk) { - if (chunk.HasChildrenHint) - throw new InvalidOperationException("Unable to read data from container chunk."); + if (chunk.RawSize > int.MaxValue) + throw new InvalidOperationException("Cannot to read data from container chunk."); + return _binaryReader.ReadBytes(chunk.BodySize); + } + /// + /// Reads the body data of a mini chunk. + /// + /// The mini chunk whose body to read. + /// The chunk body as a byte array. + /// The end of the stream is reached before the body could be read. + public byte[] ReadData(MiniChunkMetadata chunk) + { return _binaryReader.ReadBytes(chunk.BodySize); } + /// + /// Reads the specified number of bytes from the stream. + /// + /// The number of bytes to read. + /// The data as a byte array. + /// is negative. + /// The end of the stream is reached before all bytes could be read. public byte[] ReadData(int size) { - return size < 0 ? - throw new ArgumentOutOfRangeException(nameof(size), "size cannot be negative") : - _binaryReader.ReadBytes(size); + return size < 0 + ? throw new ArgumentOutOfRangeException(nameof(size), "size cannot be negative") + : _binaryReader.ReadBytes(size); } - public byte[] ReadData(ChunkMetadata chunk, ref int readSize) + /// + /// Reads the body data of a chunk and advances the byte counter. + /// + /// The chunk whose body to read. + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by the body size of . + /// + /// The chunk body as a byte array. + /// Attempt to read data from a node chunk. + /// The end of the stream is reached before the body could be read. + public byte[] ReadData(ChunkMetadata chunk, ref int readBytes) { - if (chunk.HasChildrenHint) - throw new InvalidOperationException("Unable to read data from container chunk."); + if (chunk.RawSize >= int.MaxValue) + throw new InvalidOperationException("Cannot to read data from container chunk."); var data = _binaryReader.ReadBytes(chunk.BodySize); - readSize += chunk.BodySize; + readBytes += chunk.BodySize; return data; } + /// + /// Reads the body data of a mini chunk and advances the byte counter. + /// + /// The mini chunk whose body to read. + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by the body size of . + /// + /// The chunk body as a byte array. + /// The end of the stream is reached before the body could be read. + public byte[] ReadData(MiniChunkMetadata chunk, ref int readBytes) + { + var data = _binaryReader.ReadBytes(chunk.BodySize); + readBytes += chunk.BodySize; + return data; + } - public uint ReadDword(ref int readSize) + /// + /// Reads a 4-byte unsigned integer from the stream and advances the byte counter. + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by 4. + /// + /// The value read. + /// The end of the stream is reached before the value could be read. + public uint ReadDword(ref int readBytes) { var value = _binaryReader.ReadUInt32(); - readSize += sizeof(uint); + readBytes += sizeof(uint); return value; } - public float ReadFloat(ref int readSize) + /// + /// Reads a 4-byte floating-point value from the stream and advances the byte counter. + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by 4. + /// + /// The value read. + /// The end of the stream is reached before the value could be read. + public float ReadFloat(ref int readBytes) { var value = _binaryReader.ReadSingle(); - readSize += sizeof(float); + readBytes += sizeof(float); return value; } + /// + /// Reads a 4-byte unsigned integer from the stream. + /// + /// The value read. + /// The end of the stream is reached before the value could be read. public uint ReadDword() - { + { return _binaryReader.ReadUInt32(); } + /// + /// Advances the stream position by the specified number of bytes + /// and advances the byte counter. + /// + /// The number of bytes to skip. + /// Incremented by . public void Skip(int bytesToSkip, ref int readBytes) { _binaryReader.BaseStream.Seek(bytesToSkip, SeekOrigin.Current); readBytes += bytesToSkip; } + /// + /// Advances the stream position by the specified number of bytes. + /// + /// The number of bytes to skip. public void Skip(int bytesToSkip) { _binaryReader.BaseStream.Seek(bytesToSkip, SeekOrigin.Current); } - public string ReadString(int size, Encoding encoding, bool zeroTerminated, ref int readSize) + /// + /// Reads a string from the stream and advances the byte counter. + /// + /// The number of bytes to read for the string. + /// The character encoding to use. + /// + /// to trim the string at the first null character;otherwise, . + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns, it is incremented by . + /// + /// The decoded string. + /// The string data could not be decoded. + /// The end of the stream is reached before all bytes could be read. + public string ReadString(int size, Encoding encoding, bool zeroTerminated, ref int readBytes) { var value = ReadString(encoding, size, zeroTerminated); - readSize += size; + readBytes += size; return value; } + /// + /// Reads a string from the stream. + /// + /// The number of bytes to read for the string. + /// The character encoding to use. + /// + /// to trim the string at the first null character;otherwise, . + /// + /// The decoded string.The string data could not be decoded. + /// The end of the stream is reached before all bytes could be read. public string ReadString(int size, Encoding encoding, bool zeroTerminated) { var value = ReadString(encoding, size, zeroTerminated); return value; } - private string ReadString(Encoding encoding, int size, bool zeroTerminated) - { - try - { - return _binaryReader.ReadString(encoding, size, zeroTerminated); - } - catch (Exception e) - { - throw new BinaryCorruptedException($"Unable to read string: {e.Message}", e); - } - } - + /// + /// Reads a chunk header from the stream if data is available. + /// + /// + /// The chunk metadata, or if the stream is at the end. + /// public ChunkMetadata? TryReadChunk() { var _ = 0; return TryReadChunk(ref _); } - public ChunkMetadata? TryReadChunk(ref int size) + /// + /// Reads a chunk header from the stream if data is available + /// and advances the byte counter. + /// + /// + /// Holds the number of bytes read so far. + /// When this method returns and a chunk was read, it is incremented by 8. + /// + /// + /// The chunk metadata, or if the stream is at the end. + /// + public ChunkMetadata? TryReadChunk(ref int readBytes) { if (_binaryReader.BaseStream.Position == _binaryReader.BaseStream.Length) return null; - + var chunk = ReadChunk(); - size += 8; + IncrementReadBytesByChunkSize(ref readBytes); return chunk; } + /// + /// Releases the resources used by the . + /// protected override void DisposeResources() { base.DisposeResources(); _binaryReader.Dispose(); } + + private static unsafe void IncrementReadBytesByChunkSize(ref int readBytes) + { + readBytes += sizeof(ChunkMetadata); + } + + private static unsafe void IncrementReadBytesByMiniChunkSize(ref int readBytes) + { + readBytes += sizeof(MiniChunkMetadata); + } + + private string ReadString(Encoding encoding, int size, bool zeroTerminated) + { + try + { + return _binaryReader.ReadString(encoding, size, zeroTerminated); + } + catch (Exception e) + { + throw new BinaryCorruptedException($"Unable to read string: {e.Message}", e); + } + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/IChunkFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/IChunkFileReader.cs index 49d525a1..cdc3379f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/IChunkFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/IChunkFileReader.cs @@ -1,14 +1,32 @@ using System; +using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.ChunkFiles.Data; namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +/// +/// Reads a chunked file from a stream and produces a parsed representation. +/// public interface IChunkFileReader : IDisposable { + /// + /// Reads the chunked file and returns its parsed data. + /// + /// The parsed chunk data. + /// The stream contains invalid or unexpected data. IChunkData Read(); } +/// +/// Reads a chunked file from a stream and produces a strongly-typed parsed representation. +/// +/// The type of chunk data produced by this reader. public interface IChunkFileReader : IChunkFileReader where T : IChunkData { + /// + /// Reads the chunked file and returns its parsed data. + /// + /// The parsed chunk data of type . + /// The stream contains invalid or unexpected data. new T Read(); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj index 000b3a60..db01bf5b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/PG.StarWarsGame.Files.ChunkFiles.csproj @@ -15,6 +15,7 @@ true snupkg preview + true diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Writer/MapPreviewExtractor.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Writer/MapPreviewExtractor.cs index bd6a4f61..3a873ba5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Writer/MapPreviewExtractor.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Writer/MapPreviewExtractor.cs @@ -1,6 +1,7 @@ using System; using System.IO; -using PG.StarWarsGame.Files.ChunkFiles.Binary.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; namespace PG.StarWarsGame.Files.TED.Binary.Writer; @@ -47,7 +48,7 @@ public bool ExtractPreview(Stream tedStream, Stream destination, bool extract, o } else { - var chunk = new Chunk(chunkInfo.Value, reader.ReadData(chunkInfo.Value.BodySize)); + var chunk = new RawChunk(chunkInfo.Value, reader.ReadData(chunkInfo.Value.BodySize)); destination.Write(chunk.Bytes, 0, chunk.Bytes.Length); } } From c88a2e99b020cfcdee97e17cdaefdb9bbf9d3018 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 30 Mar 2026 14:57:09 +0200 Subject: [PATCH 13/17] start testing --- .../Binary/ChunkFactoryTest.cs | 284 +++++++++++++++ .../Binary/ChunkFileBinaryEquivalenceTest.cs | 45 +++ .../Binary/Model/ChunkFileTest.cs | 96 +++++ .../Binary/Model/DataChunkTest.cs | 81 +++++ .../Model/Metadata/ChunkMetadataTest.cs | 90 +++++ .../Model/Metadata/MiniChunkMetadataTest.cs | 38 ++ .../Binary/Model/MiniChunkTest.cs | 71 ++++ .../Binary/Model/MiniNodeChunkTest.cs | 89 +++++ .../Binary/Model/NodeChunkTest.cs | 103 ++++++ .../Binary/Model/RawChunkTest.cs | 88 +++++ .../Binary/Reader/ChunkFileReaderBaseTest.cs | 84 +++++ .../Binary/Reader/ChunkReaderTest.cs | 337 ++++++++++++++++++ .../Binary/Reader/TestChunkFileReader.cs | 21 ++ .../Binary/Reader/TestChunkFileReaderTest.cs | 45 +++ .../Binary/TestChunkFileData.cs | 103 ++++++ .../Binary/ChunkFactory.cs | 2 - .../Binary/Model/DataChunk.cs | 2 +- .../Binary/Model/RawChunk.cs | 8 +- 18 files changed, 1583 insertions(+), 4 deletions(-) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/ChunkMetadataTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/MiniChunkMetadataTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkFileReaderBaseTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs new file mode 100644 index 00000000..0b08f9fc --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs @@ -0,0 +1,284 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; + +public class ChunkFactoryTest +{ + #region Data + + [Fact] + public void Data_CreatesDataChunk() + { + var chunk = ChunkFactory.Data(0x01, [0xAA, 0xBB]); + Assert.IsType(chunk); + Assert.Equal(0x01u, chunk.Info.Type); + Assert.Equal(2, chunk.Info.BodySize); + Assert.False(chunk.Info.HasChildrenHint); + Assert.Equal(new byte[] { 0xAA, 0xBB }, chunk.Data.ToArray()); + } + + [Fact] + public void Data_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.Data(1, null!)); + } + + [Fact] + public void Data_ThrowsOnEmptyArray() + { + Assert.Throws(() => ChunkFactory.Data(1, [])); + } + + [Fact] + public void Data_SingleByte() + { + var chunk = ChunkFactory.Data(0xFF, [0x42]); + Assert.Equal(1, chunk.Info.BodySize); + Assert.Equal(new byte[] { 0x42 }, chunk.Data.ToArray()); + } + + #endregion + + #region Raw + + [Fact] + public void Raw_CreatesRawChunk() + { + var chunk = ChunkFactory.Raw(0x02, 0x8000_0003u, [1, 2, 3]); + Assert.IsType(chunk); + Assert.Equal(0x02u, chunk.Info.Type); + Assert.Equal(0x8000_0003u, chunk.Info.RawSize); + Assert.Equal(new byte[] { 1, 2, 3 }, chunk.Data.ToArray()); + } + + [Fact] + public void Raw_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.Raw(1, 0, null!)); + } + + [Fact] + public void Raw_ThrowsOnEmptyData() + { + Assert.Throws(() => ChunkFactory.Raw(0x01, 0, [])); + } + + [Fact] + public void Raw_ThrowsOnSizeMismatch() + { + Assert.Throws(() => ChunkFactory.Raw(1, 5, [0xAA])); + } + + [Fact] + public void Raw_WithBit31Set_PreservesBit31() + { + var chunk = ChunkFactory.Raw(1, 0x8000_0002u, [0xAA, 0xBB]); + Assert.True(chunk.Info.HasChildrenHint); + Assert.Equal(2, chunk.Info.BodySize); + } + + [Fact] + public void Raw_WithBit31Clear_NoBit31() + { + var chunk = ChunkFactory.Raw(1, 2, [0xAA, 0xBB]); + Assert.False(chunk.Info.HasChildrenHint); + } + + #endregion + + #region Mini + + [Fact] + public void Mini_CreatesMiniChunk() + { + var chunk = ChunkFactory.Mini(0x05, [0xCC]); + Assert.IsType(chunk); + Assert.Equal(0x05, chunk.Info.Type); + Assert.Equal(1, chunk.Info.BodySize); + Assert.Equal(new byte[] { 0xCC }, chunk.Data.ToArray()); + } + + [Fact] + public void Mini_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.Mini(1, null!)); + } + + [Fact] + public void Mini_ThrowsWhenDataExceeds255() + { + var data = new byte[256]; + Assert.Throws(() => ChunkFactory.Mini(1, data)); + } + + [Fact] + public void Mini_AllowsMaxLength255() + { + var data = new byte[255]; + var chunk = ChunkFactory.Mini(1, data); + Assert.Equal(255, chunk.Info.BodySize); + } + + [Fact] + public void Mini_ThrowsOnEmptyArray() + { + Assert.Throws(() => ChunkFactory.Mini(1, [])); + } + + [Fact] + public void Mini_SingleByte() + { + var chunk = ChunkFactory.Mini(0, [0x01]); + Assert.Equal(1, chunk.Info.BodySize); + } + + #endregion + + #region Node (RootChunk) + + [Fact] + public void Node_RootChunk_CreatesNodeChunk() + { + var child = ChunkFactory.Raw(1, 2, [0xAA, 0xBB]); + var node = ChunkFactory.Node(0x10, child); + + Assert.IsType(node); + Assert.Equal(0x10u, node.Info.Type); + Assert.True(node.Info.HasChildrenHint); + Assert.Single(node.Children); + } + + [Fact] + public void Node_RootChunk_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.Node(1, (RootChunk[])null!)); + } + + [Fact] + public void Node_RootChunk_ThrowsOnEmpty() + { + Assert.Throws(() => ChunkFactory.Node(1, Array.Empty())); + } + + [Fact] + public void Node_RootChunk_MultipleChildren() + { + var c1 = ChunkFactory.Raw(1, 1, [0xAA]); + var c2 = ChunkFactory.Raw(2, 2, [0xBB, 0xCC]); + var node = ChunkFactory.Node(0x10, c1, c2); + + Assert.Equal(2, node.Children.Count); + Assert.True(node.Info.HasChildrenHint); + Assert.Equal(c1.Size + c2.Size, node.Info.BodySize); + } + + [Fact] + public void Node_RootChunk_NestedNodes() + { + var leaf = ChunkFactory.Raw(1, 1, [0xAA]); + var inner = ChunkFactory.Node(2u, leaf); + var outer = ChunkFactory.Node(3u, inner); + + Assert.Single(outer.Children); + Assert.IsType(outer.Children[0]); + } + + [Fact] + public void Node_RootChunk_SetsCorrectMetadataSize() + { + var child = ChunkFactory.Raw(1, 2, [0xAA, 0xBB]); + var node = ChunkFactory.Node(0x10, child); + + // BodySize should equal child's total Size (header + data) + Assert.Equal(child.Size, node.Info.BodySize); + } + + #endregion + + #region Node (MiniChunk) + + [Fact] + public void Node_MiniChunk_CreatesMiniNodeChunk() + { + var child = ChunkFactory.Mini(1, [0xAA]); + var node = ChunkFactory.Node(0x20, child); + + Assert.IsType(node); + Assert.Equal(0x20u, node.Info.Type); + Assert.False(node.Info.HasChildrenHint); + Assert.Single(node.Children); + } + + [Fact] + public void Node_MiniChunk_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.Node(1, (MiniChunk[])null!)); + } + + [Fact] + public void Node_MiniChunk_ThrowsOnEmpty() + { + Assert.Throws(() => ChunkFactory.Node(1, Array.Empty())); + } + + [Fact] + public void Node_MiniChunk_MultipleChildren() + { + var c1 = ChunkFactory.Mini(1, [0xAA]); + var c2 = ChunkFactory.Mini(2, [0xBB]); + var node = ChunkFactory.Node(0x20, c1, c2); + + Assert.Equal(2, node.Children.Count); + Assert.False(node.Info.HasChildrenHint); + } + + [Fact] + public void Node_MiniChunk_SetsCorrectMetadataSize() + { + var child = ChunkFactory.Mini(1, [0xAA]); + var node = ChunkFactory.Node(0x20, child); + + Assert.Equal(child.Size, node.Info.BodySize); + } + + #endregion + + #region File + + [Fact] + public void File_CreatesChunkFile() + { + var root = ChunkFactory.Raw(1, 1, [0xAA]); + var file = ChunkFactory.File(root); + + Assert.IsType(file); + Assert.Single(file.RootChunks); + } + + [Fact] + public void File_ThrowsOnNull() + { + Assert.Throws(() => ChunkFactory.File(null!)); + } + + [Fact] + public void File_ThrowsOnEmpty() + { + Assert.Throws(() => ChunkFactory.File()); + } + + [Fact] + public void File_MultipleRoots() + { + var r1 = ChunkFactory.Raw(1, 1, [0xAA]); + var r2 = ChunkFactory.Raw(2, 2, [0xBB, 0xCC]); + var file = ChunkFactory.File(r1, r2); + + Assert.Equal(2, file.RootChunks.Count); + } + + #endregion +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs new file mode 100644 index 00000000..3445ebc2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs @@ -0,0 +1,45 @@ +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; + +public class ChunkFileBinaryEquivalenceTest +{ + [Fact] + public void StructuredFile_And_RawFile_ProduceSameBinary() + { + var structuredBytes = TestChunkFileData.StructuredFile.Bytes; + var rawBytes = TestChunkFileData.RawFile.Bytes; + + Assert.Equal(structuredBytes, rawBytes); + } + + [Fact] + public void StructuredFile_MatchesExpectedBytes() + { + Assert.Equal(TestChunkFileData.ExpectedBytes, TestChunkFileData.StructuredFile.Bytes); + } + + [Fact] + public void RawFile_MatchesExpectedBytes() + { + Assert.Equal(TestChunkFileData.ExpectedBytes, TestChunkFileData.RawFile.Bytes); + } + + [Fact] + public void StructuredFile_HasExpectedRootCount() + { + Assert.Equal(3, TestChunkFileData.StructuredFile.RootChunks.Count); + } + + [Fact] + public void RawFile_HasExpectedRootCount() + { + Assert.Equal(3, TestChunkFileData.RawFile.RootChunks.Count); + } + + [Fact] + public void Files_HaveSameSize() + { + Assert.Equal(TestChunkFileData.StructuredFile.Size, TestChunkFileData.RawFile.Size); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs new file mode 100644 index 00000000..b3a7e702 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class ChunkFileTest +{ + private static RawChunk CreateRoot(uint type, byte[] data) + { + return new RawChunk(new ChunkMetadata(type, (uint)data.Length), data); + } + + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var root = CreateRoot(1, [0xAA]); + var file = new ChunkFile([root]); + + Assert.Single(file.RootChunks); + Assert.Same(root, file.RootChunks[0]); + } + + [Fact] + public void Ctor_ThrowsOnNull() + { + Assert.Throws(() => new ChunkFile(null!)); + } + + [Fact] + public void Ctor_ThrowsOnEmpty() + { + Assert.Throws(() => new ChunkFile([])); + } + + [Fact] + public void Size_SumsRootChunkSizes() + { + var r1 = CreateRoot(1, [0xAA]); + var r2 = CreateRoot(2, [0xBB, 0xCC]); + var file = new ChunkFile([r1, r2]); + + Assert.Equal(r1.Size + r2.Size, file.Size); + } + + [Fact] + public void Bytes_ReturnsCorrectBinary() + { + var r1 = CreateRoot(1, [0xAA]); + var file = new ChunkFile([r1]); + + var bytes = file.Bytes; + Assert.Equal(file.Size, bytes.Length); + Assert.Equal(r1.Bytes, bytes); + } + + [Fact] + public void GetBytes_WritesMultipleRootChunks() + { + var r1 = CreateRoot(1, [0xAA]); + var r2 = CreateRoot(2, [0xBB]); + var file = new ChunkFile([r1, r2]); + + var bytes = file.Bytes; + var r1Bytes = r1.Bytes; + var r2Bytes = r2.Bytes; + + for (var i = 0; i < r1Bytes.Length; i++) + Assert.Equal(r1Bytes[i], bytes[i]); + for (var i = 0; i < r2Bytes.Length; i++) + Assert.Equal(r2Bytes[i], bytes[r1.Size + i]); + } + + [Fact] + public void WriteTo_WritesToStream() + { + var r1 = CreateRoot(1, [0xAA]); + var file = new ChunkFile([r1]); + + using var ms = new MemoryStream(); + file.WriteTo(ms); + + Assert.Equal(file.Bytes, ms.ToArray()); + } + + [Fact] + public void WriteTo_ThrowsOnNullStream() + { + var r1 = CreateRoot(1, [0xAA]); + var file = new ChunkFile([r1]); + + Assert.Throws(() => file.WriteTo(null!)); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs new file mode 100644 index 00000000..de47ce76 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs @@ -0,0 +1,81 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class DataChunkTest +{ + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 3); + var chunk = new DataChunk(info, data); + + Assert.Equal(info, chunk.Info); + Assert.Equal(data, chunk.Data.ToArray()); + } + + [Fact] + public void Ctor_ThrowsOnEmptyData() + { + var info = new ChunkMetadata(0x10, 0); + Assert.Throws(() => new DataChunk(info, ReadOnlyMemory.Empty)); + } + + [Fact] + public void Ctor_ThrowsWhenBit31Set() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 0x8000_0003u); + Assert.Throws(() => new DataChunk(info, data)); + } + + [Fact] + public void Ctor_ThrowsWhenSizeMismatch() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 5); + Assert.Throws(() => new DataChunk(info, data)); + } + + [Fact] + public void Size_IncludesHeaderAndData() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 3); + var chunk = new DataChunk(info, data); + + // Header is 8 bytes (sizeof ChunkMetadata) + 3 bytes data + Assert.Equal(8 + 3, chunk.Size); + } + + [Fact] + public void GetBytes_WritesCorrectBinary() + { + var data = new byte[] { 0xAA, 0xBB }; + var info = new ChunkMetadata(0x01, 2); + var chunk = new DataChunk(info, data); + + var bytes = chunk.Bytes; + Assert.Equal(10, bytes.Length); + + // Type (LE) + Assert.Equal(0x01, bytes[0]); + Assert.Equal(0x00, bytes[1]); + Assert.Equal(0x00, bytes[2]); + Assert.Equal(0x00, bytes[3]); + + // Size (LE) - no bit 31 + Assert.Equal(0x02, bytes[4]); + Assert.Equal(0x00, bytes[5]); + Assert.Equal(0x00, bytes[6]); + Assert.Equal(0x00, bytes[7]); + + // Data + Assert.Equal(0xAA, bytes[8]); + Assert.Equal(0xBB, bytes[9]); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/ChunkMetadataTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/ChunkMetadataTest.cs new file mode 100644 index 00000000..6429bbec --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/ChunkMetadataTest.cs @@ -0,0 +1,90 @@ +using System.Runtime.CompilerServices; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model.Metadata; + +public class ChunkMetadataTest +{ + [Fact] + public void SizeOf_Is8Bytes() + { + Assert.Equal(8, Unsafe.SizeOf()); + } + + [Fact] + public void Ctor_SetsTypeAndRawSize() + { + var meta = new ChunkMetadata(0x1234u, 0x5678u); + Assert.Equal(0x1234u, meta.Type); + Assert.Equal(0x5678u, meta.RawSize); + } + + [Fact] + public void Default_HasZeroValues() + { + var meta = default(ChunkMetadata); + Assert.Equal(0u, meta.Type); + Assert.Equal(0u, meta.RawSize); + Assert.False(meta.HasChildrenHint); + Assert.Equal(0, meta.BodySize); + Assert.Equal((uint)meta.BodySize, meta.RawSize); + } + + [Fact] + public void HasChildrenHint_ReturnsFalse_WhenBit31NotSet() + { + var meta = new ChunkMetadata(1, 0x7FFF_FFFFu); + Assert.Equal(0x7FFF_FFFFu, meta.RawSize); + Assert.False(meta.HasChildrenHint); + } + + [Fact] + public void HasChildrenHint_ReturnsTrue_WhenBit31Set() + { + var meta = new ChunkMetadata(1, 0x8000_0000u); + Assert.Equal(0x8000_0000u, meta.RawSize); + Assert.True(meta.HasChildrenHint); + } + + [Fact] + public void HasChildrenHint_ReturnsTrue_WhenAllBitsSet() + { + var meta = new ChunkMetadata(1, 0xFFFF_FFFFu); + Assert.Equal(0xFFFF_FFFFu, meta.RawSize); + Assert.True(meta.HasChildrenHint); + } + + [Fact] + public void BodySize_MasksBit31Off() + { + var meta = new ChunkMetadata(1, 0x8000_0005u); + Assert.Equal(0x8000_0005u, meta.RawSize); + Assert.Equal(5, meta.BodySize); + } + + [Fact] + public void BodySize_ReturnsRawSize_WhenBit31NotSet() + { + var meta = new ChunkMetadata(1, 100u); + Assert.Equal(100u, meta.RawSize); + Assert.Equal(100, meta.BodySize); + Assert.Equal((uint)meta.BodySize, meta.RawSize); + } + + [Fact] + public void BodySize_ReturnsMaxValue_WhenAllLower31BitsSet() + { + var meta = new ChunkMetadata(1, 0xFFFF_FFFFu); + Assert.Equal(0xFFFF_FFFFu, meta.RawSize); + Assert.Equal(int.MaxValue, meta.BodySize); + } + + [Fact] + public void BodySize_ReturnsZero_WhenOnlyBit31Set() + { + var meta = new ChunkMetadata(1, 0x8000_0000u); + Assert.Equal(0x8000_0000u, meta.RawSize); + Assert.Equal(0, meta.BodySize); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/MiniChunkMetadataTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/MiniChunkMetadataTest.cs new file mode 100644 index 00000000..20b25107 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/Metadata/MiniChunkMetadataTest.cs @@ -0,0 +1,38 @@ +using System.Runtime.CompilerServices; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model.Metadata; + +public class MiniChunkMetadataTest +{ + [Fact] + public void SizeOf_Is2Bytes() + { + Assert.Equal(2, Unsafe.SizeOf()); + } + + [Fact] + public void Ctor_SetsTypeAndBodySize() + { + var meta = new MiniChunkMetadata(0x0A, 0xFF); + Assert.Equal(0x0A, meta.Type); + Assert.Equal(0xFF, meta.BodySize); + } + + [Fact] + public void Default_HasZeroValues() + { + var meta = default(MiniChunkMetadata); + Assert.Equal(0, meta.Type); + Assert.Equal(0, meta.BodySize); + } + + [Fact] + public void Ctor_MaxValues() + { + var meta = new MiniChunkMetadata(byte.MaxValue, byte.MaxValue); + Assert.Equal(byte.MaxValue, meta.Type); + Assert.Equal(byte.MaxValue, meta.BodySize); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs new file mode 100644 index 00000000..bbb3562b --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs @@ -0,0 +1,71 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class MiniChunkTest +{ + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var data = new byte[] { 0xAA, 0xBB }; + var info = new MiniChunkMetadata(0x05, 2); + var chunk = new MiniChunk(info, data); + + Assert.Equal(info, chunk.Info); + Assert.Equal(data, chunk.Data.ToArray()); + } + + [Fact] + public void Ctor_ThrowsOnEmptyData() + { + var info = new MiniChunkMetadata(0x05, 0); + Assert.Throws(() => new MiniChunk(info, ReadOnlyMemory.Empty)); + } + + [Fact] + public void Ctor_ThrowsWhenSizeMismatch() + { + var data = new byte[] { 1, 2, 3 }; + var info = new MiniChunkMetadata(0x05, 5); + Assert.Throws(() => new MiniChunk(info, data)); + } + + [Fact] + public void Size_IncludesHeaderAndData() + { + var data = new byte[] { 1, 2, 3 }; + var info = new MiniChunkMetadata(0x05, 3); + var chunk = new MiniChunk(info, data); + + // Header is 2 bytes (sizeof MiniChunkMetadata) + 3 bytes data + Assert.Equal(2 + 3, chunk.Size); + } + + [Fact] + public void GetBytes_WritesCorrectBinary() + { + var data = new byte[] { 0xDD }; + var info = new MiniChunkMetadata(0x0A, 1); + var chunk = new MiniChunk(info, data); + + var bytes = chunk.Bytes; + Assert.Equal(3, bytes.Length); + + Assert.Equal(0x0A, bytes[0]); // Type + Assert.Equal(0x01, bytes[1]); // Size + Assert.Equal(0xDD, bytes[2]); // Data + } + + [Fact] + public void IsChunk_ButNotRootChunk() + { + var data = new byte[] { 1 }; + var info = new MiniChunkMetadata(1, 1); + var chunk = new MiniChunk(info, data); + Assert.IsAssignableFrom(chunk); + Assert.IsNotType(chunk); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs new file mode 100644 index 00000000..a4ac8068 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs @@ -0,0 +1,89 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class MiniNodeChunkTest +{ + private static MiniChunk CreateMiniChild(byte type, byte[] data) + { + return new MiniChunk(new MiniChunkMetadata(type, (byte)data.Length), data); + } + + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var child = CreateMiniChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x30, (uint)child.Size); + var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); + + Assert.Equal(info, chunk.Info); + Assert.Single(chunk.Children); + Assert.Same(child, chunk.Children[0]); + } + + [Fact] + public void Ctor_ThrowsOnNullChildren() + { + var info = new ChunkMetadata(0x30, 0); + Assert.Throws(() => new MiniNodeChunk(info, null!)); + } + + [Fact] + public void Ctor_ThrowsOnEmptyChildren() + { + var info = new ChunkMetadata(0x30, 0); + Assert.Throws(() => new MiniNodeChunk(info, Array.Empty())); + } + + [Fact] + public void Ctor_ThrowsWhenSizeMismatch() + { + var child = CreateMiniChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x30, 999); + Assert.Throws(() => new MiniNodeChunk(info, new MiniChunk[] { child })); + } + + [Fact] + public void Ctor_ThrowsWhenRawSizeExceedsIntMax() + { + var child = CreateMiniChild(1, new byte[] { 0xAA }); + // RawSize with bit 31 set exceeds int.MaxValue + var info = new ChunkMetadata(0x30, 0x8000_0000u | (uint)child.Size); + Assert.Throws(() => new MiniNodeChunk(info, new MiniChunk[] { child })); + } + + [Fact] + public void Size_IncludesHeaderAndChildren() + { + var child = CreateMiniChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x30, (uint)child.Size); + var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); + + Assert.Equal(8 + child.Size, chunk.Size); + } + + [Fact] + public void IsRootChunk() + { + var child = CreateMiniChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x30, (uint)child.Size); + var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); + Assert.IsAssignableFrom(chunk); + } + + [Fact] + public void MultipleChildren() + { + var c1 = CreateMiniChild(1, new byte[] { 0xAA }); + var c2 = CreateMiniChild(2, new byte[] { 0xBB, 0xCC }); + var totalChildSize = c1.Size + c2.Size; + var info = new ChunkMetadata(0x30, (uint)totalChildSize); + var chunk = new MiniNodeChunk(info, new MiniChunk[] { c1, c2 }); + + Assert.Equal(2, chunk.Children.Count); + Assert.Equal(8 + totalChildSize, chunk.Size); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs new file mode 100644 index 00000000..55bf0e77 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs @@ -0,0 +1,103 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class NodeChunkTest +{ + private static RawChunk CreateChild(uint type, byte[] data) + { + return new RawChunk(new ChunkMetadata(type, (uint)data.Length), data); + } + + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var child = CreateChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); + var chunk = new NodeChunk(info, new RootChunk[] { child }); + + Assert.Equal(info, chunk.Info); + Assert.Single(chunk.Children); + Assert.Same(child, chunk.Children[0]); + } + + [Fact] + public void Ctor_ThrowsWhenBit31NotSet() + { + var child = CreateChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x20, (uint)child.Size); + Assert.Throws(() => new NodeChunk(info, new RootChunk[] { child })); + } + + [Fact] + public void Ctor_ThrowsOnNullChildren() + { + var info = new ChunkMetadata(0x20, 0x8000_0000u); + Assert.Throws(() => new NodeChunk(info, null!)); + } + + [Fact] + public void Ctor_ThrowsOnEmptyChildren() + { + var info = new ChunkMetadata(0x20, 0x8000_0000u); + Assert.Throws(() => new NodeChunk(info, Array.Empty())); + } + + [Fact] + public void Ctor_ThrowsWhenSizeMismatch() + { + var child = CreateChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x20, 0x8000_0000u | 999u); + Assert.Throws(() => new NodeChunk(info, new RootChunk[] { child })); + } + + [Fact] + public void Size_IncludesHeaderAndChildren() + { + var child = CreateChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); + var chunk = new NodeChunk(info, new RootChunk[] { child }); + + Assert.Equal(8 + child.Size, chunk.Size); + } + + [Fact] + public void GetBytes_WritesHeaderAndChildBytes() + { + var child = CreateChild(1, new byte[] { 0xFF }); + var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); + var chunk = new NodeChunk(info, new RootChunk[] { child }); + + var bytes = chunk.Bytes; + Assert.Equal(chunk.Size, bytes.Length); + + var childBytes = child.Bytes; + for (var i = 0; i < childBytes.Length; i++) + Assert.Equal(childBytes[i], bytes[8 + i]); + } + + [Fact] + public void MultipleChildren() + { + var c1 = CreateChild(1, new byte[] { 0xAA }); + var c2 = CreateChild(2, new byte[] { 0xBB, 0xCC }); + var totalChildSize = c1.Size + c2.Size; + var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)totalChildSize); + var chunk = new NodeChunk(info, new RootChunk[] { c1, c2 }); + + Assert.Equal(2, chunk.Children.Count); + Assert.Equal(8 + totalChildSize, chunk.Size); + } + + [Fact] + public void IsRootChunk() + { + var child = CreateChild(1, new byte[] { 0xAA }); + var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); + var chunk = new NodeChunk(info, new RootChunk[] { child }); + Assert.IsAssignableFrom(chunk); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs new file mode 100644 index 00000000..8ef2edda --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs @@ -0,0 +1,88 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Model; + +public class RawChunkTest +{ + [Fact] + public void Ctor_ValidArgs_SetsProperties() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 3); + var chunk = new RawChunk(info, data); + + Assert.Equal(info, chunk.Info); + Assert.Equal(data, chunk.Data.ToArray()); + } + + [Fact] + public void Ctor_AllowsBit31Set() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 0x8000_0003u); + var chunk = new RawChunk(info, data); + Assert.True(chunk.Info.HasChildrenHint); + } + + [Fact] + public void Ctor_ThrowsOnEmptyData() + { + var info = new ChunkMetadata(0x10, 0); + Assert.Throws(() => new RawChunk(info, ReadOnlyMemory.Empty)); + } + + [Fact] + public void Ctor_ThrowsWhenSizeMismatch() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 5); + Assert.Throws(() => new RawChunk(info, data)); + } + + [Fact] + public void Size_IncludesHeaderAndData() + { + var data = new byte[] { 1, 2, 3 }; + var info = new ChunkMetadata(0x10, 3); + var chunk = new RawChunk(info, data); + Assert.Equal(8 + 3, chunk.Size); + } + + [Fact] + public void GetBytes_WritesRawSizeIncludingBit31() + { + var data = new byte[] { 0xCC }; + var info = new ChunkMetadata(0x02, 0x8000_0001u); + var chunk = new RawChunk(info, data); + + var bytes = chunk.Bytes; + Assert.Equal(9, bytes.Length); + + // Type (LE) + Assert.Equal(0x02, bytes[0]); + Assert.Equal(0x00, bytes[1]); + Assert.Equal(0x00, bytes[2]); + Assert.Equal(0x00, bytes[3]); + + // RawSize (LE) - bit 31 preserved + Assert.Equal(0x01, bytes[4]); + Assert.Equal(0x00, bytes[5]); + Assert.Equal(0x00, bytes[6]); + Assert.Equal(0x80, bytes[7]); + + // Data + Assert.Equal(0xCC, bytes[8]); + } + + [Fact] + public void IsRootChunk() + { + var data = new byte[] { 1 }; + var info = new ChunkMetadata(1, 1); + var chunk = new RawChunk(info, data); + Assert.IsAssignableFrom(chunk); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkFileReaderBaseTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkFileReaderBaseTest.cs new file mode 100644 index 00000000..ca2d4acb --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkFileReaderBaseTest.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +using PG.StarWarsGame.Files.ChunkFiles.Data; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader; + +public abstract class ChunkFileReaderBaseTest + where TReader : ChunkFileReaderBase + where TData : IChunkData +{ + protected abstract TReader CreateReader(Stream stream, bool leaveStreamOpen = false); + + protected abstract byte[] CreateValidStreamContent(); + + protected abstract void AssertReadResult(TData result); + + protected abstract void CallThrowIfChunkSizeTooLarge(TReader reader, ChunkMetadata chunk); + + [Fact] + public void Read_ReturnsChunkData() + { + var content = CreateValidStreamContent(); + using var reader = CreateReader(new MemoryStream(content)); + var result = reader.Read(); + AssertReadResult(result); + } + + [Fact] + public void Read_IChunkFileReader_ReturnsIChunkData() + { + var content = CreateValidStreamContent(); + using var reader = CreateReader(new MemoryStream(content)); + IChunkFileReader interfaceReader = reader; + var result = interfaceReader.Read(); + Assert.IsType(result); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Dispose_StreamBehaviorDependsOnLeaveOpen(bool leaveOpen) + { + var ms = new MemoryStream([1, 2, 3]); + var reader = CreateReader(ms, leaveStreamOpen: leaveOpen); + reader.Dispose(); + + if (leaveOpen) + { + ms.Position = 0; + Assert.Equal(1, ms.ReadByte()); + } + else + { + Assert.Throws(() => ms.ReadByte()); + } + } + + [Fact] + public void ThrowIfChunkSizeTooLarge_DoesNotThrow_ForNormalSize() + { + using var reader = CreateReader(new MemoryStream(new byte[8])); + var meta = new ChunkMetadata(1, 100); + CallThrowIfChunkSizeTooLarge(reader, meta); + } + + [Fact] + public void ThrowIfChunkSizeTooLarge_Throws_WhenRawSizeExceedsIntMax() + { + using var reader = CreateReader(new MemoryStream(new byte[8])); + var meta = new ChunkMetadata(1, 0x8000_0001u); + Assert.Throws(() => CallThrowIfChunkSizeTooLarge(reader, meta)); + } + + [Fact] + public void ThrowIfChunkSizeTooLarge_DoesNotThrow_AtExactIntMax() + { + using var reader = CreateReader(new MemoryStream(new byte[8])); + var meta = new ChunkMetadata(1, int.MaxValue); + CallThrowIfChunkSizeTooLarge(reader, meta); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs new file mode 100644 index 00000000..e671a595 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs @@ -0,0 +1,337 @@ +using System; +using System.IO; +using System.Text; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader; + +public class ChunkReaderTest +{ + private static MemoryStream CreateStream(byte[] data) => new(data); + + private static byte[] WriteChunkHeader(uint type, uint rawSize) + { + var bytes = new byte[8]; + BitConverter.GetBytes(type).CopyTo(bytes, 0); + BitConverter.GetBytes(rawSize).CopyTo(bytes, 4); + return bytes; + } + + [Fact] + public void Ctor_ThrowsOnNullStream() + { + Assert.Throws(() => new ChunkReader(null!)); + } + + [Fact] + public void ReadChunk_ReadsHeaderCorrectly() + { + var header = WriteChunkHeader(0x01, 0x0A); + using var reader = new ChunkReader(CreateStream(header)); + + var meta = reader.ReadChunk(); + Assert.Equal(0x01u, meta.Type); + Assert.Equal(0x0Au, meta.RawSize); + } + + [Fact] + public void ReadChunk_WithRefBytes_IncrementsBy8() + { + var header = WriteChunkHeader(0x01, 0x0A); + using var reader = new ChunkReader(CreateStream(header)); + + var readBytes = 0; + var meta = reader.ReadChunk(ref readBytes); + Assert.Equal(8, readBytes); + Assert.Equal(0x01u, meta.Type); + } + + [Fact] + public void ReadChunk_ThrowsAtEndOfStream() + { + using var reader = new ChunkReader(CreateStream(Array.Empty())); + Assert.Throws(() => reader.ReadChunk()); + } + + [Fact] + public void ReadMiniChunk_ReadsHeaderCorrectly() + { + var data = new byte[] { 0x05, 0x03 }; + using var reader = new ChunkReader(CreateStream(data)); + + var readBytes = 0; + var meta = reader.ReadMiniChunk(ref readBytes); + Assert.Equal(0x05, meta.Type); + Assert.Equal(0x03, meta.BodySize); + Assert.Equal(2, readBytes); + } + + [Fact] + public void ReadData_ChunkMetadata_ReadsBody() + { + var header = WriteChunkHeader(0x01, 3); + var body = new byte[] { 0xAA, 0xBB, 0xCC }; + var stream = new byte[header.Length + body.Length]; + header.CopyTo(stream, 0); + body.CopyTo(stream, header.Length); + + using var reader = new ChunkReader(CreateStream(stream)); + var meta = reader.ReadChunk(); + var data = reader.ReadData(meta); + + Assert.Equal(body, data); + } + + [Fact] + public void ReadData_ChunkMetadata_ThrowsForContainerChunk() + { + var header = WriteChunkHeader(0x01, 0x8000_0003u); + using var reader = new ChunkReader(CreateStream(header)); + var meta = reader.ReadChunk(); + + Assert.Throws(() => reader.ReadData(meta)); + } + + [Fact] + public void ReadData_ChunkMetadata_WithRefBytes_IncrementsCorrectly() + { + var header = WriteChunkHeader(0x01, 3); + var body = new byte[] { 0xAA, 0xBB, 0xCC }; + var stream = new byte[header.Length + body.Length]; + header.CopyTo(stream, 0); + body.CopyTo(stream, header.Length); + + using var reader = new ChunkReader(CreateStream(stream)); + var meta = reader.ReadChunk(); + var readBytes = 0; + var data = reader.ReadData(meta, ref readBytes); + + Assert.Equal(body, data); + Assert.Equal(3, readBytes); + } + + [Fact] + public void ReadData_MiniChunkMetadata_ReadsBody() + { + var miniHeader = new byte[] { 0x05, 0x02 }; + var body = new byte[] { 0xDD, 0xEE }; + var stream = new byte[miniHeader.Length + body.Length]; + miniHeader.CopyTo(stream, 0); + body.CopyTo(stream, miniHeader.Length); + + using var reader = new ChunkReader(CreateStream(stream)); + var rb = 0; + var meta = reader.ReadMiniChunk(ref rb); + var data = reader.ReadData(meta); + + Assert.Equal(body, data); + } + + [Fact] + public void ReadData_MiniChunkMetadata_WithRefBytes_IncrementsCorrectly() + { + var miniHeader = new byte[] { 0x05, 0x02 }; + var body = new byte[] { 0xDD, 0xEE }; + var stream = new byte[miniHeader.Length + body.Length]; + miniHeader.CopyTo(stream, 0); + body.CopyTo(stream, miniHeader.Length); + + using var reader = new ChunkReader(CreateStream(stream)); + var rb = 0; + var meta = reader.ReadMiniChunk(ref rb); + rb = 0; + var data = reader.ReadData(meta, ref rb); + + Assert.Equal(body, data); + Assert.Equal(2, rb); + } + + [Fact] + public void ReadData_BySize_ReadsCorrectBytes() + { + var data = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + using var reader = new ChunkReader(CreateStream(data)); + + var result = reader.ReadData(3); + Assert.Equal(new byte[] { 0x01, 0x02, 0x03 }, result); + } + + [Fact] + public void ReadData_BySize_ThrowsOnNegative() + { + using var reader = new ChunkReader(CreateStream(new byte[] { 0x01 })); + Assert.Throws(() => reader.ReadData(-1)); + } + + [Fact] + public void ReadDword_ReadsUInt32() + { + var data = BitConverter.GetBytes(0x12345678u); + using var reader = new ChunkReader(CreateStream(data)); + + var value = reader.ReadDword(); + Assert.Equal(0x12345678u, value); + } + + [Fact] + public void ReadDword_WithRefBytes_IncrementsBy4() + { + var data = BitConverter.GetBytes(42u); + using var reader = new ChunkReader(CreateStream(data)); + + var readBytes = 0; + var value = reader.ReadDword(ref readBytes); + Assert.Equal(42u, value); + Assert.Equal(4, readBytes); + } + + [Fact] + public void ReadFloat_WithRefBytes_IncrementsBy4() + { + var data = BitConverter.GetBytes(3.14f); + using var reader = new ChunkReader(CreateStream(data)); + + var readBytes = 0; + var value = reader.ReadFloat(ref readBytes); + Assert.Equal(3.14f, value); + Assert.Equal(4, readBytes); + } + + [Fact] + public void Skip_AdvancesStreamPosition() + { + var data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + using var reader = new ChunkReader(CreateStream(data)); + + reader.Skip(3); + var result = reader.ReadData(2); + Assert.Equal(new byte[] { 0x04, 0x05 }, result); + } + + [Fact] + public void Skip_WithRefBytes_IncrementsCounter() + { + var data = new byte[] { 0x01, 0x02, 0x03 }; + using var reader = new ChunkReader(CreateStream(data)); + + var readBytes = 0; + reader.Skip(2, ref readBytes); + Assert.Equal(2, readBytes); + } + + [Fact] + public void TryReadChunk_ReturnsNull_AtEndOfStream() + { + using var reader = new ChunkReader(CreateStream(Array.Empty())); + var result = reader.TryReadChunk(); + Assert.Null(result); + } + + [Fact] + public void TryReadChunk_ReturnsMetadata_WhenDataAvailable() + { + var header = WriteChunkHeader(0x42, 0x10); + using var reader = new ChunkReader(CreateStream(header)); + + var result = reader.TryReadChunk(); + Assert.NotNull(result); + Assert.Equal(0x42u, result.Value.Type); + Assert.Equal(0x10u, result.Value.RawSize); + } + + [Fact] + public void TryReadChunk_WithRefBytes_IncrementsBy8() + { + var header = WriteChunkHeader(0x42, 0x10); + using var reader = new ChunkReader(CreateStream(header)); + + var readBytes = 0; + var result = reader.TryReadChunk(ref readBytes); + Assert.NotNull(result); + Assert.Equal(8, readBytes); + } + + [Fact] + public void TryReadChunk_WithRefBytes_DoesNotIncrement_AtEnd() + { + using var reader = new ChunkReader(CreateStream(Array.Empty())); + + var readBytes = 5; + var result = reader.TryReadChunk(ref readBytes); + Assert.Null(result); + Assert.Equal(5, readBytes); + } + + [Fact] + public void ReadString_ReadsCorrectly() + { + var text = "Hello"; + var encoded = Encoding.ASCII.GetBytes(text); + using var reader = new ChunkReader(CreateStream(encoded)); + + var readBytes = 0; + var result = reader.ReadString(encoded.Length, Encoding.ASCII, false, ref readBytes); + Assert.Equal(text, result); + Assert.Equal(encoded.Length, readBytes); + } + + [Fact] + public void ReadString_ZeroTerminated_TrimsAtNull() + { + var encoded = new byte[] { (byte)'H', (byte)'i', 0, (byte)'X' }; + using var reader = new ChunkReader(CreateStream(encoded)); + + var result = reader.ReadString(encoded.Length, Encoding.ASCII, true); + Assert.Equal("Hi", result); + } + + [Fact] + public void ReadString_WithoutRefBytes_Works() + { + var text = "AB"; + var encoded = Encoding.ASCII.GetBytes(text); + using var reader = new ChunkReader(CreateStream(encoded)); + + var result = reader.ReadString(encoded.Length, Encoding.ASCII, false); + Assert.Equal(text, result); + } + + [Fact] + public void Dispose_DisposesUnderlyingStream() + { + var ms = CreateStream(new byte[] { 1, 2, 3 }); + var reader = new ChunkReader(ms); + reader.Dispose(); + + Assert.Throws(() => ms.ReadByte()); + } + + [Fact] + public void Dispose_LeaveOpen_KeepsStreamOpen() + { + var ms = CreateStream(new byte[] { 1, 2, 3 }); + var reader = new ChunkReader(ms, leaveOpen: true); + reader.Dispose(); + + // Stream should still be usable + ms.Position = 0; + Assert.Equal(1, ms.ReadByte()); + } + + [Fact] + public void ReadChunk_Roundtrip_WithChunkFactory() + { + var original = PG.StarWarsGame.Files.ChunkFiles.Binary.ChunkFactory.Data(0x42, new byte[] { 1, 2, 3 }); + var bytes = original.Bytes; + + using var reader = new ChunkReader(CreateStream(bytes)); + var meta = reader.ReadChunk(); + var data = reader.ReadData(meta); + + Assert.Equal(0x42u, meta.Type); + Assert.Equal(3, meta.BodySize); + Assert.False(meta.HasChildrenHint); + Assert.Equal(new byte[] { 1, 2, 3 }, data); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs new file mode 100644 index 00000000..c0ffa408 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs @@ -0,0 +1,21 @@ +using System.IO; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader; + +public sealed class TestChunkFileReader(Stream stream, bool leaveStreamOpen = false) + : ChunkFileReaderBase(stream, leaveStreamOpen) +{ + public override TestChunkFileReaderTest.TestChunkData Read() + { + var meta = ChunkReader.ReadChunk(); + var data = ChunkReader.ReadData(meta); + return new TestChunkFileReaderTest.TestChunkData(data); + } + + public void CallThrowIfChunkSizeTooLarge(ChunkMetadata chunk) + { + ThrowIfChunkSizeTooLargeException(chunk); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs new file mode 100644 index 00000000..0f4ebecc --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs @@ -0,0 +1,45 @@ +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; +using PG.StarWarsGame.Files.ChunkFiles.Data; +using System; +using System.IO; +using Xunit; +using static PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader.TestChunkFileReaderTest; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader; + +public sealed class TestChunkFileReaderTest : ChunkFileReaderBaseTest +{ + public sealed class TestChunkData(byte[] data) : IChunkData + { + public byte[] Data { get; } = data; + + public void Dispose() { } + } + + protected override TestChunkFileReader CreateReader(Stream stream, bool leaveStreamOpen = false) + { + return new TestChunkFileReader(stream, leaveStreamOpen); + } + + protected override byte[] CreateValidStreamContent() + { + var header = new byte[8]; + BitConverter.GetBytes(0x01u).CopyTo(header, 0); + BitConverter.GetBytes(3u).CopyTo(header, 4); + var body = new byte[] { 0xAA, 0xBB, 0xCC }; + var stream = new byte[header.Length + body.Length]; + header.CopyTo(stream, 0); + body.CopyTo(stream, header.Length); + return stream; + } + + protected override void AssertReadResult(TestChunkData result) + { + Assert.Equal(new byte[] { 0xAA, 0xBB, 0xCC }, result.Data); + } + + protected override void CallThrowIfChunkSizeTooLarge(TestChunkFileReader reader, ChunkMetadata chunk) + { + reader.CallThrowIfChunkSizeTooLarge(chunk); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs new file mode 100644 index 00000000..5dd8627a --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs @@ -0,0 +1,103 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; + +/// +/// Provides a reusable, complex test chunk file built using . +/// The file structure is: +/// +/// Root[0]: DataChunk(type=0x01, data=[0xAA, 0xBB, 0xCC]) +/// Root[1]: NodeChunk(type=0x10) +/// Child[0]: DataChunk(type=0x02, data=[0x11, 0x22]) +/// Child[1]: NodeChunk(type=0x20) +/// Child[0]: DataChunk(type=0x03, data=[0x33]) +/// Child[2]: MiniNodeChunk(type=0x30) +/// Mini[0]: MiniChunk(type=0x04, data=[0x44, 0x55]) +/// Mini[1]: MiniChunk(type=0x05, data=[0x66]) +/// Root[2]: DataChunk(type=0x06, data=[0x77, 0x88, 0x99, 0xDD]) +/// +/// +public static class TestChunkFileData +{ + /// + /// Gets a complex chunk file built using structured chunk types (NodeChunk, MiniNodeChunk, and DataChunk leaves). + /// + public static ChunkFile StructuredFile { get; } = BuildStructuredFile(); + + /// + /// Gets a chunk file built entirely from RawChunk instances that produce the same binary as . + /// + public static ChunkFile RawFile { get; } = BuildRawFile(); + + /// + /// Gets the expected binary representation of the test chunk file. + /// + public static byte[] ExpectedBytes { get; } = StructuredFile.Bytes; + + private static ChunkFile BuildStructuredFile() + { + // Root[0]: leaf data chunk + var root0 = ChunkFactory.Data(0x01, [0xAA, 0xBB, 0xCC]); + + // Build children for Root[1] NodeChunk + var child0 = ChunkFactory.Data(0x02, [0x11, 0x22]); + + var grandchild = ChunkFactory.Data(0x03, [0x33]); + var child1 = ChunkFactory.Node(0x20u, grandchild); + + // MiniNodeChunk as a child - serialize mini-chunks into raw body + var miniChild0 = ChunkFactory.Mini(0x04, [0x44, 0x55]); + var miniChild1 = ChunkFactory.Mini(0x05, [0x66]); + var child2 = ChunkFactory.Node(0x30u, miniChild0, miniChild1); + + var root1 = ChunkFactory.Node(0x10u, child0, child1, child2); + + // Root[2]: leaf data chunk + var root2 = ChunkFactory.Data(0x06, [0x77, 0x88, 0x99, 0xDD]); + + return ChunkFactory.File(root0, root1, root2); + } + + private static ChunkFile BuildRawFile() + { + // Build the exact same binary using only RawChunk instances. + + // Root[0] + var root0 = ChunkFactory.Raw(0x01, 3, [0xAA, 0xBB, 0xCC]); + + // child0: RawChunk(type=0x02, size=2, data=[0x11, 0x22]) -> 10 bytes total + var rawChild0 = ChunkFactory.Raw(0x02, 2, [0x11, 0x22]); + + // grandchild: RawChunk(type=0x03, size=1, data=[0x33]) -> 9 bytes total + // child1: NodeChunk(type=0x20) wrapping grandchild -> body = grandchild.Bytes (9 bytes) + var grandchildBytes = ChunkFactory.Raw(0x03, 1, [0x33]).Bytes; + var rawChild1 = ChunkFactory.Raw(0x20, 0x8000_0000u | (uint)grandchildBytes.Length, grandchildBytes); + + // child2: MiniNodeChunk(type=0x30) with 2 mini-chunks + // Mini(0x04,[0x44,0x55]) = 4 bytes, Mini(0x05,[0x66]) = 3 bytes -> body = 7 bytes + var mc0 = ChunkFactory.Mini(0x04, [0x44, 0x55]); + var mc1 = ChunkFactory.Mini(0x05, [0x66]); + var miniBody = new byte[mc0.Size + mc1.Size]; + mc0.GetBytes(miniBody); + mc1.GetBytes(((Span)miniBody).Slice(mc0.Size)); + var rawChild2 = ChunkFactory.Raw(0x30, (uint)miniBody.Length, miniBody); + + // Root[1]: NodeChunk(type=0x10) body = rawChild0 + rawChild1 + rawChild2 + var root1BodySize = rawChild0.Size + rawChild1.Size + rawChild2.Size; + var root1Body = new byte[root1BodySize]; + var offset = 0; + rawChild0.GetBytes(root1Body); + offset += rawChild0.Size; + rawChild1.GetBytes(((Span)root1Body).Slice(offset)); + offset += rawChild1.Size; + rawChild2.GetBytes(((Span)root1Body).Slice(offset)); + var root1 = ChunkFactory.Raw(0x10, 0x8000_0000u | (uint)root1BodySize, root1Body); + + // Root[2] + var root2 = ChunkFactory.Raw(0x06, 4, [0x77, 0x88, 0x99, 0xDD]); + + return ChunkFactory.File(root0, root1, root2); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs index 29f61617..d384d069 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/ChunkFactory.cs @@ -32,7 +32,6 @@ public static DataChunk Data(uint type, byte[] data) { if (data == null) throw new ArgumentNullException(nameof(data)); - var metadata = new ChunkMetadata(type, (uint)data.Length); return new DataChunk(metadata, data); } @@ -49,7 +48,6 @@ public static RawChunk Raw(uint type, uint rawSize, byte[] data) { if (data == null) throw new ArgumentNullException(nameof(data)); - var metadata = new ChunkMetadata(type, rawSize); return new RawChunk(metadata, data); } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs index 0b5b820d..f4d03188 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs @@ -7,7 +7,7 @@ namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model; /// /// A chunk containing binary data. /// -public sealed class DataChunk : Chunk +public sealed class DataChunk : RootChunk { /// /// Gets the chunk metadata. diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs index bd336b05..22940ba6 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs @@ -44,9 +44,15 @@ public sealed class RawChunk : RootChunk /// /// The chunk metadata, written as-is. No validation is performed on bit 31. /// The raw body data. - /// body size does not match the data length. + /// + /// is empty, or + /// body size does not match the data length. + /// public RawChunk(ChunkMetadata info, ReadOnlyMemory data) { + if (data is { IsEmpty: true, Length: 0 }) + throw new ArgumentException("Data cannot be empty.", nameof(data)); + if (info.BodySize != data.Length) throw new ArgumentException( $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", From 3fd8ea29e0ff58f092e5d28c9e275b856c5afaf8 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 30 Mar 2026 16:08:51 +0200 Subject: [PATCH 14/17] support empty data and childs --- .../Binary/ChunkFactoryTest.cs | 30 +-- .../Binary/ChunkFileBinaryEquivalenceTest.cs | 45 ----- .../Binary/ChunkFileIntegrationTest.cs | 140 ++++++++++++++ .../Binary/Model/ChunkFileTest.cs | 24 +-- .../Binary/Model/DataChunkTest.cs | 27 ++- .../Binary/Model/MiniChunkTest.cs | 22 ++- .../Binary/Model/MiniNodeChunkTest.cs | 74 ++++++-- .../Binary/Model/NodeChunkTest.cs | 74 +++++--- .../Binary/Model/RawChunkTest.cs | 27 ++- .../Binary/RawChunkEquivalenceTest.cs | 173 ++++++++++++++++++ .../Binary/Reader/ChunkReaderTest.cs | 85 ++++++++- .../Binary/TestChunkFileData.cs | 122 ++++++------ ....StarWarsGame.Files.ChunkFiles.Test.csproj | 6 +- .../Binary/Model/ChunkFile.cs | 24 +-- .../Binary/Model/DataChunk.cs | 4 - .../Binary/Model/MiniChunk.cs | 6 +- .../Binary/Model/MiniNodeChunk.cs | 2 +- .../Binary/Model/NodeChunk.cs | 2 +- .../Binary/Model/NodeChunkBase.cs | 7 +- .../Binary/Model/RawChunk.cs | 4 - .../PG.StarWarsGame.Files.TED.Test.csproj | 1 + 21 files changed, 661 insertions(+), 238 deletions(-) delete mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileIntegrationTest.cs create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/RawChunkEquivalenceTest.cs diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs index 0b08f9fc..a5228afb 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFactoryTest.cs @@ -27,9 +27,10 @@ public void Data_ThrowsOnNull() } [Fact] - public void Data_ThrowsOnEmptyArray() + public void Data_AllowsEmptyArray() { - Assert.Throws(() => ChunkFactory.Data(1, [])); + var chunk = ChunkFactory.Data(1, []); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -61,9 +62,10 @@ public void Raw_ThrowsOnNull() } [Fact] - public void Raw_ThrowsOnEmptyData() + public void Raw_AllowsEmptyData() { - Assert.Throws(() => ChunkFactory.Raw(0x01, 0, [])); + var chunk = ChunkFactory.Raw(0x01, 0, []); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -123,9 +125,10 @@ public void Mini_AllowsMaxLength255() } [Fact] - public void Mini_ThrowsOnEmptyArray() + public void Mini_AllowsEmptyArray() { - Assert.Throws(() => ChunkFactory.Mini(1, [])); + var chunk = ChunkFactory.Mini(1, []); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -158,9 +161,10 @@ public void Node_RootChunk_ThrowsOnNull() } [Fact] - public void Node_RootChunk_ThrowsOnEmpty() + public void Node_RootChunk_AllowsEmpty() { - Assert.Throws(() => ChunkFactory.Node(1, Array.Empty())); + var node = ChunkFactory.Node(1, Array.Empty()); + Assert.Empty(node.Children); } [Fact] @@ -219,9 +223,10 @@ public void Node_MiniChunk_ThrowsOnNull() } [Fact] - public void Node_MiniChunk_ThrowsOnEmpty() + public void Node_MiniChunk_AllowsEmpty() { - Assert.Throws(() => ChunkFactory.Node(1, Array.Empty())); + var node = ChunkFactory.Node(1, Array.Empty()); + Assert.Empty(node.Children); } [Fact] @@ -265,9 +270,10 @@ public void File_ThrowsOnNull() } [Fact] - public void File_ThrowsOnEmpty() + public void File_AllowsEmpty() { - Assert.Throws(() => ChunkFactory.File()); + var file = ChunkFactory.File(); + Assert.Empty(file.RootChunks); } [Fact] diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs deleted file mode 100644 index 3445ebc2..00000000 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileBinaryEquivalenceTest.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Xunit; - -namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; - -public class ChunkFileBinaryEquivalenceTest -{ - [Fact] - public void StructuredFile_And_RawFile_ProduceSameBinary() - { - var structuredBytes = TestChunkFileData.StructuredFile.Bytes; - var rawBytes = TestChunkFileData.RawFile.Bytes; - - Assert.Equal(structuredBytes, rawBytes); - } - - [Fact] - public void StructuredFile_MatchesExpectedBytes() - { - Assert.Equal(TestChunkFileData.ExpectedBytes, TestChunkFileData.StructuredFile.Bytes); - } - - [Fact] - public void RawFile_MatchesExpectedBytes() - { - Assert.Equal(TestChunkFileData.ExpectedBytes, TestChunkFileData.RawFile.Bytes); - } - - [Fact] - public void StructuredFile_HasExpectedRootCount() - { - Assert.Equal(3, TestChunkFileData.StructuredFile.RootChunks.Count); - } - - [Fact] - public void RawFile_HasExpectedRootCount() - { - Assert.Equal(3, TestChunkFileData.RawFile.RootChunks.Count); - } - - [Fact] - public void Files_HaveSameSize() - { - Assert.Equal(TestChunkFileData.StructuredFile.Size, TestChunkFileData.RawFile.Size); - } -} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileIntegrationTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileIntegrationTest.cs new file mode 100644 index 00000000..e571243c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/ChunkFileIntegrationTest.cs @@ -0,0 +1,140 @@ +using System.IO; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; + +/// +/// Integration test that reads the binary representation of +/// using and reconstructs the original chunk tree structure, +/// asserting that every chunk type, size, and data value matches the expected layout. +/// +public class ChunkFileIntegrationTest +{ + /// + /// Reads the full test chunk file from and + /// verifies that the reconstructed chunk tree matches the documented structure exactly. + /// + /// Root[0]: DataChunk(type=0x01, data=[0xAA, 0xBB, 0xCC]) + /// Root[1]: NodeChunk(type=0x10) + /// Child[0]: DataChunk(type=0x02, data=[0x11, 0x22]) + /// Child[1]: NodeChunk(type=0x20) + /// Child[0]: DataChunk(type=0x03, data=[0x33]) + /// Child[2]: MiniNodeChunk(type=0x30) + /// Mini[0]: MiniChunk(type=0x04, data=[0x44, 0x55]) + /// Mini[1]: MiniChunk(type=0x05, data=[0x66]) + /// Root[2]: DataChunk(type=0x06, data=[0x77, 0x88, 0x99, 0xDD]) + /// Root[3]: DataChunk(type=0x07, data=[]) + /// Root[4]: NodeChunk(type=0x11, children=[]) + /// Root[5]: MiniNodeChunk(type=0x31) + /// Mini[0]: MiniChunk(type=0x08, data=[]) + /// Root[6]: MiniNodeChunk(type=0x32, children=[]) + /// + /// + [Fact] + public void Read_ReconstructsFullChunkTree() + { + var bytes = TestChunkFileData.ExpectedBytes; + using var stream = new MemoryStream(bytes.ToArray()); + using var reader = new ChunkReader(stream, leaveOpen: true); + + // Root[0]: DataChunk(type=0x01, data=[0xAA, 0xBB, 0xCC]) + var root0 = reader.ReadChunk(); + Assert.Equal(0x01u, root0.Type); + Assert.False(root0.HasChildrenHint); + Assert.Equal(3, root0.BodySize); + Assert.Equal([0xAA, 0xBB, 0xCC], reader.ReadData(root0)); + + // Root[1]: NodeChunk(type=0x10) with 3 children + var root1 = reader.ReadChunk(); + Assert.Equal(0x10u, root1.Type); + Assert.True(root1.HasChildrenHint); + var root1Read = 0; + + // Root[1] Child[0]: DataChunk(type=0x02, data=[0x11, 0x22]) + var r1c0 = reader.ReadChunk(ref root1Read); + Assert.Equal(0x02u, r1c0.Type); + Assert.False(r1c0.HasChildrenHint); + Assert.Equal([0x11, 0x22], reader.ReadData(r1c0)); + root1Read += r1c0.BodySize; + + // Root[1] Child[1]: NodeChunk(type=0x20) with 1 child + var r1c1 = reader.ReadChunk(ref root1Read); + Assert.Equal(0x20u, r1c1.Type); + Assert.True(r1c1.HasChildrenHint); + var r1c1Read = 0; + + // Root[1] Child[1] Child[0]: DataChunk(type=0x03, data=[0x33]) + var r1c1c0 = reader.ReadChunk(ref r1c1Read); + Assert.Equal(0x03u, r1c1c0.Type); + Assert.False(r1c1c0.HasChildrenHint); + Assert.Equal([0x33], reader.ReadData(r1c1c0)); + r1c1Read += r1c1c0.BodySize; + Assert.Equal(r1c1.BodySize, r1c1Read); + root1Read += r1c1.BodySize; + + // Root[1] Child[2]: MiniNodeChunk(type=0x30) with 2 mini-chunks + var r1c2 = reader.ReadChunk(ref root1Read); + Assert.Equal(0x30u, r1c2.Type); + Assert.False(r1c2.HasChildrenHint); + var r1c2Read = 0; + + // Root[1] Child[2] Mini[0]: MiniChunk(type=0x04, data=[0x44, 0x55]) + var r1c2m0 = reader.ReadMiniChunk(ref r1c2Read); + Assert.Equal(0x04, r1c2m0.Type); + Assert.Equal([0x44, 0x55], reader.ReadData(r1c2m0)); + r1c2Read += r1c2m0.BodySize; + + // Root[1] Child[2] Mini[1]: MiniChunk(type=0x05, data=[0x66]) + var r1c2m1 = reader.ReadMiniChunk(ref r1c2Read); + Assert.Equal(0x05, r1c2m1.Type); + Assert.Equal([0x66], reader.ReadData(r1c2m1)); + r1c2Read += r1c2m1.BodySize; + + Assert.Equal(r1c2.BodySize, r1c2Read); + root1Read += r1c2.BodySize; + Assert.Equal(root1.BodySize, root1Read); + + // Root[2]: DataChunk(type=0x06, data=[0x77, 0x88, 0x99, 0xDD]) + var root2 = reader.ReadChunk(); + Assert.Equal(0x06u, root2.Type); + Assert.False(root2.HasChildrenHint); + Assert.Equal([0x77, 0x88, 0x99, 0xDD], reader.ReadData(root2)); + + // Root[3]: DataChunk(type=0x07, data=[]) + var root3 = reader.ReadChunk(); + Assert.Equal(0x07u, root3.Type); + Assert.False(root3.HasChildrenHint); + Assert.Equal(0, root3.BodySize); + Assert.Equal([], reader.ReadData(root3)); + + // Root[4]: NodeChunk(type=0x11, children=[]) + var root4 = reader.ReadChunk(); + Assert.Equal(0x11u, root4.Type); + Assert.True(root4.HasChildrenHint); + Assert.Equal(0, root4.BodySize); + + // Root[5]: MiniNodeChunk(type=0x31) with 1 empty mini-chunk + var root5 = reader.ReadChunk(); + Assert.Equal(0x31u, root5.Type); + Assert.False(root5.HasChildrenHint); + var root5Read = 0; + + // Root[5] Mini[0]: MiniChunk(type=0x08, data=[]) + var r5m0 = reader.ReadMiniChunk(ref root5Read); + Assert.Equal(0x08, r5m0.Type); + Assert.Equal(0, r5m0.BodySize); + Assert.Equal([], reader.ReadData(r5m0)); + root5Read += r5m0.BodySize; + Assert.Equal(root5.BodySize, root5Read); + + // Root[6]: MiniNodeChunk(type=0x32, children=[]) + var root6 = reader.ReadChunk(); + Assert.Equal(0x32u, root6.Type); + Assert.False(root6.HasChildrenHint); + Assert.Equal(0, root6.BodySize); + + // Verify entire stream was consumed + Assert.Equal(stream.Length, stream.Position); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs index b3a7e702..6fcbb5f1 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/ChunkFileTest.cs @@ -30,9 +30,10 @@ public void Ctor_ThrowsOnNull() } [Fact] - public void Ctor_ThrowsOnEmpty() + public void Ctor_AllowsEmpty() { - Assert.Throws(() => new ChunkFile([])); + var file = new ChunkFile([]); + Assert.Empty(file.RootChunks); } [Fact] @@ -57,20 +58,19 @@ public void Bytes_ReturnsCorrectBinary() } [Fact] - public void GetBytes_WritesMultipleRootChunks() + public void GetBytes_WritesExactByteSequence() { + // type=0x01, size=1: [0x01,0x00,0x00,0x00, 0x01,0x00,0x00,0x00, 0xAA] + // type=0x02, size=2: [0x02,0x00,0x00,0x00, 0x02,0x00,0x00,0x00, 0xBB,0xCC] var r1 = CreateRoot(1, [0xAA]); - var r2 = CreateRoot(2, [0xBB]); + var r2 = CreateRoot(2, [0xBB, 0xCC]); var file = new ChunkFile([r1, r2]); - var bytes = file.Bytes; - var r1Bytes = r1.Bytes; - var r2Bytes = r2.Bytes; - - for (var i = 0; i < r1Bytes.Length; i++) - Assert.Equal(r1Bytes[i], bytes[i]); - for (var i = 0; i < r2Bytes.Length; i++) - Assert.Equal(r2Bytes[i], bytes[r1.Size + i]); + Assert.Equal(new byte[] + { + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xAA, + 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xBB, 0xCC, + }, file.Bytes); } [Fact] diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs index de47ce76..815053b0 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/DataChunkTest.cs @@ -19,10 +19,11 @@ public void Ctor_ValidArgs_SetsProperties() } [Fact] - public void Ctor_ThrowsOnEmptyData() + public void Ctor_AllowsEmptyData() { var info = new ChunkMetadata(0x10, 0); - Assert.Throws(() => new DataChunk(info, ReadOnlyMemory.Empty)); + var chunk = new DataChunk(info, ReadOnlyMemory.Empty); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -52,6 +53,28 @@ public void Size_IncludesHeaderAndData() Assert.Equal(8 + 3, chunk.Size); } + [Fact] + public void GetBytes_EmptyData_WritesHeaderOnly() + { + var info = new ChunkMetadata(0x05, 0); + var chunk = new DataChunk(info, ReadOnlyMemory.Empty); + + var bytes = chunk.Bytes; + Assert.Equal(8, bytes.Length); + + // Type (LE) + Assert.Equal(0x05, bytes[0]); + Assert.Equal(0x00, bytes[1]); + Assert.Equal(0x00, bytes[2]); + Assert.Equal(0x00, bytes[3]); + + // Size = 0 (LE) + Assert.Equal(0x00, bytes[4]); + Assert.Equal(0x00, bytes[5]); + Assert.Equal(0x00, bytes[6]); + Assert.Equal(0x00, bytes[7]); + } + [Fact] public void GetBytes_WritesCorrectBinary() { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs index bbb3562b..e9ef0c8b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniChunkTest.cs @@ -19,10 +19,11 @@ public void Ctor_ValidArgs_SetsProperties() } [Fact] - public void Ctor_ThrowsOnEmptyData() + public void Ctor_AllowsEmptyData() { var info = new MiniChunkMetadata(0x05, 0); - Assert.Throws(() => new MiniChunk(info, ReadOnlyMemory.Empty)); + var chunk = new MiniChunk(info, ReadOnlyMemory.Empty); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -44,6 +45,19 @@ public void Size_IncludesHeaderAndData() Assert.Equal(2 + 3, chunk.Size); } + [Fact] + public void GetBytes_EmptyData_WritesHeaderOnly() + { + var info = new MiniChunkMetadata(0x0B, 0); + var chunk = new MiniChunk(info, ReadOnlyMemory.Empty); + + var bytes = chunk.Bytes; + Assert.Equal(2, bytes.Length); + + Assert.Equal(0x0B, bytes[0]); // Type + Assert.Equal(0x00, bytes[1]); // Size = 0 + } + [Fact] public void GetBytes_WritesCorrectBinary() { @@ -65,7 +79,7 @@ public void IsChunk_ButNotRootChunk() var data = new byte[] { 1 }; var info = new MiniChunkMetadata(1, 1); var chunk = new MiniChunk(info, data); - Assert.IsAssignableFrom(chunk); - Assert.IsNotType(chunk); + Assert.IsType(chunk, false); + Assert.IsNotType(chunk, exactMatch: false); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs index a4ac8068..20055bd7 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/MiniNodeChunkTest.cs @@ -15,9 +15,9 @@ private static MiniChunk CreateMiniChild(byte type, byte[] data) [Fact] public void Ctor_ValidArgs_SetsProperties() { - var child = CreateMiniChild(1, new byte[] { 0xAA }); + var child = CreateMiniChild(1, [0xAA]); var info = new ChunkMetadata(0x30, (uint)child.Size); - var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); + var chunk = new MiniNodeChunk(info, [child]); Assert.Equal(info, chunk.Info); Assert.Single(chunk.Children); @@ -32,56 +32,98 @@ public void Ctor_ThrowsOnNullChildren() } [Fact] - public void Ctor_ThrowsOnEmptyChildren() + public void Ctor_AllowsEmptyChildren() { var info = new ChunkMetadata(0x30, 0); - Assert.Throws(() => new MiniNodeChunk(info, Array.Empty())); + var chunk = new MiniNodeChunk(info, []); + Assert.Empty(chunk.Children); } [Fact] public void Ctor_ThrowsWhenSizeMismatch() { - var child = CreateMiniChild(1, new byte[] { 0xAA }); + var child = CreateMiniChild(1, [0xAA]); var info = new ChunkMetadata(0x30, 999); - Assert.Throws(() => new MiniNodeChunk(info, new MiniChunk[] { child })); + Assert.Throws(() => new MiniNodeChunk(info, [child])); } [Fact] public void Ctor_ThrowsWhenRawSizeExceedsIntMax() { - var child = CreateMiniChild(1, new byte[] { 0xAA }); + var child = CreateMiniChild(1, [0xAA]); // RawSize with bit 31 set exceeds int.MaxValue var info = new ChunkMetadata(0x30, 0x8000_0000u | (uint)child.Size); - Assert.Throws(() => new MiniNodeChunk(info, new MiniChunk[] { child })); + Assert.Throws(() => new MiniNodeChunk(info, [child])); } [Fact] public void Size_IncludesHeaderAndChildren() { - var child = CreateMiniChild(1, new byte[] { 0xAA }); + var child = CreateMiniChild(1, [0xAA]); var info = new ChunkMetadata(0x30, (uint)child.Size); - var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); + var chunk = new MiniNodeChunk(info, [child]); Assert.Equal(8 + child.Size, chunk.Size); } + [Fact] + public void GetBytes_WritesExactByteSequence() + { + // child: type=0x01, data=[0xAA] => MiniChunk header [01 01] + [AA] = 3 bytes + var child = CreateMiniChild(1, [0xAA]); + // MiniNodeChunk header: type=0x30, RawSize=3 + var info = new ChunkMetadata(0x30, (uint)child.Size); + var chunk = new MiniNodeChunk(info, [child]); + + var bytes = chunk.Bytes; + + byte[] expected = + [ + // MiniNodeChunk header (8 bytes): type=0x00000030, RawSize=0x00000003 + 0x30, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, + // child header: type=0x01, size=0x01 + 0x01, 0x01, + // child data + 0xAA + ]; + Assert.Equal(expected, bytes); + } + + [Fact] + public void GetBytes_EmptyChildren_WritesHeaderOnly() + { + var info = new ChunkMetadata(0x30, 0); + var chunk = new MiniNodeChunk(info, []); + + var bytes = chunk.Bytes; + + byte[] expected = + [ + // MiniNodeChunk header: type=0x00000030, RawSize=0x00000000 + 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ]; + Assert.Equal(expected, bytes); + } + [Fact] public void IsRootChunk() { - var child = CreateMiniChild(1, new byte[] { 0xAA }); + var child = CreateMiniChild(1, [0xAA]); var info = new ChunkMetadata(0x30, (uint)child.Size); - var chunk = new MiniNodeChunk(info, new MiniChunk[] { child }); - Assert.IsAssignableFrom(chunk); + var chunk = new MiniNodeChunk(info, [child]); + Assert.IsType(chunk, false); } [Fact] public void MultipleChildren() { - var c1 = CreateMiniChild(1, new byte[] { 0xAA }); - var c2 = CreateMiniChild(2, new byte[] { 0xBB, 0xCC }); + var c1 = CreateMiniChild(1, [0xAA]); + var c2 = CreateMiniChild(2, [0xBB, 0xCC]); var totalChildSize = c1.Size + c2.Size; var info = new ChunkMetadata(0x30, (uint)totalChildSize); - var chunk = new MiniNodeChunk(info, new MiniChunk[] { c1, c2 }); + var chunk = new MiniNodeChunk(info, [c1, c2]); Assert.Equal(2, chunk.Children.Count); Assert.Equal(8 + totalChildSize, chunk.Size); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs index 55bf0e77..d334b5d3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/NodeChunkTest.cs @@ -15,9 +15,9 @@ private static RawChunk CreateChild(uint type, byte[] data) [Fact] public void Ctor_ValidArgs_SetsProperties() { - var child = CreateChild(1, new byte[] { 0xAA }); + var child = CreateChild(1, [0xAA]); var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); - var chunk = new NodeChunk(info, new RootChunk[] { child }); + var chunk = new NodeChunk(info, [child]); Assert.Equal(info, chunk.Info); Assert.Single(chunk.Children); @@ -27,9 +27,9 @@ public void Ctor_ValidArgs_SetsProperties() [Fact] public void Ctor_ThrowsWhenBit31NotSet() { - var child = CreateChild(1, new byte[] { 0xAA }); + var child = CreateChild(1, [0xAA]); var info = new ChunkMetadata(0x20, (uint)child.Size); - Assert.Throws(() => new NodeChunk(info, new RootChunk[] { child })); + Assert.Throws(() => new NodeChunk(info, [child])); } [Fact] @@ -40,53 +40,81 @@ public void Ctor_ThrowsOnNullChildren() } [Fact] - public void Ctor_ThrowsOnEmptyChildren() + public void Ctor_AllowsEmptyChildren() { var info = new ChunkMetadata(0x20, 0x8000_0000u); - Assert.Throws(() => new NodeChunk(info, Array.Empty())); + var chunk = new NodeChunk(info, []); + Assert.Empty(chunk.Children); } [Fact] public void Ctor_ThrowsWhenSizeMismatch() { - var child = CreateChild(1, new byte[] { 0xAA }); + var child = CreateChild(1, [0xAA]); var info = new ChunkMetadata(0x20, 0x8000_0000u | 999u); - Assert.Throws(() => new NodeChunk(info, new RootChunk[] { child })); + Assert.Throws(() => new NodeChunk(info, [child])); } [Fact] public void Size_IncludesHeaderAndChildren() { - var child = CreateChild(1, new byte[] { 0xAA }); + var child = CreateChild(1, [0xAA]); var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); - var chunk = new NodeChunk(info, new RootChunk[] { child }); + var chunk = new NodeChunk(info, [child]); Assert.Equal(8 + child.Size, chunk.Size); } [Fact] - public void GetBytes_WritesHeaderAndChildBytes() + public void GetBytes_WritesExactByteSequence() { - var child = CreateChild(1, new byte[] { 0xFF }); + // child: type=0x01, data=[0xFF] => RawChunk header [01 00 00 00 01 00 00 00] + [FF] = 9 bytes + var child = CreateChild(1, [0xFF]); + // NodeChunk header: type=0x20, RawSize = 0x8000_0000 | 9 = 0x80000009 var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); - var chunk = new NodeChunk(info, new RootChunk[] { child }); + var chunk = new NodeChunk(info, [child]); var bytes = chunk.Bytes; - Assert.Equal(chunk.Size, bytes.Length); - var childBytes = child.Bytes; - for (var i = 0; i < childBytes.Length; i++) - Assert.Equal(childBytes[i], bytes[8 + i]); + byte[] expected = + [ + // NodeChunk header (8 bytes): type=0x00000020, RawSize=0x80000009 + 0x20, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x80, + // child header: type=0x00000001, RawSize=0x00000001 + 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + // child data + 0xFF + ]; + Assert.Equal(expected, bytes); + } + + [Fact] + public void GetBytes_EmptyChildren_WritesHeaderOnly() + { + var info = new ChunkMetadata(0x20, 0x8000_0000u); + var chunk = new NodeChunk(info, []); + + var bytes = chunk.Bytes; + + byte[] expected = + [ + // NodeChunk header: type=0x00000020, RawSize=0x80000000 + 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x80 + ]; + Assert.Equal(expected, bytes); } [Fact] public void MultipleChildren() { - var c1 = CreateChild(1, new byte[] { 0xAA }); - var c2 = CreateChild(2, new byte[] { 0xBB, 0xCC }); + var c1 = CreateChild(1, [0xAA]); + var c2 = CreateChild(2, [0xBB, 0xCC]); var totalChildSize = c1.Size + c2.Size; var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)totalChildSize); - var chunk = new NodeChunk(info, new RootChunk[] { c1, c2 }); + var chunk = new NodeChunk(info, [c1, c2]); Assert.Equal(2, chunk.Children.Count); Assert.Equal(8 + totalChildSize, chunk.Size); @@ -95,9 +123,9 @@ public void MultipleChildren() [Fact] public void IsRootChunk() { - var child = CreateChild(1, new byte[] { 0xAA }); + var child = CreateChild(1, [0xAA]); var info = new ChunkMetadata(0x20, 0x8000_0000u | (uint)child.Size); - var chunk = new NodeChunk(info, new RootChunk[] { child }); - Assert.IsAssignableFrom(chunk); + var chunk = new NodeChunk(info, [child]); + Assert.IsType(chunk, false); } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs index 8ef2edda..806bda13 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Model/RawChunkTest.cs @@ -28,10 +28,11 @@ public void Ctor_AllowsBit31Set() } [Fact] - public void Ctor_ThrowsOnEmptyData() + public void Ctor_AllowsEmptyData() { var info = new ChunkMetadata(0x10, 0); - Assert.Throws(() => new RawChunk(info, ReadOnlyMemory.Empty)); + var chunk = new RawChunk(info, ReadOnlyMemory.Empty); + Assert.Equal(0, chunk.Data.Length); } [Fact] @@ -51,6 +52,28 @@ public void Size_IncludesHeaderAndData() Assert.Equal(8 + 3, chunk.Size); } + [Fact] + public void GetBytes_EmptyData_WritesHeaderOnly() + { + var info = new ChunkMetadata(0x03, 0); + var chunk = new RawChunk(info, ReadOnlyMemory.Empty); + + var bytes = chunk.Bytes; + Assert.Equal(8, bytes.Length); + + // Type (LE) + Assert.Equal(0x03, bytes[0]); + Assert.Equal(0x00, bytes[1]); + Assert.Equal(0x00, bytes[2]); + Assert.Equal(0x00, bytes[3]); + + // Size = 0 (LE) + Assert.Equal(0x00, bytes[4]); + Assert.Equal(0x00, bytes[5]); + Assert.Equal(0x00, bytes[6]); + Assert.Equal(0x00, bytes[7]); + } + [Fact] public void GetBytes_WritesRawSizeIncludingBit31() { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/RawChunkEquivalenceTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/RawChunkEquivalenceTest.cs new file mode 100644 index 00000000..62d939b8 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/RawChunkEquivalenceTest.cs @@ -0,0 +1,173 @@ +using System; +using PG.StarWarsGame.Files.ChunkFiles.Binary; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; +using Xunit; + +namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; + +/// +/// Systematically verifies that every structured chunk type produces the exact same binary +/// as a constructed +/// from the equivalent header and body bytes. +/// +public class RawChunkEquivalenceTest +{ + #region DataChunk + + [Fact] + public void DataChunk_WithData_MatchesRaw() + { + var structured = ChunkFactory.Data(0x01, [0xAA, 0xBB, 0xCC]); + var raw = ChunkFactory.Raw(0x01, 3, [0xAA, 0xBB, 0xCC]); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void DataChunk_SingleByte_MatchesRaw() + { + var structured = ChunkFactory.Data(0xFF, [0x42]); + var raw = ChunkFactory.Raw(0xFF, 1, [0x42]); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void DataChunk_EmptyData_MatchesRaw() + { + var structured = ChunkFactory.Data(0x07, []); + var raw = ChunkFactory.Raw(0x07, 0, []); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + #endregion + + #region MiniNodeChunk + + [Fact] + public void MiniNodeChunk_WithChildren_MatchesRaw() + { + var structured = ChunkFactory.Node(0x30u, + ChunkFactory.Mini(0x04, [0x44, 0x55]), + ChunkFactory.Mini(0x05, [0x66])); + + // Body = mini0.Bytes + mini1.Bytes = [0x04,0x02,0x44,0x55] + [0x05,0x01,0x66] + var body = new byte[] { 0x04, 0x02, 0x44, 0x55, 0x05, 0x01, 0x66 }; + var raw = ChunkFactory.Raw(0x30, (uint)body.Length, body); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void MiniNodeChunk_SingleChild_MatchesRaw() + { + var structured = ChunkFactory.Node(0x31u, ChunkFactory.Mini(0x08, [])); + + var body = new byte[] { 0x08, 0x00 }; + var raw = ChunkFactory.Raw(0x31, (uint)body.Length, body); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void MiniNodeChunk_EmptyChildren_MatchesRaw() + { + var structured = ChunkFactory.Node(0x32u, (MiniChunk[])[]); + var raw = ChunkFactory.Raw(0x32, 0u, []); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + #endregion + + #region NodeChunk + + [Fact] + public void NodeChunk_WithDataChildren_MatchesRaw() + { + var structured = ChunkFactory.Node(0x20u, + ChunkFactory.Data(0x03, [0x33])); + + // Body = DataChunk(0x03, [0x33]).Bytes = 8-byte header + [0x33] + var childBytes = ChunkFactory.Data(0x03, [0x33]).Bytes; + var raw = ChunkFactory.Raw(0x20, 0x8000_0000u | (uint)childBytes.Length, childBytes); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void NodeChunk_WithMultipleDataChildren_MatchesRaw() + { + var child0 = ChunkFactory.Data(0x02, [0x11, 0x22]); + var child1 = ChunkFactory.Data(0x03, [0x33]); + var structured = ChunkFactory.Node(0x10u, child0, child1); + + var body = new byte[child0.Size + child1.Size]; + child0.GetBytes(body); + child1.GetBytes(((Span)body).Slice(child0.Size)); + var raw = ChunkFactory.Raw(0x10, 0x8000_0000u | (uint)body.Length, body); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void NodeChunk_EmptyChildren_MatchesRaw() + { + var structured = ChunkFactory.Node(0x11u, (RootChunk[])[]); + var raw = ChunkFactory.Raw(0x11, 0x8000_0000u, []); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void NodeChunk_WithNestedNodeChild_MatchesRaw() + { + var grandchild = ChunkFactory.Data(0x03, [0x33]); + var innerNode = ChunkFactory.Node(0x20u, grandchild); + var structured = ChunkFactory.Node(0x10u, innerNode); + + var innerBytes = innerNode.Bytes; + var raw = ChunkFactory.Raw(0x10, 0x8000_0000u | (uint)innerBytes.Length, innerBytes); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + [Fact] + public void NodeChunk_WithMiniNodeChild_MatchesRaw() + { + var miniNode = ChunkFactory.Node(0x30u, + ChunkFactory.Mini(0x04, [0x44, 0x55]), + ChunkFactory.Mini(0x05, [0x66])); + var structured = ChunkFactory.Node(0x10u, miniNode); + + var miniNodeBytes = miniNode.Bytes; + var raw = ChunkFactory.Raw(0x10, 0x8000_0000u | (uint)miniNodeBytes.Length, miniNodeBytes); + + Assert.Equal(structured.Bytes, raw.Bytes); + } + + #endregion + + #region ChunkFile + + [Fact] + public void ChunkFile_StructuredFile_MatchesExpectedBytes() + { + Assert.Equal(TestChunkFileData.ExpectedBytes.ToArray(), TestChunkFileData.StructuredFile.Bytes); + } + + [Fact] + public void ChunkFile_StructuredFile_HasExpectedSize() + { + Assert.Equal(TestChunkFileData.ExpectedBytes.Length, TestChunkFileData.StructuredFile.Size); + } + + [Fact] + public void ChunkFile_StructuredFile_HasExpectedRootCount() + { + Assert.Equal(7, TestChunkFileData.StructuredFile.RootChunks.Count); + } + + #endregion +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs index e671a595..0e2be661 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/ChunkReaderTest.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Text; +using PG.StarWarsGame.Files.Binary; +using PG.StarWarsGame.Files.ChunkFiles.Binary; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; using Xunit; @@ -50,10 +52,33 @@ public void ReadChunk_WithRefBytes_IncrementsBy8() [Fact] public void ReadChunk_ThrowsAtEndOfStream() { - using var reader = new ChunkReader(CreateStream(Array.Empty())); + using var reader = new ChunkReader(CreateStream([])); Assert.Throws(() => reader.ReadChunk()); } + [Fact] + public void ReadChunk_ThrowsOnPartialHeader() + { + // Only 4 bytes instead of the required 8 + using var reader = new ChunkReader(CreateStream([0x01, 0x00, 0x00, 0x00])); + Assert.Throws(() => reader.ReadChunk()); + } + + [Fact] + public void ReadMiniChunk_ThrowsAtEndOfStream() + { + using var reader = new ChunkReader(CreateStream([])); + Assert.Throws(() => { var rb = 0; reader.ReadMiniChunk(ref rb); }); + } + + [Fact] + public void ReadMiniChunk_ThrowsOnPartialHeader() + { + // Only 1 byte instead of the required 2 + using var reader = new ChunkReader(CreateStream([0x05])); + Assert.Throws(() => { var rb = 0; reader.ReadMiniChunk(ref rb); }); + } + [Fact] public void ReadMiniChunk_ReadsHeaderCorrectly() { @@ -157,10 +182,19 @@ public void ReadData_BySize_ReadsCorrectBytes() Assert.Equal(new byte[] { 0x01, 0x02, 0x03 }, result); } + [Fact] + public void ReadData_BySize_ReturnsAvailableBytes_WhenSizeExceedsStream() + { + var data = new byte[] { 0x01, 0x02 }; + using var reader = new ChunkReader(CreateStream(data)); + var result = reader.ReadData(10); + Assert.Equal(data, result); + } + [Fact] public void ReadData_BySize_ThrowsOnNegative() { - using var reader = new ChunkReader(CreateStream(new byte[] { 0x01 })); + using var reader = new ChunkReader(CreateStream([0x01])); Assert.Throws(() => reader.ReadData(-1)); } @@ -223,7 +257,7 @@ public void Skip_WithRefBytes_IncrementsCounter() [Fact] public void TryReadChunk_ReturnsNull_AtEndOfStream() { - using var reader = new ChunkReader(CreateStream(Array.Empty())); + using var reader = new ChunkReader(CreateStream([])); var result = reader.TryReadChunk(); Assert.Null(result); } @@ -255,7 +289,7 @@ public void TryReadChunk_WithRefBytes_IncrementsBy8() [Fact] public void TryReadChunk_WithRefBytes_DoesNotIncrement_AtEnd() { - using var reader = new ChunkReader(CreateStream(Array.Empty())); + using var reader = new ChunkReader(CreateStream([])); var readBytes = 5; var result = reader.TryReadChunk(ref readBytes); @@ -263,10 +297,12 @@ public void TryReadChunk_WithRefBytes_DoesNotIncrement_AtEnd() Assert.Equal(5, readBytes); } + #region ReadString + [Fact] public void ReadString_ReadsCorrectly() { - var text = "Hello"; + const string text = "Hello"; var encoded = Encoding.ASCII.GetBytes(text); using var reader = new ChunkReader(CreateStream(encoded)); @@ -279,7 +315,7 @@ public void ReadString_ReadsCorrectly() [Fact] public void ReadString_ZeroTerminated_TrimsAtNull() { - var encoded = new byte[] { (byte)'H', (byte)'i', 0, (byte)'X' }; + var encoded = "Hi\0X"u8.ToArray(); using var reader = new ChunkReader(CreateStream(encoded)); var result = reader.ReadString(encoded.Length, Encoding.ASCII, true); @@ -289,7 +325,7 @@ public void ReadString_ZeroTerminated_TrimsAtNull() [Fact] public void ReadString_WithoutRefBytes_Works() { - var text = "AB"; + const string text = "AB"; var encoded = Encoding.ASCII.GetBytes(text); using var reader = new ChunkReader(CreateStream(encoded)); @@ -297,10 +333,39 @@ public void ReadString_WithoutRefBytes_Works() Assert.Equal(text, result); } + [Fact] + public void ReadString_ThrowsBinaryCorrupted_WhenSizeExceedsStream() + { + var encoded = "Hi"u8.ToArray(); + using var reader = new ChunkReader(CreateStream(encoded)); + Assert.Throws(() => reader.ReadString(10, Encoding.ASCII, false)); + } + + [Fact] + public void ReadString_ZeroTerminated_ThrowsBinaryCorrupted_WhenNoNullTerminator() + { + // No null terminator present — the reader cannot find the terminator and throws + var encoded = "Hello"u8.ToArray(); + using var reader = new ChunkReader(CreateStream(encoded)); + + Assert.Throws(() => + reader.ReadString(encoded.Length, Encoding.ASCII, zeroTerminated: true)); + } + + [Fact] + public void ReadString_ZeroTerminated_ThrowsBinaryCorrupted_WhenSizeExceedsStream() + { + var encoded = "Hi"u8.ToArray(); + using var reader = new ChunkReader(CreateStream(encoded)); + Assert.Throws(() => reader.ReadString(10, Encoding.ASCII, zeroTerminated: true)); + } + + #endregion + [Fact] public void Dispose_DisposesUnderlyingStream() { - var ms = CreateStream(new byte[] { 1, 2, 3 }); + var ms = CreateStream([1, 2, 3]); var reader = new ChunkReader(ms); reader.Dispose(); @@ -310,7 +375,7 @@ public void Dispose_DisposesUnderlyingStream() [Fact] public void Dispose_LeaveOpen_KeepsStreamOpen() { - var ms = CreateStream(new byte[] { 1, 2, 3 }); + var ms = CreateStream([1, 2, 3]); var reader = new ChunkReader(ms, leaveOpen: true); reader.Dispose(); @@ -322,7 +387,7 @@ public void Dispose_LeaveOpen_KeepsStreamOpen() [Fact] public void ReadChunk_Roundtrip_WithChunkFactory() { - var original = PG.StarWarsGame.Files.ChunkFiles.Binary.ChunkFactory.Data(0x42, new byte[] { 1, 2, 3 }); + var original = ChunkFactory.Data(0x42, [1, 2, 3]); var bytes = original.Bytes; using var reader = new ChunkReader(CreateStream(bytes)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs index 5dd8627a..972e9417 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/TestChunkFileData.cs @@ -17,87 +17,73 @@ namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary; /// Mini[0]: MiniChunk(type=0x04, data=[0x44, 0x55]) /// Mini[1]: MiniChunk(type=0x05, data=[0x66]) /// Root[2]: DataChunk(type=0x06, data=[0x77, 0x88, 0x99, 0xDD]) +/// Root[3]: DataChunk(type=0x07, data=[]) -- empty data chunk +/// Root[4]: NodeChunk(type=0x11, children=[]) -- empty node chunk +/// Root[5]: MiniNodeChunk(type=0x31) -- mini-node chunk with one empty mini-chunk +/// Mini[0]: MiniChunk(type=0x08, data=[]) -- empty mini-chunk +/// Root[6]: MiniNodeChunk(type=0x32, children=[]) -- empty mini-node chunk /// /// public static class TestChunkFileData { /// - /// Gets a complex chunk file built using structured chunk types (NodeChunk, MiniNodeChunk, and DataChunk leaves). + /// Gets a complex chunk file built using structured chunk types (NodeChunk, MiniNodeChunk, and DataChunk leaves), + /// including chunks with empty data, a MiniNodeChunk with one empty MiniChunk, and an empty MiniNodeChunk to exercise those allowed edge cases. /// public static ChunkFile StructuredFile { get; } = BuildStructuredFile(); /// - /// Gets a chunk file built entirely from RawChunk instances that produce the same binary as . + /// Gets the expected binary representation of the test chunk file, + /// matching the binary output of . /// - public static ChunkFile RawFile { get; } = BuildRawFile(); - - /// - /// Gets the expected binary representation of the test chunk file. - /// - public static byte[] ExpectedBytes { get; } = StructuredFile.Bytes; + public static ReadOnlyMemory ExpectedBytes { get; } = new byte[] + { + // Root[0]: DataChunk(type=0x01, data=[0xAA, 0xBB, 0xCC]) + 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC, + // Root[1]: NodeChunk(type=0x10, bodySize=0x2A | bit31) + 0x10, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x80, + // Child[0]: DataChunk(type=0x02, data=[0x11, 0x22]) + 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x11, 0x22, + // Child[1]: NodeChunk(type=0x20, bodySize=0x09 | bit31) + 0x20, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x80, + // Grandchild: DataChunk(type=0x03, data=[0x33]) + 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x33, + // Child[2]: MiniNodeChunk(type=0x30, bodySize=7) + 0x30, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + // Mini[0]: MiniChunk(type=0x04, data=[0x44, 0x55]) + 0x04, 0x02, 0x44, 0x55, + // Mini[1]: MiniChunk(type=0x05, data=[0x66]) + 0x05, 0x01, 0x66, + // Root[2]: DataChunk(type=0x06, data=[0x77, 0x88, 0x99, 0xDD]) + 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x77, 0x88, 0x99, 0xDD, + // Root[3]: DataChunk(type=0x07, data=[]) + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Root[4]: NodeChunk(type=0x11, children=[]) + 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + // Root[5]: MiniNodeChunk(type=0x31) with Mini[0]: MiniChunk(type=0x08, data=[]) + 0x31, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x08, 0x00, + // Root[6]: MiniNodeChunk(type=0x32, children=[]) + 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; private static ChunkFile BuildStructuredFile() { - // Root[0]: leaf data chunk - var root0 = ChunkFactory.Data(0x01, [0xAA, 0xBB, 0xCC]); - - // Build children for Root[1] NodeChunk - var child0 = ChunkFactory.Data(0x02, [0x11, 0x22]); - - var grandchild = ChunkFactory.Data(0x03, [0x33]); - var child1 = ChunkFactory.Node(0x20u, grandchild); - - // MiniNodeChunk as a child - serialize mini-chunks into raw body - var miniChild0 = ChunkFactory.Mini(0x04, [0x44, 0x55]); - var miniChild1 = ChunkFactory.Mini(0x05, [0x66]); - var child2 = ChunkFactory.Node(0x30u, miniChild0, miniChild1); - - var root1 = ChunkFactory.Node(0x10u, child0, child1, child2); - - // Root[2]: leaf data chunk - var root2 = ChunkFactory.Data(0x06, [0x77, 0x88, 0x99, 0xDD]); - - return ChunkFactory.File(root0, root1, root2); + return ChunkFactory.File( + ChunkFactory.Data(0x01, [0xAA, 0xBB, 0xCC]), + ChunkFactory.Node(0x10u, + ChunkFactory.Data(0x02, [0x11, 0x22]), + ChunkFactory.Node(0x20u, + ChunkFactory.Data(0x03, [0x33])), + ChunkFactory.Node(0x30u, + ChunkFactory.Mini(0x04, [0x44, 0x55]), + ChunkFactory.Mini(0x05, [0x66]))), + ChunkFactory.Data(0x06, [0x77, 0x88, 0x99, 0xDD]), + ChunkFactory.Data(0x07, []), + ChunkFactory.Node(0x11u, (RootChunk[])[]), + ChunkFactory.Node(0x31u, ChunkFactory.Mini(0x08, [])), + ChunkFactory.Node(0x32u, (MiniChunk[])[]) + ); } - private static ChunkFile BuildRawFile() - { - // Build the exact same binary using only RawChunk instances. - - // Root[0] - var root0 = ChunkFactory.Raw(0x01, 3, [0xAA, 0xBB, 0xCC]); - - // child0: RawChunk(type=0x02, size=2, data=[0x11, 0x22]) -> 10 bytes total - var rawChild0 = ChunkFactory.Raw(0x02, 2, [0x11, 0x22]); - - // grandchild: RawChunk(type=0x03, size=1, data=[0x33]) -> 9 bytes total - // child1: NodeChunk(type=0x20) wrapping grandchild -> body = grandchild.Bytes (9 bytes) - var grandchildBytes = ChunkFactory.Raw(0x03, 1, [0x33]).Bytes; - var rawChild1 = ChunkFactory.Raw(0x20, 0x8000_0000u | (uint)grandchildBytes.Length, grandchildBytes); - - // child2: MiniNodeChunk(type=0x30) with 2 mini-chunks - // Mini(0x04,[0x44,0x55]) = 4 bytes, Mini(0x05,[0x66]) = 3 bytes -> body = 7 bytes - var mc0 = ChunkFactory.Mini(0x04, [0x44, 0x55]); - var mc1 = ChunkFactory.Mini(0x05, [0x66]); - var miniBody = new byte[mc0.Size + mc1.Size]; - mc0.GetBytes(miniBody); - mc1.GetBytes(((Span)miniBody).Slice(mc0.Size)); - var rawChild2 = ChunkFactory.Raw(0x30, (uint)miniBody.Length, miniBody); - - // Root[1]: NodeChunk(type=0x10) body = rawChild0 + rawChild1 + rawChild2 - var root1BodySize = rawChild0.Size + rawChild1.Size + rawChild2.Size; - var root1Body = new byte[root1BodySize]; - var offset = 0; - rawChild0.GetBytes(root1Body); - offset += rawChild0.Size; - rawChild1.GetBytes(((Span)root1Body).Slice(offset)); - offset += rawChild1.Size; - rawChild2.GetBytes(((Span)root1Body).Slice(offset)); - var root1 = ChunkFactory.Raw(0x10, 0x8000_0000u | (uint)root1BodySize, root1Body); - - // Root[2] - var root2 = ChunkFactory.Raw(0x06, 4, [0x77, 0x88, 0x99, 0xDD]); - - return ChunkFactory.File(root0, root1, root2); - } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj index cb0168a3..e1181964 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/PG.StarWarsGame.Files.ChunkFiles.Test.csproj @@ -1,8 +1,8 @@  - net8.0;net10.0 $(TargetFrameworks);net481 + true false @@ -31,7 +31,7 @@ - + - + \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs index 852ebb8c..d9346444 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/ChunkFile.cs @@ -24,24 +24,11 @@ public sealed class ChunkFile : IBinaryFile /// /// Initializes a new instance of the class. /// - /// - /// The root-level chunks. Must contain at least one element. - /// - /// - /// is . - /// - /// - /// is empty. - /// + /// The root-level chunks. + /// is . public ChunkFile(IReadOnlyList rootChunks) { - if (rootChunks == null) - throw new ArgumentNullException(nameof(rootChunks)); - - if (rootChunks.Count == 0) - throw new ArgumentException("ChunkFile must have at least one root chunk.", nameof(rootChunks)); - - RootChunks = rootChunks; + RootChunks = rootChunks ?? throw new ArgumentNullException(nameof(rootChunks)); } /// @@ -77,14 +64,11 @@ public void GetBytes(Span bytes) /// Writes the file's binary representation to a stream. /// /// The destination stream. - /// - /// is . - /// + /// is . public void WriteTo(Stream stream) { if (stream == null) throw new ArgumentNullException(nameof(stream)); - stream.Write(Bytes, 0, Size); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs index f4d03188..89afff91 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/DataChunk.cs @@ -28,15 +28,11 @@ public sealed class DataChunk : RootChunk /// The chunk metadata. Must not have bit 31 set. /// The binary data payload. /// - /// is empty, or /// has bit 31 set, or /// body size does not match the data length. /// public DataChunk(ChunkMetadata info, ReadOnlyMemory data) { - if (data is { IsEmpty: true, Length: 0 }) - throw new ArgumentException("Data cannot be empty.", nameof(data)); - if (info.HasChildrenHint) throw new ArgumentException( "DataChunk metadata must not have bit 31 set.", nameof(info)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs index de72659c..cde78a5f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniChunk.cs @@ -36,15 +36,11 @@ public sealed class MiniChunk : Chunk /// The mini-chunk metadata. /// The binary data payload. Maximum length is 255 bytes. /// - /// is empty, or /// size does not match the data length. /// public MiniChunk(MiniChunkMetadata info, ReadOnlyMemory data) { - if (data is { IsEmpty: true, Length: 0 }) - throw new ArgumentException("Data cannot be empty.", nameof(data)); - - if (info.BodySize!= data.Length) + if (info.BodySize != data.Length) throw new ArgumentException( $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", nameof(info)); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs index 0db79f73..8e5b50f5 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/MiniNodeChunk.cs @@ -19,7 +19,7 @@ public sealed class MiniNodeChunk : NodeChunkBase /// Initializes a new instance of the class. /// /// The chunk metadata. Must not have bit 31 set. - /// The mini-chunk children. Must contain at least one element. + /// The mini-chunk children. /// Chunks larger than 2GB are not supported. public MiniNodeChunk(ChunkMetadata info, IReadOnlyList children) : base(info, children) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs index 05a17e6d..eabc9e18 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunk.cs @@ -16,7 +16,7 @@ public sealed class NodeChunk : NodeChunkBase /// Initializes a new instance of the class. /// /// The chunk metadata. Must have bit 31 set. - /// The child chunks. Must contain at least one element. + /// The child chunks. /// does not have bit 31 set. public NodeChunk(ChunkMetadata info, IReadOnlyList children) : base(info, children) { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs index 3d900e8e..e654025b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/NodeChunkBase.cs @@ -29,10 +29,9 @@ public abstract class NodeChunkBase : RootChunk where TChild : Chunk /// Initializes a new instance of the class. /// /// The chunk metadata. - /// The child chunks. Must contain at least one element. + /// The child chunks. /// is . /// - /// is empty, or /// body size does not match the sum of children sizes. /// protected NodeChunkBase(ChunkMetadata info, IReadOnlyList children) @@ -40,10 +39,6 @@ protected NodeChunkBase(ChunkMetadata info, IReadOnlyList children) if (children == null) throw new ArgumentNullException(nameof(children)); - if (children.Count == 0) - throw new ArgumentException( - $"{GetType().Name} must have at least one child.", nameof(children)); - var actualSize = children.Sum(c => c.Size); if (info.BodySize != actualSize) throw new ArgumentException( diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs index 22940ba6..e4ec33fc 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/RawChunk.cs @@ -45,14 +45,10 @@ public sealed class RawChunk : RootChunk /// The chunk metadata, written as-is. No validation is performed on bit 31. /// The raw body data. /// - /// is empty, or /// body size does not match the data length. /// public RawChunk(ChunkMetadata info, ReadOnlyMemory data) { - if (data is { IsEmpty: true, Length: 0 }) - throw new ArgumentException("Data cannot be empty.", nameof(data)); - if (info.BodySize != data.Length) throw new ArgumentException( $"Metadata size ({info.BodySize}) does not match data length ({data.Length}).", diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj index 04f7ff7c..acb477d9 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED.Test/PG.StarWarsGame.Files.TED.Test.csproj @@ -3,6 +3,7 @@ net8.0;net10.0 $(TargetFrameworks);net481 + true false From d2f54ca0a30faec27eaec1fb7476949964143d83 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 3 Apr 2026 10:50:51 +0200 Subject: [PATCH 15/17] add FileName property to TestChunkFileReader and corresponding tests --- .../Binary/Reader/TestChunkFileReader.cs | 2 ++ .../Binary/Reader/TestChunkFileReaderTest.cs | 23 +++++++++++++++++++ .../Binary/Model/Metadata/ChunkMetadata.cs | 1 - .../Binary/Reader/ChunkFileReaderBase.cs | 4 ++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs index c0ffa408..38f9465a 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReader.cs @@ -7,6 +7,8 @@ namespace PG.StarWarsGame.Files.ChunkFiles.Test.Binary.Reader; public sealed class TestChunkFileReader(Stream stream, bool leaveStreamOpen = false) : ChunkFileReaderBase(stream, leaveStreamOpen) { + public new string? FileName => base.FileName; + public override TestChunkFileReaderTest.TestChunkData Read() { var meta = ChunkReader.ReadChunk(); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs index 0f4ebecc..0c957c73 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles.Test/Binary/Reader/TestChunkFileReaderTest.cs @@ -42,4 +42,27 @@ protected override void CallThrowIfChunkSizeTooLarge(TestChunkFileReader reader, { reader.CallThrowIfChunkSizeTooLarge(chunk); } + + [Fact] + public void FileName_IsNullForMemoryStream() + { + using var reader = CreateReader(new MemoryStream()); + Assert.Null(reader.FileName); + } + + [Fact] + public void FileName_IsPopulatedFromFileStream() + { + var tempFile = Path.GetTempFileName(); + try + { + using var fs = File.OpenRead(tempFile); + using var reader = CreateReader(fs); + Assert.Equal(tempFile, reader.FileName); + } + finally + { + File.Delete(tempFile); + } + } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs index 6dbb9a96..a415e595 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Model/Metadata/ChunkMetadata.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Runtime.InteropServices; namespace PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs index 5a98ea03..f001d112 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.ChunkFiles/Binary/Reader/ChunkFileReaderBase.cs @@ -1,6 +1,7 @@ using System; using System.IO; using AnakinRaW.CommonUtilities; +using PG.Commons.Utilities; using PG.StarWarsGame.Files.ChunkFiles.Binary.Model; using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; using PG.StarWarsGame.Files.ChunkFiles.Data; @@ -27,6 +28,9 @@ public abstract class ChunkFileReaderBase(Stream stream, bool leaveStreamOpen /// protected readonly ChunkReader ChunkReader = new(stream, leaveStreamOpen); + /// + public string? FileName { get; } = stream.TryGetFilePath(); + /// public abstract T Read(); From a1769b912c25f027a78b3cae3a7b9c0a2f4874d1 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 3 Apr 2026 10:52:14 +0200 Subject: [PATCH 16/17] start implementing TED reader --- .../Binary/Reader/TedFileReader.cs | 163 +++++++++++++++++- .../Binary/Reader/TedFileReaderFactory.cs | 2 +- .../Binary/TedChunkType.cs | 27 +++ .../Data/IMapData.cs | 135 ++++++++++++++- 4 files changed, 320 insertions(+), 7 deletions(-) create mode 100644 src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/TedChunkType.cs diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs index 0556a80b..42e1a936 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReader.cs @@ -1,14 +1,169 @@ -using System; -using System.IO; +using PG.StarWarsGame.Files.Binary; +using PG.StarWarsGame.Files.ChunkFiles.Binary.Model.Metadata; using PG.StarWarsGame.Files.ChunkFiles.Binary.Reader; using PG.StarWarsGame.Files.TED.Data; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Numerics; namespace PG.StarWarsGame.Files.TED.Binary.Reader; -internal sealed class TedFileReader(Stream stream) : ChunkFileReaderBase(stream), ITedFileReader +internal sealed class TedFileReader(TedLoadOptions loadOptions, Stream stream) + : ChunkFileReaderBase(stream), ITedFileReader { + protected TedLoadOptions LoadOptions { get; } = loadOptions; + public override IMapData Read() { - throw new NotImplementedException(); + ChunkMetadata? chunk; + MapInfo? mapInfo = null; + + byte[]? previewImageData = null; + int? previewImageDataSize = null; + List startPositionMarkers = []; + + var chunkCount = 0; + while ((chunk = ChunkReader.TryReadChunk()) is not null) + { + if (chunkCount++ == 0 && chunk.Value.Type != (uint)TedChunkType.MapInfo) + throw new BinaryCorruptedException($"The first chunk of a TED file must be of type {TedChunkType.MapInfo}."); + + switch ((TedChunkType)chunk.Value.Type) + { + case TedChunkType.MapInfo: + mapInfo = ReadMapInformation(chunk.Value); + break; + case TedChunkType.StartPositions: + ReadStartPositionMarkers(chunk.Value, startPositionMarkers); + break; + case TedChunkType.MapPreviewImageData: + ReadMapPreviewImageData(chunk.Value, out previewImageDataSize, out previewImageData); + break; + default: + throw new BinaryCorruptedException($"Unknown chunk type: {chunk.Value.Type}."); + } + } + + if (mapInfo is null) + throw new BinaryCorruptedException("The TED file does not have map information data."); + + mapInfo = mapInfo with + { + EmbeddedPreviewTextureFile = previewImageData, + EmbeddedPreviewTextureFileSize = previewImageDataSize, + StartPositionMarkers = startPositionMarkers + }; + + return new MapData(mapInfo); + } + + private void ReadStartPositionMarkers(ChunkMetadata chunk, List startPositionMarkers) + { + var readBytes = 0; + var positionsCount = (int)ChunkReader.ReadDword(ref readBytes); + + startPositionMarkers.Clear(); + + for (var i = 0; i < positionsCount; i++) + { + var x = ChunkReader.ReadFloat(ref readBytes); + var y = ChunkReader.ReadFloat(ref readBytes); + startPositionMarkers.Add(new Vector2(x, y)); + } + + if (readBytes != chunk.BodySize) + throw new BinaryCorruptedException( + $"Unable to read Start Position Markers chunk. Expected {chunk.BodySize} bytes, but read {readBytes} bytes."); } + + private void ReadMapPreviewImageData(ChunkMetadata chunk, out int? previewImageDataSize, out byte[]? previewImageData) + { + Debug.Assert(!chunk.HasChildrenHint); + + previewImageDataSize = chunk.BodySize; + + if (LoadOptions.HasFlag(TedLoadOptions.PreviewImageData)) + { + previewImageData = ChunkReader.ReadData(chunk); + return; + } + + ChunkReader.Skip(chunk.BodySize); + previewImageData = null; + } + + private MapInfo ReadMapInformation(ChunkMetadata chunk) + { + var readBytes = 0; + + var mapFileName = FileName ?? "[NO FILE NAME]"; + int version = 0; + var type = -1; + var playerCount = 1; + var mapLevels = 1; + var supportedWeather = 1; + var factionOwner = 0; + var mapType = 0; + var supportedSystems = 0; + var mapName = string.Empty; + var planetName = string.Empty; + var gameTypes = string.Empty; + var customMap = false; + var width = 0.0f; + var height = 0.0f; + var newMultiplayerMarkerSystem = false; + + MiniChunkMetadata? miniChunk; + while ((miniChunk = ChunkReader.ReadMiniChunk(ref readBytes)) is not null) + { + switch ((MapInfoChunkType)miniChunk.Value.Type) + { + case MapInfoChunkType.Version: + version = (int)ChunkReader.ReadDword(ref readBytes); + Debug.Assert(version == 513); // 01 02 00 00 (LE) + break; + } + } + + if (version == 0) + throw new BinaryCorruptedException("Map Information chunk is missing the version mini-chunk."); + + if (readBytes != chunk.BodySize) + throw new BinaryCorruptedException( + $"Unable to read Map Information chunk. Expected {chunk.BodySize} bytes, but read {readBytes} bytes."); + + if (string.IsNullOrEmpty(mapName)) + mapName = mapFileName; + + return new MapInfo + { + MapFileName = mapFileName, + Version = version, + Type = (MapMode)type, + MaxPlayers = playerCount, + Levels = mapLevels, + Terrain = (MapEnvironmentType)mapType, + Owner = (MapOwnerType)factionOwner, + MapName = mapName, + PlanetName = planetName, + GameTypes = gameTypes.ToUpper(), // The game actually uses the current locale + CustomMap = customMap, + MapSize = new Vector2(width, height), + NewMultiplayerMarkerSystem = newMultiplayerMarkerSystem + }; + } +} + + +[Flags] +public enum TedLoadOptions +{ + /// + /// Loads the entire file. + /// + Full = 0, + PreviewImageData = 1, + XRefs = 2 } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs index d9be9eb8..9bde79cb 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/Reader/TedFileReaderFactory.cs @@ -7,6 +7,6 @@ internal class TedFileReaderFactory(IServiceProvider serviceProvider) : ITedFile { public ITedFileReader GetReader(Stream dataStream) { - return new TedFileReader(dataStream); + return new TedFileReader(TedLoadOptions.Full, dataStream); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/TedChunkType.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/TedChunkType.cs new file mode 100644 index 00000000..a8308562 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Binary/TedChunkType.cs @@ -0,0 +1,27 @@ +namespace PG.StarWarsGame.Files.TED.Binary; + +internal enum TedChunkType : uint +{ + MapInfo = 0x0, + StartPositions = 0x3, + MapPreviewImageData = 0x13, +} + +internal enum MapInfoChunkType : uint +{ + Version = 0, + Type = 1, + PlayerCount = 2, + MapLevels = 3, + SupportedWeather = 4, + FactionOwner = 5, + MapType = 6, + SupportedSystems = 7, + MapName = 8, + PlanetName = 9, + GameTypes = 10, + CustomMap = 11, + Width = 16, + Height = 17, + NewMultiplayerMarkerSystem = 18, +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs index bd335930..1bafc19e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.TED/Data/IMapData.cs @@ -1,8 +1,139 @@ -using AnakinRaW.CommonUtilities; +using System; +using System.Collections.Generic; +using System.Numerics; +using AnakinRaW.CommonUtilities; using PG.StarWarsGame.Files.ChunkFiles.Data; namespace PG.StarWarsGame.Files.TED.Data; -public class IMapData : DisposableObject, IChunkData +public interface IMapData : IChunkData { + public MapInfo MapInfo { get; } +} + +public class MapData : DisposableObject, IMapData +{ + public MapInfo MapInfo { get; } + + public MapData(MapInfo mapInfo) + { + MapInfo = mapInfo ?? throw new ArgumentNullException(nameof(mapInfo)); + } + + protected override void DisposeResources() + { + base.DisposeResources(); + MapInfo.Dispose(); + } +} + +public sealed record MapInfo : IDisposable +{ + private byte[]? _previewImage; + + public int Version { get; init; } + + public string MapFileName + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } = string.Empty; + + public string MapName + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } = string.Empty; + + public string DisplayName => $"({MaxPlayers}) {MapName}"; + + public string PlanetName + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } = string.Empty; + + public string GameTypes + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } = string.Empty; + + public MapMode Type { get; init; } + + public MapEnvironmentType Terrain { get; init; } + + public MapOwnerType Owner { get; init; } + + public int MaxPlayers { get; init; } + + public int Levels { get; init; } + + public bool CustomMap { get; init; } + + public bool NewMultiplayerMarkerSystem { get; init; } + + public Vector2 MapSize { get; init; } + + public IReadOnlyList StartPositionMarkers + { + get; + init => field = value ?? throw new ArgumentNullException(nameof(value)); + } = []; + + public byte[]? EmbeddedPreviewTextureFile + { + get + { + if (_previewImage is null) + return null; + return (byte[])_previewImage.Clone(); + } + init => _previewImage = value; + } + + public int? EmbeddedPreviewTextureFileSize { get; init; } + + // We use the presence of the file size to determine if a preview image exists, + // as the file data itself may not have been loaded. + public bool PreviewImageExists => EmbeddedPreviewTextureFileSize.HasValue; + + public void Dispose() + { + if (_previewImage is not null) + { + Array.Clear(_previewImage, 0, _previewImage.Length); + _previewImage = null; + } + } +} + + +// TODO: What happens if a map has 0 (Galactic) set??? +public enum MapMode +{ + Land = 1, + Space = 2 +} + +public enum MapEnvironmentType +{ + Temperate = 0x0, + Arctic = 0x1, + Desert = 0x2, + Forest = 0x3, + Swamp = 0x4, + Volcanic = 0x5, + Urban = 0x6, + Space = 0x7, +} + +public enum MapOwnerType +{ + Rebel = 0x0, + Empire = 0x1, + Pirate = 0x2, + Branched = 0x3, + Underworld = 0x4, + Hutts = 0x5, } \ No newline at end of file From a5bb72999abffcb2c6577566bb4f8a3a35fac03f Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 17 May 2026 11:39:24 +0200 Subject: [PATCH 17/17] update submodel --- modules/ModdingToolBase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase index da072f43..3901d1a8 160000 --- a/modules/ModdingToolBase +++ b/modules/ModdingToolBase @@ -1 +1 @@ -Subproject commit da072f43e6b85aab35b43d11f6b36eab61bdcfa6 +Subproject commit 3901d1a899b8830ef691c06684b023a85f290b84