1use crate::{
5 object::{Action, ActionId, ContentHash, State, Tree},
6 store::{
7 CompressionConfig, Result,
8 compression::{compress, decompress, is_compressed},
9 },
10};
11
12pub fn encode_blob_content(content: &[u8], config: &CompressionConfig) -> Result<Vec<u8>> {
13 Ok(compress(content, config)?.unwrap_or_else(|| content.to_vec()))
14}
15
16pub fn decode_blob_content(data: &[u8]) -> Result<Vec<u8>> {
17 if is_compressed(data) {
18 Ok(decompress(data)?)
19 } else {
20 Ok(data.to_vec())
21 }
22}
23
24pub fn encode_tree(tree: &Tree, config: &CompressionConfig) -> Result<(ContentHash, Vec<u8>)> {
25 let hash = tree.hash();
26 let serialized = rmp_serde::to_vec(tree)?;
27 let data = compress(&serialized, config)?.unwrap_or(serialized);
28 Ok((hash, data))
29}
30
31pub fn decode_tree(data: &[u8]) -> Result<Tree> {
32 let decoded = decode_body(data)?;
33 Ok(rmp_serde::from_slice(&decoded)?)
34}
35
36pub fn encode_state(state: &State, config: &CompressionConfig) -> Result<Vec<u8>> {
37 let serialized = rmp_serde::to_vec(state)?;
38 Ok(compress(&serialized, config)?.unwrap_or(serialized))
39}
40
41pub fn decode_state(data: &[u8]) -> Result<State> {
42 let decoded = decode_body(data)?;
43 Ok(rmp_serde::from_slice(&decoded)?)
44}
45
46pub fn encode_action(
47 action: &mut Action,
48 config: &CompressionConfig,
49) -> Result<(ActionId, Vec<u8>)> {
50 let id = action.id();
51 let serialized = rmp_serde::to_vec(action)?;
52 let data = compress(&serialized, config)?.unwrap_or(serialized);
53 Ok((id, data))
54}
55
56pub fn decode_action(data: &[u8]) -> Result<Action> {
57 let decoded = decode_body(data)?;
58 Ok(rmp_serde::from_slice(&decoded)?)
59}
60
61fn decode_body(data: &[u8]) -> Result<Vec<u8>> {
62 if is_compressed(data) {
63 Ok(decompress(data)?)
64 } else {
65 Ok(data.to_vec())
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::object::{Attribution, ChangeId, Operation, Principal, TreeEntry};
73
74 #[test]
75 fn encode_decode_blob_content_matches_old_recipe() {
76 let content = b"codec blob content ".repeat(64);
77 for config in compression_configs() {
78 let expected = old_encode_raw(&content, &config).unwrap();
79 let encoded = encode_blob_content(&content, &config).unwrap();
80 assert_eq!(encoded, expected);
81 assert_eq!(decode_blob_content(&encoded).unwrap(), content);
82 }
83 }
84
85 #[test]
86 fn encode_decode_tree_matches_old_recipe() {
87 let blob_hash = ContentHash::compute(b"codec-tree-blob");
88 let tree =
89 Tree::from_entries(vec![TreeEntry::file("file.txt", blob_hash, false).unwrap()]);
90 for config in compression_configs() {
91 let serialized = rmp_serde::to_vec(&tree).unwrap();
92 let expected = old_encode_raw(&serialized, &config).unwrap();
93 let (hash, encoded) = encode_tree(&tree, &config).unwrap();
94 assert_eq!(hash, tree.hash());
95 assert_eq!(encoded, expected);
96 assert_eq!(decode_tree(&encoded).unwrap(), tree);
97 }
98 }
99
100 #[test]
101 fn encode_decode_state_matches_old_recipe() {
102 let attribution = sample_attribution();
103 let state = State::new(ContentHash::compute(b"codec-tree"), vec![], attribution)
104 .with_intent("codec state");
105 for config in compression_configs() {
106 let serialized = rmp_serde::to_vec(&state).unwrap();
107 let expected = old_encode_raw(&serialized, &config).unwrap();
108 let encoded = encode_state(&state, &config).unwrap();
109 assert_eq!(encoded, expected);
110 assert_eq!(decode_state(&encoded).unwrap(), state);
111 }
112 }
113
114 #[test]
115 fn encode_decode_action_matches_old_recipe() {
116 let attribution = sample_attribution();
117 for config in compression_configs() {
118 let mut action = Action::new(
119 None,
120 ChangeId::generate(),
121 Operation::Snapshot,
122 "codec action",
123 attribution.clone(),
124 );
125 let id = action.id();
126 let serialized = rmp_serde::to_vec(&action).unwrap();
127 let expected = old_encode_raw(&serialized, &config).unwrap();
128
129 let (encoded_id, encoded) = encode_action(&mut action, &config).unwrap();
130 assert_eq!(encoded_id, id);
131 assert_eq!(encoded, expected);
132
133 let decoded = decode_action(&encoded).unwrap();
134 assert_eq!(decoded.compute_id(), id);
135 assert_eq!(decoded.from_state, action.from_state);
136 assert_eq!(decoded.to_state, action.to_state);
137 assert_eq!(decoded.operation, action.operation);
138 assert_eq!(decoded.description, action.description);
139 assert_eq!(decoded.semantic_changes, action.semantic_changes);
140 assert_eq!(decoded.attribution, action.attribution);
141 assert_eq!(decoded.timestamp, action.timestamp);
142 }
143 }
144
145 fn old_encode_raw(data: &[u8], config: &CompressionConfig) -> Result<Vec<u8>> {
146 Ok(compress(data, config)?.unwrap_or_else(|| data.to_vec()))
147 }
148
149 fn compression_configs() -> Vec<CompressionConfig> {
150 #[cfg(feature = "zstd")]
151 {
152 vec![
153 CompressionConfig::default(),
154 CompressionConfig::disabled(),
155 CompressionConfig {
156 enabled: true,
157 level: 9,
158 min_size: 0,
159 max_delta_size: CompressionConfig::default().max_delta_size,
160 },
161 ]
162 }
163 #[cfg(not(feature = "zstd"))]
164 {
165 vec![CompressionConfig::default(), CompressionConfig::disabled()]
166 }
167 }
168
169 fn sample_attribution() -> Attribution {
170 Attribution::human(Principal::new("Codec Test", "codec@example.com"))
171 }
172}