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 ///