bc_envelope/extension/
compress.rs

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