Skip to main content

dua/snapshot/
mod.rs

1//! Streaming codec for dua traversal snapshots.
2
3use crate::traverse::{EntryData, Traversal, TreeIndex};
4use anyhow::{Context, Result, anyhow, bail};
5use std::{
6    borrow::Cow,
7    collections::HashSet,
8    io::{self, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write},
9    path::Path,
10    time::{Duration, SystemTime, UNIX_EPOCH},
11};
12
13const MAGIC: &[u8; 8] = b"DUASNAP\0";
14const VERSION: u16 = 1;
15const HEADER_LEN: usize = 12;
16const MAX_NAME_LEN: usize = 1024 * 1024;
17const MAX_RECORD_LEN: usize = 2 * 1024 * 1024;
18const DIGEST_LEN: usize = 32;
19
20/// Encoding 0 stores Unix `OsStr` bytes verbatim.
21#[cfg(unix)]
22const PATH_ENCODING: u8 = 0;
23/// Encoding 1 stores Windows `OsStr` wide units as little-endian `u16`s.
24#[cfg(windows)]
25const PATH_ENCODING: u8 = 1;
26
27const FLAG_DIRECTORY: u8 = 0x01;
28const FLAG_METADATA_IO_ERROR: u8 = 0x02;
29const FLAG_ENTRY_COUNT: u8 = 0x04;
30const KNOWN_FLAGS: u8 = FLAG_DIRECTORY | FLAG_METADATA_IO_ERROR | FLAG_ENTRY_COUNT;
31
32/// A verified traversal snapshot and its input roots in their original order.
33#[derive(Debug)]
34pub struct Snapshot {
35    /// The reconstructed traversal tree.
36    pub traversal: Traversal,
37    /// Top-level tree nodes in original input order.
38    pub roots: Vec<TreeIndex>,
39}
40
41pub(crate) struct DecodedEntry<'a> {
42    /// Zero-based depth in the serialized forest; top-level roots have depth zero.
43    pub(crate) depth: usize,
44    pub(crate) data: EntryData,
45    /// Lossless platform-native bytes borrowed from the decoder's reusable record buffer.
46    pub(crate) native_name: &'a [u8],
47    pub(crate) sibling_ordinal: u64,
48}
49
50impl<'a> DecodedEntry<'a> {
51    pub(crate) fn name(&self) -> Cow<'a, Path> {
52        native_name_from_bytes(self.native_name)
53    }
54}
55
56struct DecodeSummary {
57    total_size: u128,
58    total_entries: u64,
59    digest: [u8; DIGEST_LEN],
60}
61
62struct OpenNode {
63    id: u64,
64    is_dir: bool,
65    has_child: bool,
66    last_child_ordinal: u64,
67}
68
69enum SnapshotReader<R> {
70    Raw(BufReader<R>),
71    Zlib {
72        reader: BufReader<R>,
73        decompressor: gix::zlib::Decompress,
74        /// Remembers `StreamEnd` across reads so final output can be returned before the next read
75        /// verifies that no trailing compressed input remains.
76        finished: bool,
77    },
78}
79
80struct Decoder<R> {
81    reader: HashingReader<BufReader<SnapshotReader<R>>>,
82    open_nodes: Vec<OpenNode>,
83    sibling_names: Vec<Vec<u8>>,
84    record: Vec<u8>,
85    node_count: u64,
86    total_size: u128,
87    total_entries: u64,
88    summary: Option<DecodeSummary>,
89}
90
91impl<R: Read> SnapshotReader<R> {
92    fn new(reader: R) -> Result<Self> {
93        let mut reader = BufReader::new(reader);
94        if is_zlib_header(reader.fill_buf()?) {
95            Ok(Self::Zlib {
96                reader,
97                decompressor: gix::zlib::Decompress::new(),
98                finished: false,
99            })
100        } else {
101            Ok(Self::Raw(reader))
102        }
103    }
104}
105
106impl<R: Read> Read for SnapshotReader<R> {
107    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
108        match self {
109            Self::Raw(reader) => reader.read(buffer),
110            Self::Zlib {
111                reader,
112                decompressor,
113                finished,
114            } => {
115                if buffer.is_empty() {
116                    return Ok(0);
117                }
118                if *finished {
119                    return if reader.fill_buf()?.is_empty() {
120                        Ok(0)
121                    } else {
122                        Err(invalid_data("compressed snapshot has trailing data"))
123                    };
124                }
125                loop {
126                    let (status, consumed, written, eof) = {
127                        let input = reader.fill_buf()?;
128                        let eof = input.is_empty();
129                        let input_before = decompressor.total_in();
130                        let output_before = decompressor.total_out();
131                        let flush = if eof {
132                            gix::zlib::FlushDecompress::Finish
133                        } else {
134                            gix::zlib::FlushDecompress::None
135                        };
136                        let status =
137                            decompressor
138                                .decompress(input, buffer, flush)
139                                .map_err(|err| {
140                                    io::Error::new(
141                                        io::ErrorKind::InvalidData,
142                                        format!("could not decompress snapshot: {err}"),
143                                    )
144                                })?;
145                        (
146                            status,
147                            (decompressor.total_in() - input_before) as usize,
148                            (decompressor.total_out() - output_before) as usize,
149                            eof,
150                        )
151                    };
152                    reader.consume(consumed);
153
154                    match status {
155                        gix::zlib::Status::StreamEnd => {
156                            *finished = true;
157                            if written == 0 && !reader.fill_buf()?.is_empty() {
158                                return Err(invalid_data("compressed snapshot has trailing data"));
159                            }
160                            return Ok(written);
161                        }
162                        gix::zlib::Status::Ok | gix::zlib::Status::BufError => {
163                            if written != 0 {
164                                return Ok(written);
165                            }
166                            if eof {
167                                return Err(invalid_data("compressed snapshot is truncated"));
168                            }
169                            if consumed == 0 {
170                                return Err(invalid_data(
171                                    "compressed snapshot decoder made no progress",
172                                ));
173                            }
174                        }
175                    }
176                }
177            }
178        }
179    }
180}
181
182fn is_zlib_header(bytes: &[u8]) -> bool {
183    let [compression, flags, ..] = bytes else {
184        return false;
185    };
186    // CM = 8 selects the DEFLATE compression method.
187    compression & 0x0f == 8
188        // CINFO <= 7 limits the DEFLATE window to 32 KiB.
189        && compression >> 4 <= 7
190        // FCHECK requires the two header bytes to form a multiple of 31.
191        && u16::from_be_bytes([*compression, *flags]) % 31 == 0
192        // FDICT must be clear because preset dictionaries are unsupported.
193        && flags & 0x20 == 0
194}
195
196fn verify_checksum(reader: impl Read) -> Result<[u8; DIGEST_LEN]> {
197    let mut reader = SnapshotReader::new(reader)?;
198    let mut hash = gix::hash::hasher(gix::hash::Kind::Sha256);
199    let mut buffer = vec![0; 64 * 1024];
200    let mut tail = Vec::with_capacity(DIGEST_LEN + buffer.len());
201    loop {
202        let read = reader.read(&mut buffer)?;
203        if read == 0 {
204            break;
205        }
206        tail.extend_from_slice(&buffer[..read]);
207        if tail.len() > DIGEST_LEN {
208            let hashed = tail.len() - DIGEST_LEN;
209            hash.update(&tail[..hashed]);
210            tail.copy_within(hashed.., 0);
211            tail.truncate(DIGEST_LEN);
212        }
213    }
214    if tail.len() != DIGEST_LEN {
215        bail!("snapshot checksum is truncated");
216    }
217    let actual = hash.try_finalize()?;
218    if actual.as_slice() != tail {
219        bail!("snapshot checksum mismatch");
220    }
221    let mut digest = [0; DIGEST_LEN];
222    digest.copy_from_slice(actual.as_slice());
223    Ok(digest)
224}
225
226/// A checksum-verified, seekable snapshot that can replay entries without materializing the tree.
227///
228/// Construction verifies the decompressed stream's checksum without decoding its records.
229/// Records are structurally validated during each replay. The backing data must remain unchanged;
230/// structural errors and changes may be reported after callbacks have observed earlier entries.
231pub struct Replay<R> {
232    reader: R,
233    start: u64,
234    digest: [u8; DIGEST_LEN],
235}
236
237pub(crate) struct ReplayEntries<'a, R> {
238    decoder: Decoder<&'a mut R>,
239    expected_digest: [u8; DIGEST_LEN],
240}
241
242impl<R: Read> ReplayEntries<'_, R> {
243    pub(crate) fn next_entry(&mut self) -> Result<Option<DecodedEntry<'_>>> {
244        self.decoder.next_entry(Some(&self.expected_digest))
245    }
246}
247
248impl<R: Read + Seek> Replay<R> {
249    /// Verify `reader`'s checksum and prepare it for bounded-memory replay.
250    pub fn new(mut reader: R) -> Result<Self> {
251        let start = reader
252            .stream_position()
253            .context("could not determine snapshot stream position")?;
254        let digest = verify_checksum(&mut reader)?;
255        // Let's keep this for safety, even though it's redundant.
256        reader
257            .seek(SeekFrom::Start(start))
258            .context("could not rewind snapshot")?;
259        Ok(Self {
260            reader,
261            start,
262            digest,
263        })
264    }
265
266    pub(crate) fn for_each_entry(
267        &mut self,
268        mut on_entry: impl for<'entry> FnMut(DecodedEntry<'entry>) -> Result<()>,
269    ) -> Result<()> {
270        let mut entries = self.entries()?;
271        while let Some(entry) = entries.next_entry()? {
272            on_entry(entry)?;
273        }
274        Ok(())
275    }
276
277    pub(crate) fn entries(&mut self) -> Result<ReplayEntries<'_, R>> {
278        self.reader
279            .seek(SeekFrom::Start(self.start))
280            .context("could not rewind snapshot")?;
281        Ok(ReplayEntries {
282            decoder: Decoder::new(&mut self.reader)?,
283            expected_digest: self.digest,
284        })
285    }
286}
287
288/// Write `traversal` as a deterministic version-1 snapshot, optionally compressed with zlib.
289///
290/// `roots` must contain the traversal's top-level nodes in original input order.
291pub fn write(
292    writer: impl Write,
293    traversal: &Traversal,
294    roots: &[TreeIndex],
295    compression_level: Option<i32>,
296) -> Result<()> {
297    let Some(level) = compression_level else {
298        return write_raw(writer, traversal, roots);
299    };
300    let compression = gix::zlib::Compression::new(level)
301        .with_context(|| format!("snapshot compression level {level} is outside 0..=9"))?;
302    let mut writer = gix::zlib::stream::deflate::Write::new(writer, compression);
303    write_raw(&mut writer, traversal, roots)?;
304    Ok(())
305}
306
307fn write_raw(writer: impl Write, traversal: &Traversal, roots: &[TreeIndex]) -> Result<()> {
308    let mut writer = gix::hash::io::Write::new(BufWriter::new(writer), gix::hash::Kind::Sha256);
309    let mut header = [0; HEADER_LEN];
310    header[..MAGIC.len()].copy_from_slice(MAGIC);
311    header[8..10].copy_from_slice(&VERSION.to_le_bytes());
312    header[10] = PATH_ENCODING;
313    writer.write_all(&header)?;
314
315    let mut seen_roots = HashSet::with_capacity(roots.len());
316    for &root in roots {
317        if !seen_roots.insert(root) {
318            bail!("snapshot roots contain duplicate node {root:?}");
319        }
320    }
321    drop(seen_roots);
322
323    let mut stack = Vec::new();
324    let mut record = Vec::new();
325    let mut node_count = 0u64;
326    for &root in roots {
327        stack.push((root, traversal.root_index, 0));
328        while let Some((index, expected_parent, parent_id)) = stack.pop() {
329            if index == traversal.root_index {
330                bail!("snapshot traversal contains a cycle or shared node at {index:?}");
331            }
332            let entry = traversal
333                .tree
334                .entry(index)
335                .ok_or_else(|| anyhow!("snapshot node {index:?} does not exist"))?;
336            if traversal.tree.parent(index) != Some(expected_parent) {
337                bail!("snapshot traversal contains a cycle or shared node at {index:?}");
338            }
339            let name = traversal
340                .tree
341                .native_name(index)
342                .expect("existing tree entry has a name");
343            validate_name(name, parent_id == 0)?;
344            let node_id = node_count
345                .checked_add(1)
346                .context("snapshot contains too many nodes")?;
347            let parent_distance = node_id
348                .checked_sub(parent_id)
349                .filter(|distance| *distance != 0)
350                .context("snapshot parent was not written before its child")?;
351
352            record.clear();
353            push_uleb128(&mut record, u128::from(parent_distance));
354            let mut flags = (u8::from(entry.is_dir) * FLAG_DIRECTORY)
355                | (u8::from(entry.metadata_io_error) * FLAG_METADATA_IO_ERROR);
356            if entry.entry_count.is_some() {
357                flags |= FLAG_ENTRY_COUNT;
358            }
359            record.push(flags);
360            push_uleb128(
361                &mut record,
362                u128::try_from(name.len()).context("snapshot name is too long")?,
363            );
364            record.extend_from_slice(name);
365            push_uleb128(&mut record, entry.size);
366            let (seconds, nanos) = split_time(entry.mtime)?;
367            push_uleb128(&mut record, u128::from(zigzag_encode(seconds)));
368            push_uleb128(&mut record, u128::from(nanos));
369            if let Some(count) = entry.entry_count {
370                push_uleb128(&mut record, u128::from(count));
371            }
372            if record.len() > MAX_RECORD_LEN {
373                bail!("snapshot record exceeds the {MAX_RECORD_LEN}-byte limit");
374            }
375            write_uleb128(&mut writer, record.len() as u128)?;
376            writer.write_all(&record)?;
377
378            let mut children = traversal.tree.children(index).collect::<Vec<_>>();
379            for &child in &children {
380                validate_name(
381                    traversal
382                        .tree
383                        .native_name(child)
384                        .ok_or_else(|| anyhow!("snapshot child {child:?} does not exist"))?,
385                    false,
386                )?;
387            }
388            if !children.is_empty() && !entry.is_dir {
389                bail!("snapshot file node {index:?} has children");
390            }
391            children.sort_by(|left, right| {
392                traversal
393                    .tree
394                    .native_name(*left)
395                    .cmp(&traversal.tree.native_name(*right))
396                    .then_with(|| left.index().cmp(&right.index()))
397            });
398            stack.extend(
399                children
400                    .into_iter()
401                    .rev()
402                    .map(|child| (child, index, node_id)),
403            );
404            node_count = node_id;
405        }
406    }
407
408    writer.write_all(&[0])?;
409    write_uleb128(&mut writer, u128::from(node_count))?;
410    let gix::hash::io::Write { mut inner, hash } = writer;
411    let digest = hash.try_finalize()?;
412    inner.write_all(digest.as_slice())?;
413    inner.flush()?;
414    Ok(())
415}
416
417/// Read and fully verify a version-1 snapshot before returning its traversal.
418pub fn read(reader: impl Read) -> Result<Snapshot> {
419    let mut traversal = Traversal::new();
420    traversal.cost = Some(Duration::ZERO);
421    let mut parents = Vec::new();
422    let mut roots = Vec::new();
423    let summary = decode(reader, |entry| {
424        parents.truncate(entry.depth);
425        let parent = parents.last().copied().unwrap_or(traversal.root_index);
426        let node = traversal
427            .tree
428            .try_add_child_native(parent, entry.native_name, entry.data)
429            .map_err(|err| anyhow!("could not add snapshot entry: {err}"))?;
430        if entry.depth == 0 {
431            roots
432                .try_reserve(1)
433                .context("could not grow snapshot root table")?;
434            roots.push(node);
435        }
436        parents
437            .try_reserve(1)
438            .context("could not grow snapshot ancestor stack")?;
439        parents.push(node);
440        Ok(())
441    })?;
442
443    traversal
444        .tree
445        .update(traversal.root_index, |synthetic_root| {
446            synthetic_root.size = summary.total_size;
447            synthetic_root.entry_count = (!roots.is_empty()).then_some(summary.total_entries);
448        });
449
450    Ok(Snapshot { traversal, roots })
451}
452
453fn decode(
454    reader: impl Read,
455    mut on_entry: impl for<'entry> FnMut(DecodedEntry<'entry>) -> Result<()>,
456) -> Result<DecodeSummary> {
457    let mut decoder = Decoder::new(reader)?;
458    while let Some(entry) = decoder.next_entry(None)? {
459        on_entry(entry)?;
460    }
461    Ok(decoder.summary.expect("end of snapshot stores its summary"))
462}
463
464impl<R: Read> Decoder<R> {
465    fn new(reader: R) -> Result<Self> {
466        let mut reader = HashingReader::new(BufReader::new(SnapshotReader::new(reader)?));
467        let mut header = [0; HEADER_LEN];
468        read_exact(&mut reader, &mut header)?;
469        if &header[..MAGIC.len()] != MAGIC {
470            bail!("invalid snapshot at byte 0: bad magic");
471        }
472        let version = u16::from_le_bytes([header[8], header[9]]);
473        if version != VERSION {
474            bail!("invalid snapshot at byte 8: unsupported version {version}");
475        }
476        if header[10] != PATH_ENCODING {
477            bail!(
478                "invalid snapshot at byte 10: path encoding {} is incompatible with this host",
479                header[10]
480            );
481        }
482        if header[11] != 0 {
483            bail!("invalid snapshot at byte 11: unknown header flags");
484        }
485
486        Ok(Self {
487            reader,
488            open_nodes: Vec::new(),
489            sibling_names: Vec::new(),
490            record: Vec::new(),
491            node_count: 0,
492            total_size: 0,
493            total_entries: 0,
494            summary: None,
495        })
496    }
497
498    fn next_entry(
499        &mut self,
500        expected_digest: Option<&[u8; DIGEST_LEN]>,
501    ) -> Result<Option<DecodedEntry<'_>>> {
502        if self.summary.is_some() {
503            return Ok(None);
504        }
505
506        let length_offset = self.reader.offset;
507        let record_len = read_u64(&mut self.reader)
508            .map_err(|err| anyhow!("invalid snapshot integer at byte {length_offset}: {err}"))?;
509        if record_len == 0 {
510            self.finish()?;
511            if expected_digest.is_some_and(|expected| {
512                self.summary
513                    .as_ref()
514                    .expect("end of snapshot stores its summary")
515                    .digest
516                    != *expected
517            }) {
518                bail!("snapshot changed since it was verified");
519            }
520            return Ok(None);
521        }
522        let record_len = usize::try_from(record_len)
523            .context("snapshot record length exceeds this address space")?;
524        if record_len > MAX_RECORD_LEN {
525            bail!(
526                "invalid snapshot at byte {length_offset}: record exceeds the {MAX_RECORD_LEN}-byte limit"
527            );
528        }
529
530        self.record.clear();
531        if self.record.capacity() < record_len {
532            self.record
533                .try_reserve(record_len)
534                .context("could not allocate snapshot record")?;
535        }
536        self.record.resize(record_len, 0);
537        let record_offset = self.reader.offset;
538        read_exact(&mut self.reader, &mut self.record)?;
539
540        let node_id = self
541            .node_count
542            .checked_add(1)
543            .context("snapshot contains too many nodes")?;
544        let (parent_id, native_name, data) = parse_record(&self.record, node_id)
545            .map_err(|err| anyhow!("invalid snapshot record at byte {record_offset}: {err}"))?;
546        if node_id >= u64::from(u32::MAX) {
547            bail!("snapshot exceeds the tree node-index limit");
548        }
549
550        let sibling_ordinal = if parent_id == 0 {
551            self.open_nodes.clear();
552            0
553        } else {
554            while self
555                .open_nodes
556                .last()
557                .is_some_and(|node| node.id != parent_id)
558            {
559                self.open_nodes.pop();
560            }
561            let parent_depth = self.open_nodes.len().saturating_sub(1);
562            let parent = self.open_nodes.last_mut().with_context(|| {
563                format!(
564                    "invalid snapshot record at byte {record_offset}: parent is outside the current depth-first subtree"
565                )
566            })?;
567            if !parent.is_dir {
568                bail!("invalid snapshot record at byte {record_offset}: parent is not a directory");
569            }
570            let previous = &mut self.sibling_names[parent_depth];
571            let ordinal = if parent.has_child && previous.as_slice() > native_name {
572                bail!(
573                    "invalid snapshot record at byte {record_offset}: sibling names are not in canonical order"
574                );
575            } else if parent.has_child && previous.as_slice() == native_name {
576                parent
577                    .last_child_ordinal
578                    .checked_add(1)
579                    .context("snapshot contains too many duplicate sibling names")?
580            } else {
581                0
582            };
583            previous.clear();
584            previous.extend_from_slice(native_name);
585            parent.has_child = true;
586            parent.last_child_ordinal = ordinal;
587            ordinal
588        };
589
590        let depth = self.open_nodes.len();
591        if depth == 0 {
592            self.total_size = self
593                .total_size
594                .checked_add(data.size)
595                .context("snapshot root size total overflows u128")?;
596            self.total_entries = self
597                .total_entries
598                .checked_add(data.entry_count.unwrap_or(1))
599                .context("snapshot root entry count total overflows u64")?;
600        }
601
602        self.open_nodes
603            .try_reserve(1)
604            .context("could not grow snapshot ancestor stack")?;
605        if self.sibling_names.len() <= depth {
606            self.sibling_names
607                .try_reserve(1)
608                .context("could not grow snapshot sibling buffers")?;
609            self.sibling_names.push(Vec::new());
610        } else {
611            self.sibling_names[depth].clear();
612        }
613        self.open_nodes.push(OpenNode {
614            id: node_id,
615            is_dir: data.is_dir,
616            has_child: false,
617            last_child_ordinal: 0,
618        });
619        self.node_count = node_id;
620        Ok(Some(DecodedEntry {
621            depth,
622            data,
623            native_name,
624            sibling_ordinal,
625        }))
626    }
627
628    fn finish(&mut self) -> Result<()> {
629        let count_offset = self.reader.offset;
630        let footer_count = read_u64(&mut self.reader)
631            .map_err(|err| anyhow!("invalid snapshot node count at byte {count_offset}: {err}"))?;
632        if footer_count != self.node_count {
633            bail!(
634                "invalid snapshot at byte {count_offset}: footer names {footer_count} nodes but read {}",
635                self.node_count
636            );
637        }
638
639        let actual = self.reader.hash.clone().try_finalize()?;
640        let mut expected = [0; DIGEST_LEN];
641        self.reader
642            .inner
643            .read_exact(&mut expected)
644            .context("snapshot checksum is truncated")?;
645        if actual.as_slice() != expected {
646            bail!("snapshot checksum mismatch");
647        }
648        let mut trailing = [0];
649        if self.reader.inner.read(&mut trailing)? != 0 {
650            bail!("snapshot has trailing data");
651        }
652
653        let mut digest = [0; DIGEST_LEN];
654        digest.copy_from_slice(actual.as_slice());
655        self.summary = Some(DecodeSummary {
656            total_size: self.total_size,
657            total_entries: self.total_entries,
658            digest,
659        });
660        Ok(())
661    }
662}
663
664fn parse_record(record: &[u8], node_id: u64) -> Result<(u64, &[u8], EntryData)> {
665    let mut cursor = io::Cursor::new(record);
666    let parent_distance = read_u64(&mut cursor)?;
667    let parent_id = node_id
668        .checked_sub(parent_distance)
669        .filter(|_| parent_distance != 0)
670        .context("parent distance is zero or points forward")?;
671
672    let mut flags = [0];
673    cursor.read_exact(&mut flags)?;
674    let flags = flags[0];
675    if flags & !KNOWN_FLAGS != 0 {
676        bail!("unknown node flags {flags:#04x}");
677    }
678
679    let name_len = usize::try_from(read_u64(&mut cursor)?)
680        .context("snapshot name length exceeds this address space")?;
681    if name_len > MAX_NAME_LEN {
682        bail!("name exceeds the {MAX_NAME_LEN}-byte limit");
683    }
684    let name_start = usize::try_from(cursor.position()).context("record offset is too large")?;
685    let name_end = name_start
686        .checked_add(name_len)
687        .filter(|end| *end <= record.len())
688        .context("record ends within its name")?;
689    let name = &record[name_start..name_end];
690    cursor.set_position(name_end as u64);
691    validate_name(name, parent_id == 0)?;
692
693    let size = read_uleb128(&mut cursor, 128)?;
694    let seconds = zigzag_decode(read_u64(&mut cursor)?);
695    let nanos = u32::try_from(read_u64(&mut cursor)?)
696        .ok()
697        .filter(|nanos| *nanos < 1_000_000_000)
698        .context("modification time has invalid nanoseconds")?;
699    let mtime = join_time(seconds, nanos)?;
700    let entry_count = if flags & FLAG_ENTRY_COUNT != 0 {
701        Some(read_u64(&mut cursor)?)
702    } else {
703        None
704    };
705    if cursor.position() != record.len() as u64 {
706        bail!("record length does not match its fields");
707    }
708
709    Ok((
710        parent_id,
711        name,
712        EntryData {
713            size,
714            mtime,
715            entry_count,
716            metadata_io_error: flags & FLAG_METADATA_IO_ERROR != 0,
717            is_dir: flags & FLAG_DIRECTORY != 0,
718        },
719    ))
720}
721
722fn split_time(time: SystemTime) -> Result<(i64, u32)> {
723    match time.duration_since(UNIX_EPOCH) {
724        Ok(duration) => Ok((
725            i64::try_from(duration.as_secs())
726                .context("modification time is outside the snapshot range")?,
727            duration.subsec_nanos(),
728        )),
729        Err(error) => {
730            let duration = error.duration();
731            let nanos = duration.subsec_nanos();
732            let seconds = if nanos == 0 {
733                -i128::from(duration.as_secs())
734            } else {
735                -i128::from(duration.as_secs()) - 1
736            };
737            Ok((
738                i64::try_from(seconds)
739                    .context("modification time is outside the snapshot range")?,
740                if nanos == 0 { 0 } else { 1_000_000_000 - nanos },
741            ))
742        }
743    }
744}
745
746fn join_time(seconds: i64, nanos: u32) -> Result<SystemTime> {
747    let time = if seconds >= 0 {
748        UNIX_EPOCH.checked_add(Duration::new(seconds.cast_unsigned(), nanos))
749    } else if nanos == 0 {
750        UNIX_EPOCH.checked_sub(Duration::new(seconds.unsigned_abs(), 0))
751    } else {
752        UNIX_EPOCH.checked_sub(Duration::new(
753            seconds.unsigned_abs() - 1,
754            1_000_000_000 - nanos,
755        ))
756    };
757    let time = time.context("modification time is not representable on this host")?;
758    if split_time(time)? != (seconds, nanos) {
759        bail!("modification time loses precision on this host");
760    }
761    Ok(time)
762}
763
764fn zigzag_encode(value: i64) -> u64 {
765    (value.cast_unsigned() << 1) ^ (value >> 63).cast_unsigned()
766}
767
768fn zigzag_decode(value: u64) -> i64 {
769    (value >> 1).cast_signed() ^ -(value & 1).cast_signed()
770}
771
772fn push_uleb128(out: &mut Vec<u8>, mut value: u128) {
773    loop {
774        let byte = (value & 0x7f) as u8;
775        value >>= 7;
776        out.push(byte | (u8::from(value != 0) * 0x80));
777        if value == 0 {
778            return;
779        }
780    }
781}
782
783fn write_uleb128(out: &mut impl Write, value: u128) -> io::Result<()> {
784    let mut encoded = [0; 19];
785    let mut len = 0;
786    let mut value = value;
787    loop {
788        let byte = (value & 0x7f) as u8;
789        value >>= 7;
790        encoded[len] = byte | (u8::from(value != 0) * 0x80);
791        len += 1;
792        if value == 0 {
793            return out.write_all(&encoded[..len]);
794        }
795    }
796}
797
798fn read_u64(input: &mut impl Read) -> io::Result<u64> {
799    u64::try_from(read_uleb128(input, 64)?).map_err(|_| invalid_data("ULEB128 value overflows u64"))
800}
801
802fn read_uleb128(input: &mut impl Read, bits: u32) -> io::Result<u128> {
803    let groups = bits.div_ceil(7);
804    let mut value = 0u128;
805    for group in 0..groups {
806        let mut byte = [0];
807        input.read_exact(&mut byte)?;
808        let payload = u128::from(byte[0] & 0x7f);
809        let shift = group * 7;
810        let remaining = bits - shift;
811        if remaining < 7 && payload >= 1u128 << remaining {
812            return Err(invalid_data("ULEB128 value overflows its field"));
813        }
814        value |= payload << shift;
815        if byte[0] & 0x80 == 0 {
816            if group != 0 && payload == 0 {
817                return Err(invalid_data("ULEB128 value is not canonical"));
818            }
819            return Ok(value);
820        }
821    }
822    Err(invalid_data("ULEB128 value is overlong"))
823}
824
825#[cfg(unix)]
826fn native_name_from_bytes(name: &[u8]) -> Cow<'_, Path> {
827    use std::os::unix::ffi::OsStrExt as _;
828    Cow::Borrowed(Path::new(std::ffi::OsStr::from_bytes(name)))
829}
830
831#[cfg(all(test, unix))]
832fn native_name_bytes(path: &Path) -> Vec<u8> {
833    use std::os::unix::ffi::OsStrExt as _;
834    path.as_os_str().as_bytes().to_vec()
835}
836
837#[cfg(all(test, windows))]
838fn native_name_bytes(path: &Path) -> Vec<u8> {
839    use std::os::windows::ffi::OsStrExt as _;
840    path.as_os_str()
841        .encode_wide()
842        .flat_map(u16::to_le_bytes)
843        .collect()
844}
845
846#[cfg(windows)]
847fn native_name_from_bytes(name: &[u8]) -> Cow<'_, Path> {
848    use std::os::windows::ffi::OsStringExt as _;
849    debug_assert_eq!(name.len() % 2, 0);
850    let wide = name
851        .chunks_exact(2)
852        .map(|bytes| u16::from_le_bytes([bytes[0], bytes[1]]))
853        .collect::<Vec<_>>();
854    Cow::Owned(std::path::PathBuf::from(std::ffi::OsString::from_wide(
855        &wide,
856    )))
857}
858
859#[cfg(unix)]
860fn validate_name(name: &[u8], is_root: bool) -> Result<()> {
861    if name.contains(&0) {
862        bail!("path contains NUL");
863    }
864    if !is_root && (name.is_empty() || name.contains(&b'/') || matches!(name, b"." | b"..")) {
865        bail!("child name is not one native path component");
866    }
867    if name.len() > MAX_NAME_LEN {
868        bail!("name exceeds the {MAX_NAME_LEN}-byte limit");
869    }
870    Ok(())
871}
872
873#[cfg(windows)]
874fn validate_name(name: &[u8], is_root: bool) -> Result<()> {
875    if name.len() % 2 != 0 {
876        bail!("Windows path has an odd byte length");
877    }
878    let mut wide = name
879        .chunks_exact(2)
880        .map(|bytes| u16::from_le_bytes([bytes[0], bytes[1]]));
881    if wide.clone().any(|unit| unit == 0) {
882        bail!("path contains NUL");
883    }
884    if !is_root {
885        let first = wide.next();
886        let second = wide.next();
887        if first.is_none()
888            || name.chunks_exact(2).any(|bytes| {
889                let unit = u16::from_le_bytes([bytes[0], bytes[1]]);
890                unit == u16::from(b'/') || unit == u16::from(b'\\')
891            })
892            || first == Some(u16::from(b'.')) && second.is_none()
893            || first == Some(u16::from(b'.'))
894                && second == Some(u16::from(b'.'))
895                && wide.next().is_none()
896        {
897            bail!("child name is not one native path component");
898        }
899    }
900    if name.len() > MAX_NAME_LEN {
901        bail!("name exceeds the {MAX_NAME_LEN}-byte limit");
902    }
903    Ok(())
904}
905
906struct HashingReader<R> {
907    inner: R,
908    hash: gix::hash::Hasher,
909    offset: u64,
910}
911
912impl<R> HashingReader<R> {
913    fn new(inner: R) -> Self {
914        Self {
915            inner,
916            hash: gix::hash::hasher(gix::hash::Kind::Sha256),
917            offset: 0,
918        }
919    }
920}
921
922impl<R: Read> Read for HashingReader<R> {
923    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
924        let read = self.inner.read(buffer)?;
925        self.hash.update(&buffer[..read]);
926        self.offset = self
927            .offset
928            .checked_add(read as u64)
929            .ok_or_else(|| invalid_data("snapshot byte offset overflow"))?;
930        Ok(read)
931    }
932}
933
934fn read_exact<R: Read>(reader: &mut HashingReader<R>, buffer: &mut [u8]) -> Result<()> {
935    reader
936        .read_exact(buffer)
937        .with_context(|| format!("snapshot is truncated at byte {}", reader.offset))
938}
939
940fn invalid_data(message: &'static str) -> io::Error {
941    io::Error::new(io::ErrorKind::InvalidData, message)
942}
943
944#[cfg(test)]
945mod tests;