1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

/* Automatically managed default lints */
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
/* End of automatically managed default lints */
#![allow(clippy::derive_partial_eq_without_eq)]
#![warn(
    missing_docs,
    rustdoc::missing_crate_level_docs,
    unreachable_pub,
    rust_2018_idioms
)]

//! Compression-related code.

use aws_smithy_runtime_api::box_error::BoxError;
use aws_smithy_types::config_bag::{Storable, StoreReplace};
use std::io::Write;
use std::str::FromStr;

pub mod body;
mod gzip;
pub mod http;

// Valid compression algorithm names
/// The name of the `gzip` algorithm.
pub const GZIP_NAME: &str = "gzip";

/// The maximum-allowable value per internal standards is 10 Megabytes.
const MAX_MIN_COMPRESSION_SIZE_BYTES: u32 = 10_485_760;

/// Types implementing this trait can compress data.
///
/// Compression algorithms are used reduce the size of data. This trait
/// requires Send + Sync because trait implementors are often used in an
/// async context.
pub trait Compress: Send + Sync {
    /// Given a slice of bytes, and a [Write] implementor, compress and write
    /// bytes to the writer until done.
    // I wanted to use `impl Write` but that's not object-safe
    fn compress_bytes(&mut self, bytes: &[u8], writer: &mut dyn Write) -> Result<(), BoxError>;
}

/// Options for configuring request compression.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct CompressionOptions {
    /// Valid values are 0-9 with lower values configuring less (but faster) compression
    level: u32,
    min_compression_size_bytes: u32,
    enabled: bool,
}

impl Default for CompressionOptions {
    fn default() -> Self {
        Self {
            level: 6,
            min_compression_size_bytes: 10240,
            enabled: true,
        }
    }
}

impl CompressionOptions {
    /// The compression level to use.
    pub fn level(&self) -> u32 {
        self.level
    }

    /// The minimum size of data to compress.
    ///
    /// Data smaller than this will not be compressed.
    pub fn min_compression_size_bytes(&self) -> u32 {
        self.min_compression_size_bytes
    }

    /// Whether compression is enabled.
    pub fn is_enabled(&self) -> bool {
        self.enabled
    }

    /// Set whether compression is enabled.
    pub fn with_enabled(self, enabled: bool) -> Self {
        Self { enabled, ..self }
    }

    /// Set the compression level.
    ///
    /// Valid values are `0..=9` with lower values configuring less _(but faster)_ compression
    pub fn with_level(self, level: u32) -> Result<Self, BoxError> {
        Self::validate_level(level)?;
        Ok(Self { level, ..self })
    }

    /// Set the minimum size of data to compress.
    ///
    /// Data smaller than this will not be compressed.
    /// Valid values are `0..=10_485_760`. The default is `10_240`.
    pub fn with_min_compression_size_bytes(
        self,
        min_compression_size_bytes: u32,
    ) -> Result<Self, BoxError> {
        Self::validate_min_compression_size_bytes(min_compression_size_bytes)?;
        Ok(Self {
            min_compression_size_bytes,
            ..self
        })
    }

    fn validate_level(level: u32) -> Result<(), BoxError> {
        if level > 9 {
            return Err(format!(
                "compression level `{}` is invalid, valid values are 0..=9",
                level
            )
            .into());
        };
        Ok(())
    }

    fn validate_min_compression_size_bytes(
        min_compression_size_bytes: u32,
    ) -> Result<(), BoxError> {
        if min_compression_size_bytes > MAX_MIN_COMPRESSION_SIZE_BYTES {
            return Err(format!(
                "min compression size `{}` is invalid, valid values are 0..=10_485_760",
                min_compression_size_bytes
            )
            .into());
        };
        Ok(())
    }
}

impl Storable for CompressionOptions {
    type Storer = StoreReplace<Self>;
}

/// An enum encompassing all supported compression algorithms.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CompressionAlgorithm {
    /// The [gzip](https://en.wikipedia.org/wiki/Gzip) compression algorithm
    Gzip,
}

impl FromStr for CompressionAlgorithm {
    type Err = BoxError;

    /// Create a new `CompressionAlgorithm` from an algorithm name.
    ///
    /// Valid algorithm names are:
    /// - "gzip"
    ///
    /// Passing an invalid name will return an error.
    fn from_str(compression_algorithm: &str) -> Result<Self, Self::Err> {
        if compression_algorithm.eq_ignore_ascii_case(GZIP_NAME) {
            Ok(Self::Gzip)
        } else {
            Err(format!("unknown compression algorithm `{compression_algorithm}`").into())
        }
    }
}

impl CompressionAlgorithm {
    #[cfg(feature = "http-body-0-4-x")]
    /// Return the `HttpChecksum` implementor for this algorithm.
    pub fn into_impl_http_body_0_4_x(
        self,
        options: &CompressionOptions,
    ) -> Box<dyn http::http_body_0_4_x::CompressRequest> {
        match self {
            Self::Gzip => Box::new(gzip::Gzip::from(options)),
        }
    }

    #[cfg(feature = "http-body-1-x")]
    /// Return the `HttpChecksum` implementor for this algorithm.
    pub fn into_impl_http_body_1_x(
        self,
        options: &CompressionOptions,
    ) -> Box<dyn http::http_body_1_x::CompressRequest> {
        match self {
            Self::Gzip => Box::new(gzip::Gzip::from(options)),
        }
    }

    /// Return the name of this algorithm in string form
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Gzip { .. } => GZIP_NAME,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::CompressionAlgorithm;
    use pretty_assertions::assert_eq;

    #[test]
    fn test_compression_algorithm_from_str_unknown() {
        let error = "some unknown compression algorithm"
            .parse::<CompressionAlgorithm>()
            .expect_err("it should error");
        assert_eq!(
            "unknown compression algorithm `some unknown compression algorithm`",
            error.to_string()
        );
    }

    #[test]
    fn test_compression_algorithm_from_str_gzip() {
        let algo = "gzip".parse::<CompressionAlgorithm>().unwrap();
        assert_eq!("gzip", algo.as_str());
    }
}