Skip to main content

actix_http/header/shared/
quality.rs

1use std::fmt;
2
3use derive_more::{Display, Error};
4
5const MAX_QUALITY_INT: u16 = 1000;
6const MAX_QUALITY_FLOAT: f32 = 1.0;
7
8/// Represents a quality used in q-factor values.
9///
10/// The default value is equivalent to `q=1.0` (the [max](Self::MAX) value).
11///
12/// # Implementation notes
13/// The quality value is defined as a number between 0.0 and 1.0 with three decimal places.
14/// This means there are 1001 possible values. Since floating point numbers are not exact and the
15/// smallest floating point data type (`f32`) consumes four bytes, we use an `u16` value to store
16/// the quality internally.
17/// Float conversions round to the nearest thousandth.
18///
19/// [RFC 7231 §5.3.1] gives more information on quality values in HTTP header fields.
20///
21/// # Examples
22/// ```
23/// use actix_http::header::{Quality, q};
24/// assert_eq!(q(1.0), Quality::MAX);
25///
26/// assert_eq!(q(0.42).to_string(), "0.42");
27/// assert_eq!(q(1.0).to_string(), "1");
28/// assert_eq!(Quality::MIN.to_string(), "0.001");
29/// assert_eq!(Quality::ZERO.to_string(), "0");
30/// ```
31///
32/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
34pub struct Quality(pub(super) u16);
35
36impl Quality {
37    /// The maximum quality value, equivalent to `q=1.0`.
38    pub const MAX: Quality = Quality(MAX_QUALITY_INT);
39
40    /// The minimum, non-zero quality value, equivalent to `q=0.001`.
41    pub const MIN: Quality = Quality(1);
42
43    /// The zero quality value, equivalent to `q=0.0`.
44    pub const ZERO: Quality = Quality(0);
45
46    /// Converts a float in the range 0.0–1.0 to a `Quality`.
47    ///
48    /// Intentionally private. External uses should rely on the `TryFrom` impl.
49    ///
50    /// # Panics
51    /// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
52    fn from_f32(value: f32) -> Self {
53        // Check that `value` is within range should be done before calling this method.
54        // Just in case, this debug_assert should catch if we were forgetful.
55        debug_assert!(
56            (0.0..=MAX_QUALITY_FLOAT).contains(&value),
57            "q value must be between 0.0 and 1.0"
58        );
59
60        Quality((value * MAX_QUALITY_INT as f32).round() as u16)
61    }
62}
63
64/// The default value is [`Quality::MAX`].
65impl Default for Quality {
66    fn default() -> Quality {
67        Quality::MAX
68    }
69}
70
71impl fmt::Display for Quality {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self.0 {
74            0 => f.write_str("0"),
75            MAX_QUALITY_INT => f.write_str("1"),
76
77            // some number in the range 1–999
78            x => {
79                f.write_str("0.")?;
80
81                // This implementation avoids string allocation for removing trailing zeroes.
82                // In benchmarks it is twice as fast as approach using something like
83                // `format!("{}").trim_end_matches('0')` for non-fast-path quality values.
84
85                if x < 10 {
86                    // x in is range 1–9
87
88                    f.write_str("00")?;
89
90                    // 0 is already handled so it's not possible to have a trailing 0 in this range
91                    // we can just write the integer
92                    itoa_fmt(f, x)
93                } else if x < 100 {
94                    // x in is range 10–99
95
96                    f.write_str("0")?;
97
98                    if x % 10 == 0 {
99                        // trailing 0, divide by 10 and write
100                        itoa_fmt(f, x / 10)
101                    } else {
102                        itoa_fmt(f, x)
103                    }
104                } else {
105                    // x is in range 100–999
106
107                    if x % 100 == 0 {
108                        // two trailing 0s, divide by 100 and write
109                        itoa_fmt(f, x / 100)
110                    } else if x % 10 == 0 {
111                        // one trailing 0, divide by 10 and write
112                        itoa_fmt(f, x / 10)
113                    } else {
114                        itoa_fmt(f, x)
115                    }
116                }
117            }
118        }
119    }
120}
121
122/// Write integer to a `fmt::Write`.
123pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
124    let mut buf = itoa::Buffer::new();
125    wr.write_str(buf.format(value))
126}
127
128#[derive(Debug, Clone, Display, Error)]
129#[display("quality out of bounds")]
130#[non_exhaustive]
131pub struct QualityOutOfBounds;
132
133impl TryFrom<f32> for Quality {
134    type Error = QualityOutOfBounds;
135
136    #[inline]
137    fn try_from(value: f32) -> Result<Self, Self::Error> {
138        if (0.0..=MAX_QUALITY_FLOAT).contains(&value) {
139            Ok(Quality::from_f32(value))
140        } else {
141            Err(QualityOutOfBounds)
142        }
143    }
144}
145
146/// Convenience function to create a [`Quality`] from an `f32` (0.0–1.0).
147///
148/// Not recommended for use with user input. Rely on the `TryFrom` impls where possible.
149///
150/// # Panics
151/// Panics if value is out of range.
152///
153/// # Examples
154/// ```
155/// # use actix_http::header::{q, Quality};
156/// let q1 = q(1.0);
157/// assert_eq!(q1, Quality::MAX);
158///
159/// let q2 = q(0.001);
160/// assert_eq!(q2, Quality::MIN);
161///
162/// let q3 = q(0.0);
163/// assert_eq!(q3, Quality::ZERO);
164///
165/// let q4 = q(0.42);
166/// ```
167///
168/// An out-of-range `f32` quality will panic.
169/// ```should_panic
170/// # use actix_http::header::q;
171/// let _q2 = q(1.42);
172/// ```
173#[inline]
174pub fn q<T>(quality: T) -> Quality
175where
176    T: TryInto<Quality>,
177    T::Error: fmt::Debug,
178{
179    quality.try_into().expect("quality value was out of bounds")
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn q_helper() {
188        assert_eq!(q(0.5), Quality(500));
189    }
190
191    #[test]
192    fn float_conversion_preserves_thousandths() {
193        for value in 0..=1000 {
194            let quality = Quality::try_from(value as f32 / 1000.0).unwrap();
195
196            assert_eq!(quality, Quality(value), "quality={value}/1000");
197        }
198    }
199
200    #[test]
201    fn float_conversion_rounds_to_nearest_thousandth() {
202        for (value, expected) in [
203            (0.0004, Quality::ZERO),
204            (0.0005, Quality::MIN),
205            (0.2344, Quality(234)),
206            (0.2345, Quality(235)),
207            (0.9995, Quality::MAX),
208        ] {
209            assert_eq!(Quality::try_from(value).unwrap(), expected, "{value}");
210        }
211    }
212
213    #[test]
214    fn display_output() {
215        assert_eq!(Quality::ZERO.to_string(), "0");
216        assert_eq!(Quality::MIN.to_string(), "0.001");
217        assert_eq!(Quality::MAX.to_string(), "1");
218
219        assert_eq!(q(0.0).to_string(), "0");
220        assert_eq!(q(1.0).to_string(), "1");
221        assert_eq!(q(0.001).to_string(), "0.001");
222        assert_eq!(q(0.5).to_string(), "0.5");
223        assert_eq!(q(0.22).to_string(), "0.22");
224        assert_eq!(q(0.123).to_string(), "0.123");
225        assert_eq!(q(0.999).to_string(), "0.999");
226
227        for x in 0..=1000 {
228            // if trailing zeroes are handled correctly, we would not expect the serialized length
229            // to ever exceed "0." + 3 decimal places = 5 in length
230            assert!(q(x as f32 / 1000.0).to_string().len() <= 5);
231        }
232    }
233
234    #[test]
235    #[should_panic]
236    fn negative_quality() {
237        q(-1.0);
238    }
239
240    #[test]
241    #[should_panic]
242    fn quality_out_of_bounds() {
243        q(2.0);
244    }
245}