gix_protocol/handshake/refs/
shared.rs1use bstr::{BStr, BString, ByteSlice};
2
3use crate::{
4 fetch::response::ShallowUpdate,
5 handshake::{refs::parse::Error, Ref},
6};
7
8impl From<InternalRef> for Ref {
9 fn from(v: InternalRef) -> Self {
10 match v {
11 InternalRef::Symbolic {
12 path,
13 target: Some(target),
14 tag,
15 object,
16 } => Ref::Symbolic {
17 full_ref_name: path,
18 target,
19 tag,
20 object,
21 },
22 InternalRef::Symbolic {
23 path,
24 target: None,
25 tag: None,
26 object,
27 } => Ref::Direct {
28 full_ref_name: path,
29 object,
30 },
31 InternalRef::Symbolic {
32 path,
33 target: None,
34 tag: Some(tag),
35 object,
36 } => Ref::Peeled {
37 full_ref_name: path,
38 tag,
39 object,
40 },
41 InternalRef::Peeled { path, tag, object } => Ref::Peeled {
42 full_ref_name: path,
43 tag,
44 object,
45 },
46 InternalRef::Direct { path, object } => Ref::Direct {
47 full_ref_name: path,
48 object,
49 },
50 InternalRef::SymbolicForLookup { .. } => {
51 unreachable!("this case should have been removed during processing")
52 }
53 }
54 }
55}
56
57#[cfg_attr(test, derive(PartialEq, Eq, Debug, Clone))]
58pub(crate) enum InternalRef {
59 Peeled {
61 path: BString,
62 tag: gix_hash::ObjectId,
63 object: gix_hash::ObjectId,
64 },
65 Direct { path: BString, object: gix_hash::ObjectId },
67 Symbolic {
69 path: BString,
70 target: Option<BString>,
75 tag: Option<gix_hash::ObjectId>,
76 object: gix_hash::ObjectId,
77 },
78 SymbolicForLookup { path: BString, target: Option<BString> },
81}
82
83impl InternalRef {
84 fn unpack_direct(self) -> Option<(BString, gix_hash::ObjectId)> {
85 match self {
86 InternalRef::Direct { path, object } => Some((path, object)),
87 _ => None,
88 }
89 }
90 fn lookup_symbol_has_path(&self, predicate_path: &BStr) -> bool {
91 matches!(self, InternalRef::SymbolicForLookup { path, .. } if path == predicate_path)
92 }
93}
94
95pub(crate) fn from_capabilities<'a>(
96 capabilities: impl Iterator<Item = gix_transport::client::capabilities::Capability<'a>>,
97) -> Result<Vec<InternalRef>, Error> {
98 let mut out_refs = Vec::new();
99 let symref_values = capabilities.filter_map(|c| {
100 if c.name() == b"symref".as_bstr() {
101 c.value().map(ToOwned::to_owned)
102 } else {
103 None
104 }
105 });
106 for symref in symref_values {
107 let (left, right) = symref.split_at(symref.find_byte(b':').ok_or_else(|| Error::MalformedSymref {
108 symref: symref.to_owned(),
109 })?);
110 if left.is_empty() || right.is_empty() {
111 return Err(Error::MalformedSymref {
112 symref: symref.to_owned(),
113 });
114 }
115 out_refs.push(InternalRef::SymbolicForLookup {
116 path: left.into(),
117 target: match &right[1..] {
118 b"(null)" => None,
119 name => Some(name.into()),
120 },
121 });
122 }
123 Ok(out_refs)
124}
125
126pub(in crate::handshake::refs) fn parse_v1(
127 num_initial_out_refs: usize,
128 out_refs: &mut Vec<InternalRef>,
129 out_shallow: &mut Vec<ShallowUpdate>,
130 line: &BStr,
131) -> Result<(), Error> {
132 let trimmed = line.trim_end();
133 let (hex_hash, path) = trimmed.split_at(
134 trimmed
135 .find(b" ")
136 .ok_or_else(|| Error::MalformedV1RefLine(trimmed.to_owned().into()))?,
137 );
138 let path = &path[1..];
139 if path.is_empty() {
140 return Err(Error::MalformedV1RefLine(trimmed.to_owned().into()));
141 }
142 match path.strip_suffix(b"^{}") {
143 Some(stripped) => {
144 if hex_hash.iter().all(|b| *b == b'0') && stripped == b"capabilities" {
145 return Ok(());
147 }
148 let (previous_path, tag) =
149 out_refs
150 .pop()
151 .and_then(InternalRef::unpack_direct)
152 .ok_or(Error::InvariantViolation {
153 message: "Expecting peeled refs to be preceded by direct refs",
154 })?;
155 if previous_path != stripped {
156 return Err(Error::InvariantViolation {
157 message: "Expecting peeled refs to have the same base path as the previous, unpeeled one",
158 });
159 }
160 out_refs.push(InternalRef::Peeled {
161 path: previous_path,
162 tag,
163 object: gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?,
164 });
165 }
166 None => {
167 let object = match gix_hash::ObjectId::from_hex(hex_hash.as_bytes()) {
168 Ok(id) => id,
169 Err(_) if hex_hash.as_bstr() == "shallow" => {
170 let id = gix_hash::ObjectId::from_hex(path)?;
171 out_shallow.push(ShallowUpdate::Shallow(id));
172 return Ok(());
173 }
174 Err(err) => return Err(err.into()),
175 };
176 match out_refs
177 .iter()
178 .take(num_initial_out_refs)
179 .position(|r| r.lookup_symbol_has_path(path.into()))
180 {
181 Some(position) => match out_refs.swap_remove(position) {
182 InternalRef::SymbolicForLookup { path: _, target } => out_refs.push(InternalRef::Symbolic {
183 path: path.into(),
184 tag: None, object,
186 target,
187 }),
188 _ => unreachable!("Bug in lookup_symbol_has_path - must return lookup symbols"),
189 },
190 None => out_refs.push(InternalRef::Direct {
191 object,
192 path: path.into(),
193 }),
194 }
195 }
196 }
197 Ok(())
198}
199
200pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result<Ref, Error> {
201 let trimmed = line.trim_end();
202 let mut tokens = trimmed.splitn(4, |b| *b == b' ');
203 match (tokens.next(), tokens.next()) {
204 (Some(hex_hash), Some(path)) => {
205 let id = if hex_hash == b"unborn" {
206 None
207 } else {
208 Some(gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?)
209 };
210 if path.is_empty() {
211 return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
212 }
213 let mut symref_target = None;
214 let mut peeled = None;
215 for attribute in tokens.by_ref().take(2) {
216 let mut tokens = attribute.splitn(2, |b| *b == b':');
217 match (tokens.next(), tokens.next()) {
218 (Some(attribute), Some(value)) => {
219 if value.is_empty() {
220 return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
221 }
222 match attribute {
223 b"peeled" => {
224 peeled = Some(gix_hash::ObjectId::from_hex(value.as_bytes())?);
225 }
226 b"symref-target" => {
227 symref_target = Some(value);
228 }
229 _ => {
230 return Err(Error::UnknownAttribute {
231 attribute: attribute.to_owned().into(),
232 line: trimmed.to_owned().into(),
233 })
234 }
235 }
236 }
237 _ => return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())),
238 }
239 }
240 if tokens.next().is_some() {
241 return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
242 }
243 Ok(match (symref_target, peeled) {
244 (Some(target_name), peeled) => match target_name {
245 b"(null)" => match peeled {
246 None => Ref::Direct {
247 full_ref_name: path.into(),
248 object: id.ok_or(Error::InvariantViolation {
249 message: "got 'unborn' while (null) was a symref target",
250 })?,
251 },
252 Some(peeled) => Ref::Peeled {
253 full_ref_name: path.into(),
254 object: peeled,
255 tag: id.ok_or(Error::InvariantViolation {
256 message: "got 'unborn' while (null) was a symref target",
257 })?,
258 },
259 },
260 name => match id {
261 Some(id) => Ref::Symbolic {
262 full_ref_name: path.into(),
263 tag: peeled.map(|_| id),
264 object: peeled.unwrap_or(id),
265 target: name.into(),
266 },
267 None => Ref::Unborn {
268 full_ref_name: path.into(),
269 target: name.into(),
270 },
271 },
272 },
273 (None, Some(peeled)) => Ref::Peeled {
274 full_ref_name: path.into(),
275 object: peeled,
276 tag: id.ok_or(Error::InvariantViolation {
277 message: "got 'unborn' as tag target",
278 })?,
279 },
280 (None, None) => Ref::Direct {
281 object: id.ok_or(Error::InvariantViolation {
282 message: "got 'unborn' as object name of direct reference",
283 })?,
284 full_ref_name: path.into(),
285 },
286 })
287 }
288 _ => Err(Error::MalformedV2RefLine(trimmed.to_owned().into())),
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use gix_transport::client;
295
296 use crate::handshake::{refs, refs::shared::InternalRef};
297
298 #[test]
299 fn extract_symbolic_references_from_capabilities() -> Result<(), client::Error> {
300 let caps = client::Capabilities::from_bytes(
301 b"\0unrelated symref=HEAD:refs/heads/main symref=ANOTHER:refs/heads/foo symref=MISSING_NAMESPACE_TARGET:(null) agent=git/2.28.0",
302 )?
303 .0;
304 let out = refs::shared::from_capabilities(caps.iter()).expect("a working example");
305
306 assert_eq!(
307 out,
308 vec![
309 InternalRef::SymbolicForLookup {
310 path: "HEAD".into(),
311 target: Some("refs/heads/main".into())
312 },
313 InternalRef::SymbolicForLookup {
314 path: "ANOTHER".into(),
315 target: Some("refs/heads/foo".into())
316 },
317 InternalRef::SymbolicForLookup {
318 path: "MISSING_NAMESPACE_TARGET".into(),
319 target: None
320 }
321 ]
322 );
323 Ok(())
324 }
325}