use std::{borrow::Cow, convert::Infallible, str::FromStr};
#[cfg(feature = "date")]
use chrono::NaiveDate;
#[cfg(feature = "url")]
use url::Url;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OsReleaseEntry<'a> {
key: Cow<'a, str>,
value: Cow<'a, str>,
}
impl<'a> OsReleaseEntry<'a> {
pub fn new<K, V>(key: K, value: V) -> Self
where
K: Into<Cow<'a, str>>,
V: Into<Cow<'a, str>>,
{
let key = key.into();
let value = value.into();
Self { key, value }
}
pub fn key(&self) -> &str {
&self.key
}
pub fn value(&self) -> &str {
&self.value
}
pub fn value_as_list(&self) -> impl Iterator<Item = &str> {
self.value.split_whitespace()
}
#[cfg(feature = "url")]
#[cfg_attr(docsrs, doc(cfg(feature = "url")))]
pub fn value_as_url(&self) -> Result<Url, url::ParseError> {
Url::parse(&self.value)
}
#[cfg(feature = "date")]
#[cfg_attr(docsrs, doc(cfg(feature = "date")))]
pub fn value_as_date(&self) -> Result<NaiveDate, chrono::ParseError> {
NaiveDate::parse_from_str(&self.value, "%Y-%m-%d")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OsReleaseLine<'a> {
Empty,
Entry(OsReleaseEntry<'a>),
}
impl<'a> OsReleaseLine<'a> {
pub fn into_entry(self) -> Option<OsReleaseEntry<'a>> {
match self {
Self::Empty => None,
Self::Entry(entry) => Some(entry),
}
}
}
impl FromStr for OsReleaseLine<'static> {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(parse_line(s).map_or(Self::Empty, Self::Entry))
}
}
fn parse_line(line: &str) -> Option<OsReleaseEntry<'static>> {
if line.is_empty() || line.starts_with('#') {
return None;
}
let (key, value) = line.split_once('=')?;
let key = key.to_owned();
let value = match trim_quote(value) {
(value, Some('\'')) => value.to_owned(),
(value, _) => unescape(value),
};
Some(OsReleaseEntry::new(key, value))
}
fn trim_quote(value: &str) -> (&str, Option<char>) {
let quotes = &['"', '\''];
for "e in quotes {
if let Some(value) = value.strip_prefix(quote) {
let value = value.strip_suffix(quote).unwrap_or(value);
return (value, Some(quote));
}
}
(value, None)
}
fn unescape(value: &str) -> String {
let mut output = String::new();
let mut escaped = false;
for c in value.chars() {
if escaped {
escaped = false;
output.push(c);
continue;
}
if c == '\\' {
escaped = true;
continue;
}
output.push(c);
}
output
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_line() {
fn entry<'a>(key: &'a str, value: &'a str) -> OsReleaseEntry<'a> {
OsReleaseEntry::new(key, value)
}
assert!(parse_line("").is_none());
assert!(parse_line("# comment").is_none());
assert_eq!(parse_line("A=B").unwrap(), entry("A", "B"));
assert_eq!(parse_line(r#"A="B C""#).unwrap(), entry("A", "B C"));
assert_eq!(
parse_line(r#"A="B C\"\"""#).unwrap(),
entry("A", r#"B C"""#)
);
assert_eq!(
parse_line(r#"A='B C\"\"'"#).unwrap(),
entry("A", r#"B C\"\""#)
);
}
}