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}