Skip to main content

libcdio_rs/udf/
entry.rs

1// Copyright (C) 2026 Shiva Kiran Koninty <shiva@skran.xyz>
2//
3// This file is part of libcdio-rs.
4//
5// libcdio-rs is free software: you can redistribute it and/or
6// modify it under the terms of the GNU General Public License as
7// published by the Free Software Foundation, either version 3 of the
8// License, or (at your option) any later version.
9//
10// libcdio-rs is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13// General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with libcdio-rs. If not, see <https://www.gnu.org/licenses/>.
17
18//! Routines related to UDF filesystem entries.
19
20use std::{
21    error::Error,
22    ffi::{CStr, CString},
23    io,
24    marker::PhantomData,
25    ptr::NonNull,
26};
27
28use file_mode::Mode;
29use libcdio_sys::{udf_dirent_s, udf_t};
30use thiserror::Error;
31use time::OffsetDateTime;
32
33use crate::udf::Udf;
34
35impl Udf {
36    /// Returns the root entry of the UDF filesystem.
37    pub fn root(&self) -> Result<UdfEntry<'_>, UdfGetEntryError> {
38        // SAFETY: UdfEntry will own the returned value.
39        let entry = unsafe { libcdio_sys::udf_get_root(self.udf.as_ptr(), true, 0) };
40
41        NonNull::new(entry)
42            .map(UdfEntry::new)
43            .ok_or_else(|| UdfGetEntryError::new("/", "udf_get_root() returned NULL".into()))
44    }
45
46    /// Returns the root entry of the UDF filesystem, at the given partition.
47    pub fn root_from_partition(&self, partition: u16) -> Result<UdfEntry<'_>, UdfGetEntryError> {
48        // SAFETY: UdfEntry will own the returned value.
49        let entry = unsafe { libcdio_sys::udf_get_root(self.udf.as_ptr(), false, partition) };
50
51        NonNull::new(entry)
52            .map(UdfEntry::new)
53            .ok_or_else(|| UdfGetEntryError::new("/", "udf_get_root() returned NULL".into()))
54    }
55
56    /// Returns UDF entry at `path`.
57    ///
58    /// Only Unix-style `/` may be used as a path separator.
59    pub fn entry(&self, path: String) -> Result<UdfEntry<'_>, UdfGetEntryError> {
60        let root = self.root()?;
61        let path = CString::new(path).map_err(|err| {
62            UdfGetEntryError::new(
63                String::from_utf8(err.clone().into_vec()).expect("path was a valid string"),
64                err.into(),
65            )
66        })?;
67        // SAFETY: UdfEntry will own the returned value.
68        let entry = unsafe { libcdio_sys::udf_fopen(root.entry.as_ptr(), path.as_ptr()) };
69
70        NonNull::new(entry).map(UdfEntry::new).ok_or_else(|| {
71            UdfGetEntryError::new(
72                path.into_string()
73                    .expect("path was originally a valid string"),
74                "udf_fopen() returned NULL".into(),
75            )
76        })
77    }
78}
79
80#[derive(Debug, Error)]
81#[error(transparent)]
82pub struct UdfGetEntryError(Box<GetEntryErrRepr>);
83
84#[derive(Debug, Error)]
85#[error("could not get UDF entry at `{path}`")]
86struct GetEntryErrRepr {
87    path: String,
88    source: Box<dyn Error + Send + Sync>,
89}
90
91impl UdfGetEntryError {
92    /// The path of the UDF entry that caused the error.
93    pub fn path(&self) -> &str {
94        &self.0.path
95    }
96    fn new(path: impl Into<String>, source: Box<dyn Error + Send + Sync>) -> Self {
97        Self(Box::new(GetEntryErrRepr {
98            path: path.into(),
99            source,
100        }))
101    }
102}
103
104/// A UDF file/directory entry.
105pub struct UdfEntry<'a> {
106    entry: NonNull<udf_dirent_s>,
107    pub gid: u32,
108    pub uid: u32,
109    // udf_dirent_s has internal references to its parent udf_t
110    _parent: PhantomData<&'a udf_t>,
111}
112
113impl UdfEntry<'_> {
114    /// Returns the modification time.
115    pub fn modify_time(&self) -> Result<OffsetDateTime, UdfInvalidEntryError> {
116        // SAFETY: Returns -1 in case the value is invalid, checked immediately below
117        let time = unsafe { libcdio_sys::udf_get_modification_time(self.entry.as_ptr()) };
118        if time == -1 {
119            return Err(UdfInvalidEntryError::new(
120                self.filename().ok(),
121                "udf_get_modification_time() returned -1".into(),
122            ));
123        }
124
125        OffsetDateTime::from_unix_timestamp(time)
126            .map_err(|err| UdfInvalidEntryError::new(self.filename().ok(), err.into()))
127    }
128
129    /// Returns the file name.
130    pub fn filename(&self) -> Result<&str, UdfInvalidEntryError> {
131        const CURRENT_DIR_FILENAME: &str = ".";
132
133        // SAFETY: self.entry is non null, therefore this method should not return null
134        let filename = unsafe { libcdio_sys::udf_get_filename(self.entry.as_ptr()) };
135        if filename.is_null() {
136            return Err(UdfInvalidEntryError::new(
137                Option::<&str>::None,
138                "udf_get_filename() returned NULL".into(),
139            ));
140        }
141        let filename = unsafe { CStr::from_ptr(filename) };
142        // filename returns an empty string after opening the root directory.
143        // this probably represents "."
144        if filename.is_empty() {
145            return Ok(CURRENT_DIR_FILENAME);
146        }
147
148        filename
149            .to_str()
150            .map_err(|err| UdfInvalidEntryError::new(Option::<&str>::None, err.into()))
151    }
152
153    /// Returns the next entry.
154    pub fn next(self) -> Option<Self> {
155        // SAFETY: This function moves self. Use mem::forget to stop the destructor.
156        let next_entry = unsafe { libcdio_sys::udf_readdir(self.entry.as_ptr()) };
157        std::mem::forget(self);
158
159        NonNull::new(next_entry).map(Self::new)
160    }
161
162    /// Opens `self` and return the first entry.
163    pub fn open_dir(&self) -> Option<Self> {
164        let sub_entry = unsafe { libcdio_sys::udf_opendir(self.entry.as_ptr()) };
165
166        Some(Self::new(NonNull::new(sub_entry)?))
167    }
168
169    /// Checks if the entry is a directory.
170    pub fn is_dir(&self) -> bool {
171        unsafe { libcdio_sys::udf_is_dir(self.entry.as_ptr()) }
172    }
173
174    /// Returns the file length.
175    pub fn file_length(&self) -> u64 {
176        // SAFETY: entry is not null, making this function infallible
177        unsafe { libcdio_sys::udf_get_file_length(self.entry.as_ptr()) }
178    }
179
180    /// Returns the POSIX file mode.
181    pub fn mode(&self) -> Mode {
182        // `mode_t` is non-portable (16 or 32 bit)
183        #[allow(clippy::useless_conversion)]
184        let mode = u32::from(unsafe { libcdio_sys::udf_get_posix_filemode(self.entry.as_ptr()) });
185        Mode::new(mode, u32::MAX)
186    }
187
188    /// Returns the number of hard links of the entry.
189    pub fn link_count(&self) -> u16 {
190        unsafe { libcdio_sys::udf_get_link_count(self.entry.as_ptr()) }
191    }
192
193    /// Returns a type that implements [`io::Read`] to allow for reading the
194    /// file data of a UDF entry.
195    pub fn reader(&self) -> UdfEntryReader<'_> {
196        UdfEntryReader {
197            bytes_read: 0,
198            entry: self,
199        }
200    }
201
202    fn new(entry: NonNull<udf_dirent_s>) -> Self {
203        let uid = unsafe { (*entry.as_ptr()).fe.uid };
204        let gid = unsafe { (*entry.as_ptr()).fe.gid };
205
206        Self {
207            entry,
208            gid: u32::from_le(gid),
209            uid: u32::from_le(uid),
210            _parent: PhantomData,
211        }
212    }
213}
214
215impl Drop for UdfEntry<'_> {
216    fn drop(&mut self) {
217        let _ = unsafe { libcdio_sys::udf_dirent_free(self.entry.as_ptr()) };
218    }
219}
220
221/// UDF entry has invalid data
222#[derive(Debug, Error)]
223#[error(transparent)]
224pub struct UdfInvalidEntryError(Box<InvalidEntryErrRepr>);
225
226#[derive(Debug, Error)]
227#[error("found invalid data in UDF entry `{}`", name.as_deref().unwrap_or_default())]
228struct InvalidEntryErrRepr {
229    name: Option<String>,
230    source: Box<dyn Error + Send + Sync>,
231}
232
233impl UdfInvalidEntryError {
234    /// Returns file name of the entry with invalid data.
235    pub fn name(&self) -> Option<&str> {
236        self.0.name.as_deref()
237    }
238    fn new(name: Option<impl Into<String>>, source: Box<dyn Error + Send + Sync>) -> Self {
239        Self(Box::new(InvalidEntryErrRepr {
240            name: name.map(Into::into),
241            source,
242        }))
243    }
244}
245
246/// A type that implements [`io::Read`], to allow for reading the
247/// file data of a UDF entry.
248// This is NOT thread safe, as udf_dirent_s internally holds
249// its current file position with a non-atomic integer
250pub struct UdfEntryReader<'a> {
251    bytes_read: usize,
252    entry: &'a UdfEntry<'a>,
253}
254
255impl UdfEntryReader<'_> {
256    /// Sets the current file position of the entry.
257    fn set_position(&mut self, block_num: usize) {
258        // SAFETY: UdfEntryReader and UdfEntry are not marked
259        // as thread safe
260        let _ = unsafe {
261            libcdio_sys::udf_setpos(
262                self.entry.entry.as_ptr(),
263                (block_num * Udf::BLOCK_SIZE)
264                    .try_into()
265                    .expect("block's byte offset should fit an `off_t`"),
266            )
267        };
268    }
269}
270
271impl io::Read for UdfEntryReader<'_> {
272    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
273        let file_size = self.entry.file_length() as usize;
274        let mut buf_read = 0;
275
276        // As of writing, udf_dirent_s stores the current file position
277        // at p_udf->i_position. This is used by libcdio's UDF read and UDF
278        // seek routines.
279        // This causes state leakage when more than one instance of
280        // UdfEntryReader are used from the same UdfEntry.
281        // Fix this by resetting the file position value to zero before
282        // actions that change it.
283        self.set_position(0);
284
285        while self.bytes_read < file_size && buf_read < buf.len() {
286            let block_num = self.bytes_read / Udf::BLOCK_SIZE;
287            self.set_position(block_num);
288            let mut block = [0_u8; Udf::BLOCK_SIZE];
289            let ret = unsafe {
290                libcdio_sys::udf_read_block(self.entry.entry.as_ptr(), block.as_mut_ptr().cast(), 1)
291            };
292            // cast is safe as Udf::BLOCK_SIZE < i16::MAX
293            if ret != block.len() as _ {
294                return Err(io::Error::other(format!(
295                    "error reading udf block number: {block_num}",
296                )));
297            }
298            let block_start = self.bytes_read % block.len();
299            let buf_rem = buf.len() - buf_read;
300            let block_rem = (block.len() - block_start).min(file_size - self.bytes_read);
301            let len = buf_rem.min(block_rem);
302            buf[buf_read..buf_read + len].copy_from_slice(&block[block_start..block_start + len]);
303            buf_read += len;
304            self.bytes_read += len;
305        }
306
307        Ok(buf_read)
308    }
309}
310
311impl io::Seek for UdfEntryReader<'_> {
312    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
313        self.bytes_read = match pos {
314            io::SeekFrom::Start(offset) => offset as usize,
315            io::SeekFrom::End(offset) => {
316                self.entry.file_length().saturating_add_signed(offset) as usize
317            }
318            io::SeekFrom::Current(offset) => self.bytes_read.saturating_add_signed(offset as isize),
319        };
320
321        Ok(self.bytes_read as u64)
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use std::{io::Read, path::PathBuf};
328
329    use time::macros::datetime;
330
331    use crate::udf::tests::test_udf_file;
332
333    use super::*;
334
335    fn test_udf_file1() -> PathBuf {
336        PathBuf::from("tests/data/udf1.iso")
337    }
338
339    #[test]
340    fn root() {
341        let udf = Udf::new(test_udf_file()).unwrap();
342        udf.root().unwrap();
343    }
344
345    #[test]
346    fn root_from_partition() {
347        let udf = Udf::new(test_udf_file()).unwrap();
348        udf.root_from_partition(0).unwrap();
349    }
350
351    #[test]
352    fn modify_time() {
353        let udf = Udf::new(test_udf_file()).unwrap();
354        let modify_time = udf.root().unwrap().modify_time().unwrap();
355        assert_eq!(modify_time, datetime!(2014-02-20 1:26:20.0 +00:00:00));
356    }
357
358    #[test]
359    fn filename() {
360        let udf = Udf::new(test_udf_file()).unwrap();
361        let root = udf.root().unwrap();
362        assert_eq!(root.filename().unwrap(), "/");
363    }
364
365    #[test]
366    fn next() {
367        let udf = Udf::new(test_udf_file()).unwrap();
368        let root = udf.root().unwrap();
369        let next = root.next().unwrap();
370        assert_eq!(next.filename().unwrap(), ".");
371
372        let next = next.next().unwrap();
373        assert_eq!(next.filename().unwrap(), "FéжΘvrier");
374    }
375
376    #[test]
377    fn is_dir() {
378        let udf = Udf::new(test_udf_file()).unwrap();
379        let root = udf.root().unwrap();
380        assert!(root.is_dir());
381    }
382
383    #[test]
384    fn file_length() {
385        let udf = Udf::new(test_udf_file()).unwrap();
386        let root = udf.root().unwrap();
387        let file = root.next().unwrap().next().unwrap();
388        assert_eq!(file.file_length(), 10);
389    }
390
391    #[test]
392    fn mode() {
393        let udf = Udf::new(test_udf_file()).unwrap();
394        let root = udf.root().unwrap();
395        let entry = root.next().unwrap();
396        let entry = entry.next().unwrap();
397        assert_eq!(&entry.mode().to_string(), "-r-xr-xr-x");
398    }
399
400    #[test]
401    fn link_count() {
402        let udf = Udf::new(test_udf_file()).unwrap();
403        let root = udf.root().unwrap();
404        let entry = root.next().unwrap().next().unwrap();
405        assert_eq!(entry.link_count(), 1);
406    }
407
408    #[test]
409    fn fields() {
410        let udf = Udf::new(test_udf_file1()).unwrap();
411        let root = udf.root().unwrap();
412        let entry = root.next().unwrap().next().unwrap();
413        assert_eq!(entry.uid, 2000);
414        assert_eq!(entry.gid, 3000);
415    }
416
417    #[test]
418    fn open_dir() {
419        let udf = Udf::new(test_udf_file1()).unwrap();
420        let entry = udf.root().unwrap();
421        // /licenses
422        let entry = entry.next().unwrap().next().unwrap();
423        // /licenses/.
424        entry.open_dir().unwrap();
425    }
426
427    #[test]
428    fn read() {
429        let udf = Udf::new(test_udf_file1()).unwrap();
430        let root = udf.root().unwrap();
431        // /licenses
432        let entry = root.next().unwrap().next().unwrap();
433        // /licenses/.
434        let entry = entry.open_dir().unwrap().next().unwrap();
435        // /licenses/COPYING
436        let entry = entry.next().unwrap();
437
438        let mut reader = entry.reader();
439        let mut contents = String::new();
440        let bytes_read = reader.read_to_string(&mut contents).unwrap();
441
442        let gpl = std::fs::read_to_string("COPYING").unwrap();
443        assert_eq!(gpl.len(), bytes_read);
444        assert_eq!(gpl, contents);
445    }
446
447    #[test]
448    fn entry() {
449        let udf = Udf::new(test_udf_file1()).unwrap();
450        udf.entry("/licenses/COPYING.LESSER".to_owned()).unwrap();
451    }
452}