diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c2aa8df53..8b9421276 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,7 +23,7 @@ jobs: language: [ 'csharp' ] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Setup .NET Core SDK diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f0e609aa..8430c50e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Setup .NET @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Setup .NET diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49e295203..0b9ea23e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - uses: actions/setup-dotnet@v5 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..f053e3b1e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository purpose + +PetroglyphTools is a set of .NET libraries (published as `AlamoEngineTools.*` NuGet packages) for reading and writing the proprietary file formats used by Petroglyph's Alamo / Glyphx engines — most notably Star Wars: Empire at War. Each format gets its own library; `PG.Commons` is the shared foundation. + +## Solution layout + +The solution file is **`PetroglyphTools.slnx`** (the new XML solution format — not `.sln`). Each format library lives in a top-level folder containing two sibling projects: `PG.` (production) and `PG..Test` (xUnit v3 test runner). Notable cross-cutting projects: + +- `PG.Commons` — CRC32 hashing, numerics, `ServiceBase`, file-name validation, stream/string utilities. Foundation for everything else. +- `PG.StarWarsGame.Files` — abstract `PetroglyphFileHolder` and `PetroglyphFileInformation` record types that all file holders extend. +- `PG.Testing` — `PGTestBase` (built on `AnakinRaW.CommonUtilities.Testing.TestBaseWithFileSystem` + Testably.Abstractions for an in-memory `IFileSystem`). +- `PG.StarWarsGame.Files.Testing` — shared file-builder test base. +- `PG.Benchmarks` — BenchmarkDotNet harness (not in the slnx test pipeline). + +Format libraries: `PG.StarWarsGame.Files.{MEG,DAT,MTD,Xml,AnimationSfxMap}` and `PG.StarWarsGame.Localisation`. The `Xml` projects are present in the slnx but currently have `` — they are not built or tested by the default solution build. + +## Common commands + +Run from the repo root. The repo uses `Microsoft.Testing.Platform` (declared in `global.json`), so test projects build as executables and `dotnet test` invokes MTP rather than VSTest. + +```pwsh +dotnet build --configuration Release +dotnet test --configuration Release # full test suite (matches CI) +dotnet test PG.Commons/PG.Commons.Test # tests for one project +dotnet test PG.Commons/PG.Commons.Test --filter "FullyQualifiedName~PgFileNameUtilitiesTest" +dotnet pack --configuration Release --output ./packages # produces NuGet packages under bin/Packages/ +``` + +Benchmarks are run by executing the `PG.Benchmarks` project directly (`dotnet run -c Release --project PG.Benchmarks`). + +## Target frameworks + +- Production libraries: `netstandard2.0;netstandard2.1;net10.0` (some are `netstandard2.0;netstandard2.1` only — e.g. `PG.StarWarsGame.Files`). +- Test projects: `net8.0;net10.0`, plus `net481` on Windows. +- `Directory.Build.props` enforces `Nullable=enable`, `ImplicitUsings=disable`, `LangVersion=latest`, and `EnableNETAnalyzers=true`. Most production projects additionally set `TreatWarningsAsErrors=true` and `GenerateDocumentationFile=true` — XML doc comments on public API are mandatory; missing-doc warnings will fail the build. +- Modern language features on `netstandard2.0` are enabled via the `Nullable`, `IsExternalInit`, and `Required` polyfill packages — do not remove these references when editing csproj files. + +## Architectural conventions + +**Service registration via `*ServiceContribution` classes.** Each library exposes a static class (e.g. `MegServiceContribution.SupportMEG(IServiceCollection)`, `DatServiceContribution.SupportDAT(...)`, `PetroglyphCommons.ContributeServices(...)`) that registers all of its services in DI. Consumers compose the libraries they need; tests call the relevant `Support*` extension from a `Common*TestBase` deriving from `PGTestBase`. **When adding a new service, register it in the corresponding `*ServiceContribution` method** — there is no auto-discovery. + +**`ServiceBase` for services, `PetroglyphFileHolder` for file containers.** Services derive from `PG.Commons.Services.ServiceBase`, which captures `IServiceProvider`, `IFileSystem` (always resolved from DI — never use `System.IO` directly so the in-memory test FS works), and an `ILogger`. File holders derive from `PetroglyphFileHolder` and pair a parsed model with a `PetroglyphFileInformation` record (a `record` so it supports `with` expressions; the holder stores its own copy and disposes it). + +**MEG packable files.** `PetroglyphMegPackableFileInformation` skips the on-disk existence check when `IsInsideMeg` is true. When writing code that constructs file information for content extracted from a MEG, use this subtype rather than `PetroglyphFileInformation` directly. + +**MEG versions.** The MEG library handles three on-disk format versions (`MegFileVersion.V1/V2/V3`) — V1 is what EaW/FoC ship; V3 may be encrypted. Binary readers/writers are split into `Binary/Reader/V1/...` and `Binary/Construction/V1/...` subfolders; do not assume there is only one version when adding code under `Binary/`. + +**CRC32 is foundational.** Many Petroglyph formats key entries by CRC32 of an ASCII (uppercased) name. `ICrc32HashingService` and `CrcUtilities` in `PG.Commons` are the canonical entry points; do not introduce a second CRC32 implementation. + +## Branching & release + +- Day-to-day work targets **`develop`** (CI: `test.yml` runs build+test on Windows and Ubuntu for PRs into `develop`). +- Merges into **`master`** trigger `release.yml`: it runs the test workflow, then `dotnet pack`, pushes packages to nuget.org, and creates a GitHub release tagged `v{SemVer2}`. Versioning is computed by **Nerdbank.GitVersioning** from `version.json` — do not hand-edit version numbers in csproj files. +- The `.vscode/settings.json` references `PetroglyphTools.sln`, which does not exist in the repo. Use the `.slnx` file (or open the folder) instead. diff --git a/Directory.Build.props b/Directory.Build.props index 9a9ba6324..9ec498ce9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -32,7 +32,7 @@ all - 3.9.50 + 3.10.85 diff --git a/PG.Benchmarks/PG.Benchmarks.csproj b/PG.Benchmarks/PG.Benchmarks.csproj index 2d0225c52..e6da6f989 100644 --- a/PG.Benchmarks/PG.Benchmarks.csproj +++ b/PG.Benchmarks/PG.Benchmarks.csproj @@ -1,4 +1,4 @@ - + false net10.0 @@ -19,10 +19,10 @@ - - + + - + diff --git a/PG.Commons/PG.Commons.Test/PG.Commons.Test.csproj b/PG.Commons/PG.Commons.Test/PG.Commons.Test.csproj index 72c7c2445..95a920ced 100644 --- a/PG.Commons/PG.Commons.Test/PG.Commons.Test.csproj +++ b/PG.Commons/PG.Commons.Test/PG.Commons.Test.csproj @@ -12,7 +12,7 @@ - + diff --git a/PG.Commons/PG.Commons/Data/CrcBasedEqualityComparer.cs b/PG.Commons/PG.Commons/Data/CrcBasedEqualityComparer.cs index 4d058af23..b7a9d217c 100644 --- a/PG.Commons/PG.Commons/Data/CrcBasedEqualityComparer.cs +++ b/PG.Commons/PG.Commons/Data/CrcBasedEqualityComparer.cs @@ -28,7 +28,7 @@ namespace PG.Commons.Data; public sealed class CrcBasedEqualityComparer : IEqualityComparer where T : IHasCrc32 { /// - /// Returns a default equality comparer for the type specified by the generic argument. + /// Gets the default equality comparer for the type specified by the generic argument. /// public static readonly CrcBasedEqualityComparer Instance = new(); @@ -45,8 +45,8 @@ public sealed class CrcBasedEqualityComparer : IEqualityComparer where T : /// The values of and are equal. /// /// - /// The first object of type to compare. - /// The second object of type to compare. + /// The first object to compare. + /// The second object to compare. /// /// if the specified objects are both , /// are the same reference, or have equal CRC32 checksums; otherwise, . diff --git a/PG.Commons/PG.Commons/Data/IHasCrc32.cs b/PG.Commons/PG.Commons/Data/IHasCrc32.cs index febd2943f..3a8d745dd 100644 --- a/PG.Commons/PG.Commons/Data/IHasCrc32.cs +++ b/PG.Commons/PG.Commons/Data/IHasCrc32.cs @@ -6,7 +6,7 @@ namespace PG.Commons.Data; /// -/// An interface representing data that has a CRC32 checksum. +/// Represents data that has a CRC32 checksum. /// /// /// It is up to the implementation to decide which properties are considered for the CRC32 checksum. diff --git a/PG.Commons/PG.Commons/Hashing/Crc32.cs b/PG.Commons/PG.Commons/Hashing/Crc32.cs index 7be57a2c0..1988a58d1 100644 --- a/PG.Commons/PG.Commons/Hashing/Crc32.cs +++ b/PG.Commons/PG.Commons/Hashing/Crc32.cs @@ -57,7 +57,7 @@ public override string ToString() } /// - /// When , the checksum is represented by a signed integer; unsigned otherwise. + /// to represent the checksum as a signed integer; otherwise, . public string ToString(bool asSignedInteger) { var sb = new StringBuilder("CRC: "); @@ -95,7 +95,7 @@ public override int GetHashCode() /// /// Returns the CRC32 checksum as a byte array. /// - /// The CRC32 checksum as a byte array. + /// The CRC32 checksum in little endian byte order. public unsafe byte[] GetBytes() { Span data = stackalloc byte[sizeof(Crc32)]; @@ -106,6 +106,7 @@ public unsafe byte[] GetBytes() /// /// Writes the CRC32 checksum into a span of bytes in little endian. /// + /// The span to write the checksum into. public void GetBytes(Span destination) { BinaryPrimitives.WriteUInt32LittleEndian(destination, _checksum); @@ -178,15 +179,15 @@ public void GetBytes(Span destination) } /// - /// Defines an implicit conversion of an CRC32 checksum to an . + /// Defines an explicit conversion of a CRC32 checksum to a . /// - /// The checksum data. + /// The checksum to convert. public static explicit operator uint(Crc32 crc) => crc._checksum; /// - /// Defines an implicit conversion of an CRC32 checksum to an , which might be negative. + /// Defines an explicit conversion of a CRC32 checksum to an , which might be negative. /// - /// The checksum data. + /// The checksum to convert. public static explicit operator int(Crc32 crc) { unchecked diff --git a/PG.Commons/PG.Commons/Numerics/Vector2Int.cs b/PG.Commons/PG.Commons/Numerics/Vector2Int.cs index fbcdc919d..34419faa9 100644 --- a/PG.Commons/PG.Commons/Numerics/Vector2Int.cs +++ b/PG.Commons/PG.Commons/Numerics/Vector2Int.cs @@ -18,8 +18,8 @@ namespace PG.Commons.Numerics; public int Second { get; } /// - /// Constructs a vector from the given . If the span does not contain enough elements, - /// the default integer value 0 is used to initialize the respecting component. + /// Initializes a new instance of the struct from the given . + /// If the span does not contain enough elements, the default integer value 0 is used to initialize the respective component. /// /// The span of elements to assign to the vector. public Vector2Int(ReadOnlySpan values) @@ -32,7 +32,7 @@ public Vector2Int(ReadOnlySpan values) } /// - /// Creates a vector whose elements have the specified values. + /// Initializes a new instance of the struct whose elements have the specified values. /// /// The value to assign to the field. /// The value to assign to the field. @@ -56,7 +56,7 @@ public bool Equals(Vector2Int other) /// Returns a value that indicates whether this instance and a specified object are equal. /// /// The object to compare with the current instance. - /// if the current instance and are equal; otherwise, . If obj is , the method returns . + /// if the current instance and are equal; otherwise, . If is , the method returns . public override bool Equals(object? obj) { return obj is Vector2Int other && Equals(other); diff --git a/PG.Commons/PG.Commons/Numerics/Vector3Int.cs b/PG.Commons/PG.Commons/Numerics/Vector3Int.cs index 6e0bc842d..d33a71eed 100644 --- a/PG.Commons/PG.Commons/Numerics/Vector3Int.cs +++ b/PG.Commons/PG.Commons/Numerics/Vector3Int.cs @@ -23,8 +23,8 @@ namespace PG.Commons.Numerics; public int Third { get; } /// - /// Constructs a vector from the given . If the span does not contain enough elements, - /// the default integer value 0 is used to initialize the respecting component. + /// Initializes a new instance of the struct from the given . + /// If the span does not contain enough elements, the default integer value 0 is used to initialize the respective component. /// /// The span of elements to assign to the vector. public Vector3Int(ReadOnlySpan values) @@ -39,7 +39,7 @@ public Vector3Int(ReadOnlySpan values) } /// - /// Creates a vector whose elements have the specified values. + /// Initializes a new instance of the struct whose elements have the specified values. /// /// The value to assign to the field. /// The value to assign to the field. @@ -65,7 +65,7 @@ public bool Equals(Vector3Int other) /// Returns a value that indicates whether this instance and a specified object are equal. /// /// The object to compare with the current instance. - /// if the current instance and are equal; otherwise, . If obj is , the method returns . + /// if the current instance and are equal; otherwise, . If is , the method returns . public override bool Equals(object? obj) { return obj is Vector3Int other && Equals(other); diff --git a/PG.Commons/PG.Commons/Numerics/Vector4Int.cs b/PG.Commons/PG.Commons/Numerics/Vector4Int.cs index ed692318c..fd4394098 100644 --- a/PG.Commons/PG.Commons/Numerics/Vector4Int.cs +++ b/PG.Commons/PG.Commons/Numerics/Vector4Int.cs @@ -28,8 +28,8 @@ namespace PG.Commons.Numerics; public int Fourth { get; } /// - /// Constructs a vector from the given . If the span does not contain enough elements, - /// the default integer value 0 is used to initialize the respecting component. + /// Initializes a new instance of the struct from the given . + /// If the span does not contain enough elements, the default integer value 0 is used to initialize the respective component. /// /// The span of elements to assign to the vector. public Vector4Int(ReadOnlySpan values) @@ -46,7 +46,7 @@ public Vector4Int(ReadOnlySpan values) } /// - /// Creates a vector whose elements have the specified values. + /// Initializes a new instance of the struct whose elements have the specified values. /// /// The value to assign to the field. /// The value to assign to the field. @@ -74,7 +74,7 @@ public bool Equals(Vector4Int other) /// Returns a value that indicates whether this instance and a specified object are equal. /// /// The object to compare with the current instance. - /// if the current instance and are equal; otherwise, . If obj is , the method returns . + /// if the current instance and are equal; otherwise, . If is , the method returns . public override bool Equals(object? obj) { return obj is Vector4Int other && Equals(other); diff --git a/PG.Commons/PG.Commons/PG.Commons.csproj b/PG.Commons/PG.Commons/PG.Commons.csproj index 4003737fd..d19a406cb 100644 --- a/PG.Commons/PG.Commons/PG.Commons.csproj +++ b/PG.Commons/PG.Commons/PG.Commons.csproj @@ -19,15 +19,15 @@ true - - - - - + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PG.Commons/PG.Commons/Services/ServiceBase.cs b/PG.Commons/PG.Commons/Services/ServiceBase.cs index dc781c76e..0b5f640ed 100644 --- a/PG.Commons/PG.Commons/Services/ServiceBase.cs +++ b/PG.Commons/PG.Commons/Services/ServiceBase.cs @@ -11,7 +11,7 @@ namespace PG.Commons.Services; /// -/// Base class for services. +/// Provides a base class for services. /// public abstract class ServiceBase : DisposableObject { diff --git a/PG.Commons/PG.Commons/Utilities/CrcUtilities.cs b/PG.Commons/PG.Commons/Utilities/CrcUtilities.cs index 61c1110f4..24e4df327 100644 --- a/PG.Commons/PG.Commons/Utilities/CrcUtilities.cs +++ b/PG.Commons/PG.Commons/Utilities/CrcUtilities.cs @@ -24,7 +24,7 @@ public static class Crc32Utilities /// CRC32 sorted list of elements. /// The CRC-to-range table. /// is . - /// is not sorted. + /// is not sorted. /// /// The given input list [1,2,2,2,3] returns the following dictionary: {{1, 0..1}, {2, 1..4}, {3, 4..5}} /// @@ -125,7 +125,7 @@ public static IList SortByCrc32(IEnumerable items) where T : IHasCrc32 /// /// The type of the elements of source. /// A sequence of values to check for correct sorting. - /// is . + /// is . /// is not sorted by CRC32 checksum. public static void EnsureSortedByCrc32(IEnumerable items) where T : IHasCrc32 { @@ -140,8 +140,8 @@ public static void EnsureSortedByCrc32(IEnumerable items) where T : IHasCr /// /// The type of the elements of source. /// A sequence of values to check for correct sorting. - /// is the specified collection is sorted; otherwise, . - /// is . + /// if the specified collection is sorted; otherwise, . + /// is . public static bool IsSortedByCrc32(IEnumerable items) where T : IHasCrc32 { if (items == null) diff --git a/PG.Commons/PG.Commons/Utilities/EncodingExtensions.cs b/PG.Commons/PG.Commons/Utilities/EncodingExtensions.cs index fa3ae6e94..75c14cd70 100644 --- a/PG.Commons/PG.Commons/Utilities/EncodingExtensions.cs +++ b/PG.Commons/PG.Commons/Utilities/EncodingExtensions.cs @@ -36,7 +36,7 @@ public static class EncodingExtensions /// The encoding to be used. /// The number of characters to encode. /// The number of bytes - /// is . + /// is . /// is less than zero. /// is not supported. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/PG.Commons/PG.Commons/Utilities/PGFileNameUtilities.cs b/PG.Commons/PG.Commons/Utilities/PGFileNameUtilities.cs index 1cdb2556a..bc643b115 100644 --- a/PG.Commons/PG.Commons/Utilities/PGFileNameUtilities.cs +++ b/PG.Commons/PG.Commons/Utilities/PGFileNameUtilities.cs @@ -13,7 +13,7 @@ namespace PG.Commons.Utilities; public static class PGFileNameUtilities { /// - /// Checks whether a given filename is can be used for a Petroglyph Star Wars game. + /// Checks whether a given filename can be used for a Petroglyph Star Wars game. /// /// /// A filename is considered to be invalid under the following conditions:
@@ -22,8 +22,11 @@ public static class PGFileNameUtilities /// c) The filename contains a non ASCII character (char > 0xFF).
///
/// The filename to check. - /// Detailed information status. Can be used for error message reporting. - /// when the filename is valid; otherwise. + /// + /// When this method returns, contains the detailed validation status, which can be used for error message reporting. + /// This parameter is treated as uninitialized. + /// + /// if the filename is valid; otherwise, . public static bool IsValidFileName(ReadOnlySpan filename, out FileNameValidationResult result) { var validator = WindowsFileNameValidator.Instance; diff --git a/PG.Commons/PG.Commons/Utilities/StreamExtensions.cs b/PG.Commons/PG.Commons/Utilities/StreamExtensions.cs index d57230445..7ae091b0d 100644 --- a/PG.Commons/PG.Commons/Utilities/StreamExtensions.cs +++ b/PG.Commons/PG.Commons/Utilities/StreamExtensions.cs @@ -29,7 +29,10 @@ public static string GetFilePath(this Stream stream) /// Gets the file path of the file opened in the . The path may be relative. /// /// The stream to get the file path from. - /// Stores the status whether is a MEG stream. + /// + /// When this method returns, contains a value that indicates whether is a MEG stream. + /// This parameter is treated as uninitialized. + /// /// The file path of the opened file. /// does not have path information. public static string GetFilePath(this Stream stream, out bool isMegStream) @@ -57,7 +60,7 @@ public static string GetFilePath(this Stream stream, out bool isMegStream) /// The stream to retrieve the file path from. /// /// When this method returns, contains the file path of the opened file if the operation was successful; - /// otherwise, . This parameter is passed uninitialized. + /// otherwise, . This parameter is treated as uninitialized. /// /// /// if the file path was successfully retrieved; otherwise, . @@ -73,7 +76,7 @@ public static bool TryGetFilePath(this Stream stream, [NotNullWhen(true)] out st /// The stream to retrieve the file path from. /// /// When this method returns, contains the file path of the opened file if the operation was successful; - /// otherwise, . This parameter is passed uninitialized. + /// otherwise, . This parameter is treated as uninitialized. /// /// /// When this method returns, contains a value indicating whether the stream is a MEG file data stream. diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Binary/Converter/DatBinaryConverterTest.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Binary/Converter/DatBinaryConverterTest.cs index f035a51b7..556936072 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Binary/Converter/DatBinaryConverterTest.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Binary/Converter/DatBinaryConverterTest.cs @@ -1,10 +1,9 @@ -using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.DAT.Binary; using PG.StarWarsGame.Files.DAT.Binary.Metadata; using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; using PG.Testing; using System; using System.Collections.Generic; @@ -75,7 +74,7 @@ public void BinaryToModel_Sorted() var model = _converter.BinaryToModel(binary); Assert.Equal(binary.RecordNumber, model.Count); - Assert.Equal(DatFileType.OrderedByCrc32, model.KeySortOrder); + Assert.Equal(DatLayoutKind.OrderedByCrc32, model.Layout); Assert.Equal([ CreateEntry(true, "a"), @@ -112,7 +111,7 @@ public void BinaryToModel_Unsorted() var model = _converter.BinaryToModel(binary); Assert.Equal(binary.RecordNumber, model.Count); - Assert.Equal(DatFileType.NotOrdered, model.KeySortOrder); + Assert.Equal(DatLayoutKind.NotOrdered, model.Layout); Assert.Equal([ CreateEntry(false, "a"), @@ -212,7 +211,7 @@ public void ModelToBinary_WantsSortedButIsNot_ThrowsArgumentException() // Model claims to be sorted, while it does not enforce it. private class InvalidDatModel(IEnumerable entries) : DatModel(entries) { - public override DatFileType KeySortOrder => DatFileType.OrderedByCrc32; + public override DatLayoutKind Layout => DatLayoutKind.OrderedByCrc32; public override ImmutableFrugalList EntriesWithCrc(Crc32 key) { diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Binary/Reader/DatFileReaderTest.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Binary/Reader/DatFileReaderTest.cs index ff2e30cb4..3a87f1600 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Binary/Reader/DatFileReaderTest.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Binary/Reader/DatFileReaderTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,7 +7,7 @@ using PG.Commons.Hashing; using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.DAT.Binary; -using PG.StarWarsGame.Files.DAT.Files; +using PG.StarWarsGame.Files.DAT.Data; using Xunit; namespace PG.StarWarsGame.Files.DAT.Test.Binary.Reader; @@ -22,36 +22,36 @@ public DatFileReaderTest() } [Fact] - public void PeekFileType_ThrowsArgumentNullException() + public void PeekLayout_ThrowsArgumentNullException() { - Assert.Throws(() => _reader.PeekFileType(null!)); + Assert.Throws(() => _reader.PeekLayout(null!)); } [Fact] - public void PeekFileType_ThrowsBinaryCorruptedException() + public void PeekLayout_ThrowsBinaryCorruptedException() { - Assert.Throws(() => _reader.PeekFileType(new MemoryStream())); + Assert.Throws(() => _reader.PeekLayout(new MemoryStream())); } [Theory] - [MemberData(nameof(DatFileTypeTestData))] - public void PeekFileType(Stream stream, DatFileType expectedFileType) + [MemberData(nameof(DatLayoutKindTestData))] + public void PeekLayout(Stream stream, DatLayoutKind expectedLayout) { - var fileType = _reader.PeekFileType(stream); - Assert.Equal(expectedFileType, fileType); + var layout = _reader.PeekLayout(stream); + Assert.Equal(expectedLayout, layout); // Ensure that stream is not disposed after read operation stream.Position = 1; } - public static IEnumerable DatFileTypeTestData() + public static IEnumerable DatLayoutKindTestData() { return [ [ // Empty .DAT: While the file type is not specified by the interface, this test must not crash. new MemoryStream([0x0, 0x0, 0x0, 0x0]), - DatFileType.OrderedByCrc32 + DatLayoutKind.OrderedByCrc32 ], [ new MemoryStream([ @@ -66,7 +66,7 @@ public static IEnumerable DatFileTypeTestData() 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 ]), - DatFileType.OrderedByCrc32 + DatLayoutKind.OrderedByCrc32 ], [ new MemoryStream([ @@ -81,39 +81,39 @@ public static IEnumerable DatFileTypeTestData() 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 ]), - DatFileType.NotOrdered + DatLayoutKind.NotOrdered ], [ TestingHelpers.GetEmbeddedResource(typeof(DatFileReaderTest), "Files.EmptyKeyWithValue.dat"), - DatFileType.OrderedByCrc32 + DatLayoutKind.OrderedByCrc32 ], [ TestingHelpers.GetEmbeddedResource(typeof(DatFileReaderTest), "Files.SingleEmptyEntry.dat"), - DatFileType.OrderedByCrc32 + DatLayoutKind.OrderedByCrc32 ], [ TestingHelpers.GetEmbeddedResource(typeof(DatFileReaderTest), "Files.SingleEntry.dat"), - DatFileType.OrderedByCrc32 + DatLayoutKind.OrderedByCrc32 ], [ TestingHelpers.GetEmbeddedResource(typeof(DatFileReaderTest), "Files.Sorted_TwoEntries.dat"), - DatFileType.OrderedByCrc32 + DatLayoutKind.OrderedByCrc32 ], [ TestingHelpers.GetEmbeddedResource(typeof(DatFileReaderTest), "Files.Sorted_TwoEntriesDuplicate.dat"), - DatFileType.OrderedByCrc32 + DatLayoutKind.OrderedByCrc32 ], [ TestingHelpers.GetEmbeddedResource(typeof(DatFileReaderTest), "Files.mastertextfile_english.dat"), - DatFileType.OrderedByCrc32 + DatLayoutKind.OrderedByCrc32 ], [ TestingHelpers.GetEmbeddedResource(typeof(DatFileReaderTest), "Files.Index_WithDuplicates.dat"), - DatFileType.NotOrdered + DatLayoutKind.NotOrdered ], [ TestingHelpers.GetEmbeddedResource(typeof(DatFileReaderTest), "Files.creditstext_english.dat"), - DatFileType.NotOrdered + DatLayoutKind.NotOrdered ] ]; } diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/DatModelTest.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/DatModelTest.cs index 032eb4dd1..02adc964d 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/DatModelTest.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/DatModelTest.cs @@ -1,17 +1,16 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using PG.Commons.Hashing; using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; using Xunit; namespace PG.StarWarsGame.Files.DAT.Test.Data; public abstract class DatModelTest { - protected abstract DatFileType ExpectedFileType { get; } + protected abstract DatLayoutKind ExpectedLayout { get; } protected abstract IDatModel CreateModel(IList entries); @@ -28,7 +27,7 @@ public void Ctor() { var model = CreateModel(CreateDataEntries()); - Assert.Equal(ExpectedFileType, model.KeySortOrder); + Assert.Equal(ExpectedLayout, model.Layout); Assert.Equal(4, model.Count); Assert.Equivalent(new HashSet{ "1", "3", "4" }.ToList(), model.Keys.ToList()); Assert.Equivalent(new HashSet { new(1), new(3), new(4) }.ToList(), model.CrcKeys.ToList()); diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/SortedDatModelTest.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/SortedDatModelTest.cs index 3b3154535..679dab12f 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/SortedDatModelTest.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/SortedDatModelTest.cs @@ -1,16 +1,15 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using PG.Commons.Hashing; using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; using Xunit; namespace PG.StarWarsGame.Files.DAT.Test.Data; public class SortedDatModelTest : DatModelTest { - protected override DatFileType ExpectedFileType => DatFileType.OrderedByCrc32; + protected override DatLayoutKind ExpectedLayout => DatLayoutKind.OrderedByCrc32; private SortedDatModel CreateSortedModel(IList entries) { @@ -60,6 +59,6 @@ public void ToUnsortedModel() var unsorted = model.ToUnsortedModel(); Assert.Equal(model.ToList(), unsorted.ToList()); - Assert.Equal(DatFileType.NotOrdered, unsorted.KeySortOrder); + Assert.Equal(DatLayoutKind.NotOrdered, unsorted.Layout); } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/UnsortedDatModelTest.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/UnsortedDatModelTest.cs index a03a4c23a..203f0ba5b 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/UnsortedDatModelTest.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Data/UnsortedDatModelTest.cs @@ -1,15 +1,14 @@ -using System.Collections.Generic; +using System.Collections.Generic; using PG.Commons.Hashing; using PG.Commons.Utilities; using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; using Xunit; namespace PG.StarWarsGame.Files.DAT.Test.Data; public class UnsortedDatModelTest : DatModelTest { - protected override DatFileType ExpectedFileType => DatFileType.NotOrdered; + protected override DatLayoutKind ExpectedLayout => DatLayoutKind.NotOrdered; private UnsortedDatModel CreateUnsortedModel(IList entries) { @@ -46,6 +45,6 @@ public void ToSortedModel() var sorted = model.ToSortedModel(); Assert.True(Crc32Utilities.IsSortedByCrc32(sorted)); - Assert.Equal(DatFileType.OrderedByCrc32, sorted.KeySortOrder); + Assert.Equal(DatLayoutKind.OrderedByCrc32, sorted.Layout); } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/PG.StarWarsGame.Files.DAT.Test.csproj b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/PG.StarWarsGame.Files.DAT.Test.csproj index ec3c98a61..51ea74661 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/PG.StarWarsGame.Files.DAT.Test.csproj +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/PG.StarWarsGame.Files.DAT.Test.csproj @@ -34,7 +34,7 @@
- + diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/DatBuilderBaseTest.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/DatBuilderBaseTest.cs index 9ae8cd72f..e715208bf 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/DatBuilderBaseTest.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/DatBuilderBaseTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -266,7 +266,7 @@ public void BuildModel() builder.AddEntry("key3", "value"); var model = builder.BuildModel(); - Assert.Equal(builder.TargetKeySortOrder, model.KeySortOrder); + Assert.Equal(builder.TargetLayout, model.Layout); Assert.Equal(3, model.Count); Assert.Equal(["key1", "key2", "key3"], model.Keys); } @@ -276,12 +276,12 @@ public void IntegrationTest_Sorted_MasterText_CreateFromModelAndBuild() { using (var fs = FileSystem.FileStream.New("MasterTextFile.dat", FileMode.Create)) { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.mastertextfile_english.dat"); + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.mastertextfile_english.dat"); stream.CopyTo(fs); } - var masterTextModel = ServiceProvider.GetRequiredService().LoadAs("MasterTextFile.dat", - IsOrderedBuilder ? DatFileType.OrderedByCrc32 : DatFileType.NotOrdered).Content; + var masterTextModel = ServiceProvider.GetRequiredService().LoadFileAs("MasterTextFile.dat", + IsOrderedBuilder ? DatLayoutKind.OrderedByCrc32 : DatLayoutKind.NotOrdered).Content; var builder = CreateBuilder(); diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/EmpireAtWarCreditsTextBuilderTest.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/EmpireAtWarCreditsTextBuilderTest.cs index 78f38b1af..64f3849e0 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/EmpireAtWarCreditsTextBuilderTest.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/EmpireAtWarCreditsTextBuilderTest.cs @@ -1,7 +1,6 @@ -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing; using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; using PG.StarWarsGame.Files.DAT.Services; using PG.StarWarsGame.Files.DAT.Services.Builder; using PG.StarWarsGame.Files.DAT.Services.Builder.Validation; @@ -51,7 +50,7 @@ public void Ctor() Assert.NotNull(builder.SortedEntries); Assert.NotNull(builder.Entries); Assert.Equal(BuilderOverrideKind.AllowDuplicate, builder.KeyOverwriteBehavior); - Assert.Equal(DatFileType.NotOrdered, builder.TargetKeySortOrder); + Assert.Equal(DatLayoutKind.NotOrdered, builder.TargetLayout); Assert.IsType(builder.KeyValidator); } @@ -60,11 +59,11 @@ public void IntegrationTest_Unsorted_Credits_CreateFromModelAndBuild() { using (var fs = FileSystem.FileStream.New("Credits.dat", FileMode.Create)) { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.creditstext_english.dat"); + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.creditstext_english.dat"); stream.CopyTo(fs); } - var creditsModel = ServiceProvider.GetRequiredService().LoadAs("Credits.dat", DatFileType.NotOrdered).Content; + var creditsModel = ServiceProvider.GetRequiredService().LoadFileAs("Credits.dat", DatLayoutKind.NotOrdered).Content; var builder = CreateBuilder(); diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/EmpireAtWarMasterTextBuilderTest.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/EmpireAtWarMasterTextBuilderTest.cs index 3ffc7d8fb..65e299495 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/EmpireAtWarMasterTextBuilderTest.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/Builder/EmpireAtWarMasterTextBuilderTest.cs @@ -1,6 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; using PG.StarWarsGame.Files.DAT.Services.Builder; using PG.StarWarsGame.Files.DAT.Services.Builder.Validation; using Xunit; @@ -41,7 +40,7 @@ public void Ctor() Assert.NotNull(builder.SortedEntries); Assert.NotNull(builder.Entries); Assert.Equal(OverrideKind, builder.KeyOverwriteBehavior); - Assert.Equal(DatFileType.OrderedByCrc32, builder.TargetKeySortOrder); + Assert.Equal(DatLayoutKind.OrderedByCrc32, builder.TargetLayout); Assert.IsType(builder.KeyValidator); } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/DatFileServiceTest.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/DatFileServiceTest.cs deleted file mode 100644 index 118ac0265..000000000 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/DatFileServiceTest.cs +++ /dev/null @@ -1,293 +0,0 @@ -using AnakinRaW.CommonUtilities.Testing; -using Microsoft.Extensions.DependencyInjection; -using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; -using PG.StarWarsGame.Files.DAT.Services; -using PG.Testing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Testably.Abstractions.Testing; -using Xunit; - -namespace PG.StarWarsGame.Files.DAT.Test.Services; - -public class DatFileServiceTest : PGTestBase -{ - private readonly DatFileService _service; - - public DatFileServiceTest() - { - _service = new DatFileService(ServiceProvider); - } - - protected override void SetupServices(IServiceCollection serviceCollection) - { - base.SetupServices(serviceCollection); - serviceCollection.SupportDAT(); - } - - [Fact] - public void CreateDatFile_Throws() - { - Assert.Throws(() => - _service.CreateDatFile(null!, new List(), DatFileType.NotOrdered)); - - Assert.Throws(() => - _service.CreateDatFile(FileSystem.FileStream.New("test.dat", FileMode.Create), null!, DatFileType.NotOrdered)); - } - - [Fact] - public void CreateDatFile_PreserveOrder() - { - var binary = DatTestData.CreateUnsortedBinary(); - var model = DatTestData.CreateUnsortedModel(); - - var fs = FileSystem.FileStream.New("test.dat", FileMode.Create); - _service.CreateDatFile(fs, model, DatFileType.NotOrdered); - fs.Dispose(); - - Assert.Equal(binary.Bytes, FileSystem.File.ReadAllBytes("test.dat")); - } - - [Fact] - public void CreateDatFile_SortEntries() - { - var binary = DatTestData.CreateSortedBinary(); - var model = DatTestData.CreateUnsortedModel(); - - var fs = FileSystem.FileStream.New("test.dat", FileMode.Create); - _service.CreateDatFile(fs, model, DatFileType.OrderedByCrc32); - fs.Dispose(); - - Assert.Equal(binary.Bytes, FileSystem.File.ReadAllBytes("test.dat")); - } - - [Fact] - public void GetDatFileType_MatchesExpected() - { - using (var fs = FileSystem.FileStream.New("MasterTextFile.dat", FileMode.Create)) - { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.mastertextfile_english.dat"); - stream.CopyTo(fs); - } - using (var fs = FileSystem.FileStream.New("Credits.dat", FileMode.Create)) - { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.creditstext_english.dat"); - stream.CopyTo(fs); - } - - Assert.Equal(DatFileType.OrderedByCrc32, _service.GetDatFileType("MasterTextFile.dat")); - Assert.Equal(DatFileType.NotOrdered, _service.GetDatFileType("Credits.dat")); - } - - [Fact] - public void Load() - { - var sortedBinary = DatTestData.CreateSortedBinary(); - var unsortedBinary = DatTestData.CreateUnsortedBinary(); - - FileSystem.Initialize() - .WithFile("sorted.dat").Which(a => a.HasBytesContent(sortedBinary.Bytes)) - .WithFile("unsorted.dat").Which(a => a.HasBytesContent(unsortedBinary.Bytes)); - - var sortedFileHolder = _service.Load("sorted.dat"); - Assert.Equal(FileSystem.Path.GetFullPath("sorted.dat"), sortedFileHolder.FilePath); - Assert.Equal(FileSystem.Path.GetFullPath("sorted.dat"), sortedFileHolder.FileInformation.FilePath); - Assert.Equal(DatFileType.OrderedByCrc32, sortedFileHolder.Content.KeySortOrder); - Assert.Equal(DatTestData.CreateSortedModel(), sortedFileHolder.Content.ToList()); - - var unsortedFileHolder = _service.Load("unsorted.dat"); - Assert.Equal(FileSystem.Path.GetFullPath("unsorted.dat"), unsortedFileHolder.FilePath); - Assert.Equal(FileSystem.Path.GetFullPath("unsorted.dat"), unsortedFileHolder.FileInformation.FilePath); - Assert.Equal(DatFileType.NotOrdered, unsortedFileHolder.Content.KeySortOrder); - Assert.Equal(DatTestData.CreateUnsortedModel(), unsortedFileHolder.Content.ToList()); - } - - [Fact] - public void LoadAs_SortedAsUnsorted() - { - var sortedBinary = DatTestData.CreateSortedBinary(); - - FileSystem.Initialize() - .WithFile("sorted.dat").Which(a => a.HasBytesContent(sortedBinary.Bytes)); - - var unsortedFileHolder = _service.LoadAs("sorted.dat", DatFileType.NotOrdered); - - Assert.Equal(FileSystem.Path.GetFullPath("sorted.dat"), unsortedFileHolder.FilePath); - Assert.Equal(FileSystem.Path.GetFullPath("sorted.dat"), unsortedFileHolder.FileInformation.FilePath); - // Entries are still sorted, but the key sort oder was adjusted - Assert.Equal(DatFileType.NotOrdered, unsortedFileHolder.Content.KeySortOrder); - Assert.Equal(DatTestData.CreateSortedModel(), unsortedFileHolder.Content.ToList()); - } - - [Fact] - public void LoadAs_UnsortedAsSorted_Throws() - { - var unsortedBinary = DatTestData.CreateUnsortedBinary(); - - FileSystem.Initialize() - .WithFile("unsorted.dat").Which(a => a.HasBytesContent(unsortedBinary.Bytes)); - - Assert.Throws(() => - _service.LoadAs("unsorted.dat", DatFileType.OrderedByCrc32)); - } - - [Fact] - public void LoadStore_Sorted() - { - FileSystem.Initialize(); - using (var fs = FileSystem.FileStream.New("MasterTextFile.dat", FileMode.Create)) - { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.mastertextfile_english.dat"); - stream.CopyTo(fs); - } - - var datFile = _service.Load("MasterTextFile.dat").Content; - Assert.Equal(DatFileType.OrderedByCrc32, datFile.KeySortOrder); - - using (var fs = FileSystem.FileStream.New("NewSorted.dat", FileMode.Create)) - _service.CreateDatFile(fs, datFile, DatFileType.OrderedByCrc32); - - var asUnsortedDatFile = _service.LoadAs("MasterTextFile.dat", DatFileType.NotOrdered).Content; - Assert.Equal(DatFileType.NotOrdered, asUnsortedDatFile.KeySortOrder); - - using (var fs = FileSystem.FileStream.New("NewUnsorted.dat", FileMode.Create)) - _service.CreateDatFile(fs, asUnsortedDatFile, DatFileType.NotOrdered); - - - using (var fs = FileSystem.FileStream.New("NewSorted.dat", FileMode.Create)) - _service.CreateDatFile(fs, datFile, DatFileType.OrderedByCrc32); - using (var fs = FileSystem.FileStream.New("NewUnsorted.dat", FileMode.Create)) - _service.CreateDatFile(fs, asUnsortedDatFile, DatFileType.OrderedByCrc32); - - var expectedBytes = FileSystem.File.ReadAllBytes("MasterTextFile.dat"); - var actualBytesSorted = FileSystem.File.ReadAllBytes("NewSorted.dat"); - var actualBytesUnsorted = FileSystem.File.ReadAllBytes("NewUnsorted.dat"); - Assert.Equal(expectedBytes, actualBytesSorted); - Assert.Equal(expectedBytes, actualBytesUnsorted); - } - - [Fact] - public void LoadStore_Unsorted() - { - FileSystem.Initialize(); - using (var fs = FileSystem.FileStream.New("Credits.dat", FileMode.Create)) - { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.creditstext_english.dat"); - stream.CopyTo(fs); - } - - var datFile = _service.Load("Credits.dat").Content; - Assert.Equal(DatFileType.NotOrdered, datFile.KeySortOrder); - - using (var fs = FileSystem.FileStream.New("New.dat", FileMode.Create)) - _service.CreateDatFile(fs, datFile, DatFileType.NotOrdered); - - var expectedBytes = FileSystem.File.ReadAllBytes("Credits.dat"); - var actualBytesSorted = FileSystem.File.ReadAllBytes("New.dat"); - Assert.Equal(expectedBytes, actualBytesSorted); - } - - [Fact] - public void LoadStore_UnsortedAsSortedThrows() - { - FileSystem.Initialize(); - using (var fs = FileSystem.FileStream.New("Credits.dat", FileMode.Create)) - { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.creditstext_english.dat"); - stream.CopyTo(fs); - } - - var datFile = _service.Load("Credits.dat").Content; - - using (var fs = FileSystem.FileStream.New("New.dat", FileMode.Create)) - _service.CreateDatFile(fs, datFile, DatFileType.OrderedByCrc32); - - var creditBytes = FileSystem.File.ReadAllBytes("Credits.dat"); - var resortedBytes = FileSystem.File.ReadAllBytes("New.dat"); - Assert.NotEqual(creditBytes, resortedBytes); - - Assert.Throws(() => _service.LoadAs("Credits.dat", DatFileType.OrderedByCrc32)); - } - - [Fact] - public void Load_Empty() - { - FileSystem.Initialize(); - using (var fs = FileSystem.FileStream.New("Empty.dat", FileMode.Create)) - { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.Empty.dat"); - stream.CopyTo(fs); - } - - var model = _service.Load("Empty.dat"); - Assert.Empty(model.Content); - - model = _service.Load(FileSystem.File.OpenRead("Empty.dat")); - Assert.Empty(model.Content); - } - - [Fact] - public void Load_EmptyKeyWithValue() - { - FileSystem.Initialize(); - using (var fs = FileSystem.FileStream.New("EmptyKeyWithValue.dat", FileMode.Create)) - { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.EmptyKeyWithValue.dat"); - stream.CopyTo(fs); - } - - var model = _service.Load("EmptyKeyWithValue.dat"); - Assert.Single(model.Content); - Assert.True(model.Content.ContainsKey(string.Empty)); - - model = _service.Load(FileSystem.File.OpenRead("EmptyKeyWithValue.dat")); - Assert.Single(model.Content); - Assert.True(model.Content.ContainsKey(string.Empty)); - } - - [Fact] - public void Load_Sorted_TwoEntriesDuplicate() - { - FileSystem.Initialize(); - using (var fs = FileSystem.FileStream.New("Sorted_TwoEntriesDuplicate.dat", FileMode.Create)) - { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.Sorted_TwoEntriesDuplicate.dat"); - stream.CopyTo(fs); - } - - var model = _service.Load("Sorted_TwoEntriesDuplicate.dat"); - Assert.Equal(2, model.Content.Count); - Assert.Single(model.Content.Keys); - - model = _service.Load(FileSystem.File.OpenRead("Sorted_TwoEntriesDuplicate.dat")); - Assert.Equal(2, model.Content.Count); - Assert.Single(model.Content.Keys); - } - - - [Fact] - public void LoadModifyCreate() - { - FileSystem.Initialize(); - using (var fs = FileSystem.FileStream.New("Index_WithDuplicates.dat", FileMode.Create)) - { - using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatFileServiceTest), "Files.Index_WithDuplicates.dat"); - stream.CopyTo(fs); - } - - var model = _service.LoadAs("Index_WithDuplicates.dat", DatFileType.NotOrdered).Content; - Assert.Equal(DatFileType.NotOrdered, model.KeySortOrder); - var modelService = ServiceProvider.GetRequiredService(); - Assert.True(modelService.GetDuplicateEntries(model).Any()); - var withoutDups = modelService.RemoveDuplicates(model); - Assert.False(modelService.GetDuplicateEntries(withoutDups).Any()); - var sorted = modelService.SortModel(withoutDups); - Assert.Equal(DatFileType.OrderedByCrc32, sorted.KeySortOrder); - - using (var fs = FileSystem.FileStream.New("newSorted.dat", FileMode.Create)) - _service.CreateDatFile(fs, sorted, DatFileType.OrderedByCrc32); - } -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/DatServiceTest.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/DatServiceTest.cs new file mode 100644 index 000000000..45d6eb924 --- /dev/null +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/DatServiceTest.cs @@ -0,0 +1,408 @@ +using AnakinRaW.CommonUtilities.Testing; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.DAT.Data; +using PG.StarWarsGame.Files.DAT.Services; +using PG.Testing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Testably.Abstractions.Testing; +using Xunit; + +namespace PG.StarWarsGame.Files.DAT.Test.Services; + +public class DatServiceTest : PGTestBase +{ + private readonly IDatService _service; + + public DatServiceTest() + { + _service = ServiceProvider.GetRequiredService(); + } + + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + serviceCollection.SupportDAT(); + } + + [Fact] + public void CreateDatBinary_Throws() + { + Assert.Throws(() => + _service.CreateDatBinary(null!, new List(), DatLayoutKind.NotOrdered)); + + Assert.Throws(() => + _service.CreateDatBinary(FileSystem.FileStream.New("test.dat", FileMode.Create), null!, DatLayoutKind.NotOrdered)); + } + + [Fact] + public void CreateDatBinary_PreserveOrder() + { + var binary = DatTestData.CreateUnsortedBinary(); + var model = DatTestData.CreateUnsortedModel(); + + var fs = FileSystem.FileStream.New("test.dat", FileMode.Create); + _service.CreateDatBinary(fs, model, DatLayoutKind.NotOrdered); + fs.Dispose(); + + Assert.Equal(binary.Bytes, FileSystem.File.ReadAllBytes("test.dat")); + } + + [Fact] + public void CreateDatBinary_SortEntries() + { + var binary = DatTestData.CreateSortedBinary(); + var model = DatTestData.CreateUnsortedModel(); + + var fs = FileSystem.FileStream.New("test.dat", FileMode.Create); + _service.CreateDatBinary(fs, model, DatLayoutKind.OrderedByCrc32); + fs.Dispose(); + + Assert.Equal(binary.Bytes, FileSystem.File.ReadAllBytes("test.dat")); + } + + [Fact] + public void CreateDatBinary_MemoryStream_PreserveOrder() + { + var binary = DatTestData.CreateUnsortedBinary(); + var model = DatTestData.CreateUnsortedModel(); + + using var ms = new MemoryStream(); + _service.CreateDatBinary(ms, model, DatLayoutKind.NotOrdered); + + Assert.Equal(binary.Bytes, ms.ToArray()); + } + + [Fact] + public void CreateDatBinary_MemoryStream_SortEntries() + { + var binary = DatTestData.CreateSortedBinary(); + var model = DatTestData.CreateUnsortedModel(); + + using var ms = new MemoryStream(); + _service.CreateDatBinary(ms, model, DatLayoutKind.OrderedByCrc32); + + Assert.Equal(binary.Bytes, ms.ToArray()); + } + + [Fact] + public void GetDatLayoutKind_MatchesExpected() + { + using (var fs = FileSystem.FileStream.New("MasterTextFile.dat", FileMode.Create)) + { + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.mastertextfile_english.dat"); + stream.CopyTo(fs); + } + using (var fs = FileSystem.FileStream.New("Credits.dat", FileMode.Create)) + { + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.creditstext_english.dat"); + stream.CopyTo(fs); + } + + Assert.Equal(DatLayoutKind.OrderedByCrc32, _service.GetDatLayoutKind("MasterTextFile.dat")); + Assert.Equal(DatLayoutKind.NotOrdered, _service.GetDatLayoutKind("Credits.dat")); + } + + [Fact] + public void LoadFile() + { + var sortedBinary = DatTestData.CreateSortedBinary(); + var unsortedBinary = DatTestData.CreateUnsortedBinary(); + + FileSystem.Initialize() + .WithFile("sorted.dat").Which(a => a.HasBytesContent(sortedBinary.Bytes)) + .WithFile("unsorted.dat").Which(a => a.HasBytesContent(unsortedBinary.Bytes)); + + var sortedFileHolder = _service.LoadFile("sorted.dat"); + Assert.Equal(FileSystem.Path.GetFullPath("sorted.dat"), sortedFileHolder.FilePath); + Assert.Equal(FileSystem.Path.GetFullPath("sorted.dat"), sortedFileHolder.FileInformation.FilePath); + Assert.Equal(DatLayoutKind.OrderedByCrc32, sortedFileHolder.Content.Layout); + Assert.Equal(DatTestData.CreateSortedModel(), sortedFileHolder.Content.ToList()); + + var unsortedFileHolder = _service.LoadFile("unsorted.dat"); + Assert.Equal(FileSystem.Path.GetFullPath("unsorted.dat"), unsortedFileHolder.FilePath); + Assert.Equal(FileSystem.Path.GetFullPath("unsorted.dat"), unsortedFileHolder.FileInformation.FilePath); + Assert.Equal(DatLayoutKind.NotOrdered, unsortedFileHolder.Content.Layout); + Assert.Equal(DatTestData.CreateUnsortedModel(), unsortedFileHolder.Content.ToList()); + } + + [Fact] + public void LoadFileAs_SortedAsUnsorted() + { + var sortedBinary = DatTestData.CreateSortedBinary(); + + FileSystem.Initialize() + .WithFile("sorted.dat").Which(a => a.HasBytesContent(sortedBinary.Bytes)); + + var unsortedFileHolder = _service.LoadFileAs("sorted.dat", DatLayoutKind.NotOrdered); + + Assert.Equal(FileSystem.Path.GetFullPath("sorted.dat"), unsortedFileHolder.FilePath); + Assert.Equal(FileSystem.Path.GetFullPath("sorted.dat"), unsortedFileHolder.FileInformation.FilePath); + // Entries are still sorted, but the key sort oder was adjusted + Assert.Equal(DatLayoutKind.NotOrdered, unsortedFileHolder.Content.Layout); + Assert.Equal(DatTestData.CreateSortedModel(), unsortedFileHolder.Content.ToList()); + } + + [Fact] + public void LoadFileAs_UnsortedAsSorted_Throws() + { + var unsortedBinary = DatTestData.CreateUnsortedBinary(); + + FileSystem.Initialize() + .WithFile("unsorted.dat").Which(a => a.HasBytesContent(unsortedBinary.Bytes)); + + Assert.Throws(() => + _service.LoadFileAs("unsorted.dat", DatLayoutKind.OrderedByCrc32)); + } + + [Fact] + public void LoadStore_Sorted() + { + FileSystem.Initialize(); + using (var fs = FileSystem.FileStream.New("MasterTextFile.dat", FileMode.Create)) + { + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.mastertextfile_english.dat"); + stream.CopyTo(fs); + } + + var datFile = _service.LoadFile("MasterTextFile.dat").Content; + Assert.Equal(DatLayoutKind.OrderedByCrc32, datFile.Layout); + + using (var fs = FileSystem.FileStream.New("NewSorted.dat", FileMode.Create)) + _service.CreateDatBinary(fs, datFile, DatLayoutKind.OrderedByCrc32); + + var asUnsortedDatFile = _service.LoadFileAs("MasterTextFile.dat", DatLayoutKind.NotOrdered).Content; + Assert.Equal(DatLayoutKind.NotOrdered, asUnsortedDatFile.Layout); + + using (var fs = FileSystem.FileStream.New("NewUnsorted.dat", FileMode.Create)) + _service.CreateDatBinary(fs, asUnsortedDatFile, DatLayoutKind.NotOrdered); + + + using (var fs = FileSystem.FileStream.New("NewSorted.dat", FileMode.Create)) + _service.CreateDatBinary(fs, datFile, DatLayoutKind.OrderedByCrc32); + using (var fs = FileSystem.FileStream.New("NewUnsorted.dat", FileMode.Create)) + _service.CreateDatBinary(fs, asUnsortedDatFile, DatLayoutKind.OrderedByCrc32); + + var expectedBytes = FileSystem.File.ReadAllBytes("MasterTextFile.dat"); + var actualBytesSorted = FileSystem.File.ReadAllBytes("NewSorted.dat"); + var actualBytesUnsorted = FileSystem.File.ReadAllBytes("NewUnsorted.dat"); + Assert.Equal(expectedBytes, actualBytesSorted); + Assert.Equal(expectedBytes, actualBytesUnsorted); + } + + [Fact] + public void LoadStore_Unsorted() + { + FileSystem.Initialize(); + using (var fs = FileSystem.FileStream.New("Credits.dat", FileMode.Create)) + { + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.creditstext_english.dat"); + stream.CopyTo(fs); + } + + var datFile = _service.LoadFile("Credits.dat").Content; + Assert.Equal(DatLayoutKind.NotOrdered, datFile.Layout); + + using (var fs = FileSystem.FileStream.New("New.dat", FileMode.Create)) + _service.CreateDatBinary(fs, datFile, DatLayoutKind.NotOrdered); + + var expectedBytes = FileSystem.File.ReadAllBytes("Credits.dat"); + var actualBytesSorted = FileSystem.File.ReadAllBytes("New.dat"); + Assert.Equal(expectedBytes, actualBytesSorted); + } + + [Fact] + public void LoadStore_UnsortedAsSortedThrows() + { + FileSystem.Initialize(); + using (var fs = FileSystem.FileStream.New("Credits.dat", FileMode.Create)) + { + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.creditstext_english.dat"); + stream.CopyTo(fs); + } + + var datFile = _service.LoadFile("Credits.dat").Content; + + using (var fs = FileSystem.FileStream.New("New.dat", FileMode.Create)) + _service.CreateDatBinary(fs, datFile, DatLayoutKind.OrderedByCrc32); + + var creditBytes = FileSystem.File.ReadAllBytes("Credits.dat"); + var resortedBytes = FileSystem.File.ReadAllBytes("New.dat"); + Assert.NotEqual(creditBytes, resortedBytes); + + Assert.Throws(() => _service.LoadFileAs("Credits.dat", DatLayoutKind.OrderedByCrc32)); + } + + [Fact] + public void LoadFile_Empty() + { + FileSystem.Initialize(); + using (var fs = FileSystem.FileStream.New("Empty.dat", FileMode.Create)) + { + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.Empty.dat"); + stream.CopyTo(fs); + } + + var model = _service.LoadFile("Empty.dat"); + Assert.Empty(model.Content); + + model = _service.LoadFile(FileSystem.File.OpenRead("Empty.dat")); + Assert.Empty(model.Content); + } + + [Fact] + public void LoadFile_EmptyKeyWithValue() + { + FileSystem.Initialize(); + using (var fs = FileSystem.FileStream.New("EmptyKeyWithValue.dat", FileMode.Create)) + { + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.EmptyKeyWithValue.dat"); + stream.CopyTo(fs); + } + + var model = _service.LoadFile("EmptyKeyWithValue.dat"); + Assert.Single(model.Content); + Assert.True(model.Content.ContainsKey(string.Empty)); + + model = _service.LoadFile(FileSystem.File.OpenRead("EmptyKeyWithValue.dat")); + Assert.Single(model.Content); + Assert.True(model.Content.ContainsKey(string.Empty)); + } + + [Fact] + public void LoadFile_Sorted_TwoEntriesDuplicate() + { + FileSystem.Initialize(); + using (var fs = FileSystem.FileStream.New("Sorted_TwoEntriesDuplicate.dat", FileMode.Create)) + { + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.Sorted_TwoEntriesDuplicate.dat"); + stream.CopyTo(fs); + } + + var model = _service.LoadFile("Sorted_TwoEntriesDuplicate.dat"); + Assert.Equal(2, model.Content.Count); + Assert.Single(model.Content.Keys); + + model = _service.LoadFile(FileSystem.File.OpenRead("Sorted_TwoEntriesDuplicate.dat")); + Assert.Equal(2, model.Content.Count); + Assert.Single(model.Content.Keys); + } + + + [Fact] + public void LoadModifyCreate() + { + FileSystem.Initialize(); + using (var fs = FileSystem.FileStream.New("Index_WithDuplicates.dat", FileMode.Create)) + { + using var stream = TestingHelpers.GetEmbeddedResource(typeof(DatServiceTest), "Files.Index_WithDuplicates.dat"); + stream.CopyTo(fs); + } + + var model = _service.LoadFileAs("Index_WithDuplicates.dat", DatLayoutKind.NotOrdered).Content; + Assert.Equal(DatLayoutKind.NotOrdered, model.Layout); + var modelService = ServiceProvider.GetRequiredService(); + Assert.True(modelService.GetDuplicateEntries(model).Any()); + var withoutDups = modelService.RemoveDuplicates(model); + Assert.False(modelService.GetDuplicateEntries(withoutDups).Any()); + var sorted = modelService.SortModel(withoutDups); + Assert.Equal(DatLayoutKind.OrderedByCrc32, sorted.Layout); + + using (var fs = FileSystem.FileStream.New("newSorted.dat", FileMode.Create)) + _service.CreateDatBinary(fs, sorted, DatLayoutKind.OrderedByCrc32); + } + + [Fact] + public void LoadModel() + { + var sorted = DatTestData.CreateSortedBinary().Bytes; + foreach (var model in new[] + { + _service.LoadModel(new MemoryStream(sorted)), + _service.LoadModel(sorted), + _service.LoadModel(sorted.AsSpan()) + }) + { + Assert.Equal(DatLayoutKind.OrderedByCrc32, model.Layout); + Assert.Equal(DatTestData.CreateSortedModel(), model.ToList()); + } + + var unsorted = DatTestData.CreateUnsortedBinary().Bytes; + foreach (var model in new[] + { + _service.LoadModel(new MemoryStream(unsorted)), + _service.LoadModel(unsorted), + _service.LoadModel(unsorted.AsSpan()) + }) + { + Assert.Equal(DatLayoutKind.NotOrdered, model.Layout); + Assert.Equal(DatTestData.CreateUnsortedModel(), model.ToList()); + } + } + + [Fact] + public void LoadModelAs_SortedAsUnsorted() + { + var sorted = DatTestData.CreateSortedBinary().Bytes; + foreach (var model in new[] + { + _service.LoadModelAs(new MemoryStream(sorted), DatLayoutKind.NotOrdered), + _service.LoadModelAs(sorted, DatLayoutKind.NotOrdered), + _service.LoadModelAs(sorted.AsSpan(), DatLayoutKind.NotOrdered) + }) + { + // Entries are still sorted, but the key sort order was adjusted. + Assert.Equal(DatLayoutKind.NotOrdered, model.Layout); + Assert.Equal(DatTestData.CreateSortedModel(), model.ToList()); + } + } + + [Fact] + public void LoadModelAs_UnsortedAsSorted_Throws() + { + var unsorted = DatTestData.CreateUnsortedBinary().Bytes; + + Assert.Throws(() => _service.LoadModelAs(new MemoryStream(unsorted), DatLayoutKind.OrderedByCrc32)); + Assert.Throws(() => _service.LoadModelAs(unsorted, DatLayoutKind.OrderedByCrc32)); + Assert.Throws(() => _service.LoadModelAs(unsorted.AsSpan(), DatLayoutKind.OrderedByCrc32)); + } + + [Fact] + public void GetDatLayoutKind_FromMemory_MatchesExpected() + { + var sorted = DatTestData.CreateSortedBinary().Bytes; + var unsorted = DatTestData.CreateUnsortedBinary().Bytes; + + Assert.Equal(DatLayoutKind.OrderedByCrc32, _service.GetDatLayoutKind(new MemoryStream(sorted))); + Assert.Equal(DatLayoutKind.OrderedByCrc32, _service.GetDatLayoutKind(sorted)); + Assert.Equal(DatLayoutKind.OrderedByCrc32, _service.GetDatLayoutKind(sorted.AsSpan())); + + Assert.Equal(DatLayoutKind.NotOrdered, _service.GetDatLayoutKind(new MemoryStream(unsorted))); + Assert.Equal(DatLayoutKind.NotOrdered, _service.GetDatLayoutKind(unsorted)); + Assert.Equal(DatLayoutKind.NotOrdered, _service.GetDatLayoutKind(unsorted.AsSpan())); + } + + [Fact] + public void LoadModel_NonSeekableStream() + { + using var nonSeekable = new NonSeekableReadStream(DatTestData.CreateUnsortedBinary().Bytes); + + var model = _service.LoadModel(nonSeekable); + + Assert.Equal(DatLayoutKind.NotOrdered, model.Layout); + Assert.Equal(DatTestData.CreateUnsortedModel(), model.ToList()); + } + + [Fact] + public void LoadModel_NullArgs_Throws() + { + Assert.Throws(() => _service.LoadModel((Stream)null!)); + Assert.Throws(() => _service.LoadModel((byte[])null!)); + Assert.Throws(() => _service.LoadModelAs((Stream)null!, DatLayoutKind.NotOrdered)); + Assert.Throws(() => _service.LoadModelAs((byte[])null!, DatLayoutKind.NotOrdered)); + Assert.Throws(() => _service.GetDatLayoutKind((Stream)null!)); + Assert.Throws(() => _service.GetDatLayoutKind((byte[])null!)); + } +} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/ModelService/DatModelServiceTest.RemoveDuplicates.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/ModelService/DatModelServiceTest.RemoveDuplicates.cs index ca7bc61e9..7015f072e 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/ModelService/DatModelServiceTest.RemoveDuplicates.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/ModelService/DatModelServiceTest.RemoveDuplicates.cs @@ -1,9 +1,8 @@ -using System; +using System; using System.Collections.Generic; using PG.Commons.Hashing; using PG.Commons.Utilities; using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; using Xunit; namespace PG.StarWarsGame.Files.DAT.Test.Services; @@ -21,7 +20,7 @@ public void RemoveDuplicates_Throws() public void RemoveDuplicates(IList entries, IList expected) { var model = CreateModel(entries); - if (model.KeySortOrder == DatFileType.OrderedByCrc32) + if (model.Layout == DatLayoutKind.OrderedByCrc32) expected = Crc32Utilities.SortByCrc32(expected); var newModel = Service.RemoveDuplicates(model); diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/ModelService/DatModelServiceTest.SortModel.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/ModelService/DatModelServiceTest.SortModel.cs index 603b6ab30..fc9378018 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/ModelService/DatModelServiceTest.SortModel.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/Services/ModelService/DatModelServiceTest.SortModel.cs @@ -1,9 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using PG.Commons.Hashing; using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; using Xunit; namespace PG.StarWarsGame.Files.DAT.Test.Services; @@ -22,11 +21,11 @@ public void SortModel(IList entries, IList expec { var model = CreateModel(entries); var sorted = Service.SortModel(model); - Assert.Equal(DatFileType.OrderedByCrc32, sorted.KeySortOrder); + Assert.Equal(DatLayoutKind.OrderedByCrc32, sorted.Layout); Assert.Equal(expectedList, sorted.ToList()); var sortedMock = Service.SortModel(new UnsortedDatModel(entries)); - Assert.Equal(DatFileType.OrderedByCrc32, sortedMock.KeySortOrder); + Assert.Equal(DatLayoutKind.OrderedByCrc32, sortedMock.Layout); Assert.Equal(expectedList, sortedMock.ToList()); } diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Converters/DatBinaryConverter.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Converters/DatBinaryConverter.cs index a13e49982..5446c5df8 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Converters/DatBinaryConverter.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Converters/DatBinaryConverter.cs @@ -9,7 +9,6 @@ using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.DAT.Binary.Metadata; using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; namespace PG.StarWarsGame.Files.DAT.Binary; @@ -52,7 +51,7 @@ public DatBinaryFile ModelToBinary(IDatModel model) lastCrc = keyChecksum; } - if (model.KeySortOrder == DatFileType.OrderedByCrc32 && !isSorted) + if (model.Layout == DatLayoutKind.OrderedByCrc32 && !isSorted) throw new ArgumentException("MasterTextModel must be sorted.", nameof(model)); return new DatBinaryFile(header, new BinaryTable(indexRecords), new BinaryTable(values), new BinaryTable(keys)); diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/DatFileConstants.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/DatFileConstants.cs index a8ce00f01..ba0fba36e 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/DatFileConstants.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/DatFileConstants.cs @@ -11,7 +11,7 @@ namespace PG.StarWarsGame.Files.DAT.Binary; public static class DatFileConstants { /// - /// Returns the text key encoding, which is ASCII. + /// Represents the text key encoding, which is ASCII. /// public static readonly Encoding TextKeyEncoding = Encoding.ASCII; @@ -21,7 +21,7 @@ public static class DatFileConstants internal static readonly Encoding TextKeyEncoding_Latin1 = Encoding.GetEncoding(28591); /// - /// Returns the text value encoding, which is 16bit UTF Little Endian. + /// Represents the text value encoding, which is 16bit UTF Little Endian. /// public static readonly Encoding TextValueEncoding = Encoding.Unicode; } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Reader/DatFileReader.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Reader/DatFileReader.cs index cab15465b..fe40635c1 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Reader/DatFileReader.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Reader/DatFileReader.cs @@ -10,7 +10,7 @@ using PG.Commons.Services; using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.DAT.Binary.Metadata; -using PG.StarWarsGame.Files.DAT.Files; +using PG.StarWarsGame.Files.DAT.Data; namespace PG.StarWarsGame.Files.DAT.Binary; @@ -71,7 +71,7 @@ public DatBinaryFile ReadBinary(Stream byteStream) } } - public DatFileType PeekFileType(Stream byteStream) + public DatLayoutKind PeekLayout(Stream byteStream) { if (byteStream == null) throw new ArgumentNullException(nameof(byteStream)); @@ -89,7 +89,7 @@ public DatFileType PeekFileType(Stream byteStream) var currentCrc = new Crc32(reader.ReadUInt32()); if (currentCrc < lastCrc) - return DatFileType.NotOrdered; + return DatLayoutKind.NotOrdered; reader.ReadUInt32(); // Value Length reader.ReadUInt32(); // Key Length @@ -97,7 +97,7 @@ public DatFileType PeekFileType(Stream byteStream) lastCrc = currentCrc; } - return DatFileType.OrderedByCrc32; + return DatLayoutKind.OrderedByCrc32; } catch (EndOfStreamException) { diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Reader/IDatFileReader.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Reader/IDatFileReader.cs index 59baee9e3..a4bdb7b1c 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Reader/IDatFileReader.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Binary/Reader/IDatFileReader.cs @@ -4,11 +4,11 @@ using System.IO; using PG.StarWarsGame.Files.Binary.File; using PG.StarWarsGame.Files.DAT.Binary.Metadata; -using PG.StarWarsGame.Files.DAT.Files; +using PG.StarWarsGame.Files.DAT.Data; namespace PG.StarWarsGame.Files.DAT.Binary; internal interface IDatFileReader : IBinaryFileReader { - DatFileType PeekFileType(Stream byteStream); + DatLayoutKind PeekLayout(Stream byteStream); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/DatServiceContribution.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/DatServiceContribution.cs index 7dd1ad6a9..e2bdc3fba 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/DatServiceContribution.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/DatServiceContribution.cs @@ -19,7 +19,7 @@ public static class DatServiceContribution public static void SupportDAT(this IServiceCollection serviceCollection) { serviceCollection - .AddSingleton(sp => new DatFileService(sp)) + .AddSingleton(sp => new DatService(sp)) .AddSingleton(sp => new DatModelService(sp)) .AddTransient(sp => new DatFileReader(sp)) .AddTransient(sp => new DatBinaryConverter(sp)); diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/ConstructingDatModel.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/ConstructingDatModel.cs index 741a71fc9..47f6d1535 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/ConstructingDatModel.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/ConstructingDatModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; @@ -9,17 +9,16 @@ using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.Commons.Utilities; -using PG.StarWarsGame.Files.DAT.Files; namespace PG.StarWarsGame.Files.DAT.Data; -internal class ConstructingDatModel(IEnumerable entries, DatFileType fileType) : IDatModel +internal class ConstructingDatModel(IEnumerable entries, DatLayoutKind fileType) : IDatModel { - private readonly IList _entries = fileType == DatFileType.NotOrdered ? entries.ToList() : Crc32Utilities.SortByCrc32(entries); + private readonly IList _entries = fileType == DatLayoutKind.NotOrdered ? entries.ToList() : Crc32Utilities.SortByCrc32(entries); public int Count => _entries.Count; - public DatFileType KeySortOrder { get; } = fileType; + public DatLayoutKind Layout { get; } = fileType; public DatStringEntry this[int index] => _entries[index]; diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatLayoutKind.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatLayoutKind.cs new file mode 100644 index 000000000..15bc1a0c0 --- /dev/null +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatLayoutKind.cs @@ -0,0 +1,28 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using PG.Commons.Hashing; + +namespace PG.StarWarsGame.Files.DAT.Data; + +/// +/// Represents the available entry ordering of a Petroglyph Localized String Table (.DAT). +/// +public enum DatLayoutKind +{ + /// + /// Represents a sorted DAT. This only allows for unique text keys and is expected to + /// be sorted on the text key's in an ascending manner. Any + /// MasterText DAT is of this archive type. + /// + OrderedByCrc32, + + /// + /// Represents an unsorted .DAT file. This file type allows (and encourages) non-unique text + /// keys, as this format is used for the CreditsText DAT, where the keys + /// act as formatting information/instructions rather than queriable keys. The order of the + /// entries is also equivalent to the order of the credits displayed on screen, so the insert + /// order of the key-value pairs is important and should be retained. + /// + NotOrdered +} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatModel.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatModel.cs index 5bb593b5b..305f7b202 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatModel.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; @@ -9,7 +9,6 @@ using System.Linq; using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; -using PG.StarWarsGame.Files.DAT.Files; namespace PG.StarWarsGame.Files.DAT.Data; @@ -29,7 +28,7 @@ internal abstract class DatModel : IDatModel public ISet CrcKeys => new HashSet(_firstCrcKeyValueDictionary.Keys); - public abstract DatFileType KeySortOrder { get; } + public abstract DatLayoutKind Layout { get; } protected DatModel(IEnumerable entries) { diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatStringEntry.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatStringEntry.cs index e248b1bb0..3d11b0bc3 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatStringEntry.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/DatStringEntry.cs @@ -10,7 +10,7 @@ namespace PG.StarWarsGame.Files.DAT.Data; /// -/// A key-value pair that can be stored in a DAT file. +/// Represents a key-value pair that can be stored in a DAT file. /// /// /// Equality is based on , and . @@ -39,7 +39,7 @@ namespace PG.StarWarsGame.Files.DAT.Data; public Crc32 Crc32 { get; } /// - /// Initializes a new instance of the structure with a specified key, checksum and value. + /// Initializes a new instance of the struct. /// /// The entry's key. /// The CRC32 checksum of the key. @@ -53,8 +53,7 @@ public DatStringEntry(string key, Crc32 keyChecksum, string value) : this(key, k } /// - /// Initializes a new instance of the structure with a specified key, checksum, - /// value and the original extended ASCII key. + /// Initializes a new instance of the struct. /// /// The entry's key. /// The CRC32 checksum of the key. diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/IDatModel.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/IDatModel.cs index 8e4426a67..3f2434cc4 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/IDatModel.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/IDatModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; @@ -6,12 +6,11 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using PG.Commons.Hashing; -using PG.StarWarsGame.Files.DAT.Files; namespace PG.StarWarsGame.Files.DAT.Data; /// -/// A list of key-value string entries which are used by Petroglyph games to store in-game text. +/// Represents a list of key-value string entries that are used by Petroglyph games to store in-game text. /// public interface IDatModel : IReadOnlyList { @@ -26,13 +25,13 @@ public interface IDatModel : IReadOnlyList ISet CrcKeys { get; } /// - /// Gets a value indicating how keys are organized in the . + /// Gets a value that indicates how keys are organized in the . /// /// /// Game credit models may also be sorted by pure chance. /// So this property does not provide a safe way to determine the semantics of this model. /// - public DatFileType KeySortOrder { get; } + public DatLayoutKind Layout { get; } /// /// Determines whether the contains the specified key. @@ -58,7 +57,7 @@ public interface IDatModel : IReadOnlyList /// /// When this method returns, contains the value associated with the specified key, /// if the key is found; otherwise, the default value for the type of the parameter. - /// This parameter is passed uninitialized. + /// This parameter is treated as uninitialized. /// /// if the contains an element with the specified key; otherwise, . /// is . @@ -75,14 +74,14 @@ public interface IDatModel : IReadOnlyList /// Gets a list of entries with the matching CRC32 checksum or an empty list, if the CRC32 checksum is not found. /// /// The key to match. - /// List of matching entries. + /// A list of the matching entries. ImmutableFrugalList EntriesWithCrc(Crc32 key); /// /// Gets a list of entries with the matching key or an empty list, if the key is not found. /// /// The key to match. - /// List of matching entries. + /// A list of the matching entries. /// is . ImmutableFrugalList EntriesWithKey(string key); @@ -102,7 +101,7 @@ public interface IDatModel : IReadOnlyList /// /// When this method returns, contains the value associated with the specified key, /// if the key is found; otherwise, the default value for the type of the parameter. - /// This parameter is passed uninitialized. + /// This parameter is treated as uninitialized. /// /// if the contains an element with the specified key; otherwise, . bool TryGetValue(Crc32 key, [NotNullWhen(true)] out string? value); diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/SortedDatModel.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/SortedDatModel.cs index d9bd6788e..22a63958f 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/SortedDatModel.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/SortedDatModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; @@ -6,7 +6,6 @@ using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.Commons.Utilities; -using PG.StarWarsGame.Files.DAT.Files; namespace PG.StarWarsGame.Files.DAT.Data; @@ -14,8 +13,7 @@ internal sealed class SortedDatModel : DatModel, ISortedDatModel { private readonly IReadOnlyDictionary _crcToIndexMap; - public override DatFileType KeySortOrder => DatFileType.OrderedByCrc32; - + public override DatLayoutKind Layout => DatLayoutKind.OrderedByCrc32; public SortedDatModel(IEnumerable entries) : base(entries) { diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/UnsortedDatModel.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/UnsortedDatModel.cs index ea09095e4..e328c579a 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/UnsortedDatModel.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Data/UnsortedDatModel.cs @@ -1,17 +1,16 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System.Collections.Generic; using AnakinRaW.CommonUtilities.Collections; using PG.Commons.Hashing; using PG.Commons.Utilities; -using PG.StarWarsGame.Files.DAT.Files; namespace PG.StarWarsGame.Files.DAT.Data; internal sealed class UnsortedDatModel(IEnumerable entries) : DatModel(entries), IUnsortedDatModel { - public override DatFileType KeySortOrder => DatFileType.NotOrdered; + public override DatLayoutKind Layout => DatLayoutKind.NotOrdered; public override ImmutableFrugalList EntriesWithCrc(Crc32 key) { diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Files/DatFileType.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Files/DatFileType.cs deleted file mode 100644 index 021d2d295..000000000 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Files/DatFileType.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -using PG.Commons.Hashing; - -namespace PG.StarWarsGame.Files.DAT.Files; - -/// -/// Represents the available entry ordering of a Petroglyph translation data file archive as defined -/// in the .DAT file specification. -/// -public enum DatFileType -{ - /// - /// Represents a sorted .DAT file. This file only allows for unique text keys and is expected to - /// be sorted on the text key's in an ascending manner. Any - /// mastertextfile .DAT file is of this archive type. - /// - OrderedByCrc32, - - /// - /// Represents an unsorted .DAT file. This file type allows (and encourages) non-unique text - /// keys, as this format is used for the creditstextfile .DAT file, where the keys - /// act as formatting information/instructions rather than queriable keys. The order of the - /// entries is also equivalent to the order of the credits displayed on screen, so the insert - /// order of the key-value pairs is important and should be retained. - /// - NotOrdered -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.csproj b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.csproj index e0cafb007..f6eb7bf22 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.csproj +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.csproj @@ -30,7 +30,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/AddEntryResult.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/AddEntryResult.cs index 274d7cb1e..e40b4e066 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/AddEntryResult.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/AddEntryResult.cs @@ -9,12 +9,12 @@ namespace PG.StarWarsGame.Files.DAT.Services.Builder; /// -/// Status information whether an entry was added to an . +/// Represents status information about whether an entry was added to an . /// public readonly struct AddEntryResult { /// - /// Gets whether the entry was added or not. + /// Gets a value that indicates whether the entry was added. /// [MemberNotNullWhen(true, nameof(AddedEntry))] public bool Added => Status is AddEntryState.Added or AddEntryState.AddedDuplicate && AddedEntry is not null; @@ -25,23 +25,23 @@ public readonly struct AddEntryResult public AddEntryState Status { get; } /// - /// Indicates whether a previous entry was overwritten. + /// Gets a value that indicates whether a previous entry was overwritten. /// [MemberNotNullWhen(true, nameof(OverwrittenEntry))] public bool WasOverwrite => OverwrittenEntry is not null; /// - /// The entry which was added or if no entry was added. + /// Gets the entry that was added, or if no entry was added. /// public DatStringEntry? AddedEntry { get; } /// - /// The entry which was overwritten or if no entry was overwritten. + /// Gets the entry that was overwritten, or if no entry was overwritten. /// public DatStringEntry? OverwrittenEntry { get; } /// - /// A user readable message why the entry was not added. if the entry was added successfully or no message was provided. + /// Gets a user-readable message describing why the entry was not added, or if the entry was added successfully or no message was provided. /// [ExcludeFromCodeCoverage] public string? Message { get; } diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/EmpireAtWarCreditsTextBuilder.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/EmpireAtWarCreditsTextBuilder.cs index ec0cdde87..e94a447c4 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/EmpireAtWarCreditsTextBuilder.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/EmpireAtWarCreditsTextBuilder.cs @@ -1,20 +1,20 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; -using PG.StarWarsGame.Files.DAT.Files; +using PG.StarWarsGame.Files.DAT.Data; namespace PG.StarWarsGame.Files.DAT.Services.Builder; /// -/// A for building Credits DAT files used by the +/// Represents an for building Credits DAT files used by the /// Petroglyph game Star Wars: Empire at War and its extension Empire at War: Forces of Corruption. /// public sealed class EmpireAtWarCreditsTextBuilder : PetroglyphStarWarsGameDatBuilder { /// - /// An instance of this class always returns . - public override DatFileType TargetKeySortOrder => DatFileType.NotOrdered; + /// An instance of this class always returns . + public override DatLayoutKind TargetLayout => DatLayoutKind.NotOrdered; /// /// Initializes a new instance of the class. diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/EmpireAtWarMasterTextBuilder.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/EmpireAtWarMasterTextBuilder.cs index f914dd720..72c4bf731 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/EmpireAtWarMasterTextBuilder.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/EmpireAtWarMasterTextBuilder.cs @@ -1,25 +1,27 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; -using PG.StarWarsGame.Files.DAT.Files; +using PG.StarWarsGame.Files.DAT.Data; namespace PG.StarWarsGame.Files.DAT.Services.Builder; /// -/// A for building MasterText DAT files used by the +/// Represents an for building MasterText DAT files used by the /// Petroglyph game Star Wars: Empire at War and its extension Empire at War: Forces of Corruption. /// public sealed class EmpireAtWarMasterTextBuilder : PetroglyphStarWarsGameDatBuilder { /// - /// An instance of this class always returns . - public override DatFileType TargetKeySortOrder => DatFileType.OrderedByCrc32; + /// An instance of this class always returns . + public override DatLayoutKind TargetLayout => DatLayoutKind.OrderedByCrc32; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// Specifies how the build treats duplicates. + /// + /// to overwrite an existing entry when a duplicate key is added; otherwise, . + /// /// The service provider. public EmpireAtWarMasterTextBuilder(bool overwriteDuplicates, IServiceProvider services) : base(overwriteDuplicates ? BuilderOverrideKind.Overwrite : BuilderOverrideKind.NoOverwrite, services) diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/PetroglyphStarWarsGameDatBuilder.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/PetroglyphStarWarsGameDatBuilder.cs index 8788b3df8..7b1618d00 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/PetroglyphStarWarsGameDatBuilder.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Builders/PetroglyphStarWarsGameDatBuilder.cs @@ -11,7 +11,7 @@ namespace PG.StarWarsGame.Files.DAT.Services.Builder; /// -/// Base class for an used by the Petroglyph game Star Wars: Empire at War and its extension Empire at War: Forces of Corruption. +/// Provides a base class for an used by the Petroglyph game Star Wars: Empire at War and its extension Empire at War: Forces of Corruption. /// public abstract class PetroglyphStarWarsGameDatBuilder : DatBuilderBase { diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/DatBuilderBase.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/DatBuilderBase.cs index 15cd16d88..9bfa58f0b 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/DatBuilderBase.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/DatBuilderBase.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; @@ -18,7 +18,7 @@ namespace PG.StarWarsGame.Files.DAT.Services.Builder; /// -/// Base class for a service providing the fundamental implementations. +/// Provides a base class for a service with the fundamental implementations. /// public abstract class DatBuilderBase : FileBuilderBase, DatFileInformation>, IDatBuilder { @@ -28,10 +28,10 @@ public abstract class DatBuilderBase : FileBuilderBase public sealed override IReadOnlyList BuilderData => - TargetKeySortOrder == DatFileType.OrderedByCrc32 ? SortedEntries : Entries; + TargetLayout == DatLayoutKind.OrderedByCrc32 ? SortedEntries : Entries; /// - public abstract DatFileType TargetKeySortOrder { get; } + public abstract DatLayoutKind TargetLayout { get; } /// public BuilderOverrideKind KeyOverwriteBehavior { get; } @@ -137,7 +137,7 @@ public bool IsKeyValid(string? key) /// public IDatModel BuildModel() { - if (TargetKeySortOrder == DatFileType.OrderedByCrc32) + if (TargetLayout == DatLayoutKind.OrderedByCrc32) return new SortedDatModel(BuilderData); return new UnsortedDatModel(BuilderData); } @@ -145,8 +145,8 @@ public IDatModel BuildModel() /// protected sealed override void BuildFileCore(FileSystemStream fileStream, DatFileInformation fileInformation, IReadOnlyList data) { - var datService = Services.GetRequiredService(); - datService.CreateDatFile(fileStream, data, TargetKeySortOrder); + var datService = Services.GetRequiredService(); + datService.CreateDatBinary(fileStream, data, TargetLayout); } /// diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/IDatBuilder.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/IDatBuilder.cs index ea174ea2c..4b1c93cb4 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/IDatBuilder.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/IDatBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; @@ -11,17 +11,17 @@ namespace PG.StarWarsGame.Files.DAT.Services.Builder; /// -/// Service to create DAT files ensuring validation of keys. +/// Provides methods to create DAT files while ensuring validation of keys. /// public interface IDatBuilder : IFileBuilder, DatFileInformation> { /// - /// Gets the key sort order of the DATs created by the . + /// Gets the layout of the DAT created by the . /// - DatFileType TargetKeySortOrder { get; } + DatLayoutKind TargetLayout { get; } /// - /// Gets a value indicating how the treats an already existing key. + /// Gets a value that indicates how the treats an already existing key. /// BuilderOverrideKind KeyOverwriteBehavior { get; } @@ -78,8 +78,8 @@ public interface IDatBuilder : IFileBuilder, DatFi /// /// Checks whether the specified key is valid for this . /// - /// The key to validate - /// if the passed file information are valid; otherwise, . + /// The key to validate. + /// if is valid for this ; otherwise, . bool IsKeyValid(string? key); /// diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/EmpireAtWarKeyValidator.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/EmpireAtWarKeyValidator.cs index ab784de17..efc019ca7 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/EmpireAtWarKeyValidator.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/EmpireAtWarKeyValidator.cs @@ -6,12 +6,12 @@ namespace PG.StarWarsGame.Files.DAT.Services.Builder.Validation; /// -/// Validator for DAT keys using the rules of a Petroglyph Star Wars game. +/// Validates DAT keys using the rules of a Petroglyph Star Wars game. /// public sealed class EmpireAtWarKeyValidator : IDatKeyValidator { /// - /// Returns a singleton instance of the . + /// Gets the singleton instance of the class. /// public static readonly EmpireAtWarKeyValidator Instance = new(); diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/IDatKeyValidator.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/IDatKeyValidator.cs index d7580e2fa..fd1002bb3 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/IDatKeyValidator.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/IDatKeyValidator.cs @@ -6,7 +6,7 @@ namespace PG.StarWarsGame.Files.DAT.Services.Builder.Validation; /// -/// A validator that checks whether a string can be used as a key for DAT files. +/// Defines a validator that checks whether a string can be used as a key for DAT files. /// public interface IDatKeyValidator { diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/NotNullKeyValidator.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/NotNullKeyValidator.cs index 502f751f1..5f049252d 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/NotNullKeyValidator.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Builder/Validation/NotNullKeyValidator.cs @@ -6,7 +6,7 @@ namespace PG.StarWarsGame.Files.DAT.Services.Builder.Validation; /// -/// A validator that checks the passed key is not . +/// Validates that the passed key is not . /// public sealed class NotNullKeyValidator : IDatKeyValidator { diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/IDatFileService.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/IDatFileService.cs deleted file mode 100644 index 3b3c45c5e..000000000 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/IDatFileService.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; - -namespace PG.StarWarsGame.Files.DAT.Services; - -/// -/// A service to load and create Petroglyph .DAT files -/// -public interface IDatFileService -{ - /// - /// Takes a collection of s and packs them into a DAT file. - /// - /// The file stream to write the DAT content to. - /// A list of key-value-pairs to be stored in the DAT file. - /// - /// Determines whether the output file's entries will be ordered (usually used for - /// mastertextfile_LANGUAGE.dat) or the sort-order of the provided entries will be preserved (usually used - /// for creditstextfile_LANGUAGE.dat). - /// - /// or is . - /// The DAT file could not be created. - void CreateDatFile(FileSystemStream fileStream, IEnumerable entries, DatFileType fileType); - - /// - /// Loads a *.DAT file from the provided path into a - /// - /// The path to the DAT file. - /// The loaded DAT file. - /// is . - /// is not a DAT archive. - /// is not found. - /// is empty. - IDatFile Load(string filePath); - - /// - /// Loads a *.DAT file from the provided path and - /// - /// The path to the DAT file. - /// The requested type of the DAT model. - /// The loaded DAT file - /// is not compatible to the loaded file. - /// is not a DAT archive. - /// is not found. - /// is . - /// is empty. - IDatFile LoadAs(string filePath, DatFileType requestedFileType); - - /// - /// Loads a *.DAT file from the provided path into a - /// - /// The DAT file stream. - /// The loaded DAT file. - /// is . - /// is not a DAT archive. - /// is not readable or seekable. - IDatFile Load(FileSystemStream fileStream); - - /// - /// Loads a *.DAT file from the provided path and - /// - /// The DAT file stream. - /// The requested type of the DAT model. - /// The loaded DAT file - /// is not compatible to the loaded file. - /// is not a DAT archive. - /// is . - /// is not readable or seekable. - IDatFile LoadAs(FileSystemStream fileStream, DatFileType requestedFileType); - - /// - /// Determines whether a provided DAT file is or . - /// - /// - /// For empty or single-entry DAT files this method returns - /// - /// The path to the DAT file. - /// The loaded DAT file - /// is . - DatFileType GetDatFileType(string filePath); -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/IDatModelService.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/IDatModelService.cs index 013b02169..8874ff587 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/IDatModelService.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/IDatModelService.cs @@ -8,7 +8,7 @@ namespace PG.StarWarsGame.Files.DAT.Services; /// -/// A service to work with DAT models. +/// Provides operations for working with DAT models. /// public interface IDatModelService { @@ -76,8 +76,10 @@ public interface IDatModelService /// /// The base model. /// The model to merge into . - /// When this method returns, the collection contains keys that got added or overwritten. - /// Specifies how to treat existing keys. + /// + /// When this method returns, contains the keys that got added or overwritten. This parameter is treated as uninitialized. + /// + /// One of the enumeration values that specifies how to treat existing keys. /// The merged model. /// or is . /// or is not sorted. @@ -89,8 +91,10 @@ IDatModel MergeSorted(IDatModel baseDatModel, IDatModel datToMerge, out ICollect /// /// The base model. /// The model to merge into . - /// When this method returns, the collection contains keys that got added or overwritten. - /// Specifies how to treat existing keys. + /// + /// When this method returns, contains the keys that got added or overwritten. This parameter is treated as uninitialized. + /// + /// One of the enumeration values that specifies how to treat existing keys. /// The merged model. /// or is . /// or is not unsorted. diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/IDatService.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/IDatService.cs new file mode 100644 index 000000000..50fcfd617 --- /dev/null +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/IDatService.cs @@ -0,0 +1,180 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using PG.StarWarsGame.Files.Binary; +using PG.StarWarsGame.Files.DAT.Data; +using PG.StarWarsGame.Files.DAT.Files; + +namespace PG.StarWarsGame.Files.DAT.Services; + +/// +/// Provides methods to load and create Petroglyph Localized String Tables (.DAT). +/// +public interface IDatService +{ + /// + /// Creates a binary DAT from a collection of string entries and writes it to a specified stream. + /// + /// The stream to write the DAT content to. + /// A list of key-value-pairs to be stored in the DAT file. + /// + /// Determines whether the output entries will be ordered (usually used for + /// mastertextfile_LANGUAGE.dat) or the sort-order of the provided entries will be preserved + /// (usually used for creditstextfile_LANGUAGE.dat). + /// + /// or is . + /// The DAT data could not be written. + void CreateDatBinary(Stream stream, IEnumerable entries, DatLayoutKind layout); + + /// + /// Loads a DAT file into a + /// + /// The path to the DAT file. + /// The loaded DAT file. + /// is . + /// is not a DAT archive. + /// is not found. + /// is empty. + IDatFile LoadFile(string filePath); + + /// + /// Loads a DAT file into a using the specified layout. + /// + /// The path to the DAT file. + /// The requested type of the DAT model. + /// The loaded DAT file + /// is not compatible to the loaded file. + /// is not a DAT archive. + /// is not found. + /// is . + /// is empty. + IDatFile LoadFileAs(string filePath, DatLayoutKind requestedLayout); + + /// + /// Loads a DAT file into a . + /// + /// The DAT file stream. + /// The loaded DAT file. + /// is . + /// is not a DAT archive. + /// is not readable or seekable. + IDatFile LoadFile(FileSystemStream fileStream); + + /// + /// Loads a DAT file into a using the specified layout. + /// + /// The DAT file stream. + /// The requested type of the DAT model. + /// The loaded DAT file + /// is not compatible to the loaded file. + /// is not a DAT archive. + /// is . + /// is not readable or seekable. + IDatFile LoadFileAs(FileSystemStream fileStream, DatLayoutKind requestedLayout); + + /// + /// Loads a DAT model from the specified stream. + /// + /// The DAT stream. + /// The loaded DAT model. + /// is . + /// is not a DAT archive. + IDatModel LoadModel(Stream stream); + + /// + /// Loads a DAT model from the specified byte array. + /// + /// The DAT bytes. + /// The loaded DAT model. + /// is . + /// is not a DAT archive. + IDatModel LoadModel(byte[] data); + + /// + /// Loads a DAT model from the specified read-only span of bytes. + /// + /// The DAT bytes. + /// The loaded DAT model. + /// is not a DAT archive. + IDatModel LoadModel(ReadOnlySpan data); + + /// + /// Loads a DAT model from the specified stream and layout kind. + /// + /// The DAT stream. + /// The requested layout of the DAT model. + /// The loaded DAT model + /// is not compatible to the loaded DAT. + /// is not a DAT archive. + /// is . + IDatModel LoadModelAs(Stream stream, DatLayoutKind requestedLayout); + + /// + /// Loads a DAT model from the specified bytes and type. + /// + /// The DAT bytes. + /// The requested layout of the DAT model. + /// The loaded DAT model + /// is not compatible to the loaded DAT. + /// is not a DAT archive. + /// is . + IDatModel LoadModelAs(byte[] data, DatLayoutKind requestedLayout); + + /// + /// Loads a DAT model from the specified read-only span of bytes and layout kind. + /// + /// The DAT bytes. + /// The requested layout of the DAT model. + /// The loaded DAT model + /// is not compatible to the loaded DAT. + /// is not a DAT archive. + IDatModel LoadModelAs(ReadOnlySpan data, DatLayoutKind requestedLayout); + + /// + /// Determines whether the specified DAT file is or . + /// + /// + /// For empty or single-entry DAT files this method returns + /// + /// The path to the DAT file. + /// The layout of the DAT. + /// is . + DatLayoutKind GetDatLayoutKind(string filePath); + + /// + /// Determines whether the specified DAT stream is or . + /// + /// + /// For empty or single-entry DAT data this method returns + /// + /// The DAT data stream. + /// The layout of the DAT. + /// is . + DatLayoutKind GetDatLayoutKind(Stream stream); + + /// + /// Determines whether the specified byte array is a DAT using the layout or . + /// + /// + /// For empty or single-entry DAT data this method returns + /// + /// The DAT data bytes. + /// The layout of the DAT. + /// is . + DatLayoutKind GetDatLayoutKind(byte[] data); + + /// + /// Determines whether the specified read-only span of bytes is a DAT using the layout + /// or . + /// + /// + /// For empty or single-entry DAT data this method returns + /// + /// The DAT bytes. + /// The layout of the DAT. + DatLayoutKind GetDatLayoutKind(ReadOnlySpan data); +} diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Internal/DatFileService.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Internal/DatFileService.cs deleted file mode 100644 index 15bcf21d0..000000000 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Internal/DatFileService.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using PG.Commons.Services; -using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; -using Microsoft.Extensions.DependencyInjection; -using PG.StarWarsGame.Files.DAT.Binary; - -namespace PG.StarWarsGame.Files.DAT.Services; - -internal class DatFileService(IServiceProvider services) : ServiceBase(services), IDatFileService -{ - public void CreateDatFile(FileSystemStream fileStream, IEnumerable entries, DatFileType datFileType) - { - if (fileStream is null) - throw new ArgumentNullException(nameof(fileStream)); - if (entries is null) - throw new ArgumentNullException(nameof(entries)); - - var datModel = new ConstructingDatModel(entries, datFileType); - - var datBinary = Services.GetRequiredService().ModelToBinary(datModel); - - datBinary.WriteTo(fileStream); - } - - public IDatFile Load(string filePath) - { - if (filePath == null) - throw new ArgumentNullException(nameof(filePath)); - using var fs = FileSystem.FileStream.New(filePath, FileMode.Open, FileAccess.Read); - var fileType = GetDatFileType(fs); - fs.Seek(0, SeekOrigin.Begin); - return LoadAs(fs, fileType); - } - - public IDatFile Load(FileSystemStream fileStream) - { - if (fileStream == null) - throw new ArgumentNullException(nameof(fileStream)); - var currentPos = fileStream.Position; - var fileType = GetDatFileType(fileStream); - fileStream.Seek(currentPos, SeekOrigin.Begin); - return LoadAs(fileStream, fileType); - } - - public IDatFile LoadAs(string filePath, DatFileType requestedFileType) - { - if (filePath == null) - throw new ArgumentNullException(nameof(filePath)); - using var fs = FileSystem.FileStream.New(filePath, FileMode.Open, FileAccess.Read); - return LoadAs(fs, requestedFileType); - } - - public IDatFile LoadAs(FileSystemStream fileStream, DatFileType requestedFileType) - { - if (fileStream == null) - throw new ArgumentNullException(nameof(fileStream)); - - var reader = Services.GetRequiredService(); - var datBinary = reader.ReadBinary(fileStream); - - var converter = Services.GetRequiredService(); - var datModel = converter.BinaryToModel(datBinary); - - if (requestedFileType == DatFileType.NotOrdered && datModel is ISortedDatModel sorted) - datModel = sorted.ToUnsortedModel(); - - if (requestedFileType == DatFileType.OrderedByCrc32 && datModel is IUnsortedDatModel) - throw new InvalidOperationException("Unsorted DAT file cannot be loaded as sorted DAT file"); - - var filePath = FileSystem.Path.GetFullPath(fileStream.Name); - var fileInfo = new DatFileInformation { FilePath = filePath }; - - return new DatFile(datModel, fileInfo, Services); - } - - - public DatFileType GetDatFileType(string filePath) - { - using var fs = FileSystem.FileStream.New(filePath, FileMode.Open, FileAccess.Read); - return GetDatFileType(fs); - } - - private DatFileType GetDatFileType(Stream fileStream) - { - return Services.GetRequiredService().PeekFileType(fileStream); - } -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Internal/DatModelService.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Internal/DatModelService.cs index d9790dc11..f27f53184 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Internal/DatModelService.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Internal/DatModelService.cs @@ -10,7 +10,6 @@ using PG.Commons.Services; using PG.Commons.Utilities; using PG.StarWarsGame.Files.DAT.Data; -using PG.StarWarsGame.Files.DAT.Files; namespace PG.StarWarsGame.Files.DAT.Services; @@ -40,7 +39,7 @@ public IDatModel RemoveDuplicates(IDatModel datModel) var newEntries = new LinkedHashSet(datModel, CrcBasedEqualityComparer.Instance) .ToList(); - if (datModel.KeySortOrder == DatFileType.OrderedByCrc32) + if (datModel.Layout == DatLayoutKind.OrderedByCrc32) { newEntries = Crc32Utilities.SortByCrc32(newEntries); return new SortedDatModel(newEntries); @@ -82,9 +81,9 @@ public IDatModel MergeSorted(IDatModel baseDatModel, IDatModel datToMerge, if (datToMerge == null) throw new ArgumentNullException(nameof(datToMerge)); - if (baseDatModel.KeySortOrder != DatFileType.OrderedByCrc32) + if (baseDatModel.Layout != DatLayoutKind.OrderedByCrc32) throw new ArgumentException("DAT model not sorted.", nameof(baseDatModel)); - if (datToMerge.KeySortOrder != DatFileType.OrderedByCrc32) + if (datToMerge.Layout != DatLayoutKind.OrderedByCrc32) throw new ArgumentException("DAT model not sorted.", nameof(datToMerge)); var newEntries = baseDatModel.ToList(); @@ -130,9 +129,9 @@ public IDatModel MergeUnsorted(IDatModel baseDatModel, IDatModel datToMerge, out if (datToMerge == null) throw new ArgumentNullException(nameof(datToMerge)); - if (baseDatModel.KeySortOrder != DatFileType.NotOrdered) + if (baseDatModel.Layout != DatLayoutKind.NotOrdered) throw new ArgumentException("DAT model not unsorted.", nameof(baseDatModel)); - if (datToMerge.KeySortOrder != DatFileType.NotOrdered) + if (datToMerge.Layout != DatLayoutKind.NotOrdered) throw new ArgumentException("DAT model not unsorted.", nameof(datToMerge)); mergedKeys = new List(); diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Internal/DatService.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Internal/DatService.cs new file mode 100644 index 000000000..124d01532 --- /dev/null +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/Internal/DatService.cs @@ -0,0 +1,192 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using PG.Commons.Services; +using PG.StarWarsGame.Files.DAT.Data; +using PG.StarWarsGame.Files.DAT.Files; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.DAT.Binary; + +namespace PG.StarWarsGame.Files.DAT.Services; + +internal class DatService(IServiceProvider services) : ServiceBase(services), IDatService +{ + public void CreateDatBinary(Stream stream, IEnumerable entries, DatLayoutKind layout) + { + if (stream is null) + throw new ArgumentNullException(nameof(stream)); + if (entries is null) + throw new ArgumentNullException(nameof(entries)); + + var datModel = new ConstructingDatModel(entries, layout); + + var datBinary = Services.GetRequiredService().ModelToBinary(datModel); + + datBinary.WriteTo(stream); + } + + public IDatFile LoadFile(string filePath) + { + if (filePath == null) + throw new ArgumentNullException(nameof(filePath)); + using var fs = FileSystem.FileStream.New(filePath, FileMode.Open, FileAccess.Read); + var layout = GetDatLayoutKind(fs); + fs.Seek(0, SeekOrigin.Begin); + return LoadFileAs(fs, layout); + } + + public IDatFile LoadFile(FileSystemStream fileStream) + { + if (fileStream == null) + throw new ArgumentNullException(nameof(fileStream)); + var currentPos = fileStream.Position; + var layout = GetDatLayoutKind(fileStream); + fileStream.Seek(currentPos, SeekOrigin.Begin); + return LoadFileAs(fileStream, layout); + } + + public IDatFile LoadFileAs(string filePath, DatLayoutKind requestedLayout) + { + if (filePath == null) + throw new ArgumentNullException(nameof(filePath)); + using var fs = FileSystem.FileStream.New(filePath, FileMode.Open, FileAccess.Read); + return LoadFileAs(fs, requestedLayout); + } + + public IDatFile LoadFileAs(FileSystemStream fileStream, DatLayoutKind requestedLayout) + { + if (fileStream == null) + throw new ArgumentNullException(nameof(fileStream)); + + var datModel = ReadModel(fileStream, requestedLayout); + + var filePath = FileSystem.Path.GetFullPath(fileStream.Name); + var fileInfo = new DatFileInformation { FilePath = filePath }; + + return new DatFile(datModel, fileInfo, Services); + } + + public IDatModel LoadModel(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + return LoadModelFromStream(stream, null); + } + + public IDatModel LoadModel(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + using var stream = new MemoryStream(data, writable: false); + return LoadModel(stream); + } + + public IDatModel LoadModel(ReadOnlySpan data) + { + using var stream = new MemoryStream(data.ToArray(), writable: false); + return LoadModel(stream); + } + + public IDatModel LoadModelAs(Stream stream, DatLayoutKind requestedLayout) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + return LoadModelFromStream(stream, requestedLayout); + } + + public IDatModel LoadModelAs(byte[] data, DatLayoutKind requestedLayout) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + using var stream = new MemoryStream(data, writable: false); + return LoadModelAs(stream, requestedLayout); + } + + public IDatModel LoadModelAs(ReadOnlySpan data, DatLayoutKind requestedLayout) + { + using var stream = new MemoryStream(data.ToArray(), writable: false); + return LoadModelAs(stream, requestedLayout); + } + + public DatLayoutKind GetDatLayoutKind(string filePath) + { + if (filePath == null) + throw new ArgumentNullException(nameof(filePath)); + using var fs = FileSystem.FileStream.New(filePath, FileMode.Open, FileAccess.Read); + return GetDatLayoutKind(fs); + } + + public DatLayoutKind GetDatLayoutKind(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + return Services.GetRequiredService().PeekLayout(stream); + } + + public DatLayoutKind GetDatLayoutKind(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + using var stream = new MemoryStream(data, writable: false); + return GetDatLayoutKind(stream); + } + + public DatLayoutKind GetDatLayoutKind(ReadOnlySpan data) + { + using var stream = new MemoryStream(data.ToArray(), writable: false); + return GetDatLayoutKind(stream); + } + + private IDatModel LoadModelFromStream(Stream stream, DatLayoutKind? requestedLayout) + { + // Reading the model requires seeking back to the start after peeking the file type. If the source + // stream cannot seek, copy it into memory first so the in-memory load path can re-read it. + if (!stream.CanSeek) + { + using var seekableCopy = new MemoryStream(); + stream.CopyTo(seekableCopy); + seekableCopy.Position = 0; + return ReadModelFromSeekableStream(seekableCopy, requestedLayout); + } + + return ReadModelFromSeekableStream(stream, requestedLayout); + } + + private IDatModel ReadModelFromSeekableStream(Stream stream, DatLayoutKind? requestedLayout) + { + DatLayoutKind layout; + if (requestedLayout.HasValue) + { + layout = requestedLayout.Value; + } + else + { + var startPosition = stream.Position; + layout = GetDatLayoutKind(stream); + stream.Seek(startPosition, SeekOrigin.Begin); + } + + return ReadModel(stream, layout); + } + + private IDatModel ReadModel(Stream stream, DatLayoutKind requestedLayout) + { + var reader = Services.GetRequiredService(); + var datBinary = reader.ReadBinary(stream); + + var converter = Services.GetRequiredService(); + var datModel = converter.BinaryToModel(datBinary); + + if (requestedLayout == DatLayoutKind.NotOrdered && datModel is ISortedDatModel sorted) + datModel = sorted.ToUnsortedModel(); + + if (requestedLayout == DatLayoutKind.OrderedByCrc32 && datModel is IUnsortedDatModel) + throw new InvalidOperationException("Unsorted DAT file cannot be loaded as sorted DAT file"); + + return datModel; + } +} diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/MergedKeyResult.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/MergedKeyResult.cs index fca4f1c06..d63345222 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/MergedKeyResult.cs +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/Services/MergedKeyResult.cs @@ -21,7 +21,7 @@ public readonly struct MergedKeyResult public DatStringEntry? OldEntry { get; } /// - /// Gets a value indicating whether the new entry was added or overwritten. + /// Gets a value that indicates whether the new entry was added or overwritten. /// public MergeOperation Status => !OldEntry.HasValue ? MergeOperation.Added : MergeOperation.Overwritten; diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Construction/ConstructingMegArchiveBuilderBaseTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Construction/ConstructingMegArchiveBuilderBaseTest.cs index 0afdc1cff..daec4c1fa 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Construction/ConstructingMegArchiveBuilderBaseTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Construction/ConstructingMegArchiveBuilderBaseTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -22,7 +22,7 @@ public abstract class ConstructingMegArchiveBuilderBaseTest : CommonMegTestBase protected abstract int GetExpectedHeaderSize(); - protected abstract MegFileVersion GetExpectedFileVersion(); + protected abstract MegVersion GetExpectedFileVersion(); protected override void SetupServices(IServiceCollection serviceCollection) { @@ -117,7 +117,7 @@ public void BuildConstructingMegArchive_NonASCIITreatment() FileSystem.File.Create("file.meg"); var entry = MegDataEntryTest.CreateEntry("A", default, 0, 5); - var megFile = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var megFile = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var builderEntries = new List { @@ -238,6 +238,6 @@ internal abstract class SmallMaxFileSizeConstructingService( internal override uint MaxFileSize => maxFileSize; - protected abstract override MegFileVersion FileVersion { get; } + protected abstract override MegVersion MegVersion { get; } } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Construction/ConstructingMegArchiveBuilderV1Test.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Construction/ConstructingMegArchiveBuilderV1Test.cs index a9889222f..956e77fe7 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Construction/ConstructingMegArchiveBuilderV1Test.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Construction/ConstructingMegArchiveBuilderV1Test.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using PG.StarWarsGame.Files.MEG.Binary; @@ -23,9 +23,9 @@ protected override int GetExpectedHeaderSize() return MegHeader.SizeValue; } - protected override MegFileVersion GetExpectedFileVersion() + protected override MegVersion GetExpectedFileVersion() { - return MegFileVersion.V1; + return MegVersion.V1; } [Fact] @@ -47,7 +47,7 @@ protected override uint GetMaxEntrySizeForTooLargeTest(MegDataEntryBuilderInfo e private sealed class SmallMaxFileSizeConstructingServiceV1(uint maxEntrySize, uint maxFileSize, IServiceProvider services) : SmallMaxFileSizeConstructingService(maxEntrySize, maxFileSize, services) { - protected override MegFileVersion FileVersion => MegFileVersion.V1; + protected override MegVersion MegVersion => MegVersion.V1; } protected override uint GetTotalMegSizeForTooLargeTest(IEnumerable entries) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/MegBinaryServiceFactoryTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/MegBinaryServiceFactoryTest.cs index 57dfc5b07..df7fe79a8 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/MegBinaryServiceFactoryTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/MegBinaryServiceFactoryTest.cs @@ -2,6 +2,7 @@ using PG.StarWarsGame.Files.MEG.Binary; using PG.StarWarsGame.Files.MEG.Binary.Size; using PG.StarWarsGame.Files.MEG.Binary.V1; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Files; using Xunit; @@ -17,41 +18,41 @@ public MegBinaryServiceFactoryTest() } [Theory] - [InlineData(MegFileVersion.V1, typeof(MegFileBinaryReaderV1))] - public void GetReader_V1_ReturnsCorrectType(MegFileVersion version, Type expectedType) + [InlineData(MegVersion.V1, typeof(MegFileBinaryReaderV1))] + public void GetReader_V1_ReturnsCorrectType(MegVersion version, Type expectedType) { var reader = _factory.GetReader(version); Assert.IsType(expectedType, reader); } [Theory] - [InlineData(MegFileVersion.V1, typeof(MegBinaryConverterV1))] - public void GetConverter_V1_ReturnsCorrectType(MegFileVersion version, Type expectedType) + [InlineData(MegVersion.V1, typeof(MegBinaryConverterV1))] + public void GetConverter_V1_ReturnsCorrectType(MegVersion version, Type expectedType) { var converter = _factory.GetConverter(version); Assert.IsType(expectedType, converter); } [Theory] - [InlineData(MegFileVersion.V1, typeof(ConstructingMegArchiveBuilderV1))] - public void GetConstructionBuilder_V1_ReturnsCorrectType(MegFileVersion version, Type expectedType) + [InlineData(MegVersion.V1, typeof(ConstructingMegArchiveBuilderV1))] + public void GetConstructionBuilder_V1_ReturnsCorrectType(MegVersion version, Type expectedType) { var builder = _factory.GetConstructionBuilder(version); Assert.IsType(expectedType, builder); } [Theory] - [InlineData(MegFileVersion.V1, typeof(MegV1SizeCalculator))] - public void GetMegSizeCalculator_ReturnsCorrectType(MegFileVersion version, Type expectedType) + [InlineData(MegVersion.V1, typeof(MegV1SizeCalculator))] + public void GetMegSizeCalculator_ReturnsCorrectType(MegVersion version, Type expectedType) { var calculator = _factory.GetMegSizeCalculator(version); Assert.IsType(expectedType, calculator); } [Theory] - [InlineData(MegFileVersion.V2)] - [InlineData(MegFileVersion.V3)] - public void V2andV3_Unsupported_ThrowsNotImplementedException(MegFileVersion version) + [InlineData(MegVersion.V2)] + [InlineData(MegVersion.V3)] + public void V2andV3_Unsupported_ThrowsNotImplementedException(MegVersion version) { Assert.Throws(() => _factory.GetConstructionBuilder(version)); Assert.Throws(() => _factory.GetMegSizeCalculator(version)); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Version/MegVersionIdentifierTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Version/MegVersionIdentifierTest.cs index 914009b7c..f6a4caf84 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Version/MegVersionIdentifierTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Binary/Version/MegVersionIdentifierTest.cs @@ -1,7 +1,8 @@ -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing; using Microsoft.Extensions.DependencyInjection; using PG.StarWarsGame.Files.Binary; using PG.StarWarsGame.Files.MEG.Binary; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Files; using System; using System.IO; @@ -23,43 +24,43 @@ public MegVersionIdentifierTest() } [Fact] - public void GetMegFileVersion_ThrowsArgNull() + public void GetMegVersion_ThrowsArgNull() { - Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(null!, out _)); + Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegVersion(null!, out _)); } [Fact] - public void GetMegFileVersion_ThrowsArg() + public void GetMegVersion_ThrowsArg() { - Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MegTestConstants.NonSeekableStream(), out _)); + Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MegTestConstants.NonSeekableStream(), out _)); } [Fact] - public void GetMegFileVersion_EmptyStream() + public void GetMegVersion_EmptyStream() { - Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream([]), out _)); + Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream([]), out _)); } [Fact] - public void GetMegFileVersion_InvalidFlags() + public void GetMegVersion_InvalidFlags() { var data = new byte[] { 0x77, 0x77, 0x77, 0x77, 0xa4, 0x70, 0x7d, 0x3f }; - Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out _)); + Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out _)); } [Fact] - public void GetMegFileVersion_InvalidId() + public void GetMegVersion_InvalidId() { var data = new byte[] { 0xff, 0xff, 0xff, 0xff, 0xaa, 0x77, 0x77, 0x33 }; - Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out _)); + Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out _)); } /* @@ -67,68 +68,68 @@ public void GetMegFileVersion_InvalidId() */ [Fact] - public void GetMegFileVersion_V1_EmptyFile() + public void GetMegVersion_V1_EmptyFile() { var data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V1, version); + Assert.Equal(MegVersion.V1, version); Assert.False(encrypted); } [Fact] - public void GetMegFileVersion_V1_SomeFileWithJunk() + public void GetMegVersion_V1_SomeFileWithJunk() { var data = new byte[] { 1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 3, 4, 5 // Junk }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V1, version); + Assert.Equal(MegVersion.V1, version); Assert.False(encrypted); } [Fact] - public void GetMegFileVersion_V1_NumFilesMatchesId() + public void GetMegVersion_V1_NumFilesMatchesId() { var data = new byte[] { 0xa4, 0x70, 0x7d, 0x3f, 0xa4, 0x70, 0x7d, 0x3f, 1, 2, 3, 4, 5 // Junk }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V1, version); + Assert.Equal(MegVersion.V1, version); Assert.False(encrypted); } [Fact] - public void GetMegFileVersion_V1_NumFilesMatchesFlags() + public void GetMegVersion_V1_NumFilesMatchesFlags() { var data = new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 1, 2, 3, 4, 5 // Junk }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V1, version); + Assert.Equal(MegVersion.V1, version); Assert.False(encrypted); } [Fact] - public void GetMegFileVersion_V1_NumFilesMatchesEncryptedFlag() + public void GetMegVersion_V1_NumFilesMatchesEncryptedFlag() { var data = new byte[] { 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0x8f, 1, 2, 3, 4, 5 // Junk }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V1, version); + Assert.Equal(MegVersion.V1, version); Assert.False(encrypted); } @@ -137,18 +138,18 @@ public void GetMegFileVersion_V1_NumFilesMatchesEncryptedFlag() */ [Fact] - public void GetMegFileVersion_V2_ThrowsIncompleteData() + public void GetMegVersion_V2_ThrowsIncompleteData() { var data = new byte[] { 0xff, 0xff, 0xff, 0xff, 0xa4, 0x70, 0x7d, 0x3f }; - Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out _)); + Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out _)); } [Fact] - public void GetMegFileVersion_V2_Empty() + public void GetMegVersion_V2_Empty() { var data = new byte[] { @@ -158,14 +159,14 @@ public void GetMegFileVersion_V2_Empty() 0,0,0,0, 0,0,0,0, }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V2, version); + Assert.Equal(MegVersion.V2, version); Assert.False(encrypted); } [Fact] - public void GetMegFileVersion_V2_WithJunk() + public void GetMegVersion_V2_WithJunk() { var data = new byte[] { @@ -176,14 +177,14 @@ public void GetMegFileVersion_V2_WithJunk() 0,0,0,0, 1,2,3,4 // Some junk }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V2, version); + Assert.Equal(MegVersion.V2, version); Assert.False(encrypted); } [Fact] - public void GetMegFileVersion_V2_1File_ButCorrupted() + public void GetMegVersion_V2_1File_ButCorrupted() { var data = new byte[] { @@ -193,11 +194,11 @@ public void GetMegFileVersion_V2_1File_ButCorrupted() 1, 0, 0, 0, 1, 0, 0, 0 }; - Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out _)); + Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out _)); } [Fact] - public void GetMegFileVersion_V2_1FileEmpty() + public void GetMegVersion_V2_1FileEmpty() { var data = new byte[] { @@ -214,19 +215,19 @@ public void GetMegFileVersion_V2_1FileEmpty() 0x2b,0,0,0, 0,0,0,0, }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V2, version); + Assert.Equal(MegVersion.V2, version); Assert.False(encrypted); } [Fact] - public void GetMegFileVersion_V2_2Files() + public void GetMegVersion_V2_2Files() { var data = TestingHelpers.GetEmbeddedResource(typeof(MegVersionIdentifierTest), "Files.v2_2_files_data.meg"); - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(data, out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(data, out var encrypted); - Assert.Equal(MegFileVersion.V2, version); + Assert.Equal(MegVersion.V2, version); Assert.False(encrypted); } @@ -236,7 +237,7 @@ public void GetMegFileVersion_V2_2Files() */ [Fact] - public void GetMegFileVersion_V3_EmptyFile() + public void GetMegVersion_V3_EmptyFile() { var data = new byte[] { @@ -247,14 +248,14 @@ public void GetMegFileVersion_V3_EmptyFile() 0,0,0,0, 0,0,0,0, }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V3, version); + Assert.Equal(MegVersion.V3, version); Assert.False(encrypted); } [Fact] - public void GetMegFileVersion_V3_EmptyFile_Junk() + public void GetMegVersion_V3_EmptyFile_Junk() { var data = new byte[] { @@ -266,14 +267,14 @@ public void GetMegFileVersion_V3_EmptyFile_Junk() 0,0,0,0, 1,2,3,4 }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V3, version); + Assert.Equal(MegVersion.V3, version); Assert.False(encrypted); } [Fact] - public void GetMegFileVersion_V3_1FileEmpty() + public void GetMegVersion_V3_1FileEmpty() { var data = new byte[] { @@ -292,14 +293,14 @@ public void GetMegFileVersion_V3_1FileEmpty() 0x2f,0,0,0, 0,0, }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V3, version); + Assert.Equal(MegVersion.V3, version); Assert.False(encrypted); } [Fact] - public void GetMegFileVersion_V3_CorruptedCauseEncryptFlagSet() + public void GetMegVersion_V3_CorruptedCauseEncryptFlagSet() { var data = new byte[] { @@ -318,11 +319,11 @@ public void GetMegFileVersion_V3_CorruptedCauseEncryptFlagSet() 0x2f,0,0,0, 0,0, }; - Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out _)); + Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out _)); } [Fact] - public void GetMegFileVersion_V3_1File_ButCorrupted() + public void GetMegVersion_V3_1File_ButCorrupted() { var data = new byte[] { @@ -333,16 +334,16 @@ public void GetMegFileVersion_V3_1File_ButCorrupted() 1, 0, 0, 0, 3, 0, 0, 0 }; - Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out _)); + Assert.Throws(() => new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out _)); } [Fact] - public void GetMegFileVersion_V3_2Files() + public void GetMegVersion_V3_2Files() { var data = TestingHelpers.GetEmbeddedResource(typeof(MegVersionIdentifierTest), "Files.v3n_2_files_data.meg"); - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(data, out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(data, out var encrypted); - Assert.Equal(MegFileVersion.V3, version); + Assert.Equal(MegVersion.V3, version); Assert.False(encrypted); } @@ -352,7 +353,7 @@ public void GetMegFileVersion_V3_2Files() */ [Fact] - public void GetMegFileVersion_V3_Enc_Empty() + public void GetMegVersion_V3_Enc_Empty() { var data = new byte[] { @@ -363,14 +364,14 @@ public void GetMegFileVersion_V3_Enc_Empty() 0,0,0,0, 0,0,0,0, }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V3, version); + Assert.Equal(MegVersion.V3, version); Assert.True(encrypted); } [Fact] - public void GetMegFileVersion_V3_Enc_WithJunk() + public void GetMegVersion_V3_Enc_WithJunk() { var data = new byte[] { @@ -382,9 +383,9 @@ public void GetMegFileVersion_V3_Enc_WithJunk() 0xAA,0,0,0, 1,2,3,4, // Junk }; - var version = new MegVersionIdentifier(_serviceProvider).GetMegFileVersion(new MemoryStream(data), out var encrypted); + var version = new MegVersionIdentifier(_serviceProvider).GetMegVersion(new MemoryStream(data), out var encrypted); - Assert.Equal(MegFileVersion.V3, version); + Assert.Equal(MegVersion.V3, version); Assert.True(encrypted); } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Archives/ConstructingMegArchiveTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Archives/ConstructingMegArchiveTest.cs index fa5ef5bc3..9172ef66e 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Archives/ConstructingMegArchiveTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Archives/ConstructingMegArchiveTest.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using PG.Commons.Hashing; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.Entries; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; @@ -16,16 +17,16 @@ public class ConstructingMegArchiveTest : CommonMegTestBase [Fact] public void Ctor_Throw_NullArgument() { - Assert.Throws(() =>new ConstructingMegArchive(null!, MegFileVersion.V1, 0, false)); + Assert.Throws(() =>new ConstructingMegArchive(null!, MegVersion.V1, 0, false)); } [Fact] public void Ctor_Empty() { var entries = new List(); - var cArchive = new ConstructingMegArchive(entries, MegFileVersion.V3, 123, true); + var cArchive = new ConstructingMegArchive(entries, MegVersion.V3, 123, true); - Assert.Equal(MegFileVersion.V3, cArchive.MegVersion); + Assert.Equal(MegVersion.V3, cArchive.MegVersion); Assert.Equal(new MegArchive(new List()).ToList(), cArchive.Archive.ToList()); Assert.Equal(123u, cArchive.ExpectedFileSize); Assert.True(cArchive.Encrypted); @@ -38,7 +39,7 @@ public void Ctor_CreateArchive() var entry2 = MegDataEntryTest.CreateEntry("pathB", new Crc32(2), 2, 2); FileSystem.File.Create("file.meg"); - var mf = new MegFile(new MegArchive([entry1, entry2]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var mf = new MegFile(new MegArchive([entry1, entry2]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var locEntry = MegDataEntryTest.CreateEntry("pathC", new Crc32(3), 3, 3); @@ -53,9 +54,9 @@ public void Ctor_CreateArchive() reference1, reference2 }; - var cArchive = new ConstructingMegArchive(entries, MegFileVersion.V3, 123, false); + var cArchive = new ConstructingMegArchive(entries, MegVersion.V3, 123, false); - Assert.Equal(MegFileVersion.V3, cArchive.MegVersion); + Assert.Equal(MegVersion.V3, cArchive.MegVersion); Assert.False(cArchive.Encrypted); var expectedArchiveList = new List diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Archives/VirtualMegArchiveTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Archives/VirtualMegArchiveTest.cs index 42775f702..1bbef0f14 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Archives/VirtualMegArchiveTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Archives/VirtualMegArchiveTest.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using PG.Commons.Hashing; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.Entries; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; @@ -19,7 +20,7 @@ protected override MegDataEntryReference CreateEntry(string path, Crc32 crc = de { FileSystem.File.Create("file.meg").Dispose(); var entry = MegDataEntryTest.CreateEntry(path, crc); - var megFile = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var megFile = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); return new MegDataEntryReference(new MegDataEntryLocationReference(megFile, entry)); } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Entries/MegDataEntryReferenceTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Entries/MegDataEntryReferenceTest.cs index 16d4cb8f3..53476899a 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Entries/MegDataEntryReferenceTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/Entries/MegDataEntryReferenceTest.cs @@ -1,4 +1,5 @@ using PG.Commons.Hashing; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.Entries; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; @@ -13,7 +14,7 @@ public class MegDataEntryReferenceTest : MegDataEntryBaseTest(() => new MegDataEntryLocationReference(megFile, null!)); } @@ -25,13 +26,13 @@ public void Ctor_Throws() public void Ctor() { FileSystem.File.Create("file.meg"); - var megFile = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegFileVersion.V1), + var megFile = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var entry = MegDataEntryTest.CreateEntry("path"); var reference = new MegDataEntryLocationReference(megFile, entry); - Assert.Same(megFile, reference.MegFile); + Assert.Same(megFile, reference.Source); Assert.Same(entry, reference.DataEntry); } @@ -41,9 +42,9 @@ public void Equals_Hashcode() FileSystem.File.Create("a.meg"); FileSystem.File.Create("b.meg"); - var megFileA = new MegFile(new MegArchive([]), new MegFileInformation("a.meg", MegFileVersion.V1), + var megFileA = new MegFile(new MegArchive([]), new MegFileInformation("a.meg", MegVersion.V1), ServiceProvider); - var megFileB = new MegFile(new MegArchive([]), new MegFileInformation("b.meg", MegFileVersion.V1), + var megFileB = new MegFile(new MegArchive([]), new MegFileInformation("b.meg", MegVersion.V1), ServiceProvider); var entry = MegDataEntryTest.CreateEntry("path"); @@ -76,7 +77,7 @@ public void Exists() var entry = MegDataEntryTest.CreateEntry("path"); FileSystem.File.Create("file.meg"); - var megFile = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegFileVersion.V1), + var megFile = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var locationExists = new MegDataEntryLocationReference(megFile, entry); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/EntryLocations/MegDataEntryLocationTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/EntryLocations/MegDataEntryLocationTest.cs index ce040020c..31a2ecb69 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/EntryLocations/MegDataEntryLocationTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/EntryLocations/MegDataEntryLocationTest.cs @@ -37,9 +37,13 @@ public void Equality_HashCode() var location3 = default(MegDataEntryLocation); Assert.Equal(location1, location2); + Assert.True(location1 == location2); + Assert.False(location1 != location2); Assert.Equal(location1.GetHashCode(), location2.GetHashCode()); Assert.NotEqual(location1, location3); + Assert.False(location1 == location3); + Assert.True(location1 != location3); Assert.NotEqual(location1, new object()); Assert.NotEqual((object?)null, location1); Assert.NotEqual(location1.GetHashCode(), location3.GetHashCode()); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/EntryLocations/MegDataEntryOriginInfoTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/EntryLocations/MegDataEntryOriginInfoTest.cs index bfb95ade5..98f624c06 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/EntryLocations/MegDataEntryOriginInfoTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/EntryLocations/MegDataEntryOriginInfoTest.cs @@ -1,10 +1,13 @@ -using System; -using System.IO.Abstractions; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; using PG.StarWarsGame.Files.MEG.Files; using PG.StarWarsGame.Files.MEG.Test.Data.Entries; using PG.Testing; +using System; +using System.IO; +using System.IO.Abstractions; +using Testably.Abstractions.Testing; using Xunit; namespace PG.StarWarsGame.Files.MEG.Test.Data.EntryLocations; @@ -38,7 +41,7 @@ public void Ctor_FileInfo() public void Ctor_ReferenceLocation() { using var _ = FileSystem.File.Create("test.meg"); - var meg = new MegFile(new MegArchive([]), new MegFileInformation("test.meg", MegFileVersion.V1), + var meg = new MegFile(new MegArchive([]), new MegFileInformation("test.meg", MegVersion.V1), ServiceProvider); var location = new MegDataEntryLocationReference(meg, MegDataEntryTest.CreateEntry("path")); @@ -134,11 +137,67 @@ public void Ctor_Bytes_Span_DefensiveCopy() #endregion + #region GetDataStream + + [Fact] + public void GetDataStream_File_NotFound_Throws() + { + var originInfo = new MegDataEntryOriginInfo(FileSystem.FileInfo.New("test.txt")); + Assert.Throws(originInfo.GetDataStream); + } + + [Fact] + public void GetDataStream_File() + { + FileSystem.Initialize().WithFile("test.txt").Which(m => m.HasBytesContent([1, 2, 3])); + + var originInfo = new MegDataEntryOriginInfo(FileSystem.FileInfo.New("test.txt")); + var stream = originInfo.GetDataStream(); + Assert.Equal(3, stream.Length); + + var resultStream = new MemoryStream(new byte[3]); + stream.CopyTo(resultStream); + Assert.Equal([1, 2, 3], resultStream.ToArray()); + } + + [Fact] + public void GetDataStream_LocationReference() + { + FileSystem.Initialize().WithFile("a.meg").Which(m => m.HasBytesContent([1, 2, 3, 4, 5])); + + var entry = MegDataEntryTest.CreateEntry("file.txt", offset: 1, size: 2); + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("a.meg", MegVersion.V1), ServiceProvider); + var originInfo = new MegDataEntryOriginInfo(new MegDataEntryLocationReference(meg, entry)); + + var stream = originInfo.GetDataStream(); + Assert.Equal(2, stream.Length); + + var resultStream = new MemoryStream(new byte[2]); + stream.CopyTo(resultStream); + Assert.Equal([2, 3], resultStream.ToArray()); + } + + [Fact] + public void GetDataStream_Bytes() + { + var bytes = new byte[] { 10, 20, 30, 40, 50 }; + var originInfo = new MegDataEntryOriginInfo(bytes); + + using var resultStream = originInfo.GetDataStream(); + Assert.Equal(5, resultStream.Length); + + var sink = new MemoryStream(); + resultStream.CopyTo(sink); + Assert.Equal(bytes, sink.ToArray()); + } + + #endregion + [Fact] public void EqualsHashCode() { using var _ = FileSystem.File.Create("test.meg"); - var meg = new MegFile(new MegArchive([]), new MegFileInformation("test.meg", MegFileVersion.V1), + var meg = new MegFile(new MegArchive([]), new MegFileInformation("test.meg", MegVersion.V1), ServiceProvider); var location = new MegDataEntryLocationReference(meg, MegDataEntryTest.CreateEntry("path")); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/MegDataEntryBuilderInfoTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/MegDataEntryBuilderInfoTest.cs index ffc125124..4effca9eb 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/MegDataEntryBuilderInfoTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Data/MegDataEntryBuilderInfoTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; @@ -75,7 +75,7 @@ public void Ctor_OriginIsLocalFile_FileTooLarge_ThrowsMegEntrySizeException() public void Ctor_OriginIsEntryReference() { FileSystem.File.Create("file.meg"); - var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var origin = new MegDataEntryOriginInfo(new MegDataEntryLocationReference( meg, MegDataEntryTest.CreateEntry("path", default, 123, 321, true))); @@ -97,7 +97,7 @@ public void Ctor_OriginIsEntryReference() public void Ctor_OriginIsEntryReference_OverridesProperties(string? overridePath, bool encrypted) { FileSystem.File.Create("file.meg"); - var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var origin = new MegDataEntryOriginInfo(new MegDataEntryLocationReference( meg, MegDataEntryTest.CreateEntry("path", default, 123, 321, true))); @@ -114,7 +114,7 @@ public void Ctor_OriginIsEntryReference_OverridesProperties(string? overridePath public void Ctor_OriginIsEntryReference_OverridesProperties_PathEmpty_Throws(string path) { FileSystem.File.Create("file.meg"); - var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var origin = new MegDataEntryOriginInfo(new MegDataEntryLocationReference( meg, MegDataEntryTest.CreateEntry("path", default, 123, 321, true))); Assert.Throws(() => new MegDataEntryBuilderInfo(origin, path, true)); @@ -174,7 +174,7 @@ public void FromEntry_NullArgs() { FileSystem.File.Create("file.meg"); var entry = MegDataEntryTest.CreateEntry("test.xml", default, 123, 321, true); - var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); Assert.Throws(() => MegDataEntryBuilderInfo.FromEntry(null!, entry)); Assert.Throws(() => MegDataEntryBuilderInfo.FromEntry(meg, null!)); @@ -184,7 +184,7 @@ public void FromEntry_NullArgs() public void FromEntry_EntryNotInMeg_ThrowsArgumentException() { FileSystem.File.Create("file.meg"); - var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var entry = MegDataEntryTest.CreateEntry("test.xml", default, 123, 321, true); Assert.Throws(() => MegDataEntryBuilderInfo.FromEntry(meg, entry)); @@ -200,7 +200,7 @@ public void FromEntry_SetsProperties(string? overridePath, bool encrypted) { FileSystem.File.Create("file.meg"); var entry = MegDataEntryTest.CreateEntry("test.xml", default, 123, 321, true); - var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var info = MegDataEntryBuilderInfo.FromEntry(meg, entry, overridePath, encrypted); @@ -216,7 +216,7 @@ public void FromEntry_OriginIsEntryReference_EntryPathEmpty_ThrowsArgumentExcept { FileSystem.File.Create("file.meg"); var entry = MegDataEntryTest.CreateEntry("path", default, 123, 321, true); - var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); Assert.Throws(() => MegDataEntryBuilderInfo.FromEntry(meg, entry, string.Empty, false)); @@ -236,7 +236,7 @@ public void FromEntryReference_NullArgs() public void FromEntryReference_EntryNotInMeg_ThrowsArgumentException() { FileSystem.File.Create("file.meg"); - var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var entry = MegDataEntryTest.CreateEntry("test.xml", default, 123, 321, true); Assert.Throws(() => @@ -253,7 +253,7 @@ public void FromEntryReference_OriginIsLocalFile(string? overridePath, bool encr { FileSystem.File.Create("file.meg"); var entry = MegDataEntryTest.CreateEntry("test.xml", default, 123, 321, true); - var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var info = MegDataEntryBuilderInfo.FromEntryReference(new MegDataEntryLocationReference(meg, entry), overridePath, encrypted); @@ -269,7 +269,7 @@ public void FromEntryReference_OriginIsEntryReference_EntryPathEmpty_ThrowsArgum { FileSystem.File.Create("file.meg"); var entry = MegDataEntryTest.CreateEntry("path", default, 123, 321, true); - var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); Assert.Throws(() => MegDataEntryBuilderInfo.FromEntryReference( new MegDataEntryLocationReference(meg, entry), string.Empty, false)); } @@ -390,7 +390,7 @@ public void RefreshSize_FromMegEntry() { FileSystem.File.Create("file.meg"); var entry = MegDataEntryTest.CreateEntry("path", default, 123, 321, true); - var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var info = MegDataEntryBuilderInfo.FromEntryReference(new MegDataEntryLocationReference(meg, entry)); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Exceptions/EntryNotInMegExceptionTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Exceptions/EntryNotInMegExceptionTest.cs index 6fa04f8fd..4f9dff8ee 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Exceptions/EntryNotInMegExceptionTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Exceptions/EntryNotInMegExceptionTest.cs @@ -1,4 +1,5 @@ -using AnakinRaW.CommonUtilities.Testing.Extensions; +using AnakinRaW.CommonUtilities.Testing.Extensions; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; using PG.StarWarsGame.Files.MEG.Files; @@ -15,7 +16,7 @@ public void Ctor() FileSystem.File.Create("a.meg"); FileSystem.File.Create("b.meg"); - var megFileA = new MegFile(new MegArchive([]), new MegFileInformation("a.meg", MegFileVersion.V1), + var megFileA = new MegFile(new MegArchive([]), new MegFileInformation("a.meg", MegVersion.V1), ServiceProvider); var entry = MegDataEntryTest.CreateEntry("text.xml"); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegEncryptionDataTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegEncryptionDataTest.cs index b2714467c..ec6be1ced 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegEncryptionDataTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegEncryptionDataTest.cs @@ -1,5 +1,6 @@ using System; using System.Security.Cryptography; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Files; using Xunit; diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegFileInformationTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegFileInformationTest.cs index 009a3693d..648279dc9 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegFileInformationTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegFileInformationTest.cs @@ -1,4 +1,5 @@ -using System; +using System; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Files; using Xunit; @@ -9,21 +10,21 @@ public class MegFileInformationTest [Fact] public void Ctor_Throws() { - Assert.Throws(() => new MegFileInformation(null!, MegFileVersion.V2)); - Assert.Throws(() => new MegFileInformation("", MegFileVersion.V2)); - Assert.Throws(() => new MegFileInformation("path", MegFileVersion.V1, MegEncryptionDataTest.CreateRandomData())); - Assert.Throws(() => new MegFileInformation("path", MegFileVersion.V2, MegEncryptionDataTest.CreateRandomData())); + Assert.Throws(() => new MegFileInformation(null!, MegVersion.V2)); + Assert.Throws(() => new MegFileInformation("", MegVersion.V2)); + Assert.Throws(() => new MegFileInformation("path", MegVersion.V1, MegEncryptionDataTest.CreateRandomData())); + Assert.Throws(() => new MegFileInformation("path", MegVersion.V2, MegEncryptionDataTest.CreateRandomData())); } [Theory] - [InlineData(MegFileVersion.V1)] - [InlineData(MegFileVersion.V2)] - [InlineData(MegFileVersion.V3)] - public void Ctor(MegFileVersion version) + [InlineData(MegVersion.V1)] + [InlineData(MegVersion.V2)] + [InlineData(MegVersion.V3)] + public void Ctor(MegVersion version) { var fileInfo = new MegFileInformation("path", version); Assert.Equal("path", fileInfo.FilePath); - Assert.Equal(version, fileInfo.FileVersion); + Assert.Equal(version, fileInfo.Version); Assert.Null(fileInfo.EncryptionData); Assert.False(fileInfo.HasEncryption); } @@ -32,9 +33,9 @@ public void Ctor(MegFileVersion version) public void Ctor_Encrypted() { var encData = MegEncryptionDataTest.CreateRandomData(); - var fileInfo = new MegFileInformation("path", MegFileVersion.V3, encData); + var fileInfo = new MegFileInformation("path", MegVersion.V3, encData); Assert.Equal("path", fileInfo.FilePath); - Assert.Equal(MegFileVersion.V3, fileInfo.FileVersion); + Assert.Equal(MegVersion.V3, fileInfo.Version); Assert.Same(encData, fileInfo.EncryptionData); Assert.True(fileInfo.HasEncryption); } @@ -43,7 +44,7 @@ public void Ctor_Encrypted() public void Dispose() { var encData = MegEncryptionDataTest.CreateRandomData(); - var fileInfo = new MegFileInformation("path", MegFileVersion.V3, encData); + var fileInfo = new MegFileInformation("path", MegVersion.V3, encData); fileInfo.Dispose(); Assert.True(encData.IsDisposed); } @@ -54,11 +55,11 @@ public void CopyRecord() var encData = MegEncryptionDataTest.CreateRandomData(); var orgKey = encData.Key; var orgIv = encData.IV; - var fileInfo = new MegFileInformation("path", MegFileVersion.V3, encData); + var fileInfo = new MegFileInformation("path", MegVersion.V3, encData); var other = fileInfo with { FilePath = "otherPath"}; Assert.Equal("otherPath", other.FilePath); - Assert.Equal(MegFileVersion.V3, other.FileVersion); + Assert.Equal(MegVersion.V3, other.Version); Assert.NotSame(encData, other.EncryptionData); Assert.True(other.HasEncryption); Assert.Equal(orgKey, other.EncryptionData.Key); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegFileTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegFileTest.cs index 8d65dcdd3..133f49969 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegFileTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Files/MegFileTest.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Security.Cryptography; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Files; using Testably.Abstractions.Testing; @@ -13,7 +14,7 @@ public class MegFileTest : CommonMegTestBase public void Ctor_ThrowsArgumentNullException() { FileSystem.Initialize().WithFile("test.meg"); - var param = new MegFileInformation("test.meg", MegFileVersion.V1); + var param = new MegFileInformation("test.meg", MegVersion.V1); var model = new MegArchive([]); Assert.Throws(() => new MegFile(null!, param, ServiceProvider)); @@ -25,7 +26,7 @@ public void Ctor_ThrowsArgumentNullException() public void Ctor_SetupProperties() { const string name = "test.meg"; - var param = new MegFileInformation(name, MegFileVersion.V2); + var param = new MegFileInformation(name, MegVersion.V2); var model = new MegArchive([]); FileSystem.Initialize().WithFile("test.meg"); @@ -34,7 +35,7 @@ public void Ctor_SetupProperties() Assert.Same(model, megFile.Content); Assert.Same(model, megFile.Archive); - Assert.Equal(MegFileVersion.V2, megFile.FileInformation.FileVersion); + Assert.Equal(MegVersion.V2, megFile.FileInformation.Version); Assert.False(megFile.FileInformation.HasEncryption); Assert.Equal(FileSystem.Path.GetFullPath(name), megFile.FileInformation.FilePath); @@ -49,7 +50,7 @@ public void Ctor_SetupProperties_DisposeFileInfoParam() var copyKey = encData.Key; - var param = new MegFileInformation(name, MegFileVersion.V3, encData); + var param = new MegFileInformation(name, MegVersion.V3, encData); FileSystem.Initialize().WithFile("test.meg"); @@ -73,7 +74,7 @@ public void Ctor_SetupProperties_Encrypted() var encData = new MegEncryptionData(key, iv); - var param = new MegFileInformation("test.meg", MegFileVersion.V3, encData); + var param = new MegFileInformation("test.meg", MegVersion.V3, encData); FileSystem.Initialize().WithFile("test.meg"); @@ -81,7 +82,7 @@ public void Ctor_SetupProperties_Encrypted() var megFile = new MegFile(model, param, ServiceProvider); Assert.Same(model, megFile.Content); - Assert.Equal(MegFileVersion.V3, megFile.FileInformation.FileVersion); + Assert.Equal(MegVersion.V3, megFile.FileInformation.Version); Assert.True(megFile.FileInformation.HasEncryption); Assert.Equal(iv, megFile.FileInformation.EncryptionData!.IV); Assert.Equal(key, megFile.FileInformation.EncryptionData!.Key); @@ -95,7 +96,7 @@ public void EncryptionKeyHandling() var encData = new MegEncryptionData(keyIv, keyIv); - var param = new MegFileInformation("test.meg", MegFileVersion.V3, encData); + var param = new MegFileInformation("test.meg", MegVersion.V3, encData); var model = new MegArchive([]); FileSystem.Initialize().WithFile("test.meg"); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/MegServiceContributionTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/MegServiceContributionTest.cs index 78352cb30..8cd7c7ed5 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/MegServiceContributionTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/MegServiceContributionTest.cs @@ -12,11 +12,10 @@ public class MegServiceContributionTest : CommonMegTestBase [Fact] public void SupportMEG_Registers() { - Assert.DoesNotThrow(() => ServiceProvider.GetRequiredService()); + Assert.DoesNotThrow(() => ServiceProvider.GetRequiredService()); Assert.DoesNotThrow(() => ServiceProvider.GetRequiredService()); Assert.DoesNotThrow(() => ServiceProvider.GetRequiredService()); Assert.DoesNotThrow(() => ServiceProvider.GetRequiredService()); - Assert.DoesNotThrow(() => ServiceProvider.GetRequiredService()); Assert.DoesNotThrow(() => ServiceProvider.GetRequiredService()); Assert.DoesNotThrow(() => ServiceProvider.GetRequiredService()); } diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/MegTestConstants.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/MegTestConstants.cs index 6fc895592..7cc9668bd 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/MegTestConstants.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/MegTestConstants.cs @@ -519,16 +519,33 @@ internal class NonSeekableStream : Stream 47, 71, 97, 109, 101, 95, 79, 98, 106, 101, 99, 116, 95, 70, 105, 108, 101, 115, 62 ]; + // A minimal V1 MEG holding a single zero-size entry named "file" and no data section. + internal static readonly byte[] EmptyEntryMegFileV1 = + [ + 1, 0, 0, 0, 1, 0, 0, 0, // Header: one file name, one file + 4, 0, 102, 105, 108, 101, // "file" + // CRC32 of "file", record index 0, size 0, offset 34 (== metadata size), name index 0 + 16, 54, 159, 140, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0 + ]; + internal class FakeFileInfo(string fullName, long length) : IFileInfo { public string FullName => fullName; public long Length { get; set; } = length; public bool Exists => true; + /// + /// Optional content returned by . Its length can intentionally differ from + /// to simulate a file whose reported size disagrees with its actual data. + /// + public byte[]? ReadBytes { get; set; } + #region Other IFileInfo Members public void Delete() => throw new NotImplementedException(); public void Refresh() { } - public FileSystemStream OpenRead() => throw new NotImplementedException(); + public FileSystemStream OpenRead() => ReadBytes is null + ? throw new NotImplementedException() + : new FakeFileSystemStream(new MemoryStream(ReadBytes, writable: false), fullName); public FileSystemStream OpenWrite() => throw new NotImplementedException(); public FileSystemStream Open(FileMode mode) => throw new NotImplementedException(); public FileSystemStream Open(FileMode mode, FileAccess access) => throw new NotImplementedException(); @@ -572,4 +589,7 @@ public void Refresh() { } #endregion } + + // Minimal FileSystemStream wrapper around an arbitrary stream, used by FakeFileInfo.OpenRead. + internal sealed class FakeFileSystemStream(Stream stream, string path) : FileSystemStream(stream, path, isAsync: false); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/PG.StarWarsGame.Files.MEG.Test.csproj b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/PG.StarWarsGame.Files.MEG.Test.csproj index 5538311f3..d112b9520 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/PG.StarWarsGame.Files.MEG.Test.csproj +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/PG.StarWarsGame.Files.MEG.Test.csproj @@ -23,7 +23,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Builders/EmpireAtWarMegBuilderTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Builders/EmpireAtWarMegBuilderTest.cs index cb2d616ec..79a7e5f1e 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Builders/EmpireAtWarMegBuilderTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Builders/EmpireAtWarMegBuilderTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -55,7 +55,7 @@ protected override void AddDataToBuilder(IReadOnlyCollection(); - var meg = megFileService.Load("new.meg"); + var megFileService = ServiceProvider.GetRequiredService(); + var meg = megFileService.LoadFile("new.meg"); Assert.Equal(4, meg.Archive.Count); @@ -148,11 +148,10 @@ public void BuildMeg() Assert.NotNull(packedEntry3); Assert.NotNull(packedEntry4); - var extractor = ServiceProvider.GetRequiredService(); - var entry1Data = extractor.GetData(new MegDataEntryLocationReference(meg, packedEntry1)); - var entry2Data = extractor.GetData(new MegDataEntryLocationReference(meg, packedEntry2)); - var entry3Data = extractor.GetData(new MegDataEntryLocationReference(meg, packedEntry3)); - var entry4Data = extractor.GetData(new MegDataEntryLocationReference(meg, packedEntry4)); + var entry1Data = meg.GetData(packedEntry1); + var entry2Data = meg.GetData(packedEntry2); + var entry3Data = meg.GetData(packedEntry3); + var entry4Data = meg.GetData(packedEntry4); using var ms = new MemoryStream(); entry1Data.CopyTo(ms); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Builders/MegBuilderTestBase.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Builders/MegBuilderTestBase.cs index 6060f7540..fb40631dd 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Builders/MegBuilderTestBase.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Builders/MegBuilderTestBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -42,7 +42,7 @@ protected sealed override MegFileInformation CreateFileInfo(bool valid, string p { if (!FileInfoIsAlwaysValid && !valid) return CreateInvalidFileInfo(path); - return new MegFileInformation(path, MegFileVersion.V1); + return new MegFileInformation(path, MegVersion.V1); } protected virtual MegFileInformation CreateInvalidFileInfo(string path) @@ -159,7 +159,7 @@ public void Dispose_ThrowsOnAddingOrBuildingMethods() Assert.Throws(() => builder.AddEntry(new MegDataEntryLocationReference(CreateEmptyTestMeg(), MegDataEntryTest.CreateEntry("file.txt")))); Assert.Throws(() => - builder.Build(new MegFileInformation("a.meg", MegFileVersion.V1), false)); + builder.Build(new MegFileInformation("a.meg", MegVersion.V1), false)); Assert.DoesNotThrow(() => { _ = builder.DataEntries; }); Assert.DoesNotThrow(builder.Clear); @@ -493,7 +493,7 @@ public void AddEntry_Override() Assert.Single(builder.DataEntries); Assert.Same(addedFile.AddedBuilderInfo, resultSecondAdd.OverwrittenBuilderInfo); Assert.True(builder.DataEntries.First().OriginInfo.IsEntryReference); - Assert.Same(meg, builder.DataEntries.First().OriginInfo.MegFileLocation!.MegFile); + Assert.Same(meg, builder.DataEntries.First().OriginInfo.MegFileLocation!.Source); } } @@ -656,7 +656,7 @@ public void GetMinRequiredMegFiles_EntryTooLarge_Throws() var builder = new MaxFileSizeMegBuilder(uint.MaxValue, ServiceProvider, 36); builder.AddFile("1.txt", "1.txt"); - Assert.Throws(() => builder.GetMinRequiredMegFiles(MegFileVersion.V1)); + Assert.Throws(() => builder.GetMinRequiredMegFiles(MegVersion.V1)); } [Fact] @@ -669,7 +669,7 @@ public void GetMinRequiredMegFiles_SingleFile() builder.AddFile("1.txt", "1.txt"); builder.AddFile("2.txt", "2.txt"); - Assert.Equal(1, builder.GetMinRequiredMegFiles(MegFileVersion.V1)); + Assert.Equal(1, builder.GetMinRequiredMegFiles(MegVersion.V1)); } [Fact] @@ -682,7 +682,7 @@ public void GetMinRequiredMegFiles_MultipleFiles() builder.AddFile("1.txt", "1.txt"); builder.AddFile("2.txt", "2.txt"); - Assert.Equal(2, builder.GetMinRequiredMegFiles(MegFileVersion.V1)); + Assert.Equal(2, builder.GetMinRequiredMegFiles(MegVersion.V1)); } #endregion @@ -703,11 +703,10 @@ public void Build_FromBytes_RoundTrip() Assert.True(FileSystem.File.Exists("out.meg")); - var loaded = ServiceProvider.GetRequiredService().Load("out.meg"); + var loaded = ServiceProvider.GetRequiredService().LoadFile("out.meg"); Assert.Single(loaded.Archive); - var extractor = ServiceProvider.GetRequiredService(); - using var extracted = extractor.GetData(new MegDataEntryLocationReference(loaded, loaded.Archive[0])); + using var extracted = loaded.GetData(loaded.Archive[0]); var sink = new MemoryStream(); extracted.CopyTo(sink); Assert.Equal(contents, sink.ToArray()); @@ -780,7 +779,7 @@ private IMegFile CreateEmptyTestMeg() private IMegFile CreateTestMeg() { FileSystem.File.WriteAllBytes("test.meg", MegTestConstants.ContentMegFileV1); - return ServiceProvider.GetRequiredService().Load("test.meg"); + return ServiceProvider.GetRequiredService().LoadFile("test.meg"); } private class MaxFileSizeMegBuilder(uint maxFileSize, IServiceProvider services, uint? maxMegSize = null) : MegBuilderBase(services) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/Entry/BinaryMegDataEntryValidatorTestBase.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/Entry/BinaryMegDataEntryValidatorTestBase.cs index 0c3d3391e..cce4e74d5 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/Entry/BinaryMegDataEntryValidatorTestBase.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/Entry/BinaryMegDataEntryValidatorTestBase.cs @@ -37,7 +37,7 @@ public void Validate_OriginNotFound_EntryReference() { FileSystem.File.Create("file.meg"); var entry = MegDataEntryTest.CreateEntry("DUMMY", default, 0, 1); - var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + var meg = new MegFile(new MegArchive([]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); var location = new MegDataEntryLocationReference(meg, entry); var origin = new MegDataEntryOriginInfo(location); @@ -91,7 +91,7 @@ public SharedDataBuilder() { FileSystem.File.Create("file.meg"); _entry = MegDataEntryTest.CreateEntry("DUMMY", default, 0, 1); - _meg = new MegFile(new MegArchive([_entry]), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + _meg = new MegFile(new MegArchive([_entry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); } public MegDataEntryBuilderInfo CreateInfo(string overridePath, bool encrypted = false) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidatorTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidatorTest.cs index 9be5335ae..895e22222 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidatorTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidatorTest.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using PG.StarWarsGame.Files.MEG.Binary.Size; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Files; using PG.StarWarsGame.Files.MEG.Services.Builder.Validation; using PG.StarWarsGame.Files.MEG.Test.Files; @@ -35,7 +36,7 @@ public static IEnumerable ValidTestData() var data = new SharedDataBuilder(); yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V1), + data.CreateData(new MegFileInformation("path", MegVersion.V1), [data.CreateInfo("path")]) ]; } @@ -45,34 +46,34 @@ public static IEnumerable InvalidTestData() var data = new SharedDataBuilder(); yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V1), + data.CreateData(new MegFileInformation("path", MegVersion.V1), [data.CreateInfo("path", encrypted: true)]) ]; // Currently not supported. Tests will fail, as soon we do. Move to ValidTestData then. yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V2), + data.CreateData(new MegFileInformation("path", MegVersion.V2), [data.CreateInfo("path")]) ]; yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V3), + data.CreateData(new MegFileInformation("path", MegVersion.V3), [data.CreateInfo("path")]) ]; yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V3, MegEncryptionDataTest.CreateRandomData()), + data.CreateData(new MegFileInformation("path", MegVersion.V3, MegEncryptionDataTest.CreateRandomData()), [data.CreateInfo("path", encrypted: true)]) ]; yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V3, MegEncryptionDataTest.CreateRandomData()), + data.CreateData(new MegFileInformation("path", MegVersion.V3, MegEncryptionDataTest.CreateRandomData()), [data.CreateInfo("path")]) ]; yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V3, MegEncryptionDataTest.CreateRandomData()), + data.CreateData(new MegFileInformation("path", MegVersion.V3, MegEncryptionDataTest.CreateRandomData()), [ data.CreateInfo("path", encrypted: false), data.CreateInfo("path", encrypted: true) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidatorTestBase.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidatorTestBase.cs index ba6da3a09..e3f46cf1b 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidatorTestBase.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidatorTestBase.cs @@ -22,7 +22,7 @@ public void Validate_Null_Throws() { var validator = CreateValidator(); Assert.Throws(() => validator.Validate(null!, [])); - Assert.Throws(() => validator.Validate(new MegFileInformation("p", MegFileVersion.V1), null!)); + Assert.Throws(() => validator.Validate(new MegFileInformation("p", MegVersion.V1), null!)); } [Theory] @@ -50,7 +50,7 @@ public void Validate_EntryFileNotFound_ReturnsInvalid() var fileInfo = FileSystem.FileInfo.New(filePath); var entryInfo = MegDataEntryBuilderInfo.FromFile(fileInfo, "PATH"); - var info = data.CreateData(new MegFileInformation("p", MegFileVersion.V1), [entryInfo]); + var info = data.CreateData(new MegFileInformation("p", MegVersion.V1), [entryInfo]); // Delete the file to trigger FileNotFoundException during RefreshSize FileSystem.File.Delete(filePath); @@ -70,7 +70,7 @@ public void Validate_EntryTooLarge_ReturnsInvalid() var entryInfo = MegDataEntryBuilderInfo.FromFile(bigFile, "ANY"); var info = - data.CreateData(new MegFileInformation("p", MegFileVersion.V1), [entryInfo]); + data.CreateData(new MegFileInformation("p", MegVersion.V1), [entryInfo]); // Now exceed the max size bigFile.Length = (long)MaxMegEntrySize + 1; @@ -85,11 +85,11 @@ public static IEnumerable BaseValidTestData() var data = new SharedDataBuilder(); yield return [ - data.CreateData(new MegFileInformation("p", MegFileVersion.V1), []) + data.CreateData(new MegFileInformation("p", MegVersion.V1), []) ]; yield return [ - data.CreateData(new MegFileInformation("p", MegFileVersion.V1), [data.CreateInfo("p1")]) + data.CreateData(new MegFileInformation("p", MegVersion.V1), [data.CreateInfo("p1")]) ]; } @@ -100,14 +100,14 @@ public static IEnumerable BaseInvalidTestData() // Encryption mismatch: Encrypted entries but no encryption data in FileInfo yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V3), + data.CreateData(new MegFileInformation("path", MegVersion.V3), [data.CreateInfo("path", encrypted: true)]) ]; // Encryption mismatch: No encrypted entries but encryption data in FileInfo yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V3, MegEncryptionDataTest.CreateRandomData()), + data.CreateData(new MegFileInformation("path", MegVersion.V3, MegEncryptionDataTest.CreateRandomData()), [data.CreateInfo("path")]) ]; } @@ -121,7 +121,7 @@ public void Validate_TotalSizeTooLarge_ReturnsInvalid() var entry1 = data.CreateInfo("p1"); // Size 1 var entry2 = data.CreateInfo("p2"); // Size 1 - var info = data.CreateData(new MegFileInformation("p", MegFileVersion.V1), [entry1, entry2]); + var info = data.CreateData(new MegFileInformation("p", MegVersion.V1), [entry1, entry2]); // Limit is 100, 62 is fine. Assert.True(validator.Validate(info.FileInformation, info.DataEntries).IsValid); @@ -129,7 +129,7 @@ public void Validate_TotalSizeTooLarge_ReturnsInvalid() // Now use a very large file to exceed 100 var bigFile = new MegTestConstants.FakeFileInfo("large_file.bin", 1000); var entry3 = MegDataEntryBuilderInfo.FromFile(bigFile, "p3"); - var info2 = data.CreateData(new MegFileInformation("p", MegFileVersion.V1), [entry3]); + var info2 = data.CreateData(new MegFileInformation("p", MegVersion.V1), [entry3]); var result = validator.Validate(info2.FileInformation, info2.DataEntries); Assert.False(result.IsValid); @@ -151,7 +151,7 @@ public SharedDataBuilder() { FileSystem.File.Create("file.meg").Dispose(); _entry = MegDataEntryTest.CreateEntry("DUMMY", default, 0, 1); - _meg = new MegFile(new MegArchive(new List { _entry }), new MegFileInformation("file.meg", MegFileVersion.V1), ServiceProvider); + _meg = new MegFile(new MegArchive(new List { _entry }), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); } public MegDataEntryBuilderInfo CreateInfo(string overridePath, bool encrypted = false) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/EmpireAtWarMegFileInformationValidatorTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/EmpireAtWarMegFileInformationValidatorTest.cs index 0d2e94b28..e097bbb41 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/EmpireAtWarMegFileInformationValidatorTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/Builder/Validation/FileInfo/EmpireAtWarMegFileInformationValidatorTest.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using PG.StarWarsGame.Files.MEG.Binary.Size; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Files; using PG.StarWarsGame.Files.MEG.Services.Builder.Validation; using PG.StarWarsGame.Files.MEG.Test.Files; @@ -36,7 +37,7 @@ public static IEnumerable ValidTestData() var data = new SharedDataBuilder(); yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V1), + data.CreateData(new MegFileInformation("path", MegVersion.V1), [data.CreateInfo("path")]) ]; } @@ -46,33 +47,33 @@ public static IEnumerable InvalidTestData() var data = new SharedDataBuilder(); yield return [ - data.CreateData(new MegFileInformation(new string('a', 260), MegFileVersion.V1), + data.CreateData(new MegFileInformation(new string('a', 260), MegVersion.V1), [data.CreateInfo("path")]) ]; yield return [ - data.CreateData(new MegFileInformation("pathÄ", MegFileVersion.V1), + data.CreateData(new MegFileInformation("pathÄ", MegVersion.V1), [data.CreateInfo("path")]) ]; yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V2), + data.CreateData(new MegFileInformation("path", MegVersion.V2), [data.CreateInfo("path")]) ]; yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V3), + data.CreateData(new MegFileInformation("path", MegVersion.V3), [data.CreateInfo("path")]) ]; yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V3, MegEncryptionDataTest.CreateRandomData()), + data.CreateData(new MegFileInformation("path", MegVersion.V3, MegEncryptionDataTest.CreateRandomData()), [data.CreateInfo("path", encrypted: true)]) ]; yield return [ - data.CreateData(new MegFileInformation("path", MegFileVersion.V3, MegEncryptionDataTest.CreateRandomData()), + data.CreateData(new MegFileInformation("path", MegVersion.V3, MegEncryptionDataTest.CreateRandomData()), [ data.CreateInfo("path", encrypted: false), data.CreateInfo("path", encrypted: true) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegDataSourceTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegDataSourceTest.cs new file mode 100644 index 000000000..2828779ca --- /dev/null +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegDataSourceTest.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.MEG.Data; +using PG.StarWarsGame.Files.MEG.Data.Archives; +using PG.StarWarsGame.Files.MEG.Files; +using PG.StarWarsGame.Files.MEG.Services; +using PG.StarWarsGame.Files.MEG.Test.Files; +using Testably.Abstractions.Testing; +using Xunit; +using static PG.StarWarsGame.Files.MEG.Test.Data.Entries.MegDataEntryTest; + +namespace PG.StarWarsGame.Files.MEG.Test.Services; + +public class FileMegDataSourceTest : MegDataSourceTestSuite +{ + protected override IMegDataSource CreateMegDataSource(byte[] megBytes) + { + FileSystem.File.WriteAllBytes("test.meg", megBytes); + return ServiceProvider.GetRequiredService().LoadFile("test.meg"); + } + + [Fact] + public void GetData_FileCannotBeRead_Throws() + { + var entry = CreateEntry("file.txt", default, 0, 12); + + _ = FileSystem.File.Create("test.meg"); // intentionally left open so the file is locked + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("test.meg", MegVersion.V1), ServiceProvider); + + Assert.Throws(() => meg.GetData(entry)); + } + + [Fact] + public void GetData_EmptyEntry_BackingFileMissing_Throws() + { + // A zero-size entry whose backing file is gone still fails. + FileSystem.Initialize().WithFile("test.meg"); + var entry = CreateEntry("file.txt", offset: 2, size: 0); + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("test.meg", MegVersion.V1), ServiceProvider); + + FileSystem.File.Delete("test.meg"); + + Assert.Throws(() => meg.GetData(entry)); + } + + [Fact] + public void Dispose_ViaInterface_DisposesHolder() + { + // The MegFile holder owns its FileInformation (and any encryption material on it). + // Disposing via the IMegDataSource interface must flow through to the holder's disposal. + FileSystem.Initialize().WithFile("test.meg"); + var megFile = new MegFile( + new MegArchive([]), + new MegFileInformation("test.meg", MegVersion.V3, MegEncryptionDataTest.CreateRandomData()), + ServiceProvider); + IMegDataSource source = megFile; + + source.Dispose(); + + Assert.Throws(() => megFile.FileInformation); + } +} + +public class InMemoryMegDataSourceTest : MegDataSourceTestSuite +{ + protected override IMegDataSource CreateMegDataSource(byte[] megBytes) + { + return ServiceProvider.GetRequiredService().LoadArchive(megBytes); + } +} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegDataSourceTestSuite.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegDataSourceTestSuite.cs new file mode 100644 index 000000000..b6af3c4b2 --- /dev/null +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegDataSourceTestSuite.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Extensions; +using PG.StarWarsGame.Files.MEG.Data; +using PG.StarWarsGame.Files.MEG.Test.Binary.Reader.V1; +using PG.StarWarsGame.Files.MEG.Test.Data.Entries; +using Xunit; + +namespace PG.StarWarsGame.Files.MEG.Test.Services; + +public abstract class MegDataSourceTestSuite : CommonMegTestBase +{ + protected abstract IMegDataSource CreateMegDataSource(byte[] megBytes); + + [Fact] + public void GetData_ReturnsEntryContent() + { + var source = CreateMegDataSource(MegTestConstants.ContentMegFileV1); + + Assert.Equal(2, source.Archive.Count); + Assert.Equal(MegTestConstants.CampaignFilesContent, ReadAll(source.GetData(source.Archive[0]))); + Assert.Equal(MegTestConstants.GameObjectFilesContent, ReadAll(source.GetData(source.Archive[1]))); + } + + [Fact] + public void GetData_FromUnorderedMeg() + { + var bytes = TestingHelpers.GetEmbeddedResourceAsByteArray(typeof(MegFileBinaryReaderV1IntegrationTest), + "Files.v1_out_of_order.meg"); + var source = CreateMegDataSource(bytes); + + using var ms = new MemoryStream(); + using (var stream = source.GetData(source.Archive[0])) + stream.CopyTo(ms); + using (var stream = source.GetData(source.Archive[1])) + stream.CopyTo(ms); + + Assert.Equal("456123"u8, ms.ToArray()); + Assert.True(source.Archive[0].Location.Offset > source.Archive[1].Location.Offset); + } + + [Fact] + public void GetData_NullEntry_Throws() + { + var source = CreateMegDataSource(MegTestConstants.ContentMegFileV1); + Assert.Throws(() => source.GetData(null!)); + } + + [Fact] + public void GetData_EntryNotInArchive_Throws() + { + var source = CreateMegDataSource(MegTestConstants.ContentMegFileV1); + Assert.Throws(() => source.GetData(MegDataEntryTest.CreateEntry("not/in/archive.xml"))); + } + + [Fact] + public void GetData_EmptyEntry_ReturnsEmptyStream() + { + var source = CreateMegDataSource(MegTestConstants.EmptyEntryMegFileV1); + + var entry = Assert.Single(source.Archive); + Assert.Equal(0u, entry.Location.Size); + + using var stream = source.GetData(entry); + Assert.Equal(0, stream.Length); + } + + [Fact] + public void GetData_ReturnsIndependentStreams() + { + var source = CreateMegDataSource(MegTestConstants.ContentMegFileV1); + + using var s1 = source.GetData(source.Archive[0]); + using var s2 = source.GetData(source.Archive[0]); + + s1.ReadByte(); + + // Reading from one stream must not move the position of the other. + Assert.Equal(1, s1.Position); + Assert.Equal(0, s2.Position); + } + + [Fact] + public void Dispose_IsIdempotent() + { + var source = CreateMegDataSource(MegTestConstants.ContentMegFileV1); + + source.Dispose(); + Assert.DoesNotThrow(source.Dispose); + } + + private static byte[] ReadAll(Stream stream) + { + using (stream) + { + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + } +} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegDataStreamFactoryTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegDataStreamFactoryTest.cs deleted file mode 100644 index 030e8a6cd..000000000 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegDataStreamFactoryTest.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.IO; -using PG.StarWarsGame.Files.MEG.Data.Archives; -using PG.StarWarsGame.Files.MEG.Data.EntryLocations; -using PG.StarWarsGame.Files.MEG.Files; -using PG.StarWarsGame.Files.MEG.Services; -using PG.StarWarsGame.Files.MEG.Test.Data.Entries; -using Testably.Abstractions.Testing; -using Xunit; - -namespace PG.StarWarsGame.Files.MEG.Test.Services; - -public class MegDataStreamFactoryTest : CommonMegTestBase -{ - private readonly MegDataStreamFactory _streamFactory; - - public MegDataStreamFactoryTest() - { - _streamFactory = new MegDataStreamFactory(ServiceProvider); - } - - [Fact] - public void Ctor_Throws() - { - Assert.Throws(() => new MegDataStreamFactory(null!)); - } - - [Fact] - public void GetDataStream_Throws() - { - Assert.Throws(() => _streamFactory.GetStream((MegDataEntryOriginInfo)null!)); - Assert.Throws(() => _streamFactory.GetStream((MegDataEntryLocationReference)null!)); - } - - [Fact] - public void GetFileData_OriginInfo_Throws_FileNotFound() - { - var originInfo = new MegDataEntryOriginInfo(FileSystem.FileInfo.New("test.txt")); - Assert.Throws(() => _streamFactory.GetStream(originInfo)); - } - - [Fact] - public void GetFileData_OriginInfo_File() - { - FileSystem.Initialize().WithFile("test.txt").Which(m => m.HasBytesContent([1,2,3])); - - var originInfo = new MegDataEntryOriginInfo(FileSystem.FileInfo.New("test.txt")); - var stream = _streamFactory.GetStream(originInfo); - Assert.Equal(3, stream.Length); - - var resultStream = new MemoryStream(new byte[3]); - stream.CopyTo(resultStream); - Assert.Equal([1, 2, 3], resultStream.ToArray()); - } - - [Fact] - public void GetFileData_OriginInfo_LocationReference() - { - FileSystem.Initialize().WithFile("a.meg").Which(m => m.HasBytesContent([1, 2, 3, 4, 5])); - - var entry = MegDataEntryTest.CreateEntry("file.txt", offset: 1, size: 2); - - var archive = new MegArchive([entry]); - - var meg = new MegFile(archive, new MegFileInformation("a.meg", MegFileVersion.V1), ServiceProvider); - - var originInfo = new MegDataEntryOriginInfo(new MegDataEntryLocationReference(meg, entry)); - - var stream = _streamFactory.GetStream(originInfo); - Assert.Equal(2, stream.Length); - - var resultStream = new MemoryStream(new byte[2]); - stream.CopyTo(resultStream); - Assert.Equal([2, 3], resultStream.ToArray()); - } - - - [Fact] - public void GetFileData_LocationReference_Throws_FileNotInMeg() - { - FileSystem.Initialize().WithFile("a.meg"); - var entry = MegDataEntryTest.CreateEntry("file.txt"); - - var archive = new MegArchive([]); - - var meg = new MegFile(archive, new MegFileInformation("a.meg", MegFileVersion.V1), - ServiceProvider); - - var location = new MegDataEntryLocationReference(meg, entry); - - Assert.Throws(() => _streamFactory.GetStream(location)); - } - - [Fact] - public void GetFileData_LocationReference_EmptyData_MegFileNotExists_Throws() - { - FileSystem.Initialize().WithFile("a.meg"); - - var entry = MegDataEntryTest.CreateEntry("file.txt", offset: 2, size: 0); - - var archive = new MegArchive([entry]); - - var meg = new MegFile(archive, new MegFileInformation("a.meg", MegFileVersion.V1), - ServiceProvider); - - FileSystem.File.Delete("a.meg"); - - var location = new MegDataEntryLocationReference(meg, entry); - - Assert.Throws(() => _streamFactory.GetStream(location)); - } - - [Fact] - public void GetFileData_LocationReference_EmptyDataFile() - { - FileSystem.Initialize().WithFile("a.meg"); - var entry = MegDataEntryTest.CreateEntry("file.txt", offset: 2, size: 0); - - var archive = new MegArchive([entry]); - - var meg = new MegFile(archive, new MegFileInformation("a.meg", MegFileVersion.V1), - ServiceProvider); - - var location = new MegDataEntryLocationReference(meg, entry); - - var stream = _streamFactory.GetStream(location); - Assert.Equal(0, stream.Length); - } - - [Fact] - public void GetFileData_OriginInfo_Bytes() - { - var bytes = new byte[] { 10, 20, 30, 40, 50 }; - var originInfo = new MegDataEntryOriginInfo(bytes); - - using var resultStream = _streamFactory.GetStream(originInfo); - Assert.Equal(5, resultStream.Length); - - var sink = new MemoryStream(); - resultStream.CopyTo(sink); - Assert.Equal(bytes, sink.ToArray()); - } - - [Fact] - public void GetFileData_OriginInfo_Bytes_Replayable() - { - var bytes = new byte[] { 10, 20, 30, 40, 50 }; - var originInfo = new MegDataEntryOriginInfo(bytes); - - using (var first = _streamFactory.GetStream(originInfo)) - first.CopyTo(new MemoryStream()); - - using var second = _streamFactory.GetStream(originInfo); - var sink = new MemoryStream(); - second.CopyTo(sink); - Assert.Equal(bytes, sink.ToArray()); - } - - [Fact] - public void GetFileData_OriginInfo_Bytes_Empty() - { - var originInfo = new MegDataEntryOriginInfo([]); - - using var resultStream = _streamFactory.GetStream(originInfo); - Assert.Equal(0, resultStream.Length); - } - - [Fact] - public void GetFileData_LocationReference_File() - { - FileSystem.Initialize().WithFile("a.meg").Which(m => m.HasBytesContent([1, 2, 3, 4, 5])); - - var entry = MegDataEntryTest.CreateEntry("file.txt", offset: 1, size: 2); - - var archive = new MegArchive([entry]); - - var meg = new MegFile(archive, new MegFileInformation("a.meg", MegFileVersion.V1), - ServiceProvider); - - var location = new MegDataEntryLocationReference(meg, entry); - - var stream = _streamFactory.GetStream(location); - Assert.Equal(2, stream.Length); - - var resultStream = new MemoryStream(new byte[2]); - stream.CopyTo(resultStream); - Assert.Equal([2, 3], resultStream.ToArray()); - } -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegFileExtractorTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegFileExtractorTest.cs index ff2f3039c..bf3908cec 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegFileExtractorTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegFileExtractorTest.cs @@ -1,11 +1,10 @@ -using AnakinRaW.CommonUtilities.Testing; using AnakinRaW.CommonUtilities.Testing.Attributes; using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; using PG.StarWarsGame.Files.MEG.Files; using PG.StarWarsGame.Files.MEG.Services; -using PG.StarWarsGame.Files.MEG.Test.Binary.Reader.V1; using System; using System.IO; using Testably.Abstractions.Testing; @@ -17,10 +16,12 @@ namespace PG.StarWarsGame.Files.MEG.Test.Services; public class MegFileExtractorTest : CommonMegTestBase { private readonly MegFileExtractor _extractor; + private readonly IMegService _megService; public MegFileExtractorTest() { _extractor = new MegFileExtractor(ServiceProvider); + _megService = ServiceProvider.GetRequiredService(); } [Fact] @@ -82,27 +83,6 @@ public void GetAbsoluteFilePath_ThrowsInvalidOperation() Assert.Throws(() => _extractor.GetAbsolutePath(entry, "someRoot", false)); } - [Fact] - public void GetFileData_ThrowsArgumentNull() - { - Assert.Throws(() => _extractor.GetData(null!)); - } - - [Fact] - public void GetFileData_CannotReadFile_Throws() - { - // Size 12 is not valid, as the entry does not really exist. - var entry = CreateEntry("file.txt", default, 0, 12); - - FileSystem.File.Create("test.meg"); - var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("test.meg", MegFileVersion.V1), - ServiceProvider); - - var location = new MegDataEntryLocationReference(meg, entry); - - Assert.Throws(() => _extractor.GetData(location)); - } - [Fact] public void ExtractFile_ThrowsArgumentsIncorrect() { @@ -114,7 +94,7 @@ public void ExtractFile_ThrowsArgumentsIncorrect() { } - var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("test.meg", MegFileVersion.V1), + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("test.meg", MegVersion.V1), ServiceProvider); var location = new MegDataEntryLocationReference(meg, entry); @@ -133,7 +113,7 @@ public void ExtractData_Throws_IllegalPath_Windows(string filePathWhereToExtract FileSystem.Initialize().WithFile("a.meg"); var entry = CreateEntry("path"); - var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("a.meg", MegFileVersion.V1), + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("a.meg", MegVersion.V1), ServiceProvider); var location = new MegDataEntryLocationReference(meg, entry); @@ -147,7 +127,7 @@ public void ExtractData_Throws_IllegalPath_Linux(string filePathWhereToExtract) FileSystem.Initialize().WithFile("a.meg"); var entry = CreateEntry("path"); - var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("a.meg", MegFileVersion.V1), + var meg = new MegFile(new MegArchive([entry]), new MegFileInformation("a.meg", MegVersion.V1), ServiceProvider); var location = new MegDataEntryLocationReference(meg, entry); @@ -155,54 +135,13 @@ public void ExtractData_Throws_IllegalPath_Linux(string filePathWhereToExtract) Assert.Throws(() => _extractor.ExtractEntry(location, filePathWhereToExtract, false)); } - [Fact] - public void GetFileData() - { - FileSystem.Initialize() - .WithFile("test.meg").Which(m => m.HasBytesContent(MegTestConstants.ContentMegFileV1)); - - var meg = ServiceProvider.GetRequiredService().Load("test.meg"); - - // CampaignFiles.xml - var entry = meg.Content[0]; - var location = new MegDataEntryLocationReference(meg, entry); - - using var stream = _extractor.GetData(location); - - var ms = new MemoryStream(); - stream.CopyTo(ms); - - Assert.Equal(MegTestConstants.CampaignFilesContent, ms.ToArray()); - } - - [Fact] - public void GetFileData_FromUnorderedMeg() - { - var unorderedMeg = TestingHelpers.GetEmbeddedResourceAsByteArray(typeof(MegFileBinaryReaderV1IntegrationTest), "Files.v1_out_of_order.meg"); - - FileSystem.Initialize() - .WithFile("test.meg").Which(m => m.HasBytesContent(unorderedMeg)); - - var meg = ServiceProvider.GetRequiredService().Load("test.meg"); - - var ms = new MemoryStream(); - - using (var stream = _extractor.GetData(new MegDataEntryLocationReference(meg, meg.Content[0]))) - stream.CopyTo(ms); - using (var stream = _extractor.GetData(new MegDataEntryLocationReference(meg, meg.Content[1]))) - stream.CopyTo(ms); - - Assert.Equal("456123"u8, ms.ToArray()); - Assert.True(meg.Content[0].Location.Offset > meg.Content[1].Location.Offset); - } - [Fact] public void ExtractData_NoOverwrite() { FileSystem.Initialize() .WithFile("test.meg").Which(m => m.HasBytesContent(MegTestConstants.ContentMegFileV1)); - var meg = ServiceProvider.GetRequiredService().Load("test.meg"); + var meg = ServiceProvider.GetRequiredService().LoadFile("test.meg"); // CampaignFiles.xml var entry = meg.Content[0]; @@ -239,7 +178,7 @@ public void ExtractData_Overwrite() Assert.Equal(existingFileData, FileSystem.File.ReadAllBytes("file.txt")); - var meg = ServiceProvider.GetRequiredService().Load("test.meg"); + var meg = _megService.LoadFile("test.meg"); // CampaignFiles.xml var entry = meg.Content[0]; @@ -260,7 +199,7 @@ public void ExtractData_CreateDirectories() FileSystem.Initialize() .WithFile("test.meg").Which(m => m.HasBytesContent(MegTestConstants.ContentMegFileV1)); - var meg = ServiceProvider.GetRequiredService().Load("test.meg"); + var meg = _megService.LoadFile("test.meg"); // CampaignFiles.xml var entry = meg.Content[0]; @@ -273,4 +212,49 @@ public void ExtractData_CreateDirectories() var actualFileData = FileSystem.File.ReadAllBytes(filePathWhereToExtract); Assert.Equal(MegTestConstants.CampaignFilesContent, actualFileData); } + + [Fact] + public void ExtractEntry_FromInMemoryArchive_WritesContentToFile() + { + var source = LoadTestArchive(); + var location = new MegDataEntryLocationReference(source, source.Archive[0]); + + var extracted = _extractor.ExtractEntry(location, "file.txt", false); + + Assert.True(extracted); + Assert.True(FileSystem.File.Exists("file.txt")); + Assert.Equal(MegTestConstants.CampaignFilesContent, FileSystem.File.ReadAllBytes("file.txt")); + } + + [Fact] + public void ExtractEntry_NoOverwrite_SkipsExistingFile() + { + var source = LoadTestArchive(); + var location = new MegDataEntryLocationReference(source, source.Archive[0]); + FileSystem.File.WriteAllText("file.txt", "existing"); + + var extracted = _extractor.ExtractEntry(location, "file.txt", false); + + Assert.False(extracted); + Assert.Equal("existing", FileSystem.File.ReadAllText("file.txt")); + } + + [Fact] + public void ExtractEntry_Overwrite_ReplacesExistingFile() + { + var source = LoadTestArchive(); + var location = new MegDataEntryLocationReference(source, source.Archive[0]); + FileSystem.File.WriteAllText("file.txt", "existing"); + + var extracted = _extractor.ExtractEntry(location, "file.txt", true); + + Assert.True(extracted); + Assert.Equal(MegTestConstants.CampaignFilesContent, FileSystem.File.ReadAllBytes("file.txt")); + } + + private IMegDataSource LoadTestArchive() + { + using var stream = new MemoryStream(MegTestConstants.ContentMegFileV1); + return _megService.LoadArchive(stream); + } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegFileServiceIntegrationTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegFileServiceIntegrationTest.cs deleted file mode 100644 index 1caf6d853..000000000 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegFileServiceIntegrationTest.cs +++ /dev/null @@ -1,397 +0,0 @@ -using PG.StarWarsGame.Files.MEG.Utilities; -using Microsoft.Extensions.DependencyInjection; -using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.MEG.Data; -using PG.StarWarsGame.Files.MEG.Data.Archives; -using PG.StarWarsGame.Files.MEG.Data.EntryLocations; -using PG.StarWarsGame.Files.MEG.Files; -using PG.StarWarsGame.Files.MEG.Services; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using AnakinRaW.CommonUtilities.Testing; -using Testably.Abstractions.Testing; -using Xunit; - -namespace PG.StarWarsGame.Files.MEG.Test.Services; - -public class MegFileServiceIntegrationTest : CommonMegTestBase -{ - private readonly IMegFileService _megFileService; - - public MegFileServiceIntegrationTest() - { - _megFileService = ServiceProvider.GetRequiredService(); - } - - #region Create Meg Archive - - [Fact] - public void CreateMegArchive_Throws() - { - Assert.Throws(() => _megFileService.CreateMegArchive(null!, MegFileVersion.V1, null, new List())); - Assert.Throws(() => - { - using var fs = FileSystem.File.OpenWrite("path"); - _megFileService.CreateMegArchive(fs, MegFileVersion.V3, null, null!); - }); - } - - [Fact] - public void CreateMegArchive_DoesNotCreateDirectories_Throws() - { - const string megFileName = "test/a.meg"; - - Assert.False(FileSystem.Directory.Exists("test")); - - Assert.Throws(() => - { - using var fs = FileSystem.File.OpenWrite(megFileName); - _megFileService.CreateMegArchive(fs, MegFileVersion.V1, null, []); - }); - } - - [Fact] - public void CreateMegArchive_EntryNotFoundInMeg_Throws() - { - const string megFileName = "test.meg"; - const string dummyMegFile = "dummy.meg"; - const string newFileName = "new.meg"; - - FileSystem.Initialize() - .WithFile(megFileName).Which(m => m.HasBytesContent(MegTestConstants.ContentMegFileV1)) - .WithFile(dummyMegFile).Which(m => m.HasBytesContent([0, 0, 0, 0, 0, 0, 0, 0])); - - var meg = _megFileService.Load(megFileName); - - var dummyMeg = new MegFile(new MegArchive([]), new MegFileInformation(dummyMegFile, MegFileVersion.V1), - ServiceProvider); - - var builderInfo = new List - { - new(new MegDataEntryOriginInfo(new MegDataEntryLocationReference(dummyMeg, meg.Archive[0]))) - }; - - Assert.Throws(() => - { - using var fs = FileSystem.File.OpenWrite(newFileName); - _megFileService.CreateMegArchive(fs, meg.FileInformation.FileVersion, null, builderInfo); - }); - } - - private sealed class ManualStreamFactory(Stream streamToReturn) : IMegDataStreamFactory - { - public Stream GetStream(MegDataEntryOriginInfo originInfo) => streamToReturn; - public MegEntryStream GetStream(MegDataEntryLocationReference locationReference) => throw new NotImplementedException(); - } - - [Theory] - [InlineData(CreateMegArchiveInvalidOperationType.DataEntrySizeMismatch)] - [InlineData(CreateMegArchiveInvalidOperationType.FilePositionMismatch)] - public void CreateMegArchive_ThrowsInvalidOperationException(CreateMegArchiveInvalidOperationType type) - { - const string megFileName = "new.meg"; - const string entryFileName = "file.txt"; - - FileSystem.File.WriteAllBytes(entryFileName, [1, 2, 3]); - var fileInfo = FileSystem.FileInfo.New(entryFileName); - var builderInfo = MegDataEntryBuilderInfo.FromFile(fileInfo, entryFileName); - - Stream streamToReturn = type switch - { - CreateMegArchiveInvalidOperationType.DataEntrySizeMismatch => new MemoryStream([1, 2, 3, 4]), - _ => new MemoryStream([1, 2, 3]) - }; - - var manualFactory = new ManualStreamFactory(streamToReturn); - - var sc = new ServiceCollection(); - SetupServices(sc); - sc.AddSingleton(FileSystem); - sc.AddSingleton(manualFactory); - var serviceProvider = sc.BuildServiceProvider(); - - var megFileService = serviceProvider.GetRequiredService(); - - var ex = Assert.Throws(() => - { - using var fs = FileSystem.File.OpenWrite(megFileName); - if (type == CreateMegArchiveInvalidOperationType.FilePositionMismatch) - fs.WriteByte(0); - megFileService.CreateMegArchive(fs, MegFileVersion.V1, null, [builderInfo]); - }); - - var expectedMessagePart = type switch - { - CreateMegArchiveInvalidOperationType.DataEntrySizeMismatch => "Actual data entry size", - CreateMegArchiveInvalidOperationType.FilePositionMismatch => "Actual file position", - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; - - Assert.Contains(expectedMessagePart, ex.Message); - } - - [Fact] - public void CreateMegArchive_MegWithEntriesOfSameNameButWithDifferentData() - { - const string megFileName = "test.meg"; - - var expectedBytes = new byte[] - { - 2, 0, 0, 0, 2, 0, 0, 0, // Header - 4, 0, 102, 105, 108, 101, // "file" - 4, 0, 102, 105, 108, 101, // "file" - 16, 54, 159, 140, 0, 0, 0, 0, 3, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, - 16, 54, 159, 140, 1, 0, 0, 0, 3, 0, 0, 0, 63, 0, 0, 0, 1, 0, 0, 0, - 49, 50, 51, // 123 - 52, 53, 54 // 456 - }; - - - FileSystem.Initialize().WithFile("1.txt").Which(m => m.HasStringContent("123")); - FileSystem.Initialize().WithFile("2.txt").Which(m => m.HasStringContent("456")); - - var builderInfo = new List - { - MegDataEntryBuilderInfo.FromFile(FileSystem.FileInfo.New("1.txt"), "file"), - MegDataEntryBuilderInfo.FromFile(FileSystem.FileInfo.New("2.txt"), "file") - }; - - using (var fs = FileSystem.File.OpenWrite(megFileName)) - { - _megFileService.CreateMegArchive(fs, MegFileVersion.V1, null, builderInfo); - } - - var bytes = FileSystem.File.ReadAllBytes(megFileName); - - Assert.Equal(expectedBytes, bytes); - } - - #endregion - - #region Generic Read - - [Fact] - public void Load_InvalidBinary() - { - const string megFileName = "test.meg"; - const string fileData = "some random data"; - - FileSystem.Initialize().WithFile(megFileName).Which(m => m.HasStringContent(fileData)); - - Assert.Throws(() => _megFileService.Load(megFileName)); - } - - [Fact] - public void Load_ThrowFileNotFound() - { - Assert.Throws(() => _megFileService.Load("notFound.meg")); - } - - [Fact] - public void Load_NullArgs() - { - Assert.Throws(() => _megFileService.Load((string)null!)); - Assert.Throws(() => _megFileService.Load((FileSystemStream)null!)); - } - - #endregion - - #region Read / Write V1 - - [Fact] - public void MegV1_WithEntries() - { - const string megFileName = "test.meg"; - - FileSystem.Initialize().WithFile(megFileName).Which(m => m.HasBytesContent(MegTestConstants.ContentMegFileV1)); - - var expectedData = new ExpectedMegTestData - { - IsMegFileVersion = MegFileVersion.V1, - IsMegEncrypted = false, - MegFileCount = 2, - EntryNames = new List - { - "DATA\\XML\\CAMPAIGNFILES.XML", - "DATA\\XML\\GAMEOBJECTFILES.XML" - }, - NewMegFilePath = "new.meg", - NewMegFileVersion = MegFileVersion.V1, - NewMegIsBinaryEqual = true - }; - TestMegFiles(megFileName, expectedData); - } - - [Fact] - public void MegV1_Empty() - { - const string megFileName = "test.meg"; - const string megResource = "Files.v1_empty.meg"; - - FileSystem.Initialize().WithFile(megFileName) - .Which(m => m.HasBytesContent(TestingHelpers.GetEmbeddedResourceAsByteArray(GetType(), megResource))); - - var expectedData = new ExpectedMegTestData - { - IsMegFileVersion = MegFileVersion.V1, - IsMegEncrypted = false, - MegFileCount = 0, - EntryNames = new List(), - NewMegFilePath = "new.meg", - NewMegFileVersion = MegFileVersion.V1, - NewMegIsBinaryEqual = true - }; - TestMegFiles(megFileName, expectedData); - } - - [Fact] - public void MegV1_EntriesHaveNonAsciiNames() - { - const string megFileName = "test.meg"; - const string megResource = "Files.v1_2_files_with_extended_ascii_name.meg"; - - FileSystem.Initialize().WithFile(megFileName) - .Which(m => m.HasBytesContent(TestingHelpers.GetEmbeddedResourceAsByteArray(GetType(), megResource))); - - var expectedData = new ExpectedMegTestData - { - IsMegFileVersion = MegFileVersion.V1, - IsMegEncrypted = false, - MegFileCount = 2, - EntryNames = new List - { - "TEST?.TXT", - "TEST?.TXT" - }, - NewMegFilePath = "new.meg", - NewMegFileVersion = MegFileVersion.V1, - NewMegIsBinaryEqual = false - }; - - TestMegFiles(megFileName, expectedData); - } - - #endregion - - #region GetFileVersion - - [Fact] - public void GetMegFileVersion() - { - WriteMegFromResources("Files.v1_1_file_data.meg", "v1.meg"); - WriteMegFromResources("Files.v2_2_files_data.meg", "v2.meg"); - WriteMegFromResources("Files.v3n_2_files_data.meg", "v3.meg"); - - Assert.Equal(MegFileVersion.V1, _megFileService.GetMegFileVersion("v1.meg", out var encrypted)); - Assert.False(encrypted); - Assert.Equal(MegFileVersion.V2, _megFileService.GetMegFileVersion("v2.meg", out encrypted)); - Assert.False(encrypted); - Assert.Equal(MegFileVersion.V3, _megFileService.GetMegFileVersion("v3.meg", out encrypted)); - Assert.False(encrypted); - } - - private void WriteMegFromResources(string resourceName, string fileName) - { - using var rs = TestingHelpers.GetEmbeddedResource(GetType(), resourceName); - using var fs = FileSystem.File.OpenWrite(fileName); - rs.CopyTo(fs); - } - - - [Fact] - public void GetMegFileVersion_Throws_FileNotFound() - { - Assert.Throws(() => _megFileService.GetMegFileVersion("test.meg", out _)); - } - - #endregion - - private void TestMegFiles(string megFilePath, ExpectedMegTestData expectedData) - { - var megVersion = _megFileService.GetMegFileVersion(megFilePath, out var encrypted); - Assert.Equal(expectedData.IsMegFileVersion, megVersion); - Assert.Equal(expectedData.IsMegEncrypted, encrypted); - - var meg = _megFileService.Load(megFilePath); - TestMegModelContent(meg, expectedData, false); - - for (var i = 0; i < meg.Archive.Count; i++) - { - var entry = meg.Archive[i]; - var expected = expectedData.EntryNames[i]; - Assert.Equal(expected, entry.Path); - } - - using var param = new MegFileInformation( - expectedData.NewMegFilePath, - expectedData.NewMegFileVersion, - expectedData.EncryptionData); - - var builderInformation = meg.Archive.Select(e => - new MegDataEntryBuilderInfo(new MegDataEntryOriginInfo(new MegDataEntryLocationReference(meg, e)))); - - using (var fs = FileSystem.File.OpenWrite(expectedData.NewMegFilePath)) - { - _megFileService.CreateMegArchive(fs, expectedData.NewMegFileVersion, expectedData.EncryptionData, - builderInformation); - } - - Assert.True(FileSystem.File.Exists(expectedData.NewMegFilePath)); - - var createdVersion = _megFileService.GetMegFileVersion(expectedData.NewMegFilePath, out var newEncrypted); - Assert.Equal(expectedData.NewMegFileVersion, createdVersion); - Assert.Equal(expectedData.EncryptionData is null, !newEncrypted); - - - var actualBytes = FileSystem.File.ReadAllBytes(expectedData.NewMegFilePath); - var expectedBytes = FileSystem.File.ReadAllBytes(megFilePath); - if (expectedData.NewMegIsBinaryEqual) - Assert.Equal(expectedBytes, actualBytes); - else - Assert.NotEqual(expectedBytes, actualBytes); - - var newMeg = _megFileService.Load(megFilePath); - TestMegModelContent(newMeg, expectedData, true); - } - - private static void TestMegModelContent(IMegFile meg, ExpectedMegTestData expectedData, bool isNewMeg) - { - Assert.NotNull(meg); - Assert.Equal(expectedData.MegFileCount, meg.Content.Count); - Assert.Equal(expectedData.IsMegFileVersion, meg.FileInformation.FileVersion); - Assert.Equal(expectedData.EntryNames.Count, meg.Archive.Count); - - if (isNewMeg) - Assert.Equal(expectedData.EncryptionData is null, !meg.FileInformation.HasEncryption); - else - Assert.Equal(expectedData.IsMegEncrypted, meg.FileInformation.HasEncryption); - - for (var i = 0; i < meg.Archive.Count; i++) - { - var entry = meg.Archive[i]; - var expected = expectedData.EntryNames[i]; - Assert.Equal(expected, entry.Path); - } - } - - public enum CreateMegArchiveInvalidOperationType - { - DataEntrySizeMismatch, - FilePositionMismatch, - } - - private record ExpectedMegTestData - { - public MegFileVersion IsMegFileVersion { get; init; } - public bool IsMegEncrypted { get; init; } - public int MegFileCount { get; init; } - public IList EntryNames { get; init; } = null!; - public string NewMegFilePath { get; init; } = null!; - public bool NewMegIsBinaryEqual { get; init; } - public MegFileVersion NewMegFileVersion { get; init; } - public MegEncryptionData? EncryptionData { get; } - } -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegServiceIntegrationTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegServiceIntegrationTest.cs new file mode 100644 index 000000000..22ea8f3f4 --- /dev/null +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/MegServiceIntegrationTest.cs @@ -0,0 +1,655 @@ +using AnakinRaW.CommonUtilities.Hashing; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons; +using PG.StarWarsGame.Files.Binary; +using PG.StarWarsGame.Files.MEG.Data; +using PG.StarWarsGame.Files.MEG.Data.Archives; +using PG.StarWarsGame.Files.MEG.Data.EntryLocations; +using PG.StarWarsGame.Files.MEG.Files; +using PG.StarWarsGame.Files.MEG.Services; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using AnakinRaW.CommonUtilities.Testing; +using PG.Testing; +using Testably.Abstractions.Testing; +using Xunit; + +namespace PG.StarWarsGame.Files.MEG.Test.Services; + +public class MegServiceIntegrationTest : CommonMegTestBase +{ + private const string ContentMegFileName = "test.meg"; + + private readonly IMegService _megService; + + public MegServiceIntegrationTest() + { + _megService = ServiceProvider.GetRequiredService(); + } + + #region Create Meg Archive + + [Fact] + public void CreateMegArchive_Throws() + { + Assert.Throws(() => _megService.CreateMegArchive(null!, MegVersion.V1, null, new List())); + Assert.Throws(() => + { + using var fs = FileSystem.File.OpenWrite("path"); + _megService.CreateMegArchive(fs, MegVersion.V3, null, null!); + }); + } + + [Fact] + public void CreateMegArchive_DoesNotCreateDirectories_Throws() + { + const string megFileName = "test/a.meg"; + + Assert.False(FileSystem.Directory.Exists("test")); + + Assert.Throws(() => + { + using var fs = FileSystem.File.OpenWrite(megFileName); + _megService.CreateMegArchive(fs, MegVersion.V1, null, []); + }); + } + + [Fact] + public void CreateMegArchive_EntryNotFoundInMeg_Throws() + { + const string megFileName = "test.meg"; + const string dummyMegFile = "dummy.meg"; + const string newFileName = "new.meg"; + + FileSystem.Initialize() + .WithFile(megFileName).Which(m => m.HasBytesContent(MegTestConstants.ContentMegFileV1)) + .WithFile(dummyMegFile).Which(m => m.HasBytesContent([0, 0, 0, 0, 0, 0, 0, 0])); + + var meg = _megService.LoadFile(megFileName); + + var dummyMeg = new MegFile(new MegArchive([]), new MegFileInformation(dummyMegFile, MegVersion.V1), + ServiceProvider); + + var builderInfo = new List + { + new(new MegDataEntryOriginInfo(new MegDataEntryLocationReference(dummyMeg, meg.Archive[0]))) + }; + + Assert.Throws(() => + { + using var fs = FileSystem.File.OpenWrite(newFileName); + _megService.CreateMegArchive(fs, meg.FileInformation.Version, null, builderInfo); + }); + } + + [Fact] + public void CreateMegArchive_FilePositionMismatch_ThrowsInvalidOperationException() + { + const string megFileName = "new.meg"; + const string entryFileName = "file.txt"; + + FileSystem.File.WriteAllBytes(entryFileName, [1, 2, 3]); + var builderInfo = MegDataEntryBuilderInfo.FromFile(FileSystem.FileInfo.New(entryFileName), entryFileName); + + var ex = Assert.Throws(() => + { + using var fs = FileSystem.File.OpenWrite(megFileName); + // Advance the stream so its position no longer matches the expected entry offset. + fs.WriteByte(0); + _megService.CreateMegArchive(fs, MegVersion.V1, null, [builderInfo]); + }); + + Assert.Contains("Actual file position", ex.Message); + } + + [Fact] + public void CreateMegArchive_DataEntrySizeMismatch_ThrowsInvalidOperationException() + { + const string megFileName = "new.meg"; + + // A file whose reported length (4) disagrees with the data its stream actually yields (3 bytes) + var fileInfo = new MegTestConstants.FakeFileInfo("file.txt", length: 4) { ReadBytes = [1, 2, 3] }; + var builderInfo = MegDataEntryBuilderInfo.FromFile(fileInfo, "file"); + + var ex = Assert.Throws(() => + { + using var fs = FileSystem.File.OpenWrite(megFileName); + _megService.CreateMegArchive(fs, MegVersion.V1, null, [builderInfo]); + }); + + Assert.Contains("Actual data entry size", ex.Message); + } + + [Fact] + public void CreateMegArchive_MegWithEntriesOfSameNameButWithDifferentData() + { + const string megFileName = "test.meg"; + + var expectedBytes = new byte[] + { + 2, 0, 0, 0, 2, 0, 0, 0, // Header + 4, 0, 102, 105, 108, 101, // "file" + 4, 0, 102, 105, 108, 101, // "file" + 16, 54, 159, 140, 0, 0, 0, 0, 3, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, + 16, 54, 159, 140, 1, 0, 0, 0, 3, 0, 0, 0, 63, 0, 0, 0, 1, 0, 0, 0, + 49, 50, 51, // 123 + 52, 53, 54 // 456 + }; + + + FileSystem.Initialize().WithFile("1.txt").Which(m => m.HasStringContent("123")); + FileSystem.Initialize().WithFile("2.txt").Which(m => m.HasStringContent("456")); + + var builderInfo = new List + { + MegDataEntryBuilderInfo.FromFile(FileSystem.FileInfo.New("1.txt"), "file"), + MegDataEntryBuilderInfo.FromFile(FileSystem.FileInfo.New("2.txt"), "file") + }; + + using (var fs = FileSystem.File.OpenWrite(megFileName)) + { + _megService.CreateMegArchive(fs, MegVersion.V1, null, builderInfo); + } + + var bytes = FileSystem.File.ReadAllBytes(megFileName); + + Assert.Equal(expectedBytes, bytes); + } + + [Fact] + public void CreateMegArchive_MemoryStream_WritesSameBytesAsFileStream() + { + var expectedBytes = new byte[] + { + 2, 0, 0, 0, 2, 0, 0, 0, // Header + 4, 0, 102, 105, 108, 101, // "file" + 4, 0, 102, 105, 108, 101, // "file" + 16, 54, 159, 140, 0, 0, 0, 0, 3, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, + 16, 54, 159, 140, 1, 0, 0, 0, 3, 0, 0, 0, 63, 0, 0, 0, 1, 0, 0, 0, + 49, 50, 51, // 123 + 52, 53, 54 // 456 + }; + + FileSystem.Initialize().WithFile("1.txt").Which(m => m.HasStringContent("123")); + FileSystem.Initialize().WithFile("2.txt").Which(m => m.HasStringContent("456")); + + var builderInfo = new List + { + MegDataEntryBuilderInfo.FromFile(FileSystem.FileInfo.New("1.txt"), "file"), + MegDataEntryBuilderInfo.FromFile(FileSystem.FileInfo.New("2.txt"), "file") + }; + + using var ms = new MemoryStream(); + _megService.CreateMegArchive(ms, MegVersion.V1, null, builderInfo); + + Assert.Equal(expectedBytes, ms.ToArray()); + } + + #endregion + + #region Generic Read + + [Fact] + public void LoadFile_InvalidBinary() + { + const string megFileName = "test.meg"; + const string fileData = "some random data"; + + FileSystem.Initialize().WithFile(megFileName).Which(m => m.HasStringContent(fileData)); + + Assert.Throws(() => _megService.LoadFile(megFileName)); + } + + [Fact] + public void LoadFile_ThrowFileNotFound() + { + Assert.Throws(() => _megService.LoadFile("notFound.meg")); + } + + [Fact] + public void LoadFile_NullArgs() + { + Assert.Throws(() => _megService.LoadFile((string)null!)); + Assert.Throws(() => _megService.LoadFile((FileSystemStream)null!)); + } + + [Fact] + public void LoadFile_EncryptedArchive_ThrowsNotImplemented() + { + const string megFileName = "encrypted.meg"; + FileSystem.Initialize().WithFile(megFileName).Which(m => m.HasBytesContent(EncryptedV3EmptyMegHeader)); + + Assert.Throws(() => _megService.LoadFile(megFileName)); + + using var fs = FileSystem.File.OpenRead(megFileName); + Assert.Throws(() => _megService.LoadFile(fs)); + } + + [Fact] + public void LoadArchive_EncryptedArchive_ThrowsNotSupported() + { + // File-stream branch — even though LoadArchive(Stream) with a FileSystemStream would otherwise + // produce a file-backed MegFile, an encrypted archive is by-design unsupported on this entry point. + const string megFileName = "encrypted.meg"; + FileSystem.Initialize().WithFile(megFileName).Which(m => m.HasBytesContent(EncryptedV3EmptyMegHeader)); + using (var fs = FileSystem.File.OpenRead(megFileName)) + { + Assert.Throws(() => _megService.LoadArchive(fs)); + } + + // Memory branches + using var ms = new MemoryStream(EncryptedV3EmptyMegHeader); + Assert.Throws(() => _megService.LoadArchive(ms)); + Assert.Throws(() => _megService.LoadArchive(EncryptedV3EmptyMegHeader)); + Assert.Throws(() => _megService.LoadArchive(EncryptedV3EmptyMegHeader.AsSpan())); + } + + private static readonly byte[] EncryptedV3EmptyMegHeader = + [ + 0xff, 0xff, 0xff, 0x8f, + 0xa4, 0x70, 0x7d, 0x3f, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + ]; + + #endregion + + #region Read / Write V1 + + [Fact] + public void MegV1_WithEntries() + { + const string megFileName = "test.meg"; + + FileSystem.Initialize().WithFile(megFileName).Which(m => m.HasBytesContent(MegTestConstants.ContentMegFileV1)); + + var expectedData = new ExpectedMegTestData + { + IsMegVersion = MegVersion.V1, + IsMegEncrypted = false, + MegFileCount = 2, + EntryNames = new List + { + "DATA\\XML\\CAMPAIGNFILES.XML", + "DATA\\XML\\GAMEOBJECTFILES.XML" + }, + NewMegFilePath = "new.meg", + NewMegVersion = MegVersion.V1, + NewMegIsBinaryEqual = true + }; + TestMegFiles(megFileName, expectedData); + } + + [Fact] + public void MegV1_Empty() + { + const string megFileName = "test.meg"; + const string megResource = "Files.v1_empty.meg"; + + FileSystem.Initialize().WithFile(megFileName) + .Which(m => m.HasBytesContent(TestingHelpers.GetEmbeddedResourceAsByteArray(GetType(), megResource))); + + var expectedData = new ExpectedMegTestData + { + IsMegVersion = MegVersion.V1, + IsMegEncrypted = false, + MegFileCount = 0, + EntryNames = new List(), + NewMegFilePath = "new.meg", + NewMegVersion = MegVersion.V1, + NewMegIsBinaryEqual = true + }; + TestMegFiles(megFileName, expectedData); + } + + [Fact] + public void MegV1_EntriesHaveNonAsciiNames() + { + const string megFileName = "test.meg"; + const string megResource = "Files.v1_2_files_with_extended_ascii_name.meg"; + + FileSystem.Initialize().WithFile(megFileName) + .Which(m => m.HasBytesContent(TestingHelpers.GetEmbeddedResourceAsByteArray(GetType(), megResource))); + + var expectedData = new ExpectedMegTestData + { + IsMegVersion = MegVersion.V1, + IsMegEncrypted = false, + MegFileCount = 2, + EntryNames = new List + { + "TEST?.TXT", + "TEST?.TXT" + }, + NewMegFilePath = "new.meg", + NewMegVersion = MegVersion.V1, + NewMegIsBinaryEqual = false + }; + + TestMegFiles(megFileName, expectedData); + } + + #endregion + + #region GetMegVersion + + [Theory] + [InlineData("Files.v1_1_file_data.meg", MegVersion.V1)] + [InlineData("Files.v2_2_files_data.meg", MegVersion.V2)] + [InlineData("Files.v3n_2_files_data.meg", MegVersion.V3)] + public void GetMegVersion_AllOverloads_ReturnSameVersion(string megResource, MegVersion expectedVersion) + { + var bytes = TestingHelpers.GetEmbeddedResourceAsByteArray(GetType(), megResource); + FileSystem.File.WriteAllBytes(ContentMegFileName, bytes); + + Assert.Equal(expectedVersion, _megService.GetMegVersion(ContentMegFileName, out var encryptedFromFile)); + Assert.False(encryptedFromFile); + + using (var stream = new MemoryStream(bytes)) + { + Assert.Equal(expectedVersion, _megService.GetMegVersion(stream, out var encryptedFromStream)); + Assert.False(encryptedFromStream); + } + + Assert.Equal(expectedVersion, _megService.GetMegVersion(bytes, out var encryptedFromArray)); + Assert.False(encryptedFromArray); + + Assert.Equal(expectedVersion, _megService.GetMegVersion(bytes.AsSpan(), out var encryptedFromSpan)); + Assert.False(encryptedFromSpan); + } + + [Fact] + public void GetMegVersion_Throws_FileNotFound() + { + Assert.Throws(() => _megService.GetMegVersion("notFound.meg", out _)); + } + + [Fact] + public void GetMegVersion_NullArgs_Throws() + { + Assert.Throws(() => _megService.GetMegVersion((string)null!, out _)); + Assert.Throws(() => _megService.GetMegVersion((Stream)null!, out _)); + Assert.Throws(() => _megService.GetMegVersion((byte[])null!, out _)); + } + + [Fact] + public void LoadArchive_NullArgs_Throws() + { + Assert.Throws(() => _megService.LoadArchive((Stream)null!)); + Assert.Throws(() => _megService.LoadArchive((byte[])null!)); + } + + #endregion + + #region Load Archive + + // The different ways a MEG archive can be loaded through IMegService. They all yield an equivalent + // IMegDataSource; the file-based overloads additionally produce a lazily file-backed IMegFile. + public enum LoadOverload + { + FilePath, + FileStream, + LoadArchiveFileStream, + LoadArchiveStream, + LoadArchiveByteArray, + LoadArchiveSpan + } + + [Theory] + [InlineData(LoadOverload.FilePath)] + [InlineData(LoadOverload.FileStream)] + [InlineData(LoadOverload.LoadArchiveFileStream)] + [InlineData(LoadOverload.LoadArchiveStream)] + [InlineData(LoadOverload.LoadArchiveByteArray)] + [InlineData(LoadOverload.LoadArchiveSpan)] + public void Load_ParsesAndReadsEntries(LoadOverload overload) + { + var source = LoadContentMeg(overload); + + Assert.Equal(2, source.Archive.Count); + Assert.Equal("DATA\\XML\\CAMPAIGNFILES.XML", source.Archive[0].Path); + Assert.Equal("DATA\\XML\\GAMEOBJECTFILES.XML", source.Archive[1].Path); + Assert.Equal(MegTestConstants.CampaignFilesContent, ReadAll(source.GetData(source.Archive[0]))); + Assert.Equal(MegTestConstants.GameObjectFilesContent, ReadAll(source.GetData(source.Archive[1]))); + } + + [Theory] + [InlineData(LoadOverload.FilePath)] + [InlineData(LoadOverload.FileStream)] + [InlineData(LoadOverload.LoadArchiveFileStream)] + public void Load_FromFile_ReturnsFileBackedMeg(LoadOverload overload) + { + Assert.IsAssignableFrom(LoadContentMeg(overload)); + } + + [Theory] + [InlineData(LoadOverload.LoadArchiveStream)] + [InlineData(LoadOverload.LoadArchiveByteArray)] + [InlineData(LoadOverload.LoadArchiveSpan)] + public void LoadArchive_FromMemory_IsNotFileBacked(LoadOverload overload) + { + Assert.False(LoadContentMeg(overload) is IMegFile); + } + + [Fact] + public void LoadArchive_NonFileStream_SourceStreamMayBeDisposed() + { + IMegDataSource source; + using (var stream = new MemoryStream(MegTestConstants.ContentMegFileV1)) + { + source = _megService.LoadArchive(stream); + } + + // The source stream is already disposed; the data is still served from the in-memory copy. + Assert.Equal(MegTestConstants.CampaignFilesContent, ReadAll(source.GetData(source.Archive[0]))); + } + + [Fact] + public void LoadArchive_ByteArray_InputMayBeMutated() + { + var data = (byte[])MegTestConstants.ContentMegFileV1.Clone(); + var source = _megService.LoadArchive(data); + + // Wiping the caller's buffer must not affect the loaded MEG (the buffer was copied defensively). + Array.Clear(data, 0, data.Length); + + Assert.Equal(MegTestConstants.CampaignFilesContent, ReadAll(source.GetData(source.Archive[0]))); + } + + [Fact] + public void LoadArchive_NonSeekableStream_Works() + { + using var nonSeekable = new NonSeekableReadStream(MegTestConstants.ContentMegFileV1); + + var source = _megService.LoadArchive(nonSeekable); + + Assert.Equal(2, source.Archive.Count); + Assert.Equal(MegTestConstants.GameObjectFilesContent, ReadAll(source.GetData(source.Archive[1]))); + } + + [Fact] + public void LoadArchive_InMemoryMegs_HaveDistinctNames() + { + var first = _megService.LoadArchive(MegTestConstants.ContentMegFileV1); + var second = _megService.LoadArchive(MegTestConstants.ContentMegFileV1); + + // Two in-memory MEGs loaded from identical bytes should have different identifiers + var firstDescription = new MegDataEntryLocationReference(first, first.Archive[0]).ToString(); + var secondDescription = new MegDataEntryLocationReference(second, second.Archive[0]).ToString(); + + Assert.NotEqual(firstDescription, secondDescription); + } + + // Opening Stream as input leaves a consumer the option to pass a System.IO.FileStream instance. + // In this case, we still want to ensure the loaded MEG is using the file and not an in-memory MEG. + [Fact] + public void LoadArchive_RealSystemIOFileStream() + { + using var services = CreateRealFileSystemServiceProvider(); + var megService = services.GetRequiredService(); + + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + try + { + File.WriteAllBytes(tempFile, MegTestConstants.ContentMegFileV1); + + IMegDataSource source; + using (var fileStream = File.OpenRead(tempFile)) + source = megService.LoadArchive(fileStream); + + Assert.IsAssignableFrom(source); + Assert.Equal(2, source.Archive.Count); + Assert.Equal(MegTestConstants.CampaignFilesContent, ReadAll(source.GetData(source.Archive[0]))); + } + finally + { + if (File.Exists(tempFile)) + File.Delete(tempFile); + } + } + + private IMegDataSource LoadContentMeg(LoadOverload overload) + { + switch (overload) + { + case LoadOverload.FilePath: + WriteContentMegToFile(); + return _megService.LoadFile(ContentMegFileName); + case LoadOverload.FileStream: + { + WriteContentMegToFile(); + using var fs = FileSystem.FileStream.New(ContentMegFileName, FileMode.Open, FileAccess.Read); + return _megService.LoadFile(fs); + } + case LoadOverload.LoadArchiveFileStream: + { + WriteContentMegToFile(); + using var fs = FileSystem.FileStream.New(ContentMegFileName, FileMode.Open, FileAccess.Read); + return _megService.LoadArchive(fs); + } + case LoadOverload.LoadArchiveStream: + { + using var stream = new MemoryStream(MegTestConstants.ContentMegFileV1); + return _megService.LoadArchive(stream); + } + case LoadOverload.LoadArchiveByteArray: + return _megService.LoadArchive(MegTestConstants.ContentMegFileV1); + case LoadOverload.LoadArchiveSpan: + return _megService.LoadArchive(MegTestConstants.ContentMegFileV1.AsSpan()); + default: + throw new ArgumentOutOfRangeException(nameof(overload), overload, null); + } + } + + private void WriteContentMegToFile() + { + FileSystem.File.WriteAllBytes(ContentMegFileName, MegTestConstants.ContentMegFileV1); + } + + private static byte[] ReadAll(Stream stream) + { + using (stream) + { + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + } + + private static ServiceProvider CreateRealFileSystemServiceProvider() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(new Testably.Abstractions.RealFileSystem()); + serviceCollection.AddSingleton(sp => new HashingService(sp)); + PetroglyphCommons.ContributeServices(serviceCollection); + serviceCollection.SupportMEG(); + return serviceCollection.BuildServiceProvider(); + } + + #endregion + + private void TestMegFiles(string megFilePath, ExpectedMegTestData expectedData) + { + var megVersion = _megService.GetMegVersion(megFilePath, out var encrypted); + Assert.Equal(expectedData.IsMegVersion, megVersion); + Assert.Equal(expectedData.IsMegEncrypted, encrypted); + + var meg = _megService.LoadFile(megFilePath); + TestMegModelContent(meg, expectedData, false); + + for (var i = 0; i < meg.Archive.Count; i++) + { + var entry = meg.Archive[i]; + var expected = expectedData.EntryNames[i]; + Assert.Equal(expected, entry.Path); + } + + using var param = new MegFileInformation( + expectedData.NewMegFilePath, + expectedData.NewMegVersion, + expectedData.EncryptionData); + + var builderInformation = meg.Archive.Select(e => + new MegDataEntryBuilderInfo(new MegDataEntryOriginInfo(new MegDataEntryLocationReference(meg, e)))); + + using (var fs = FileSystem.File.OpenWrite(expectedData.NewMegFilePath)) + { + _megService.CreateMegArchive(fs, expectedData.NewMegVersion, expectedData.EncryptionData, + builderInformation); + } + + Assert.True(FileSystem.File.Exists(expectedData.NewMegFilePath)); + + var createdVersion = _megService.GetMegVersion(expectedData.NewMegFilePath, out var newEncrypted); + Assert.Equal(expectedData.NewMegVersion, createdVersion); + Assert.Equal(expectedData.EncryptionData is null, !newEncrypted); + + + var actualBytes = FileSystem.File.ReadAllBytes(expectedData.NewMegFilePath); + var expectedBytes = FileSystem.File.ReadAllBytes(megFilePath); + if (expectedData.NewMegIsBinaryEqual) + Assert.Equal(expectedBytes, actualBytes); + else + Assert.NotEqual(expectedBytes, actualBytes); + + var newMeg = _megService.LoadFile(megFilePath); + TestMegModelContent(newMeg, expectedData, true); + } + + private static void TestMegModelContent(IMegFile meg, ExpectedMegTestData expectedData, bool isNewMeg) + { + Assert.NotNull(meg); + Assert.Equal(expectedData.MegFileCount, meg.Content.Count); + Assert.Equal(expectedData.IsMegVersion, meg.FileInformation.Version); + Assert.Equal(expectedData.EntryNames.Count, meg.Archive.Count); + + if (isNewMeg) + Assert.Equal(expectedData.EncryptionData is null, !meg.FileInformation.HasEncryption); + else + Assert.Equal(expectedData.IsMegEncrypted, meg.FileInformation.HasEncryption); + + for (var i = 0; i < meg.Archive.Count; i++) + { + var entry = meg.Archive[i]; + var expected = expectedData.EntryNames[i]; + Assert.Equal(expected, entry.Path); + } + } + + private record ExpectedMegTestData + { + public MegVersion IsMegVersion { get; init; } + public bool IsMegEncrypted { get; init; } + public int MegFileCount { get; init; } + public IList EntryNames { get; init; } = null!; + public string NewMegFilePath { get; init; } = null!; + public bool NewMegIsBinaryEqual { get; init; } + public MegVersion NewMegVersion { get; init; } + public MegEncryptionData? EncryptionData { get; } + } +} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/VirtualMegArchiveBuilderTest.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/VirtualMegArchiveBuilderTest.cs index 8046cacd9..17e46ccee 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/VirtualMegArchiveBuilderTest.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/Services/VirtualMegArchiveBuilderTest.cs @@ -1,6 +1,9 @@ -using System; +using System; using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.DependencyInjection; using PG.Commons.Hashing; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.Entries; using PG.StarWarsGame.Files.MEG.Files; @@ -33,7 +36,7 @@ public void BuildFrom_ListOfReferences_DoesNotExists_Throws() var entry1 = MegDataEntryTest.CreateEntry("A", new Crc32(1)); FileSystem.File.Create("test.meg"); - var megFile = new MegFile(new MegArchive([]), new MegFileInformation("test.meg", MegFileVersion.V1), ServiceProvider); + var megFile = new MegFile(new MegArchive([]), new MegFileInformation("test.meg", MegVersion.V1), ServiceProvider); var entries = new List { @@ -54,10 +57,10 @@ public void BuildFrom_ListOfReferences_DoNotReplace() var entry3 = MegDataEntryTest.CreateEntry("C", new Crc32(1)); FileSystem.File.Create("1.meg"); - var meg1 = new MegFile(new MegArchive([entry1]), new MegFileInformation("1.meg", MegFileVersion.V1), ServiceProvider); + var meg1 = new MegFile(new MegArchive([entry1]), new MegFileInformation("1.meg", MegVersion.V1), ServiceProvider); FileSystem.File.Create("2.meg"); - var meg2 = new MegFile(new MegArchive([entry2, entry3]), new MegFileInformation("2.meg", MegFileVersion.V1), ServiceProvider); + var meg2 = new MegFile(new MegArchive([entry2, entry3]), new MegFileInformation("2.meg", MegVersion.V1), ServiceProvider); var entries = new List { @@ -72,10 +75,10 @@ public void BuildFrom_ListOfReferences_DoNotReplace() Assert.Equal(2, archive.Count); Assert.Equal(entry2, archive[0].Location.DataEntry); - Assert.Equal(meg2, archive[0].Location.MegFile); + Assert.Equal(meg2, archive[0].Location.Source); Assert.Equal(entry1, archive[1].Location.DataEntry); - Assert.Equal(meg1, archive[1].Location.MegFile); + Assert.Equal(meg1, archive[1].Location.Source); } [Fact] @@ -88,10 +91,10 @@ public void BuildFrom_ListOfReferences_Replace() var entry3 = MegDataEntryTest.CreateEntry("C", new Crc32(1)); FileSystem.File.Create("1.meg"); - var meg1 = new MegFile(new MegArchive([entry1]), new MegFileInformation("1.meg", MegFileVersion.V1), ServiceProvider); + var meg1 = new MegFile(new MegArchive([entry1]), new MegFileInformation("1.meg", MegVersion.V1), ServiceProvider); FileSystem.File.Create("2.meg"); - var meg2 = new MegFile(new MegArchive([entry2, entry3]), new MegFileInformation("2.meg", MegFileVersion.V1), ServiceProvider); + var meg2 = new MegFile(new MegArchive([entry2, entry3]), new MegFileInformation("2.meg", MegVersion.V1), ServiceProvider); var entries = new List { @@ -106,10 +109,10 @@ public void BuildFrom_ListOfReferences_Replace() Assert.Equal(2, archive.Count); Assert.Equal(entry2, archive[0].Location.DataEntry); - Assert.Equal(meg2, archive[0].Location.MegFile); + Assert.Equal(meg2, archive[0].Location.Source); Assert.Equal(entry3, archive[1].Location.DataEntry); - Assert.Equal(meg2, archive[1].Location.MegFile); + Assert.Equal(meg2, archive[1].Location.Source); } [Fact] @@ -122,7 +125,7 @@ public void BuildFrom_SingleMeg() var entry3 = MegDataEntryTest.CreateEntry("C", new Crc32(1)); FileSystem.File.Create("test.meg"); - var megFile = new MegFile(new MegArchive([entry1, entry2, entry3]), new MegFileInformation("test.meg", MegFileVersion.V1), ServiceProvider); + var megFile = new MegFile(new MegArchive([entry1, entry2, entry3]), new MegFileInformation("test.meg", MegVersion.V1), ServiceProvider); var archive = service.BuildFrom(megFile); @@ -130,10 +133,10 @@ public void BuildFrom_SingleMeg() Assert.Equal(2, archive.Count); Assert.Equal(entry1, archive[0].Location.DataEntry); - Assert.Equal(megFile, archive[0].Location.MegFile); + Assert.Equal(megFile, archive[0].Location.Source); Assert.Equal(entry3, archive[1].Location.DataEntry); - Assert.Equal(megFile, archive[1].Location.MegFile); + Assert.Equal(megFile, archive[1].Location.Source); } [Fact] @@ -149,10 +152,10 @@ public void BuildFrom_ListOfMegs_DoNotReplace() var entry2_3 = MegDataEntryTest.CreateEntry("D", new Crc32(1)); FileSystem.File.Create("1.meg"); - var meg1 = new MegFile(new MegArchive([entry1_1, entry1_2]), new MegFileInformation("1.meg", MegFileVersion.V1), ServiceProvider); + var meg1 = new MegFile(new MegArchive([entry1_1, entry1_2]), new MegFileInformation("1.meg", MegVersion.V1), ServiceProvider); FileSystem.File.Create("2.meg"); - var meg2 = new MegFile(new MegArchive([entry2_1, entry2_2, entry2_3]), new MegFileInformation("2.meg", MegFileVersion.V1), ServiceProvider); + var meg2 = new MegFile(new MegArchive([entry2_1, entry2_2, entry2_3]), new MegFileInformation("2.meg", MegVersion.V1), ServiceProvider); var archive = service.BuildFrom(new List{meg1, meg2}, false); @@ -160,10 +163,10 @@ public void BuildFrom_ListOfMegs_DoNotReplace() Assert.Equal(2, archive.Count); Assert.Equal(entry2_1, archive[0].Location.DataEntry); - Assert.Equal(meg2, archive[0].Location.MegFile); + Assert.Equal(meg2, archive[0].Location.Source); Assert.Equal(entry1_1, archive[1].Location.DataEntry); - Assert.Equal(meg1, archive[1].Location.MegFile); + Assert.Equal(meg1, archive[1].Location.Source); } [Fact] @@ -179,10 +182,10 @@ public void BuildFrom_ListOfMegs_Replace() var entry2_3 = MegDataEntryTest.CreateEntry("D", new Crc32(1)); FileSystem.File.Create("1.meg"); - var meg1 = new MegFile(new MegArchive([entry1_1, entry1_2]), new MegFileInformation("1.meg", MegFileVersion.V1), ServiceProvider); + var meg1 = new MegFile(new MegArchive([entry1_1, entry1_2]), new MegFileInformation("1.meg", MegVersion.V1), ServiceProvider); FileSystem.File.Create("2.meg"); - var meg2 = new MegFile(new MegArchive([entry2_1, entry2_2, entry2_3]), new MegFileInformation("2.meg", MegFileVersion.V1), ServiceProvider); + var meg2 = new MegFile(new MegArchive([entry2_1, entry2_2, entry2_3]), new MegFileInformation("2.meg", MegVersion.V1), ServiceProvider); var archive = service.BuildFrom(new List { meg1, meg2 }, true); @@ -190,9 +193,41 @@ public void BuildFrom_ListOfMegs_Replace() Assert.Equal(2, archive.Count); Assert.Equal(entry2_1, archive[0].Location.DataEntry); - Assert.Equal(meg2, archive[0].Location.MegFile); + Assert.Equal(meg2, archive[0].Location.Source); Assert.Equal(entry2_2, archive[1].Location.DataEntry); - Assert.Equal(meg2, archive[1].Location.MegFile); + Assert.Equal(meg2, archive[1].Location.Source); + } + + [Fact] + public void BuildFrom_InMemoryMeg() + { + var service = new VirtualMegArchiveBuilder(); + + using var stream = new MemoryStream(MegTestConstants.ContentMegFileV1); + var inMemoryMeg = ServiceProvider.GetRequiredService().LoadArchive(stream); + + var archive = service.BuildFrom(inMemoryMeg); + + Assert.Equal(2, archive.Count); + Assert.Same(inMemoryMeg, archive[0].Location.Source); + Assert.False(archive[0].Location.Source is IMegFile); + } + + [Fact] + public void BuildFrom_MixedFileAndInMemoryMegs() + { + var service = new VirtualMegArchiveBuilder(); + + FileSystem.File.Create("file.meg"); + var fileEntry = MegDataEntryTest.CreateEntry("A", new Crc32(1)); + var fileMeg = new MegFile(new MegArchive([fileEntry]), new MegFileInformation("file.meg", MegVersion.V1), ServiceProvider); + + using var stream = new MemoryStream(MegTestConstants.ContentMegFileV1); + var inMemoryMeg = ServiceProvider.GetRequiredService().LoadArchive(stream); + + var archive = service.BuildFrom([fileMeg, inMemoryMeg], false); + + Assert.Equal(3, archive.Count); } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/ConstructingMegArchiveBuilderBase.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/ConstructingMegArchiveBuilderBase.cs index 548c100bb..fbb78899e 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/ConstructingMegArchiveBuilderBase.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/ConstructingMegArchiveBuilderBase.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; @@ -15,7 +15,6 @@ using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.Entries; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; -using PG.StarWarsGame.Files.MEG.Files; using PG.StarWarsGame.Files.MEG.Utilities; namespace PG.StarWarsGame.Files.MEG.Binary; @@ -26,7 +25,7 @@ internal abstract class ConstructingMegArchiveBuilderBase(IServiceProvider servi internal virtual uint MaxEntryFileSize { get; } = MaxMegSizeProvider.GetMegMaxSize(MaxMegSizeMode.Binary).MaxEntrySize; internal virtual uint MaxFileSize { get; } = MaxMegSizeProvider.GetMegMaxSize(MaxMegSizeMode.Binary).MaxFileSize; - protected abstract MegFileVersion FileVersion { get; } + protected abstract MegVersion MegVersion { get; } // TODO: Test encryption cases public IConstructingMegArchive BuildConstructingMegArchive(IEnumerable builderEntries) @@ -34,7 +33,7 @@ public IConstructingMegArchive BuildConstructingMegArchive(IEnumerable().GetMegSizeCalculator(FileVersion); + var calculator = Services.GetRequiredService().GetMegSizeCalculator(MegVersion); var binaryInformation = GetBinaryInformation(builderEntries, calculator); @@ -54,10 +53,10 @@ public IConstructingMegArchive BuildConstructingMegArchive(IEnumerable entries, IMegSizeCalculator calculator) { @@ -80,7 +79,7 @@ private MegFileBinaryInformation GetBinaryInformation( Debug.Assert(calculator.MetadataSize <= calculator.CurrentSize); - return new MegFileBinaryInformation((uint)calculator.MetadataSize, FileVersion, encryptMeg, entryInfoList); + return new MegBinaryInformation((uint)calculator.MetadataSize, MegVersion, encryptMeg, entryInfoList); } private MegDataEntryBinaryInformation CreateEntryBinaryInformation( diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/MegFileBinaryInformation.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/MegBinaryInformation.cs similarity index 63% rename from PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/MegFileBinaryInformation.cs rename to PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/MegBinaryInformation.cs index 9e778da4d..1aea9b462 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/MegFileBinaryInformation.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/MegBinaryInformation.cs @@ -1,14 +1,14 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. +using PG.StarWarsGame.Files.MEG.Data; using System.Collections.Generic; -using PG.StarWarsGame.Files.MEG.Files; namespace PG.StarWarsGame.Files.MEG.Binary; -internal class MegFileBinaryInformation( +internal class MegBinaryInformation( uint metadataSize, - MegFileVersion megFileVersion, + MegVersion MegVersion, bool encrypted, IEnumerable entries) { @@ -16,7 +16,7 @@ internal class MegFileBinaryInformation( public bool Encrypted { get; } = encrypted; - public MegFileVersion MegFileVersion { get; } = megFileVersion; + public MegVersion MegVersion { get; } = MegVersion; public IEnumerable Entries { get; } = entries; } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/V1/ConstructingMegArchiveBuilderV1.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/V1/ConstructingMegArchiveBuilderV1.cs index 8c8ad7124..654ecfb5e 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/V1/ConstructingMegArchiveBuilderV1.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Construction/V1/ConstructingMegArchiveBuilderV1.cs @@ -1,8 +1,8 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. +using PG.StarWarsGame.Files.MEG.Data; using System; -using PG.StarWarsGame.Files.MEG.Files; namespace PG.StarWarsGame.Files.MEG.Binary.V1; @@ -11,5 +11,5 @@ internal sealed class ConstructingMegArchiveBuilderV1(IServiceProvider services) // NB: We do not override the MaxEntryFileSize, because this limitation, so far, // only applies to Empire at War / Forces of Corruption but not to the V1 format in general. - protected override MegFileVersion FileVersion => MegFileVersion.V1; + protected override MegVersion MegVersion => MegVersion.V1; } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/IMegBinaryServiceFactory.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/IMegBinaryServiceFactory.cs index 5f26bfe37..35d4a248f 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/IMegBinaryServiceFactory.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/IMegBinaryServiceFactory.cs @@ -1,21 +1,22 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using PG.StarWarsGame.Files.MEG.Binary.Size; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Files; namespace PG.StarWarsGame.Files.MEG.Binary; /// -/// Base service to handle the transformation from binary to a and vice versa. +/// Provides factory methods for the services that transform between binary data and an . /// internal interface IMegBinaryServiceFactory { - IMegFileBinaryReader GetReader(MegFileVersion megVersion); + IMegFileBinaryReader GetReader(MegVersion megVersion); - IMegBinaryConverter GetConverter(MegFileVersion megVersion); + IMegBinaryConverter GetConverter(MegVersion megVersion); - IConstructingMegArchiveBuilder GetConstructionBuilder(MegFileVersion megVersion); + IConstructingMegArchiveBuilder GetConstructionBuilder(MegVersion megVersion); - IMegSizeCalculator GetMegSizeCalculator(MegFileVersion megFileVersion); + IMegSizeCalculator GetMegSizeCalculator(MegVersion megVersion); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/MegBinaryServiceFactory.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/MegBinaryServiceFactory.cs index 9ecbd5afd..bf908e491 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/MegBinaryServiceFactory.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/MegBinaryServiceFactory.cs @@ -1,10 +1,10 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; using PG.StarWarsGame.Files.MEG.Binary.Size; using PG.StarWarsGame.Files.MEG.Binary.V1; -using PG.StarWarsGame.Files.MEG.Files; +using PG.StarWarsGame.Files.MEG.Data; namespace PG.StarWarsGame.Files.MEG.Binary; @@ -12,35 +12,35 @@ internal class MegBinaryServiceFactory(IServiceProvider serviceProvider) : IMegB { private readonly IServiceProvider _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - public IMegFileBinaryReader GetReader(MegFileVersion megVersion) + public IMegFileBinaryReader GetReader(MegVersion megVersion) { - if (megVersion == MegFileVersion.V1) + if (megVersion == MegVersion.V1) return new MegFileBinaryReaderV1(_serviceProvider); throw new NotImplementedException("MEGs other than V1 are currently not supported."); } - public IMegBinaryConverter GetConverter(MegFileVersion megVersion) + public IMegBinaryConverter GetConverter(MegVersion megVersion) { - if (megVersion == MegFileVersion.V1) + if (megVersion == MegVersion.V1) return new MegBinaryConverterV1(_serviceProvider); throw new NotImplementedException("MEGs other than V1 are currently not supported."); } - public IConstructingMegArchiveBuilder GetConstructionBuilder(MegFileVersion megVersion) + public IConstructingMegArchiveBuilder GetConstructionBuilder(MegVersion megVersion) { - if (megVersion == MegFileVersion.V1) + if (megVersion == MegVersion.V1) return new ConstructingMegArchiveBuilderV1(_serviceProvider); throw new NotImplementedException("MEGs other than V1 are currently not supported."); } - public IMegSizeCalculator GetMegSizeCalculator(MegFileVersion megVersion) + public IMegSizeCalculator GetMegSizeCalculator(MegVersion megVersion) { return megVersion switch { - MegFileVersion.V1 => new MegV1SizeCalculator(), + MegVersion.V1 => new MegV1SizeCalculator(), _ => throw new NotImplementedException($"MEG version {megVersion} is currently not supported.") }; } diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/MegFileConstants.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/MegFileConstants.cs index fb3ba1115..15d5ed824 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/MegFileConstants.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/MegFileConstants.cs @@ -32,7 +32,7 @@ public class MegFileConstants internal const uint MegMaxFileSize = uint.MaxValue; /// - /// The max number of characters allowed in Empire at War game for MEG entry paths. + /// Represents the maximum number of characters allowed in Empire at War for MEG entry paths. /// public const int EawMaxEntryPathLength = 259; @@ -52,12 +52,12 @@ public class MegFileConstants // // Therefore, we limit the max entry size, and thus the max file size to int.MaxValue (2GB) for Eaw/Foc MEG files. /// - /// The max size of a MEG entry for Empire at War / Forces of Corruption. + /// Represents the maximum size of a MEG entry for Empire at War / Forces of Corruption. /// public const int EawMegMaxEntrySize = int.MaxValue; - + /// - /// The max file size of a MEG file for Empire at War / Forces of Corruption. + /// Represents the maximum size of a MEG file for Empire at War / Forces of Corruption. /// public const int EawMegMaxFileSize = EawMegMaxEntrySize; @@ -73,7 +73,7 @@ public class MegFileConstants // 'ß.txt' --> '?.txt' // 'ä.txt' --> '?.txt' /// - /// ASCII encoding is used for MEG entries. + /// Represents the ASCII encoding used for MEG entries. /// public static readonly Encoding MegDataEntryPathEncoding = Encoding.ASCII; diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegFileDescriptor.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegFileDescriptor.cs index ed09b4fea..105a4e196 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegFileDescriptor.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegFileDescriptor.cs @@ -1,33 +1,48 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; using PG.Commons.Data; using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.MEG.Files; +using PG.StarWarsGame.Files.MEG.Data; namespace PG.StarWarsGame.Files.MEG.Binary.Metadata; internal interface IMegFileDescriptor : IBinary, IHasCrc32, IComparable { + /// + /// Gets the offset, in bytes, where the described file's data starts within the .MEG archive. + /// public uint FileOffset { get; } + /// + /// Gets the size, in bytes, of the described file's data. + /// public uint FileSize { get; } + /// + /// Gets the index of the described file's name within the file name table. + /// /// /// The .MEG specification allows , however in .NET we are /// limited to for indexing native list-like structures.
///
- /// For .MEG archives this values type must be treated as .
+ /// For .MEG archives this values type must be treated as .
/// When reading or writing a .MEG binary, assertions should be implemented to verify data validity. ///
public int FileNameIndex { get; } + /// + /// Gets the index of the described file within the file table. + /// /// /// The .MEG specification allows , however in .NET we are /// limited to for indexing native list-like structures.
///
public int Index { get; } - + + /// + /// Gets a value that indicates whether the described file's data is encrypted. + /// public bool Encrypted { get; } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegFileMetadata.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegFileMetadata.cs index bc3ce612a..6415ad187 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegFileMetadata.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegFileMetadata.cs @@ -8,9 +8,18 @@ namespace PG.StarWarsGame.Files.MEG.Binary.Metadata; internal interface IMegFileMetadata : IBinaryFile { + /// + /// Gets the header of the .MEG archive. + /// IMegHeader Header { get; } + /// + /// Gets the table that holds the file names of the .MEG archive. + /// BinaryTable FileNameTable { get; } + /// + /// Gets the table that holds the file descriptors of the .MEG archive. + /// IMegFileTable FileTable { get; } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegHeader.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegHeader.cs index 4862c1007..fdba60ec0 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegHeader.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/IMegHeader.cs @@ -7,9 +7,12 @@ namespace PG.StarWarsGame.Files.MEG.Binary.Metadata; internal interface IMegHeader : IBinary { + /// + /// Gets the number of files contained in the .MEG archive. + /// /// /// The .MEG specification allows , however in .NET we are - /// limited to for indexing native list-like structures. + /// limited to for indexing native list-like structures. /// int FileNumber { get; } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/V1/MegFileTableRecord.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/V1/MegFileTableRecord.cs index f32ae2423..3a1f6a3bb 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/V1/MegFileTableRecord.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/V1/MegFileTableRecord.cs @@ -106,7 +106,7 @@ int IComparable.CompareTo(IMegFileDescriptor? other) } /// - /// Determines whether one record is greater than or equal to another by its checksum. + /// Determines whether one record is greater than or equal to another by its checksum. /// /// The first record to compare. /// The second record to compare. @@ -117,10 +117,10 @@ int IComparable.CompareTo(IMegFileDescriptor? other) } /// - /// Determines whether one record is less than or equal to another by its checksum. + /// Determines whether one record is less than or equal to another by its checksum. /// - /// The first record checksum to compare. - /// The second record to + /// The first record to compare. + /// The second record to compare. /// if is less than or equal to ; otherwise, . public static bool operator <=(MegFileTableRecord a, MegFileTableRecord b) { diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/V1/MegMetadata.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/V1/MegMetadata.cs index 02ee39631..5abce7fef 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/V1/MegMetadata.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Metadata/V1/MegMetadata.cs @@ -7,7 +7,7 @@ namespace PG.StarWarsGame.Files.MEG.Binary.Metadata.V1; /// -/// Meg archive representation WITHOUT content data. +/// Represents a .MEG archive's metadata, without the actual content data. /// internal class MegMetadata : BinaryFile, IMegFileMetadata { diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Reader/Validation/IMegBinaryValidator.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Reader/Validation/IMegBinaryValidator.cs index 5f7e1a330..85a7fcdd8 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Reader/Validation/IMegBinaryValidator.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Reader/Validation/IMegBinaryValidator.cs @@ -15,7 +15,7 @@ internal interface IMegBinaryValidator where TMetadata : IMegFileM { /// /// Validates the integrity and correctness of the specified MEG file metadata against the provided binary data - /// and throws an if the file is invalid. + /// and throws a if the file is invalid. /// /// The metadata of the MEG file to validate. /// The actual size of the metadata in bytes as read from the binary data. diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Version/IMegVersionIdentifier.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Version/IMegVersionIdentifier.cs index d88351043..f73a62fb7 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Version/IMegVersionIdentifier.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Version/IMegVersionIdentifier.cs @@ -1,12 +1,12 @@ // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. +using PG.StarWarsGame.Files.MEG.Data; using System.IO; -using PG.StarWarsGame.Files.MEG.Files; namespace PG.StarWarsGame.Files.MEG.Binary; internal interface IMegVersionIdentifier { - MegFileVersion GetMegFileVersion(Stream data, out bool encrypted); + MegVersion GetMegVersion(Stream data, out bool encrypted); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Version/MegVersionIdentifier.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Version/MegVersionIdentifier.cs index 2e3e54f62..e16806c3a 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Version/MegVersionIdentifier.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Binary/Version/MegVersionIdentifier.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; @@ -8,24 +8,25 @@ using Microsoft.Extensions.Logging; using PG.Commons.Services; using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.MEG.Files; +using PG.StarWarsGame.Files.MEG.Data; namespace PG.StarWarsGame.Files.MEG.Binary; internal class MegVersionIdentifier(IServiceProvider services) : ServiceBase(services), IMegVersionIdentifier { /// - /// This method is optimized in a way to retrieve the MEGs file version as efficient as possible. - /// If an invalid MEG archive is detected a is thrown. - /// However, this method does not completely verify whether the passed stream is a valid MEG archive or not. + /// Determines the MEG file version of the specified stream as efficiently as possible. /// - /// The MEG archive stream - /// Indicates whether the archive is encrypted or not. - /// The determined of the MEG stream. - /// The is null. - /// The is not readable or seekable. + /// + /// This method does not completely verify whether the passed stream is a valid MEG archive. + /// + /// The MEG archive stream. + /// When this method returns, contains if the archive is encrypted; otherwise, . This parameter is treated as uninitialized. + /// The determined of the MEG stream. + /// is . + /// is not readable or seekable. /// The read data is not a valid MEG archive. - public unsafe MegFileVersion GetMegFileVersion(Stream stream, out bool encrypted) + public unsafe MegVersion GetMegVersion(Stream stream, out bool encrypted) { if (stream == null) throw new ArgumentNullException(nameof(stream)); @@ -51,7 +52,7 @@ public unsafe MegFileVersion GetMegFileVersion(Stream stream, out bool encrypted // Note: In V1 we *could* have the situation where we store as many files in the meg to coincidentally match the magic number. // Thus, we don't check for the magic number as that it would not gain us anything. if (flags == id) - return MegFileVersion.V1; + return MegVersion.V1; Logger.LogTrace("Checking MEG version: Must be V2 or V3."); @@ -63,7 +64,7 @@ public unsafe MegFileVersion GetMegFileVersion(Stream stream, out bool encrypted { Logger.LogTrace("Checking MEG version: MEG has encrypted flag. Version is V3."); encrypted = true; - return MegFileVersion.V3; + return MegVersion.V3; } var dataStart = reader.ReadUInt32(); @@ -125,21 +126,21 @@ public unsafe MegFileVersion GetMegFileVersion(Stream stream, out bool encrypted if (!TryReadUInt32(reader, out var filenamesSize)) { if (numFiles == 0) - return MegFileVersion.V2; + return MegVersion.V2; throw new BinaryCorruptedException("Unrecognized :MEG file version"); } if (numFiles == 0) { if (filenamesSize == 0) - return MegFileVersion.V3; + return MegVersion.V3; // An empty V2 file that has some junk attached. - return MegFileVersion.V2; + return MegVersion.V2; } delegate* recordCheckMethod; - MegFileVersion versionToCheck; + MegVersion versionToCheck; // known start of the FileTable var fileTableOffset = checked(dataStart - numFiles * 20); @@ -150,13 +151,13 @@ public unsafe MegFileVersion GetMegFileVersion(Stream stream, out bool encrypted if (fileTableOffset == 24 + filenamesSize) { Logger.LogTrace("Checking MEG version: Checking V3 case..."); - versionToCheck = MegFileVersion.V3; + versionToCheck = MegVersion.V3; recordCheckMethod = &FileRecordIsV3; } else { Logger.LogTrace("Checking MEG version: Checking V2 case..."); - versionToCheck = MegFileVersion.V2; + versionToCheck = MegVersion.V2; recordCheckMethod = &FileRecordIsV2; } @@ -165,7 +166,7 @@ public unsafe MegFileVersion GetMegFileVersion(Stream stream, out bool encrypted if (CheckFirstAndLastRecord(reader, dataStart, numFiles, recordCheckMethod)) return versionToCheck; - if (versionToCheck == MegFileVersion.V2) + if (versionToCheck == MegVersion.V2) throw new BinaryCorruptedException("Unrecognized .MEG file version."); Logger.LogTrace("Checking MEG version: V3 case did not pass, checking for V2..."); @@ -176,7 +177,7 @@ public unsafe MegFileVersion GetMegFileVersion(Stream stream, out bool encrypted reader.BaseStream.Position = fileTableOffset; if (CheckFirstAndLastRecord(reader, dataStart, numFiles, &FileRecordIsV2)) - return MegFileVersion.V2; + return MegVersion.V2; } throw new BinaryCorruptedException("Unrecognized .MEG file version."); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/ConstructingMegArchive.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/ConstructingMegArchive.cs index 11bae168f..bd616cc86 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/ConstructingMegArchive.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/ConstructingMegArchive.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using PG.StarWarsGame.Files.MEG.Data.Entries; -using PG.StarWarsGame.Files.MEG.Files; namespace PG.StarWarsGame.Files.MEG.Data.Archives; @@ -12,7 +11,7 @@ internal sealed class ConstructingMegArchive : MegDataEntryHolderBase virtualEntries, - MegFileVersion megVersion, + MegVersion megVersion, uint expectedFileSize, bool encrypted) : base(virtualEntries) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/IConstructingMegArchive.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/IConstructingMegArchive.cs index d0ad2bdb0..ad2730828 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/IConstructingMegArchive.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/IConstructingMegArchive.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. using PG.StarWarsGame.Files.MEG.Data.Entries; -using PG.StarWarsGame.Files.MEG.Files; namespace PG.StarWarsGame.Files.MEG.Data.Archives; @@ -13,7 +12,7 @@ internal interface IConstructingMegArchive : IMegDataEntryHolder -/// Contains all data entries of a single, physical .MEG file. +/// Represents the list of data entries contained in a MEG archive. +///
+/// The entries are sorted by their CRC32 checksum, which is calculated over the file name of the entry. ///
public interface IMegArchive : IMegDataEntryHolder; \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/IMegDataEntryHolder.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/IMegDataEntryHolder.cs index 02a99e346..de320cd30 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/IMegDataEntryHolder.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/IMegDataEntryHolder.cs @@ -10,7 +10,7 @@ namespace PG.StarWarsGame.Files.MEG.Data.Archives; /// -/// Holds a CRC32-sorted list of MEG data entries. +/// Represents a CRC32-sorted list of MEG data entries. /// /// The type of the data entry. public interface IMegDataEntryHolder : IReadOnlyList where T : IMegDataEntry @@ -18,8 +18,8 @@ public interface IMegDataEntryHolder : IReadOnlyList where T : IMegDataEnt /// /// Determines whether the contains a specific file entry. /// - /// The entry to locate in the . - /// if the is found in the archive; otherwise, . + /// The entry to locate in the . + /// if is found in the ; otherwise, . bool Contains(T entry); /// @@ -30,24 +30,24 @@ public interface IMegDataEntryHolder : IReadOnlyList where T : IMegDataEnt int IndexOf(T entry); /// - /// Gets a list of data entries with the matching CRC32 checksum or an empty list, if the CRC32 checksum is not found. + /// Gets the list of data entries with the matching CRC32 checksum, or an empty list if the CRC32 checksum is not found. /// - /// The CRC to match. - /// List of matching data entries. + /// The CRC32 checksum to match. + /// The list of matching data entries. ImmutableFrugalList EntriesWithCrc(Crc32 crc); /// - /// Get the first data entry with the matching CRC32 checksum. + /// Gets the first data entry with the matching CRC32 checksum. /// - /// The CRC to match. + /// The CRC32 checksum to match. /// The first entry in the with the specified checksum. /// is not found in the . T FirstEntryWithCrc(Crc32 crc); /// - /// Tries to find any by matching the specified search pattern. + /// Finds all entries matching the specified search pattern. ///
- /// The resulting list is ordered by CRC32 of the file name, just are a MEG archive is ordered. The list is empty, if no matches are found. + /// The resulting list is ordered by CRC32 of the file name, just as a MEG archive is ordered. The list is empty if no matches are found. ///
/// /// The search pattern supports globbing. So **/*.xml is a valid query. @@ -58,8 +58,8 @@ public interface IMegDataEntryHolder : IReadOnlyList where T : IMegDataEnt /// The search pattern might produce false-positive and false negatives, since this method is not designed to resolve paths. /// /// The globbing pattern. - /// When the pattern ignores the character casing. - /// A list with all entries matching the specified pattern. + /// to ignore the character casing of the pattern; otherwise, . + /// A list of all entries matching the specified pattern. /// is empty. /// is . ImmutableFrugalList FindAllEntries(string searchPattern, bool caseInsensitive); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/MegArchive.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/MegArchive.cs index f1f354c5c..8a655d3bc 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/MegArchive.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/MegArchive.cs @@ -2,16 +2,17 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. using System.Collections.Generic; +using System.Diagnostics; using PG.StarWarsGame.Files.MEG.Data.Entries; namespace PG.StarWarsGame.Files.MEG.Data.Archives; /// +[DebuggerDisplay("{Count} entries")] internal sealed class MegArchive : MegDataEntryHolderBase, IMegArchive { /// - /// Initializes a new instance of the class - /// by coping all elements of the given list. + /// Initializes a new instance of the class by copying all elements of the specified list. /// /// The list of entries in this archive. internal MegArchive(IList entries) : base(entries) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/MegDataEntryHolderBase.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/MegDataEntryHolderBase.cs index f8090e969..59a19e06e 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/MegDataEntryHolderBase.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/MegDataEntryHolderBase.cs @@ -19,7 +19,7 @@ namespace PG.StarWarsGame.Files.MEG.Data.Archives; public abstract class MegDataEntryHolderBase : IMegDataEntryHolder where T : IMegDataEntry { /// - /// All data entries of this instance. + /// Represents all data entries of this instance. /// protected readonly ReadOnlyCollection Entries; @@ -34,10 +34,11 @@ public abstract class MegDataEntryHolderBase : IMegDataEntryHolder where T public int Count => Entries.Count; /// - /// Initializes a new instance of the class - /// by coping all elements of the given list. + /// Initializes a new instance of the class by copying all elements of the specified list. /// /// The list of entries in this archive. + /// is . + /// is not sorted by CRC32 checksum. protected MegDataEntryHolderBase(IList entries) { if (entries == null) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/VirtualMegArchive.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/VirtualMegArchive.cs index f44280e64..2beaffbfc 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/VirtualMegArchive.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Archives/VirtualMegArchive.cs @@ -2,16 +2,17 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. using System.Collections.Generic; +using System.Diagnostics; using PG.StarWarsGame.Files.MEG.Data.Entries; namespace PG.StarWarsGame.Files.MEG.Data.Archives; /// +[DebuggerDisplay("{Count} entries")] internal sealed class VirtualMegArchive : MegDataEntryHolderBase, IVirtualMegArchive { /// - /// Initializes a new instance of the class - /// by coping all elements of the given list. + /// Initializes a new instance of the class by copying all elements of the specified list. /// /// The list of files in this archive. internal VirtualMegArchive(IList files) : base(files) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/IMegDataEntry.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/IMegDataEntry.cs index 2990cc6b2..6edd82dda 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/IMegDataEntry.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/IMegDataEntry.cs @@ -16,7 +16,8 @@ namespace PG.StarWarsGame.Files.MEG.Data.Entries; public interface IMegDataEntry : IHasCrc32, IComparable { /// - /// Gets the path of the entry as defined in the *.MEG file.
+ /// Gets the path of the entry as defined in the *.MEG file. + ///
/// Usually this file path is relative to the game or mod's DATA directory, e.g. Data/My/file.xml ///
/// @@ -31,7 +32,7 @@ public interface IMegDataEntry : IHasCrc32, IComparable public interface IMegDataEntry : IMegDataEntry where T : IDataEntryLocation { /// - /// Get the location information of this data entry. + /// Gets the location information of this data entry. /// public T Location { get; } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntry.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntry.cs index e126fbb07..2f42d0d9f 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntry.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntry.cs @@ -23,12 +23,12 @@ public sealed class MegDataEntry : MegDataEntryBase, IEqua public override Crc32 Crc32 { get; } /// - /// The original file path value without the default MEG encoding applied. + /// Gets the original file path value without the default MEG encoding applied. /// public string OriginalPath { get; } /// - /// Indicates whether the file is encrypted + /// Gets a value that indicates whether the file is encrypted. /// public bool Encrypted { get; } @@ -36,12 +36,12 @@ public sealed class MegDataEntry : MegDataEntryBase, IEqua /// Initializes a new instance of the class. ///
/// The file path of the entry. - /// The CRC32 checksum of the filePath - /// The location information of the entry inside it's MEG file. - /// Indicates whether this entry is encrypted or not. + /// The CRC32 checksum of the file path. + /// The location information of the entry inside its MEG file. + /// if this entry is encrypted; otherwise, . /// The original file path value. /// or is . - /// is empty or only consists of while spaces. + /// is empty or only consists of white spaces. /// is empty. internal MegDataEntry(string entryPath, Crc32 crc32, MegDataEntryLocation location, bool encrypted, string originalPath) : base(location) { diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntryBase.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntryBase.cs index ae83b7297..314b49999 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntryBase.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntryBase.cs @@ -27,7 +27,7 @@ public abstract class MegDataEntryBase : IMegDataEntry, IEquatable class with a given data entry location. /// /// The location information of this entry. - /// The is . + /// is . protected MegDataEntryBase(T location) { Location = location ?? throw new ArgumentNullException(nameof(location)); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntryReference.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntryReference.cs index 20f705222..b58b578d3 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntryReference.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/Entries/MegDataEntryReference.cs @@ -23,7 +23,7 @@ public sealed class MegDataEntryReference : MegDataEntryBase Location.DataEntry.Crc32; /// - /// Initializes a new instance of the . + /// Initializes a new instance of the class. /// /// The full location information of this data entry. public MegDataEntryReference(MegDataEntryLocationReference location) : base(location) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/IDataEntryLocation.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/IDataEntryLocation.cs index cfda731cc..4caf943ad 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/IDataEntryLocation.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/IDataEntryLocation.cs @@ -4,6 +4,6 @@ namespace PG.StarWarsGame.Files.MEG.Data.EntryLocations; /// -/// Denotes a type to be used as a data entry location. +/// Represents a type to be used as a data entry location. /// public interface IDataEntryLocation; \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryLocation.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryLocation.cs index 79d8e19d0..a0b098eb8 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryLocation.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryLocation.cs @@ -6,7 +6,7 @@ namespace PG.StarWarsGame.Files.MEG.Data.EntryLocations; /// -/// Location of an archived MEG data entry inside a .MEG file. +/// Represents the location of an archived MEG data entry inside a .MEG file. /// public readonly struct MegDataEntryLocation : IDataEntryLocation, IEquatable { @@ -21,9 +21,9 @@ namespace PG.StarWarsGame.Files.MEG.Data.EntryLocations; public uint Size { get; } /// - /// Initializes a new instance of the structure to a given file offset and file size. + /// Initializes a new instance of the structure. /// - /// The offset of the file in bytes from the start its .MEG file. + /// The offset of the file in bytes from the start of its .MEG file. /// The size of the file in bytes. public MegDataEntryLocation(uint offset, uint size) { diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryLocationReference.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryLocationReference.cs index 57904ff6f..2150bdbac 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryLocationReference.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryLocationReference.cs @@ -2,20 +2,21 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. using System; +using System.Diagnostics.CodeAnalysis; using PG.StarWarsGame.Files.MEG.Data.Entries; using PG.StarWarsGame.Files.MEG.Files; namespace PG.StarWarsGame.Files.MEG.Data.EntryLocations; /// -/// Location reference of an existing MEG data entry and its owning .MEG file. +/// Represents a location reference of an existing MEG data entry and its owning MEG. /// public sealed class MegDataEntryLocationReference : IDataEntryLocation, IEquatable { /// - /// Gets the owning .MEG file of . + /// Gets the MEG that owns . /// - public IMegFile MegFile { get; } + public IMegDataSource Source { get; } /// /// Gets the referenced MEG data entry. @@ -23,19 +24,19 @@ public sealed class MegDataEntryLocationReference : IDataEntryLocation, IEquatab public MegDataEntry DataEntry { get; } /// - /// Gets a value indicating whether the data exists in the meg file referenced in this instance. + /// Gets a value that indicates whether the data exists in the MEG referenced in this instance. /// - public bool Exists => MegFile.Archive.Contains(DataEntry); - + public bool Exists => Source.Archive.Contains(DataEntry); + /// - /// Initializes a new instance of the . + /// Initializes a new instance of the class. /// - /// The owning .MEG file - /// The referenced . - /// The or is . - public MegDataEntryLocationReference(IMegFile megFile, MegDataEntry dataEntry) + /// The MEG that owns the entry. + /// The referenced data entry. + /// or is . + public MegDataEntryLocationReference(IMegDataSource source, MegDataEntry dataEntry) { - MegFile = megFile ?? throw new ArgumentNullException(nameof(megFile)); + Source = source ?? throw new ArgumentNullException(nameof(source)); DataEntry = dataEntry ?? throw new ArgumentNullException(nameof(dataEntry)); } @@ -46,7 +47,7 @@ public bool Equals(MegDataEntryLocationReference? other) return false; if (ReferenceEquals(this, other)) return true; - return MegFile.Equals(other.MegFile) && DataEntry.Equals(other.DataEntry); + return Source.Equals(other.Source) && DataEntry.Equals(other.DataEntry); } /// @@ -58,12 +59,23 @@ public override bool Equals(object? obj) /// public override int GetHashCode() { - return HashCode.Combine(MegFile, DataEntry); + return HashCode.Combine(Source, DataEntry); } /// public override string ToString() { - return $"{MegFile.FilePath}::{DataEntry.Path}"; + return $"{DescribeSource(Source)}::{DataEntry.Path}"; + } + + [ExcludeFromCodeCoverage] + internal static string DescribeSource(IMegDataSource source) + { + return source switch + { + IMegFile megFile => megFile.FilePath, + InMemoryMeg inMemoryMeg => inMemoryMeg.Name, + _ => "" + }; } -} \ No newline at end of file +} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryOriginInfo.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryOriginInfo.cs index 3456b8555..32d3b8d40 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryOriginInfo.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/EntryLocations/MegDataEntryOriginInfo.cs @@ -3,17 +3,18 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.IO.Abstractions; namespace PG.StarWarsGame.Files.MEG.Data.EntryLocations; /// -/// The origin of a MEG data entry which is either packed in a MEG archive, present on the file system, or backed by an in-memory byte buffer. +/// Represents the origin of a MEG data entry which is either packed in a MEG archive, present on the file system, or as an in-memory byte buffer. /// public sealed class MegDataEntryOriginInfo : IDataEntryLocation, IEquatable { /// - /// Gets a value indicating whether the data entry originates from a local file on the file system. + /// Gets a value that indicates whether the data entry originates from a local file on the file system. /// /// /// If this property returns , the property is guaranteed to be non-. @@ -25,19 +26,19 @@ public sealed class MegDataEntryOriginInfo : IDataEntryLocation, IEquatable FileInfo != null; /// - /// Gets a value indicating whether the data entry originates from a MEG archive reference. + /// Gets a value that indicates whether the data entry originates from a MEG archive reference. /// /// /// If this property returns , the property is guaranteed to be non-. /// /// - /// if the data entry is from a referend MEG entry; otherwise, . + /// if the data entry is from a referenced MEG entry; otherwise, . /// [MemberNotNullWhen(true, nameof(MegFileLocation))] public bool IsEntryReference => MegFileLocation != null; /// - /// Gets a value indicating whether the data entry originates from an in-memory byte buffer. + /// Gets a value that indicates whether the data entry originates from an in-memory byte buffer. /// /// /// If this property returns , the property is guaranteed to be non-. @@ -46,12 +47,12 @@ public sealed class MegDataEntryOriginInfo : IDataEntryLocation, IEquatable Bytes != null; /// - /// Gets the MEG file's data entry. if not present. + /// Gets the MEG file's data entry. if not present. /// public MegDataEntryLocationReference? MegFileLocation { get; } /// - /// Gets the file's path on the file system. if not present. + /// Gets the file's path on the file system. if not present. /// public IFileInfo? FileInfo { get; } @@ -78,7 +79,7 @@ public MegDataEntryOriginInfo(IFileInfo fileInfo) /// Initializes a new instance of the class to the specified MEG file's data entry. /// /// The MEG file's data entry. - /// If is . + /// is . public MegDataEntryOriginInfo(MegDataEntryLocationReference locationReference) { MegFileLocation = locationReference ?? throw new ArgumentNullException(nameof(locationReference)); @@ -132,6 +133,7 @@ public override int GetHashCode() } /// + [ExcludeFromCodeCoverage] public override string ToString() { if (IsLocalFile) @@ -140,4 +142,15 @@ public override string ToString() return $"MEG Entry: '{MegFileLocation}'"; return $"Bytes: {Bytes!.Length} bytes"; } + + internal Stream GetDataStream() + { + if (IsBytes) + return new MemoryStream(Bytes, writable: false); + + if (IsLocalFile) + return FileInfo.OpenRead(); + + return MegFileLocation!.Source.GetData(MegFileLocation.DataEntry); + } } diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/IMegDataSource.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/IMegDataSource.cs new file mode 100644 index 000000000..a339f44ac --- /dev/null +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/IMegDataSource.cs @@ -0,0 +1,29 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using PG.StarWarsGame.Files.MEG.Data.Archives; +using PG.StarWarsGame.Files.MEG.Data.Entries; +using PG.StarWarsGame.Files.MEG.Utilities; + +namespace PG.StarWarsGame.Files.MEG.Data; + +/// +/// Represents a MEG archive along with the necessary means to access its data. +/// +public interface IMegDataSource : IDisposable +{ + /// + /// Gets the archive metadata: the table of contents describing the data entries contained in this MEG. + /// + IMegArchive Archive { get; } + + /// + /// Gets a read-only stream over the data of the specified . + /// + /// The data entry to read. + /// A read-only stream containing the entry's data. + /// is . + /// is not contained in this MEG. + MegEntryStream GetData(MegDataEntry entry); +} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/InMemoryMeg.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/InMemoryMeg.cs new file mode 100644 index 000000000..a19526cc6 --- /dev/null +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/InMemoryMeg.cs @@ -0,0 +1,54 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Diagnostics; +using System.IO; +using AnakinRaW.CommonUtilities; +using PG.StarWarsGame.Files.MEG.Data.Archives; +using PG.StarWarsGame.Files.MEG.Data.Entries; +using PG.StarWarsGame.Files.MEG.Utilities; + +namespace PG.StarWarsGame.Files.MEG.Data; + +/// +/// An that holds a whole MEG in memory and serves entry data from a byte buffer. +/// +[DebuggerDisplay("{Name} ({Archive.Count})")] +internal sealed class InMemoryMeg : DisposableObject, IMegDataSource +{ + private readonly byte[] _megData; + + /// + public IMegArchive Archive { get; } + + internal string Name { get; } = $""; + + /// + /// Initializes a new instance of the class with the specified archive and data. + /// + /// The table of contents of the MEG. + /// The raw bytes of the whole MEG archive. + internal InMemoryMeg(IMegArchive archive, byte[] megData) + { + Archive = archive ?? throw new ArgumentNullException(nameof(archive)); + _megData = megData ?? throw new ArgumentNullException(nameof(megData)); + } + + /// + public MegEntryStream GetData(MegDataEntry entry) + { + if (entry is null) + throw new ArgumentNullException(nameof(entry)); + if (!Archive.Contains(entry)) + throw new EntryNotInMegException(this, entry); + if (entry.Encrypted) + throw new NotImplementedException("Encrypted archives are currently not supported"); + + if (entry.Location.Size == 0) + return MegEntryStream.CreateEmptyStream(entry.Path); + + var stream = new MemoryStream(_megData, writable: false); + return new MegEntryStream(entry.Path, stream, entry.Location.Offset, entry.Location.Size); + } +} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/MegDataEntryBuilderInfo.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/MegDataEntryBuilderInfo.cs index e0ac2b583..2309d8f23 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/MegDataEntryBuilderInfo.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/MegDataEntryBuilderInfo.cs @@ -14,12 +14,12 @@ namespace PG.StarWarsGame.Files.MEG.Data; /// -/// Represents a container with information for of an MEG entry used for building .MEG files. +/// Represents a container with information for an MEG entry used for building .MEG files. /// public sealed class MegDataEntryBuilderInfo { /// - /// The actual location of a MEG data entry. + /// Gets the actual location of a MEG data entry. /// public MegDataEntryOriginInfo OriginInfo { get; } @@ -29,7 +29,7 @@ public sealed class MegDataEntryBuilderInfo public string EntryPath { get; } /// - /// Gets whether the data entry shall be encrypted or not within the constructed MEG file. + /// Gets a value that indicates whether the data entry is encrypted within the constructed MEG file. /// public bool Encrypted { get; } @@ -66,7 +66,7 @@ internal MegDataEntryBuilderInfo(MegDataEntryOriginInfo originInfo, string? over /// -or- /// does not exist in . /// - /// or is . + /// or is . public static MegDataEntryBuilderInfo FromEntry(IMegFile megFile, MegDataEntry dataEntry, string? overrideEntryPath = null, bool? overrideEncrypted = null) { if (megFile == null) @@ -111,7 +111,7 @@ public static MegDataEntryBuilderInfo FromEntryReference(MegDataEntryLocationRef /// The path of the entry within the MEG archive. /// When not , the specified path will be used; otherwise the full path of path will be used. /// - /// Sets whether the data shall be encrypted or not. Default is . + /// to encrypt the data; otherwise, . The default is . /// or is . /// /// does not exist. @@ -135,7 +135,7 @@ public static MegDataEntryBuilderInfo FromFile(IFileInfo file, string entryPath, /// /// The read-only span containing the entry bytes. /// The path of the entry within the MEG archive. - /// Sets whether the data shall be encrypted or not. Default is . + /// to encrypt the data; otherwise, . The default is . /// is . /// is empty. public static MegDataEntryBuilderInfo FromBytes(ReadOnlySpan bytes, string entryPath, bool encrypt = false) @@ -150,7 +150,7 @@ public static MegDataEntryBuilderInfo FromBytes(ReadOnlySpan bytes, string /// /// The buffer containing the entry bytes. /// The path of the entry within the MEG archive. - /// Sets whether the data shall be encrypted or not. Default is . + /// to encrypt the data; otherwise, . The default is . /// or is . /// is empty. public static MegDataEntryBuilderInfo FromBytes(byte[] bytes, string entryPath, bool encrypt = false) diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegEncryptionData.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/MegEncryptionData.cs similarity index 91% rename from PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegEncryptionData.cs rename to PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/MegEncryptionData.cs index f6472206a..a54d0db11 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegEncryptionData.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/MegEncryptionData.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; using AnakinRaW.CommonUtilities; -namespace PG.StarWarsGame.Files.MEG.Files; +namespace PG.StarWarsGame.Files.MEG.Data; /// /// Stores an AES-CBC 128-bit encryption key and initialization vector. @@ -24,7 +24,7 @@ public byte[] IV get { if (_ivValue is null) - throw new ObjectDisposedException(GetType().Name); + throw new ObjectDisposedException(nameof(MegEncryptionData)); return (byte[])_ivValue.Clone(); } } @@ -38,7 +38,7 @@ public byte[] Key get { if (_keyValue is null) - throw new ObjectDisposedException(GetType().Name); + throw new ObjectDisposedException(nameof(MegEncryptionData)); return (byte[])_keyValue.Clone(); } } @@ -83,7 +83,7 @@ protected override void DisposeResources() internal MegEncryptionData Copy() { if (_keyValue is null || _ivValue is null) - throw new ObjectDisposedException(GetType().Name); + throw new ObjectDisposedException(nameof(MegEncryptionData)); return new MegEncryptionData(_keyValue, _ivValue); } diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFileVersion.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/MegVersion.cs similarity index 92% rename from PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFileVersion.cs rename to PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/MegVersion.cs index f927bca45..0ac15d134 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFileVersion.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Data/MegVersion.cs @@ -1,13 +1,13 @@ // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -namespace PG.StarWarsGame.Files.MEG.Files; +namespace PG.StarWarsGame.Files.MEG.Data; /// /// Represents the available file versions of a PG .MEG archive as defined in the /// .MEG file specification. /// -public enum MegFileVersion +public enum MegVersion { /// /// .MEG file version 1. diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Exceptions/EntryNotInMegException.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Exceptions/EntryNotInMegException.cs index f85a792cc..c41014951 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Exceptions/EntryNotInMegException.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Exceptions/EntryNotInMegException.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. using System; +using PG.StarWarsGame.Files.MEG.Data; +using PG.StarWarsGame.Files.MEG.Data.Entries; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; namespace PG.StarWarsGame.Files.MEG; @@ -22,8 +24,18 @@ public sealed class EntryNotInMegException : Exception /// /// The non-existing data entry location. internal EntryNotInMegException(MegDataEntryLocationReference locationReference) + : this(locationReference.Source, locationReference.DataEntry) { - _entry = locationReference.DataEntry.Path; - _megFile = locationReference.MegFile.FilePath; + } + + /// + /// Initializes a new instance of the class for an entry that is not contained in the given MEG source. + /// + /// The MEG source that does not contain . + /// The data entry that is not contained in . + internal EntryNotInMegException(IMegDataSource source, MegDataEntry entry) + { + _entry = entry.Path; + _megFile = MegDataEntryLocationReference.DescribeSource(source); } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Exceptions/MegSizeException.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Exceptions/MegSizeException.cs index 2ff0f336a..4db8fc610 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Exceptions/MegSizeException.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Exceptions/MegSizeException.cs @@ -13,7 +13,8 @@ public class MegSizeException : Exception { /// /// Initializes a new instance of the class with a specified error message. - /// The message that describes the error. + /// + /// The message that describes the error. public MegSizeException(string? message) : base(message) { } diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/IMegFile.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/IMegFile.cs index 77ff05e75..87df0e458 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/IMegFile.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/IMegFile.cs @@ -1,19 +1,15 @@ // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; namespace PG.StarWarsGame.Files.MEG.Files; /// -/// The provided holder for a Petroglyph -/// .MEG file. -/// *.MEG or Mega files are a proprietary archive type bundling files together in a RAM friendly way. +/// Represents a Petroglyph Mega File. /// -public interface IMegFile : IPetroglyphFileHolder -{ - /// - /// Gets the archive model of this MEG file. - /// - IMegArchive Archive { get; } -} \ No newline at end of file +/// +/// Mega files are an archive type bundling files together in a RAM friendly way. +/// +public interface IMegFile : IPetroglyphFileHolder, IMegDataSource; diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFile.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFile.cs index 2f029d159..77bdeae5b 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFile.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFile.cs @@ -1,23 +1,28 @@ // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. -using System; using PG.StarWarsGame.Files.MEG.Data.Archives; +using PG.StarWarsGame.Files.MEG.Data.Entries; +using PG.StarWarsGame.Files.MEG.Utilities; +using System; +using System.Diagnostics; +using System.IO; namespace PG.StarWarsGame.Files.MEG.Files; -/// /// -/// This class does not hold the actual data of the files packaged in a *.MEG file, -/// but all necessary meta-information to extract a requested file on-demand. +/// This class does not hold the actual data of the files packaged in a *.MEG file, +/// but all necessary meta-information to extract a requested file on-demand. /// +/// +[DebuggerDisplay("{FilePath} ({Archive.Count})")] internal sealed class MegFile : PetroglyphFileHolder, IMegFile { /// public IMegArchive Archive => Content; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// It is safe to dispose the after an instance of this class has been created. @@ -29,4 +34,26 @@ public MegFile(IMegArchive model, MegFileInformation fileInformation, IServicePr base(model, fileInformation, serviceProvider) { } -} \ No newline at end of file + + /// + public MegEntryStream GetData(MegDataEntry entry) + { + if (entry is null) + throw new ArgumentNullException(nameof(entry)); + if (!Archive.Contains(entry)) + throw new EntryNotInMegException(this, entry); + if (entry.Encrypted) + throw new NotImplementedException("Encrypted archives are currently not supported"); + + if (!FileSystem.File.Exists(FilePath)) + throw new FileNotFoundException($"MEG file '{FilePath}' does not exist", FilePath); + + // Cause MIKE.NL's tool uses the offset megFile[megSize + 1] for empty Entries we would cause an ArgumentOutOfRangeException + // when trying to access this index on a real file. Therefore, we return the Null stream. + if (entry.Location.Size == 0) + return MegEntryStream.CreateEmptyStream(entry.Path); + + var megFileStream = FileSystem.FileStream.New(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + return new MegEntryStream(entry.Path, megFileStream, entry.Location.Offset, entry.Location.Size); + } +} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFileInformation.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFileInformation.cs index b298b1ab3..aa2a96b23 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFileInformation.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Files/MegFileInformation.cs @@ -1,6 +1,7 @@ // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. +using PG.StarWarsGame.Files.MEG.Data; using System; using System.Diagnostics.CodeAnalysis; @@ -15,12 +16,12 @@ namespace PG.StarWarsGame.Files.MEG.Files; public sealed record MegFileInformation : PetroglyphFileInformation { /// - /// Gets the MEG file version of the MEG file. + /// Gets the MEG version of the MEG file. /// - public MegFileVersion FileVersion { get; } + public MegVersion Version { get; } /// - /// Gets a value indicating whether an is encrypted. + /// Gets a value that indicates whether the is encrypted. /// [MemberNotNullWhen(true, nameof(EncryptionData))] public bool HasEncryption => EncryptionData is not null; @@ -34,19 +35,19 @@ public sealed record MegFileInformation : PetroglyphFileInformation /// Initializes a new instance of the class. /// /// The file path of the MEG file. - /// The file version of the MEG file. - /// The encryption data of MEG file or if the MEG file is not encrypted. - /// is null. + /// The MEG version of the file. + /// The encryption data of the MEG file, or if the MEG file is not encrypted. + /// is . /// is empty. /// - /// is not but is not . + /// is not but is not . /// [SetsRequiredMembers] - public MegFileInformation(string path, MegFileVersion fileVersion, MegEncryptionData? encryptionData = null) : base(path) + public MegFileInformation(string path, MegVersion version, MegEncryptionData? encryptionData = null) : base(path) { - if (encryptionData is not null && fileVersion != MegFileVersion.V3) - throw new ArgumentException("Encrypted MEG files are required to be version V3.", nameof(fileVersion)); - FileVersion = fileVersion; + if (encryptionData is not null && version != MegVersion.V3) + throw new ArgumentException("Encrypted MEG files are required to be version V3.", nameof(version)); + Version = version; EncryptionData = encryptionData; } @@ -55,7 +56,7 @@ public MegFileInformation(string path, MegFileVersion fileVersion, MegEncryption [SetsRequiredMembers] private MegFileInformation(MegFileInformation original) : base(original) { - FileVersion = original.FileVersion; + Version = original.Version; EncryptionData = original.EncryptionData?.Copy(); } #pragma warning restore IDE0051 diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/MegServiceContribution.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/MegServiceContribution.cs index 2ecd172be..03c6bcff1 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/MegServiceContribution.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/MegServiceContribution.cs @@ -17,14 +17,13 @@ public static class MegServiceContribution /// /// Adds all necessary services provided by this library to the specified . /// - /// The to add services to. + /// The collection to add services to. public static void SupportMEG(this IServiceCollection serviceCollection) { - serviceCollection.AddSingleton(sp => new MegFileService(sp)); + serviceCollection.AddSingleton(sp => new MegService(sp)); serviceCollection.AddSingleton(sp => new MegFileExtractor(sp)); serviceCollection.AddSingleton(sp => new MegBinaryServiceFactory(sp)); serviceCollection.AddSingleton(sp => new MegVersionIdentifier(sp)); - serviceCollection.AddSingleton(sp => new MegDataStreamFactory(sp)); serviceCollection.AddSingleton(_ => new VirtualMegArchiveBuilder()); serviceCollection.AddSingleton(sp => new PetroglyphRelativeDataEntryPathResolver(sp)); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.csproj b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.csproj index 917fce0b1..4f21ca434 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.csproj +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.csproj @@ -22,14 +22,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive
- + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Builders/EmpireAtWarMegBuilder.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Builders/EmpireAtWarMegBuilder.cs index a9eebc1ef..21e479f4b 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Builders/EmpireAtWarMegBuilder.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Builders/EmpireAtWarMegBuilder.cs @@ -18,21 +18,17 @@ public sealed class EmpireAtWarMegBuilder : PetroglyphGameMegBuilder /// public override IMegDataEntryPathNormalizer DataEntryPathNormalizer { get; } = new EmpireAtWarMegDataEntryPathNormalizer(); - /// - /// Gets the data entry validator to validate MEG data entries to be compliant to Empire at War - /// + /// public override IMegDataEntryValidator DataEntryValidator { get; } = new EmpireAtWarMegDataEntryValidator(); - /// - /// Gets the validator to validate whether an is compliant to Empire at War - /// + /// public override IMegFileInformationValidator MegFileInformationValidator { get; } + /// /// - /// 2GB (2^31 - 1 bytes), which is the safe limit for Petroglyph's games + /// The maximum file size, in bytes, which is 2GB (2^31 - 1 bytes), the safe limit for Petroglyph's games /// Star Wars: Empire at War and its extension Empire at War: Forces of Corruption. /// - /// public override uint MaxMegFileSize { get; } = MaxMegSizeProvider.GetMegMaxSize(MaxMegSizeMode.EawFoc).MaxFileSize; /// @@ -44,7 +40,7 @@ public sealed class EmpireAtWarMegBuilder : PetroglyphGameMegBuilder /// The path for this . /// The service provider. /// or is . - /// is empty. + /// is empty. public EmpireAtWarMegBuilder(string baseDirectory, IServiceProvider services) : base(baseDirectory, services) { MegFileInformationValidator = new EmpireAtWarMegFileInformationValidator(services); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Builders/PetroglyphGameMegBuilder.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Builders/PetroglyphGameMegBuilder.cs index b01d6c0b6..e0be38b5d 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Builders/PetroglyphGameMegBuilder.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Builders/PetroglyphGameMegBuilder.cs @@ -68,7 +68,7 @@ public abstract class PetroglyphGameMegBuilder : MegBuilderBase /// The path for this . /// The service provider. /// or is . - /// is empty. + /// is empty. protected PetroglyphGameMegBuilder(string baseDirectory, IServiceProvider services) : base(services) { ThrowHelper.ThrowIfNullOrEmpty(baseDirectory); @@ -78,9 +78,10 @@ protected PetroglyphGameMegBuilder(string baseDirectory, IServiceProvider servic } /// - /// Returns a relative path from a path and the of the . - /// Returns if is invalid or not a part of . - ///
+ /// Resolves a relative entry path from the specified path and the of the . + ///
+ /// + /// The returned path is neither fully normalized nor validated by the rules of the . ///
/// For example: ///
@@ -89,10 +90,11 @@ protected PetroglyphGameMegBuilder(string baseDirectory, IServiceProvider servic /// "/gameBasePath/xml/file.xml" --> "xml/file.xml" /// "/NOTgamePath/xml/file.xml" --> null /// "../xml/file.xml" --> null - ///
- /// The returned path is neither fully normalized nor validated by the rules of the . + /// /// The path to get the relative path from. - /// The resolved, relative entry path. + /// + /// The resolved, relative entry path, or if is invalid or not a part of . + /// public string? ResolveEntryPath(string? path) { return _pathResolver.ResolvePath(path, BaseDirectory); diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/IMegBuilder.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/IMegBuilder.cs index 062d6a6d1..04ab88626 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/IMegBuilder.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/IMegBuilder.cs @@ -14,12 +14,12 @@ namespace PG.StarWarsGame.Files.MEG.Services.Builder; /// -/// Service to create MEG files from local files or other MEG data entries ensuring custom validation and normalization rules. +/// Represents a service that creates MEG files from local files or other MEG data entries, ensuring custom validation and normalization rules. /// public interface IMegBuilder : IFileBuilder, MegFileInformation> { /// - /// Gets a value indicating whether the normalizes a data entry's path before adding it. + /// Gets a value that indicates whether the normalizes a data entry's path before adding it. /// /// /// Path normalization is performed before encoding. @@ -27,7 +27,7 @@ public interface IMegBuilder : IFileBuilder - /// Gets a value indicating whether the overwrites an already existing data entry with the new version when trying to a data entry + /// Gets a value that indicates whether the overwrites an already existing data entry with the new version when trying to a data entry /// or does not add the new data entry. /// /// @@ -63,7 +63,7 @@ public interface IMegBuilder : IFileBuilder /// The local path to the file which get added as an data entry. /// The desired file path of the data entry inside the MEG archive. - /// Indicates whether the data entry shall be encrypted. + /// to encrypt the data entry; otherwise, . /// The result of this operation. /// or is . /// or is empty. @@ -93,7 +93,7 @@ MegDataEntryAddResult AddEntry(MegDataEntryLocationReference entryReference, str /// /// The read-only span containing the entry bytes. /// The desired file path of the data entry inside the MEG archive. - /// Indicates whether the data entry shall be encrypted. + /// to encrypt the data entry; otherwise, . /// The result of this operation. /// is . /// is empty. @@ -108,7 +108,7 @@ MegDataEntryAddResult AddEntry(MegDataEntryLocationReference entryReference, str /// /// The buffer containing the entry bytes. /// The desired file path of the data entry inside the MEG archive. - /// Indicates whether the data entry shall be encrypted. + /// to encrypt the data entry; otherwise, . /// The result of this operation. /// or is . /// is empty. @@ -142,7 +142,7 @@ MegDataEntryAddResult AddEntry(MegDataEntryLocationReference entryReference, str /// A function that generates the file path for each part based on its index (1-based). /// Not invoked if only a single file is created. /// - /// A value indicating whether to overwrite existing files. + /// to overwrite existing files; otherwise, . /// /// is . /// -or- @@ -163,9 +163,10 @@ MegDataEntryAddResult AddEntry(MegDataEntryLocationReference entryReference, str /// due to the size constraints of MEG files the builder produces. /// /// - /// The method will return at least value 1. + /// The method will return at least value 1. /// + /// The version of the MEG file, which determines the size constraints. /// The minimum number of MEG files required to accommodate the provided data entries. /// It is impossible to create MEG files because of the current state of the builder. - int GetMinRequiredMegFiles(MegFileVersion megVersion); + int GetMinRequiredMegFiles(MegVersion megVersion); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegBuilderBase.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegBuilderBase.cs index b1db398a9..5222981fe 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegBuilderBase.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegBuilderBase.cs @@ -43,25 +43,25 @@ public abstract class MegBuilderBase /// /// - /// by default. + /// The default is . /// public virtual bool OverwritesDuplicateEntries => true; /// /// - /// by default. + /// The file information validator. The default is a . /// public virtual IMegFileInformationValidator MegFileInformationValidator { get; } /// /// - /// by default. + /// The data entry validator. The default is a . /// public virtual IMegDataEntryValidator DataEntryValidator { get; } = new BinaryMegDataEntryValidator(); /// /// - /// , meaning no normalizer is specified. + /// The data entry path normalizer, or if no normalizer is specified. The default is . /// public virtual IMegDataEntryPathNormalizer? DataEntryPathNormalizer => null; @@ -69,7 +69,7 @@ public abstract class MegBuilderBase /// Gets the maximum allowed size, in bytes, for a MEG file created by this builder. /// /// - /// 4GB (2^32 - 1 bytes) by default + /// The maximum allowed size, in bytes, for a MEG file. The default is 4GB (2^32 - 1 bytes). /// public virtual uint MaxMegFileSize { get; } = MaxMegSizeProvider.GetMegMaxSize(MaxMegSizeMode.Binary).MaxFileSize; @@ -167,7 +167,7 @@ public void BuildMany(MegFileInformation initialFileInformation, Func entries) } /// - public int GetMinRequiredMegFiles(MegFileVersion megVersion) + public int GetMinRequiredMegFiles(MegVersion megVersion) { return SplitIntoMinRequiredParts(megVersion, DataEntries).Count; } @@ -225,7 +225,7 @@ public int GetMinRequiredMegFiles(MegFileVersion megVersion) /// It is impossible to split into multiple MEG files because of the current state of the builder. /// protected ICollection> SplitIntoMinRequiredParts( - MegFileVersion megVersion, + MegVersion megVersion, IEnumerable builderInfo) { var metadataSizeCalculator = Services.GetRequiredService() @@ -264,8 +264,8 @@ protected ICollection> SplitIntoMinRequired /// protected sealed override void BuildFileCore(FileSystemStream fileStream, MegFileInformation fileInformation, IReadOnlyCollection data) { - var megService = Services.GetRequiredService(); - megService.CreateMegArchive(fileStream, fileInformation.FileVersion, fileInformation.EncryptionData, data); + var megService = Services.GetRequiredService(); + megService.CreateMegArchive(fileStream, fileInformation.Version, fileInformation.EncryptionData, data); } /// diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegDataEntryAddResult.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegDataEntryAddResult.cs index ca84dd482..830f12dae 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegDataEntryAddResult.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegDataEntryAddResult.cs @@ -10,12 +10,12 @@ namespace PG.StarWarsGame.Files.MEG.Services.Builder; /// -/// Status information whether a file or data entry was added to an . +/// Represents the result of adding a file or data entry to an . /// public readonly struct MegDataEntryAddResult { /// - /// Gets whether the file or data entry was added or not. + /// Gets a value that indicates whether the file or data entry was added. /// [MemberNotNullWhen(true, nameof(AddedBuilderInfo))] public bool Added => Status == MegDataEntryAddStatus.Added && AddedBuilderInfo is not null; @@ -26,23 +26,23 @@ public readonly struct MegDataEntryAddResult public MegDataEntryAddStatus Status { get; } /// - /// Indicates whether a previous data entry was overwritten. + /// Gets a value that indicates whether a previous data entry was overwritten. /// [MemberNotNullWhen(true, nameof(OverwrittenBuilderInfo))] public bool WasOverwrite => OverwrittenBuilderInfo is not null; /// - /// The data entry info which was added or if no entry was added. + /// Gets the data entry information that was added, or if no entry was added. /// public MegDataEntryBuilderInfo? AddedBuilderInfo { get; } /// - /// The data entry info which was overwritten or if no data entry was overwritten. + /// Gets the data entry information that was overwritten, or if no data entry was overwritten. /// public MegDataEntryBuilderInfo? OverwrittenBuilderInfo { get; } /// - /// A user readable message why the entry was not added. if the entry was added successfully or no message was provided. + /// Gets a user-readable message why the entry was not added, or if the entry was added successfully or no message was provided. /// public string? Message { get; } diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegDataEntryAddStatus.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegDataEntryAddStatus.cs index 63e373ef8..fd6b2f483 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegDataEntryAddStatus.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/MegDataEntryAddStatus.cs @@ -4,7 +4,7 @@ namespace PG.StarWarsGame.Files.MEG.Services.Builder; /// -/// Status of adding a file or data entry to an . +/// Specifies the status of adding a file or data entry to an . /// public enum MegDataEntryAddStatus { diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/EmpireAtWarMegDataEntryPathNormalizer.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/EmpireAtWarMegDataEntryPathNormalizer.cs index a009b0382..0c685f088 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/EmpireAtWarMegDataEntryPathNormalizer.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/EmpireAtWarMegDataEntryPathNormalizer.cs @@ -20,8 +20,8 @@ public sealed class EmpireAtWarMegDataEntryPathNormalizer : PetroglyphMegDataEnt /// /// Normalizes the specified MEG data entry path the same way as the Empire at War Alamo engine normalizes meg entry paths. /// - /// The normalized entry path. /// The read-only span containing the entry's file path to normalize. + /// The normalized entry path. public override string Normalize(ReadOnlySpan entryPath) { if (entryPath.Length == 0) @@ -86,7 +86,7 @@ private static void AppendRosToSb(ReadOnlySpan value, StringBuilder sb) /// The number of characters written to the destination buffer after normalization. /// /// - /// Thrown if the destination buffer is not large enough to store the normalized path. + /// is not large enough to store the normalized path. /// protected override int Normalize(ReadOnlySpan entryPath, Span destination) { diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/IMegDataEntryPathNormalizer.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/IMegDataEntryPathNormalizer.cs index c624b7393..1477ca4f0 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/IMegDataEntryPathNormalizer.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/IMegDataEntryPathNormalizer.cs @@ -34,7 +34,7 @@ public interface IMegDataEntryPathNormalizer /// /// The read-only span containing the entry's file path to normalize. /// The span to write the normalized path into. - /// The number of chars written to are stored to this variable. + /// When this method returns, contains the number of characters written to . This parameter is treated as uninitialized. /// if the normalization was completed and copied to ; otherwise, . bool TryNormalize(ReadOnlySpan entryPath, Span destination, out int charsWritten); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/MegDataEntryPathNormalizerBase.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/MegDataEntryPathNormalizerBase.cs index f3b293262..f2845d00d 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/MegDataEntryPathNormalizerBase.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/MegDataEntryPathNormalizerBase.cs @@ -42,7 +42,7 @@ public bool TryNormalize(ReadOnlySpan entryPath, Span destination, o /// /// The read-only span containing the entry's file path to normalize. /// The span to write the normalized path into. - /// The number of chars written to are stored to this variable.. + /// The number of characters written to . /// is too short. protected abstract int Normalize(ReadOnlySpan entryPath, Span destination); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/PetroglyphMegDataEntryPathNormalizer.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/PetroglyphMegDataEntryPathNormalizer.cs index 4f30afe0f..83a11966a 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/PetroglyphMegDataEntryPathNormalizer.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Normalization/PetroglyphMegDataEntryPathNormalizer.cs @@ -27,8 +27,8 @@ public class PetroglyphMegDataEntryPathNormalizer : MegDataEntryPathNormalizerBa /// /// Normalizes the specified MEG data entry path according to Petroglyph game requirements. /// - /// The normalized entry path. /// The read-only span containing the entry's file path to normalize. + /// The normalized entry path. /// /// This method ensures that the path is converted to uppercase and uses Windows-style directory separators (backslash). /// @@ -48,7 +48,7 @@ public override string Normalize(ReadOnlySpan entryPath) /// The number of characters written to the destination buffer after normalization. /// /// - /// Thrown if the destination buffer is not large enough to store the normalized path. + /// is not large enough to store the normalized path. /// /// /// This method ensures that the path is converted to uppercase and uses Windows-style directory separators (backslash). diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/Entry/MegDataEntryValidationResult.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/Entry/MegDataEntryValidationResult.cs index 60f8d45b8..389e31b6b 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/Entry/MegDataEntryValidationResult.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/Entry/MegDataEntryValidationResult.cs @@ -32,7 +32,7 @@ public readonly struct MegDataEntryValidationResult public MegDataEntryValidationStatus Status { get; } /// - /// Gets a value whether the validation was successful. + /// Gets a value that indicates whether the validation was successful. /// public bool IsValid => Status == MegDataEntryValidationStatus.Valid; diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidator.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidator.cs index 585a8e6f3..6423c952d 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidator.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/BinaryMegFileInformationValidator.cs @@ -31,8 +31,8 @@ public class BinaryMegFileInformationValidator : IMegFileInformationValidator /// that are validated by this implementation. /// For , this includes all versions /// - protected virtual IReadOnlyCollection SupportedVersions { get; } - = [MegFileVersion.V1, MegFileVersion.V2, MegFileVersion.V3]; + protected virtual IReadOnlyCollection SupportedVersions { get; } + = [MegVersion.V1, MegVersion.V2, MegVersion.V3]; /// /// Gets the maximum allowed size for a MEG file in bytes, as defined by the MEG specification. @@ -74,8 +74,8 @@ public MegFileInfoValidationResult Validate(MegFileInformation fileInformation, if (dataEntries == null) throw new ArgumentNullException(nameof(dataEntries)); - if (!SupportedVersions.Contains(fileInformation.FileVersion)) - return new MegFileInfoValidationResult(false, $"MEG version {fileInformation.FileVersion} is currently not supported."); + if (!SupportedVersions.Contains(fileInformation.Version)) + return new MegFileInfoValidationResult(false, $"MEG version {fileInformation.Version} is currently not supported."); var isEncrypted = dataEntries.Any(e => e.Encrypted); var hasEncryptionData = fileInformation.HasEncryption; @@ -90,11 +90,11 @@ public MegFileInfoValidationResult Validate(MegFileInformation fileInformation, try { sizeCalculator = ServiceProvider.GetRequiredService() - .GetMegSizeCalculator(fileInformation.FileVersion); + .GetMegSizeCalculator(fileInformation.Version); } catch (NotImplementedException) { - return new MegFileInfoValidationResult(false, $"MEG version {fileInformation.FileVersion} is currently not supported."); + return new MegFileInfoValidationResult(false, $"MEG version {fileInformation.Version} is currently not supported."); } try diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/EmpireAtWarMegFileInformationValidator.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/EmpireAtWarMegFileInformationValidator.cs index fb9f75845..ff33074bf 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/EmpireAtWarMegFileInformationValidator.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/EmpireAtWarMegFileInformationValidator.cs @@ -16,26 +16,26 @@ namespace PG.StarWarsGame.Files.MEG.Services.Builder.Validation; /// -/// Validates a MEG file information whether it is compliant to a Petroglyph Star Wars game. +/// Validates whether a MEG file information is compliant to a Petroglyph Star Wars game. /// public sealed class EmpireAtWarMegFileInformationValidator : BinaryMegFileInformationValidator { // The game arbitrary varies between 260 and 256, so we chose the larger value here. Mind that the value is 260 - 1, // because we need to reserve one byte for the zero-terminator '\0'. /// - /// The max number of characters allowed in a PG game for file paths. + /// Represents the maximum number of characters allowed in a Petroglyph game for file paths. /// public const int PetroglyphMaxFilePathLength = 259; /// - /// Gets the collection of supported values for the validator. + /// Gets the collection of supported values for the validator. /// /// /// A read-only collection containing the supported versions of the .MEG file format /// that are validated by this implementation. - /// For , this includes only . + /// For , this includes only . /// - protected override IReadOnlyCollection SupportedVersions { get; } = [MegFileVersion.V1]; + protected override IReadOnlyCollection SupportedVersions { get; } = [MegVersion.V1]; private readonly IFileSystem _fileSystem; diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/MegFileInfoValidationResult.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/MegFileInfoValidationResult.cs index dd4b7cc03..b522adc64 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/MegFileInfoValidationResult.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/Validation/FileInfo/MegFileInfoValidationResult.cs @@ -16,14 +16,14 @@ public readonly struct MegFileInfoValidationResult public string? FailReason { get; } /// - /// Gets a value whether the validation was successful. + /// Gets a value that indicates whether the validation was successful. /// public bool IsValid { get; } /// /// Initializes a new instance of the struct. /// - /// A value indicating whether the validation result is valid. + /// if the validation result is valid; otherwise, . /// The reason for validation failure, or if the validation is successful. public MegFileInfoValidationResult(bool valid, string? failReason) { diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IMegFileExtractor.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IMegFileExtractor.cs index a29cb7254..bded7243e 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IMegFileExtractor.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IMegFileExtractor.cs @@ -5,13 +5,15 @@ using System.IO; using PG.StarWarsGame.Files.MEG.Data.Entries; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; -using PG.StarWarsGame.Files.MEG.Utilities; namespace PG.StarWarsGame.Files.MEG.Services; /// -/// Service for extracting file from a .MEG archive. +/// Represents a service for extracting files from a .MEG archive to the file system. /// +/// +/// To read an entry's data into a stream (without writing it to disk), use . +/// public interface IMegFileExtractor { /// @@ -27,7 +29,7 @@ public interface IMegFileExtractor /// /// If is , /// - /// result := + filename(). + /// result := rootPath + filename(dataEntry). /// ///
///
@@ -37,13 +39,13 @@ public interface IMegFileExtractor /// a) if is not rooted (see here) ///
/// - /// result := + path(). + /// result := rootPath + path(dataEntry). /// /// ///
/// b) if is rooted. /// - /// result := path() + /// result := path(dataEntry) /// /// ///
@@ -58,8 +60,8 @@ public interface IMegFileExtractor /// It's the consumers responsibility to prevent path traversals. /// /// The file to get the path from. - /// Base directory of the built file path. - /// option to preserve the directory hierarchy of the file name. + /// The base directory of the built file path. + /// to preserve the directory hierarchy of the file name; otherwise, . /// The absolute file path. /// or is . /// is empty or contains only whitespace. @@ -67,18 +69,6 @@ public interface IMegFileExtractor string GetAbsolutePath(IMegDataEntry dataEntry, string rootPath, bool preserveDirectoryHierarchy); - /// - /// Gets the data stream of the given . - /// - /// The data entry information. - /// A stream containing the files contents. - /// is . - /// has properties. - /// The data entry does not exist in the .MEG file. - /// The operation is not permitted by the operating system due to missing permissions. - MegEntryStream GetData(MegDataEntryLocationReference dataEntryLocation); - - /// /// Extracts a single file from a .MEG archive to a given location. /// @@ -92,8 +82,8 @@ public interface IMegFileExtractor /// /// The data entry information. /// The destination file path. - /// When set to existing files will be overwritten; otherwise the extraction will be skipped. - /// if the file was extracted. if and only if the extraction was skipped. + /// to overwrite existing files; otherwise, to skip the extraction. + /// if the file was extracted; otherwise, if the extraction was skipped. /// or is . /// is empty, contains only whitespace or is not a legal file path in general. /// has properties. diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IMegFileService.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IMegFileService.cs deleted file mode 100644 index 0eaab17b7..000000000 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IMegFileService.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.MEG.Data; -using PG.StarWarsGame.Files.MEG.Files; -using PG.StarWarsGame.Files.MEG.Services.Builder; - -namespace PG.StarWarsGame.Files.MEG.Services; - -/// -/// A service to load and create Petroglyph .MEG files -/// -public interface IMegFileService -{ - /// - /// Creates a MEG file from a collection of data entries and writes it to a specified file stream. - /// - /// - /// This is a low-level operation. It's recommended to use instead, as this provides data validation and normalization. - /// - /// Notes: - ///
- /// - Any MEG entry file path will be re-encoded to ASCII automatically. - ///
- /// - In the case references an encrypted MEG data entry, the entry will be decrypted first. - ///
- /// - The items of will be correctly sorted by this operation. - ///
- ///
- /// The destination file stream to write the MEG archive to. - /// The MEG file version to use. - /// Optional encryption data. - /// A collection of file references to be packed into the MEG archive. - /// or is . - /// The MEG file could not be created. - /// A data entry file was not found. - /// This library does not support creating the MEG archive from the specified arguments. - /// The MEG archive or its entries are exceeding the supported file size. - /// Attempted to create MEG archive which does not match the expected binary result. - void CreateMegArchive(FileSystemStream fileStream, MegFileVersion fileVersion, MegEncryptionData? encryptionData, IEnumerable builderInformation); - - /// - /// Loads a *.MEG file's metadata into a . - /// - /// The MEG file path. - /// The MEG file's metadata. - /// This library does not support the specified MEG archive. - /// The MEG archive or its entries are exceeding the supported file size. - /// is not a MEG archive. - /// is not found. - /// is . - /// is empty. - /// Attempts to load an encrypted MEG archive. - IMegFile Load(string filePath); - - /// - /// Loads a *.MEG metadata stream into a . - /// - /// The MEG file path. - /// The MEG file's metadata. - /// - /// - /// This library does not support the specified MEG archive. - /// - /// - /// is not readable or seekable. - /// - /// - /// The MEG archive or its entries are exceeding the supported file size. - /// is not a MEG archive. - /// is . - /// Attempts to load an encrypted MEG archive. - IMegFile Load(FileSystemStream stream); - - /// - /// Retrieves the from a .MEG file. - /// - /// The .MEG file. - /// Indicates whether the .MEG archive is encrypted or not. - /// The version of the .MEG archive. - /// The input stream was not recognized as a valid MEG archive. - /// is not found. - MegFileVersion GetMegFileVersion(string file, out bool encrypted); -} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IMegService.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IMegService.cs new file mode 100644 index 000000000..fe888a11b --- /dev/null +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IMegService.cs @@ -0,0 +1,194 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using PG.StarWarsGame.Files.Binary; +using PG.StarWarsGame.Files.MEG.Data; +using PG.StarWarsGame.Files.MEG.Files; +using PG.StarWarsGame.Files.MEG.Services.Builder; + +namespace PG.StarWarsGame.Files.MEG.Services; + +/// +/// Represents a service to load and create Petroglyph MEG archives. +/// +public interface IMegService +{ + /// + /// Creates a binary MEG archive from a collection of data entries and writes it to a specified stream. + /// + /// + /// This is a low-level operation. It's recommended to use instead, as this provides data validation and normalization. + /// + /// Notes: + ///
+ /// - Any MEG entry file path will be re-encoded to ASCII automatically. + ///
+ /// - In the case references an encrypted MEG data entry, the entry will be decrypted first. + ///
+ /// - The items of will be correctly sorted by this operation. + ///
+ ///
+ /// The destination stream to write the MEG archive to. + /// The MEG version to use. + /// Optional encryption data. + /// A collection of file references to be packed into the MEG archive. + /// or is . + /// The MEG could not be created. + /// A data entry file was not found. + /// This library does not support creating the MEG archive from the specified arguments. + /// The MEG archive or its entries are exceeding the supported size. + /// Attempted to create MEG archive which does not match the expected binary result. + void CreateMegArchive(Stream stream, MegVersion megVersion, MegEncryptionData? encryptionData, IEnumerable builderInformation); + + /// + /// Loads a MEG file into a . + /// + /// The MEG file path. + /// The loaded . + /// This library does not support the specified MEG archive. + /// The MEG archive or its entries are exceeding the supported file size. + /// is not a MEG archive. + /// is not found. + /// is . + /// is empty. + /// The archive is encrypted. + IMegFile LoadFile(string filePath); + + /// + /// Loads a MEG file from stream into a . + /// + /// The MEG file stream. + /// The loaded . + /// + /// + /// This library does not support the specified MEG archive. + /// + /// + /// OR + /// + /// + /// is not readable or seekable. + /// + /// + /// The MEG archive or its entries are exceeding the supported file size. + /// is not a MEG archive. + /// is . + /// The archive is encrypted. + IMegFile LoadFile(FileSystemStream stream); + + /// + /// Loads a MEG archive from a stream. + /// + /// + /// If is a file system stream the method returns an ; otherwise the + /// whole stream is copied into memory and the returned source is self-contained. The caller may dispose + /// immediately afterwards. + /// + /// The stream containing the MEG archive. + /// The loaded . + /// is . + /// + /// + /// This library does not support the specified MEG archive. + /// + /// + /// OR + /// + /// + /// The archive is encrypted. + /// + /// + /// The MEG archive or its entries are exceeding the supported file size. + /// is not a MEG archive. + IMegDataSource LoadArchive(Stream stream); + + /// + /// Loads a MEG archive from an in-memory byte buffer. + /// + /// + /// The buffer is copied, so that subsequent mutations of are not reflected. + /// + /// The bytes of the whole MEG archive. + /// The loaded . + /// is . + /// + /// + /// This library does not support the specified MEG archive. + /// + /// + /// OR + /// + /// + /// The archive is encrypted. + /// + /// + /// The MEG archive or its entries are exceeding the supported size. + /// is not a MEG archive. + IMegDataSource LoadArchive(byte[] data); + + /// + /// Loads a MEG archive from a read-only span of bytes. + /// + /// + /// The span is copied, so that subsequent mutations of the underlying memory are not reflected. + /// + /// The bytes of the whole MEG archive. + /// The loaded . + /// + /// + /// This library does not support the specified MEG archive. + /// + /// + /// OR + /// + /// + /// The archive is encrypted. + /// + /// + /// The MEG archive or its entries are exceeding the supported size. + /// is not a MEG archive. + IMegDataSource LoadArchive(ReadOnlySpan data); + + /// + /// Retrieves the from a MEG file. + /// + /// The MEG file path. + /// When this method returns, contains a value that indicates whether the MEG archive is encrypted. This parameter is treated as uninitialized. + /// The version of the MEG archive. + /// The input stream was not recognized as a valid MEG archive. + /// is not found. + MegVersion GetMegVersion(string file, out bool encrypted); + + /// + /// Retrieves the from a MEG archive stream. + /// + /// The stream containing the MEG archive. Read from its current position. + /// When this method returns, contains a value that indicates whether the MEG archive is encrypted. This parameter is treated as uninitialized. + /// The version of the MEG archive. + /// is . + /// The input stream was not recognized as a valid MEG archive. + MegVersion GetMegVersion(Stream stream, out bool encrypted); + + /// + /// Retrieves the from an in-memory MEG archive buffer. + /// + /// The bytes of the MEG archive. + /// When this method returns, contains a value that indicates whether the MEG archive is encrypted. This parameter is treated as uninitialized. + /// The version of the MEG archive. + /// is . + /// The input was not recognized as a valid MEG archive. + MegVersion GetMegVersion(byte[] data, out bool encrypted); + + /// + /// Retrieves the from a read-only span of MEG archive bytes. + /// + /// The bytes of the MEG archive. + /// When this method returns, contains a value that indicates whether the MEG archive is encrypted. This parameter is treated as uninitialized. + /// The version of the MEG archive. + /// The input was not recognized as a valid MEG archive. + MegVersion GetMegVersion(ReadOnlySpan data, out bool encrypted); +} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IVirtualMegArchiveBuilder.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IVirtualMegArchiveBuilder.cs index 977fcb66b..8bbe465a3 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IVirtualMegArchiveBuilder.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/IVirtualMegArchiveBuilder.cs @@ -1,10 +1,10 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System.Collections.Generic; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.Entries; -using PG.StarWarsGame.Files.MEG.Files; namespace PG.StarWarsGame.Files.MEG.Services; @@ -20,24 +20,26 @@ public interface IVirtualMegArchiveBuilder /// The resulting archive is correctly sorted as specified. /// /// The collection of data references. - /// When , entries with the same CRC32 checksum get replaced. + /// to replace entries with the same CRC32 checksum; otherwise, . /// The virtual MEG archive. - /// When a does not point to a real location. + /// A does not point to a real location. IVirtualMegArchive BuildFrom(IEnumerable fileEntries, bool replaceExisting); /// - /// Converts an into a virtual, in-memory archive. The archive does not contain duplicates. + /// Converts an into a virtual, MEG archive. + /// The archive does not contain duplicates. /// - /// The MEG file to convert. + /// The MEG to convert. /// The virtual MEG archive. - IVirtualMegArchive BuildFrom(IMegFile megFile); + IVirtualMegArchive BuildFrom(IMegDataSource meg); /// - /// Merges a list of physical MEG files into a virtual, in-memory archive. The archive does not contain duplicates. + /// Merges a collection of into a virtual MEG archive. + /// The archive does not contain duplicates. /// - /// The physical MEG files to merge into a virtual MEG archive. - /// When , entries from different files with the same CRC32 checksum get replaced. - /// Duplicate entries within the same MEG file are ignored, so that only the first entry is recognized. + /// The MEGs to merge into a virtual MEG archive. + /// to replace entries from different MEGs with the same CRC32 checksum; otherwise, . + /// Duplicate entries within the same MEG are ignored, so that only the first entry is recognized. /// The virtual MEG archive. - IVirtualMegArchive BuildFrom(IList megFiles, bool replaceExisting); -} \ No newline at end of file + IVirtualMegArchive BuildFrom(IEnumerable megs, bool replaceExisting); +} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/IMegDataStreamFactory.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/IMegDataStreamFactory.cs deleted file mode 100644 index ad5f2b724..000000000 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/IMegDataStreamFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -using System.IO; -using PG.StarWarsGame.Files.MEG.Data.EntryLocations; -using PG.StarWarsGame.Files.MEG.Utilities; - -namespace PG.StarWarsGame.Files.MEG.Services; - -internal interface IMegDataStreamFactory -{ - Stream GetStream(MegDataEntryOriginInfo originInfo); - - MegEntryStream GetStream(MegDataEntryLocationReference locationReference); -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegDataStreamFactory.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegDataStreamFactory.cs deleted file mode 100644 index c0f2decb0..000000000 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegDataStreamFactory.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -using System; -using System.IO; -using System.IO.Abstractions; -using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Services; -using PG.StarWarsGame.Files.MEG.Data.Entries; -using PG.StarWarsGame.Files.MEG.Data.EntryLocations; -using PG.StarWarsGame.Files.MEG.Utilities; - -namespace PG.StarWarsGame.Files.MEG.Services; - -internal sealed class MegDataStreamFactory(IServiceProvider serviceProvider) - : ServiceBase(serviceProvider), IMegDataStreamFactory -{ - public Stream GetStream(MegDataEntryOriginInfo originInfo) - { - if (originInfo == null) - throw new ArgumentNullException(nameof(originInfo)); - - if (originInfo.IsBytes) - return new MemoryStream(originInfo.Bytes, writable: false); - - if (originInfo.FileInfo is not null) - return originInfo.FileInfo.OpenRead(); - - return GetStream(originInfo.MegFileLocation!); - } - - public MegEntryStream GetStream(MegDataEntryLocationReference locationReference) - { - if (locationReference == null) - throw new ArgumentNullException(nameof(locationReference)); - - if (!locationReference.Exists) - throw new EntryNotInMegException(locationReference); - - if (locationReference.DataEntry.Encrypted) - { - throw new NotImplementedException("Encrypted archives are currently not supported"); - } - - return CreateStream(locationReference.MegFile.FilePath, locationReference.DataEntry); - } - - private MegEntryStream CreateStream(string megFilePath, MegDataEntry entry) - { - if (!FileSystem.File.Exists(megFilePath)) - throw new FileNotFoundException($"MEG file '{megFilePath}' does not exist", megFilePath); - - // Cause MIKE.NL's tool uses the offset megFile[megSize + 1] for empty Entries we would cause an ArgumentOutOfRangeException - // when trying to access this index on a real file. Therefore, we return the Null stream. - if (entry.Location.Size == 0) - return MegEntryStream.CreateEmptyStream(entry.Path); - - var fs = Services.GetRequiredService(); - - var megFileStream = fs.FileStream.New(megFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); - return new MegEntryStream(entry.Path, megFileStream, entry.Location.Offset, entry.Location.Size); - } -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegFileExtractor.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegFileExtractor.cs index 4a63aa869..6718c8dca 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegFileExtractor.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegFileExtractor.cs @@ -3,12 +3,10 @@ using System; using System.IO; -using Microsoft.Extensions.DependencyInjection; using PG.Commons.Services; using PG.StarWarsGame.Files.MEG.Data.Entries; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; using AnakinRaW.CommonUtilities; -using PG.StarWarsGame.Files.MEG.Utilities; namespace PG.StarWarsGame.Files.MEG.Services; @@ -47,15 +45,6 @@ public string GetAbsolutePath(IMegDataEntry dataEntry, string rootPath, bool pre return FileSystem.Path.GetFullPath(FileSystem.Path.Combine(absoluteRootPath, dataEntry.Path)); } - /// - public MegEntryStream GetData(MegDataEntryLocationReference dataEntryLocation) - { - if (dataEntryLocation is null) - throw new ArgumentNullException(nameof(dataEntryLocation)); - - return Services.GetRequiredService().GetStream(dataEntryLocation); - } - /// public bool ExtractEntry(MegDataEntryLocationReference dataEntryLocation, string filePath, bool overwrite) { @@ -77,7 +66,7 @@ public bool ExtractEntry(MegDataEntryLocationReference dataEntryLocation, string using var destinationStream = FileSystem.FileStream.New(fullFilePath, fileMode, FileAccess.Write, FileShare.None); - using var dataStream = Services.GetRequiredService().GetStream(dataEntryLocation); + using var dataStream = dataEntryLocation.Source.GetData(dataEntryLocation.DataEntry); dataStream.CopyTo(destinationStream); return true; diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegFileService.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegFileService.cs deleted file mode 100644 index d905ef8e6..000000000 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegFileService.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.IO.Abstractions; -using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Services; -using PG.StarWarsGame.Files.MEG.Binary; -using PG.StarWarsGame.Files.MEG.Binary.Metadata; -using PG.StarWarsGame.Files.MEG.Data; -using PG.StarWarsGame.Files.MEG.Files; -using AnakinRaW.CommonUtilities; - -namespace PG.StarWarsGame.Files.MEG.Services; - -/// -internal sealed class MegFileService(IServiceProvider services) : ServiceBase(services), IMegFileService -{ - private IMegBinaryServiceFactory BinaryServiceFactory { get; } = services.GetRequiredService(); - - public void CreateMegArchive( - FileSystemStream fileStream, - MegFileVersion fileVersion, - MegEncryptionData? encryptionData, - IEnumerable builderInformation) - { - if (fileStream == null) - throw new ArgumentNullException(nameof(fileStream)); - - if (builderInformation == null) - throw new ArgumentNullException(nameof(builderInformation)); - - var constructionArchive = BinaryServiceFactory.GetConstructionBuilder(fileVersion) - .BuildConstructingMegArchive(builderInformation); - - if (constructionArchive.Encrypted) - throw new NotImplementedException("Encrypted archives are currently not supported"); - - if (constructionArchive.Encrypted) - { - if (encryptionData is null) - throw new NotSupportedException("Creating an encrypted MEG archive requires encryption key."); - if (fileVersion == MegFileVersion.V3) - throw new NotSupportedException("Creating an encrypted MEG archive requires the MEG version to be V3."); - } - - - var metadata = BinaryServiceFactory.GetConverter(constructionArchive.MegVersion) - .ModelToBinary(constructionArchive.Archive); - - metadata.WriteTo(fileStream); - - long dataBytesWritten = metadata.Size; - - var streamFactory = Services.GetRequiredService(); - - foreach (var file in constructionArchive) - { - using var dataStream = streamFactory.GetStream(file.Location); - - // TODO: Test in encryption case - if (dataStream.Length != file.DataEntry.Location.Size) - throw new InvalidOperationException( - $"Actual data entry size '{dataStream.Length}' does not match expected value: {file.DataEntry.Location.Size}"); - - if (fileStream.Position != file.DataEntry.Location.Offset) - throw new InvalidOperationException( - $"Actual file position '{fileStream.Position}' does not match expected entry offset: {file.DataEntry.Location.Offset}"); - - dataStream.CopyTo(fileStream); - - dataBytesWritten += dataStream.Length; - } - - Debug.Assert(dataBytesWritten == constructionArchive.ExpectedFileSize); - } - - public IMegFile Load(string filePath) - { - ThrowHelper.ThrowIfNullOrEmpty(filePath); - var fullPath = FileSystem.Path.GetFullPath(filePath); - using var fs = FileSystem.FileStream.New(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); - return Load(fs); - } - - public IMegFile Load(FileSystemStream stream) - { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - - var startPosition = stream.Position; - var megVersion = GetMegFileVersion(stream, out var encrypted); - - if (encrypted) - throw new NotImplementedException("Encrypted archives are currently not supported"); - - using var megFileInfo = new MegFileInformation(FileSystem.Path.GetFullPath(stream.Name), megVersion); - - stream.Seek(startPosition, SeekOrigin.Begin); - - var megMetadata = Load(stream, megFileInfo); - - var converter = BinaryServiceFactory.GetConverter(megVersion); - var megArchive = converter.BinaryToModel(megMetadata); - return new MegFile(megArchive, megFileInfo, Services); - } - - private IMegFileMetadata Load(Stream megStream, MegFileInformation megFileInfo) - { - using var binaryReader = BinaryServiceFactory.GetReader(megFileInfo.FileVersion); - return binaryReader.ReadBinary(megStream); - } - - public MegFileVersion GetMegFileVersion(string file, out bool encrypted) - { - ThrowHelper.ThrowIfNullOrWhiteSpace(file); - - using var fs = FileSystem.FileStream.New(file, FileMode.Open, FileAccess.Read, FileShare.Read); - return GetMegFileVersion(fs, out encrypted); - } - - private MegFileVersion GetMegFileVersion(Stream stream, out bool encrypted) - { - return Services.GetRequiredService().GetMegFileVersion(stream, out encrypted); - } -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegService.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegService.cs new file mode 100644 index 000000000..28fa4e90f --- /dev/null +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/MegService.cs @@ -0,0 +1,216 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Services; +using PG.StarWarsGame.Files.MEG.Binary; +using PG.StarWarsGame.Files.MEG.Data; +using PG.StarWarsGame.Files.MEG.Data.Archives; +using PG.StarWarsGame.Files.MEG.Files; +using AnakinRaW.CommonUtilities; + +namespace PG.StarWarsGame.Files.MEG.Services; + +/// +internal sealed class MegService(IServiceProvider services) : ServiceBase(services), IMegService +{ + private const string LoadArchiveEncryptedMessage = "Loading an encrypted MEG archive via LoadArchive is not supported."; + + private IMegBinaryServiceFactory BinaryServiceFactory { get; } = services.GetRequiredService(); + + public void CreateMegArchive( + Stream stream, + MegVersion megVersion, + MegEncryptionData? encryptionData, + IEnumerable builderInformation) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + if (builderInformation == null) + throw new ArgumentNullException(nameof(builderInformation)); + + var constructionArchive = BinaryServiceFactory.GetConstructionBuilder(megVersion) + .BuildConstructingMegArchive(builderInformation); + + if (constructionArchive.Encrypted) + throw new NotImplementedException("Encrypted archives are currently not supported"); + + if (constructionArchive.Encrypted) + { + if (encryptionData is null) + throw new NotSupportedException("Creating an encrypted MEG archive requires encryption key."); + if (megVersion == MegVersion.V3) + throw new NotSupportedException("Creating an encrypted MEG archive requires the MEG version to be V3."); + } + + + var metadata = BinaryServiceFactory.GetConverter(constructionArchive.MegVersion) + .ModelToBinary(constructionArchive.Archive); + + metadata.WriteTo(stream); + + long dataBytesWritten = metadata.Size; + + foreach (var file in constructionArchive) + { + using var dataStream = file.Location.GetDataStream(); + + // TODO: Test in encryption case + if (dataStream.Length != file.DataEntry.Location.Size) + throw new InvalidOperationException( + $"Actual data entry size '{dataStream.Length}' does not match expected value: {file.DataEntry.Location.Size}"); + + if (stream.Position != file.DataEntry.Location.Offset) + throw new InvalidOperationException( + $"Actual file position '{stream.Position}' does not match expected entry offset: {file.DataEntry.Location.Offset}"); + + dataStream.CopyTo(stream); + + dataBytesWritten += dataStream.Length; + } + + Debug.Assert(dataBytesWritten == constructionArchive.ExpectedFileSize); + } + + public IMegFile LoadFile(string filePath) + { + ThrowHelper.ThrowIfNullOrEmpty(filePath); + var fullPath = FileSystem.Path.GetFullPath(filePath); + using var fs = FileSystem.FileStream.New(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); + return LoadFile(fs); + } + + public IMegFile LoadFile(FileSystemStream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + return LoadMegFromFile(stream, stream.Name); + } + + public IMegDataSource LoadArchive(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + if (TryGetFileName(stream, out var fileName)) + { + var (version, archive, encrypted) = ReadArchive(stream); + if (encrypted) + throw new NotSupportedException(LoadArchiveEncryptedMessage); + using var megFileInfo = new MegFileInformation(FileSystem.Path.GetFullPath(fileName), version); + return new MegFile(archive!, megFileInfo, Services); + } + + using var buffer = new MemoryStream(stream.CanSeek ? (int)(stream.Length - stream.Position) : 0); + stream.CopyTo(buffer); + buffer.Position = 0; + + var (_, memArchive, memEncrypted) = ReadArchive(buffer); + if (memEncrypted) + throw new NotSupportedException(LoadArchiveEncryptedMessage); + return new InMemoryMeg(memArchive!, buffer.ToArray()); + } + + public IMegDataSource LoadArchive(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + return LoadArchive(data.AsSpan()); + } + + public IMegDataSource LoadArchive(ReadOnlySpan data) + { + return LoadMegFromMemory(data); + } + + public MegVersion GetMegVersion(string file, out bool encrypted) + { + ThrowHelper.ThrowIfNullOrWhiteSpace(file); + + using var fs = FileSystem.FileStream.New(file, FileMode.Open, FileAccess.Read, FileShare.Read); + return GetMegVersion(fs, out encrypted); + } + + public MegVersion GetMegVersion(Stream stream, out bool encrypted) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + return Services.GetRequiredService().GetMegVersion(stream, out encrypted); + } + + public MegVersion GetMegVersion(byte[] data, out bool encrypted) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + using var stream = new MemoryStream(data, writable: false); + return GetMegVersion(stream, out encrypted); + } + + public MegVersion GetMegVersion(ReadOnlySpan data, out bool encrypted) + { + return GetMegVersion(data.ToArray(), out encrypted); + } + + private MegFile LoadMegFromFile(Stream stream, string name) + { + var (version, archive, encrypted) = ReadArchive(stream); + if (encrypted) + throw new NotImplementedException("Loading an encrypted MEG archive is not yet implemented."); + + using var megFileInfo = new MegFileInformation(FileSystem.Path.GetFullPath(name), version); + return new MegFile(archive!, megFileInfo, Services); + } + + private InMemoryMeg LoadMegFromMemory(ReadOnlySpan megData) + { + var copiedMegData = megData.ToArray(); + using var dataStream = new MemoryStream(copiedMegData, writable: false); + var (_, archive, encrypted) = ReadArchive(dataStream); + if (encrypted) + throw new NotSupportedException(LoadArchiveEncryptedMessage); + + return new InMemoryMeg(archive!, copiedMegData); + } + + // Reads the version and the archive model from a seekable stream positioned at the start of the MEG. + // Does not throw on encrypted input; the caller decides which exception is appropriate. + private (MegVersion Version, IMegArchive? Archive, bool Encrypted) ReadArchive(Stream stream) + { + var startPosition = stream.Position; + var megVersion = GetMegVersion(stream, out var encrypted); + + if (encrypted) + return (megVersion, null, true); + + stream.Seek(startPosition, SeekOrigin.Begin); + + using var binaryReader = BinaryServiceFactory.GetReader(megVersion); + var metadata = binaryReader.ReadBinary(stream); + + var archive = BinaryServiceFactory.GetConverter(megVersion).BinaryToModel(metadata); + return (megVersion, archive, false); + } + + private static bool TryGetFileName(Stream stream, [NotNullWhen(true)] out string? fileName) + { + // NB: We cannot use stream.TryGetFileName extension cause that would + // include MegDataStream which we do not support here. + fileName = stream switch + { + FileSystemStream fileSystemStream => fileSystemStream.Name, + FileStream fileStream => fileStream.Name, + _ => null + }; + return fileName is not null; + } +} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/VirtualMegArchiveBuilder.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/VirtualMegArchiveBuilder.cs index 99e987aed..6960104f1 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/VirtualMegArchiveBuilder.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Internal/VirtualMegArchiveBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; @@ -7,10 +7,10 @@ using PG.Commons.Data; using PG.Commons.Hashing; using PG.Commons.Utilities; +using PG.StarWarsGame.Files.MEG.Data; using PG.StarWarsGame.Files.MEG.Data.Archives; using PG.StarWarsGame.Files.MEG.Data.Entries; using PG.StarWarsGame.Files.MEG.Data.EntryLocations; -using PG.StarWarsGame.Files.MEG.Files; namespace PG.StarWarsGame.Files.MEG.Services; @@ -18,14 +18,14 @@ namespace PG.StarWarsGame.Files.MEG.Services; internal sealed class VirtualMegArchiveBuilder : IVirtualMegArchiveBuilder { /// - public IVirtualMegArchive BuildFrom(IMegFile megFile) + public IVirtualMegArchive BuildFrom(IMegDataSource meg) { - if (megFile == null) - throw new ArgumentNullException(nameof(megFile)); + if (meg == null) + throw new ArgumentNullException(nameof(meg)); - var entryReferences = megFile.Archive + var entryReferences = meg.Archive .Distinct(CrcBasedEqualityComparer.Instance) - .Select(entry => new MegDataEntryReference(new MegDataEntryLocationReference(megFile, entry))); + .Select(entry => new MegDataEntryReference(new MegDataEntryLocationReference(meg, entry))); return new VirtualMegArchive(Crc32Utilities.SortByCrc32(entryReferences)); } @@ -33,20 +33,20 @@ public IVirtualMegArchive BuildFrom(IMegFile megFile) /// public IVirtualMegArchive BuildFrom(IEnumerable fileEntries, bool replaceExisting) { - if (fileEntries == null) + if (fileEntries == null) throw new ArgumentNullException(nameof(fileEntries)); return BuildFrom(fileEntries, replaceExisting, true); } /// - public IVirtualMegArchive BuildFrom(IList megFiles, bool replaceExisting) + public IVirtualMegArchive BuildFrom(IEnumerable megs, bool replaceExisting) { - if (megFiles == null) - throw new ArgumentNullException(nameof(megFiles)); + if (megs == null) + throw new ArgumentNullException(nameof(megs)); - var entries = megFiles.SelectMany(f => f.Archive.Distinct(CrcBasedEqualityComparer.Instance) - .Select(entry => new MegDataEntryReference(new MegDataEntryLocationReference(f, entry)))); + var entries = megs.SelectMany(m => m.Archive.Distinct(CrcBasedEqualityComparer.Instance) + .Select(entry => new MegDataEntryReference(new MegDataEntryLocationReference(m, entry)))); return BuildFrom(entries, replaceExisting, false); } @@ -72,4 +72,4 @@ private static IVirtualMegArchive BuildFrom(IEnumerable f return new VirtualMegArchive(sortedEntries.Values); } -} \ No newline at end of file +} diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Utilities/MegEntryStream.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Utilities/MegEntryStream.cs index 746869ac3..71b928ef6 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Utilities/MegEntryStream.cs +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Utilities/MegEntryStream.cs @@ -9,7 +9,7 @@ namespace PG.StarWarsGame.Files.MEG.Utilities; /// -/// Represent a read-only, non-seekable file stream that points to a single data entry inside a MEG file. +/// Represents a read-only stream that exposes the bytes of a single data entry inside a MEG archive. /// public sealed class MegEntryStream : Stream, IMegFileDataStream { diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.Test/PG.StarWarsGame.Files.MTD.Test.csproj b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.Test/PG.StarWarsGame.Files.MTD.Test.csproj index f8c69bc94..12a85ad08 100644 --- a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.Test/PG.StarWarsGame.Files.MTD.Test.csproj +++ b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.Test/PG.StarWarsGame.Files.MTD.Test.csproj @@ -17,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.Test/Services/MtdFileServiceTest.cs b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.Test/Services/MtdFileServiceTest.cs deleted file mode 100644 index 47eb9b448..000000000 --- a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.Test/Services/MtdFileServiceTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using AnakinRaW.CommonUtilities.Testing; -using AnakinRaW.CommonUtilities.Testing.Extensions; -using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Hashing; -using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.MTD.Files; -using PG.StarWarsGame.Files.MTD.Services; -using PG.Testing; -using Testably.Abstractions.Testing; -using Xunit; - -namespace PG.StarWarsGame.Files.MTD.Test.Services; - -public class MtdFileServiceTest : CommonMtdTestBase -{ - private readonly MtdFileService _mtdFileService; - - public MtdFileServiceTest() - { - _mtdFileService = new MtdFileService(ServiceProvider); - } - - [Fact] - public void Load_ArgumentException_Throws() - { - Assert.Throws(() => _mtdFileService.Load("")); - Assert.Throws(() => _mtdFileService.Load((string)null!)); - Assert.Throws(() => _mtdFileService.Load((Stream)null!)); - } - - - [Fact] - public void Load_FileNotFound_Throws() - { - Assert.Throws(() => _mtdFileService.Load("test.mtd")); - } - - [Theory] - [MemberData(nameof(MtdTestData.InvalidMtdData), MemberType = typeof(MtdTestData))] - public void Load_CorruptedFile_Throws(byte[] data) - { - FileSystem.Initialize().WithFile("test.mtd").Which(m => m.HasBytesContent(data)); - - Assert.Throws(() => _mtdFileService.Load("test.mtd")); - Assert.Throws(() => _mtdFileService.Load(new TestMegDataStream("test.mtd", data))); - } - - [Theory] - [MemberData(nameof(MtdTestData.ValidMtdData), MemberType = typeof(MtdTestData))] - public void Load_ValidBinary(byte[] data, IList files) - { - FileSystem.Initialize().WithFile("test.mtd").Which(m => m.HasBytesContent(data)); - - CompareFileWithExpected(files, _mtdFileService.Load("test.mtd")); - CompareFileWithExpected(files, _mtdFileService.Load(new TestMegDataStream("test.mtd", data))); - } - - [Theory] - [MemberData(nameof(MtdTestData.ValidMtdData), MemberType = typeof(MtdTestData))] - public void Load_StreamStaysOpen(byte[] data, IList files) - { - FileSystem.Initialize().WithFile("test.mtd").Which(m => m.HasBytesContent(data)); - - using var fs = FileSystem.File.OpenRead("test.mtd"); - - CompareFileWithExpected(files, _mtdFileService.Load(fs)); - - // Resetting the position should not throw - fs.Position = 0; - } - - [Fact] - public void Load_FocMtd() - { - var focFile = TestingHelpers.GetEmbeddedResource(GetType(), "Files.MT_COMMANDBAR.MTD"); - Assert.DoesNotThrow(() => _mtdFileService.Load(new TestMegDataStream("MT_COMMANDBAR.MTD", focFile))); - } - - private void CompareFileWithExpected(IList expectedFiles, IMtdFile mtdFile) - { - var hashingService = ServiceProvider.GetRequiredService(); - for (var i = 0; i < mtdFile.Content.Count; i++) - { - var expected = expectedFiles[i]; - var crc = hashingService.GetCrc32(expected.ExpectedName, Encoding.ASCII); - - Assert.True(mtdFile.Content.Contains(crc)); - Assert.True(mtdFile.Content.TryGetEntry(crc, out var actual)); - expected.AsserEquals(actual); - } - } - - [Fact] - public void MTD_FileWithCollision() - { - var testStream = new TestMegDataStream("MT_COMMANDBAR.MTD", MtdTestData.MtdWithKnownCollision()); - var fileWithCollision = _mtdFileService.Load(testStream); - - var expectedCrc = new Crc32(3596410486); - - Assert.Equal(2, fileWithCollision.Content.Count); - Assert.True(fileWithCollision.Content.Contains(expectedCrc)); - Assert.Equal(2, fileWithCollision.Content.EntriesWithCrc(expectedCrc).Count); - } -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.Test/Services/MtdServiceTest.cs b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.Test/Services/MtdServiceTest.cs new file mode 100644 index 000000000..1a13500dc --- /dev/null +++ b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.Test/Services/MtdServiceTest.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Text; +using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Extensions; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Hashing; +using PG.StarWarsGame.Files.Binary; +using PG.StarWarsGame.Files.MTD.Data; +using PG.StarWarsGame.Files.MTD.Files; +using PG.StarWarsGame.Files.MTD.Services; +using PG.Testing; +using Testably.Abstractions.Testing; +using Xunit; + +namespace PG.StarWarsGame.Files.MTD.Test.Services; + +public class MtdServiceTest : CommonMtdTestBase +{ + private readonly IMtdService _mtdService; + + public MtdServiceTest() + { + _mtdService = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void LoadFile_ArgumentException_Throws() + { + Assert.Throws(() => _mtdService.LoadFile("")); + Assert.Throws(() => _mtdService.LoadFile((string)null!)); + Assert.Throws(() => _mtdService.LoadFile((FileSystemStream)null!)); + Assert.Throws(() => _mtdService.LoadModel((Stream)null!)); + Assert.Throws(() => _mtdService.LoadModel((byte[])null!)); + } + + + [Fact] + public void LoadFile_FileNotFound_Throws() + { + Assert.Throws(() => _mtdService.LoadFile("test.mtd")); + } + + [Theory] + [MemberData(nameof(MtdTestData.InvalidMtdData), MemberType = typeof(MtdTestData))] + public void LoadFile_CorruptedFile_Throws(byte[] data) + { + FileSystem.Initialize().WithFile("test.mtd").Which(m => m.HasBytesContent(data)); + + Assert.Throws(() => _mtdService.LoadFile("test.mtd")); + Assert.Throws(() => _mtdService.LoadModel(new TestMegDataStream("test.mtd", data))); + Assert.Throws(() => _mtdService.LoadModel(new MemoryStream(data))); + Assert.Throws(() => _mtdService.LoadModel(data)); + Assert.Throws(() => _mtdService.LoadModel(data.AsSpan())); + } + + [Theory] + [MemberData(nameof(MtdTestData.ValidMtdData), MemberType = typeof(MtdTestData))] + public void LoadFile_ValidBinary(byte[] data, IList files) + { + FileSystem.Initialize().WithFile("test.mtd").Which(m => m.HasBytesContent(data)); + + CompareFileWithExpected(files, _mtdService.LoadFile("test.mtd")); + + // The same data parses identically through the in-memory model overloads. + CompareDirectoryWithExpected(files, _mtdService.LoadModel(new TestMegDataStream("test.mtd", data))); + CompareDirectoryWithExpected(files, _mtdService.LoadModel(new MemoryStream(data))); + CompareDirectoryWithExpected(files, _mtdService.LoadModel(data)); + CompareDirectoryWithExpected(files, _mtdService.LoadModel(data.AsSpan())); + } + + [Theory] + [MemberData(nameof(MtdTestData.ValidMtdData), MemberType = typeof(MtdTestData))] + public void LoadFile_StreamStaysOpen(byte[] data, IList files) + { + FileSystem.Initialize().WithFile("test.mtd").Which(m => m.HasBytesContent(data)); + + using var fs = FileSystem.File.OpenRead("test.mtd"); + + CompareFileWithExpected(files, _mtdService.LoadFile(fs)); + + // Resetting the position should not throw + fs.Position = 0; + } + + [Fact] + public void LoadModel_FocMtd() + { + var focFile = TestingHelpers.GetEmbeddedResource(GetType(), "Files.MT_COMMANDBAR.MTD"); + Assert.DoesNotThrow(() => _mtdService.LoadModel(new TestMegDataStream("MT_COMMANDBAR.MTD", focFile))); + } + + [Theory] + [MemberData(nameof(MtdTestData.ValidMtdData), MemberType = typeof(MtdTestData))] + public void LoadModel_NonSeekableStream(byte[] data, IList files) + { + using var nonSeekable = new NonSeekableReadStream(data); + + CompareDirectoryWithExpected(files, _mtdService.LoadModel(nonSeekable)); + } + + private void CompareFileWithExpected(IList expectedFiles, IMtdFile mtdFile) + { + CompareDirectoryWithExpected(expectedFiles, mtdFile.Content); + } + + private void CompareDirectoryWithExpected(IList expectedFiles, IMegaTextureDirectory directory) + { + var hashingService = ServiceProvider.GetRequiredService(); + for (var i = 0; i < directory.Count; i++) + { + var expected = expectedFiles[i]; + var crc = hashingService.GetCrc32(expected.ExpectedName, Encoding.ASCII); + + Assert.True(directory.Contains(crc)); + Assert.True(directory.TryGetEntry(crc, out var actual)); + expected.AsserEquals(actual); + } + } + + [Fact] + public void MTD_FileWithCollision() + { + var testStream = new TestMegDataStream("MT_COMMANDBAR.MTD", MtdTestData.MtdWithKnownCollision()); + var directory = _mtdService.LoadModel(testStream); + + var expectedCrc = new Crc32(3596410486); + + Assert.Equal(2, directory.Count); + Assert.True(directory.Contains(expectedCrc)); + Assert.Equal(2, directory.EntriesWithCrc(expectedCrc).Count); + } +} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Binary/MtdFileConstants.cs b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Binary/MtdFileConstants.cs index ce09ab42f..710b0fa8e 100644 --- a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Binary/MtdFileConstants.cs +++ b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Binary/MtdFileConstants.cs @@ -6,7 +6,7 @@ namespace PG.StarWarsGame.Files.MTD.Binary; /// -/// Simple class wrapper around global defaults used for all MTD file definitions. +/// Provides global default values used for all MTD file definitions. /// public static class MtdFileConstants { diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Data/IMegaTextureDirectory.cs b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Data/IMegaTextureDirectory.cs index d2a610685..89d107ca2 100644 --- a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Data/IMegaTextureDirectory.cs +++ b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Data/IMegaTextureDirectory.cs @@ -21,7 +21,7 @@ public interface IMegaTextureDirectory : IReadOnlyCollection - /// Get the last data entry with the matching CRC32 checksum. + /// Gets the last data entry with the matching CRC32 checksum. ///
/// The CRC to match. /// The last entry in the with the specified checksum. @@ -33,9 +33,9 @@ public interface IMegaTextureDirectory : IReadOnlyCollection /// The checksum of the entry to get. /// - /// When this method returns, the entry associated with the specified key, if the key is found; + /// When this method returns, contains the entry associated with the specified checksum, if the checksum is found; /// otherwise, the default value for the type of the parameter. - /// This parameter is passed uninitialized. + /// This parameter is treated as uninitialized. /// if the contains an element with the specified checksum; otherwise, . bool TryGetEntry(Crc32 crc32, [NotNullWhen(true)] out MegaTextureFileIndex? entry); @@ -43,6 +43,6 @@ public interface IMegaTextureDirectory : IReadOnlyCollection /// The CRC to match. - /// List of matching data entries. + /// The matching data entries, or an empty list if the checksum is not found. ImmutableFrugalList EntriesWithCrc(Crc32 crc); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Data/MegaTextureFileIndex.cs b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Data/MegaTextureFileIndex.cs index ef6da9ea1..4744fcc10 100644 --- a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Data/MegaTextureFileIndex.cs +++ b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Data/MegaTextureFileIndex.cs @@ -27,7 +27,7 @@ public sealed class MegaTextureFileIndex : IEquatable, IHa public Rectangle Area { get; } /// - /// Gets a value indicating whether the entry has an alpha channel. + /// Gets a value that indicates whether the entry has an alpha channel. /// public bool HasAlpha { get; } @@ -37,12 +37,12 @@ public sealed class MegaTextureFileIndex : IEquatable, IHa public Crc32 Crc32 { get; } /// - /// Initializes a new instance of the class with the specified information. + /// Initializes a new instance of the class. /// /// The file name of the entry. /// The checksum of the file name. /// The area of the entry within the Mega Texture. - /// Information whether this entry has an alpha channel. + /// if the entry has an alpha channel; otherwise, . /// is longer than 63 characters. /// is empty. /// does contain non-ASCII characters. diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/MtdServiceContribution.cs b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/MtdServiceContribution.cs index 9ffa78845..46bd40beb 100644 --- a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/MtdServiceContribution.cs +++ b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/MtdServiceContribution.cs @@ -19,7 +19,7 @@ public static class MtdServiceContribution /// The to add services to. public static void SupportMTD(this IServiceCollection serviceCollection) { - serviceCollection.AddSingleton(sp => new MtdFileService(sp)); + serviceCollection.AddSingleton(sp => new MtdService(sp)); serviceCollection.AddSingleton(sp => new MtdBinaryConverter(sp)); serviceCollection.AddSingleton(sp => new MdtFileReader(sp)); } diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.csproj b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.csproj index 5b877d339..07ba7cdde 100644 --- a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.csproj +++ b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD.csproj @@ -17,7 +17,7 @@ snupkg - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/IMtdFileService.cs b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/IMtdFileService.cs deleted file mode 100644 index 5e2ad6510..000000000 --- a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/IMtdFileService.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -using System; -using System.IO; -using PG.StarWarsGame.Files.Binary; -using PG.StarWarsGame.Files.MTD.Files; - -namespace PG.StarWarsGame.Files.MTD.Services; - -/// -/// A service to load Petroglyph .MTD files -/// -public interface IMtdFileService -{ - /// - /// Loads a *.MTD file into a . - /// - /// The MTD file path. - /// A representation of the MTD file. - /// is not found. - /// is . - /// is empty. - /// is not a valid MTD file. - IMtdFile Load(string filePath); - - /// - /// Loads a *.MTD stream into a . - /// - /// The MTD file stream. - /// A representation of the MTD file. - /// is not readable or seekable. - /// is not a valid MTD file. - /// is . - IMtdFile Load(Stream stream); -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/IMtdService.cs b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/IMtdService.cs new file mode 100644 index 000000000..a786ef981 --- /dev/null +++ b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/IMtdService.cs @@ -0,0 +1,64 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.IO; +using System.IO.Abstractions; +using PG.StarWarsGame.Files.Binary; +using PG.StarWarsGame.Files.MTD.Data; +using PG.StarWarsGame.Files.MTD.Files; + +namespace PG.StarWarsGame.Files.MTD.Services; + +/// +/// A service to load Petroglyph Mega-Texture Directories. +/// +public interface IMtdService +{ + /// + /// Loads an MTD file into a . + /// + /// The MTD file path. + /// A representation of the MTD file. + /// is not found. + /// is . + /// is empty. + /// is not a valid MTD file. + IMtdFile LoadFile(string filePath); + + /// + /// Loads an MTD file stream into a . + /// + /// The MTD file stream. + /// A representation of the MTD file. + /// is not readable or seekable. + /// is not a valid MTD file. + /// is . + IMtdFile LoadFile(FileSystemStream fileStream); + + /// + /// Loads MTD data from a stream into a . + /// + /// The stream containing the MTD data. + /// A representation of the MTD data. + /// is not valid MTD data. + /// is . + IMegaTextureDirectory LoadModel(Stream stream); + + /// + /// Loads MTD data from an in-memory byte buffer into a . + /// + /// The MTD bytes. + /// A representation of the MTD data. + /// is not valid MTD data. + /// is . + IMegaTextureDirectory LoadModel(byte[] data); + + /// + /// Loads MTD data from a read-only span of bytes into a . + /// + /// The MTD bytes. + /// A representation of the MTD data. + /// is not valid MTD data. + IMegaTextureDirectory LoadModel(ReadOnlySpan data); +} diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/MtdFileService.cs b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/MtdFileService.cs deleted file mode 100644 index fcfcf663f..000000000 --- a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/MtdFileService.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -using System; -using System.IO; -using AnakinRaW.CommonUtilities; -using Microsoft.Extensions.DependencyInjection; -using PG.Commons.Services; -using PG.Commons.Utilities; -using PG.StarWarsGame.Files.MTD.Binary; -using PG.StarWarsGame.Files.MTD.Files; - -namespace PG.StarWarsGame.Files.MTD.Services; - -internal class MtdFileService(IServiceProvider serviceProvider) : ServiceBase(serviceProvider), IMtdFileService -{ - private readonly IMtdBinaryConverter _binaryConverter = serviceProvider.GetRequiredService(); - private readonly IMtdFileReader _fileReader = serviceProvider.GetRequiredService(); - - public IMtdFile Load(string filePath) - { - ThrowHelper.ThrowIfNullOrEmpty(filePath); - var fullPath = FileSystem.Path.GetFullPath(filePath); - using var fs = FileSystem.FileStream.New(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); - return Load(fs); - } - - public IMtdFile Load(Stream stream) - { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - - var binaryModel = _fileReader.ReadBinary(stream); - var model = _binaryConverter.BinaryToModel(binaryModel); - - var filePath = stream.GetFilePath(out var isInMeg); - - // If the .MTD file is not embedded in a MEG, we want the absolute path. - if (!isInMeg) - filePath = FileSystem.Path.GetFullPath(filePath); - - var megFileInfo = new MtdFileInformation { FilePath = filePath, IsInsideMeg = isInMeg}; - - return new MtdFile(model, megFileInfo, Services); - } -} \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/MtdService.cs b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/MtdService.cs new file mode 100644 index 000000000..e85294551 --- /dev/null +++ b/PG.StarWarsGame.Files.MTD/PG.StarWarsGame.Files.MTD/Services/MtdService.cs @@ -0,0 +1,74 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.IO; +using System.IO.Abstractions; +using AnakinRaW.CommonUtilities; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Services; +using PG.Commons.Utilities; +using PG.StarWarsGame.Files.MTD.Binary; +using PG.StarWarsGame.Files.MTD.Data; +using PG.StarWarsGame.Files.MTD.Files; + +namespace PG.StarWarsGame.Files.MTD.Services; + +internal class MtdService(IServiceProvider serviceProvider) : ServiceBase(serviceProvider), IMtdService +{ + private readonly IMtdBinaryConverter _binaryConverter = serviceProvider.GetRequiredService(); + private readonly IMtdFileReader _fileReader = serviceProvider.GetRequiredService(); + + public IMtdFile LoadFile(string filePath) + { + ThrowHelper.ThrowIfNullOrEmpty(filePath); + var fullPath = FileSystem.Path.GetFullPath(filePath); + using var fs = FileSystem.FileStream.New(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); + return LoadFile(fs); + } + + public IMtdFile LoadFile(FileSystemStream fileStream) + { + if (fileStream == null) + throw new ArgumentNullException(nameof(fileStream)); + + var model = ReadModel(fileStream); + + var filePath = fileStream.GetFilePath(out var isInMeg); + + // If the .MTD file is not embedded in a MEG, we want the absolute path. + if (!isInMeg) + filePath = FileSystem.Path.GetFullPath(filePath); + + var megFileInfo = new MtdFileInformation { FilePath = filePath, IsInsideMeg = isInMeg}; + + return new MtdFile(model, megFileInfo, Services); + } + + public IMegaTextureDirectory LoadModel(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + return ReadModel(stream); + } + + public IMegaTextureDirectory LoadModel(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + using var stream = new MemoryStream(data, writable: false); + return LoadModel(stream); + } + + public IMegaTextureDirectory LoadModel(ReadOnlySpan data) + { + using var stream = new MemoryStream(data.ToArray(), writable: false); + return LoadModel(stream); + } + + private IMegaTextureDirectory ReadModel(Stream stream) + { + var binaryModel = _fileReader.ReadBinary(stream); + return _binaryConverter.BinaryToModel(binaryModel); + } +} diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Testing/FileBuilderTestBase.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Testing/FileBuilderTestBase.cs index 3954daab7..fce219033 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Testing/FileBuilderTestBase.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Testing/FileBuilderTestBase.cs @@ -8,17 +8,56 @@ namespace PG.StarWarsGame.Files.Testing; +/// +/// Provides a shared set of tests for verifying the behavior of an implementation. +/// +/// The type of the file builder under test. +/// The type of the model that the builder produces output from. +/// The type of the file information that describes the builder's output. public abstract class FileBuilderTestBase : PGTestBase where TBuilder : IFileBuilder - where TModel : notnull + where TModel : notnull where TFileInfo : PetroglyphFileInformation { + /// + /// Gets the default name of the file that the builder writes during the tests. + /// + /// The default file name. protected virtual string DefaultFileName => "file.txt"; + /// + /// Gets a value that indicates whether the file information produced by the builder is always considered valid. + /// + /// + /// if the file information is always valid; otherwise, . The default is . + /// protected virtual bool FileInfoIsAlwaysValid => false; + + /// + /// Creates a new instance of the file builder under test. + /// + /// The newly created file builder. protected abstract TBuilder CreateBuilder(); + + /// + /// Creates the file information for the specified path. + /// + /// to create valid file information; otherwise, . + /// The path of the file the information describes. + /// The created file information. protected abstract TFileInfo CreateFileInfo(bool valid, string path); + + /// + /// Adds the specified data to the builder. + /// + /// The data to add to the builder. + /// The builder the data is added to. protected abstract void AddDataToBuilder(TModel data, TBuilder builder); + + /// + /// Creates a model and its expected serialized byte representation that together form valid builder input. + /// + /// A tuple containing the valid model and the bytes the builder is expected to produce from it. protected abstract (TModel Data, byte[] Bytes) CreateValidData(); [Fact] diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryBase.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryBase.cs index dac0146da..665629727 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryBase.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryBase.cs @@ -42,8 +42,8 @@ public int Size public abstract void GetBytes(Span bytes); /// - /// Calculates the size in bytes of this instance + /// Calculates the size, in bytes, of this instance. /// - /// The size in bytes. + /// The size, in bytes. protected abstract int GetSizeCore(); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryFile.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryFile.cs index d9484186d..b0be10089 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryFile.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryFile.cs @@ -9,7 +9,7 @@ namespace PG.StarWarsGame.Files.Binary; /// -/// Base class for a Petroglyph binary file. +/// Provides the base class for a Petroglyph binary file. /// public abstract class BinaryFile : BinaryBase, IBinaryFile { diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryTable.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryTable.cs index 677df2bd8..0d8abd0ef 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryTable.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/BinaryTable.cs @@ -28,7 +28,7 @@ public class BinaryTable : BinaryBase, IBinaryTable where T : IBinary public int Count => Items.Count; /// - /// Initializes a new instance of the that contains elements copied from the specified collection. + /// Initializes a new instance of the class that contains elements copied from the specified collection. /// /// The collection whose elements are copied to the new table. /// is . diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/File/IBinaryFileReader.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/File/IBinaryFileReader.cs index 6e699d28f..4a7007977 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/File/IBinaryFileReader.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/File/IBinaryFileReader.cs @@ -7,7 +7,7 @@ namespace PG.StarWarsGame.Files.Binary.File; /// -/// A reader that is capable of converting a stream of bytes into a binary model. +/// Represents a reader that is capable of converting a stream of bytes into a binary model. /// /// The type of the binary model. public interface IBinaryFileReader where TBinaryModel : IBinaryFile @@ -15,7 +15,7 @@ public interface IBinaryFileReader where TBinaryModel : IBinar /// /// Builds a binary file from a given byte array. /// - /// The binary data + /// The stream that contains the binary data. /// The converted binary file. /// is . /// cannot be converted to . diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinary.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinary.cs index 436f7ca7a..690cdc91e 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinary.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinary.cs @@ -23,6 +23,6 @@ public interface IBinary /// /// Fills the specified span with the bytes of the binary. /// - /// The span of bites to fill. + /// The span to fill with the bytes of the binary. void GetBytes(Span bytes); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinaryConverter.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinaryConverter.cs index c59750434..5cf63a3b9 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinaryConverter.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinaryConverter.cs @@ -7,7 +7,7 @@ namespace PG.StarWarsGame.Files.Binary; /// -/// A builder that is capable of converting a generic file model to its binary +/// Represents a builder that is capable of converting a generic file model to its binary /// representation and vice versa. /// /// The type of the binary model. diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinaryTable.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinaryTable.cs index af7a09b97..02b2515d0 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinaryTable.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/IBinaryTable.cs @@ -9,7 +9,7 @@ namespace PG.StarWarsGame.Files.Binary; // limited to *signed* int32 for indexing native list-like structures. /// -/// A read-only list-like table used by Petroglyph binary models to hold index-based metadata information. +/// Represents a read-only list-like table used by Petroglyph binary models to hold index-based metadata information. /// /// The type of the binary elements of the table. public interface IBinaryTable : IBinary, IReadOnlyList where T : IBinary; \ No newline at end of file diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/PetroglyphBinaryReader.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/PetroglyphBinaryReader.cs index b67d1a5d4..adffc5ac3 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/PetroglyphBinaryReader.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Binary/PetroglyphBinaryReader.cs @@ -17,7 +17,7 @@ namespace PG.StarWarsGame.Files.Binary; public sealed class PetroglyphBinaryReader : BinaryReader { /// - /// Initializes a new instance of the based on the specified stream and optionally leaves the stream open. + /// Initializes a new instance of the class based on the specified stream and optionally leaves the stream open. /// /// The input stream. /// to leave the stream open after the BinaryReader object is disposed; otherwise, . @@ -43,12 +43,12 @@ public PetroglyphBinaryReader(Stream input, bool leaveOpen) : base(input, Invali /// does not restore the file position after an unsuccessful read. /// /// The number of characters to read. - /// The encoding to produce the string. - /// When set to , the resulting string is truncated to the first found null-terminator ('\0'). Default is . - /// The string being read. - /// The number of bytes read, mismatches the expected number of bytes. + /// The encoding used to produce the string. + /// to truncate the resulting string at the first null-terminator ('\0'); otherwise, . The default is . + /// The string that was read. + /// The number of bytes read mismatches the expected number of bytes. /// An I/O error occurred. - /// is but the string read did not contain a null-terminator. + /// is but the string read did not contain a null-terminator. /// is not supported. public string ReadString(Encoding encoding, int numberOfChars, bool isZeroTerminated = false) { @@ -92,18 +92,18 @@ public string ReadString(Encoding encoding, int numberOfChars, bool isZeroTermin /// does not restore the file position after an unsuccessful read. /// /// The character span to write the string into. - /// The encoding to produce the string. + /// The encoding used to produce the string. /// The number of characters to read. - /// When set to , the resulting string is truncated to the first found null-terminator ('\0'). Default is . + /// to truncate the resulting string at the first null-terminator ('\0'); otherwise, . The default is . /// /// The total number of characters read into the buffer. /// This might be less than if is /// and the read string contained multiple zero-terminators. /// - /// The number of bytes read, mismatches the expected number of bytes. + /// The number of bytes read mismatches the expected number of bytes. /// does not have enough capacity to accommodate the resulting characters. /// An I/O error occurred. - /// is but the string read did not contain a null-terminator. + /// is but the string read did not contain a null-terminator. /// is not supported. public int ReadString(Span destination, Encoding encoding, int numberOfChars, bool isZeroTerminated = false) { diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/IPetroglyphFileHolder.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/IPetroglyphFileHolder.cs index 313b3b394..a5f2a2820 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/IPetroglyphFileHolder.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/IPetroglyphFileHolder.cs @@ -6,7 +6,7 @@ namespace PG.StarWarsGame.Files; /// -/// A wrapper around Petroglyph game files that holds the file's content in an accessible data structure as well as other file information. +/// Represents a wrapper around Petroglyph game files that holds the file's content in an accessible data structure as well as other file information. /// public interface IPetroglyphFileHolder : IDisposable { @@ -73,7 +73,7 @@ public interface IPetroglyphFileHolder : IDisposable } /// -/// A generic wrapper around Petroglyph game files that holds the file's content in an accessible data structure as well as other file information. +/// Represents a generic wrapper around Petroglyph game files that holds the file's content in an accessible data structure as well as other file information. /// /// The type of the content this file holds. /// The type of the file information. diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileHolder.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileHolder.cs index f3f40b2e3..f15e93546 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileHolder.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileHolder.cs @@ -51,22 +51,22 @@ public TFileInfo FileInformation public string FilePath { get; } /// - /// The logger of this service. + /// Gets the logger of this service. /// protected internal ILogger Logger { get; } /// - /// The file system implementation to be used. + /// Gets the file system implementation to be used. /// protected internal IFileSystem FileSystem { get; } /// - /// Returns the service provider. + /// Gets the service provider. /// protected internal IServiceProvider Services { get; } /// - /// Initializes a new instance of the class with the specified model and file information. + /// Initializes a new instance of the class with the specified model and file information. /// /// /// can be safely disposed after initialization without affecting . diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileInformation.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileInformation.cs index 0127db3a2..2988f627f 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileInformation.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileInformation.cs @@ -8,14 +8,14 @@ namespace PG.StarWarsGame.Files; /// -/// Contains file information about a . +/// Contains file information about a . /// public abstract record PetroglyphFileInformation : IDisposable { private readonly string _filePath; /// - /// Gets or sets the path of the file e.g, "c:/my/path/myfile.txt" + /// Gets or sets the path of the file, e.g, "c:/my/path/myfile.txt". /// /// /// The path is taken as-is and may be relative. @@ -71,7 +71,7 @@ public void Dispose() /// /// Disposes this instance and frees managed resources. /// - /// When set to managed resources get disposed. + /// to release managed resources; otherwise, . protected virtual void Dispose(bool disposing) { } diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphMegPackableFileInformation.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphMegPackableFileInformation.cs index 75bdd3922..291ab7c9a 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphMegPackableFileInformation.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphMegPackableFileInformation.cs @@ -7,20 +7,21 @@ namespace PG.StarWarsGame.Files; /// -/// Contains file information about a that may be packed inside a MEG archive. +/// Contains file information about a that may be packed inside a MEG archive. /// public abstract record PetroglyphMegPackableFileInformation : PetroglyphFileInformation { /// - /// Gets a value indicating whether the file is packed inside a MEG archive. + /// Gets a value that indicates whether the file is packed inside a MEG archive. /// + /// if the file is packed inside a MEG archive; otherwise, . public bool IsInsideMeg { get; init; } /// - /// Initializes a new instance of the class with a given path. + /// Initializes a new instance of the class with a given path. /// /// The fully qualified name of the new file, or the relative file name. - /// Information whether the file is inside a MEG archive. + /// if the file is packed inside a MEG archive; otherwise, . /// is . /// is empty. [SetsRequiredMembers] @@ -30,7 +31,7 @@ protected PetroglyphMegPackableFileInformation(string path, bool isInMeg) : base } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// protected PetroglyphMegPackableFileInformation() { diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Services/Builder/FileBuilderBase.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Services/Builder/FileBuilderBase.cs index b23ff186d..dba0eec16 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Services/Builder/FileBuilderBase.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Services/Builder/FileBuilderBase.cs @@ -10,7 +10,7 @@ namespace PG.StarWarsGame.Files.Services.Builder; /// -/// Base class for a service providing the fundamental implementations. +/// Provides the base class for a service with the fundamental implementations. /// /// The type of the file information data. /// The type of the data to build files from. @@ -87,8 +87,8 @@ public bool ValidateFileInformation(TFileInformation fileInformation) ///
/// The file information to validate. /// The data of this builder for additional context. - /// Stores an optional message into the variable to reason the validation result. - /// if is valid; otherwise, . + /// When this method returns, contains an optional message that explains the validation result, or if no reason is available. This parameter is treated as uninitialized. + /// if is valid; otherwise, . protected abstract bool ValidateFileInformationCore(TFileInformation fileInformation, TData builderData, out string? failedReason); } \ No newline at end of file diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Services/Builder/IFileBuilder.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Services/Builder/IFileBuilder.cs index 8fd1e2551..0f631241d 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Services/Builder/IFileBuilder.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/Services/Builder/IFileBuilder.cs @@ -7,7 +7,7 @@ namespace PG.StarWarsGame.Files.Services.Builder; /// -/// Service to write data into their file representation. +/// Defines a service that writes data into its file representation. /// /// The type of the file information data. /// The type of the data to build files from. @@ -24,7 +24,7 @@ public interface IFileBuilder : IDisposable /// Creates a file from the . /// /// The file parameters of the new file. - /// When set to an existing file of the same path will be overwritten; otherwise an is thrown if the file already exists. + /// to overwrite an existing file of the same path; otherwise, to throw an if the file already exists. /// is . /// is not valid. /// The file could not be created due to an IO error. @@ -33,8 +33,8 @@ public interface IFileBuilder : IDisposable /// /// Checks whether the specified file information is valid for the . /// - /// The file information to validate - /// if the passed file information are valid; otherwise, . + /// The file information to validate. + /// if the passed file information is valid; otherwise, . /// is . public bool ValidateFileInformation(TFileInformation fileInformation); } \ No newline at end of file diff --git a/PG.Testing/Hashing/ParseIntCrc32HashingService.cs b/PG.Testing/Hashing/ParseIntCrc32HashingService.cs index 2f406bc11..eb6c417f0 100644 --- a/PG.Testing/Hashing/ParseIntCrc32HashingService.cs +++ b/PG.Testing/Hashing/ParseIntCrc32HashingService.cs @@ -6,31 +6,39 @@ namespace PG.Testing.Hashing; +/// +/// Represents a test that derives the CRC32 value by parsing the input as an integer. +/// public class ParseIntCrc32HashingService : ICrc32HashingService { + /// public Crc32 GetCrc32(string value, Encoding encoding) { var intValue = int.Parse(value); return new Crc32(intValue); } + /// public Crc32 GetCrc32(ReadOnlySpan data) { var sum = data.ToArray().Sum(x => x); return new Crc32(sum); } + /// public Crc32 GetCrc32(Stream data) { var bytes = new byte[data.Length]; return GetCrc32(bytes.AsSpan()); } + /// public Crc32 GetCrc32(ReadOnlySpan value, Encoding encoding) { throw new NotImplementedException(); } + /// public Crc32 GetCrc32Upper(ReadOnlySpan value, Encoding encoding) { throw new NotImplementedException(); diff --git a/PG.Testing/NonSeekableReadStream.cs b/PG.Testing/NonSeekableReadStream.cs new file mode 100644 index 000000000..27777efb1 --- /dev/null +++ b/PG.Testing/NonSeekableReadStream.cs @@ -0,0 +1,50 @@ +using System; +using System.IO; + +namespace PG.Testing; + +/// +/// Represents a read-only, forward-only wrapper that reports as . +/// +/// +/// Useful for exercising code paths that must handle non-seekable streams. +/// +/// The data exposed by the stream. +public sealed class NonSeekableReadStream(byte[] data) : Stream +{ + private readonly MemoryStream _inner = new(data, writable: false); + + /// + public override bool CanRead => true; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException(); + /// + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count); + /// + public override void Flush() { } + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + /// + public override void SetLength(long value) => throw new NotSupportedException(); + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + _inner.Dispose(); + base.Dispose(disposing); + } +} diff --git a/PG.Testing/PG.Testing.csproj b/PG.Testing/PG.Testing.csproj index ef2f0c356..021173777 100644 --- a/PG.Testing/PG.Testing.csproj +++ b/PG.Testing/PG.Testing.csproj @@ -7,8 +7,8 @@ false - - + + diff --git a/PG.Testing/PGTestBase.cs b/PG.Testing/PGTestBase.cs index e14a4d856..5241f9775 100644 --- a/PG.Testing/PGTestBase.cs +++ b/PG.Testing/PGTestBase.cs @@ -5,8 +5,12 @@ namespace PG.Testing; +/// +/// Provides a base class for tests that require the Petroglyph commons services and an in-memory file system. +/// public abstract class PGTestBase : TestBaseWithFileSystem { + /// protected override void SetupServices(IServiceCollection serviceCollection) { base.SetupServices(serviceCollection); diff --git a/PG.Testing/TestMegDataStream.cs b/PG.Testing/TestMegDataStream.cs index 3d5e16195..486d3ac31 100644 --- a/PG.Testing/TestMegDataStream.cs +++ b/PG.Testing/TestMegDataStream.cs @@ -3,55 +3,79 @@ namespace PG.Testing; +/// +/// Represents a test that wraps an inner stream and associates it with a MEG entry path. +/// public class TestMegDataStream : Stream, IMegFileDataStream { private readonly Stream _innerStream; + /// public override bool CanRead => _innerStream.CanRead; + /// public override bool CanSeek => _innerStream.CanSeek; + /// public override bool CanWrite => _innerStream.CanWrite; + /// public override long Length => _innerStream.Length; + /// public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; } + /// + /// Initializes a new instance of the class. + /// + /// The path of the entry used in the MEG archive. + /// The data exposed by the stream. public TestMegDataStream(string entryPath, byte[] data) { EntryPath = entryPath; _innerStream = new MemoryStream(data); } + /// + /// Initializes a new instance of the class. + /// + /// The path of the entry used in the MEG archive. + /// The inner stream that supplies the data. public TestMegDataStream(string entryPath, Stream dataStream) { EntryPath = entryPath; _innerStream = dataStream; } + /// public string EntryPath { get; } + /// public override void Flush() { _innerStream.Flush(); } + /// public override int Read(byte[] buffer, int offset, int count) { return _innerStream.Read(buffer, offset, count); } + /// public override long Seek(long offset, SeekOrigin origin) { return _innerStream.Seek(offset, origin); } + /// public override void SetLength(long value) { _innerStream.SetLength(value); } + /// public override void Write(byte[] buffer, int offset, int count) { _innerStream.Write(buffer, offset, count); diff --git a/version.json b/version.json index a55aab6a9..fec756f59 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.1", + "version": "4.0", "assemblyVersion": { "precision": "major" },