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.extend_from_slice(&(prefix_len as u64).to_le_bytes());
283        delta.extend_from_slice(&(suffix_len as u64).to_le_bytes());
284        delta.extend_from_slice(&(target.len() as u64).to_le_bytes());
285        delta.extend_from_slice(changed);
286        (base.to_vec(), delta)
287    } else {
288        (base.to_vec(), target.to_vec())
289    }
290}
291
292pub fn apply_delta(base: &[u8], delta: &[u8]) -> Vec<u8> {
293    if delta.len() < 24 {
294        return delta.to_vec();
295    }
296    let prefix_len = u64::from_le_bytes(delta[0..8].try_into().unwrap_or([0; 8])) as usize;
297    let suffix_len = u64::from_le_bytes(delta[8..16].try_into().unwrap_or([0; 8])) as usize;
298    let total_len = u64::from_le_bytes(delta[16..24].try_into().unwrap_or([0; 8])) as usize;
299    let changed = &delta[24..];
300
301    let mut result = Vec::with_capacity(total_len);
302    result.extend_from_slice(&base[..prefix_len.min(base.len())]);
303    result.extend_from_slice(changed);
304    result.extend_from_slice(&base[base.len().saturating_sub(suffix_len)..]);
305    result
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    fn roundtrip<T: Serialize + for<'de> Deserialize<'de>>(val: &T) -> T {
313        let json = serde_json::to_string(val).expect("serialize");
314        serde_json::from_str(&json).expect("deserialize")
315    }
316
317    fn make_hash(hex: &str) -> HashProto {
318        HashProto {
319            value: hex.to_string(),
320        }
321    }
322
323    fn make_patch(id: &str, op: &str, parents: &[&str]) -> PatchProto {
324        PatchProto {
325            id: make_hash(id),
326            operation_type: op.to_string(),
327            touch_set: vec![format!("file_{id}")],
328            target_path: Some(format!("file_{id}")),
329            payload: String::new(),
330            parent_ids: parents.iter().map(|p| make_hash(p)).collect(),
331            author: "alice".to_string(),
332            message: format!("patch {id}"),
333            timestamp: 1000,
334        }
335    }
336
337    fn make_branch(name: &str, target: &str) -> BranchProto {
338        BranchProto {
339            name: name.to_string(),
340            target_id: make_hash(target),
341        }
342    }
343
344    #[test]
345    fn test_handshake_roundtrip() {
346        let req = HandshakeRequest {
347            client_version: 1,
348            client_name: "test".to_string(),
349        };
350        let rt: HandshakeRequest = roundtrip(&req);
351        assert_eq!(rt.client_version, 1);
352        assert_eq!(rt.client_name, "test");
353
354        let resp = HandshakeResponse {
355            server_version: 1,
356            server_name: "hub".to_string(),
357            compatible: true,
358        };
359        let rt: HandshakeResponse = roundtrip(&resp);
360        assert!(rt.compatible);
361    }
362
363    #[test]
364    fn test_auth_method_roundtrip() {
365        let methods = vec![
366            AuthMethod::None,
367            AuthMethod::Signature {
368                public_key: "pk".to_string(),
369                signature: "sig".to_string(),
370            },
371            AuthMethod::Token("tok".to_string()),
372        ];
373        for m in &methods {
374            let rt: AuthMethod = roundtrip(m);
375            match (m, &rt) {
376                (AuthMethod::None, AuthMethod::None) => {}
377                (
378                    AuthMethod::Signature {
379                        public_key: a,
380                        signature: b,
381                    },
382                    AuthMethod::Signature {
383                        public_key: c,
384                        signature: d,
385                    },
386                ) => {
387                    assert_eq!(a, c);
388                    assert_eq!(b, d);
389                }
390                (AuthMethod::Token(a), AuthMethod::Token(b)) => assert_eq!(a, b),
391                _ => panic!("auth method mismatch"),
392            }
393        }
394    }
395
396    #[test]
397    fn test_patch_proto_roundtrip() {
398        let p = make_patch("a".repeat(64).as_str(), "Create", &[]);
399        let rt: PatchProto = roundtrip(&p);
400        assert_eq!(rt.operation_type, "Create");
401        assert_eq!(rt.touch_set.len(), 1);
402        assert!(rt.target_path.is_some());
403        assert!(rt.parent_ids.is_empty());
404        assert_eq!(rt.author, "alice");
405    }
406
407    #[test]
408    fn test_patch_proto_with_parents() {
409        let parent = "b".repeat(64);
410        let p = make_patch("a".repeat(64).as_str(), "Modify", &[&parent]);
411        let rt: PatchProto = roundtrip(&p);
412        assert_eq!(rt.parent_ids.len(), 1);
413        assert_eq!(hash_to_hex(&rt.parent_ids[0]), parent);
414    }
415
416    #[test]
417    fn test_push_request_roundtrip() {
418        let req = PushRequest {
419            repo_id: "my-repo".to_string(),
420            patches: vec![make_patch("a".repeat(64).as_str(), "Create", &[])],
421            branches: vec![make_branch("main", "a".repeat(64).as_str())],
422            blobs: vec![BlobRef {
423                hash: make_hash("deadbeef"),
424                data: "aGVsbG8=".to_string(),
425            }],
426            signature: Some(vec![1u8; 64]),
427            known_branches: Some(vec![make_branch("main", "prev".repeat(32).as_str())]),
428            force: true,
429        };
430        let rt: PushRequest = roundtrip(&req);
431        assert_eq!(rt.repo_id, "my-repo");
432        assert_eq!(rt.patches.len(), 1);
433        assert_eq!(rt.branches.len(), 1);
434        assert_eq!(rt.blobs.len(), 1);
435        assert!(rt.signature.is_some());
436        assert!(rt.known_branches.is_some());
437        assert!(rt.force);
438    }
439
440    #[test]
441    fn test_push_request_defaults() {
442        let req = PushRequest {
443            repo_id: "r".to_string(),
444            patches: vec![],
445            branches: vec![],
446            blobs: vec![],
447            signature: None,
448            known_branches: None,
449            force: false,
450        };
451        let json = serde_json::to_string(&req).unwrap();
452        let rt: PushRequest = serde_json::from_str(&json).unwrap();
453        assert!(rt.signature.is_none());
454        assert!(rt.known_branches.is_none());
455        assert!(!rt.force);
456    }
457
458    #[test]
459    fn test_pull_request_roundtrip() {
460        let req = PullRequest {
461            repo_id: "r".to_string(),
462            known_branches: vec![make_branch("main", "a".repeat(32).as_str())],
463            max_depth: Some(10),
464        };
465        let rt: PullRequest = roundtrip(&req);
466        assert_eq!(rt.max_depth, Some(10));
467
468        let req2 = PullRequest {
469            repo_id: "r".to_string(),
470            known_branches: vec![],
471            max_depth: None,
472        };
473        let rt2: PullRequest = roundtrip(&req2);
474        assert!(rt2.max_depth.is_none());
475    }
476
477    #[test]
478    fn test_pull_response_roundtrip() {
479        let resp = PullResponse {
480            success: true,
481            error: None,
482            patches: vec![make_patch("a".repeat(64).as_str(), "Create", &[])],
483            branches: vec![make_branch("main", "a".repeat(64).as_str())],
484            blobs: vec![BlobRef {
485                hash: make_hash("abc"),
486                data: "dGVzdA==".to_string(),
487            }],
488        };
489        let rt: PullResponse = roundtrip(&resp);
490        assert!(rt.success);
491        assert_eq!(rt.patches.len(), 1);
492        assert_eq!(rt.blobs.len(), 1);
493    }
494
495    #[test]
496    fn test_pull_response_error() {
497        let resp = PullResponse {
498            success: false,
499            error: Some("not found".to_string()),
500            patches: vec![],
501            branches: vec![],
502            blobs: vec![],
503        };
504        let rt: PullResponse = roundtrip(&resp);
505        assert!(!rt.success);
506        assert_eq!(rt.error, Some("not found".to_string()));
507    }
508
509    #[test]
510    fn test_blob_ref_roundtrip() {
511        let blob = BlobRef {
512            hash: make_hash("cafebabe"),
513            data: "SGVsbG8gV29ybGQ=".to_string(),
514        };
515        let rt: BlobRef = roundtrip(&blob);
516        assert_eq!(rt.data, "SGVsbG8gV29ybGQ=");
517    }
518
519    #[test]
520    fn test_hash_helpers() {
521        let h = hex_to_hash("abcdef1234");
522        assert_eq!(hash_to_hex(&h), "abcdef1234");
523    }
524
525    #[test]
526    fn test_canonical_push_bytes_deterministic() {
527        let req = PushRequest {
528            repo_id: "test".to_string(),
529            patches: vec![make_patch("a".repeat(64).as_str(), "Create", &[])],
530            branches: vec![make_branch("main", "a".repeat(64).as_str())],
531            blobs: vec![],
532            signature: None,
533            known_branches: None,
534            force: false,
535        };
536        let b1 = canonical_push_bytes(&req);
537        let b2 = canonical_push_bytes(&req);
538        assert_eq!(b1, b2);
539    }
540
541    #[test]
542    fn test_canonical_push_bytes_different_repos() {
543        let make_req = |repo: &str| PushRequest {
544            repo_id: repo.to_string(),
545            patches: vec![],
546            branches: vec![],
547            blobs: vec![],
548            signature: None,
549            known_branches: None,
550            force: false,
551        };
552        let b1 = canonical_push_bytes(&make_req("repo-a"));
553        let b2 = canonical_push_bytes(&make_req("repo-b"));
554        assert_ne!(b1, b2);
555    }
556
557    #[test]
558    fn test_repo_info_response_roundtrip() {
559        let resp = RepoInfoResponse {
560            repo_id: "my-repo".to_string(),
561            patch_count: 42,
562            branches: vec![make_branch("main", "a".repeat(32).as_str())],
563            success: true,
564            error: None,
565        };
566        let rt: RepoInfoResponse = roundtrip(&resp);
567        assert_eq!(rt.patch_count, 42);
568        assert!(rt.success);
569
570        let err = RepoInfoResponse {
571            repo_id: "x".to_string(),
572            patch_count: 0,
573            branches: vec![],
574            success: false,
575            error: Some("not found".to_string()),
576        };
577        let rt2: RepoInfoResponse = roundtrip(&err);
578        assert!(!rt2.success);
579        assert_eq!(rt2.error, Some("not found".to_string()));
580    }
581
582    #[test]
583    fn test_list_repos_response_roundtrip() {
584        let resp = ListReposResponse {
585            repo_ids: vec!["a".to_string(), "b".to_string()],
586        };
587        let rt: ListReposResponse = roundtrip(&resp);
588        assert_eq!(rt.repo_ids, vec!["a", "b"]);
589    }
590
591    #[test]
592    fn test_push_response_roundtrip() {
593        let resp = PushResponse {
594            success: true,
595            error: None,
596            existing_patches: vec![make_hash("abc"), make_hash("def")],
597        };
598        let rt: PushResponse = roundtrip(&resp);
599        assert_eq!(rt.existing_patches.len(), 2);
600    }
601
602    #[test]
603    fn test_delta_roundtrip() {
604        let base = b"Hello, World!";
605        let target = b"Hello, Rust!";
606        let (_base_copy, delta) = compute_delta(base, target);
607        let result = apply_delta(base, &delta);
608        assert_eq!(result, target);
609    }
610
611    #[test]
612    fn test_delta_no_change() {
613        let base = b"identical data here";
614        let target = b"identical data here";
615        let (_base_copy, delta) = compute_delta(base, target);
616        assert!(delta.len() < target.len() + 24);
617        let result = apply_delta(base, &delta);
618        assert_eq!(result, target);
619    }
620
621    #[test]
622    fn test_delta_completely_different() {
623        let base = b"AAAA";
624        let target = b"BBBB";
625        let (_base_copy, delta) = compute_delta(base, target);
626        let result = apply_delta(base, &delta);
627        assert_eq!(result, target);
628    }
629
630    #[test]
631    fn test_pull_request_v2_roundtrip() {
632        let req = PullRequestV2 {
633            repo_id: "my-repo".to_string(),
634            known_branches: vec![make_branch("main", "a".repeat(32).as_str())],
635            max_depth: Some(10),
636            known_blob_hashes: vec![make_hash("deadbeef")],
637            capabilities: ClientCapabilities {
638                supports_delta: true,
639                supports_compression: true,
640                max_blob_size: 1024 * 1024,
641            },
642        };
643        let rt: PullRequestV2 = roundtrip(&req);
644        assert_eq!(rt.repo_id, "my-repo");
645        assert_eq!(rt.max_depth, Some(10));
646        assert!(rt.capabilities.supports_delta);
647        assert_eq!(rt.known_blob_hashes.len(), 1);
648    }
649
650    #[test]
651    fn test_handshake_v2_roundtrip() {
652        let req = HandshakeRequestV2 {
653            client_version: 2,
654            client_name: "suture-cli".to_string(),
655            capabilities: ClientCapabilities {
656                supports_delta: true,
657                supports_compression: false,
658                max_blob_size: 512 * 1024,
659            },
660        };
661        let rt: HandshakeRequestV2 = roundtrip(&req);
662        assert_eq!(rt.client_version, 2);
663        assert!(rt.capabilities.supports_delta);
664        assert!(!rt.capabilities.supports_compression);
665
666        let resp = HandshakeResponseV2 {
667            server_version: 2,
668            server_name: "suture-hub".to_string(),
669            compatible: true,
670            server_capabilities: ServerCapabilities {
671                supports_delta: true,
672                supports_compression: true,
673                max_blob_size: 10 * 1024 * 1024,
674                protocol_versions: vec![1, 2],
675            },
676        };
677        let rt: HandshakeResponseV2 = roundtrip(&resp);
678        assert!(rt.compatible);
679        assert_eq!(rt.server_capabilities.protocol_versions, vec![1, 2]);
680    }
681
682    #[test]
683    fn test_client_capabilities_roundtrip() {
684        let caps = ClientCapabilities {
685            supports_delta: false,
686            supports_compression: true,
687            max_blob_size: 999,
688        };
689        let rt: ClientCapabilities = roundtrip(&caps);
690        assert!(!rt.supports_delta);
691        assert!(rt.supports_compression);
692        assert_eq!(rt.max_blob_size, 999);
693    }
694
695    #[test]
696    fn test_blob_delta_roundtrip() {
697        let delta = BlobDelta {
698            base_hash: make_hash("aaa"),
699            target_hash: make_hash("bbb"),
700            encoding: DeltaEncoding::BinaryPatch,
701            delta_data: "ZGF0YQ==".to_string(),
702        };
703        let rt: BlobDelta = roundtrip(&delta);
704        assert_eq!(hash_to_hex(&rt.base_hash), "aaa");
705        assert_eq!(hash_to_hex(&rt.target_hash), "bbb");
706        assert!(matches!(rt.encoding, DeltaEncoding::BinaryPatch));
707        assert_eq!(rt.delta_data, "ZGF0YQ==");
708
709        let full = BlobDelta {
710            base_hash: make_hash("aaa"),
711            target_hash: make_hash("bbb"),
712            encoding: DeltaEncoding::FullBlob,
713            delta_data: "Ynl0ZXM=".to_string(),
714        };
715        let rt: BlobDelta = roundtrip(&full);
716        assert!(matches!(rt.encoding, DeltaEncoding::FullBlob));
717    }
718}