Skip to main content

libcdio_rs/iso9660/
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 ISO 9660 entries.
19
20use std::{
21    error::Error,
22    ffi::{CStr, CString},
23    io,
24    ptr::NonNull,
25};
26
27use libcdio_sys::{iso9660_stat_s, iso9660_stat_s__STAT_DIR};
28use thiserror::Error;
29use time::OffsetDateTime;
30
31use crate::iso9660::{Iso, util};
32
33impl Iso {
34    /// Returns a list of entries under `path`.
35    ///
36    /// Only Unix-style `/` may be used as a separator.
37    pub fn read_dir(&self, path: String) -> Result<Vec<IsoEntry<'_>>, IsoGetEntryError> {
38        let path = CString::new(path).map_err(|err| {
39            IsoGetEntryError::new(
40                String::from_utf8(err.clone().into_vec()).expect("path was a valid string"),
41                err.into(),
42            )
43        })?;
44        let dirlist = unsafe { libcdio_sys::iso9660_ifs_readdir(self.ptr.as_ptr(), path.as_ptr()) };
45        if dirlist.is_null() {
46            return Err(IsoGetEntryError::new(
47                path.into_string().expect("path was a valid string"),
48                "iso9660_ifs_readdir() returned NULL".into(),
49            ));
50        }
51        // SAFETY: dirlist is not null and the data will be owned by `IsoEntry`.
52        let dirlist = unsafe { util::cdiolist_to_vec(dirlist) };
53        let dirlist = dirlist
54            .into_iter()
55            .filter_map(|entry| {
56                Some(IsoEntry {
57                    iso: self,
58                    stat: NonNull::new(entry.cast())?,
59                })
60            })
61            .collect();
62
63        Ok(dirlist)
64    }
65
66    /// Returns ISO 9660 entry at `path`.
67    pub fn entry(&self, path: String) -> Result<IsoEntry<'_>, IsoGetEntryError> {
68        let path = CString::new(path).map_err(|err| {
69            IsoGetEntryError::new(
70                String::from_utf8(err.clone().into_vec()).expect("path was a valid string"),
71                err.into(),
72            )
73        })?;
74        let stat = unsafe { libcdio_sys::iso9660_ifs_stat(self.ptr.as_ptr(), path.as_ptr()) };
75
76        NonNull::new(stat)
77            .ok_or_else(|| {
78                IsoGetEntryError::new(
79                    path.into_string().expect("path was a valid string"),
80                    "iso9660_ifs_stat() returned NULL".into(),
81                )
82            })
83            .map(|stat| IsoEntry { iso: self, stat })
84    }
85}
86
87#[derive(Debug, Error)]
88#[error(transparent)]
89pub struct IsoGetEntryError(Box<GetEntryErrRepr>);
90
91#[derive(Debug, Error)]
92#[error("could not get ISO 9660 entry at `{path}`")]
93struct GetEntryErrRepr {
94    path: String,
95    source: Box<dyn Error + Send + Sync>,
96}
97
98impl IsoGetEntryError {
99    /// Returns the path of the ISO 9660 entry.
100    pub fn path(&self) -> &str {
101        &self.0.path
102    }
103
104    fn new(path: impl Into<String>, source: Box<dyn Error + Send + Sync>) -> Self {
105        Self(Box::new(GetEntryErrRepr {
106            path: path.into(),
107            source,
108        }))
109    }
110}
111
112/// ISO 9660 file/directory entry.
113pub struct IsoEntry<'a> {
114    /// The parent ISO 9660 object
115    pub(crate) iso: &'a Iso,
116    pub(crate) stat: NonNull<iso9660_stat_s>,
117}
118
119impl IsoEntry<'_> {
120    /// Returns the raw filename of the entry.
121    ///
122    /// See [`Self::filename()`]
123    pub fn filename_raw(&self) -> Result<&str, IsoInvalidEntryError> {
124        // SAFETY: self.entry is not null since its behind a NonNull<T>
125        let name = unsafe { (*self.stat.as_ptr()).filename.as_ptr() };
126        if name.is_null() {
127            return Err(IsoInvalidEntryError::new(
128                Default::default(),
129                "iso9660_stat_s.filename is NULL".into(),
130            ));
131        };
132
133        // SAFETY: The filename should be a null terminated string
134        unsafe { CStr::from_ptr(name).to_str() }
135            .map_err(|err| IsoInvalidEntryError::new(Default::default(), err.into()))
136    }
137
138    /// Returns the entry's filename.
139    pub fn filename(&self) -> Result<String, IsoInvalidEntryError> {
140        let filename = unsafe { (*self.stat.as_ptr()).filename.as_ptr() };
141        if filename.is_null() {
142            return Err(IsoInvalidEntryError::new(
143                Default::default(),
144                "iso9660_stat_s.filename is NULL".into(),
145            ));
146        }
147
148        let filename = unsafe { CStr::from_ptr(filename) };
149        let mut translated_name = vec![0; filename.count_bytes() + 1];
150        let joliet_level = self.iso.joliet_level().map(u8::from).unwrap_or(0);
151
152        let len = unsafe {
153            libcdio_sys::iso9660_name_translate_ext(
154                filename.as_ptr(),
155                translated_name.as_mut_ptr().cast(),
156                joliet_level,
157            )
158        };
159        // iso9660_name_translate_ext will not return negative numbers,
160        // therefore the cast should be safe
161        translated_name.truncate(len as usize);
162
163        String::from_utf8(translated_name)
164            .map_err(|err| IsoInvalidEntryError::new(Default::default(), err.into()))
165    }
166
167    /// Returns Multi-extent aware file size, in bytes.
168    pub fn total_size(&self) -> u64 {
169        unsafe { (*self.stat.as_ptr()).total_size }
170    }
171
172    /// Return the logical sector number.
173    pub fn lsn(&self) -> i32 {
174        unsafe { (*self.stat.as_ptr()).lsn }
175    }
176
177    /// Returns `true` if the stat represents a directory.
178    pub fn is_dir(&self) -> bool {
179        unsafe { (*self.stat.as_ptr()).type_ == iso9660_stat_s__STAT_DIR }
180    }
181
182    /// Returns the timestamp on the entry.
183    pub fn timestamp(&self) -> Result<OffsetDateTime, IsoInvalidEntryError> {
184        let tm = unsafe { (*self.stat.as_ptr()).tm };
185        util::convert_tm_local(tm)
186            .map_err(|err| IsoInvalidEntryError::new(self.filename().unwrap_or_default(), err))
187    }
188
189    /// Returns a type that implements [`io::Read`], for reading an ISO 9660 entry.
190    pub fn reader(&self) -> IsoEntryReader<'_> {
191        IsoEntryReader {
192            bytes_read: 0,
193            entry: self,
194        }
195    }
196}
197
198impl Drop for IsoEntry<'_> {
199    fn drop(&mut self) {
200        unsafe { libcdio_sys::iso9660_stat_free(self.stat.as_ptr()) }
201    }
202}
203
204#[derive(Debug, Error)]
205#[error(transparent)]
206pub struct IsoInvalidEntryError(Box<InvalidEntryErrRepr>);
207
208#[derive(Debug, Error)]
209#[error("inavlid data in ISO 9660 entry named `{name}`")]
210struct InvalidEntryErrRepr {
211    name: String,
212    source: Box<dyn Error + Send + Sync>,
213}
214
215impl IsoInvalidEntryError {
216    /// Returns the name of the ISO 9660 entry.
217    pub fn name(&self) -> &str {
218        &self.0.name
219    }
220
221    fn new(name: String, source: Box<dyn Error + Send + Sync>) -> Self {
222        Self(Box::new(InvalidEntryErrRepr { name, source }))
223    }
224}
225
226/// A type that implements [`io::Read`], for reading an ISO 9660 entry.
227pub struct IsoEntryReader<'a> {
228    bytes_read: usize,
229    entry: &'a IsoEntry<'a>,
230}
231
232impl io::Read for IsoEntryReader<'_> {
233    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
234        let file_size = self.entry.total_size() as usize;
235        let mut buf_read = 0;
236        while self.bytes_read < file_size && buf_read < buf.len() {
237            let lsn = self.entry.lsn() + (self.bytes_read / Iso::BLOCK_SIZE) as i32;
238            let mut block = [0_u8; Iso::BLOCK_SIZE];
239            let ret = unsafe {
240                libcdio_sys::iso9660_iso_seek_read(
241                    self.entry.iso.ptr.as_ptr(),
242                    block.as_mut_ptr().cast(),
243                    lsn,
244                    1,
245                )
246            };
247            // the returned value is either BLOCK_SIZE or zero on error, thus
248            // excess bytes past the last read must be handled.
249            // cast is safe as Iso::BLOCK_SIZE < i16::MAX
250            if ret != block.len() as _ {
251                return Err(io::Error::other(format!(
252                    "error reading block at lsn: {lsn}",
253                )));
254            }
255
256            // offset start to skip the first bytes given out during
257            // a previous partial read() call
258            let block_start = self.bytes_read % block.len();
259            let buf_rem = buf.len() - buf_read;
260            // skip out the excess bytes past file_size using .min()
261            let block_rem = (block.len() - block_start).min(file_size - self.bytes_read);
262            let len = buf_rem.min(block_rem);
263            buf[buf_read..buf_read + len].copy_from_slice(&block[block_start..block_start + len]);
264            buf_read += len;
265            self.bytes_read += len;
266        }
267
268        Ok(buf_read)
269    }
270}
271
272impl io::Seek for IsoEntryReader<'_> {
273    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
274        self.bytes_read = match pos {
275            io::SeekFrom::Start(offset) => offset as usize,
276            io::SeekFrom::End(offset) => {
277                self.entry.total_size().saturating_add_signed(offset) as usize
278            }
279            io::SeekFrom::Current(offset) => self.bytes_read.saturating_add_signed(offset as isize),
280        };
281
282        Ok(self.bytes_read as u64)
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use std::{io::Read, path::PathBuf};
289
290    use time::macros::datetime;
291
292    use crate::iso9660::{
293        Iso,
294        tests::{test_joliet_file, test_rockridge_file},
295    };
296
297    #[test]
298    fn read_dir() {
299        let iso = Iso::new(test_joliet_file()).unwrap();
300        let entries = iso.read_dir("/".to_owned()).unwrap();
301        assert_eq!(entries.len(), 3);
302    }
303
304    #[test]
305    fn filename() {
306        let iso = Iso::new(test_rockridge_file()).unwrap();
307        let entries = iso.read_dir("/".to_owned()).unwrap();
308        let names: Vec<_> = entries.iter().map(|e| e.filename_raw().unwrap()).collect();
309        assert_eq!(
310            &names,
311            &[".", "..", "copy", "Copy2", "COPYING", "fd0", "tmp", "zero"]
312        );
313    }
314
315    #[test]
316    fn filename_translated() {
317        let iso = Iso::new(test_rockridge_file()).unwrap();
318        let entries = iso.read_dir("/".to_owned()).unwrap();
319        let names: Vec<_> = entries.iter().map(|e| e.filename().unwrap()).collect();
320        assert_eq!(
321            &names,
322            &[".", "..", "copy", "copy2", "copying", "fd0", "tmp", "zero"]
323        );
324    }
325
326    #[test]
327    fn entry() {
328        let iso = Iso::new(test_rockridge_file()).unwrap();
329        let entry = iso.entry("/copy".to_string()).unwrap();
330        assert_eq!(entry.filename().unwrap(), "copy");
331    }
332
333    #[test]
334    fn total_size() {
335        let iso = Iso::new(test_rockridge_file()).unwrap();
336        let entry = iso.entry("/COPYING".to_string()).unwrap();
337        assert_eq!(entry.total_size(), 17992);
338    }
339
340    #[test]
341    fn lsn() {
342        let iso = Iso::new(test_rockridge_file()).unwrap();
343        let entry = iso.entry("/COPYING".to_string()).unwrap();
344        assert_eq!(entry.lsn(), 27);
345    }
346
347    #[test]
348    fn is_dir() {
349        let iso = Iso::new(test_rockridge_file()).unwrap();
350        let file = iso.entry("/COPYING".to_string()).unwrap();
351        assert!(!file.is_dir());
352
353        let dir = iso.entry("/copy".to_string()).unwrap();
354        assert!(dir.is_dir());
355    }
356
357    #[test]
358    fn timestamp() {
359        let iso = Iso::new(test_rockridge_file()).unwrap();
360        let entry = iso.entry("/COPYING".to_string()).unwrap();
361        assert_eq!(
362            entry.timestamp().unwrap(),
363            datetime!(2005-03-05 20:55:51.0 +05:30:00),
364        );
365    }
366
367    #[test]
368    fn read() {
369        let iso = Iso::new(PathBuf::from("tests/data/xa.iso")).unwrap();
370        let entry = iso.entry("copying".to_string()).unwrap();
371        let gpl = std::fs::read_to_string("COPYING").unwrap();
372        let mut reader = entry.reader();
373
374        let mut result = String::new();
375        let retval = reader.read_to_string(&mut result).unwrap();
376        assert_eq!(gpl.len(), retval);
377        assert_eq!(gpl, result);
378    }
379}