Skip to main content

lino_rest_api/
etag.rs

1//! Entity tags and conditional requests (specification section 7).
2
3use sha2::{Digest, Sha256};
4
5use crate::headers::Headers;
6use crate::problem::LinoHttpError;
7
8/// Compute the strong entity tag of an encoded representation.
9///
10/// # Examples
11///
12/// ```
13/// use lino_rest_api::etag::compute_etag;
14///
15/// assert!(compute_etag("(a 1)").starts_with('"'));
16/// ```
17pub fn compute_etag(body: &str) -> String {
18    let digest = Sha256::digest(body.as_bytes());
19    let mut hex = String::with_capacity(64);
20    for byte in digest {
21        hex.push_str(&format!("{byte:02x}"));
22    }
23    format!("\"{hex}\"")
24}
25
26/// Split an `If-Match` or `If-None-Match` header into entity tags.
27///
28/// Any weak prefix is removed, because this API only issues strong tags.
29pub fn parse_etag_list(header_value: Option<&str>) -> Vec<String> {
30    let Some(header_value) = header_value else {
31        return Vec::new();
32    };
33    header_value
34        .split(',')
35        .map(|tag| {
36            tag.trim()
37                .strip_prefix("W/")
38                .unwrap_or(tag.trim())
39                .to_string()
40        })
41        .filter(|tag| !tag.is_empty())
42        .collect()
43}
44
45/// Test whether an entity tag list matches the current tag.
46pub fn etag_matches(header_value: Option<&str>, current_etag: &str) -> bool {
47    parse_etag_list(header_value)
48        .iter()
49        .any(|tag| tag == "*" || tag == current_etag)
50}
51
52/// Outcome of evaluating the conditional headers of a request.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct PreconditionResult {
55    /// Whether the response should be `304 Not Modified`.
56    pub not_modified: bool,
57}
58
59/// Evaluate the conditional request headers of a request.
60///
61/// # Errors
62///
63/// A `412` on a failed `If-Match`, and a `428` when one is required and absent.
64pub fn evaluate_preconditions(
65    headers: &Headers,
66    method: &str,
67    current_etag: &str,
68    require_precondition: bool,
69) -> Result<PreconditionResult, LinoHttpError> {
70    let safe = method == "GET" || method == "HEAD";
71    let if_match = headers.get("if-match");
72    let if_none_match = headers.get("if-none-match");
73
74    if !safe {
75        if let Some(if_match) = if_match {
76            if !etag_matches(Some(if_match), current_etag) {
77                return Err(LinoHttpError::new(
78                    412,
79                    Some("The entity tag in If-Match does not match the current representation"),
80                ));
81            }
82        } else if require_precondition {
83            return Err(LinoHttpError::new(
84                428,
85                Some("This request requires an If-Match precondition"),
86            ));
87        }
88    }
89
90    if let Some(if_none_match) = if_none_match {
91        if etag_matches(Some(if_none_match), current_etag) {
92            if safe {
93                return Ok(PreconditionResult { not_modified: true });
94            }
95            return Err(LinoHttpError::new(
96                412,
97                Some("The entity tag in If-None-Match matches the current representation"),
98            ));
99        }
100    }
101
102    Ok(PreconditionResult {
103        not_modified: false,
104    })
105}