#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum State {
#[default]
Ground,
Escape,
OscStart,
OscParam,
OscString,
OscEscape,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OscResult {
Title(String),
}
#[derive(Debug, Default)]
pub struct OscParser {
state: State,
param: u8,
buffer: Vec<u8>,
}
const ESC: u8 = 0x1B;
const BEL: u8 = 0x07;
impl OscParser {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn feed(&mut self, byte: u8) -> Option<OscResult> {
match self.state {
State::Ground => {
if byte == ESC {
self.state = State::Escape;
}
None
}
State::Escape => {
if byte == b']' {
self.state = State::OscStart;
self.param = 0;
self.buffer.clear();
} else {
self.reset();
}
None
}
State::OscStart => {
if byte.is_ascii_digit() {
self.param = byte - b'0';
self.state = State::OscParam;
} else {
self.reset();
}
None
}
State::OscParam => {
if byte.is_ascii_digit() {
self.param = self.param.saturating_mul(10).saturating_add(byte - b'0');
} else if byte == b';' {
self.state = State::OscString;
} else {
self.reset();
}
None
}
State::OscString => {
if byte == BEL {
self.emit_title()
} else if byte == ESC {
self.state = State::OscEscape;
None
} else {
self.buffer.push(byte);
None
}
}
State::OscEscape => {
if byte == b'\\' {
self.emit_title()
} else {
self.reset();
None
}
}
}
}
pub fn feed_slice(&mut self, data: &[u8]) -> Vec<OscResult> {
data.iter().filter_map(|&b| self.feed(b)).collect()
}
pub fn reset(&mut self) {
self.state = State::Ground;
self.param = 0;
self.buffer.clear();
}
fn emit_title(&mut self) -> Option<OscResult> {
let result = if self.param <= 2 {
String::from_utf8_lossy(&self.buffer)
.into_owned()
.pipe(OscResult::Title)
.pipe(Some)
} else {
None
};
self.reset();
result
}
}
trait Pipe: Sized {
fn pipe<T>(self, f: impl FnOnce(Self) -> T) -> T {
f(self)
}
}
impl<T> Pipe for T {}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn parse_title_bel() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b]0;Hello\x07");
assert_eq!(results, vec![OscResult::Title("Hello".to_string())]);
}
#[test]
fn parse_title_st() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b]0;World\x1b\\");
assert_eq!(results, vec![OscResult::Title("World".to_string())]);
}
#[test]
fn parse_title_osc2() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b]2;Window Title\x07");
assert_eq!(results, vec![OscResult::Title("Window Title".to_string())]);
}
#[test]
fn partial_then_complete() {
let mut parser = OscParser::new();
assert!(parser.feed_slice(b"\x1b]0;Hel").is_empty());
let results = parser.feed_slice(b"lo\x07");
assert_eq!(results, vec![OscResult::Title("Hello".to_string())]);
}
#[test]
fn ignore_non_title_osc() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b]7;file:///path\x07");
assert!(results.is_empty());
}
#[test]
fn invalid_sequence_reset() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1bXinvalid\x07");
assert!(results.is_empty());
let results = parser.feed_slice(b"\x1b]0;Valid\x07");
assert_eq!(results, vec![OscResult::Title("Valid".to_string())]);
}
#[test]
fn multiple_sequences() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b]0;First\x07text\x1b]0;Second\x1b\\");
assert_eq!(
results,
vec![
OscResult::Title("First".to_string()),
OscResult::Title("Second".to_string()),
]
);
}
#[test]
fn utf8_title() {
let mut parser = OscParser::new();
let results = parser.feed_slice("\x1b]0;日本語タイトル\x07".as_bytes());
assert_eq!(
results,
vec![OscResult::Title("日本語タイトル".to_string())]
);
}
#[test]
fn empty_title() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b]0;\x07");
assert_eq!(results, vec![OscResult::Title(String::new())]);
}
#[test]
fn multi_digit_param() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b]10;#ffffff\x07");
assert!(results.is_empty());
}
#[test]
fn invalid_after_escape() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b[0m");
assert!(results.is_empty());
let results = parser.feed_slice(b"\x1b]0;Test\x07");
assert_eq!(results, vec![OscResult::Title("Test".to_string())]);
}
#[test]
fn invalid_osc_param_non_digit_start() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b]X;Title\x07");
assert!(results.is_empty());
}
#[test]
fn invalid_osc_param_invalid_char() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b]0X;Title\x07");
assert!(results.is_empty());
let results = parser.feed_slice(b"\x1b]0;Valid\x07");
assert_eq!(results, vec![OscResult::Title("Valid".to_string())]);
}
#[test]
fn invalid_st_terminator() {
let mut parser = OscParser::new();
let results = parser.feed_slice(b"\x1b]0;Title\x1bX");
assert!(results.is_empty());
let results = parser.feed_slice(b"\x1b]0;Valid\x07");
assert_eq!(results, vec![OscResult::Title("Valid".to_string())]);
}
#[test]
fn reset_method() {
let mut parser = OscParser::new();
let _ = parser.feed_slice(b"\x1b]0;Part");
parser.reset();
let results = parser.feed_slice(b"ial\x07");
assert!(results.is_empty());
}
}