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