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 Ok(rmp_serde::from_slice(&decoded)?)
57}
58
59pub fn encode_action(
60 action: &mut Action,
61 config: &CompressionConfig,
62) -> Result<(ActionId, Vec<u8>)> {
63 let id = action.id();
64 let serialized = rmp_serde::to_vec(action)?;
65 let data = compress(&serialized, config)?.unwrap_or(serialized);
66 Ok((id, data))
67}
68
69pub fn decode_action(data: &[u8]) -> Result<Action> {
70 let decoded = decode_body(data)?;
71 Ok(rmp_serde::from_slice(&decoded)?)
72}
73
74fn decode_body(data: &[u8]) -> Result<Vec<u8>> {
75 if is_compressed(data) {
76 Ok(decompress(data)?)
77 } else {
78 Ok(data.to_vec())
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::object::{Attribution, ChangeId, Operation, Principal, TreeEntry};
86
87 #[test]
88 fn encode_decode_blob_content_matches_old_recipe() {
89 let content = b"codec blob content ".repeat(64);
90 for config in compression_configs() {
91 let expected = old_encode_raw(&content, &config).unwrap();
92 let encoded = encode_blob_content(&content, &config).unwrap();
93 assert_eq!(encoded, expected);
94 assert_eq!(decode_blob_content(&encoded).unwrap(), content);
95 }
96 }
97
98 #[test]
99 fn encode_decode_tree_matches_old_recipe() {
100 let blob_hash = ContentHash::compute(b"codec-tree-blob");
101 let tree = Tree::from_entries(vec![TreeEntry::file("file.txt", blob_hash, false).unwrap()]);
102 for config in compression_configs() {
103 let serialized = rmp_serde::to_vec(&tree).unwrap();
104 let expected = old_encode_raw(&serialized, &config).unwrap();
105 let (hash, encoded) = encode_tree(&tree, &config).unwrap();
106 assert_eq!(hash, tree.hash());
107 assert_eq!(encoded, expected);
108 assert_eq!(decode_tree(&encoded).unwrap(), tree);
109 }
110 }
111
112 #[test]
113 fn encode_decode_state_matches_old_recipe() {
114 let attribution = sample_attribution();
115 let state = State::new(ContentHash::compute(b"codec-tree"), vec![], attribution)
116 .with_intent("codec state");
117 for config in compression_configs() {
118 let serialized = rmp_serde::to_vec(&state).unwrap();
119 let expected = old_encode_raw(&serialized, &config).unwrap();
120 let encoded = encode_state(&state, &config).unwrap();
121 assert_eq!(encoded, expected);
122 assert_eq!(decode_state(&encoded).unwrap(), state);
123 }
124 }
125
126 #[test]
127 fn encode_decode_action_matches_old_recipe() {
128 let attribution = sample_attribution();
129 for config in compression_configs() {
130 let mut action = Action::new(
131 None,
132 ChangeId::generate(),
133 Operation::Snapshot,
134 "codec action",
135 attribution.clone(),
136 );
137 let id = action.id();
138 let serialized = rmp_serde::to_vec(&action).unwrap();
139 let expected = old_encode_raw(&serialized, &config).unwrap();
140
141 let (encoded_id, encoded) = encode_action(&mut action, &config).unwrap();
142 assert_eq!(encoded_id, id);
143 assert_eq!(encoded, expected);
144
145 let decoded = decode_action(&encoded).unwrap();
146 assert_eq!(decoded.compute_id(), id);
147 assert_eq!(decoded.from_state, action.from_state);
148 assert_eq!(decoded.to_state, action.to_state);
149 assert_eq!(decoded.operation, action.operation);
150 assert_eq!(decoded.description, action.description);
151 assert_eq!(decoded.semantic_changes, action.semantic_changes);
152 assert_eq!(decoded.attribution, action.attribution);
153 assert_eq!(decoded.timestamp, action.timestamp);
154 }
155 }
156
157 fn old_encode_raw(data: &[u8], config: &CompressionConfig) -> Result<Vec<u8>> {
158 Ok(compress(data, config)?.unwrap_or_else(|| data.to_vec()))
159 }
160
161 fn compression_configs() -> Vec<CompressionConfig> {
162 #[cfg(feature = "zstd")]
163 {
164 vec![
165 CompressionConfig::default(),
166 CompressionConfig::disabled(),
167 CompressionConfig {
168 enabled: true,
169 level: 9,
170 min_size: 0,
171 max_delta_size: CompressionConfig::default().max_delta_size,
172 },
173 ]
174 }
175 #[cfg(not(feature = "zstd"))]
176 {
177 vec![CompressionConfig::default(), CompressionConfig::disabled()]
178 }
179 }
180
181 fn sample_attribution() -> Attribution {
182 Attribution::human(Principal::new("Codec Test", "codec@example.com"))
183 }
184}