ISO 9660 support (#29)

* Begin defining ISO9660 models

* Fix initial models

* Improve ISO9660 models

* ISO9660 models

* Fix fields

* Nullable and enums

* Fix flags attribute

* Start ISO9660 Reader code

* semicolon

* Fix build

* Use EqualsExactly

* Fix build

* Update ISO9660.cs

* Update ISO9660.cs

* Move DirectoryRecordDateTime class

* ParseDirectoryRecordDateTime function

* ST refactoring

* fix return

* return array

* ISO9660 wrapper

* Initial printing code

* semicolon

* Fix

* orphan variable name

* fix null ref

* ISO9660 Printer

* Fix DirectoryRecord parsing

* fix

* test

* test

* Fix reader

* extractable ISO

* partial wrapper class

* namespace

* extension property

* path tables

* Cleanup

* rename directory

* fix

* fix

* typo

* fix2

* Fix

* Cleanup reader

* Count list property

* Parse Path Tables

* fix model name

* fix

* fix2

* fix3

* print path table records

* debug errors

* debug error

* debug

* undo debug

* Fix casting

* Validate path table locations

* print table numbers

* Printer cleanup

* fix printer

* printer++

* Print DecDateTime

* else if

* BootSystemUse

* Use Linq

* custom zero check

* fix

* Update ISO9660.cs

* Update ISO9660.cs

* Update ISO9660.cs

* Update ISO9660.cs

* fix

* debug

* debug2

* debug3

* debug4

* debug5

* debug6

* debug7

* debug8

* Array.TrueForAll

* Update ISO9660.cs

* fix

* cleanup

* typo

* Use BothInt numerics

* using SabreTools.Numerics

* Test nonnull

* nonnull bothint

* Print invalid BothInts

* Print directory record

* typo

* fix

* null check

* directory descriptors

* cleanup printer

* semicolon

* Fix

* flags never null

* more non nullable

* no null

* AppendLineBothEndian

* fix

* rename to Directory

* fix

* namespace

* full namespace

* fix

* cleanup

* Parse directories

* fix

* Fix

* Test parse directories

* PeekByteValue

* Revert CI changes

* Dummy extractor

* Review and fixes

* Fixes

* Final fix

* big endian directory search

* fix big endian changes

* Final fix
This commit is contained in:
Deterous
2025-10-29 21:46:08 +09:00
committed by GitHub
parent 27cf01ce84
commit 36c2cc2f15
29 changed files with 2730 additions and 0 deletions

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,209 @@
using SabreTools.Numerics;
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// Abstract Volume Descriptor with common fields used by Primary/Supplementary/Enhanced Volume Descriptors
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public abstract class BaseVolumeDescriptor : VolumeDescriptor
{
// Virtual variable of 1 byte goes here
// PrimaryVolumeDescriptor: UnusedByte
// SupplementaryVolumeDescriptor: VolumeFlags
/// <summary>
/// 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
/// </summary>
public byte[] SystemIdentifier { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[] VolumeIdentifier { get; set; }
/// <summary>
/// 8 unused bytes at offset 72, should be all 0x00
/// </summary>
public byte[] Unused8Bytes { get; set; }
/// <summary>
/// Number of logical blocks in this volume
/// </summary>
public BothInt32 VolumeSpaceSize { get; set; }
// Virtual variable of 32 bytes goes here:
// PrimaryVolumeDescriptor: Unused32Bytes
// SupplementaryVolumeDescriptor: EscapeSequences
/// <summary>
/// Number of Volumes (discs) in this VolumeSet
/// </summary>
public BothInt16 VolumeSetSize { get; set; }
/// <summary>
/// Volume (disc) number in this volume set
/// </summary>
public BothInt16 VolumeSequenceNumber { get; set; }
/// <summary>
/// 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
/// </summary>
public BothInt16 LogicalBlockSize { get; set; }
/// <summary>
/// Number of bytes in the path table
/// </summary>
public BothInt32 PathTableSize { get; set; }
/// <summary>
/// Sector number of the start of the little-endian path table, type L
/// Stored as int32-LSB
/// </summary>
public int PathTableLocationL { get; set; }
/// <summary>
/// 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
/// </summary>
public int OptionalPathTableLocationL { get; set; }
/// <summary>
/// Sector number of the start of the big-endian path table, type M
/// Stored as int32-MSB
/// </summary>
public int PathTableLocationM { get; set; }
/// <summary>
/// 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
/// </summary>
public int OptionalPathTableLocationM { get; set; }
/// <summary>
/// Root directory entry, 34 bytes
/// DirectoryIdentifier = 0x00
/// </summary>
public DirectoryRecord RootDirectoryRecord { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[] VolumeSetIdentifier { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[] PublisherIdentifier { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[] DataPreparerIdentifier { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[] ApplicationIdentifier { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[] CopyrightFileIdentifier { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[] AbstractFileIdentifier { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[] BibliographicFileIdentifier { get; set; }
/// <summary>
/// PVD-style DateTime format for the Creation date/time of the Volume
/// </summary>
public DecDateTime VolumeCreationDateTime { get; set; }
/// <summary>
/// PVD-style DateTime format for the Modification date/time of the Volume
/// </summary>
public DecDateTime VolumeModificationDateTime { get; set; }
/// <summary>
/// PVD-style DateTime format for the Expiration date/time of the Volume
/// </summary>
public DecDateTime VolumeExpirationDateTime { get; set; }
/// <summary>
/// PVD-style DateTime format for the Effective date/time of the Volume
/// </summary>
public DecDateTime VolumeEffectiveDateTime { get; set; }
/// <summary>
/// Version number of the Records / Path Table format
/// For Primary/Supplementary, this is 0x01
/// For Enhanced, this is 0x02
/// </summary>
public byte FileStructureVersion { get; set; }
/// <summary>
/// 1 reserved byte, should be 0x00
/// </summary>
public byte ReservedByte { get; set; }
/// <summary>
/// 512 bytes for Application Use, contents not defined by ISO9660
/// </summary>
public byte[] ApplicationUse { get; set; }
/// <summary>
/// 653 reserved bytes, should be all 0x00
/// </summary>
public byte[] Reserved653Bytes { get; set; }
}
}

View File

@@ -0,0 +1,27 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// Boot Record Volume Descriptor
/// Volume Descriptor with VolumeDescriptorType = 0x00
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class BootRecordVolumeDescriptor : VolumeDescriptor
{
/// <summary>
/// 32-byte name of the intended system that can use this record
/// a-characters only
/// </summary>
public byte[] BootSystemIdentifier { get; set; }
/// <summary>
/// 32-byte name of this boot system
/// a-characters only
/// </summary>
public byte[] BootIdentifier { get; set; }
/// <summary>
/// 1997 bytes for Boot System Use, contents not defined by ISO9660
/// </summary>
public byte[] BootSystemUse { get; set; }
}
}

View File

@@ -0,0 +1,118 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// ISO9660 filesystem extent
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public static class Constants
{
#region Volume Descriptor Constants
/// <summary>
/// Minimum size of a logical sector
/// </summary>
public const int MinimumSectorSize = 2048;
/// <summary>
/// Number of logical sectors in the System Area
/// </summary>
public const int SystemAreaSectors = 16;
/// <summary>
/// Identifier used for ISO9660, "CD001"
/// </summary>
public static readonly byte[] StandardIdentifier = [0x43, 0x44, 0x30, 0x30, 0x31];
/// <summary>
/// File Identifier of the current directory
/// </summary>
public static readonly byte[] CurrentDirectory = [0x00];
/// <summary>
/// File Identifier of the parent directory
/// </summary>
public static readonly byte[] ParentDirectory = [0x01];
#endregion
#region CD-i Constants
/// <summary>
/// Identifier present on non-ISO9660 CD-i discs, "CD-I "
/// </summary>
public static readonly byte[] StandardIdentifierCDI = [0x43, 0x44, 0x2D, 0x49, 0x20];
#endregion
#region Primary/Supplementary Volume Descriptors Constants
/// <summary>
/// Character used for separating a file name from a file extension, Fullstop character "."
/// This value is used in Primary Volume Descriptors
/// </summary>
public const byte Separator1 = 0x2E;
/// <summary>
/// Character used for separating the file name/extension, from the file version number, Semicolon character ";"
/// This value is used in Primary Volume Descriptors
/// </summary>
public const byte Separator2 = 0x3B;
/// <summary>
/// Character used for padding a byte array on the right, Space character " "
/// This value is used in Primary/Supplementary Volume Descriptors
/// </summary>
public const byte Filler = 0x20;
/// <summary>
/// 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)
/// </summary>
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];
/// <summary>
/// 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)
/// </summary>
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
/// <summary>
/// UCS-2 Character used for separating a file name from a file extension, Fullstop character "."
/// This is used in Joliet-style Enhanced Volume Descriptors
/// </summary>
public static readonly byte[] JolietSeparator1 = [0x00, 0x2E];
/// <summary>
/// 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
/// </summary>
public static readonly byte[] JolietSeparator2 = [0x00, 0x3B];
/// <summary>
/// Character used for padding a byte array on the right, null character
/// This is used in Joliet-style Enhanced Volume Descriptors
/// </summary>
public const byte JolietFiller = 0x20;
/// <summary>
/// Joliet extension uses Enhanced Volume Descriptor with this VolumeFlags value
/// </summary>
public const byte JolietVolumeFlags = 0x00;
/// <summary>
/// Joliet extension uses Enhanced Volume Descriptor with this EscapeSequences value
/// Escape Sequences: (25 2F 40) (25 2F 43) (25 2F 45)
/// </summary>
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
}
}

View File

@@ -0,0 +1,54 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// Datetime format represented by decimal ASCII
/// - Base (Primary/Supplementary/Enhanced) Volume Descriptor
/// - Extended Attribute Record
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class DecDateTime
{
/// <summary>
/// 4-byte ASCII digits
/// </summary>
public byte[] Year { get; set; }
/// <summary>
/// 2-byte ASCII digits
/// </summary>
public byte[] Month { get; set; }
/// <summary>
/// 2-byte ASCII digits
/// </summary>
public byte[] Day { get; set; }
/// <summary>
/// 2-byte ASCII digits
/// </summary>
public byte[] Hour { get; set; }
/// <summary>
/// 2-byte ASCII digits
/// </summary>
public byte[] Minute { get; set; }
/// <summary>
/// 2-byte ASCII digits
/// </summary>
public byte[] Second { get; set; }
/// <summary>
/// 2-byte ASCII digits
/// </summary>
public byte[] Centisecond { get; set; }
/// <summary>
/// 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)
/// </summary>
public byte TimezoneOffset { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// ISO9660 Directory Extent containing file and directory descriptors parsed from the file extent into directory records
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class DirectoryExtent : FileExtent
{
/// <summary>
/// Directory records (each a descriptor of a directory or a file)
/// </summary>
public DirectoryRecord[] DirectoryRecords { get; set; }
}
}

View File

@@ -0,0 +1,88 @@
using SabreTools.Numerics;
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// ISO9660 Directory Record, a directory descriptor that points to an extent representing a file or directory
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class DirectoryRecord
{
/// <summary>
/// Length of Directory Record
/// </summary>
public byte DirectoryRecordLength { get; set; }
/// <summary>
/// Length of the extended attribute record
/// If no extended attribute record is used, set to 0x00
/// </summary>
public byte ExtendedAttributeRecordLength { get; set; }
/// <summary>
/// Logical block number of the first logical block allocated to this extent
/// </summary>
public BothInt32 ExtentLocation { get; set; }
/// <summary>
/// Number of bytes allocated to this extent
/// </summary>
public BothInt32 ExtentLength { get; set; }
/// <summary>
/// Datetime of recording for the Directory Record
/// If not specified, all values are 0x00
/// </summary>
public DirectoryRecordDateTime RecordingDateTime { get; set; }
/// <summary>
/// Flags for indicating attributes of the directory record
/// </summary>
public FileFlags FileFlags { get; set; }
/// <summary>
/// Assigned file unit size for the file section (interleaved mode)
/// 0x00 if the file is not recorded in interleaved mode
/// </summary>
public byte FileUnitSize { get; set; }
/// <summary>
/// Assigned interleave gap size for the file section (interleaved mode)
/// 0x00 if the file is not recorded in interleaved mode
/// </summary>
public byte InterleaveGapSize { get; set; }
/// <summary>
/// Volume sequence ordinal number of the volume in the volume set on which the record extent is recorded
/// </summary>
public BothInt16 VolumeSequenceNumber { get; set; }
/// <summary>
/// Length of the FileIdentifier field in bytes
/// </summary>
public byte FileIdentifierLength { get; set; }
/// <summary>
/// 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)
/// </summary>
public byte[] FileIdentifier { get; set; }
/// <summary>
/// If record length prior to this is odd, the FileIdentifier is followed by a single padding byte (0x00)
/// Optional field
/// </summary>
public byte? PaddingField { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[]? SystemUse { get; set; }
}
}

View File

@@ -0,0 +1,47 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// Datetime format used by ISO9660 DirectoryRecord
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class DirectoryRecordDateTime
{
/// <summary>
/// Number of years since 1900
/// </summary>
public byte YearsSince1990 { get; set; }
/// <summary>
/// Month of the year, 1-12
/// </summary>
public byte Month { get; set; }
/// <summary>
/// Day of the month, 1-31
/// </summary>
public byte Day { get; set; }
/// <summary>
/// Hour of the day, 0-23
/// </summary>
public byte Hour { get; set; }
/// <summary>
/// Minute of the hour, 0-59
/// </summary>
public byte Minute { get; set; }
/// <summary>
/// Second of the minute, 0-59
/// </summary>
public byte Second { get; set; }
/// <summary>
/// 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)
/// </summary>
public byte TimezoneOffset { get; set; }
}
}

View File

@@ -0,0 +1,227 @@
using System;
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// Enum for VolumeDescriptor.Type
/// All values 4-254 are reserved
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public enum VolumeDescriptorType : byte
{
/// <summary>
/// Primary Volume Descriptor
/// </summary>
BOOT_RECORD_VOLUME_DESCRIPTOR = 0x00,
/// <summary>
/// Primary Volume Descriptor
/// </summary>
PRIMARY_VOLUME_DESCRIPTOR = 0x01,
/// <summary>
/// Supplementary Volume Descriptor
/// </summary>
SUPPLEMENTARY_VOLUME_DESCRIPTOR = 0x02,
/// <summary>
/// Enhanced Volume Descriptor (including Joliet extensions)
/// </summary>
ENHANCED_VOLUME_DESCRIPTOR = SUPPLEMENTARY_VOLUME_DESCRIPTOR,
/// <summary>
/// Volume Partition Descriptor
/// </summary>
VOLUME_PARTITION_DESCRIPTOR = 0x03,
/// <summary>
/// Volume Descriptor Set Terminator
/// </summary>
VOLUME_DESCRIPTOR_SET_TERMINATOR = 0xFF,
}
/// <summary>
/// Enum for DirectoryRecord.FileFlags
/// Flag 6 (Bit 5 LSB): Reserved: 0
/// Flag 7 (Bit 5 LSB): Reserved: 0
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
[Flags]
public enum FileFlags : byte
{
/// <summary>
/// Flag 1 (Bit 0 LSB): Existence
/// 1 if file should be hidden from the user upon inquiry
/// 0 otherwise
/// </summary>
EXISTENCE = 0x01,
/// <summary>
/// Flag 2 (Bit 1 LSB): Directory
/// 1 if the directory Record identifies a directory
/// 0 otherwise
/// </summary>
DIRECTORY = 0x02,
/// <summary>
/// Flag 3 (Bit 2 LSB): Associated File
/// 1 if the file is an associated file
/// 0 otherwise
/// </summary>
ASSOCIATED_FILE = 0x04,
/// <summary>
/// Flag 4 (Bit 3 LSB): Record
/// 1 if file has record format specified by non-zero record format of extended attribute record
/// 0 otherwise
/// </summary>
RECORD = 0x08,
/// <summary>
/// Flag 5 (Bit 4 LSB): Protection
/// 1 if owner/group ID is set for the file and permissions field is set properly
/// 0 otherwise
/// </summary>
PROTECTION = 0x10,
/// <summary>
/// Flag 6 (Bit 5 LSB): Reserved by ISO9660
/// </summary>
RESERVED_BIT5 = 0x20,
/// <summary>
/// Flag 7 (Bit 6 LSB): Reserved by ISO9660
/// </summary>
RESERVED_BIT6 = 0x40,
/// <summary>
/// Flag 8 (Bit 7 LSB): Multi-extent
/// 1 if Directory Extent is not the final record for the file
/// 0 otherwise
/// </summary>
MULTI_EXTENT = 0x80,
}
/// <summary>
/// Enum for SupplementaryVolumeDescriptor.VolumeFlags
/// Flag 1 (Bit 0, LSB) is used
/// All other flags/bits are reserved (0x00)
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
[Flags]
public enum VolumeFlags : byte
{
/// <summary>
/// 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
/// </summary>
UNREGISTERED_ESCAPE_SEQUENCES = 0x01,
}
/// <summary>
/// Enum for ExtendedAttributeRecord.Permissions
/// Every 2nd bit is fixed to 1 (i.e. minimum value of 0xAAAA)
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
[Flags]
public enum Permissions : ushort
{
/// <summary>
/// Flag 1 (Bit 0): 1 if system users may not read, 0 otherwise
/// </summary>
SYSTEM_USER_CANNOT_READ = 0b0000_0000_0000_0001,
/// <summary>
/// Flag 2 (Bit 2): 1 if system users may not execute, 0 otherwise
/// </summary>
SYSTEM_USER_CANNOT_EXECUTE = 0b0000_0000_0000_0100,
/// <summary>
/// Flag 3 (Bit 4): 1 if owner may not read, 0 otherwise
/// </summary>
OWNER_CANNOT_READ = 0b0000_0000_0001_0000,
/// <summary>
/// Flag 4 (Bit 6): 1 if owner may not execute, 0 otherwise
/// </summary>
OWNER_CANNOT_EXECUTE = 0b0000_0000_0100_0000,
/// <summary>
/// Flag 5 (Bit 8): 1 if group members may not read, 0 otherwise
/// </summary>
GROUP_MEMBER_CANNOT_READ = 0b0000_0001_0000_0000,
/// <summary>
/// Flag 6 (Bit 10): 1 if group members may not execute, 0 otherwise
/// </summary>
GROUP_MEMBER_CANNOT_EXECUTE = 0b0000_0100_0000_0000,
/// <summary>
/// Flag 7 (Bit 12): 1 if non-group members may not read, 0 if any user can read
/// </summary>
NON_GROUP_MEMBER_CANNOT_READ = 0b0001_0000_0000_0000,
/// <summary>
/// Flag 8 (Bit 14): 1 if non-group members may not execute, 0 if any user can execute
/// </summary>
NON_GROUP_MEMBER_CANNOT_EXECUTE = 0b0100_0000_0000_0000,
/// <summary>
/// Fixed values in the enum, every other bit set to 1
/// </summary>
PERMISSIONS_MASK = 0b1010_1010_1010_1010,
}
/// <summary>
/// Enum for ExtendedAttributeRecord.RecordFormat
/// 4-127 (0x04-7F): Reserved
/// 128-255 (0x80-FF): System Use
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public enum RecordFormat : byte
{
/// <summary>
/// Record format unspecified by this type
/// </summary>
UNSPECIFIED = 0x00,
/// <summary>
/// Sequence of fixed-length records
/// </summary>
FIXED_LENGTH_RECORDS = 0x01,
/// <summary>
/// Sequence of variable-length records, Record Control World is LSB
/// </summary>
VARIABLE_LENGTH_RECORDS_LSB = 0x02,
/// <summary>
/// Sequence of variable-length records, Record Control World is MSB
/// </summary>
VARIABLE_LENGTH_RECORDS_MSG = 0x03,
}
/// <summary>
/// Enum for ExtendedAttributeRecord.RecordAttributes
/// 3-255 (0x03-FF): Reserved by ISO9660
/// </summary>
/// <see href="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public enum RecordAttributes : byte
{
/// <summary>
/// Records are preceeded by linefeed and followed by carriage return
/// </summary>
LINEFEED_CARRIAGE_RETURN = 0x00,
/// <summary>
/// First byte of each record is Fortran-style
/// </summary>
FORTRAN_STYLE = 0x01,
/// <summary>
/// Record contains necessary control information within itself
/// </summary>
SELF_DEFINED = 0x02,
}
}

View File

@@ -0,0 +1,111 @@
using SabreTools.Numerics;
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// ISO9660 Extended Attribute Record
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class ExtendedAttributeRecord
{
/// <summary>
/// Owner ID number for this file
/// 0x0000 if no owner, implies 0x0000 Group ID
/// </summary>
public BothInt16 OwnerIdentification { get; set; }
/// <summary>
/// Group ID number for the owner of this file
/// 0x0000 if no group, implies 0x0000 Owner ID
/// </summary>
public BothInt16 GroupIdentification { get; set; }
/// <summary>
/// 16-bit flag variable with 8 flags where every other bit is set to 1
/// i.e. minimum value of 0b1010101010101010 (0xAAAA)
/// </summary>
public Permissions Permissions { get; set; }
/// <summary>
/// Datetime of when the file content was created
/// </summary>
public DecDateTime FileCreationDateTime { get; set; }
/// <summary>
/// Datetime of when the file content was last modified
/// </summary>
public DecDateTime FileModificationDateTime { get; set; }
/// <summary>
/// Datetime of when the file content expires
/// </summary>
public DecDateTime FileExpirationDateTime { get; set; }
/// <summary>
/// Datetime of when the file content is effective from
/// </summary>
public DecDateTime FileEffectiveDateTime { get; set; }
/// <summary>
/// Record format type
/// </summary>
public RecordFormat RecordFormat { get; set; }
/// <summary>
/// Record attributes
/// Note: If RecordType is zero, this field is ignored by readers
/// </summary>
public RecordAttributes RecordAttributes { get; set; }
/// <summary>
/// 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
/// </summary>
public BothInt16 RecordLength { get; set; }
/// <summary>
/// 32-byte name of the intended system
/// Primary: a-characters or a1-characters only, padded to the right with spaces
/// </summary>
public byte[] SystemIdentifier { get; set; }
/// <summary>
/// 64-bytes for system use
/// </summary>
public byte[] SystemUse { get; set; }
/// <summary>
/// Extended Attribyte Record Version
/// ISO9660 sets this to 0x01
/// </summary>
public byte ExtendedAttributeRecordVersion { get; set; }
/// <summary>
/// Length of the escape sequences field
/// </summary>
public byte EscapeSequencesLength { get; set; }
/// <summary>
/// 64-bytes reserved (0x00)
/// </summary>
public byte[] Reserved64Bytes { get; set; }
/// <summary>
/// Length of the Application use field
/// </summary>
public BothInt16 ApplicationLength { get; set; }
/// <summary>
/// ApplicationLength-bytes for application use
/// </summary>
public byte[] ApplicationUse { get; set; }
/// <summary>
/// EscapeSequencesLength-bytes list of escape sequences to interpret this file
/// Optional, and if present, padded to the right with 0x00
/// </summary>
public byte[]? EscapeSequences { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// ISO9660 File Extent, the file data itself
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public class FileExtent
{
/// <summary>
/// File's extended attribute record
/// Optional field, and never present for Directory-type File Extents
/// </summary>
public ExtendedAttributeRecord? ExtendedAttributeRecord { get; set; }
/// <summary>
/// Byte array of data within the file extent (after the Extended Attribyte Record)
/// </summary>
public byte[]? Data { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// 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)
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class FileSystem
{
/// <summary>
/// Set of volumes (disc images) that make up an ISO9660 file system
/// </summary>
public Volume[] VolumeSet { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// Generic Volume Descriptor
/// Volume Descriptor with contents not defined by ISO9660
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class GenericVolumeDescriptor : VolumeDescriptor
{
/// <summary>
/// 2041 bytes
/// </summary>
public byte[] Data { get; set; }
}
}

View File

@@ -0,0 +1,35 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// 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.
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class PathTableGroup
{
/// <summary>
/// Type-L Path Table (Little Endian)
/// Note: This is meant to exist, but is nullable in case PathTableM is valid
/// </summary>
public PathTableRecord[]? PathTableL { get; set; }
/// <summary>
/// Optional Type-L Path Table (Little Endian)
/// Note: This is optional
/// </summary>
public PathTableRecord[]? OptionalPathTableL { get; set; }
/// <summary>
/// Type-M Path Table (Big Endian)
/// Note: This is meant to exist, but is nullable in case PathTableL is valid
/// </summary>
public PathTableRecord[]? PathTableM { get; set; }
/// <summary>
/// Optional Type-M Path Table (Big Endian)
/// Note: This is optional
/// </summary>
public PathTableRecord[]? OptionalPathTableM { get; set; }
}
}

View File

@@ -0,0 +1,42 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// ISO9660 Path Table Record
/// Each path table record is numbered (starting from 1), which corresponds to the ordinal number of the corresponding directory
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class PathTableRecord
{
/// <summary>
/// Length of Directory Identifier
/// </summary>
public byte DirectoryIdentifierLength { get; set; }
/// <summary>
/// Length of the the extended attribute record
/// </summary>
public byte ExtendedAttributeRecordLength { get; set; }
/// <summary>
/// Location of the first logical block number of the first logical block allocated to the extent
/// </summary>
public int ExtentLocation { get; set; }
/// <summary>
/// Location of the first logical block number of the first logical block allocated to the extent
/// </summary>
public short ParentDirectoryNumber { get; set; }
/// <summary>
/// Directory name
/// Either d-characters or d1-characters, or a single 0x00 byte
/// </summary>
public byte[] DirectoryIdentifier { get; set; }
/// <summary>
/// If DirectoryIdentifierLength is odd, the DirectoryIdentifier is followed by a single padding byte (0x00)
/// Optional field
/// </summary>
public byte? PaddingField { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// Primary Volume Descriptor
/// Volume Descriptor with VolumeDescriptorType = 0x01
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class PrimaryVolumeDescriptor : BaseVolumeDescriptor
{
/// <summary>
/// 1 unused byte at offset 7, should be 0x00
/// Note: This is used for VolumeFlags on SupplementaryVolumeDescriptor
/// </summary>
public byte UnusedByte { get; set; }
/// <summary>
/// 32 unused bytes at offset 88, should be all 0x00
/// Note: These is used for EscapeSequences on SupplementaryVolumeDescriptor
/// </summary>
public byte[] Unused32Bytes { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// 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.
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class SupplementaryVolumeDescriptor : BaseVolumeDescriptor
{
/// <summary>
/// Remaining bits are reserved (set to 0)
/// Note: Joliet Extension implies Constants.JolietVolumeFlags (0x00)
/// </summary>
public VolumeFlags VolumeFlags { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[] EscapeSequences { get; set; }
}
}

View File

@@ -0,0 +1,50 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// 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
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class Volume
{
/// <summary>
/// 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
/// </summary>
public byte[] SystemArea { get; set; }
#region Data Area
/// <summary>
/// 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
/// </summary>
public VolumeDescriptor[] VolumeDescriptorSet { get; set; }
/// <summary>
/// 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
/// </summary>
public PathTableGroup[] PathTableGroups { get; set; }
/// <summary>
/// Map of sector numbers and the directory at that sector number
/// Each Directory contains child directory and file descriptors
/// </summary>
public Dictionary<int, DirectoryExtent> DirectoryDescriptors { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,30 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// 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
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public abstract class VolumeDescriptor
{
/// <summary>
/// The type of VolumeDescriptor
/// </summary>
public VolumeDescriptorType Type { get; set; }
/// <summary>
/// 5-byte magic
/// Set to Constants.StandardIdentifier ("CD001")
/// On non-ISO9660 CD-i discs, set to Constants.StandardIdentifierCDI ("CD-I ")
/// </summary>
public byte[] Identifier { get; set; }
/// <summary>
/// The Volume Descriptor version number
/// 1 for all specific Volume Descriptors other than Enhanced Volume Descriptor
/// 2 for Enhanced Volume Descriptor (including Joliet)
/// </summary>
public byte Version { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// Volume Descriptor Set Terminator
/// Blank Descriptor with VolumeDescriptorType = 0xFF
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class VolumeDescriptorSetTerminator : VolumeDescriptor
{
/// <summary>
/// 2041 reserved bytes, should be 0x00
/// </summary>
public byte[] Reserved2041Bytes { get; set; }
}
}

View File

@@ -0,0 +1,44 @@
using SabreTools.Numerics;
namespace SabreTools.Data.Models.ISO9660
{
/// <summary>
/// Volume Partition Descriptor
/// Volume Descriptor with VolumeDescriptorType = 0x03
/// </summary>
/// <see cref="https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf"/>
public sealed class VolumePartitionDescriptor : VolumeDescriptor
{
/// <summary>
/// 1 unused byte at offset 7, should be 0x00
/// </summary>
public byte UnusedByte { get; set; }
/// <summary>
/// 32-byte name of the intended system that can use this record
/// a-characters only
/// </summary>
public byte[] SystemIdentifier { get; set; }
/// <summary>
/// 32-byte name of this volume partition
/// d-characters only
/// </summary>
public byte[] VolumePartitionIdentifier { get; set; }
/// <summary>
/// Logical block number of the first logical block allocated to this volume partition
/// </summary>
public BothInt32 VolumePartitionLocation { get; set; }
/// <summary>
/// Number of logical blocks allocated to this volume partition
/// </summary>
public BothInt32 VolumePartitionSize { get; set; }
/// <summary>
/// 1960 bytes for System Use, contents not defined by ISO9660
/// </summary>
public byte[] SystemUse { get; set; }
}
}

View File

@@ -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;
}
/// <summary>
/// Export the item information as pretty-printed text
/// </summary>
private static StringBuilder PrettyPrint(this Wrapper.ISO9660 item)
{
var builder = new StringBuilder();
ISO9660.Print(builder, item.Model);
return builder;
}
/// <summary>
/// Export the item information as pretty-printed text
/// </summary>

View File

@@ -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<Volume>
{
/// <inheritdoc/>
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<int, DirectoryExtent>? 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;
}
}
}

View File

@@ -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<Volume>
{
/// <inheritdoc/>
public override Volume? Deserialize(Stream? data) => Deserialize(data, Constants.MinimumSectorSize);
/// <inheritdoc cref="Deserialize(Stream?)" />
/// <param name="sectorLength">Size of the logical sector used in the volume</param>
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
/// <summary>
/// Parse a Stream into an array of VolumeDescriptor objects
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <returns>Filled VolumeDescriptor[] on success, null on error</returns>
public static VolumeDescriptor[]? ParseVolumeDescriptorSet(Stream data, short sectorLength)
{
var obj = new List<VolumeDescriptor>();
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];
}
/// <summary>
/// Parse a Stream into a VolumeDescriptor
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <returns>Filled VolumeDescriptor on success, null on error</returns>
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),
};
}
/// <summary>
/// Parse a Stream into a BootRecordVolumeDescriptor
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <returns>Filled BootRecordVolumeDescriptor on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a PrimaryVolumeDescriptor
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <returns>Filled PrimaryVolumeDescriptor on success, null on error</returns>
public static PrimaryVolumeDescriptor? ParsePrimaryVolumeDescriptor(Stream data, short sectorLength)
{
var obj = new PrimaryVolumeDescriptor();
obj.Type = VolumeDescriptorType.PRIMARY_VOLUME_DESCRIPTOR;
return (PrimaryVolumeDescriptor?)ParseBaseVolumeDescriptor(data, sectorLength, obj);
}
/// <summary>
/// Parse a Stream into a SupplementaryVolumeDescriptor
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <returns>Filled SupplementaryVolumeDescriptor on success, null on error</returns>
public static SupplementaryVolumeDescriptor? ParseSupplementaryVolumeDescriptor(Stream data, short sectorLength)
{
var obj = new SupplementaryVolumeDescriptor();
obj.Type = VolumeDescriptorType.SUPPLEMENTARY_VOLUME_DESCRIPTOR;
return (SupplementaryVolumeDescriptor?)ParseBaseVolumeDescriptor(data, sectorLength, obj);
}
/// <summary>
/// Parse a Stream into a BaseVolumeDescriptor
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <returns>Filled BaseVolumeDescriptor on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a VolumePartitionDescriptor
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <returns>Filled VolumePartitionDescriptor on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a VolumeDescriptorSetTerminator
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <returns>Filled VolumeDescriptorSetTerminator on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a GenericVolumeDescriptor
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="type">Type </param>
/// <returns>Filled GenericVolumeDescriptor on success, null on error</returns>
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
/// <summary>
/// Parse a Stream into an array of PathTableGroup
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="vd">Primary/Supplementary/Enhanced Volume Descriptor pointing to path table(s)</param>
/// <returns>Filled PathTableGroup[] on success, null on error</returns>
public static PathTableGroup[]? ParsePathTableGroups(Stream data, short sectorLength, VolumeDescriptor[] vdSet)
{
var groups = new List<PathTableGroup>();
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];
}
/// <summary>
/// Parse a Stream into a list of PathTableGroup
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="vd">Primary/Supplementary/Enhanced Volume Descriptor pointing to path table(s)</param>
/// <returns>Filled list of PathTableGroup on success, null on error</returns>
public static List<PathTableGroup>? ParsePathTableGroup(Stream data, short sectorLength, BaseVolumeDescriptor vd)
{
var groups = new List<PathTableGroup>();
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;
}
/// <summary>
/// Parse a Stream into an array of path table records
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="tableSize">Size of the path table</param>
/// <param name="littleEndian">True if path table is little endian, false if big endian</param>
/// <returns>Filled array of path table records on success, null on error</returns>
public static PathTableRecord[]? ParsePathTable(Stream data, short sectorLength, int tableSize, bool littleEndian)
{
var pathTable = new List<PathTableRecord>();
// 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
/// <summary>
/// Parse a Stream into a map of sector numbers to Directory
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="vd">Set of volume descriptors for a volume</param>
/// <returns>Filled Dictionary of int to Directory on success, null on error</returns>
public static Dictionary<int, DirectoryExtent>? ParseDirectoryDescriptors(Stream data, short sectorLength, VolumeDescriptor[] vdSet)
{
var directories = new Dictionary<int, DirectoryExtent>();
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;
}
/// <summary>
/// Parse a Stream into a map of sector numbers to Directory
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorLength">Number of bytes in a logical sector (usually 2048)</param>
/// <param name="blockLength">Number of bytes in a logical block (usually 2048)</param>
/// <param name="dr">Directory record pointing to the directory extent</param>
/// <param name="bigEndian">True if the Big Endian extent location/length should be parsed</param>
/// <returns>Filled Dictionary of int to Directory on success, null on error</returns>
public static Dictionary<int, DirectoryExtent>? 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<DirectoryRecord>();
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<int, DirectoryExtent>();
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;
}
/// <summary>
/// Parse a Stream into a DirectoryRecord
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="root">true if root directory record, false otherwise</param>
/// <returns>Filled DirectoryRecord on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a DirectoryRecordDateTime
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled DirectoryRecordDateTime on success, null on error</returns>
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
/// <summary>
/// Parse a Stream into a DecDateTime
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled DecDateTime on success, null on error</returns>
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;
}
}
}

View File

@@ -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))

View File

@@ -0,0 +1,23 @@
using System;
using System.IO;
using SabreTools.Data.Models.ISO9660;
namespace SabreTools.Serialization.Wrappers
{
public partial class ISO9660 : IExtractable
{
/// <inheritdoc/>
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;
}
}
}

View File

@@ -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<Volume>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "ISO 9660 Volume";
#endregion
#region Extension Properties
/// <summary>
/// Volume Descriptors
/// </summary>
public VolumeDescriptor[] VolumeDescriptorSet => Model.VolumeDescriptorSet ?? [];
/// <summary>
/// Path Tables
/// </summary>
public PathTableGroup[] PathTableGroups => Model.PathTableGroups ?? [];
/// <summary>
/// Directory Descriptors
/// </summary>
public Dictionary<int, DirectoryExtent> DirectoryDescriptors => Model.DirectoryDescriptors ?? [];
#endregion
#region Constructors
/// <inheritdoc/>
public ISO9660(Volume model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public ISO9660(Volume model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public ISO9660(Volume model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public ISO9660(Volume model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public ISO9660(Volume model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public ISO9660(Volume model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an ISO9660 Volume from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the archive</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>An ISO 9660 Volume wrapper on success, null on failure</returns>
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);
}
/// <summary>
/// Create an ISO9660 Volume from a Stream
/// </summary>
/// <param name="data">Stream representing the archive</param>
/// <returns>An ISO9660 Volume wrapper on success, null on failure</returns>
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
}
}

View File

@@ -82,6 +82,16 @@ namespace SabreTools.Serialization.Wrappers
/// </summary>
InstallShieldCAB,
/// <summary>
/// PS3 ISO Rebuild Data
/// </summary>
IRD,
/// <summary>
/// ISO 9660 Volume (Disc image)
/// </summary>
ISO9660,
/// <summary>
/// Link Data Security encrypted file
/// </summary>