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 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}