diff --git a/Source/AssetRipper.IO.Files.Tests/SerializedFileTests.cs b/Source/AssetRipper.IO.Files.Tests/SerializedFileTests.cs new file mode 100644 index 000000000..774c6a6cd --- /dev/null +++ b/Source/AssetRipper.IO.Files.Tests/SerializedFileTests.cs @@ -0,0 +1,93 @@ +using AssetRipper.IO.Endian; +using AssetRipper.IO.Files.SerializedFiles; +using AssetRipper.IO.Files.SerializedFiles.Parser; +using AssetRipper.IO.Files.Streams.Smart; + +namespace AssetRipper.IO.Files.Tests; + +internal class SerializedFileTests +{ + [Theory] + public void WritingSerializedFileDoesNotThrow(FormatVersion generation) + { + Assert.DoesNotThrow(() => + { + SerializedFileBuilder builder = new() { Generation = generation }; + SerializedFile file = builder.Build(); + using SmartStream stream = SmartStream.CreateMemory(); + file.Write(stream); + }); + } + + [Theory] + public void WrittenSerializedFileCanBeRead(FormatVersion generation) + { + SerializedFileBuilder builder = new() { Generation = generation }; + SerializedFile file = builder.Build(); + using SmartStream stream = SmartStream.CreateMemory(); + file.Write(stream); + stream.Flush(); + stream.Position = 0; + Assert.That(SerializedFileScheme.Default.CanRead(stream)); + } + + [Theory] + public void ReadingAndWritingAreConsistent(FormatVersion generation) + { + SerializedFileBuilder builder = new() { Generation = generation }; + SerializedFile original = builder.Build(); + AssertReadingAndWritingAreConsistent(original); + } + + private static void AssertReadingAndWritingAreConsistent(SerializedFile original) + { + SerializedFile read; + { + using SmartStream stream = SmartStream.CreateMemory(); + original.Write(stream); + stream.Flush(); + stream.Position = 0; + read = SerializedFileScheme.Default.Read(stream, original.FilePath, original.Name); + } + + Assert.Multiple(() => + { + Assert.That(read.Generation, Is.EqualTo(original.Generation)); + Assert.That(read.Version, Is.EqualTo(original.Version)); + Assert.That(read.Platform, Is.EqualTo(original.Platform)); + Assert.That(read.EndianType, Is.EqualTo(original.EndianType)); + Assert.That(read.Flags, Is.EqualTo(original.Flags)); + Assert.That(read.Dependencies.ToArray(), Is.EqualTo(original.Dependencies.ToArray())); + Assert.That(read.Objects.ToArray(), Is.EqualTo(original.Objects.ToArray())); + }); + } + + [Theory] + public void SerializeFileHeaderReadingMatchesWriting(FormatVersion generation) + { + SerializedFileHeader header = new() + { + Version = generation, + MetadataSize = 256,//arbitrary number greater than 0 + }; + using MemoryStream stream = new(); + using (EndianWriter writer = new(stream, EndianType.BigEndian)) + { + header.Write(writer); + } + stream.Flush(); + Assert.That(stream.Position, Is.GreaterThan(0)); + stream.Position = 0; + SerializedFileHeader readHeader = new(); + using (EndianReader reader = new(stream, EndianType.BigEndian)) + { + readHeader.Read(reader); + } + + Assert.Multiple(() => + { + Assert.That(stream.Position, Is.EqualTo(stream.Length)); + Assert.That(readHeader, Is.EqualTo(header)); + }); + } +} diff --git a/Source/AssetRipper.IO.Files/SerializedFiles/Parser/FileIdentifier.cs b/Source/AssetRipper.IO.Files/SerializedFiles/Parser/FileIdentifier.cs index 527927208..c8361e5ca 100644 --- a/Source/AssetRipper.IO.Files/SerializedFiles/Parser/FileIdentifier.cs +++ b/Source/AssetRipper.IO.Files/SerializedFiles/Parser/FileIdentifier.cs @@ -39,7 +39,7 @@ namespace AssetRipper.IO.Files.SerializedFiles.Parser PathName = FilenameUtils.FixFileIdentifier(PathNameOrigin); } - public void Write(SerializedWriter writer) + public readonly void Write(SerializedWriter writer) { if (HasAssetPath(writer.Generation)) { @@ -53,7 +53,7 @@ namespace AssetRipper.IO.Files.SerializedFiles.Parser writer.WriteStringZeroTerm(PathNameOrigin); } - public string GetFilePath() + public readonly string GetFilePath() { if (Type == AssetType.Meta) { @@ -62,7 +62,7 @@ namespace AssetRipper.IO.Files.SerializedFiles.Parser return PathName; } - public override string? ToString() + public override readonly string? ToString() { if (Type == AssetType.Meta) { diff --git a/Source/AssetRipper.IO.Files/SerializedFiles/Parser/SerializedFileHeader.cs b/Source/AssetRipper.IO.Files/SerializedFiles/Parser/SerializedFileHeader.cs index c49c94c67..dc6f3cf0e 100644 --- a/Source/AssetRipper.IO.Files/SerializedFiles/Parser/SerializedFileHeader.cs +++ b/Source/AssetRipper.IO.Files/SerializedFiles/Parser/SerializedFileHeader.cs @@ -5,7 +5,7 @@ namespace AssetRipper.IO.Files.SerializedFiles.Parser /// /// The file header is found at the beginning of an asset file. The header is always using big endian byte order. /// - public sealed class SerializedFileHeader + public sealed record class SerializedFileHeader { /// /// Size of the metadata parts of the file @@ -129,7 +129,7 @@ namespace AssetRipper.IO.Files.SerializedFiles.Parser if (HasLargeFilesSupport(Version)) { writer.Write(0); - writer.Write(0); + writer.Write(0u); } else { @@ -143,7 +143,7 @@ namespace AssetRipper.IO.Files.SerializedFiles.Parser //0x0c if (HasLargeFilesSupport(Version)) { - writer.Write(0); + writer.Write(0u); } else { @@ -163,7 +163,7 @@ namespace AssetRipper.IO.Files.SerializedFiles.Parser writer.Write((uint)MetadataSize); writer.Write(FileSize); writer.Write(DataOffset); - writer.Write((long)0); + writer.Write(0L); } } } diff --git a/Source/AssetRipper.IO.Files/SerializedFiles/SerializedFile.cs b/Source/AssetRipper.IO.Files/SerializedFiles/SerializedFile.cs index 9228e3379..74d3c4391 100644 --- a/Source/AssetRipper.IO.Files/SerializedFiles/SerializedFile.cs +++ b/Source/AssetRipper.IO.Files/SerializedFiles/SerializedFile.cs @@ -21,7 +21,38 @@ namespace AssetRipper.IO.Files.SerializedFiles public FormatVersion Generation { get; private set; } public UnityVersion Version { get; private set; } public BuildTarget Platform { get; private set; } - public TransferInstructionFlags Flags { get; private set; } + public TransferInstructionFlags Flags + { + get + { + TransferInstructionFlags flags; + if (SerializedFileMetadata.HasPlatform(Generation) && Platform == BuildTarget.NoTarget) + { + if (FilePath.EndsWith(".unity", StringComparison.Ordinal)) + { + flags = TransferInstructionFlags.SerializeEditorMinimalScene; + } + else + { + flags = TransferInstructionFlags.NoTransferInstructionFlags; + } + } + else + { + flags = TransferInstructionFlags.SerializeGameRelease; + } + + if (FilenameUtils.IsEngineResource(Name) || (Generation < FormatVersion.Unknown_10 && FilenameUtils.IsBuiltinExtra(Name))) + { + flags |= TransferInstructionFlags.IsBuiltinResourcesFile; + } + if (EndianType is EndianType.BigEndian) + { + flags |= TransferInstructionFlags.SwapEndianess; + } + return flags; + } + } public EndianType EndianType { get; private set; } public ReadOnlySpan Dependencies => m_dependencies; public ReadOnlySpan Objects => m_objects; @@ -29,36 +60,6 @@ namespace AssetRipper.IO.Files.SerializedFiles public ReadOnlySpan RefTypes => m_refTypes; public bool HasTypeTree { get; private set; } - private static TransferInstructionFlags GetFlags(SerializedFileHeader header, SerializedFileMetadata metadata, string name, string filePath) - { - TransferInstructionFlags flags; - if (SerializedFileMetadata.HasPlatform(header.Version) && metadata.TargetPlatform == BuildTarget.NoTarget) - { - if (filePath.EndsWith(".unity", StringComparison.Ordinal)) - { - flags = TransferInstructionFlags.SerializeEditorMinimalScene; - } - else - { - flags = TransferInstructionFlags.NoTransferInstructionFlags; - } - } - else - { - flags = TransferInstructionFlags.SerializeGameRelease; - } - - if (FilenameUtils.IsEngineResource(name) || (header.Version < FormatVersion.Unknown_10 && FilenameUtils.IsBuiltinExtra(name))) - { - flags |= TransferInstructionFlags.IsBuiltinResourcesFile; - } - if (header.Endianess || metadata.SwapEndianess) - { - flags |= TransferInstructionFlags.SwapEndianess; - } - return flags; - } - private static EndianType GetEndianType(SerializedFileHeader header, SerializedFileMetadata metadata) { bool swapEndianess = SerializedFileHeader.HasEndianess(header.Version) ? header.Endianess : metadata.SwapEndianess; @@ -109,7 +110,6 @@ namespace AssetRipper.IO.Files.SerializedFiles Generation = header.Version; Version = metadata.UnityVersion; Platform = metadata.TargetPlatform; - Flags = GetFlags(header, metadata, Name, FilePath); EndianType = GetEndianType(header, metadata); m_dependencies = metadata.Externals; m_objects = metadata.Object; @@ -121,14 +121,14 @@ namespace AssetRipper.IO.Files.SerializedFiles public override void Write(Stream stream) { long initialPosition = stream.Position; - using SerializedWriter writer = new(stream, EndianType, Generation, Version); SerializedFileHeader header = new() { Version = Generation, Endianess = EndianType == EndianType.BigEndian, }; - header.Write(writer); + header.Write(new EndianWriter(stream, EndianType.BigEndian)); + using SerializedWriter writer = new(stream, EndianType, Generation, Version); SerializedFileMetadata metadata = new() { UnityVersion = Version, @@ -144,30 +144,30 @@ namespace AssetRipper.IO.Files.SerializedFiles long objectDataPosition; if (SerializedFileMetadata.IsMetadataAtTheEnd(Generation)) { - metadataPosition = stream.Position; - metadata.Write(writer); - metadataSize = stream.Position - metadataPosition; AlignStream(writer);//Object data must always be aligned. objectDataPosition = stream.Position; WriteObjectData(writer, metadata.Object); + metadataPosition = stream.Position; + metadata.Write(writer); + metadataSize = stream.Position - metadataPosition; } else { - AlignStream(writer);//Object data must always be aligned. - objectDataPosition = stream.Position; - WriteObjectData(writer, metadata.Object); metadataPosition = stream.Position; metadata.Write(writer); metadataSize = stream.Position - metadataPosition; + AlignStream(writer);//Object data must always be aligned. + objectDataPosition = stream.Position; + WriteObjectData(writer, metadata.Object); } - + long finalPosition = stream.Position; stream.Position = initialPosition; header.FileSize = finalPosition - initialPosition; header.MetadataSize = metadataSize; header.DataOffset = objectDataPosition - initialPosition; - header.Write(writer); + header.Write(new EndianWriter(stream, EndianType.BigEndian)); stream.Position = finalPosition; @@ -177,7 +177,7 @@ namespace AssetRipper.IO.Files.SerializedFiles { if (objectInfo.ObjectData is not null) { - writer.Write(objectInfo.ObjectData); + writer.Write(objectInfo.ObjectData); } AlignStream(writer); @@ -234,5 +234,21 @@ namespace AssetRipper.IO.Files.SerializedFiles SmartStream stream = SmartStream.OpenRead(filePath); return SerializedFileScheme.Default.Read(stream, filePath, fileName); } + + public static SerializedFile FromBuilder(SerializedFileBuilder builder) + { + return new() + { + Generation = builder.Generation, + Version = builder.Version, + Platform = builder.Platform, + EndianType = builder.EndianType, + m_dependencies = builder.Dependencies.ToArray(), + m_objects = builder.Objects.ToArray(), + m_types = builder.Types.ToArray(), + m_refTypes = builder.RefTypes.ToArray(), + HasTypeTree = builder.HasTypeTree, + }; + } } } diff --git a/Source/AssetRipper.IO.Files/SerializedFiles/SerializedFileBuilder.cs b/Source/AssetRipper.IO.Files/SerializedFiles/SerializedFileBuilder.cs new file mode 100644 index 000000000..4dc3dae13 --- /dev/null +++ b/Source/AssetRipper.IO.Files/SerializedFiles/SerializedFileBuilder.cs @@ -0,0 +1,22 @@ +using AssetRipper.IO.Endian; +using AssetRipper.IO.Files.SerializedFiles.Parser; + +namespace AssetRipper.IO.Files.SerializedFiles; + +public sealed class SerializedFileBuilder +{ + public FormatVersion Generation { get; set; } + public UnityVersion Version { get; set; } + public BuildTarget Platform { get; set; } + public EndianType EndianType { get; set; } + public List Dependencies { get; } = new(); + public List Objects { get; } = new(); + public List Types { get; } = new(); + public List RefTypes { get; } = new(); + public bool HasTypeTree { get; set; } + + public SerializedFile Build() + { + return SerializedFile.FromBuilder(this); + } +}