1#![forbid(unsafe_code)]
16#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
17
18mod segment;
19
20#[cfg(feature = "testfix")]
21pub mod testfix;
22
23#[cfg(feature = "vfs")]
24mod vfs;
25#[cfg(feature = "vfs")]
26pub use vfs::Ad1Vfs;
27
28use std::collections::HashSet;
29use std::fs::File;
30use std::io::Read;
31use std::path::Path;
32
33use flate2::read::ZlibDecoder;
34use safe_read::{le_u32, le_u64};
35use segment::SegmentSet;
36
37pub const AD1_SEGMENTED_MARKER: &[u8] = b"ADSEGMENTEDFILE\x00";
39
40const HEADER_WINDOW: usize = 0x300;
43const MAX_NAME_LEN: usize = 4096;
45const MAX_META_DATA: usize = 65_536;
47const MAX_META_RECORDS: usize = 4096;
49const MAX_ENTRIES: usize = 5_000_000;
51const MAX_CHUNK_SIZE: u32 = 64 * 1024 * 1024;
53
54const ITEM_TYPE_FOLDER: u32 = 0x05;
56
57#[derive(Debug, thiserror::Error)]
59pub enum Ad1Error {
60 #[error("I/O error: {0}")]
61 Io(#[from] std::io::Error),
62 #[error("not an AD1 image: {0}")]
63 NotAd1(String),
64 #[error("unsupported AD1 feature: {0}")] Unsupported(String),
66 #[error("malformed AD1 structure: {0}")]
67 Malformed(String),
68}
69
70#[derive(Debug, Clone)]
72pub struct Ad1Entry {
73 pub path: String,
75 pub is_dir: bool,
77 pub size: u64,
79 pub item_type: u32,
81 pub md5: Option<String>,
83 pub sha1: Option<String>,
85 pub modified: Option<String>,
87 pub accessed: Option<String>,
89 pub changed: Option<String>,
91 pub(crate) zlib_addr: u64,
93}
94
95#[derive(Debug)]
97pub struct Ad1Reader {
98 segments: SegmentSet,
99 image_version: u32,
100 chunk_size: u32,
101 segment_count: u32,
102 entries: Vec<Ad1Entry>,
103}
104
105struct RawItem {
107 next_item_addr: u64,
108 first_child_addr: u64,
109 first_metadata_addr: u64,
110 zlib_metadata_addr: u64,
111 decompressed_size: u64,
112 item_type: u32,
113 name: String,
114}
115
116impl Ad1Reader {
117 pub fn open(first_segment: &Path) -> Result<Self, Ad1Error> {
125 let mut f = File::open(first_segment)?;
126 let mut head = vec![0u8; HEADER_WINDOW];
127 let n = f.read(&mut head)?;
128 head.truncate(n);
129
130 if head.len() >= 8 && &head[0..7] == b"ADCRYPT" {
132 return Err(Ad1Error::Unsupported(format!(
133 "ADCRYPT (encrypted AD1) — decryption is out of scope; signature {}",
134 hex_preview(&head)
135 )));
136 }
137 if head.len() < 15 || &head[0..15] != b"ADSEGMENTEDFILE" {
138 return Err(Ad1Error::NotAd1(format!(
139 "expected ADSEGMENTEDFILE, found signature {}",
140 hex_preview(&head)
141 )));
142 }
143
144 let segment_count = le_u32(&head, 0x1c).max(1);
146 let fragments_size = le_u32(&head, 0x22);
147 if fragments_size == 0 {
148 return Err(Ad1Error::Malformed(
149 "segment header fragments_size is 0".into(),
150 ));
151 }
152
153 let image_version = le_u32(&head, 0x210);
155 let chunk_size = le_u32(&head, 0x218);
156 let first_item_addr = le_u64(&head, 0x224);
157 if chunk_size == 0 || chunk_size > MAX_CHUNK_SIZE {
158 return Err(Ad1Error::Malformed(format!(
159 "implausible zlib chunk size {chunk_size} (image version {image_version})"
160 )));
161 }
162
163 let segments = SegmentSet::open(first_segment, segment_count, fragments_size)?;
164
165 let mut entries = Vec::new();
166 if first_item_addr != 0 {
167 walk_tree(&segments, first_item_addr, &mut entries)?;
168 }
169
170 Ok(Self {
171 segments,
172 image_version,
173 chunk_size,
174 segment_count,
175 entries,
176 })
177 }
178
179 #[must_use]
181 pub fn entries(&self) -> &[Ad1Entry] {
182 &self.entries
183 }
184
185 #[must_use]
187 pub fn image_version(&self) -> u32 {
188 self.image_version
189 }
190
191 #[must_use]
193 pub fn chunk_size(&self) -> u32 {
194 self.chunk_size
195 }
196
197 #[must_use]
199 pub fn segment_count(&self) -> u32 {
200 self.segment_count
201 }
202
203 #[must_use]
206 pub fn missing_segments(&self) -> Vec<u32> {
207 self.segments.missing()
208 }
209
210 pub fn read_at(
220 &self,
221 entry: &Ad1Entry,
222 offset: u64,
223 buf: &mut [u8],
224 ) -> Result<usize, Ad1Error> {
225 if entry.is_dir || entry.size == 0 || offset >= entry.size || buf.is_empty() {
226 return Ok(0);
227 }
228 let want_total = (buf.len() as u64).min(entry.size - offset) as usize;
229 if want_total == 0 {
230 return Ok(0);
231 }
232 if entry.zlib_addr == 0 {
233 return Err(Ad1Error::Malformed(format!(
234 "file '{}' has {} bytes but no chunk table",
235 entry.path, entry.size
236 )));
237 }
238
239 let cs = u64::from(self.chunk_size);
240 let count = le_u64(&self.segments.read(entry.zlib_addr, 8)?, 0);
241 let max_addrs = self.segments.capacity() / 8 + 2;
244 if count == 0 || count >= max_addrs {
245 return Err(Ad1Error::Malformed(format!(
246 "file '{}' declares implausible chunk count {count}",
247 entry.path
248 )));
249 }
250 let table_len = (count as usize).saturating_add(1).saturating_mul(8);
251 let addr_bytes = self
252 .segments
253 .read(entry.zlib_addr.saturating_add(8), table_len)?;
254 let addr = |i: u64| le_u64(&addr_bytes, (i.saturating_mul(8)) as usize);
255
256 let end = offset.saturating_add(want_total as u64);
257 let mut produced = 0usize;
258 let mut cur = offset;
259 let mut ci = offset / cs;
260 while cur < end && ci < count {
261 let (start, stop) = (addr(ci), addr(ci + 1));
262 if stop < start {
263 return Err(Ad1Error::Malformed(format!(
264 "file '{}' chunk {ci} has non-monotonic addresses",
265 entry.path
266 )));
267 }
268 let comp = self.segments.read(start, (stop - start) as usize)?;
269 let raw = inflate(&comp, self.chunk_size as usize)?;
270 let chunk_base = ci.saturating_mul(cs);
271 let chunk_end = chunk_base.saturating_add(raw.len() as u64);
272 if cur < chunk_base {
276 break;
277 }
278 if cur < chunk_end {
279 let from = (cur - chunk_base) as usize;
280 let n = (raw.len() - from).min((end - cur) as usize);
281 buf[produced..produced + n].copy_from_slice(&raw[from..from + n]);
282 produced += n;
283 cur += n as u64;
284 }
285 ci += 1;
286 }
287 Ok(produced)
288 }
289}
290
291fn hex_preview(buf: &[u8]) -> String {
293 use std::fmt::Write as _;
294 let take = buf.len().min(16);
295 let mut s = String::with_capacity(take * 2);
296 for b in &buf[..take] {
297 let _ = write!(s, "{b:02x}");
298 }
299 s
300}
301
302fn inflate(comp: &[u8], max: usize) -> Result<Vec<u8>, Ad1Error> {
304 let mut out = Vec::new();
305 ZlibDecoder::new(comp)
306 .take(max as u64)
307 .read_to_end(&mut out)?;
308 Ok(out)
309}
310
311fn read_item(seg: &SegmentSet, addr: u64) -> Result<RawItem, Ad1Error> {
313 let head = seg.read(addr, 0x30)?;
314 let name_len = le_u32(&head, 0x2c) as usize;
315 if name_len > MAX_NAME_LEN {
316 return Err(Ad1Error::Malformed(format!(
317 "item at {addr:#x} declares name length {name_len} (> {MAX_NAME_LEN})"
318 )));
319 }
320 let name_bytes = seg.read(addr + 0x30, name_len)?;
321 let name: String = String::from_utf8_lossy(&name_bytes)
323 .chars()
324 .map(|c| if c == '/' { '_' } else { c })
325 .collect();
326 Ok(RawItem {
327 next_item_addr: le_u64(&head, 0x00),
328 first_child_addr: le_u64(&head, 0x08),
329 first_metadata_addr: le_u64(&head, 0x10),
330 zlib_metadata_addr: le_u64(&head, 0x18),
331 decompressed_size: le_u64(&head, 0x20),
332 item_type: le_u32(&head, 0x28),
333 name,
334 })
335}
336
337#[derive(Default)]
339struct Meta {
340 md5: Option<String>,
341 sha1: Option<String>,
342 modified: Option<String>,
343 accessed: Option<String>,
344 changed: Option<String>,
345}
346
347fn read_metadata(seg: &SegmentSet, first_addr: u64) -> Result<Meta, Ad1Error> {
349 let mut meta = Meta::default();
350 let mut addr = first_addr;
351 let mut seen = HashSet::new();
352 let mut count = 0usize;
353 while addr != 0 {
354 if !seen.insert(addr) {
355 break; }
357 count += 1;
358 if count > MAX_META_RECORDS {
362 return Err(Ad1Error::Malformed(format!(
363 "metadata chain exceeds {MAX_META_RECORDS} records"
364 )));
365 }
366 let h = seg.read(addr, 0x14)?;
367 let next = le_u64(&h, 0x00);
368 let category = le_u32(&h, 0x08);
369 let key = le_u32(&h, 0x0c);
370 let dlen = le_u32(&h, 0x10) as usize;
371 if dlen > MAX_META_DATA {
372 return Err(Ad1Error::Malformed(format!(
373 "metadata record at {addr:#x} declares data length {dlen} (> {MAX_META_DATA})"
374 )));
375 }
376 let data = seg.read(addr + 0x14, dlen)?;
377 let as_str = || {
378 String::from_utf8_lossy(&data)
379 .trim_end_matches('\0')
380 .to_string()
381 };
382 match (category, key) {
383 (0x01, 0x5001) => meta.md5 = Some(as_str()),
384 (0x01, 0x5002) => meta.sha1 = Some(as_str()),
385 (0x05, 0x07) => meta.accessed = Some(as_str()),
386 (0x05, 0x08) => meta.modified = Some(as_str()),
387 (0x05, 0x09) => meta.changed = Some(as_str()),
388 _ => {}
389 }
390 addr = next;
391 }
392 Ok(meta)
393}
394
395fn walk_tree(
400 seg: &SegmentSet,
401 first_item_addr: u64,
402 entries: &mut Vec<Ad1Entry>,
403) -> Result<(), Ad1Error> {
404 let mut stack: Vec<(u64, Option<String>)> = vec![(first_item_addr, None)];
405 let mut seen = HashSet::new();
406 while let Some((addr, parent_path)) = stack.pop() {
407 if addr == 0 {
408 continue;
409 }
410 if !seen.insert(addr) {
411 return Err(Ad1Error::Malformed(format!(
412 "tree cycle: item at {addr:#x} visited twice"
413 )));
414 }
415 if entries.len() >= MAX_ENTRIES {
418 return Err(Ad1Error::Malformed(format!(
419 "tree exceeds {MAX_ENTRIES} entries"
420 )));
421 }
422 let item = read_item(seg, addr)?;
423 let path = match &parent_path {
424 None => item.name.clone(),
425 Some(p) => format!("{p}/{}", item.name),
426 };
427 let is_dir = item.item_type == ITEM_TYPE_FOLDER;
428 let meta = read_metadata(seg, item.first_metadata_addr)?;
429 entries.push(Ad1Entry {
430 path: path.clone(),
431 is_dir,
432 size: item.decompressed_size,
433 item_type: item.item_type,
434 md5: meta.md5,
435 sha1: meta.sha1,
436 modified: meta.modified,
437 accessed: meta.accessed,
438 changed: meta.changed,
439 zlib_addr: item.zlib_metadata_addr,
440 });
441 if item.next_item_addr != 0 {
443 stack.push((item.next_item_addr, parent_path.clone()));
444 }
445 if item.first_child_addr != 0 {
446 stack.push((item.first_child_addr, Some(path)));
447 }
448 }
449 Ok(())
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 #[test]
457 fn marker_is_the_documented_string() {
458 assert_eq!(&AD1_SEGMENTED_MARKER[..15], b"ADSEGMENTEDFILE");
459 }
460
461 #[test]
462 fn open_missing_file_is_io_error() {
463 assert!(matches!(
464 Ad1Reader::open(Path::new("/nonexistent.ad1")),
465 Err(Ad1Error::Io(_))
466 ));
467 }
468
469 #[test]
470 fn hex_preview_caps_at_16_bytes() {
471 let buf = [0xabu8; 32];
472 assert_eq!(hex_preview(&buf).len(), 32); }
474}