1use std::{
21 error::Error,
22 ffi::{CStr, CString},
23 io,
24 marker::PhantomData,
25 ptr::NonNull,
26};
27
28use file_mode::Mode;
29use libcdio_sys::{udf_dirent_s, udf_t};
30use thiserror::Error;
31use time::OffsetDateTime;
32
33use crate::udf::Udf;
34
35impl Udf {
36 pub fn root(&self) -> Result<UdfEntry<'_>, UdfGetEntryError> {
38 let entry = unsafe { libcdio_sys::udf_get_root(self.udf.as_ptr(), true, 0) };
40
41 NonNull::new(entry)
42 .map(UdfEntry::new)
43 .ok_or_else(|| UdfGetEntryError::new("/", "udf_get_root() returned NULL".into()))
44 }
45
46 pub fn root_from_partition(&self, partition: u16) -> Result<UdfEntry<'_>, UdfGetEntryError> {
48 let entry = unsafe { libcdio_sys::udf_get_root(self.udf.as_ptr(), false, partition) };
50
51 NonNull::new(entry)
52 .map(UdfEntry::new)
53 .ok_or_else(|| UdfGetEntryError::new("/", "udf_get_root() returned NULL".into()))
54 }
55
56 pub fn entry(&self, path: String) -> Result<UdfEntry<'_>, UdfGetEntryError> {
60 let root = self.root()?;
61 let path = CString::new(path).map_err(|err| {
62 UdfGetEntryError::new(
63 String::from_utf8(err.clone().into_vec()).expect("path was a valid string"),
64 err.into(),
65 )
66 })?;
67 let entry = unsafe { libcdio_sys::udf_fopen(root.entry.as_ptr(), path.as_ptr()) };
69
70 NonNull::new(entry).map(UdfEntry::new).ok_or_else(|| {
71 UdfGetEntryError::new(
72 path.into_string()
73 .expect("path was originally a valid string"),
74 "udf_fopen() returned NULL".into(),
75 )
76 })
77 }
78}
79
80#[derive(Debug, Error)]
81#[error(transparent)]
82pub struct UdfGetEntryError(Box<GetEntryErrRepr>);
83
84#[derive(Debug, Error)]
85#[error("could not get UDF entry at `{path}`")]
86struct GetEntryErrRepr {
87 path: String,
88 source: Box<dyn Error + Send + Sync>,
89}
90
91impl UdfGetEntryError {
92 pub fn path(&self) -> &str {
94 &self.0.path
95 }
96 fn new(path: impl Into<String>, source: Box<dyn Error + Send + Sync>) -> Self {
97 Self(Box::new(GetEntryErrRepr {
98 path: path.into(),
99 source,
100 }))
101 }
102}
103
104pub struct UdfEntry<'a> {
106 entry: NonNull<udf_dirent_s>,
107 pub gid: u32,
108 pub uid: u32,
109 _parent: PhantomData<&'a udf_t>,
111}
112
113impl UdfEntry<'_> {
114 pub fn modify_time(&self) -> Result<OffsetDateTime, UdfInvalidEntryError> {
116 let time = unsafe { libcdio_sys::udf_get_modification_time(self.entry.as_ptr()) };
118 if time == -1 {
119 return Err(UdfInvalidEntryError::new(
120 self.filename().ok(),
121 "udf_get_modification_time() returned -1".into(),
122 ));
123 }
124
125 OffsetDateTime::from_unix_timestamp(time)
126 .map_err(|err| UdfInvalidEntryError::new(self.filename().ok(), err.into()))
127 }
128
129 pub fn filename(&self) -> Result<&str, UdfInvalidEntryError> {
131 const CURRENT_DIR_FILENAME: &str = ".";
132
133 let filename = unsafe { libcdio_sys::udf_get_filename(self.entry.as_ptr()) };
135 if filename.is_null() {
136 return Err(UdfInvalidEntryError::new(
137 Option::<&str>::None,
138 "udf_get_filename() returned NULL".into(),
139 ));
140 }
141 let filename = unsafe { CStr::from_ptr(filename) };
142 if filename.is_empty() {
145 return Ok(CURRENT_DIR_FILENAME);
146 }
147
148 filename
149 .to_str()
150 .map_err(|err| UdfInvalidEntryError::new(Option::<&str>::None, err.into()))
151 }
152
153 pub fn next(self) -> Option<Self> {
155 let next_entry = unsafe { libcdio_sys::udf_readdir(self.entry.as_ptr()) };
157 std::mem::forget(self);
158
159 NonNull::new(next_entry).map(Self::new)
160 }
161
162 pub fn open_dir(&self) -> Option<Self> {
164 let sub_entry = unsafe { libcdio_sys::udf_opendir(self.entry.as_ptr()) };
165
166 Some(Self::new(NonNull::new(sub_entry)?))
167 }
168
169 pub fn is_dir(&self) -> bool {
171 unsafe { libcdio_sys::udf_is_dir(self.entry.as_ptr()) }
172 }
173
174 pub fn file_length(&self) -> u64 {
176 unsafe { libcdio_sys::udf_get_file_length(self.entry.as_ptr()) }
178 }
179
180 pub fn mode(&self) -> Mode {
182 #[allow(clippy::useless_conversion)]
184 let mode = u32::from(unsafe { libcdio_sys::udf_get_posix_filemode(self.entry.as_ptr()) });
185 Mode::new(mode, u32::MAX)
186 }
187
188 pub fn link_count(&self) -> u16 {
190 unsafe { libcdio_sys::udf_get_link_count(self.entry.as_ptr()) }
191 }
192
193 pub fn reader(&self) -> UdfEntryReader<'_> {
196 UdfEntryReader {
197 bytes_read: 0,
198 entry: self,
199 }
200 }
201
202 fn new(entry: NonNull<udf_dirent_s>) -> Self {
203 let uid = unsafe { (*entry.as_ptr()).fe.uid };
204 let gid = unsafe { (*entry.as_ptr()).fe.gid };
205
206 Self {
207 entry,
208 gid: u32::from_le(gid),
209 uid: u32::from_le(uid),
210 _parent: PhantomData,
211 }
212 }
213}
214
215impl Drop for UdfEntry<'_> {
216 fn drop(&mut self) {
217 let _ = unsafe { libcdio_sys::udf_dirent_free(self.entry.as_ptr()) };
218 }
219}
220
221#[derive(Debug, Error)]
223#[error(transparent)]
224pub struct UdfInvalidEntryError(Box<InvalidEntryErrRepr>);
225
226#[derive(Debug, Error)]
227#[error("found invalid data in UDF entry `{}`", name.as_deref().unwrap_or_default())]
228struct InvalidEntryErrRepr {
229 name: Option<String>,
230 source: Box<dyn Error + Send + Sync>,
231}
232
233impl UdfInvalidEntryError {
234 pub fn name(&self) -> Option<&str> {
236 self.0.name.as_deref()
237 }
238 fn new(name: Option<impl Into<String>>, source: Box<dyn Error + Send + Sync>) -> Self {
239 Self(Box::new(InvalidEntryErrRepr {
240 name: name.map(Into::into),
241 source,
242 }))
243 }
244}
245
246pub struct UdfEntryReader<'a> {
251 bytes_read: usize,
252 entry: &'a UdfEntry<'a>,
253}
254
255impl UdfEntryReader<'_> {
256 fn set_position(&mut self, block_num: usize) {
258 let _ = unsafe {
261 libcdio_sys::udf_setpos(
262 self.entry.entry.as_ptr(),
263 (block_num * Udf::BLOCK_SIZE)
264 .try_into()
265 .expect("block's byte offset should fit an `off_t`"),
266 )
267 };
268 }
269}
270
271impl io::Read for UdfEntryReader<'_> {
272 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
273 let file_size = self.entry.file_length() as usize;
274 let mut buf_read = 0;
275
276 self.set_position(0);
284
285 while self.bytes_read < file_size && buf_read < buf.len() {
286 let block_num = self.bytes_read / Udf::BLOCK_SIZE;
287 self.set_position(block_num);
288 let mut block = [0_u8; Udf::BLOCK_SIZE];
289 let ret = unsafe {
290 libcdio_sys::udf_read_block(self.entry.entry.as_ptr(), block.as_mut_ptr().cast(), 1)
291 };
292 if ret != block.len() as _ {
294 return Err(io::Error::other(format!(
295 "error reading udf block number: {block_num}",
296 )));
297 }
298 let block_start = self.bytes_read % block.len();
299 let buf_rem = buf.len() - buf_read;
300 let block_rem = (block.len() - block_start).min(file_size - self.bytes_read);
301 let len = buf_rem.min(block_rem);
302 buf[buf_read..buf_read + len].copy_from_slice(&block[block_start..block_start + len]);
303 buf_read += len;
304 self.bytes_read += len;
305 }
306
307 Ok(buf_read)
308 }
309}
310
311impl io::Seek for UdfEntryReader<'_> {
312 fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
313 self.bytes_read = match pos {
314 io::SeekFrom::Start(offset) => offset as usize,
315 io::SeekFrom::End(offset) => {
316 self.entry.file_length().saturating_add_signed(offset) as usize
317 }
318 io::SeekFrom::Current(offset) => self.bytes_read.saturating_add_signed(offset as isize),
319 };
320
321 Ok(self.bytes_read as u64)
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use std::{io::Read, path::PathBuf};
328
329 use time::macros::datetime;
330
331 use crate::udf::tests::test_udf_file;
332
333 use super::*;
334
335 fn test_udf_file1() -> PathBuf {
336 PathBuf::from("tests/data/udf1.iso")
337 }
338
339 #[test]
340 fn root() {
341 let udf = Udf::new(test_udf_file()).unwrap();
342 udf.root().unwrap();
343 }
344
345 #[test]
346 fn root_from_partition() {
347 let udf = Udf::new(test_udf_file()).unwrap();
348 udf.root_from_partition(0).unwrap();
349 }
350
351 #[test]
352 fn modify_time() {
353 let udf = Udf::new(test_udf_file()).unwrap();
354 let modify_time = udf.root().unwrap().modify_time().unwrap();
355 assert_eq!(modify_time, datetime!(2014-02-20 1:26:20.0 +00:00:00));
356 }
357
358 #[test]
359 fn filename() {
360 let udf = Udf::new(test_udf_file()).unwrap();
361 let root = udf.root().unwrap();
362 assert_eq!(root.filename().unwrap(), "/");
363 }
364
365 #[test]
366 fn next() {
367 let udf = Udf::new(test_udf_file()).unwrap();
368 let root = udf.root().unwrap();
369 let next = root.next().unwrap();
370 assert_eq!(next.filename().unwrap(), ".");
371
372 let next = next.next().unwrap();
373 assert_eq!(next.filename().unwrap(), "FéжΘvrier");
374 }
375
376 #[test]
377 fn is_dir() {
378 let udf = Udf::new(test_udf_file()).unwrap();
379 let root = udf.root().unwrap();
380 assert!(root.is_dir());
381 }
382
383 #[test]
384 fn file_length() {
385 let udf = Udf::new(test_udf_file()).unwrap();
386 let root = udf.root().unwrap();
387 let file = root.next().unwrap().next().unwrap();
388 assert_eq!(file.file_length(), 10);
389 }
390
391 #[test]
392 fn mode() {
393 let udf = Udf::new(test_udf_file()).unwrap();
394 let root = udf.root().unwrap();
395 let entry = root.next().unwrap();
396 let entry = entry.next().unwrap();
397 assert_eq!(&entry.mode().to_string(), "-r-xr-xr-x");
398 }
399
400 #[test]
401 fn link_count() {
402 let udf = Udf::new(test_udf_file()).unwrap();
403 let root = udf.root().unwrap();
404 let entry = root.next().unwrap().next().unwrap();
405 assert_eq!(entry.link_count(), 1);
406 }
407
408 #[test]
409 fn fields() {
410 let udf = Udf::new(test_udf_file1()).unwrap();
411 let root = udf.root().unwrap();
412 let entry = root.next().unwrap().next().unwrap();
413 assert_eq!(entry.uid, 2000);
414 assert_eq!(entry.gid, 3000);
415 }
416
417 #[test]
418 fn open_dir() {
419 let udf = Udf::new(test_udf_file1()).unwrap();
420 let entry = udf.root().unwrap();
421 let entry = entry.next().unwrap().next().unwrap();
423 entry.open_dir().unwrap();
425 }
426
427 #[test]
428 fn read() {
429 let udf = Udf::new(test_udf_file1()).unwrap();
430 let root = udf.root().unwrap();
431 let entry = root.next().unwrap().next().unwrap();
433 let entry = entry.open_dir().unwrap().next().unwrap();
435 let entry = entry.next().unwrap();
437
438 let mut reader = entry.reader();
439 let mut contents = String::new();
440 let bytes_read = reader.read_to_string(&mut contents).unwrap();
441
442 let gpl = std::fs::read_to_string("COPYING").unwrap();
443 assert_eq!(gpl.len(), bytes_read);
444 assert_eq!(gpl, contents);
445 }
446
447 #[test]
448 fn entry() {
449 let udf = Udf::new(test_udf_file1()).unwrap();
450 udf.entry("/licenses/COPYING.LESSER".to_owned()).unwrap();
451 }
452}