using System; using System.Collections.Generic; using System.IO; using SabreTools.Data.Extensions; using SabreTools.Data.Models.UDF; using SabreTools.IO.Extensions; using SabreTools.Matching; using SabreTools.Numerics.Extensions; using SabreTools.Text.Extensions; #pragma warning disable IDE0017 // Simplify object initialization namespace SabreTools.Serialization.Readers { public class UDF : BaseBinaryReader { /// /// Maximum number of Volume Descriptor Pointers followed in a Volume Descriptor Sequence /// private const int MaximumVolumeDescriptorPointers = 16; /// public override Volume? Deserialize(Stream? data) => Deserialize(data, includeDirectories: true); /// /// /// True to parse the whole directory hierarchy, false to parse only the volume and file set structures /// (use FindFileEntry to look up single paths afterwards) /// /// Logical sectors are addressed from the start of the stream public Volume? Deserialize(Stream? data, bool includeDirectories) { // If the data is invalid if (data is null || !data.CanRead || !data.CanSeek) return null; try { // Create a new Volume to fill var volume = new Volume(); // Read the Volume Recognition Sequence volume.VolumeRecognitionSequence = ParseVolumeRecognitionSequence(data); // Read the Anchor Volume Descriptor Pointer at sector 256, then at N - 256 and N (anchor points, ECMA-167 3/8.4.2.1) var anchor = ParseAnchorVolumeDescriptorPointer(data, Constants.AnchorVolumeDescriptorPointerSector); long lastSector = (data.Length / Constants.SectorSize) - 1; if (anchor is null && lastSector > Constants.AnchorVolumeDescriptorPointerSector) anchor = ParseAnchorVolumeDescriptorPointer(data, lastSector - Constants.AnchorVolumeDescriptorPointerSector); if (anchor is null && lastSector > Constants.AnchorVolumeDescriptorPointerSector) anchor = ParseAnchorVolumeDescriptorPointer(data, lastSector); if (anchor is null) return null; volume.AnchorVolumeDescriptorPointer = anchor; // Read the Main Volume Descriptor Sequence, then the equivalent Reserve one (ECMA-167 3/8.4.2.2) if the main one is unusable volume.VolumeDescriptorSequence = ParseVolumeDescriptorSequence(data, anchor.MainVolumeDescriptorSequenceExtent); if (!HasLogicalVolume(volume)) volume.VolumeDescriptorSequence = ParseVolumeDescriptorSequence(data, anchor.ReserveVolumeDescriptorSequenceExtent); if (!HasLogicalVolume(volume)) return null; // Read the File Entries of Metadata Files, needed to map Metadata Partition blocks var lvd = volume.GetLogicalVolumeDescriptor()!; foreach (var map in lvd.PartitionMaps) { if (map is not MetadataPartitionMap metadata) continue; int physical = lvd.GetType1PartitionReference(metadata.PartitionNumber); if (physical < 0) continue; var state = new HierarchyState(data, volume, includeDirectories: false); state.QueueEntry(UDFExtensions.GetKey((ushort)physical, metadata.MetadataFileLocation)); state.QueueEntry(UDFExtensions.GetKey((ushort)physical, metadata.MetadataMirrorFileLocation)); state.Run(); } // Read the File Set Descriptor var fsd = ParseFileSetDescriptor(data, volume, lvd.LogicalVolumeContentsUse.ExtentLocation); if (fsd is null) return null; volume.FileSetDescriptor = fsd; // Read the directory hierarchy from the root directory if (includeDirectories) { var state = new HierarchyState(data, volume, includeDirectories: true); state.QueueEntry(fsd.RootDirectoryICB.ExtentLocation.GetKey()); state.Run(); } return volume; } catch { // Ignore the actual error return null; } } #region Lookup /// /// Find the File Entry of a path by walking the directories on the path only /// /// /// Names are compared exactly first, then case-insensitively; deleted entries are ignored; /// File Entries and directories read on the way are added to the volume (reused by later lookups) /// /// Stream containing the volume /// Volume parsed by Deserialize (directories may be excluded) /// Path from the root directory, separated by '/' or '\' /// Logical block address key of the File Entry on success, -1 otherwise /// File Entry on success, null if the path does not exist or could not be read public static FileEntry? FindFileEntry(Stream data, Volume volume, string path, out long key) { key = -1; if (volume.FileSetDescriptor is null) return null; long current = volume.FileSetDescriptor.RootDirectoryICB.ExtentLocation.GetKey(); var entry = LoadEntry(data, volume, current); string[] names = path.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries); foreach (string name in names) { if (entry is null || entry.ICBTag.FileType != FileType.DIRECTORY) return null; if (!volume.Directories.TryGetValue(current, out var fids)) return null; var fid = FindInDirectory(fids, name); if (fid is null) return null; current = fid.ICB.ExtentLocation.GetKey(); entry = LoadEntry(data, volume, current); } if (entry is null) return null; key = current; return entry; } /// /// Read a File Entry (with its Allocation Extent Descriptors, and its directory if it is one) unless already read /// private static FileEntry? LoadEntry(Stream data, Volume volume, long key) { if (!volume.FileEntries.TryGetValue(key, out var entry) || (entry.ICBTag.FileType == FileType.DIRECTORY && !volume.Directories.ContainsKey(key))) { var state = new HierarchyState(data, volume, includeDirectories: false); state.QueueEntry(key); state.Run(); volume.FileEntries.TryGetValue(key, out entry); } return entry; } /// /// Find a name in the File Identifier Descriptors of a directory, exact match first, then case-insensitive /// private static FileIdentifierDescriptor? FindInDirectory(FileIdentifierDescriptor[] fids, string name) { FileIdentifierDescriptor? caseInsensitive = null; foreach (var fid in fids) { if ((fid.FileCharacteristics & (FileCharacteristics.DELETED | FileCharacteristics.PARENT)) != 0) continue; string? identifier = fid.FileIdentifier.DecodeDCharacters(); if (identifier is null) continue; if (string.Equals(identifier, name, StringComparison.Ordinal)) return fid; if (caseInsensitive is null && string.Equals(identifier, name, StringComparison.OrdinalIgnoreCase)) caseInsensitive = fid; } return caseInsensitive; } #endregion #region Volume Structure Parsing /// /// Parse the Volume Recognition Sequence starting at sector 16 /// /// Stream to parse /// Volume Structure Descriptors up to the first unknown one public static VolumeStructureDescriptor[] ParseVolumeRecognitionSequence(Stream data) { List obj = []; long sector = Constants.VolumeRecognitionSequenceSector; while (true) { byte[] block = ReadSectors(data, sector, 1); int offset = 0; var vsd = ParseVolumeStructureDescriptor(block, ref offset); if (!IsKnownStandardIdentifier(vsd.StandardIdentifier)) break; obj.Add(vsd); sector++; // The extended area ends with the Terminating Extended Area Descriptor if (vsd.StandardIdentifier.EqualsExactly(Constants.TerminatingExtendedAreaDescriptor)) break; } return [.. obj]; } /// /// Parse a Volume Structure Descriptor /// public static VolumeStructureDescriptor ParseVolumeStructureDescriptor(byte[] data, ref int offset) { var obj = new VolumeStructureDescriptor(); obj.StructureType = data.ReadByteValue(ref offset); obj.StandardIdentifier = data.ReadBytes(ref offset, 5); obj.StructureVersion = data.ReadByteValue(ref offset); obj.StructureData = data.ReadBytes(ref offset, Constants.SectorSize - 7); return obj; } /// /// Parse an Anchor Volume Descriptor Pointer /// /// Stream to parse /// Logical sector of the anchor /// Filled AnchorVolumeDescriptorPointer on success, null on error public static AnchorVolumeDescriptorPointer? ParseAnchorVolumeDescriptorPointer(Stream data, long sector) { byte[] block = ReadSectors(data, sector, 1); if (!block.IsValidDescriptor(0, block.Length, (uint)sector)) return null; int offset = 0; var tag = ParseDescriptorTag(block, ref offset); if (tag.TagIdentifier != TagIdentifier.ANCHOR_VOLUME_DESCRIPTOR_POINTER) return null; var obj = new AnchorVolumeDescriptorPointer(); obj.DescriptorTag = tag; obj.MainVolumeDescriptorSequenceExtent = ParseExtentDescriptor(block, ref offset); obj.ReserveVolumeDescriptorSequenceExtent = ParseExtentDescriptor(block, ref offset); obj.Reserved = block.ReadBytes(ref offset, 480); return obj; } /// /// Parse a Volume Descriptor Sequence, following Volume Descriptor Pointers /// /// Stream to parse /// Extent of the sequence /// Descriptors up to the Terminating Descriptor or the first invalid descriptor public static Descriptor[] ParseVolumeDescriptorSequence(Stream data, ExtentDescriptor extent) { List obj = []; long sector = extent.ExtentLocation; long end = sector + (extent.ExtentLength / Constants.SectorSize); int pointers = 0; while (sector < end) { byte[] block = ReadSectors(data, sector, 1); if (!block.IsValidDescriptor(0, block.Length, (uint)sector)) break; var descriptor = ParseVolumeDescriptor(block); obj.Add(descriptor); TagIdentifier tagIdentifier = descriptor.DescriptorTag.TagIdentifier; if (tagIdentifier == TagIdentifier.TERMINATING_DESCRIPTOR) break; // Continue the sequence in the extent of a Volume Descriptor Pointer if (descriptor is VolumeDescriptorPointer pointer) { if (++pointers > MaximumVolumeDescriptorPointers) break; sector = pointer.NextVolumeDescriptorSequenceExtent.ExtentLocation; end = sector + (pointer.NextVolumeDescriptorSequenceExtent.ExtentLength / Constants.SectorSize); continue; } sector++; } return [.. obj]; } /// /// Parse a validated descriptor of the Volume Descriptor Sequence /// /// Logical sector holding the descriptor /// Filled Descriptor public static Descriptor ParseVolumeDescriptor(byte[] block) { int offset = 0; var tag = ParseDescriptorTag(block, ref offset); return tag.TagIdentifier switch { TagIdentifier.PRIMARY_VOLUME_DESCRIPTOR => ParsePrimaryVolumeDescriptor(block, ref offset, tag), TagIdentifier.VOLUME_DESCRIPTOR_POINTER => ParseVolumeDescriptorPointer(block, ref offset, tag), TagIdentifier.PARTITION_DESCRIPTOR => ParsePartitionDescriptor(block, ref offset, tag), TagIdentifier.LOGICAL_VOLUME_DESCRIPTOR => ParseLogicalVolumeDescriptor(block, ref offset, tag), // Other descriptors are kept as raw data TagIdentifier.ANCHOR_VOLUME_DESCRIPTOR_POINTER or TagIdentifier.IMPLEMENTATION_USE_VOLUME_DESCRIPTOR or TagIdentifier.UNALLOCATED_SPACE_DESCRIPTOR or TagIdentifier.TERMINATING_DESCRIPTOR or TagIdentifier.LOGICAL_VOLUME_INTEGRITY_DESCRIPTOR or TagIdentifier.FILE_SET_DESCRIPTOR or TagIdentifier.FILE_IDENTIFIER_DESCRIPTOR or TagIdentifier.ALLOCATION_EXTENT_DESCRIPTOR or TagIdentifier.INDIRECT_ENTRY or TagIdentifier.TERMINAL_ENTRY or TagIdentifier.FILE_ENTRY or TagIdentifier.EXTENDED_ATTRIBUTE_HEADER_DESCRIPTOR or TagIdentifier.UNALLOCATED_SPACE_ENTRY or TagIdentifier.SPACE_BITMAP_DESCRIPTOR or TagIdentifier.PARTITION_INTEGRITY_ENTRY or TagIdentifier.EXTENDED_FILE_ENTRY => ParseGenericDescriptor(block, ref offset, tag), _ => ParseGenericDescriptor(block, ref offset, tag), }; } /// /// Parse a descriptor that is not parsed further after its tag /// private static GenericDescriptor ParseGenericDescriptor(byte[] data, ref int offset, DescriptorTag tag) { var obj = new GenericDescriptor(); obj.DescriptorTag = tag; obj.Data = data.ReadBytes(ref offset, data.Length - offset); return obj; } /// /// Parse a Primary Volume Descriptor after its tag /// private static PrimaryVolumeDescriptor ParsePrimaryVolumeDescriptor(byte[] data, ref int offset, DescriptorTag tag) { var obj = new PrimaryVolumeDescriptor(); obj.DescriptorTag = tag; obj.VolumeDescriptorSequenceNumber = data.ReadUInt32LittleEndian(ref offset); obj.PrimaryVolumeDescriptorNumber = data.ReadUInt32LittleEndian(ref offset); obj.VolumeIdentifier = data.ReadBytes(ref offset, 32); obj.VolumeSequenceNumber = data.ReadUInt16LittleEndian(ref offset); obj.MaximumVolumeSequenceNumber = data.ReadUInt16LittleEndian(ref offset); obj.InterchangeLevel = data.ReadUInt16LittleEndian(ref offset); obj.MaximumInterchangeLevel = data.ReadUInt16LittleEndian(ref offset); obj.CharacterSetList = data.ReadUInt32LittleEndian(ref offset); obj.MaximumCharacterSetList = data.ReadUInt32LittleEndian(ref offset); obj.VolumeSetIdentifier = data.ReadBytes(ref offset, 128); obj.DescriptorCharacterSet = ParseCharacterSetSpecification(data, ref offset); obj.ExplanatoryCharacterSet = ParseCharacterSetSpecification(data, ref offset); obj.VolumeAbstract = ParseExtentDescriptor(data, ref offset); obj.VolumeCopyrightNotice = ParseExtentDescriptor(data, ref offset); obj.ApplicationIdentifier = ParseEntityIdentifier(data, ref offset); obj.RecordingDateAndTime = ParseTimestamp(data, ref offset); obj.ImplementationIdentifier = ParseEntityIdentifier(data, ref offset); obj.ImplementationUse = data.ReadBytes(ref offset, 64); obj.PredecessorVolumeDescriptorSequenceLocation = data.ReadUInt32LittleEndian(ref offset); obj.Flags = data.ReadUInt16LittleEndian(ref offset); obj.Reserved = data.ReadBytes(ref offset, 22); return obj; } /// /// Parse a Volume Descriptor Pointer after its tag /// private static VolumeDescriptorPointer ParseVolumeDescriptorPointer(byte[] data, ref int offset, DescriptorTag tag) { var obj = new VolumeDescriptorPointer(); obj.DescriptorTag = tag; obj.VolumeDescriptorSequenceNumber = data.ReadUInt32LittleEndian(ref offset); obj.NextVolumeDescriptorSequenceExtent = ParseExtentDescriptor(data, ref offset); obj.Reserved = data.ReadBytes(ref offset, 484); return obj; } /// /// Parse a Partition Descriptor after its tag /// private static PartitionDescriptor ParsePartitionDescriptor(byte[] data, ref int offset, DescriptorTag tag) { var obj = new PartitionDescriptor(); obj.DescriptorTag = tag; obj.VolumeDescriptorSequenceNumber = data.ReadUInt32LittleEndian(ref offset); obj.PartitionFlags = data.ReadUInt16LittleEndian(ref offset); obj.PartitionNumber = data.ReadUInt16LittleEndian(ref offset); obj.PartitionContents = ParseEntityIdentifier(data, ref offset); obj.PartitionContentsUse = data.ReadBytes(ref offset, 128); obj.AccessType = data.ReadUInt32LittleEndian(ref offset); obj.PartitionStartingLocation = data.ReadUInt32LittleEndian(ref offset); obj.PartitionLength = data.ReadUInt32LittleEndian(ref offset); obj.ImplementationIdentifier = ParseEntityIdentifier(data, ref offset); obj.ImplementationUse = data.ReadBytes(ref offset, 128); obj.Reserved = data.ReadBytes(ref offset, 156); return obj; } /// /// Parse a Logical Volume Descriptor after its tag /// private static LogicalVolumeDescriptor ParseLogicalVolumeDescriptor(byte[] data, ref int offset, DescriptorTag tag) { var obj = new LogicalVolumeDescriptor(); obj.DescriptorTag = tag; obj.VolumeDescriptorSequenceNumber = data.ReadUInt32LittleEndian(ref offset); obj.DescriptorCharacterSet = ParseCharacterSetSpecification(data, ref offset); obj.LogicalVolumeIdentifier = data.ReadBytes(ref offset, 128); obj.LogicalBlockSize = data.ReadUInt32LittleEndian(ref offset); obj.DomainIdentifier = ParseEntityIdentifier(data, ref offset); obj.LogicalVolumeContentsUse = ParseLongAllocationDescriptor(data, ref offset); obj.MapTableLength = data.ReadUInt32LittleEndian(ref offset); obj.NumberOfPartitionMaps = data.ReadUInt32LittleEndian(ref offset); obj.ImplementationIdentifier = ParseEntityIdentifier(data, ref offset); obj.ImplementationUse = data.ReadBytes(ref offset, 128); obj.IntegritySequenceExtent = ParseExtentDescriptor(data, ref offset); // Partition maps, bounded by the map table length and the logical sector List maps = []; int end = Math.Min(data.Length, offset + (int)Math.Min(obj.MapTableLength, (uint)data.Length)); for (uint i = 0; i < obj.NumberOfPartitionMaps && offset + 2 <= end; i++) { int start = offset; byte length = data[offset + 1]; if (length < 2) break; maps.Add(ParsePartitionMap(data, ref offset)); offset = start + length; } obj.PartitionMaps = [.. maps]; return obj; } /// /// Parse a Partition Map /// private static PartitionMap ParsePartitionMap(byte[] data, ref int offset) { int start = offset; byte type = data[offset]; byte length = data[offset + 1]; if (type == Constants.Type1PartitionMap && length >= 6) { var type1 = new Type1PartitionMap(); type1.PartitionMapType = data.ReadByteValue(ref offset); type1.PartitionMapLength = data.ReadByteValue(ref offset); type1.VolumeSequenceNumber = data.ReadUInt16LittleEndian(ref offset); type1.PartitionNumber = data.ReadUInt16LittleEndian(ref offset); return type1; } if (type == Constants.Type2PartitionMap && length >= 64) { offset += 4; var identifier = ParseEntityIdentifier(data, ref offset); offset = start; int identifierOffset = 0; string? name = identifier.Identifier.ReadNullTerminatedAnsiString(ref identifierOffset); Type2PartitionMap type2 = name == Constants.MetadataPartitionIdentifier ? new MetadataPartitionMap() : new Type2PartitionMap(); type2.PartitionMapType = data.ReadByteValue(ref offset); type2.PartitionMapLength = data.ReadByteValue(ref offset); type2.Reserved = data.ReadBytes(ref offset, 2); type2.PartitionTypeIdentifier = ParseEntityIdentifier(data, ref offset); type2.VolumeSequenceNumber = data.ReadUInt16LittleEndian(ref offset); type2.PartitionNumber = data.ReadUInt16LittleEndian(ref offset); if (type2 is MetadataPartitionMap metadata) { metadata.MetadataFileLocation = data.ReadUInt32LittleEndian(ref offset); metadata.MetadataMirrorFileLocation = data.ReadUInt32LittleEndian(ref offset); metadata.MetadataBitmapFileLocation = data.ReadUInt32LittleEndian(ref offset); metadata.AllocationUnitSize = data.ReadUInt32LittleEndian(ref offset); metadata.AlignmentUnitSize = data.ReadUInt16LittleEndian(ref offset); metadata.Flags = data.ReadByteValue(ref offset); metadata.Reserved2 = data.ReadBytes(ref offset, 5); } else { type2.PartitionTypeData = data.ReadBytes(ref offset, 24); } return type2; } // Unknown partition map type, keep the raw data var unknown = new Type2PartitionMap(); unknown.PartitionMapType = data.ReadByteValue(ref offset); unknown.PartitionMapLength = data.ReadByteValue(ref offset); unknown.PartitionTypeData = data.ReadBytes(ref offset, Math.Max(0, length - 2)); return unknown; } #endregion #region File Structure Parsing /// /// Parse the File Set Descriptor /// /// Stream to parse /// Volume containing the Volume Descriptor Sequence /// Logical block address of the File Set Descriptor /// Filled FileSetDescriptor on success, null on error public static FileSetDescriptor? ParseFileSetDescriptor(Stream data, Volume volume, LogicalBlockAddress location) { if (!volume.TryGetSector(location.GetKey(), out long sector)) return null; byte[] block = ReadSectors(data, sector, 1); if (!block.IsValidDescriptor(0, block.Length, location.LogicalBlockNumber)) return null; int offset = 0; var tag = ParseDescriptorTag(block, ref offset); if (tag.TagIdentifier != TagIdentifier.FILE_SET_DESCRIPTOR) return null; var obj = new FileSetDescriptor(); obj.DescriptorTag = tag; obj.RecordingDateAndTime = ParseTimestamp(block, ref offset); obj.InterchangeLevel = block.ReadUInt16LittleEndian(ref offset); obj.MaximumInterchangeLevel = block.ReadUInt16LittleEndian(ref offset); obj.CharacterSetList = block.ReadUInt32LittleEndian(ref offset); obj.MaximumCharacterSetList = block.ReadUInt32LittleEndian(ref offset); obj.FileSetNumber = block.ReadUInt32LittleEndian(ref offset); obj.FileSetDescriptorNumber = block.ReadUInt32LittleEndian(ref offset); obj.LogicalVolumeIdentifierCharacterSet = ParseCharacterSetSpecification(block, ref offset); obj.LogicalVolumeIdentifier = block.ReadBytes(ref offset, 128); obj.FileSetCharacterSet = ParseCharacterSetSpecification(block, ref offset); obj.FileSetIdentifier = block.ReadBytes(ref offset, 32); obj.CopyrightFileIdentifier = block.ReadBytes(ref offset, 32); obj.AbstractFileIdentifier = block.ReadBytes(ref offset, 32); obj.RootDirectoryICB = ParseLongAllocationDescriptor(block, ref offset); obj.DomainIdentifier = ParseEntityIdentifier(block, ref offset); obj.NextExtent = ParseLongAllocationDescriptor(block, ref offset); obj.SystemStreamDirectoryICB = ParseLongAllocationDescriptor(block, ref offset); obj.Reserved = block.ReadBytes(ref offset, 32); return obj; } /// /// Parse a File Entry or Extended File Entry /// /// Logical block holding the entry /// Logical block number the entry was read from /// Filled FileEntry on success, null on error public static FileEntry? ParseFileEntry(byte[] block, uint logicalBlockNumber) { if (!block.IsValidDescriptor(0, block.Length, logicalBlockNumber)) return null; int offset = 0; var tag = ParseDescriptorTag(block, ref offset); bool extended = tag.TagIdentifier == TagIdentifier.EXTENDED_FILE_ENTRY; if (!extended && tag.TagIdentifier != TagIdentifier.FILE_ENTRY) return null; var obj = extended ? new ExtendedFileEntry() : new FileEntry(); obj.DescriptorTag = tag; obj.ICBTag = ParseICBTag(block, ref offset); obj.Uid = block.ReadUInt32LittleEndian(ref offset); obj.Gid = block.ReadUInt32LittleEndian(ref offset); obj.Permissions = block.ReadUInt32LittleEndian(ref offset); obj.FileLinkCount = block.ReadUInt16LittleEndian(ref offset); obj.RecordFormat = block.ReadByteValue(ref offset); obj.RecordDisplayAttributes = block.ReadByteValue(ref offset); obj.RecordLength = block.ReadUInt32LittleEndian(ref offset); obj.InformationLength = block.ReadUInt64LittleEndian(ref offset); if (obj is ExtendedFileEntry efe) { efe.ObjectSize = block.ReadUInt64LittleEndian(ref offset); efe.LogicalBlocksRecorded = block.ReadUInt64LittleEndian(ref offset); efe.AccessDateAndTime = ParseTimestamp(block, ref offset); efe.ModificationDateAndTime = ParseTimestamp(block, ref offset); efe.CreationDateAndTime = ParseTimestamp(block, ref offset); efe.AttributeDateAndTime = ParseTimestamp(block, ref offset); efe.Checkpoint = block.ReadUInt32LittleEndian(ref offset); efe.Reserved = block.ReadUInt32LittleEndian(ref offset); efe.ExtendedAttributeICB = ParseLongAllocationDescriptor(block, ref offset); efe.StreamDirectoryICB = ParseLongAllocationDescriptor(block, ref offset); } else { obj.LogicalBlocksRecorded = block.ReadUInt64LittleEndian(ref offset); obj.AccessDateAndTime = ParseTimestamp(block, ref offset); obj.ModificationDateAndTime = ParseTimestamp(block, ref offset); obj.AttributeDateAndTime = ParseTimestamp(block, ref offset); obj.Checkpoint = block.ReadUInt32LittleEndian(ref offset); obj.ExtendedAttributeICB = ParseLongAllocationDescriptor(block, ref offset); } obj.ImplementationIdentifier = ParseEntityIdentifier(block, ref offset); obj.UniqueID = block.ReadUInt64LittleEndian(ref offset); obj.LengthOfExtendedAttributes = block.ReadUInt32LittleEndian(ref offset); obj.LengthOfAllocationDescriptors = block.ReadUInt32LittleEndian(ref offset); // Both fields must fit into the logical block if (obj.LengthOfExtendedAttributes > block.Length - offset || obj.LengthOfAllocationDescriptors > block.Length - offset - obj.LengthOfExtendedAttributes) { return null; } obj.ExtendedAttributes = block.ReadBytes(ref offset, (int)obj.LengthOfExtendedAttributes); obj.AllocationDescriptors = block.ReadBytes(ref offset, (int)obj.LengthOfAllocationDescriptors); return obj; } /// /// Parse an Allocation Extent Descriptor /// /// Logical block holding the descriptor /// Logical block number the descriptor was read from /// Filled AllocationExtentDescriptor on success, null on error public static AllocationExtentDescriptor? ParseAllocationExtentDescriptor(byte[] block, uint logicalBlockNumber) { if (!block.IsValidDescriptor(0, block.Length, logicalBlockNumber)) return null; int offset = 0; var tag = ParseDescriptorTag(block, ref offset); if (tag.TagIdentifier != TagIdentifier.ALLOCATION_EXTENT_DESCRIPTOR) return null; var obj = new AllocationExtentDescriptor(); obj.DescriptorTag = tag; obj.PreviousAllocationExtentLocation = block.ReadUInt32LittleEndian(ref offset); obj.LengthOfAllocationDescriptors = block.ReadUInt32LittleEndian(ref offset); obj.AllocationDescriptors = block.ReadBytes(ref offset, (int)obj.LengthOfAllocationDescriptors); return obj; } /// /// Parse the File Identifier Descriptors of a directory /// /// Directory contents /// Length of the directory contents in bytes /// File Identifier Descriptors on success, null if the directory is invalid public static FileIdentifierDescriptor[]? ParseDirectory(byte[] data, int length) { List obj = []; int offset = 0; while (offset < length) { var tag = ParseDescriptorTag(data, ref offset); if (tag.TagIdentifier != TagIdentifier.FILE_IDENTIFIER_DESCRIPTOR) return null; var fid = ParseFileIdentifierDescriptor(data, ref offset, tag); obj.Add(fid); } return [.. obj]; } /// /// Parse a File Identifier Descriptor /// private static FileIdentifierDescriptor ParseFileIdentifierDescriptor(byte[] data, ref int offset, DescriptorTag tag) { var obj = new FileIdentifierDescriptor(); obj.DescriptorTag = tag; obj.FileVersionNumber = data.ReadUInt16LittleEndian(ref offset); obj.FileCharacteristics = (FileCharacteristics)data.ReadByteValue(ref offset); obj.LengthOfFileIdentifier = data.ReadByteValue(ref offset); obj.ICB = ParseLongAllocationDescriptor(data, ref offset); obj.LengthOfImplementationUse = data.ReadUInt16LittleEndian(ref offset); // The descriptor is padded to 4 bytes int used = 38 + obj.LengthOfImplementationUse + obj.LengthOfFileIdentifier; int padded = (used + 3) & ~3; obj.ImplementationUse = data.ReadBytes(ref offset, obj.LengthOfImplementationUse); obj.FileIdentifier = data.ReadBytes(ref offset, obj.LengthOfFileIdentifier); obj.Padding = data.ReadBytes(ref offset, padded - used); return obj; } #endregion #region Common Structures /// /// Parse a Descriptor Tag /// public static DescriptorTag ParseDescriptorTag(byte[] data, ref int offset) { var obj = new DescriptorTag(); obj.TagIdentifier = (TagIdentifier)data.ReadUInt16LittleEndian(ref offset); obj.DescriptorVersion = data.ReadUInt16LittleEndian(ref offset); obj.TagChecksum = data.ReadByteValue(ref offset); obj.Reserved = data.ReadByteValue(ref offset); obj.TagSerialNumber = data.ReadUInt16LittleEndian(ref offset); obj.DescriptorCRC = data.ReadUInt16LittleEndian(ref offset); obj.DescriptorCRCLength = data.ReadUInt16LittleEndian(ref offset); obj.TagLocation = data.ReadUInt32LittleEndian(ref offset); return obj; } /// /// Parse an Extent Descriptor (extent_ad) /// public static ExtentDescriptor ParseExtentDescriptor(byte[] data, ref int offset) { var obj = new ExtentDescriptor(); obj.ExtentLength = data.ReadUInt32LittleEndian(ref offset); obj.ExtentLocation = data.ReadUInt32LittleEndian(ref offset); return obj; } /// /// Parse a Long Allocation Descriptor (long_ad) /// public static LongAllocationDescriptor ParseLongAllocationDescriptor(byte[] data, ref int offset) { var obj = new LongAllocationDescriptor(); obj.ExtentLength = data.ReadUInt32LittleEndian(ref offset); obj.ExtentLocation.LogicalBlockNumber = data.ReadUInt32LittleEndian(ref offset); obj.ExtentLocation.PartitionReferenceNumber = data.ReadUInt16LittleEndian(ref offset); obj.ImplementationUse = data.ReadBytes(ref offset, 6); return obj; } /// /// Parse an Entity Identifier (regid) /// public static EntityIdentifier ParseEntityIdentifier(byte[] data, ref int offset) { var obj = new EntityIdentifier(); obj.Flags = data.ReadByteValue(ref offset); obj.Identifier = data.ReadBytes(ref offset, 23); obj.IdentifierSuffix = data.ReadBytes(ref offset, 8); return obj; } /// /// Parse a Character Set Specification (charspec) /// public static CharacterSetSpecification ParseCharacterSetSpecification(byte[] data, ref int offset) { var obj = new CharacterSetSpecification(); obj.CharacterSetType = data.ReadByteValue(ref offset); obj.CharacterSetInformation = data.ReadBytes(ref offset, 63); return obj; } /// /// Parse a Timestamp /// public static Timestamp ParseTimestamp(byte[] data, ref int offset) { var obj = new Timestamp(); obj.TypeAndTimezone = data.ReadUInt16LittleEndian(ref offset); obj.Year = data.ReadInt16LittleEndian(ref offset); obj.Month = data.ReadByteValue(ref offset); obj.Day = data.ReadByteValue(ref offset); obj.Hour = data.ReadByteValue(ref offset); obj.Minute = data.ReadByteValue(ref offset); obj.Second = data.ReadByteValue(ref offset); obj.Centiseconds = data.ReadByteValue(ref offset); obj.HundredsOfMicroseconds = data.ReadByteValue(ref offset); obj.Microseconds = data.ReadByteValue(ref offset); return obj; } /// /// Parse an ICB Tag (icbtag) /// public static ICBTag ParseICBTag(byte[] data, ref int offset) { var obj = new ICBTag(); obj.PriorRecordedNumberOfDirectEntries = data.ReadUInt32LittleEndian(ref offset); obj.StrategyType = data.ReadUInt16LittleEndian(ref offset); obj.StrategyParameter = data.ReadBytes(ref offset, 2); obj.MaximumNumberOfEntries = data.ReadUInt16LittleEndian(ref offset); obj.Reserved = data.ReadByteValue(ref offset); obj.FileType = (FileType)data.ReadByteValue(ref offset); obj.ParentICBLocation.LogicalBlockNumber = data.ReadUInt32LittleEndian(ref offset); obj.ParentICBLocation.PartitionReferenceNumber = data.ReadUInt16LittleEndian(ref offset); obj.Flags = data.ReadUInt16LittleEndian(ref offset); return obj; } #endregion #region Helpers /// /// Check whether the volume has a Logical Volume Descriptor with a supported logical block size /// private static bool HasLogicalVolume(Volume volume) { var lvd = volume.GetLogicalVolumeDescriptor(); return lvd is not null && lvd.LogicalBlockSize == Constants.SectorSize; } /// /// Check whether a standard identifier belongs to the Volume Recognition Sequence /// private static bool IsKnownStandardIdentifier(byte[] identifier) { return identifier.EqualsExactly(Constants.BeginningExtendedAreaDescriptor) || identifier.EqualsExactly(Constants.NSR02) || identifier.EqualsExactly(Constants.NSR03) || identifier.EqualsExactly(Constants.TerminatingExtendedAreaDescriptor) || identifier.EqualsExactly(Constants.CD001) || identifier.EqualsExactly(Constants.BOOT2) || identifier.EqualsExactly(Constants.CDW02); } /// /// Read whole logical sectors /// private static byte[] ReadSectors(Stream data, long sector, int count) { data.SeekIfPossible(sector * Constants.SectorSize, SeekOrigin.Begin); return data.ReadBytes(count * Constants.SectorSize); } #endregion #region Directory Hierarchy /// /// Kind of a pending read /// private enum ReadKind { /// /// File Entry or Extended File Entry /// Entry, /// /// Allocation Extent Descriptor of a File Entry /// AllocationExtent, /// /// Part of the contents of a directory /// DirectoryData, } /// /// Contents of a directory being assembled from its extents /// private sealed class DirectoryContents { public long Key; public byte[] Data = []; public int Pending; } /// /// A read waiting for the elevator /// private sealed class PendingRead { public long Sector; public int Count; public ReadKind Kind; public long Key; public DirectoryContents? Directory; public int DirectoryOffset; public int Length; } /// /// Orders pending reads by sector /// private sealed class PendingReadComparer : IComparer { public static readonly PendingReadComparer Instance = new(); public int Compare(PendingRead? x, PendingRead? y) => (x?.Sector ?? 0).CompareTo(y?.Sector ?? 0); } /// /// Reads File Entries, Allocation Extent Descriptors and directories in ascending sector order (C-SCAN), /// merging adjacent sectors into one request; structures found in the last request are served from it /// private sealed class HierarchyState { private readonly Stream _data; private readonly Volume _volume; private readonly bool _includeDirectories; /// /// Pending reads ordered by sector /// private readonly List _pending = []; /// /// Reads served from the last request, processed before the next request /// private readonly Queue> _ready = new(); /// /// Keys of Allocation Extent Descriptors already queued, with the File Entries waiting for them /// private readonly Dictionary> _queuedExtents = []; /// /// Sector after the last request /// private long _head = 0; /// /// Data and first sector of the last request /// private byte[] _lastRun = []; private long _lastRunSector = -1; /// Stream containing the volume /// Volume to fill /// True to read the entries of every directory read public HierarchyState(Stream data, Volume volume, bool includeDirectories) { _data = data; _volume = volume; _includeDirectories = includeDirectories; } /// /// Queue a File Entry, or resolve it if it was already read /// public void QueueEntry(long key) { if (_volume.FileEntries.TryGetValue(key, out var entry)) { ResolveEntry(key, entry); return; } if (_volume.TryGetSector(key, out long sector)) Queue(new PendingRead { Sector = sector, Count = 1, Kind = ReadKind.Entry, Key = key }); } /// /// Process pending reads until none are left /// public void Run() { while (true) { while (_ready.Count > 0) { var ready = _ready.Dequeue(); Process(ready.Key, ready.Value); } if (_pending.Count == 0) return; // Next pending read at or after the head, wrapping around to the lowest sector int first = FirstAtOrAfter(_head); if (first >= _pending.Count) first = 0; // Merge adjacent or overlapping reads into one request long runStart = _pending[first].Sector; long runEnd = runStart + _pending[first].Count; int last = first; while (last + 1 < _pending.Count && _pending[last + 1].Sector <= runEnd) { last++; runEnd = Math.Max(runEnd, _pending[last].Sector + _pending[last].Count); } var run = _pending.GetRange(first, last - first + 1); _pending.RemoveRange(first, last - first + 1); _head = runEnd; byte[]? buffer = null; try { buffer = ReadSectors(_data, runStart, (int)(runEnd - runStart)); } catch (IOException) { // Unreadable sectors: retry each read alone below } if (buffer is not null) { _lastRun = buffer; _lastRunSector = runStart; } foreach (var read in run) { byte[]? sectors = buffer is not null ? Slice(buffer, (int)(read.Sector - runStart) * Constants.SectorSize, read.Count * Constants.SectorSize) : TryReadSectors(read.Sector, read.Count); if (sectors is not null) Process(read, sectors); } } } /// /// Add a read to the pending reads, or to the ready reads if the last request contains it /// private void Queue(PendingRead read) { long runEnd = _lastRunSector + (_lastRun.Length / Constants.SectorSize); if (_lastRunSector >= 0 && read.Sector >= _lastRunSector && read.Sector + read.Count <= runEnd) { int offset = (int)(read.Sector - _lastRunSector) * Constants.SectorSize; _ready.Enqueue(new KeyValuePair(read, Slice(_lastRun, offset, read.Count * Constants.SectorSize))); return; } int index = _pending.BinarySearch(read, PendingReadComparer.Instance); if (index < 0) index = ~index; // Keep reads of the same sector in queue order while (index < _pending.Count && _pending[index].Sector == read.Sector) { index++; } _pending.Insert(index, read); } /// /// Index of the first pending read at or after a sector /// private int FirstAtOrAfter(long sector) { int low = 0, high = _pending.Count; while (low < high) { int middle = low + ((high - low) / 2); if (_pending[middle].Sector < sector) low = middle + 1; else high = middle; } return low; } /// /// Process the data of a read /// private void Process(PendingRead read, byte[] sectors) { switch (read.Kind) { case ReadKind.Entry: var entry = ParseFileEntry(sectors, (uint)read.Key); if (entry is null) return; _volume.FileEntries[read.Key] = entry; ResolveEntry(read.Key, entry); break; case ReadKind.AllocationExtent: var aed = ParseAllocationExtentDescriptor(sectors, (uint)read.Key); if (aed is null) return; _volume.AllocationExtentDescriptors[read.Key] = aed; if (_queuedExtents.TryGetValue(read.Key, out var owners)) { foreach (long owner in owners) { if (_volume.FileEntries.TryGetValue(owner, out var ownerEntry)) ResolveEntry(owner, ownerEntry); } } break; case ReadKind.DirectoryData: var directory = read.Directory!; Array.Copy(sectors, 0, directory.Data, read.DirectoryOffset, Math.Min(read.Length, sectors.Length)); if (--directory.Pending == 0) CompleteDirectory(directory.Key, directory.Data); break; default: // All read kinds are handled above break; } } /// /// Queue what a File Entry still needs: its Allocation Extent Descriptors, and its contents if it is a directory /// private void ResolveEntry(long key, FileEntry entry) { var descriptors = _volume.GetAllocationDescriptors(key, entry, out long missingKey); if (descriptors is null) { if (missingKey >= 0) QueueAllocationExtent(missingKey, key); return; } if (entry.ICBTag.FileType != FileType.DIRECTORY || _volume.Directories.ContainsKey(key)) return; int length = (int)entry.InformationLength; if (entry.ICBTag.GetAllocationDescriptorType() == AllocationDescriptorType.EMBEDDED) { if (length <= entry.AllocationDescriptors.Length) CompleteDirectory(key, entry.AllocationDescriptors.Length == length ? entry.AllocationDescriptors : Slice(entry.AllocationDescriptors, 0, length)); return; } // Queue the recorded extents; extents that are not recorded read as zeros var directory = new DirectoryContents { Key = key, Data = new byte[length] }; List reads = []; int offset = 0; foreach (var ad in descriptors) { if (offset >= length) break; int extentLength = (int)Math.Min(UDFExtensions.GetExtentLength(ad.ExtentLength), (uint)(length - offset)); if (UDFExtensions.GetExtentType(ad.ExtentLength) == ExtentType.RECORDED_AND_ALLOCATED) { if (!_volume.TryGetSector(ad.ExtentLocation.GetKey(), out long sector)) return; int count = (extentLength + Constants.SectorSize - 1) / Constants.SectorSize; reads.Add(new PendingRead { Sector = sector, Count = count, Kind = ReadKind.DirectoryData, Key = key, Directory = directory, DirectoryOffset = offset, Length = extentLength, }); } offset += extentLength; } directory.Pending = reads.Count; if (reads.Count == 0) { CompleteDirectory(key, directory.Data); return; } foreach (var read in reads) { Queue(read); } } /// /// Queue an Allocation Extent Descriptor needed by a File Entry /// private void QueueAllocationExtent(long key, long owner) { if (_queuedExtents.TryGetValue(key, out var owners)) { if (!owners.Contains(owner)) owners.Add(owner); return; } _queuedExtents[key] = [owner]; if (_volume.TryGetSector(key, out long sector)) Queue(new PendingRead { Sector = sector, Count = 1, Kind = ReadKind.AllocationExtent, Key = key }); } /// /// Parse the contents of a directory and queue the File Entries it identifies /// private void CompleteDirectory(long key, byte[] contents) { var fids = ParseDirectory(contents, contents.Length); if (fids is null) return; _volume.Directories[key] = fids; if (!_includeDirectories) return; foreach (var fid in fids) { if ((fid.FileCharacteristics & (FileCharacteristics.DELETED | FileCharacteristics.PARENT)) != 0) continue; QueueEntry(fid.ICB.ExtentLocation.GetKey()); } } /// /// Read sectors alone, null if they cannot be read /// private byte[]? TryReadSectors(long sector, int count) { try { return ReadSectors(_data, sector, count); } catch (IOException) { return null; } } /// /// Copy part of an array /// private static byte[] Slice(byte[] data, int offset, int length) { var slice = new byte[length]; Array.Copy(data, offset, slice, 0, length); return slice; } } #endregion } }