logo
  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
221
222
223
224
225
use std::{
    convert::{TryFrom, TryInto},
    fmt,
};

use derive_more::{Display, Error};

const MAX_QUALITY_INT: u16 = 1000;
const MAX_QUALITY_FLOAT: f32 = 1.0;

/// Represents a quality used in q-factor values.
///
/// The default value is equivalent to `q=1.0` (the [max](Self::MAX) value).
///
/// # Implementation notes
/// The quality value is defined as a number between 0.0 and 1.0 with three decimal places.
/// This means there are 1001 possible values. Since floating point numbers are not exact and the
/// smallest floating point data type (`f32`) consumes four bytes, we use an `u16` value to store
/// the quality internally.
///
/// [RFC 7231 §5.3.1] gives more information on quality values in HTTP header fields.
///
/// # Examples
/// ```
/// use actix_http::header::{Quality, q};
/// assert_eq!(q(1.0), Quality::MAX);
///
/// assert_eq!(q(0.42).to_string(), "0.42");
/// assert_eq!(q(1.0).to_string(), "1");
/// assert_eq!(Quality::MIN.to_string(), "0.001");
/// assert_eq!(Quality::ZERO.to_string(), "0");
/// ```
///
/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Quality(pub(super) u16);

impl Quality {
    /// The maximum quality value, equivalent to `q=1.0`.
    pub const MAX: Quality = Quality(MAX_QUALITY_INT);

    /// The minimum, non-zero quality value, equivalent to `q=0.001`.
    pub const MIN: Quality = Quality(1);

    /// The zero quality value, equivalent to `q=0.0`.
    pub const ZERO: Quality = Quality(0);

    /// Converts a float in the range 0.0–1.0 to a `Quality`.
    ///
    /// Intentionally private. External uses should rely on the `TryFrom` impl.
    ///
    /// # Panics
    /// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
    fn from_f32(value: f32) -> Self {
        // Check that `value` is within range should be done before calling this method.
        // Just in case, this debug_assert should catch if we were forgetful.
        debug_assert!(
            (0.0..=MAX_QUALITY_FLOAT).contains(&value),
            "q value must be between 0.0 and 1.0"
        );

        Quality((value * MAX_QUALITY_INT as f32) as u16)
    }
}

/// The default value is [`Quality::MAX`].
impl Default for Quality {
    fn default() -> Quality {
        Quality::MAX
    }
}

impl fmt::Display for Quality {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.0 {
            0 => f.write_str("0"),
            MAX_QUALITY_INT => f.write_str("1"),

            // some number in the range 1–999
            x => {
                f.write_str("0.")?;

                // This implementation avoids string allocation for removing trailing zeroes.
                // In benchmarks it is twice as fast as approach using something like
                // `format!("{}").trim_end_matches('0')` for non-fast-path quality values.

                if x < 10 {
                    // x in is range 1–9

                    f.write_str("00")?;

                    // 0 is already handled so it's not possible to have a trailing 0 in this range
                    // we can just write the integer
                    itoa_fmt(f, x)
                } else if x < 100 {
                    // x in is range 10–99

                    f.write_str("0")?;

                    if x % 10 == 0 {
                        // trailing 0, divide by 10 and write
                        itoa_fmt(f, x / 10)
                    } else {
                        itoa_fmt(f, x)
                    }
                } else {
                    // x is in range 100–999

                    if x % 100 == 0 {
                        // two trailing 0s, divide by 100 and write
                        itoa_fmt(f, x / 100)
                    } else if x % 10 == 0 {
                        // one trailing 0, divide by 10 and write
                        itoa_fmt(f, x / 10)
                    } else {
                        itoa_fmt(f, x)
                    }
                }
            }
        }
    }
}

/// Write integer to a `fmt::Write`.
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
    let mut buf = itoa::Buffer::new();
    wr.write_str(buf.format(value))
}

#[derive(Debug, Clone, Display, Error)]
#[display(fmt = "quality out of bounds")]
#[non_exhaustive]
pub struct QualityOutOfBounds;

impl TryFrom<f32> for Quality {
    type Error = QualityOutOfBounds;

    #[inline]
    fn try_from(value: f32) -> Result<Self, Self::Error> {
        if (0.0..=MAX_QUALITY_FLOAT).contains(&value) {
            Ok(Quality::from_f32(value))
        } else {
            Err(QualityOutOfBounds)
        }
    }
}

/// Convenience function to create a [`Quality`] from an `f32` (0.0–1.0).
///
/// Not recommended for use with user input. Rely on the `TryFrom` impls where possible.
///
/// # Panics
/// Panics if value is out of range.
///
/// # Examples
/// ```
/// # use actix_http::header::{q, Quality};
/// let q1 = q(1.0);
/// assert_eq!(q1, Quality::MAX);
///
/// let q2 = q(0.001);
/// assert_eq!(q2, Quality::MIN);
///
/// let q3 = q(0.0);
/// assert_eq!(q3, Quality::ZERO);
///
/// let q4 = q(0.42);
/// ```
///
/// An out-of-range `f32` quality will panic.
/// ```should_panic
/// # use actix_http::header::q;
/// let _q2 = q(1.42);
/// ```
#[inline]
pub fn q<T>(quality: T) -> Quality
where
    T: TryInto<Quality>,
    T::Error: fmt::Debug,
{
    quality.try_into().expect("quality value was out of bounds")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn q_helper() {
        assert_eq!(q(0.5), Quality(500));
    }

    #[test]
    fn display_output() {
        assert_eq!(Quality::ZERO.to_string(), "0");
        assert_eq!(Quality::MIN.to_string(), "0.001");
        assert_eq!(Quality::MAX.to_string(), "1");

        assert_eq!(q(0.0).to_string(), "0");
        assert_eq!(q(1.0).to_string(), "1");
        assert_eq!(q(0.001).to_string(), "0.001");
        assert_eq!(q(0.5).to_string(), "0.5");
        assert_eq!(q(0.22).to_string(), "0.22");
        assert_eq!(q(0.123).to_string(), "0.123");
        assert_eq!(q(0.999).to_string(), "0.999");

        for x in 0..=1000 {
            // if trailing zeroes are handled correctly, we would not expect the serialized length
            // to ever exceed "0." + 3 decimal places = 5 in length
            assert!(q(x as f32 / 1000.0).to_string().len() <= 5);
        }
    }

    #[test]
    #[should_panic]
    fn negative_quality() {
        q(-1.0);
    }

    #[test]
    #[should_panic]
    fn quality_out_of_bounds() {
        q(2.0);
    }
}