2022-11-17 21:53:36 +00:00
|
|
|
//! Functions to generate nettext representations of data structures
|
|
|
|
|
2022-11-17 16:55:50 +00:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
2022-11-17 22:58:44 +00:00
|
|
|
use crate::dec::{self, decode};
|
2022-11-17 16:55:50 +00:00
|
|
|
use crate::{is_string_char, is_whitespace};
|
|
|
|
|
2022-11-17 22:58:44 +00:00
|
|
|
/// A term meant to be encoded into a nettext representation
|
2022-11-17 16:55:50 +00:00
|
|
|
pub struct Term<'a>(T<'a>);
|
|
|
|
|
|
|
|
enum T<'a> {
|
|
|
|
Str(&'a [u8]),
|
|
|
|
OwnedStr(Vec<u8>),
|
|
|
|
Dict(HashMap<&'a [u8], T<'a>>),
|
|
|
|
List(Vec<T<'a>>),
|
2022-11-17 22:58:44 +00:00
|
|
|
Err(Error),
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
|
2022-11-17 22:58:44 +00:00
|
|
|
/// An error that happenned when creating a nettext encoder term
|
2022-11-17 16:55:50 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum Error {
|
|
|
|
InvalidCharacter(u8),
|
|
|
|
InvalidRaw,
|
|
|
|
NotADictionnary,
|
|
|
|
}
|
|
|
|
|
2022-11-17 22:58:44 +00:00
|
|
|
// ---- helpers to transform datatypes into encoder terms ----
|
|
|
|
|
|
|
|
/// Trait for anything that can be encoded as nettext
|
|
|
|
pub trait Encode {
|
|
|
|
fn term(&self) -> Term<'_>;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, 'b> Encode for dec::Term<'a, 'b> {
|
|
|
|
fn term(&self) -> Term<'_> {
|
|
|
|
Term(T::Str(self.raw()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-17 16:55:50 +00:00
|
|
|
// ---- helpers to build terms ----
|
|
|
|
|
2022-11-17 22:58:44 +00:00
|
|
|
/// Term corresponding to a string (that may contain whitespace)
|
2022-11-17 16:55:50 +00:00
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// use nettext::enc::*;
|
|
|
|
///
|
2022-11-17 22:58:44 +00:00
|
|
|
/// assert_eq!(encode(string("Hello world .")).unwrap(), b"Hello world .");
|
2022-11-17 16:55:50 +00:00
|
|
|
/// ```
|
2022-11-17 22:58:44 +00:00
|
|
|
pub fn string(s: &str) -> Term<'_> {
|
2022-11-17 16:55:50 +00:00
|
|
|
for c in s.as_bytes().iter() {
|
|
|
|
if !(is_string_char(*c) || is_whitespace(*c)) {
|
2022-11-17 22:58:44 +00:00
|
|
|
return Term(T::Err(Error::InvalidCharacter(*c)));
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
}
|
2022-11-17 22:58:44 +00:00
|
|
|
Term(T::Str(s.as_bytes()))
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Include a raw nettext value
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// use nettext::enc::*;
|
|
|
|
///
|
2022-11-17 22:58:44 +00:00
|
|
|
/// assert_eq!(encode(raw(b"Hello { a = b, c = d} .")).unwrap(), b"Hello { a = b, c = d} .");
|
2022-11-17 16:55:50 +00:00
|
|
|
/// ```
|
2022-11-17 22:58:44 +00:00
|
|
|
pub fn raw(bytes: &[u8]) -> Term<'_> {
|
2022-11-17 16:55:50 +00:00
|
|
|
if decode(bytes).is_err() {
|
2022-11-17 22:58:44 +00:00
|
|
|
return Term(T::Err(Error::InvalidRaw));
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
2022-11-17 22:58:44 +00:00
|
|
|
Term(T::Str(bytes))
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
|
2022-11-17 22:58:44 +00:00
|
|
|
/// Term corresponding to a list of terms
|
2022-11-17 16:55:50 +00:00
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// use nettext::enc::*;
|
|
|
|
///
|
2022-11-17 22:58:44 +00:00
|
|
|
/// assert_eq!(encode(list([
|
2022-11-17 23:01:23 +00:00
|
|
|
/// string("Hello"),
|
|
|
|
/// string("world")
|
|
|
|
/// ])).unwrap(), b"Hello world");
|
2022-11-17 16:55:50 +00:00
|
|
|
/// ```
|
|
|
|
pub fn list<'a, I: IntoIterator<Item = Term<'a>>>(terms: I) -> Term<'a> {
|
2022-11-17 22:58:44 +00:00
|
|
|
let mut tmp = Vec::with_capacity(8);
|
|
|
|
for t in terms {
|
|
|
|
match t.0 {
|
|
|
|
T::Err(e) => return Term(T::Err(e)),
|
|
|
|
x => tmp.push(x),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Term(T::List(tmp))
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
|
2022-11-17 22:58:44 +00:00
|
|
|
/// Term corresponding to a dictionnary of items
|
2022-11-17 16:55:50 +00:00
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// use nettext::enc::*;
|
|
|
|
///
|
2022-11-17 22:58:44 +00:00
|
|
|
/// assert_eq!(encode(dict([
|
2022-11-17 23:01:23 +00:00
|
|
|
/// ("a", string("Hello")),
|
|
|
|
/// ("b", string("world"))
|
|
|
|
/// ])).unwrap(), b"{\n a = Hello,\n b = world,\n}");
|
2022-11-17 16:55:50 +00:00
|
|
|
/// ```
|
|
|
|
pub fn dict<'a, I: IntoIterator<Item = (&'a str, Term<'a>)>>(pairs: I) -> Term<'a> {
|
2022-11-17 22:58:44 +00:00
|
|
|
let mut tmp = HashMap::new();
|
|
|
|
for (k, v) in pairs {
|
|
|
|
match v.0 {
|
|
|
|
T::Err(e) => return Term(T::Err(e)),
|
|
|
|
vv => {
|
|
|
|
tmp.insert(k.as_bytes(), vv);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Term(T::Dict(tmp))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Term corresponding to a byte slice,
|
|
|
|
/// encoding using base64 url-safe encoding without padding
|
|
|
|
///
|
|
|
|
/// Example:
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// use nettext::enc::*;
|
|
|
|
///
|
|
|
|
/// assert_eq!(encode(bytes(b"hello, world!")).unwrap(), b"aGVsbG8sIHdvcmxkIQ");
|
|
|
|
/// ```
|
|
|
|
pub fn bytes(b: &[u8]) -> Term<'static> {
|
|
|
|
Term(T::OwnedStr(
|
|
|
|
base64::encode_config(b, base64::URL_SAFE_NO_PAD).into_bytes(),
|
2022-11-17 16:55:50 +00:00
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Term<'a> {
|
2022-11-17 22:58:44 +00:00
|
|
|
/// Append a term to an existing term.
|
|
|
|
/// Transforms the initial term into a list if necessary.
|
|
|
|
pub fn append(self, t: Term<'a>) -> Term<'a> {
|
|
|
|
match t.0 {
|
|
|
|
T::Err(e) => Term(T::Err(e)),
|
|
|
|
tt => match self.0 {
|
|
|
|
T::List(mut v) => {
|
|
|
|
v.push(tt);
|
|
|
|
Term(T::List(v))
|
|
|
|
}
|
|
|
|
x => Term(T::List(vec![x, tt])),
|
|
|
|
},
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-17 22:58:44 +00:00
|
|
|
/// Inserts a key-value pair into a term that is a dictionnary.
|
|
|
|
/// Fails if `self` is not a dictionnary.
|
|
|
|
pub fn insert(self, k: &'a str, v: Term<'a>) -> Term<'a> {
|
|
|
|
match v.0 {
|
|
|
|
T::Err(e) => Term(T::Err(e)),
|
|
|
|
vv => match self.0 {
|
|
|
|
T::Dict(mut d) => {
|
|
|
|
d.insert(k.as_bytes(), vv);
|
|
|
|
Term(T::Dict(d))
|
|
|
|
}
|
|
|
|
_ => Term(T::Err(Error::NotADictionnary)),
|
|
|
|
},
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---- encoding function ----
|
|
|
|
|
2022-11-17 22:58:44 +00:00
|
|
|
/// Generate the nettext representation of a term
|
2022-11-17 23:01:23 +00:00
|
|
|
pub fn encode(t: Term<'_>) -> Result<Vec<u8>, Error> {
|
2022-11-17 16:55:50 +00:00
|
|
|
let mut buf = Vec::with_capacity(128);
|
2022-11-17 22:58:44 +00:00
|
|
|
encode_aux(&mut buf, t.0, 0)?;
|
|
|
|
Ok(buf)
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
|
2022-11-17 23:01:23 +00:00
|
|
|
fn encode_aux(buf: &mut Vec<u8>, term: T<'_>, indent: usize) -> Result<(), Error> {
|
2022-11-17 16:55:50 +00:00
|
|
|
match term {
|
|
|
|
T::Str(s) => buf.extend_from_slice(s),
|
|
|
|
T::OwnedStr(s) => buf.extend_from_slice(&s),
|
2022-11-17 22:58:44 +00:00
|
|
|
T::Dict(mut d) => {
|
2022-11-17 16:55:50 +00:00
|
|
|
buf.extend_from_slice(b"{\n");
|
|
|
|
let indent2 = indent + 2;
|
2022-11-17 22:58:44 +00:00
|
|
|
let mut keys = d.keys().cloned().collect::<Vec<_>>();
|
2022-11-17 16:55:50 +00:00
|
|
|
keys.sort();
|
|
|
|
for k in keys {
|
2022-11-17 22:58:44 +00:00
|
|
|
let v = d.remove(k).unwrap();
|
2022-11-17 16:55:50 +00:00
|
|
|
for _ in 0..indent2 {
|
|
|
|
buf.push(b' ');
|
|
|
|
}
|
|
|
|
buf.extend_from_slice(k);
|
|
|
|
buf.extend_from_slice(b" = ");
|
2022-11-17 22:58:44 +00:00
|
|
|
encode_aux(buf, v, indent2)?;
|
2022-11-17 16:55:50 +00:00
|
|
|
buf.extend_from_slice(b",\n");
|
|
|
|
}
|
|
|
|
for _ in 0..indent {
|
|
|
|
buf.push(b' ');
|
|
|
|
}
|
|
|
|
buf.push(b'}');
|
|
|
|
}
|
|
|
|
T::List(l) => {
|
|
|
|
let indent2 = indent + 2;
|
2022-11-17 22:58:44 +00:00
|
|
|
for (i, v) in l.into_iter().enumerate() {
|
2022-11-17 16:55:50 +00:00
|
|
|
if buf.iter().rev().take_while(|c| **c != b'\n').count() > 80 {
|
|
|
|
buf.push(b'\n');
|
|
|
|
for _ in 0..indent2 {
|
|
|
|
buf.push(b' ');
|
|
|
|
}
|
|
|
|
} else if i > 0 {
|
|
|
|
buf.push(b' ');
|
|
|
|
}
|
2022-11-17 22:58:44 +00:00
|
|
|
encode_aux(buf, v, indent2)?;
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
}
|
2022-11-17 22:58:44 +00:00
|
|
|
T::Err(e) => return Err(e),
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
2022-11-17 22:58:44 +00:00
|
|
|
Ok(())
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn complex1() {
|
|
|
|
let input = list([
|
2022-11-17 22:58:44 +00:00
|
|
|
string("HELLO"),
|
|
|
|
string("alexhelloworld"),
|
2022-11-17 16:55:50 +00:00
|
|
|
dict([
|
2022-11-17 22:58:44 +00:00
|
|
|
("from", string("jxx")),
|
|
|
|
("subject", string("hello")),
|
|
|
|
("data", raw(b"{ f1 = plop, f2 = kuko }")),
|
2022-11-17 16:55:50 +00:00
|
|
|
]),
|
|
|
|
]);
|
|
|
|
let expected = b"HELLO alexhelloworld {
|
|
|
|
data = { f1 = plop, f2 = kuko },
|
|
|
|
from = jxx,
|
|
|
|
subject = hello,
|
|
|
|
}";
|
2022-11-17 22:58:44 +00:00
|
|
|
let enc = encode(input).unwrap();
|
2022-11-17 16:55:50 +00:00
|
|
|
eprintln!("{}", std::str::from_utf8(&enc).unwrap());
|
|
|
|
eprintln!("{}", std::str::from_utf8(&expected[..]).unwrap());
|
2022-11-17 22:58:44 +00:00
|
|
|
assert_eq!(&enc, &expected[..]);
|
2022-11-17 16:55:50 +00:00
|
|
|
}
|
|
|
|
}
|