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 = Tree::from_entries(vec![TreeEntry::file("file.txt", blob_hash, false).unwrap()]);
89 for config in compression_configs() {
90 let serialized = rmp_serde::to_vec(&tree).unwrap();
91 let expected = old_encode_raw(&serialized, &config).unwrap();
92 let (hash, encoded) = encode_tree(&tree, &config).unwrap();
93 assert_eq!(hash, tree.hash());
94 assert_eq!(encoded, expected);
95 assert_eq!(decode_tree(&encoded).unwrap(), tree);
96 }
97 }
98
99 #[test]
100 fn encode_decode_state_matches_old_recipe() {
101 let attribution = sample_attribution();
102 let state = State::new(ContentHash::compute(b"codec-tree"), vec![], attribution)
103 .with_intent("codec state");
104 for config in compression_configs() {
105 let serialized = rmp_serde::to_vec(&state).unwrap();
106 let expected = old_encode_raw(&serialized, &config).unwrap();
107 let encoded = encode_state(&state, &config).unwrap();
108 assert_eq!(encoded, expected);
109 assert_eq!(decode_state(&encoded).unwrap(), state);
110 }
111 }
112
113 #[test]
114 fn encode_decode_action_matches_old_recipe() {
115 let attribution = sample_attribution();
116 for config in compression_configs() {
117 let mut action = Action::new(
118 None,
119 ChangeId::generate(),
120 Operation::Snapshot,
121 "codec action",
122 attribution.clone(),
123 );
124 let id = action.id();
125 let serialized = rmp_serde::to_vec(&action).unwrap();
126 let expected = old_encode_raw(&serialized, &config).unwrap();
127
128 let (encoded_id, encoded) = encode_action(&mut action, &config).unwrap();
129 assert_eq!(encoded_id, id);
130 assert_eq!(encoded, expected);
131
132 let decoded = decode_action(&encoded).unwrap();
133 assert_eq!(decoded.compute_id(), id);
134 assert_eq!(decoded.from_state, action.from_state);
135 assert_eq!(decoded.to_state, action.to_state);
136 assert_eq!(decoded.operation, action.operation);
137 assert_eq!(decoded.description, action.description);
138 assert_eq!(decoded.semantic_changes, action.semantic_changes);
139 assert_eq!(decoded.attribution, action.attribution);
140 assert_eq!(decoded.timestamp, action.timestamp);
141 }
142 }
143
144 fn old_encode_raw(data: &[u8], config: &CompressionConfig) -> Result<Vec<u8>> {
145 Ok(compress(data, config)?.unwrap_or_else(|| data.to_vec()))
146 }
147
148 fn compression_configs() -> Vec<CompressionConfig> {
149 #[cfg(feature = "zstd")]
150 {
151 vec![
152 CompressionConfig::default(),
153 CompressionConfig::disabled(),
154 CompressionConfig {
155 enabled: true,
156 level: 9,
157 min_size: 0,
158 max_delta_size: CompressionConfig::default().max_delta_size,
159 },
160 ]
161 }
162 #[cfg(not(feature = "zstd"))]
163 {
164 vec![CompressionConfig::default(), CompressionConfig::disabled()]
165 }
166 }
167
168 fn sample_attribution() -> Attribution {
169 Attribution::human(Principal::new("Codec Test", "codec@example.com"))
170 }
171}