using System.Collections.Generic; using System.IO; using SabreTools.Data.Extensions; using SabreTools.Data.Models.ISO9660; using SabreTools.IO.Extensions; using SabreTools.Matching; using SabreTools.Numerics.Extensions; #pragma warning disable CA1822 // Mark members as static #pragma warning disable IDE0017 // Simplify object initialization namespace SabreTools.Serialization.Readers { public class ISO9660 : BaseBinaryReader { /// public override Volume? Deserialize(Stream? data) => Deserialize(data, Constants.MinimumSectorSize); /// /// Size of the logical sector used in the volume public Volume? Deserialize(Stream? data, short sectorLength) { // If the data is invalid if (data is 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.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 is 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 is null || dirs.Count == 0) return null; volume.DirectoryDescriptors = dirs; return volume; } catch { // Ignore the actual error return null; } } #region Volume Descriptor Parsing /// /// Parse a Stream into an array of VolumeDescriptor objects /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Filled VolumeDescriptor[] on success, null on error public static VolumeDescriptor[] ParseVolumeDescriptorSet(Stream data, short sectorLength) { var obj = new List(); bool setTerminated = false; while (data.Position < data.Length) { var volumeDescriptor = ParseVolumeDescriptor(data, sectorLength); // If no valid volume descriptor could be read, return the current set if (volumeDescriptor is 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.SeekIfPossible(-sectorLength, SeekOrigin.Current); return [.. obj]; } // Add the valid read volume descriptor to the set obj.Add(volumeDescriptor); // If the set terminator was read, set the set terminated flag (further set terminators may be present) if (!setTerminated && volumeDescriptor.Type == VolumeDescriptorType.VOLUME_DESCRIPTOR_SET_TERMINATOR) setTerminated = true; } return [.. obj]; } /// /// Parse a Stream into a VolumeDescriptor /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Filled VolumeDescriptor on success, null on error public static VolumeDescriptor? ParseVolumeDescriptor(Stream data, short sectorLength) { var type = (VolumeDescriptorType)data.ReadByteValue(); return type switch { // Known Volume Descriptors defined by ISO9660 VolumeDescriptorType.BOOT_RECORD_VOLUME_DESCRIPTOR => ParseBootRecordVolumeDescriptor(data, sectorLength), VolumeDescriptorType.PRIMARY_VOLUME_DESCRIPTOR => ParsePrimaryVolumeDescriptor(data, sectorLength), VolumeDescriptorType.SUPPLEMENTARY_VOLUME_DESCRIPTOR => ParseSupplementaryVolumeDescriptor(data, sectorLength), VolumeDescriptorType.VOLUME_PARTITION_DESCRIPTOR => ParseVolumePartitionDescriptor(data, sectorLength), VolumeDescriptorType.VOLUME_DESCRIPTOR_SET_TERMINATOR => ParseVolumeDescriptorSetTerminator(data, sectorLength), // Unknown Volume Descriptor _ => ParseGenericVolumeDescriptor(data, sectorLength, type), }; } /// /// Parse a Stream into a BootRecordVolumeDescriptor /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Filled BootRecordVolumeDescriptor on success, null on error public static BootRecordVolumeDescriptor? ParseBootRecordVolumeDescriptor(Stream data, short sectorLength) { var obj = new BootRecordVolumeDescriptor(); obj.Type = VolumeDescriptorType.VOLUME_PARTITION_DESCRIPTOR; obj.Identifier = data.ReadBytes(5); // Validate Identifier, return null and rewind if invalid if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) { data.SeekIfPossible(-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.SeekIfPossible(sectorLength - Constants.MinimumSectorSize, SeekOrigin.Current); return obj; } /// /// Parse a Stream into a PrimaryVolumeDescriptor /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Filled PrimaryVolumeDescriptor on success, null on error public static PrimaryVolumeDescriptor? ParsePrimaryVolumeDescriptor(Stream data, short sectorLength) { var obj = new PrimaryVolumeDescriptor(); obj.Type = VolumeDescriptorType.PRIMARY_VOLUME_DESCRIPTOR; obj.Identifier = data.ReadBytes(5); // Validate Identifier, return null and rewind if invalid if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) { data.SeekIfPossible(-6, SeekOrigin.Current); return null; } obj.Version = data.ReadByteValue(); obj.UnusedByte = data.ReadByteValue(); obj.SystemIdentifier = data.ReadBytes(32); obj.VolumeIdentifier = data.ReadBytes(32); obj.Unused8Bytes = data.ReadBytes(8); obj.VolumeSpaceSize = data.ReadInt32BothEndian(); obj.Unused32Bytes = data.ReadBytes(32); 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(); obj.RootDirectoryRecord = ParseDirectoryRecord(data, true); 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.ReadBytes(sectorLength - Constants.MinimumSectorSize); return obj; } /// /// Parse a Stream into a SupplementaryVolumeDescriptor /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Filled SupplementaryVolumeDescriptor on success, null on error public static SupplementaryVolumeDescriptor? ParseSupplementaryVolumeDescriptor(Stream data, short sectorLength) { var obj = new SupplementaryVolumeDescriptor(); obj.Type = VolumeDescriptorType.SUPPLEMENTARY_VOLUME_DESCRIPTOR; obj.Identifier = data.ReadBytes(5); // Validate Identifier, return null and rewind if invalid if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) { data.SeekIfPossible(-6, SeekOrigin.Current); return null; } obj.Version = data.ReadByteValue(); obj.VolumeFlags = (VolumeFlags)data.ReadByteValue(); obj.SystemIdentifier = data.ReadBytes(32); obj.VolumeIdentifier = data.ReadBytes(32); obj.Unused8Bytes = data.ReadBytes(8); obj.VolumeSpaceSize = data.ReadInt32BothEndian(); obj.EscapeSequences = data.ReadBytes(32); 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(); obj.RootDirectoryRecord = ParseDirectoryRecord(data, true); 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.ReadBytes(sectorLength - Constants.MinimumSectorSize); return obj; } /// /// Parse a Stream into a VolumePartitionDescriptor /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Filled VolumePartitionDescriptor on success, null on error public static VolumePartitionDescriptor? ParseVolumePartitionDescriptor(Stream data, short sectorLength) { var obj = new VolumePartitionDescriptor(); obj.Type = VolumeDescriptorType.VOLUME_PARTITION_DESCRIPTOR; obj.Identifier = data.ReadBytes(5); // Validate Identifier, return null and rewind if invalid if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) { data.SeekIfPossible(-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.ReadBytes(sectorLength - Constants.MinimumSectorSize); return obj; } /// /// Parse a Stream into a VolumeDescriptorSetTerminator /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Filled VolumeDescriptorSetTerminator on success, null on error public static VolumeDescriptorSetTerminator? ParseVolumeDescriptorSetTerminator(Stream data, short sectorLength) { var obj = new VolumeDescriptorSetTerminator(); obj.Type = VolumeDescriptorType.VOLUME_DESCRIPTOR_SET_TERMINATOR; obj.Identifier = data.ReadBytes(5); // Validate Identifier, return null and rewind if invalid if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) { data.SeekIfPossible(-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.ReadBytes(sectorLength - Constants.MinimumSectorSize); return obj; } /// /// Parse a Stream into a GenericVolumeDescriptor /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Type /// Filled GenericVolumeDescriptor on success, null on error public static GenericVolumeDescriptor? ParseGenericVolumeDescriptor(Stream data, short sectorLength, VolumeDescriptorType type) { var obj = new GenericVolumeDescriptor(); obj.Type = type; obj.Identifier = data.ReadBytes(5); // Validate Identifier, return null and rewind if invalid if (!obj.Identifier.EqualsExactly(Constants.StandardIdentifier)) { data.SeekIfPossible(-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.ReadBytes(sectorLength - Constants.MinimumSectorSize); return obj; } #endregion #region Path Table Parsing /// /// Parse a Stream into an array of PathTableGroup /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Primary/Supplementary/Enhanced Volume Descriptor pointing to path table(s) /// Filled PathTableGroup[] on success, null on error public static PathTableGroup[]? ParsePathTableGroups(Stream data, short sectorLength, VolumeDescriptor[] vdSet) { var groups = new List(); foreach (VolumeDescriptor vd in vdSet) { // Parse the path table group in the base volume descriptor var pathTableGroups = ParsePathTableGroup(data, sectorLength, vd); if (pathTableGroups.Count > 0) groups.AddRange(pathTableGroups); } // Return error (null) if no valid path table groups were found if (groups.Count == 0) return null; return [.. groups]; } /// /// Parse a Stream into a list of PathTableGroup /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Primary/Supplementary/Enhanced Volume Descriptor pointing to path table(s) /// Filled list of PathTableGroup on success, null on error public static List ParsePathTableGroup(Stream data, short sectorLength, VolumeDescriptor vd) { int sizeL, sizeB, locationL, locationL2, locationM, locationM2; if (vd is PrimaryVolumeDescriptor pvd) { sizeL = pvd.PathTableSize.LittleEndian; sizeB = pvd.PathTableSize.BigEndian; locationL = pvd.PathTableLocationL; locationL2 = pvd.OptionalPathTableLocationL; locationM = pvd.PathTableLocationM; locationM2 = pvd.OptionalPathTableLocationM; } else if (vd is SupplementaryVolumeDescriptor svd) { sizeL = svd.PathTableSize.LittleEndian; sizeB = svd.PathTableSize.BigEndian; locationL = svd.PathTableLocationL; locationL2 = svd.OptionalPathTableLocationL; locationM = svd.PathTableLocationM; locationM2 = svd.OptionalPathTableLocationM; } else { return []; } var groups = new List(); short blockLength = vd.GetLogicalBlockSize(sectorLength); var groupL = new PathTableGroup(); if (locationL != 0 && ((locationL * blockLength) + sizeL) < data.Length) { data.SeekIfPossible(locationL * blockLength, SeekOrigin.Begin); groupL.PathTableL = ParsePathTable(data, sizeL, true); } if (locationL2 != 0 && ((locationL2 * blockLength) + sizeL) < data.Length) { data.SeekIfPossible(locationL2 * blockLength, SeekOrigin.Begin); groupL.OptionalPathTableL = ParsePathTable(data, sizeL, true); } if (locationM != 0 && ((locationM * blockLength) + sizeL) < data.Length) { data.SeekIfPossible(locationM * blockLength, SeekOrigin.Begin); groupL.PathTableM = ParsePathTable(data, sizeL, false); } if (locationM2 != 0 && ((locationM2 * blockLength) + sizeL) < data.Length) { data.SeekIfPossible(locationM2 * blockLength, SeekOrigin.Begin); groupL.OptionalPathTableM = ParsePathTable(data, sizeL, false); } // If no valid path tables were found, don't add the table group if (groupL.PathTableL is not null || groupL.OptionalPathTableL is not null || groupL.PathTableM is not null || groupL.OptionalPathTableM is not 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.SeekIfPossible(locationL * blockLength, SeekOrigin.Begin); groupB.PathTableL = ParsePathTable(data, sizeB, true); } if (locationL2 != 0 && ((locationL2 * blockLength) + sizeB) < data.Length) { data.SeekIfPossible(locationL2 * blockLength, SeekOrigin.Begin); groupB.OptionalPathTableL = ParsePathTable(data, sizeB, true); } if (locationM != 0 && ((locationM * blockLength) + sizeB) < data.Length) { data.SeekIfPossible(locationM * blockLength, SeekOrigin.Begin); groupB.PathTableM = ParsePathTable(data, sizeB, false); } if (locationM2 != 0 && ((locationM2 * blockLength) + sizeB) < data.Length) { data.SeekIfPossible(locationM2 * blockLength, SeekOrigin.Begin); groupB.OptionalPathTableM = ParsePathTable(data, sizeB, false); } // If no valid path tables were found, don't add the table group if (groupB.PathTableL is not null || groupB.OptionalPathTableL is not null || groupB.PathTableM is not null || groupB.OptionalPathTableM is not null) groups.Add(groupB); return groups; } /// /// Parse a Stream into an array of path table records /// /// Stream to parse /// Size of the path table /// True if path table is little endian, false if big endian /// Filled array of path table records on success, null on error public static PathTableRecord[]? ParsePathTable(Stream data, int tableSize, bool littleEndian) { var pathTable = new List(); // TODO: Better deal with invalid path table sizes < 10 (manually detect valid records to determine size) // Current status: Trusting path table length field (tableSize) int pos = 0; while (pos < tableSize) { var record = new PathTableRecord(); var directoryIdentifierLength = data.ReadByteValue(); // Check that the current record can fit within the current path table size pos += 8 + directoryIdentifierLength; if (directoryIdentifierLength % 2 != 0) pos += 1; if (pos > tableSize) { // Invalid record length, quit early // TODO: Try detect record length and recover? break; } record.DirectoryIdentifierLength = directoryIdentifierLength; record.ExtendedAttributeRecordLength = data.ReadByteValue(); // Read numerics with correct endianness if (littleEndian) { record.ExtentLocation = data.ReadInt32LittleEndian(); record.ParentDirectoryNumber = data.ReadInt16LittleEndian(); } else { record.ExtentLocation = data.ReadInt32BigEndian(); record.ParentDirectoryNumber = data.ReadInt16BigEndian(); } // Read the directory identifier record.DirectoryIdentifier = data.ReadBytes(record.DirectoryIdentifierLength); // Padding field is present is directory identifier length is odd if (record.DirectoryIdentifierLength % 2 != 0) record.PaddingField = data.ReadByteValue(); pathTable.Add(record); } // Return error (null) if no valid path table records were found if (pathTable.Count == 0) return null; return [.. pathTable]; } #endregion #region Directory Descriptor Parsing /// /// Parse a Stream into a map of sector numbers to Directory /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Set of volume descriptors for a volume /// Filled Dictionary of int to FileExtent on success, null on error public static Dictionary? ParseDirectoryDescriptors(Stream data, short sectorLength, VolumeDescriptor[] vdSet) { var directories = new Dictionary(); foreach (VolumeDescriptor vd in vdSet) { DirectoryRecord rootDirectoryRecord; if (vd is PrimaryVolumeDescriptor pvd) rootDirectoryRecord = pvd.RootDirectoryRecord; else if (vd is SupplementaryVolumeDescriptor svd) rootDirectoryRecord = svd.RootDirectoryRecord; else continue; // Determine logical block size short blockLength = vd.GetLogicalBlockSize(sectorLength); // Parse the root directory pointed to from the base volume descriptor var descriptors = ParseDirectory(data, sectorLength, blockLength, rootDirectoryRecord, false); if (descriptors is null || descriptors.Count == 0) continue; // Merge dictionaries foreach (var kvp in descriptors) { if (!directories.ContainsKey(kvp.Key)) directories.Add(kvp.Key, kvp.Value); } } // Return error (null) if no valid directory descriptors were found if (directories.Count == 0) return null; return directories; } /// /// Parse a Stream into a map of sector numbers to Directory /// /// Stream to parse /// Number of bytes in a logical sector (usually 2048) /// Number of bytes in a logical block (usually 2048) /// Directory record pointing to the directory extent /// True if the Big Endian extent location/length should be parsed /// Filled Dictionary of int to FileExtent on success, null on error public static Dictionary? ParseDirectory(Stream data, short sectorLength, short blockLength, DirectoryRecord dr, bool bigEndian) { var directories = new Dictionary(); int blocksPerSector = sectorLength / blockLength; // Use provided extent endinanness int extentLocation = bigEndian ? dr.ExtentLocation.BigEndian : dr.ExtentLocation.LittleEndian; uint extentLength = bigEndian ? dr.ExtentLength.BigEndian : dr.ExtentLength.LittleEndian; long extentOffset = extentLocation * (long)blockLength; long extentFinal = extentOffset + extentLength; // Deal with extent length ambiguity if (!dr.ExtentLength.IsValid) { // If provided extent length is invalid, use the other value if (extentLength <= 0 || extentFinal > data.Length) extentLength = bigEndian ? dr.ExtentLength.LittleEndian : dr.ExtentLength.BigEndian; } // Validate extent length if (extentLength <= 0 || extentFinal > data.Length) return null; // Move stream to directory location data.SeekIfPossible(extentOffset, SeekOrigin.Begin); // Check if the current extent is a directory #if NET20 || NET35 if ((dr.FileFlags & FileFlags.DIRECTORY) == FileFlags.DIRECTORY) #else if (dr.FileFlags.HasFlag(FileFlags.DIRECTORY)) #endif { // Start of directory should not be 0 int firstRecordLength = data.PeekByteValue(); if (firstRecordLength == 0) return null; // Read all directory records in this directory var records = new List(); uint 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) { int paddingLength = (int)((uint)sectorLength - (pos % (uint)sectorLength)); pos += (uint)paddingLength; _ = data.ReadBytes(paddingLength); // Finish parsing records if end reached if (pos >= extentLength) break; // Start of sector should not be 0, ignore entire directory int nextRecordLength = data.PeekByteValue(); if (nextRecordLength <= 0) return null; 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); // Compare recordLength with number of bytes in directoryRecord and return null if mismatch var readLength = 33 + directoryRecord.FileIdentifier.Length + (directoryRecord.PaddingField is null ? 0 : 1) + directoryRecord.SystemUse.Length; if (readLength != recordLength) return null; records.Add(directoryRecord); } // Add current directory to dictionary var currentDirectory = new DirectoryExtent(); currentDirectory.DirectoryRecords = [.. records]; directories.Add(extentLocation, 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 is null) continue; // Add new directories to dictionary foreach (var kvp in dir) { if (!directories.ContainsKey(kvp.Key)) directories.Add(kvp.Key, kvp.Value); } } } else { // Extent is a file, parse the Extended Attribute Record var fileExtent = new FileExtent(); if (dr.ExtendedAttributeRecordLength > 0) { var ear = ParseExtendedAttributeRecord(data); if (ear is not null) { fileExtent.ExtendedAttributeRecord = ear; } // Do not parse file data into file extent, too large // Put the file extent is the dictionary #if NETCOREAPP directories.TryAdd(extentLocation, fileExtent); #else if (!directories.ContainsKey(extentLocation)) directories.Add(extentLocation, fileExtent); #endif } } // If the extent location field is ambiguous, also parse the big-endian directory extent if (!bigEndian && dr.ExtentLocation.IsValid) { var bigEndianDir = ParseDirectory(data, sectorLength, blockLength, dr, true); if (bigEndianDir is not null) { // Add new directories to dictionary foreach (var kvp in bigEndianDir) { if (!directories.ContainsKey(kvp.Key)) directories.Add(kvp.Key, kvp.Value); } } } return directories; } /// /// Parse a Stream into a DirectoryRecord /// /// Stream to parse /// true if root directory record, false otherwise /// Filled DirectoryRecord on success, null on error public static DirectoryRecord ParseDirectoryRecord(Stream data, bool root) { var obj = new DirectoryRecord(); obj.DirectoryRecordLength = data.ReadByteValue(); obj.ExtendedAttributeRecordLength = data.ReadByteValue(); obj.ExtentLocation = data.ReadInt32BothEndian(); obj.ExtentLength = data.ReadUInt32BothEndian(); obj.RecordingDateTime = ParseDirectoryRecordDateTime(data); obj.FileFlags = (FileFlags)data.ReadByteValue(); obj.FileUnitSize = data.ReadByteValue(); obj.InterleaveGapSize = data.ReadByteValue(); obj.VolumeSequenceNumber = data.ReadInt16BothEndian(); obj.FileIdentifierLength = data.ReadByteValue(); // Root directory within the volume descriptor has a single byte file identifier if (root) obj.FileIdentifier = data.ReadBytes(1); else if (obj.FileIdentifierLength > 0) obj.FileIdentifier = data.ReadBytes(obj.FileIdentifierLength); // If file identifier length is even, there is a padding field byte if (obj.FileIdentifierLength % 2 == 0) obj.PaddingField = data.ReadByteValue(); // Root directory within the volume descriptor has no system use bytes, fixed at 34bytes if (root) return obj; // Calculate actual size of record int totalBytes = 33 + obj.FileIdentifierLength; // Calculate the size of the system use section (remaining allocated bytes) int systemUseLength = obj.DirectoryRecordLength - 33 - obj.FileIdentifierLength; // Account for padding field after file identifier if (obj.FileIdentifierLength % 2 == 0) { totalBytes += 1; systemUseLength -= 1; } // If System Use is empty, or if DirectoryRecordLength is bad, return early if (systemUseLength < 1) { // Total record size must be even, read a padding byte if (totalBytes % 2 != 0) obj.SystemUse = data.ReadBytes(1); return obj; } // Total used bytes must be even, read a padding byte totalBytes += systemUseLength; if (totalBytes % 2 != 0) systemUseLength += 1; // Read system use field obj.SystemUse = data.ReadBytes(systemUseLength); return obj; } /// /// Parse a Stream into a ExtendedAttributeRecord /// /// Stream to parse /// true if root directory record, false otherwise /// Filled ExtendedAttributeRecord on success, null on error public static ExtendedAttributeRecord ParseExtendedAttributeRecord(Stream data) { var obj = new ExtendedAttributeRecord(); obj.OwnerIdentification = data.ReadInt16BothEndian(); obj.GroupIdentification = data.ReadInt16BothEndian(); obj.Permissions = (Permissions)data.ReadUInt16LittleEndian(); obj.FileCreationDateTime = ParseDecDateTime(data); obj.FileModificationDateTime = ParseDecDateTime(data); obj.FileExpirationDateTime = ParseDecDateTime(data); obj.FileEffectiveDateTime = ParseDecDateTime(data); obj.RecordFormat = (RecordFormat)data.ReadByteValue(); obj.RecordAttributes = (RecordAttributes)data.ReadByteValue(); obj.RecordLength = data.ReadInt16BothEndian(); obj.SystemIdentifier = data.ReadBytes(32); obj.SystemUse = data.ReadBytes(64); obj.ExtendedAttributeRecordVersion = data.ReadByteValue(); obj.EscapeSequencesLength = data.ReadByteValue(); obj.Reserved64Bytes = data.ReadBytes(64); obj.ApplicationLength = data.ReadInt16BothEndian(); if (obj.ApplicationLength > 0) obj.ApplicationUse = data.ReadBytes(obj.ApplicationLength); if (obj.EscapeSequencesLength > 0) obj.EscapeSequences = data.ReadBytes(obj.EscapeSequencesLength); return obj; } /// /// Parse a Stream into a DirectoryRecordDateTime /// /// Stream to parse /// Filled DirectoryRecordDateTime on success, null on error public static DirectoryRecordDateTime ParseDirectoryRecordDateTime(Stream data) { var obj = new DirectoryRecordDateTime(); obj.YearsSince1990 = data.ReadByteValue(); obj.Month = data.ReadByteValue(); obj.Day = data.ReadByteValue(); obj.Hour = data.ReadByteValue(); obj.Minute = data.ReadByteValue(); obj.Second = data.ReadByteValue(); obj.TimezoneOffset = data.ReadByteValue(); return obj; } #endregion /// /// Parse a Stream into a DecDateTime /// /// Stream to parse /// Filled DecDateTime on success, null on error public static DecDateTime ParseDecDateTime(Stream data) { var obj = new DecDateTime(); obj.Year = data.ReadBytes(4); obj.Month = data.ReadBytes(2); obj.Day = data.ReadBytes(2); obj.Hour = data.ReadBytes(2); obj.Minute = data.ReadBytes(2); obj.Second = data.ReadBytes(2); obj.Centisecond = data.ReadBytes(2); obj.TimezoneOffset = data.ReadByteValue(); return obj; } } }