1use http::header;
2use http::header::{HeaderName, HeaderValue};
3use std::collections::{hash_map::Entry, HashMap};
4use std::fs::File;
5use std::io::{BufRead, BufReader};
6use std::path::Path;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum Error {
11 #[error("invalid line `{0}`")]
12 InvalidLine(String),
13 #[error("invalid header name")]
14 InvalidHeaderName(#[from] header::InvalidHeaderName),
15 #[error("invalid header value")]
16 InvalidHeaderValue(#[from] header::InvalidHeaderValue),
17 #[error("invalid file path")]
18 InvalidPath(#[from] std::io::Error),
19}
20
21pub struct HeaderMap {
22 inner: header::HeaderMap,
23}
24
25pub type Headers = HashMap<String, HeaderMap>;
26
27#[derive(Debug, PartialEq)]
28enum Line<'a> {
29 Empty,
30 Comment,
31 Target(&'a str),
32 Header((&'a str, &'a str)),
33}
34
35struct State {
36 target: Option<String>,
37 headers: Headers,
38}
39
40impl State {
41 fn new() -> Self {
42 State {
43 target: None,
44 headers: Headers::new(),
45 }
46 }
47 fn set_target(&mut self, t: &str) {
48 self.target = Some(t.to_owned())
49 }
50
51 fn append_headers(&mut self, key: &str, value: &str) -> Result<(), Error> {
52 let hn = HeaderName::from_bytes(key.as_bytes()).map_err(Error::InvalidHeaderName)?;
53 let hv = HeaderValue::from_str(value).map_err(Error::InvalidHeaderValue)?;
54
55 if let Some(h) = &self.target {
56 match self.headers.entry(h.to_owned()) {
57 Entry::Occupied(mut e) => {
58 e.get_mut().append(hn, hv);
59 }
60 Entry::Vacant(e) => {
61 let mut values = HeaderMap::new();
62 values.insert(hn, hv);
63 e.insert(values);
64 }
65 }
66 }
67
68 Ok(())
69 }
70}
71
72impl HeaderMap {
73 fn new() -> Self {
74 HeaderMap {
75 inner: header::HeaderMap::new(),
76 }
77 }
78
79 pub fn get_string(&self, header: &str) -> String {
80 self.inner
81 .get_all(header)
82 .iter()
83 .flat_map(|h| h.to_str().ok())
84 .collect::<Vec<_>>()
85 .join(",")
86 .to_owned()
87 }
88
89 pub fn append(&mut self, key: HeaderName, value: HeaderValue) -> bool {
90 self.inner.append(key, value)
91 }
92
93 pub fn insert(&mut self, key: HeaderName, value: HeaderValue) -> Option<HeaderValue> {
94 self.inner.insert(key, value)
95 }
96
97 pub fn get_all(&self) -> header::HeaderMap {
98 self.inner.clone()
99 }
100}
101
102pub fn from_path<T: AsRef<Path>>(path: T) -> Result<Headers, Error> {
103 let file = File::open(&path).map_err(Error::InvalidPath)?;
104 parse(BufReader::new(file))
105}
106
107pub fn parse<T: BufRead>(io: T) -> Result<Headers, Error> {
108 let mut state = State::new();
109
110 for res in io.lines() {
111 if let Ok(line) = res {
112 match parse_line(&line)? {
113 Line::Target(s) => state.set_target(s),
114 Line::Header((key, value)) => state.append_headers(key, value)?,
115 Line::Empty | Line::Comment => {}
116 }
117 }
118 }
119
120 Ok(state.headers)
121}
122
123fn parse_line(line: &str) -> Result<Line, Error> {
124 let line = line.trim();
125 if line.is_empty() {
126 return Ok(Line::Empty);
127 }
128
129 let c = line.chars().next().unwrap_or_default();
130 if c == '#' {
131 return Ok(Line::Comment);
132 }
133
134 if c == '/' {
135 return Ok(Line::Target(line));
136 }
137
138 if line.starts_with("http://") || line.starts_with("https://") {
139 return Ok(Line::Target(line));
140 }
141
142 let mut header = line.splitn(2, ':');
143
144 if let (Some(key), Some(value)) = (header.next(), header.next()) {
145 return Ok(Line::Header((key.trim(), value.trim())));
146 }
147
148 Err(Error::InvalidLine(line.to_owned()))
149}
150
151#[cfg(test)]
152mod tests {
153 use crate::{parse_line, Line};
154 #[test]
155 fn test_parse_line_with_target() {
156 let line = "/path/index.html";
157 assert_eq!(Line::Target(line), parse_line(line).unwrap());
158 let line = "https://example.com/*";
159 assert_eq!(Line::Target(line), parse_line(line).unwrap());
160 let line = "http://example.com/*";
161 assert_eq!(Line::Target(line), parse_line(line).unwrap());
162 }
163
164 #[test]
165 fn test_parse_line_with_ignored_lines() {
166 assert_eq!(Line::Empty, parse_line(" ").unwrap());
167 assert_eq!(Line::Comment, parse_line("# comment").unwrap());
168 }
169
170 #[test]
171 fn test_parse_line_with_key_value_headers() {
172 assert_eq!(
173 Line::Header(("foo", "bar")),
174 parse_line("foo: bar").unwrap()
175 );
176 assert_eq!(
177 Line::Header(("foo", "bar : baz")),
178 parse_line("foo: bar : baz").unwrap()
179 );
180 }
181
182 #[test]
183 fn test_parse_line_with_invalid_lines() {
184 assert!(parse_line("text without any meaning").is_err());
185 }
186}