hadris-udf 2.2.0

Pure Rust UDF filesystem library for optical media and disk images with no-std support
Documentation
//! Primary Volume Descriptor (ECMA-167 3/10.1)

use super::{CharSpec, DescriptorTag, EntityIdentifier, ExtentDescriptor, TagIdentifier};
use crate::error::Result;
use crate::time::UdfTimestamp;

/// Primary Volume Descriptor (ECMA-167 3/10.1)
///
/// @hadris-spec ECMA-167:3/10.1
/// @hadris-compliance partial
/// @hadris-note The descriptor is modeled and round-trip tested, but clause-complete validation has not yet been established.
/// @hadris-tests write::tests::test_roundtrip_basic_verification
/// @hadris-fuzz udf_read
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct PrimaryVolumeDescriptor {
    /// Descriptor tag
    pub tag: DescriptorTag,
    /// Volume Descriptor Sequence Number
    pub vds_number: u32,
    /// Primary Volume Descriptor Number
    pub pvd_number: u32,
    /// Volume Identifier (dstring, 32 bytes)
    pub volume_identifier: [u8; 32],
    /// Volume Sequence Number
    pub volume_sequence_number: u16,
    /// Maximum Volume Sequence Number
    pub max_volume_sequence_number: u16,
    /// Interchange Level
    pub interchange_level: u16,
    /// Maximum Interchange Level
    pub max_interchange_level: u16,
    /// Character Set List
    pub character_set_list: u32,
    /// Maximum Character Set List
    pub max_character_set_list: u32,
    /// Volume Set Identifier (dstring, 128 bytes)
    pub volume_set_identifier: [u8; 128],
    /// Descriptor Character Set
    pub descriptor_char_set: CharSpec,
    /// Explanatory Character Set
    pub explanatory_char_set: CharSpec,
    /// Volume Abstract
    pub volume_abstract: ExtentDescriptor,
    /// Volume Copyright Notice
    pub volume_copyright: ExtentDescriptor,
    /// Application Identifier
    pub application_identifier: EntityIdentifier,
    /// Recording Date and Time
    pub recording_date_time: UdfTimestamp,
    /// Implementation Identifier
    pub implementation_identifier: EntityIdentifier,
    /// Implementation Use (64 bytes)
    pub implementation_use: [u8; 64],
    /// Predecessor Volume Descriptor Sequence Location
    pub predecessor_vds_location: u32,
    /// Flags
    pub flags: u16,
    /// Reserved
    reserved: [u8; 22],
}

unsafe impl bytemuck::Zeroable for PrimaryVolumeDescriptor {}
unsafe impl bytemuck::Pod for PrimaryVolumeDescriptor {}

impl PrimaryVolumeDescriptor {
    #[cfg(feature = "alloc")]
    pub(crate) fn into_native(mut self) -> Self {
        self.tag = DescriptorTag::from_disk_bytes(bytemuck::bytes_of(&self.tag))
            .expect("DescriptorTag has its fixed on-disk size");
        self.vds_number = self.vds_number.to_le();
        self.pvd_number = self.pvd_number.to_le();
        self.volume_sequence_number = self.volume_sequence_number.to_le();
        self.max_volume_sequence_number = self.max_volume_sequence_number.to_le();
        self.interchange_level = self.interchange_level.to_le();
        self.max_interchange_level = self.max_interchange_level.to_le();
        self.character_set_list = self.character_set_list.to_le();
        self.max_character_set_list = self.max_character_set_list.to_le();
        self.volume_abstract = self.volume_abstract.into_native();
        self.volume_copyright = self.volume_copyright.into_native();
        self.recording_date_time = self.recording_date_time.into_native();
        self.predecessor_vds_location = self.predecessor_vds_location.to_le();
        self.flags = self.flags.to_le();
        self
    }

    /// Validate this descriptor
    pub fn validate(&self, location: u32) -> Result<()> {
        self.tag
            .validate(TagIdentifier::PrimaryVolumeDescriptor, location)
    }

    /// Get the volume identifier as a string
    #[cfg(feature = "alloc")]
    pub fn volume_id(&self) -> alloc::string::String {
        decode_dstring(&self.volume_identifier)
    }
}

/// Decode a UDF dstring (compressed unicode)
#[cfg(feature = "alloc")]
pub fn decode_dstring(data: &[u8]) -> alloc::string::String {
    if data.is_empty() {
        return alloc::string::String::new();
    }

    // First byte is compression ID
    let compression_id = data[0];
    // Last byte is the dstring length, which includes the compression ID byte
    let len = data[data.len() - 1] as usize;

    if len <= 1 || len > data.len() - 1 {
        return alloc::string::String::new();
    }

    let content = &data[1..len];

    match compression_id {
        8 => {
            // CS0 compression ID 8 stores one Unicode code point per byte.
            content.iter().map(|byte| char::from(*byte)).collect()
        }
        16 => decode_utf16_be(content),
        _ => alloc::string::String::new(),
    }
}

#[cfg(feature = "alloc")]
pub(crate) fn decode_utf16_be(content: &[u8]) -> alloc::string::String {
    char::decode_utf16(
        content
            .chunks_exact(2)
            .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]])),
    )
    .map(|unit| unit.unwrap_or(char::REPLACEMENT_CHARACTER))
    .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    static_assertions::const_assert_eq!(size_of::<PrimaryVolumeDescriptor>(), 512);

    #[cfg(feature = "alloc")]
    fn dstring_field<const N: usize>(compression_id: u8, content: &[u8]) -> [u8; N] {
        let mut field = [0u8; N];
        field[0] = compression_id;
        field[1..1 + content.len()].copy_from_slice(content);
        field[N - 1] = (content.len() + 1) as u8;
        field
    }

    #[cfg(feature = "alloc")]
    #[test]
    fn test_decode_dstring_8bit_no_trailing_nul() {
        let field: [u8; 32] = dstring_field(8, b"SMOKETEST");
        let decoded = decode_dstring(&field);
        assert_eq!(decoded, "SMOKETEST");
        assert!(!decoded.contains('\0'));
    }

    #[cfg(feature = "alloc")]
    #[test]
    fn test_decode_dstring_16bit_surrogate_pair() {
        let mut content = alloc::vec::Vec::new();
        for unit in "a\u{1F600}b".encode_utf16() {
            content.extend_from_slice(&unit.to_be_bytes());
        }
        let field: [u8; 32] = dstring_field(16, &content);
        assert_eq!(decode_dstring(&field), "a\u{1F600}b");
    }

    #[cfg(feature = "alloc")]
    #[test]
    fn test_decode_dstring_unpaired_surrogate_is_replaced() {
        let field: [u8; 8] = dstring_field(16, &0xD800u16.to_be_bytes());
        assert_eq!(decode_dstring(&field), "\u{FFFD}");
    }

    #[cfg(feature = "alloc")]
    #[test]
    fn test_decode_dstring_hostile_lengths() {
        assert_eq!(decode_dstring(&[]), "");
        assert_eq!(decode_dstring(&[8]), "");
        assert_eq!(decode_dstring(&[8, 0]), "");
        assert_eq!(decode_dstring(&[8, b'a', 1]), "");
        assert_eq!(decode_dstring(&[8, b'a', 0xFF]), "");
        assert_eq!(decode_dstring(&[8, b'a', b'b', b'c', 5]), "");
        assert_eq!(decode_dstring(&[8, b'a', b'b', 3]), "ab");
    }
}