fn is_whitespace(b: u8) -> bool {
matches!(b, 0x00 | 0x09 | 0x0A | 0x0C | 0x0D | 0x20 | 0x80 | 0xFF)
}
fn is_delimiter(b: u8) -> bool {
matches!(
b,
b'%' | b'(' | b')' | b'/' | b'<' | b'>' | b'[' | b']' | b'{' | b'}'
)
}
#[derive(Debug, Clone)]
pub struct Words<'a> {
data: &'a [u8],
at: usize,
}
impl<'a> Words<'a> {
#[must_use]
pub fn new(bytes: &'a [u8]) -> Self {
Self { data: bytes, at: 0 }
}
fn skip_spaces_and_comments(&mut self) -> Option<u8> {
loop {
let mut c = *self.data.get(self.at)?;
self.at += 1;
while is_whitespace(c) {
c = *self.data.get(self.at)?;
self.at += 1;
}
if c != b'%' {
return Some(c);
}
loop {
let c = *self.data.get(self.at)?;
self.at += 1;
if c == b'\r' || c == b'\n' {
break;
}
}
}
}
fn since(&self, start: usize) -> &'a [u8] {
self.data.get(start..self.at).unwrap_or_default()
}
fn regular(&mut self, start: usize) -> &'a [u8] {
while let Some(&c) = self.data.get(self.at) {
if is_delimiter(c) || is_whitespace(c) {
break;
}
self.at += 1;
}
self.since(start)
}
fn name(&mut self, start: usize) -> &'a [u8] {
while let Some(&c) = self.data.get(self.at) {
if is_whitespace(c) || is_delimiter(c) {
return self.since(start);
}
self.at += 1;
}
&[]
}
fn angle_open(&mut self, start: usize) -> &'a [u8] {
let Some(&first) = self.data.get(self.at) else {
return self.since(start);
};
self.at += 1;
if first == b'<' {
return self.since(start);
}
let mut c = first;
while self.at < self.data.len() && c != b'>' {
c = self.data.get(self.at).copied().unwrap_or(b'>');
self.at += 1;
}
self.since(start)
}
fn angle_close(&mut self, start: usize) -> &'a [u8] {
if self.data.get(self.at) == Some(&b'>') {
self.at += 1;
}
self.since(start)
}
fn parens(&mut self, start: usize) -> &'a [u8] {
let mut level = 1i32;
while level > 0 {
let Some(&c) = self.data.get(self.at) else {
break;
};
self.at += 1;
match c {
b'(' => level += 1,
b')' => level -= 1,
_ => {}
}
}
self.since(start)
}
}
impl<'a> Iterator for Words<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
let first = self.skip_spaces_and_comments()?;
let start = self.at.checked_sub(1)?;
let word = if is_delimiter(first) {
match first {
b'/' => self.name(start),
b'<' => self.angle_open(start),
b'>' => self.angle_close(start),
b'(' => self.parens(start),
_ => self.since(start),
}
} else {
self.regular(start)
};
(!word.is_empty()).then_some(word)
}
}
#[cfg(test)]
mod tests {
use super::Words;
fn words(bytes: &[u8]) -> Vec<&[u8]> {
Words::new(bytes).collect()
}
#[test]
fn splits_operators_and_hex_strings() {
assert_eq!(
words(b"1 begincidchar <20> <100> endcidchar"),
vec![&b"1"[..], b"begincidchar", b"<20>", b"<100>", b"endcidchar"]
);
}
#[test]
fn empty_input_yields_nothing() {
assert!(words(b"").is_empty());
assert!(words(b" \n\t").is_empty());
}
#[test]
fn comments_run_to_the_end_of_the_line() {
assert_eq!(words(b"a % comment here\nb"), vec![&b"a"[..], b"b"]);
assert_eq!(words(b"a % comment here"), vec![&b"a"[..]]);
assert_eq!(words(b"%one\n%two\nz"), vec![&b"z"[..]]);
}
#[test]
fn double_angle_is_its_own_word() {
assert_eq!(words(b"<</X 1>>"), vec![&b"<<"[..], b"/X", b"1", b">>"]);
assert_eq!(words(b"<<"), vec![&b"<<"[..]]);
assert_eq!(words(b">"), vec![&b">"[..]]);
assert_eq!(words(b">>"), vec![&b">>"[..]]);
}
#[test]
fn truncated_hex_strings_survive() {
assert_eq!(words(b"<"), vec![&b"<"[..]]);
assert_eq!(words(b"<a1"), vec![&b"<a1"[..]]);
assert_eq!(words(b"<a1>"), vec![&b"<a1>"[..]]);
}
#[test]
fn parenthesised_strings_are_returned_whole() {
assert_eq!(words(b"(a(b)c) x"), vec![&b"(a(b)c)"[..], b"x"]);
assert_eq!(words(b"(unterminated"), vec![&b"(unterminated"[..]]);
assert_eq!(words(br"(a\(b) x"), vec![&br"(a\(b) x"[..]]);
assert_eq!(words(b"(Japan1)"), vec![&b"(Japan1)"[..]]);
}
#[test]
fn a_name_at_end_of_data_ends_the_program() {
assert_eq!(words(b"a /Ordering"), vec![&b"a"[..]]);
assert_eq!(words(b"a /Ordering "), vec![&b"a"[..], b"/Ordering"]);
assert_eq!(words(b"a /Ordering("), vec![&b"a"[..], b"/Ordering", b"("]);
}
#[test]
fn bracket_delimiters_are_single_byte_words() {
assert_eq!(
words(b"[1]{2})"),
vec![&b"["[..], b"1", b"]", b"{", b"2", b"}", b")"]
);
}
#[test]
fn high_whitespace_bytes_separate_words() {
assert_eq!(words(&[b'a', 0x80, b'b']), vec![&b"a"[..], b"b"]);
assert_eq!(words(&[b'a', 0xFF, b'b']), vec![&b"a"[..], b"b"]);
assert_eq!(words(&[b'a', 0xFE, b'b']), vec![&[b'a', 0xFE, b'b'][..]]);
}
#[test]
fn tokens_stay_inside_the_input() {
let inputs: &[&[u8]] = &[
b"<<<<<<",
b"((((",
b"))))",
b"/",
b"%",
b"<>",
b"><",
&[0x00, 0xFF, 0x80, 0x25, 0x28],
&[0x3C; 64],
];
for input in inputs {
let mut total = 0usize;
for w in Words::new(input) {
assert!(!w.is_empty());
assert!(w.len() <= input.len());
total += w.len();
assert!(total <= input.len(), "words overlap in {input:?}");
}
}
}
#[test]
fn a_cid_range_splits_into_five_words() {
let words: Vec<&[u8]> = Words::new(b"begincidrange <20> <7e> 1 endcidrange").collect();
assert_eq!(words.first().copied(), Some(&b"begincidrange"[..]));
assert_eq!(words.get(1).copied(), Some(&b"<20>"[..]));
assert_eq!(words.len(), 5);
}
}