gunnar_sendpack/capabilities.rs
1//! The capability list, which both ends parse and both ends write.
2//!
3//! In protocol v0 — the only protocol push has — capabilities ride on the
4//! **first line** of the reference advertisement and of the command list, after
5//! a NUL byte. Every later line carries none. That asymmetry is the single
6//! most-repeated bug in the format, and it is why [`split`] exists as one
7//! function rather than as a `line.split(0)` written at four call sites.
8
9use crate::error::{Error, Result};
10
11/// A space-separated capability list, kept as raw strings.
12///
13/// Not a bitfield: `agent=…` and `object-format=…` carry values, unknown
14/// capabilities must survive round-tripping for diagnostics, and the set grows
15/// with every git release. A server that dropped what it did not recognise
16/// would report a peer's capabilities as smaller than they were.
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct Capabilities(Vec<String>);
19
20impl Capabilities {
21 /// Parse a space-separated capability list.
22 ///
23 /// Empty items are dropped. git's own `receive-pack` client emits
24 /// `…\0 report-status-v2 …` with a **leading** space, which a naive
25 /// `split(' ')` turns into a phantom capability with an empty name.
26 pub fn parse(raw: &str) -> Self {
27 Capabilities(
28 raw.split(' ')
29 .map(str::trim)
30 .filter(|s| !s.is_empty())
31 .map(str::to_owned)
32 .collect(),
33 )
34 }
35
36 /// Parse from bytes, refusing a list that is not UTF-8.
37 ///
38 /// Reference *names* are bytes (see [`crate::command`]); capability names
39 /// are not — every one git defines is ASCII, and a non-ASCII one would be a
40 /// peer this grammar has no way to satisfy.
41 pub fn parse_bytes(raw: &[u8]) -> Result<Self> {
42 let text = std::str::from_utf8(raw)
43 .map_err(|_| Error::protocol("the capability list is not UTF-8"))?;
44 Ok(Self::parse(text))
45 }
46
47 /// Build from an ordered list.
48 pub fn from_items<S: Into<String>>(items: impl IntoIterator<Item = S>) -> Self {
49 Capabilities(items.into_iter().map(Into::into).collect())
50 }
51
52 /// Is this capability present, with or without a value?
53 pub fn has(&self, name: &str) -> bool {
54 self.0.iter().any(|c| {
55 c == name || (c.starts_with(name) && c.as_bytes().get(name.len()) == Some(&b'='))
56 })
57 }
58
59 /// The value of `name=value`, or `None` when absent or valueless.
60 pub fn value(&self, name: &str) -> Option<&str> {
61 self.0.iter().find_map(|c| {
62 let rest = c.strip_prefix(name)?;
63 rest.strip_prefix('=')
64 })
65 }
66
67 /// Every capability, in the order it appeared.
68 pub fn all(&self) -> &[String] {
69 &self.0
70 }
71
72 /// The wire form: the items joined by single spaces.
73 pub fn render(&self) -> String {
74 self.0.join(" ")
75 }
76
77 /// Is the list empty?
78 pub fn is_empty(&self) -> bool {
79 self.0.is_empty()
80 }
81}
82
83/// Split a first line into its content and its capability list.
84///
85/// Returns `(content, Some(capabilities))` when a NUL is present and
86/// `(line, None)` when it is not. **A line with a NUL and nothing after it has
87/// an empty capability list, not an absent one**, which is why the second
88/// element is `Option<&[u8]>` rather than a slice that could be empty for two
89/// different reasons.
90pub fn split(line: &[u8]) -> (&[u8], Option<&[u8]>) {
91 match line.iter().position(|&b| b == 0) {
92 Some(i) => (&line[..i], Some(&line[i + 1..])),
93 None => (line, None),
94 }
95}
96
97/// Append `capabilities` to `line` the way the wire wants it: a NUL, then the
98/// list. A no-op for an empty list, because `git` reads a trailing NUL with
99/// nothing after it as a capability list it then fails to parse.
100pub fn attach(line: &mut Vec<u8>, capabilities: &[String]) {
101 if capabilities.is_empty() {
102 return;
103 }
104 line.push(0);
105 line.extend_from_slice(capabilities.join(" ").as_bytes());
106}