diff --git a/SabreTools.Data.Models/PCEngineCDROM/BootSector.cs b/SabreTools.Data.Models/PCEngineCDROM/BootSector.cs
new file mode 100644
index 000000000..0fb895432
--- /dev/null
+++ b/SabreTools.Data.Models/PCEngineCDROM/BootSector.cs
@@ -0,0 +1,26 @@
+namespace SabreTools.Data.Models.PCEngineCDROM
+{
+ ///
+ /// Standard format for the first sector of the CD-ROM header
+ /// This is likely read by the system to verify the disc's authenticity
+ ///
+ public sealed class BootSector
+ {
+ ///
+ /// Copyright notice and credits
+ ///
+ /// Shift-JIS format, lines separated by NULL character, ends in NULL
+ public byte[] CopyrightString { get; set; } = new byte[806];
+
+ ///
+ /// HuC6280 machine code, presumably for initialization
+ /// Note that the actual Boot ROM is on the System Card
+ ///
+ public byte[] BootROM { get; set; } = new byte[432];
+
+ ///
+ /// Reserved zeroed padding bytes to fill remainder of sector
+ ///
+ public byte[] Padding { get; set; } = new byte[810];
+ }
+}
diff --git a/SabreTools.Data.Models/PCEngineCDROM/Constants.cs b/SabreTools.Data.Models/PCEngineCDROM/Constants.cs
new file mode 100644
index 000000000..e39b53ff1
--- /dev/null
+++ b/SabreTools.Data.Models/PCEngineCDROM/Constants.cs
@@ -0,0 +1,23 @@
+namespace SabreTools.Data.Models.PCEngineCDROM
+{
+ ///
+ /// PC Engine CDROM constant values and arrays
+ ///
+ public static class Constants
+ {
+ ///
+ /// Standard block size for PC Engine CDROM disc images
+ ///
+ public static readonly int SectorSize = 2048;
+
+ ///
+ /// Start of a PC Engine CDROM Header
+ ///
+ public static readonly byte[] MagicBytes = [0x82, 0xB1, 0x82, 0xCC, 0x83, 0x76, 0x83, 0x8D, 0x83, 0x4F, 0x83, 0x89, 0x83, 0x80, 0x82, 0xCC];
+
+ ///
+ /// Start of an empty CD-ROM pregap sector
+ ///
+ public static readonly byte[] PregapBytes = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
+ }
+}
diff --git a/SabreTools.Data.Models/PCEngineCDROM/Enums.cs b/SabreTools.Data.Models/PCEngineCDROM/Enums.cs
new file mode 100644
index 000000000..c32aefcc4
--- /dev/null
+++ b/SabreTools.Data.Models/PCEngineCDROM/Enums.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace SabreTools.Data.Models.PCEngineCDROM
+{
+ ///
+ /// Opening Mode flags
+ ///
+ [Flags]
+ public enum OpenMode : byte
+ {
+ ///
+ /// "Data Read to VRAM"
+ /// Bit set if data is to be read to VRAM
+ ///
+ READ_TO_VRAM = 0x01,
+
+ ///
+ /// "Data Read to ADPCM Buffer"
+ /// Bit set if data is to be read to the ADPCM buffer
+ ///
+ READ_TO_ADPCM = 0x02,
+
+ ///
+ /// "BG Display"
+ /// Bit set if background display is off
+ /// Unset if it should be on
+ ///
+ DISPLAY_OFF = 0x20,
+
+ ///
+ /// "ADPCM Play"
+ /// Bit set if ADPCM should not play
+ /// Unset if it should play
+ ///
+ ADCPM_DISABLED = 0x40,
+
+ ///
+ /// "ADPCM Play Mode"
+ /// Bit set if ADPCM should play on repeat
+ /// Unset if it should play only once
+ ///
+ ADPCM_REPEAT = 0x80,
+
+ ///
+ /// Mask for bits that are reserved
+ ///
+ RESERVED_MASK = 0x1C,
+ }
+}
diff --git a/SabreTools.Data.Models/PCEngineCDROM/Header.cs b/SabreTools.Data.Models/PCEngineCDROM/Header.cs
new file mode 100644
index 000000000..f2d642e2e
--- /dev/null
+++ b/SabreTools.Data.Models/PCEngineCDROM/Header.cs
@@ -0,0 +1,19 @@
+namespace SabreTools.Data.Models.PCEngineCDROM
+{
+ ///
+ /// This is the standard header of PC Engine CD-ROM
+ /// Located at the first non-zero user data sector of the first data track
+ ///
+ public sealed class Header
+ {
+ ///
+ /// Standard sector of data to verify the disc
+ ///
+ public BootSector BootSector { get; set; } = new();
+
+ ///
+ /// Initial Program Loader for the PC Engine
+ ///
+ public IPL IPL { get; set; } = new();
+ }
+}
diff --git a/SabreTools.Data.Models/PCEngineCDROM/IPL.cs b/SabreTools.Data.Models/PCEngineCDROM/IPL.cs
new file mode 100644
index 000000000..28443add5
--- /dev/null
+++ b/SabreTools.Data.Models/PCEngineCDROM/IPL.cs
@@ -0,0 +1,133 @@
+using SabreTools.Numerics;
+
+namespace SabreTools.Data.Models.PCEngineCDROM
+{
+ ///
+ /// IPL (Initial Program Loader) Block Data Format
+ /// Format information from the Hu7 CD System BIOS Manual "CD-ROM BIOS Ver1.00"
+ ///
+ public sealed class IPL
+ {
+ ///
+ /// "Load Start Record Number of CD"
+ /// "Top record no. where the program is contained"
+ /// The offset (entry point) of the initial start up program machine code
+ ///
+ /// Big-Endian
+ public UInt24 IPLBLK { get; set; } = new();
+
+ ///
+ /// "Load Block Length of CD"
+ /// "No. of records for program to read"
+ /// Number of sectors to read for the start up program, from the entry point
+ ///
+ public byte IPLBLN { get; set; }
+
+ ///
+ /// "Program Load Address"
+ /// "Main memory address for program read"
+ /// Address for where the program code should be placed into memory
+ ///
+ /// Little-Endian
+ public ushort IPLSTA { get; set; }
+
+ ///
+ /// "Program Execute Address"
+ /// "Starting address of execution after program read"
+ /// Address for where the program code should be executed from memory, after reading
+ ///
+ /// Little-Endian
+ public ushort IPLJMP { get; set; }
+
+ ///
+ /// "IPL Set MPR2-6 (+ max_mapping)"
+ /// "Bank no. to set to MPR before program read"
+ /// "When calling BIOS or using interrupt routine from BIOS, MPR0,1,7 cannot be changed"
+ /// Memory Page Register
+ /// MPR0 (I/O, $0000 to $1FFF)
+ /// MPR1 (WORK RAM, $2000 to $3FFF)
+ /// MPR2-6 (USER AREA, $4000 to $5FFF, ..., $C000 to $DFFF)
+ /// MPR7 (BIOS ROM, $E000 to $FFFF)
+ ///
+ public byte[] IPLMPR { get; set; } = new byte[5];
+
+ ///
+ /// "Opening mode"
+ ///
+ public OpenMode OpenMode { get; set; }
+
+ ///
+ /// "Opening Graphic Data Record Number"
+ /// "Specifies the top record of data to load"
+ ///
+ /// Big-Endian
+ public UInt24 GRPBLK { get; set; } = new();
+
+ ///
+ /// "Opening Graphic Data Length"
+ /// "Specifies the total record that contains color palette data, BAT data, and BG font data"
+ ///
+ public byte GRPBLN { get; set; }
+
+ ///
+ /// "Opening Graphic Data Read Address"
+ /// "Specifies the top VRAM address into which BG font data is read"
+ ///
+ /// Little-Endian
+ public ushort GRPADR { get; set; }
+
+ ///
+ /// "Opening ADPCM Data Record Number"
+ /// "Specifies the top record of data to load"
+ ///
+ /// Big-Endian
+ public UInt24 ADPBLK { get; set; } = new();
+
+ ///
+ /// "Opening ADPCM Data Length"
+ /// "Specifies the number of ADPCM data record"
+ ///
+ public byte ADPBLN { get; set; }
+
+ ///
+ /// "Opening ADPCM Sampling Rate"
+ /// "Specifies the ADPCM sampling rate"
+ ///
+ public byte ADPRATE { get; set; }
+
+ ///
+ /// Reserved bytes, zeroed
+ ///
+ /// 7 bytes
+ public byte[] Reserved { get; set; } = new byte[7];
+
+ ///
+ /// "ID String"
+ /// Null-terminated ASCII string
+ /// "PC Engine CD-ROM SYSTEM\0"
+ ///
+ /// 24 bytes, NULL terminated
+ public byte[] SystemString { get; set; } = new byte[24];
+
+ ///
+ /// Null-terminated ASCII string
+ /// "Copyright HUDSON SOFT / NEC Home Electronics,Ltd.\0"
+ ///
+ /// 50 bytes, NULL terminated
+ public byte[] CopyrightString { get; set; } = new byte[50];
+
+ ///
+ /// "Program Name"
+ /// ASCII string, not null-terminated
+ ///
+ /// 16 bytes, padding with trailing SPACE \x20
+ public byte[] ProgramName { get; set; } = new byte[16];
+
+ ///
+ /// Additional optional string
+ /// ASCII string, not null-terminated
+ ///
+ /// 6 bytes, padding with trailing SPACE \x20
+ public byte[] AdditionalString { get; set; } = new byte[16];
+ }
+}
diff --git a/SabreTools.Serialization.Readers.Test/PCEngineCDROMTests.cs b/SabreTools.Serialization.Readers.Test/PCEngineCDROMTests.cs
new file mode 100644
index 000000000..ea57d32dd
--- /dev/null
+++ b/SabreTools.Serialization.Readers.Test/PCEngineCDROMTests.cs
@@ -0,0 +1,72 @@
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace SabreTools.Serialization.Readers.Test
+{
+ public class PCEngineCDROMTests
+ {
+ [Fact]
+ public void NullArray_Null()
+ {
+ byte[]? data = null;
+ int offset = 0;
+ var deserializer = new PCEngineCDROM();
+
+ var actual = deserializer.Deserialize(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyArray_Null()
+ {
+ byte[]? data = [];
+ int offset = 0;
+ var deserializer = new PCEngineCDROM();
+
+ var actual = deserializer.Deserialize(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidArray_Null()
+ {
+ byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)];
+ int offset = 0;
+ var deserializer = new PCEngineCDROM();
+
+ var actual = deserializer.Deserialize(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void NullStream_Null()
+ {
+ Stream? data = null;
+ var deserializer = new PCEngineCDROM();
+
+ var actual = deserializer.Deserialize(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyStream_Null()
+ {
+ Stream? data = new MemoryStream([]);
+ var deserializer = new PCEngineCDROM();
+
+ var actual = deserializer.Deserialize(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidStream_Null()
+ {
+ Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]);
+ var deserializer = new PCEngineCDROM();
+
+ var actual = deserializer.Deserialize(data);
+ Assert.Null(actual);
+ }
+ }
+}
diff --git a/SabreTools.Serialization.Readers/PCEngineCDROM.cs b/SabreTools.Serialization.Readers/PCEngineCDROM.cs
new file mode 100644
index 000000000..ae5f76849
--- /dev/null
+++ b/SabreTools.Serialization.Readers/PCEngineCDROM.cs
@@ -0,0 +1,90 @@
+using System.IO;
+using System.Text;
+using SabreTools.Data.Models.PCEngineCDROM;
+using SabreTools.IO.Extensions;
+using SabreTools.Matching;
+using SabreTools.Numerics.Extensions;
+
+namespace SabreTools.Serialization.Readers
+{
+ public class PCEngineCDROM : BaseBinaryReader
+ {
+ ///
+ public override Header? Deserialize(Stream? data)
+ {
+ // If the data is invalid
+ if (data is null || !data.CanRead)
+ return null;
+
+ // Simple check for a valid stream length
+ if (data.Length - data.Position < 2 * Constants.SectorSize)
+ return null;
+
+ try
+ {
+ // Verify expected signature
+ var magic = data.PeekBytes(16);
+ if (!magic.EqualsExactly(Constants.MagicBytes))
+ return null;
+
+ // Deserialize the Header
+ var header = new Header();
+ header.BootSector = ParseBootSector(data);
+ header.IPL = ParseIPL(data);
+
+ return header;
+ }
+ catch
+ {
+ // Ignore the actual error
+ return null;
+ }
+ }
+
+ ///
+ /// Parse a Stream into a BootSector
+ ///
+ /// Stream to parse
+ /// Filled BootSector on success, null on error
+ public static BootSector ParseBootSector(Stream data)
+ {
+ var bootSector = new BootSector();
+
+ bootSector.CopyrightString = data.ReadBytes(806);
+ bootSector.BootROM = data.ReadBytes(432);
+ bootSector.Padding = data.ReadBytes(810);
+
+ return bootSector;
+ }
+
+ ///
+ /// Parse a Stream into a IPL
+ ///
+ /// Stream to parse
+ /// Filled IPL on success, null on error
+ public static IPL ParseIPL(Stream data)
+ {
+ var ipl = new IPL();
+
+ ipl.IPLBLK = data.ReadUInt24BigEndian();
+ ipl.IPLBLN = data.ReadByteValue();
+ ipl.IPLSTA = data.ReadUInt16LittleEndian();
+ ipl.IPLJMP = data.ReadUInt16LittleEndian();
+ ipl.IPLMPR = data.ReadBytes(5);
+ ipl.OpenMode = (OpenMode)data.ReadByteValue();
+ ipl.GRPBLK = data.ReadUInt24BigEndian();
+ ipl.GRPBLN = data.ReadByteValue();
+ ipl.GRPADR = data.ReadUInt16LittleEndian();
+ ipl.ADPBLK = data.ReadUInt24BigEndian();
+ ipl.ADPBLN = data.ReadByteValue();
+ ipl.ADPRATE = data.ReadByteValue();
+ ipl.Reserved = data.ReadBytes(7);
+ ipl.SystemString = data.ReadBytes(24);
+ ipl.CopyrightString = data.ReadBytes(50);
+ ipl.ProgramName = data.ReadBytes(16);
+ ipl.AdditionalString = data.ReadBytes(6);
+
+ return ipl;
+ }
+ }
+}
diff --git a/SabreTools.Wrappers.Test/PCEngineCDROMTests.cs b/SabreTools.Wrappers.Test/PCEngineCDROMTests.cs
new file mode 100644
index 000000000..0011316f4
--- /dev/null
+++ b/SabreTools.Wrappers.Test/PCEngineCDROMTests.cs
@@ -0,0 +1,60 @@
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace SabreTools.Wrappers.Test
+{
+ public class PCEngineCDROMTests
+ {
+ [Fact]
+ public void NullArray_Null()
+ {
+ byte[]? data = null;
+ int offset = 0;
+ var actual = PCEngineCDROM.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyArray_Null()
+ {
+ byte[]? data = [];
+ int offset = 0;
+ var actual = PCEngineCDROM.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidArray_Null()
+ {
+ byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)];
+ int offset = 0;
+ var actual = PCEngineCDROM.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void NullStream_Null()
+ {
+ Stream? data = null;
+ var actual = PCEngineCDROM.Create(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyStream_Null()
+ {
+ Stream? data = new MemoryStream([]);
+ var actual = PCEngineCDROM.Create(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidStream_Null()
+ {
+ Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]);
+ var actual = PCEngineCDROM.Create(data);
+ Assert.Null(actual);
+ }
+ }
+}
diff --git a/SabreTools.Wrappers.Test/PCEngineDiscImageTests.cs b/SabreTools.Wrappers.Test/PCEngineDiscImageTests.cs
new file mode 100644
index 000000000..894788384
--- /dev/null
+++ b/SabreTools.Wrappers.Test/PCEngineDiscImageTests.cs
@@ -0,0 +1,60 @@
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace SabreTools.Wrappers.Test
+{
+ public class PCEngineDiscImageTests
+ {
+ [Fact]
+ public void NullArray_Null()
+ {
+ byte[]? data = null;
+ int offset = 0;
+ var actual = PCEngineDiscImage.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyArray_Null()
+ {
+ byte[]? data = [];
+ int offset = 0;
+ var actual = PCEngineDiscImage.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidArray_Null()
+ {
+ byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)];
+ int offset = 0;
+ var actual = PCEngineDiscImage.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void NullStream_Null()
+ {
+ Stream? data = null;
+ var actual = PCEngineDiscImage.Create(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyStream_Null()
+ {
+ Stream? data = new MemoryStream([]);
+ var actual = PCEngineDiscImage.Create(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidStream_Null()
+ {
+ Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]);
+ var actual = PCEngineDiscImage.Create(data);
+ Assert.Null(actual);
+ }
+ }
+}
diff --git a/SabreTools.Wrappers/PCEngineCDROM.Printing.cs b/SabreTools.Wrappers/PCEngineCDROM.Printing.cs
new file mode 100644
index 000000000..b7c71d951
--- /dev/null
+++ b/SabreTools.Wrappers/PCEngineCDROM.Printing.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using SabreTools.Data.Models.PCEngineCDROM;
+using SabreTools.Text.Extensions;
+
+namespace SabreTools.Wrappers
+{
+ public partial class PCEngineCDROM : IPrintable
+ {
+#if NETCOREAPP
+ ///
+ public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
+#else
+ ///
+ public string ExportJSON() => Newtonsoft.Json.JsonConvert.SerializeObject(Model, _jsonSerializerOptions);
+#endif
+
+ ///
+ public void PrintInformation(StringBuilder builder)
+ {
+ builder.AppendLine("PC Engine CD-ROM Header Information:");
+ builder.AppendLine("-------------------------");
+ builder.AppendLine();
+
+ Print(builder, Model.BootSector);
+ Print(builder, Model.IPL);
+ }
+
+ internal static void Print(StringBuilder builder, BootSector bootSector)
+ {
+ builder.AppendLine(" Boot Sector:");
+ builder.AppendLine(" -------------------------");
+
+#if NET462_OR_GREATER || NETCOREAPP
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+#endif
+
+ var message = Environment.NewLine + " " + Encoding.GetEncoding(932).GetString(bootSector.CopyrightString).Replace("\0", Environment.NewLine + " ");
+
+ builder.AppendLine(message, " Copyright String");
+ builder.AppendLine(bootSector.BootROM, " HuC6280 Machine Code");
+ if (bootSector.Padding is null)
+ builder.AppendLine(bootSector.Padding, " Padding Bytes");
+ else if (Array.TrueForAll(bootSector.Padding, b => b == 0))
+ builder.AppendLine("Zeroed", " Padding Bytes");
+ else
+ builder.AppendLine("Not Zeroed", " Padding Bytes");
+
+ builder.AppendLine();
+ }
+
+ internal static void Print(StringBuilder builder, IPL ipl)
+ {
+ builder.AppendLine(" Initial Program Loader:");
+ builder.AppendLine(" -------------------------");
+
+ builder.AppendLine((uint)ipl.IPLBLK, " Load Start Record Number of CD");
+ builder.AppendLine(ipl.IPLBLN, " Load Block Length of CD");
+ builder.AppendLine(ipl.IPLSTA, " Program Load Address");
+ builder.AppendLine(ipl.IPLJMP, " Program Execute Address");
+ builder.AppendLine(ipl.IPLMPR, " Memory Page Register (2-6)");
+ builder.AppendLine((byte)ipl.OpenMode, " OpenMode");
+ builder.AppendLine((uint)ipl.GRPBLK, " Opening Graphic Data Record Number");
+ builder.AppendLine(ipl.GRPBLN, " Opening Graphic Data Length");
+ builder.AppendLine(ipl.GRPADR, " Opening Graphic Data Read Address");
+ builder.AppendLine((uint)ipl.ADPBLK, " Opening ADPCM Data Record Number");
+ builder.AppendLine(ipl.ADPBLN, " Opening ADPCM Data Length");
+ builder.AppendLine(ipl.ADPRATE, " Opening ADPCM Sampling Rate");
+ if (ipl.Reserved is not null && Array.TrueForAll(ipl.Reserved, b => b == 0))
+ builder.AppendLine($"Zeroed", " Reserved Bytes");
+ else
+ builder.AppendLine(ipl.Reserved, " Reserved Bytes");
+
+ builder.AppendLine(Encoding.GetEncoding(932).GetString(ipl.SystemString), " ID String");
+ builder.AppendLine(Encoding.GetEncoding(932).GetString(ipl.CopyrightString), " Copyright String");
+ builder.AppendLine(Encoding.GetEncoding(932).GetString(ipl.ProgramName), " Program Name");
+ builder.AppendLine(Encoding.GetEncoding(932).GetString(ipl.AdditionalString), " Additional String");
+
+ builder.AppendLine();
+ }
+ }
+}
diff --git a/SabreTools.Wrappers/PCEngineCDROM.cs b/SabreTools.Wrappers/PCEngineCDROM.cs
new file mode 100644
index 000000000..fb6ac2a36
--- /dev/null
+++ b/SabreTools.Wrappers/PCEngineCDROM.cs
@@ -0,0 +1,123 @@
+using System.Collections.Generic;
+using System.IO;
+using SabreTools.Data.Models.PCEngineCDROM;
+using SabreTools.IO.Extensions;
+using SabreTools.Matching;
+using SabreTools.Numerics.Extensions;
+
+namespace SabreTools.Wrappers
+{
+ public partial class PCEngineCDROM : WrapperBase
+ {
+ #region Descriptive Properties
+
+ ///
+ public override string DescriptionString => "PC Engine CD-ROM² / TurboGrafx-CD Header";
+
+ #endregion
+
+ #region Extension Properties
+
+ ///
+ public BootSector BootSector => Model.BootSector;
+
+ ///
+ public IPL IPL => Model.IPL;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ public PCEngineCDROM(Header model, byte[] data) : base(model, data) { }
+
+ ///
+ public PCEngineCDROM(Header model, byte[] data, int offset) : base(model, data, offset) { }
+
+ ///
+ public PCEngineCDROM(Header model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
+
+ ///
+ public PCEngineCDROM(Header model, Stream data) : base(model, data) { }
+
+ ///
+ public PCEngineCDROM(Header model, Stream data, long offset) : base(model, data, offset) { }
+
+ ///
+ public PCEngineCDROM(Header model, Stream data, long offset, long length) : base(model, data, offset, length) { }
+
+ #endregion
+
+ #region Static Constructors
+
+ ///
+ /// Create a PCEngineCDROM Header from a byte array and offset
+ ///
+ /// Byte array representing the PCEngineCDROM Header
+ /// Offset within the array to parse
+ /// A PCEngineCDROM Header wrapper on success, null on failure
+ public static PCEngineCDROM? Create(byte[]? data, int offset)
+ {
+ // If the data is invalid
+ if (data is 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 a PCEngineCDROM Header from a Stream
+ ///
+ /// Stream representing the PCEngineCDROM Header
+ /// A PCEngineCDROM Header wrapper on success, null on failure
+ public static PCEngineCDROM? Create(Stream? data)
+ {
+ // If the data is invalid
+ if (data is null || !data.CanRead)
+ return null;
+
+ try
+ {
+ // Quit early if no data in stream
+ if (data.Length - data.Position < 2 * Constants.SectorSize)
+ return null;
+
+ // PC Engine CD-ROM header can exist after some amount of pre-gap (check 250 sectors)
+ for (int i = 0; i < 250; i++)
+ {
+ byte[] startBytes = data.PeekBytes(16);
+ if (startBytes.EqualsExactly(Constants.MagicBytes))
+ break;
+ else if (startBytes.EqualsExactly(Constants.PregapBytes) && data.Length - data.Position >= 3 * Constants.SectorSize)
+ data.SeekIfPossible(Constants.SectorSize, SeekOrigin.Current);
+ else
+ return null;
+ }
+
+ // Cache the current offset (after the pregap)
+ long currentOffset = data.Position;
+
+ var model = new Serialization.Readers.PCEngineCDROM().Deserialize(data);
+ if (model is null)
+ return null;
+
+ // Reset stream
+ data.Seek(currentOffset, SeekOrigin.Begin);
+
+ return new PCEngineCDROM(model, data, currentOffset);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/SabreTools.Wrappers/PCEngineDiscImage.cs b/SabreTools.Wrappers/PCEngineDiscImage.cs
new file mode 100644
index 000000000..2927a4f9d
--- /dev/null
+++ b/SabreTools.Wrappers/PCEngineDiscImage.cs
@@ -0,0 +1,126 @@
+using System.Collections.Generic;
+using System.IO;
+using SabreTools.Data.Models.PCEngineCDROM;
+using SabreTools.IO.Extensions;
+using SabreTools.Matching;
+using SabreTools.Numerics.Extensions;
+
+namespace SabreTools.Wrappers
+{
+ public partial class PCEngineDiscImage : WrapperBase
+ {
+ #region Descriptive Properties
+
+ ///
+ public override string DescriptionString => "PC Engine CD-ROM² / TurboGrafx-CD Disc Image";
+
+ #endregion
+
+ #region Extension Properties
+
+ ///
+ public BootSector BootSector => Model.BootSector;
+
+ ///
+ public IPL IPL => Model.IPL;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ public PCEngineDiscImage(Header model, byte[] data) : base(model, data) { }
+
+ ///
+ public PCEngineDiscImage(Header model, byte[] data, int offset) : base(model, data, offset) { }
+
+ ///
+ public PCEngineDiscImage(Header model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
+
+ ///
+ public PCEngineDiscImage(Header model, Stream data) : base(model, data) { }
+
+ ///
+ public PCEngineDiscImage(Header model, Stream data, long offset) : base(model, data, offset) { }
+
+ ///
+ public PCEngineDiscImage(Header model, Stream data, long offset, long length) : base(model, data, offset, length) { }
+
+ #endregion
+
+ #region Static Constructors
+
+ ///
+ /// Create a PCEngineCDROM Header from a byte array and offset
+ ///
+ /// Byte array representing the PCEngineCDROM Header
+ /// Offset within the array to parse
+ /// A PCEngineCDROM Header wrapper on success, null on failure
+ public static PCEngineCDROM? Create(byte[]? data, int offset)
+ {
+ // If the data is invalid
+ if (data is 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 a PCEngineCDROM Header from a Stream
+ ///
+ /// Stream representing the PCEngineCDROM Header
+ /// A PCEngineCDROM Header wrapper on success, null on failure
+ public static PCEngineCDROM? Create(Stream? data)
+ {
+ // If the data is invalid
+ if (data is null || !data.CanRead)
+ return null;
+
+ try
+ {
+ // Create user data sub-stream
+ var userData = new Data.Extensions.CDROMExtensions.UserDataStream(data);
+
+ // Quit early if no data in stream
+ if (userData.Length - userData.Position < 2 * Constants.SectorSize)
+ return null;
+
+ // PC Engine CD-ROM header can exist after some amount of pre-gap (check 250 sectors)
+ for (int i = 0; i < 250; i++)
+ {
+ byte[] startBytes = userData.PeekBytes(16);
+ if (startBytes.EqualsExactly(Constants.MagicBytes))
+ break;
+ else if (startBytes.EqualsExactly(Constants.PregapBytes) && userData.Length - userData.Position >= 3 * Constants.SectorSize)
+ userData.SeekIfPossible(Constants.SectorSize, SeekOrigin.Current);
+ else
+ return null;
+ }
+
+ // Cache the current offset (after the pregap)
+ long currentOffset = userData.Position;
+
+ var model = new Serialization.Readers.PCEngineCDROM().Deserialize(userData);
+ if (model is null)
+ return null;
+
+ // Reset stream
+ userData.Seek(currentOffset, SeekOrigin.Begin);
+
+ return new PCEngineCDROM(model, userData, currentOffset);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/SabreTools.Wrappers/WrapperFactory.cs b/SabreTools.Wrappers/WrapperFactory.cs
index e4015d422..f4cef0b7e 100644
--- a/SabreTools.Wrappers/WrapperFactory.cs
+++ b/SabreTools.Wrappers/WrapperFactory.cs
@@ -99,6 +99,14 @@ public static class WrapperFactory
// Cache the current offset
long initialOffset = stream.Position;
+ // Attempt to open standard ISO9660 CDROM wrapper
+ var cdromWrapper = CDROM.Create(stream);
+ if (cdromWrapper is not null)
+ return cdromWrapper;
+
+ // Reset position in stream
+ stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
+
// Try to get a 3DO / M2 disc image wrapper
var operaDiscImageWrapper = OperaDiscImage.Create(stream);
if (operaDiscImageWrapper is not null)
@@ -107,12 +115,14 @@ public static class WrapperFactory
// Reset position in stream
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
- // Fallback to standard CDROM wrapper
- var cdromWrapper = CDROM.Create(stream);
- if (cdromWrapper is null)
- return null;
+ // Try to get a PC Engine CDROM wrapper
+ // This reads a lot for detection, do this step last
+ var pcEngineDiscImageWrapper = PCEngineDiscImage.Create(stream);
+ if (pcEngineDiscImageWrapper is not null)
+ return pcEngineDiscImageWrapper;
- return cdromWrapper;
+ // No known filesystems found
+ return null;
}
///
@@ -129,7 +139,7 @@ public static class WrapperFactory
// Cache the current offset
long initialOffset = stream.Position;
- // Try to get an Xbox disc image wrapper
+ // Try to get an Xbox disc image wrapper (must be before ISO9660)
var xboxWrapper = XboxISO.Create(stream);
if (xboxWrapper is not null)
return xboxWrapper;
@@ -137,6 +147,14 @@ public static class WrapperFactory
// Reset position in stream
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
+ // Try to standard ISO9660 wrapper
+ var isoWrapper = ISO9660.Create(stream);
+ if (isoWrapper is not null)
+ return isoWrapper;
+
+ // Reset position in stream
+ stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
+
// Try to get a 3DO / M2 disc image wrapper
var operaFSWrapper = OperaFS.Create(stream);
if (operaFSWrapper is not null)
@@ -145,12 +163,14 @@ public static class WrapperFactory
// Reset position in stream
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
- // Fallback to standard ISO9660 wrapper
- var isoWrapper = ISO9660.Create(stream);
- if (isoWrapper is null)
- return null;
+ // Try to get a PC Engine CDROM disc image wrapper
+ // This reads a lot for detection, do this step last
+ var pcEngineCDROMWrapper = PCEngineCDROM.Create(stream);
+ if (pcEngineCDROMWrapper is not null)
+ return pcEngineCDROMWrapper;
- return isoWrapper;
+ // No known filesystems found
+ return null;
}
///
@@ -335,6 +355,14 @@ public static WrapperType GetFileType(byte[]? magic, string? extension)
return WrapperType.CDROM;
}
+ // Some CD-ROM images have no sync bytes in pregap
+ if (magic.StartsWith([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
+ && (extension.Equals("bin", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals("skeleton", StringComparison.OrdinalIgnoreCase)))
+ {
+ return WrapperType.CDROM;
+ }
+
#endregion
#region CFB