use super::{BareItem, parse::INTEGER_MAX};
use crate::{
class,
error::WriteError,
write::{CountSink, Sink, reserve, write_decimal_u64},
};
type WResult<T> = core::result::Result<T, WriteError>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Kind {
Item,
List,
Dictionary,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum State {
Empty,
Member,
Key,
Inner,
InnerItem,
}
#[derive(Clone, Copy)]
enum Val<'v> {
Bare(&'v BareItem<'v>),
Integer(i64),
Decimal(i64),
String(&'v str),
Token(&'v str),
Boolean(bool),
Date(i64),
Display(&'v str),
}
#[derive(Debug)]
pub struct Writer<'s, S: Sink> {
out: &'s mut S,
kind: Kind,
state: State,
pending: bool,
}
impl<'s, S: Sink> Writer<'s, S> {
pub fn item(out: &'s mut S) -> Self {
Self {
out,
kind: Kind::Item,
state: State::Empty,
pending: false,
}
}
pub fn list(out: &'s mut S) -> Self {
Self {
out,
kind: Kind::List,
state: State::Empty,
pending: false,
}
}
pub fn dictionary(out: &'s mut S) -> Self {
Self {
out,
kind: Kind::Dictionary,
state: State::Empty,
pending: false,
}
}
fn emit(&mut self, prefix: &[u8], key: Option<&str>, value: Option<Val<'_>>) -> WResult<()> {
let mut count = CountSink::new();
put(prefix, key, value, &mut count)?;
reserve(&*self.out, count.count())?;
put(prefix, key, value, &mut *self.out)
}
fn value(&mut self, v: Val<'_>) -> WResult<&mut Self> {
if self.pending {
if !matches!(v, Val::Boolean(true) | Val::Bare(BareItem::Boolean(true))) {
self.emit(b"=", None, Some(v))?;
}
self.pending = false;
return Ok(self);
}
match (self.kind, self.state) {
(Kind::Item, State::Empty) => self.emit(b"", None, Some(v))?,
(Kind::List, State::Empty) => self.emit(b"", None, Some(v))?,
(Kind::List, State::Member) => self.emit(b", ", None, Some(v))?,
(Kind::Dictionary, State::Key) => {
if !matches!(v, Val::Boolean(true) | Val::Bare(BareItem::Boolean(true))) {
self.emit(b"=", None, Some(v))?;
}
}
(_, State::Inner) => {
self.emit(b"", None, Some(v))?;
self.state = State::InnerItem;
return Ok(self);
}
(_, State::InnerItem) => {
self.emit(b" ", None, Some(v))?;
return Ok(self);
}
_ => return Err(WriteError::InvalidSequence),
}
self.state = State::Member;
Ok(self)
}
pub fn bare(&mut self, b: &BareItem<'_>) -> WResult<&mut Self> {
self.value(Val::Bare(b))
}
pub fn integer(&mut self, v: i64) -> WResult<&mut Self> {
self.value(Val::Integer(v))
}
pub fn decimal(&mut self, thousandths: i64) -> WResult<&mut Self> {
self.value(Val::Decimal(thousandths))
}
pub fn string(&mut self, s: &str) -> WResult<&mut Self> {
self.value(Val::String(s))
}
pub fn token(&mut self, t: &str) -> WResult<&mut Self> {
self.value(Val::Token(t))
}
pub fn boolean(&mut self, b: bool) -> WResult<&mut Self> {
self.value(Val::Boolean(b))
}
pub fn date(&mut self, secs: i64) -> WResult<&mut Self> {
self.value(Val::Date(secs))
}
pub fn display_string(&mut self, s: &str) -> WResult<&mut Self> {
self.value(Val::Display(s))
}
pub fn param(&mut self, key: &str, v: &BareItem<'_>) -> WResult<&mut Self> {
if self.pending || !matches!(self.state, State::Member | State::Key | State::InnerItem) {
return Err(WriteError::InvalidSequence);
}
let value = (!matches!(v, BareItem::Boolean(true))).then_some(Val::Bare(v));
self.emit(b";", Some(key), value)?;
if self.state == State::Key {
self.state = State::Member;
}
Ok(self)
}
pub fn param_key(&mut self, key: &str) -> WResult<&mut Self> {
if self.pending || !matches!(self.state, State::Member | State::Key | State::InnerItem) {
return Err(WriteError::InvalidSequence);
}
self.emit(b";", Some(key), None)?;
if self.state == State::Key {
self.state = State::Member;
}
self.pending = true;
Ok(self)
}
pub fn key(&mut self, key: &str) -> WResult<&mut Self> {
if self.pending {
return Err(WriteError::InvalidSequence);
}
let prefix: &[u8] = match (self.kind, self.state) {
(Kind::Dictionary, State::Empty) => b"",
(Kind::Dictionary, State::Member | State::Key) => b", ",
_ => return Err(WriteError::InvalidSequence),
};
self.emit(prefix, Some(key), None)?;
self.state = State::Key;
Ok(self)
}
pub fn open_inner_list(&mut self) -> WResult<&mut Self> {
if self.pending {
return Err(WriteError::InvalidSequence);
}
let prefix: &[u8] = match (self.kind, self.state) {
(Kind::List, State::Empty) => b"(",
(Kind::List, State::Member) => b", (",
(Kind::Dictionary, State::Key) => b"=(",
_ => return Err(WriteError::InvalidSequence),
};
self.emit(prefix, None, None)?;
self.state = State::Inner;
Ok(self)
}
pub fn close_inner_list(&mut self) -> WResult<&mut Self> {
if self.pending || !matches!(self.state, State::Inner | State::InnerItem) {
return Err(WriteError::InvalidSequence);
}
self.emit(b")", None, None)?;
self.state = State::Member;
Ok(self)
}
pub fn finish(self) -> WResult<()> {
if self.pending {
return Err(WriteError::InvalidSequence);
}
match self.state {
State::Member | State::Key => Ok(()),
State::Empty | State::Inner | State::InnerItem => Err(WriteError::InvalidSequence),
}
}
}
fn put_display_byte(b: u8, out: &mut impl Sink) -> WResult<()> {
if b == b'%' || b == b'"' || !(0x20..=0x7e).contains(&b) {
const HEX: &[u8; 16] = b"0123456789abcdef";
let hi = HEX.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
let lo = HEX.get(usize::from(b & 0xf)).copied().unwrap_or(b'0');
out.put(&[b'%', hi, lo])?;
} else {
out.put(&[b])?;
}
Ok(())
}
const fn b64_value(c: u8) -> u8 {
match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
_ => 63,
}
}
const fn b64_symbol(v: u8) -> u8 {
match v {
0..=25 => b'A' + v,
26..=51 => b'a' + (v - 26),
52..=61 => b'0' + (v - 52),
62 => b'+',
_ => b'/',
}
}
fn is_key(k: &[u8]) -> bool {
matches!(k.first(), Some(&b) if class::is_sf_key_start(b))
&& k.iter().skip(1).all(|&b| class::is_sf_key_char(b))
}
fn is_sf_token(t: &[u8]) -> bool {
matches!(t.first(), Some(&b) if b == b'*' || b.is_ascii_alphabetic())
&& t.iter().skip(1).all(|&b| class::is_sf_token_char(b))
}
fn put(
prefix: &[u8],
key: Option<&str>,
value: Option<Val<'_>>,
out: &mut impl Sink,
) -> WResult<()> {
out.put(prefix)?;
if let Some(k) = key {
if !is_key(k.as_bytes()) {
return Err(WriteError::NotToken);
}
out.put(k.as_bytes())?;
if value.is_some() {
out.put(b"=")?;
}
}
match value {
None => Ok(()),
Some(v) => put_value(v, out),
}
}
fn put_integer(v: i64, out: &mut impl Sink) -> WResult<()> {
if !(-INTEGER_MAX..=INTEGER_MAX).contains(&v) {
return Err(WriteError::OutOfRange);
}
if v < 0 {
out.put(b"-")?;
}
write_decimal_u64(v.unsigned_abs(), out)
}
fn put_value(v: Val<'_>, out: &mut impl Sink) -> WResult<()> {
match v {
Val::Integer(i) => put_integer(i, out),
Val::Decimal(t) => {
if !(-INTEGER_MAX..=INTEGER_MAX).contains(&t) {
return Err(WriteError::OutOfRange);
}
if t < 0 {
out.put(b"-")?;
}
let a = t.unsigned_abs();
write_decimal_u64(a / 1000, out)?;
out.put(b".")?;
let frac = a % 1000;
let digits = [
b'0' + (frac / 100) as u8,
b'0' + (frac / 10 % 10) as u8,
b'0' + (frac % 10) as u8,
];
let keep = if frac % 100 == 0 {
1
} else if frac % 10 == 0 {
2
} else {
3
};
out.put(digits.get(..keep).unwrap_or(&[]))?;
Ok(())
}
Val::String(s) => {
if let Some(at) = s.bytes().position(|b| !(0x20..=0x7e).contains(&b)) {
return Err(WriteError::forbidden(at));
}
out.put(b"\"")?;
for chunk in s.as_bytes().split_inclusive(|&b| b == b'"' || b == b'\\') {
match chunk.split_last() {
Some((&last, head)) if last == b'"' || last == b'\\' => {
out.put(head)?;
out.put(b"\\")?;
out.put(&[last])?;
}
_ => out.put(chunk)?,
}
}
out.put(b"\"")?;
Ok(())
}
Val::Token(t) => {
if !is_sf_token(t.as_bytes()) {
return Err(WriteError::NotToken);
}
out.put(t.as_bytes())?;
Ok(())
}
Val::Boolean(b) => {
out.put(if b { b"?1" } else { b"?0" })?;
Ok(())
}
Val::Date(d) => {
if d < crate::HttpDate::MIN.unix() || d > crate::HttpDate::MAX.unix() {
return Err(WriteError::OutOfRange);
}
out.put(b"@")?;
put_integer(d, out)
}
Val::Display(s) => {
out.put(b"%\"")?;
for &b in s.as_bytes() {
put_display_byte(b, out)?;
}
out.put(b"\"")?;
Ok(())
}
Val::Bare(b) => match *b {
BareItem::Integer(i) => put_integer(i, out),
BareItem::Decimal(d) => put_value(Val::Decimal(d.thousandths()), out),
BareItem::String(s) => {
out.put(b"\"")?;
out.put(s.raw().as_bytes())?;
out.put(b"\"")?;
Ok(())
}
BareItem::Token(t) => put_value(Val::Token(t), out),
BareItem::ByteSeq(s) => {
let body: &[u8] = s.raw().split(|&b| b == b'=').next().unwrap_or(&[]);
out.put(b":")?;
let (pad, mask): (&[u8], u8) = match body.len() % 4 {
2 => (b"==", 0x30),
3 => (b"=", 0x3c),
_ => (b"", 0x3f),
};
if let Some((&last, head)) = body.split_last() {
out.put(head)?;
out.put(&[b64_symbol(b64_value(last) & mask)])?;
}
out.put(pad)?;
out.put(b":")?;
Ok(())
}
BareItem::Boolean(v) => put_value(Val::Boolean(v), out),
BareItem::Date(d) => put_value(Val::Date(d), out),
BareItem::DisplayString(s) => {
out.put(b"%\"")?;
let raw = s.raw();
let mut i = 0usize;
while let Some(&b) = raw.get(i) {
let byte = if b == b'%' {
let hi = raw
.get(i + 1)
.copied()
.and_then(class::hex_value)
.unwrap_or(0);
let lo = raw
.get(i + 2)
.copied()
.and_then(class::hex_value)
.unwrap_or(0);
i += 3;
(hi << 4) | lo
} else {
i += 1;
b
};
put_display_byte(byte, out)?;
}
out.put(b"\"")?;
Ok(())
}
},
}
}
#[cfg(test)]
#[allow(unused_results)]
mod tests {
use super::*;
use crate::{SliceSink, sfv};
fn item(f: impl FnOnce(&mut Writer<'_, SliceSink<'_>>) -> WResult<()>) -> std::string::String {
let mut buf = [0u8; 256];
let mut out = SliceSink::new(&mut buf);
let mut w = Writer::item(&mut out);
f(&mut w).unwrap();
w.finish().unwrap();
std::string::String::from_utf8(out.written().to_vec()).unwrap()
}
#[test]
fn test_writer_bare_items_canonical() {
assert_eq!(
item(|w| w.integer(-999_999_999_999_999).map(|_| ())),
"-999999999999999"
);
assert_eq!(item(|w| w.decimal(1500).map(|_| ())), "1.5");
assert_eq!(item(|w| w.decimal(-1000).map(|_| ())), "-1.0");
assert_eq!(item(|w| w.decimal(-5).map(|_| ())), "-0.005");
assert_eq!(item(|w| w.decimal(1230).map(|_| ())), "1.23");
assert_eq!(item(|w| w.string("a\"b\\c").map(|_| ())), "\"a\\\"b\\\\c\"");
assert_eq!(item(|w| w.token("*foo/bar:1").map(|_| ())), "*foo/bar:1");
assert_eq!(item(|w| w.boolean(false).map(|_| ())), "?0");
assert_eq!(item(|w| w.date(0).map(|_| ())), "@0");
assert_eq!(
item(|w| w.display_string("f\u{fc}%\"").map(|_| ())),
"%\"f%c3%bc%25%22\""
);
let parsed = sfv::parse_item(b":YQ:").unwrap();
assert_eq!(item(|w| w.bare(&parsed.bare).map(|_| ())), ":YQ==:");
let parsed = sfv::parse_item(b":iZ==:").unwrap();
assert_eq!(item(|w| w.bare(&parsed.bare).map(|_| ())), ":iQ==:");
let parsed = sfv::parse_item(b":YWJ=:").unwrap();
assert_eq!(item(|w| w.bare(&parsed.bare).map(|_| ())), ":YWI=:");
let parsed = sfv::parse_item(b"::").unwrap();
assert_eq!(item(|w| w.bare(&parsed.bare).map(|_| ())), "::");
}
#[test]
fn test_writer_refusals_write_nothing() {
let mut buf = [0u8; 64];
let mut out = SliceSink::new(&mut buf);
let mut w = Writer::item(&mut out);
assert_eq!(
w.integer(1_000_000_000_000_000).err(),
Some(WriteError::OutOfRange)
);
assert_eq!(
w.decimal(1_000_000_000_000_000).err(),
Some(WriteError::OutOfRange)
);
assert_eq!(w.date(253_402_300_800).err(), Some(WriteError::OutOfRange));
assert_eq!(
w.string("caf\u{e9}").err(),
Some(WriteError::Forbidden { at: 3 })
);
assert_eq!(
w.string("a\nb").err(),
Some(WriteError::Forbidden { at: 1 })
);
assert_eq!(w.token("1a").err(), Some(WriteError::NotToken));
assert_eq!(w.token("").err(), Some(WriteError::NotToken));
assert_eq!(
w.param("a", &BareItem::Integer(1)).err(),
Some(WriteError::InvalidSequence)
);
assert_eq!(w.key("a").err(), Some(WriteError::InvalidSequence));
assert_eq!(w.open_inner_list().err(), Some(WriteError::InvalidSequence));
w.integer(1).unwrap();
assert_eq!(w.integer(2).err(), Some(WriteError::InvalidSequence));
assert_eq!(
w.param("A", &BareItem::Integer(1)).err(),
Some(WriteError::NotToken)
);
assert_eq!(
w.param("1a", &BareItem::Integer(1)).err(),
Some(WriteError::NotToken)
);
w.finish().unwrap();
assert_eq!(out.written(), b"1");
}
#[test]
fn test_writer_list_and_inner_lists() {
let mut buf = [0u8; 128];
let mut out = SliceSink::new(&mut buf);
let mut w = Writer::list(&mut out);
w.token("a")
.unwrap()
.param("x", &BareItem::Boolean(true))
.unwrap();
w.open_inner_list().unwrap();
assert_eq!(
w.param("p", &BareItem::Integer(1)).err(),
Some(WriteError::InvalidSequence)
);
w.integer(1)
.unwrap()
.param("q", &BareItem::Boolean(false))
.unwrap()
.integer(2)
.unwrap();
w.close_inner_list()
.unwrap()
.param("lvl", &BareItem::Integer(5))
.unwrap();
w.open_inner_list().unwrap().close_inner_list().unwrap();
w.finish().unwrap();
assert_eq!(out.written(), b"a;x, (1;q=?0 2);lvl=5, ()");
assert!(sfv::parse_list(out.written()).is_ok());
}
#[test]
fn test_writer_param_key_then_value() {
let mut buf = [0u8; 64];
let mut out = SliceSink::new(&mut buf);
let mut w = Writer::list(&mut out);
w.token("a")
.unwrap()
.param_key("s")
.unwrap()
.string("x y")
.unwrap();
assert_eq!(
w.param_key("t").unwrap().key("k").err(),
Some(WriteError::InvalidSequence)
);
assert_eq!(
w.param("u", &BareItem::Integer(1)).err(),
Some(WriteError::InvalidSequence)
);
w.decimal(250).unwrap();
w.open_inner_list()
.unwrap()
.integer(1)
.unwrap()
.param_key("d")
.unwrap()
.display_string("\u{e9}")
.unwrap();
w.close_inner_list()
.unwrap()
.param_key("b")
.unwrap()
.boolean(true)
.unwrap();
w.finish().unwrap();
assert_eq!(out.written(), br#"a;s="x y";t=0.25, (1;d=%"%c3%a9");b"#);
let mut buf = [0u8; 16];
let mut out = SliceSink::new(&mut buf);
let mut w = Writer::item(&mut out);
w.integer(1).unwrap().param_key("k").unwrap();
assert_eq!(w.finish(), Err(WriteError::InvalidSequence));
}
#[test]
fn test_writer_finish_refusals() {
let mut buf = [0u8; 16];
let mut out = SliceSink::new(&mut buf);
assert_eq!(
Writer::list(&mut out).finish(),
Err(WriteError::InvalidSequence)
);
assert_eq!(
Writer::dictionary(&mut out).finish(),
Err(WriteError::InvalidSequence)
);
assert_eq!(
Writer::item(&mut out).finish(),
Err(WriteError::InvalidSequence)
);
let mut w = Writer::list(&mut out);
w.open_inner_list().unwrap();
assert_eq!(w.finish(), Err(WriteError::InvalidSequence));
let mut w = Writer::dictionary(&mut out);
assert_eq!(w.integer(1).err(), Some(WriteError::InvalidSequence));
assert_eq!(w.open_inner_list().err(), Some(WriteError::InvalidSequence));
}
#[test]
fn test_writer_dictionary_true_is_key_alone() {
let mut buf = [0u8; 128];
let mut out = SliceSink::new(&mut buf);
let mut w = Writer::dictionary(&mut out);
w.key("a").unwrap().boolean(true).unwrap();
w.key("b").unwrap();
w.key("c")
.unwrap()
.param("p", &BareItem::Integer(1))
.unwrap();
w.key("d").unwrap().boolean(false).unwrap();
w.finish().unwrap();
assert_eq!(out.written(), b"a, b, c;p=1, d=?0");
assert!(sfv::parse_dictionary(out.written()).is_ok());
}
#[test]
fn test_writer_full_writes_nothing_for_the_call() {
let mut buf = [0u8; 4];
let mut out = SliceSink::new(&mut buf);
let mut w = Writer::item(&mut out);
assert_eq!(w.string("abcd").err(), Some(WriteError::Full));
w.token("ab").unwrap();
assert_eq!(
w.param("cd", &BareItem::Integer(1)).err(),
Some(WriteError::Full)
);
w.finish().unwrap();
assert_eq!(out.written(), b"ab");
}
}