1use std::io::{self, Read, Seek, SeekFrom, Write};
3
4pub struct CdImage<R> {
6 reader: R,
7 sector: u64,
8 offset: u64,
9 root: (u32, u32),
10}
11
12#[derive(Debug, Clone)]
15pub struct CdFile {
16 pub size: u64,
18 extent: u32,
19 sector: u64,
20 offset: u64,
21}
22impl CdFile {
23 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(§ors[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 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 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 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}