Deserialize a change from a reader (V3 format). Reads a complete V3 change file, validates the structure, decompresses sections, and verifies the content hash against the trailer. # Arguments `reader` - Where to read the serialized change from # Returns A tuple of `(change, hash)` where hash is the verified content hash. # Errors Returns an error if: - The file header is invalid (wrong magi
(reader: &mut R)
| 456 | /// - Any section fails to decompress or deserialize |
| 457 | /// - The content hash doesn't match the trailer |
| 458 | pub fn deserialize<R: Read>(reader: &mut R) -> Result<(Self, Hash), ChangeError> { |
| 459 | use format_v3::*; |
| 460 | |
| 461 | // Detect V2 (bincode) format and give a clear error. |
| 462 | // V2 files start with a little-endian u64 version number (1 or 2). |
| 463 | // V3 files start with b"ATOM". If the first 4 bytes look like a |
| 464 | // small integer rather than ASCII "ATOM", it's an old format. |
| 465 | // |
| 466 | // We read into a buffer first so we can pass it to ChangeReader |
| 467 | // if it IS V3 (since Read is not Seek). |
| 468 | let mut header_peek = [0u8; 4]; |
| 469 | reader.read_exact(&mut header_peek)?; |
| 470 | |
| 471 | if &header_peek != b"ATOM" { |
| 472 | // Check if it looks like a V2 version number (1 or 2 as LE u64) |
| 473 | let maybe_version = u32::from_le_bytes(header_peek); |
| 474 | if maybe_version == 1 || maybe_version == 2 { |
| 475 | return Err(ChangeError::Invalid(format!( |
| 476 | "This change file uses the legacy V2 format (version {}). \ |
| 477 | It must be re-recorded with the current version of Atomic. \ |
| 478 | Delete .atomic/ and run: atomic init && atomic add -r . && atomic record -m \"re-record\"", |
| 479 | maybe_version |
| 480 | ))); |
| 481 | } |
| 482 | return Err(ChangeError::Format(FormatError::InvalidMagic { |
| 483 | got: header_peek, |
| 484 | })); |
| 485 | } |
| 486 | |
| 487 | // It's V3 — reconstruct a reader with the magic bytes prepended. |
| 488 | // Chain the 4 bytes we already read with the rest of the stream. |
| 489 | let rest = reader; |
| 490 | let mut combined = std::io::Cursor::new(header_peek).chain(rest); |
| 491 | |
| 492 | // 1. Open the reader (reads header + hash table) |
| 493 | let mut change_reader = ChangeReader::open(&mut combined)?; |
| 494 | let hash_table = change_reader.hash_table().clone(); |
| 495 | |
| 496 | // 2. Read all sections |
| 497 | let mut header: Option<ChangeHeader> = None; |
| 498 | let mut dependencies: Vec<Hash> = Vec::new(); |
| 499 | let mut provenance: Vec<Provenance> = Vec::new(); |
| 500 | let mut hunks: Vec<GraphOp<Option<Hash>>> = Vec::new(); |
| 501 | let mut file_ops: Vec<FileOps> = Vec::new(); |
| 502 | let mut contents: Vec<u8> = Vec::new(); |
| 503 | let mut unhashed: Option<serde_json::Value> = None; |
| 504 | |
| 505 | while let Some(section) = change_reader.next_section()? { |
| 506 | match section.section_type { |
| 507 | SectionType::Header => { |
| 508 | header = Some(section.deserialize()?); |
| 509 | } |
| 510 | SectionType::Dependencies => { |
| 511 | let dep_indices: Vec<u16> = section.deserialize()?; |
| 512 | for idx in dep_indices { |
| 513 | if let Some(hash_bytes) = hash_table.resolve(idx) { |
| 514 | dependencies.push(Hash::from_bytes(*hash_bytes)); |
| 515 | } |
nothing calls this directly
no test coverage detected