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