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}