Skip to main content

hadris_udf/
dir.rs

1//! UDF Directory operations
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6use super::descriptor::{DescriptorTag, LongAllocationDescriptor, TagIdentifier};
7use crate::error::{Error, Result};
8
9/// A UDF directory entry
10#[derive(Debug, Clone)]
11pub struct UdfDirEntry {
12    /// Entry name
13    pub name: String,
14    /// Whether this is a directory
15    pub is_directory: bool,
16    /// File size in bytes
17    pub size: u64,
18    /// ICB location for this entry
19    pub icb: LongAllocationDescriptor,
20    /// File characteristics
21    pub characteristics: FileCharacteristics,
22}
23
24impl UdfDirEntry {
25    /// Get the entry name
26    pub fn name(&self) -> &str {
27        &self.name
28    }
29
30    /// Check if this is a directory
31    pub fn is_dir(&self) -> bool {
32        self.is_directory
33    }
34
35    /// Check if this is a regular file
36    pub fn is_file(&self) -> bool {
37        !self.is_directory
38    }
39
40    /// Check if this is hidden
41    pub fn is_hidden(&self) -> bool {
42        self.characteristics.contains(FileCharacteristics::HIDDEN)
43    }
44
45    /// Check if this is a parent directory reference (..)
46    pub fn is_parent(&self) -> bool {
47        self.characteristics.contains(FileCharacteristics::PARENT)
48    }
49}
50
51/// File Identifier Descriptor (ECMA-167 4/14.4)
52///
53/// Note: Due to Rust alignment rules, this struct is 40 bytes in memory,
54/// but the on-disk format is 38 bytes. Use `from_bytes` for parsing.
55#[repr(C)]
56#[derive(Debug, Clone, Copy)]
57pub struct FileIdentifierDescriptor {
58    /// Descriptor tag
59    pub tag: DescriptorTag,
60    /// File Version Number
61    pub file_version_number: u16,
62    /// File Characteristics
63    pub file_characteristics: u8,
64    /// Length of File Identifier
65    pub file_identifier_length: u8,
66    /// ICB (Information Control Block)
67    pub icb: LongAllocationDescriptor,
68    /// Length of Implementation Use
69    pub implementation_use_length: u16,
70    // Followed by:
71    // - Implementation Use (implementation_use_length bytes)
72    // - File Identifier (file_identifier_length bytes)
73    // - Padding to 4-byte boundary
74}
75
76unsafe impl bytemuck::Zeroable for FileIdentifierDescriptor {}
77unsafe impl bytemuck::Pod for FileIdentifierDescriptor {}
78
79impl FileIdentifierDescriptor {
80    /// Base size without variable-length fields (on-disk format)
81    /// Note: The Rust struct is 40 bytes due to alignment padding,
82    /// so we parse fields manually in from_bytes()
83    pub const BASE_SIZE: usize = 38;
84
85    /// Calculate total size of this FID
86    pub fn total_size(&self) -> usize {
87        let base = Self::BASE_SIZE;
88        let variable =
89            self.implementation_use_length as usize + self.file_identifier_length as usize;
90        // Pad to 4-byte boundary
91        (base + variable + 3) & !3
92    }
93
94    /// Parse from a byte buffer
95    pub fn from_bytes(data: &[u8]) -> Result<(Self, &[u8])> {
96        if data.len() < Self::BASE_SIZE {
97            return Err(Error::Io(hadris_io::Error::new(
98                hadris_io::ErrorKind::UnexpectedEof,
99                "buffer too small for FID",
100            )));
101        }
102
103        // Parse fields manually due to alignment differences between
104        // on-disk format (38 bytes packed) and Rust struct (40 bytes aligned)
105        let tag = DescriptorTag::from_disk_bytes(&data[0..16])?;
106        let file_version_number = u16::from_le_bytes([data[16], data[17]]);
107        let file_characteristics = data[18];
108        let file_identifier_length = data[19];
109        // `data` starts at an arbitrary FID offset, so the ICB field can be
110        // unaligned; read it without requiring alignment.
111        let icb =
112            bytemuck::pod_read_unaligned::<LongAllocationDescriptor>(&data[20..36]).into_native();
113        let implementation_use_length = u16::from_le_bytes([data[36], data[37]]);
114
115        let fid = Self {
116            tag,
117            file_version_number,
118            file_characteristics,
119            file_identifier_length,
120            icb,
121            implementation_use_length,
122        };
123
124        if fid.tag.identifier() != TagIdentifier::FileIdentifierDescriptor {
125            return Err(Error::InvalidTag {
126                expected: TagIdentifier::FileIdentifierDescriptor.to_u16(),
127                found: fid.tag.tag_identifier,
128            });
129        }
130
131        let total_size = fid.total_size();
132        if data.len() < total_size {
133            return Err(Error::Io(hadris_io::Error::new(
134                hadris_io::ErrorKind::UnexpectedEof,
135                "buffer too small for FID data",
136            )));
137        }
138
139        Ok((fid, &data[Self::BASE_SIZE..total_size]))
140    }
141}
142
143bitflags::bitflags! {
144    /// File characteristics (ECMA-167 4/14.4.3)
145    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
146    pub struct FileCharacteristics: u8 {
147        /// Existence flag (file exists if set)
148        const EXISTENCE = 0x01;
149        /// Directory flag
150        const DIRECTORY = 0x02;
151        /// Deleted flag
152        const DELETED = 0x04;
153        /// Parent directory entry (..)
154        const PARENT = 0x08;
155        /// Metadata flag
156        const METADATA = 0x10;
157        /// Hidden flag (UDF extension)
158        const HIDDEN = 0x20;
159    }
160}
161
162/// UDF Directory handle
163pub struct UdfDir {
164    /// Directory entries
165    entries: Vec<UdfDirEntry>,
166}
167
168impl UdfDir {
169    /// Create a new directory from parsed entries
170    pub(crate) fn new(entries: Vec<UdfDirEntry>) -> Self {
171        Self { entries }
172    }
173
174    /// Get directory entries
175    pub fn entries(&self) -> impl Iterator<Item = &UdfDirEntry> {
176        self.entries.iter().filter(|e| !e.is_parent())
177    }
178
179    /// Get all entries including parent
180    pub fn all_entries(&self) -> impl Iterator<Item = &UdfDirEntry> {
181        self.entries.iter()
182    }
183
184    /// Find an entry by name
185    pub fn find(&self, name: &str) -> Option<&UdfDirEntry> {
186        self.entries.iter().find(|e| e.name == name)
187    }
188
189    /// Get the number of entries (excluding parent)
190    pub fn len(&self) -> usize {
191        self.entries.iter().filter(|e| !e.is_parent()).count()
192    }
193
194    /// Check if directory is empty
195    pub fn is_empty(&self) -> bool {
196        self.len() == 0
197    }
198}
199
200/// Decode a UDF filename from CS0 (OSTA Compressed Unicode)
201pub fn decode_filename(data: &[u8]) -> String {
202    if data.is_empty() {
203        return String::new();
204    }
205
206    let compression_id = data[0];
207    let content = &data[1..];
208
209    match compression_id {
210        8 => {
211            // CS0 compression ID 8 stores one Unicode code point per byte.
212            content.iter().map(|byte| char::from(*byte)).collect()
213        }
214        16 => {
215            // 16-bit characters (UTF-16 BE)
216            let mut result = String::new();
217            for chunk in content.chunks(2) {
218                if chunk.len() == 2 {
219                    let code_unit = u16::from_be_bytes([chunk[0], chunk[1]]);
220                    if let Some(c) = char::from_u32(code_unit as u32) {
221                        result.push(c);
222                    }
223                }
224            }
225            result
226        }
227        _ => String::new(),
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    // Note: Rust struct is 40 bytes (with alignment padding), but on-disk is 38 bytes
236    static_assertions::const_assert_eq!(size_of::<FileIdentifierDescriptor>(), 40);
237
238    #[test]
239    fn test_file_characteristics() {
240        let chars = FileCharacteristics::DIRECTORY | FileCharacteristics::EXISTENCE;
241        assert!(chars.contains(FileCharacteristics::DIRECTORY));
242        assert!(!chars.contains(FileCharacteristics::HIDDEN));
243    }
244
245    #[test]
246    fn test_decode_filename_8bit() {
247        let data = [8, b'h', b'e', b'l', b'l', b'o'];
248        assert_eq!(decode_filename(&data), "hello");
249    }
250
251    #[test]
252    fn test_decode_filename_16bit() {
253        // UTF-16 BE: "hi"
254        let data = [16, 0x00, b'h', 0x00, b'i'];
255        assert_eq!(decode_filename(&data), "hi");
256    }
257}