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