use http::header;
use http::header::{HeaderName, HeaderValue};
use std::collections::{hash_map::Entry, HashMap};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid line `{0}`")]
InvalidLine(String),
#[error("invalid header name")]
InvalidHeaderName(#[from] header::InvalidHeaderName),
#[error("invalid header value")]
InvalidHeaderValue(#[from] header::InvalidHeaderValue),
#[error("invalid file path")]
InvalidPath(#[from] std::io::Error),
}
pub struct HeaderMap {
inner: header::HeaderMap,
}
pub type Headers = HashMap<String, HeaderMap>;
#[derive(Debug, PartialEq)]
enum Line<'a> {
Empty,
Comment,
Target(&'a str),
Header((&'a str, &'a str)),
}
struct State {
target: Option<String>,
headers: Headers,
}
impl State {
fn new() -> Self {
State {
target: None,
headers: Headers::new(),
}
}
fn set_target(&mut self, t: &str) {
self.target = Some(t.to_owned())
}
fn append_headers(&mut self, key: &str, value: &str) -> Result<(), Error> {
let hn = HeaderName::from_bytes(key.as_bytes()).map_err(Error::InvalidHeaderName)?;
let hv = HeaderValue::from_str(value).map_err(Error::InvalidHeaderValue)?;
if let Some(h) = &self.target {
match self.headers.entry(h.to_owned()) {
Entry::Occupied(mut e) => {
e.get_mut().append(hn, hv);
}
Entry::Vacant(e) => {
let mut values = HeaderMap::new();
values.insert(hn, hv);
e.insert(values);
}
}
}
Ok(())
}
}
impl HeaderMap {
fn new() -> Self {
HeaderMap {
inner: header::HeaderMap::new(),
}
}
pub fn get_string(&self, header: &str) -> String {
self.inner
.get_all(header)
.iter()
.flat_map(|h| h.to_str().ok())
.collect::<Vec<_>>()
.join(",")
.to_owned()
}
pub fn append(&mut self, key: HeaderName, value: HeaderValue) -> bool {
self.inner.append(key, value)
}
pub fn insert(&mut self, key: HeaderName, value: HeaderValue) -> Option<HeaderValue> {
self.inner.insert(key, value)
}
pub fn get_all(&self) -> header::HeaderMap {
self.inner.clone()
}
}
pub fn from_path<T: AsRef<Path>>(path: T) -> Result<Headers, Error> {
let file = File::open(&path).map_err(Error::InvalidPath)?;
parse(BufReader::new(file))
}
pub fn parse<T: BufRead>(io: T) -> Result<Headers, Error> {
let mut state = State::new();
for res in io.lines() {
if let Ok(line) = res {
match parse_line(&line)? {
Line::Target(s) => state.set_target(s),
Line::Header((key, value)) => state.append_headers(key, value)?,
Line::Empty | Line::Comment => {}
}
}
}
Ok(state.headers)
}
fn parse_line(line: &str) -> Result<Line, Error> {
let line = line.trim();
if line.is_empty() {
return Ok(Line::Empty);
}
let c = line.chars().next().unwrap_or_default();
if c == '#' {
return Ok(Line::Comment);
}
if c == '/' {
return Ok(Line::Target(line));
}
if line.starts_with("http://") || line.starts_with("https://") {
return Ok(Line::Target(line));
}
let mut header = line.splitn(2, ':');
if let (Some(key), Some(value)) = (header.next(), header.next()) {
return Ok(Line::Header((key.trim(), value.trim())));
}
Err(Error::InvalidLine(line.to_owned()))
}
#[cfg(test)]
mod tests {
use crate::{parse_line, Line};
#[test]
fn test_parse_line_with_target() {
let line = "/path/index.html";
assert_eq!(Line::Target(line), parse_line(line).unwrap());
let line = "https://example.com/*";
assert_eq!(Line::Target(line), parse_line(line).unwrap());
let line = "http://example.com/*";
assert_eq!(Line::Target(line), parse_line(line).unwrap());
}
#[test]
fn test_parse_line_with_ignored_lines() {
assert_eq!(Line::Empty, parse_line(" ").unwrap());
assert_eq!(Line::Comment, parse_line("# comment").unwrap());
}
#[test]
fn test_parse_line_with_key_value_headers() {
assert_eq!(
Line::Header(("foo", "bar")),
parse_line("foo: bar").unwrap()
);
assert_eq!(
Line::Header(("foo", "bar : baz")),
parse_line("foo: bar : baz").unwrap()
);
}
#[test]
fn test_parse_line_with_invalid_lines() {
assert!(parse_line("text without any meaning").is_err());
}
}