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