use alloc::{borrow::Cow, string::ToString};
use core::{
fmt::{Display, Write},
iter::FusedIterator,
};
pub const LINK_ATTR_REL: &str = "rel";
pub const LINK_ATTR_ANCHOR: &str = "anchor";
pub const LINK_ATTR_HREFLANG: &str = "hreflang";
pub const LINK_ATTR_MEDIA: &str = "media";
pub const LINK_ATTR_TITLE: &str = "title";
pub const LINK_ATTR_TITLE_STAR: &str = "title*";
#[doc(hidden)]
pub const LINK_ATTR_TYPE: &str = "type";
pub const LINK_ATTR_RESOURCE_TYPE: &str = "rt";
pub const LINK_ATTR_INTERFACE_DESCRIPTION: &str = "if";
pub const LINK_ATTR_MAXIMUM_SIZE_ESTIMATE: &str = "sz";
pub const LINK_ATTR_VALUE: &str = "v";
pub const LINK_ATTR_CONTENT_FORMAT: &str = "ct";
pub const LINK_ATTR_OBSERVABLE: &str = "obs";
pub const LINK_ATTR_ENDPOINT_NAME: &str = "ep";
pub const LINK_ATTR_REGISTRATION_LIFETIME: &str = "lt";
pub const LINK_ATTR_SECTOR: &str = "d";
pub const LINK_ATTR_REGISTRATION_BASE_URI: &str = "base";
pub const LINK_ATTR_GROUP_NAME: &str = "gp";
pub const LINK_ATTR_ENDPOINT_TYPE: &str = "et";
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ErrorLinkFormat {
ParseError,
}
const QUOTE_ESCAPE_CHAR: char = '\\';
const ATTR_SEPARATOR_CHAR: char = ';';
const LINK_SEPARATOR_CHAR: char = ',';
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct LinkFormatParser<'a> {
pub(super) inner: &'a str,
}
impl<'a> LinkFormatParser<'a> {
pub fn new(inner: &'a str) -> LinkFormatParser<'a> {
LinkFormatParser { inner }
}
}
impl<'a> Iterator for LinkFormatParser<'a> {
type Item = Result<(&'a str, LinkAttributeParser<'a>), ErrorLinkFormat>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.inner.is_empty() {
return None;
}
let mut iter = self.inner.chars();
loop {
match iter.next() {
Some(c) if c.is_ascii_whitespace() => continue,
Some('<') => break,
Some(_) => {
self.inner = "";
return Some(Err(ErrorLinkFormat::ParseError));
}
None => {
self.inner = "";
return None;
}
}
}
let link_ref = iter.as_str();
for c in iter.by_ref() {
if c == '>' {
break;
}
}
let link_len =
iter.as_str().as_ptr() as usize - link_ref.as_ptr() as usize;
let link_ref = (&link_ref[..link_len]).trim_end_matches('>');
let mut attr_keys = iter.as_str();
loop {
match iter.next() {
Some(LINK_SEPARATOR_CHAR) | None => {
break;
}
Some(c) if c == '"' => {
loop {
match iter.next() {
Some('"') | None => break,
Some(QUOTE_ESCAPE_CHAR) => {
iter.next();
}
_ => (),
}
}
}
_ => (),
}
}
let attr_len =
iter.as_str().as_ptr() as usize - attr_keys.as_ptr() as usize;
attr_keys =
(&attr_keys[..attr_len]).trim_end_matches(LINK_SEPARATOR_CHAR);
self.inner = iter.as_str();
return Some(Ok((
link_ref,
LinkAttributeParser {
inner: attr_keys.trim_matches(ATTR_SEPARATOR_CHAR),
},
)));
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct LinkAttributeParser<'a> {
pub(super) inner: &'a str,
}
impl<'a> Iterator for LinkAttributeParser<'a> {
type Item = (&'a str, Unquote<'a>);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.inner.is_empty() {
return None;
}
let mut iter = self.inner.chars();
loop {
match iter.next() {
Some(ATTR_SEPARATOR_CHAR) | None => {
break;
}
Some(c) if c == '"' => {
loop {
match iter.next() {
Some('"') | None => {
break;
}
Some(QUOTE_ESCAPE_CHAR) => {
iter.next();
}
_ => (),
}
}
}
_ => (),
}
}
let attr_len =
iter.as_str().as_ptr() as usize - self.inner.as_ptr() as usize;
let attr_str = &self.inner[..attr_len];
self.inner = iter.as_str();
let attr_str = attr_str.trim_end_matches(ATTR_SEPARATOR_CHAR);
let (key, value) = if let Some(i) = attr_str.find('=') {
let (key, value) = attr_str.split_at(i);
(key, &value[1..])
} else {
(attr_str, "")
};
return Some((key.trim(), Unquote::new(value.trim())));
}
}
#[derive(Clone, Debug)]
pub struct Unquote<'a> {
inner: core::str::Chars<'a>,
state: UnquoteState,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum UnquoteState {
NotStarted,
NotQuoted,
Quoted,
}
impl<'a> Eq for Unquote<'a> {}
impl<'a> PartialEq for Unquote<'a> {
fn eq(&self, other: &Self) -> bool {
let self_s = self.inner.as_str();
let other_s = other.inner.as_str();
self.state == other.state
&& self_s.as_ptr() == other_s.as_ptr()
&& self_s.len() == other_s.len()
}
}
impl<'a> From<Unquote<'a>> for Cow<'a, str> {
fn from(iter: Unquote<'a>) -> Self {
iter.to_cow()
}
}
impl<'a> FusedIterator for Unquote<'a> {}
impl<'a> Display for Unquote<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.clone().try_for_each(|c| f.write_char(c))
}
}
impl<'a> Unquote<'a> {
pub fn new(quoted_str: &'a str) -> Unquote<'a> {
Unquote {
inner: quoted_str.chars(),
state: UnquoteState::NotStarted,
}
}
pub fn into_raw_str(self) -> &'a str {
assert_eq!(self.state, UnquoteState::NotStarted);
self.inner.as_str()
}
pub fn to_cow(&self) -> Cow<'a, str> {
let str_ref = self.inner.as_str();
if self.is_quoted() {
if str_ref.find('\\').is_some() {
Cow::from(self.to_string())
} else {
Cow::from(&str_ref[1..str_ref.len() - 1])
}
} else {
Cow::from(str_ref)
}
}
pub fn is_quoted(&self) -> bool {
match self.state {
UnquoteState::NotStarted => self.inner.as_str().starts_with('"'),
UnquoteState::NotQuoted => false,
UnquoteState::Quoted => true,
}
}
}
impl<'a> Iterator for Unquote<'a> {
type Item = char;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
return match self.state {
UnquoteState::NotStarted => match self.inner.next() {
Some('"') => {
self.state = UnquoteState::Quoted;
continue;
}
c => {
self.state = UnquoteState::NotQuoted;
c
}
},
UnquoteState::NotQuoted => self.inner.next(),
UnquoteState::Quoted => match self.inner.next() {
Some('"') => {
self.inner = "".chars();
None
}
Some(QUOTE_ESCAPE_CHAR) => self.inner.next(),
c => c,
},
};
}
}
}
#[derive(Debug)]
pub struct LinkFormatWrite<'a, T: ?Sized> {
write: &'a mut T,
is_first: bool,
add_newlines: bool,
error: Option<core::fmt::Error>,
}
impl<'a, T: Write + ?Sized> LinkFormatWrite<'a, T> {
pub fn new(write: &'a mut T) -> LinkFormatWrite<'a, T> {
LinkFormatWrite {
write,
is_first: true,
add_newlines: false,
error: None,
}
}
pub fn set_add_newlines(&mut self, add_newlines: bool) {
self.add_newlines = add_newlines;
}
pub fn link<'b>(
&'b mut self,
link: &str,
) -> LinkAttributeWrite<'a, 'b, T> {
if self.is_first {
self.is_first = false;
} else if self.error.is_none() {
self.error = self.write.write_char(LINK_SEPARATOR_CHAR).err();
if self.add_newlines {
self.error = self.write.write_str("\n\r").err();
}
}
if self.error.is_none() {
self.error = self.write.write_char('<').err();
}
if self.error.is_none() {
self.error = write!(self.write, "{}", link).err();
}
if self.error.is_none() {
self.error = self.write.write_char('>').err();
}
LinkAttributeWrite(self)
}
pub fn finish(self) -> Result<(), core::fmt::Error> {
if let Some(e) = self.error {
Err(e)
} else {
Ok(())
}
}
}
#[derive(Debug)]
pub struct LinkAttributeWrite<'a, 'b, T: ?Sized>(
&'b mut LinkFormatWrite<'a, T>,
);
impl<'a, 'b, T: Write + ?Sized> LinkAttributeWrite<'a, 'b, T> {
fn internal_attr_key_eq(&mut self, key: &str) {
debug_assert!(key
.find(|c: char| c.is_ascii_whitespace() || c == '=')
.is_none());
if self.0.error.is_none() {
self.0.error = self.0.write.write_char(ATTR_SEPARATOR_CHAR).err();
}
if self.0.error.is_none() {
self.0.error = self.0.write.write_str(key).err();
}
if self.0.error.is_none() {
self.0.error = self.0.write.write_char('=').err();
}
}
pub fn attr(mut self, key: &str, value: &str) -> Self {
if value.find(|c: char| !c.is_ascii_alphanumeric()).is_some() {
return self.attr_quoted(key, value);
}
self.internal_attr_key_eq(key);
if self.0.error.is_none() {
self.0.error = self.0.write.write_str(value).err();
}
self
}
pub fn attr_u32(mut self, key: &str, value: u32) -> Self {
self.internal_attr_key_eq(key);
if self.0.error.is_none() {
self.0.error = write!(self.0.write, "{}", value).err();
}
self
}
pub fn attr_u16(self, key: &str, value: u16) -> Self {
self.attr_u32(key, value as u32)
}
pub fn attr_quoted(mut self, key: &str, value: &str) -> Self {
self.internal_attr_key_eq(key);
if self.0.error.is_none() {
self.0.error = self.0.write.write_char('"').err();
}
for c in value.chars() {
if (c == '"' || c == '\\') && self.0.error.is_none() {
self.0.error =
self.0.write.write_char(QUOTE_ESCAPE_CHAR).err();
}
if self.0.error.is_none() {
self.0.error = self.0.write.write_char(c).err();
}
}
if self.0.error.is_none() {
self.0.error = self.0.write.write_char('"').err();
}
self
}
pub fn finish(self) -> Result<(), core::fmt::Error> {
if let Some(e) = self.0.error {
Err(e)
} else {
Ok(())
}
}
}
#[cfg(test)]
mod test {
use super::*;
use alloc::string::{String, ToString};
#[test]
fn link_format_write_1() {
let mut buffer = String::new();
let mut write = LinkFormatWrite::new(&mut buffer);
write
.link("/sensor/light")
.attr_quoted(LINK_ATTR_INTERFACE_DESCRIPTION, "sensor")
.finish()
.expect("Write link failed");
assert_eq!(write.finish(), Ok(()));
assert_eq!(&buffer, r#"</sensor/light>;if="sensor""#);
}
#[test]
fn link_format_write_2() {
let mut buffer = String::new();
let mut write = LinkFormatWrite::new(&mut buffer);
write
.link("/sensor/light")
.attr_quoted(LINK_ATTR_INTERFACE_DESCRIPTION, "sensor")
.attr(LINK_ATTR_TITLE, "My Light")
.finish()
.expect("Write link failed");
write
.link("/sensor/temp")
.attr_quoted(LINK_ATTR_INTERFACE_DESCRIPTION, "sensor")
.attr(LINK_ATTR_TITLE, "My Thermostat")
.attr_u32(LINK_ATTR_VALUE, 20)
.finish()
.expect("Write link failed");
assert_eq!(write.finish(), Ok(()));
assert_eq!(
&buffer,
r#"</sensor/light>;if="sensor";title="My Light",</sensor/temp>;if="sensor";title="My Thermostat";v=20"#
);
}
#[test]
fn unquote_1() {
let unquote = Unquote::new(r#""sensor""#);
assert_eq!(&unquote.to_string(), "sensor");
}
#[test]
fn unquote_2() {
let unquote = Unquote::new("sensor");
assert_eq!(&unquote.to_string(), "sensor");
}
#[test]
fn unquote_3() {
let unquote = Unquote::new(r#""the \"foo\" bar""#);
assert_eq!(&unquote.to_string(), r#"the "foo" bar"#);
}
#[test]
fn unquote_4() {
let unquote = Unquote::new(r#""\"the foo bar\"""#);
assert_eq!(&unquote.to_string(), r#""the foo bar""#);
}
#[test]
fn unquote_5() {
let unquote = Unquote::new(r#""the \\\"foo\\\" bar""#);
assert_eq!(&unquote.to_string(), r#"the \"foo\" bar"#);
}
#[test]
fn link_format_parser_1() {
let link_format = r#"</sensors>;ct=40"#;
let mut parser = LinkFormatParser::new(link_format);
match parser.next() {
Some(Ok((link, mut attr_iter))) => {
assert_eq!(link, "/sensors");
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("ct", r#"40"#))
);
assert_eq!(attr_iter.next(), None);
}
x => {
panic!("{:?}", x);
}
}
assert_eq!(parser.next(), None);
}
#[test]
fn link_format_parser_2() {
let link_format = r#"
</sensors/temp>;if="sensor",
</sensors/light>;if="sensor""#;
let mut parser = LinkFormatParser::new(link_format);
match parser.next() {
Some(Ok((link, mut attr_iter))) => {
assert_eq!(link, "/sensors/temp");
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("if", r#""sensor""#))
);
assert_eq!(attr_iter.next(), None);
}
x => {
panic!("{:?}", x);
}
}
match parser.next() {
Some(Ok((link, mut attr_iter))) => {
assert_eq!(link, "/sensors/light");
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("if", r#""sensor""#))
);
assert_eq!(attr_iter.next(), None);
}
x => {
panic!("{:?}", x);
}
}
assert_eq!(parser.next(), None);
}
#[test]
fn link_format_parser_3() {
let link_format = r#"</sensors>;ct=40;title="Sensor Index",
</sensors/temp>;rt="temperature-c";if="sensor",
</sensors/light>;rt="light-lux";if="sensor",
<http://www.example.com/sensors/t123>;anchor="/sensors/temp"
;rel="describedby",
</t>;anchor="/sensors/temp";rel="alternate""#;
let mut parser = LinkFormatParser::new(link_format);
match parser.next() {
Some(Ok((link, mut attr_iter))) => {
assert_eq!(link, "/sensors");
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("ct", r#"40"#))
);
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("title", r#""Sensor Index""#))
);
assert_eq!(attr_iter.next(), None);
}
x => {
panic!("{:?}", x);
}
}
match parser.next() {
Some(Ok((link, mut attr_iter))) => {
assert_eq!(link, "/sensors/temp");
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("rt", r#""temperature-c""#))
);
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("if", r#""sensor""#))
);
assert_eq!(attr_iter.next(), None);
}
x => {
panic!("{:?}", x);
}
}
match parser.next() {
Some(Ok((link, mut attr_iter))) => {
assert_eq!(link, "/sensors/light");
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("rt", r#""light-lux""#))
);
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("if", r#""sensor""#))
);
assert_eq!(attr_iter.next(), None);
}
x => {
panic!("{:?}", x);
}
}
match parser.next() {
Some(Ok((link, mut attr_iter))) => {
assert_eq!(link, "http://www.example.com/sensors/t123");
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("anchor", r#""/sensors/temp""#))
);
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("rel", r#""describedby""#))
);
assert_eq!(attr_iter.next(), None);
}
x => {
panic!("{:?}", x);
}
}
match parser.next() {
Some(Ok((link, mut attr_iter))) => {
assert_eq!(link, "/t");
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("anchor", r#""/sensors/temp""#))
);
assert_eq!(
attr_iter
.next()
.map(|attr| (attr.0, attr.1.into_raw_str())),
Some(("rel", r#""alternate""#))
);
assert_eq!(attr_iter.next(), None);
}
x => {
panic!("{:?}", x);
}
}
assert_eq!(parser.next(), None);
}
}