1use std::fs;
30use std::io::{BufReader, Read, Seek, SeekFrom, Write};
31use std::path::{Path, PathBuf};
32
33use anyhow::{anyhow, bail, Context, Result};
34use bytes::{Buf, Bytes};
35use serde::{Deserialize, Serialize};
36
37use crate::io::*;
38
39const ISO9660_SECTOR_SIZE: usize = 2048;
41
42#[derive(Debug, Serialize)]
43pub struct IsoFs {
44 descriptors: Vec<VolumeDescriptor>,
45 #[serde(skip_serializing)]
46 file: fs::File,
47}
48
49impl IsoFs {
50 pub fn from_file(mut file: fs::File) -> Result<Self> {
51 let length = file.metadata()?.len();
52 let descriptors = get_volume_descriptors(&mut file)?;
53 let iso_fs = Self { descriptors, file };
54 let primary = iso_fs.get_primary_volume_descriptor()?;
55 if primary.volume_space_size * ISO9660_SECTOR_SIZE as u64 > length {
56 bail!("ISO image is incomplete");
57 }
58
59 Ok(iso_fs)
60 }
61
62 pub fn as_file(&mut self) -> Result<&mut fs::File> {
63 self.file.rewind().context("seeking to start of ISO")?;
64 Ok(&mut self.file)
65 }
66
67 pub fn get_root_directory(&self) -> Result<Directory> {
68 let primary = self
69 .get_primary_volume_descriptor()
70 .context("getting root directory")?;
71 Ok(primary.root.clone())
72 }
73
74 pub fn walk(&mut self) -> Result<IsoFsWalkIterator<'_>> {
75 let root_dir = self.get_root_directory()?;
76 let buf = self.list_dir(&root_dir)?;
77 Ok(IsoFsWalkIterator {
78 iso: &mut self.file,
79 parent_dirs: Vec::new(),
80 current_dir: Some(buf),
81 dirpath: PathBuf::new(),
82 })
83 }
84
85 pub fn list_dir(&mut self, dir: &Directory) -> Result<IsoFsIterator> {
87 IsoFsIterator::new(&mut self.file, dir)
88 }
89
90 pub fn get_path(&mut self, path: &str) -> Result<DirectoryRecord> {
92 let mut dir = self.get_root_directory()?;
93 let mut components = path_components(path);
94 let filename = match components.pop() {
95 Some(f) => f,
96 None => return Ok(DirectoryRecord::Directory(dir)),
97 };
98
99 for c in &components {
100 dir = self
101 .get_dir_record(&dir, c)?
102 .ok_or_else(|| NotFound(format!("intermediate directory {c} does not exist")))?
103 .try_into_dir()
104 .map_err(|_| {
105 NotFound(format!("component {c:?} in path {path} is not a directory"))
106 })?;
107 }
108
109 self.get_dir_record(&dir, filename)?.ok_or_else(|| {
110 anyhow!(NotFound(format!(
111 "no record for {} in directory {}",
112 filename,
113 components.join("/")
114 )))
115 })
116 }
117
118 fn get_dir_record(&mut self, dir: &Directory, name: &str) -> Result<Option<DirectoryRecord>> {
120 for record in self
121 .list_dir(dir)
122 .with_context(|| format!("listing directory {}", dir.name))?
123 {
124 let record = record?;
125 match &record {
126 DirectoryRecord::Directory(d) if d.name == name => return Ok(Some(record)),
127 DirectoryRecord::File(f) if f.name == name => return Ok(Some(record)),
128 _ => continue,
129 }
130 }
131 Ok(None)
132 }
133
134 pub fn read_file(&mut self, file: &File) -> Result<impl Read + '_> {
136 self.file
137 .seek(SeekFrom::Start(file.address.as_offset()))
138 .with_context(|| format!("seeking to file {}", file.name))?;
139 Ok(BufReader::with_capacity(
140 BUFFER_SIZE,
141 (&self.file).take(file.length as u64),
142 ))
143 }
144
145 pub fn overwrite_file(&mut self, file: &File) -> Result<impl Write + '_> {
147 self.file
148 .seek(SeekFrom::Start(file.address.as_offset()))
149 .with_context(|| format!("seeking to file {}", file.name))?;
150 Ok(LimitWriter::new(
151 &mut self.file,
152 file.length as u64,
153 format!("end of file {}", file.name),
154 ))
155 }
156
157 fn get_primary_volume_descriptor(&self) -> Result<&PrimaryVolumeDescriptor> {
158 for d in &self.descriptors {
159 if let VolumeDescriptor::Primary(p) = d {
160 return Ok(p);
161 }
162 }
163 Err(anyhow!("no primary volume descriptor found in ISO"))
164 }
165}
166
167#[derive(Debug, Serialize)]
168#[serde(tag = "type", rename_all = "lowercase")]
169enum VolumeDescriptor {
170 Boot(BootVolumeDescriptor),
171 Primary(PrimaryVolumeDescriptor),
172 Supplementary,
173 Unknown { type_id: u8 },
174}
175
176#[derive(Debug, Serialize)]
177struct BootVolumeDescriptor {
178 boot_system_id: String,
179 boot_id: String,
180}
181
182#[derive(Debug, Serialize)]
183struct PrimaryVolumeDescriptor {
184 system_id: String,
185 volume_id: String,
186 volume_space_size: u64,
187 root: Directory,
188}
189
190#[derive(Debug, Serialize)]
191#[serde(tag = "type", rename_all = "lowercase")]
192pub enum DirectoryRecord {
193 Directory(Directory),
194 File(File),
195}
196
197impl DirectoryRecord {
198 pub fn try_into_dir(self) -> Result<Directory> {
199 match self {
200 Self::Directory(d) => Ok(d),
201 Self::File(f) => Err(anyhow!("entry {} is a file", f.name)),
202 }
203 }
204
205 pub fn try_into_file(self) -> Result<File> {
206 match self {
207 Self::Directory(f) => Err(anyhow!("entry {} is a directory", f.name)),
208 Self::File(f) => Ok(f),
209 }
210 }
211}
212
213#[derive(Debug, Serialize, Clone)]
214pub struct Directory {
215 pub name: String,
216 pub address: Address,
217 pub length: u32,
218}
219
220#[derive(Debug, Serialize, Clone)]
221pub struct File {
222 pub name: String,
223 pub address: Address,
224 pub length: u32,
225}
226
227#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
228pub struct Address(u32);
229
230impl Address {
231 pub fn as_offset(&self) -> u64 {
232 self.0 as u64 * ISO9660_SECTOR_SIZE as u64
233 }
234
235 pub fn as_sector(&self) -> u32 {
236 self.0
237 }
238}
239
240#[derive(Debug, thiserror::Error)]
242#[error("{0}")]
243pub struct NotFound(String);
244
245fn get_volume_descriptors(f: &mut fs::File) -> Result<Vec<VolumeDescriptor>> {
247 const ISO9660_VOLUME_DESCRIPTORS: Address = Address(0x10);
248 f.seek(SeekFrom::Start(ISO9660_VOLUME_DESCRIPTORS.as_offset()))
249 .context("seeking to volume descriptors")?;
250
251 let mut descriptors: Vec<VolumeDescriptor> = Vec::new();
252 while let Some(d) = get_next_volume_descriptor(f)
253 .with_context(|| format!("getting volume descriptor #{}", descriptors.len() + 1))?
254 {
255 descriptors.push(d);
256 }
257
258 Ok(descriptors)
259}
260
261fn get_next_volume_descriptor(f: &mut fs::File) -> Result<Option<VolumeDescriptor>> {
263 const TYPE_BOOT: u8 = 0;
264 const TYPE_PRIMARY: u8 = 1;
265 const TYPE_SUPPLEMENTARY: u8 = 2;
266 const TYPE_TERMINATOR: u8 = 255;
267
268 let mut buf = vec![0; ISO9660_SECTOR_SIZE];
269 f.read_exact(&mut buf)
270 .context("reading volume descriptor")?;
271 let buf = &mut Bytes::from(buf);
272
273 Ok(match buf.get_u8() {
274 TYPE_BOOT => Some(VolumeDescriptor::Boot(BootVolumeDescriptor::parse(buf)?)),
275 TYPE_PRIMARY => Some(VolumeDescriptor::Primary(PrimaryVolumeDescriptor::parse(
276 buf,
277 )?)),
278 TYPE_SUPPLEMENTARY => Some(VolumeDescriptor::Supplementary),
279 TYPE_TERMINATOR => None,
280 t => Some(VolumeDescriptor::Unknown { type_id: t }),
281 })
282}
283
284impl BootVolumeDescriptor {
285 fn parse(buf: &mut Bytes) -> Result<Self> {
287 verify_descriptor_header(buf).context("parsing boot descriptor")?;
288 Ok(Self {
289 boot_system_id: parse_iso9660_string(buf, 32, IsoString::StrA)
290 .context("parsing boot system ID")?,
291 boot_id: parse_iso9660_string(buf, 32, IsoString::StrA).context("parsing boot ID")?,
292 })
293 }
294}
295
296impl PrimaryVolumeDescriptor {
297 fn parse(buf: &mut Bytes) -> Result<Self> {
299 verify_descriptor_header(buf).context("parsing primary descriptor")?;
300 let system_id =
301 parse_iso9660_string(eat(buf, 1), 32, IsoString::StrA).context("parsing system id")?;
302 let volume_id = parse_iso9660_string(buf, 32, IsoString::StrA).context("parsing volume id")?;
304 eat(buf, 8); let volume_space_size = buf.get_u32_le() as u64;
306 let root = match get_next_directory_record(eat(buf, 156 - 84), 34, true)? {
307 Some(DirectoryRecord::Directory(d)) => d,
308 _ => bail!("failed to parse root directory record from primary descriptor"),
309 };
310 Ok(Self {
311 system_id,
312 volume_id,
313 volume_space_size,
314 root,
315 })
316 }
317}
318
319fn verify_descriptor_header(buf: &mut Bytes) -> Result<()> {
321 const VOLUME_DESCRIPTOR_ID: &[u8] = b"CD001";
322 const VOLUME_DESCRIPTOR_VERSION: u8 = 1;
323
324 let id = buf.copy_to_bytes(5);
325 if id != VOLUME_DESCRIPTOR_ID {
326 bail!("unknown descriptor ID: {:?}", id);
327 }
328
329 let version = buf.get_u8();
330 if version != VOLUME_DESCRIPTOR_VERSION {
331 bail!("unknown descriptor version: {}", version);
332 }
333
334 Ok(())
335}
336
337pub struct IsoFsIterator {
338 dir: Bytes,
339 length: u32,
340}
341
342impl IsoFsIterator {
343 fn new(iso: &mut fs::File, dir: &Directory) -> Result<Self> {
344 iso.seek(SeekFrom::Start(dir.address.as_offset()))
345 .with_context(|| format!("seeking to directory {}", dir.name))?;
346
347 let mut buf = vec![0; dir.length as usize];
348 iso.read_exact(&mut buf)
349 .with_context(|| format!("reading directory {}", dir.name))?;
350
351 Ok(Self {
352 dir: Bytes::from(buf),
353 length: dir.length,
354 })
355 }
356}
357
358impl Iterator for IsoFsIterator {
359 type Item = Result<DirectoryRecord>;
360 fn next(&mut self) -> Option<Self::Item> {
361 get_next_directory_record(&mut self.dir, self.length, false)
362 .context("reading next record")
363 .transpose()
364 }
365}
366
367pub struct IsoFsWalkIterator<'a> {
368 iso: &'a mut fs::File,
369 parent_dirs: Vec<IsoFsIterator>,
370 current_dir: Option<IsoFsIterator>,
371 dirpath: PathBuf,
372}
373
374impl Iterator for IsoFsWalkIterator<'_> {
375 type Item = Result<(String, DirectoryRecord)>;
376 fn next(&mut self) -> Option<Self::Item> {
377 self.walk_iterator_next().transpose()
378 }
379}
380
381impl IsoFsWalkIterator<'_> {
382 fn walk_iterator_next(&mut self) -> Result<Option<(String, DirectoryRecord)>> {
384 while let Some(ref mut current_dir) = self.current_dir {
385 match current_dir.next() {
386 Some(Ok(r)) => {
387 let mut path = self.dirpath.clone();
390 match &r {
391 DirectoryRecord::Directory(d) => {
392 self.parent_dirs.push(self.current_dir.take().unwrap());
393 self.dirpath.push(&d.name);
394 self.current_dir = Some(IsoFsIterator::new(self.iso, d)?);
395 path.push(&d.name);
396 }
397 DirectoryRecord::File(f) => path.push(&f.name),
398 };
399 return Ok(Some((path.into_os_string().into_string().unwrap(), r)));
401 }
402 Some(Err(e)) => return Err(e),
403 None => {
404 self.current_dir = self.parent_dirs.pop();
405 self.dirpath.pop();
406 }
407 }
408 }
409 Ok(None)
410 }
411}
412
413fn get_next_directory_record(
415 buf: &mut Bytes,
416 length: u32,
417 is_root: bool,
418) -> Result<Option<DirectoryRecord>> {
419 loop {
420 if !buf.has_remaining() {
421 return Ok(None);
422 }
423
424 let len = buf.get_u8() as usize;
425 if len == 0 {
426 let jump = {
427 let pos = length as usize - buf.remaining();
429 ((pos + ISO9660_SECTOR_SIZE) & !(ISO9660_SECTOR_SIZE - 1)) - pos
431 };
432 if jump >= buf.remaining() {
433 return Ok(None);
434 }
435 buf.advance(jump);
436 continue;
437 } else if len > buf.remaining() + 1 {
438 bail!("incomplete directory record; corrupt ISO?");
441 }
442
443 let address = Address(eat(buf, 1).get_u32_le());
444 let length = eat(buf, 4).get_u32_le();
445 let flags = eat(buf, 25 - 14).get_u8();
446 let name_length = eat(buf, 32 - 26).get_u8() as usize;
447 let name = if name_length == 1 && (buf[0] == 0 || buf[0] == 1) {
448 let c = buf.get_u8();
449 if is_root && c == 0 {
450 Some(".".into())
453 } else {
454 None
456 }
457 } else {
458 Some(
459 parse_iso9660_string(buf, name_length, IsoString::File)
460 .context("parsing record name")?,
461 )
462 };
463
464 eat(buf, len - (33 + name_length));
466
467 if let Some(name) = name {
468 if flags & 2 > 0 {
469 return Ok(Some(DirectoryRecord::Directory(Directory {
470 name,
471 address,
472 length,
473 })));
474 } else {
475 return Ok(Some(DirectoryRecord::File(File {
476 name,
477 address,
478 length,
479 })));
480 }
481 }
482 }
483}
484
485#[allow(unused)]
486enum IsoString {
487 StrA,
488 StrD,
489 File,
490}
491
492fn parse_iso9660_string(buf: &mut Bytes, len: usize, kind: IsoString) -> Result<String> {
494 const FILE_CHARS: [u8; 17] = *b"!\"%&'()*+,-.:<=>?"; const A_CHARS: [u8; 2] = *b";/"; if len > buf.remaining() {
500 bail!("incomplete string name; corrupt ISO?");
501 }
502 let mut s = String::with_capacity(len);
503 let mut bytes = buf.copy_to_bytes(len);
504 if matches!(kind, IsoString::File) {
505 if bytes.ends_with(b";1") {
506 bytes.truncate(bytes.len() - 2);
507 }
508 if bytes.ends_with(b".") {
509 bytes.truncate(bytes.len() - 1);
510 }
511 }
512 for byte in &bytes {
513 #[allow(clippy::if_same_then_else)] if byte.is_ascii_alphabetic() || byte.is_ascii_digit() || *byte == b'_' || *byte == b' ' {
515 s.push(char::from(*byte));
516 } else if FILE_CHARS.contains(byte) && matches!(kind, IsoString::File | IsoString::StrA) {
517 s.push(char::from(*byte));
518 } else if A_CHARS.contains(byte) && matches!(kind, IsoString::StrA) {
519 s.push(char::from(*byte));
520 } else if A_CHARS.contains(byte) && matches!(kind, IsoString::File) {
521 s.push('.'); } else if *byte == 0 {
523 break;
524 } else {
525 bail!("invalid string name {:?}", bytes);
526 }
527 }
528 if matches!(kind, IsoString::StrA | IsoString::StrD) {
529 s.truncate(s.trim_end_matches(' ').len());
530 }
531 Ok(s)
532}
533
534fn eat(buf: &mut Bytes, n: usize) -> &mut Bytes {
535 buf.advance(n);
536 buf
537}
538
539fn path_components(s: &str) -> Vec<&str> {
542 use std::path::Component::*;
544 let mut ret = Vec::new();
545 for c in Path::new(s).components() {
546 match c {
547 Prefix(_) | RootDir | CurDir => (),
548 ParentDir => {
549 ret.pop();
550 }
551 Normal(c) => {
552 ret.push(c.to_str().unwrap()); }
554 }
555 }
556 ret
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562
563 use std::io::copy;
564
565 use tempfile::tempfile;
566 use xz2::read::XzDecoder;
567
568 fn open_iso() -> IsoFs {
569 let iso_bytes: &[u8] = include_bytes!("../fixtures/iso/synthetic.iso.xz");
570 let mut decoder = XzDecoder::new(iso_bytes);
571 let mut iso_file = tempfile().unwrap();
572 copy(&mut decoder, &mut iso_file).unwrap();
573 IsoFs::from_file(iso_file).unwrap()
574 }
575
576 #[test]
577 fn open_truncated_iso() {
578 let iso_bytes: &[u8] = include_bytes!("../fixtures/iso/synthetic.iso.xz");
579 let mut decoder = XzDecoder::new(iso_bytes);
580 let mut iso_file = tempfile().unwrap();
581 copy(&mut decoder, &mut iso_file).unwrap();
582 iso_file
583 .set_len(iso_file.metadata().unwrap().len() / 2)
584 .unwrap();
585 assert_eq!(
586 IsoFs::from_file(iso_file).unwrap_err().to_string(),
587 "ISO image is incomplete"
588 );
589 }
590
591 #[test]
592 fn test_primary_volume_descriptor() {
593 let iso = open_iso();
594 let desc = iso.get_primary_volume_descriptor().unwrap();
595 assert_eq!(desc.system_id, "system-ID-string");
596 assert_eq!(desc.volume_id, "volume-ID-string");
597 assert_eq!(desc.root.name, ".");
598 assert_eq!(desc.volume_space_size, 338);
599 }
600
601 #[test]
602 fn test_get_path() {
603 let mut iso = open_iso();
604 assert_eq!(iso.get_path("/").unwrap().try_into_dir().unwrap().name, ".");
606 assert_eq!(
608 iso.get_path("./CONTENT")
609 .unwrap()
610 .try_into_dir()
611 .unwrap()
612 .name,
613 "CONTENT"
614 );
615 iso.get_path("./CONTENT")
617 .unwrap()
618 .try_into_file()
619 .unwrap_err();
620 iso.get_path("CONTENT/FILE.TXT")
622 .unwrap()
623 .try_into_dir()
624 .unwrap_err();
625 assert!(iso.get_path("MISSING").unwrap_err().is::<NotFound>());
627 assert!(iso
629 .get_path("MISSING/STUFF.TXT")
630 .unwrap_err()
631 .is::<NotFound>());
632 assert!(iso
634 .get_path("CONTENT/FILE.TXT/STUFF.TXT")
635 .unwrap_err()
636 .is::<NotFound>());
637 }
638
639 #[test]
640 fn test_list_dir() {
641 let mut iso = open_iso();
642 let dir = iso.get_path("CONTENT").unwrap().try_into_dir().unwrap();
643 let names = iso
644 .list_dir(&dir)
645 .unwrap()
646 .map(|e| match e {
647 Ok(DirectoryRecord::Directory(d)) => d.name,
648 Ok(DirectoryRecord::File(f)) => f.name,
649 Err(e) => panic!("{}", e),
650 })
651 .collect::<Vec<String>>();
652 assert_eq!(names, vec!["DIR", "FILE.TXT"]);
653 }
654
655 #[test]
656 fn test_read_file() {
657 let mut iso = open_iso();
658 let file = iso
659 .get_path("REALLY/VERY/DEEPLY/NESTED/FILE.TXT")
660 .unwrap()
661 .try_into_file()
662 .unwrap();
663 let mut data = String::new();
664 iso.read_file(&file)
665 .unwrap()
666 .read_to_string(&mut data)
667 .unwrap();
668 assert_eq!(data.as_str(), "foo\n");
669 }
670
671 #[test]
672 fn test_walk() {
673 let expected = vec![
674 "CONTENT",
675 "CONTENT/DIR",
676 "CONTENT/DIR/SUBFILE.TXT",
677 "CONTENT/FILE.TXT",
678 "LARGEDIR",
679 "NAMES",
681 r#"NAMES/!"%&'()*.+,-"#,
682 "NAMES/:<=>?",
683 "NAMES/ABC",
684 "NAMES/ABC.D",
685 "NAMES/ABC.DE",
686 "NAMES/ABC.DEF",
687 "NAMES/ABCDE000",
688 "NAMES/ABCDEFGH",
689 "NAMES/ABCDEFGH.I",
690 "NAMES/ABCDEFGH.IJ",
691 "NAMES/ABCDEFGH.IJK",
692 "NAMES/ABCDEFGH.M",
693 "NAMES/ABCDEFGH.MN",
694 "NAMES/ABCDEFGH.MNO",
695 "REALLY",
696 "REALLY/VERY",
697 "REALLY/VERY/DEEPLY",
698 "REALLY/VERY/DEEPLY/NESTED",
699 "REALLY/VERY/DEEPLY/NESTED/FILE.TXT",
700 ];
701 let mut expected = expected
702 .iter()
703 .map(|s| s.to_string())
704 .collect::<Vec<String>>();
705 for i in 1..=150 {
706 expected.push(format!("LARGEDIR/{i}.DAT"));
707 }
708 expected.sort_unstable();
709
710 let mut iso = open_iso();
711 let names = iso
712 .walk()
713 .unwrap()
714 .map(|v| v.unwrap().0)
715 .collect::<Vec<String>>();
716 assert_eq!(names, expected);
717 }
718
719 #[test]
720 fn test_path_components() {
721 assert_eq!(path_components("z"), vec!["z"]);
723 assert_eq!(path_components("/a/./../b"), vec!["b"]);
725 assert_eq!(path_components("./a/../../b"), vec!["b"]);
727 assert_eq!(path_components("/"), Vec::new() as Vec<&str>);
729 assert_eq!(path_components(""), Vec::new() as Vec<&str>);
731 }
732}