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