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}