diff --git a/ExtractionTool/Features/MainFeature.cs b/ExtractionTool/Features/MainFeature.cs
index 114a9ec8..0694eebb 100644
--- a/ExtractionTool/Features/MainFeature.cs
+++ b/ExtractionTool/Features/MainFeature.cs
@@ -165,6 +165,11 @@ private void ExtractFile(string file)
bzip2.Extract(OutputPath, Debug);
break;
+ // CD-ROM bin file
+ case CDROM cdrom:
+ cdrom.Extract(OutputPath, Debug);
+ break;
+
// CFB
case CFB cfb:
cfb.Extract(OutputPath, Debug);
diff --git a/SabreTools.Serialization/Models/CDROM/CDROM.cs b/SabreTools.Serialization/Models/CDROM/CDROM.cs
new file mode 100644
index 00000000..8edd1cea
--- /dev/null
+++ b/SabreTools.Serialization/Models/CDROM/CDROM.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.CDROM
+{
+ ///
+ /// A CD-ROM disc image, made up of multiple data tracks, ISO 10149 / ECMA-130
+ /// Specifically not a mixed-mode CD disc image, pure CD-ROM disc
+ ///
+ ///
+ public sealed class CDROM
+ {
+ ///
+ /// CD-ROM data tracks
+ ///
+ public DataTrack[] Tracks { get; set; } = [];
+ }
+}
diff --git a/SabreTools.Serialization/Models/CDROM/DataTrack.cs b/SabreTools.Serialization/Models/CDROM/DataTrack.cs
new file mode 100644
index 00000000..7feaf29d
--- /dev/null
+++ b/SabreTools.Serialization/Models/CDROM/DataTrack.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using SabreTools.Data.Models.ISO9660;
+
+namespace SabreTools.Data.Models.CDROM
+{
+ ///
+ /// A CD-ROM data track containing a ISO9660 / ECMA-119 filesystem
+ ///
+ ///
+ public sealed class DataTrack
+ {
+ ///
+ /// CD-ROM data sectors
+ ///
+ public Sector[] Sectors { get; set; } = [];
+
+ ///
+ /// ISO9660 volume within the data track
+ ///
+ public Volume Volume { get; set; }
+ }
+}
diff --git a/SabreTools.Serialization/Models/CDROM/Enums.cs b/SabreTools.Serialization/Models/CDROM/Enums.cs
new file mode 100644
index 00000000..2893f82d
--- /dev/null
+++ b/SabreTools.Serialization/Models/CDROM/Enums.cs
@@ -0,0 +1,42 @@
+using System;
+
+namespace SabreTools.Data.Models.CDROM
+{
+ ///
+ /// Enum for a CD-ROM's sector mode
+ /// Explicitly does not contain non-CD-ROM modes like AUDIO, CDG, CDI, and length-specific modes
+ ///
+ ///
+ public enum SectorMode
+ {
+ ///
+ /// CD-ROM Unknown Mode
+ ///
+ UNKNOWN,
+
+ ///
+ /// CD-ROM Mode 0 (All bytes after header are 0x00)
+ ///
+ MODE0,
+
+ ///
+ /// CD-ROM Mode 1
+ ///
+ MODE1,
+
+ ///
+ /// CD-ROM Mode 2 (Formless)
+ ///
+ MODE2,
+
+ ///
+ /// CD-ROM XA Mode 2 Form 1
+ ///
+ MODE2_FORM1,
+
+ ///
+ /// CD-ROM XA Mode 2 Form 2
+ ///
+ MODE2_FORM2,
+ }
+}
diff --git a/SabreTools.Serialization/Models/CDROM/Mode1.cs b/SabreTools.Serialization/Models/CDROM/Mode1.cs
new file mode 100644
index 00000000..e2bb2dba
--- /dev/null
+++ b/SabreTools.Serialization/Models/CDROM/Mode1.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.CDROM
+{
+ ///
+ /// A CD-ROM Mode1 sector
+ ///
+ ///
+ public sealed class Mode1 : Sector
+ {
+ ///
+ /// User Data, 2048 bytes
+ ///
+ public byte[] UserData { get; set; } = new byte[2048];
+
+ ///
+ /// Error Detection Code, 4 bytes
+ ///
+ public byte[] EDC { get; set; } = new byte[4];
+
+ ///
+ /// Reserved 8 bytes
+ ///
+ public byte[] Intermediate { get; set; } = new byte[8];
+
+ ///
+ /// Error Correction Code, 4 bytes
+ ///
+ public byte[] ECC { get; set; } = new byte[276];
+ }
+}
diff --git a/SabreTools.Serialization/Models/CDROM/Mode2Form1.cs b/SabreTools.Serialization/Models/CDROM/Mode2Form1.cs
new file mode 100644
index 00000000..d4bb4286
--- /dev/null
+++ b/SabreTools.Serialization/Models/CDROM/Mode2Form1.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.CDROM
+{
+ ///
+ /// A CD-ROM Mode 2 Form 1 sector
+ ///
+ ///
+ public sealed class Mode2Form1 : Sector
+ {
+ ///
+ /// Mode 2 subheader, 8 bytes
+ ///
+ public byte[] Subheader { get; set; } = new byte[8];
+
+ ///
+ /// User data, 2048 bytes
+ ///
+ public byte[] UserData { get; set; } = new byte[2048];
+
+ ///
+ /// Error Detection Code, 4 bytes
+ ///
+ public byte[] EDC { get; set; } = new byte[4];
+
+ ///
+ /// Error Correction Code, 4 bytes
+ ///
+ public byte[] ECC { get; set; } = new byte[276];
+ }
+}
diff --git a/SabreTools.Serialization/Models/CDROM/Mode2Form2.cs b/SabreTools.Serialization/Models/CDROM/Mode2Form2.cs
new file mode 100644
index 00000000..205afa55
--- /dev/null
+++ b/SabreTools.Serialization/Models/CDROM/Mode2Form2.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.CDROM
+{
+ ///
+ /// A CD-ROM Mode 2 Form 2 sector
+ /// Larger user data at expense of no error correction, just error detection
+ ///
+ ///
+ public sealed class Mode2Form2 : Sector
+ {
+ ///
+ /// Mode 2 subheader, 8 bytes
+ ///
+ public byte[] Subheader { get; set; } = new byte[8];
+
+ ///
+ /// User data, 2324 bytes
+ ///
+ public byte[] UserData { get; set; } = new byte[2324];
+
+ ///
+ /// Error Detection Code, 4 bytes
+ ///
+ public byte[] EDC { get; set; } = new byte[4];
+ }
+}
diff --git a/SabreTools.Serialization/Models/CDROM/Sector.cs b/SabreTools.Serialization/Models/CDROM/Sector.cs
new file mode 100644
index 00000000..1c0a33c6
--- /dev/null
+++ b/SabreTools.Serialization/Models/CDROM/Sector.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.CDROM
+{
+ ///
+ /// A CD-ROM sector
+ ///
+ ///
+ public abstract class Sector
+ {
+ ///
+ /// Sync pattern, 12 bytes
+ ///
+ public byte[] SyncPattern { get; set; } = new byte[12];
+
+ ///
+ /// Sector Address, 3 bytes
+ ///
+ public byte[] Address { get; set; } = new byte[3];
+
+ ///
+ /// CD-ROM mode
+ ///
+ public byte Mode { get; set; }
+ }
+}
diff --git a/SabreTools.Serialization/Readers/CDROMVolume.cs b/SabreTools.Serialization/Readers/CDROMVolume.cs
new file mode 100644
index 00000000..66c3a925
--- /dev/null
+++ b/SabreTools.Serialization/Readers/CDROMVolume.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using SabreTools.Data.Extensions;
+using SabreTools.Data.Models.CDROM;
+using SabreTools.Data.Models.ISO9660;
+using SabreTools.IO.Extensions;
+
+namespace SabreTools.Serialization.Readers
+{
+ public class CDROMVolume : ISO9660
+ {
+ #region Constants
+
+ private const int SectorSize = 2352;
+
+ #endregion
+
+ ///
+ public override Volume? Deserialize(Stream? data)
+ {
+ // If the data is invalid
+ if (data == null || !data.CanRead)
+ return null;
+
+ // Simple check for a valid stream length
+ if (SectorSize * (Constants.SystemAreaSectors + 2) > data.Length - data.Position)
+ return null;
+
+ try
+ {
+ // Create a new Volume to fill
+ var volume = new Volume();
+
+ // Read the System Area
+ volume.SystemArea = ParseCDROMSystemArea(data);
+
+ // Read the set of Volume Descriptors
+ var vdSet = ParseCDROMVolumeDescriptorSet(data);
+ if (vdSet == null || vdSet.Length == 0)
+ return null;
+
+ volume.VolumeDescriptorSet = vdSet;
+
+ // Only the VolumeDescriptorSet can be read using the CDROMVolume Reader
+ // TODO: CDROM Reader that outputs CDROM.DataTrack that uses custom Stream to wrap ISO9660 for Volume
+
+ return volume;
+ }
+ catch
+ {
+ // Ignore the actual error
+ return null;
+ }
+ }
+
+ ///
+ /// Parse a Stream into the System Area
+ ///
+ /// Stream to parse
+ /// Filled byte[] on success, null on error
+ public static byte[]? ParseCDROMSystemArea(Stream data)
+ {
+ var systemArea = new byte[Constants.SystemAreaSectors * Constants.MinimumSectorSize];
+ // Process in sectors
+ for (int i = 0; i < Constants.SystemAreaSectors; i++)
+ {
+ // Ignore sector header
+ var mode = SkipSectorHeader(data);
+
+ // Read user data
+ var userData = data.ReadBytes(Constants.MinimumSectorSize);
+
+ // Copy user data into System Area
+ Buffer.BlockCopy(userData, 0, systemArea, i * Constants.MinimumSectorSize, Constants.MinimumSectorSize);
+
+ // Ignore sector trailer
+ SkipSectorTrailer(data, mode);
+ }
+
+ return systemArea;
+ }
+
+ ///
+ /// Parse a CD-ROM Stream into the System Area
+ ///
+ /// Stream to parse
+ /// Filled byte[] on success, null on error
+ public static VolumeDescriptor[]? ParseCDROMVolumeDescriptorSet(Stream data)
+ {
+ var obj = new List();
+
+ bool setTerminated = false;
+ while (data.Position < data.Length)
+ {
+ // Ignore sector header
+ var mode = SkipSectorHeader(data);
+
+ var volumeDescriptor = ParseVolumeDescriptor(data, Constants.MinimumSectorSize);
+
+ // Ignore sector trailer
+ SkipSectorTrailer(data, mode);
+
+ // If no valid volume descriptor could be read, return the current set
+ if (volumeDescriptor == null)
+ return [.. obj];
+
+ // If the set has already been terminated and the returned volume descriptor is not another terminator,
+ // assume the read volume descriptor is not a valid volume descriptor and return the current set
+ if (setTerminated && volumeDescriptor.Type != VolumeDescriptorType.VOLUME_DESCRIPTOR_SET_TERMINATOR)
+ {
+ // Reset stream to before the just-read volume descriptor
+ data.SeekIfPossible(-SectorSize, SeekOrigin.Current);
+ return [.. obj];
+ }
+
+ // Add the valid read volume descriptor to the set
+ obj.Add(volumeDescriptor);
+
+ // If the set terminator was read, set the set terminated flag (further set terminators may be present)
+ if (!setTerminated && volumeDescriptor.Type == VolumeDescriptorType.VOLUME_DESCRIPTOR_SET_TERMINATOR)
+ setTerminated = true;
+ }
+
+ return [.. obj];
+ }
+
+ ///
+ /// Skip the header bytes of a CD-ROM sector
+ ///
+ private static SectorMode SkipSectorHeader(Stream data)
+ {
+ // Ignore sector header
+ _ = data.ReadBytes(15);
+
+ // Read sector mode
+ byte mode = data.ReadByteValue();
+ if (mode == 0)
+ return SectorMode.MODE0;
+ else if (mode == 1)
+ return SectorMode.MODE1;
+ else if (mode == 2)
+ {
+ // Ignore subheader
+ var subheader = data.ReadBytes(8);
+ if ((subheader[2] & 0x20) == 0x20)
+ return SectorMode.MODE2_FORM2;
+ else
+ return SectorMode.MODE2_FORM1;
+ }
+ else
+ return SectorMode.UNKNOWN;
+ }
+
+ ///
+ /// Skip the trailer bytes of a CD-ROM sector
+ ///
+ private static void SkipSectorTrailer(Stream data, SectorMode mode)
+ {
+ if (mode == SectorMode.MODE1 || mode == SectorMode.MODE0 || mode == SectorMode.UNKNOWN)
+ {
+ _ = data.ReadBytes(288);
+ }
+ else if (mode == SectorMode.MODE2 || mode == SectorMode.MODE2_FORM1 || mode == SectorMode.MODE2_FORM2)
+ {
+ // TODO: Better deal with Form 2
+ _ = data.ReadBytes(280);
+ }
+ }
+ }
+}
diff --git a/SabreTools.Serialization/WrapperFactory.cs b/SabreTools.Serialization/WrapperFactory.cs
index 9700abaa..40e24b06 100644
--- a/SabreTools.Serialization/WrapperFactory.cs
+++ b/SabreTools.Serialization/WrapperFactory.cs
@@ -19,6 +19,7 @@ public static class WrapperFactory
WrapperType.BFPK => BFPK.Create(data),
WrapperType.BSP => BSP.Create(data),
WrapperType.BZip2 => BZip2.Create(data),
+ WrapperType.CDROM => CDROM.Create(data),
WrapperType.CFB => CFB.Create(data),
WrapperType.CHD => CHD.Create(data),
WrapperType.CIA => CIA.Create(data),
@@ -202,6 +203,15 @@ public static WrapperType GetFileType(byte[]? magic, string? extension)
#endregion
+ #region CDROM
+
+ if (magic.StartsWith([0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) &&
+ (extension.Equals("bin", StringComparison.OrdinalIgnoreCase) ||
+ extension.Equals("skeleton", StringComparison.OrdinalIgnoreCase)))
+ return WrapperType.CDROM;
+
+ #endregion
+
#region CFB
if (magic.StartsWith(Data.Models.CFB.Constants.SignatureBytes))
diff --git a/SabreTools.Serialization/Wrappers/CDROM.Extraction.cs b/SabreTools.Serialization/Wrappers/CDROM.Extraction.cs
new file mode 100644
index 00000000..caa9fc12
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/CDROM.Extraction.cs
@@ -0,0 +1,9 @@
+namespace SabreTools.Serialization.Wrappers
+{
+ public partial class CDROM : IExtractable
+ {
+ ///
+ public override bool Extract(string outputDirectory, bool includeDebug)
+ => false;
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/CDROM.Printing.cs b/SabreTools.Serialization/Wrappers/CDROM.Printing.cs
new file mode 100644
index 00000000..d1c4ab98
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/CDROM.Printing.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Text;
+using SabreTools.Data.Extensions;
+
+namespace SabreTools.Serialization.Wrappers
+{
+ public partial class CDROM : IPrintable
+ {
+ ///
+ public new void PrintInformation(StringBuilder builder)
+ {
+ builder.AppendLine("CD-ROM Data Track Information:");
+ builder.AppendLine("-------------------------");
+ builder.AppendLine();
+
+ if (Model.SystemArea == null || Model.SystemArea.Length == 0)
+ builder.AppendLine(Model.SystemArea, "System Area");
+ else if (Array.TrueForAll(Model.SystemArea, b => b == 0))
+ builder.AppendLine("Zeroed", "System Area");
+ else
+ builder.AppendLine("Not Zeroed", "System Area");
+ builder.AppendLine();
+
+ Print(builder, Model.VolumeDescriptorSet);
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/CDROM.cs b/SabreTools.Serialization/Wrappers/CDROM.cs
new file mode 100644
index 00000000..6ce07a52
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/CDROM.cs
@@ -0,0 +1,90 @@
+using System.IO;
+using SabreTools.Data.Models.ISO9660;
+
+namespace SabreTools.Serialization.Wrappers
+{
+ public partial class CDROM : ISO9660
+ {
+ #region Descriptive Properties
+
+ ///
+ public override string DescriptionString => "CD-ROM Data Track";
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ public CDROM(Volume model, byte[] data) : base(model, data) { }
+
+ ///
+ public CDROM(Volume model, byte[] data, int offset) : base(model, data, offset) { }
+
+ ///
+ public CDROM(Volume model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
+
+ ///
+ public CDROM(Volume model, Stream data) : base(model, data) { }
+
+ ///
+ public CDROM(Volume model, Stream data, long offset) : base(model, data, offset) { }
+
+ ///
+ public CDROM(Volume model, Stream data, long offset, long length) : base(model, data, offset, length) { }
+
+ #endregion
+
+ #region Static Constructors
+
+ ///
+ /// Create an CDROM data track from a byte array and offset
+ ///
+ /// Byte array representing the archive
+ /// Offset within the array to parse
+ /// A CDROM data track wrapper on success, null on failure
+ public new static CDROM? Create(byte[]? data, int offset)
+ {
+ // If the data is invalid
+ if (data == null || data.Length == 0)
+ return null;
+
+ // If the offset is out of bounds
+ if (offset < 0 || offset >= data.Length)
+ return null;
+
+ // Create a memory stream and use that
+ var dataStream = new MemoryStream(data, offset, data.Length - offset);
+ return Create(dataStream);
+ }
+
+ ///
+ /// Create an CDROM data track from a Stream
+ ///
+ /// Stream representing the archive
+ /// A CDROM data track wrapper on success, null on failure
+ public new static CDROM? Create(Stream? data)
+ {
+ // If the data is invalid
+ if (data == null || !data.CanRead)
+ return null;
+
+ try
+ {
+ // Cache the current offset
+ long currentOffset = data.Position;
+
+ var model = new Readers.CDROMVolume().Deserialize(data);
+ if (model == null)
+ return null;
+
+ return new CDROM(model, data, currentOffset);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/WrapperType.cs b/SabreTools.Serialization/Wrappers/WrapperType.cs
index ce086fe3..bbf470fa 100644
--- a/SabreTools.Serialization/Wrappers/WrapperType.cs
+++ b/SabreTools.Serialization/Wrappers/WrapperType.cs
@@ -35,6 +35,11 @@ public enum WrapperType
///
BZip2,
+ ///
+ /// CD-ROM bin file
+ ///
+ CDROM,
+
///
/// Compound File Binary
///