337 lines
13 KiB
Rust
337 lines
13 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
use percent_encoding::{percent_decode, percent_encode, NON_ALPHANUMERIC};
|
|
use std::str;
|
|
|
|
/// The `FragmentDirectiveParameter` represents one of
|
|
/// `[prefix-,]start[,end][,-suffix]` without any surrounding `-` or `,`.
|
|
///
|
|
/// The token is stored as percent-decoded string.
|
|
/// Therefore, interfaces exist to
|
|
/// - create a `FragmentDirectiveParameter` from a percent-encoded string.
|
|
/// This function will determine from occurrence and position of a dash
|
|
/// if the token represents a `prefix`, `suffix` or either `start` or `end`.
|
|
/// - create a percent-encoded string from the value the token holds.
|
|
pub enum TextDirectiveParameter {
|
|
Prefix(String),
|
|
StartOrEnd(String),
|
|
Suffix(String),
|
|
}
|
|
|
|
impl TextDirectiveParameter {
|
|
/// Creates a token from a percent-encoded string.
|
|
/// Based on position of a dash the correct token type is determined.
|
|
/// Returns `None` in case of an ill-formed token:
|
|
/// - starts and ends with a dash (i.e. `-token-`)
|
|
/// - only consists of a dash (i.e. `-`) or is empty
|
|
/// - conversion from percent-encoded string to utf8 fails.
|
|
pub fn from_percent_encoded(token: &[u8]) -> Option<Self> {
|
|
if token.is_empty() {
|
|
return None;
|
|
}
|
|
let starts_with_dash = *token.first().unwrap() == b'-';
|
|
let ends_with_dash = *token.last().unwrap() == b'-';
|
|
if starts_with_dash && ends_with_dash {
|
|
// `-token-` is not valid.
|
|
return None;
|
|
}
|
|
if token.len() == 1 && starts_with_dash {
|
|
// `-` is not valid.
|
|
return None;
|
|
}
|
|
// Note: Trimming of the raw strings is currently not mentioned in the spec.
|
|
// However, it looks as it is implicitly expected.
|
|
if starts_with_dash {
|
|
if let Ok(decoded_suffix) = percent_decode(&token[1..]).decode_utf8() {
|
|
return Some(TextDirectiveParameter::Suffix(String::from(
|
|
decoded_suffix.trim(),
|
|
)));
|
|
}
|
|
return None;
|
|
}
|
|
if ends_with_dash {
|
|
if let Ok(decoded_prefix) = percent_decode(&token[..token.len() - 1]).decode_utf8() {
|
|
return Some(TextDirectiveParameter::Prefix(String::from(
|
|
decoded_prefix.trim(),
|
|
)));
|
|
}
|
|
return None;
|
|
}
|
|
if let Ok(decoded_text) = percent_decode(&token).decode_utf8() {
|
|
return Some(TextDirectiveParameter::StartOrEnd(String::from(
|
|
decoded_text.trim(),
|
|
)));
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Returns the value of the token as percent-decoded `String`.
|
|
pub fn value(&self) -> &String {
|
|
match self {
|
|
TextDirectiveParameter::Prefix(value) => &value,
|
|
TextDirectiveParameter::StartOrEnd(value) => &value,
|
|
TextDirectiveParameter::Suffix(value) => &value,
|
|
}
|
|
}
|
|
|
|
/// Creates a percent-encoded string of the token's value.
|
|
/// This includes placing a dash appropriately
|
|
/// to indicate whether this token is prefix, suffix or start/end.
|
|
///
|
|
/// This method always returns a new object.
|
|
pub fn to_percent_encoded_string(&self) -> String {
|
|
let encode = |text: &String| percent_encode(text.as_bytes(), NON_ALPHANUMERIC).to_string();
|
|
match self {
|
|
Self::Prefix(text) => encode(text) + "-",
|
|
Self::StartOrEnd(text) => encode(text),
|
|
Self::Suffix(text) => {
|
|
let encoded = encode(text);
|
|
let mut result = String::with_capacity(encoded.len() + 1);
|
|
result.push_str("-");
|
|
result.push_str(&encoded);
|
|
result
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This struct represents one parsed text directive using Rust types.
|
|
///
|
|
/// A text fragment is encoded into a URL fragment like this:
|
|
/// `text=[prefix-,]start[,end][,-suffix]`
|
|
///
|
|
/// The text directive is considered valid if at least `start` is not None.
|
|
/// (see `Self::is_valid()`).
|
|
#[derive(Default)]
|
|
pub struct TextDirective {
|
|
prefix: Option<TextDirectiveParameter>,
|
|
start: Option<TextDirectiveParameter>,
|
|
end: Option<TextDirectiveParameter>,
|
|
suffix: Option<TextDirectiveParameter>,
|
|
}
|
|
impl TextDirective {
|
|
/// Creates an instance from string parts.
|
|
/// This function is intended to be used when a fragment directive string should be created.
|
|
/// Returns `None` if `start` is empty.
|
|
pub fn from_parts(prefix: String, start: String, end: String, suffix: String) -> Option<Self> {
|
|
if !start.is_empty() {
|
|
Some(Self {
|
|
prefix: if !prefix.is_empty() {
|
|
Some(TextDirectiveParameter::Prefix(prefix.trim().into()))
|
|
} else {
|
|
None
|
|
},
|
|
start: Some(TextDirectiveParameter::StartOrEnd(start.trim().into())),
|
|
end: if !end.is_empty() {
|
|
Some(TextDirectiveParameter::StartOrEnd(end.trim().into()))
|
|
} else {
|
|
None
|
|
},
|
|
suffix: if !suffix.is_empty() {
|
|
Some(TextDirectiveParameter::Suffix(suffix.trim().into()))
|
|
} else {
|
|
None
|
|
},
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Creates an instance from a percent-encoded string
|
|
/// that originates from a fragment directive.
|
|
///
|
|
/// `text_fragment` is supposed to have this format:
|
|
/// ```ignore
|
|
/// text=[prefix-,]start[,end][,-suffix]
|
|
/// ```
|
|
/// This function returns `None` if `text_fragment`
|
|
/// does not start with `text=`, it contains 0 or more
|
|
/// than 4 elements or prefix/suffix/start or end
|
|
/// occur too many times.
|
|
/// It also returns `None` if any of the tokens parses to fail.
|
|
pub fn from_percent_encoded_string(text_directive: &str) -> Option<Self> {
|
|
// first check if the string starts with `text=`
|
|
if text_directive.len() < 6 {
|
|
return None;
|
|
}
|
|
if !text_directive.starts_with("text=") {
|
|
return None;
|
|
}
|
|
|
|
let mut parsed_text_directive = Self::default();
|
|
let valid = text_directive[5..]
|
|
.split(",")
|
|
// Parse the substrings into `TextDirectiveParameter`s. This will determine
|
|
// for each substring if it is a Prefix, Suffix or Start/End,
|
|
// or if it is invalid.
|
|
.map(|token| TextDirectiveParameter::from_percent_encoded(token.as_bytes()))
|
|
// populate `parsed_text_directive` and check its validity by inserting the parameters
|
|
// one by one. Given that the parameters are sorted by their position in the source,
|
|
// the validity of the text directive can be determined while adding the parameters.
|
|
.map(|token| match token {
|
|
Some(TextDirectiveParameter::Prefix(..)) => {
|
|
if !parsed_text_directive.is_empty() {
|
|
// `prefix-` must be the first result.
|
|
return false;
|
|
}
|
|
parsed_text_directive.prefix = token;
|
|
return true;
|
|
}
|
|
Some(TextDirectiveParameter::StartOrEnd(..)) => {
|
|
if parsed_text_directive.suffix.is_some() {
|
|
// start or end must come before `-suffix`.
|
|
return false;
|
|
}
|
|
if parsed_text_directive.start.is_none() {
|
|
parsed_text_directive.start = token;
|
|
return true;
|
|
}
|
|
if parsed_text_directive.end.is_none() {
|
|
parsed_text_directive.end = token;
|
|
return true;
|
|
}
|
|
// if `start` and `end` is already filled,
|
|
// this is invalid as well.
|
|
return false;
|
|
}
|
|
Some(TextDirectiveParameter::Suffix(..)) => {
|
|
if parsed_text_directive.start.is_some()
|
|
&& parsed_text_directive.suffix.is_none()
|
|
{
|
|
// `start` must be present and `-suffix` must not be present.
|
|
// `end` may be present.
|
|
parsed_text_directive.suffix = token;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
// empty or invalid token renders the whole text directive invalid.
|
|
None => false,
|
|
})
|
|
.all(|valid| valid);
|
|
if valid {
|
|
return Some(parsed_text_directive);
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Creates a percent-encoded string for the current `TextDirective`.
|
|
/// In the unlikely case that the `TextDirective` is invalid (i.e. `start` is None),
|
|
/// which should have been caught earlier,this method returns an empty string.
|
|
pub fn to_percent_encoded_string(&self) -> String {
|
|
if !self.is_valid() {
|
|
return String::default();
|
|
}
|
|
String::from("text=")
|
|
+ &[&self.prefix, &self.start, &self.end, &self.suffix]
|
|
.iter()
|
|
.filter_map(|&token| token.as_ref())
|
|
.map(|token| token.to_percent_encoded_string())
|
|
.collect::<Vec<_>>()
|
|
.join(",")
|
|
}
|
|
|
|
pub fn start(&self) -> &Option<TextDirectiveParameter> {
|
|
&self.start
|
|
}
|
|
|
|
pub fn end(&self) -> &Option<TextDirectiveParameter> {
|
|
&self.end
|
|
}
|
|
|
|
pub fn prefix(&self) -> &Option<TextDirectiveParameter> {
|
|
&self.prefix
|
|
}
|
|
|
|
pub fn suffix(&self) -> &Option<TextDirectiveParameter> {
|
|
&self.suffix
|
|
}
|
|
|
|
fn is_empty(&self) -> bool {
|
|
self.prefix.is_none() && self.start.is_none() && self.end.is_none() && self.suffix.is_none()
|
|
}
|
|
|
|
/// A `TextDirective` object is valid if it contains the `start` token.
|
|
/// All other tokens are optional.
|
|
fn is_valid(&self) -> bool {
|
|
self.start.is_some()
|
|
}
|
|
}
|
|
/// Parses a fragment directive into a list of `TextDirective` objects and removes
|
|
/// the fragment directive from the input url.
|
|
///
|
|
/// If the hash does not contain a fragment directive, `hash` is not modified
|
|
/// and this function returns `None`.
|
|
/// Otherwise, the fragment directive is removed from `hash` and parsed.
|
|
/// The function returns a tuple of three elements:
|
|
/// - The input url hash without the fragment directive. Trailing `#`s are removed as well.
|
|
/// - The unparsed fragment directive.
|
|
/// - All parsed valid text directives. Invalid text directives are silently ignored.
|
|
pub fn parse_fragment_directive_and_remove_it_from_hash(
|
|
hash: &str,
|
|
) -> Option<(&str, &str, Vec<TextDirective>)> {
|
|
// The Fragment Directive is preceded by a `:~:`,
|
|
// which is only allowed to appear in the hash once.
|
|
let mut fragment_directive_iter = hash.split(":~:");
|
|
let hash_without_fragment_directive =
|
|
&hash[..fragment_directive_iter.next().unwrap_or_default().len()];
|
|
|
|
if let Some(fragment_directive) = fragment_directive_iter.next() {
|
|
if fragment_directive_iter.next().is_some() {
|
|
// There are multiple occurrences of `:~:`, which is not allowed.
|
|
return Some((hash_without_fragment_directive, fragment_directive, vec![]));
|
|
}
|
|
// - fragments are separated by `&`.
|
|
// - if a fragment does not start with `text=`, it is not a text directive and will be ignored.
|
|
// - if parsing of the text fragment fails (for whatever reason), it will be ignored.
|
|
let text_directives: Vec<_> = fragment_directive
|
|
.split("&")
|
|
.map(|maybe_text_fragment| {
|
|
TextDirective::from_percent_encoded_string(&maybe_text_fragment)
|
|
})
|
|
.filter_map(|maybe_text_directive| maybe_text_directive)
|
|
.collect();
|
|
|
|
return Some((
|
|
hash_without_fragment_directive,
|
|
fragment_directive,
|
|
text_directives,
|
|
));
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Creates a percent-encoded text fragment string.
|
|
///
|
|
/// The returned string starts with `:~:`, so that it can be appended
|
|
/// to a normal fragment.
|
|
/// Text directives which are not valid (ie., they are missing the `start` parameter),
|
|
/// are skipped.
|
|
///
|
|
/// Returns `None` if `fragment_directives` is empty.
|
|
pub fn create_fragment_directive_string(text_directives: &Vec<TextDirective>) -> Option<String> {
|
|
if text_directives.is_empty() {
|
|
return None;
|
|
}
|
|
let encoded_fragment_directives: Vec<_> = text_directives
|
|
.iter()
|
|
.filter(|&fragment_directive| fragment_directive.is_valid())
|
|
.map(|fragment_directive| fragment_directive.to_percent_encoded_string())
|
|
.filter(|text_directive| !text_directive.is_empty())
|
|
.collect();
|
|
if encoded_fragment_directives.is_empty() {
|
|
return None;
|
|
}
|
|
Some(String::from(":~:") + &encoded_fragment_directives.join("&"))
|
|
}
|
|
|
|
/// Creates the percent-encoded text directive string for a single text directive.
|
|
pub fn create_text_directive_string(text_directive: &TextDirective) -> Option<String> {
|
|
if text_directive.is_valid() {
|
|
Some(text_directive.to_percent_encoded_string())
|
|
} else {
|
|
None
|
|
}
|
|
}
|