CD-ROM (Volume Descriptor Set) support (#37)

* CD-ROM Wrapper

* Fix CDROMVolume Reader

* Fix SectorMode enum
This commit is contained in:
Deterous
2025-11-03 09:21:19 +09:00
committed by GitHub
parent c13df79848
commit ecbd147f6d
14 changed files with 513 additions and 0 deletions

View File

@@ -165,6 +165,11 @@ namespace ExtractionTool.Features
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);

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.CDROM
{
/// <summary>
/// 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
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-130_2nd_edition_june_1996.pdf"/>
public sealed class CDROM
{
/// <summary>
/// CD-ROM data tracks
/// </summary>
public DataTrack[] Tracks { get; set; } = [];
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using SabreTools.Data.Models.ISO9660;
namespace SabreTools.Data.Models.CDROM
{
/// <summary>
/// A CD-ROM data track containing a ISO9660 / ECMA-119 filesystem
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-130_2nd_edition_june_1996.pdf"/>
public sealed class DataTrack
{
/// <summary>
/// CD-ROM data sectors
/// </summary>
public Sector[] Sectors { get; set; } = [];
/// <summary>
/// ISO9660 volume within the data track
/// </summary>
public Volume Volume { get; set; }
}
}

View File

@@ -0,0 +1,42 @@
using System;
namespace SabreTools.Data.Models.CDROM
{
/// <summary>
/// Enum for a CD-ROM's sector mode
/// Explicitly does not contain non-CD-ROM modes like AUDIO, CDG, CDI, and length-specific modes
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-130_2nd_edition_june_1996.pdf"/>
public enum SectorMode
{
/// <summary>
/// CD-ROM Unknown Mode
/// </summary>
UNKNOWN,
/// <summary>
/// CD-ROM Mode 0 (All bytes after header are 0x00)
/// </summary>
MODE0,
/// <summary>
/// CD-ROM Mode 1
/// </summary>
MODE1,
/// <summary>
/// CD-ROM Mode 2 (Formless)
/// </summary>
MODE2,
/// <summary>
/// CD-ROM XA Mode 2 Form 1
/// </summary>
MODE2_FORM1,
/// <summary>
/// CD-ROM XA Mode 2 Form 2
/// </summary>
MODE2_FORM2,
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.CDROM
{
/// <summary>
/// A CD-ROM Mode1 sector
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-130_2nd_edition_june_1996.pdf"/>
public sealed class Mode1 : Sector
{
/// <summary>
/// User Data, 2048 bytes
/// </summary>
public byte[] UserData { get; set; } = new byte[2048];
/// <summary>
/// Error Detection Code, 4 bytes
/// </summary>
public byte[] EDC { get; set; } = new byte[4];
/// <summary>
/// Reserved 8 bytes
/// </summary>
public byte[] Intermediate { get; set; } = new byte[8];
/// <summary>
/// Error Correction Code, 4 bytes
/// </summary>
public byte[] ECC { get; set; } = new byte[276];
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.CDROM
{
/// <summary>
/// A CD-ROM Mode 2 Form 1 sector
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-130_2nd_edition_june_1996.pdf"/>
public sealed class Mode2Form1 : Sector
{
/// <summary>
/// Mode 2 subheader, 8 bytes
/// </summary>
public byte[] Subheader { get; set; } = new byte[8];
/// <summary>
/// User data, 2048 bytes
/// </summary>
public byte[] UserData { get; set; } = new byte[2048];
/// <summary>
/// Error Detection Code, 4 bytes
/// </summary>
public byte[] EDC { get; set; } = new byte[4];
/// <summary>
/// Error Correction Code, 4 bytes
/// </summary>
public byte[] ECC { get; set; } = new byte[276];
}
}

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.CDROM
{
/// <summary>
/// A CD-ROM Mode 2 Form 2 sector
/// Larger user data at expense of no error correction, just error detection
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-130_2nd_edition_june_1996.pdf"/>
public sealed class Mode2Form2 : Sector
{
/// <summary>
/// Mode 2 subheader, 8 bytes
/// </summary>
public byte[] Subheader { get; set; } = new byte[8];
/// <summary>
/// User data, 2324 bytes
/// </summary>
public byte[] UserData { get; set; } = new byte[2324];
/// <summary>
/// Error Detection Code, 4 bytes
/// </summary>
public byte[] EDC { get; set; } = new byte[4];
}
}

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.CDROM
{
/// <summary>
/// A CD-ROM sector
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-130_2nd_edition_june_1996.pdf"/>
public abstract class Sector
{
/// <summary>
/// Sync pattern, 12 bytes
/// </summary>
public byte[] SyncPattern { get; set; } = new byte[12];
/// <summary>
/// Sector Address, 3 bytes
/// </summary>
public byte[] Address { get; set; } = new byte[3];
/// <summary>
/// CD-ROM mode
/// </summary>
public byte Mode { get; set; }
}
}

View File

@@ -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
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Parse a Stream into the System Area
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled byte[] on success, null on error</returns>
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;
}
/// <summary>
/// Parse a CD-ROM Stream into the System Area
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled byte[] on success, null on error</returns>
public static VolumeDescriptor[]? ParseCDROMVolumeDescriptorSet(Stream data)
{
var obj = new List<VolumeDescriptor>();
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];
}
/// <summary>
/// Skip the header bytes of a CD-ROM sector
/// </summary>
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;
}
/// <summary>
/// Skip the trailer bytes of a CD-ROM sector
/// </summary>
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);
}
}
}
}

View File

@@ -19,6 +19,7 @@ namespace SabreTools.Serialization
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 @@ namespace SabreTools.Serialization
#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))

View File

@@ -0,0 +1,9 @@
namespace SabreTools.Serialization.Wrappers
{
public partial class CDROM : IExtractable
{
/// <inheritdoc/>
public override bool Extract(string outputDirectory, bool includeDebug)
=> false;
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Text;
using SabreTools.Data.Extensions;
namespace SabreTools.Serialization.Wrappers
{
public partial class CDROM : IPrintable
{
/// <inheritdoc/>
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);
}
}
}

View File

@@ -0,0 +1,90 @@
using System.IO;
using SabreTools.Data.Models.ISO9660;
namespace SabreTools.Serialization.Wrappers
{
public partial class CDROM : ISO9660
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "CD-ROM Data Track";
#endregion
#region Constructors
/// <inheritdoc/>
public CDROM(Volume model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public CDROM(Volume model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public CDROM(Volume model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public CDROM(Volume model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public CDROM(Volume model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public CDROM(Volume model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an CDROM data track from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the archive</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>A CDROM data track wrapper on success, null on failure</returns>
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);
}
/// <summary>
/// Create an CDROM data track from a Stream
/// </summary>
/// <param name="data">Stream representing the archive</param>
/// <returns>A CDROM data track wrapper on success, null on failure</returns>
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
}
}

View File

@@ -35,6 +35,11 @@ namespace SabreTools.Serialization.Wrappers
/// </summary>
BZip2,
/// <summary>
/// CD-ROM bin file
/// </summary>
CDROM,
/// <summary>
/// Compound File Binary
/// </summary>