diff --git a/ExtractionTool/Features/MainFeature.cs b/ExtractionTool/Features/MainFeature.cs index 763fb463..36dc8eaf 100644 --- a/ExtractionTool/Features/MainFeature.cs +++ b/ExtractionTool/Features/MainFeature.cs @@ -190,6 +190,11 @@ namespace ExtractionTool.Features iscab.Extract(OutputPath, Debug); break; + // ISO 9660 volume + case ISO9660 iso9660: + iso9660.Extract(OutputPath, Debug); + break; + // LZ-compressed file, KWAJ variant case LZKWAJ kwaj: kwaj.Extract(OutputPath, Debug); diff --git a/SabreTools.Serialization/Extensions/ISO9660.cs b/SabreTools.Serialization/Extensions/ISO9660.cs new file mode 100644 index 00000000..0db4e8f0 --- /dev/null +++ b/SabreTools.Serialization/Extensions/ISO9660.cs @@ -0,0 +1,38 @@ +using SabreTools.Data.Models.ISO9660; + +namespace SabreTools.Data.Extensions +{ + public static class ISO9660 + { + public static short GetLogicalBlockSize(this BaseVolumeDescriptor bvd, short sectorLength) + { + short blockLength = sectorLength; + if (bvd.LogicalBlockSize.IsValid) + { + // Validate logical block length + if (bvd.LogicalBlockSize >= 512 && bvd.LogicalBlockSize <= sectorLength && (bvd.LogicalBlockSize & (bvd.LogicalBlockSize - 1)) == 0) + blockLength = bvd.LogicalBlockSize; + } + else + { + // If logical block size is ambiguous check if only one is valid, otherwise default to sector length + short le = bvd.LogicalBlockSize.LittleEndian; + short be = bvd.LogicalBlockSize.LittleEndian; + bool le_valid = true; + bool be_valid = true; + if (le < 512 || le > sectorLength || (le & (le - 1)) != 0) + le_valid = false; + if (be < 512 || be > sectorLength || (be & (be - 1)) != 0) + be_valid = false; + if (le_valid && !be_valid) + blockLength = le; + else if (be_valid && !le_valid) + blockLength = be; + else + blockLength = sectorLength; + } + + return blockLength; + } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/BaseVolumeDescriptor.cs b/SabreTools.Serialization/Models/ISO9660/BaseVolumeDescriptor.cs new file mode 100644 index 00000000..3725528d --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/BaseVolumeDescriptor.cs @@ -0,0 +1,209 @@ +using SabreTools.Numerics; + +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Abstract Volume Descriptor with common fields used by Primary/Supplementary/Enhanced Volume Descriptors + /// + /// + public abstract class BaseVolumeDescriptor : VolumeDescriptor + { + // Virtual variable of 1 byte goes here + // PrimaryVolumeDescriptor: UnusedByte + // SupplementaryVolumeDescriptor: VolumeFlags + + /// + /// 32-byte name of the intended system + /// Primary: a-characters only, padded to the right with spaces + /// Supplementary: a1-characters only, padded to the right with spaces + /// Enhanced: Some other agreed upon character encoding, padded to the right with filler + /// + public byte[] SystemIdentifier { get; set; } + + /// + /// 32-byte name of the volume + /// Primary: d-characters only, padded to the right with spaces + /// Supplementary: d1-characters only, padded to the right with spaces + /// Enhanced: Some other agreed upon character encoding, padded to the right with filler + /// + public byte[] VolumeIdentifier { get; set; } + + /// + /// 8 unused bytes at offset 72, should be all 0x00 + /// + public byte[] Unused8Bytes { get; set; } + + /// + /// Number of logical blocks in this volume + /// + public BothInt32 VolumeSpaceSize { get; set; } + + // Virtual variable of 32 bytes goes here: + // PrimaryVolumeDescriptor: Unused32Bytes + // SupplementaryVolumeDescriptor: EscapeSequences + + /// + /// Number of Volumes (discs) in this VolumeSet + /// + public BothInt16 VolumeSetSize { get; set; } + + /// + /// Volume (disc) number in this volume set + /// + public BothInt16 VolumeSequenceNumber { get; set; } + + /// + /// Number of bytes per logical block, usually 2048 + /// Must be a power of 2, minimum 2^9, and not greater than the logical sector size + /// + public BothInt16 LogicalBlockSize { get; set; } + + /// + /// Number of bytes in the path table + /// + public BothInt32 PathTableSize { get; set; } + + /// + /// Sector number of the start of the little-endian path table, type L + /// Stored as int32-LSB + /// + public int PathTableLocationL { get; set; } + + /// + /// Sector number of the start of the optional little-endian path table, type L + /// The "optional path table" does not exist if this value is 0 + /// Stored as int32-LSB + /// + public int OptionalPathTableLocationL { get; set; } + + /// + /// Sector number of the start of the big-endian path table, type M + /// Stored as int32-MSB + /// + public int PathTableLocationM { get; set; } + + /// + /// Sector number of the start of the optional big-endian path table, type M + /// The "optional path table" Does not exist if this value is 0 + /// Stored as int32-MSB + /// + public int OptionalPathTableLocationM { get; set; } + + /// + /// Root directory entry, 34 bytes + /// DirectoryIdentifier = 0x00 + /// + public DirectoryRecord RootDirectoryRecord { get; set; } + + /// + /// 128-byte name of the volume set + /// If not specified, all spaces (0x20) + /// Primary: d-characters only, padded to the right with spaces + /// Supplementary: d1-characters only, padded to the right with spaces + /// Enhanced: Some other agreed upon character encoding, padded to the right with filler + /// + public byte[] VolumeSetIdentifier { get; set; } + + /// + /// 128-byte name of the publisher + /// If specified, starts with 0x5F, followed by filename of file in root directory + /// If not specified, all spaces (0x20) + /// Primary: a-characters only, padded to the right with spaces + /// Supplementary: a1-characters only, padded to the right with spaces + /// Enhanced: Some other agreed upon character encoding, padded to the right with filler + /// + public byte[] PublisherIdentifier { get; set; } + + /// + /// 128-byte name of the data preparer + /// If specified, starts with 0x5F, followed by filename of file in root directory + /// If not specified, all spaces (0x20) + /// Primary: a-characters only, padded to the right with spaces + /// Supplementary: a1-characters only, padded to the right with spaces + /// Enhanced: Some other agreed upon character encoding, padded to the right with filler + /// + public byte[] DataPreparerIdentifier { get; set; } + + /// + /// 128-byte name of the application + /// If specified, starts with 0x5F, followed by filename of file in root directory + /// If not specified, all spaces (0x20) + /// Primary: a-characters only, padded to the right with spaces + /// Supplementary: a1-characters only, padded to the right with spaces + /// Enhanced: Some other agreed upon character encoding, padded to the right with filler + /// + public byte[] ApplicationIdentifier { get; set; } + + /// + /// 37-byte filename of the Copyright file + /// If specified, filename of a file in root directory + /// If not specified, all spaces (0x20) + /// Primary: d-characters only, padded to the right with spaces + /// Supplementary: d1-characters only, padded to the right with spaces + /// Enhanced: Some other agreed upon character encoding, padded to the right with filler + /// + public byte[] CopyrightFileIdentifier { get; set; } + + /// + /// 37-byte filename of the Abstract file + /// If specified, filename of a file in root directory + /// If not specified, all spaces (0x20) + /// Primary: d-characters only, padded to the right with spaces + /// Supplementary: d1-characters only, padded to the right with spaces + /// Enhanced: Some other agreed upon character encoding, padded to the right with filler + /// + public byte[] AbstractFileIdentifier { get; set; } + + /// + /// 37-byte filename of the Bibliographic file + /// If specified, filename of a file in root directory + /// If not specified, all spaces (0x20) + /// Primary: d-characters only, padded to the right with spaces + /// Supplementary: d1-characters only, padded to the right with spaces + /// Enhanced: Some other agreed upon character encoding, padded to the right with filler + /// + public byte[] BibliographicFileIdentifier { get; set; } + + /// + /// PVD-style DateTime format for the Creation date/time of the Volume + /// + public DecDateTime VolumeCreationDateTime { get; set; } + + /// + /// PVD-style DateTime format for the Modification date/time of the Volume + /// + public DecDateTime VolumeModificationDateTime { get; set; } + + /// + /// PVD-style DateTime format for the Expiration date/time of the Volume + /// + public DecDateTime VolumeExpirationDateTime { get; set; } + + /// + /// PVD-style DateTime format for the Effective date/time of the Volume + /// + public DecDateTime VolumeEffectiveDateTime { get; set; } + + /// + /// Version number of the Records / Path Table format + /// For Primary/Supplementary, this is 0x01 + /// For Enhanced, this is 0x02 + /// + public byte FileStructureVersion { get; set; } + + /// + /// 1 reserved byte, should be 0x00 + /// + public byte ReservedByte { get; set; } + + /// + /// 512 bytes for Application Use, contents not defined by ISO9660 + /// + public byte[] ApplicationUse { get; set; } + + /// + /// 653 reserved bytes, should be all 0x00 + /// + public byte[] Reserved653Bytes { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/BootRecordVolumeDescriptor.cs b/SabreTools.Serialization/Models/ISO9660/BootRecordVolumeDescriptor.cs new file mode 100644 index 00000000..0ebfffb8 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/BootRecordVolumeDescriptor.cs @@ -0,0 +1,27 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Boot Record Volume Descriptor + /// Volume Descriptor with VolumeDescriptorType = 0x00 + /// + /// + public sealed class BootRecordVolumeDescriptor : VolumeDescriptor + { + /// + /// 32-byte name of the intended system that can use this record + /// a-characters only + /// + public byte[] BootSystemIdentifier { get; set; } + + /// + /// 32-byte name of this boot system + /// a-characters only + /// + public byte[] BootIdentifier { get; set; } + + /// + /// 1997 bytes for Boot System Use, contents not defined by ISO9660 + /// + public byte[] BootSystemUse { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/Constants.cs b/SabreTools.Serialization/Models/ISO9660/Constants.cs new file mode 100644 index 00000000..0eecfc12 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/Constants.cs @@ -0,0 +1,118 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// ISO9660 filesystem extent + /// + /// + public static class Constants + { + #region Volume Descriptor Constants + + /// + /// Minimum size of a logical sector + /// + public const int MinimumSectorSize = 2048; + + /// + /// Number of logical sectors in the System Area + /// + public const int SystemAreaSectors = 16; + + /// + /// Identifier used for ISO9660, "CD001" + /// + public static readonly byte[] StandardIdentifier = [0x43, 0x44, 0x30, 0x30, 0x31]; + + /// + /// File Identifier of the current directory + /// + public static readonly byte[] CurrentDirectory = [0x00]; + + /// + /// File Identifier of the parent directory + /// + public static readonly byte[] ParentDirectory = [0x01]; + + #endregion + + #region CD-i Constants + + /// + /// Identifier present on non-ISO9660 CD-i discs, "CD-I " + /// + public static readonly byte[] StandardIdentifierCDI = [0x43, 0x44, 0x2D, 0x49, 0x20]; + + #endregion + + #region Primary/Supplementary Volume Descriptors Constants + + /// + /// Character used for separating a file name from a file extension, Fullstop character "." + /// This value is used in Primary Volume Descriptors + /// + public const byte Separator1 = 0x2E; + + /// + /// Character used for separating the file name/extension, from the file version number, Semicolon character ";" + /// This value is used in Primary Volume Descriptors + /// + public const byte Separator2 = 0x3B; + + /// + /// Character used for padding a byte array on the right, Space character " " + /// This value is used in Primary/Supplementary Volume Descriptors + /// + public const byte Filler = 0x20; + + /// + /// Valid a-characters: A subset of 57 ASCII characters including A-Z, 0-9, and some special characters (0x20-0x22, 0x25-0x3F, 0x41-0x5A, 0x5F) + /// A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 _ ! " % & ' ( ) * + , - . / : ; < = > ? + /// Note: a1-characters are a user-defined subset of c-characters (UCS-2) + /// Note: Joliet extension implies all MSB UCS-2 characters except control characters and * / : ; ? \ (0x0000-0x001F, 0x002A, 0x002F, 0x003A, 0x003B, 0x003F, 0x005C) + /// + public static readonly byte[] ValidACharacters = [0x20, 0x21, 0x22, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5F]; + + /// + /// Valid d-characters: A subset of 37 ASCII characters including A-Z, 0-9, and underscore only (0x30-0x3F, 0x41-0x5A, 0x5F) + /// A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 _ + /// Note: d1-characters are a user-defined subset of c-characters (UCS-2) + /// Note: Joliet extension implies all MSB UCS-2 characters except control characters and * / : ; ? \ (0x0000-0x001F, 0x002A, 0x002F, 0x003A, 0x003B, 0x003F, 0x005C) + /// + public static readonly byte[] ValidDCharacters = [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x78, 0x59, 0x5A, 0x5F]; + + #endregion + + #region Joliet Extension Constants + + /// + /// UCS-2 Character used for separating a file name from a file extension, Fullstop character "." + /// This is used in Joliet-style Enhanced Volume Descriptors + /// + public static readonly byte[] JolietSeparator1 = [0x00, 0x2E]; + + /// + /// UCS-2 Character used for separating the file name/extension, from the file version number, Semicolon character ";" + /// This is used in Joliet-style Enhanced Volume Descriptors + /// + public static readonly byte[] JolietSeparator2 = [0x00, 0x3B]; + + /// + /// Character used for padding a byte array on the right, null character + /// This is used in Joliet-style Enhanced Volume Descriptors + /// + public const byte JolietFiller = 0x20; + + /// + /// Joliet extension uses Enhanced Volume Descriptor with this VolumeFlags value + /// + public const byte JolietVolumeFlags = 0x00; + + /// + /// Joliet extension uses Enhanced Volume Descriptor with this EscapeSequences value + /// Escape Sequences: (25 2F 40) (25 2F 43) (25 2F 45) + /// + public static readonly byte[] JolietEscapeSequences = [0x25, 0x2F, 0x40, 0x25, 0x2F, 0x43, 0x25, 0x2F, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + + #endregion + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/DecDateTime.cs b/SabreTools.Serialization/Models/ISO9660/DecDateTime.cs new file mode 100644 index 00000000..07ea83f6 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/DecDateTime.cs @@ -0,0 +1,54 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Datetime format represented by decimal ASCII + /// - Base (Primary/Supplementary/Enhanced) Volume Descriptor + /// - Extended Attribute Record + /// + /// + public sealed class DecDateTime + { + /// + /// 4-byte ASCII digits + /// + public byte[] Year { get; set; } + + /// + /// 2-byte ASCII digits + /// + public byte[] Month { get; set; } + + /// + /// 2-byte ASCII digits + /// + public byte[] Day { get; set; } + + /// + /// 2-byte ASCII digits + /// + public byte[] Hour { get; set; } + + /// + /// 2-byte ASCII digits + /// + public byte[] Minute { get; set; } + + /// + /// 2-byte ASCII digits + /// + public byte[] Second { get; set; } + + /// + /// 2-byte ASCII digits + /// + public byte[] Centisecond { get; set; } + + /// + /// Time zone offset (from GMT = UTC 0), represented by a single byte + /// Unit = 15min offset + /// 0 = offset of -12 hours (UTC-12) + /// 100 = offset of +13 hours (UTC+13) + /// + public byte TimezoneOffset { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/DirectoryExtent.cs b/SabreTools.Serialization/Models/ISO9660/DirectoryExtent.cs new file mode 100644 index 00000000..884a9be7 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/DirectoryExtent.cs @@ -0,0 +1,14 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// ISO9660 Directory Extent containing file and directory descriptors parsed from the file extent into directory records + /// + /// + public sealed class DirectoryExtent : FileExtent + { + /// + /// Directory records (each a descriptor of a directory or a file) + /// + public DirectoryRecord[] DirectoryRecords { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/DirectoryRecord.cs b/SabreTools.Serialization/Models/ISO9660/DirectoryRecord.cs new file mode 100644 index 00000000..f958b6cf --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/DirectoryRecord.cs @@ -0,0 +1,88 @@ +using SabreTools.Numerics; + +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// ISO9660 Directory Record, a directory descriptor that points to an extent representing a file or directory + /// + /// + public sealed class DirectoryRecord + { + /// + /// Length of Directory Record + /// + public byte DirectoryRecordLength { get; set; } + + /// + /// Length of the extended attribute record + /// If no extended attribute record is used, set to 0x00 + /// + public byte ExtendedAttributeRecordLength { get; set; } + + /// + /// Logical block number of the first logical block allocated to this extent + /// + public BothInt32 ExtentLocation { get; set; } + + /// + /// Number of bytes allocated to this extent + /// + public BothInt32 ExtentLength { get; set; } + + /// + /// Datetime of recording for the Directory Record + /// If not specified, all values are 0x00 + /// + public DirectoryRecordDateTime RecordingDateTime { get; set; } + + /// + /// Flags for indicating attributes of the directory record + /// + public FileFlags FileFlags { get; set; } + + /// + /// Assigned file unit size for the file section (interleaved mode) + /// 0x00 if the file is not recorded in interleaved mode + /// + public byte FileUnitSize { get; set; } + + /// + /// Assigned interleave gap size for the file section (interleaved mode) + /// 0x00 if the file is not recorded in interleaved mode + /// + public byte InterleaveGapSize { get; set; } + + /// + /// Volume sequence ordinal number of the volume in the volume set on which the record extent is recorded + /// + public BothInt16 VolumeSequenceNumber { get; set; } + + /// + /// Length of the FileIdentifier field in bytes + /// + public byte FileIdentifierLength { get; set; } + + /// + /// If FileFlags.Directory is 1, this is the name of the directory + /// If FileFlags.Directory is 0, this is the name of the file + /// File: Uses either d-characters or d1-characters and Separator1 and Separator2 + /// Directory: Uses either d-characters or d1-characters, or: + /// Is exactly Constants.CurrentDirectory (0x00) or Constants.ParentDirectory (0x01) + /// + public byte[] FileIdentifier { get; set; } + + /// + /// If record length prior to this is odd, the FileIdentifier is followed by a single padding byte (0x00) + /// Optional field + /// + public byte? PaddingField { get; set; } + + /// + /// Optional bytes at the end of a directory record for system use + /// Must be an even number of bytes long (pad with a single 0x00 to make it even) + /// Note: This is where SUSP contents are located, including Rock Ridge extension + /// Optional field + /// + public byte[]? SystemUse { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/DirectoryRecordDateTime.cs b/SabreTools.Serialization/Models/ISO9660/DirectoryRecordDateTime.cs new file mode 100644 index 00000000..4184e4fb --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/DirectoryRecordDateTime.cs @@ -0,0 +1,47 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Datetime format used by ISO9660 DirectoryRecord + /// + /// + public sealed class DirectoryRecordDateTime + { + /// + /// Number of years since 1900 + /// + public byte YearsSince1990 { get; set; } + + /// + /// Month of the year, 1-12 + /// + public byte Month { get; set; } + + /// + /// Day of the month, 1-31 + /// + public byte Day { get; set; } + + /// + /// Hour of the day, 0-23 + /// + public byte Hour { get; set; } + + /// + /// Minute of the hour, 0-59 + /// + public byte Minute { get; set; } + + /// + /// Second of the minute, 0-59 + /// + public byte Second { get; set; } + + /// + /// Time zone offset (from GMT = UTC 0), represented by a single byte + /// Unit = 15min offset + /// 0 = offset of -12 hours (UTC-12) + /// 100 = offset of +13 hours (UTC+13) + /// + public byte TimezoneOffset { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/Enums.cs b/SabreTools.Serialization/Models/ISO9660/Enums.cs new file mode 100644 index 00000000..d42409a7 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/Enums.cs @@ -0,0 +1,227 @@ +using System; + +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Enum for VolumeDescriptor.Type + /// All values 4-254 are reserved + /// + /// + public enum VolumeDescriptorType : byte + { + /// + /// Primary Volume Descriptor + /// + BOOT_RECORD_VOLUME_DESCRIPTOR = 0x00, + + /// + /// Primary Volume Descriptor + /// + PRIMARY_VOLUME_DESCRIPTOR = 0x01, + + /// + /// Supplementary Volume Descriptor + /// + SUPPLEMENTARY_VOLUME_DESCRIPTOR = 0x02, + + /// + /// Enhanced Volume Descriptor (including Joliet extensions) + /// + ENHANCED_VOLUME_DESCRIPTOR = SUPPLEMENTARY_VOLUME_DESCRIPTOR, + + /// + /// Volume Partition Descriptor + /// + VOLUME_PARTITION_DESCRIPTOR = 0x03, + + /// + /// Volume Descriptor Set Terminator + /// + VOLUME_DESCRIPTOR_SET_TERMINATOR = 0xFF, + } + + /// + /// Enum for DirectoryRecord.FileFlags + /// Flag 6 (Bit 5 LSB): Reserved: 0 + /// Flag 7 (Bit 5 LSB): Reserved: 0 + /// + /// + [Flags] + public enum FileFlags : byte + { + /// + /// Flag 1 (Bit 0 LSB): Existence + /// 1 if file should be hidden from the user upon inquiry + /// 0 otherwise + /// + EXISTENCE = 0x01, + + /// + /// Flag 2 (Bit 1 LSB): Directory + /// 1 if the directory Record identifies a directory + /// 0 otherwise + /// + DIRECTORY = 0x02, + + /// + /// Flag 3 (Bit 2 LSB): Associated File + /// 1 if the file is an associated file + /// 0 otherwise + /// + ASSOCIATED_FILE = 0x04, + + /// + /// Flag 4 (Bit 3 LSB): Record + /// 1 if file has record format specified by non-zero record format of extended attribute record + /// 0 otherwise + /// + RECORD = 0x08, + + /// + /// Flag 5 (Bit 4 LSB): Protection + /// 1 if owner/group ID is set for the file and permissions field is set properly + /// 0 otherwise + /// + PROTECTION = 0x10, + + /// + /// Flag 6 (Bit 5 LSB): Reserved by ISO9660 + /// + RESERVED_BIT5 = 0x20, + + /// + /// Flag 7 (Bit 6 LSB): Reserved by ISO9660 + /// + RESERVED_BIT6 = 0x40, + + /// + /// Flag 8 (Bit 7 LSB): Multi-extent + /// 1 if Directory Extent is not the final record for the file + /// 0 otherwise + /// + MULTI_EXTENT = 0x80, + } + + /// + /// Enum for SupplementaryVolumeDescriptor.VolumeFlags + /// Flag 1 (Bit 0, LSB) is used + /// All other flags/bits are reserved (0x00) + /// + /// + [Flags] + public enum VolumeFlags : byte + { + /// + /// Flag 1 (Bit 0, LSB): + /// 1 if SupplementaryVolumeDescriptor.EscapeSequences has at least 1 unregistered escape sequence + /// 0 if all escape sequences in SupplementaryVolumeDescriptor.EscapeSequences are registered + /// + UNREGISTERED_ESCAPE_SEQUENCES = 0x01, + } + + /// + /// Enum for ExtendedAttributeRecord.Permissions + /// Every 2nd bit is fixed to 1 (i.e. minimum value of 0xAAAA) + /// + /// + [Flags] + public enum Permissions : ushort + { + /// + /// Flag 1 (Bit 0): 1 if system users may not read, 0 otherwise + /// + SYSTEM_USER_CANNOT_READ = 0b0000_0000_0000_0001, + + /// + /// Flag 2 (Bit 2): 1 if system users may not execute, 0 otherwise + /// + SYSTEM_USER_CANNOT_EXECUTE = 0b0000_0000_0000_0100, + + /// + /// Flag 3 (Bit 4): 1 if owner may not read, 0 otherwise + /// + OWNER_CANNOT_READ = 0b0000_0000_0001_0000, + + /// + /// Flag 4 (Bit 6): 1 if owner may not execute, 0 otherwise + /// + OWNER_CANNOT_EXECUTE = 0b0000_0000_0100_0000, + + /// + /// Flag 5 (Bit 8): 1 if group members may not read, 0 otherwise + /// + GROUP_MEMBER_CANNOT_READ = 0b0000_0001_0000_0000, + + /// + /// Flag 6 (Bit 10): 1 if group members may not execute, 0 otherwise + /// + GROUP_MEMBER_CANNOT_EXECUTE = 0b0000_0100_0000_0000, + + /// + /// Flag 7 (Bit 12): 1 if non-group members may not read, 0 if any user can read + /// + NON_GROUP_MEMBER_CANNOT_READ = 0b0001_0000_0000_0000, + + /// + /// Flag 8 (Bit 14): 1 if non-group members may not execute, 0 if any user can execute + /// + NON_GROUP_MEMBER_CANNOT_EXECUTE = 0b0100_0000_0000_0000, + + /// + /// Fixed values in the enum, every other bit set to 1 + /// + PERMISSIONS_MASK = 0b1010_1010_1010_1010, + } + + /// + /// Enum for ExtendedAttributeRecord.RecordFormat + /// 4-127 (0x04-7F): Reserved + /// 128-255 (0x80-FF): System Use + /// + /// + public enum RecordFormat : byte + { + /// + /// Record format unspecified by this type + /// + UNSPECIFIED = 0x00, + + /// + /// Sequence of fixed-length records + /// + FIXED_LENGTH_RECORDS = 0x01, + + /// + /// Sequence of variable-length records, Record Control World is LSB + /// + VARIABLE_LENGTH_RECORDS_LSB = 0x02, + + /// + /// Sequence of variable-length records, Record Control World is MSB + /// + VARIABLE_LENGTH_RECORDS_MSG = 0x03, + } + + /// + /// Enum for ExtendedAttributeRecord.RecordAttributes + /// 3-255 (0x03-FF): Reserved by ISO9660 + /// + /// + public enum RecordAttributes : byte + { + /// + /// Records are preceeded by linefeed and followed by carriage return + /// + LINEFEED_CARRIAGE_RETURN = 0x00, + + /// + /// First byte of each record is Fortran-style + /// + FORTRAN_STYLE = 0x01, + + /// + /// Record contains necessary control information within itself + /// + SELF_DEFINED = 0x02, + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/ExtendedAttributeRecord.cs b/SabreTools.Serialization/Models/ISO9660/ExtendedAttributeRecord.cs new file mode 100644 index 00000000..04cf0fb5 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/ExtendedAttributeRecord.cs @@ -0,0 +1,111 @@ +using SabreTools.Numerics; + +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// ISO9660 Extended Attribute Record + /// + /// + public sealed class ExtendedAttributeRecord + { + /// + /// Owner ID number for this file + /// 0x0000 if no owner, implies 0x0000 Group ID + /// + public BothInt16 OwnerIdentification { get; set; } + + /// + /// Group ID number for the owner of this file + /// 0x0000 if no group, implies 0x0000 Owner ID + /// + public BothInt16 GroupIdentification { get; set; } + + /// + /// 16-bit flag variable with 8 flags where every other bit is set to 1 + /// i.e. minimum value of 0b1010101010101010 (0xAAAA) + /// + public Permissions Permissions { get; set; } + + /// + /// Datetime of when the file content was created + /// + public DecDateTime FileCreationDateTime { get; set; } + + /// + /// Datetime of when the file content was last modified + /// + public DecDateTime FileModificationDateTime { get; set; } + + /// + /// Datetime of when the file content expires + /// + public DecDateTime FileExpirationDateTime { get; set; } + + /// + /// Datetime of when the file content is effective from + /// + public DecDateTime FileEffectiveDateTime { get; set; } + + /// + /// Record format type + /// + public RecordFormat RecordFormat { get; set; } + + /// + /// Record attributes + /// Note: If RecordType is zero, this field is ignored by readers + /// + public RecordAttributes RecordAttributes { get; set; } + + /// + /// Record Length + /// If RecordType is 0, this field is 0 + /// If RecordType is 1, this field is length in bytes + /// If RecordType is 2 or 3, this field is maximum length in bytes of a record in the file + /// + public BothInt16 RecordLength { get; set; } + + /// + /// 32-byte name of the intended system + /// Primary: a-characters or a1-characters only, padded to the right with spaces + /// + public byte[] SystemIdentifier { get; set; } + + /// + /// 64-bytes for system use + /// + public byte[] SystemUse { get; set; } + + /// + /// Extended Attribyte Record Version + /// ISO9660 sets this to 0x01 + /// + public byte ExtendedAttributeRecordVersion { get; set; } + + /// + /// Length of the escape sequences field + /// + public byte EscapeSequencesLength { get; set; } + + /// + /// 64-bytes reserved (0x00) + /// + public byte[] Reserved64Bytes { get; set; } + + /// + /// Length of the Application use field + /// + public BothInt16 ApplicationLength { get; set; } + + /// + /// ApplicationLength-bytes for application use + /// + public byte[] ApplicationUse { get; set; } + + /// + /// EscapeSequencesLength-bytes list of escape sequences to interpret this file + /// Optional, and if present, padded to the right with 0x00 + /// + public byte[]? EscapeSequences { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/FileExtent.cs b/SabreTools.Serialization/Models/ISO9660/FileExtent.cs new file mode 100644 index 00000000..38596fcc --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/FileExtent.cs @@ -0,0 +1,20 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// ISO9660 File Extent, the file data itself + /// + /// + public class FileExtent + { + /// + /// File's extended attribute record + /// Optional field, and never present for Directory-type File Extents + /// + public ExtendedAttributeRecord? ExtendedAttributeRecord { get; set; } + + /// + /// Byte array of data within the file extent (after the Extended Attribyte Record) + /// + public byte[]? Data { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/FileSystem.cs b/SabreTools.Serialization/Models/ISO9660/FileSystem.cs new file mode 100644 index 00000000..dd54e4d3 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/FileSystem.cs @@ -0,0 +1,15 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// ISO9660 / EMCA-119 file system composed of a set of volumes (set of disc images) + /// Files may be spread across volumes (disc images), or be contained entirely within a single volume (disc image) + /// + /// + public sealed class FileSystem + { + /// + /// Set of volumes (disc images) that make up an ISO9660 file system + /// + public Volume[] VolumeSet { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/GenericVolumeDescriptor.cs b/SabreTools.Serialization/Models/ISO9660/GenericVolumeDescriptor.cs new file mode 100644 index 00000000..c3227034 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/GenericVolumeDescriptor.cs @@ -0,0 +1,15 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Generic Volume Descriptor + /// Volume Descriptor with contents not defined by ISO9660 + /// + /// + public sealed class GenericVolumeDescriptor : VolumeDescriptor + { + /// + /// 2041 bytes + /// + public byte[] Data { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/PathTableGroup.cs b/SabreTools.Serialization/Models/ISO9660/PathTableGroup.cs new file mode 100644 index 00000000..f7893741 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/PathTableGroup.cs @@ -0,0 +1,35 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// ISO9660 Path Table Group, lists of path table records for each directory on the volume + /// Each path table is intended to point to the same set of directories. All non-null path tables should be identical! + /// For each directory on the filesystem (except root), the Path Table contains a record which identifies the directory, its parent, and its location. + /// + /// + public sealed class PathTableGroup + { + /// + /// Type-L Path Table (Little Endian) + /// Note: This is meant to exist, but is nullable in case PathTableM is valid + /// + public PathTableRecord[]? PathTableL { get; set; } + + /// + /// Optional Type-L Path Table (Little Endian) + /// Note: This is optional + /// + public PathTableRecord[]? OptionalPathTableL { get; set; } + + /// + /// Type-M Path Table (Big Endian) + /// Note: This is meant to exist, but is nullable in case PathTableL is valid + /// + public PathTableRecord[]? PathTableM { get; set; } + + /// + /// Optional Type-M Path Table (Big Endian) + /// Note: This is optional + /// + public PathTableRecord[]? OptionalPathTableM { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/PathTableRecord.cs b/SabreTools.Serialization/Models/ISO9660/PathTableRecord.cs new file mode 100644 index 00000000..c467a7d2 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/PathTableRecord.cs @@ -0,0 +1,42 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// ISO9660 Path Table Record + /// Each path table record is numbered (starting from 1), which corresponds to the ordinal number of the corresponding directory + /// + /// + public sealed class PathTableRecord + { + /// + /// Length of Directory Identifier + /// + public byte DirectoryIdentifierLength { get; set; } + + /// + /// Length of the the extended attribute record + /// + public byte ExtendedAttributeRecordLength { get; set; } + + /// + /// Location of the first logical block number of the first logical block allocated to the extent + /// + public int ExtentLocation { get; set; } + + /// + /// Location of the first logical block number of the first logical block allocated to the extent + /// + public short ParentDirectoryNumber { get; set; } + + /// + /// Directory name + /// Either d-characters or d1-characters, or a single 0x00 byte + /// + public byte[] DirectoryIdentifier { get; set; } + + /// + /// If DirectoryIdentifierLength is odd, the DirectoryIdentifier is followed by a single padding byte (0x00) + /// Optional field + /// + public byte? PaddingField { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/PrimaryVolumeDescriptor.cs b/SabreTools.Serialization/Models/ISO9660/PrimaryVolumeDescriptor.cs new file mode 100644 index 00000000..ed81ad26 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/PrimaryVolumeDescriptor.cs @@ -0,0 +1,22 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Primary Volume Descriptor + /// Volume Descriptor with VolumeDescriptorType = 0x01 + /// + /// + public sealed class PrimaryVolumeDescriptor : BaseVolumeDescriptor + { + /// + /// 1 unused byte at offset 7, should be 0x00 + /// Note: This is used for VolumeFlags on SupplementaryVolumeDescriptor + /// + public byte UnusedByte { get; set; } + + /// + /// 32 unused bytes at offset 88, should be all 0x00 + /// Note: These is used for EscapeSequences on SupplementaryVolumeDescriptor + /// + public byte[] Unused32Bytes { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/SupplementaryVolumerDescriptor.cs b/SabreTools.Serialization/Models/ISO9660/SupplementaryVolumerDescriptor.cs new file mode 100644 index 00000000..8c4f5a3c --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/SupplementaryVolumerDescriptor.cs @@ -0,0 +1,24 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Supplementary or Enhanced (including Joliet Extension) Volume Descriptor + /// Volume Descriptor with VolumeDescriptorType = 0x02 + /// Supplementary/Enhanced Volume Descriptors are typically utilised when an alternate character encoding for the identifiers is desired. + /// + /// + public sealed class SupplementaryVolumeDescriptor : BaseVolumeDescriptor + { + /// + /// Remaining bits are reserved (set to 0) + /// Note: Joliet Extension implies Constants.JolietVolumeFlags (0x00) + /// + public VolumeFlags VolumeFlags { get; set; } + + /// + /// List of escape sequences to use, up to 32 bytes, padded with 0x00 to the right + /// If all bytes are set to 0x00, then a1-characters are identical to a-characters + /// Note: Joliet Extension implies Constants.JolietEscapeSequences + /// + public byte[] EscapeSequences { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/Volume.cs b/SabreTools.Serialization/Models/ISO9660/Volume.cs new file mode 100644 index 00000000..2a6e7387 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/Volume.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// A volume (or disc image) with a ISO9660 / ECMA-119 filesystem + /// A set of volumes (set of disc images) makes up an entire file system (files may be spread across volumes/discs) + /// Note: Volume can be accessed in logical sectors, usually 2048 bytes, but can be other higher powers of 2 + /// Note: Volume is made up of logical blocks, usually 2048 bytes, but can be any power of two (minimum 512 / 2^9) + /// The logical block size cannot be smaller than the logical sector size + /// + /// + public sealed class Volume + { + /// + /// System Area, made up of 16 logical blocks + /// 32,768 bytes, assuming logical block size of 2048 bytes + /// ISO9660 does not specify the content of the System Area + /// + public byte[] SystemArea { get; set; } + + #region Data Area + + /// + /// Set of Volume Descriptors + /// Valid ISO9660 volumes have: + /// - At least one Primary Volume Descriptor (Type = 1) + /// - Zero or more Supplementary/Enhanced Volume Descriptors (Type = 2) + /// - Zero or more Volume Partition Descriptors (Type = 3) + /// - Zero or more Boot Volume Descriptors (Type = 0) + /// - At least one Volume Descriptor Set Terminator (Type = 255), as the final element(s) in the set + /// + public VolumeDescriptor[] VolumeDescriptorSet { get; set; } + + /// + /// List of path table records for each directory on the volume + /// One (or two) Path Table Groups is provided for each Base Volume Descriptor + /// Note: If a Base Volume Descriptor's Path Table Size field is ambiguous, two Path Table Groups may be given + /// + public PathTableGroup[] PathTableGroups { get; set; } + + /// + /// Map of sector numbers and the directory at that sector number + /// Each Directory contains child directory and file descriptors + /// + public Dictionary DirectoryDescriptors { get; set; } + + #endregion + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/VolumeDescriptor.cs b/SabreTools.Serialization/Models/ISO9660/VolumeDescriptor.cs new file mode 100644 index 00000000..77e931cd --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/VolumeDescriptor.cs @@ -0,0 +1,30 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Abstract ISO9660 Volume Descriptor + /// Each VolumeDescriptor consists of 1 logical sector (usually 2048 bytes) + /// The first 7 bytes are a fixed header, the remaining bytes are application specific + /// + /// + public abstract class VolumeDescriptor + { + /// + /// The type of VolumeDescriptor + /// + public VolumeDescriptorType Type { get; set; } + + /// + /// 5-byte magic + /// Set to Constants.StandardIdentifier ("CD001") + /// On non-ISO9660 CD-i discs, set to Constants.StandardIdentifierCDI ("CD-I ") + /// + public byte[] Identifier { get; set; } + + /// + /// The Volume Descriptor version number + /// 1 for all specific Volume Descriptors other than Enhanced Volume Descriptor + /// 2 for Enhanced Volume Descriptor (including Joliet) + /// + public byte Version { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/VolumeDescriptorSetTerminator.cs b/SabreTools.Serialization/Models/ISO9660/VolumeDescriptorSetTerminator.cs new file mode 100644 index 00000000..f194b776 --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/VolumeDescriptorSetTerminator.cs @@ -0,0 +1,15 @@ +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Volume Descriptor Set Terminator + /// Blank Descriptor with VolumeDescriptorType = 0xFF + /// + /// + public sealed class VolumeDescriptorSetTerminator : VolumeDescriptor + { + /// + /// 2041 reserved bytes, should be 0x00 + /// + public byte[] Reserved2041Bytes { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/ISO9660/VolumePartitionDescriptor.cs b/SabreTools.Serialization/Models/ISO9660/VolumePartitionDescriptor.cs new file mode 100644 index 00000000..cc2f1f3f --- /dev/null +++ b/SabreTools.Serialization/Models/ISO9660/VolumePartitionDescriptor.cs @@ -0,0 +1,44 @@ +using SabreTools.Numerics; + +namespace SabreTools.Data.Models.ISO9660 +{ + /// + /// Volume Partition Descriptor + /// Volume Descriptor with VolumeDescriptorType = 0x03 + /// + /// + public sealed class VolumePartitionDescriptor : VolumeDescriptor + { + /// + /// 1 unused byte at offset 7, should be 0x00 + /// + public byte UnusedByte { get; set; } + + /// + /// 32-byte name of the intended system that can use this record + /// a-characters only + /// + public byte[] SystemIdentifier { get; set; } + + /// + /// 32-byte name of this volume partition + /// d-characters only + /// + public byte[] VolumePartitionIdentifier { get; set; } + + /// + /// Logical block number of the first logical block allocated to this volume partition + /// + public BothInt32 VolumePartitionLocation { get; set; } + + /// + /// Number of logical blocks allocated to this volume partition + /// + public BothInt32 VolumePartitionSize { get; set; } + + /// + /// 1960 bytes for System Use, contents not defined by ISO9660 + /// + public byte[] SystemUse { get; set; } + } +} diff --git a/SabreTools.Serialization/Printer.cs b/SabreTools.Serialization/Printer.cs index ea14ee45..ed98aab3 100644 --- a/SabreTools.Serialization/Printer.cs +++ b/SabreTools.Serialization/Printer.cs @@ -45,6 +45,7 @@ namespace SabreTools.Serialization Wrapper.InstallShieldArchiveV3 item => item.PrettyPrint(), Wrapper.InstallShieldCabinet item => item.PrettyPrint(), Wrapper.IRD item => item.PrettyPrint(), + Wrapper.ISO9660 item => item.PrettyPrint(), Wrapper.LinearExecutable item => item.PrettyPrint(), Wrapper.LZKWAJ item => item.PrettyPrint(), Wrapper.LZQBasic item => item.PrettyPrint(), @@ -101,6 +102,7 @@ namespace SabreTools.Serialization Wrapper.InstallShieldArchiveV3 item => item.ExportJSON(), Wrapper.InstallShieldCabinet item => item.ExportJSON(), Wrapper.IRD item => item.ExportJSON(), + Wrapper.ISO9660 item => item.ExportJSON(), Wrapper.LinearExecutable item => item.ExportJSON(), Wrapper.LZKWAJ item => item.ExportJSON(), Wrapper.LZQBasic item => item.ExportJSON(), @@ -260,6 +262,16 @@ namespace SabreTools.Serialization return builder; } + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this Wrapper.ISO9660 item) + { + var builder = new StringBuilder(); + ISO9660.Print(builder, item.Model); + return builder; + } + /// /// Export the item information as pretty-printed text /// diff --git a/SabreTools.Serialization/Printers/ISO9660.cs b/SabreTools.Serialization/Printers/ISO9660.cs new file mode 100644 index 00000000..f327a7d8 --- /dev/null +++ b/SabreTools.Serialization/Printers/ISO9660.cs @@ -0,0 +1,496 @@ +using System; +using System.Collections.Generic; +using System.Text; +using SabreTools.Data.Models.ISO9660; +using SabreTools.Numerics; + +namespace SabreTools.Data.Printers +{ + public class ISO9660 : IPrinter + { + /// + public void PrintInformation(StringBuilder builder, Volume model) + => Print(builder, model); + + public static void Print(StringBuilder builder, Volume volume) + { + builder.AppendLine("ISO 9660 Information:"); + builder.AppendLine("-------------------------"); + builder.AppendLine(); + + if (volume.SystemArea == null || volume.SystemArea.Length == 0) + builder.AppendLine(volume.SystemArea, "System Area"); + else if (Array.TrueForAll(volume.SystemArea, b => b == 0)) + builder.AppendLine("Zeroed", "System Area"); + else + builder.AppendLine("Not Zeroed", "System Area"); + builder.AppendLine(); + + Print(builder, volume.VolumeDescriptorSet); + Print(builder, volume.PathTableGroups); + Print(builder, volume.DirectoryDescriptors); + } + + #region Volume Descriptors + + private static void Print(StringBuilder builder, VolumeDescriptor[]? vdSet) + { + builder.AppendLine(" Volume Descriptors:"); + builder.AppendLine(" -------------------------"); + if (vdSet == null) + { + builder.AppendLine(" No volume descriptor set"); + builder.AppendLine(); + return; + } + + foreach (var vd in vdSet) + { + if (vd is BootRecordVolumeDescriptor brvd) + Print(builder, brvd); + else if (vd is BaseVolumeDescriptor bvd) + Print(builder, bvd); + else if (vd is VolumePartitionDescriptor vpd) + Print(builder, vpd); + else if (vd is VolumeDescriptorSetTerminator vdst) + Print(builder, vdst); + else if (vd is GenericVolumeDescriptor gvd) + Print(builder, gvd); + else + { + builder.AppendLine(" Unknown Volume Descriptor"); + builder.AppendLine(); + } + } + } + + private static void Print(StringBuilder builder, BootRecordVolumeDescriptor vd) + { + builder.AppendLine(" Boot Record Volume Descriptor:"); + builder.AppendLine(" -------------------------"); + + builder.AppendLine(vd.BootSystemIdentifier, " Boot System Identifier"); + builder.AppendLine(vd.BootSystemIdentifier, " Boot Identifier"); + + if (vd.BootSystemUse == null || vd.BootSystemUse.Length == 0) + builder.AppendLine(vd.BootSystemUse, " Boot System Use"); + else if (Array.TrueForAll(vd.BootSystemUse, b => b == 0)) + builder.AppendLine("Zeroed", " Boot System Use"); + else + builder.AppendLine("Not Zeroed", " Boot System Use"); + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, BaseVolumeDescriptor vd) + { + // TOOD: Determine encoding based on vd.Type, svd.EscapeSequence (and manual detection?) + + if (vd.Type == VolumeDescriptorType.PRIMARY_VOLUME_DESCRIPTOR) + builder.AppendLine(" Primary Volume Descriptor:"); + else if (vd.Type == VolumeDescriptorType.SUPPLEMENTARY_VOLUME_DESCRIPTOR) + builder.AppendLine(" Supplementary Volume Descriptor:"); + else + builder.AppendLine(" Unidentified Base Volume Descriptor:"); + builder.AppendLine(" -------------------------"); + + if (vd is PrimaryVolumeDescriptor pvd) + { + builder.AppendLine(pvd.UnusedByte, " Unused Byte"); + } + else if (vd is SupplementaryVolumeDescriptor svd) + { + builder.AppendLine(" Volume Flags:"); +#if NET20 || NET35 + builder.AppendLine((svd.VolumeFlags & VolumeFlags.UNREGISTERED_ESCAPE_SEQUENCES) != 0, " Unregistered Escape Sequences"); +#else + builder.AppendLine(svd.VolumeFlags.HasFlag(VolumeFlags.UNREGISTERED_ESCAPE_SEQUENCES), " Unregistered Escape Sequences"); +#endif + if ((byte)svd.VolumeFlags > 1) + builder.AppendLine("Not Zeroed", " Reserved Flags"); + else + builder.AppendLine("Zeroed", " Reserved Flags"); + } + + // TODO: Decode all byte arrays into strings (based on encoding above) + + builder.AppendLine(vd.SystemIdentifier, " System Identifier"); + builder.AppendLine(vd.VolumeIdentifier, " Volume Identifier"); + + + if (vd.Unused8Bytes != null && Array.TrueForAll(vd.Unused8Bytes, b => b == 0)) + builder.AppendLine("Zeroed", " Unused 8 Bytes"); + else + builder.AppendLine(vd.Unused8Bytes, " Unused 8 Bytes"); + + builder.AppendLineBothEndian(vd.VolumeSpaceSize, " Volume Space Size"); + + if (vd is PrimaryVolumeDescriptor pvd2) + { + if (pvd2.Unused32Bytes != null && Array.TrueForAll(pvd2.Unused32Bytes, b => b == 0)) + builder.AppendLine("Zeroed", " Unused 32 Bytes"); + else + builder.AppendLine(pvd2.Unused32Bytes, " Unused 32 Bytes"); + } + if (vd is SupplementaryVolumeDescriptor svd2) + { + // TODO: Trim trailing 0x00 and split array into characters (multi-byte encoding detection) + builder.AppendLine(svd2.EscapeSequences, " Escape Sequences"); + } + + builder.AppendLineBothEndian(vd.VolumeSetSize, " Volume Set Size"); + builder.AppendLineBothEndian(vd.VolumeSequenceNumber, " Volume Sequence Number"); + builder.AppendLineBothEndian(vd.LogicalBlockSize, " Logical Block Size"); + builder.AppendLineBothEndian(vd.PathTableSize, " Path Table Size"); + builder.AppendLine(vd.PathTableLocationL, " Type-L Path Table Location"); + builder.AppendLine(vd.OptionalPathTableLocationL, " Optional Type-L Path Table Location"); + builder.AppendLine(vd.PathTableLocationM, " Type-M Path Table Location"); + builder.AppendLine(vd.OptionalPathTableLocationM, " Optional Type-M Path Table Location"); + + builder.AppendLine(" Root Directory Record:"); + Print(builder, vd.RootDirectoryRecord); + + builder.AppendLine(vd.VolumeSetIdentifier, " Volume Set Identifier"); + builder.AppendLine(vd.PublisherIdentifier, " Publisher Identifier"); + builder.AppendLine(vd.DataPreparerIdentifier, " Data Preparer Identifier"); + builder.AppendLine(vd.ApplicationIdentifier, " Application Identifier"); + builder.AppendLine(vd.CopyrightFileIdentifier, " Copyright Identifier"); + builder.AppendLine(vd.AbstractFileIdentifier, " Abstract Identifier"); + builder.AppendLine(vd.BibliographicFileIdentifier, " Bibliographic Identifier"); + + builder.AppendLine(" Volume Creation Date Time:"); + Print(builder, vd.VolumeCreationDateTime); + builder.AppendLine(" Volume Modification Date Time:"); + Print(builder, vd.VolumeModificationDateTime); + builder.AppendLine(" Volume Expiration Date Time:"); + Print(builder, vd.VolumeExpirationDateTime); + builder.AppendLine(" Volume Effective Date Time:"); + Print(builder, vd.VolumeEffectiveDateTime); + + builder.AppendLine(vd.FileStructureVersion, " File Structure Version"); + + builder.AppendLine(vd.ReservedByte, " Reserved Byte"); + + if (vd.ApplicationUse == null || vd.ApplicationUse.Length == 0) + builder.AppendLine(vd.ApplicationUse, " Application Use"); + else if (Array.TrueForAll(vd.ApplicationUse, b => b == 0)) + builder.AppendLine("Zeroed", " Application Use"); + else + builder.AppendLine("Not Zeroed", " Application Use"); + + if (vd.Reserved653Bytes == null || vd.Reserved653Bytes.Length == 0) + builder.AppendLine(vd.Reserved653Bytes, " Reserved 653 Bytes"); + else if (Array.TrueForAll(vd.Reserved653Bytes, b => b == 0)) + builder.AppendLine("Zeroed", " Reserved 653 Bytes"); + else + builder.AppendLine("Not Zeroed", " Reserved 653 Bytes"); + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, VolumePartitionDescriptor vd) + { + builder.AppendLine(" Volume Partition Descriptor:"); + builder.AppendLine(" -------------------------"); + + builder.AppendLine(vd.SystemIdentifier, " System Identifier"); + builder.AppendLine(vd.VolumePartitionIdentifier, " Volume Partition Identifier"); + builder.AppendLineBothEndian(vd.VolumePartitionLocation, " Volume Partition Location"); + builder.AppendLineBothEndian(vd.VolumePartitionSize, " Volume Partition Size"); + + if (vd.SystemUse == null || vd.SystemUse.Length == 0) + builder.AppendLine(vd.SystemUse, " System Use"); + else if (Array.TrueForAll(vd.SystemUse, b => b == 0)) + builder.AppendLine("Zeroed", " System Use"); + else + builder.AppendLine("Not Zeroed", " System Use"); + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, VolumeDescriptorSetTerminator vd) + { + builder.AppendLine(" Volume Descriptor Set Terminator:"); + builder.AppendLine(" -------------------------"); + + if (vd.Reserved2041Bytes == null || vd.Reserved2041Bytes.Length == 0) + builder.AppendLine(vd.Reserved2041Bytes, " Reserved Bytes"); + else if (Array.TrueForAll(vd.Reserved2041Bytes, b => b == 0)) + builder.AppendLine("Zeroed", " Reserved Bytes"); + else + builder.AppendLine("Not Zeroed", " Reserved Bytes"); + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, GenericVolumeDescriptor vd) + { + builder.AppendLine(" Unidentified Volume Descriptor:"); + builder.AppendLine(" -------------------------"); + + if (vd.Data == null || vd.Data.Length == 0) + builder.AppendLine(vd.Data, " Data"); + else if (Array.TrueForAll(vd.Data, b => b == 0)) + builder.AppendLine("Zeroed", " Data"); + else + builder.AppendLine("Not Zeroed", " Data"); + + builder.AppendLine(); + } + + #endregion + + #region Path Tables + + private static void Print(StringBuilder builder, PathTableGroup[]? ptgs) + { + builder.AppendLine(" Path Table Group(s):"); + builder.AppendLine(" -------------------------"); + if (ptgs == null) + { + builder.AppendLine(" No path table groups"); + builder.AppendLine(); + return; + } + + for (int tableNum = 0; tableNum < ptgs.Length; tableNum++) + { + if (ptgs[tableNum].PathTableL != null) + { + builder.AppendLine($" Type-L Path Table {tableNum}:"); + builder.AppendLine(" -------------------------"); + Print(builder, ptgs[tableNum].PathTableL); + } + else + { + builder.AppendLine($" No Type-L Path Table {tableNum}:"); + builder.AppendLine(); + } + if (ptgs[tableNum].OptionalPathTableL != null) + { + builder.AppendLine($" Optional Type-L Path Table {tableNum}:"); + builder.AppendLine(" -------------------------"); + Print(builder, ptgs[tableNum].OptionalPathTableL); + } + else + { + builder.AppendLine($" No Optional Type-L Path Table {tableNum}:"); + builder.AppendLine(); + } + if (ptgs[tableNum].PathTableM != null) + { + builder.AppendLine($" Type-M Path Table {tableNum}:"); + builder.AppendLine(" -------------------------"); + Print(builder, ptgs[tableNum].PathTableM); + } + else + { + builder.AppendLine($" No Type-M Path Table {tableNum}:"); + builder.AppendLine(); + } + if (ptgs[tableNum].OptionalPathTableM != null) + { + builder.AppendLine($" Optional Type-M Path Table {tableNum}:"); + builder.AppendLine(" -------------------------"); + Print(builder, ptgs[tableNum].OptionalPathTableM); + } + else + { + builder.AppendLine($" No Optional Type-M Path Table {tableNum}:"); + builder.AppendLine(); + } + } + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, PathTableRecord[] records) + { + if (records.Length == 0) + { + builder.AppendLine(" No records"); + builder.AppendLine(); + return; + } + + for (int recordNum = 0; recordNum < records.Length; recordNum++) + { + builder.AppendLine($" Path Table Record {recordNum}"); + builder.AppendLine(records[recordNum].DirectoryIdentifierLength, " Directory Identifier Length"); + builder.AppendLine(records[recordNum].ExtendedAttributeRecordLength, " Extended Attribute Record Length"); + builder.AppendLine(records[recordNum].ExtentLocation, " Extent Location"); + builder.AppendLine(records[recordNum].DirectoryIdentifier, " Directory Identifier"); + if (records[recordNum].PaddingField != null) + builder.AppendLine(records[recordNum].PaddingField, " Padding Field"); + } + + builder.AppendLine(); + } + + #endregion + + #region Directories + + private static void Print(StringBuilder builder, Dictionary? dirs) + { + builder.AppendLine(" Directory Descriptors Information:"); + builder.AppendLine(" -------------------------"); + if (dirs == null) + { + builder.AppendLine(" No directory descriptors"); + builder.AppendLine(); + return; + } + + foreach(var kvp in dirs) + { + builder.AppendLine($" Directory at Sector {kvp.Key}"); + builder.AppendLine(" -------------------------"); + Print(builder, kvp.Value); + } + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, DirectoryExtent? dir) + { + if (dir == null) + { + builder.AppendLine(" No directory descriptor"); + builder.AppendLine(); + return; + } + if (dir.DirectoryRecords == null) + { + builder.AppendLine(" No directory records"); + builder.AppendLine(); + return; + } + + for (int recordNum = 0; recordNum < dir.DirectoryRecords.Length; recordNum++) + { + builder.AppendLine($" Directory Record {recordNum}:"); + builder.AppendLine(" -------------------------"); + Print(builder, dir.DirectoryRecords[recordNum]); + builder.AppendLine(); + } + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, DirectoryRecord? dr) + { + if (dr == null) + { + builder.AppendLine(" No directory record"); + builder.AppendLine(); + return; + } + + builder.AppendLine(dr.DirectoryRecordLength, " Directory Record Length"); + builder.AppendLine(dr.ExtendedAttributeRecordLength, " Extended Attribute Record Length"); + + builder.AppendLineBothEndian(dr.ExtentLocation, " Extent Location"); + builder.AppendLineBothEndian(dr.ExtentLength, " Extent Length"); + + Print(builder, dr.RecordingDateTime); + + builder.AppendLine(" File Flags:"); + builder.AppendLine((dr.FileFlags & FileFlags.EXISTENCE) == FileFlags.EXISTENCE, " Existence"); + builder.AppendLine((dr.FileFlags & FileFlags.DIRECTORY) == FileFlags.DIRECTORY, " Directory"); + builder.AppendLine((dr.FileFlags & FileFlags.ASSOCIATED_FILE) == FileFlags.ASSOCIATED_FILE, " Associated File"); + builder.AppendLine((dr.FileFlags & FileFlags.RECORD) == FileFlags.RECORD, " Record"); + builder.AppendLine((dr.FileFlags & FileFlags.PROTECTION) == FileFlags.PROTECTION, " Protection"); + builder.AppendLine((dr.FileFlags & FileFlags.RESERVED_BIT5) == FileFlags.RESERVED_BIT5, " Reserved Flag (Bit 5)"); + builder.AppendLine((dr.FileFlags & FileFlags.RESERVED_BIT6) == FileFlags.RESERVED_BIT6, " Reserved Flag (Bit 6)"); + builder.AppendLine((dr.FileFlags & FileFlags.MULTI_EXTENT) == FileFlags.MULTI_EXTENT, " Multi-Extent"); + + builder.AppendLine(dr.FileUnitSize, " File Unit Size"); + builder.AppendLine(dr.InterleaveGapSize, " Interleave Gap Size"); + + builder.AppendLineBothEndian(dr.VolumeSequenceNumber, " Volume Sequence Number"); + + builder.AppendLine(dr.FileIdentifierLength, " File Identifier Length"); + builder.AppendLine(dr.FileIdentifier, " File Identifier"); + builder.AppendLine(dr.PaddingField, " Padding Field"); + + if (dr.SystemUse == null || dr.SystemUse.Length == 0) + builder.AppendLine(dr.SystemUse, " System Use"); + else if (Array.TrueForAll(dr.SystemUse, b => b == 0)) + builder.AppendLine($"Zeroed ({dr.SystemUse.Length} bytes)", " System Use"); + else + builder.AppendLine($"Not Zeroed ({dr.SystemUse.Length} bytes)", " System Use"); + } + + private static void Print(StringBuilder builder, DirectoryRecordDateTime? drdt) + { + if (drdt == null) + { + builder.AppendLine("[NULL]", " Directory Record Date Time"); + return; + } + builder.AppendLine(" Directory Record Date Time:"); + + builder.AppendLine(drdt.YearsSince1990, " Years Since 1900"); + builder.AppendLine(drdt.Month, " Month"); + builder.AppendLine(drdt.Day, " Day"); + builder.AppendLine(drdt.Hour, " Hour"); + builder.AppendLine(drdt.Minute, " Minute"); + builder.AppendLine(drdt.Second, " Second"); + string tz = $"{((drdt.TimezoneOffset-48)*15/60):+0;-0}:{((drdt.TimezoneOffset-48)*15%60+60)%60:00} (0x{drdt.TimezoneOffset.ToString("X2")})"; + builder.AppendLine(tz, " Timezone Offset"); + } + + #endregion + + private static void Print(StringBuilder builder, DecDateTime? dt) + { + if (dt == null) + { + builder.AppendLine(" [NULL]"); + return; + } + + if (IsDigits(dt.Year)) + builder.AppendLine(Encoding.ASCII.GetString(dt.Year), " Year"); + else + builder.AppendLine(dt.Year, " Year"); + if (IsDigits(dt.Month)) + builder.AppendLine(Encoding.ASCII.GetString(dt.Month), " Month"); + else + builder.AppendLine(dt.Month, " Month"); + if (IsDigits(dt.Day)) + builder.AppendLine(Encoding.ASCII.GetString(dt.Day), " Day"); + else + builder.AppendLine(dt.Day, " Day"); + if (IsDigits(dt.Hour)) + builder.AppendLine(Encoding.ASCII.GetString(dt.Hour), " Hour"); + else + builder.AppendLine(dt.Hour, " Hour"); + if (IsDigits(dt.Minute)) + builder.AppendLine(Encoding.ASCII.GetString(dt.Minute), " Minute"); + else + builder.AppendLine(dt.Minute, " Minute"); + if (IsDigits(dt.Second)) + builder.AppendLine(Encoding.ASCII.GetString(dt.Second), " Second"); + else + builder.AppendLine(dt.Second, " Second"); + if (IsDigits(dt.Centisecond)) + builder.AppendLine(Encoding.ASCII.GetString(dt.Centisecond), " Centisecond"); + else + builder.AppendLine(dt.Centisecond, " Centisecond"); + string tz = $"{((dt.TimezoneOffset-48)*15/60):+0;-0}:{((dt.TimezoneOffset-48)*15%60+60)%60:00} (0x{dt.TimezoneOffset.ToString("X2")})"; + builder.AppendLine(tz, " Timezone Offset"); + } + + private static bool IsDigits(byte[] arr) + { + foreach (byte b in arr) + { + if (b < 48 || b > 57) + return false; + } + return true; + } + } +} diff --git a/SabreTools.Serialization/Readers/ISO9660.cs b/SabreTools.Serialization/Readers/ISO9660.cs new file mode 100644 index 00000000..6951a341 --- /dev/null +++ b/SabreTools.Serialization/Readers/ISO9660.cs @@ -0,0 +1,809 @@ +using System; +using System.Collections.Generic; +using System.IO; +using SabreTools.Data.Extensions; +using SabreTools.Data.Models.ISO9660; +using SabreTools.IO.Extensions; +using SabreTools.Numerics; + +namespace SabreTools.Serialization.Readers +{ + public class ISO9660 : BaseBinaryReader + { + /// + public override Volume? Deserialize(Stream? data) => Deserialize(data, Constants.MinimumSectorSize); + + /// + /// Size of the logical sector used in the volume + public Volume? Deserialize(Stream? data, short sectorLength) + { + // If the data is invalid + if (data == null || !data.CanRead) + return null; + + // Ensure the logical sector size is valid (2^n where n>=11) + if (sectorLength < Constants.MinimumSectorSize || (sectorLength & (sectorLength - 1)) != 0) + return null; + + // Simple check for a valid stream length + if (sectorLength * (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 = data.ReadBytes(Constants.SystemAreaSectors * sectorLength); + + // Read the set of Volume Descriptors + var vdSet = ParseVolumeDescriptorSet(data, sectorLength); + if (vdSet == null || vdSet.Length == 0) + return null; + volume.VolumeDescriptorSet = vdSet; + + // Parse the path table group(s) for each base volume descriptor + var ptgs = ParsePathTableGroups(data, sectorLength, volume.VolumeDescriptorSet); + if (ptgs == null || ptgs.Length == 0) + return null; + volume.PathTableGroups = ptgs; + + // Parse the root directory descriptor(s) for each base volume descriptor + var dirs = ParseDirectoryDescriptors(data, sectorLength, volume.VolumeDescriptorSet); + if (dirs == null || dirs.Count == 0) + return null; + volume.DirectoryDescriptors = dirs; + + return volume; + } + catch + { + // Ignore the actual error + return null; + } + } + + #region Volume Descriptor Parsing + + /// + /// Parse a Stream into an array of VolumeDescriptor objects + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Filled VolumeDescriptor[] on success, null on error + public static VolumeDescriptor[]? ParseVolumeDescriptorSet(Stream data, short sectorLength) + { + var obj = new List(); + + bool setTerminated = false; + while (data.Position < data.Length) + { + var volumeDescriptor = ParseVolumeDescriptor(data, sectorLength); + + // 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.Seek(-sectorLength, 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]; + } + + /// + /// Parse a Stream into a VolumeDescriptor + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Filled VolumeDescriptor on success, null on error + public static VolumeDescriptor? ParseVolumeDescriptor(Stream data, short sectorLength) + { + var type = (VolumeDescriptorType)data.ReadByteValue(); + + return type switch + { + // Known Volume Descriptors defined by ISO9660 + VolumeDescriptorType.BOOT_RECORD_VOLUME_DESCRIPTOR => ParseBootRecordVolumeDescriptor(data, sectorLength), + VolumeDescriptorType.PRIMARY_VOLUME_DESCRIPTOR => ParsePrimaryVolumeDescriptor(data, sectorLength), + VolumeDescriptorType.SUPPLEMENTARY_VOLUME_DESCRIPTOR => ParseSupplementaryVolumeDescriptor(data, sectorLength), + VolumeDescriptorType.VOLUME_PARTITION_DESCRIPTOR => ParseVolumePartitionDescriptor(data, sectorLength), + VolumeDescriptorType.VOLUME_DESCRIPTOR_SET_TERMINATOR => ParseVolumeDescriptorSetTerminator(data, sectorLength), + + // Unknown Volume Descriptor + _ => ParseGenericVolumeDescriptor(data, sectorLength, type), + }; + } + + /// + /// Parse a Stream into a BootRecordVolumeDescriptor + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Filled BootRecordVolumeDescriptor on success, null on error + public static BootRecordVolumeDescriptor? ParseBootRecordVolumeDescriptor(Stream data, short sectorLength) + { + var obj = new BootRecordVolumeDescriptor(); + + obj.Type = VolumeDescriptorType.VOLUME_PARTITION_DESCRIPTOR; + obj.Identifier = data.ReadBytes(5); + + // Validate Identifier, return null and rewind if invalid + if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) + { + data.Seek(-6, SeekOrigin.Current); + return null; + } + + obj.Version = data.ReadByteValue(); + obj.BootSystemIdentifier = data.ReadBytes(32); + obj.BootIdentifier = data.ReadBytes(32); + obj.BootSystemUse = data.ReadBytes(1977); + + // Skip remainder of the logical sector + if (sectorLength > Constants.MinimumSectorSize) + data.Seek(sectorLength - Constants.MinimumSectorSize, SeekOrigin.Current); + + return obj; + } + + /// + /// Parse a Stream into a PrimaryVolumeDescriptor + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Filled PrimaryVolumeDescriptor on success, null on error + public static PrimaryVolumeDescriptor? ParsePrimaryVolumeDescriptor(Stream data, short sectorLength) + { + var obj = new PrimaryVolumeDescriptor(); + + obj.Type = VolumeDescriptorType.PRIMARY_VOLUME_DESCRIPTOR; + return (PrimaryVolumeDescriptor?)ParseBaseVolumeDescriptor(data, sectorLength, obj); + } + + /// + /// Parse a Stream into a SupplementaryVolumeDescriptor + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Filled SupplementaryVolumeDescriptor on success, null on error + public static SupplementaryVolumeDescriptor? ParseSupplementaryVolumeDescriptor(Stream data, short sectorLength) + { + var obj = new SupplementaryVolumeDescriptor(); + + obj.Type = VolumeDescriptorType.SUPPLEMENTARY_VOLUME_DESCRIPTOR; + return (SupplementaryVolumeDescriptor?)ParseBaseVolumeDescriptor(data, sectorLength, obj); + } + + /// + /// Parse a Stream into a BaseVolumeDescriptor + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Filled BaseVolumeDescriptor on success, null on error + public static BaseVolumeDescriptor? ParseBaseVolumeDescriptor(Stream data, short sectorLength, BaseVolumeDescriptor obj) + { + obj.Identifier = data.ReadBytes(5); + + // Validate Identifier, return null and rewind if invalid + if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) + { + data.Seek(-6, SeekOrigin.Current); + return null; + } + + obj.Version = data.ReadByteValue(); + + // Read the child-specific field + if (obj is PrimaryVolumeDescriptor objPVD) + objPVD.UnusedByte = data.ReadByteValue(); + else if (obj is SupplementaryVolumeDescriptor objSVD) + objSVD.VolumeFlags = (VolumeFlags)data.ReadByteValue(); + else + { + // Rewind and return for unknown descriptor + data.Seek(-8, SeekOrigin.Current); + return null; + } + + obj.SystemIdentifier = data.ReadBytes(32); + obj.VolumeIdentifier = data.ReadBytes(32); + obj.Unused8Bytes = data.ReadBytes(8); + obj.VolumeSpaceSize = data.ReadInt32BothEndian(); + + // Read the child-specific field + if (obj is PrimaryVolumeDescriptor objPVD2) + objPVD2.Unused32Bytes = data.ReadBytes(32); + else if (obj is SupplementaryVolumeDescriptor objSVD2) + objSVD2.EscapeSequences = data.ReadBytes(32); + else + { + // Rewind and return for unknown descriptor + data.Seek(-120, SeekOrigin.Current); + return null; + } + + obj.VolumeSetSize = data.ReadInt16BothEndian(); + obj.VolumeSequenceNumber = data.ReadInt16BothEndian(); + obj.LogicalBlockSize = data.ReadInt16BothEndian(); + obj.PathTableSize = data.ReadInt32BothEndian(); + obj.PathTableLocationL = data.ReadInt32LittleEndian(); + obj.OptionalPathTableLocationL = data.ReadInt32LittleEndian(); + obj.PathTableLocationM = data.ReadInt32BigEndian(); + obj.OptionalPathTableLocationM = data.ReadInt32BigEndian(); + + var dr = ParseDirectoryRecord(data, true); + if (dr == null) + return null; + obj.RootDirectoryRecord = dr; + + obj.VolumeSetIdentifier = data.ReadBytes(128); + obj.PublisherIdentifier = data.ReadBytes(128); + obj.DataPreparerIdentifier = data.ReadBytes(128); + obj.ApplicationIdentifier = data.ReadBytes(128); + obj.CopyrightFileIdentifier = data.ReadBytes(37); + obj.AbstractFileIdentifier = data.ReadBytes(37); + obj.BibliographicFileIdentifier = data.ReadBytes(37); + + obj.VolumeCreationDateTime = ParseDecDateTime(data); + obj.VolumeModificationDateTime = ParseDecDateTime(data); + obj.VolumeExpirationDateTime = ParseDecDateTime(data); + obj.VolumeEffectiveDateTime = ParseDecDateTime(data); + + obj.FileStructureVersion = data.ReadByteValue(); + obj.ReservedByte = data.ReadByteValue(); + obj.ApplicationUse = data.ReadBytes(512); + obj.Reserved653Bytes = data.ReadBytes(653); + + // Skip remainder of the logical sector + if (sectorLength > Constants.MinimumSectorSize) + data.Seek(sectorLength - Constants.MinimumSectorSize, SeekOrigin.Current); + + return obj; + } + + /// + /// Parse a Stream into a VolumePartitionDescriptor + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Filled VolumePartitionDescriptor on success, null on error + public static VolumePartitionDescriptor? ParseVolumePartitionDescriptor(Stream data, short sectorLength) + { + var obj = new VolumePartitionDescriptor(); + + obj.Type = VolumeDescriptorType.VOLUME_PARTITION_DESCRIPTOR; + obj.Identifier = data.ReadBytes(5); + + // Validate Identifier, return null and rewind if invalid + if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) + { + data.Seek(-6, SeekOrigin.Current); + return null; + } + + obj.Version = data.ReadByteValue(); + obj.UnusedByte = data.ReadByteValue(); + obj.SystemIdentifier = data.ReadBytes(32); + obj.VolumePartitionIdentifier = data.ReadBytes(32); + obj.VolumePartitionLocation = data.ReadInt32BothEndian(); + obj.VolumePartitionSize = data.ReadInt32BothEndian(); + obj.SystemUse = data.ReadBytes(1960); + + // Skip remainder of the logical sector + if (sectorLength > Constants.MinimumSectorSize) + data.Seek(sectorLength - Constants.MinimumSectorSize, SeekOrigin.Current); + + return obj; + } + + /// + /// Parse a Stream into a VolumeDescriptorSetTerminator + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Filled VolumeDescriptorSetTerminator on success, null on error + public static VolumeDescriptorSetTerminator? ParseVolumeDescriptorSetTerminator(Stream data, short sectorLength) + { + var obj = new VolumeDescriptorSetTerminator(); + + obj.Type = VolumeDescriptorType.VOLUME_DESCRIPTOR_SET_TERMINATOR; + obj.Identifier = data.ReadBytes(5); + + // Validate Identifier, return null and rewind if invalid + if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) + { + data.Seek(-6, SeekOrigin.Current); + return null; + } + + obj.Version = data.ReadByteValue(); + obj.Reserved2041Bytes = data.ReadBytes(2041); + + // Skip remainder of the logical sector + if (sectorLength > Constants.MinimumSectorSize) + data.Seek(sectorLength - Constants.MinimumSectorSize, SeekOrigin.Current); + + return obj; + } + + /// + /// Parse a Stream into a GenericVolumeDescriptor + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Type + /// Filled GenericVolumeDescriptor on success, null on error + public static GenericVolumeDescriptor? ParseGenericVolumeDescriptor(Stream data, short sectorLength, VolumeDescriptorType type) + { + var obj = new GenericVolumeDescriptor(); + + obj.Type = type; + obj.Identifier = data.ReadBytes(5); + + // Validate Identifier, return null and rewind if invalid + if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) + { + data.Seek(-6, SeekOrigin.Current); + return null; + } + + obj.Version = data.ReadByteValue(); + obj.Data = data.ReadBytes(2041); + + // Skip remainder of the logical sector + if (sectorLength > Constants.MinimumSectorSize) + data.Seek(sectorLength - Constants.MinimumSectorSize, SeekOrigin.Current); + + return obj; + } + + #endregion + + #region Path Table Parsing + + /// + /// Parse a Stream into an array of PathTableGroup + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Primary/Supplementary/Enhanced Volume Descriptor pointing to path table(s) + /// Filled PathTableGroup[] on success, null on error + public static PathTableGroup[]? ParsePathTableGroups(Stream data, short sectorLength, VolumeDescriptor[] vdSet) + { + var groups = new List(); + foreach (VolumeDescriptor vd in vdSet) + { + if (vd is BaseVolumeDescriptor bvd) + { + // Parse the path table group in the base volume descriptor + var pathTableGroups = ParsePathTableGroup(data, sectorLength, bvd); + if (pathTableGroups != null && pathTableGroups.Count > 0) + groups.AddRange(pathTableGroups); + } + } + + // Return error (null) if no valid path table groups were found + if (groups.Count == 0) + return null; + + return [.. groups]; + } + + /// + /// Parse a Stream into a list of PathTableGroup + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Primary/Supplementary/Enhanced Volume Descriptor pointing to path table(s) + /// Filled list of PathTableGroup on success, null on error + public static List? ParsePathTableGroup(Stream data, short sectorLength, BaseVolumeDescriptor vd) + { + var groups = new List(); + + int sizeL = vd.PathTableSize.LittleEndian; + int sizeB = vd.PathTableSize.BigEndian; + int locationL = vd.PathTableLocationL; + int locationL2 = vd.OptionalPathTableLocationL; + int locationM = vd.PathTableLocationM; + int locationM2 = vd.OptionalPathTableLocationM; + + short blockLength = vd.GetLogicalBlockSize(sectorLength); + + var groupL = new PathTableGroup(); + if (locationL != 0 && ((locationL * blockLength) + sizeL) < data.Length) + { + data.Seek(locationL * blockLength, SeekOrigin.Begin); + groupL.PathTableL = ParsePathTable(data, sectorLength, sizeL, true); + } + if (locationL2 != 0 && ((locationL2 * blockLength) + sizeL) < data.Length) + { + data.Seek(locationL2 * blockLength, SeekOrigin.Begin); + groupL.OptionalPathTableL = ParsePathTable(data, sectorLength, sizeL, true); + } + if (locationM != 0 && ((locationM * blockLength) + sizeL) < data.Length) + { + data.Seek(locationM * blockLength, SeekOrigin.Begin); + groupL.PathTableM = ParsePathTable(data, sectorLength, sizeL, false); + } + if (locationM2 != 0 && ((locationM2 * blockLength) + sizeL) < data.Length) + { + data.Seek(locationM2 * blockLength, SeekOrigin.Begin); + groupL.OptionalPathTableM = ParsePathTable(data, sectorLength, sizeL, false); + } + + // If no valid path tables were found, don't add the table group + if (groupL.PathTableL != null || groupL.OptionalPathTableL != null || groupL.PathTableM != null || groupL.OptionalPathTableM != null) + groups.Add(groupL); + + // If the both-endian path table size value is consistent, return the single path table group + if (sizeL == sizeB) + return groups; + + // Get the other-sized path table group + var groupB = new PathTableGroup(); + if (locationL != 0 && ((locationL * blockLength) + sizeB) < data.Length) + { + data.Seek(locationL * blockLength, SeekOrigin.Begin); + groupB.PathTableL = ParsePathTable(data, sectorLength, sizeB, true); + } + if (locationL2 != 0 && ((locationL2 * blockLength) + sizeB) < data.Length) + { + data.Seek(locationL2 * blockLength, SeekOrigin.Begin); + groupB.OptionalPathTableL = ParsePathTable(data, sectorLength, sizeB, true); + } + if (locationM != 0 && ((locationM * blockLength) + sizeB) < data.Length) + { + data.Seek(locationM * blockLength, SeekOrigin.Begin); + groupB.PathTableM = ParsePathTable(data, sectorLength, sizeB, false); + } + if (locationM2 != 0 && ((locationM2 * blockLength) + sizeB) < data.Length) + { + data.Seek(locationM2 * blockLength, SeekOrigin.Begin); + groupB.OptionalPathTableM = ParsePathTable(data, sectorLength, sizeB, false); + } + + // If no valid path tables were found, don't add the table group + if (groupB.PathTableL != null || groupB.OptionalPathTableL != null || groupB.PathTableM != null || groupB.OptionalPathTableM != null) + groups.Add(groupB); + + return groups; + } + + /// + /// Parse a Stream into an array of path table records + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Size of the path table + /// True if path table is little endian, false if big endian + /// Filled array of path table records on success, null on error + public static PathTableRecord[]? ParsePathTable(Stream data, short sectorLength, int tableSize, bool littleEndian) + { + var pathTable = new List(); + + // TODO: Better deal with invalid path table sizes < 10 (manually detect valid records to determine size) + // Current status: Trusting path table length field (tableSize) + int pos = 0; + while (pos < tableSize) + { + var record = new PathTableRecord(); + var directoryIdentifierLength = data.ReadByteValue(); + + // Check that the current record can fit within the current path table size + pos += 8 + directoryIdentifierLength; + if (directoryIdentifierLength % 2 != 0) + pos += 1; + if (pos > tableSize) + { + // Invalid record length, quit early + // TODO: Try detect record length and recover? + break; + } + + record.DirectoryIdentifierLength = directoryIdentifierLength; + + record.ExtendedAttributeRecordLength = data.ReadByteValue(); + + // Read numerics with correct endianness + if (littleEndian) + { + record.ExtentLocation = data.ReadInt32LittleEndian(); + record.ParentDirectoryNumber = data.ReadInt16LittleEndian(); + } + else + { + record.ExtentLocation = data.ReadInt32BigEndian(); + record.ParentDirectoryNumber = data.ReadInt16BigEndian(); + } + + // Read the directory identifier + record.DirectoryIdentifier = data.ReadBytes(record.DirectoryIdentifierLength); + + // Padding field is present is directory identifier length is odd + if (record.DirectoryIdentifierLength % 2 != 0) + record.PaddingField = data.ReadByteValue(); + + pathTable.Add(record); + } + + // Return error (null) if no valid path table records were found + if (pathTable.Count == 0) + return null; + + return [.. pathTable]; + } + + #endregion + + #region Directory Descriptor Parsing + + /// + /// Parse a Stream into a map of sector numbers to Directory + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Set of volume descriptors for a volume + /// Filled Dictionary of int to Directory on success, null on error + public static Dictionary? ParseDirectoryDescriptors(Stream data, short sectorLength, VolumeDescriptor[] vdSet) + { + var directories = new Dictionary(); + foreach (VolumeDescriptor vd in vdSet) + { + if (vd is BaseVolumeDescriptor bvd) + { + // Determine logical block size + short blockLength = bvd.GetLogicalBlockSize(sectorLength); + + // Parse the root directory pointed to from the base volume descriptor + var descriptors = ParseDirectory(data, sectorLength, blockLength, bvd.RootDirectoryRecord, false); + if (descriptors == null || descriptors.Count == 0) + continue; + // Merge dictionaries + foreach (var kvp in descriptors) + { + if (!directories.ContainsKey(kvp.Key)) + directories.Add(kvp.Key, kvp.Value); + } + } + } + + // Return error (null) if no valid directory descriptors were found + if (directories.Count == 0) + return null; + + return directories; + } + + /// + /// Parse a Stream into a map of sector numbers to Directory + /// + /// Stream to parse + /// Number of bytes in a logical sector (usually 2048) + /// Number of bytes in a logical block (usually 2048) + /// Directory record pointing to the directory extent + /// True if the Big Endian extent location/length should be parsed + /// Filled Dictionary of int to Directory on success, null on error + public static Dictionary? ParseDirectory(Stream data, short sectorLength, short blockLength, DirectoryRecord dr, bool bigEndian) + { + // Do not parse file extents +#if NET20 || NET35 + if ((dr.FileFlags & FileFlags.DIRECTORY) == 0) + return null; +#else + if (!dr.FileFlags.HasFlag(FileFlags.DIRECTORY)) + return null; +#endif + + int blocksPerSector = sectorLength / blockLength; + + // Validate both-endian extent location + // TODO: Validate both-endian extent length (use the longest / non-zero one) + int extentLocation = bigEndian ? dr.ExtentLocation.LittleEndian : dr.ExtentLocation.BigEndian; + int extentLength = bigEndian ? dr.ExtentLength.LittleEndian : dr.ExtentLength.BigEndian; + + // Validate extent within data stream + if ((extentLocation * blockLength) + extentLength > data.Length) + return null; + + // Move stream to directory location + data.Seek(extentLocation * blockLength, SeekOrigin.Begin); + + // Read all directory records in this directory + var records = new List(); + int pos = 0; + while (pos < extentLength) + { + // Peek next byte to check whether the next record length is not greater than the end of the dir extent + var recordLength = data.PeekByteValue(); + + // If record length of 0x00, next record begins in next sector + if (recordLength == 0) + { + // TODO: Skip to start of next sector rather than incrementing by 1 + pos++; + continue; + } + + // Ensure record will end in this extent + // TODO: Smartly detect record length for invalid record lengths + pos += recordLength; + if (pos > extentLength) + break; + + // Get the next directory record + var directoryRecord = ParseDirectoryRecord(data, false); + if (directoryRecord != null) + records.Add(directoryRecord); + } + + // Add current directory to dictionary + var directories = new Dictionary(); + var currentDirectory = new DirectoryExtent(); + currentDirectory.DirectoryRecords = [.. records]; + directories.Add(extentLocation * blocksPerSector, currentDirectory); + + // Add all child directories to dictionary recursively + foreach (var record in records) + { + // Don't traverse to parent or self + if (record.FileIdentifier.EqualsExactly(Constants.CurrentDirectory) || record.FileIdentifier.EqualsExactly(Constants.ParentDirectory)) + continue; + // Recursively parse child directory + int sectorNum = record.ExtentLocation * blocksPerSector; + var dir = ParseDirectory(data, sectorLength, blockLength, record, false); + if (dir == null) + continue; + // Add new directories to dictionary + foreach (var kvp in dir) + { + if (!directories.ContainsKey(kvp.Key)) + directories.Add(kvp.Key, kvp.Value); + } + } + + // If the extent location field is ambiguous, also parse the big-endian directory extent + if (!dr.ExtentLocation.IsValid) + { + var bigEndianDir = ParseDirectory(data, sectorLength, blockLength, dr, true); + if (bigEndianDir != null) + { + // Add new directories to dictionary + foreach (var kvp in bigEndianDir) + { + if (!directories.ContainsKey(kvp.Key)) + directories.Add(kvp.Key, kvp.Value); + } + } + } + + return directories; + } + + /// + /// Parse a Stream into a DirectoryRecord + /// + /// Stream to parse + /// true if root directory record, false otherwise + /// Filled DirectoryRecord on success, null on error + public static DirectoryRecord? ParseDirectoryRecord(Stream data, bool root) + { + var obj = new DirectoryRecord(); + + obj.DirectoryRecordLength = data.ReadByteValue(); + obj.ExtendedAttributeRecordLength = data.ReadByteValue(); + obj.ExtentLocation = data.ReadInt32BothEndian(); + obj.ExtentLength = data.ReadInt32BothEndian(); + + obj.RecordingDateTime = ParseDirectoryRecordDateTime(data); + + obj.FileFlags = (FileFlags)data.ReadByteValue(); + obj.FileUnitSize = data.ReadByteValue(); + obj.InterleaveGapSize = data.ReadByteValue(); + obj.VolumeSequenceNumber = data.ReadInt16BothEndian(); + obj.FileIdentifierLength = data.ReadByteValue(); + + // Root directory within the volume descriptor has a single byte file identifier + if (root) + obj.FileIdentifier = data.ReadBytes(1); + else if (obj.FileIdentifierLength > 0) + obj.FileIdentifier = data.ReadBytes(obj.FileIdentifierLength); + + // If file identifier length is even, there is a padding field byte + if (obj.FileIdentifierLength % 2 == 0) + obj.PaddingField = data.ReadByteValue(); + + // Root directory within the volume descriptor has no system use bytes, fixed at 34bytes + if (root) + return obj; + + // Calculate actual size of record + int totalBytes = 33 + obj.FileIdentifierLength; + // Calculate the size of the system use section (remaining allocated bytes) + int systemUseLength = obj.DirectoryRecordLength - 33 - obj.FileIdentifierLength; + // Account for padding field after file identifier + if (obj.FileIdentifierLength % 2 == 0) + { + totalBytes += 1; + systemUseLength -= 1; + } + + // If System Use is empty, or if DirectoryRecordLength is bad, return early + if (systemUseLength < 1) + { + // Total record size must be even, read a padding byte + if (totalBytes % 2 != 0) + obj.SystemUse = data.ReadBytes(1); + + return obj; + } + + // Total used bytes must be even, read a padding byte + totalBytes += systemUseLength; + if (totalBytes % 2 != 0) + systemUseLength += 1; + + // Read system use field + obj.SystemUse = data.ReadBytes(systemUseLength); + + return obj; + } + + /// + /// Parse a Stream into a DirectoryRecordDateTime + /// + /// Stream to parse + /// Filled DirectoryRecordDateTime on success, null on error + public static DirectoryRecordDateTime? ParseDirectoryRecordDateTime(Stream data) + { + var obj = new DirectoryRecordDateTime(); + + obj.YearsSince1990 = data.ReadByteValue(); + obj.Month = data.ReadByteValue(); + obj.Day = data.ReadByteValue(); + obj.Hour = data.ReadByteValue(); + obj.Minute = data.ReadByteValue(); + obj.Second = data.ReadByteValue(); + obj.TimezoneOffset = data.ReadByteValue(); + + return obj; + } + + #endregion + + /// + /// Parse a Stream into a DecDateTime + /// + /// Stream to parse + /// Filled DecDateTime on success, null on error + public static DecDateTime ParseDecDateTime(Stream data) + { + var obj = new DecDateTime(); + + obj.Year = data.ReadBytes(4); + obj.Month = data.ReadBytes(2); + obj.Day = data.ReadBytes(2); + obj.Hour = data.ReadBytes(2); + obj.Minute = data.ReadBytes(2); + obj.Second = data.ReadBytes(2); + obj.Centisecond = data.ReadBytes(2); + obj.TimezoneOffset = data.ReadByteValue(); + + return obj; + } + } +} diff --git a/SabreTools.Serialization/WrapperFactory.cs b/SabreTools.Serialization/WrapperFactory.cs index 8e16b504..0e2c485e 100644 --- a/SabreTools.Serialization/WrapperFactory.cs +++ b/SabreTools.Serialization/WrapperFactory.cs @@ -28,6 +28,8 @@ namespace SabreTools.Serialization WrapperType.IniFile => null,// TODO: Implement wrapper WrapperType.InstallShieldArchiveV3 => InstallShieldArchiveV3.Create(data), WrapperType.InstallShieldCAB => InstallShieldCabinet.Create(data), + WrapperType.IRD => null,// TODO: Implement wrapper + WrapperType.ISO9660 => ISO9660.Create(data), WrapperType.LDSCRYPT => LDSCRYPT.Create(data), WrapperType.LZKWAJ => LZKWAJ.Create(data), WrapperType.LZQBasic => LZQBasic.Create(data), @@ -329,6 +331,23 @@ namespace SabreTools.Serialization #endregion + #region IRD + + if (magic.StartsWith([0x33, 0x49, 0x52, 0x44])) + return WrapperType.IRD; + + if (extension.Equals("ird", StringComparison.OrdinalIgnoreCase)) + return WrapperType.IRD; + + #endregion + + #region ISO9660 + + if (extension.Equals("iso", StringComparison.OrdinalIgnoreCase)) + return WrapperType.ISO9660; + + #endregion + #region LDSCRYPT if (magic.StartsWith(Data.Models.LDSCRYPT.Constants.SignatureBytes)) diff --git a/SabreTools.Serialization/Wrappers/ISO9660.Extraction.cs b/SabreTools.Serialization/Wrappers/ISO9660.Extraction.cs new file mode 100644 index 00000000..1df18731 --- /dev/null +++ b/SabreTools.Serialization/Wrappers/ISO9660.Extraction.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using SabreTools.Data.Models.ISO9660; + +namespace SabreTools.Serialization.Wrappers +{ + public partial class ISO9660 : IExtractable + { + /// + public bool Extract(string outputDirectory, bool includeDebug) + { + // If we have no path tables or directory descriptors, there is nothing to extract + if (PathTableGroups.Length == 0 && DirectoryDescriptors.Count == 0) + return true; + + bool allExtracted = false; + + // TODO: Extract all directories and file extents + + return allExtracted; + } + } +} diff --git a/SabreTools.Serialization/Wrappers/ISO9660.cs b/SabreTools.Serialization/Wrappers/ISO9660.cs new file mode 100644 index 00000000..cc9a6c3c --- /dev/null +++ b/SabreTools.Serialization/Wrappers/ISO9660.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.IO; +using SabreTools.Data.Models.ISO9660; + +namespace SabreTools.Serialization.Wrappers +{ + public partial class ISO9660 : WrapperBase + { + #region Descriptive Properties + + /// + public override string DescriptionString => "ISO 9660 Volume"; + + #endregion + + #region Extension Properties + + /// + /// Volume Descriptors + /// + public VolumeDescriptor[] VolumeDescriptorSet => Model.VolumeDescriptorSet ?? []; + + /// + /// Path Tables + /// + public PathTableGroup[] PathTableGroups => Model.PathTableGroups ?? []; + + /// + /// Directory Descriptors + /// + public Dictionary DirectoryDescriptors => Model.DirectoryDescriptors ?? []; + + #endregion + + #region Constructors + + /// + public ISO9660(Volume model, byte[] data) : base(model, data) { } + + /// + public ISO9660(Volume model, byte[] data, int offset) : base(model, data, offset) { } + + /// + public ISO9660(Volume model, byte[] data, int offset, int length) : base(model, data, offset, length) { } + + /// + public ISO9660(Volume model, Stream data) : base(model, data) { } + + /// + public ISO9660(Volume model, Stream data, long offset) : base(model, data, offset) { } + + /// + public ISO9660(Volume model, Stream data, long offset, long length) : base(model, data, offset, length) { } + + #endregion + + #region Static Constructors + + /// + /// Create an ISO9660 Volume from a byte array and offset + /// + /// Byte array representing the archive + /// Offset within the array to parse + /// An ISO 9660 Volume wrapper on success, null on failure + public static ISO9660? 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 ISO9660 Volume from a Stream + /// + /// Stream representing the archive + /// An ISO9660 Volume wrapper on success, null on failure + public static ISO9660? 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.ISO9660().Deserialize(data); + if (model == null) + return null; + + return new ISO9660(model, data, currentOffset); + } + catch + { + return null; + } + } + + #endregion + } +} diff --git a/SabreTools.Serialization/Wrappers/WrapperType.cs b/SabreTools.Serialization/Wrappers/WrapperType.cs index 72a67a17..f943eeae 100644 --- a/SabreTools.Serialization/Wrappers/WrapperType.cs +++ b/SabreTools.Serialization/Wrappers/WrapperType.cs @@ -82,6 +82,16 @@ namespace SabreTools.Serialization.Wrappers /// InstallShieldCAB, + /// + /// PS3 ISO Rebuild Data + /// + IRD, + + /// + /// ISO 9660 Volume (Disc image) + /// + ISO9660, + /// /// Link Data Security encrypted file ///