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";
12pub const PATH_RELEASE: &str = "/mgmt/release";
13pub const PATH_WORK: &str = "/mgmt/work";
14
15pub const RES_OK: u8 = 0x00;
16pub const RES_DISALLOWED: u8 = 0x01;
17pub const RES_INVALID_REQ: u8 = 0x02;
18pub const RES_NOT_FOUND: u8 = 0x03;
19pub const RES_REMOTE_FAIL: u8 = 0xff;
20
21pub const IDX_REPOSITORY: u64 = 0x00;
22pub const IDX_RESULT_CODE: u64 = 0x01;
23
24const ACTION_UPDATE_REF: &str = "update_ref";
25const ACTION_DELETE_REF: &str = "delete_ref";
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct RefUpdate {
29    pub refname: String,
30    pub old: Option<String>,
31    pub new: Option<String>,
32    pub force: bool,
33}
34
35pub fn status_bytes(code: u8, body: impl AsRef<[u8]>) -> Vec<u8> {
36    let mut out = Vec::with_capacity(1 + body.as_ref().len());
37    out.push(code);
38    out.extend_from_slice(body.as_ref());
39    out
40}
41
42pub fn metadata_status(code: u8) -> Vec<u8> {
43    msgpack::pack(&Value::Map(vec![(
44        Value::UInt(IDX_RESULT_CODE),
45        Value::UInt(code as u64),
46    )]))
47}
48
49pub fn repository_request(repository: &str) -> Vec<u8> {
50    msgpack::pack(&Value::Map(vec![(
51        Value::UInt(IDX_REPOSITORY),
52        Value::Str(repository.to_string()),
53    )]))
54}
55
56pub fn repository_from_request(data: &[u8]) -> Result<String> {
57    let value =
58        msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
59    let map = value
60        .as_map()
61        .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
62    map_get(map, IDX_REPOSITORY)
63        .and_then(Value::as_str)
64        .map(ToOwned::to_owned)
65        .ok_or_else(|| Error::msg("request missing repository"))
66}
67
68pub fn fetch_request(repository: &str, have: &[String]) -> Vec<u8> {
69    msgpack::pack(&Value::Map(vec![
70        (
71            Value::UInt(IDX_REPOSITORY),
72            Value::Str(repository.to_string()),
73        ),
74        (
75            Value::Str("have".to_string()),
76            Value::Array(have.iter().map(|v| Value::Str(v.clone())).collect()),
77        ),
78    ]))
79}
80
81pub fn parse_fetch_request(data: &[u8]) -> Result<(String, Vec<String>)> {
82    let value =
83        msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
84    let map = value
85        .as_map()
86        .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
87    let repo = map_get(map, IDX_REPOSITORY)
88        .and_then(Value::as_str)
89        .map(ToOwned::to_owned)
90        .ok_or_else(|| Error::msg("request missing repository"))?;
91    let have = map_get_str(map, "have")
92        .and_then(Value::as_array)
93        .map(|arr| {
94            arr.iter()
95                .filter_map(Value::as_str)
96                .map(ToOwned::to_owned)
97                .collect()
98        })
99        .unwrap_or_default();
100    Ok((repo, have))
101}
102
103pub fn push_request(repository: &str, bundle: Vec<u8>, updates: Vec<RefUpdate>) -> Vec<u8> {
104    msgpack::pack(&Value::Map(vec![
105        (
106            Value::UInt(IDX_REPOSITORY),
107            Value::Str(repository.to_string()),
108        ),
109        (Value::Str("bundle".to_string()), Value::Bin(bundle)),
110        (
111            Value::Str("operations".to_string()),
112            Value::Array(updates.into_iter().map(update_to_operation_value).collect()),
113        ),
114    ]))
115}
116
117pub fn parse_push_request(data: &[u8]) -> Result<(String, Vec<u8>, Vec<RefUpdate>)> {
118    let value =
119        msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
120    let map = value
121        .as_map()
122        .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
123    let repo = map_get(map, IDX_REPOSITORY)
124        .and_then(Value::as_str)
125        .map(ToOwned::to_owned)
126        .ok_or_else(|| Error::msg("request missing repository"))?;
127    let bundle = map_get_str(map, "bundle")
128        .and_then(Value::as_bin)
129        .map(ToOwned::to_owned)
130        .unwrap_or_default();
131    let updates = if let Some(arr) = map_get_str(map, "operations").and_then(Value::as_array) {
132        arr.iter()
133            .map(operation_value_to_update)
134            .collect::<Result<Vec<_>>>()?
135    } else {
136        map_get_str(map, "updates")
137            .and_then(Value::as_array)
138            .map(|arr| arr.iter().map(value_to_update).collect::<Result<Vec<_>>>())
139            .transpose()?
140            .unwrap_or_default()
141    };
142    Ok((repo, bundle, updates))
143}
144
145pub fn response_bin(data: &[u8]) -> Result<Vec<u8>> {
146    match msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid response: {e}")))? {
147        Value::Bin(bytes) => Ok(bytes),
148        other => Err(Error::msg(format!(
149            "expected binary response, got {other:?}"
150        ))),
151    }
152}
153
154#[allow(dead_code)]
155fn update_to_value(update: RefUpdate) -> Value {
156    let mut items = vec![
157        (Value::Str("ref".to_string()), Value::Str(update.refname)),
158        (Value::Str("force".to_string()), Value::Bool(update.force)),
159    ];
160    items.push((
161        Value::Str("old".to_string()),
162        update.old.map(Value::Str).unwrap_or(Value::Nil),
163    ));
164    items.push((
165        Value::Str("new".to_string()),
166        update.new.map(Value::Str).unwrap_or(Value::Nil),
167    ));
168    Value::Map(items)
169}
170
171fn update_to_operation_value(update: RefUpdate) -> Value {
172    let action = if update.new.is_some() {
173        ACTION_UPDATE_REF
174    } else {
175        ACTION_DELETE_REF
176    };
177    let mut items = vec![
178        (
179            Value::Str("action".to_string()),
180            Value::Str(action.to_string()),
181        ),
182        (Value::Str("ref".to_string()), Value::Str(update.refname)),
183        (Value::Str("force".to_string()), Value::Bool(update.force)),
184    ];
185    if let Some(old) = update.old {
186        items.push((Value::Str("old".to_string()), Value::Str(old)));
187    }
188    if let Some(new) = update.new {
189        items.push((Value::Str("sha".to_string()), Value::Str(new)));
190    }
191    Value::Map(items)
192}
193
194fn operation_value_to_update(value: &Value) -> Result<RefUpdate> {
195    let map = value
196        .as_map()
197        .ok_or_else(|| Error::msg("operation must be a map"))?;
198    let action = map_get_str(map, "action")
199        .and_then(Value::as_str)
200        .ok_or_else(|| Error::msg("operation missing action"))?;
201    let refname = map_get_str(map, "ref")
202        .and_then(Value::as_str)
203        .map(ToOwned::to_owned)
204        .ok_or_else(|| Error::msg("operation missing ref"))?;
205    let old = map_get_str(map, "old")
206        .and_then(Value::as_str)
207        .map(ToOwned::to_owned);
208    let force = map_get_str(map, "force")
209        .and_then(Value::as_bool)
210        .unwrap_or(false);
211
212    match action {
213        ACTION_UPDATE_REF => {
214            let new = map_get_str(map, "sha")
215                .or_else(|| map_get_str(map, "new"))
216                .and_then(Value::as_str)
217                .map(ToOwned::to_owned)
218                .ok_or_else(|| Error::msg("update_ref operation missing sha"))?;
219            Ok(RefUpdate {
220                refname,
221                old,
222                new: Some(new),
223                force,
224            })
225        }
226        ACTION_DELETE_REF => Ok(RefUpdate {
227            refname,
228            old,
229            new: None,
230            force,
231        }),
232        other => Err(Error::msg(format!("unknown push operation {other}"))),
233    }
234}
235
236fn value_to_update(value: &Value) -> Result<RefUpdate> {
237    let map = value
238        .as_map()
239        .ok_or_else(|| Error::msg("update must be a map"))?;
240    let refname = map_get_str(map, "ref")
241        .and_then(Value::as_str)
242        .map(ToOwned::to_owned)
243        .ok_or_else(|| Error::msg("update missing ref"))?;
244    let old = map_get_str(map, "old")
245        .and_then(Value::as_str)
246        .map(ToOwned::to_owned);
247    let new = map_get_str(map, "new")
248        .and_then(Value::as_str)
249        .map(ToOwned::to_owned);
250    let force = map_get_str(map, "force")
251        .and_then(Value::as_bool)
252        .unwrap_or(false);
253    Ok(RefUpdate {
254        refname,
255        old,
256        new,
257        force,
258    })
259}
260
261fn map_get<'a>(map: &'a [(Value, Value)], key: u64) -> Option<&'a Value> {
262    map.iter().find_map(|(k, v)| {
263        if matches!(k, Value::UInt(v) if *v == key) {
264            Some(v)
265        } else {
266            None
267        }
268    })
269}
270
271fn map_get_str<'a>(map: &'a [(Value, Value)], key: &str) -> Option<&'a Value> {
272    map.iter().find_map(|(k, v)| match k {
273        Value::Str(s) if s == key => Some(v),
274        _ => None,
275    })
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn repository_roundtrip_uses_upstream_index() {
284        assert_eq!(
285            repository_from_request(&repository_request("group/repo")).unwrap(),
286            "group/repo"
287        );
288    }
289
290    #[test]
291    fn push_roundtrip_preserves_updates() {
292        let update = RefUpdate {
293            refname: "refs/heads/main".to_string(),
294            old: None,
295            new: Some("abc".to_string()),
296            force: true,
297        };
298        let (repo, bundle, updates) = parse_push_request(&push_request(
299            "repo",
300            b"bundle".to_vec(),
301            vec![update.clone()],
302        ))
303        .unwrap();
304        assert_eq!(repo, "repo");
305        assert_eq!(bundle, b"bundle");
306        assert_eq!(updates, vec![update]);
307    }
308
309    #[test]
310    fn push_request_uses_operations_field() {
311        let update = RefUpdate {
312            refname: "refs/heads/main".to_string(),
313            old: Some("old".to_string()),
314            new: Some("new".to_string()),
315            force: true,
316        };
317        let packed = push_request("repo", Vec::new(), vec![update]);
318        let value = msgpack::unpack_exact(&packed).unwrap();
319        let map = value.as_map().unwrap();
320
321        assert!(map_get_str(map, "operations").is_some());
322        assert!(map_get_str(map, "updates").is_none());
323    }
324
325    #[test]
326    fn parse_push_request_accepts_update_and_delete_operations() {
327        let request = msgpack::pack(&Value::Map(vec![
328            (Value::UInt(IDX_REPOSITORY), Value::Str("repo".into())),
329            (Value::Str("bundle".into()), Value::Bin(Vec::new())),
330            (
331                Value::Str("operations".into()),
332                Value::Array(vec![
333                    Value::Map(vec![
334                        (
335                            Value::Str("action".into()),
336                            Value::Str(ACTION_UPDATE_REF.into()),
337                        ),
338                        (
339                            Value::Str("ref".into()),
340                            Value::Str("refs/heads/main".into()),
341                        ),
342                        (Value::Str("sha".into()), Value::Str("new".into())),
343                        (Value::Str("force".into()), Value::Bool(true)),
344                    ]),
345                    Value::Map(vec![
346                        (
347                            Value::Str("action".into()),
348                            Value::Str(ACTION_DELETE_REF.into()),
349                        ),
350                        (
351                            Value::Str("ref".into()),
352                            Value::Str("refs/heads/old".into()),
353                        ),
354                    ]),
355                ]),
356            ),
357        ]));
358
359        let (_, _, updates) = parse_push_request(&request).unwrap();
360
361        assert_eq!(
362            updates,
363            vec![
364                RefUpdate {
365                    refname: "refs/heads/main".into(),
366                    old: None,
367                    new: Some("new".into()),
368                    force: true
369                },
370                RefUpdate {
371                    refname: "refs/heads/old".into(),
372                    old: None,
373                    new: None,
374                    force: false
375                }
376            ]
377        );
378    }
379
380    #[test]
381    fn parse_push_request_keeps_legacy_updates_compatibility() {
382        let update = RefUpdate {
383            refname: "refs/heads/main".into(),
384            old: Some("old".into()),
385            new: Some("new".into()),
386            force: false,
387        };
388        let request = msgpack::pack(&Value::Map(vec![
389            (Value::UInt(IDX_REPOSITORY), Value::Str("repo".into())),
390            (Value::Str("bundle".into()), Value::Bin(Vec::new())),
391            (
392                Value::Str("updates".into()),
393                Value::Array(vec![update_to_value(update.clone())]),
394            ),
395        ]));
396
397        let (_, _, updates) = parse_push_request(&request).unwrap();
398
399        assert_eq!(updates, vec![update]);
400    }
401
402    #[test]
403    fn status_response_is_status_byte_plus_payload() {
404        assert_eq!(status_bytes(RES_OK, b"ok"), b"\0ok");
405    }
406}