Skip to main content

mkit_core/pack/
rewrite.rs

1//! Remove objects from a pack without leaving deltas over removed bases.
2//!
3//! Rewrites are deterministic within a build's feature set. Native writers
4//! (`pack-zstd`) may compress entries; decode-only wasm writers emit v1 packs.
5//! Both preserve the same objects, but their pack bytes may differ. Callers
6//! that need stable pack ids should rewrite on a `pack-zstd` build.
7
8use super::{
9    DecodeLimits, DecodedEntry, DeltaBaseSource, ENTRY_FRAME_LEN, HEADER_LEN, PackError,
10    PackWriter, decode_entries_with, decompress_zstd_entry,
11};
12use crate::hash::Hash;
13use std::collections::HashSet;
14
15/// A rewritten pack and the entries affected by the exclusions.
16#[derive(Debug, Clone, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct Rewritten {
19    /// The rewritten pack. Equal to the input bytes when `unchanged`.
20    pub bytes: Vec<u8>,
21    /// Dropped ids, deduplicated in first-occurrence pack order.
22    pub removed: Vec<Hash>,
23    /// Ids of delta entries re-emitted as raw, in pack order.
24    pub rawified: Vec<Hash>,
25    /// True when nothing was removed or rawified.
26    pub unchanged: bool,
27}
28
29/// Drop every entry named by `excluded`, rawifying surviving deltas whose
30/// direct base is excluded. A delta over a rawified entry remains a delta.
31///
32/// All entries, including dropped entries, are validated and decoded using
33/// exactly [`decode_entries_with`]'s [`DecodeLimits`] accounting. External
34/// bases come only from the caller's repository-scoped `bases`; an excluded
35/// id that is neither an entry nor a base causes no lookup. Unchanged packs
36/// are returned verbatim, even if this build would encode them differently.
37///
38/// # Errors
39///
40/// Invalid input produces the decoder's error, including
41/// [`PackError::DeltaBaseMissing`] when a required excluded base cannot be
42/// supplied. A changed output exceeding the writer's caps produces the
43/// writer's existing error. No partial output is returned.
44#[allow(clippy::implicit_hasher)] // The orchestrator fixes this additive API signature.
45pub fn rewrite_excluding<B: DeltaBaseSource>(
46    pack: &[u8],
47    excluded: &HashSet<Hash>,
48    bases: &mut B,
49    limits: DecodeLimits,
50) -> Result<Rewritten, PackError> {
51    let mut rewrite = Rewrite::new(pack, excluded);
52    decode_entries_with(pack, bases, limits, |entry| rewrite.accept(&entry))?;
53    rewrite.finish()
54}
55
56struct Rewrite<'a> {
57    pack: &'a [u8],
58    excluded: &'a HashSet<Hash>,
59    pos: usize,
60    writer: PackWriter,
61    removed: Vec<Hash>,
62    removed_seen: HashSet<Hash>,
63    rawified: Vec<Hash>,
64    writer_error: Option<PackError>,
65}
66
67impl<'a> Rewrite<'a> {
68    fn new(pack: &'a [u8], excluded: &'a HashSet<Hash>) -> Self {
69        Self {
70            pack,
71            excluded,
72            pos: HEADER_LEN,
73            writer: PackWriter::new(),
74            removed: Vec::new(),
75            removed_seen: HashSet::new(),
76            rawified: Vec::new(),
77            writer_error: None,
78        }
79    }
80
81    fn accept(&mut self, entry: &DecodedEntry<'_>) -> Result<(), PackError> {
82        // The decoder validated all framing before its first sink call. Walk
83        // only wire offsets here: a second PackEntries iterator would allocate
84        // another decompressed raw payload while the decoder still holds it.
85        let start = self.pos;
86        let payload_start = start
87            .checked_add(ENTRY_FRAME_LEN)
88            .ok_or(PackError::UnexpectedEof)?;
89        let frame = self
90            .pack
91            .get(start..payload_start)
92            .ok_or(PackError::UnexpectedEof)?;
93        let len = u32::from_le_bytes(
94            frame[1..]
95                .try_into()
96                .map_err(|_| PackError::UnexpectedEof)?,
97        );
98        let payload_end = payload_start
99            .checked_add(usize::try_from(len).map_err(|_| PackError::UnexpectedEof)?)
100            .ok_or(PackError::UnexpectedEof)?;
101        let payload = self
102            .pack
103            .get(payload_start..payload_end)
104            .ok_or(PackError::UnexpectedEof)?;
105        self.pos = payload_end;
106
107        if self.excluded.contains(&entry.id) {
108            if self.removed_seen.insert(entry.id) {
109                self.removed.push(entry.id);
110            }
111            return Ok(());
112        }
113        let base = if entry.from_delta {
114            Some(
115                <Hash>::try_from(payload.get(..32).ok_or(PackError::DeltaEntryTruncated)?)
116                    .map_err(|_| PackError::DeltaEntryTruncated)?,
117            )
118        } else {
119            None
120        };
121        let rawify = base.is_some_and(|id| self.excluded.contains(&id));
122        if rawify {
123            self.rawified.push(entry.id);
124        }
125        if self.writer_error.is_some() {
126            return Ok(());
127        }
128        let written = match base {
129            Some(base) if !rawify => {
130                let stream = &payload[32..];
131                if frame[0] == 0x04 {
132                    // decode_entries_with drops this entry's owned stream
133                    // before calling the sink. Its claim stays reserved, so
134                    // this replacement uses the same charged bytes, never a
135                    // second resident copy. No other frame is decompressed.
136                    let stream = decompress_zstd_entry(stream)?;
137                    self.writer.push_delta(&base, &stream)
138                } else {
139                    self.writer.push_delta(&base, stream)
140                }
141            }
142            _ => self.writer.push_raw(entry.id, entry.bytes).map(|_| ()),
143        };
144        // Finish validating input even if output exceeds a writer cap: a
145        // later decoder error wins, and an unchanged pack needs no rewrite.
146        if let Err(error) = written {
147            self.writer_error = Some(error);
148        }
149        Ok(())
150    }
151
152    fn finish(self) -> Result<Rewritten, PackError> {
153        let unchanged = self.removed.is_empty() && self.rawified.is_empty();
154        let bytes = if unchanged {
155            // Release speculative output before copying the original pack.
156            drop(self.writer);
157            self.pack.to_vec()
158        } else {
159            if let Some(error) = self.writer_error {
160                return Err(error);
161            }
162            self.writer.finish()?
163        };
164        Ok(Rewritten {
165            bytes,
166            removed: self.removed,
167            rawified: self.rawified,
168            unchanged,
169        })
170    }
171}
172
173#[cfg(test)]
174mod tests;