#[must_use]
fn percent_decode(fragment: &str) -> Option<String> {
let bytes = fragment.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
decoded.push(u8::from_str_radix(fragment.get(i + 1..i + 3)?, 16).ok()?);
i += 3;
} else {
decoded.push(bytes[i]);
i += 1;
}
}
String::from_utf8(decoded).ok()
}
#[must_use]
fn unescape(token: &str) -> Option<String> {
let mut out = String::with_capacity(token.len());
let mut chars = token.chars();
while let Some(c) = chars.next() {
if c != '~' {
out.push(c);
continue;
}
match chars.next() {
Some('0') => out.push('~'),
Some('1') => out.push('/'),
_ => return None,
}
}
Some(out)
}
#[must_use]
pub(crate) fn tokens(fragment: &str) -> Option<Vec<String>> {
let pointer = percent_decode(fragment)?;
if pointer.is_empty() {
return Some(Vec::new());
}
pointer
.strip_prefix('/')?
.split('/')
.map(unescape)
.collect()
}
#[must_use]
pub(crate) fn array_index(token: &str) -> Option<usize> {
if token.len() > 1 && token.starts_with('0') {
return None;
}
if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
token.parse().ok()
}
#[must_use]
pub(crate) fn walk<'v>(
value: &'v serde_json::Value,
path: &[String],
) -> Option<&'v serde_json::Value> {
let mut current = value;
for token in path {
current = match current {
serde_json::Value::Object(map) => map.get(token)?,
serde_json::Value::Array(items) => items.get(array_index(token)?)?,
_ => return None,
};
}
Some(current)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_fragment_is_decoded_whole_and_then_split() {
assert_eq!(tokens("/x-a%2Fb").unwrap(), vec!["x-a", "b"]);
assert_eq!(
tokens("/channels/source%2Fpath").unwrap(),
vec!["channels", "source", "path"]
);
assert_eq!(
tokens("/channels/source~1path").unwrap(),
vec!["channels", "source/path"]
);
assert_eq!(
tokens("/channels/source%7E1path").unwrap(),
vec!["channels", "source/path"]
);
assert_eq!(tokens("").unwrap(), Vec::<String>::new());
assert_eq!(tokens("/a%20b").unwrap(), vec!["a b"]);
assert_eq!(tokens("/a~0b").unwrap(), vec!["a~b"]);
}
#[test]
fn malformed_fragments_are_not_pointers() {
for bad in [
"/a~2b", "/a~", "/a%2", "/a%zz", "/a%FF", "no-leading-slash",
] {
assert!(tokens(bad).is_none(), "{bad} must not be a pointer");
}
}
#[test]
fn array_indices_follow_rfc_6901() {
assert_eq!(array_index("0"), Some(0));
assert_eq!(array_index("12"), Some(12));
for bad in ["01", "007", "-1", "1.0", "x", "", " 1"] {
assert!(array_index(bad).is_none(), "{bad} must not be an index");
}
}
#[test]
fn walk_steps_through_objects_and_arrays() {
let document = json!({
"channels": { "a/b": { "items": [ { "name": "first" }, { "name": "second" } ] } }
});
let path = tokens("/channels/a~1b/items/1/name").unwrap();
assert_eq!(walk(&document, &path), Some(&json!("second")));
for pointer in [
"/channels/ghost",
"/channels/a~1b/items/9",
"/channels/a~1b/items/01",
"/channels/a~1b/items/0/name/deeper",
] {
let path = tokens(pointer).unwrap();
assert!(walk(&document, &path).is_none(), "{pointer} must not walk");
}
assert_eq!(walk(&document, &[]), Some(&document));
}
}