Skip to main content

bazaar/groupcompress/
wire.rs

1//! Outer wire framing for groupcompress blocks.
2//!
3//! The over-the-wire form prepends a small text header with one record per
4//! contained factory, then the inner [`GroupCompressBlock`] payload. This
5//! module parses just the header so the manager-side Python wrapper can keep
6//! orchestrating the block construction itself.
7//!
8//! See `bzrformats.groupcompress._LazyGroupContentManager.from_bytes` for the
9//! Python original.
10
11use flate2::read::ZlibDecoder;
12use flate2::write::ZlibEncoder;
13use flate2::Compression;
14use std::io::{Read, Write};
15
16/// One factory record described by the wire header.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct WireFactory {
19    /// `\x00`-separated key segments.
20    pub key: Vec<Vec<u8>>,
21    /// `None` for absent parent info, else a list of keys.
22    pub parents: Option<Vec<Vec<Vec<u8>>>>,
23    pub start: u64,
24    pub end: u64,
25}
26
27#[derive(Debug)]
28pub enum Error {
29    UnknownStorageKind(Vec<u8>),
30    InvalidLength(&'static str),
31    InvalidInteger,
32    MissingTrailingNewline,
33    NotMultipleOfFour,
34    Decompress(std::io::Error),
35}
36
37impl std::fmt::Display for Error {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Error::UnknownStorageKind(b) => {
41                write!(f, "unknown storage kind: {}", String::from_utf8_lossy(b))
42            }
43            Error::InvalidLength(msg) => write!(f, "invalid length: {}", msg),
44            Error::InvalidInteger => write!(f, "invalid integer in wire header"),
45            Error::MissingTrailingNewline => {
46                write!(f, "header lines did not end with a trailing newline")
47            }
48            Error::NotMultipleOfFour => {
49                write!(f, "header was not an even multiple of 4 lines")
50            }
51            Error::Decompress(e) => write!(f, "zlib decompression failed: {}", e),
52        }
53    }
54}
55
56impl std::error::Error for Error {}
57
58/// Parsed result of the outer wire frame: the factory records plus the byte
59/// range of the inner `GroupCompressBlock` payload within the input slice.
60#[derive(Debug)]
61pub struct WireFrame<'a> {
62    pub factories: Vec<WireFactory>,
63    /// Slice of the original input that contains the inner block payload.
64    pub block_bytes: &'a [u8],
65}
66
67/// Parse the outer wire framing of a groupcompress block.
68///
69/// `bytes` is the full record as written by `_wire_bytes` on the Python side.
70pub fn parse_wire(bytes: &[u8]) -> Result<WireFrame<'_>, Error> {
71    // The Python original splits on `\n` with a 4-element limit, yielding
72    // `(storage_kind, z_header_len, header_len, block_len, rest)`.
73    let mut splits = bytes.splitn(5, |&b| b == b'\n');
74    let storage_kind = splits.next().ok_or(Error::InvalidLength("storage kind"))?;
75    let z_header_len = splits.next().ok_or(Error::InvalidLength("z_header_len"))?;
76    let header_len = splits.next().ok_or(Error::InvalidLength("header_len"))?;
77    let block_len = splits.next().ok_or(Error::InvalidLength("block_len"))?;
78    let rest = splits.next().ok_or(Error::InvalidLength("rest"))?;
79
80    if storage_kind != b"groupcompress-block" {
81        return Err(Error::UnknownStorageKind(storage_kind.to_vec()));
82    }
83
84    let z_header_len = parse_int(z_header_len)? as usize;
85    let header_len = parse_int(header_len)? as usize;
86    let block_len = parse_int(block_len)? as usize;
87
88    if rest.len() < z_header_len {
89        return Err(Error::InvalidLength("compressed header shorter than rest"));
90    }
91    let z_header = &rest[..z_header_len];
92    let block_bytes = &rest[z_header_len..];
93    if block_bytes.len() != block_len {
94        return Err(Error::InvalidLength("block bytes length mismatch"));
95    }
96
97    let mut header = Vec::with_capacity(header_len);
98    ZlibDecoder::new(z_header)
99        .read_to_end(&mut header)
100        .map_err(Error::Decompress)?;
101    if header.len() != header_len {
102        return Err(Error::InvalidLength("decompressed header length mismatch"));
103    }
104
105    let factories = parse_header_lines(&header)?;
106    Ok(WireFrame {
107        factories,
108        block_bytes,
109    })
110}
111
112/// Build the per-factory header bytes consumed by the wire format.
113///
114/// Each factory contributes four `\n`-terminated lines: the `\x00`-joined key,
115/// the parents (`b"None:"` for absent parents, otherwise tab-separated keys),
116/// the start byte and the end byte.
117pub fn build_header_lines(factories: &[WireFactory]) -> Vec<u8> {
118    let mut out = Vec::new();
119    for factory in factories {
120        // key
121        let mut first = true;
122        for segment in &factory.key {
123            if !first {
124                out.push(b'\x00');
125            }
126            first = false;
127            out.extend_from_slice(segment);
128        }
129        out.push(b'\n');
130        // parents
131        match &factory.parents {
132            None => out.extend_from_slice(b"None:"),
133            Some(parents) => {
134                let mut first_parent = true;
135                for parent in parents {
136                    if !first_parent {
137                        out.push(b'\t');
138                    }
139                    first_parent = false;
140                    let mut first_seg = true;
141                    for seg in parent {
142                        if !first_seg {
143                            out.push(b'\x00');
144                        }
145                        first_seg = false;
146                        out.extend_from_slice(seg);
147                    }
148                }
149            }
150        }
151        out.push(b'\n');
152        // start
153        out.extend_from_slice(format!("{}", factory.start).as_bytes());
154        out.push(b'\n');
155        // end
156        out.extend_from_slice(format!("{}", factory.end).as_bytes());
157        out.push(b'\n');
158    }
159    out
160}
161
162/// Build the framing prefix for the wire format: the storage-kind line, the
163/// three length lines, and the zlib-compressed header bytes.
164///
165/// The caller appends the inner block payload (`block_bytes`) after the
166/// returned prefix to form the complete wire record.
167pub fn build_wire_prefix(
168    factories: &[WireFactory],
169    block_bytes_len: usize,
170) -> std::io::Result<Vec<u8>> {
171    let header = build_header_lines(factories);
172    let header_len = header.len();
173    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
174    encoder.write_all(&header)?;
175    let z_header = encoder.finish()?;
176    let z_header_len = z_header.len();
177
178    let mut prefix = Vec::with_capacity(64 + z_header_len);
179    prefix.extend_from_slice(b"groupcompress-block\n");
180    prefix.extend_from_slice(
181        format!("{}\n{}\n{}\n", z_header_len, header_len, block_bytes_len).as_bytes(),
182    );
183    prefix.extend_from_slice(&z_header);
184    Ok(prefix)
185}
186
187fn parse_int(b: &[u8]) -> Result<u64, Error> {
188    std::str::from_utf8(b)
189        .map_err(|_| Error::InvalidInteger)?
190        .parse()
191        .map_err(|_| Error::InvalidInteger)
192}
193
194fn parse_header_lines(header: &[u8]) -> Result<Vec<WireFactory>, Error> {
195    // Header is a sequence of lines, each terminated by `\n`. The Python code
196    // splits on `\n`, expects an empty trailing element, then walks groups of
197    // four lines: key, parents, start, end.
198    let mut lines: Vec<&[u8]> = header.split(|&b| b == b'\n').collect();
199    let trailing = lines.pop().ok_or(Error::MissingTrailingNewline)?;
200    if !trailing.is_empty() {
201        return Err(Error::MissingTrailingNewline);
202    }
203    if lines.len() % 4 != 0 {
204        return Err(Error::NotMultipleOfFour);
205    }
206    let mut out = Vec::with_capacity(lines.len() / 4);
207    for chunk in lines.chunks_exact(4) {
208        let key = chunk[0]
209            .split(|&b| b == b'\x00')
210            .map(|s| s.to_vec())
211            .collect();
212        let parents = if chunk[1] == b"None:" {
213            None
214        } else {
215            Some(
216                chunk[1]
217                    .split(|&b| b == b'\t')
218                    .filter(|seg| !seg.is_empty())
219                    .map(|seg| {
220                        seg.split(|&b| b == b'\x00')
221                            .map(|s| s.to_vec())
222                            .collect::<Vec<Vec<u8>>>()
223                    })
224                    .collect(),
225            )
226        };
227        let start = parse_int(chunk[2])?;
228        let end = parse_int(chunk[3])?;
229        out.push(WireFactory {
230            key,
231            parents,
232            start,
233            end,
234        });
235    }
236    Ok(out)
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use flate2::write::ZlibEncoder;
243    use flate2::Compression;
244    use std::io::Write;
245
246    fn build_wire(header_lines: &[u8], block_bytes: &[u8]) -> Vec<u8> {
247        let mut z = ZlibEncoder::new(Vec::new(), Compression::default());
248        z.write_all(header_lines).unwrap();
249        let z_header = z.finish().unwrap();
250        let mut out = Vec::new();
251        out.extend_from_slice(b"groupcompress-block\n");
252        out.extend_from_slice(format!("{}\n", z_header.len()).as_bytes());
253        out.extend_from_slice(format!("{}\n", header_lines.len()).as_bytes());
254        out.extend_from_slice(format!("{}\n", block_bytes.len()).as_bytes());
255        out.extend_from_slice(&z_header);
256        out.extend_from_slice(block_bytes);
257        out
258    }
259
260    #[test]
261    fn round_trip_single_factory() {
262        let header = b"file-id\x00rev\nNone:\n0\n42\n";
263        let block = b"BLOCK_PAYLOAD";
264        let wire = build_wire(header, block);
265        let frame = parse_wire(&wire).unwrap();
266        assert_eq!(frame.block_bytes, block);
267        assert_eq!(frame.factories.len(), 1);
268        let f = &frame.factories[0];
269        assert_eq!(f.key, vec![b"file-id".to_vec(), b"rev".to_vec()]);
270        assert!(f.parents.is_none());
271        assert_eq!(f.start, 0);
272        assert_eq!(f.end, 42);
273    }
274
275    #[test]
276    fn parents_split_on_tab_and_nul() {
277        let header = b"k\nf\x00p1\tf\x00p2\n0\n10\n";
278        let wire = build_wire(header, b"");
279        let frame = parse_wire(&wire).unwrap();
280        let parents = frame.factories[0].parents.as_ref().unwrap();
281        assert_eq!(
282            *parents,
283            vec![
284                vec![b"f".to_vec(), b"p1".to_vec()],
285                vec![b"f".to_vec(), b"p2".to_vec()],
286            ]
287        );
288    }
289
290    #[test]
291    fn empty_parents_list_is_some_empty() {
292        // Python emits `b""` for an empty parents tuple, which then splits on
293        // `\t` into a single empty segment that the filter drops.
294        let header = b"k\n\n0\n10\n";
295        let wire = build_wire(header, b"");
296        let frame = parse_wire(&wire).unwrap();
297        assert_eq!(frame.factories[0].parents.as_ref().unwrap().len(), 0);
298    }
299
300    #[test]
301    fn rejects_unknown_storage_kind() {
302        let bytes = b"something-else\n0\n0\n0\n";
303        assert!(matches!(
304            parse_wire(bytes),
305            Err(Error::UnknownStorageKind(_))
306        ));
307    }
308
309    #[test]
310    fn rejects_non_multiple_of_four_header_lines() {
311        let header = b"a\nb\nc\n";
312        let wire = build_wire(header, b"");
313        assert!(matches!(parse_wire(&wire), Err(Error::NotMultipleOfFour)));
314    }
315
316    #[test]
317    fn build_header_lines_emits_python_format() {
318        let factories = vec![WireFactory {
319            key: vec![b"file-id".to_vec(), b"rev".to_vec()],
320            parents: None,
321            start: 0,
322            end: 42,
323        }];
324        assert_eq!(
325            build_header_lines(&factories),
326            b"file-id\x00rev\nNone:\n0\n42\n"
327        );
328    }
329
330    #[test]
331    fn build_header_lines_emits_multiple_parents_with_tab_separator() {
332        let factories = vec![WireFactory {
333            key: vec![b"k".to_vec()],
334            parents: Some(vec![
335                vec![b"f".to_vec(), b"p1".to_vec()],
336                vec![b"f".to_vec(), b"p2".to_vec()],
337            ]),
338            start: 1,
339            end: 2,
340        }];
341        assert_eq!(
342            build_header_lines(&factories),
343            b"k\nf\x00p1\tf\x00p2\n1\n2\n"
344        );
345    }
346
347    #[test]
348    fn build_header_lines_empty_parents_list_emits_empty_line() {
349        // A `Some(vec![])` round-trips through the parser because the empty
350        // segment from splitting `b""` on `\t` is filtered out.
351        let factories = vec![WireFactory {
352            key: vec![b"k".to_vec()],
353            parents: Some(vec![]),
354            start: 0,
355            end: 1,
356        }];
357        assert_eq!(build_header_lines(&factories), b"k\n\n0\n1\n");
358    }
359
360    #[test]
361    fn build_wire_prefix_round_trips_via_parse_wire() {
362        let factories = vec![
363            WireFactory {
364                key: vec![b"file-a".to_vec(), b"rev1".to_vec()],
365                parents: None,
366                start: 0,
367                end: 32,
368            },
369            WireFactory {
370                key: vec![b"file-b".to_vec(), b"rev2".to_vec()],
371                parents: Some(vec![vec![b"file-a".to_vec(), b"rev1".to_vec()]]),
372                start: 32,
373                end: 96,
374            },
375        ];
376        let block = b"BLOCK_PAYLOAD";
377        let mut wire = build_wire_prefix(&factories, block.len()).unwrap();
378        wire.extend_from_slice(block);
379
380        let frame = parse_wire(&wire).unwrap();
381        assert_eq!(frame.block_bytes, block);
382        assert_eq!(frame.factories, factories);
383    }
384}