Skip to content

Commit

Permalink
Merge pull request #106 from hoodie/feature/value-types
Browse files Browse the repository at this point in the history
fix: escape according to value types
  • Loading branch information
hoodie authored Aug 13, 2024
2 parents c0ffeec + 156177d commit 967a7e5
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 100 deletions.
13 changes: 13 additions & 0 deletions src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,19 @@ mod tests {

use super::*;

#[test]
fn get_url() {
let url = "http://hoodie.de/";
let event = Event::new().url(url).done();

let serialized = event.to_string();
let reparsed =
Other::from(crate::parser::Component::<'_>::try_from(serialized.as_str()).unwrap());

assert_eq!(event.get_url(), Some(url));
assert_eq!(reparsed.get_url(), Some(url));
}

#[test]
fn get_properties_unset() {
let event = Event::new();
Expand Down
2 changes: 1 addition & 1 deletion src/components/alarm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use super::*;
/// When it is time for the Alarm to occur we have to define what is actually supposed to happen.
/// The RFC5545 know three different actions, two of which are currently implemented.
///
/// 1. Display
/// 1. Display
/// 2. Audio
/// 3. Email (not yet implemented)
///
Expand Down
7 changes: 5 additions & 2 deletions src/components/date_time.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#![allow(dead_code, unused)]
use std::str::FromStr;

use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, Offset, TimeZone as _, Utc};
use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, TimeZone as _, Utc};

use crate::{Property, ValueType};

Expand Down Expand Up @@ -131,7 +130,9 @@ impl CalendarDateTime {
}
}

