diff --git a/Directory.Build.props b/Directory.Build.props index af7e034c8..9a9ba6324 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -35,7 +35,7 @@ 3.9.50 - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/PG.Benchmarks/PG.Benchmarks.csproj b/PG.Benchmarks/PG.Benchmarks.csproj index 55ca31a2f..2d0225c52 100644 --- a/PG.Benchmarks/PG.Benchmarks.csproj +++ b/PG.Benchmarks/PG.Benchmarks.csproj @@ -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 e18063d1d..72c7c2445 100644 --- a/PG.Commons/PG.Commons.Test/PG.Commons.Test.csproj +++ b/PG.Commons/PG.Commons.Test/PG.Commons.Test.csproj @@ -14,12 +14,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,7 +27,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PG.Commons/PG.Commons/PG.Commons.csproj b/PG.Commons/PG.Commons/PG.Commons.csproj index 554fe90d1..4003737fd 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.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 364145737..ec3c98a61 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 @@ -36,8 +36,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -45,7 +45,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive 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 38048ae4e..e0cafb007 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.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 b3f2494cb..bfb95ade5 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 @@ -16,6 +16,7 @@ public void Ctor_InvalidArgs_Throws() { Assert.Throws(() => new MegDataEntryOriginInfo((IFileInfo)null!)); Assert.Throws(() => new MegDataEntryOriginInfo((MegDataEntryLocationReference)null!)); + Assert.Throws(() => new MegDataEntryOriginInfo((byte[])null!)); } [Fact] @@ -26,9 +27,11 @@ public void Ctor_FileInfo() Assert.Same(fi, originInfo.FileInfo); Assert.Null(originInfo.MegFileLocation); + Assert.Null(originInfo.Bytes); Assert.True(originInfo.IsLocalFile); Assert.False(originInfo.IsEntryReference); + Assert.False(originInfo.IsBytes); } [Fact] @@ -44,11 +47,93 @@ public void Ctor_ReferenceLocation() Assert.Equal(location, originInfo.MegFileLocation); Assert.Null(originInfo.FileInfo); + Assert.Null(originInfo.Bytes); Assert.True(originInfo.IsEntryReference); Assert.False(originInfo.IsLocalFile); + Assert.False(originInfo.IsBytes); } + #region CTOR_Byte[] + + [Fact] + public void Ctor_Bytes() + { + var bytes = new byte[] { 1, 2, 3, 4 }; + var originInfo = new MegDataEntryOriginInfo(bytes); + + Assert.NotSame(bytes, originInfo.Bytes); + Assert.Equal(bytes, originInfo.Bytes); + Assert.Null(originInfo.FileInfo); + Assert.Null(originInfo.MegFileLocation); + + Assert.True(originInfo.IsBytes); + Assert.False(originInfo.IsLocalFile); + Assert.False(originInfo.IsEntryReference); + } + + [Fact] + public void Ctor_Bytes_Empty_OK() + { + var bytes = Array.Empty(); + var originInfo = new MegDataEntryOriginInfo(bytes); + + Assert.True(originInfo.IsBytes); + Assert.Empty(originInfo.Bytes!); + } + + [Fact] + public void Ctor_Bytes_DefensiveCopy() + { + var bytes = new byte[] { 1, 2, 3 }; + var originInfo = new MegDataEntryOriginInfo(bytes); + + bytes[0] = 99; + + Assert.Equal([1, 2, 3], originInfo.Bytes); + } + + #endregion + + #region CTOR_ReadOnlySpan + + [Fact] + public void Ctor_Bytes_Span() + { + Span bytes = [1, 2, 3, 4]; + var originInfo = new MegDataEntryOriginInfo(bytes); + + Assert.Equal(bytes.ToArray(), originInfo.Bytes); + Assert.Null(originInfo.FileInfo); + Assert.Null(originInfo.MegFileLocation); + + Assert.True(originInfo.IsBytes); + Assert.False(originInfo.IsLocalFile); + Assert.False(originInfo.IsEntryReference); + } + + [Fact] + public void Ctor_Bytes_Span_Empty_OK() + { + var originInfo = new MegDataEntryOriginInfo(ReadOnlySpan.Empty); + + Assert.True(originInfo.IsBytes); + Assert.Empty(originInfo.Bytes!); + } + + [Fact] + public void Ctor_Bytes_Span_DefensiveCopy() + { + Span bytes = [1, 2, 3]; + var originInfo = new MegDataEntryOriginInfo(bytes); + + bytes[0] = 99; + + Assert.Equal([1, 2, 3], originInfo.Bytes); + } + + #endregion + [Fact] public void EqualsHashCode() { @@ -92,4 +177,29 @@ public void EqualsHashCode() Assert.Equal(originLoc.GetHashCode(), otherOriginLoc.GetHashCode()); Assert.Equal(originPath.GetHashCode(), otherOriginPath.GetHashCode()); } -} \ No newline at end of file + + [Fact] + public void EqualsHashCode_Bytes() + { + var bytes = new byte[] { 1, 2, 3 }; + var origin = new MegDataEntryOriginInfo(bytes); + + // Self-equality. + Assert.Equal(origin, origin); + Assert.Equal(origin, (object)origin); + Assert.False(origin.Equals(null)); + + // Each ctor call clones the buffer, so two origins from the same input are NOT equal + // (they own distinct internal buffers). + var otherOrigin = new MegDataEntryOriginInfo(bytes); + Assert.NotEqual(origin, otherOrigin); + + Assert.NotEqual(origin, new MegDataEntryOriginInfo(FileSystem.FileInfo.New("test.xml"))); + + var spanOrigin = new MegDataEntryOriginInfo((ReadOnlySpan)bytes); + Assert.Equal(spanOrigin, spanOrigin); + Assert.Equal(spanOrigin, (object)spanOrigin); + + Assert.NotEqual(origin, otherOrigin); + } +} 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 dd99c3afa..ffc125124 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 @@ -276,8 +276,115 @@ public void FromEntryReference_OriginIsEntryReference_EntryPathEmpty_ThrowsArgum #endregion + #region Factory FromBytes + + [Theory] + [InlineData("entry.bin", true)] + [InlineData("entry.bin", false)] + public void FromBytes(string entryPath, bool encrypted) + { + var bytes = new byte[] { 1, 2, 3, 4, 5 }; + + var info = MegDataEntryBuilderInfo.FromBytes(bytes, entryPath, encrypted); + + Assert.True(info.OriginInfo.IsBytes); + Assert.NotSame(bytes, info.OriginInfo.Bytes); + Assert.Equal(bytes, info.OriginInfo.Bytes); + Assert.Equal(entryPath, info.EntryPath); + Assert.Equal(5u, info.Size); + Assert.Equal(encrypted, info.Encrypted); + } + + [Fact] + public void FromBytes_NullBytes_Throws() + { + Assert.Throws(() => MegDataEntryBuilderInfo.FromBytes(null!, "entry")); + } + + [Fact] + public void FromBytes_NullOrEmptyEntryPath_Throws() + { + var bytes = new byte[] { 1, 2, 3 }; + Assert.Throws(() => MegDataEntryBuilderInfo.FromBytes(bytes, null!)); + Assert.Throws(() => MegDataEntryBuilderInfo.FromBytes(bytes, "")); + } + + [Fact] + public void FromBytes_Empty_OK() + { + var info = MegDataEntryBuilderInfo.FromBytes((byte[])[], "entry.bin"); + + Assert.True(info.OriginInfo.IsBytes); + Assert.Equal(0u, info.Size); + } + + #endregion + + #region Factory FromReadOnlySpan + + [Theory] + [InlineData("entry.bin", true)] + [InlineData("entry.bin", false)] + public void FromBytes_Span(string entryPath, bool encrypted) + { + Span bytes = [1, 2, 3, 4, 5]; + + var info = MegDataEntryBuilderInfo.FromBytes(bytes, entryPath, encrypted); + + Assert.True(info.OriginInfo.IsBytes); + Assert.Equal(bytes.ToArray(), info.OriginInfo.Bytes); + Assert.Equal(entryPath, info.EntryPath); + Assert.Equal(5u, info.Size); + Assert.Equal(encrypted, info.Encrypted); + } + + [Fact] + public void FromBytes_Soan_NullOrEmptyEntryPath_Throws() + { + byte[] bytes = [1, 2, 3]; + Assert.Throws(() => MegDataEntryBuilderInfo.FromBytes(bytes.AsSpan(), null!)); + Assert.Throws(() => MegDataEntryBuilderInfo.FromBytes(bytes.AsSpan(), "")); + } + + [Fact] + public void FromBytes_Span_Empty_OK() + { + var info = MegDataEntryBuilderInfo.FromBytes([], "entry.bin"); + + Assert.True(info.OriginInfo.IsBytes); + Assert.Equal(0u, info.Size); + } + + #endregion + #region RefreshSize + [Fact] + public void RefreshSize_FromBytes_Unchanged() + { + var bytes = new byte[] { 1, 2, 3, 4 }; + var info = MegDataEntryBuilderInfo.FromBytes(bytes, "entry.bin"); + + Assert.Equal(4u, info.Size); + + info.RefreshSize(); + + Assert.Equal(4u, info.Size); + } + + [Fact] + public void RefreshSize_FromBytes_Span_Unchanged() + { + Span bytes = [1, 2, 3, 4]; + var info = MegDataEntryBuilderInfo.FromBytes(bytes, "entry.bin"); + + Assert.Equal(4u, info.Size); + + info.RefreshSize(); + + Assert.Equal(4u, info.Size); + } + [Fact] public void RefreshSize_FromMegEntry() { @@ -324,7 +431,7 @@ public void RefreshSize_FromFile_FileDeleted_ThrowsFileNotFoundException() FileSystem.File.Delete("test.xml"); - Assert.Throws(() => info.RefreshSize()); + Assert.Throws(info.RefreshSize); } [Fact] @@ -339,7 +446,7 @@ public void RefreshSize_FileTooLarge_ThrowsMegEntrySizeException() mockFileInfo.Length = (long)uint.MaxValue + 1; - Assert.Throws(() => info.RefreshSize()); + Assert.Throws(info.RefreshSize); } #endregion 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 6094fbb22..5538311f3 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 @@ -22,9 +22,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -32,7 +32,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive 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 ea06bbea9..6060f7540 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,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using AnakinRaW.CommonUtilities.Extensions; using AnakinRaW.CommonUtilities.Testing.Extensions; @@ -512,6 +513,138 @@ public void AddEntry_AddFileSize_FileTooLarge_Throws() #endregion + #region AddBytes + + [Fact] + public void AddBytes_Throws() + { + var builder = CreateBuilder(); + var bytes = new byte[] { 1, 2, 3 }; + + Assert.Throws(() => builder.AddBytes(null!, "path")); + Assert.Throws(() => builder.AddBytes(bytes, null!)); + Assert.Throws(() => builder.AddBytes(bytes, "")); + } + + [Fact] + public void AddBytes_AddsEntry() + { + var builder = CreateBuilder(); + var bytes = new byte[] { 1, 2, 3, 4, 5 }; + + var result = builder.AddBytes(bytes, "entry.bin"); + + Assert.True(result.Added, $"Actual: {result.Status}"); + Assert.Single(builder.DataEntries); + + var entry = builder.DataEntries.First(); + Assert.True(entry.OriginInfo.IsBytes); + Assert.NotSame(bytes, entry.OriginInfo.Bytes); + Assert.Equal(bytes, entry.OriginInfo.Bytes); + Assert.Equal(5u, entry.Size); + Assert.False(entry.Encrypted); + } + + [Fact] + public void AddBytes_Span_AddsEntry() + { + var builder = CreateBuilder(); + Span bytes = [1, 2, 3, 4, 5]; + + var result = builder.AddBytes(bytes, "entry.bin"); + + Assert.True(result.Added, $"Actual: {result.Status}"); + Assert.Single(builder.DataEntries); + + var entry = builder.DataEntries.First(); + Assert.True(entry.OriginInfo.IsBytes); + Assert.Equal(bytes.ToArray(), entry.OriginInfo.Bytes); + Assert.Equal(5u, entry.Size); + Assert.False(entry.Encrypted); + } + + [Fact] + public void AddBytes_MutationAfterAdd_DoesNotAffectEntry() + { + var builder = CreateBuilder(); + var bytes = new byte[] { 1, 2, 3 }; + + builder.AddBytes(bytes, "entry.bin"); + + bytes[0] = 99; + + Assert.Equal([1, 2, 3], builder.DataEntries.First().OriginInfo.Bytes); + } + + [Fact] + public void AddBytes_Span_MutationAfterAdd_DoesNotAffectEntry() + { + var builder = CreateBuilder(); + Span bytes = [1, 2, 3]; + + builder.AddBytes(bytes, "entry.bin"); + + bytes[0] = 99; + + Assert.Equal([1, 2, 3], builder.DataEntries.First().OriginInfo.Bytes); + } + + [Fact] + public void AddBytes_Empty_AddsEntry() + { + var builder = CreateBuilder(); + + var result = builder.AddBytes((byte[])[], "entry.bin"); + + Assert.True(result.Added, $"Actual: {result.Status}"); + Assert.Equal(0u, builder.DataEntries.First().Size); + } + + [Fact] + public void AddBytes_Span_Empty_AddsEntry() + { + var builder = CreateBuilder(); + + var result = builder.AddBytes(ReadOnlySpan.Empty, "entry.bin"); + + Assert.True(result.Added, $"Actual: {result.Status}"); + Assert.Equal(0u, builder.DataEntries.First().Size); + } + + [Fact] + public void AddBytes_TooLarge_ReturnsEntryFileTooLarge() + { + var builder = new MaxFileSizeMegBuilder(3, ServiceProvider); + + var result = builder.AddBytes((byte[])[1, 2, 3, 4], "entry.bin"); + + Assert.Equal(MegDataEntryAddStatus.EntryFileTooLarge, result.Status); + Assert.Empty(builder.DataEntries); + } + + [Fact] + public void AddBytes_Span_TooLarge_ReturnsEntryFileTooLarge() + { + var builder = new MaxFileSizeMegBuilder(3, ServiceProvider); + + var result = builder.AddBytes((ReadOnlySpan)[1, 2, 3, 4], "entry.bin"); + + Assert.Equal(MegDataEntryAddStatus.EntryFileTooLarge, result.Status); + Assert.Empty(builder.DataEntries); + } + + [Fact] + public void AddBytes_DisposedBuilder_Throws() + { + var builder = CreateBuilder(); + builder.Dispose(); + + Assert.Throws(() => builder.AddBytes((byte[])[1, 2, 3], "entry.bin")); + Assert.Throws(() => builder.AddBytes((ReadOnlySpan)[1, 2, 3], "entry.bin")); + } + + #endregion + #region GetMinRequiredMegFiles [Fact] @@ -554,6 +687,34 @@ public void GetMinRequiredMegFiles_MultipleFiles() #endregion + #region Build_FromBytes + + [Fact] + public void Build_FromBytes_RoundTrip() + { + var contents = new byte[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 }; + + var builder = CreateBuilder(); + var addResult = builder.AddBytes(contents, "bytes-entry.bin"); + Assert.True(addResult.Added, $"Actual: {addResult.Status}"); + + var fileInfo = CreateFileInfo(true, "out.meg"); + builder.Build(fileInfo, true); + + Assert.True(FileSystem.File.Exists("out.meg")); + + var loaded = ServiceProvider.GetRequiredService().Load("out.meg"); + Assert.Single(loaded.Archive); + + var extractor = ServiceProvider.GetRequiredService(); + using var extracted = extractor.GetData(new MegDataEntryLocationReference(loaded, loaded.Archive[0])); + var sink = new MemoryStream(); + extracted.CopyTo(sink); + Assert.Equal(contents, sink.ToArray()); + } + + #endregion + #region BuildMany [Fact] 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 index f6f596466..030e8a6cd 100644 --- 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 @@ -127,6 +127,44 @@ public void GetFileData_LocationReference_EmptyDataFile() 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() { 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 eda45ed08..3456b8555 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 @@ -8,7 +8,7 @@ namespace PG.StarWarsGame.Files.MEG.Data.EntryLocations; /// -/// The origin of a MEG data entry which is either packed in a MEG archive or present on the file system. +/// 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. /// public sealed class MegDataEntryOriginInfo : IDataEntryLocation, IEquatable { @@ -36,6 +36,15 @@ public sealed class MegDataEntryOriginInfo : IDataEntryLocation, IEquatable MegFileLocation != null; + /// + /// Gets a value indicating whether the data entry originates from an in-memory byte buffer. + /// + /// + /// If this property returns , the property is guaranteed to be non-. + /// + [MemberNotNullWhen(true, nameof(Bytes))] + public bool IsBytes => Bytes != null; + /// /// Gets the MEG file's data entry. if not present. /// @@ -47,7 +56,16 @@ public sealed class MegDataEntryOriginInfo : IDataEntryLocation, IEquatable - /// Initializes a new instance of the structure to the specified file. + /// Gets the in-memory byte buffer that holds this data entry's bytes. if not present. + /// + /// + /// This is a defensive copy of the buffer passed to the constructor. Mutations to the constructor's + /// input after construction do not affect this property. + /// + public byte[]? Bytes { get; } + + /// + /// Initializes a new instance of the class to the specified file. /// /// The origin file. /// is . @@ -57,7 +75,7 @@ public MegDataEntryOriginInfo(IFileInfo fileInfo) } /// - /// Initializes a new instance of the structure to the specified MEG file's data entry. + /// Initializes a new instance of the class to the specified MEG file's data entry. /// /// The MEG file's data entry. /// If is . @@ -66,6 +84,29 @@ public MegDataEntryOriginInfo(MegDataEntryLocationReference locationReference) MegFileLocation = locationReference ?? throw new ArgumentNullException(nameof(locationReference)); } + /// + /// Initializes a new instance of the class from the specified byte span. + /// The span is copied; subsequent mutations to do not affect this origin. + /// + /// The read-only span containing the entry bytes. + public MegDataEntryOriginInfo(ReadOnlySpan bytes) + { + Bytes = bytes.ToArray(); + } + + /// + /// Initializes a new instance of the class from the specified byte buffer. + /// The buffer is copied; subsequent mutations to do not affect this origin. + /// + /// The buffer containing the entry bytes. + /// is . + public MegDataEntryOriginInfo(byte[] bytes) + { + if (bytes == null) + throw new ArgumentNullException(nameof(bytes)); + Bytes = (byte[])bytes.Clone(); + } + /// public bool Equals(MegDataEntryOriginInfo? other) { @@ -73,8 +114,9 @@ public bool Equals(MegDataEntryOriginInfo? other) return false; if (ReferenceEquals(this, other)) return true; - return Equals(MegFileLocation, other.MegFileLocation) && - string.Equals(FileInfo?.FullName, other.FileInfo?.FullName, StringComparison.Ordinal); + return Equals(MegFileLocation, other.MegFileLocation) && + string.Equals(FileInfo?.FullName, other.FileInfo?.FullName, StringComparison.Ordinal) && + ReferenceEquals(Bytes, other.Bytes); } /// @@ -86,14 +128,16 @@ public override bool Equals(object? obj) /// public override int GetHashCode() { - return HashCode.Combine(MegFileLocation, FileInfo?.FullName); + return HashCode.Combine(MegFileLocation, FileInfo?.FullName, Bytes); } /// public override string ToString() { - return IsLocalFile - ? $"Local File: '{FileInfo.FullName}'" - : $"MEG Entry: '{MegFileLocation}'"; + if (IsLocalFile) + return $"Local File: '{FileInfo.FullName}'"; + if (IsEntryReference) + return $"MEG Entry: '{MegFileLocation}'"; + return $"Bytes: {Bytes!.Length} bytes"; } -} \ No newline at end of file +} 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 20091b70a..e0ac2b583 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 @@ -5,6 +5,7 @@ using PG.StarWarsGame.Files.MEG.Data.EntryLocations; using PG.StarWarsGame.Files.MEG.Files; using System; +using System.Diagnostics; using System.IO; using System.IO.Abstractions; using AnakinRaW.CommonUtilities; @@ -119,7 +120,7 @@ public static MegDataEntryBuilderInfo FromEntryReference(MegDataEntryLocationRef /// public static MegDataEntryBuilderInfo FromFile(IFileInfo file, string entryPath, bool encrypt = false) { - if (file == null) + if (file == null) throw new ArgumentNullException(nameof(file)); if (!file.Exists) throw new ArgumentException($"The specified file '{file.FullName}' does not exist.", nameof(file)); @@ -128,6 +129,38 @@ public static MegDataEntryBuilderInfo FromFile(IFileInfo file, string entryPath, new MegDataEntryOriginInfo(file), entryPath, encrypt); } + /// + /// Creates a new instance of the class from an read-only span of bytes. + /// The span is copied; subsequent mutations to do not affect the resulting entry. + /// + /// 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 . + /// is . + /// is empty. + public static MegDataEntryBuilderInfo FromBytes(ReadOnlySpan bytes, string entryPath, bool encrypt = false) + { + ThrowHelper.ThrowIfNullOrEmpty(entryPath); + return new MegDataEntryBuilderInfo(new MegDataEntryOriginInfo(bytes), entryPath, encrypt); + } + + /// + /// Creates a new instance of the class from an in-memory byte buffer. + /// The buffer is copied; subsequent mutations to do not affect the resulting entry. + /// + /// 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 . + /// or is . + /// is empty. + public static MegDataEntryBuilderInfo FromBytes(byte[] bytes, string entryPath, bool encrypt = false) + { + if (bytes == null) + throw new ArgumentNullException(nameof(bytes)); + ThrowHelper.ThrowIfNullOrEmpty(entryPath); + return new MegDataEntryBuilderInfo(new MegDataEntryOriginInfo(bytes), entryPath, encrypt); + } + /// /// Updates the size of the data entry associated with this instance. /// @@ -136,7 +169,13 @@ public static MegDataEntryBuilderInfo FromFile(IFileInfo file, string entryPath, public void RefreshSize() { if (OriginInfo.IsEntryReference) + { Size = OriginInfo.MegFileLocation.DataEntry.Location.Size; + } + else if (OriginInfo.IsBytes) + { + Size = (uint)OriginInfo.Bytes.Length; + } else { var fileInfo = OriginInfo.FileInfo!; @@ -154,17 +193,20 @@ private static string GetEntryPath(MegDataEntryOriginInfo originInfo, string? ov { if (overrideEntryPath is not null) return overrideEntryPath; - return originInfo.IsLocalFile - ? originInfo.FileInfo.FullName - : originInfo.MegFileLocation!.DataEntry.Path; + if (originInfo.IsLocalFile) + return originInfo.FileInfo.FullName; + if (originInfo.IsEntryReference) + return originInfo.MegFileLocation.DataEntry.Path; + Debug.Fail("A byte-buffer-backed entry requires an explicit entry path. Callers must validate before invoking the internal constructor."); + throw new InvalidOperationException("A byte-buffer-backed entry requires an explicit entry path."); } private static bool GetEncryption(MegDataEntryOriginInfo originInfo, bool? overrideEncrypted) { if (overrideEncrypted is not null) - return overrideEncrypted.Value; - // Fallback for the case, origin is a file system path but overrideEncrypted was forgotten to set explicitly. - if (originInfo.IsLocalFile) + return overrideEncrypted.Value; + // Fallback for the case, origin is a file system path or byte buffer but overrideEncrypted was forgotten to set explicitly. + if (originInfo.IsLocalFile || originInfo.IsBytes) return false; return originInfo.MegFileLocation!.DataEntry.Encrypted; } 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 a72cc2922..917fce0b1 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/IMegBuilder.cs b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/Services/Builder/IMegBuilder.cs index dad1ce749..062d6a6d1 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 @@ -70,7 +70,7 @@ public interface IMegBuilder : IFileBuilder - /// Adds an existing data entry to the and returns a status information to indicate whether the entry was successfully added. + /// Adds an existing data entry to the and returns a status information to indicate whether the entry was successfully added. /// /// /// The actual data entry's file path might be different to depending whether this normalizes path before adding, or not. @@ -84,6 +84,36 @@ public interface IMegBuilder : IFileBuilder + /// Adds an in-memory byte buffer copied from the specified span as a data entry to the . + /// The buffer is copied; subsequent mutations to do not affect the resulting entry. + /// + /// + /// The actual data entry's file path might be different to due to optional normalization and mandatory encoding. + /// + /// 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. + /// The result of this operation. + /// is . + /// is empty. + MegDataEntryAddResult AddBytes(ReadOnlySpan bytes, string entryPath, bool encrypt = false); + + /// + /// Adds an in-memory byte buffer as a data entry to the . + /// The buffer is copied; subsequent mutations to do not affect the resulting entry. + /// + /// + /// The actual data entry's file path might be different to due to optional normalization and mandatory encoding. + /// + /// 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. + /// The result of this operation. + /// or is . + /// is empty. + MegDataEntryAddResult AddBytes(byte[] bytes, string entryPath, bool encrypt = false); + /// /// Removes the first occurrence of a specific object from the . /// 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 13d1f2a35..b1db398a9 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 @@ -108,7 +108,7 @@ public MegDataEntryAddResult AddEntry( bool? overrideEncrypt = null) { ThrowIfDisposed(); - + if (overridePathInMeg is not null && string.IsNullOrWhiteSpace(overridePathInMeg)) throw new ArgumentException("Override path in MEG cannot be empty or whitespace.", nameof(overridePathInMeg)); if (entryReference == null) @@ -119,13 +119,33 @@ public MegDataEntryAddResult AddEntry( if (!entryReference.Exists) return MegDataEntryAddResult.FromEntryNotFound(entryReference); - + return AddBuilderInfo( - new MegDataEntryOriginInfo(entryReference), + new MegDataEntryOriginInfo(entryReference), entryPath, encrypt); } + /// + public MegDataEntryAddResult AddBytes(ReadOnlySpan bytes, string entryPath, bool encrypt = false) + { + ThrowIfDisposed(); + ThrowHelper.ThrowIfNullOrEmpty(entryPath); + + return AddBuilderInfo(new MegDataEntryOriginInfo(bytes), entryPath, encrypt); + } + + /// + public MegDataEntryAddResult AddBytes(byte[] bytes, string entryPath, bool encrypt = false) + { + ThrowIfDisposed(); + if (bytes == null) + throw new ArgumentNullException(nameof(bytes)); + ThrowHelper.ThrowIfNullOrEmpty(entryPath); + + return AddBuilderInfo(new MegDataEntryOriginInfo(bytes), entryPath, encrypt); + } + /// public bool Remove(MegDataEntryBuilderInfo info) { 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 index 4927289df..c0f2decb0 100644 --- 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 @@ -17,9 +17,12 @@ internal sealed class MegDataStreamFactory(IServiceProvider serviceProvider) { public Stream GetStream(MegDataEntryOriginInfo originInfo) { - if (originInfo == null) + 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(); 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 f073d5297..f8c69bc94 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 @@ -16,9 +16,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -30,7 +30,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive 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 8329ec9c7..5b877d339 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.Xml/PG.StarWarsGame.Files.Xml.Test/PG.StarWarsGame.Files.Xml.Test.csproj b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.Test/PG.StarWarsGame.Files.Xml.Test.csproj index 5beecf928..fec7f5e52 100644 --- a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.Test/PG.StarWarsGame.Files.Xml.Test.csproj +++ b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.Test/PG.StarWarsGame.Files.Xml.Test.csproj @@ -10,7 +10,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PG.StarWarsGame.Files.Test.csproj b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PG.StarWarsGame.Files.Test.csproj index 0999433e0..870cd0a1e 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PG.StarWarsGame.Files.Test.csproj +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PG.StarWarsGame.Files.Test.csproj @@ -12,16 +12,16 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PG.Testing/PG.Testing.csproj b/PG.Testing/PG.Testing.csproj index 46fbbb00b..ef2f0c356 100644 --- a/PG.Testing/PG.Testing.csproj +++ b/PG.Testing/PG.Testing.csproj @@ -7,8 +7,8 @@ false - - + + diff --git a/version.json b/version.json index ed5df3e05..a55aab6a9 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.0", + "version": "3.1", "assemblyVersion": { "precision": "major" },