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