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}