mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-09-23 23:35:09 +00:00
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:
@@ -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);
|
||||
|
||||
38
SabreTools.Serialization/Extensions/ISO9660.cs
Normal file
38
SabreTools.Serialization/Extensions/ISO9660.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
209
SabreTools.Serialization/Models/ISO9660/BaseVolumeDescriptor.cs
Normal file
209
SabreTools.Serialization/Models/ISO9660/BaseVolumeDescriptor.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
118
SabreTools.Serialization/Models/ISO9660/Constants.cs
Normal file
118
SabreTools.Serialization/Models/ISO9660/Constants.cs
Normal 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
|
||||
}
|
||||
}
|
||||
54
SabreTools.Serialization/Models/ISO9660/DecDateTime.cs
Normal file
54
SabreTools.Serialization/Models/ISO9660/DecDateTime.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
14
SabreTools.Serialization/Models/ISO9660/DirectoryExtent.cs
Normal file
14
SabreTools.Serialization/Models/ISO9660/DirectoryExtent.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
88
SabreTools.Serialization/Models/ISO9660/DirectoryRecord.cs
Normal file
88
SabreTools.Serialization/Models/ISO9660/DirectoryRecord.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
227
SabreTools.Serialization/Models/ISO9660/Enums.cs
Normal file
227
SabreTools.Serialization/Models/ISO9660/Enums.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
20
SabreTools.Serialization/Models/ISO9660/FileExtent.cs
Normal file
20
SabreTools.Serialization/Models/ISO9660/FileExtent.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
15
SabreTools.Serialization/Models/ISO9660/FileSystem.cs
Normal file
15
SabreTools.Serialization/Models/ISO9660/FileSystem.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
35
SabreTools.Serialization/Models/ISO9660/PathTableGroup.cs
Normal file
35
SabreTools.Serialization/Models/ISO9660/PathTableGroup.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
42
SabreTools.Serialization/Models/ISO9660/PathTableRecord.cs
Normal file
42
SabreTools.Serialization/Models/ISO9660/PathTableRecord.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
50
SabreTools.Serialization/Models/ISO9660/Volume.cs
Normal file
50
SabreTools.Serialization/Models/ISO9660/Volume.cs
Normal 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
|
||||
}
|
||||
}
|
||||
30
SabreTools.Serialization/Models/ISO9660/VolumeDescriptor.cs
Normal file
30
SabreTools.Serialization/Models/ISO9660/VolumeDescriptor.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
496
SabreTools.Serialization/Printers/ISO9660.cs
Normal file
496
SabreTools.Serialization/Printers/ISO9660.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
809
SabreTools.Serialization/Readers/ISO9660.cs
Normal file
809
SabreTools.Serialization/Readers/ISO9660.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
23
SabreTools.Serialization/Wrappers/ISO9660.Extraction.cs
Normal file
23
SabreTools.Serialization/Wrappers/ISO9660.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
SabreTools.Serialization/Wrappers/ISO9660.cs
Normal file
111
SabreTools.Serialization/Wrappers/ISO9660.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user