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";
12
13pub const RES_OK: u8 = 0x00;
14pub const RES_DISALLOWED: u8 = 0x01;
15pub const RES_INVALID_REQ: u8 = 0x02;
16pub const RES_NOT_FOUND: u8 = 0x03;
17pub const RES_REMOTE_FAIL: u8 = 0xff;
18
19pub const IDX_REPOSITORY: u64 = 0x00;
20pub const IDX_RESULT_CODE: u64 = 0x01;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct RefUpdate {
24 pub refname: String,
25 pub old: Option<String>,
26 pub new: Option<String>,
27 pub force: bool,
28}
29
30pub fn status_bytes(code: u8, body: impl AsRef<[u8]>) -> Vec<u8> {
31 let mut out = Vec::with_capacity(1 + body.as_ref().len());
32 out.push(code);
33 out.extend_from_slice(body.as_ref());
34 out
35}
36
37pub fn metadata_status(code: u8) -> Vec<u8> {
38 msgpack::pack(&Value::Map(vec![(
39 Value::UInt(IDX_RESULT_CODE),
40 Value::UInt(code as u64),
41 )]))
42}
43
44pub fn repository_request(repository: &str) -> Vec<u8> {
45 msgpack::pack(&Value::Map(vec![(
46 Value::UInt(IDX_REPOSITORY),
47 Value::Str(repository.to_string()),
48 )]))
49}
50
51pub fn repository_from_request(data: &[u8]) -> Result<String> {
52 let value =
53 msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
54 let map = value
55 .as_map()
56 .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
57 map_get(map, IDX_REPOSITORY)
58 .and_then(Value::as_str)
59 .map(ToOwned::to_owned)
60 .ok_or_else(|| Error::msg("request missing repository"))
61}
62
63pub fn fetch_request(repository: &str, have: &[String]) -> Vec<u8> {
64 msgpack::pack(&Value::Map(vec![
65 (
66 Value::UInt(IDX_REPOSITORY),
67 Value::Str(repository.to_string()),
68 ),
69 (
70 Value::Str("have".to_string()),
71 Value::Array(have.iter().map(|v| Value::Str(v.clone())).collect()),
72 ),
73 ]))
74}
75
76pub fn parse_fetch_request(data: &[u8]) -> Result<(String, Vec<String>)> {
77 let value =
78 msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
79 let map = value
80 .as_map()
81 .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
82 let repo = map_get(map, IDX_REPOSITORY)
83 .and_then(Value::as_str)
84 .map(ToOwned::to_owned)
85 .ok_or_else(|| Error::msg("request missing repository"))?;
86 let have = map_get_str(map, "have")
87 .and_then(Value::as_array)
88 .map(|arr| {
89 arr.iter()
90 .filter_map(Value::as_str)
91 .map(ToOwned::to_owned)
92 .collect()
93 })
94 .unwrap_or_default();
95 Ok((repo, have))
96}
97
98pub fn push_request(repository: &str, bundle: Vec<u8>, updates: Vec<RefUpdate>) -> Vec<u8> {
99 msgpack::pack(&Value::Map(vec![
100 (
101 Value::UInt(IDX_REPOSITORY),
102 Value::Str(repository.to_string()),
103 ),
104 (Value::Str("bundle".to_string()), Value::Bin(bundle)),
105 (
106 Value::Str("updates".to_string()),
107 Value::Array(updates.into_iter().map(update_to_value).collect()),
108 ),
109 ]))
110}
111
112pub fn parse_push_request(data: &[u8]) -> Result<(String, Vec<u8>, Vec<RefUpdate>)> {
113 let value =
114 msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
115 let map = value
116 .as_map()
117 .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
118 let repo = map_get(map, IDX_REPOSITORY)
119 .and_then(Value::as_str)
120 .map(ToOwned::to_owned)
121 .ok_or_else(|| Error::msg("request missing repository"))?;
122 let bundle = map_get_str(map, "bundle")
123 .and_then(Value::as_bin)
124 .map(ToOwned::to_owned)
125 .unwrap_or_default();
126 let updates = map_get_str(map, "updates")
127 .and_then(Value::as_array)
128 .map(|arr| arr.iter().map(value_to_update).collect::<Result<Vec<_>>>())
129 .transpose()?
130 .unwrap_or_default();
131 Ok((repo, bundle, updates))
132}
133
134pub fn response_bin(data: &[u8]) -> Result<Vec<u8>> {
135 match msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid response: {e}")))? {
136 Value::Bin(bytes) => Ok(bytes),
137 other => Err(Error::msg(format!(
138 "expected binary response, got {other:?}"
139 ))),
140 }
141}
142
143fn update_to_value(update: RefUpdate) -> Value {
144 let mut items = vec![
145 (Value::Str("ref".to_string()), Value::Str(update.refname)),
146 (Value::Str("force".to_string()), Value::Bool(update.force)),
147 ];
148 items.push((
149 Value::Str("old".to_string()),
150 update.old.map(Value::Str).unwrap_or(Value::Nil),
151 ));
152 items.push((
153 Value::Str("new".to_string()),
154 update.new.map(Value::Str).unwrap_or(Value::Nil),
155 ));
156 Value::Map(items)
157}
158
159fn value_to_update(value: &Value) -> Result<RefUpdate> {
160 let map = value
161 .as_map()
162 .ok_or_else(|| Error::msg("update must be a map"))?;
163 let refname = map_get_str(map, "ref")
164 .and_then(Value::as_str)
165 .map(ToOwned::to_owned)
166 .ok_or_else(|| Error::msg("update missing ref"))?;
167 let old = map_get_str(map, "old")
168 .and_then(Value::as_str)
169 .map(ToOwned::to_owned);
170 let new = map_get_str(map, "new")
171 .and_then(Value::as_str)
172 .map(ToOwned::to_owned);
173 let force = map_get_str(map, "force")
174 .and_then(Value::as_bool)
175 .unwrap_or(false);
176 Ok(RefUpdate {
177 refname,
178 old,
179 new,
180 force,
181 })
182}
183
184fn map_get<'a>(map: &'a [(Value, Value)], key: u64) -> Option<&'a Value> {
185 map.iter().find_map(|(k, v)| {
186 if matches!(k, Value::UInt(v) if *v == key) {
187 Some(v)
188 } else {
189 None
190 }
191 })
192}
193
194fn map_get_str<'a>(map: &'a [(Value, Value)], key: &str) -> Option<&'a Value> {
195 map.iter().find_map(|(k, v)| match k {
196 Value::Str(s) if s == key => Some(v),
197 _ => None,
198 })
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn repository_roundtrip_uses_upstream_index() {
207 assert_eq!(
208 repository_from_request(&repository_request("group/repo")).unwrap(),
209 "group/repo"
210 );
211 }
212
213 #[test]
214 fn push_roundtrip_preserves_updates() {
215 let update = RefUpdate {
216 refname: "refs/heads/main".to_string(),
217 old: None,
218 new: Some("abc".to_string()),
219 force: true,
220 };
221 let (repo, bundle, updates) = parse_push_request(&push_request(
222 "repo",
223 b"bundle".to_vec(),
224 vec![update.clone()],
225 ))
226 .unwrap();
227 assert_eq!(repo, "repo");
228 assert_eq!(bundle, b"bundle");
229 assert_eq!(updates, vec![update]);
230 }
231
232 #[test]
233 fn status_response_is_status_byte_plus_payload() {
234 assert_eq!(status_bytes(RES_OK, b"ok"), b"\0ok");
235 }
236}