use std::{
fmt::{self, Display, Formatter, Write},
str::FromStr,
};
use mime::Mime;
use crate::{
error::{InvalidValueError, ParseErrorKind},
fold::FoldingWriter,
syntax::{caret_decode, write_param_value},
values::{LanguageTag, Timestamp, Token, Uri},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Pref(u8);
impl Pref {
pub const fn new(value: u8) -> Result<Self, InvalidValueError> {
if value >= 1 && value <= 100 {
Ok(Self(value))
} else {
Err(InvalidValueError::new("preference"))
}
}
#[inline]
pub const fn get(&self) -> u8 {
self.0
}
}
impl FromStr for Pref {
type Err = InvalidValueError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<u8>().map_err(|_| InvalidValueError::new("preference")).and_then(Self::new)
}
}
impl Display for Pref {
#[inline]
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Pid {
pub id: u32,
pub source: Option<u32>,
}
impl Display for Pid {
#[inline]
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(&self.id, f)?;
if let Some(source) = self.source {
write!(f, ".{source}")?;
}
Ok(())
}
}
impl FromStr for Pid {
type Err = InvalidValueError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
const ERROR: InvalidValueError = InvalidValueError::new("pid");
let (id, source) = match s.split_once('.') {
Some((id, source)) => (id, Some(source.parse().map_err(|_| ERROR)?)),
None => (s, None),
};
Ok(Self {
id: id.parse().map_err(|_| ERROR)?,
source,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeValue {
Work,
Home,
Voice,
Fax,
Cell,
Video,
Pager,
Textphone,
Text,
Contact,
Acquaintance,
Friend,
Met,
CoWorker,
Colleague,
CoResident,
Neighbor,
Child,
Parent,
Sibling,
Spouse,
Kin,
Muse,
Crush,
Date,
Sweetheart,
Me,
Agent,
Emergency,
Billing,
Delivery,
Extension(Token),
}
impl Display for TypeValue {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(match self {
Self::Work => "work",
Self::Home => "home",
Self::Voice => "voice",
Self::Fax => "fax",
Self::Cell => "cell",
Self::Video => "video",
Self::Pager => "pager",
Self::Textphone => "textphone",
Self::Text => "text",
Self::Contact => "contact",
Self::Acquaintance => "acquaintance",
Self::Friend => "friend",
Self::Met => "met",
Self::CoWorker => "co-worker",
Self::Colleague => "colleague",
Self::CoResident => "co-resident",
Self::Neighbor => "neighbor",
Self::Child => "child",
Self::Parent => "parent",
Self::Sibling => "sibling",
Self::Spouse => "spouse",
Self::Kin => "kin",
Self::Muse => "muse",
Self::Crush => "crush",
Self::Date => "date",
Self::Sweetheart => "sweetheart",
Self::Me => "me",
Self::Agent => "agent",
Self::Emergency => "emergency",
Self::Billing => "billing",
Self::Delivery => "delivery",
Self::Extension(token) => return Display::fmt(token, f),
})
}
}
impl FromStr for TypeValue {
type Err = InvalidValueError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.to_ascii_lowercase().as_str() {
"work" => Self::Work,
"home" => Self::Home,
"voice" => Self::Voice,
"fax" => Self::Fax,
"cell" => Self::Cell,
"video" => Self::Video,
"pager" => Self::Pager,
"textphone" => Self::Textphone,
"text" => Self::Text,
"contact" => Self::Contact,
"acquaintance" => Self::Acquaintance,
"friend" => Self::Friend,
"met" => Self::Met,
"co-worker" => Self::CoWorker,
"colleague" => Self::Colleague,
"co-resident" => Self::CoResident,
"neighbor" => Self::Neighbor,
"child" => Self::Child,
"parent" => Self::Parent,
"sibling" => Self::Sibling,
"spouse" => Self::Spouse,
"kin" => Self::Kin,
"muse" => Self::Muse,
"crush" => Self::Crush,
"date" => Self::Date,
"sweetheart" => Self::Sweetheart,
"me" => Self::Me,
"agent" => Self::Agent,
"emergency" => Self::Emergency,
"billing" => Self::Billing,
"delivery" => Self::Delivery,
_ => Self::Extension(Token::from_str(s)?),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Calscale {
Gregorian,
Extension(Token),
}
impl Display for Calscale {
#[inline]
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Gregorian => f.write_str("gregorian"),
Self::Extension(token) => Display::fmt(token, f),
}
}
}
impl FromStr for Calscale {
type Err = InvalidValueError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("gregorian") {
Ok(Self::Gregorian)
} else {
Token::from_str(s).map(Self::Extension)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Phonetic {
Ipa,
Jyut,
Piny,
Script,
Extension(Token),
}
impl Display for Phonetic {
#[inline]
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Ipa => f.write_str("ipa"),
Self::Jyut => f.write_str("jyut"),
Self::Piny => f.write_str("piny"),
Self::Script => f.write_str("script"),
Self::Extension(token) => Display::fmt(token, f),
}
}
}
impl FromStr for Phonetic {
type Err = InvalidValueError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("ipa") {
Ok(Self::Ipa)
} else if s.eq_ignore_ascii_case("jyut") {
Ok(Self::Jyut)
} else if s.eq_ignore_ascii_case("piny") {
Ok(Self::Piny)
} else if s.eq_ignore_ascii_case("script") {
Ok(Self::Script)
} else {
Token::from_str(s).map(Self::Extension)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PropId(String);
impl PropId {
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for PropId {
type Err = InvalidValueError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if (1..=255).contains(&s.len())
&& s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
{
Ok(Self(s.to_string()))
} else {
Err(InvalidValueError::new("property identifier"))
}
}
}
impl Display for PropId {
#[inline]
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Script(String);
impl Script {
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for Script {
type Err = InvalidValueError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() == 4 && s.bytes().all(|b| b.is_ascii_alphabetic()) {
Ok(Self(s.to_string()))
} else {
Err(InvalidValueError::new("script subtag"))
}
}
}
impl Display for Script {
#[inline]
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TzParam {
Text(String),
Uri(Uri),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnyParameter {
pub name: Token,
pub values: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Parameters {
pub language: Option<LanguageTag>,
pub pref: Option<Pref>,
pub altid: Option<String>,
pub pids: Vec<Pid>,
pub types: Vec<TypeValue>,
pub media_type: Option<Mime>,
pub calscale: Option<Calscale>,
pub sort_as: Vec<String>,
pub geo: Option<Uri>,
pub tz: Option<TzParam>,
pub label: Option<String>,
pub author: Option<Uri>,
pub author_name: Option<String>,
pub created: Option<Timestamp>,
pub derived: Option<bool>,
pub phonetic: Option<Phonetic>,
pub prop_id: Option<PropId>,
pub script: Option<Script>,
pub service_type: Option<String>,
pub username: Option<String>,
pub any: Vec<AnyParameter>,
}
impl Parameters {
#[inline]
pub fn new() -> Self {
Self::default()
}
pub(crate) fn write(&self, w: &mut FoldingWriter) -> fmt::Result {
if let Some(language) = &self.language {
write!(w, ";LANGUAGE={language}")?;
}
if let Some(pref) = &self.pref {
write!(w, ";PREF={pref}")?;
}
if let Some(altid) = &self.altid {
w.write_str(";ALTID=")?;
write_param_value(w, altid)?;
}
if !self.pids.is_empty() {
w.write_str(";PID=")?;
for (i, pid) in self.pids.iter().enumerate() {
if i > 0 {
w.write_char(',')?;
}
write!(w, "{pid}")?;
}
}
if !self.types.is_empty() {
w.write_str(";TYPE=")?;
for (i, type_value) in self.types.iter().enumerate() {
if i > 0 {
w.write_char(',')?;
}
write!(w, "{type_value}")?;
}
}
if let Some(media_type) = &self.media_type {
w.write_str(";MEDIATYPE=")?;
write_param_value(w, media_type.as_ref())?;
}
if let Some(calscale) = &self.calscale {
write!(w, ";CALSCALE={calscale}")?;
}
if !self.sort_as.is_empty() {
w.write_str(";SORT-AS=")?;
for (i, sort_as) in self.sort_as.iter().enumerate() {
if i > 0 {
w.write_char(',')?;
}
write_param_value(w, sort_as)?;
}
}
if let Some(geo) = &self.geo {
write!(w, ";GEO=\"{geo}\"")?;
}
if let Some(tz) = &self.tz {
match tz {
TzParam::Text(text) => {
w.write_str(";TZ=")?;
write_param_value(w, text)?;
},
TzParam::Uri(uri) => write!(w, ";TZ=\"{uri}\"")?,
}
}
if let Some(label) = &self.label {
w.write_str(";LABEL=")?;
write_param_value(w, label)?;
}
if let Some(author) = &self.author {
write!(w, ";AUTHOR=\"{author}\"")?;
}
if let Some(author_name) = &self.author_name {
w.write_str(";AUTHOR-NAME=")?;
write_param_value(w, author_name)?;
}
if let Some(created) = &self.created {
write!(w, ";CREATED={created}")?;
}
if let Some(derived) = self.derived {
write!(w, ";DERIVED={}", if derived { "true" } else { "false" })?;
}
if let Some(phonetic) = &self.phonetic {
write!(w, ";PHONETIC={phonetic}")?;
}
if let Some(prop_id) = &self.prop_id {
write!(w, ";PROP-ID={prop_id}")?;
}
if let Some(script) = &self.script {
write!(w, ";SCRIPT={script}")?;
}
if let Some(service_type) = &self.service_type {
w.write_str(";SERVICE-TYPE=")?;
write_param_value(w, service_type)?;
}
if let Some(username) = &self.username {
w.write_str(";USERNAME=")?;
write_param_value(w, username)?;
}
for any in &self.any {
write!(w, ";{}=", any.name)?;
for (i, value) in any.values.iter().enumerate() {
if i > 0 {
w.write_char(',')?;
}
write_param_value(w, value)?;
}
}
Ok(())
}
pub(crate) fn parse(
raw_params: Vec<(&str, &str)>,
) -> Result<(Self, Option<String>), ParseErrorKind> {
let mut parameters = Self::default();
let mut value_type = None;
for (name, raw) in raw_params {
let invalid = || ParseErrorKind::InvalidParameter(name.to_string());
let upper_name = name.to_ascii_uppercase();
match upper_name.as_str() {
"LANGUAGE" => {
parameters.language = Some(single_value(raw).parse().map_err(|_| invalid())?);
},
"VALUE" => value_type = Some(single_value(raw).to_ascii_lowercase()),
"PREF" => parameters.pref = Some(single_value(raw).parse().map_err(|_| invalid())?),
"ALTID" => parameters.altid = Some(single_value(raw)),
"PID" => {
for item in list_values(raw, true) {
parameters.pids.push(item.parse().map_err(|_| invalid())?);
}
},
"TYPE" => {
for item in list_values(raw, true) {
parameters.types.push(item.parse().map_err(|_| invalid())?);
}
},
"MEDIATYPE" => {
parameters.media_type = Some(single_value(raw).parse().map_err(|_| invalid())?);
},
"CALSCALE" => {
parameters.calscale = Some(single_value(raw).parse().map_err(|_| invalid())?);
},
"SORT-AS" => parameters.sort_as.extend(list_values(raw, false)),
"GEO" => {
parameters.geo = Some(single_value(raw).parse().map_err(|_| invalid())?);
},
"TZ" => {
parameters.tz = Some(if raw.starts_with('"') {
TzParam::Uri(single_value(raw).parse().map_err(|_| invalid())?)
} else {
TzParam::Text(single_value(raw))
});
},
"LABEL" => parameters.label = Some(single_value(raw)),
"AUTHOR" => {
parameters.author = Some(single_value(raw).parse().map_err(|_| invalid())?);
},
"AUTHOR-NAME" => parameters.author_name = Some(single_value(raw)),
"CREATED" => {
parameters.created = Some(single_value(raw).parse().map_err(|_| invalid())?);
},
"DERIVED" => {
let value = single_value(raw);
parameters.derived = Some(if value.eq_ignore_ascii_case("true") {
true
} else if value.eq_ignore_ascii_case("false") {
false
} else {
return Err(invalid());
});
},
"PHONETIC" => {
parameters.phonetic = Some(single_value(raw).parse().map_err(|_| invalid())?);
},
"PROP-ID" => {
parameters.prop_id = Some(single_value(raw).parse().map_err(|_| invalid())?);
},
"SCRIPT" => {
parameters.script = Some(single_value(raw).parse().map_err(|_| invalid())?);
},
"SERVICE-TYPE" => parameters.service_type = Some(single_value(raw)),
"USERNAME" => parameters.username = Some(single_value(raw)),
_ => parameters.any.push(AnyParameter {
name: name.parse().map_err(|_| invalid())?,
values: list_values(raw, false),
}),
}
}
Ok((parameters, value_type))
}
}
fn unquote(s: &str) -> &str {
if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') { &s[1..s.len() - 1] } else { s }
}
fn single_value(raw: &str) -> String {
caret_decode(unquote(raw))
}
fn list_values(raw: &str, split_inside_quotes: bool) -> Vec<String> {
let mut items = Vec::new();
let bytes = raw.as_bytes();
let mut start = 0;
let mut in_quotes = false;
for (i, b) in bytes.iter().enumerate() {
match b {
b'"' => in_quotes = !in_quotes,
b',' if !in_quotes => {
items.push(&raw[start..i]);
start = i + 1;
},
_ => (),
}
}
items.push(&raw[start..]);
let mut values = Vec::with_capacity(items.len());
for item in items {
let quoted = item.starts_with('"');
let inner = unquote(item);
if quoted && split_inside_quotes && inner.contains(',') {
values.extend(inner.split(',').map(caret_decode));
} else {
values.push(caret_decode(inner));
}
}
values
}