1use crate::mkit::common::v1::{RefEntry, RefExpectation};
24use crate::mkit::rpc::v1::Error as RpcError;
25use crate::mkit::rpc::v1::ErrorCode;
26use crate::mkit::rpc::v1::signer::{SignerFrame, signer_frame};
27use crate::mkit::rpc::v1::ssh::{SshFrame, ssh_frame};
28
29use mkit_core::hash::Hash;
30use mkit_core::protocol::{TransportError, TransportResult};
31use mkit_core::refs::{Ref, RefWriteCondition, validate_ref_name_grammar};
32
33#[inline]
41pub fn signer_error_frame(code: ErrorCode, message: impl Into<String>) -> SignerFrame {
42 SignerFrame {
43 body: Some(signer_frame::Body::Error(Box::new(
44 RpcError::default()
45 .with_code(code)
46 .with_message(message)
47 .with_details(Vec::new()),
48 ))),
49 ..Default::default()
50 }
51}
52
53#[inline]
56pub fn ssh_error_frame(code: ErrorCode, message: impl Into<String>) -> SshFrame {
57 SshFrame {
58 body: Some(ssh_frame::Body::Error(Box::new(
59 RpcError::default()
60 .with_code(code)
61 .with_message(message)
62 .with_details(Vec::new()),
63 ))),
64 ..Default::default()
65 }
66}
67
68pub const MAX_REF_NAME: usize = mkit_core::refs::MAX_REF_NAME_BYTES;
77
78pub const CHUNK_DATA_MAX: usize = 800 * 1024;
82
83#[must_use]
89pub fn cond_to_wire(c: RefWriteCondition) -> (Vec<u8>, RefExpectation) {
90 match c {
91 RefWriteCondition::Any => (Vec::new(), RefExpectation::Any),
92 RefWriteCondition::Missing => (Vec::new(), RefExpectation::Missing),
93 RefWriteCondition::Match(h) => (h.to_vec(), RefExpectation::Match),
94 }
95}
96
97#[must_use]
122pub fn map_update_ref_error(
123 e: RpcError,
124 condition: RefWriteCondition,
125 transport: &str,
126) -> TransportError {
127 let is_invalid_request = e.code.is_some_and(|c| c == ErrorCode::InvalidRequest);
128 let has_cas_details = e.details.as_deref().is_some_and(|d| !d.is_empty());
129 if is_invalid_request && !matches!(condition, RefWriteCondition::Any) && has_cas_details {
130 TransportError::RefConflict
131 } else {
132 rpc_error_to_transport(e, transport)
133 }
134}
135
136#[must_use]
150pub fn rpc_error_to_transport(e: RpcError, transport: &str) -> TransportError {
151 if e.code.is_some_and(|c| c == ErrorCode::KeyNotFound) {
152 return TransportError::PackNotFound;
153 }
154 if e.code.is_some_and(|c| c == ErrorCode::UserDeclined) {
155 return TransportError::AccessDenied;
156 }
157 if e.code.is_none_or(|c| c == ErrorCode::Unspecified) {
158 return TransportError::ProtocolError;
159 }
160 let msg = e.message.unwrap_or_default();
161 if msg.is_empty() {
162 TransportError::RemoteError(format!("{transport} server returned an unspecified error"))
163 } else {
164 TransportError::RemoteError(msg)
165 }
166}
167
168#[must_use]
172pub fn unexpected_frame(
173 transport: &str,
174 want: &str,
175 got: Option<ssh_frame::Body>,
176) -> TransportError {
177 TransportError::RemoteError(format!(
178 "{transport} server returned {} when {want} was expected",
179 body_name(&got),
180 ))
181}
182
183#[must_use]
187pub fn body_name(b: &Option<ssh_frame::Body>) -> &'static str {
188 use ssh_frame::Body;
189 match b {
190 Some(Body::Hello(_)) => "hello",
191 Some(Body::HelloResponse(_)) => "hello_response",
192 Some(Body::Error(_)) => "error",
193 Some(Body::Close(_)) => "close",
194 Some(Body::ListRefs(_)) => "list_refs",
195 Some(Body::ListRefsResponse(_)) => "list_refs_response",
196 Some(Body::ReadRef(_)) => "read_ref",
197 Some(Body::ReadRefResponse(_)) => "read_ref_response",
198 Some(Body::UpdateRef(_)) => "update_ref",
199 Some(Body::UpdateRefResponse(_)) => "update_ref_response",
200 Some(Body::PackExists(_)) => "pack_exists",
201 Some(Body::PackExistsResponse(_)) => "pack_exists_response",
202 Some(Body::UploadPack(_)) => "upload_pack",
203 Some(Body::UploadPackResponse(_)) => "upload_pack_response",
204 Some(Body::DownloadPack(_)) => "download_pack",
205 Some(Body::DownloadPackHeader(_)) => "download_pack_header",
206 Some(Body::PackChunk(_)) => "pack_chunk",
207 None => "(empty body)",
208 }
209}
210
211pub fn ref_entry_to_ref(e: RefEntry) -> TransportResult<Ref> {
223 let name = e.name.unwrap_or_default();
224 if !validate_ref_name_grammar(&name) {
225 return Err(TransportError::InvalidRef(name));
226 }
227 let oid = e.object_id.unwrap_or_default();
228 if oid.len() != 32 {
229 return Err(TransportError::InvalidResponse);
230 }
231 let mut h: Hash = [0u8; 32];
232 h.copy_from_slice(&oid);
233 Ok(Ref {
234 name,
235 hash: Some(h),
236 })
237}
238
239pub fn list_response_refs(prefix: &str, entries: Vec<RefEntry>) -> TransportResult<Vec<Ref>> {
251 let trimmed = prefix.trim_end_matches('/');
252 let prefix_len = if trimmed.is_empty() {
253 0
254 } else {
255 trimmed.len() + 1
256 };
257 entries
258 .into_iter()
259 .filter(|e| prefix_len + e.name.as_ref().map_or(0, String::len) <= MAX_REF_NAME)
260 .map(ref_entry_to_ref)
261 .collect()
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use buffa::Message;
268
269 #[test]
272 fn list_response_refs_skips_names_over_the_bound() {
273 let entry = |name: &str| {
274 RefEntry::default()
275 .with_name(name)
276 .with_object_id(vec![7u8; 32])
277 };
278 let long = vec!["x".repeat(100); 6].join("/");
279 assert_eq!(long.len(), 605);
280 let fits = "y".repeat(MAX_REF_NAME - "refs/heads/".len());
281 let over = format!("{fits}y");
282 let listed = list_response_refs(
283 "refs/heads/",
284 vec![entry("main"), entry(&long), entry(&fits), entry(&over)],
285 )
286 .unwrap();
287 let names: Vec<_> = listed.iter().map(|r| r.name.as_str()).collect();
288 assert_eq!(names, ["main", fits.as_str()]);
289 let full = format!("refs/heads/{long}");
291 let listed = list_response_refs("", vec![entry(&full), entry("refs/heads/a")]).unwrap();
292 assert_eq!(listed.len(), 1);
293 assert!(list_response_refs("", vec![entry("bad name")]).is_err());
295 }
296
297 #[test]
298 fn signer_error_frame_round_trips() {
299 let frame = signer_error_frame(ErrorCode::InvalidRequest, "bad");
300 let bytes = frame.encode_to_vec();
301 let decoded = SignerFrame::decode(&mut &bytes[..]).expect("decode");
302 let Some(signer_frame::Body::Error(e)) = decoded.body else {
303 panic!("expected Error body");
304 };
305 assert_eq!(e.code, Some(ErrorCode::InvalidRequest.into()));
306 assert_eq!(e.message.as_deref(), Some("bad"));
307 assert_eq!(e.details.as_deref(), Some(&[][..]));
308 }
309
310 #[test]
311 fn ssh_error_frame_round_trips() {
312 let frame = ssh_error_frame(ErrorCode::KeyNotFound, "missing");
313 let bytes = frame.encode_to_vec();
314 let decoded = SshFrame::decode(&mut &bytes[..]).expect("decode");
315 let Some(ssh_frame::Body::Error(e)) = decoded.body else {
316 panic!("expected Error body");
317 };
318 assert_eq!(e.code, Some(ErrorCode::KeyNotFound.into()));
319 assert_eq!(e.message.as_deref(), Some("missing"));
320 }
321
322 #[test]
323 fn cond_to_wire_encodes_any() {
324 let (id, exp) = cond_to_wire(RefWriteCondition::Any);
325 assert!(id.is_empty());
326 assert_eq!(exp, RefExpectation::Any);
327 }
328
329 #[test]
330 fn cond_to_wire_encodes_missing() {
331 let (id, exp) = cond_to_wire(RefWriteCondition::Missing);
332 assert!(id.is_empty());
333 assert_eq!(exp, RefExpectation::Missing);
334 }
335
336 #[test]
337 fn cond_to_wire_encodes_match() {
338 let h: Hash = [7u8; 32];
339 let (id, exp) = cond_to_wire(RefWriteCondition::Match(h));
340 assert_eq!(id, h.to_vec());
341 assert_eq!(exp, RefExpectation::Match);
342 }
343
344 #[test]
345 fn rpc_error_to_transport_maps_known_codes() {
346 let not_found = RpcError::default()
347 .with_code(ErrorCode::KeyNotFound)
348 .with_message("missing pack");
349 assert!(matches!(
350 rpc_error_to_transport(not_found, "ssh"),
351 TransportError::PackNotFound
352 ));
353
354 let declined = RpcError::default().with_code(ErrorCode::UserDeclined);
355 assert!(matches!(
356 rpc_error_to_transport(declined, "ssh"),
357 TransportError::AccessDenied
358 ));
359 }
360
361 #[test]
362 fn rpc_error_to_transport_falls_back_with_label() {
363 let empty = RpcError::default().with_code(ErrorCode::InvalidRequest);
364 match rpc_error_to_transport(empty, "enc") {
365 TransportError::RemoteError(msg) => assert!(msg.contains("enc server")),
366 other => panic!("unexpected variant: {other:?}"),
367 }
368 }
369
370 #[test]
371 fn rpc_error_to_transport_treats_zero_code_as_protocol_error() {
372 let zero_with_message = RpcError::default()
377 .with_code(ErrorCode::Unspecified)
378 .with_message("something went wrong");
379 assert!(matches!(
380 rpc_error_to_transport(zero_with_message, "ssh"),
381 TransportError::ProtocolError
382 ));
383 }
384
385 #[test]
386 fn rpc_error_to_transport_treats_absent_code_as_protocol_error() {
387 let absent = RpcError::default().with_message("no code at all");
391 assert_eq!(absent.code, None);
392 assert!(matches!(
393 rpc_error_to_transport(absent, "enc"),
394 TransportError::ProtocolError
395 ));
396 }
397
398 #[test]
399 fn rpc_error_to_transport_distinguishes_unspecified_from_mapped_internal_error() {
400 let internal = RpcError::default()
404 .with_code(ErrorCode::Internal)
405 .with_message("boom");
406 match rpc_error_to_transport(internal, "ssh") {
407 TransportError::RemoteError(msg) => assert_eq!(msg, "boom"),
408 other => panic!("unexpected variant: {other:?}"),
409 }
410 }
411
412 #[test]
413 fn unexpected_frame_includes_labels() {
414 let err = unexpected_frame("ssh", "UpdateRefResponse", None);
415 match err {
416 TransportError::RemoteError(msg) => {
417 assert!(msg.contains("ssh server"));
418 assert!(msg.contains("UpdateRefResponse"));
419 assert!(msg.contains("(empty body)"));
420 }
421 other => panic!("unexpected variant: {other:?}"),
422 }
423 }
424
425 #[test]
426 fn ref_entry_to_ref_rejects_bad_oid_length() {
427 let bad = RefEntry::default()
428 .with_name("refs/heads/main")
429 .with_object_id(vec![1, 2, 3]);
430 assert!(matches!(
431 ref_entry_to_ref(bad),
432 Err(TransportError::InvalidResponse)
433 ));
434 }
435
436 #[test]
437 fn ref_entry_to_ref_rejects_invalid_name() {
438 let bad = RefEntry::default()
439 .with_name("not a ref")
440 .with_object_id(vec![0u8; 32]);
441 assert!(matches!(
442 ref_entry_to_ref(bad),
443 Err(TransportError::InvalidRef(_))
444 ));
445 }
446
447 #[test]
448 fn ref_entry_to_ref_accepts_valid() {
449 let oid = vec![9u8; 32];
450 let ok = RefEntry::default()
451 .with_name("refs/heads/main")
452 .with_object_id(oid.clone());
453 let r = ref_entry_to_ref(ok).expect("valid");
454 assert_eq!(r.name, "refs/heads/main");
455 assert_eq!(r.hash.unwrap().to_vec(), oid);
456 }
457}