use smallvec::SmallVec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LocalAction {
Disconnect,
#[allow(dead_code)]
Passthrough(SmallVec<[u8; 64]>),
}
pub struct LocalEscapeDetector {
after_newline: bool,
saw_tilde: bool,
}
impl LocalEscapeDetector {
pub fn new() -> Self {
Self {
after_newline: true, saw_tilde: false,
}
}
pub fn process(&mut self, data: &[u8]) -> Option<LocalAction> {
for &byte in data {
match byte {
b'\r' | b'\n' => {
self.after_newline = true;
self.saw_tilde = false;
}
b'~' if self.after_newline => {
self.saw_tilde = true;
self.after_newline = false;
}
b'.' if self.saw_tilde => {
return Some(LocalAction::Disconnect);
}
_ => {
self.after_newline = false;
self.saw_tilde = false;
}
}
}
None }
#[allow(dead_code)]
pub fn reset(&mut self) {
self.after_newline = true;
self.saw_tilde = false;
}
}
impl Default for LocalEscapeDetector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normal_input_passes_through() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"hello world"), None);
assert_eq!(detector.process(b"test\n"), None);
}
#[test]
fn test_disconnect_after_newline() {
let mut detector = LocalEscapeDetector::new();
detector.process(b"hello\n");
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_disconnect_at_start() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_tilde_without_dot() {
let mut detector = LocalEscapeDetector::new();
detector.process(b"\n");
assert_eq!(detector.process(b"~x"), None);
}
#[test]
fn test_dot_without_tilde() {
let mut detector = LocalEscapeDetector::new();
detector.process(b"\n");
assert_eq!(detector.process(b"."), None);
}
#[test]
fn test_tilde_not_after_newline() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"x~."), None);
}
#[test]
fn test_carriage_return_enables_escape() {
let mut detector = LocalEscapeDetector::new();
detector.process(b"hello\r");
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_reset() {
let mut detector = LocalEscapeDetector::new();
detector.process(b"x");
detector.reset();
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_default() {
let _detector = LocalEscapeDetector::default();
}
#[test]
fn test_multiple_sequences() {
let mut detector = LocalEscapeDetector::new();
detector.process(b"hello\n");
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
detector.reset();
detector.process(b"world\r");
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_partial_sequence_in_chunks() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"\n"), None);
assert_eq!(detector.process(b"~"), None);
assert_eq!(detector.process(b"."), Some(LocalAction::Disconnect));
}
#[test]
fn test_data_ending_with_tilde() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"\n~"), None);
assert_eq!(detector.process(b"."), Some(LocalAction::Disconnect));
}
#[test]
fn test_data_ending_with_newline() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"hello\n"), None);
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_consecutive_newlines() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"\n\n\n"), None);
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_mixed_cr_and_lf() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"\r\n"), None);
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_lfcr_sequence() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"\n\r"), None);
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_large_buffer_with_escape() {
let mut detector = LocalEscapeDetector::new();
let mut data = vec![b'x'; 1000];
data.push(b'\n');
data.push(b'~');
data.push(b'.');
data.extend_from_slice(&[b'y'; 500]);
assert_eq!(detector.process(&data), Some(LocalAction::Disconnect));
}
#[test]
fn test_tilde_after_text() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"hello~.world"), None);
}
#[test]
fn test_multiple_tildes() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"\n~~."), None);
}
#[test]
fn test_tilde_then_newline() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"\n~\n"), None);
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_empty_input() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b""), None);
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_escape_in_binary_data() {
let mut detector = LocalEscapeDetector::new();
let data = [0x00, 0xFF, b'\n', b'~', b'.', 0x7F];
assert_eq!(detector.process(&data), Some(LocalAction::Disconnect));
}
#[test]
fn test_only_newline_then_only_tilde() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"\n"), None);
assert_eq!(detector.process(b"~"), None);
assert_eq!(detector.process(b"."), Some(LocalAction::Disconnect));
}
#[test]
fn test_state_after_non_dot() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process(b"\n~x"), None);
assert_eq!(detector.process(b"~."), None);
assert_eq!(detector.process(b"\n~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_rapid_escape_attempts() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(
detector.process(b"\n~x\n~y\n~z\n~."),
Some(LocalAction::Disconnect)
);
}
#[test]
fn test_unicode_does_not_interfere() {
let mut detector = LocalEscapeDetector::new();
assert_eq!(detector.process("한글\n".as_bytes()), None);
assert_eq!(detector.process(b"~."), Some(LocalAction::Disconnect));
}
#[test]
fn test_local_action_eq() {
assert_eq!(LocalAction::Disconnect, LocalAction::Disconnect);
}
#[test]
fn test_local_action_debug() {
let action = LocalAction::Disconnect;
let debug_str = format!("{:?}", action);
assert!(debug_str.contains("Disconnect"));
}
#[test]
fn test_local_action_clone() {
let action = LocalAction::Disconnect;
let cloned = action.clone();
assert_eq!(action, cloned);
}
}