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