Skip to main content

hadris_udf/descriptor/
mod.rs

1//! UDF Volume Descriptors
2//!
3//! This module contains structures for parsing UDF volume descriptors
4//! according to ECMA-167.
5
6mod anchor;
7mod fileset;
8mod logical;
9mod partition;
10mod primary;
11mod tag;
12
13pub use anchor::AnchorVolumeDescriptorPointer;
14pub use fileset::FileSetDescriptor;
15pub use logical::{LogicalVolumeDescriptor, Type1PartitionMap};
16pub use partition::{PartitionContents, PartitionDescriptor};
17pub use primary::PrimaryVolumeDescriptor;
18pub use tag::{DescriptorTag, TagIdentifier};
19
20use super::super::{Read, Seek, SeekFrom};
21use crate::error::{Error, Result};
22
23/// Extent descriptor (ECMA-167 3/7.1)
24///
25/// @hadris-spec ECMA-167:3/7.1
26/// @hadris-compliance partial
27/// @hadris-note The layout is modeled and tested, but all extent semantics are not validated at this layer.
28/// @hadris-tests comprehensive_udf::test_extent_descriptor
29/// @hadris-fuzz udf_read
30#[repr(C)]
31#[derive(Debug, Clone, Copy, Default, bytemuck::Zeroable, bytemuck::Pod)]
32pub struct ExtentDescriptor {
33    /// Length in bytes
34    pub length: u32,
35    /// Location (logical sector number)
36    pub location: u32,
37}
38
39impl ExtentDescriptor {
40    /// Check if this extent is empty
41    pub fn is_empty(&self) -> bool {
42        self.length == 0
43    }
44
45    pub(crate) fn into_native(mut self) -> Self {
46        self.length = self.length.to_le();
47        self.location = self.location.to_le();
48        self
49    }
50}
51
52/// Long allocation descriptor (ECMA-167 4/14.14.2)
53///
54/// Used to reference data that may span multiple partitions
55///
56/// @hadris-spec ECMA-167:4/14.14.2
57/// @hadris-compliance partial
58/// @hadris-note The layout is modeled and tested, but all partition-reference and extent semantics are not validated at this layer.
59/// @hadris-tests comprehensive_udf::test_allocation_descriptor_sizes
60/// @hadris-fuzz udf_read
61#[repr(C)]
62#[derive(Debug, Clone, Copy, Default)]
63pub struct LongAllocationDescriptor {
64    /// Extent length (high 2 bits indicate type)
65    pub extent_length: u32,
66    /// Logical block number
67    pub logical_block_num: u32,
68    /// Partition reference number
69    pub partition_ref_num: u16,
70    /// Implementation use (6 bytes)
71    pub impl_use: [u8; 6],
72}
73
74unsafe impl bytemuck::Zeroable for LongAllocationDescriptor {}
75unsafe impl bytemuck::Pod for LongAllocationDescriptor {}
76
77impl LongAllocationDescriptor {
78    pub(crate) fn into_native(mut self) -> Self {
79        self.extent_length = self.extent_length.to_le();
80        self.logical_block_num = self.logical_block_num.to_le();
81        self.partition_ref_num = self.partition_ref_num.to_le();
82        self
83    }
84
85    /// Get the extent length in bytes (excluding type bits)
86    pub fn length(&self) -> u32 {
87        self.extent_length & 0x3FFFFFFF
88    }
89
90    /// Get the extent type
91    pub fn extent_type(&self) -> ExtentType {
92        ExtentType::from_bits((self.extent_length >> 30) as u8)
93    }
94
95    /// Check if this is a recorded and allocated extent
96    pub fn is_recorded(&self) -> bool {
97        matches!(self.extent_type(), ExtentType::RecordedAllocated)
98    }
99
100    /// Get the extent location (legacy accessor)
101    pub fn extent_location(&self) -> LbAddr {
102        LbAddr {
103            logical_block_num: self.logical_block_num,
104            partition_ref_num: self.partition_ref_num,
105        }
106    }
107}
108
109/// Short allocation descriptor (ECMA-167 4/14.14.1)
110///
111/// @hadris-spec ECMA-167:4/14.14.1
112/// @hadris-compliance partial
113/// @hadris-note The layout is modeled and tested, but all allocation-length semantics are not validated at this layer.
114/// @hadris-tests comprehensive_udf::test_allocation_descriptor_sizes
115/// @hadris-fuzz udf_read
116#[repr(C)]
117#[derive(Debug, Clone, Copy, Default, bytemuck::Zeroable, bytemuck::Pod)]
118pub struct ShortAllocationDescriptor {
119    /// Extent length (high 2 bits indicate type)
120    pub extent_length: u32,
121    /// Extent position (logical block number within partition)
122    pub extent_position: u32,
123}
124
125impl ShortAllocationDescriptor {
126    #[cfg(feature = "alloc")]
127    pub(crate) fn into_native(mut self) -> Self {
128        self.extent_length = self.extent_length.to_le();
129        self.extent_position = self.extent_position.to_le();
130        self
131    }
132
133    /// Get the extent length in bytes
134    pub fn length(&self) -> u32 {
135        self.extent_length & 0x3FFFFFFF
136    }
137
138    /// Get the extent type
139    pub fn extent_type(&self) -> ExtentType {
140        ExtentType::from_bits((self.extent_length >> 30) as u8)
141    }
142}
143
144/// Extent type (allocation descriptor type field)
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum ExtentType {
147    /// Recorded and allocated
148    RecordedAllocated,
149    /// Allocated but not recorded
150    AllocatedNotRecorded,
151    /// Not allocated and not recorded
152    NotAllocatedNotRecorded,
153    /// Next extent of descriptors
154    NextExtent,
155}
156
157impl ExtentType {
158    fn from_bits(bits: u8) -> Self {
159        match bits & 0x03 {
160            0 => Self::RecordedAllocated,
161            1 => Self::AllocatedNotRecorded,
162            2 => Self::NotAllocatedNotRecorded,
163            3 => Self::NextExtent,
164            _ => unreachable!(),
165        }
166    }
167}
168
169/// Logical block address (ECMA-167 4/7.1)
170#[repr(C)]
171#[derive(Debug, Clone, Copy, Default)]
172pub struct LbAddr {
173    /// Logical block number
174    pub logical_block_num: u32,
175    /// Partition reference number
176    pub partition_ref_num: u16,
177}
178
179/// Entity identifier (ECMA-167 1/7.4)
180///
181/// @hadris-spec ECMA-167:1/7.4
182/// @hadris-compliance partial
183/// @hadris-note The identifier layout is modeled, but suffix-specific constraints are not all validated.
184/// @hadris-tests comprehensive_udf::test_partition_contents
185/// @hadris-fuzz udf_read
186#[repr(C)]
187#[derive(Debug, Clone, Copy)]
188pub struct EntityIdentifier {
189    /// Flags
190    pub flags: u8,
191    /// Identifier (23 bytes, padded with zeros)
192    pub identifier: [u8; 23],
193    /// Identifier suffix (8 bytes)
194    pub suffix: [u8; 8],
195}
196
197impl EntityIdentifier {
198    /// Empty identifier
199    pub const EMPTY: Self = Self {
200        flags: 0,
201        identifier: [0; 23],
202        suffix: [0; 8],
203    };
204
205    /// Get the identifier as a string (trimmed)
206    #[cfg(feature = "alloc")]
207    pub fn as_str(&self) -> alloc::string::String {
208        let end = self.identifier.iter().position(|&b| b == 0).unwrap_or(23);
209        alloc::string::String::from_utf8_lossy(&self.identifier[..end]).into_owned()
210    }
211
212    /// Check if this is a specific identifier
213    pub fn is(&self, id: &[u8]) -> bool {
214        let end = self.identifier.iter().position(|&b| b == 0).unwrap_or(23);
215        &self.identifier[..end] == id
216    }
217}
218
219impl Default for EntityIdentifier {
220    fn default() -> Self {
221        Self::EMPTY
222    }
223}
224
225unsafe impl bytemuck::Zeroable for EntityIdentifier {}
226unsafe impl bytemuck::Pod for EntityIdentifier {}
227
228/// Character set specification (ECMA-167 1/7.2.1)
229///
230/// @hadris-spec ECMA-167:1/7.2.1
231/// @hadris-compliance partial
232/// @hadris-note OSTA CS0 writing is tested, but every ECMA-167 character-set constraint is not validated.
233/// @hadris-tests write::cs0_tests::selects_eight_bit_for_latin1, write::cs0_tests::selects_sixteen_bit_for_wide_unicode
234/// @hadris-fuzz udf_read
235#[repr(C)]
236#[derive(Debug, Clone, Copy)]
237pub struct CharSpec {
238    /// Character set type (0 = CS0, OSTA Compressed Unicode)
239    pub char_set_type: u8,
240    /// Character set information (63 bytes)
241    pub char_set_info: [u8; 63],
242}
243
244impl CharSpec {
245    /// OSTA Compressed Unicode (CS0)
246    pub const OSTA_COMPRESSED_UNICODE: Self = {
247        let mut info = [0; 63];
248        let name = b"OSTA Compressed Unicode";
249        let mut index = 0;
250        while index < name.len() {
251            info[index] = name[index];
252            index += 1;
253        }
254        Self {
255            char_set_type: 0,
256            char_set_info: info,
257        }
258    };
259}
260
261impl Default for CharSpec {
262    fn default() -> Self {
263        Self::OSTA_COMPRESSED_UNICODE
264    }
265}
266
267unsafe impl bytemuck::Zeroable for CharSpec {}
268unsafe impl bytemuck::Pod for CharSpec {}
269
270/// Volume Recognition Sequence magic numbers
271pub mod vrs {
272    /// Beginning of Extended Area (BEA01)
273    pub const BEA01: &[u8; 5] = b"BEA01";
274    /// NSR02 - UDF 1.02-1.50
275    pub const NSR02: &[u8; 5] = b"NSR02";
276    /// NSR03 - UDF 2.00+
277    pub const NSR03: &[u8; 5] = b"NSR03";
278    /// Terminal Entry Area (TEA01)
279    pub const TEA01: &[u8; 5] = b"TEA01";
280    /// ISO 9660 CD-ROM
281    pub const CD001: &[u8; 5] = b"CD001";
282}
283
284io_transform! {
285
286/// Parse the Volume Recognition Sequence to detect UDF
287///
288/// Returns the UDF NSR version found, or an error if not UDF
289pub async fn parse_vrs<R: Read + Seek>(reader: &mut R) -> Result<VrsType> {
290    // VRS starts at sector 16
291    reader.seek(SeekFrom::Start(16 * 2048)).await?;
292
293    let mut buffer = [0u8; 2048];
294    let mut found_bea = false;
295    let mut found_nsr = None;
296
297    // Scan up to 16 sectors for VRS
298    for _ in 0..16 {
299        reader.read_exact(&mut buffer).await?;
300
301        // Check structure type (byte 0) and version (byte 6)
302        if buffer[0] != 0 || buffer[6] != 1 {
303            continue;
304        }
305
306        let id = &buffer[1..6];
307        match id {
308            b"BEA01" => found_bea = true,
309            b"NSR02" if found_bea => found_nsr = Some(VrsType::Nsr02),
310            b"NSR03" if found_bea => found_nsr = Some(VrsType::Nsr03),
311            b"TEA01" if found_nsr.is_some() => return Ok(found_nsr.unwrap()),
312            b"CD001" => continue, // ISO 9660 descriptor, skip
313            _ => continue,
314        }
315    }
316
317    match found_nsr {
318        Some(nsr) => Ok(nsr),
319        None => Err(Error::InvalidVrs),
320    }
321}
322
323} // io_transform!
324
325/// VRS type detected
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub enum VrsType {
328    /// NSR02 - UDF 1.02 to 1.50
329    Nsr02,
330    /// NSR03 - UDF 2.00 and later
331    Nsr03,
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    static_assertions::const_assert_eq!(size_of::<ExtentDescriptor>(), 8);
339    static_assertions::const_assert_eq!(size_of::<LongAllocationDescriptor>(), 16);
340    static_assertions::const_assert_eq!(size_of::<ShortAllocationDescriptor>(), 8);
341    static_assertions::const_assert_eq!(size_of::<EntityIdentifier>(), 32);
342    static_assertions::const_assert_eq!(size_of::<CharSpec>(), 64);
343}