static_files_module/
compression_algorithm.rs

1// Copyright 2024 Wladimir Palant
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Handles various compression algorithms allowed in `Accept-Encoding` and `Content-Encoding` HTTP
16//! headers.
17
18use serde::Deserialize;
19use std::fmt::Display;
20use std::str::FromStr;
21
22/// Represents a compression algorithm choice.
23#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
24pub enum CompressionAlgorithm {
25    /// gzip compression
26    #[serde(rename = "gz")]
27    Gzip,
28    /// deflate (zlib) compression
29    #[serde(rename = "zz")]
30    Deflate,
31    /// compress compression
32    #[serde(rename = "z")]
33    Compress,
34    /// Brotli compression
35    #[serde(rename = "br")]
36    Brotli,
37    /// Zstandard compression
38    #[serde(rename = "zst")]
39    Zstandard,
40}
41
42impl CompressionAlgorithm {
43    /// Returns the file extension corresponding to the algorithm.
44    pub fn ext(&self) -> &'static str {
45        match self {
46            Self::Gzip => "gz",
47            Self::Deflate => "zz",
48            Self::Compress => "z",
49            Self::Brotli => "br",
50            Self::Zstandard => "zst",
51        }
52    }
53
54    /// Determines the algorithm corresponding to the file extension if any.
55    pub fn from_ext(ext: &str) -> Option<Self> {
56        match ext {
57            "gz" => Some(Self::Gzip),
58            "zz" => Some(Self::Deflate),
59            "z" => Some(Self::Compress),
60            "br" => Some(Self::Brotli),
61            "zst" => Some(Self::Zstandard),
62            _ => None,
63        }
64    }
65
66    /// Returns the algorithm name as used in `Accept-Encoding` HTTP header.
67    pub fn name(&self) -> &'static str {
68        match self {
69            Self::Gzip => "gzip",
70            Self::Deflate => "deflate",
71            Self::Compress => "compress",
72            Self::Brotli => "br",
73            Self::Zstandard => "zstd",
74        }
75    }
76
77    /// Determines the algorithm corresponding to a name from `Accept-Encoding` HTTP header.
78    pub fn from_name(name: &str) -> Option<Self> {
79        match name {
80            "gzip" => Some(Self::Gzip),
81            "deflate" => Some(Self::Deflate),
82            "compress" => Some(Self::Compress),
83            "br" => Some(Self::Brotli),
84            "zstd" => Some(Self::Zstandard),
85            _ => None,
86        }
87    }
88}
89
90impl FromStr for CompressionAlgorithm {
91    type Err = UnsupportedCompressionAlgorithm;
92
93    /// Coverts a file extension into a compression algorithm.
94    fn from_str(s: &str) -> Result<Self, Self::Err> {
95        CompressionAlgorithm::from_ext(s).ok_or(UnsupportedCompressionAlgorithm(s.to_owned()))
96    }
97}
98
99impl Display for CompressionAlgorithm {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
101        write!(f, "{}", self.name())
102    }
103}
104
105/// The error type returned by `CompressionAlgorithm::from_str()`
106#[derive(Debug, PartialEq, Eq)]
107pub struct UnsupportedCompressionAlgorithm(String);
108
109impl Display for UnsupportedCompressionAlgorithm {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
111        write!(f, "Unsupported compression algorithm: {}", self.0)
112    }
113}
114
115/// Parses an encoding specifier from `Accept-Encoding` HTTP header into an
116/// algorithm/quality pair.
117fn parse_encoding(encoding: &str) -> Option<(&str, u16)> {
118    let mut params = encoding.split(';');
119    let algorithm = params.next()?.trim();
120    let mut quality = 1000;
121    for param in params {
122        if let Some((name, value)) = param.split_once('=') {
123            if name.trim() == "q" {
124                if let Ok(value) = f64::from_str(value.trim()) {
125                    quality = (value * 1000.0) as u16;
126                }
127            }
128        }
129    }
130    Some((algorithm, quality))
131}
132
133/// Compares the requested encodings from `Accept-Encoding` HTTP header with a list of supported
134/// algorithms and returns any matches, sorted by the respective quality value.
135pub(crate) fn find_matches(
136    requested: &str,
137    supported: &[CompressionAlgorithm],
138) -> Vec<CompressionAlgorithm> {
139    let mut requested = requested
140        .split(',')
141        .filter_map(parse_encoding)
142        .collect::<Vec<_>>();
143    requested.sort_by_key(|(_, quality)| -(*quality as i32));
144
145    let mut result = Vec::new();
146    for (algorithm, _) in requested {
147        if algorithm == "*" {
148            for algorithm in supported {
149                if !result.contains(algorithm) {
150                    result.push(*algorithm);
151                }
152            }
153            break;
154        } else if let Some(algorithm) = CompressionAlgorithm::from_name(algorithm) {
155            if supported.contains(&algorithm) && !result.contains(&algorithm) {
156                result.push(algorithm);
157            }
158        }
159    }
160    result
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_find_matches() {
169        assert_eq!(
170            find_matches(
171                "",
172                &[CompressionAlgorithm::Gzip, CompressionAlgorithm::Brotli]
173            ),
174            Vec::new()
175        );
176
177        assert_eq!(
178            find_matches(
179                "identity",
180                &[CompressionAlgorithm::Gzip, CompressionAlgorithm::Brotli]
181            ),
182            Vec::new()
183        );
184
185        assert_eq!(
186            find_matches(
187                "*",
188                &[CompressionAlgorithm::Gzip, CompressionAlgorithm::Brotli]
189            ),
190            vec![CompressionAlgorithm::Gzip, CompressionAlgorithm::Brotli]
191        );
192
193        assert_eq!(
194            find_matches(
195                "br, *",
196                &[CompressionAlgorithm::Gzip, CompressionAlgorithm::Brotli]
197            ),
198            vec![CompressionAlgorithm::Brotli, CompressionAlgorithm::Gzip]
199        );
200
201        assert_eq!(
202            find_matches(
203                "br;q=0.9, *",
204                &[CompressionAlgorithm::Gzip, CompressionAlgorithm::Brotli]
205            ),
206            vec![CompressionAlgorithm::Gzip, CompressionAlgorithm::Brotli]
207        );
208
209        assert_eq!(
210            find_matches(
211                "deflate;q=0.7, gzip;q=0.9, zstd;q=0.8, br;q=1.0, compress;q=0.5",
212                &[
213                    CompressionAlgorithm::Deflate,
214                    CompressionAlgorithm::Gzip,
215                    CompressionAlgorithm::Compress,
216                    CompressionAlgorithm::Brotli,
217                    CompressionAlgorithm::Zstandard,
218                ]
219            ),
220            vec![
221                CompressionAlgorithm::Brotli,
222                CompressionAlgorithm::Gzip,
223                CompressionAlgorithm::Zstandard,
224                CompressionAlgorithm::Deflate,
225                CompressionAlgorithm::Compress,
226            ]
227        );
228
229        assert_eq!(
230            find_matches(
231                "deflate;q=0.7, zstd;q=0.8, br;q=1.0",
232                &[
233                    CompressionAlgorithm::Deflate,
234                    CompressionAlgorithm::Gzip,
235                    CompressionAlgorithm::Brotli,
236                    CompressionAlgorithm::Zstandard,
237                ]
238            ),
239            vec![
240                CompressionAlgorithm::Brotli,
241                CompressionAlgorithm::Zstandard,
242                CompressionAlgorithm::Deflate,
243            ]
244        );
245    }
246}