Skip to main content

rns_git/
protocol.rs

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