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