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;
+ }
+ }
+}