use std::borrow::Cow;
use std::fmt::Write;
use crate::unicode::is_hidden;
#[derive(Clone, Debug)]
pub(crate) enum Segment<'a> {
Root,
Key(Cow<'a, [u8]>),
Index(usize),
}
pub(crate) const MAX_PATH: usize = 256;
pub(crate) fn bounded<'s>(count: usize, segment: impl Fn(usize) -> &'s str) -> String {
let mut path = String::new();
let mut len = 0;
let fits = (0..count).all(|i| {
len += segment(i).len();
len <= MAX_PATH
});
if fits {
(0..count).for_each(|i| path.push_str(segment(i)));
return path;
}
let half = MAX_PATH / 2;
for i in 0..count {
let s = segment(i);
let room = half - path.len();
if s.len() <= room {
path.push_str(s);
continue;
}
if s.len() > half {
let mut end = room;
while !s.is_char_boundary(end) {
end -= 1;
}
path.push_str(&s[..end]);
}
break;
}
path.push('…');
let (mut from, mut len) = (count, 0);
while from > 0 {
let s = segment(from - 1);
if len + s.len() <= half {
from -= 1;
len += s.len();
continue;
}
if s.len() > half {
let mut start = s.len() - (half - len);
while !s.is_char_boundary(start) {
start += 1;
}
path.push_str(&s[start..]);
}
break;
}
(from..count).for_each(|i| path.push_str(segment(i)));
path
}
pub(crate) fn rendered(segment: &Segment<'_>) -> String {
let mut path = String::new();
push(&mut path, segment);
path
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PathStep<'a> {
Key(&'a [u8]),
Index(usize),
}
#[doc(hidden)]
pub fn json_path<'a>(steps: impl IntoIterator<Item = PathStep<'a>>) -> String {
let segments: Vec<String> = std::iter::once("$".to_owned())
.chain(steps.into_iter().map(|step| match step {
PathStep::Key(key) => rendered(&Segment::Key(Cow::Borrowed(key))),
PathStep::Index(i) => rendered(&Segment::Index(i)),
}))
.collect();
bounded(segments.len(), |i| &segments[i])
}
fn push(path: &mut String, segment: &Segment<'_>) {
match segment {
Segment::Root => path.push('$'),
Segment::Key(key) => push_key(path, key),
Segment::Index(i) => {
let _ = write!(path, "[{i}]");
}
}
}
pub(crate) fn push_key(path: &mut String, key: &[u8]) {
let chars = decode(key);
if is_identifier(&chars) {
path.push('.');
path.extend(chars.iter().flatten());
} else {
path.push('[');
quote(path, &chars);
path.push(']');
}
}
#[doc(hidden)]
pub fn key_text(key: &[u8]) -> Cow<'_, str> {
text(Cow::Borrowed(key))
}
pub(crate) fn text(key: Cow<'_, [u8]>) -> Cow<'_, str> {
let lossy = |bytes: &[u8]| -> String {
decode(bytes)
.into_iter()
.map(|c| c.unwrap_or('\u{FFFD}'))
.collect()
};
match key {
Cow::Borrowed(bytes) => match std::str::from_utf8(bytes) {
Ok(text) => Cow::Borrowed(text),
Err(_) => Cow::Owned(lossy(bytes)),
},
Cow::Owned(bytes) => {
Cow::Owned(String::from_utf8(bytes).unwrap_or_else(|e| lossy(e.as_bytes())))
}
}
}
fn decode(mut key: &[u8]) -> Vec<Result<char, u32>> {
let mut chars = Vec::new();
loop {
let valid = match std::str::from_utf8(key) {
Ok(text) => text,
Err(e) => std::str::from_utf8(&key[..e.valid_up_to()]).unwrap_or_default(),
};
chars.extend(valid.chars().map(Ok));
let Some(&[_, high, low]) = key.get(valid.len()..valid.len() + 3) else {
return chars;
};
chars.push(Err(0xD000
| (u32::from(high & 0x3F) << 6)
| u32::from(low & 0x3F)));
key = &key[valid.len() + 3..];
}
}
fn is_identifier(chars: &[Result<char, u32>]) -> bool {
use unicode_properties::GeneralCategoryGroup::{Letter, Number};
use unicode_properties::UnicodeGeneralCategory;
let mut chars = chars.iter();
chars
.next()
.is_some_and(|c| matches!(c, Ok(c) if c.is_ascii_alphabetic() || *c == '_' || *c == '$'))
&& chars.all(|c| {
matches!(c, Ok(c) if *c == '_' || *c == '$'
|| matches!(c.general_category_group(), Letter | Number))
})
}
fn quote(out: &mut String, chars: &[Result<char, u32>]) {
out.push('"');
for c in chars {
let c = match c {
Ok(c) => *c,
Err(unit) => {
let _ = write!(out, "\\u{unit:04x}");
continue;
}
};
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{8}' => out.push_str("\\b"),
'\u{c}' => out.push_str("\\f"),
c if c.is_control() || is_hidden(c) || c == '…' => {
let mut units = [0u16; 2];
for unit in c.encode_utf16(&mut units) {
let _ = write!(out, "\\u{unit:04x}");
}
}
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::{MAX_PATH, bounded, push_key};
fn rendered(key: &str) -> String {
let mut path = "$".to_owned();
push_key(&mut path, key.as_bytes());
path
}
#[test]
fn keys_render_as_check_json5_rendered_them_but_keep_non_ascii() {
assert_eq!(rendered("server"), "$.server");
assert_eq!(rendered("$ref"), "$.$ref");
assert_eq!(rendered("café"), "$.café");
assert_eq!(rendered("1st"), "$[\"1st\"]");
assert_eq!(rendered("b c"), "$[\"b c\"]");
assert_eq!(rendered("ünicode"), "$[\"ünicode\"]");
assert_eq!(rendered("日本 語"), "$[\"日本 語\"]");
assert_eq!(rendered("q\"\\\n"), "$[\"q\\\"\\\\\\n\"]");
assert_eq!(rendered("😀"), "$[\"😀\"]");
assert_eq!(
rendered("a\u{7f}\u{85}\u{202e}\u{ad}…\u{e0041}"),
"$[\"a\\u007f\\u0085\\u202e\\u00ad\\u2026\\udb40\\udc41\"]"
);
assert_eq!(rendered(""), "$[\"\"]");
assert_eq!(rendered("a\u{093E}"), "$[\"a\u{093E}\"]");
assert_eq!(rendered("a\u{24B6}"), "$[\"a\u{24B6}\"]");
assert_eq!(rendered("a\u{0345}"), "$[\"a\u{0345}\"]");
assert_eq!(rendered("a\u{00B2}"), "$.a\u{00B2}");
let mut path = "$".to_owned();
push_key(&mut path, b"a\xED\xA0\x80b");
assert_eq!(path, "$[\"a\\ud800b\"]");
}
#[test]
fn a_lone_surrogate_becomes_one_replacement_character() {
use std::borrow::Cow;
let lone = [0xED, 0xA0, 0x80];
assert!(matches!(
super::text(Cow::Borrowed(b"ok")),
Cow::Borrowed("ok")
));
assert_eq!(super::text(Cow::Borrowed(&lone)), "\u{FFFD}");
let owned = [b"a".as_slice(), &lone, b"b"].concat();
assert_eq!(super::text(Cow::Owned(owned)), "a\u{FFFD}b");
}
#[test]
fn long_paths_keep_their_ends() {
let path = |segments: &[&str]| bounded(segments.len(), |i| segments[i]);
let key = format!(".{}", "k".repeat(MAX_PATH - 2));
assert_eq!(path(&["$", &key]), format!("${key}"));
let key = format!("{key}k");
let long = path(&["$", &key]);
assert_eq!(long.len(), MAX_PATH + '…'.len_utf8());
assert!(long.starts_with("$.kkk") && long.ends_with("kkk") && long.contains('…'));
let deep: Vec<&str> = std::iter::once("$")
.chain(std::iter::repeat_n("[0]", 1000))
.chain([".é", ".last"])
.collect();
let head = format!("${}", "[0]".repeat(42));
let tail = format!("{}.é.last", "[0]".repeat(40));
assert_eq!(path(&deep), format!("{head}…{tail}"));
let wide = format!(".{}", "é".repeat(MAX_PATH));
let wide = path(&["$", &wide]);
assert!(wide.len() <= MAX_PATH + '…'.len_utf8());
let half = MAX_PATH / 2;
let x = format!(".{}", "x".repeat(half - 1));
let y = format!(".{}", "y".repeat(half - 1));
assert_eq!(path(&["$", &x, &y, ".z"]), "$….z");
let s = format!(".a{}", "é".repeat(200));
let cut = "é".repeat(62);
assert_eq!(path(&["$", &s, &s, ".zz"]), format!("$.a{cut}…{cut}.zz"));
}
}