aws_smithy_compression/
lib.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6/* Automatically managed default lints */
7#![cfg_attr(docsrs, feature(doc_cfg))]
8/* End of automatically managed default lints */
9#![allow(clippy::derive_partial_eq_without_eq)]
10#![warn(
11    missing_docs,
12    rustdoc::missing_crate_level_docs,
13    unreachable_pub,
14    rust_2018_idioms
15)]
16
17//! Compression-related code.
18
19use aws_smithy_runtime_api::box_error::BoxError;
20use aws_smithy_types::config_bag::{Storable, StoreReplace};
21use std::io::Write;
22use std::str::FromStr;
23
24pub mod body;
25mod gzip;
26pub mod http;
27
28// Valid compression algorithm names
29/// The name of the `gzip` algorithm.
30pub const GZIP_NAME: &str = "gzip";
31
32/// The maximum-allowable value per internal standards is 10 Megabytes.
33const MAX_MIN_COMPRESSION_SIZE_BYTES: u32 = 10_485_760;
34
35/// Types implementing this trait can compress data.
36///
37/// Compression algorithms are used reduce the size of data. This trait
38/// requires Send + Sync because trait implementors are often used in an
39/// async context.
40pub trait Compress: Send + Sync {
41    /// Given a slice of bytes, and a [Write] implementor, compress and write
42    /// bytes to the writer until done.
43    // I wanted to use `impl Write` but that's not object-safe
44    fn compress_bytes(&mut self, bytes: &[u8], writer: &mut dyn Write) -> Result<(), BoxError>;
45}
46
47/// Options for configuring request compression.
48#[derive(Debug, Clone, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct CompressionOptions {
51    /// Valid values are 0-9 with lower values configuring less (but faster) compression
52    level: u32,
53    min_compression_size_bytes: u32,
54    enabled: bool,
55}
56
57impl Default for CompressionOptions {
58    fn default() -> Self {
59        Self {
60            level: 6,
61            min_compression_size_bytes: 10240,
62            enabled: true,
63        }
64    }
65}
66
67impl CompressionOptions {
68    /// The compression level to use.
69    pub fn level(&self) -> u32 {
70        self.level
71    }
72
73    /// The minimum size of data to compress.
74    ///
75    /// Data smaller than this will not be compressed.
76    pub fn min_compression_size_bytes(&self) -> u32 {
77        self.min_compression_size_bytes
78    }
79
80    /// Whether compression is enabled.
81    pub fn is_enabled(&self) -> bool {
82        self.enabled
83    }
84
85    /// Set whether compression is enabled.
86    pub fn with_enabled(self, enabled: bool) -> Self {
87        Self { enabled, ..self }
88    }
89
90    /// Set the compression level.
91    ///
92    /// Valid values are `0..=9` with lower values configuring less _(but faster)_ compression
93    pub fn with_level(self, level: u32) -> Result<Self, BoxError> {
94        Self::validate_level(level)?;
95        Ok(Self { level, ..self })
96    }
97
98    /// Set the minimum size of data to compress.
99    ///
100    /// Data smaller than this will not be compressed.
101    /// Valid values are `0..=10_485_760`. The default is `10_240`.
102    pub fn with_min_compression_size_bytes(
103        self,
104        min_compression_size_bytes: u32,
105    ) -> Result<Self, BoxError> {
106        Self::validate_min_compression_size_bytes(min_compression_size_bytes)?;
107        Ok(Self {
108            min_compression_size_bytes,
109            ..self
110        })
111    }
112
113    fn validate_level(level: u32) -> Result<(), BoxError> {
114        if level > 9 {
115            return Err(
116                format!("compression level `{level}` is invalid, valid values are 0..=9").into(),
117            );
118        };
119        Ok(())
120    }
121
122    fn validate_min_compression_size_bytes(
123        min_compression_size_bytes: u32,
124    ) -> Result<(), BoxError> {
125        if min_compression_size_bytes > MAX_MIN_COMPRESSION_SIZE_BYTES {
126            return Err(format!(
127                "min compression size `{min_compression_size_bytes}` is invalid, valid values are 0..=10_485_760"
128            )
129            .into());
130        };
131        Ok(())
132    }
133}
134
135impl Storable for CompressionOptions {
136    type Storer = StoreReplace<Self>;
137}
138
139/// An enum encompassing all supported compression algorithms.
140#[derive(Debug, Copy, Clone, PartialEq, Eq)]
141#[non_exhaustive]
142pub enum CompressionAlgorithm {
143    /// The [gzip](https://en.wikipedia.org/wiki/Gzip) compression algorithm
144    Gzip,
145}
146
147impl FromStr for CompressionAlgorithm {
148    type Err = BoxError;
149
150    /// Create a new `CompressionAlgorithm` from an algorithm name.
151    ///
152    /// Valid algorithm names are:
153    /// - "gzip"
154    ///
155    /// Passing an invalid name will return an error.
156    fn from_str(compression_algorithm: &str) -> Result<Self, Self::Err> {
157        if compression_algorithm.eq_ignore_ascii_case(GZIP_NAME) {
158            Ok(Self::Gzip)
159        } else {
160            Err(format!("unknown compression algorithm `{compression_algorithm}`").into())
161        }
162    }
163}
164
165impl CompressionAlgorithm {
166    #[cfg(feature = "http-body-0-4-x")]
167    /// Return the `HttpChecksum` implementor for this algorithm.
168    pub fn into_impl_http_body_0_4_x(
169        self,
170        options: &CompressionOptions,
171    ) -> Box<dyn http::http_body_0_4_x::CompressRequest> {
172        match self {
173            Self::Gzip => Box::new(gzip::Gzip::from(options)),
174        }
175    }
176
177    #[cfg(feature = "http-body-1-x")]
178    /// Return the `HttpChecksum` implementor for this algorithm.
179    pub fn into_impl_http_body_1_x(
180        self,
181        options: &CompressionOptions,
182    ) -> Box<dyn http::http_body_1_x::CompressRequest> {
183        match self {
184            Self::Gzip => Box::new(gzip::Gzip::from(options)),
185        }
186    }
187
188    /// Return the name of this algorithm in string form
189    pub fn as_str(&self) -> &'static str {
190        match self {
191            Self::Gzip { .. } => GZIP_NAME,
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use crate::CompressionAlgorithm;
199    use pretty_assertions::assert_eq;
200
201    #[test]
202    fn test_compression_algorithm_from_str_unknown() {
203        let error = "some unknown compression algorithm"
204            .parse::<CompressionAlgorithm>()
205            .expect_err("it should error");
206        assert_eq!(
207            "unknown compression algorithm `some unknown compression algorithm`",
208            error.to_string()
209        );
210    }
211
212    #[test]
213    fn test_compression_algorithm_from_str_gzip() {
214        let algo = "gzip".parse::<CompressionAlgorithm>().unwrap();
215        assert_eq!("gzip", algo.as_str());
216    }
217}