1use 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 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 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 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 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
112pub struct IsoEntry<'a> {
114 pub(crate) iso: &'a Iso,
116 pub(crate) stat: NonNull<iso9660_stat_s>,
117}
118
119impl IsoEntry<'_> {
120 pub fn filename_raw(&self) -> Result<&str, IsoInvalidEntryError> {
124 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 unsafe { CStr::from_ptr(name).to_str() }
135 .map_err(|err| IsoInvalidEntryError::new(Default::default(), err.into()))
136 }
137
138 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 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 pub fn total_size(&self) -> u64 {
169 unsafe { (*self.stat.as_ptr()).total_size }
170 }
171
172 pub fn lsn(&self) -> i32 {
174 unsafe { (*self.stat.as_ptr()).lsn }
175 }
176
177 pub fn is_dir(&self) -> bool {
179 unsafe { (*self.stat.as_ptr()).type_ == iso9660_stat_s__STAT_DIR }
180 }
181
182 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 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 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
226pub 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 if ret != block.len() as _ {
251 return Err(io::Error::other(format!(
252 "error reading block at lsn: {lsn}",
253 )));
254 }
255
256 let block_start = self.bytes_read % block.len();
259 let buf_rem = buf.len() - buf_read;
260 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}