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