Skip to main content

objects/store/
codec.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Object body codecs for loose-object backends.
3
4use 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}