use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive] pub struct SDParam {
pub name: String,
pub value: String,
}
impl SDParam {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Result<Self, String> {
let name = name.into();
Self::validate_name(&name)?;
let value = value.into();
Ok(Self { name, value })
}
pub fn escape_value(&self) -> String {
let mut escaped = String::new();
for c in self.value.chars() {
if matches!(c, '\\' | '"' | ']') {
escaped.push('\\');
}
escaped.push(c);
}
escaped
}
fn validate_name(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("PARAM-NAME must not be empty".to_string());
}
if name.len() > 32 {
return Err(format!(
"PARAM-NAME must not be less than 32 characters: {name}"
));
}
for c in name.chars() {
match c {
'=' => return Err(format!("PARAM-NAME must not contain '=': {name}")),
']' => return Err(format!("PARAM-NAME must not contain ']': {name}")),
' ' => return Err(format!("PARAM-NAME must not contain ' ': {name}")),
'"' => return Err(format!("PARAM-NAME must not contain '\"': {name}")),
c => {
let codepoint = c as u32;
if !(33..=126).contains(&codepoint) {
return Err(format!(
"PARAM-NAME must only contain printable ASCII characters: {name}"
));
}
}
}
}
Ok(())
}
}
impl fmt::Display for SDParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}=\"{}\"", self.name, self.escape_value())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SDElement {
pub id: String,
params: Vec<SDParam>,
}
impl SDElement {
pub fn new(id: impl Into<String>) -> Result<Self, String> {
let id = id.into();
Self::validate_id(&id)?;
Ok(Self { id, params: vec![] })
}
pub fn add_param(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Result<(), String> {
let param = SDParam::new(name, value)?;
self.params.push(param);
Ok(())
}
const fn registered_ids() -> [&'static str; 3] {
["timeQuality", "origin", "meta"]
}
fn validate_id(id: &str) -> Result<(), String> {
if id.is_empty() {
return Err("SD-ID must not be empty".to_string());
}
if id.len() > 32 {
return Err(format!("SD-ID must not be less than 32 characters: {id}"));
}
let mut has_at_sign = false;
for c in id.chars() {
match c {
'@' => has_at_sign = true,
'=' => return Err(format!("SD-ID must not contain '=': {id}")),
']' => return Err(format!("SD-ID must not contain ']': {id}")),
' ' => return Err(format!("SD-ID must not contain ' ': {id}")),
'"' => return Err(format!("SD-ID must not contain '\"': {id}")),
c => {
let codepoint = c as u32;
if !(33..=126).contains(&codepoint) {
return Err(format!(
"SD-ID must only contain printable ASCII characters: {id}"
));
}
}
}
}
if !has_at_sign && !Self::registered_ids().contains(&id) {
return Err(format!(
"SD-ID must contain '@' or be one of the registered IDs: {id}"
));
}
Ok(())
}
}
impl fmt::Display for SDElement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}", self.id)?;
for param in &self.params {
write!(f, " {param}")?;
}
write!(f, "]")
}
}