From dab850103bfcc7a3870c692aad5ffe27b55b3f7a Mon Sep 17 00:00:00 2001 From: Deterous <138427222+Deterous@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:41:55 +0900 Subject: [PATCH] XDVDFS FileWriter (#83) --- .../XDVDFS/LayoutDescriptor.cs | 2 +- .../XDVDFS/VolumeDescriptor.cs | 1 + SabreTools.Serialization.Readers/XDVDFS.cs | 14 +- .../XDVDFSTests.cs | 16 +++ SabreTools.Serialization.Writers/XDVDFS.cs | 135 ++++++++++++++++++ SabreTools.Wrappers/XDVDFS.Printing.cs | 2 +- SabreTools.Wrappers/XDVDFS.Writing.cs | 30 ++++ 7 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 SabreTools.Serialization.Writers.Test/XDVDFSTests.cs create mode 100644 SabreTools.Serialization.Writers/XDVDFS.cs create mode 100644 SabreTools.Wrappers/XDVDFS.Writing.cs diff --git a/SabreTools.Data.Models/XDVDFS/LayoutDescriptor.cs b/SabreTools.Data.Models/XDVDFS/LayoutDescriptor.cs index b01b01f0..43f5dd8b 100644 --- a/SabreTools.Data.Models/XDVDFS/LayoutDescriptor.cs +++ b/SabreTools.Data.Models/XDVDFS/LayoutDescriptor.cs @@ -19,7 +19,7 @@ namespace SabreTools.Data.Models.XDVDFS /// Seemingly unused 8 bytes after the signature, should be zeroed /// /// 8 bytes - public byte[] Unusued8Bytes { get; set; } = new byte[8]; + public byte[] Unused8Bytes { get; set; } = new byte[8]; /// /// Version number of xblayout(?) tool used to master the filesystem diff --git a/SabreTools.Data.Models/XDVDFS/VolumeDescriptor.cs b/SabreTools.Data.Models/XDVDFS/VolumeDescriptor.cs index 0bdbf024..f4a4e097 100644 --- a/SabreTools.Data.Models/XDVDFS/VolumeDescriptor.cs +++ b/SabreTools.Data.Models/XDVDFS/VolumeDescriptor.cs @@ -23,6 +23,7 @@ namespace SabreTools.Data.Models.XDVDFS /// /// UInt32 size of the root directory descriptor in bytes /// + /// Little-endian public uint RootSize { get; set; } /// diff --git a/SabreTools.Serialization.Readers/XDVDFS.cs b/SabreTools.Serialization.Readers/XDVDFS.cs index 68875df2..b4145c32 100644 --- a/SabreTools.Serialization.Readers/XDVDFS.cs +++ b/SabreTools.Serialization.Readers/XDVDFS.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using SabreTools.Data.Models.XDVDFS; using SabreTools.IO.Extensions; +using SabreTools.Matching; using SabreTools.Numerics.Extensions; #pragma warning disable IDE0017 // Simplify object initialization @@ -94,7 +95,7 @@ namespace SabreTools.Serialization.Readers var signature = System.Text.Encoding.ASCII.GetString(obj.Signature); if (!signature.Equals(Constants.LayoutDescriptorSignature)) return null; - obj.Unusued8Bytes = data.ReadBytes(8); + obj.Unused8Bytes = data.ReadBytes(8); obj.XBLayoutVersion = ParseFourPartVersionType(data); obj.XBPremasterVersion = ParseFourPartVersionType(data); @@ -208,7 +209,8 @@ namespace SabreTools.Serialization.Readers // If invalid record read or next descriptor cannot fit in the current sector, skip ahead if (dr is null || (data.Position - initialOffset) % Constants.SectorSize > (Constants.SectorSize - Constants.MinimumRecordLength)) { - data.SeekIfPossible(Constants.SectorSize - (int)((data.Position - initialOffset) % Constants.SectorSize), SeekOrigin.Current); + int padding = Constants.SectorSize - (int)((data.Position - initialOffset) % Constants.SectorSize); + obj.Padding = data.ReadBytes(padding); continue; } @@ -219,6 +221,7 @@ namespace SabreTools.Serialization.Readers obj.DirectoryRecords = [.. records]; + // Previously set padding assumed to be all 0xFF up to sector boundaries int remainder = Constants.SectorSize - (int)(size % Constants.SectorSize); if (remainder > 0 && remainder < Constants.SectorSize) obj.Padding = data.ReadBytes(remainder); @@ -235,11 +238,12 @@ namespace SabreTools.Serialization.Readers { var obj = new DirectoryRecord(); - obj.LeftChildOffset = data.ReadUInt16LittleEndian(); - obj.RightChildOffset = data.ReadUInt16LittleEndian(); - if (obj.LeftChildOffset == 0xFFFF && obj.RightChildOffset == 0xFFFF) + byte[] start = data.PeekBytes(4); + if (start.EqualsExactly([0xFF, 0xFF, 0xFF, 0xFF])) return null; + obj.LeftChildOffset = data.ReadUInt16LittleEndian(); + obj.RightChildOffset = data.ReadUInt16LittleEndian(); obj.ExtentOffset = data.ReadUInt32LittleEndian(); obj.ExtentSize = data.ReadUInt32LittleEndian(); obj.FileFlags = (FileFlags)data.ReadByteValue(); diff --git a/SabreTools.Serialization.Writers.Test/XDVDFSTests.cs b/SabreTools.Serialization.Writers.Test/XDVDFSTests.cs new file mode 100644 index 00000000..7db95b6c --- /dev/null +++ b/SabreTools.Serialization.Writers.Test/XDVDFSTests.cs @@ -0,0 +1,16 @@ +using System.IO; +using Xunit; + +namespace SabreTools.Serialization.Writers.Test +{ + public class XDVDFSTests + { + [Fact] + public void SerializeFile_Null_False() + { + var serializer = new XDVDFS(); + bool actual = serializer.SerializeFile(null, null); + Assert.False(actual); + } + } +} diff --git a/SabreTools.Serialization.Writers/XDVDFS.cs b/SabreTools.Serialization.Writers/XDVDFS.cs new file mode 100644 index 00000000..aba77810 --- /dev/null +++ b/SabreTools.Serialization.Writers/XDVDFS.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; +using System.Text; +using SabreTools.Data.Models.XDVDFS; +using SabreTools.IO; +using SabreTools.IO.Extensions; +using SabreTools.Numerics; +using SabreTools.Numerics.Extensions; + +namespace SabreTools.Serialization.Writers +{ + public class XDVDFS : IFileWriter + { + /// + public bool Debug { get; set; } = false; + + /// + public bool SerializeFile(Volume? obj, string? path) + { + if (string.IsNullOrEmpty(path)) + return false; + + if(obj is null || !ValidateVolume(obj)) + return false; + + // Create the file stream + using var fs = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None); + + SerializeHeader(fs, obj); + + // Loop over all directory descriptors in order of offset + uint[] keys = new uint[obj.DirectoryDescriptors.Count]; + obj.DirectoryDescriptors.Keys.CopyTo(keys, 0); + Array.Sort(keys); + + // Write directory descriptors + for (int i = 0; i < keys.Length; i++) + { + uint sectorOffset = keys[i]; + fs.Seek(sectorOffset * Constants.SectorSize, SeekOrigin.Begin); + SerializeDirectoryDescriptor(fs, obj.DirectoryDescriptors[sectorOffset]); + } + + fs.Flush(); + + return true; + } + + public static bool ValidateVolume(Volume? obj) + { + // If the data is invalid + if (obj?.VolumeDescriptor?.StartSignature is null) + return false; + + // If the magic doesn't match + string magic = Encoding.ASCII.GetString(obj.VolumeDescriptor.StartSignature); + if (magic != Constants.VolumeDescriptorSignature) + return false; + + // Validate model + if (obj.ReservedArea.Length != 0x10000) + return false; + if (obj.VolumeDescriptor.Reserved.Length != 1991) + return false; + if (obj.VolumeDescriptor.EndSignature.Length != 20) + return false; + if (obj.LayoutDescriptor is not null) + { + if (obj.LayoutDescriptor.Signature.Length != 24) + return false; + if (obj.LayoutDescriptor.Unused8Bytes.Length != 8) + return false; + if (obj.LayoutDescriptor.Reserved.Length != 1968) + return false; + } + + return true; + } + + public static void SerializeHeader(Stream stream, Volume obj) + { + stream.Write(obj.ReservedArea, 0, obj.ReservedArea.Length); + + stream.Write(obj.VolumeDescriptor.StartSignature, 0, obj.VolumeDescriptor.StartSignature.Length); + stream.WriteLittleEndian(obj.VolumeDescriptor.RootOffset); + stream.WriteLittleEndian(obj.VolumeDescriptor.RootSize); + stream.WriteLittleEndian(obj.VolumeDescriptor.MasteringTimestamp); + stream.WriteByte(obj.VolumeDescriptor.UnknownByte); + stream.Write(obj.VolumeDescriptor.Reserved, 0, obj.VolumeDescriptor.Reserved.Length); + stream.Write(obj.VolumeDescriptor.EndSignature, 0, obj.VolumeDescriptor.EndSignature.Length); + + if (obj.LayoutDescriptor is not null) + { + stream.Write(obj.LayoutDescriptor.Signature, 0, obj.LayoutDescriptor.Signature.Length); + stream.Write(obj.LayoutDescriptor.Unused8Bytes, 0, obj.LayoutDescriptor.Unused8Bytes.Length); + SerializeFourPartVersionType(stream, obj.LayoutDescriptor.XBLayoutVersion); + SerializeFourPartVersionType(stream, obj.LayoutDescriptor.XBPremasterVersion); + SerializeFourPartVersionType(stream, obj.LayoutDescriptor.XBGameDiscVersion); + SerializeFourPartVersionType(stream, obj.LayoutDescriptor.XBOther1Version); + SerializeFourPartVersionType(stream, obj.LayoutDescriptor.XBOther2Version); + SerializeFourPartVersionType(stream, obj.LayoutDescriptor.XBOther3Version); + stream.Write(obj.LayoutDescriptor.Reserved, 0, obj.LayoutDescriptor.Reserved.Length); + } + } + + public static void SerializeFourPartVersionType(Stream stream, FourPartVersionType obj) + { + stream.WriteLittleEndian(obj.Major); + stream.WriteLittleEndian(obj.Minor); + stream.WriteLittleEndian(obj.Build); + stream.WriteLittleEndian(obj.Revision); + } + + public static void SerializeDirectoryDescriptor(Stream stream, DirectoryDescriptor obj) + { + foreach (var dr in obj.DirectoryRecords) + SerializeDirectoryRecord(stream, dr); + if (obj.Padding is not null && obj.Padding.Length > 0) + stream.Write(obj.Padding, 0, obj.Padding.Length); + } + + public static void SerializeDirectoryRecord(Stream stream, DirectoryRecord obj) + { + stream.WriteLittleEndian(obj.LeftChildOffset); + stream.WriteLittleEndian(obj.RightChildOffset); + stream.WriteLittleEndian(obj.ExtentOffset); + stream.WriteLittleEndian(obj.ExtentSize); + stream.WriteByte((byte)obj.FileFlags); + stream.WriteByte(obj.FilenameLength); + stream.Write(obj.Filename, 0, obj.Filename.Length); + if (obj.Padding is not null && obj.Padding.Length > 0) + stream.Write(obj.Padding, 0, obj.Padding.Length); + } + } +} diff --git a/SabreTools.Wrappers/XDVDFS.Printing.cs b/SabreTools.Wrappers/XDVDFS.Printing.cs index 60b9a114..fa62a20e 100644 --- a/SabreTools.Wrappers/XDVDFS.Printing.cs +++ b/SabreTools.Wrappers/XDVDFS.Printing.cs @@ -68,7 +68,7 @@ namespace SabreTools.Wrappers builder.AppendLine(" -------------------------"); builder.AppendLine(Encoding.ASCII.GetString(ld.Signature), " Signature"); - builder.AppendLine(ld.Unusued8Bytes, " Unusued 8 Bytes"); + builder.AppendLine(ld.Unused8Bytes, " Unusued 8 Bytes"); builder.AppendLine(GetVersionString(ld.XBLayoutVersion), " xblayout Version"); builder.AppendLine(GetVersionString(ld.XBPremasterVersion), " xbpremaster Version"); builder.AppendLine(GetVersionString(ld.XBGameDiscVersion), " xbgamedisc Version"); diff --git a/SabreTools.Wrappers/XDVDFS.Writing.cs b/SabreTools.Wrappers/XDVDFS.Writing.cs new file mode 100644 index 00000000..959a5f05 --- /dev/null +++ b/SabreTools.Wrappers/XDVDFS.Writing.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using SabreTools.Data.Models.XDVDFS; +using SabreTools.Numerics.Extensions; + +namespace SabreTools.Wrappers +{ + public partial class XDVDFS : IWritable + { + /// + public bool Write(string outputDirectory, bool includeDebug) + { + // Get the base path + string outputFilename = Filename is null + ? Guid.NewGuid().ToString() + : Path.GetFileName(Filename); + outputFilename += ".xiso"; + string outputPath = Path.Combine(outputDirectory, outputFilename); + + var writer = new SabreTools.Serialization.Writers.XDVDFS(); + if (!writer.SerializeFile(Model, outputPath)) + { + if (includeDebug) Console.WriteLine("Model was invalid, cannot write!"); + return false; + } + + return true; + } + } +}