#![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
#![warn(
clippy::all,
clippy::pedantic,
clippy::absolute_paths,
clippy::allow_attributes_without_reason,
clippy::cargo,
clippy::dbg_macro,
clippy::exit,
clippy::todo,
clippy::unimplemented,
clippy::unwrap_used,
missing_debug_implementations,
missing_docs
)]
#![allow(clippy::module_name_repetitions, reason = "Occasionally useful")]
#![allow(clippy::too_many_lines, reason = "This is not bad in my opinion")]
use {
crate::folding::{FoldingWriter, UnfoldingReader},
std::{
borrow::Borrow,
error::Error,
fmt::{self, Debug, Display, Formatter},
hash::{Hash, Hasher},
io::{self, Read, Write},
iter::Iterator,
str::FromStr,
},
};
mod folding;
#[derive(Debug)]
pub struct Parser<R: Read> {
unfolder: UnfoldingReader<R>,
}
impl<R: Read> Parser<R> {
pub fn new(reader: R) -> Self {
Self {
unfolder: UnfoldingReader::new(reader),
}
}
}
impl<R: Read> Iterator for Parser<R> {
type Item = Result<Contentline, ParseError>;
fn next(&mut self) -> Option<Self::Item> {
match self.unfolder.next_line()? {
Ok(next_line) => {
Some(Contentline::parse(next_line).map_err(ParseError::InvalidContentline))
}
Err(err) => Some(Err(ParseError::IoError(err))),
}
}
}
#[derive(Debug)]
pub enum ParseError {
IoError(io::Error),
InvalidContentline(ParseContentlineError),
}
impl Display for ParseError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::IoError(err) => Display::fmt(err, f),
Self::InvalidContentline(err) => Display::fmt(err, f),
}
}
}
impl Error for ParseError {}
#[derive(Debug)]
pub struct Writer<W: Write> {
folder: FoldingWriter<W>,
}
impl<W: Write> Writer<W> {
pub fn new(writer: W) -> Self {
Self {
folder: FoldingWriter::new(writer),
}
}
pub fn write(&mut self, contentline: &Contentline) -> io::Result<()> {
contentline.write(|s| self.folder.write(s))?;
self.folder.end_line()?;
self.folder.flush()
}
pub fn write_all<C, I>(&mut self, contentlines: I) -> io::Result<()>
where
C: Borrow<Contentline>,
I: IntoIterator<Item = C>,
{
for line in contentlines {
line.borrow().write(|s| self.folder.write(s))?;
self.folder.end_line()?;
}
self.folder.flush()
}
pub fn inner(&self) -> &W {
self.folder.inner()
}
pub fn inner_mut(&mut self) -> &mut W {
self.folder.inner_mut()
}
pub fn into_inner(self) -> W {
self.folder.into_inner()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Contentline {
group: Option<Identifier<String>>,
name: Identifier<String>,
params: Vec<Param>,
value: Value<String>,
}
impl Contentline {
#[must_use]
pub fn group(&self) -> Option<&str> {
self.group.as_ref().map(AsRef::as_ref)
}
#[must_use]
pub fn name(&self) -> &str {
self.name.as_ref()
}
#[must_use]
pub fn params(&self) -> &[Param] {
&self.params
}
#[must_use]
pub fn value(&self) -> &str {
self.value.as_ref()
}
#[must_use]
pub fn new<N, V>(name: N, value: V) -> Self
where
N: AsRef<str>,
V: AsRef<str>,
{
Self::try_new(name, value).unwrap_or_else(|err| panic!("{err}"))
}
pub fn try_new<N, V>(name: N, value: V) -> Result<Self, InvalidContentline>
where
N: AsRef<str>,
V: AsRef<str>,
{
let name =
Identifier::new(name.as_ref().to_owned()).map_err(InvalidContentline::InvalidName)?;
let value =
Value::new(value.as_ref().to_owned()).map_err(InvalidContentline::InvalidValue)?;
Ok(Self {
group: None,
name,
params: Vec::new(),
value,
})
}
#[must_use]
pub fn set_group<G>(self, group: G) -> Self
where
G: AsRef<str>,
{
Self::try_set_group(self, group).unwrap_or_else(|err| panic!("{err}"))
}
pub fn try_set_group<G>(self, group: G) -> Result<Self, InvalidContentline>
where
G: AsRef<str>,
{
let group = Some(
Identifier::new(group.as_ref().to_owned()).map_err(InvalidContentline::InvalidGroup)?,
);
Ok(Self { group, ..self })
}
#[must_use]
pub fn unset_group(self) -> Self {
Self {
group: None,
..self
}
}
#[must_use]
pub fn add_param<I, N, V>(self, name: N, values: I) -> Self
where
I: IntoIterator<Item = V>,
N: AsRef<str>,
V: AsRef<str>,
{
Self::try_add_param(self, name, values).unwrap_or_else(|err| panic!("{err}"))
}
pub fn try_add_param<I, N, V>(self, name: N, values: I) -> Result<Self, InvalidContentline>
where
I: IntoIterator<Item = V>,
N: AsRef<str>,
V: AsRef<str>,
{
let param = Param::try_new(name, values)?;
Ok(Self {
params: {
let mut params = self.params;
params.push(param);
params
},
..self
})
}
#[must_use]
pub fn set_params<I, P>(self, params: I) -> Self
where
I: IntoIterator<Item = P>,
P: TryInto<Param>,
P::Error: Display,
{
self.try_set_params(params)
.unwrap_or_else(|err| panic!("{err}"))
}
pub fn try_set_params<I, P>(self, params: I) -> Result<Self, P::Error>
where
I: IntoIterator<Item = P>,
P: TryInto<Param>,
{
Ok(Self {
params: params
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
..self
})
}
fn parse(mut contentline: &str) -> Result<Self, ParseContentlineError> {
let error = || ParseContentlineError {
invalid_contentline: contentline.to_owned(),
};
let (group, name) = parse_group_and_name(&mut contentline).map_err(|_| error())?;
let params = parse_params(&mut contentline).map_err(|_| error())?;
if !contentline.starts_with(':') {
return Err(error());
}
contentline = &contentline[1..];
let value = parse_value(&mut contentline).map_err(|_| error())?;
Ok(Contentline {
group,
name,
params,
value,
})
}
fn write<E, W>(&self, mut writer: W) -> Result<(), E>
where
W: FnMut(&str) -> Result<(), E>,
{
if let Some(group) = &self.group {
write_identifier(group, &mut writer)?;
writer(".")?;
}
write_identifier(&self.name, &mut writer)?;
write_params(&self.params, &mut writer)?;
writer(":")?;
write_value(&self.value, &mut writer)?;
Ok(())
}
}
impl FromStr for Contentline {
type Err = ParseContentlineError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Contentline::parse(s)
}
}
impl Display for Contentline {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.write(|s| f.write_str(s))
}
}
#[derive(Debug)]
pub enum InvalidContentline {
InvalidGroup(InvalidIdentifier),
InvalidName(InvalidIdentifier),
InvalidValue(InvalidValue),
InvalidParam(InvalidParam),
}
impl Display for InvalidContentline {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::InvalidGroup(err) => write!(f, "Invalid group: {err}"),
Self::InvalidName(err) => write!(f, "Invalid name: {err}"),
Self::InvalidValue(err) => write!(f, "Invalid value: {err}"),
Self::InvalidParam(err) => write!(f, "Invalid parameter: {err}"),
}
}
}
impl Error for InvalidContentline {}
#[derive(Debug)]
pub struct ParseContentlineError {
invalid_contentline: String,
}
impl ParseContentlineError {
#[must_use]
pub fn invalid_contentline(&self) -> &str {
&self.invalid_contentline
}
}
impl Display for ParseContentlineError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid contentline")
}
}
impl Error for ParseContentlineError {}
macro_rules! empty {
() => {};
}
macro_rules! as_str_wrapper {
(
@metainfo {
case_sensitive: true,
$($metainfo:tt)*
};
$($remainder:tt)*
) => {
as_str_wrapper! {
@metainfo {
is_case_sensitive: empty!{},
$($metainfo)*
};
$($remainder)*
}
};
(
@metainfo {
case_sensitive: false,
$($metainfo:tt)*
};
$($remainder:tt)*
) => {
as_str_wrapper! {
@metainfo {
is_not_case_sensitive: empty!{},
$($metainfo)*
};
$($remainder)*
}
};
(
@metainfo {
$(is_case_sensitive: $is_case_sensitive:item,)?
$(is_not_case_sensitive: $is_not_case_sensitive:item,)?
param_name: $param_name:ident,
doc_name: $doc_name:literal,
error_name: $error_name:ident,
error_message: $error_message:literal,
valid_if: $valid_if:literal,
};
pub struct $type_name:ident { ... }
impl $_type_name:ident {
pub const fn is_valid($_param_name:ident: &str) -> bool {
$( $is_valid_impl:tt )+
}
}
) => {
$( $is_case_sensitive
#[derive(Hash)]
)*
#[doc = concat!("A wrapper of a [`AsRef<str>`] that is guaranteed to be a valid ", $doc_name, ".")]
#[doc = concat!("A ", $doc_name, " is considered valid if it ", $valid_if, ".")]
#[derive(Clone, Debug)]
pub struct $type_name<T> {
value: T,
}
impl<T: AsRef<str>> $type_name<T> {
#[doc = concat!("Creates a new [`", stringify!($type_name), "`].")]
#[doc = concat!("Fails if `", stringify!($param_name), "` is not a valid ", $doc_name, ".")]
pub fn new($param_name: T) -> Result<Self, $error_name> {
if Self::is_valid($param_name.as_ref()) {
Ok(Self::new_unchecked($param_name))
} else {
Err($error_name)
}
}
#[doc = concat!("Creates a new [`", stringify!($type_name), "`]. Does not check if `", stringify!($param_name), "` is valid.")]
#[doc = concat!("It is up to the caller to ensure that `", stringify!($param_name), "` is a valid ", $doc_name, ".")]
pub fn new_unchecked($param_name: T) -> Self {
debug_assert!(Self::is_valid($param_name.as_ref()));
Self { value: $param_name }
}
#[doc = concat!("Extracts a string slice containing the ", $doc_name, ".")]
pub fn as_str(&self) -> &str {
self.value.as_ref()
}
#[doc = concat!("Returns true if `", stringify!($param_name), "` is a valid ", $doc_name, ".")]
#[doc = concat!("A ", $doc_name, " is considered valid if it ", $valid_if, ".")]
fn is_valid($param_name: &str) -> bool {
$( $is_valid_impl )+
}
}
impl<T: AsRef<str>> Eq for $type_name<T> {}
impl<T, U> PartialEq<$type_name<U>> for $type_name<T>
where
T: AsRef<str>,
U: AsRef<str>,
{
fn eq(&self, other: &$type_name<U>) -> bool {
self == other.value.as_ref()
}
}
impl<T: AsRef<str>> PartialEq<String> for $type_name<T> {
fn eq(&self, other: &String) -> bool {
self == other.as_str()
}
}
impl<T: AsRef<str>> PartialEq<&str> for $type_name<T> {
fn eq(&self, other: &&str) -> bool {
self == *other
}
}
impl<T: AsRef<str>> PartialEq<str> for $type_name<T> {
fn eq(&self, other: &str) -> bool {
$( $is_case_sensitive
self.value.as_ref() == other
)?
$( $is_not_case_sensitive
self.value.as_ref().eq_ignore_ascii_case(other)
)?
}
}
$( $is_not_case_sensitive
impl<T: AsRef<str>> Hash for $type_name<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
for c in self.value.as_ref().as_bytes() {
c.to_ascii_uppercase().hash(state);
}
state.write_u8(0xff);
}
}
)?
impl<T: AsRef<str>> AsRef<str> for $type_name<T> {
fn as_ref(&self) -> &str {
self.value.as_ref()
}
}
impl<T: AsRef<str>> From<$type_name<T>> for String {
fn from($param_name: $type_name<T>) -> Self {
$param_name.value.as_ref().to_owned()
}
}
impl<T: AsRef<str>> Display for $type_name<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.value.as_ref())
}
}
#[doc = concat!("Indicates an attempt to create a [`", stringify!($type_name), "`] from an invalid ", $doc_name, ".")]
#[doc = concat!("A [`", stringify!($type_name), "`] is considered valid if ", $valid_if, ".")]
#[derive(Debug)]
pub struct $error_name;
impl Display for $error_name {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, $error_message)
}
}
impl Error for $error_name {}
};
}
as_str_wrapper! {
@metainfo {
case_sensitive: false,
param_name: identifier,
doc_name: "identifier",
error_name: InvalidIdentifier,
error_message: "an identifier can only contain alphanumeric characters and dashes ('-')",
valid_if: "is not empty and all characters are alphanumeric ascii characters or dashes (`-`)",
};
pub struct Identifier { ... }
impl Identifier {
pub const fn is_valid(identifier: &str) -> bool {
if identifier.is_empty() {
return false;
}
let identifier = identifier.as_bytes();
let mut index = 0;
while index < identifier.len() {
if !is_identifier_char(identifier[index]) {
return false;
}
index += 1;
}
true
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Param {
name: Identifier<String>,
values: Vec<ParamValue<String>>,
}
impl Param {
#[must_use]
pub fn name(&self) -> &str {
self.name.as_ref()
}
#[must_use]
pub fn values(&self) -> &[ParamValue<String>] {
&self.values
}
pub fn new<I, N, V>(name: N, values: I) -> Self
where
I: IntoIterator<Item = V>,
N: AsRef<str>,
V: AsRef<str>,
{
Self::try_new(name, values).unwrap_or_else(|err| panic!("{err}"))
}
pub fn try_new<I, N, V>(name: N, values: I) -> Result<Self, InvalidContentline>
where
I: IntoIterator<Item = V>,
N: AsRef<str>,
V: AsRef<str>,
{
let values = values
.into_iter()
.map(|value| ParamValue::new(value.as_ref().to_owned()))
.collect::<Result<Vec<_>, InvalidParamValue>>()
.map_err(InvalidParam::InvalidValue)
.map_err(InvalidContentline::InvalidParam)?;
if values.is_empty() {
return Err(InvalidContentline::InvalidParam(InvalidParam::EmptyValues));
}
let name = Identifier::new(name.as_ref().to_owned())
.map_err(InvalidParam::InvalidName)
.map_err(InvalidContentline::InvalidParam)?;
Ok(Self { name, values })
}
}
impl<I, N, V> TryFrom<(N, I)> for Param
where
I: IntoIterator<Item = V>,
N: AsRef<str>,
V: AsRef<str>,
{
type Error = InvalidContentline;
fn try_from((name, values): (N, I)) -> Result<Self, Self::Error> {
Param::try_new(name, values)
}
}
#[derive(Debug)]
pub enum InvalidParam {
EmptyValues,
InvalidName(InvalidIdentifier),
InvalidValue(InvalidParamValue),
}
impl Display for InvalidParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::EmptyValues => write!(f, "Empty parameter values"),
Self::InvalidName(err) => write!(f, "Invalid parameter name: {err}"),
Self::InvalidValue(err) => write!(f, "Invalid parameter value: {err}"),
}
}
}
impl Error for InvalidParam {}
as_str_wrapper! {
@metainfo {
case_sensitive: true,
param_name: value,
doc_name: "parameter value",
error_name: InvalidParamValue,
error_message: "a parameter value cannot contain any ascii control characters except horizontal tabs and linefeeds",
valid_if: r"contains no ascii control characters except horizontal tabs (`'\t'`) and linefeeds (`'\n'`)",
};
pub struct ParamValue { ... }
impl ParamValue {
pub const fn is_valid(value: &str) -> bool {
let value = value.as_bytes();
let mut index = 0;
while index < value.len() {
if is_control(value[index]) && value[index] != b'\n' {
return false;
}
index += 1;
}
true
}
}
}
as_str_wrapper! {
@metainfo {
case_sensitive: true,
param_name: value,
doc_name: "value",
error_name: InvalidValue,
error_message: "a value cannot contain any ascii control characters except horizontal tabs",
valid_if: r"contains no ascii control characters except horizontal tabs (`'\t'`)",
};
pub struct Value { ... }
impl Value {
pub const fn is_valid(value: &str) -> bool {
let value = value.as_bytes();
let mut index = 0;
while index < value.len() {
if is_control(value[index]) {
return false;
}
index += 1;
}
true
}
}
}
fn parse_group_and_name(
contentline: &mut &str,
) -> Result<(Option<Identifier<String>>, Identifier<String>), IntermediateParsingError> {
let identifier = parse_identifier(contentline)?;
if contentline.starts_with('.') {
*contentline = &contentline[1..];
let group = Some(identifier);
let name = parse_identifier(contentline)?;
Ok((group, name))
} else {
let group = None;
let name = identifier;
Ok((group, name))
}
}
fn parse_params(contentline: &mut &str) -> Result<Vec<Param>, IntermediateParsingError> {
let mut params = Vec::new();
while contentline.starts_with(';') {
*contentline = &contentline[1..];
params.push(parse_param(contentline)?);
}
Ok(params)
}
fn parse_param(contentline: &mut &str) -> Result<Param, IntermediateParsingError> {
let name = parse_identifier(contentline)?;
if !contentline.starts_with('=') {
return Err(IntermediateParsingError);
}
*contentline = &contentline[1..];
let values = parse_param_values(contentline)?;
Ok(Param { name, values })
}
fn parse_param_values(
contentline: &mut &str,
) -> Result<Vec<ParamValue<String>>, IntermediateParsingError> {
let mut param_values = vec![parse_param_value(contentline)?];
while contentline.starts_with(',') {
*contentline = &contentline[1..];
param_values.push(parse_param_value(contentline)?);
}
Ok(param_values)
}
fn parse_param_value(
contentline: &mut &str,
) -> Result<ParamValue<String>, IntermediateParsingError> {
let value = if contentline.starts_with('"') {
parse_quoted_string(contentline)?
} else {
parse_paramtext(contentline)
};
Ok(ParamValue::new_unchecked(value))
}
fn parse_quoted_string(contentline: &mut &str) -> Result<String, IntermediateParsingError> {
debug_assert!(contentline.starts_with('"'));
*contentline = &contentline[1..];
let quoted_string_length = contentline
.bytes()
.position(|c| !is_qsafe_char(c))
.ok_or(IntermediateParsingError)?;
if &contentline[quoted_string_length..=quoted_string_length] == "\"" {
let quoted_string = parse_param_value_rfc6868(&contentline[..quoted_string_length]);
*contentline = &contentline[quoted_string_length + 1..];
Ok(quoted_string)
} else {
Err(IntermediateParsingError)
}
}
fn parse_paramtext(contentline: &mut &str) -> String {
let paramtext_length = contentline
.bytes()
.position(|c| !is_safe_char(c))
.unwrap_or(contentline.len());
let paramtext = parse_param_value_rfc6868(&contentline[..paramtext_length]);
*contentline = &contentline[paramtext_length..];
paramtext
}
fn parse_param_value_rfc6868(param_value: &str) -> String {
debug_assert!(param_value.bytes().all(is_qsafe_char));
match param_value.find('^') {
Some(next_index) => {
let mut result = String::with_capacity(param_value.len());
let mut param_value = param_value;
let mut next_index = Some(next_index);
while let Some(index) = next_index {
result.push_str(¶m_value[..index]);
if let Some(escaped_char) = param_value.get(index + 1..index + 2) {
match escaped_char {
"n" => result.push('\n'),
"'" => result.push('\"'),
"^" => result.push('^'),
other => {
result.push('^');
result.push_str(other);
}
}
param_value = ¶m_value[index + 2..];
} else {
result.push('^');
param_value = ¶m_value[index + 1..];
}
next_index = param_value.find('^');
}
result.push_str(param_value);
result
}
None => param_value.to_owned(),
}
}
fn parse_value(contentline: &mut &str) -> Result<Value<String>, IntermediateParsingError> {
if Value::<&str>::is_valid(contentline) {
let value = Value::new_unchecked(contentline.to_owned());
*contentline = "";
Ok(value)
} else {
Err(IntermediateParsingError)
}
}
fn parse_identifier(
contentline: &mut &str,
) -> Result<Identifier<String>, IntermediateParsingError> {
let identifier_length = contentline
.bytes()
.position(|c| !is_identifier_char(c))
.unwrap_or(contentline.len());
if identifier_length == 0 {
Err(IntermediateParsingError)
} else {
let identifier = &contentline[..identifier_length];
*contentline = &contentline[identifier_length..];
let identifier = Identifier::new_unchecked(identifier.to_owned());
Ok(identifier)
}
}
struct IntermediateParsingError;
fn write_params<E, W>(params: &Vec<Param>, writer: &mut W) -> Result<(), E>
where
W: FnMut(&str) -> Result<(), E>,
{
for param in params {
writer(";")?;
write_identifier(¶m.name, writer)?;
writer("=")?;
write_param_values(¶m.values, writer)?;
}
Ok(())
}
fn write_param_values<E, T, W>(values: &[ParamValue<T>], writer: &mut W) -> Result<(), E>
where
T: AsRef<str>,
W: FnMut(&str) -> Result<(), E>,
{
debug_assert!(!values.is_empty());
write_param_value(&values[0], writer)?;
for param_value in &values[1..] {
writer(",")?;
write_param_value(param_value, writer)?;
}
Ok(())
}
fn write_param_value<E, T, W>(value: &ParamValue<T>, writer: &mut W) -> Result<(), E>
where
T: AsRef<str>,
W: FnMut(&str) -> Result<(), E>,
{
let value = value.value.as_ref();
if value.contains([';', ':', '.']) {
writer("\"")?;
write_param_value_rfc6868(value, writer)?;
writer("\"")
} else {
write_param_value_rfc6868(value, writer)
}
}
fn write_param_value_rfc6868<E, W>(mut value: &str, writer: &mut W) -> Result<(), E>
where
W: FnMut(&str) -> Result<(), E>,
{
while let Some(index) = value.find(['\n', '^', '"']) {
writer(&value[..index])?;
match &value[index..=index] {
"\n" => writer("^n"),
"^" => writer("^^"),
"\"" => writer("^'"),
_ => unreachable!(),
}?;
value = &value[index + 1..];
}
writer(value)
}
fn write_value<E, T, W>(value: &Value<T>, writer: &mut W) -> Result<(), E>
where
T: AsRef<str>,
W: FnMut(&str) -> Result<(), E>,
{
writer(value.value.as_ref())
}
fn write_identifier<E, T, W>(identifier: &Identifier<T>, writer: &mut W) -> Result<(), E>
where
T: AsRef<str>,
W: FnMut(&str) -> Result<(), E>,
{
let identifier = identifier.value.as_ref().to_uppercase();
writer(&identifier)
}
const fn is_safe_char(c: u8) -> bool {
!is_control(c) && (c != b'"') && (c != b';') && (c != b':') && (c != b',')
}
const fn is_qsafe_char(c: u8) -> bool {
!is_control(c) && (c != b'"')
}
const fn is_control(c: u8) -> bool {
c.is_ascii_control() && (c != b'\t')
}
const fn is_identifier_char(c: u8) -> bool {
c.is_ascii_alphanumeric() || (c == b'-')
}
#[cfg(test)]
mod tests {
use crate::Contentline;
#[test]
fn equality_reflexivity() {
let line = "group.NAME;PARAM=pval:Value"
.parse::<Contentline>()
.unwrap();
assert_eq!(line, line);
}
mod case_sensitivity {
use crate::Contentline;
#[test]
fn case_insensitive() {
let line0 = "Group.lowerUPPER;PaRaM=parameter value:value"
.parse::<Contentline>()
.unwrap();
let line1 = "group.LOWERupper;PARAm=parameter value:value"
.parse::<Contentline>()
.unwrap();
assert_eq!(line0, line1);
}
#[test]
fn case_sensitive_param_values() {
let line0 = "HUI;TEST=Test:Value".parse::<Contentline>().unwrap();
let line1 = "HUI;TEST=TEST:Value".parse::<Contentline>().unwrap();
assert_ne!(line0, line1);
}
#[test]
fn case_sensitive_property_values() {
let line0 = "YOU;ARE=A:TEST".parse::<Contentline>().unwrap();
let line1 = "YOU;ARE=A:Test".parse::<Contentline>().unwrap();
assert_ne!(line0, line1);
}
}
mod parse {
use crate::{Contentline, Parser};
#[test]
fn name_and_value() {
let contentline = "NOTE:This is a note.";
let mut parser = Parser::new(contentline.as_bytes());
assert_eq!(
parser.next().unwrap().unwrap(),
Contentline::new("NOTE", "This is a note."),
);
assert!(parser.next().is_none());
}
#[test]
fn group_name_params_value() {
let contentline =
"test-group.TEST-CASE;test-param=PARAM1;another-test-param=PARAM2:value";
let mut parser = Parser::new(contentline.as_bytes());
assert_eq!(
parser.next().unwrap().unwrap(),
Contentline::new("TEST-CASE", "value")
.set_group("test-group")
.set_params([
("test-param", ["PARAM1"]),
("another-test-param", ["PARAM2"])
])
);
assert!(parser.next().is_none());
}
#[test]
fn empty_value() {
let empty_value = "EMPTY-VALUE:";
let mut parser = Parser::new(empty_value.as_bytes());
assert_eq!(
parser.next().unwrap().unwrap(),
Contentline::new("EMPTY-VALUE", "")
);
assert!(parser.next().is_none());
}
#[test]
fn empty_param() {
let empty_param = "EMPTY-PARAM;paramtext=;quoted-string=\"\";multiple=,,,,\"\",\"\",,\"\",,,\"\":value";
let mut parser = Parser::new(empty_param.as_bytes());
assert_eq!(
parser.next().unwrap().unwrap(),
Contentline::new("EMPTY-PARAM", "value").set_params([
("paramtext", [""].as_slice()),
("quoted-string", &[""]),
("multiple", &[""; 11])
])
);
assert!(parser.next().is_none());
}
#[test]
fn rfc6868() {
let contentline = "RFC6868-TEST;caret=^^;newline=^n;double-quote=^';all-in-quotes=\"^^^n^'\";weird=^^^^n;others=^g^5^k^?^%^&^a:value";
let mut parser = Parser::new(contentline.as_bytes());
assert_eq!(
parser.next().unwrap().unwrap(),
Contentline::new("RFC6868-TEST", "value").set_params([
("caret", ["^"]),
("newline", ["\n"]),
("double-quote", ["\""]),
("all-in-quotes", ["^\n\""]),
("weird", ["^^n"]),
("others", ["^g^5^k^?^%^&^a"]),
])
);
assert!(parser.next().is_none());
}
}
mod write {
use {
crate::{Contentline, Writer},
std::{iter, str},
};
#[test]
fn name_and_value() {
let contentline = Contentline::new("NAME", "VALUE");
let expected = "NAME:VALUE\r\n";
let actual = {
let mut buffer = Vec::new();
let mut writer = Writer::new(&mut buffer);
writer.write(&contentline).unwrap();
str::from_utf8(&buffer).unwrap().to_owned()
};
assert_eq!(&actual, expected);
}
#[test]
fn group_name_params_value() {
let contentline = Contentline::new("TEST-NAME", "test value \"with quotes\"")
.set_group("TEST-GROUP")
.set_params([
("PARAM-1", ["param value 1"]),
("PARAM-2", ["param value of parameter: 2"]),
]);
let expected = "\
TEST-GROUP.TEST-NAME;PARAM-1=param value 1;PARAM-2=\"param value of paramete\r
r: 2\":test value \"with quotes\"\r
";
let actual = {
let mut buffer = Vec::new();
let mut writer = Writer::new(&mut buffer);
writer.write(&contentline).unwrap();
str::from_utf8(&buffer).unwrap().to_owned()
};
assert_eq!(&actual, expected);
}
#[test]
fn identifiers_converted_to_uppercase() {
let contentline = Contentline::new("name", "value")
.set_group("lower-group")
.add_param("PaRaM", ["param value"]);
let expected = "LOWER-GROUP.NAME;PARAM=param value:value\r\n";
let actual = {
let mut buffer = Vec::new();
let mut writer = Writer::new(&mut buffer);
writer.write(&contentline).unwrap();
str::from_utf8(&buffer).unwrap().to_owned()
};
assert_eq!(&actual, expected);
}
#[test]
fn empty_value() {
let contentline = Contentline::new("NAME", "");
let expected = "NAME:\r\n";
let actual = {
let mut buffer = Vec::new();
let mut writer = Writer::new(&mut buffer);
writer.write(&contentline).unwrap();
str::from_utf8(&buffer).unwrap().to_owned()
};
assert_eq!(&actual, expected);
}
#[test]
fn empty_param() {
let num_params = 15;
let contentline = Contentline::new("NAME", "value")
.add_param("PARAM", iter::repeat("").take(num_params));
let expected = {
let mut expected = String::from("NAME;PARAM=");
expected.push_str(&",".repeat(num_params - 1));
expected.push_str(":value\r\n");
expected
};
let actual = {
let mut buffer = Vec::new();
let mut writer = Writer::new(&mut buffer);
writer.write(&contentline).unwrap();
str::from_utf8(&buffer).unwrap().to_owned()
};
assert_eq!(actual, expected);
}
#[test]
fn rfc6868() {
let contentline = Contentline::new("RFC6868-TEST", "value").set_params([
("CARET", ["^"]),
("NEWLINE", ["\n"]),
("DOUBLE-QUOTE", ["\""]),
("ALL-IN-QUOTES", ["^;\n;\""]),
("WEIRD", ["^^n"]),
]);
let expected = "RFC6868-TEST;CARET=^^;NEWLINE=^n;DOUBLE-QUOTE=^';ALL-IN-QUOTES=\"^^;^n;^'\";W\r\n EIRD=^^^^n:value\r\n";
let actual = {
let mut buffer = Vec::new();
let mut writer = Writer::new(&mut buffer);
writer.write(&contentline).unwrap();
str::from_utf8(&buffer).unwrap().to_owned()
};
assert_eq!(&actual, expected);
}
}
}