using System.Collections.Generic; using System.Text; using SabreTools.Data.Models.UDF; using SabreTools.Numerics.Extensions; namespace SabreTools.Data.Extensions { public static class UDFExtensions { /// /// Maximum number of Allocation Extent Descriptors followed for one File Entry /// private const int MaximumAllocationExtents = 1024; /// /// CRC-ITU-T (CCITT, polynomial 0x1021) table for descriptor CRCs /// private static readonly ushort[] CrcTable = CreateCrcTable(); /// /// Get the dictionary key of a logical block address /// /// Logical block address /// ((long)PartitionReferenceNumber << 32) | LogicalBlockNumber public static long GetKey(this LogicalBlockAddress address) => GetKey(address.PartitionReferenceNumber, address.LogicalBlockNumber); /// /// Get the dictionary key of a logical block address /// /// Index of the partition map /// Logical block number within the partition /// ((long)partitionReferenceNumber << 32) | logicalBlockNumber public static long GetKey(ushort partitionReferenceNumber, uint logicalBlockNumber) => ((long)partitionReferenceNumber << 32) | logicalBlockNumber; /// /// Get the extent type from an allocation descriptor's extent length /// public static ExtentType GetExtentType(uint extentLength) => (ExtentType)(extentLength >> Constants.ExtentTypeShift); /// /// Get the extent length in bytes from an allocation descriptor's extent length /// public static uint GetExtentLength(uint extentLength) => extentLength & Constants.ExtentLengthMask; /// /// Get the type of the allocation descriptors of an ICB /// public static AllocationDescriptorType GetAllocationDescriptorType(this ICBTag tag) => (AllocationDescriptorType)(tag.Flags & Constants.AllocationDescriptorTypeMask); /// /// Get the prevailing Logical Volume Descriptor (highest Volume Descriptor Sequence Number) /// /// Volume to search /// Logical Volume Descriptor on success, null if none was recorded public static LogicalVolumeDescriptor? GetLogicalVolumeDescriptor(this Volume volume) { LogicalVolumeDescriptor? prevailing = null; foreach (var descriptor in volume.VolumeDescriptorSequence) { if (descriptor is LogicalVolumeDescriptor lvd && (prevailing is null || lvd.VolumeDescriptorSequenceNumber > prevailing.VolumeDescriptorSequenceNumber)) { prevailing = lvd; } } return prevailing; } /// /// Get the prevailing Partition Descriptor of a partition (highest Volume Descriptor Sequence Number) /// /// Volume to search /// Partition number /// Partition Descriptor on success, null if none was recorded public static PartitionDescriptor? GetPartitionDescriptor(this Volume volume, ushort partitionNumber) { PartitionDescriptor? prevailing = null; foreach (var descriptor in volume.VolumeDescriptorSequence) { if (descriptor is PartitionDescriptor pd && pd.PartitionNumber == partitionNumber && (prevailing is null || pd.VolumeDescriptorSequenceNumber > prevailing.VolumeDescriptorSequenceNumber)) { prevailing = pd; } } return prevailing; } /// /// Get the partition reference number of the Type 1 partition map for a partition number /// /// Logical Volume Descriptor holding the partition maps /// Partition number /// Partition reference number on success, -1 if there is no such map public static int GetType1PartitionReference(this LogicalVolumeDescriptor lvd, ushort partitionNumber) { for (int i = 0; i < lvd.PartitionMaps.Length; i++) { if (lvd.PartitionMaps[i] is Type1PartitionMap map && map.PartitionNumber == partitionNumber) return i; } return -1; } /// /// Map a logical block of a partition to a logical sector of the volume /// Supports Type 1 partitions and Metadata Partitions (the Metadata File's File Entry must be in FileEntries) /// /// Volume containing the Volume Descriptor Sequence /// Logical block address key, see GetKey /// Logical sector on success /// True if the block could be mapped, false otherwise public static bool TryGetSector(this Volume volume, long key, out long sector) { sector = -1; var lvd = volume.GetLogicalVolumeDescriptor(); if (lvd is null) return false; int partitionReference = (int)(key >> 32); uint block = (uint)key; if (partitionReference < 0 || partitionReference >= lvd.PartitionMaps.Length) return false; var map = lvd.PartitionMaps[partitionReference]; if (map is Type1PartitionMap type1) { var pd = volume.GetPartitionDescriptor(type1.PartitionNumber); if (pd is null || block >= pd.PartitionLength) return false; sector = (long)pd.PartitionStartingLocation + block; return true; } else if (map is MetadataPartitionMap metadata) { // The Metadata File (or its mirror) is recorded in the Type 1 partition of the same partition number int physical = lvd.GetType1PartitionReference(metadata.PartitionNumber); if (physical < 0) return false; if (!TryGetMetadataFileBlock(volume, lvd, GetKey((ushort)physical, metadata.MetadataFileLocation), block, out long physicalKey) && !TryGetMetadataFileBlock(volume, lvd, GetKey((ushort)physical, metadata.MetadataMirrorFileLocation), block, out physicalKey)) { return false; } return volume.TryGetSector(physicalKey, out sector); } // Virtual and Sparable partitions are not supported return false; } /// /// Map a logical block of a Metadata Partition through the Metadata File's allocation descriptors /// private static bool TryGetMetadataFileBlock(Volume volume, LogicalVolumeDescriptor lvd, long fileKey, uint block, out long physicalKey) { physicalKey = -1; if (!volume.FileEntries.TryGetValue(fileKey, out var fileEntry)) return false; var descriptors = volume.GetAllocationDescriptors(fileKey, fileEntry); if (descriptors is null) return false; uint blockSize = lvd.LogicalBlockSize; long first = 0; foreach (var ad in descriptors) { long blocks = (GetExtentLength(ad.ExtentLength) + blockSize - 1) / blockSize; if (block < first + blocks) { if (GetExtentType(ad.ExtentLength) != ExtentType.RECORDED_AND_ALLOCATED) return false; physicalKey = GetKey(ad.ExtentLocation.PartitionReferenceNumber, (uint)(ad.ExtentLocation.LogicalBlockNumber + (block - first))); return true; } first += blocks; } return false; } /// /// Get the allocation descriptors of a File Entry, including those continued in Allocation Extent Descriptors, /// as long allocation descriptors (short allocation descriptors get the partition of the File Entry) /// /// Volume containing the Allocation Extent Descriptors /// Logical block address key of the File Entry /// File Entry /// Allocation descriptors (empty for embedded data), null if they are not supported or incomplete public static List? GetAllocationDescriptors(this Volume volume, long key, FileEntry fileEntry) => volume.GetAllocationDescriptors(key, fileEntry, out _); /// /// Get the allocation descriptors of a File Entry, including those continued in Allocation Extent Descriptors, /// as long allocation descriptors (short allocation descriptors get the partition of the File Entry) /// /// Volume containing the Allocation Extent Descriptors /// Logical block address key of the File Entry /// File Entry /// Key of the first Allocation Extent Descriptor that is not in the volume, -1 if none /// Allocation descriptors (empty for embedded data), null if they are not supported or incomplete public static List? GetAllocationDescriptors(this Volume volume, long key, FileEntry fileEntry, out long missingKey) { missingKey = -1; var type = fileEntry.ICBTag.GetAllocationDescriptorType(); if (type == AllocationDescriptorType.EMBEDDED) return []; // Extended allocation descriptors are not allowed by UDF if (type != AllocationDescriptorType.SHORT && type != AllocationDescriptorType.LONG) return null; List result = []; ushort partition = (ushort)(key >> 32); byte[] descriptors = fileEntry.AllocationDescriptors; int length = descriptors.Length; for (int extents = 0; extents <= MaximumAllocationExtents; extents++) { long nextKey = -1; int size = type == AllocationDescriptorType.SHORT ? 8 : 16; for (int offset = 0; offset + size <= length; offset += size) { var ad = new LongAllocationDescriptor(); ad.ExtentLength = descriptors.ToUInt32LittleEndian(offset); if (type == AllocationDescriptorType.SHORT) { ad.ExtentLocation.LogicalBlockNumber = descriptors.ToUInt32LittleEndian(offset + 4); ad.ExtentLocation.PartitionReferenceNumber = partition; } else { ad.ExtentLocation.LogicalBlockNumber = descriptors.ToUInt32LittleEndian(offset + 4); ad.ExtentLocation.PartitionReferenceNumber = descriptors.ToUInt16LittleEndian(offset + 8); for (int i = 0; i < 6; i++) { ad.ImplementationUse[i] = descriptors[offset + 10 + i]; } } // A zero length terminates the allocation descriptors if (GetExtentLength(ad.ExtentLength) == 0) return result; if (GetExtentType(ad.ExtentLength) == ExtentType.NEXT_EXTENT) { nextKey = ad.ExtentLocation.GetKey(); break; } result.Add(ad); } if (nextKey < 0) return result; if (!volume.AllocationExtentDescriptors.TryGetValue(nextKey, out var aed)) { missingKey = nextKey; return null; } partition = (ushort)(nextKey >> 32); descriptors = aed.AllocationDescriptors; length = descriptors.Length; } // Too many continuation extents, assume a loop return null; } /// /// Check a descriptor tag: checksum, version, CRC (when it fits into the data) and location /// /// ECMA-167 3/7.2; receiving systems allow descriptor version 2 or 3 (3/7.2.2) /// Data holding the descriptor /// Offset of the descriptor /// Number of bytes of the descriptor available from the offset /// Expected tag location (logical sector or logical block number) /// True if the tag is valid, false otherwise public static bool IsValidDescriptor(this byte[] data, int offset, int available, uint location) { byte checksum = 0; for (int i = 0; i < 16; i++) { if (i != 4) checksum += data[offset + i]; } if (checksum != data[offset + 4]) return false; ushort version = data.ToUInt16LittleEndian(offset + 2); if (version != 2 && version != 3) return false; ushort crcLength = data.ToUInt16LittleEndian(offset + 10); if (16 + crcLength <= available) { if (ComputeCrc(data, offset + 16, crcLength) != data.ToUInt16LittleEndian(offset + 8)) return false; } return data.ToUInt32LittleEndian(offset + 12) == location; } /// /// Decode OSTA Compressed Unicode d-characters (first byte = compression ID) /// /// Encoded characters /// Decoded string, empty for no characters, null for an unknown compression ID public static string? DecodeDCharacters(this byte[]? data) => DecodeDCharacters(data, data?.Length ?? 0); /// /// Decode an OSTA Compressed Unicode dstring (last byte = number of used bytes) /// /// Encoded dstring field /// Decoded string, empty for no characters, null for an unknown compression ID public static string? DecodeDString(this byte[]? data) { if (data is null || data.Length == 0) return string.Empty; int length = data[data.Length - 1]; if (length > data.Length - 1) length = data.Length - 1; return DecodeDCharacters(data, length); } /// /// Decode the first length bytes of OSTA Compressed Unicode d-characters /// private static string? DecodeDCharacters(byte[]? data, int length) { if (data is null || length == 0) return string.Empty; // 254 and 255 are the same encodings, used by UDF for deleted entries byte compressionId = data[0]; var builder = new StringBuilder(length); if (compressionId == Constants.CompressionId8 || compressionId == 254) { for (int i = 1; i < length; i++) { builder.Append((char)data[i]); } } else if (compressionId == Constants.CompressionId16 || compressionId == 255) { for (int i = 1; i + 1 < length; i += 2) { builder.Append((char)data.ToUInt16BigEndian(i)); } } else { return null; } return builder.ToString(); } // TODO: Replace with CRC-16/XMODEM from Hashing once its CRC code is faster /// /// Compute the CRC-ITU-T of a range of bytes /// private static ushort ComputeCrc(byte[] data, int offset, int length) { ushort crc = 0; for (int i = offset; i < offset + length; i++) { crc = (ushort)(CrcTable[((crc >> 8) ^ data[i]) & 0xFF] ^ (crc << 8)); } return crc; } /// /// Create the CRC-ITU-T table /// private static ushort[] CreateCrcTable() { var table = new ushort[256]; for (int i = 0; i < 256; i++) { ushort crc = (ushort)(i << 8); for (int bit = 0; bit < 8; bit++) { crc = (crc & 0x8000) != 0 ? (ushort)((crc << 1) ^ 0x1021) : (ushort)(crc << 1); } table[i] = crc; } return table; } } }