Skip to main content

leyline/core/response/
link.rs

1use http::header::LINK;
2use url::Url;
3
4use super::Response;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub struct Link {
9    pub url: Url,
10    pub rel: Vec<String>,
11    pub params: Vec<(String, String)>,
12}
13
14impl Link {
15    pub fn has_rel(&self, rel: &str) -> bool {
16        self.rel.iter().any(|r| r.eq_ignore_ascii_case(rel))
17    }
18}
19
20impl Response {
21    pub fn links(&self) -> Vec<Link> {
22        let mut out = Vec::new();
23        for value in self.headers.get_all(LINK) {
24            if let Ok(text) = value.to_str() {
25                out.extend(parse_links(text, &self.url));
26            }
27        }
28        out
29    }
30
31    pub fn link(&self, rel: &str) -> Option<Url> {
32        self.links()
33            .into_iter()
34            .find(|link| link.has_rel(rel))
35            .map(|link| link.url)
36    }
37}
38
39pub(crate) fn parse_links(text: &str, base: &Url) -> Vec<Link> {
40    let mut out = Vec::new();
41    Parser::new(text).parse_into(base, &mut out);
42    out
43}
44
45struct Parser<'a> {
46    src: &'a str,
47    pos: usize,
48}
49
50impl<'a> Parser<'a> {
51    fn new(src: &'a str) -> Self {
52        Self { src, pos: 0 }
53    }
54
55    fn peek(&self) -> Option<u8> {
56        self.src.as_bytes().get(self.pos).copied()
57    }
58
59    fn skip_ws(&mut self) {
60        while matches!(self.peek(), Some(b' ' | b'\t')) {
61            self.pos += 1;
62        }
63    }
64
65    fn eat(&mut self, byte: u8) -> bool {
66        self.skip_ws();
67        let found = self.peek() == Some(byte);
68        if found {
69            self.pos += 1;
70        }
71        found
72    }
73
74    fn parse_into(mut self, base: &Url, out: &mut Vec<Link>) {
75        loop {
76            while self.eat(b',') {}
77            if self.peek().is_none() {
78                return;
79            }
80            match self.link_value(base) {
81                Some(link) => out.push(link),
82                None => self.skip_value(),
83            }
84        }
85    }
86
87    fn link_value(&mut self, base: &Url) -> Option<Link> {
88        if !self.eat(b'<') {
89            return None;
90        }
91        let end = self.src[self.pos..].find('>')? + self.pos;
92        let target = &self.src[self.pos..end];
93        self.pos = end + 1;
94        let mut rel = None;
95        let mut params = Vec::new();
96        while self.eat(b';') {
97            let Some((name, value)) = self.param() else {
98                continue;
99            };
100            if name.eq_ignore_ascii_case("rel") {
101                rel.get_or_insert(value);
102            } else {
103                params.push((name, value));
104            }
105        }
106        let url = base.join(target.trim()).ok()?;
107        let rel = rel
108            .map(|r| r.split_ascii_whitespace().map(str::to_owned).collect())
109            .unwrap_or_default();
110        Some(Link { url, rel, params })
111    }
112
113    fn param(&mut self) -> Option<(String, String)> {
114        self.skip_ws();
115        let name = self.token();
116        if name.is_empty() {
117            return None;
118        }
119        if !self.eat(b'=') {
120            return Some((name.to_ascii_lowercase(), String::new()));
121        }
122        self.skip_ws();
123        let value = if self.peek() == Some(b'"') {
124            self.quoted()
125        } else {
126            self.token().to_owned()
127        };
128        Some((name.to_ascii_lowercase(), value))
129    }
130
131    fn token(&mut self) -> &'a str {
132        let start = self.pos;
133        while self
134            .peek()
135            .is_some_and(|b| !matches!(b, b';' | b',' | b'=' | b' ' | b'\t' | b'"'))
136        {
137            self.pos += 1;
138        }
139        &self.src[start..self.pos]
140    }
141
142    fn quoted(&mut self) -> String {
143        self.pos += 1;
144        let mut out = String::new();
145        let mut chars = self.src[self.pos..].char_indices();
146        while let Some((offset, c)) = chars.next() {
147            match c {
148                '"' => {
149                    self.pos += offset + 1;
150                    return out;
151                }
152                '\\' => out.extend(chars.next().map(|(_, escaped)| escaped)),
153                other => out.push(other),
154            }
155        }
156        self.pos = self.src.len();
157        out
158    }
159
160    fn skip_value(&mut self) {
161        while let Some(b) = self.peek() {
162            match b {
163                b',' => return,
164                b'"' => {
165                    self.quoted();
166                }
167                b'<' => match self.src[self.pos..].find('>') {
168                    Some(end) => self.pos += end + 1,
169                    None => self.pos = self.src.len(),
170                },
171                _ => self.pos += 1,
172            }
173        }
174    }
175}