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_DELETE: &str = "/git/delete";
12pub const PATH_RELEASE: &str = "/mgmt/release";
13pub const PATH_WORK: &str = "/mgmt/work";
14
15pub const RES_OK: u8 = 0x00;
16pub const RES_DISALLOWED: u8 = 0x01;
17pub const RES_INVALID_REQ: u8 = 0x02;
18pub const RES_NOT_FOUND: u8 = 0x03;
19pub const RES_REMOTE_FAIL: u8 = 0xff;
20
21pub const IDX_REPOSITORY: u64 = 0x00;
22pub const IDX_RESULT_CODE: u64 = 0x01;
23
24const ACTION_UPDATE_REF: &str = "update_ref";
25const ACTION_DELETE_REF: &str = "delete_ref";
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct RefUpdate {
29 pub refname: String,
30 pub old: Option<String>,
31 pub new: Option<String>,
32 pub force: bool,
33}
34
35pub fn status_bytes(code: u8, body: impl AsRef<[u8]>) -> Vec<u8> {
36 let mut out = Vec::with_capacity(1 + body.as_ref().len());
37 out.push(code);
38 out.extend_from_slice(body.as_ref());
39 out
40}
41
42pub fn metadata_status(code: u8) -> Vec<u8> {
43 msgpack::pack(&Value::Map(vec![(
44 Value::UInt(IDX_RESULT_CODE),
45 Value::UInt(code as u64),
46 )]))
47}
48
49pub fn repository_request(repository: &str) -> Vec<u8> {
50 msgpack::pack(&Value::Map(vec![(
51 Value::UInt(IDX_REPOSITORY),
52 Value::Str(repository.to_string()),
53 )]))
54}
55
56pub fn repository_from_request(data: &[u8]) -> Result<String> {
57 let value =
58 msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
59 let map = value
60 .as_map()
61 .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
62 map_get(map, IDX_REPOSITORY)
63 .and_then(Value::as_str)
64 .map(ToOwned::to_owned)
65 .ok_or_else(|| Error::msg("request missing repository"))
66}
67
68pub fn fetch_request(repository: &str, have: &[String]) -> Vec<u8> {
69 msgpack::pack(&Value::Map(vec![
70 (
71 Value::UInt(IDX_REPOSITORY),
72 Value::Str(repository.to_string()),
73 ),
74 (
75 Value::Str("have".to_string()),
76 Value::Array(have.iter().map(|v| Value::Str(v.clone())).collect()),
77 ),
78 ]))
79}
80
81pub fn parse_fetch_request(data: &[u8]) -> Result<(String, Vec<String>)> {
82 let value =
83 msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
84 let map = value
85 .as_map()
86 .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
87 let repo = map_get(map, IDX_REPOSITORY)
88 .and_then(Value::as_str)
89 .map(ToOwned::to_owned)
90 .ok_or_else(|| Error::msg("request missing repository"))?;
91 let have = map_get_str(map, "have")
92 .and_then(Value::as_array)
93 .map(|arr| {
94 arr.iter()
95 .filter_map(Value::as_str)
96 .map(ToOwned::to_owned)
97 .collect()
98 })
99 .unwrap_or_default();
100 Ok((repo, have))
101}
102
103pub fn push_request(repository: &str, bundle: Vec<u8>, updates: Vec<RefUpdate>) -> Vec<u8> {
104 msgpack::pack(&Value::Map(vec![
105 (
106 Value::UInt(IDX_REPOSITORY),
107 Value::Str(repository.to_string()),
108 ),
109 (Value::Str("bundle".to_string()), Value::Bin(bundle)),
110 (
111 Value::Str("operations".to_string()),
112 Value::Array(updates.into_iter().map(update_to_operation_value).collect()),
113 ),
114 ]))
115}
116
117pub fn parse_push_request(data: &[u8]) -> Result<(String, Vec<u8>, Vec<RefUpdate>)> {
118 let value =
119 msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
120 let map = value
121 .as_map()
122 .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
123 let repo = map_get(map, IDX_REPOSITORY)
124 .and_then(Value::as_str)
125 .map(ToOwned::to_owned)
126 .ok_or_else(|| Error::msg("request missing repository"))?;
127 let bundle = map_get_str(map, "bundle")
128 .and_then(Value::as_bin)
129 .map(ToOwned::to_owned)
130 .unwrap_or_default();
131 let updates = if let Some(arr) = map_get_str(map, "operations").and_then(Value::as_array) {
132 arr.iter()
133 .map(operation_value_to_update)
134 .collect::<Result<Vec<_>>>()?
135 } else {
136 map_get_str(map, "updates")
137 .and_then(Value::as_array)
138 .map(|arr| arr.iter().map(value_to_update).collect::<Result<Vec<_>>>())
139 .transpose()?
140 .unwrap_or_default()
141 };
142 Ok((repo, bundle, updates))
143}
144
145pub fn response_bin(data: &[u8]) -> Result<Vec<u8>> {
146 match msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid response: {e}")))? {
147 Value::Bin(bytes) => Ok(bytes),
148 other => Err(Error::msg(format!(
149 "expected binary response, got {other:?}"
150 ))),
151 }
152}
153
154#[allow(dead_code)]
155fn update_to_value(update: RefUpdate) -> Value {
156 let mut items = vec![
157 (Value::Str("ref".to_string()), Value::Str(update.refname)),
158 (Value::Str("force".to_string()), Value::Bool(update.force)),
159 ];
160 items.push((
161 Value::Str("old".to_string()),
162 update.old.map(Value::Str).unwrap_or(Value::Nil),
163 ));
164 items.push((
165 Value::Str("new".to_string()),
166 update.new.map(Value::Str).unwrap_or(Value::Nil),
167 ));
168 Value::Map(items)
169}
170
171fn update_to_operation_value(update: RefUpdate) -> Value {
172 let action = if update.new.is_some() {
173 ACTION_UPDATE_REF
174 } else {
175 ACTION_DELETE_REF
176 };
177 let mut items = vec![
178 (
179 Value::Str("action".to_string()),
180 Value::Str(action.to_string()),
181 ),
182 (Value::Str("ref".to_string()), Value::Str(update.refname)),
183 (Value::Str("force".to_string()), Value::Bool(update.force)),
184 ];
185 if let Some(old) = update.old {
186 items.push((Value::Str("old".to_string()), Value::Str(old)));
187 }
188 if let Some(new) = update.new {
189 items.push((Value::Str("sha".to_string()), Value::Str(new)));
190 }
191 Value::Map(items)
192}
193
194fn operation_value_to_update(value: &Value) -> Result<RefUpdate> {
195 let map = value
196 .as_map()
197 .ok_or_else(|| Error::msg("operation must be a map"))?;
198 let action = map_get_str(map, "action")
199 .and_then(Value::as_str)
200 .ok_or_else(|| Error::msg("operation missing action"))?;
201 let refname = map_get_str(map, "ref")
202 .and_then(Value::as_str)
203 .map(ToOwned::to_owned)
204 .ok_or_else(|| Error::msg("operation missing ref"))?;
205 let old = map_get_str(map, "old")
206 .and_then(Value::as_str)
207 .map(ToOwned::to_owned);
208 let force = map_get_str(map, "force")
209 .and_then(Value::as_bool)
210 .unwrap_or(false);
211
212 match action {
213 ACTION_UPDATE_REF => {
214 let new = map_get_str(map, "sha")
215 .or_else(|| map_get_str(map, "new"))
216 .and_then(Value::as_str)
217 .map(ToOwned::to_owned)
218 .ok_or_else(|| Error::msg("update_ref operation missing sha"))?;
219 Ok(RefUpdate {
220 refname,
221 old,
222 new: Some(new),
223 force,
224 })
225 }
226 ACTION_DELETE_REF => Ok(RefUpdate {
227 refname,
228 old,
229 new: None,
230 force,
231 }),
232 other => Err(Error::msg(format!("unknown push operation {other}"))),
233 }
234}
235
236fn value_to_update(value: &Value) -> Result<RefUpdate> {
237 let map = value
238 .as_map()
239 .ok_or_else(|| Error::msg("update must be a map"))?;
240 let refname = map_get_str(map, "ref")
241 .and_then(Value::as_str)
242 .map(ToOwned::to_owned)
243 .ok_or_else(|| Error::msg("update missing ref"))?;
244 let old = map_get_str(map, "old")
245 .and_then(Value::as_str)
246 .map(ToOwned::to_owned);
247 let new = map_get_str(map, "new")
248 .and_then(Value::as_str)
249 .map(ToOwned::to_owned);
250 let force = map_get_str(map, "force")
251 .and_then(Value::as_bool)
252 .unwrap_or(false);
253 Ok(RefUpdate {
254 refname,
255 old,
256 new,
257 force,
258 })
259}
260
261fn map_get<'a>(map: &'a [(Value, Value)], key: u64) -> Option<&'a Value> {
262 map.iter().find_map(|(k, v)| {
263 if matches!(k, Value::UInt(v) if *v == key) {
264 Some(v)
265 } else {
266 None
267 }
268 })
269}
270
271fn map_get_str<'a>(map: &'a [(Value, Value)], key: &str) -> Option<&'a Value> {
272 map.iter().find_map(|(k, v)| match k {
273 Value::Str(s) if s == key => Some(v),
274 _ => None,
275 })
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn repository_roundtrip_uses_upstream_index() {
284 assert_eq!(
285 repository_from_request(&repository_request("group/repo")).unwrap(),
286 "group/repo"
287 );
288 }
289
290 #[test]
291 fn push_roundtrip_preserves_updates() {
292 let update = RefUpdate {
293 refname: "refs/heads/main".to_string(),
294 old: None,
295 new: Some("abc".to_string()),
296 force: true,
297 };
298 let (repo, bundle, updates) = parse_push_request(&push_request(
299 "repo",
300 b"bundle".to_vec(),
301 vec![update.clone()],
302 ))
303 .unwrap();
304 assert_eq!(repo, "repo");
305 assert_eq!(bundle, b"bundle");
306 assert_eq!(updates, vec![update]);
307 }
308
309 #[test]
310 fn push_request_uses_operations_field() {
311 let update = RefUpdate {
312 refname: "refs/heads/main".to_string(),
313 old: Some("old".to_string()),
314 new: Some("new".to_string()),
315 force: true,
316 };
317 let packed = push_request("repo", Vec::new(), vec![update]);
318 let value = msgpack::unpack_exact(&packed).unwrap();
319 let map = value.as_map().unwrap();
320
321 assert!(map_get_str(map, "operations").is_some());
322 assert!(map_get_str(map, "updates").is_none());
323 }
324
325 #[test]
326 fn parse_push_request_accepts_update_and_delete_operations() {
327 let request = msgpack::pack(&Value::Map(vec![
328 (Value::UInt(IDX_REPOSITORY), Value::Str("repo".into())),
329 (Value::Str("bundle".into()), Value::Bin(Vec::new())),
330 (
331 Value::Str("operations".into()),
332 Value::Array(vec![
333 Value::Map(vec![
334 (
335 Value::Str("action".into()),
336 Value::Str(ACTION_UPDATE_REF.into()),
337 ),
338 (
339 Value::Str("ref".into()),
340 Value::Str("refs/heads/main".into()),
341 ),
342 (Value::Str("sha".into()), Value::Str("new".into())),
343 (Value::Str("force".into()), Value::Bool(true)),
344 ]),
345 Value::Map(vec![
346 (
347 Value::Str("action".into()),
348 Value::Str(ACTION_DELETE_REF.into()),
349 ),
350 (
351 Value::Str("ref".into()),
352 Value::Str("refs/heads/old".into()),
353 ),
354 ]),
355 ]),
356 ),
357 ]));
358
359 let (_, _, updates) = parse_push_request(&request).unwrap();
360
361 assert_eq!(
362 updates,
363 vec![
364 RefUpdate {
365 refname: "refs/heads/main".into(),
366 old: None,
367 new: Some("new".into()),
368 force: true
369 },
370 RefUpdate {
371 refname: "refs/heads/old".into(),
372 old: None,
373 new: None,
374 force: false
375 }
376 ]
377 );
378 }
379
380 #[test]
381 fn parse_push_request_keeps_legacy_updates_compatibility() {
382 let update = RefUpdate {
383 refname: "refs/heads/main".into(),
384 old: Some("old".into()),
385 new: Some("new".into()),
386 force: false,
387 };
388 let request = msgpack::pack(&Value::Map(vec![
389 (Value::UInt(IDX_REPOSITORY), Value::Str("repo".into())),
390 (Value::Str("bundle".into()), Value::Bin(Vec::new())),
391 (
392 Value::Str("updates".into()),
393 Value::Array(vec![update_to_value(update.clone())]),
394 ),
395 ]));
396
397 let (_, _, updates) = parse_push_request(&request).unwrap();
398
399 assert_eq!(updates, vec![update]);
400 }
401
402 #[test]
403 fn status_response_is_status_byte_plus_payload() {
404 assert_eq!(status_bytes(RES_OK, b"ok"), b"\0ok");
405 }
406}