use std::io::BufRead;
use crate::cookie::cookie_prefix;
use crate::{Cookie, ParseError, ParseErrorKind};
pub struct NetscapeCookieParser<R> {
reader: R,
line: Vec<u8>,
line_number: usize,
done: bool,
}
impl<R: BufRead> NetscapeCookieParser<R> {
pub fn new(reader: R) -> Self {
Self {
reader,
line: Vec::new(),
line_number: 0,
done: false,
}
}
}
pub(crate) fn parse_line_inner(line: &[u8]) -> Result<Option<Cookie>, ParseErrorKind> {
let line = trim_line_end(line);
if line.iter().all(u8::is_ascii_whitespace) {
return Ok(None);
}
let (line, http_only) = if let Some(rest) = line.strip_prefix(b"#HttpOnly_") {
(rest, true)
} else if line.starts_with(b"#") {
return Ok(None);
} else {
(line, false)
};
if line.starts_with(b"#") {
return Ok(None);
}
let fields = fields(line);
if fields.len() != 7 {
return Err(ParseErrorKind::MissingFields {
found: fields.len(),
});
}
let name = fields[5].to_vec();
let value = fields[6].to_vec();
if has_invalid_octets(&name) || has_invalid_octets(&value) {
return Err(ParseErrorKind::InvalidOctets);
}
Ok(Some(Cookie {
domain: domain(fields[0]),
tail_match: fields[1].eq_ignore_ascii_case(b"TRUE"),
path: fields[2].to_vec(),
secure: fields[3].eq_ignore_ascii_case(b"TRUE"),
expires: parse_expires(fields[4])?,
prefix: cookie_prefix(&name),
name,
value,
http_only,
}))
}
fn trim_line_end(mut line: &[u8]) -> &[u8] {
if let Some(without_lf) = line.strip_suffix(b"\n") {
line = without_lf;
}
if let Some(without_cr) = line.strip_suffix(b"\r") {
line = without_cr;
}
line
}
fn fields(line: &[u8]) -> Vec<&[u8]> {
let mut fields: Vec<&[u8]> = line.split(|byte| *byte == b'\t').collect();
if fields.len() >= 4 && is_legacy_path_bool(fields[2]) {
fields.insert(2, b"/");
}
if fields.len() == 6 {
fields.push(b"");
}
fields
}
fn domain(domain: &[u8]) -> Vec<u8> {
domain.strip_prefix(b".").unwrap_or(domain).to_vec()
}
fn is_legacy_path_bool(value: &[u8]) -> bool {
b"TRUE".starts_with(value) || b"FALSE".starts_with(value)
}
fn parse_expires(value: &[u8]) -> Result<u64, ParseErrorKind> {
if value.is_empty() {
return Err(ParseErrorKind::InvalidExpires);
}
let mut number = 0u64;
for byte in value {
if !byte.is_ascii_digit() {
return Err(ParseErrorKind::InvalidExpires);
}
number = number
.checked_mul(10)
.and_then(|number| number.checked_add(u64::from(byte - b'0')))
.ok_or(ParseErrorKind::InvalidExpires)?;
}
Ok(number)
}
fn has_invalid_octets(value: &[u8]) -> bool {
value.iter().any(|byte| *byte < 0x20 || *byte == 0x7f)
}
impl<R: BufRead> Iterator for NetscapeCookieParser<R> {
type Item = Result<Cookie, ParseError>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
loop {
self.line.clear();
match self.reader.read_until(b'\n', &mut self.line) {
Ok(0) => {
self.done = true;
return None;
}
Ok(_) => {
self.line_number += 1;
match parse_line_inner(&self.line) {
Ok(Some(cookie)) => return Some(Ok(cookie)),
Ok(None) => continue,
Err(kind) => {
return Some(Err(ParseError {
line: self.line_number,
kind,
}));
}
}
}
Err(error) => {
self.done = true;
return Some(Err(ParseError {
line: self.line_number + 1,
kind: ParseErrorKind::Io(error),
}));
}
}
}
}
}