/// TODO: make public or delete
#[cfg(feature = "chrono-tz")]
#[allow(dead_code)]
pub(crate) fn with_timezone(dt: NaiveDateTime, tz_id: chrono_tz::Tz) -> Self {
Self::WithTimezone {
date_time: dt,
Expand Down Expand Up @@ -267,7 +268,9 @@ impl DatePerhapsTime {
}
}

/// TODO: make public or delete
#[cfg(feature = "chrono-tz")]
#[allow(dead_code)]
pub fn with_timezone<T: chrono::TimeZone + chrono_tz::OffsetName>(
dt: DateTime<T>,
) -> DatePerhapsTime {
Expand Down
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ mod components;
#[cfg(feature = "parser")]
pub mod parser;
mod properties;
mod value_types;

pub use crate::{
calendar::{Calendar, CalendarComponent},
Expand All @@ -108,7 +109,8 @@ pub use crate::{
date_time::{CalendarDateTime, DatePerhapsTime},
Component, Event, EventLike, Todo, Venue,
},
properties::{Class, EventStatus, Parameter, Property, TodoStatus, ValueType},
properties::{Class, EventStatus, Parameter, Property, TodoStatus},
value_types::ValueType,
};

#[cfg(feature = "chrono-tz")]
Expand Down
9 changes: 9 additions & 0 deletions src/parser/parsed_string.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::borrow::Cow;

use crate::ValueType;

/// A zero-copy string parsed from an iCal input.
#[derive(Debug, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand All @@ -26,6 +28,13 @@ impl ParseString<'_> {
}

impl<'a> ParseString<'a> {
pub fn unescape_by_value_type(self, value_type: ValueType) -> ParseString<'a> {
match value_type {
ValueType::Text => self.unescape_text(),
_ => self,
}
}

pub fn unescape_text(self) -> ParseString<'a> {
if self.0.contains(r#"\\"#)
|| self.0.contains(r#"\,"#)
Expand Down
108 changes: 93 additions & 15 deletions src/parser/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{
str::FromStr,
};

use crate::{parser::utils::valid_key_sequence_cow, properties::fold_line};
use crate::{parser::utils::valid_key_sequence_cow, properties::fold_line, value_types::ValueType};

use super::{
parameters::{parameters, Parameter},
Expand Down Expand Up @@ -124,7 +124,7 @@ impl FromStr for crate::Property {
}

#[test]
fn test_property() {
fn properties() {
assert_parser!(
property,
"KEY:VALUE\n",
Expand Down Expand Up @@ -164,31 +164,97 @@ fn test_property() {
params: vec![Parameter::new_ref("foo", Some("bar"))]
}
);
}

#[test]
fn unescape_text() {
assert_eq!(
"DESCRIPTION:https\\://example.com\n"
.parse::<crate::Property>()
.map(|p| p.val)
.unwrap(),
"https://example.com"
);

assert_parser!(
property,
"DESCRIPTION:https\\://example.com\n",
Property {
name: "DESCRIPTION".into(),
val: "https://example.com".into(),
params: Default::default()
}
);
}

#[test]
fn property_escape_url_as_url() {
assert_eq!(
"URL:https://example.com\n"
.parse::<crate::Property>()
.map(|p| p.val)
.unwrap(),
"https://example.com"
);

assert_parser!(
property,
"URL:https://example.com\n",
Property {
name: "URL".into(),
val: "https://example.com".into(),
params: Default::default()
}
);
}

// TODO: newlines followed by spaces must be ignored
#[test]
fn property_escape_description_as_text() {
assert_parser!(
property,
"KEY4;foo=bar:VALUE\\n newline separated\n",
"DESCRIPTION;foo=bar:VALUE\\n newline separated\n",
Property {
name: "KEY4".into(),
name: "DESCRIPTION".into(),
val: "VALUE\n newline separated".into(),
params: vec![Parameter::new_ref("foo", Some("bar"))]
}
);
}

#[test]
fn property_escape_by_value_param() {
assert_parser!(
property,
"KEY3;foo=bar:VALUE\\n newline separated\n",
"FOO;VALUE=TEXT:VALUE\\n newline separated\n",
Property {
name: "KEY3".into(),
name: "FOO".into(),
val: "VALUE\n newline separated".into(),
params: vec![Parameter::new_ref("foo", Some("bar"))]
params: vec![Parameter::new_ref("VALUE", Some("TEXT"))]
}
);

assert_parser!(
property,
"FOO_AS_TEXT;VALUE=TEXT:https\\://example.com\n",
Property {
name: "FOO_AS_TEXT".into(),
val: "https://example.com".into(),
params: vec![Parameter::new_ref("VALUE", Some("TEXT"))]
}
);
assert_parser!(
property,
"FOO_AS_URL;VALUE=URL:https\\://example.com\n",
Property {
name: "FOO_AS_URL".into(),
val: "https\\://example.com".into(),
params: vec![Parameter::new_ref("VALUE", Some("URL"))]
}
);
}

#[test]
fn test_property_with_dash() {
fn property_with_dash() {
assert_parser!(
property,
"X-HOODIE-KEY:VALUE\n",
Expand Down Expand Up @@ -291,6 +357,15 @@ fn parse_property_with_no_value() {
assert_parser!(property, sample_0, expectation);
}

fn determin_value_type(name: &ParseString, params: &[Parameter]) -> Option<ValueType> {
params
.iter()
.find(|param| param.key == "VALUE")
.and_then(|Parameter { val, .. }| val.as_ref())
.and_then(|typ| ValueType::from_str(typ.as_str()).ok())
.or_else(|| ValueType::by_name(name.as_str()))
}

pub fn property<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, Property, E> {
Expand Down Expand Up @@ -318,8 +393,7 @@ pub fn property<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
// this is for single line prop parsing, just so I can leave off the '\n'
take_while(|_| true),
))
.map(ParseString::from)
.map(ParseString::unescape_text),
.map(ParseString::from),
),
),
context(
Expand All @@ -329,10 +403,14 @@ pub fn property<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
)),
opt(line_ending),
))
.map(|(((key, params), val), _)| Property {
name: key,
val,
params,
.map(|(((name, params), val), _)| {
let val = if let Some(value_type) = determin_value_type(&name, &params) {
val.unescape_by_value_type(value_type)
} else {
val
};

Property { name, val, params }
})),
)
.parse(input)
Expand Down
Loading

0 comments on commit 967a7e5

Please sign in to comment.