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