Skip to main content

nano_web/
response_buffer.rs

1use bytes::Bytes;
2use std::sync::Arc;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum Encoding {
6    Identity,
7    Gzip,
8    Brotli,
9    Zstd,
10}
11
12impl Encoding {
13    pub const ALL: [Self; 4] = [Self::Identity, Self::Gzip, Self::Brotli, Self::Zstd];
14
15    /// Parse Accept-Encoding header, priority: br > zstd > gzip > identity.
16    /// Splits on comma to avoid substring false positives (e.g. "br" matching "vibrant").
17    /// Respects q=0 (encoding explicitly rejected by client).
18    pub fn from_accept_encoding(accept: &str) -> Self {
19        let mut best = Self::Identity;
20        for part in accept.split(',') {
21            let mut segments = part.split(';');
22            let token = segments.next().unwrap_or("").trim();
23
24            // q=0 means the encoding is explicitly rejected
25            let rejected = segments.any(|s| {
26                s.trim()
27                    .strip_prefix("q=")
28                    .and_then(|v| v.trim().parse::<f32>().ok())
29                    .is_some_and(|q| q == 0.0)
30            });
31            if rejected {
32                continue;
33            }
34
35            match token {
36                "br" => return Self::Brotli, // highest priority, short-circuit
37                "zstd" => best = Self::Zstd,
38                "gzip" if !matches!(best, Self::Zstd) => best = Self::Gzip,
39                _ => {}
40            }
41        }
42        best
43    }
44}
45
46#[derive(Debug, Clone)]
47pub struct ResponseBuffer {
48    pub body: Bytes,
49    pub content_type: Arc<str>,
50    pub content_encoding: Option<&'static str>,
51    pub etag: Arc<str>,
52    pub last_modified: Arc<str>,
53    pub cache_control: Arc<str>,
54    pub content_length: Arc<str>,
55    /// Whether Vary: Accept-Encoding should be sent (true for all compressible types)
56    pub vary_encoding: bool,
57}
58
59impl ResponseBuffer {
60    pub fn new(
61        body: Bytes,
62        content_type: Arc<str>,
63        content_encoding: Option<&'static str>,
64        etag: Arc<str>,
65        last_modified: Arc<str>,
66        cache_control: Arc<str>,
67        vary_encoding: bool,
68    ) -> Self {
69        let content_length: Arc<str> = Arc::from(body.len().to_string().as_str());
70        Self {
71            body,
72            content_type,
73            content_encoding,
74            etag,
75            last_modified,
76            cache_control,
77            content_length,
78            vary_encoding,
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_encoding_priority() {
89        assert_eq!(
90            Encoding::from_accept_encoding("gzip, br, zstd"),
91            Encoding::Brotli
92        );
93        assert_eq!(Encoding::from_accept_encoding("br"), Encoding::Brotli);
94        assert_eq!(Encoding::from_accept_encoding("gzip, zstd"), Encoding::Zstd);
95        assert_eq!(Encoding::from_accept_encoding("zstd"), Encoding::Zstd);
96        assert_eq!(Encoding::from_accept_encoding("gzip"), Encoding::Gzip);
97        assert_eq!(
98            Encoding::from_accept_encoding("deflate"),
99            Encoding::Identity
100        );
101        assert_eq!(Encoding::from_accept_encoding(""), Encoding::Identity);
102    }
103
104    #[test]
105    fn test_encoding_no_substring_false_positives() {
106        assert_eq!(
107            Encoding::from_accept_encoding("vibrant"),
108            Encoding::Identity
109        );
110        assert_eq!(Encoding::from_accept_encoding("broken"), Encoding::Identity);
111    }
112
113    #[test]
114    fn test_encoding_with_quality_values() {
115        assert_eq!(
116            Encoding::from_accept_encoding("gzip;q=1.0, br;q=0.8"),
117            Encoding::Brotli
118        );
119        assert_eq!(
120            Encoding::from_accept_encoding("gzip;q=0.5, zstd;q=1.0"),
121            Encoding::Zstd
122        );
123    }
124
125    #[test]
126    fn test_encoding_respects_q_zero() {
127        // q=0 means explicitly rejected
128        assert_eq!(
129            Encoding::from_accept_encoding("br;q=0, gzip"),
130            Encoding::Gzip
131        );
132        assert_eq!(
133            Encoding::from_accept_encoding("br;q=0, zstd;q=0, gzip"),
134            Encoding::Gzip
135        );
136        assert_eq!(
137            Encoding::from_accept_encoding("br;q=0, zstd;q=0, gzip;q=0"),
138            Encoding::Identity
139        );
140        assert_eq!(
141            Encoding::from_accept_encoding("gzip;q=0"),
142            Encoding::Identity
143        );
144    }
145}