1use alloc::vec::Vec;
8
9use crate::err::{Error, Result};
10use crate::oid::Oid;
11use crate::pack::Pack;
12
13pub struct Bundle<'a> {
14 pub refs: Vec<(&'a [u8], Oid)>,
16 pub prerequisites: Vec<Oid>,
18 pub pack: Pack<'a>,
19}
20
21impl<'a> Bundle<'a> {
22 pub fn parse(data: &'a [u8]) -> Result<Self> {
23 let (signature, mut rest) = split_line(data)?;
24 let v3 = match signature {
25 b"# v2 git bundle" => false,
26 b"# v3 git bundle" => true,
27 _ => return Err(Error::Unsupported("bundle signature")),
28 };
29
30 if v3 {
33 while rest.first() == Some(&b'@') {
34 let (line, next) = split_line(rest)?;
35 rest = next;
36 match &line[1..] {
37 b"object-format=sha1" => {}
38 _ => return Err(Error::Unsupported("bundle capability")),
39 }
40 }
41 }
42
43 let mut refs = Vec::new();
44 let mut prerequisites = Vec::new();
45 loop {
46 let (line, next) = split_line(rest)?;
47 rest = next;
48 if line.is_empty() {
49 break;
50 }
51 if let Some(v) = line.strip_prefix(b"-") {
52 let hex = v.get(..40).ok_or(Error::Corrupt("bundle prerequisite"))?;
54 prerequisites.push(Oid::from_hex(hex)?);
55 } else {
56 let hex = line.get(..40).ok_or(Error::Corrupt("bundle ref"))?;
57 let name = line
58 .get(41..)
59 .filter(|n| !n.is_empty() && line[40] == b' ')
60 .ok_or(Error::Corrupt("bundle ref name"))?;
61 refs.push((name, Oid::from_hex(hex)?));
62 }
63 }
64
65 Ok(Self {
66 refs,
67 prerequisites,
68 pack: Pack::parse(rest)?,
69 })
70 }
71
72 pub fn find_ref(&self, name: &[u8]) -> Option<Oid> {
74 self.refs
75 .iter()
76 .find(|(n, _)| *n == name)
77 .map(|(_, oid)| *oid)
78 }
79}
80
81fn split_line(data: &[u8]) -> Result<(&[u8], &[u8])> {
82 let nl = data
83 .iter()
84 .position(|&b| b == b'\n')
85 .ok_or(Error::Corrupt("bundle header line"))?;
86 Ok((&data[..nl], &data[nl + 1..]))
87}
88
89pub fn write(refs: &[(&[u8], Oid)], shallow: &[Oid], pack_data: &[u8]) -> Result<Vec<u8>> {
96 use crate::object::{self, Kind};
97
98 let mut prerequisites: Vec<Oid> = Vec::new();
99 if !shallow.is_empty() {
100 let pack = Pack::parse(pack_data)?;
101 for oid in shallow {
102 let Some((Kind::Commit, body)) = pack.read_object(oid)? else {
103 return Err(Error::Corrupt("shallow oid not in pack"));
104 };
105 for parent in object::parse_commit(&body)?.parents {
106 if !pack.contains(&parent) && !prerequisites.contains(&parent) {
107 prerequisites.push(parent);
108 }
109 }
110 }
111 }
112
113 let mut out = Vec::with_capacity(pack_data.len() + 64 * (refs.len() + 2));
114 out.extend_from_slice(b"# v2 git bundle\n");
115 for oid in &prerequisites {
116 out.push(b'-');
117 push_hex(&mut out, oid);
118 out.push(b'\n');
119 }
120 for (name, oid) in refs {
121 push_hex(&mut out, oid);
122 out.push(b' ');
123 out.extend_from_slice(name);
124 out.push(b'\n');
125 }
126 out.push(b'\n');
127 out.extend_from_slice(pack_data);
128 Ok(out)
129}
130
131fn push_hex(out: &mut Vec<u8>, oid: &Oid) {
132 const HEX: &[u8; 16] = b"0123456789abcdef";
133 for &b in oid.as_bytes() {
134 out.push(HEX[usize::from(b >> 4)]);
135 out.push(HEX[usize::from(b & 0xf)]);
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::sha1;
143 use alloc::vec;
144
145 fn empty_pack() -> Vec<u8> {
147 let mut c = Vec::new();
148 c.extend_from_slice(b"PACK");
149 c.extend_from_slice(&2u32.to_be_bytes());
150 c.extend_from_slice(&0u32.to_be_bytes());
151 let digest = sha1::digest(&c);
152 c.extend_from_slice(&digest);
153 c
154 }
155
156 #[test]
157 fn write_parse_roundtrip() {
158 let oid = Oid::from_bytes([0x42; 20]);
159 let pack = empty_pack();
160 let data = write(&[(b"refs/heads/main", oid)], &[], &pack).unwrap();
161
162 let bundle = Bundle::parse(&data).unwrap();
163 assert_eq!(bundle.refs, vec![(&b"refs/heads/main"[..], oid)]);
164 assert!(bundle.prerequisites.is_empty());
165 assert!(bundle.pack.is_empty());
166 }
167}