Skip to main content

libcdio_rs/iso9660/
rock.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 Rock Ridge extensions.
19
20use std::{ffi::CStr, mem::MaybeUninit};
21
22use file_mode::Mode;
23use libcdio_sys::{bool_3way_t_nope, bool_3way_t_yep, iso_rock_time_s};
24use thiserror::Error;
25use time::OffsetDateTime;
26
27use crate::iso9660::{Iso, entry::IsoEntry, util};
28
29impl Iso {
30    /// Checks if any file has Rock Ridge extensions.
31    ///
32    /// This can be time consuming, therefore `file_limit` can be provided to
33    /// limit the number of files to scan.
34    pub fn have_rock_ridge(&self, file_limit: Option<u64>) -> Result<bool, RockRidgeSearchError> {
35        let file_limit = file_limit.unwrap_or(u64::MAX);
36        let result = unsafe { libcdio_sys::iso9660_have_rr(self.ptr.as_ptr(), file_limit) };
37
38        #[allow(non_upper_case_globals)]
39        match result {
40            bool_3way_t_yep => Ok(true),
41            bool_3way_t_nope => Ok(false),
42            _ => Err(RockRidgeSearchError),
43        }
44    }
45}
46
47#[non_exhaustive]
48#[derive(Debug, Error)]
49#[error("error searching for rock ridge extensions: file limit reached")]
50pub struct RockRidgeSearchError;
51
52impl IsoEntry<'_> {
53    /// Returns the Rock Ridge attributes of the entry.
54    pub fn rock_ridge(&self) -> Option<RockRidgeAttributes> {
55        let rock = unsafe { (*self.stat.as_ptr()).rr };
56        if rock.b3_rock != bool_3way_t_yep {
57            return None;
58        }
59
60        Some(RockRidgeAttributes {
61            create_time: convert_rock_timefield(rock.create),
62            group_id: rock.st_gid,
63            hard_links: rock.st_nlinks,
64            mode: Mode::new(rock.st_mode, u32::MAX),
65            modify_time: convert_rock_timefield(rock.modify),
66            symlink_to: {
67                if rock.psz_symlink.is_null() {
68                    None
69                } else {
70                    let symlink = unsafe { CStr::from_ptr(rock.psz_symlink) };
71                    symlink
72                        .to_str()
73                        .ok()
74                        .filter(|link| !link.is_empty())
75                        .map(ToString::to_string)
76                }
77            },
78            user_id: rock.st_uid,
79        })
80    }
81}
82
83/// ISO 9660 Rock Ridge extensions.
84#[derive(Clone, Debug)]
85#[non_exhaustive]
86pub struct RockRidgeAttributes {
87    pub create_time: Option<OffsetDateTime>,
88    pub group_id: u32,
89    pub hard_links: u32,
90    pub mode: Mode,
91    pub modify_time: Option<OffsetDateTime>,
92    pub symlink_to: Option<String>,
93    pub user_id: u32,
94}
95
96fn convert_rock_timefield(field: iso_rock_time_s) -> Option<OffsetDateTime> {
97    if !field.b_used {
98        return None;
99    };
100
101    let mut tm = MaybeUninit::uninit();
102    if field.b_longdate {
103        // SAFETY: ltime is valid as indicated by the if check
104        unsafe { libcdio_sys::iso9660_get_ltime(&raw const field.t.ltime, tm.as_mut_ptr()) };
105    } else {
106        // SAFETY: dtime is valid as indicated by the if check
107        unsafe { libcdio_sys::iso9660_get_dtime(&raw const field.t.dtime, true, tm.as_mut_ptr()) };
108    }
109    // SAFETY: The above ffi calls are infallible, thus tm should be initialized.
110    let tm = unsafe { tm.assume_init() };
111
112    util::convert_tm_local(tm).ok()
113}
114
115#[cfg(test)]
116mod tests {
117    use time::macros::datetime;
118
119    use crate::iso9660::tests::{test_joliet_file, test_rockridge_file};
120
121    use super::*;
122
123    #[test]
124    fn have_rock_ridge() {
125        let iso = Iso::new(test_rockridge_file()).unwrap();
126        assert!(iso.have_rock_ridge(None).unwrap());
127    }
128
129    #[test]
130    fn rock_ridge() {
131        let iso = Iso::new(test_rockridge_file()).unwrap();
132        let entry = iso.entry("/COPYING".to_string()).unwrap();
133        assert!(entry.rock_ridge().is_some());
134
135        let iso = Iso::new(test_joliet_file()).unwrap();
136        let entry = iso.entry("/libcdio/COPYING".to_string()).unwrap();
137        assert!(entry.rock_ridge().is_none());
138    }
139
140    #[test]
141    fn mode() {
142        let iso = Iso::new(test_rockridge_file()).unwrap();
143
144        let entry = iso.entry("/zero".to_string()).unwrap();
145        let mode = entry.rock_ridge().unwrap().mode;
146        assert_eq!(&mode.to_string(), "cr--r--r--");
147
148        let entry = iso.entry("/fd0".to_string()).unwrap();
149        let mode = entry.rock_ridge().unwrap().mode;
150        assert_eq!(&mode.to_string(), "br--r--r--");
151
152        let entry = iso.entry("/Copy2".to_string()).unwrap();
153        let mode = entry.rock_ridge().unwrap().mode;
154        assert_eq!(&mode.to_string(), "lr-xr-xr-x");
155
156        let entry = iso.entry("/copy".to_string()).unwrap();
157        let mode = entry.rock_ridge().unwrap().mode;
158        assert_eq!(&mode.to_string(), "dr-xr-xr-x");
159    }
160
161    #[test]
162    fn symlink_to() {
163        let iso = Iso::new(test_rockridge_file()).unwrap();
164
165        let entry = iso.entry("/COPYING".to_string()).unwrap();
166        let rock = entry.rock_ridge().unwrap();
167        assert!(rock.symlink_to.is_none());
168
169        let entry = iso.entry("/Copy2".to_string()).unwrap();
170        let rock = entry.rock_ridge().unwrap();
171        assert_eq!(rock.symlink_to.unwrap(), "COPYING");
172
173        let entry = iso.entry("/tmp/COPYING".to_string()).unwrap();
174        let rock = entry.rock_ridge().unwrap();
175        assert_eq!(rock.symlink_to.unwrap(), "../copying/COPYING");
176    }
177
178    #[test]
179    fn hard_links() {
180        let iso = Iso::new(test_rockridge_file()).unwrap();
181        let entry = iso.entry("/COPYING".to_string()).unwrap();
182        let rock = entry.rock_ridge().unwrap();
183        assert_eq!(rock.hard_links, 1);
184
185        let entry = iso.entry("/copy".to_string()).unwrap();
186        let rock = entry.rock_ridge().unwrap();
187        assert_eq!(rock.hard_links, 2);
188    }
189
190    #[test]
191    fn user_id() {
192        let iso = Iso::new(test_rockridge_file()).unwrap();
193        let entry = iso.entry("/COPYING".to_string()).unwrap();
194        let rock = entry.rock_ridge().unwrap();
195        assert_eq!(rock.user_id, 0);
196    }
197
198    #[test]
199    fn group_id() {
200        let iso = Iso::new(test_rockridge_file()).unwrap();
201        let entry = iso.entry("/COPYING".to_string()).unwrap();
202        let rock = entry.rock_ridge().unwrap();
203        assert_eq!(rock.group_id, 0);
204    }
205
206    #[test]
207    fn time() {
208        let iso = Iso::new(test_rockridge_file()).unwrap();
209        let entry = iso.entry("/COPYING".to_string()).unwrap();
210        let rock = entry.rock_ridge().unwrap();
211        assert_eq!(
212            rock.modify_time.unwrap(),
213            datetime!(2005-03-05 20:55:51.0 +05:30:00)
214        );
215    }
216}