1use core::ops::Range;
10
11use crate::block::header::{BLOCK_MAGIC, BlockHeader, is_block_magic};
12use crate::error::{Result, err};
13use crate::version::Version;
14
15pub const ASDF_HEADER_PREFIX: &[u8] = b"#ASDF ";
17pub const ASDF_STANDARD_PREFIX: &[u8] = b"#ASDF_STANDARD ";
19pub const BLOCK_INDEX_HEADER: &[u8] = b"#ASDF BLOCK INDEX";
21pub const YAML_DIRECTIVE_PREFIX: &[u8] = b"%YAML ";
23pub const YAML_DOCUMENT_END_MARKER: &[u8] = b"\n...";
25
26#[derive(Clone, PartialEq, Eq, Debug)]
28pub struct BlockLocation {
29 pub index: usize,
31 pub header_pos: u64,
33 pub data_pos: u64,
35 pub header: BlockHeader,
37}
38
39impl BlockLocation {
40 pub fn end_pos(&self) -> u64 {
48 self.data_pos.saturating_add(self.header.allocated_size)
49 }
50}
51
52#[derive(Clone, PartialEq, Eq, Debug)]
57pub enum IndexRejection {
58 Unparseable,
60 CountMismatch {
62 listed: usize,
64 found: usize,
66 },
67 FirstOffsetMismatch {
70 listed: u64,
72 actual: u64,
74 },
75 NotBlockMagic {
77 offset: u64,
79 },
80 NotMonotonic,
83 LastBlockNotAdjacent,
86}
87
88#[derive(Clone, Debug)]
90pub struct Layout {
91 pub format_version: Version,
93 pub standard_version: Option<Version>,
95 pub comments: Vec<String>,
98 pub tree: Option<Range<usize>>,
100 pub blocks: Vec<BlockLocation>,
102 pub block_index_pos: Option<u64>,
104 pub block_index_offsets: Vec<u64>,
109 pub index_rejection: Option<IndexRejection>,
111}
112
113impl Layout {
114 pub fn has_tree(&self) -> bool {
118 self.tree.is_some()
119 }
120
121 pub fn tree_str<'a>(&self, buf: &'a [u8]) -> Option<&'a str> {
123 let range = self.tree.clone()?;
124 core::str::from_utf8(&buf[range]).ok()
125 }
126
127 pub fn used_block_index(&self) -> bool {
129 self.block_index_pos.is_some() && self.index_rejection.is_none()
130 }
131}
132
133fn read_line(buf: &[u8], pos: usize) -> Option<(&[u8], usize)> {
136 if pos >= buf.len() {
137 return None;
138 }
139 match buf[pos..].iter().position(|b| *b == b'\n') {
140 Some(rel) => {
141 let nl = pos + rel;
142 let end = if nl > pos && buf[nl - 1] == b'\r' { nl - 1 } else { nl };
144 Some((&buf[pos..end], nl + 1))
145 }
146 None => Some((&buf[pos..], buf.len())),
148 }
149}
150
151fn scan_text_section(buf: &[u8], out: &mut Layout) -> Result<usize> {
154 let Some((line, mut pos)) = read_line(buf, 0) else {
155 return Err(err!(InvalidAsdfHeader, "file is empty"));
156 };
157
158 if !line.starts_with(ASDF_HEADER_PREFIX) {
159 return Err(err!(
160 InvalidAsdfHeader,
161 "file does not begin with the {:?} token",
162 String::from_utf8_lossy(ASDF_HEADER_PREFIX)
163 ));
164 }
165 let version = core::str::from_utf8(&line[ASDF_HEADER_PREFIX.len()..])
166 .map_err(|_| err!(InvalidAsdfHeader, "ASDF version is not valid UTF-8"))?;
167 out.format_version = Version::parse(version.trim());
168
169 while let Some((line, next)) = read_line(buf, pos) {
171 if !line.starts_with(b"#") {
172 break;
173 }
174 if let Some(rest) = line.strip_prefix(ASDF_STANDARD_PREFIX) {
175 if let Ok(s) = core::str::from_utf8(rest) {
176 out.standard_version = Some(Version::parse(s.trim()));
177 }
178 } else {
179 out.comments.push(String::from_utf8_lossy(&line[1..]).into_owned());
180 }
181 pos = next;
182 }
183
184 if pos >= buf.len() {
186 return Ok(pos);
187 }
188 if is_block_magic(&buf[pos..]) {
189 return Ok(pos);
190 }
191 if !buf[pos..].starts_with(YAML_DIRECTIVE_PREFIX) {
192 if !buf[pos..].starts_with(b"---") {
195 return Ok(pos);
196 }
197 }
198
199 let tree_start = pos;
200 let tree_end = find_document_end(buf, tree_start);
201 out.tree = Some(tree_start..tree_end);
202 Ok(tree_end)
203}
204
205fn find_document_end(buf: &[u8], start: usize) -> usize {
211 let mut search = start;
212 while let Some(rel) = find_bytes(&buf[search..], YAML_DOCUMENT_END_MARKER) {
213 let marker = search + rel;
214 let after = marker + YAML_DOCUMENT_END_MARKER.len();
215 match buf.get(after) {
217 None => return buf.len(),
218 Some(b'\n') => return after + 1,
219 Some(b'\r') if buf.get(after + 1) == Some(&b'\n') => return after + 2,
220 _ => search = after,
221 }
222 }
223 match find_bytes(&buf[start..], BLOCK_MAGIC) {
225 Some(rel) => start + rel,
226 None => buf.len(),
227 }
228}
229
230fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
232 if needle.is_empty() || haystack.len() < needle.len() {
233 return None;
234 }
235 haystack.windows(needle.len()).position(|w| w == needle)
236}
237
238fn rfind_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
240 if needle.is_empty() || haystack.len() < needle.len() {
241 return None;
242 }
243 haystack.windows(needle.len()).rposition(|w| w == needle)
244}
245
246fn scan_blocks(buf: &[u8], mut pos: usize, out: &mut Layout) -> Result<()> {
251 if !is_block_magic(buf.get(pos..).unwrap_or(&[])) {
253 match find_bytes(&buf[pos.min(buf.len())..], BLOCK_MAGIC) {
254 Some(rel) => pos += rel,
255 None => return Ok(()),
256 }
257 }
258
259 while pos < buf.len() {
260 if !is_block_magic(&buf[pos..]) {
261 break;
262 }
263 let (header, consumed) = BlockHeader::parse(&buf[pos..])?;
264 let data_pos = pos + consumed;
265
266 let location = BlockLocation {
267 index: out.blocks.len(),
268 header_pos: pos as u64,
269 data_pos: data_pos as u64,
270 header: header.clone(),
271 };
272
273 if header.is_streamed() {
274 out.blocks.push(location);
277 return Ok(());
278 }
279
280 let end = location.end_pos();
281 if end > buf.len() as u64 {
282 return Err(err!(
283 UnexpectedEof,
284 "block {} claims {} bytes but the file ends at {}",
285 location.index,
286 header.allocated_size,
287 buf.len()
288 ));
289 }
290 out.blocks.push(location);
291 pos = end as usize;
292 }
293 Ok(())
294}
295
296fn parse_index_offsets(text: &str) -> Option<Vec<u64>> {
298 let mut offsets = Vec::new();
299 let mut saw_any = false;
300
301 for raw in text.lines() {
302 let line = raw.trim();
303 if line.is_empty()
304 || line.starts_with('%')
305 || line == "---"
306 || line == "..."
307 || line.starts_with('#')
308 {
309 continue;
310 }
311 if let Some(rest) = line.strip_prefix("- ") {
313 offsets.push(rest.trim().parse::<u64>().ok()?);
314 saw_any = true;
315 continue;
316 }
317 let body = line.strip_prefix("---").unwrap_or(line).trim();
319 let body = body.strip_prefix('[')?.strip_suffix(']')?;
320 for part in body.split(',') {
321 let p = part.trim();
322 if p.is_empty() {
323 continue;
324 }
325 offsets.push(p.parse::<u64>().ok()?);
326 }
327 saw_any = true;
328 }
329
330 saw_any.then_some(offsets)
331}
332
333fn scan_block_index(buf: &[u8], out: &mut Layout) {
336 let Some(pos) = rfind_bytes(buf, BLOCK_INDEX_HEADER) else {
338 return;
339 };
340 out.block_index_pos = Some(pos as u64);
341
342 let text = String::from_utf8_lossy(&buf[pos + BLOCK_INDEX_HEADER.len()..]);
343 let Some(offsets) = parse_index_offsets(&text) else {
344 out.index_rejection = Some(IndexRejection::Unparseable);
345 return;
346 };
347 out.block_index_offsets.clone_from(&offsets);
348
349 if offsets.windows(2).any(|w| w[1] <= w[0]) {
350 out.index_rejection = Some(IndexRejection::NotMonotonic);
351 return;
352 }
353 if offsets.len() != out.blocks.len() {
354 out.index_rejection =
355 Some(IndexRejection::CountMismatch { listed: offsets.len(), found: out.blocks.len() });
356 return;
357 }
358 if let (Some(first_listed), Some(first_block)) = (offsets.first(), out.blocks.first())
359 && *first_listed != first_block.header_pos
360 {
361 out.index_rejection = Some(IndexRejection::FirstOffsetMismatch {
362 listed: *first_listed,
363 actual: first_block.header_pos,
364 });
365 return;
366 }
367 for off in &offsets {
368 let ok = usize::try_from(*off).ok().and_then(|o| buf.get(o..)).is_some_and(is_block_magic);
369 if !ok {
370 out.index_rejection = Some(IndexRejection::NotBlockMagic { offset: *off });
371 return;
372 }
373 }
374 if let Some(last) = out.blocks.last()
375 && last.end_pos() != pos as u64
376 {
377 out.index_rejection = Some(IndexRejection::LastBlockNotAdjacent);
378 }
379}
380
381pub fn scan(buf: &[u8]) -> Result<Layout> {
383 let mut out = Layout {
384 format_version: Version::default(),
385 standard_version: None,
386 comments: Vec::new(),
387 tree: None,
388 blocks: Vec::new(),
389 block_index_pos: None,
390 block_index_offsets: Vec::new(),
391 index_rejection: None,
392 };
393
394 let after_text = scan_text_section(buf, &mut out)?;
395 scan_blocks(buf, after_text, &mut out)?;
396 scan_block_index(buf, &mut out);
397 Ok(out)
398}
399
400pub fn write_block_index(offsets: &[u64]) -> Vec<u8> {
402 let mut out = Vec::new();
403 out.extend_from_slice(BLOCK_INDEX_HEADER);
404 out.push(b'\n');
405 out.extend_from_slice(b"%YAML 1.1\n---\n");
406 for off in offsets {
407 out.extend_from_slice(format!("- {off}\n").as_bytes());
408 }
409 out.extend_from_slice(b"...\n");
410 out
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::block::header::BLOCK_HEADER_FULL_SIZE;
417 use crate::block::header::FLAG_STREAMED;
418 use crate::error::ErrorCode;
419
420 fn build(tree: Option<&str>, block_payloads: &[&[u8]], with_index: bool) -> Vec<u8> {
422 let mut buf = Vec::new();
423 buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n");
424 if let Some(t) = tree {
425 buf.extend_from_slice(b"%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n");
426 buf.extend_from_slice(t.as_bytes());
427 buf.extend_from_slice(b"\n...\n");
428 }
429 let mut offsets = Vec::new();
430 for payload in block_payloads {
431 offsets.push(buf.len() as u64);
432 let h = BlockHeader {
433 allocated_size: payload.len() as u64,
434 used_size: payload.len() as u64,
435 data_size: payload.len() as u64,
436 ..Default::default()
437 };
438 h.write(&mut buf);
439 buf.extend_from_slice(payload);
440 }
441 if with_index && !offsets.is_empty() {
442 buf.extend_from_slice(&write_block_index(&offsets));
443 }
444 buf
445 }
446
447 #[test]
448 fn reads_header_and_standard_version() {
449 let buf = build(Some("foo: 1"), &[], false);
450 let l = scan(&buf).unwrap();
451 assert_eq!(l.format_version.triple(), (1, 0, 0));
452 assert_eq!(l.standard_version.unwrap().triple(), (1, 6, 0));
453 }
454
455 #[test]
456 fn rejects_a_file_without_the_asdf_token() {
457 let e = scan(b"not an asdf file\n").unwrap_err();
458 assert_eq!(e.code(), ErrorCode::InvalidAsdfHeader);
459 assert_eq!(scan(b"").unwrap_err().code(), ErrorCode::InvalidAsdfHeader);
460 }
461
462 #[test]
463 fn finds_the_tree_extent() {
464 let buf = build(Some("foo: 1"), &[], false);
465 let l = scan(&buf).unwrap();
466 let tree = l.tree_str(&buf).unwrap();
467 assert!(tree.starts_with("%YAML 1.1\n"));
468 assert!(tree.trim_end().ends_with("..."));
469 assert!(tree.contains("foo: 1"));
470 }
471
472 #[test]
473 fn handles_dos_line_endings() {
474 let mut buf = Vec::new();
475 buf.extend_from_slice(b"#ASDF 1.0.0\r\n#ASDF_STANDARD 1.6.0\r\n");
476 buf.extend_from_slice(b"%YAML 1.1\r\n--- !core/asdf-1.1.0\r\nfoo: 1\r\n...\r\n");
477 let l = scan(&buf).unwrap();
478 assert_eq!(l.format_version.triple(), (1, 0, 0));
479 assert_eq!(l.standard_version.as_ref().unwrap().triple(), (1, 6, 0));
480 assert!(l.has_tree());
481 assert!(l.tree_str(&buf).unwrap().contains("foo: 1"));
482 }
483
484 #[test]
485 fn collects_other_comments() {
486 let mut buf = Vec::new();
487 buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n# a note\n");
488 buf.extend_from_slice(b"%YAML 1.1\n--- !core/asdf-1.1.0\nfoo: 1\n...\n");
489 let l = scan(&buf).unwrap();
490 assert_eq!(l.comments, [" a note"]);
491 }
492
493 #[test]
494 fn a_file_may_have_no_tree() {
495 let buf = build(None, &[b"abcd"], false);
497 let l = scan(&buf).unwrap();
498 assert!(!l.has_tree());
499 assert_eq!(l.blocks.len(), 1);
500 }
501
502 #[test]
503 fn walks_blocks_by_skipping_along() {
504 let buf = build(Some("x: 1"), &[b"aaaa", b"bbbbbbbb", b"c"], false);
505 let l = scan(&buf).unwrap();
506 assert_eq!(l.blocks.len(), 3);
507 assert_eq!(l.blocks[0].header.used_size, 4);
508 assert_eq!(l.blocks[1].header.used_size, 8);
509 assert_eq!(l.blocks[2].header.used_size, 1);
510
511 for (b, expect) in l.blocks.iter().zip([&b"aaaa"[..], b"bbbbbbbb", b"c"]) {
513 let start = b.data_pos as usize;
514 let end = start + b.header.used_size as usize;
515 assert_eq!(&buf[start..end], expect);
516 }
517 }
518
519 #[test]
520 fn tolerates_padding_between_tree_and_first_block() {
521 let mut buf = build(Some("x: 1"), &[], false);
522 buf.extend_from_slice(&[b' '; 64]); let block_at = buf.len() as u64;
524 let h = BlockHeader { allocated_size: 4, used_size: 4, data_size: 4, ..Default::default() };
525 h.write(&mut buf);
526 buf.extend_from_slice(b"data");
527
528 let l = scan(&buf).unwrap();
529 assert_eq!(l.blocks.len(), 1);
530 assert_eq!(l.blocks[0].header_pos, block_at);
531 }
532
533 #[test]
534 fn accepts_a_correct_block_index() {
535 let buf = build(Some("x: 1"), &[b"aaaa", b"bbbb"], true);
536 let l = scan(&buf).unwrap();
537 assert_eq!(l.blocks.len(), 2);
538 assert!(l.used_block_index(), "index should be accepted: {:?}", l.index_rejection);
539 }
540
541 #[test]
542 fn rejects_an_index_whose_first_offset_is_stale() {
543 let mut buf = build(Some("x: 1"), &[b"aaaa"], true);
546 let idx = rfind_bytes(&buf, BLOCK_INDEX_HEADER).unwrap();
547 let tail = write_block_index(&[9999]);
548 buf.truncate(idx);
549 buf.extend_from_slice(&tail);
550
551 let l = scan(&buf).unwrap();
552 assert!(!l.used_block_index());
553 assert!(matches!(l.index_rejection, Some(IndexRejection::FirstOffsetMismatch { .. })));
554 assert_eq!(l.blocks.len(), 1);
556 }
557
558 #[test]
559 fn rejects_a_non_monotonic_index() {
560 let mut buf = build(Some("x: 1"), &[b"aaaa", b"bbbb"], false);
561 buf.extend_from_slice(&write_block_index(&[500, 100]));
562 let l = scan(&buf).unwrap();
563 assert_eq!(l.index_rejection, Some(IndexRejection::NotMonotonic));
564 }
565
566 #[test]
567 fn rejects_an_index_with_the_wrong_count() {
568 let mut buf = build(Some("x: 1"), &[b"aaaa", b"bbbb"], false);
569 let first = buf.windows(4).position(|w| w == BLOCK_MAGIC).unwrap() as u64;
570 buf.extend_from_slice(&write_block_index(&[first]));
571 let l = scan(&buf).unwrap();
572 assert!(matches!(
573 l.index_rejection,
574 Some(IndexRejection::CountMismatch { listed: 1, found: 2 })
575 ));
576 }
577
578 #[test]
579 fn parses_both_index_styles() {
580 assert_eq!(
582 parse_index_offsets("%YAML 1.1\n---\n- 901\n- 1024\n...\n"),
583 Some(vec![901, 1024])
584 );
585 assert_eq!(
587 parse_index_offsets("%YAML 1.1\n--- [2043, 16340]\n...\n"),
588 Some(vec![2043, 16340])
589 );
590 }
591
592 #[test]
593 fn streamed_block_ends_the_scan() {
594 let mut buf = build(Some("x: 1"), &[], false);
595 let h = BlockHeader { flags: FLAG_STREAMED, ..Default::default() };
596 h.write(&mut buf);
597 buf.extend_from_slice(b"streaming payload, length unknown up front");
598
599 let l = scan(&buf).unwrap();
600 assert_eq!(l.blocks.len(), 1);
601 assert!(l.blocks[0].header.is_streamed());
602 }
603
604 #[test]
605 fn block_running_past_eof_is_an_error() {
606 let mut buf = build(Some("x: 1"), &[], false);
607 let h = BlockHeader {
608 allocated_size: 1_000_000,
609 used_size: 1_000_000,
610 data_size: 1_000_000,
611 ..Default::default()
612 };
613 h.write(&mut buf);
614 buf.extend_from_slice(b"short");
615 assert_eq!(scan(&buf).unwrap_err().code(), ErrorCode::UnexpectedEof);
616 }
617
618 #[test]
619 fn index_round_trips() {
620 let rendered = write_block_index(&[901, 2048]);
621 let text = String::from_utf8(rendered.clone()).unwrap();
622 assert!(text.starts_with("#ASDF BLOCK INDEX\n"));
623 let body = &text[BLOCK_INDEX_HEADER.len()..];
624 assert_eq!(parse_index_offsets(body), Some(vec![901, 2048]));
625 assert_eq!(BLOCK_HEADER_FULL_SIZE, 54);
626 }
627}