Skip to main content

suture_protocol/
lib.rs

1//! Suture Protocol — wire format for client-server communication.
2//!
3//! Defines the request/response types used by the Suture Hub for
4//! push, pull, authentication, and repository management operations.
5//! All types are serializable via `serde` for JSON transport.
6
7use 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    /// Optional Ed25519 signature (64 bytes, base64-encoded).
78    /// Required when the hub has authorized keys configured.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub signature: Option<Vec<u8>>,
81    /// Client's known state of branches at time of push.
82    /// Used for fast-forward validation on the hub.
83    /// Optional for backward compatibility.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub known_branches: Option<Vec<BranchProto>>,
86    /// If true, skip fast-forward validation on push.
87    #[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    /// Limit the number of patches returned from each branch tip.
103    /// None = full history, Some(n) = last n patches per branch.
104    #[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
149/// Build canonical bytes for push request signing.
150/// Format: repo_id \0 patch_count \0 (each patch: id \0 op \0 author \0 msg \0 timestamp \0) ... branch_count \0 (each: name \0 target \0) ...
151pub 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}