bc_envelope/extension/compress.rs
1//! Extension for compressing and uncompressing envelopes.
2//!
3//! This module provides functionality for compressing envelopes to reduce their
4//! size while maintaining their digests. Unlike elision, which removes content,
5//! compression preserves all the information in the envelope but represents it
6//! more efficiently.
7//!
8//! Compression is implemented using the DEFLATE algorithm and preserves the
9//! envelope's digest, making it compatible with the envelope's hierarchical
10//! digest tree structure. This means parts of an envelope can be compressed
11//! without invalidating signatures or breaking the digest tree.
12//!
13//! # Examples
14//!
15//! ```
16//! use bc_envelope::prelude::*;
17//!
18//! // Create an envelope with some larger, compressible content
19//! let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
20//! let envelope = Envelope::new(lorem);
21//!
22//! // Compress the envelope
23//! let compressed = envelope.compress().unwrap();
24//!
25//! // The compressed envelope has the same digest as the original
26//! assert_eq!(envelope.digest(), compressed.digest());
27//!
28//! // But it takes up less space when serialized
29//! assert!(compressed.to_cbor_data().len() < envelope.to_cbor_data().len());
30//!
31//! // The envelope can be uncompressed to recover the original content
32//! let uncompressed = compressed.uncompress().unwrap();
33//! assert_eq!(uncompressed.extract_subject::<String>().unwrap(), lorem);
34//! ```
35
36use anyhow::{Result, bail};
37use bc_components::{Compressed, DigestProvider};
38use dcbor::prelude::*;
39
40use crate::{Envelope, Error, base::envelope::EnvelopeCase};
41
42/// Support for compressing and uncompressing envelopes.
43impl Envelope {
44 /// Returns a compressed version of this envelope.
45 ///
46 /// This method compresses the envelope using the DEFLATE algorithm,
47 /// creating a more space-efficient representation while preserving the
48 /// envelope's digest and semantic content. The compressed envelope
49 /// maintains the same digest as the original, ensuring compatibility
50 /// with the envelope's digest tree structure.
51 ///
52 /// When an envelope is compressed, the entire envelope structure (including
53 /// its subject and assertions) is compressed as a single unit. The
54 /// compression preserves all the information but reduces the size of
55 /// the serialized envelope.
56 ///
57 /// # Returns
58 ///
59 /// A Result containing the compressed envelope or an error.
60 ///
61 /// # Errors
62 ///
63 /// - Returns `EnvelopeError::AlreadyEncrypted` if the envelope is already
64 /// encrypted
65 /// - Returns `EnvelopeError::AlreadyElided` if the envelope is already
66 /// elided
67 /// - May return various compression-related errors
68 ///
69 /// # Examples
70 ///
71 /// ```
72 /// use bc_envelope::prelude::*;
73 ///
74 /// // Create an envelope with some content
75 /// let text = "This is a fairly long text that will benefit from compression.
76 /// The longer the text, the more effective compression becomes.
77 /// DEFLATE works well on repetitive content like this.";
78 /// let envelope = Envelope::new(text);
79 ///
80 /// // Compress the envelope
81 /// let compressed = envelope.compress().unwrap();
82 ///
83 /// // Check that the compressed version has the same digest
84 /// assert_eq!(envelope.digest(), compressed.digest());
85 ///
86 /// // Verify that the compressed version takes less space
87 /// assert!(compressed.to_cbor_data().len() < envelope.to_cbor_data().len());
88 /// ```
89 pub fn compress(&self) -> Result<Self> {
90 match self.case() {
91 EnvelopeCase::Compressed(_) => Ok(self.clone()),
92 #[cfg(feature = "encrypt")]
93 EnvelopeCase::Encrypted(_) => bail!(Error::AlreadyEncrypted),
94 EnvelopeCase::Elided(_) => bail!(Error::AlreadyElided),
95 _ => {
96 let compressed = Compressed::from_uncompressed_data(
97 self.tagged_cbor().to_cbor_data(),
98 Some(self.digest().into_owned()),
99 );
100 Ok(compressed.try_into()?)
101 }
102 }
103 }
104
105 /// Returns the uncompressed variant of this envelope.
106 ///
107 /// This method reverses the compression process, restoring the envelope to
108 /// its original uncompressed form. The uncompressed envelope will have
109 /// the same digest as the compressed version.
110 ///
111 /// # Returns
112 ///
113 /// A Result containing the uncompressed envelope or an error.
114 ///
115 /// # Errors
116 ///
117 /// - Returns `EnvelopeError::NotCompressed` if the envelope is not
118 /// compressed
119 /// - Returns `EnvelopeError::MissingDigest` if the compressed envelope is
120 /// missing its digest
121 /// - Returns `EnvelopeError::InvalidDigest` if the decompressed data
122 /// doesn't match the expected digest
123 /// - May return various decompression-related errors
124 ///
125 /// # Examples
126 ///
127 /// ```
128 /// use bc_envelope::prelude::*;
129 ///
130 /// // Create and compress an envelope
131 /// let original = Envelope::new("Hello, world!");
132 /// let compressed = original.compress().unwrap();
133 ///
134 /// // Uncompress it
135 /// let uncompressed = compressed.uncompress().unwrap();
136 ///
137 /// // The uncompressed envelope should match the original
138 /// assert_eq!(
139 /// uncompressed.extract_subject::<String>().unwrap(),
140 /// "Hello, world!"
141 /// );
142 /// assert_eq!(uncompressed.digest(), original.digest());
143 ///
144 /// // Trying to uncompress a non-compressed envelope fails
145 /// assert!(original.uncompress().is_err());
146 /// ```
147 pub fn uncompress(&self) -> Result<Self> {
148 if let EnvelopeCase::Compressed(compressed) = self.case() {
149 if let Some(digest) = compressed.digest_ref_opt() {
150 if digest != self.digest().as_ref() {
151 bail!(Error::InvalidDigest);
152 }
153 let uncompressed_data = compressed.uncompress()?;
154 let envelope =
155 Envelope::from_tagged_cbor_data(uncompressed_data)?;
156 if envelope.digest().as_ref() != digest {
157 bail!(Error::InvalidDigest);
158 }
159 Ok(envelope)
160 } else {
161 bail!(Error::MissingDigest)
162 }
163 } else {
164 bail!(Error::NotCompressed)
165 }
166 }
167
168 /// Returns this envelope with its subject compressed.
169 ///
170 /// Unlike `compress()` which compresses the entire envelope, this method
171 /// only compresses the subject of the envelope, leaving the assertions
172 /// uncompressed. This is useful when you want to compress a large
173 /// subject while keeping the assertions readable and accessible.
174 ///
175 /// # Returns
176 ///
177 /// A Result containing a new envelope with a compressed subject, or an
178 /// error.
179 ///
180 /// # Errors
181 ///
182 /// May return errors from the compression process.
183 ///
184 /// # Examples
185 ///
186 /// ```
187 /// use bc_envelope::prelude::*;
188 ///
189 /// // Create an envelope with a large subject and some assertions
190 /// let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.";
191 /// let envelope = Envelope::new(lorem)
192 /// .add_assertion("note", "This is a metadata note");
193 ///
194 /// // Compress just the subject
195 /// let subject_compressed = envelope.compress_subject().unwrap();
196 ///
197 /// // The envelope's digest is preserved
198 /// assert_eq!(envelope.digest(), subject_compressed.digest());
199 ///
200 /// // The subject is now compressed
201 /// assert!(subject_compressed.subject().is_compressed());
202 ///
203 /// // But the assertions are still directly accessible
204 /// let note = subject_compressed.object_for_predicate("note").unwrap();
205 /// assert_eq!(note.extract_subject::<String>().unwrap(), "This is a metadata note");
206 /// ```
207 pub fn compress_subject(&self) -> Result<Self> {
208 if self.subject().is_compressed() {
209 Ok(self.clone())
210 } else {
211 let subject = self.subject().compress()?;
212 Ok(self.replace_subject(subject))
213 }
214 }
215
216 /// Returns this envelope with its subject uncompressed.
217 ///
218 /// This method reverses the effect of `compress_subject()`, uncompressing
219 /// the subject of the envelope while leaving the rest of the envelope
220 /// unchanged.
221 ///
222 /// # Returns
223 ///
224 /// A Result containing a new envelope with an uncompressed subject, or an
225 /// error.
226 ///
227 /// # Errors
228 ///
229 /// May return errors from the decompression process.
230 ///
231 /// # Examples
232 ///
233 /// ```
234 /// use bc_envelope::prelude::*;
235 ///
236 /// // Create an envelope and compress its subject
237 /// let original =
238 /// Envelope::new("Hello, world!").add_assertion("note", "Test note");
239 /// let compressed = original.compress_subject().unwrap();
240 ///
241 /// // Verify the subject is compressed
242 /// assert!(compressed.subject().is_compressed());
243 ///
244 /// // Uncompress the subject
245 /// let uncompressed = compressed.uncompress_subject().unwrap();
246 ///
247 /// // Verify the subject is now uncompressed
248 /// assert!(!uncompressed.subject().is_compressed());
249 ///
250 /// // The content should match the original
251 /// assert_eq!(
252 /// uncompressed.extract_subject::<String>().unwrap(),
253 /// "Hello, world!"
254 /// );
255 /// ```
256 pub fn uncompress_subject(&self) -> Result<Self> {
257 if self.subject().is_compressed() {
258 let subject = self.subject().uncompress()?;
259 Ok(self.replace_subject(subject))
260 } else {
261 Ok(self.clone())
262 }
263 }
264}