1#![allow(clippy::collapsible_match)]
3use serde::{Deserialize, Serialize};
10
11pub const PROTOCOL_VERSION: u32 = 1;
12pub const PROTOCOL_VERSION_V2: u32 = 2;
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct HandshakeRequest {
16 pub client_version: u32,
17 pub client_name: String,
18}
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
21pub struct HandshakeResponse {
22 pub server_version: u32,
23 pub server_name: String,
24 pub compatible: bool,
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub enum AuthMethod {
29 None,
30 Signature {
31 public_key: String,
32 signature: String,
33 },
34 Token(String),
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct AuthRequest {
39 pub method: AuthMethod,
40 pub timestamp: u64,
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct HashProto {
45 pub value: String,
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize)]
49pub struct PatchProto {
50 pub id: HashProto,
51 pub operation_type: String,
52 pub touch_set: Vec<String>,
53 pub target_path: Option<String>,
54 pub payload: String,
55 pub parent_ids: Vec<HashProto>,
56 pub author: String,
57 pub message: String,
58 pub timestamp: u64,
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct BranchProto {
63 pub name: String,
64 pub target_id: HashProto,
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct BlobRef {
69 pub hash: HashProto,
70 pub data: String,
71 #[serde(default)]
72 pub truncated: bool,
73}
74
75#[derive(Debug, Serialize, Deserialize)]
76pub struct PushRequest {
77 pub repo_id: String,
78 pub patches: Vec<PatchProto>,
79 pub branches: Vec<BranchProto>,
80 pub blobs: Vec<BlobRef>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub signature: Option<Vec<u8>>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub known_branches: Option<Vec<BranchProto>>,
90 #[serde(default)]
92 pub force: bool,
93}
94
95#[derive(Debug, Serialize, Deserialize)]
96pub struct PushResponse {
97 pub success: bool,
98 pub error: Option<String>,
99 pub existing_patches: Vec<HashProto>,
100}
101
102#[derive(Debug, Serialize, Deserialize)]
103pub struct PullRequest {
104 pub repo_id: String,
105 pub known_branches: Vec<BranchProto>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub max_depth: Option<u32>,
110}
111
112#[derive(Debug, Serialize, Deserialize)]
113pub struct PullResponse {
114 pub success: bool,
115 pub error: Option<String>,
116 pub patches: Vec<PatchProto>,
117 pub branches: Vec<BranchProto>,
118 pub blobs: Vec<BlobRef>,
119}
120
121#[derive(Debug, Serialize, Deserialize)]
122pub struct ListReposResponse {
123 pub repo_ids: Vec<String>,
124}
125
126#[derive(Debug, Serialize, Deserialize)]
127pub struct RepoInfoResponse {
128 pub repo_id: String,
129 pub patch_count: u64,
130 pub branches: Vec<BranchProto>,
131 pub success: bool,
132 pub error: Option<String>,
133}
134
135pub fn hash_to_hex(h: &HashProto) -> String {
136 h.value.clone()
137}
138
139pub fn compress(data: &[u8]) -> Result<Vec<u8>, String> {
140 zstd::encode_all(data, 3).map_err(|e| format!("zstd compression failed: {e}"))
141}
142
143pub fn decompress(data: &[u8]) -> Result<Vec<u8>, String> {
144 zstd::decode_all(data).map_err(|e| format!("zstd decompression failed: {e}"))
145}
146
147pub fn hex_to_hash(hex: &str) -> HashProto {
148 HashProto {
149 value: hex.to_string(),
150 }
151}
152
153pub fn canonical_push_bytes(req: &PushRequest) -> Vec<u8> {
156 let mut buf = Vec::new();
157
158 buf.extend_from_slice(req.repo_id.as_bytes());
159 buf.push(0);
160
161 buf.extend_from_slice(&(req.patches.len() as u64).to_le_bytes());
162 for patch in &req.patches {
163 buf.extend_from_slice(patch.id.value.as_bytes());
164 buf.push(0);
165 buf.extend_from_slice(patch.operation_type.as_bytes());
166 buf.push(0);
167 buf.extend_from_slice(patch.author.as_bytes());
168 buf.push(0);
169 buf.extend_from_slice(patch.message.as_bytes());
170 buf.push(0);
171 buf.extend_from_slice(&patch.timestamp.to_le_bytes());
172 buf.push(0);
173 }
174
175 buf.extend_from_slice(&(req.branches.len() as u64).to_le_bytes());
176 for branch in &req.branches {
177 buf.extend_from_slice(branch.name.as_bytes());
178 buf.push(0);
179 buf.extend_from_slice(branch.target_id.value.as_bytes());
180 buf.push(0);
181 }
182
183 buf
184}
185
186#[derive(Clone, Debug, Serialize, Deserialize)]
187pub enum DeltaEncoding {
188 BinaryPatch,
189 FullBlob,
190}
191
192#[derive(Clone, Debug, Serialize, Deserialize)]
193pub struct BlobDelta {
194 pub base_hash: HashProto,
195 pub target_hash: HashProto,
196 pub encoding: DeltaEncoding,
197 pub delta_data: String,
198}
199
200#[derive(Clone, Debug, Serialize, Deserialize)]
201pub struct ClientCapabilities {
202 pub supports_delta: bool,
203 pub supports_compression: bool,
204 pub max_blob_size: u64,
205}
206
207#[derive(Clone, Debug, Serialize, Deserialize)]
208pub struct ServerCapabilities {
209 pub supports_delta: bool,
210 pub supports_compression: bool,
211 pub max_blob_size: u64,
212 pub protocol_versions: Vec<u32>,
213}
214
215#[derive(Debug, Serialize, Deserialize)]
216pub struct PullRequestV2 {
217 pub repo_id: String,
218 pub known_branches: Vec<BranchProto>,
219 pub max_depth: Option<u32>,
220 pub known_blob_hashes: Vec<HashProto>,
221 pub capabilities: ClientCapabilities,
222}
223
224#[derive(Debug, Serialize, Deserialize)]
225pub struct PullResponseV2 {
226 pub success: bool,
227 pub error: Option<String>,
228 pub patches: Vec<PatchProto>,
229 pub branches: Vec<BranchProto>,
230 pub blobs: Vec<BlobRef>,
231 pub deltas: Vec<BlobDelta>,
232 pub protocol_version: u32,
233}
234
235#[derive(Debug, Serialize, Deserialize)]
236pub struct PushRequestV2 {
237 pub repo_id: String,
238 pub patches: Vec<PatchProto>,
239 pub branches: Vec<BranchProto>,
240 pub blobs: Vec<BlobRef>,
241 pub deltas: Vec<BlobDelta>,
242 pub signature: Option<Vec<u8>>,
243 pub known_branches: Option<Vec<BranchProto>>,
244 pub force: bool,
245}
246
247#[derive(Clone, Debug, Serialize, Deserialize)]
248pub struct HandshakeRequestV2 {
249 pub client_version: u32,
250 pub client_name: String,
251 pub capabilities: ClientCapabilities,
252}
253
254#[derive(Clone, Debug, Serialize, Deserialize)]
255pub struct HandshakeResponseV2 {
256 pub server_version: u32,
257 pub server_name: String,
258 pub compatible: bool,
259 pub server_capabilities: ServerCapabilities,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct LfsBatchRequest {
266 pub repo_id: String,
267 pub objects: Vec<LfsObjectRef>,
268 pub operation: LfsOperation,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(rename_all = "lowercase")]
273pub enum LfsOperation {
274 Upload,
275 Download,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct LfsObjectRef {
280 pub oid: String,
281 pub size: u64,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct LfsBatchResponse {
286 pub objects: Vec<LfsObjectAction>,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub token: Option<String>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct LfsObjectAction {
293 pub oid: String,
294 pub size: u64,
295 pub action: LfsAction,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub href: Option<String>,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub header: Option<std::collections::HashMap<String, String>>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
303#[serde(rename_all = "lowercase")]
304pub enum LfsAction {
305 None,
306 Upload,
307 Download,
308 Error,
309}
310
311pub fn compute_delta(base: &[u8], target: &[u8]) -> (Vec<u8>, Vec<u8>) {
312 let prefix_len = base
313 .iter()
314 .zip(target.iter())
315 .take_while(|(a, b)| a == b)
316 .count();
317
318 let max_suffix_base = base.len().saturating_sub(prefix_len);
319 let max_suffix_target = target.len().saturating_sub(prefix_len);
320 let suffix_len = base[prefix_len..]
321 .iter()
322 .rev()
323 .zip(target[prefix_len..].iter().rev())
324 .take_while(|(a, b)| a == b)
325 .count()
326 .min(max_suffix_base)
327 .min(max_suffix_target);
328
329 let changed_start = prefix_len;
330 let changed_end_target = target.len().saturating_sub(suffix_len);
331 let changed = &target[changed_start..changed_end_target];
332
333 if changed.len() < target.len() {
334 let mut delta = Vec::new();
335 delta.push(0x01);
336 delta.extend_from_slice(&(prefix_len as u64).to_le_bytes());
337 delta.extend_from_slice(&(suffix_len as u64).to_le_bytes());
338 delta.extend_from_slice(&(target.len() as u64).to_le_bytes());
339 delta.extend_from_slice(changed);
340 (base.to_vec(), delta)
341 } else {
342 let mut full = vec![0x00];
343 full.extend_from_slice(target);
344 (base.to_vec(), full)
345 }
346}
347
348pub fn apply_delta(base: &[u8], delta: &[u8]) -> Vec<u8> {
349 if delta.is_empty() {
350 return Vec::new();
351 }
352 match delta[0] {
353 0x00 => delta[1..].to_vec(),
354 0x01 => {
355 if delta.len() < 25 {
356 return delta.to_vec();
357 }
358 let prefix_len = u64::from_le_bytes(delta[1..9].try_into().unwrap_or([0; 8])) as usize;
359 let suffix_len = u64::from_le_bytes(delta[9..17].try_into().unwrap_or([0; 8])) as usize;
360 let total_len = u64::from_le_bytes(delta[17..25].try_into().unwrap_or([0; 8])) as usize;
361 let changed = &delta[25..];
362
363 let mut result = Vec::with_capacity(total_len);
364 result.extend_from_slice(&base[..prefix_len.min(base.len())]);
365 result.extend_from_slice(changed);
366 result.extend_from_slice(&base[base.len().saturating_sub(suffix_len)..]);
367 result
368 }
369 _ => delta.to_vec(),
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 fn roundtrip<T: Serialize + for<'de> Deserialize<'de>>(val: &T) -> T {
378 let json = serde_json::to_string(val).expect("serialize");
379 serde_json::from_str(&json).expect("deserialize")
380 }
381
382 fn make_hash(hex: &str) -> HashProto {
383 HashProto {
384 value: hex.to_string(),
385 }
386 }
387
388 fn make_patch(id: &str, op: &str, parents: &[&str]) -> PatchProto {
389 PatchProto {
390 id: make_hash(id),
391 operation_type: op.to_string(),
392 touch_set: vec![format!("file_{id}")],
393 target_path: Some(format!("file_{id}")),
394 payload: String::new(),
395 parent_ids: parents.iter().map(|p| make_hash(p)).collect(),
396 author: "alice".to_string(),
397 message: format!("patch {id}"),
398 timestamp: 1000,
399 }
400 }
401
402 fn make_branch(name: &str, target: &str) -> BranchProto {
403 BranchProto {
404 name: name.to_string(),
405 target_id: make_hash(target),
406 }
407 }
408
409 #[test]
410 fn test_handshake_roundtrip() {
411 let req = HandshakeRequest {
412 client_version: 1,
413 client_name: "test".to_string(),
414 };
415 let rt: HandshakeRequest = roundtrip(&req);
416 assert_eq!(rt.client_version, 1);
417 assert_eq!(rt.client_name, "test");
418
419 let resp = HandshakeResponse {
420 server_version: 1,
421 server_name: "hub".to_string(),
422 compatible: true,
423 };
424 let rt: HandshakeResponse = roundtrip(&resp);
425 assert!(rt.compatible);
426 }
427
428 #[test]
429 fn test_auth_method_roundtrip() {
430 let methods = vec![
431 AuthMethod::None,
432 AuthMethod::Signature {
433 public_key: "pk".to_string(),
434 signature: "sig".to_string(),
435 },
436 AuthMethod::Token("tok".to_string()),
437 ];
438 for m in &methods {
439 let rt: AuthMethod = roundtrip(m);
440 match (m, &rt) {
441 (AuthMethod::None, AuthMethod::None) => {}
442 (
443 AuthMethod::Signature {
444 public_key: a,
445 signature: b,
446 },
447 AuthMethod::Signature {
448 public_key: c,
449 signature: d,
450 },
451 ) => {
452 assert_eq!(a, c);
453 assert_eq!(b, d);
454 }
455 (AuthMethod::Token(a), AuthMethod::Token(b)) => assert_eq!(a, b),
456 _ => panic!("auth method mismatch"),
457 }
458 }
459 }
460
461 #[test]
462 fn test_patch_proto_roundtrip() {
463 let p = make_patch("a".repeat(64).as_str(), "Create", &[]);
464 let rt: PatchProto = roundtrip(&p);
465 assert_eq!(rt.operation_type, "Create");
466 assert_eq!(rt.touch_set.len(), 1);
467 assert!(rt.target_path.is_some());
468 assert!(rt.parent_ids.is_empty());
469 assert_eq!(rt.author, "alice");
470 }
471
472 #[test]
473 fn test_patch_proto_with_parents() {
474 let parent = "b".repeat(64);
475 let p = make_patch("a".repeat(64).as_str(), "Modify", &[&parent]);
476 let rt: PatchProto = roundtrip(&p);
477 assert_eq!(rt.parent_ids.len(), 1);
478 assert_eq!(hash_to_hex(&rt.parent_ids[0]), parent);
479 }
480
481 #[test]
482 fn test_push_request_roundtrip() {
483 let req = PushRequest {
484 repo_id: "my-repo".to_string(),
485 patches: vec![make_patch("a".repeat(64).as_str(), "Create", &[])],
486 branches: vec![make_branch("main", "a".repeat(64).as_str())],
487 blobs: vec![BlobRef {
488 hash: make_hash("deadbeef"),
489 data: "aGVsbG8=".to_string(),
490 truncated: false,
491 }],
492 signature: Some(vec![1u8; 64]),
493 known_branches: Some(vec![make_branch("main", "prev".repeat(32).as_str())]),
494 force: true,
495 };
496 let rt: PushRequest = roundtrip(&req);
497 assert_eq!(rt.repo_id, "my-repo");
498 assert_eq!(rt.patches.len(), 1);
499 assert_eq!(rt.branches.len(), 1);
500 assert_eq!(rt.blobs.len(), 1);
501 assert!(rt.signature.is_some());
502 assert!(rt.known_branches.is_some());
503 assert!(rt.force);
504 }
505
506 #[test]
507 fn test_push_request_defaults() {
508 let req = PushRequest {
509 repo_id: "r".to_string(),
510 patches: vec![],
511 branches: vec![],
512 blobs: vec![],
513 signature: None,
514 known_branches: None,
515 force: false,
516 };
517 let json = serde_json::to_string(&req).unwrap();
518 let rt: PushRequest = serde_json::from_str(&json).unwrap();
519 assert!(rt.signature.is_none());
520 assert!(rt.known_branches.is_none());
521 assert!(!rt.force);
522 }
523
524 #[test]
525 fn test_pull_request_roundtrip() {
526 let req = PullRequest {
527 repo_id: "r".to_string(),
528 known_branches: vec![make_branch("main", "a".repeat(32).as_str())],
529 max_depth: Some(10),
530 };
531 let rt: PullRequest = roundtrip(&req);
532 assert_eq!(rt.max_depth, Some(10));
533
534 let req2 = PullRequest {
535 repo_id: "r".to_string(),
536 known_branches: vec![],
537 max_depth: None,
538 };
539 let rt2: PullRequest = roundtrip(&req2);
540 assert!(rt2.max_depth.is_none());
541 }
542
543 #[test]
544 fn test_pull_response_roundtrip() {
545 let resp = PullResponse {
546 success: true,
547 error: None,
548 patches: vec![make_patch("a".repeat(64).as_str(), "Create", &[])],
549 branches: vec![make_branch("main", "a".repeat(64).as_str())],
550 blobs: vec![BlobRef {
551 hash: make_hash("abc"),
552 data: "dGVzdA==".to_string(),
553 truncated: false,
554 }],
555 };
556
557 let rt: PullResponse = roundtrip(&resp);
558 assert!(rt.success);
559 assert_eq!(rt.patches.len(), 1);
560 assert_eq!(rt.blobs.len(), 1);
561 }
562
563 #[test]
564 fn test_pull_response_error() {
565 let resp = PullResponse {
566 success: false,
567 error: Some("not found".to_string()),
568 patches: vec![],
569 branches: vec![],
570 blobs: vec![],
571 };
572 let rt: PullResponse = roundtrip(&resp);
573 assert!(!rt.success);
574 assert_eq!(rt.error, Some("not found".to_string()));
575 }
576
577 #[test]
578 fn test_blob_ref_roundtrip() {
579 let blob = BlobRef {
580 hash: make_hash("cafebabe"),
581 data: "SGVsbG8gV29ybGQ=".to_string(),
582 truncated: false,
583 };
584 let rt: BlobRef = roundtrip(&blob);
585 assert_eq!(rt.data, "SGVsbG8gV29ybGQ=");
586 }
587
588 #[test]
589 fn test_hash_helpers() {
590 let h = hex_to_hash("abcdef1234");
591 assert_eq!(hash_to_hex(&h), "abcdef1234");
592 }
593
594 #[test]
595 fn test_canonical_push_bytes_deterministic() {
596 let req = PushRequest {
597 repo_id: "test".to_string(),
598 patches: vec![make_patch("a".repeat(64).as_str(), "Create", &[])],
599 branches: vec![make_branch("main", "a".repeat(64).as_str())],
600 blobs: vec![],
601 signature: None,
602 known_branches: None,
603 force: false,
604 };
605 let b1 = canonical_push_bytes(&req);
606 let b2 = canonical_push_bytes(&req);
607 assert_eq!(b1, b2);
608 }
609
610 #[test]
611 fn test_canonical_push_bytes_different_repos() {
612 let make_req = |repo: &str| PushRequest {
613 repo_id: repo.to_string(),
614 patches: vec![],
615 branches: vec![],
616 blobs: vec![],
617 signature: None,
618 known_branches: None,
619 force: false,
620 };
621 let b1 = canonical_push_bytes(&make_req("repo-a"));
622 let b2 = canonical_push_bytes(&make_req("repo-b"));
623 assert_ne!(b1, b2);
624 }
625
626 #[test]
627 fn test_repo_info_response_roundtrip() {
628 let resp = RepoInfoResponse {
629 repo_id: "my-repo".to_string(),
630 patch_count: 42,
631 branches: vec![make_branch("main", "a".repeat(32).as_str())],
632 success: true,
633 error: None,
634 };
635 let rt: RepoInfoResponse = roundtrip(&resp);
636 assert_eq!(rt.patch_count, 42);
637 assert!(rt.success);
638
639 let err = RepoInfoResponse {
640 repo_id: "x".to_string(),
641 patch_count: 0,
642 branches: vec![],
643 success: false,
644 error: Some("not found".to_string()),
645 };
646 let rt2: RepoInfoResponse = roundtrip(&err);
647 assert!(!rt2.success);
648 assert_eq!(rt2.error, Some("not found".to_string()));
649 }
650
651 #[test]
652 fn test_list_repos_response_roundtrip() {
653 let resp = ListReposResponse {
654 repo_ids: vec!["a".to_string(), "b".to_string()],
655 };
656 let rt: ListReposResponse = roundtrip(&resp);
657 assert_eq!(rt.repo_ids, vec!["a", "b"]);
658 }
659
660 #[test]
661 fn test_push_response_roundtrip() {
662 let resp = PushResponse {
663 success: true,
664 error: None,
665 existing_patches: vec![make_hash("abc"), make_hash("def")],
666 };
667 let rt: PushResponse = roundtrip(&resp);
668 assert_eq!(rt.existing_patches.len(), 2);
669 }
670
671 #[test]
672 fn test_delta_roundtrip() {
673 let base = b"Hello, World!";
674 let target = b"Hello, Rust!";
675 let (_base_copy, delta) = compute_delta(base, target);
676 let result = apply_delta(base, &delta);
677 assert_eq!(result, target);
678 }
679
680 #[test]
681 fn test_delta_no_change() {
682 let base = b"identical data here";
683 let target = b"identical data here";
684 let (_base_copy, delta) = compute_delta(base, target);
685 assert!(delta.len() < target.len() + 25);
686 let result = apply_delta(base, &delta);
687 assert_eq!(result, target);
688 }
689
690 #[test]
691 fn test_delta_completely_different() {
692 let base = b"AAAA";
693 let target = b"BBBB";
694 let (_base_copy, delta) = compute_delta(base, target);
695 let result = apply_delta(base, &delta);
696 assert_eq!(result, target);
697 }
698
699 #[test]
700 fn test_pull_request_v2_roundtrip() {
701 let req = PullRequestV2 {
702 repo_id: "my-repo".to_string(),
703 known_branches: vec![make_branch("main", "a".repeat(32).as_str())],
704 max_depth: Some(10),
705 known_blob_hashes: vec![make_hash("deadbeef")],
706 capabilities: ClientCapabilities {
707 supports_delta: true,
708 supports_compression: true,
709 max_blob_size: 1024 * 1024,
710 },
711 };
712 let rt: PullRequestV2 = roundtrip(&req);
713 assert_eq!(rt.repo_id, "my-repo");
714 assert_eq!(rt.max_depth, Some(10));
715 assert!(rt.capabilities.supports_delta);
716 assert_eq!(rt.known_blob_hashes.len(), 1);
717 }
718
719 #[test]
720 fn test_handshake_v2_roundtrip() {
721 let req = HandshakeRequestV2 {
722 client_version: 2,
723 client_name: "suture-cli".to_string(),
724 capabilities: ClientCapabilities {
725 supports_delta: true,
726 supports_compression: false,
727 max_blob_size: 512 * 1024,
728 },
729 };
730 let rt: HandshakeRequestV2 = roundtrip(&req);
731 assert_eq!(rt.client_version, 2);
732 assert!(rt.capabilities.supports_delta);
733 assert!(!rt.capabilities.supports_compression);
734
735 let resp = HandshakeResponseV2 {
736 server_version: 2,
737 server_name: "suture-hub".to_string(),
738 compatible: true,
739 server_capabilities: ServerCapabilities {
740 supports_delta: true,
741 supports_compression: true,
742 max_blob_size: 10 * 1024 * 1024,
743 protocol_versions: vec![1, 2],
744 },
745 };
746 let rt: HandshakeResponseV2 = roundtrip(&resp);
747 assert!(rt.compatible);
748 assert_eq!(rt.server_capabilities.protocol_versions, vec![1, 2]);
749 }
750
751 #[test]
752 fn test_client_capabilities_roundtrip() {
753 let caps = ClientCapabilities {
754 supports_delta: false,
755 supports_compression: true,
756 max_blob_size: 999,
757 };
758 let rt: ClientCapabilities = roundtrip(&caps);
759 assert!(!rt.supports_delta);
760 assert!(rt.supports_compression);
761 assert_eq!(rt.max_blob_size, 999);
762 }
763
764 #[test]
765 fn test_blob_delta_roundtrip() {
766 let delta = BlobDelta {
767 base_hash: make_hash("aaa"),
768 target_hash: make_hash("bbb"),
769 encoding: DeltaEncoding::BinaryPatch,
770 delta_data: "ZGF0YQ==".to_string(),
771 };
772 let rt: BlobDelta = roundtrip(&delta);
773 assert_eq!(hash_to_hex(&rt.base_hash), "aaa");
774 assert_eq!(hash_to_hex(&rt.target_hash), "bbb");
775 assert!(matches!(rt.encoding, DeltaEncoding::BinaryPatch));
776 assert_eq!(rt.delta_data, "ZGF0YQ==");
777
778 let full = BlobDelta {
779 base_hash: make_hash("aaa"),
780 target_hash: make_hash("bbb"),
781 encoding: DeltaEncoding::FullBlob,
782 delta_data: "Ynl0ZXM=".to_string(),
783 };
784 let rt: BlobDelta = roundtrip(&full);
785 assert!(matches!(rt.encoding, DeltaEncoding::FullBlob));
786 }
787
788 fn assert_delta_roundtrip(base: &[u8], target: &[u8]) {
789 let (_base_copy, delta) = compute_delta(base, target);
790 let result = apply_delta(base, &delta);
791 assert_eq!(
792 result,
793 target,
794 "delta roundtrip failed: base_len={}, target_len={}",
795 base.len(),
796 target.len()
797 );
798 }
799
800 #[test]
801 fn test_delta_small_1_to_10_bytes() {
802 for len in 1..=10u32 {
803 let base: Vec<u8> = (0..len).map(|i| (i * 7) as u8).collect();
804 let target: Vec<u8> = (0..len).map(|i| (i * 13 + 3) as u8).collect();
805 assert_delta_roundtrip(&base, &target);
806 }
807 }
808
809 #[test]
810 fn test_delta_medium_100_to_1000_bytes() {
811 for len in [100, 200, 500, 1000] {
812 let base: Vec<u8> = (0..len).map(|i| (i % 256) as u8).collect();
813 let mut target = base.clone();
814 for i in len / 3..len * 2 / 3 {
815 target[i] = target[i].wrapping_add(1);
816 }
817 assert_delta_roundtrip(&base, &target);
818 }
819 }
820
821 #[test]
822 fn test_delta_large_10kb_plus() {
823 for len in [10_240, 50_000, 100_000] {
824 let base: Vec<u8> = (0..len).map(|i| ((i * 7 + 13) % 251) as u8).collect();
825 let mut target = base.clone();
826 target[len / 2] = 0xFF;
827 assert_delta_roundtrip(&base, &target);
828 }
829 }
830
831 #[test]
832 fn test_delta_empty_base() {
833 assert_delta_roundtrip(b"", b"some target data that is reasonably long enough");
834 }
835
836 #[test]
837 fn test_delta_empty_target() {
838 assert_delta_roundtrip(b"some base data that is reasonably long enough too", b"");
839 }
840
841 #[test]
842 fn test_delta_both_empty() {
843 assert_delta_roundtrip(b"", b"");
844 }
845
846 #[test]
847 fn test_delta_identical() {
848 let data = b"The quick brown fox jumps over the lazy dog";
849 assert_delta_roundtrip(data, data);
850 }
851
852 #[test]
853 fn test_delta_base_is_prefix_of_target() {
854 assert_delta_roundtrip(
855 b"shared prefix data",
856 b"shared prefix data and extra suffix content here",
857 );
858 }
859
860 #[test]
861 fn test_delta_target_is_prefix_of_base() {
862 assert_delta_roundtrip(
863 b"shared prefix data and extra suffix content here",
864 b"shared prefix data",
865 );
866 }
867
868 #[test]
869 fn test_delta_completely_different_same_length() {
870 let base: Vec<u8> = (0..100).map(|i| (i * 3) as u8).collect();
871 let target: Vec<u8> = (0..100).map(|i| (i * 7 + 100) as u8).collect();
872 assert_delta_roundtrip(&base, &target);
873 }
874
875 #[test]
876 fn test_delta_completely_different_different_lengths() {
877 assert_delta_roundtrip(&vec![0xAA; 50], &vec![0xBB; 200]);
878 }
879
880 #[test]
881 fn test_delta_common_middle_section() {
882 let middle = b"COMMON_MIDDLE_SECTION_THAT_IS_LONG_ENOUGH";
883 let mut base = Vec::new();
884 base.extend_from_slice(b"DIFFERENT_START_XXXXXX_");
885 base.extend_from_slice(middle);
886 base.extend_from_slice(b"_DIFFERENT_END_XXXXXX");
887
888 let mut target = Vec::new();
889 target.extend_from_slice(b"CHANGED_PREFIX_");
890 target.extend_from_slice(middle);
891 target.extend_from_slice(b"_CHANGED_SUFFIX_DATA");
892
893 assert_delta_roundtrip(&base, &target);
894 }
895
896 #[test]
897 fn test_delta_single_byte_change() {
898 let base = vec![0u8; 1000];
899 let mut target = base.clone();
900 target[500] = 1;
901 assert_delta_roundtrip(&base, &target);
902 }
903
904 #[test]
905 fn test_delta_single_byte_base_and_target() {
906 assert_delta_roundtrip(b"A", b"B");
907 }
908
909 #[test]
910 fn test_delta_single_byte_identical() {
911 assert_delta_roundtrip(b"X", b"X");
912 }
913
914 #[test]
915 fn test_delta_prefix_overlap_large() {
916 let prefix: Vec<u8> = (0..60u8).collect();
917 let mut base = prefix.clone();
918 base.extend_from_slice(&vec![0x00; 60]);
919 let mut target = prefix.clone();
920 target.extend_from_slice(&(60..120u8).collect::<Vec<_>>());
921 assert_delta_roundtrip(&base, &target);
922 }
923
924 #[test]
925 fn test_delta_suffix_overlap_large() {
926 let suffix: Vec<u8> = (60..120u8).collect();
927 let mut base = vec![0x00; 60];
928 base.extend_from_slice(&suffix);
929 let mut target = (0..60u8).collect::<Vec<_>>();
930 target.extend_from_slice(&suffix);
931 assert_delta_roundtrip(&base, &target);
932 }
933
934 #[test]
935 fn test_compress_decompress_empty() {
936 let compressed = compress(b"").unwrap();
937 assert_eq!(decompress(&compressed).unwrap(), b"");
938 }
939
940 #[test]
941 fn test_compress_decompress_small_1_to_100() {
942 for len in 1..=100u32 {
943 let data: Vec<u8> = (0..len).map(|i| (i % 256) as u8).collect();
944 let compressed = compress(&data).unwrap();
945 assert_eq!(
946 decompress(&compressed).unwrap(),
947 data,
948 "failed for len={len}"
949 );
950 }
951 }
952
953 #[test]
954 fn test_compress_decompress_medium() {
955 for len in [100, 500, 1000, 5000, 10_000] {
956 let data: Vec<u8> = (0..len).map(|i| (i % 256) as u8).collect();
957 let compressed = compress(&data).unwrap();
958 assert_eq!(
959 decompress(&compressed).unwrap(),
960 data,
961 "failed for len={len}"
962 );
963 }
964 }
965
966 #[test]
967 fn test_compress_decompress_large() {
968 let len = 200_000usize;
969 let data: Vec<u8> = (0..len).map(|i| (i % 256) as u8).collect();
970 let compressed = compress(&data).unwrap();
971 assert!(
972 compressed.len() < data.len(),
973 "compressed should be smaller for repetitive data"
974 );
975 assert_eq!(decompress(&compressed).unwrap(), data);
976 }
977
978 #[test]
979 fn test_compress_decompress_incompressible() {
980 let mut data = Vec::with_capacity(100_000);
981 let mut hasher = std::collections::hash_map::DefaultHasher::new();
982 for i in 0..100_000 {
983 use std::hash::{Hash, Hasher};
984 i.hash(&mut hasher);
985 data.push((hasher.finish() % 256) as u8);
986 hasher = std::collections::hash_map::DefaultHasher::new();
987 }
988 let compressed = compress(&data).unwrap();
989 assert_eq!(decompress(&compressed).unwrap(), data);
990 }
991
992 #[test]
993 fn test_compress_decompress_highly_compressible() {
994 let data = vec![0xAAu8; 500_000];
995 let compressed = compress(&data).unwrap();
996 assert!(
997 compressed.len() < 100,
998 "highly compressible data should be tiny"
999 );
1000 assert_eq!(decompress(&compressed).unwrap(), data);
1001 }
1002
1003 #[test]
1004 fn test_decompress_invalid_data_fails() {
1005 assert!(decompress(b"not valid zstd data").is_err());
1006 }
1007
1008 #[test]
1009 fn test_decompress_empty_input_fails() {
1010 assert!(decompress(b"").is_err());
1011 }
1012
1013 #[test]
1014 fn test_server_capabilities_roundtrip() {
1015 let caps = ServerCapabilities {
1016 supports_delta: true,
1017 supports_compression: true,
1018 max_blob_size: 50 * 1024 * 1024,
1019 protocol_versions: vec![1, 2],
1020 };
1021 let rt: ServerCapabilities = roundtrip(&caps);
1022 assert!(rt.supports_delta);
1023 assert!(rt.supports_compression);
1024 assert_eq!(rt.max_blob_size, 50 * 1024 * 1024);
1025 assert_eq!(rt.protocol_versions, vec![1, 2]);
1026 }
1027
1028 #[test]
1029 fn test_capability_version_matching() {
1030 let server_caps = ServerCapabilities {
1031 supports_delta: true,
1032 supports_compression: false,
1033 max_blob_size: 1024 * 1024,
1034 protocol_versions: vec![1, 2],
1035 };
1036 let client_caps = ClientCapabilities {
1037 supports_delta: true,
1038 supports_compression: true,
1039 max_blob_size: 1024 * 1024,
1040 };
1041 assert!(server_caps.protocol_versions.contains(&PROTOCOL_VERSION));
1042 assert!(server_caps.supports_delta && client_caps.supports_delta);
1043 assert!(!(server_caps.supports_compression && client_caps.supports_compression));
1044 assert!(client_caps.max_blob_size <= server_caps.max_blob_size);
1045 }
1046
1047 #[test]
1048 fn test_capability_version_mismatch() {
1049 let server_caps = ServerCapabilities {
1050 supports_delta: false,
1051 supports_compression: false,
1052 max_blob_size: 1024,
1053 protocol_versions: vec![1],
1054 };
1055 let client_caps = ClientCapabilities {
1056 supports_delta: true,
1057 supports_compression: true,
1058 max_blob_size: 10 * 1024 * 1024,
1059 };
1060 assert!(!server_caps.protocol_versions.contains(&PROTOCOL_VERSION_V2));
1061 assert!(!server_caps.supports_delta || !client_caps.supports_delta);
1062 assert!(client_caps.max_blob_size > server_caps.max_blob_size);
1063 }
1064
1065 #[test]
1066 fn test_push_request_v2_roundtrip() {
1067 let req = PushRequestV2 {
1068 repo_id: "my-repo".to_string(),
1069 patches: vec![make_patch("a".repeat(64).as_str(), "Create", &[])],
1070 branches: vec![make_branch("main", "a".repeat(64).as_str())],
1071 blobs: vec![BlobRef {
1072 hash: make_hash("abc"),
1073 data: "dGVzdA==".to_string(),
1074 truncated: false,
1075 }],
1076 deltas: vec![BlobDelta {
1077 base_hash: make_hash("base"),
1078 target_hash: make_hash("target"),
1079 encoding: DeltaEncoding::BinaryPatch,
1080 delta_data: "ZGVsdGE=".to_string(),
1081 }],
1082 signature: None,
1083 known_branches: None,
1084 force: false,
1085 };
1086 let rt: PushRequestV2 = roundtrip(&req);
1087 assert_eq!(rt.repo_id, "my-repo");
1088 assert_eq!(rt.deltas.len(), 1);
1089 assert_eq!(rt.patches.len(), 1);
1090 }
1091
1092 #[test]
1093 fn test_pull_response_v2_roundtrip() {
1094 let resp = PullResponseV2 {
1095 success: true,
1096 error: None,
1097 patches: vec![make_patch("a".repeat(64).as_str(), "Create", &[])],
1098 branches: vec![make_branch("main", "a".repeat(64).as_str())],
1099 blobs: vec![BlobRef {
1100 hash: make_hash("abc"),
1101 data: "dGVzdA==".to_string(),
1102 truncated: false,
1103 }],
1104 deltas: vec![BlobDelta {
1105 base_hash: make_hash("old"),
1106 target_hash: make_hash("new"),
1107 encoding: DeltaEncoding::FullBlob,
1108 delta_data: "ZnVsbA==".to_string(),
1109 }],
1110 protocol_version: 2,
1111 };
1112 let rt: PullResponseV2 = roundtrip(&resp);
1113 assert!(rt.success);
1114 assert_eq!(rt.protocol_version, 2);
1115 assert_eq!(rt.deltas.len(), 1);
1116 assert_eq!(rt.blobs.len(), 1);
1117 }
1118
1119 #[test]
1120 fn test_protocol_versions() {
1121 assert_eq!(PROTOCOL_VERSION, 1);
1122 assert_eq!(PROTOCOL_VERSION_V2, 2);
1123 assert_ne!(PROTOCOL_VERSION, PROTOCOL_VERSION_V2);
1124 }
1125
1126 #[test]
1127 fn test_auth_request_roundtrip() {
1128 let req = AuthRequest {
1129 method: AuthMethod::Token("secret".to_string()),
1130 timestamp: 12345,
1131 };
1132 let rt: AuthRequest = roundtrip(&req);
1133 assert_eq!(rt.timestamp, 12345);
1134 match rt.method {
1135 AuthMethod::Token(t) => assert_eq!(t, "secret"),
1136 _ => panic!("expected Token auth method"),
1137 }
1138 }
1139}