Skip to main content

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}