diff --git a/SabreTools.Data.Extensions.Test/UDFExtensionsTests.cs b/SabreTools.Data.Extensions.Test/UDFExtensionsTests.cs new file mode 100644 index 00000000..0d8c51df --- /dev/null +++ b/SabreTools.Data.Extensions.Test/UDFExtensionsTests.cs @@ -0,0 +1,199 @@ +using SabreTools.Data.Models.UDF; +using Xunit; + +namespace SabreTools.Data.Extensions.Test +{ + public class UDFExtensionsTests + { + #region DecodeDCharacters + + [Fact] + public void DecodeDCharacters_Null_Empty() + { + byte[]? data = null; + Assert.Equal(string.Empty, data.DecodeDCharacters()); + } + + [Fact] + public void DecodeDCharacters_8Bit_Decoded() + { + byte[] data = [8, 0x41, 0x62, 0xE4]; + Assert.Equal("Abä", data.DecodeDCharacters()); + } + + [Fact] + public void DecodeDCharacters_16Bit_Decoded() + { + byte[] data = [16, 0x00, 0x41, 0x30, 0x42]; + Assert.Equal("Aあ", data.DecodeDCharacters()); + } + + [Fact] + public void DecodeDCharacters_UnknownCompression_Null() + { + byte[] data = [1, 0x41]; + Assert.Null(data.DecodeDCharacters()); + } + + [Fact] + public void DecodeDString_UsesLengthByte() + { + byte[] data = [8, 0x41, 0x42, 0x43, 0x00, 3]; + Assert.Equal("AB", data.DecodeDString()); + } + + [Fact] + public void DecodeDString_ZeroLength_Empty() + { + byte[] data = [8, 0x41, 0x00, 0]; + Assert.Equal(string.Empty, data.DecodeDString()); + } + + #endregion + + #region TryGetSector + + [Fact] + public void TryGetSector_Type1_StartPlusBlock() + { + var volume = CreateVolume(new Type1PartitionMap { PartitionNumber = 0 }); + bool actual = volume.TryGetSector(UDFExtensions.GetKey(0, 10), out long sector); + Assert.True(actual); + Assert.Equal(1010, sector); + } + + [Fact] + public void TryGetSector_Type1_OutsidePartition_False() + { + var volume = CreateVolume(new Type1PartitionMap { PartitionNumber = 0 }); + bool actual = volume.TryGetSector(UDFExtensions.GetKey(0, 100), out _); + Assert.False(actual); + } + + [Fact] + public void TryGetSector_UnknownPartitionReference_False() + { + var volume = CreateVolume(new Type1PartitionMap { PartitionNumber = 0 }); + bool actual = volume.TryGetSector(UDFExtensions.GetKey(1, 0), out _); + Assert.False(actual); + } + + [Fact] + public void TryGetSector_Metadata_ThroughMetadataFile() + { + // Metadata File at block 5 of partition 0: blocks 0-1 of the metadata partition are blocks 20-21, + // blocks 2-3 are blocks 40-41 + var volume = CreateVolume( + new Type1PartitionMap { PartitionNumber = 0 }, + new MetadataPartitionMap { PartitionNumber = 0, MetadataFileLocation = 5, MetadataMirrorFileLocation = 6 }); + volume.FileEntries[UDFExtensions.GetKey(0, 5)] = CreateShortAdEntry((4096, 20), (4096, 40)); + + Assert.True(volume.TryGetSector(UDFExtensions.GetKey(1, 1), out long first)); + Assert.Equal(1021, first); + Assert.True(volume.TryGetSector(UDFExtensions.GetKey(1, 2), out long second)); + Assert.Equal(1040, second); + Assert.False(volume.TryGetSector(UDFExtensions.GetKey(1, 4), out _)); + } + + [Fact] + public void TryGetSector_Metadata_FallsBackToMirror() + { + var volume = CreateVolume( + new Type1PartitionMap { PartitionNumber = 0 }, + new MetadataPartitionMap { PartitionNumber = 0, MetadataFileLocation = 5, MetadataMirrorFileLocation = 6 }); + volume.FileEntries[UDFExtensions.GetKey(0, 6)] = CreateShortAdEntry((2048, 30)); + + Assert.True(volume.TryGetSector(UDFExtensions.GetKey(1, 0), out long sector)); + Assert.Equal(1030, sector); + } + + #endregion + + #region GetAllocationDescriptors + + [Fact] + public void GetAllocationDescriptors_Embedded_Empty() + { + var entry = new FileEntry(); + entry.ICBTag.Flags = (ushort)AllocationDescriptorType.EMBEDDED; + entry.AllocationDescriptors = [1, 2, 3]; + + var actual = new Volume().GetAllocationDescriptors(0, entry); + Assert.NotNull(actual); + Assert.Empty(actual); + } + + [Fact] + public void GetAllocationDescriptors_ZeroLengthTerminates() + { + var entry = CreateShortAdEntry((2048, 20), (0, 99), (2048, 40)); + + var actual = new Volume().GetAllocationDescriptors(UDFExtensions.GetKey(2, 7), entry); + Assert.NotNull(actual); + var ad = Assert.Single(actual); + Assert.Equal(20u, ad.ExtentLocation.LogicalBlockNumber); + Assert.Equal((ushort)2, ad.ExtentLocation.PartitionReferenceNumber); + } + + [Fact] + public void GetAllocationDescriptors_MissingAllocationExtent_NullWithKey() + { + var entry = CreateShortAdEntry((2048, 20), ((3u << 30) | 2048, 50)); + + var actual = new Volume().GetAllocationDescriptors(UDFExtensions.GetKey(0, 7), entry, out long missingKey); + Assert.Null(actual); + Assert.Equal(UDFExtensions.GetKey(0, 50), missingKey); + } + + [Fact] + public void GetAllocationDescriptors_AllocationExtentLoop_Null() + { + var volume = new Volume(); + var entry = CreateShortAdEntry(((3u << 30) | 2048, 50)); + volume.AllocationExtentDescriptors[UDFExtensions.GetKey(0, 50)] = new AllocationExtentDescriptor + { + AllocationDescriptors = CreateShortAdEntry(((3u << 30) | 2048, 50)).AllocationDescriptors, + }; + + var actual = volume.GetAllocationDescriptors(UDFExtensions.GetKey(0, 7), entry); + Assert.Null(actual); + } + + #endregion + + /// + /// Volume with partition 0 at sector 1000 (length 50) and the given partition maps + /// + private static Volume CreateVolume(params PartitionMap[] maps) + { + return new Volume + { + VolumeDescriptorSequence = + [ + new PartitionDescriptor { PartitionNumber = 0, PartitionStartingLocation = 1000, PartitionLength = 50 }, + new LogicalVolumeDescriptor { LogicalBlockSize = 2048, PartitionMaps = maps }, + ], + }; + } + + /// + /// File Entry with the given short allocation descriptors (extent length, position) + /// + private static FileEntry CreateShortAdEntry(params (uint Length, uint Position)[] extents) + { + var data = new byte[extents.Length * 8]; + for (int i = 0; i < extents.Length; i++) + { + for (int b = 0; b < 4; b++) + { + data[(i * 8) + b] = (byte)(extents[i].Length >> (8 * b)); + data[(i * 8) + 4 + b] = (byte)(extents[i].Position >> (8 * b)); + } + } + + var entry = new FileEntry { AllocationDescriptors = data }; + entry.ICBTag.Flags = (ushort)AllocationDescriptorType.SHORT; + return entry; + } + } +} diff --git a/SabreTools.Data.Extensions/UDFExtensions.cs b/SabreTools.Data.Extensions/UDFExtensions.cs new file mode 100644 index 00000000..f69a6611 --- /dev/null +++ b/SabreTools.Data.Extensions/UDFExtensions.cs @@ -0,0 +1,347 @@ +using System.Collections.Generic; +using System.Text; +using SabreTools.Data.Models.UDF; + +namespace SabreTools.Data.Extensions +{ + public static class UDFExtensions + { + /// + /// Maximum number of Allocation Extent Descriptors followed for one File Entry + /// + private const int MaximumAllocationExtents = 1024; + + /// + /// 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; + + var result = new List(); + 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 = ReadUInt32(descriptors, offset); + if (type == AllocationDescriptorType.SHORT) + { + ad.ExtentLocation.LogicalBlockNumber = ReadUInt32(descriptors, offset + 4); + ad.ExtentLocation.PartitionReferenceNumber = partition; + } + else + { + ad.ExtentLocation.LogicalBlockNumber = ReadUInt32(descriptors, offset + 4); + ad.ExtentLocation.PartitionReferenceNumber = (ushort)(descriptors[offset + 8] | (descriptors[offset + 9] << 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; + } + + /// + /// 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[i] << 8) | data[i + 1])); + } + } + else + { + return null; + } + + return builder.ToString(); + } + + /// + /// Read a little-endian UInt32 from a byte array + /// + private static uint ReadUInt32(byte[] data, int offset) + => (uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24)); + } +} diff --git a/SabreTools.Data.Models/UDF/AllocationExtentDescriptor.cs b/SabreTools.Data.Models/UDF/AllocationExtentDescriptor.cs new file mode 100644 index 00000000..2735047b --- /dev/null +++ b/SabreTools.Data.Models/UDF/AllocationExtentDescriptor.cs @@ -0,0 +1,24 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Allocation Extent Descriptor (4/14.5), continues the allocation descriptors of a File Entry + /// + /// + public sealed class AllocationExtentDescriptor : Descriptor + { + /// + /// Logical block of the previous allocation extent + /// + public uint PreviousAllocationExtentLocation { get; set; } + + /// + /// Length of the allocation descriptors in bytes + /// + public uint LengthOfAllocationDescriptors { get; set; } + + /// + /// Allocation descriptors, of the type given in the owning File Entry's ICB tag flags + /// + public byte[] AllocationDescriptors { get; set; } = []; + } +} diff --git a/SabreTools.Data.Models/UDF/AnchorVolumeDescriptorPointer.cs b/SabreTools.Data.Models/UDF/AnchorVolumeDescriptorPointer.cs new file mode 100644 index 00000000..0a7ac56a --- /dev/null +++ b/SabreTools.Data.Models/UDF/AnchorVolumeDescriptorPointer.cs @@ -0,0 +1,24 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Anchor Volume Descriptor Pointer (3/10.2), recorded at sector 256 (and at the end of the volume) + /// + /// + public sealed class AnchorVolumeDescriptorPointer : Descriptor + { + /// + /// Extent of the Main Volume Descriptor Sequence + /// + public ExtentDescriptor MainVolumeDescriptorSequenceExtent { get; set; } = new(); + + /// + /// Extent of the Reserve Volume Descriptor Sequence + /// + public ExtentDescriptor ReserveVolumeDescriptorSequenceExtent { get; set; } = new(); + + /// + /// Reserved, 480 bytes of 0x00 + /// + public byte[] Reserved { get; set; } = new byte[480]; + } +} diff --git a/SabreTools.Data.Models/UDF/CharacterSetSpecification.cs b/SabreTools.Data.Models/UDF/CharacterSetSpecification.cs new file mode 100644 index 00000000..2bc272a2 --- /dev/null +++ b/SabreTools.Data.Models/UDF/CharacterSetSpecification.cs @@ -0,0 +1,20 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Character Set Specification (charspec, 1/7.2.1) + /// UDF requires CS0 with the information "OSTA Compressed Unicode" + /// + /// + public sealed class CharacterSetSpecification + { + /// + /// Character set type (0 = CS0) + /// + public byte CharacterSetType { get; set; } + + /// + /// Character set information, padded with 0x00 + /// + public byte[] CharacterSetInformation { get; set; } = new byte[63]; + } +} diff --git a/SabreTools.Data.Models/UDF/Constants.cs b/SabreTools.Data.Models/UDF/Constants.cs new file mode 100644 index 00000000..96ad5c40 --- /dev/null +++ b/SabreTools.Data.Models/UDF/Constants.cs @@ -0,0 +1,126 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF / ECMA-167 constant values and arrays + /// + /// + /// + public static class Constants + { + #region Volume Recognition Constants + + /// + /// Size of a logical sector on optical media + /// + public const int SectorSize = 2048; + + /// + /// Sector of the first Volume Structure Descriptor (after the 32KiB System Area) + /// + public const int VolumeRecognitionSequenceSector = 16; + + /// + /// Sector of the first Anchor Volume Descriptor Pointer + /// + public const int AnchorVolumeDescriptorPointerSector = 256; + + /// + /// Beginning Extended Area Descriptor identifier, "BEA01" + /// + public static readonly byte[] BeginningExtendedAreaDescriptor = [0x42, 0x45, 0x41, 0x30, 0x31]; + + /// + /// NSR Descriptor identifier for ECMA-167 2nd edition (UDF 1.02 to 1.50), "NSR02" + /// + public static readonly byte[] NSR02 = [0x4E, 0x53, 0x52, 0x30, 0x32]; + + /// + /// NSR Descriptor identifier for ECMA-167 3rd edition (UDF 2.00 and later), "NSR03" + /// + public static readonly byte[] NSR03 = [0x4E, 0x53, 0x52, 0x30, 0x33]; + + /// + /// Terminating Extended Area Descriptor identifier, "TEA01" + /// + public static readonly byte[] TerminatingExtendedAreaDescriptor = [0x54, 0x45, 0x41, 0x30, 0x31]; + + /// + /// ISO 9660 volume descriptor identifier, "CD001" (bridge discs) + /// + public static readonly byte[] CD001 = [0x43, 0x44, 0x30, 0x30, 0x31]; + + /// + /// Boot Descriptor identifier, "BOOT2" + /// + public static readonly byte[] BOOT2 = [0x42, 0x4F, 0x4F, 0x54, 0x32]; + + /// + /// ECMA-168 volume descriptor identifier, "CDW02" + /// + public static readonly byte[] CDW02 = [0x43, 0x44, 0x57, 0x30, 0x32]; + + #endregion + + #region Partition Map Constants + + /// + /// Type 1 Partition Map type + /// + public const byte Type1PartitionMap = 1; + + /// + /// Type 2 Partition Map type + /// + public const byte Type2PartitionMap = 2; + + /// + /// Partition type identifier of a UDF Metadata Partition (UDF 2.50 and later) + /// + public const string MetadataPartitionIdentifier = "*UDF Metadata Partition"; + + /// + /// Partition type identifier of a UDF Sparable Partition + /// + public const string SparablePartitionIdentifier = "*UDF Sparable Partition"; + + /// + /// Partition type identifier of a UDF Virtual Partition + /// + public const string VirtualPartitionIdentifier = "*UDF Virtual Partition"; + + #endregion + + #region Character Set Constants + + /// + /// OSTA Compressed Unicode, 8 bits per character + /// + public const byte CompressionId8 = 8; + + /// + /// OSTA Compressed Unicode, 16 bits per character (big-endian) + /// + public const byte CompressionId16 = 16; + + #endregion + + #region Allocation Descriptor Constants + + /// + /// Mask of the extent length in an allocation descriptor (the top two bits hold the extent type) + /// + public const uint ExtentLengthMask = 0x3FFFFFFF; + + /// + /// Shift of the extent type in an allocation descriptor's extent length + /// + public const int ExtentTypeShift = 30; + + /// + /// Mask of the allocation descriptor type in an ICB tag's flags + /// + public const ushort AllocationDescriptorTypeMask = 0x0007; + + #endregion + } +} diff --git a/SabreTools.Data.Models/UDF/Descriptor.cs b/SabreTools.Data.Models/UDF/Descriptor.cs new file mode 100644 index 00000000..caf3e508 --- /dev/null +++ b/SabreTools.Data.Models/UDF/Descriptor.cs @@ -0,0 +1,14 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// Abstract UDF tagged descriptor, all start with a Descriptor Tag + /// + /// + public abstract class Descriptor + { + /// + /// Descriptor tag + /// + public DescriptorTag DescriptorTag { get; set; } = new(); + } +} diff --git a/SabreTools.Data.Models/UDF/DescriptorTag.cs b/SabreTools.Data.Models/UDF/DescriptorTag.cs new file mode 100644 index 00000000..f79e199f --- /dev/null +++ b/SabreTools.Data.Models/UDF/DescriptorTag.cs @@ -0,0 +1,50 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Descriptor Tag (3/7.2), the first 16 bytes of every tagged descriptor + /// + /// + public sealed class DescriptorTag + { + /// + /// Type of the descriptor + /// + public TagIdentifier TagIdentifier { get; set; } + + /// + /// Version of the descriptor, 2 (ECMA-167 2nd edition) or 3 (3rd edition) + /// + public ushort DescriptorVersion { get; set; } + + /// + /// Sum modulo 256 of bytes 0-3 and 5-15 of the tag + /// + public byte TagChecksum { get; set; } + + /// + /// Reserved, 0x00 + /// + public byte Reserved { get; set; } + + /// + /// Identification of a set of descriptors + /// + public ushort TagSerialNumber { get; set; } + + /// + /// CRC-CCITT of the DescriptorCRCLength bytes following the tag + /// + public ushort DescriptorCRC { get; set; } + + /// + /// Number of bytes after the tag covered by the DescriptorCRC + /// + public ushort DescriptorCRCLength { get; set; } + + /// + /// Number of the logical sector (volume structures) or logical block (file structures) + /// in which the descriptor is recorded + /// + public uint TagLocation { get; set; } + } +} diff --git a/SabreTools.Data.Models/UDF/EntityIdentifier.cs b/SabreTools.Data.Models/UDF/EntityIdentifier.cs new file mode 100644 index 00000000..2f61c2ab --- /dev/null +++ b/SabreTools.Data.Models/UDF/EntityIdentifier.cs @@ -0,0 +1,24 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Entity Identifier (regid, 1/7.4) + /// + /// + public sealed class EntityIdentifier + { + /// + /// Flags (bit 0: dirty, bit 1: protected) + /// + public byte Flags { get; set; } + + /// + /// Identifier, padded with 0x00 + /// + public byte[] Identifier { get; set; } = new byte[23]; + + /// + /// Identifier suffix (UDF domain identifiers: revision as a little-endian ushort, then domain flags) + /// + public byte[] IdentifierSuffix { get; set; } = new byte[8]; + } +} diff --git a/SabreTools.Data.Models/UDF/Enums.cs b/SabreTools.Data.Models/UDF/Enums.cs new file mode 100644 index 00000000..f88d0d50 --- /dev/null +++ b/SabreTools.Data.Models/UDF/Enums.cs @@ -0,0 +1,306 @@ +using System; + +namespace SabreTools.Data.Models.UDF +{ + /// + /// Enum for DescriptorTag.TagIdentifier + /// + /// + public enum TagIdentifier : ushort + { + /// + /// Primary Volume Descriptor (3/10.1) + /// + PRIMARY_VOLUME_DESCRIPTOR = 1, + + /// + /// Anchor Volume Descriptor Pointer (3/10.2) + /// + ANCHOR_VOLUME_DESCRIPTOR_POINTER = 2, + + /// + /// Volume Descriptor Pointer (3/10.3) + /// + VOLUME_DESCRIPTOR_POINTER = 3, + + /// + /// Implementation Use Volume Descriptor (3/10.4) + /// + IMPLEMENTATION_USE_VOLUME_DESCRIPTOR = 4, + + /// + /// Partition Descriptor (3/10.5) + /// + PARTITION_DESCRIPTOR = 5, + + /// + /// Logical Volume Descriptor (3/10.6) + /// + LOGICAL_VOLUME_DESCRIPTOR = 6, + + /// + /// Unallocated Space Descriptor (3/10.8) + /// + UNALLOCATED_SPACE_DESCRIPTOR = 7, + + /// + /// Terminating Descriptor (3/10.9) + /// + TERMINATING_DESCRIPTOR = 8, + + /// + /// Logical Volume Integrity Descriptor (3/10.10) + /// + LOGICAL_VOLUME_INTEGRITY_DESCRIPTOR = 9, + + /// + /// File Set Descriptor (4/14.1) + /// + FILE_SET_DESCRIPTOR = 256, + + /// + /// File Identifier Descriptor (4/14.4) + /// + FILE_IDENTIFIER_DESCRIPTOR = 257, + + /// + /// Allocation Extent Descriptor (4/14.5) + /// + ALLOCATION_EXTENT_DESCRIPTOR = 258, + + /// + /// Indirect Entry (4/14.7) + /// + INDIRECT_ENTRY = 259, + + /// + /// Terminal Entry (4/14.8) + /// + TERMINAL_ENTRY = 260, + + /// + /// File Entry (4/14.9) + /// + FILE_ENTRY = 261, + + /// + /// Extended Attribute Header Descriptor (4/14.10.1) + /// + EXTENDED_ATTRIBUTE_HEADER_DESCRIPTOR = 262, + + /// + /// Unallocated Space Entry (4/14.11) + /// + UNALLOCATED_SPACE_ENTRY = 263, + + /// + /// Space Bitmap Descriptor (4/14.12) + /// + SPACE_BITMAP_DESCRIPTOR = 264, + + /// + /// Partition Integrity Entry (4/14.13) + /// + PARTITION_INTEGRITY_ENTRY = 265, + + /// + /// Extended File Entry (4/14.17) + /// + EXTENDED_FILE_ENTRY = 266, + } + + /// + /// Enum for FileIdentifierDescriptor.FileCharacteristics (4/14.4.3) + /// Bits 5-7: Reserved: 0 + /// + /// + [Flags] + public enum FileCharacteristics : byte + { + /// + /// No characteristics set + /// + NONE = 0x00, + + /// + /// The existence of the file shall not be made known to the user (hidden) + /// + EXISTENCE = 0x01, + + /// + /// The file is a directory + /// + DIRECTORY = 0x02, + + /// + /// The descriptor shall be ignored (deleted) + /// + DELETED = 0x04, + + /// + /// The ICB identifies the parent directory + /// + PARENT = 0x08, + + /// + /// The file contains implementation use data (UDF: stream directory metadata) + /// + METADATA = 0x10, + } + + /// + /// Enum for ICBTag.FileType (4/14.6.6, UDF 2.3.5.4) + /// + /// + public enum FileType : byte + { + /// + /// Not specified + /// + UNSPECIFIED = 0, + + /// + /// Unallocated Space Entry + /// + UNALLOCATED_SPACE_ENTRY = 1, + + /// + /// Partition Integrity Entry + /// + PARTITION_INTEGRITY_ENTRY = 2, + + /// + /// Indirect Entry + /// + INDIRECT_ENTRY = 3, + + /// + /// Directory + /// + DIRECTORY = 4, + + /// + /// Sequence of bytes (a regular file) + /// + FILE = 5, + + /// + /// Block special device + /// + BLOCK_DEVICE = 6, + + /// + /// Character special device + /// + CHARACTER_DEVICE = 7, + + /// + /// Extended attributes + /// + EXTENDED_ATTRIBUTES = 8, + + /// + /// FIFO file + /// + FIFO = 9, + + /// + /// Socket + /// + SOCKET = 10, + + /// + /// Terminal Entry + /// + TERMINAL_ENTRY = 11, + + /// + /// Symbolic link + /// + SYMBOLIC_LINK = 12, + + /// + /// Stream directory + /// + STREAM_DIRECTORY = 13, + + /// + /// UDF Virtual Allocation Table + /// + VIRTUAL_ALLOCATION_TABLE = 248, + + /// + /// UDF Real-Time file + /// + REAL_TIME_FILE = 249, + + /// + /// UDF Metadata File + /// + METADATA_FILE = 250, + + /// + /// UDF Metadata Mirror File + /// + METADATA_MIRROR_FILE = 251, + + /// + /// UDF Metadata Bitmap File + /// + METADATA_BITMAP_FILE = 252, + } + + /// + /// Enum for the allocation descriptor type in ICBTag.Flags (4/14.6.8) + /// + /// + public enum AllocationDescriptorType : byte + { + /// + /// Short Allocation Descriptors (4/14.14.1) + /// + SHORT = 0, + + /// + /// Long Allocation Descriptors (4/14.14.2) + /// + LONG = 1, + + /// + /// Extended Allocation Descriptors (4/14.14.3) + /// + EXTENDED = 2, + + /// + /// The file data is embedded in the allocation descriptors field + /// + EMBEDDED = 3, + } + + /// + /// Enum for the extent type in the top two bits of an allocation descriptor's extent length (4/14.14.1.1) + /// + /// + public enum ExtentType : byte + { + /// + /// Extent recorded and allocated + /// + RECORDED_AND_ALLOCATED = 0, + + /// + /// Extent not recorded but allocated + /// + NOT_RECORDED_BUT_ALLOCATED = 1, + + /// + /// Extent not recorded and not allocated + /// + NOT_RECORDED_NOT_ALLOCATED = 2, + + /// + /// The extent is the next extent of allocation descriptors + /// + NEXT_EXTENT = 3, + } +} diff --git a/SabreTools.Data.Models/UDF/ExtendedFileEntry.cs b/SabreTools.Data.Models/UDF/ExtendedFileEntry.cs new file mode 100644 index 00000000..2aa41f3b --- /dev/null +++ b/SabreTools.Data.Models/UDF/ExtendedFileEntry.cs @@ -0,0 +1,30 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Extended File Entry (4/14.17, UDF 2.00 and later), a File Entry with object size, + /// creation time and a stream directory + /// + /// + public sealed class ExtendedFileEntry : FileEntry + { + /// + /// Size of the file and its streams in bytes + /// + public ulong ObjectSize { get; set; } + + /// + /// Creation date and time + /// + public Timestamp CreationDateAndTime { get; set; } = new(); + + /// + /// Reserved, 0x00000000 + /// + public uint Reserved { get; set; } + + /// + /// ICB of the stream directory, length 0 if none + /// + public LongAllocationDescriptor StreamDirectoryICB { get; set; } = new(); + } +} diff --git a/SabreTools.Data.Models/UDF/ExtentDescriptor.cs b/SabreTools.Data.Models/UDF/ExtentDescriptor.cs new file mode 100644 index 00000000..8b496978 --- /dev/null +++ b/SabreTools.Data.Models/UDF/ExtentDescriptor.cs @@ -0,0 +1,19 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Extent Descriptor (extent_ad, 3/7.1), an extent of logical sectors + /// + /// + public sealed class ExtentDescriptor + { + /// + /// Length of the extent in bytes + /// + public uint ExtentLength { get; set; } + + /// + /// Logical sector number of the start of the extent + /// + public uint ExtentLocation { get; set; } + } +} diff --git a/SabreTools.Data.Models/UDF/FileEntry.cs b/SabreTools.Data.Models/UDF/FileEntry.cs new file mode 100644 index 00000000..b81a30ea --- /dev/null +++ b/SabreTools.Data.Models/UDF/FileEntry.cs @@ -0,0 +1,115 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF File Entry (4/14.9), the ICB describing a file or directory and where its data is recorded + /// + /// + public class FileEntry : Descriptor + { + /// + /// ICB tag + /// + public ICBTag ICBTag { get; set; } = new(); + + /// + /// User ID + /// + public uint Uid { get; set; } + + /// + /// Group ID + /// + public uint Gid { get; set; } + + /// + /// Permissions + /// + public uint Permissions { get; set; } + + /// + /// Number of File Identifier Descriptors identifying this ICB + /// + public ushort FileLinkCount { get; set; } + + /// + /// Record format + /// + public byte RecordFormat { get; set; } + + /// + /// Record display attributes + /// + public byte RecordDisplayAttributes { get; set; } + + /// + /// Record length + /// + public uint RecordLength { get; set; } + + /// + /// Size of the file in bytes + /// + public ulong InformationLength { get; set; } + + /// + /// Number of recorded logical blocks + /// + public ulong LogicalBlocksRecorded { get; set; } + + /// + /// Access date and time + /// + public Timestamp AccessDateAndTime { get; set; } = new(); + + /// + /// Modification date and time + /// + public Timestamp ModificationDateAndTime { get; set; } = new(); + + /// + /// Attribute date and time + /// + public Timestamp AttributeDateAndTime { get; set; } = new(); + + /// + /// Checkpoint + /// + public uint Checkpoint { get; set; } + + /// + /// ICB of the extended attributes, length 0 if none + /// + public LongAllocationDescriptor ExtendedAttributeICB { get; set; } = new(); + + /// + /// Implementation identifier + /// + public EntityIdentifier ImplementationIdentifier { get; set; } = new(); + + /// + /// Unique ID + /// + public ulong UniqueID { get; set; } + + /// + /// Length of the extended attributes in bytes + /// + public uint LengthOfExtendedAttributes { get; set; } + + /// + /// Length of the allocation descriptors in bytes + /// + public uint LengthOfAllocationDescriptors { get; set; } + + /// + /// Extended attributes + /// + public byte[] ExtendedAttributes { get; set; } = []; + + /// + /// Allocation descriptors of the type given in the ICB tag flags, + /// or the file data itself if the type is AllocationDescriptorType.EMBEDDED + /// + public byte[] AllocationDescriptors { get; set; } = []; + } +} diff --git a/SabreTools.Data.Models/UDF/FileIdentifierDescriptor.cs b/SabreTools.Data.Models/UDF/FileIdentifierDescriptor.cs new file mode 100644 index 00000000..ec9822a4 --- /dev/null +++ b/SabreTools.Data.Models/UDF/FileIdentifierDescriptor.cs @@ -0,0 +1,49 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF File Identifier Descriptor (4/14.4), a directory entry naming a file or directory and pointing to its ICB + /// + /// + public sealed class FileIdentifierDescriptor : Descriptor + { + /// + /// File version number + /// + public ushort FileVersionNumber { get; set; } + + /// + /// File characteristics + /// + public FileCharacteristics FileCharacteristics { get; set; } + + /// + /// Length of the file identifier in bytes + /// + public byte LengthOfFileIdentifier { get; set; } + + /// + /// ICB of the file or directory + /// + public LongAllocationDescriptor ICB { get; set; } = new(); + + /// + /// Length of the implementation use in bytes + /// + public ushort LengthOfImplementationUse { get; set; } + + /// + /// Implementation use + /// + public byte[] ImplementationUse { get; set; } = []; + + /// + /// File identifier, d-characters (first byte = compression ID), empty for the parent directory + /// + public byte[] FileIdentifier { get; set; } = []; + + /// + /// Padding to a multiple of 4 bytes + /// + public byte[] Padding { get; set; } = []; + } +} diff --git a/SabreTools.Data.Models/UDF/FileSetDescriptor.cs b/SabreTools.Data.Models/UDF/FileSetDescriptor.cs new file mode 100644 index 00000000..129a6a30 --- /dev/null +++ b/SabreTools.Data.Models/UDF/FileSetDescriptor.cs @@ -0,0 +1,99 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF File Set Descriptor (4/14.1), the location of the root directory + /// + /// + public sealed class FileSetDescriptor : Descriptor + { + /// + /// Recording date and time + /// + public Timestamp RecordingDateAndTime { get; set; } = new(); + + /// + /// Interchange level + /// + public ushort InterchangeLevel { get; set; } + + /// + /// Maximum interchange level + /// + public ushort MaximumInterchangeLevel { get; set; } + + /// + /// Character set list + /// + public uint CharacterSetList { get; set; } + + /// + /// Maximum character set list + /// + public uint MaximumCharacterSetList { get; set; } + + /// + /// File set number + /// + public uint FileSetNumber { get; set; } + + /// + /// File Set Descriptor number + /// + public uint FileSetDescriptorNumber { get; set; } + + /// + /// Logical volume identifier character set + /// + public CharacterSetSpecification LogicalVolumeIdentifierCharacterSet { get; set; } = new(); + + /// + /// Logical volume identifier, dstring (last byte = length) + /// + public byte[] LogicalVolumeIdentifier { get; set; } = new byte[128]; + + /// + /// File set character set + /// + public CharacterSetSpecification FileSetCharacterSet { get; set; } = new(); + + /// + /// File set identifier, dstring (last byte = length) + /// + public byte[] FileSetIdentifier { get; set; } = new byte[32]; + + /// + /// Copyright file identifier, dstring (last byte = length) + /// + public byte[] CopyrightFileIdentifier { get; set; } = new byte[32]; + + /// + /// Abstract file identifier, dstring (last byte = length) + /// + public byte[] AbstractFileIdentifier { get; set; } = new byte[32]; + + /// + /// ICB of the root directory + /// + public LongAllocationDescriptor RootDirectoryICB { get; set; } = new(); + + /// + /// Domain identifier + /// + public EntityIdentifier DomainIdentifier { get; set; } = new(); + + /// + /// Next extent of File Set Descriptors, length 0 if none + /// + public LongAllocationDescriptor NextExtent { get; set; } = new(); + + /// + /// ICB of the system stream directory (ECMA-167 3rd edition), length 0 if none + /// + public LongAllocationDescriptor SystemStreamDirectoryICB { get; set; } = new(); + + /// + /// Reserved, 32 bytes of 0x00 + /// + public byte[] Reserved { get; set; } = new byte[32]; + } +} diff --git a/SabreTools.Data.Models/UDF/GenericDescriptor.cs b/SabreTools.Data.Models/UDF/GenericDescriptor.cs new file mode 100644 index 00000000..fcbcb8f4 --- /dev/null +++ b/SabreTools.Data.Models/UDF/GenericDescriptor.cs @@ -0,0 +1,15 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF tagged descriptor of a type that is not parsed further + /// (e.g. Implementation Use Volume Descriptor, Unallocated Space Descriptor, Terminating Descriptor) + /// + /// + public sealed class GenericDescriptor : Descriptor + { + /// + /// Contents of the descriptor after the tag, up to the end of the logical sector + /// + public byte[] Data { get; set; } = []; + } +} diff --git a/SabreTools.Data.Models/UDF/ICBTag.cs b/SabreTools.Data.Models/UDF/ICBTag.cs new file mode 100644 index 00000000..df4634a5 --- /dev/null +++ b/SabreTools.Data.Models/UDF/ICBTag.cs @@ -0,0 +1,49 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Information Control Block Tag (icbtag, 4/14.6) + /// + /// + public sealed class ICBTag + { + /// + /// Prior recorded number of direct entries + /// + public uint PriorRecordedNumberOfDirectEntries { get; set; } + + /// + /// Strategy type (UDF: 4, or 4096 for write-once media) + /// + public ushort StrategyType { get; set; } + + /// + /// Strategy parameter, 2 bytes + /// + public byte[] StrategyParameter { get; set; } = new byte[2]; + + /// + /// Maximum number of entries + /// + public ushort MaximumNumberOfEntries { get; set; } + + /// + /// Reserved, 0x00 + /// + public byte Reserved { get; set; } + + /// + /// File type + /// + public FileType FileType { get; set; } + + /// + /// Parent ICB location + /// + public LogicalBlockAddress ParentICBLocation { get; set; } = new(); + + /// + /// Flags, the lower 3 bits hold the AllocationDescriptorType + /// + public ushort Flags { get; set; } + } +} diff --git a/SabreTools.Data.Models/UDF/LogicalBlockAddress.cs b/SabreTools.Data.Models/UDF/LogicalBlockAddress.cs new file mode 100644 index 00000000..797559e8 --- /dev/null +++ b/SabreTools.Data.Models/UDF/LogicalBlockAddress.cs @@ -0,0 +1,19 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Recorded Address (lb_addr, 4/7.1), a logical block within a partition + /// + /// + public sealed class LogicalBlockAddress + { + /// + /// Logical block number, relative to the start of the partition + /// + public uint LogicalBlockNumber { get; set; } + + /// + /// Index of the partition map in the Logical Volume Descriptor + /// + public ushort PartitionReferenceNumber { get; set; } + } +} diff --git a/SabreTools.Data.Models/UDF/LogicalVolumeDescriptor.cs b/SabreTools.Data.Models/UDF/LogicalVolumeDescriptor.cs new file mode 100644 index 00000000..8682ac86 --- /dev/null +++ b/SabreTools.Data.Models/UDF/LogicalVolumeDescriptor.cs @@ -0,0 +1,69 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Logical Volume Descriptor (3/10.6), block size, partition maps and the location of the File Set Descriptor + /// + /// + public sealed class LogicalVolumeDescriptor : Descriptor + { + /// + /// Volume Descriptor Sequence Number, the highest number prevails + /// + public uint VolumeDescriptorSequenceNumber { get; set; } + + /// + /// Descriptor character set + /// + public CharacterSetSpecification DescriptorCharacterSet { get; set; } = new(); + + /// + /// Logical volume identifier, dstring (last byte = length) + /// + public byte[] LogicalVolumeIdentifier { get; set; } = new byte[128]; + + /// + /// Size of a logical block in bytes + /// + public uint LogicalBlockSize { get; set; } + + /// + /// Domain identifier ("*OSTA UDF Compliant", suffix holds the UDF revision) + /// + public EntityIdentifier DomainIdentifier { get; set; } = new(); + + /// + /// Logical volume contents use (UDF: extent of the File Set Descriptor) + /// + public LongAllocationDescriptor LogicalVolumeContentsUse { get; set; } = new(); + + /// + /// Length of the partition maps in bytes + /// + public uint MapTableLength { get; set; } + + /// + /// Number of partition maps + /// + public uint NumberOfPartitionMaps { get; set; } + + /// + /// Implementation identifier + /// + public EntityIdentifier ImplementationIdentifier { get; set; } = new(); + + /// + /// Implementation use, 128 bytes + /// + public byte[] ImplementationUse { get; set; } = new byte[128]; + + /// + /// Extent of the Logical Volume Integrity Sequence + /// + public ExtentDescriptor IntegritySequenceExtent { get; set; } = new(); + + /// + /// Partition maps, a partition reference number is the index into this array + /// + public PartitionMap[] PartitionMaps { get; set; } = []; + } +} diff --git a/SabreTools.Data.Models/UDF/LongAllocationDescriptor.cs b/SabreTools.Data.Models/UDF/LongAllocationDescriptor.cs new file mode 100644 index 00000000..04596bb2 --- /dev/null +++ b/SabreTools.Data.Models/UDF/LongAllocationDescriptor.cs @@ -0,0 +1,25 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Long Allocation Descriptor (long_ad, 4/14.14.2), an extent within any partition of the logical volume + /// + /// + public sealed class LongAllocationDescriptor + { + /// + /// Length of the extent in bytes (lower 30 bits) and the ExtentType (upper 2 bits) + /// A length of 0 terminates a sequence of allocation descriptors + /// + public uint ExtentLength { get; set; } + + /// + /// Logical block address of the start of the extent + /// + public LogicalBlockAddress ExtentLocation { get; set; } = new(); + + /// + /// Implementation use (UDF: flags and unique ID bits) + /// + public byte[] ImplementationUse { get; set; } = new byte[6]; + } +} diff --git a/SabreTools.Data.Models/UDF/MetadataPartitionMap.cs b/SabreTools.Data.Models/UDF/MetadataPartitionMap.cs new file mode 100644 index 00000000..c2591bf0 --- /dev/null +++ b/SabreTools.Data.Models/UDF/MetadataPartitionMap.cs @@ -0,0 +1,45 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Metadata Partition Map (UDF 2.2.10, UDF 2.50 and later), a partition whose logical blocks + /// are the contents of the Metadata File recorded in the underlying partition + /// + /// + public sealed class MetadataPartitionMap : Type2PartitionMap + { + /// + /// Logical block of the Metadata File's File Entry in the underlying partition + /// + public uint MetadataFileLocation { get; set; } + + /// + /// Logical block of the Metadata Mirror File's File Entry in the underlying partition + /// + public uint MetadataMirrorFileLocation { get; set; } + + /// + /// Logical block of the Metadata Bitmap File's File Entry, 0xFFFFFFFF if not recorded + /// + public uint MetadataBitmapFileLocation { get; set; } + + /// + /// Allocation unit size in logical blocks + /// + public uint AllocationUnitSize { get; set; } + + /// + /// Alignment unit size in logical blocks + /// + public ushort AlignmentUnitSize { get; set; } + + /// + /// Flags (bit 0: duplicate metadata flag) + /// + public byte Flags { get; set; } + + /// + /// Reserved, 5 bytes of 0x00 + /// + public byte[] Reserved2 { get; set; } = new byte[5]; + } +} diff --git a/SabreTools.Data.Models/UDF/PartitionDescriptor.cs b/SabreTools.Data.Models/UDF/PartitionDescriptor.cs new file mode 100644 index 00000000..b4307b55 --- /dev/null +++ b/SabreTools.Data.Models/UDF/PartitionDescriptor.cs @@ -0,0 +1,64 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Partition Descriptor (3/10.5), the physical location of a partition + /// + /// + public sealed class PartitionDescriptor : Descriptor + { + /// + /// Volume Descriptor Sequence Number, the highest number prevails + /// + public uint VolumeDescriptorSequenceNumber { get; set; } + + /// + /// Partition flags (bit 0: space is allocated) + /// + public ushort PartitionFlags { get; set; } + + /// + /// Partition number, referenced by the partition maps + /// + public ushort PartitionNumber { get; set; } + + /// + /// Partition contents ("+NSR02" or "+NSR03" for UDF) + /// + public EntityIdentifier PartitionContents { get; set; } = new(); + + /// + /// Partition contents use (Partition Header Descriptor for NSR), 128 bytes + /// + public byte[] PartitionContentsUse { get; set; } = new byte[128]; + + /// + /// Access type (1 = read-only, 2 = write-once, 3 = rewritable, 4 = overwritable) + /// + public uint AccessType { get; set; } + + /// + /// Logical sector number of the first sector of the partition + /// + public uint PartitionStartingLocation { get; set; } + + /// + /// Length of the partition in logical sectors + /// + public uint PartitionLength { get; set; } + + /// + /// Implementation identifier + /// + public EntityIdentifier ImplementationIdentifier { get; set; } = new(); + + /// + /// Implementation use, 128 bytes + /// + public byte[] ImplementationUse { get; set; } = new byte[128]; + + /// + /// Reserved, 156 bytes of 0x00 + /// + public byte[] Reserved { get; set; } = new byte[156]; + } +} diff --git a/SabreTools.Data.Models/UDF/PartitionMap.cs b/SabreTools.Data.Models/UDF/PartitionMap.cs new file mode 100644 index 00000000..45f8d752 --- /dev/null +++ b/SabreTools.Data.Models/UDF/PartitionMap.cs @@ -0,0 +1,19 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// Abstract UDF Partition Map (3/10.7) + /// + /// + public abstract class PartitionMap + { + /// + /// Partition map type (1 or 2) + /// + public byte PartitionMapType { get; set; } + + /// + /// Length of the partition map in bytes + /// + public byte PartitionMapLength { get; set; } + } +} diff --git a/SabreTools.Data.Models/UDF/PrimaryVolumeDescriptor.cs b/SabreTools.Data.Models/UDF/PrimaryVolumeDescriptor.cs new file mode 100644 index 00000000..668d0ec3 --- /dev/null +++ b/SabreTools.Data.Models/UDF/PrimaryVolumeDescriptor.cs @@ -0,0 +1,114 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Primary Volume Descriptor (3/10.1) + /// + /// + public sealed class PrimaryVolumeDescriptor : Descriptor + { + /// + /// Volume Descriptor Sequence Number, the highest number prevails + /// + public uint VolumeDescriptorSequenceNumber { get; set; } + + /// + /// Primary Volume Descriptor Number + /// + public uint PrimaryVolumeDescriptorNumber { get; set; } + + /// + /// Volume identifier, dstring (last byte = length) + /// + public byte[] VolumeIdentifier { get; set; } = new byte[32]; + + /// + /// Volume sequence number + /// + public ushort VolumeSequenceNumber { get; set; } + + /// + /// Maximum volume sequence number + /// + public ushort MaximumVolumeSequenceNumber { get; set; } + + /// + /// Interchange level + /// + public ushort InterchangeLevel { get; set; } + + /// + /// Maximum interchange level + /// + public ushort MaximumInterchangeLevel { get; set; } + + /// + /// Character set list + /// + public uint CharacterSetList { get; set; } + + /// + /// Maximum character set list + /// + public uint MaximumCharacterSetList { get; set; } + + /// + /// Volume set identifier, dstring (last byte = length) + /// + public byte[] VolumeSetIdentifier { get; set; } = new byte[128]; + + /// + /// Descriptor character set + /// + public CharacterSetSpecification DescriptorCharacterSet { get; set; } = new(); + + /// + /// Explanatory character set + /// + public CharacterSetSpecification ExplanatoryCharacterSet { get; set; } = new(); + + /// + /// Extent of the volume abstract + /// + public ExtentDescriptor VolumeAbstract { get; set; } = new(); + + /// + /// Extent of the volume copyright notice + /// + public ExtentDescriptor VolumeCopyrightNotice { get; set; } = new(); + + /// + /// Application identifier + /// + public EntityIdentifier ApplicationIdentifier { get; set; } = new(); + + /// + /// Recording date and time + /// + public Timestamp RecordingDateAndTime { get; set; } = new(); + + /// + /// Implementation identifier + /// + public EntityIdentifier ImplementationIdentifier { get; set; } = new(); + + /// + /// Implementation use, 64 bytes + /// + public byte[] ImplementationUse { get; set; } = new byte[64]; + + /// + /// Predecessor Volume Descriptor Sequence location + /// + public uint PredecessorVolumeDescriptorSequenceLocation { get; set; } + + /// + /// Flags (bit 0: volume set identification is common to all volumes of the set) + /// + public ushort Flags { get; set; } + + /// + /// Reserved, 22 bytes of 0x00 + /// + public byte[] Reserved { get; set; } = new byte[22]; + } +} diff --git a/SabreTools.Data.Models/UDF/ShortAllocationDescriptor.cs b/SabreTools.Data.Models/UDF/ShortAllocationDescriptor.cs new file mode 100644 index 00000000..c8778bcf --- /dev/null +++ b/SabreTools.Data.Models/UDF/ShortAllocationDescriptor.cs @@ -0,0 +1,20 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Short Allocation Descriptor (short_ad, 4/14.14.1), an extent within the partition of the descriptor + /// + /// + public sealed class ShortAllocationDescriptor + { + /// + /// Length of the extent in bytes (lower 30 bits) and the ExtentType (upper 2 bits) + /// A length of 0 terminates a sequence of allocation descriptors + /// + public uint ExtentLength { get; set; } + + /// + /// Logical block number of the start of the extent + /// + public uint ExtentPosition { get; set; } + } +} diff --git a/SabreTools.Data.Models/UDF/Timestamp.cs b/SabreTools.Data.Models/UDF/Timestamp.cs new file mode 100644 index 00000000..82621b7c --- /dev/null +++ b/SabreTools.Data.Models/UDF/Timestamp.cs @@ -0,0 +1,60 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Timestamp (1/7.3) + /// + /// + public sealed class Timestamp + { + /// + /// Type (upper 4 bits, 1 = local time) and time zone offset from UTC in minutes + /// (lower 12 bits, signed, -2047 = not specified) + /// + public ushort TypeAndTimezone { get; set; } + + /// + /// Year, 1 to 9999 + /// + public short Year { get; set; } + + /// + /// Month, 1 to 12 + /// + public byte Month { get; set; } + + /// + /// Day, 1 to 31 + /// + public byte Day { get; set; } + + /// + /// Hour, 0 to 23 + /// + public byte Hour { get; set; } + + /// + /// Minute, 0 to 59 + /// + public byte Minute { get; set; } + + /// + /// Second, 0 to 59 + /// + public byte Second { get; set; } + + /// + /// Centiseconds, 0 to 99 + /// + public byte Centiseconds { get; set; } + + /// + /// Hundreds of microseconds, 0 to 99 + /// + public byte HundredsOfMicroseconds { get; set; } + + /// + /// Microseconds, 0 to 99 + /// + public byte Microseconds { get; set; } + } +} diff --git a/SabreTools.Data.Models/UDF/Type1PartitionMap.cs b/SabreTools.Data.Models/UDF/Type1PartitionMap.cs new file mode 100644 index 00000000..0c0609e3 --- /dev/null +++ b/SabreTools.Data.Models/UDF/Type1PartitionMap.cs @@ -0,0 +1,19 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Type 1 Partition Map (3/10.7.2), logical blocks map 1:1 onto a Partition Descriptor's sectors + /// + /// + public sealed class Type1PartitionMap : PartitionMap + { + /// + /// Volume sequence number of the volume containing the partition + /// + public ushort VolumeSequenceNumber { get; set; } + + /// + /// Partition number of the Partition Descriptor + /// + public ushort PartitionNumber { get; set; } + } +} diff --git a/SabreTools.Data.Models/UDF/Type2PartitionMap.cs b/SabreTools.Data.Models/UDF/Type2PartitionMap.cs new file mode 100644 index 00000000..0c28e27b --- /dev/null +++ b/SabreTools.Data.Models/UDF/Type2PartitionMap.cs @@ -0,0 +1,37 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Type 2 Partition Map (3/10.7.3, UDF 2.2.8-2.2.10), a partition identified by its partition type identifier + /// (e.g. "*UDF Virtual Partition", "*UDF Sparable Partition", "*UDF Metadata Partition") + /// + /// + /// + public class Type2PartitionMap : PartitionMap + { + /// + /// Reserved, 2 bytes of 0x00 + /// + public byte[] Reserved { get; set; } = new byte[2]; + + /// + /// Partition type identifier + /// + public EntityIdentifier PartitionTypeIdentifier { get; set; } = new(); + + /// + /// Volume sequence number of the volume containing the partition + /// + public ushort VolumeSequenceNumber { get; set; } + + /// + /// Partition number of the underlying Partition Descriptor + /// + public ushort PartitionNumber { get; set; } + + /// + /// Partition type specific data after the partition number, 24 bytes + /// Empty for partition types that are parsed further (MetadataPartitionMap) + /// + public byte[] PartitionTypeData { get; set; } = []; + } +} diff --git a/SabreTools.Data.Models/UDF/Volume.cs b/SabreTools.Data.Models/UDF/Volume.cs new file mode 100644 index 00000000..78de031e --- /dev/null +++ b/SabreTools.Data.Models/UDF/Volume.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace SabreTools.Data.Models.UDF +{ + /// + /// A volume (or disc image) with a UDF / ECMA-167 file system + /// Logical sectors and logical blocks are 2048 bytes on optical media + /// File system structures are addressed by logical block address (partition reference number + logical block); + /// dictionaries are keyed by ((long)PartitionReferenceNumber << 32) | LogicalBlockNumber + /// + /// + /// + public sealed class Volume + { + /// + /// Volume Recognition Sequence starting at sector 16 + /// + public VolumeStructureDescriptor[] VolumeRecognitionSequence { get; set; } = []; + + /// + /// Anchor Volume Descriptor Pointer + /// + public AnchorVolumeDescriptorPointer? AnchorVolumeDescriptorPointer { get; set; } + + /// + /// Volume Descriptor Sequence (Primary, Partition, Logical Volume, ... up to the Terminating Descriptor) + /// + public Descriptor[] VolumeDescriptorSequence { get; set; } = []; + + /// + /// File Set Descriptor + /// + public FileSetDescriptor? FileSetDescriptor { get; set; } + + /// + /// File Entries (and Extended File Entries) by logical block address + /// + public Dictionary FileEntries { get; set; } = []; + + /// + /// Allocation Extent Descriptors by logical block address + /// + public Dictionary AllocationExtentDescriptors { get; set; } = []; + + /// + /// File Identifier Descriptors of each directory, by logical block address of the directory's File Entry + /// + public Dictionary Directories { get; set; } = []; + } +} diff --git a/SabreTools.Data.Models/UDF/VolumeDescriptorPointer.cs b/SabreTools.Data.Models/UDF/VolumeDescriptorPointer.cs new file mode 100644 index 00000000..bfe10afa --- /dev/null +++ b/SabreTools.Data.Models/UDF/VolumeDescriptorPointer.cs @@ -0,0 +1,24 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Volume Descriptor Pointer (3/10.3), continues the Volume Descriptor Sequence in another extent + /// + /// + public sealed class VolumeDescriptorPointer : Descriptor + { + /// + /// Volume Descriptor Sequence Number + /// + public uint VolumeDescriptorSequenceNumber { get; set; } + + /// + /// Extent of the next part of the Volume Descriptor Sequence + /// + public ExtentDescriptor NextVolumeDescriptorSequenceExtent { get; set; } = new(); + + /// + /// Reserved, 484 bytes of 0x00 + /// + public byte[] Reserved { get; set; } = new byte[484]; + } +} diff --git a/SabreTools.Data.Models/UDF/VolumeStructureDescriptor.cs b/SabreTools.Data.Models/UDF/VolumeStructureDescriptor.cs new file mode 100644 index 00000000..acac9308 --- /dev/null +++ b/SabreTools.Data.Models/UDF/VolumeStructureDescriptor.cs @@ -0,0 +1,30 @@ +namespace SabreTools.Data.Models.UDF +{ + /// + /// UDF Volume Structure Descriptor (2/9.1), one logical sector of the Volume Recognition Sequence + /// starting at sector 16, e.g. "BEA01", "NSR02"/"NSR03", "TEA01", or "CD001" on bridge discs + /// + /// + public sealed class VolumeStructureDescriptor + { + /// + /// Structure type + /// + public byte StructureType { get; set; } + + /// + /// Standard identifier, 5 bytes + /// + public byte[] StandardIdentifier { get; set; } = new byte[5]; + + /// + /// Structure version + /// + public byte StructureVersion { get; set; } + + /// + /// Structure data, the rest of the logical sector + /// + public byte[] StructureData { get; set; } = []; + } +} diff --git a/SabreTools.Serialization.Readers.Test/UDFImageBuilder.cs b/SabreTools.Serialization.Readers.Test/UDFImageBuilder.cs new file mode 100644 index 00000000..c5460fff --- /dev/null +++ b/SabreTools.Serialization.Readers.Test/UDFImageBuilder.cs @@ -0,0 +1,294 @@ +using System; +using System.Text; + +namespace SabreTools.Serialization.Readers.Test +{ + /// + /// Builds a minimal UDF 1.02 image in memory (2048-byte sectors, one Type 1 partition at sector 260) + /// + /// + /// Partition blocks: + /// 0 File Set Descriptor + /// 2 root directory File Entry, contents in block 3: + /// parent, "README.TXT" (embedded data), "deleted.txt" (deleted), "Sub" (directory), + /// "Media" (last entry, no padding) + /// 4 "README.TXT" File Entry, data "hello world" embedded + /// 5 "Sub" File Entry, contents in block 6: parent, "data.bin" (16-bit name) + /// 7 "data.bin" File Entry, long_ad to block 8 (2048 bytes), then an Allocation Extent Descriptor + /// in block 10 with a long_ad to block 9 (952 bytes) + /// 8-9 "data.bin" data (byte i = i % 251) + /// 11 "Media" File Entry, one short_ad of 100 bytes allocated but not recorded + /// + internal static class UDFImageBuilder + { + public const int SectorSize = 2048; + public const int PartitionStart = 260; + public const int PartitionLength = 40; + public const int Sectors = 300; + public const int DataBinLength = 3000; + + /// + /// Build the image + /// + /// True to break the checksum of the root directory File Entry + /// True to change a byte of the root directory File Entry that only the CRC covers + /// True to record the partition map with length 0 and a map count of 3 + /// True to replace the Terminating Descriptor by a Volume Descriptor Pointer back to the start of the sequence + /// Name of the "Sub" directory in the root directory + /// True to add a directory "loop" to "Sub" that points back to the root directory + /// True to record "Media" in a partition block past the end of the image + public static byte[] Build(bool corruptRootEntry = false, + bool corruptRootEntryCrc = false, + bool emptyPartitionMap = false, + bool pointerLoop = false, + string subName = "Sub", + bool subLoop = false, + bool mediaPastEnd = false) + { + var image = new byte[Sectors * SectorSize]; + + // Volume Recognition Sequence + WriteStructure(image, 16, "BEA01"); + WriteStructure(image, 17, "NSR02"); + WriteStructure(image, 18, "TEA01"); + + // Anchor Volume Descriptor Pointer: main and reserve sequence at sector 32, 4 sectors + int avdp = 256 * SectorSize; + WriteUInt32(image, avdp + 16, 4 * SectorSize); + WriteUInt32(image, avdp + 20, 32); + WriteUInt32(image, avdp + 24, 4 * SectorSize); + WriteUInt32(image, avdp + 28, 32); + WriteTag(image, avdp, 2, 256, 512); + + // Primary Volume Descriptor + int pvd = 32 * SectorSize; + WriteDString(image, pvd + 24, 32, "TESTVOLUME"); + WriteTag(image, pvd, 1, 32, 512); + + // Partition Descriptor: partition 0 at sector 260 + int pd = 33 * SectorSize; + WriteUInt32(image, pd + 16, 1); + image[pd + 22] = 0; + WriteUInt32(image, pd + 188, PartitionStart); + WriteUInt32(image, pd + 192, mediaPastEnd ? PartitionLength + 20 : PartitionLength); + WriteTag(image, pd, 5, 33, 512); + + // Logical Volume Descriptor: block size, domain, File Set Descriptor at block 0, one Type 1 map + int lvd = 34 * SectorSize; + WriteUInt32(image, lvd + 16, 2); + WriteDString(image, lvd + 84, 128, "TESTVOLUME"); + WriteUInt32(image, lvd + 212, SectorSize); + WriteAscii(image, lvd + 217, "*OSTA UDF Compliant"); + image[lvd + 240] = 0x02; + image[lvd + 241] = 0x01; + WriteLongAd(image, lvd + 248, SectorSize, 0); + WriteUInt32(image, lvd + 264, 6); + WriteUInt32(image, lvd + 268, emptyPartitionMap ? 3 : 1); + image[lvd + 440] = 1; + image[lvd + 441] = (byte)(emptyPartitionMap ? 0 : 6); + image[lvd + 442] = 1; + WriteTag(image, lvd, 6, 34, 446); + + // Terminating Descriptor, or a Volume Descriptor Pointer back to the start of the sequence + int td = 35 * SectorSize; + if (pointerLoop) + { + WriteUInt32(image, td + 20, 4 * SectorSize); + WriteUInt32(image, td + 24, 32); + WriteTag(image, td, 3, 35, 512); + } + else + { + WriteTag(image, td, 8, 35, 512); + } + + // File Set Descriptor: root directory ICB at block 2 + int fsd = Block(0); + WriteDString(image, fsd + 304, 32, "TESTSET"); + WriteLongAd(image, fsd + 400, SectorSize, 2); + WriteTag(image, fsd, 256, 0, 512); + + // Root directory contents + int root = Block(3); + int length = 0; + length += WriteFid(image, root + length, 3, 0x0A, 2, []); + length += WriteFid(image, root + length, 3, 0x00, 4, Name8("README.TXT")); + length += WriteFid(image, root + length, 3, 0x04, 4, Name8("deleted.txt")); + length += WriteFid(image, root + length, 3, 0x02, 5, Name8(subName)); + length += WriteFid(image, root + length, 3, 0x00, 11, Name8("Media")); + WriteFileEntry(image, 2, 4, 0, length, ShortAd(length, 3)); + if (corruptRootEntry) + image[Block(2) + 4] ^= 0xFF; + if (corruptRootEntryCrc) + image[Block(2) + 36] ^= 0xFF; + + // "README.TXT", embedded + WriteFileEntry(image, 4, 5, 3, 11, Encoding.ASCII.GetBytes("hello world")); + + // "Sub" contents + int sub = Block(6); + length = 0; + length += WriteFid(image, sub + length, 6, 0x0A, 2, []); + length += WriteFid(image, sub + length, 6, 0x00, 7, Name16("data.bin")); + if (subLoop) + length += WriteFid(image, sub + length, 6, 0x02, 2, Name8("loop")); + + WriteFileEntry(image, 5, 4, 0, length, ShortAd(length, 6)); + + // "data.bin", long allocation descriptors continued in an Allocation Extent Descriptor + var ads = new byte[32]; + WriteLongAd(ads, 0, SectorSize, 8); + WriteLongAd(ads, 16, (3u << 30) | SectorSize, 10); + WriteFileEntry(image, 7, 5, 1, DataBinLength, ads); + for (int i = 0; i < DataBinLength; i++) + { + image[Block(8) + i] = (byte)(i % 251); + } + + int aed = Block(10); + WriteUInt32(image, aed + 20, 16); + WriteLongAd(image, aed + 24, DataBinLength - SectorSize, 9); + WriteTag(image, aed, 258, 10, 40); + + // "Media", allocated but not recorded, or recorded past the end of the image + if (mediaPastEnd) + WriteFileEntry(image, 11, 5, 0, 100, ShortAd(100, PartitionLength + 10)); + else + WriteFileEntry(image, 11, 5, 0, 100, ShortAd((1u << 30) | 100, 0)); + + return image; + } + + /// + /// Offset of a partition block in the image + /// + public static int Block(int block) => (PartitionStart + block) * SectorSize; + + private static void WriteStructure(byte[] image, int sector, string identifier) + { + int offset = sector * SectorSize; + image[offset] = 0; + WriteAscii(image, offset + 1, identifier); + image[offset + 6] = 1; + } + + private static int WriteFid(byte[] image, int offset, uint block, byte characteristics, uint icb, byte[] name) + { + image[offset + 16] = 1; + image[offset + 18] = characteristics; + image[offset + 19] = (byte)name.Length; + WriteLongAd(image, offset + 20, SectorSize, icb); + Array.Copy(name, 0, image, offset + 38, name.Length); + int length = (38 + name.Length + 3) & ~3; + WriteTag(image, offset, 257, block, length); + return length; + } + + private static void WriteFileEntry(byte[] image, int block, byte fileType, ushort adType, long informationLength, byte[] ads) + { + int offset = Block(block); + image[offset + 16 + 4] = 4; + image[offset + 16 + 11] = fileType; + image[offset + 16 + 18] = (byte)adType; + WriteUInt32(image, offset + 56, (uint)informationLength); + WriteUInt32(image, offset + 172, (uint)ads.Length); + Array.Copy(ads, 0, image, offset + 176, ads.Length); + WriteTag(image, offset, 261, (uint)block, 176 + ads.Length); + } + + private static byte[] ShortAd(uint length, uint position) + { + var ad = new byte[8]; + WriteUInt32(ad, 0, length); + WriteUInt32(ad, 4, position); + return ad; + } + + private static byte[] ShortAd(int length, uint position) => ShortAd((uint)length, position); + + private static void WriteLongAd(byte[] data, int offset, uint length, uint block) + { + WriteUInt32(data, offset, length); + WriteUInt32(data, offset + 4, block); + } + + private static void WriteLongAd(byte[] data, int offset, int length, uint block) + => WriteLongAd(data, offset, (uint)length, block); + + private static byte[] Name8(string name) + { + var bytes = new byte[name.Length + 1]; + bytes[0] = 8; + Encoding.ASCII.GetBytes(name, 0, name.Length, bytes, 1); + return bytes; + } + + private static byte[] Name16(string name) + { + var bytes = new byte[(name.Length * 2) + 1]; + bytes[0] = 16; + Encoding.BigEndianUnicode.GetBytes(name, 0, name.Length, bytes, 1); + return bytes; + } + + private static void WriteDString(byte[] image, int offset, int fieldLength, string value) + { + image[offset] = 8; + WriteAscii(image, offset + 1, value); + image[offset + fieldLength - 1] = (byte)(value.Length + 1); + } + + private static void WriteAscii(byte[] image, int offset, string value) + => Encoding.ASCII.GetBytes(value, 0, value.Length, image, offset); + + private static void WriteUInt32(byte[] data, int offset, uint value) + { + data[offset] = (byte)value; + data[offset + 1] = (byte)(value >> 8); + data[offset + 2] = (byte)(value >> 16); + data[offset + 3] = (byte)(value >> 24); + } + + private static void WriteUInt32(byte[] data, int offset, int value) => WriteUInt32(data, offset, (uint)value); + + /// + /// Write a descriptor tag covering the given descriptor length (version 2, CRC, checksum) + /// + private static void WriteTag(byte[] image, int offset, ushort identifier, uint location, int length) + { + image[offset] = (byte)identifier; + image[offset + 1] = (byte)(identifier >> 8); + image[offset + 2] = 2; + ushort crc = Crc(image, offset + 16, length - 16); + image[offset + 8] = (byte)crc; + image[offset + 9] = (byte)(crc >> 8); + image[offset + 10] = (byte)(length - 16); + image[offset + 11] = (byte)((length - 16) >> 8); + WriteUInt32(image, offset + 12, location); + + byte checksum = 0; + for (int i = 0; i < 16; i++) + { + if (i != 4) + checksum += image[offset + i]; + } + + image[offset + 4] = checksum; + } + + private static ushort Crc(byte[] data, int offset, int length) + { + ushort crc = 0; + for (int i = offset; i < offset + length; i++) + { + crc ^= (ushort)(data[i] << 8); + for (int bit = 0; bit < 8; bit++) + { + crc = (crc & 0x8000) != 0 ? (ushort)((crc << 1) ^ 0x1021) : (ushort)(crc << 1); + } + } + + return crc; + } + } +} diff --git a/SabreTools.Serialization.Readers.Test/UDFTests.cs b/SabreTools.Serialization.Readers.Test/UDFTests.cs new file mode 100644 index 00000000..ba07b5e5 --- /dev/null +++ b/SabreTools.Serialization.Readers.Test/UDFTests.cs @@ -0,0 +1,289 @@ +using System.IO; +using System.Linq; +using System.Text; +using SabreTools.Data.Extensions; +using SabreTools.Data.Models.UDF; +using Xunit; + +namespace SabreTools.Serialization.Readers.Test +{ + public class UDFTests + { + [Fact] + public void NullArray_Null() + { + byte[]? data = null; + int offset = 0; + var deserializer = new UDF(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void EmptyArray_Null() + { + byte[]? data = []; + int offset = 0; + var deserializer = new UDF(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void InvalidArray_Null() + { + byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)]; + int offset = 0; + var deserializer = new UDF(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void NullStream_Null() + { + Stream? data = null; + var deserializer = new UDF(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void EmptyStream_Null() + { + Stream? data = new MemoryStream([]); + var deserializer = new UDF(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void InvalidStream_Null() + { + Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]); + var deserializer = new UDF(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void ValidImage_VolumeStructures() + { + var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build())); + + Assert.NotNull(volume); + Assert.Equal(3, volume.VolumeRecognitionSequence.Length); + Assert.NotNull(volume.AnchorVolumeDescriptorPointer); + Assert.Equal(4, volume.VolumeDescriptorSequence.Length); + var terminator = Assert.IsType(volume.VolumeDescriptorSequence[3]); + Assert.Equal(TagIdentifier.TERMINATING_DESCRIPTOR, terminator.DescriptorTag.TagIdentifier); + + var lvd = volume.GetLogicalVolumeDescriptor(); + Assert.NotNull(lvd); + Assert.Equal("TESTVOLUME", lvd.LogicalVolumeIdentifier.DecodeDString()); + Assert.IsType(Assert.Single(lvd.PartitionMaps)); + + Assert.NotNull(volume.FileSetDescriptor); + Assert.Equal("TESTSET", volume.FileSetDescriptor.FileSetIdentifier.DecodeDString()); + Assert.Equal(2u, volume.FileSetDescriptor.RootDirectoryICB.ExtentLocation.LogicalBlockNumber); + } + + [Fact] + public void ValidImage_DirectoryHierarchy() + { + var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build())); + + Assert.NotNull(volume); + + // Root, "README.TXT", "Sub", "data.bin", "Media"; the deleted entry is not followed + Assert.Equal(5, volume.FileEntries.Count); + Assert.Equal(2, volume.Directories.Count); + Assert.Single(volume.AllocationExtentDescriptors); + + // The last root entry has no padding and ends exactly at the end of the directory + var root = volume.Directories[UDFExtensions.GetKey(0, 2)]; + Assert.Equal(5, root.Length); + Assert.Equal("Media", root[4].FileIdentifier.DecodeDCharacters()); + Assert.Empty(root[4].Padding); + + var sub = volume.Directories[UDFExtensions.GetKey(0, 5)]; + Assert.Equal("data.bin", sub[1].FileIdentifier.DecodeDCharacters()); + } + + [Fact] + public void FindFileEntry_EmbeddedData() + { + var data = new MemoryStream(UDFImageBuilder.Build()); + var volume = new UDF().Deserialize(data); + Assert.NotNull(volume); + + var entry = UDF.FindFileEntry(data, volume, "README.TXT", out long key); + + Assert.NotNull(entry); + Assert.Equal(UDFExtensions.GetKey(0, 4), key); + Assert.Equal(AllocationDescriptorType.EMBEDDED, entry.ICBTag.GetAllocationDescriptorType()); + Assert.Equal("hello world", Encoding.ASCII.GetString(entry.AllocationDescriptors, 0, (int)entry.InformationLength)); + } + + [Fact] + public void FindFileEntry_CaseInsensitive() + { + var data = new MemoryStream(UDFImageBuilder.Build()); + var volume = new UDF().Deserialize(data); + Assert.NotNull(volume); + + var entry = UDF.FindFileEntry(data, volume, "/sub\\DATA.BIN", out long key); + + Assert.NotNull(entry); + Assert.Equal(UDFExtensions.GetKey(0, 7), key); + } + + [Fact] + public void FindFileEntry_DeletedOrMissing_Null() + { + var data = new MemoryStream(UDFImageBuilder.Build()); + var volume = new UDF().Deserialize(data); + Assert.NotNull(volume); + + Assert.Null(UDF.FindFileEntry(data, volume, "deleted.txt", out long deletedKey)); + Assert.Equal(-1, deletedKey); + Assert.Null(UDF.FindFileEntry(data, volume, "Sub/missing.bin", out _)); + Assert.Null(UDF.FindFileEntry(data, volume, "README.TXT/data.bin", out _)); + } + + [Fact] + public void FindFileEntry_WithoutDirectories_ReadsOnlyThePath() + { + var data = new MemoryStream(UDFImageBuilder.Build()); + var volume = new UDF().Deserialize(data, includeDirectories: false); + Assert.NotNull(volume); + Assert.Empty(volume.FileEntries); + Assert.Empty(volume.Directories); + + var entry = UDF.FindFileEntry(data, volume, "Sub/data.bin", out long key); + + Assert.NotNull(entry); + Assert.Equal(UDFExtensions.GetKey(0, 7), key); + + // Root, "Sub" and "data.bin" only + Assert.Equal(3, volume.FileEntries.Count); + Assert.Equal(2, volume.Directories.Count); + Assert.False(volume.FileEntries.ContainsKey(UDFExtensions.GetKey(0, 4))); + } + + [Fact] + public void AllocationExtent_Followed() + { + byte[] image = UDFImageBuilder.Build(); + var data = new MemoryStream(image); + var volume = new UDF().Deserialize(data); + Assert.NotNull(volume); + + long key = UDFExtensions.GetKey(0, 7); + var descriptors = volume.GetAllocationDescriptors(key, volume.FileEntries[key]); + + Assert.NotNull(descriptors); + Assert.Equal(2, descriptors.Count); + Assert.Equal(8u, descriptors[0].ExtentLocation.LogicalBlockNumber); + Assert.Equal(9u, descriptors[1].ExtentLocation.LogicalBlockNumber); + Assert.Equal((uint)UDFImageBuilder.DataBinLength, UDFExtensions.GetExtentLength(descriptors[0].ExtentLength) + UDFExtensions.GetExtentLength(descriptors[1].ExtentLength)); + + Assert.True(volume.TryGetSector(descriptors[1].ExtentLocation.GetKey(), out long sector)); + Assert.Equal(UDFImageBuilder.PartitionStart + 9, sector); + } + + [Fact] + public void NotRecordedExtent_Kept() + { + var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build())); + Assert.NotNull(volume); + + long key = UDFExtensions.GetKey(0, 11); + var descriptors = volume.GetAllocationDescriptors(key, volume.FileEntries[key]); + + Assert.NotNull(descriptors); + var ad = Assert.Single(descriptors); + Assert.Equal(ExtentType.NOT_RECORDED_BUT_ALLOCATED, UDFExtensions.GetExtentType(ad.ExtentLength)); + Assert.Equal(100u, UDFExtensions.GetExtentLength(ad.ExtentLength)); + } + + [Fact] + public void CorruptRootEntry_NoDirectories() + { + var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build(corruptRootEntry: true))); + + Assert.NotNull(volume); + Assert.NotNull(volume.FileSetDescriptor); + Assert.Empty(volume.FileEntries); + Assert.Empty(volume.Directories); + } + + [Fact] + public void CorruptRootEntryCrc_NoDirectories() + { + var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build(corruptRootEntryCrc: true))); + + Assert.NotNull(volume); + Assert.NotNull(volume.FileSetDescriptor); + Assert.Empty(volume.FileEntries); + Assert.Empty(volume.Directories); + } + + [Fact] + public void EmptyPartitionMap_EndsMapTable() + { + var data = new MemoryStream(UDFImageBuilder.Build(emptyPartitionMap: true)); + var anchor = UDF.ParseAnchorVolumeDescriptorPointer(data, Constants.AnchorVolumeDescriptorPointerSector); + Assert.NotNull(anchor); + + var sequence = UDF.ParseVolumeDescriptorSequence(data, anchor.MainVolumeDescriptorSequenceExtent); + + var lvd = Assert.Single(sequence.OfType()); + Assert.Empty(lvd.PartitionMaps); + } + + [Fact] + public void VolumeDescriptorPointerLoop_Stops() + { + var volume = new UDF().Deserialize(new MemoryStream(UDFImageBuilder.Build(pointerLoop: true))); + + Assert.NotNull(volume); + Assert.Equal(5, volume.FileEntries.Count); + } + + [Fact] + public void UnreadableSector_OtherEntriesRead() + { + // The "Sub" File Entry (block 5) cannot be read; "README.TXT" (block 4) is read in the same request + byte[] image = UDFImageBuilder.Build(); + long bad = UDFImageBuilder.Block(5); + var volume = new UDF().Deserialize(new ThrowingStream(image, bad, bad + UDFImageBuilder.SectorSize)); + + // Root, "README.TXT" and "Media" + long[] expected = [UDFExtensions.GetKey(0, 2), UDFExtensions.GetKey(0, 4), UDFExtensions.GetKey(0, 11)]; + Assert.NotNull(volume); + Assert.Equal(expected, volume.FileEntries.Keys.OrderBy(k => k)); + Assert.Single(volume.Directories); + } + + /// + /// Stream that throws an IOException for reads touching a range, like a drive with an unreadable sector + /// + private sealed class ThrowingStream(byte[] data, long badStart, long badEnd) : MemoryStream(data) + { + public override int Read(byte[] buffer, int offset, int count) + { + if (Position < badEnd && Position + count > badStart) + throw new IOException("Unreadable sector"); + + return base.Read(buffer, offset, count); + } + } + } +} diff --git a/SabreTools.Serialization.Readers/UDF.cs b/SabreTools.Serialization.Readers/UDF.cs new file mode 100644 index 00000000..0693579f --- /dev/null +++ b/SabreTools.Serialization.Readers/UDF.cs @@ -0,0 +1,1365 @@ +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; + +#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; + + /// + /// CRC-ITU-T (CCITT, polynomial 0x1021) table for descriptor CRCs + /// + private static readonly ushort[] CrcTable = CreateCrcTable(); + + /// + 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) + { + var obj = new List(); + + long sector = Constants.VolumeRecognitionSequenceSector; + while (true) + { + byte[] block = ReadSectors(data, sector, 1); + + int offset = 0; + var vsd = new VolumeStructureDescriptor(); + vsd.StructureType = block.ReadByteValue(ref offset); + vsd.StandardIdentifier = block.ReadBytes(ref offset, 5); + vsd.StructureVersion = block.ReadByteValue(ref offset); + vsd.StructureData = block.ReadBytes(ref offset, Constants.SectorSize - 7); + + 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 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 (!IsValidDescriptor(block, 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) + { + var obj = new List(); + + long sector = extent.ExtentLocation; + long end = sector + (extent.ExtentLength / Constants.SectorSize); + int pointers = 0; + while (sector < end) + { + byte[] block = ReadSectors(data, sector, 1); + if (!IsValidDescriptor(block, 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 + var maps = new List(); + 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; + + string? name = DecodeIdentifier(identifier.Identifier); + 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 (!IsValidDescriptor(block, 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 (!IsValidDescriptor(block, 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 (!IsValidDescriptor(block, 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) + { + var obj = new List(); + + int offset = 0; + while (offset < length) + { + var tag = ParseDescriptorTag(data, ref offset); + if (tag.TagIdentifier != TagIdentifier.FILE_IDENTIFIER_DESCRIPTOR) + return null; + + var fid = new FileIdentifierDescriptor(); + fid.DescriptorTag = tag; + fid.FileVersionNumber = data.ReadUInt16LittleEndian(ref offset); + fid.FileCharacteristics = (FileCharacteristics)data.ReadByteValue(ref offset); + fid.LengthOfFileIdentifier = data.ReadByteValue(ref offset); + fid.ICB = ParseLongAllocationDescriptor(data, ref offset); + fid.LengthOfImplementationUse = data.ReadUInt16LittleEndian(ref offset); + + // The descriptor is padded to 4 bytes + int used = 38 + fid.LengthOfImplementationUse + fid.LengthOfFileIdentifier; + int padded = (used + 3) & ~3; + fid.ImplementationUse = data.ReadBytes(ref offset, fid.LengthOfImplementationUse); + fid.FileIdentifier = data.ReadBytes(ref offset, fid.LengthOfFileIdentifier); + fid.Padding = data.ReadBytes(ref offset, padded - used); + + obj.Add(fid); + } + + 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 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(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; + + int version = data[offset + 2] | (data[offset + 3] << 8); + if (version != 2 && version != 3) + return false; + + int crcLength = data[offset + 10] | (data[offset + 11] << 8); + if (16 + crcLength <= available) + { + int crc = data[offset + 8] | (data[offset + 9] << 8); + if (ComputeCrc(data, offset + 16, crcLength) != crc) + return false; + } + + uint tagLocation = (uint)(data[offset + 12] | (data[offset + 13] << 8) | (data[offset + 14] << 16) | (data[offset + 15] << 24)); + return tagLocation == location; + } + + /// + /// 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; + } + + /// + /// 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); + } + + /// + /// Decode an Entity Identifier's identifier field (ASCII, padded with 0x00) + /// + private static string? DecodeIdentifier(byte[] identifier) + { + int length = Array.IndexOf(identifier, (byte)0); + if (length < 0) + length = identifier.Length; + + var chars = new char[length]; + for (int i = 0; i < length; i++) + { + chars[i] = (char)identifier[i]; + } + + return new string(chars); + } + + /// + /// 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] }; + var reads = new List(); + 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 + } +} diff --git a/SabreTools.Wrappers.Test/SabreTools.Wrappers.Test.csproj b/SabreTools.Wrappers.Test/SabreTools.Wrappers.Test.csproj index 5051ac02..be7825a4 100644 --- a/SabreTools.Wrappers.Test/SabreTools.Wrappers.Test.csproj +++ b/SabreTools.Wrappers.Test/SabreTools.Wrappers.Test.csproj @@ -39,6 +39,10 @@ + + + + diff --git a/SabreTools.Wrappers.Test/UDFTests.cs b/SabreTools.Wrappers.Test/UDFTests.cs new file mode 100644 index 00000000..91b7c1f6 --- /dev/null +++ b/SabreTools.Wrappers.Test/UDFTests.cs @@ -0,0 +1,175 @@ +using System.IO; +using System.Linq; +using System.Text; +using SabreTools.Serialization.Readers.Test; +using Xunit; + +namespace SabreTools.Wrappers.Test +{ + public class UDFTests + { + [Fact] + public void NullArray_Null() + { + byte[]? data = null; + int offset = 0; + var actual = UDF.Create(data, offset); + Assert.Null(actual); + } + + [Fact] + public void EmptyArray_Null() + { + byte[]? data = []; + int offset = 0; + var actual = UDF.Create(data, offset); + Assert.Null(actual); + } + + [Fact] + public void InvalidArray_Null() + { + byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)]; + int offset = 0; + var actual = UDF.Create(data, offset); + Assert.Null(actual); + } + + [Fact] + public void NullStream_Null() + { + Stream? data = null; + var actual = UDF.Create(data); + Assert.Null(actual); + } + + [Fact] + public void EmptyStream_Null() + { + Stream? data = new MemoryStream([]); + var actual = UDF.Create(data); + Assert.Null(actual); + } + + [Fact] + public void InvalidStream_Null() + { + Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]); + var actual = UDF.Create(data); + Assert.Null(actual); + } + + [Fact] + public void DirectoryLoop_PrintInformation() + { + var wrapper = UDF.Create(new MemoryStream(UDFImageBuilder.Build(subLoop: true))); + Assert.NotNull(wrapper); + + var builder = new StringBuilder(); + wrapper.PrintInformation(builder, true); + + Assert.Contains("\\Sub\\loop\\", builder.ToString()); + } + + [Fact] + public void DirectoryLoop_Extract() + { + var wrapper = UDF.Create(new MemoryStream(UDFImageBuilder.Build(subLoop: true))); + Assert.NotNull(wrapper); + + string output = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + try + { + Directory.CreateDirectory(output); + Assert.True(wrapper.Extract(output, false)); + Assert.Equal(UDFImageBuilder.DataBinLength, new FileInfo(Path.Combine(output, "Sub", "data.bin")).Length); + Assert.True(Directory.Exists(Path.Combine(output, "Sub", "loop"))); + } + finally + { + if (Directory.Exists(output)) + Directory.Delete(output, true); + } + } + + [Fact] + public void ParentDirectoryName_NotExtracted() + { + // "Sub" is named "..", so "data.bin" would be written next to the output directory + var wrapper = UDF.Create(new MemoryStream(UDFImageBuilder.Build(subName: ".."))); + Assert.NotNull(wrapper); + + string parent = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + string output = Path.Combine(parent, "output"); + try + { + Directory.CreateDirectory(output); + Assert.False(wrapper.Extract(output, false)); + Assert.False(File.Exists(Path.Combine(parent, "data.bin"))); + Assert.True(File.Exists(Path.Combine(output, "README.TXT"))); + } + finally + { + if (Directory.Exists(parent)) + Directory.Delete(parent, true); + } + } + + [Fact] + public void RecordedPastEnd_OtherFilesExtracted() + { + // "Media" is recorded in a partition block past the end of the image + var wrapper = UDF.Create(new MemoryStream(UDFImageBuilder.Build(mediaPastEnd: true))); + Assert.NotNull(wrapper); + + string output = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + try + { + Directory.CreateDirectory(output); + Assert.False(wrapper.Extract(output, false)); + Assert.True(File.Exists(Path.Combine(output, "README.TXT"))); + Assert.Equal(UDFImageBuilder.DataBinLength, new FileInfo(Path.Combine(output, "Sub", "data.bin")).Length); + } + finally + { + if (Directory.Exists(output)) + Directory.Delete(output, true); + } + } + + [Fact] + public void StreamAtOffset_VolumeAtOffsetExtracted() + { + // Another volume comes first, so reading from the start of the stream would find "Outer" instead of "Inner" + var stream = new MemoryStream([.. UDFImageBuilder.Build(subName: "Outer"), .. UDFImageBuilder.Build(subName: "Inner")]); + stream.Position = UDFImageBuilder.Sectors * UDFImageBuilder.SectorSize; + + var wrapper = UDF.Create(stream); + Assert.NotNull(wrapper); + + string output = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + try + { + Directory.CreateDirectory(output); + Assert.True(wrapper.Extract(output, false)); + Assert.Equal(UDFImageBuilder.DataBinLength, new FileInfo(Path.Combine(output, "Inner", "data.bin")).Length); + Assert.False(Directory.Exists(Path.Combine(output, "Outer"))); + } + finally + { + if (Directory.Exists(output)) + Directory.Delete(output, true); + } + } + + [Fact] + public void StreamAtOffset_WrapperFactoryUDF() + { + var stream = new MemoryStream([.. new byte[3 * UDFImageBuilder.SectorSize], .. UDFImageBuilder.Build()]); + stream.Position = 3 * UDFImageBuilder.SectorSize; + + var wrapper = WrapperFactory.CreateDiscImageWrapper(stream); + Assert.IsType(wrapper); + } + } +} diff --git a/SabreTools.Wrappers/UDF.Extraction.cs b/SabreTools.Wrappers/UDF.Extraction.cs new file mode 100644 index 00000000..f28e63f1 --- /dev/null +++ b/SabreTools.Wrappers/UDF.Extraction.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.IO; +using SabreTools.Data.Extensions; +using SabreTools.Data.Models.UDF; +using SabreTools.IO.Extensions; +using SabreTools.Numerics.Extensions; + +namespace SabreTools.Wrappers +{ + public partial class UDF : IExtractable + { + /// + public virtual bool Extract(string outputDirectory, bool includeDebug) + { + // If we have no file set or directories, there is nothing to extract + if (FileSetDescriptor is null || Directories.Count == 0) + return true; + + long root = FileSetDescriptor.RootDirectoryICB.ExtentLocation.GetKey(); + var visited = new HashSet { root }; + return ExtractDirectory(root, Path.GetFullPath(outputDirectory), visited, includeDebug); + } + + /// + /// Extract all files and directories within a directory + /// + private bool ExtractDirectory(long key, string outputPath, HashSet visited, bool includeDebug) + { + if (!Directories.TryGetValue(key, out var fids)) + return false; + + bool succeeded = true; + foreach (var fid in fids) + { + // Skip deleted entries and the parent directory + if ((fid.FileCharacteristics & (FileCharacteristics.DELETED | FileCharacteristics.PARENT)) != 0) + continue; + + string? name = fid.FileIdentifier.DecodeDCharacters(); + if (!IsSafeName(name)) + { + if (includeDebug) Console.WriteLine($"Skipping entry with unsafe name in {outputPath}"); + + succeeded = false; + continue; + } + + long childKey = fid.ICB.ExtentLocation.GetKey(); + if (!FileEntries.TryGetValue(childKey, out var entry)) + { + if (includeDebug) Console.WriteLine($"File Entry missing, cannot extract: {name}"); + + succeeded = false; + continue; + } + + string childPath = Path.Combine(outputPath, name!); + if (entry.ICBTag.FileType == FileType.DIRECTORY) + { + if (includeDebug) Console.WriteLine($"Extracting to directory: {childPath}"); + if (!Directory.Exists(childPath)) + Directory.CreateDirectory(childPath); + + // Directories reachable twice (hard links, loops) are extracted once + if (visited.Add(childKey)) + succeeded &= ExtractDirectory(childKey, childPath, visited, includeDebug); + } + else + { + succeeded &= ExtractFile(childKey, entry, childPath, includeDebug); + } + } + + return succeeded; + } + + /// + /// Extract a file's data + /// + private bool ExtractFile(long key, FileEntry entry, string filePath, bool includeDebug) + { + if (File.Exists(filePath) || Directory.Exists(filePath)) + { + if (includeDebug) Console.WriteLine($"File/Folder already exists, cannot extract: {filePath}"); + return false; + } + + if (includeDebug) Console.WriteLine($"Extracting: {filePath}"); + + long size = (long)entry.InformationLength; + if (entry.ICBTag.GetAllocationDescriptorType() == AllocationDescriptorType.EMBEDDED) + { + using var embedded = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.None); + embedded.Write(entry.AllocationDescriptors, 0, (int)size); + return true; + } + + var descriptors = Model.GetAllocationDescriptors(key, entry); + if (descriptors is null) + { + if (includeDebug) Console.WriteLine($"Allocation descriptors not supported or incomplete: {filePath}"); + return false; + } + + const int chunkSize = 2048 * 1024; + using var fs = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.None); + long written = 0; + foreach (var ad in descriptors) + { + if (written >= size) + break; + + long length = Math.Min(UDFExtensions.GetExtentLength(ad.ExtentLength), size - written); + bool recorded = UDFExtensions.GetExtentType(ad.ExtentLength) == ExtentType.RECORDED_AND_ALLOCATED; + + long sector = 0; + if (recorded && !Model.TryGetSector(ad.ExtentLocation.GetKey(), out sector)) + return false; + + // Make sure the extent won't EOF + if (recorded && (sector * Constants.SectorSize) + length > _dataSource.Length) + return false; + + long done = 0; + while (done < length) + { + int count = (int)Math.Min(chunkSize, length - done); + byte[] buffer; + if (recorded) + { + lock (_dataSourceLock) + { + _dataSource.SeekIfPossible((sector * Constants.SectorSize) + done, SeekOrigin.Begin); + buffer = _dataSource.ReadBytes(count); + } + } + else + { + // Extents that are not recorded read as zeros + buffer = new byte[count]; + } + + fs.Write(buffer, 0, count); + done += count; + } + + written += length; + } + + return written == size; + } + + /// + /// Check that a file identifier can be used as a single path component + /// + private static bool IsSafeName(string? name) + { + if (string.IsNullOrEmpty(name) || name == "." || name == "..") + return false; + + char[] invalid = Path.GetInvalidFileNameChars(); + foreach (char c in name!) + { + if (c == '/' || c == '\\' || c == '\0' || Array.IndexOf(invalid, c) >= 0) + return false; + } + + return true; + } + } +} diff --git a/SabreTools.Wrappers/UDF.Printing.cs b/SabreTools.Wrappers/UDF.Printing.cs new file mode 100644 index 00000000..b5986775 --- /dev/null +++ b/SabreTools.Wrappers/UDF.Printing.cs @@ -0,0 +1,310 @@ +using System.Collections.Generic; +using System.Text; +using SabreTools.Data.Extensions; +using SabreTools.Data.Models.UDF; +using SabreTools.Text.Extensions; + +namespace SabreTools.Wrappers +{ + public partial class UDF : IPrintable + { +#if NETCOREAPP + /// + public string ExportJSON(bool recursive) => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions); +#else + /// + public string ExportJSON(bool recursive) => Newtonsoft.Json.JsonConvert.SerializeObject(Model, _jsonSerializerOptions); +#endif + + /// + public void PrintInformation(StringBuilder builder, bool recursive) + { + builder.AppendLine("UDF Information:"); + builder.AppendLine("-------------------------"); + builder.AppendLine(); + + Print(builder, Model.VolumeRecognitionSequence); + Print(builder, Model.AnchorVolumeDescriptorPointer); + Print(builder, Model.VolumeDescriptorSequence); + Print(builder, Model.FileSetDescriptor); + + if (Model.FileSetDescriptor is not null) + { + builder.AppendLine(" Directory Hierarchy:"); + builder.AppendLine(" -------------------------"); + long root = Model.FileSetDescriptor.RootDirectoryICB.ExtentLocation.GetKey(); + PrintDirectory(builder, root, "\\", new HashSet { root }); + builder.AppendLine(); + } + } + + private static void Print(StringBuilder builder, VolumeStructureDescriptor[] sequence) + { + builder.AppendLine(" Volume Recognition Sequence:"); + builder.AppendLine(" -------------------------"); + if (sequence.Length == 0) + { + builder.AppendLine(" No volume structure descriptors"); + builder.AppendLine(); + return; + } + + for (int i = 0; i < sequence.Length; i++) + { + var vsd = sequence[i]; + builder.AppendLine($" Volume Structure Descriptor {i}:"); + builder.AppendLine(vsd.StructureType, " Structure Type"); + builder.AppendLine(Encoding.ASCII.GetString(vsd.StandardIdentifier), " Standard Identifier"); + builder.AppendLine(vsd.StructureVersion, " Structure Version"); + } + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, AnchorVolumeDescriptorPointer? anchor) + { + builder.AppendLine(" Anchor Volume Descriptor Pointer:"); + builder.AppendLine(" -------------------------"); + if (anchor is null) + { + builder.AppendLine(" No anchor volume descriptor pointer"); + builder.AppendLine(); + return; + } + + Print(builder, anchor.DescriptorTag, " "); + Print(builder, anchor.MainVolumeDescriptorSequenceExtent, " Main Volume Descriptor Sequence"); + Print(builder, anchor.ReserveVolumeDescriptorSequenceExtent, " Reserve Volume Descriptor Sequence"); + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, Descriptor[] sequence) + { + builder.AppendLine(" Volume Descriptor Sequence:"); + builder.AppendLine(" -------------------------"); + if (sequence.Length == 0) + { + builder.AppendLine(" No volume descriptors"); + builder.AppendLine(); + return; + } + + foreach (var descriptor in sequence) + { + builder.AppendLine($" {descriptor.DescriptorTag.TagIdentifier} (Sector {descriptor.DescriptorTag.TagLocation}):"); + Print(builder, descriptor.DescriptorTag, " "); + switch (descriptor) + { + case PrimaryVolumeDescriptor pvd: + builder.AppendLine(pvd.VolumeDescriptorSequenceNumber, " Volume Descriptor Sequence Number"); + builder.AppendLine(pvd.PrimaryVolumeDescriptorNumber, " Primary Volume Descriptor Number"); + builder.AppendLine(pvd.VolumeIdentifier.DecodeDString(), " Volume Identifier"); + builder.AppendLine(pvd.VolumeSequenceNumber, " Volume Sequence Number"); + builder.AppendLine(pvd.MaximumVolumeSequenceNumber, " Maximum Volume Sequence Number"); + builder.AppendLine(pvd.InterchangeLevel, " Interchange Level"); + builder.AppendLine(pvd.MaximumInterchangeLevel, " Maximum Interchange Level"); + builder.AppendLine(pvd.VolumeSetIdentifier.DecodeDString(), " Volume Set Identifier"); + Print(builder, pvd.ApplicationIdentifier, " Application Identifier"); + builder.AppendLine(Format(pvd.RecordingDateAndTime), " Recording Date and Time"); + Print(builder, pvd.ImplementationIdentifier, " Implementation Identifier"); + builder.AppendLine(pvd.Flags, " Flags"); + break; + + case PartitionDescriptor pd: + builder.AppendLine(pd.VolumeDescriptorSequenceNumber, " Volume Descriptor Sequence Number"); + builder.AppendLine(pd.PartitionFlags, " Partition Flags"); + builder.AppendLine(pd.PartitionNumber, " Partition Number"); + Print(builder, pd.PartitionContents, " Partition Contents"); + builder.AppendLine(pd.AccessType, " Access Type"); + builder.AppendLine(pd.PartitionStartingLocation, " Partition Starting Location"); + builder.AppendLine(pd.PartitionLength, " Partition Length"); + Print(builder, pd.ImplementationIdentifier, " Implementation Identifier"); + break; + + case LogicalVolumeDescriptor lvd: + builder.AppendLine(lvd.VolumeDescriptorSequenceNumber, " Volume Descriptor Sequence Number"); + builder.AppendLine(lvd.LogicalVolumeIdentifier.DecodeDString(), " Logical Volume Identifier"); + builder.AppendLine(lvd.LogicalBlockSize, " Logical Block Size"); + Print(builder, lvd.DomainIdentifier, " Domain Identifier"); + Print(builder, lvd.LogicalVolumeContentsUse, " File Set Descriptor Extent"); + builder.AppendLine(lvd.MapTableLength, " Map Table Length"); + builder.AppendLine(lvd.NumberOfPartitionMaps, " Number of Partition Maps"); + Print(builder, lvd.ImplementationIdentifier, " Implementation Identifier"); + Print(builder, lvd.IntegritySequenceExtent, " Integrity Sequence Extent"); + for (int i = 0; i < lvd.PartitionMaps.Length; i++) + { + Print(builder, lvd.PartitionMaps[i], i); + } + + break; + + case VolumeDescriptorPointer vdp: + builder.AppendLine(vdp.VolumeDescriptorSequenceNumber, " Volume Descriptor Sequence Number"); + Print(builder, vdp.NextVolumeDescriptorSequenceExtent, " Next Volume Descriptor Sequence"); + break; + + default: + // Other descriptors are not parsed further + break; + } + } + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, PartitionMap map, int index) + { + builder.AppendLine($" Partition Map {index} (Partition Reference Number {index}):"); + builder.AppendLine(map.PartitionMapType, " Partition Map Type"); + builder.AppendLine(map.PartitionMapLength, " Partition Map Length"); + switch (map) + { + case Type1PartitionMap type1: + builder.AppendLine(type1.VolumeSequenceNumber, " Volume Sequence Number"); + builder.AppendLine(type1.PartitionNumber, " Partition Number"); + break; + + case MetadataPartitionMap metadata: + Print(builder, metadata.PartitionTypeIdentifier, " Partition Type Identifier"); + builder.AppendLine(metadata.VolumeSequenceNumber, " Volume Sequence Number"); + builder.AppendLine(metadata.PartitionNumber, " Partition Number"); + builder.AppendLine(metadata.MetadataFileLocation, " Metadata File Location"); + builder.AppendLine(metadata.MetadataMirrorFileLocation, " Metadata Mirror File Location"); + builder.AppendLine(metadata.MetadataBitmapFileLocation, " Metadata Bitmap File Location"); + builder.AppendLine(metadata.AllocationUnitSize, " Allocation Unit Size"); + builder.AppendLine(metadata.AlignmentUnitSize, " Alignment Unit Size"); + builder.AppendLine(metadata.Flags, " Flags"); + break; + + case Type2PartitionMap type2: + Print(builder, type2.PartitionTypeIdentifier, " Partition Type Identifier"); + builder.AppendLine(type2.VolumeSequenceNumber, " Volume Sequence Number"); + builder.AppendLine(type2.PartitionNumber, " Partition Number"); + builder.AppendLine(type2.PartitionTypeData, " Partition Type Data"); + break; + + default: + // Unknown partition map types are not parsed further + break; + } + } + + private static void Print(StringBuilder builder, FileSetDescriptor? fsd) + { + builder.AppendLine(" File Set Descriptor:"); + builder.AppendLine(" -------------------------"); + if (fsd is null) + { + builder.AppendLine(" No file set descriptor"); + builder.AppendLine(); + return; + } + + Print(builder, fsd.DescriptorTag, " "); + builder.AppendLine(Format(fsd.RecordingDateAndTime), " Recording Date and Time"); + builder.AppendLine(fsd.InterchangeLevel, " Interchange Level"); + builder.AppendLine(fsd.MaximumInterchangeLevel, " Maximum Interchange Level"); + builder.AppendLine(fsd.FileSetNumber, " File Set Number"); + builder.AppendLine(fsd.FileSetDescriptorNumber, " File Set Descriptor Number"); + builder.AppendLine(fsd.LogicalVolumeIdentifier.DecodeDString(), " Logical Volume Identifier"); + builder.AppendLine(fsd.FileSetIdentifier.DecodeDString(), " File Set Identifier"); + builder.AppendLine(fsd.CopyrightFileIdentifier.DecodeDString(), " Copyright File Identifier"); + builder.AppendLine(fsd.AbstractFileIdentifier.DecodeDString(), " Abstract File Identifier"); + Print(builder, fsd.RootDirectoryICB, " Root Directory ICB"); + Print(builder, fsd.DomainIdentifier, " Domain Identifier"); + Print(builder, fsd.NextExtent, " Next Extent"); + Print(builder, fsd.SystemStreamDirectoryICB, " System Stream Directory ICB"); + builder.AppendLine(); + } + + private void PrintDirectory(StringBuilder builder, long key, string path, HashSet visited) + { + if (!Model.Directories.TryGetValue(key, out var fids)) + { + builder.AppendLine($" {path} (directory not read)"); + return; + } + + foreach (var fid in fids) + { + if ((fid.FileCharacteristics & (FileCharacteristics.DELETED | FileCharacteristics.PARENT)) != 0) + continue; + + string name = fid.FileIdentifier.DecodeDCharacters() ?? "(unknown encoding)"; + string childPath = path + name; + long childKey = fid.ICB.ExtentLocation.GetKey(); + string hidden = (fid.FileCharacteristics & FileCharacteristics.EXISTENCE) != 0 ? ", hidden" : string.Empty; + if (!Model.FileEntries.TryGetValue(childKey, out var entry)) + { + builder.AppendLine($" {childPath} (File Entry not read{hidden})"); + continue; + } + + string location = $"partition {fid.ICB.ExtentLocation.PartitionReferenceNumber}, block {fid.ICB.ExtentLocation.LogicalBlockNumber}"; + if (entry.ICBTag.FileType == FileType.DIRECTORY) + { + builder.AppendLine($" {childPath}\\ ({location}{hidden})"); + if (visited.Add(childKey)) + PrintDirectory(builder, childKey, childPath + "\\", visited); + } + else + { + builder.AppendLine($" {childPath} ({entry.InformationLength} bytes, {entry.ICBTag.FileType}, {location}{hidden})"); + } + } + } + + private static void Print(StringBuilder builder, DescriptorTag tag, string indent) + { + builder.AppendLine((ushort)tag.TagIdentifier, $"{indent}Tag Identifier"); + builder.AppendLine(tag.DescriptorVersion, $"{indent}Descriptor Version"); + builder.AppendLine(tag.TagSerialNumber, $"{indent}Tag Serial Number"); + builder.AppendLine(tag.DescriptorCRCLength, $"{indent}Descriptor CRC Length"); + builder.AppendLine(tag.TagLocation, $"{indent}Tag Location"); + } + + private static void Print(StringBuilder builder, ExtentDescriptor extent, string label) + => builder.AppendLine($"sector {extent.ExtentLocation}, {extent.ExtentLength} bytes", label); + + private static void Print(StringBuilder builder, LongAllocationDescriptor ad, string label) + { + builder.AppendLine($"partition {ad.ExtentLocation.PartitionReferenceNumber}, block {ad.ExtentLocation.LogicalBlockNumber}, " + + $"{UDFExtensions.GetExtentLength(ad.ExtentLength)} bytes", label); + } + + private static void Print(StringBuilder builder, EntityIdentifier identifier, string label) + { + int length = System.Array.IndexOf(identifier.Identifier, (byte)0); + if (length < 0) + length = identifier.Identifier.Length; + + string text = Encoding.ASCII.GetString(identifier.Identifier, 0, length); + builder.AppendLine($"{text} (flags {identifier.Flags}, suffix {BytesToHex(identifier.IdentifierSuffix)})", label); + } + + private static string Format(Timestamp timestamp) + { + if (timestamp.Year == 0 && timestamp.Month == 0 && timestamp.Day == 0) + return "Not specified"; + + // Lower 12 bits: signed offset from UTC in minutes, -2047 = not specified + int offset = timestamp.TypeAndTimezone & 0x0FFF; + if ((offset & 0x0800) != 0) + offset -= 0x1000; + + string zone = offset == -2047 ? string.Empty : $" UTC{(offset < 0 ? "-" : "+")}{System.Math.Abs(offset) / 60:D2}:{System.Math.Abs(offset) % 60:D2}"; + return $"{timestamp.Year:D4}-{timestamp.Month:D2}-{timestamp.Day:D2} {timestamp.Hour:D2}:{timestamp.Minute:D2}:{timestamp.Second:D2}.{timestamp.Centiseconds:D2}{zone}"; + } + + private static string BytesToHex(byte[] data) + { + var builder = new StringBuilder(data.Length * 2); + foreach (byte b in data) + { + builder.Append(b.ToString("X2")); + } + + return builder.ToString(); + } + } +} diff --git a/SabreTools.Wrappers/UDF.cs b/SabreTools.Wrappers/UDF.cs new file mode 100644 index 00000000..d7ec69f0 --- /dev/null +++ b/SabreTools.Wrappers/UDF.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.IO; +using SabreTools.Data.Models.UDF; +using SabreTools.IO; + +namespace SabreTools.Wrappers +{ + public partial class UDF : WrapperBase + { + #region Descriptive Properties + + /// + public override string DescriptionString => "Universal Disk Format (UDF) Volume"; + + #endregion + + #region Extension Properties + + /// + public VolumeStructureDescriptor[] VolumeRecognitionSequence => Model.VolumeRecognitionSequence; + + /// + public Descriptor[] VolumeDescriptorSequence => Model.VolumeDescriptorSequence; + + /// + public FileSetDescriptor? FileSetDescriptor => Model.FileSetDescriptor; + + /// + public Dictionary FileEntries => Model.FileEntries; + + /// + public Dictionary Directories => Model.Directories; + + #endregion + + #region Constructors + + /// + public UDF(Volume model, byte[] data) : base(model, data) { } + + /// + public UDF(Volume model, byte[] data, int offset) : base(model, data, offset) { } + + /// + public UDF(Volume model, byte[] data, int offset, int length) : base(model, data, offset, length) { } + + /// + public UDF(Volume model, Stream data) : base(model, data) { } + + /// + public UDF(Volume model, Stream data, long offset) : base(model, data, offset) { } + + /// + public UDF(Volume model, Stream data, long offset, long length) : base(model, data, offset, length) { } + + #endregion + + #region Static Constructors + + /// + /// Create a UDF Volume from a byte array and offset + /// + /// Byte array representing the UDF Volume + /// Offset within the array to parse + /// A UDF Volume wrapper on success, null on failure + public static UDF? Create(byte[]? data, int offset) + { + // If the data is invalid + if (data is 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); + } + + /// + /// Create a UDF Volume from a Stream + /// + /// Stream representing the UDF Volume + /// A UDF Volume wrapper on success, null on failure + public static UDF? Create(Stream? data) + { + // If the data is invalid + if (data is null || !data.CanRead || !data.CanSeek) + return null; + + try + { + // Cache the current offset + long currentOffset = data.Position; + + // The reader addresses logical sectors from the start of the stream, so it reads a view that starts here + var model = new Serialization.Readers.UDF().Deserialize(new ReadOnlyViewStream(data, currentOffset)); + if (model is null) + return null; + + return new UDF(model, data, currentOffset); + } + catch + { + return null; + } + } + + #endregion + } +} diff --git a/SabreTools.Wrappers/WrapperFactory.cs b/SabreTools.Wrappers/WrapperFactory.cs index 44cc24ec..6c72fd26 100644 --- a/SabreTools.Wrappers/WrapperFactory.cs +++ b/SabreTools.Wrappers/WrapperFactory.cs @@ -161,6 +161,14 @@ public static class WrapperFactory // Reset position in stream stream.SeekIfPossible(initialOffset, SeekOrigin.Begin); + // Try to get a UDF wrapper (discs without an ISO9660 file system) + var udfWrapper = UDF.Create(stream); + if (udfWrapper is not null) + return udfWrapper; + + // Reset position in stream + stream.SeekIfPossible(initialOffset, SeekOrigin.Begin); + // Try to get a 3DO / M2 disc image wrapper var operaFSWrapper = OperaFS.Create(stream); if (operaFSWrapper is not null)