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    msgpack::pack(&Value::Map(vec![
108        (
109            Value::UInt(IDX_REPOSITORY),
110            Value::Str(repository.to_string()),
111        ),
112        (
113            Value::Str("have".to_string()),
114            Value::Array(have.iter().map(|v| Value::Str(v.clone())).collect()),
115        ),
116    ]))
117}
118
119pub fn parse_fetch_request(data: &[u8]) -> Result<(String, Vec<String>)> {
120    let value =
121        msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
122    let map = value
123        .as_map()
124        .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
125    let repo = map_get(map, IDX_REPOSITORY)
126        .and_then(Value::as_str)
127        .map(ToOwned::to_owned)
128        .ok_or_else(|| Error::msg("request missing repository"))?;
129    let have = map_get_str(map, "have")
130        .and_then(Value::as_array)
131        .map(|arr| {
132            arr.iter()
133                .filter_map(Value::as_str)
134                .map(ToOwned::to_owned)
135                .collect()
136        })
137        .unwrap_or_default();
138    Ok((repo, have))
139}
140
141pub fn push_request(repository: &str, bundle: Vec<u8>, updates: Vec<RefUpdate>) -> Vec<u8> {
142    msgpack::pack(&Value::Map(vec![
143        (
144            Value::UInt(IDX_REPOSITORY),
145            Value::Str(repository.to_string()),
146        ),
147        (Value::Str("bundle".to_string()), Value::Bin(bundle)),
148        (
149            Value::Str("operations".to_string()),
150            Value::Array(updates.into_iter().map(update_to_operation_value).collect()),
151        ),
152    ]))
153}
154
155pub fn parse_push_request(data: &[u8]) -> Result<(String, Vec<u8>, Vec<RefUpdate>)> {
156    let value =
157        msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
158    let map = value
159        .as_map()
160        .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
161    let repo = map_get(map, IDX_REPOSITORY)
162        .and_then(Value::as_str)
163        .map(ToOwned::to_owned)
164        .ok_or_else(|| Error::msg("request missing repository"))?;
165    let bundle = map_get_str(map, "bundle")
166        .and_then(Value::as_bin)
167        .map(ToOwned::to_owned)
168        .unwrap_or_default();
169    let updates = if let Some(arr) = map_get_str(map, "operations").and_then(Value::as_array) {
170        arr.iter()
171            .map(operation_value_to_update)
172            .collect::<Result<Vec<_>>>()?
173    } else {
174        map_get_str(map, "updates")
175            .and_then(Value::as_array)
176            .map(|arr| arr.iter().map(value_to_update).collect::<Result<Vec<_>>>())
177            .transpose()?
178            .unwrap_or_default()
179    };
180    Ok((repo, bundle, updates))
181}
182
183pub fn response_bin(data: &[u8]) -> Result<Vec<u8>> {
184    match msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid response: {e}")))? {
185        Value::Bin(bytes) => Ok(bytes),
186        other => Err(Error::msg(format!(
187            "expected binary response, got {other:?}"
188        ))),
189    }
190}
191
192#[allow(dead_code)]
193fn update_to_value(update: RefUpdate) -> Value {
194    let mut items = vec![
195        (Value::Str("ref".to_string()), Value::Str(update.refname)),
196        (Value::Str("force".to_string()), Value::Bool(update.force)),
197    ];
198    items.push((
199        Value::Str("old".to_string()),
200        update.old.map(Value::Str).unwrap_or(Value::Nil),
201    ));
202    items.push((
203        Value::Str("new".to_string()),
204        update.new.map(Value::Str).unwrap_or(Value::Nil),
205    ));
206    Value::Map(items)
207}
208
209fn update_to_operation_value(update: RefUpdate) -> Value {
210    let action = if update.new.is_some() {
211        ACTION_UPDATE_REF
212    } else {
213        ACTION_DELETE_REF
214    };
215    let mut items = vec![
216        (
217            Value::Str("action".to_string()),
218            Value::Str(action.to_string()),
219        ),
220        (Value::Str("ref".to_string()), Value::Str(update.refname)),
221        (Value::Str("force".to_string()), Value::Bool(update.force)),
222    ];
223    if let Some(old) = update.old {
224        items.push((Value::Str("old".to_string()), Value::Str(old)));
225    }
226    if let Some(new) = update.new {
227        items.push((Value::Str("sha".to_string()), Value::Str(new)));
228    }
229    Value::Map(items)
230}
231
232fn operation_value_to_update(value: &Value) -> Result<RefUpdate> {
233    let map = value
234        .as_map()
235        .ok_or_else(|| Error::msg("operation must be a map"))?;
236    let action = map_get_str(map, "action")
237        .and_then(Value::as_str)
238        .ok_or_else(|| Error::msg("operation missing action"))?;
239    let refname = map_get_str(map, "ref")
240        .and_then(Value::as_str)
241        .map(ToOwned::to_owned)
242        .ok_or_else(|| Error::msg("operation missing ref"))?;
243    let old = map_get_str(map, "old")
244        .and_then(Value::as_str)
245        .map(ToOwned::to_owned);
246    let force = map_get_str(map, "force")
247        .and_then(Value::as_bool)
248        .unwrap_or(false);
249
250    match action {
251        ACTION_UPDATE_REF => {
252            let new = map_get_str(map, "sha")
253                .or_else(|| map_get_str(map, "new"))
254                .and_then(Value::as_str)
255                .map(ToOwned::to_owned)
256                .ok_or_else(|| Error::msg("update_ref operation missing sha"))?;
257            Ok(RefUpdate {
258                refname,
259                old,
260                new: Some(new),
261                force,
262            })
263        }
264        ACTION_DELETE_REF => Ok(RefUpdate {
265            refname,
266            old,
267            new: None,
268            force,
269        }),
270        other => Err(Error::msg(format!("unknown push operation {other}"))),
271    }
272}
273
274fn value_to_update(value: &Value) -> Result<RefUpdate> {
275    let map = value
276        .as_map()
277        .ok_or_else(|| Error::msg("update must be a map"))?;
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("update missing ref"))?;
282    let old = map_get_str(map, "old")
283        .and_then(Value::as_str)
284        .map(ToOwned::to_owned);
285    let new = map_get_str(map, "new")
286        .and_then(Value::as_str)
287        .map(ToOwned::to_owned);
288    let force = map_get_str(map, "force")
289        .and_then(Value::as_bool)
290        .unwrap_or(false);
291    Ok(RefUpdate {
292        refname,
293        old,
294        new,
295        force,
296    })
297}
298
299fn map_get<'a>(map: &'a [(Value, Value)], key: u64) -> Option<&'a Value> {
300    map.iter().find_map(|(k, v)| {
301        if matches!(k, Value::UInt(v) if *v == key) {
302            Some(v)
303        } else {
304            None
305        }
306    })
307}
308
309fn map_get_str<'a>(map: &'a [(Value, Value)], key: &str) -> Option<&'a Value> {
310    map.iter().find_map(|(k, v)| match k {
311        Value::Str(s) if s == key => Some(v),
312        _ => None,
313    })
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn repository_roundtrip_uses_upstream_index() {
322        assert_eq!(
323            repository_from_request(&repository_request("group/repo")).unwrap(),
324            "group/repo"
325        );
326    }
327
328    #[test]
329    fn remote_clone_request_roundtrip_includes_source() {
330        let req = remote_clone_request("group/repo", "https://example.invalid/repo.git");
331        assert_eq!(
332            parse_remote_clone_request(&req).unwrap(),
333            (
334                "group/repo".into(),
335                "https://example.invalid/repo.git".into()
336            )
337        );
338    }
339
340    #[test]
341    fn push_roundtrip_preserves_updates() {
342        let update = RefUpdate {
343            refname: "refs/heads/main".to_string(),
344            old: None,
345            new: Some("abc".to_string()),
346            force: true,
347        };
348        let (repo, bundle, updates) = parse_push_request(&push_request(
349            "repo",
350            b"bundle".to_vec(),
351            vec![update.clone()],
352        ))
353        .unwrap();
354        assert_eq!(repo, "repo");
355        assert_eq!(bundle, b"bundle");
356        assert_eq!(updates, vec![update]);
357    }
358
359    #[test]
360    fn push_request_uses_operations_field() {
361        let update = RefUpdate {
362            refname: "refs/heads/main".to_string(),
363            old: Some("old".to_string()),
364            new: Some("new".to_string()),
365            force: true,
366        };
367        let packed = push_request("repo", Vec::new(), vec![update]);
368        let value = msgpack::unpack_exact(&packed).unwrap();
369        let map = value.as_map().unwrap();
370
371        assert!(map_get_str(map, "operations").is_some());
372        assert!(map_get_str(map, "updates").is_none());
373    }
374
375    #[test]
376    fn parse_push_request_accepts_update_and_delete_operations() {
377        let request = msgpack::pack(&Value::Map(vec![
378            (Value::UInt(IDX_REPOSITORY), Value::Str("repo".into())),
379            (Value::Str("bundle".into()), Value::Bin(Vec::new())),
380            (
381                Value::Str("operations".into()),
382                Value::Array(vec![
383                    Value::Map(vec![
384                        (
385                            Value::Str("action".into()),
386                            Value::Str(ACTION_UPDATE_REF.into()),
387                        ),
388                        (
389                            Value::Str("ref".into()),
390                            Value::Str("refs/heads/main".into()),
391                        ),
392                        (Value::Str("sha".into()), Value::Str("new".into())),
393                        (Value::Str("force".into()), Value::Bool(true)),
394                    ]),
395                    Value::Map(vec![
396                        (
397                            Value::Str("action".into()),
398                            Value::Str(ACTION_DELETE_REF.into()),
399                        ),
400                        (
401                            Value::Str("ref".into()),
402                            Value::Str("refs/heads/old".into()),
403                        ),
404                    ]),
405                ]),
406            ),
407        ]));
408
409        let (_, _, updates) = parse_push_request(&request).unwrap();
410
411        assert_eq!(
412            updates,
413            vec![
414                RefUpdate {
415                    refname: "refs/heads/main".into(),
416                    old: None,
417                    new: Some("new".into()),
418                    force: true
419                },
420                RefUpdate {
421                    refname: "refs/heads/old".into(),
422                    old: None,
423                    new: None,
424                    force: false
425                }
426            ]
427        );
428    }
429
430    #[test]
431    fn parse_push_request_keeps_legacy_updates_compatibility() {
432        let update = RefUpdate {
433            refname: "refs/heads/main".into(),
434            old: Some("old".into()),
435            new: Some("new".into()),
436            force: false,
437        };
438        let request = msgpack::pack(&Value::Map(vec![
439            (Value::UInt(IDX_REPOSITORY), Value::Str("repo".into())),
440            (Value::Str("bundle".into()), Value::Bin(Vec::new())),
441            (
442                Value::Str("updates".into()),
443                Value::Array(vec![update_to_value(update.clone())]),
444            ),
445        ]));
446
447        let (_, _, updates) = parse_push_request(&request).unwrap();
448
449        assert_eq!(updates, vec![update]);
450    }
451
452    #[test]
453    fn status_response_is_status_byte_plus_payload() {
454        assert_eq!(status_bytes(RES_OK, b"ok"), b"\0ok");
455    }
456}