Skip to main content

rgpre/import/
cdrom.rs

1//! Read-only ISO 9660 access for GOG MODE1/2352 CD images and plain ISO images.
2use std::io::{self, Read, Seek, SeekFrom, Write};
3
4/// Validated root directory and sector layout, with a seekable image reader.
5pub struct CdImage<R> {
6    reader: R,
7    sector: u64,
8    offset: u64,
9    root: (u32, u32),
10}
11
12/// One validated root-directory file in an ISO or MODE1 image.
13/// The layout is retained so callers can seek without copying the preceding payload.
14#[derive(Debug, Clone)]
15pub struct CdFile {
16    /// File payload length, excluding sector headers and padding.
17    pub size: u64,
18    extent: u32,
19    sector: u64,
20    offset: u64,
21}
22impl CdFile {
23    /// Read a bounded payload range. Sector decoding stays inside the CD parser.
24    pub fn read_range(
25        &self,
26        reader: &mut (impl Read + Seek),
27        start: u64,
28        length: usize,
29    ) -> io::Result<Vec<u8>> {
30        if start > self.size || length as u64 > self.size - start {
31            return Err(invalid("CD file: range outside payload"));
32        }
33        if length == 0 {
34            return Ok(Vec::new());
35        }
36        let first = start / 2048;
37        let last = (start + length as u64 - 1) / 2048;
38        let physical = (u64::from(self.extent) + first) * self.sector + self.offset;
39        let span = (last - first) * self.sector + ((start + length as u64 - 1) % 2048) + 1;
40        let mut sectors =
41            vec![0; usize::try_from(span).map_err(|_| invalid("CD file: range too large"))?];
42        reader.seek(SeekFrom::Start(physical))?;
43        reader.read_exact(&mut sectors)?;
44        let mut bytes = Vec::with_capacity(length);
45        for index in first..=last {
46            let begin = if index == first { start % 2048 } else { 0 };
47            let count = (2048 - begin).min(length as u64 - bytes.len() as u64);
48            let offset = usize::try_from((index - first) * self.sector + begin)
49                .map_err(|_| invalid("CD file: range too large"))?;
50            bytes.extend_from_slice(&sectors[offset..offset + count as usize]);
51        }
52        Ok(bytes)
53    }
54}
55
56fn invalid(message: &str) -> io::Error {
57    io::Error::new(io::ErrorKind::InvalidData, message)
58}
59fn le32(bytes: &[u8], start: usize) -> u32 {
60    u32::from_le_bytes(bytes[start..start + 4].try_into().unwrap())
61}
62
63impl<R: Read + Seek> CdImage<R> {
64    /// Detect a standard 2048-byte ISO or MODE1 raw sector layout.
65    pub fn open(mut reader: R) -> io::Result<Self> {
66        for (sector, offset) in [(2048, 0), (2352, 16)] {
67            reader.seek(SeekFrom::Start(16 * sector + offset))?;
68            let mut pvd = [0; 2048];
69            if reader.read_exact(&mut pvd).is_ok() && pvd[..7] == *b"\x01CD001\x01" {
70                if pvd[156] < 34 || u16::from_le_bytes([pvd[128], pvd[129]]) != 2048 {
71                    return Err(invalid("CD image: invalid primary descriptor"));
72                }
73                let root = (le32(&pvd, 158), le32(&pvd, 166));
74                return Ok(Self {
75                    reader,
76                    sector,
77                    offset,
78                    root,
79                });
80            }
81        }
82        Err(invalid("CD image: unsupported ISO 9660 sector layout"))
83    }
84    fn block(&mut self, index: u32) -> io::Result<[u8; 2048]> {
85        self.reader.seek(SeekFrom::Start(
86            u64::from(index) * self.sector + self.offset,
87        ))?;
88        let mut bytes = [0; 2048];
89        self.reader.read_exact(&mut bytes)?;
90        Ok(bytes)
91    }
92    /// Locate a root-directory file without reading its payload.
93    /// Version suffixes are ignored; directories and multi-extent records are unsupported.
94    pub fn file(&mut self, name: &str) -> io::Result<CdFile> {
95        if name.is_empty() || name.contains(['/', '\\', ':']) || name == "." || name == ".." {
96            return Err(invalid("CD image: expected root filename"));
97        }
98        let (extent, size) = self.root;
99        if size > 16 * 1024 * 1024 {
100            return Err(invalid("CD image: root directory too large"));
101        }
102        let mut found = None;
103        for index in 0..size.div_ceil(2048) {
104            let block = self.block(
105                extent
106                    .checked_add(index)
107                    .ok_or_else(|| invalid("CD extent overflow"))?,
108            )?;
109            let limit = (size - index * 2048).min(2048) as usize;
110            let mut pos = 0;
111            while pos < limit && block[pos] != 0 {
112                let len = block[pos] as usize;
113                if len < 34 || pos + len > limit {
114                    return Err(invalid("CD image: invalid directory record"));
115                }
116                let record = &block[pos..pos + len];
117                let n = record[32] as usize;
118                if 33 + n > len {
119                    return Err(invalid("CD image: invalid filename length"));
120                }
121                if let Ok(file) = std::str::from_utf8(&record[33..33 + n])
122                    && file
123                        .split(';')
124                        .next()
125                        .unwrap_or_default()
126                        .eq_ignore_ascii_case(name)
127                {
128                    if record[25] & 0x82 != 0 {
129                        return Err(invalid("CD image: directory/multi-extent file unsupported"));
130                    }
131                    found = Some((le32(record, 2), le32(record, 10)));
132                    break;
133                }
134                pos += len;
135            }
136            if found.is_some() {
137                break;
138            }
139        }
140        let (extent, size) = found.ok_or_else(|| {
141            io::Error::new(io::ErrorKind::NotFound, format!("CD image: missing {name}"))
142        })?;
143        Ok(CdFile {
144            size: u64::from(size),
145            extent,
146            sector: self.sector,
147            offset: self.offset,
148        })
149    }
150
151    /// Copy a root-directory filename without buffering the movie.
152    pub fn copy_file(&mut self, name: &str, output: &mut impl Write) -> io::Result<u64> {
153        let file = self.file(name)?;
154        let mut start = 0;
155        while start < file.size {
156            let length = usize::try_from((file.size - start).min(64 * 1024)).unwrap();
157            output.write_all(&file.read_range(&mut self.reader, start, length)?)?;
158            start += length as u64;
159        }
160        Ok(file.size)
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use std::io::Cursor;
168    #[test]
169    fn reads_payload_without_sector_headers_and_rejects_truncation() {
170        for (sector, offset) in [(2048, 0), (2352, 16)] {
171            let mut bytes = vec![0; 23 * sector];
172            let pvd = 16 * sector + offset;
173            bytes[pvd..pvd + 7].copy_from_slice(b"\x01CD001\x01");
174            bytes[pvd + 128..pvd + 130].copy_from_slice(&2048u16.to_le_bytes());
175            bytes[pvd + 156] = 34;
176            bytes[pvd + 158..pvd + 162].copy_from_slice(&20u32.to_le_bytes());
177            bytes[pvd + 166..pvd + 170].copy_from_slice(&2048u32.to_le_bytes());
178            let record = 20 * sector + offset;
179            bytes[record] = 42;
180            bytes[record + 2..record + 6].copy_from_slice(&21u32.to_le_bytes());
181            bytes[record + 10..record + 14].copy_from_slice(&2051u32.to_le_bytes());
182            bytes[record + 32] = 8;
183            bytes[record + 33..record + 41].copy_from_slice(b"TEST.X;1");
184            bytes[21 * sector + offset..21 * sector + offset + 2048].fill(b'a');
185            bytes[22 * sector + offset..22 * sector + offset + 3].copy_from_slice(b"bcd");
186            let mut cd = CdImage::open(Cursor::new(bytes.clone())).unwrap();
187            let mut out = Vec::new();
188            let file = cd.file("test.x").unwrap();
189            assert_eq!(file.size, 2051);
190            assert_eq!(
191                file.read_range(&mut Cursor::new(bytes.clone()), 2047, 4)
192                    .unwrap(),
193                b"abcd"
194            );
195            assert_eq!(
196                file.read_range(&mut Cursor::new(bytes.clone()), 2051, 0)
197                    .unwrap(),
198                b""
199            );
200            assert!(
201                file.read_range(&mut Cursor::new(bytes.clone()), 2050, 2)
202                    .is_err()
203            );
204            assert_eq!(cd.copy_file("test.x", &mut out).unwrap(), 2051);
205            assert_eq!(out, [vec![b'a'; 2048], b"bcd".to_vec()].concat());
206            assert_eq!(
207                cd.copy_file("absent", &mut out).unwrap_err().kind(),
208                io::ErrorKind::NotFound
209            );
210            bytes.truncate(21 * sector + offset + 2);
211            let mut cd = CdImage::open(Cursor::new(bytes)).unwrap();
212            assert!(cd.copy_file("test.x", &mut Vec::new()).is_err());
213        }
214    }
215}