cdfs/
lib.rs

1// SPDX-License-Identifier: (MIT OR Apache-2.0)
2
3//! # cdfs
4//!
5//! `cdfs` is a portable, userland implementation of the ISO 9660 / ECMA-119 filesystem typically found on CDs and DVDs.
6//!
7//! # Usage
8//!
9//! To open an ISO image:
10//! ```rust
11//! # use std::fs::File;
12//! use cdfs::{DirectoryEntry, ISO9660};
13//!
14//! let file = File::open("images/test.iso")?;
15//! let iso = ISO9660::new(file)?;
16//! # Ok::<(), cdfs::ISOError>(())
17//! ```
18//!
19//! To read a file:
20//! ```rust
21//! # use std::{fs::File, io::Read};
22//! # use cdfs::{DirectoryEntry, ISO9660};
23//! # let file = File::open("images/test.iso")?;
24//! # let iso = ISO9660::new(file)?;
25//! let mut contents = Vec::new();
26//! if let Some(DirectoryEntry::File(file)) = iso.open("README.md")? {
27//!   file.read().read_to_end(&mut contents)?;
28//! }
29//! # Ok::<(), cdfs::ISOError>(())
30//! ```
31//!
32//! To iterate over items in a directory:
33//! ```rust
34//! # use std::fs::File;
35//! # use cdfs::{DirectoryEntry, ISO9660};
36//! # let file = File::open("images/test.iso")?;
37//! # let iso = ISO9660::new(file)?;
38//! if let Some(DirectoryEntry::Directory(dir)) = iso.open("/tmp")? {
39//!   for entry in dir.contents() {
40//!     println!("{}", entry?.identifier());
41//!   }
42//! }
43//! # Ok::<(), cdfs::ISOError>(())
44//! ```
45//!
46//! To get information about a file:
47//!
48//! ```rust
49//! # use std::fs::File;
50//! # use cdfs::{ISO9660, ExtraAttributes};
51//! let file = File::open("images/test.iso")?;
52//! let iso = ISO9660::new(file)?;
53//! let obj = iso.open("GPL_3_0.TXT")?.expect("GPL_3_0.TXT doesn't exist");
54//! println!("Last modified at: {:?}", obj.modify_time());
55//! # Ok::<(), cdfs::ISOError>(())
56//! ```
57//!
58//! # See Also
59//!
60//! The examples.
61
62#![warn(missing_docs)]
63
64/// [`Result`](std::result::Result) that returns an [`ISOError`].
65pub type Result<T> = std::result::Result<T, ISOError>;
66
67mod directory_entry;
68mod error;
69mod fileref;
70mod parse;
71
72use fileref::FileRef;
73use parse::volume_descriptor::VolumeDescriptor;
74
75pub use directory_entry::{
76    DirectoryEntry, ExtraAttributes, ExtraMeta, ISODirectory, ISODirectoryIterator, ISOFile,
77    ISOFileReader, PosixAttributes, PosixFileMode, PosixTimestamp, SuspExtension, Symlink,
78};
79pub use error::ISOError;
80pub use fileref::ISO9660Reader;
81
82/// Struct representing an ISO 9660 / ECMA-119 filesystem.
83pub struct ISO9660<T: ISO9660Reader> {
84    _file: FileRef<T>,
85    root: ISODirectory<T>,
86    sup_root: Option<ISODirectory<T>>,
87    primary: VolumeDescriptor,
88}
89
90/// The size of a filesystem block, currently hardcoded to 2048 although the ISO spec allows for other sizes.
91pub const BLOCK_SIZE: u16 = 2048;
92
93/// A `u8` array big enough to hold an entire filesystem block.
94pub type BlockBuffer = [u8; BLOCK_SIZE as usize];
95
96/// A quick hack to allow for a constructor even though blocks are defined as a primitive type.
97pub trait BlockBufferCtor {
98    /// Creae a new, zero initialized buffer large enough to hold a filesystem block.
99    fn new() -> Self;
100}
101
102impl BlockBufferCtor for BlockBuffer {
103    #[inline(always)]
104    fn new() -> Self {
105        [0; BLOCK_SIZE as usize]
106    }
107}
108
109macro_rules! primary_prop_str {
110    ($(#[$attr:meta])* $name:ident) => {
111        $(#[$attr])*
112        pub fn $name(&self) -> &str {
113            if let VolumeDescriptor::Primary(table) = &self.primary {
114                &table.$name
115            } else {
116                unreachable!()
117            }
118        }
119    };
120}
121
122impl<T: ISO9660Reader> ISO9660<T> {
123    /// Returns a new [`ISO9660`] instance from an [`ISO9660Reader`] instance.  `ISO9660Reader` has
124    /// a blanket implementation for all types that implement [`Read`](std::io::Read) and
125    /// [`Seek`](std::io::Seek), so this function can be called with e.g. a [`File`](std::fs::File)
126    /// or [`Cursor`](std::io::Cursor).
127    ///
128    /// # Errors
129    ///
130    /// Upon encountering an error parsing the filesystem image or an I/O error, an error variant
131    /// will be returned.
132    ///
133    /// # Example
134    ///
135    /// ```rust
136    /// # use std::fs::File;
137    /// # use cdfs::ISO9660;
138    /// let file = File::open("images/test.iso")?;
139    /// let iso = ISO9660::new(file)?;
140    /// # Ok::<(), cdfs::ISOError>(())
141    /// ```
142    pub fn new(mut reader: T) -> Result<ISO9660<T>> {
143        let blksize = usize::from(BLOCK_SIZE);
144
145        let mut buf = BlockBuffer::new();
146
147        let mut root = None;
148        let mut primary = None;
149
150        let mut sup_root = None;
151
152        // Skip the "system area"
153        let mut lba = 16;
154
155        // Read volume descriptors
156        loop {
157            let count = reader.read_at(&mut buf, lba)?;
158
159            if count != blksize {
160                return Err(ISOError::ReadSize(count));
161            }
162
163            let descriptor = VolumeDescriptor::parse(&buf)?;
164            match &descriptor {
165                Some(VolumeDescriptor::Primary(table)) => {
166                    if usize::from(table.logical_block_size) != blksize {
167                        // This is almost always the case, but technically
168                        // not guaranteed by the standard.
169                        // TODO: Implement this
170                        return Err(ISOError::InvalidFs("Block size not 2048"));
171                    }
172
173                    root = Some((
174                        table.root_directory_entry.clone(),
175                        table.root_directory_entry_identifier.clone(),
176                    ));
177                    primary = descriptor;
178                }
179                Some(VolumeDescriptor::Supplementary(table)) => {
180                    if usize::from(table.logical_block_size) != blksize {
181                        // This is almost always the case, but technically
182                        // not guaranteed by the standard.
183                        // TODO: Implement this
184                        return Err(ISOError::InvalidFs("Block size not 2048"));
185                    }
186
187                    sup_root = Some((
188                        table.root_directory_entry.clone(),
189                        table.root_directory_entry_identifier.clone(),
190                    ));
191                }
192                Some(VolumeDescriptor::VolumeDescriptorSetTerminator) => break,
193                _ => {}
194            }
195
196            lba += 1;
197        }
198
199        let file = FileRef::new(reader);
200        let file2 = file.clone();
201        let file3 = file.clone();
202
203        let (root, primary) = match (root, primary) {
204            (Some(root), Some(primary)) => (root, primary),
205            _ => {
206                return Err(ISOError::InvalidFs("No primary volume descriptor"));
207            }
208        };
209
210        Ok(ISO9660 {
211            _file: file,
212            root: ISODirectory::new(root.0, ExtraMeta::default(), root.1, file2),
213            sup_root: sup_root.map(|sup_root| {
214                ISODirectory::new(sup_root.0, ExtraMeta::default(), sup_root.1, file3)
215            }),
216            primary,
217        })
218    }
219
220    /// Returns a [`DirectoryEntry`] for a given path.
221    ///
222    /// # Arguments
223    ///
224    /// * `path` - Path to the object on the filesystem
225    ///
226    /// # Errors
227    ///
228    /// Upon encountering an I/O error or an error parsing the filesystem, an error variant is returned.
229    /// If the path cannot be found on the filesystem `Ok(None)` is returned.
230    ///
231    /// # Example
232    ///
233    /// ```rust
234    /// # use std::fs::File;
235    /// # use cdfs::ISO9660;
236    /// # let file = File::open("images/test.iso")?;
237    /// # let iso = ISO9660::new(file)?;
238    /// let entry = iso.open("/README.TXT")?;
239    /// # Ok::<(), cdfs::ISOError>(())
240    /// ```
241    pub fn open(&self, path: &str) -> Result<Option<DirectoryEntry<T>>> {
242        self.root().find_recursive(path)
243    }
244
245    /// Returns true if Rock Ridge extensions are present
246    pub fn is_rr(&self) -> bool {
247        match self.root.contents().next() {
248            Some(Ok(DirectoryEntry::Directory(dirent))) => dirent.is_rock_ridge(),
249            _ => false, // again…
250        }
251    }
252
253    /// Returns the most featureful root directory.
254    ///
255    /// # Root selection
256    /// * If the primary volume descriptor has Rock Ridge SUSP entries, use it
257    /// * ElseIf a supplementary volume descriptor (e.g. Joliet) exists, use it
258    /// * Else fall back on the primary volume descriptor with short filenames
259    ///
260    /// # See Also
261    /// ISO-9660 / ECMA-119 §§ 8.4, 8.5
262    pub fn root(&self) -> &ISODirectory<T> {
263        if self.is_rr() {
264            &self.root
265        } else {
266            match self.sup_root.as_ref() {
267                Some(sup_root) => sup_root,
268                None => &self.root,
269            }
270        }
271    }
272
273    /// Returns the root directory entry.
274    ///
275    ///
276    /// # Arguments
277    ///
278    /// * `index` - An integer indicating which root entry to return
279    ///   * 0 = primary
280    ///   * 1 = secondary (if not present, `None` is returned)
281    ///
282    /// # See Also
283    /// ISO-9660 / ECMA-119 §§ 8.4, 8.5
284    pub fn root_at(&self, index: usize) -> Option<&ISODirectory<T>> {
285        match index {
286            0 => Some(&self.root),
287            1 => self.sup_root.as_ref(),
288            _ => unimplemented!(),
289        }
290    }
291
292    /// Returns [`BLOCK_SIZE`].
293    ///
294    /// This implementation hardcodes the block size to 2048.
295    ///
296    /// # See Also
297    /// ISO-9660 / ECMA-119 § 6.1.2
298    pub fn block_size(&self) -> u16 {
299        BLOCK_SIZE
300    }
301
302    primary_prop_str! {
303        /// # See Also
304        /// ISO-9660 / ECMA-119 § 8.5.13
305        volume_set_identifier
306    }
307
308    primary_prop_str! {
309        /// # See Also
310        /// ISO-9660 / ECMA-119 § 8.5.14
311        publisher_identifier
312    }
313
314    primary_prop_str! {
315        /// # See Also
316        /// ISO-9660 / ECMA-119 § 8.5.15
317        data_preparer_identifier
318    }
319
320    primary_prop_str! {
321        /// # See Also
322        /// ISO-9660 / ECMA-119 § 8.5.16
323        application_identifier
324    }
325
326    primary_prop_str! {
327        /// # See Also
328        /// ISO-9660 / ECMA-119 § 8.5.17
329        copyright_file_identifier
330    }
331
332    primary_prop_str! {
333        /// # See Also
334        /// ISO-9660 / ECMA-119 § 8.5.18
335        abstract_file_identifier
336    }
337
338    primary_prop_str! {
339        /// # See Also
340        /// ISO-9660 / ECMA-119 § 8.5.19
341        bibliographic_file_identifier
342    }
343}