Skip to main content

rns_git/
protocol.rs

1use rns_core::msgpack::{self, Value};
2
3use crate::{Error, Result};
4
5pub const APP_NAME: &str = "git";
6pub const ASPECT_REPOSITORIES: &str = "repositories";
7
8pub const PATH_LIST: &str = "/git/list";
9pub const PATH_FETCH: &str = "/git/fetch";
10pub const PATH_PUSH: &str = "/git/push";
11pub const PATH_DELETE: &str = "/git/delete";
12
13pub const RES_OK: u8 = 0x00;
14pub const RES_DISALLOWED: u8 = 0x01;
15pub const RES_INVALID_REQ: u8 = 0x02;
16pub const RES_NOT_FOUND: u8 = 0x03;
17pub const RES_REMOTE_FAIL: u8 = 0xff;
18
19pub const IDX_REPOSITORY: u64 = 0x00;
20pub const IDX_RESULT_CODE: u64 = 0x01;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct RefUpdate {
24    pub refname: String,
25    pub old: Option<String>,
26    pub new: Option<String>,
27    pub force: bool,
28}
29
30pub fn status_bytes(code: u8, body: impl AsRef<[u8]>) -> Vec<u8> {
31    let mut out = Vec::with_capacity(1 + body.as_ref().len());
32    out.push(code);
33    out.extend_from_slice(body.as_ref());
34    out
35}
36
37pub fn metadata_status(code: u8) -> Vec<u8> {
38    msgpack::pack(&Value::Map(vec![(
39        Value::UInt(IDX_RESULT_CODE),
40        Value::UInt(code as u64),
41    )]))
42}
43
44pub fn repository_request(repository: &str) -> Vec<u8> {
45    msgpack::pack(&Value::Map(vec![(
46        Value::UInt(IDX_REPOSITORY),
47        Value::Str(repository.to_string()),
48    )]))
49}
50
51pub fn repository_from_request(data: &[u8]) -> Result<String> {
52    let value =
53        msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
54    let map = value
55        .as_map()
56        .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
57    map_get(map, IDX_REPOSITORY)
58        .and_then(Value::as_str)
59        .map(ToOwned::to_owned)
60        .ok_or_else(|| Error::msg("request missing repository"))
61}
62
63pub fn fetch_request(repository: &str, have: &[String]) -> Vec<u8> {
64    msgpack::pack(&Value::Map(vec![
65        (
66            Value::UInt(IDX_REPOSITORY),
67            Value::Str(repository.to_string()),
68        ),
69        (
70            Value::Str("have".to_string()),
71            Value::Array(have.iter().map(|v| Value::Str(v.clone())).collect()),
72        ),
73    ]))
74}
75
76pub fn parse_fetch_request(data: &[u8]) -> Result<(String, Vec<String>)> {
77    let value =
78        msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
79    let map = value
80        .as_map()
81        .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
82    let repo = map_get(map, IDX_REPOSITORY)
83        .and_then(Value::as_str)
84        .map(ToOwned::to_owned)
85        .ok_or_else(|| Error::msg("request missing repository"))?;
86    let have = map_get_str(map, "have")
87        .and_then(Value::as_array)
88        .map(|arr| {
89            arr.iter()
90                .filter_map(Value::as_str)
91                .map(ToOwned::to_owned)
92                .collect()
93        })
94        .unwrap_or_default();
95    Ok((repo, have))
96}
97
98pub fn push_request(repository: &str, bundle: Vec<u8>, updates: Vec<RefUpdate>) -> Vec<u8> {
99    msgpack::pack(&Value::Map(vec![
100        (
101            Value::UInt(IDX_REPOSITORY),
102            Value::Str(repository.to_string()),
103        ),
104        (Value::Str("bundle".to_string()), Value::Bin(bundle)),
105        (
106            Value::Str("updates".to_string()),
107            Value::Array(updates.into_iter().map(update_to_value).collect()),
108        ),
109    ]))
110}
111
112pub fn parse_push_request(data: &[u8]) -> Result<(String, Vec<u8>, Vec<RefUpdate>)> {
113    let value =
114        msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
115    let map = value
116        .as_map()
117        .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
118    let repo = map_get(map, IDX_REPOSITORY)
119        .and_then(Value::as_str)
120        .map(ToOwned::to_owned)
121        .ok_or_else(|| Error::msg("request missing repository"))?;
122    let bundle = map_get_str(map, "bundle")
123        .and_then(Value::as_bin)
124        .map(ToOwned::to_owned)
125        .unwrap_or_default();
126    let updates = map_get_str(map, "updates")
127        .and_then(Value::as_array)
128        .map(|arr| arr.iter().map(value_to_update).collect::<Result<Vec<_>>>())
129        .transpose()?
130        .unwrap_or_default();
131    Ok((repo, bundle, updates))
132}
133
134pub fn response_bin(data: &[u8]) -> Result<Vec<u8>> {
135    match msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid response: {e}")))? {
136        Value::Bin(bytes) => Ok(bytes),
137        other => Err(Error::msg(format!(
138            "expected binary response, got {other:?}"
139        ))),
140    }
141}
142
143fn update_to_value(update: RefUpdate) -> Value {
144    let mut items = vec![
145        (Value::Str("ref".to_string()), Value::Str(update.refname)),
146        (Value::Str("force".to_string()), Value::Bool(update.force)),
147    ];
148    items.push((
149        Value::Str("old".to_string()),
150        update.old.map(Value::Str).unwrap_or(Value::Nil),
151    ));
152    items.push((
153        Value::Str("new".to_string()),
154        update.new.map(Value::Str).unwrap_or(Value::Nil),
155    ));
156    Value::Map(items)
157}
158
159fn value_to_update(value: &Value) -> Result<RefUpdate> {
160    let map = value
161        .as_map()
162        .ok_or_else(|| Error::msg("update must be a map"))?;
163    let refname = map_get_str(map, "ref")
164        .and_then(Value::as_str)
165        .map(ToOwned::to_owned)
166        .ok_or_else(|| Error::msg("update missing ref"))?;
167    let old = map_get_str(map, "old")
168        .and_then(Value::as_str)
169        .map(ToOwned::to_owned);
170    let new = map_get_str(map, "new")
171        .and_then(Value::as_str)
172        .map(ToOwned::to_owned);
173    let force = map_get_str(map, "force")
174        .and_then(Value::as_bool)
175        .unwrap_or(false);
176    Ok(RefUpdate {
177        refname,
178        old,
179        new,
180        force,
181    })
182}
183
184fn map_get<'a>(map: &'a [(Value, Value)], key: u64) -> Option<&'a Value> {
185    map.iter().find_map(|(k, v)| {
186        if matches!(k, Value::UInt(v) if *v == key) {
187            Some(v)
188        } else {
189            None
190        }
191    })
192}
193
194fn map_get_str<'a>(map: &'a [(Value, Value)], key: &str) -> Option<&'a Value> {
195    map.iter().find_map(|(k, v)| match k {
196        Value::Str(s) if s == key => Some(v),
197        _ => None,
198    })
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn repository_roundtrip_uses_upstream_index() {
207        assert_eq!(
208            repository_from_request(&repository_request("group/repo")).unwrap(),
209            "group/repo"
210        );
211    }
212
213    #[test]
214    fn push_roundtrip_preserves_updates() {
215        let update = RefUpdate {
216            refname: "refs/heads/main".to_string(),
217            old: None,
218            new: Some("abc".to_string()),
219            force: true,
220        };
221        let (repo, bundle, updates) = parse_push_request(&push_request(
222            "repo",
223            b"bundle".to_vec(),
224            vec![update.clone()],
225        ))
226        .unwrap();
227        assert_eq!(repo, "repo");
228        assert_eq!(bundle, b"bundle");
229        assert_eq!(updates, vec![update]);
230    }
231
232    #[test]
233    fn status_response_is_status_byte_plus_payload() {
234        assert_eq!(status_bytes(RES_OK, b"ok"), b"\0ok");
235    }
236}