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"
},