//! K407–K418: bounded serial console policy atop K403–K406 line handling. //! `ls` dispatch is separate from disk I/O: callers must render validated //! directory records, never synthesize a successful listing here. use crate::serial_rx::{BoundedLine, BoundedLineEvent, CAPACITY}; pub const HELP_COMMANDS: &str = "help"; pub const HELP_ECHO_COMMANDS: &str = "help echo"; pub const HELP_ECHO_LS_COMMANDS: &str = "help echo ls"; pub const HELP_ECHO_LS_CAT_COMMANDS: &str = "help echo ls cat"; pub const HELP_ECHO_LS_CAT_RUN_COMMANDS: &str = "help echo ls cat run"; pub const HELP_ECHO_LS_CAT_RUN_WAIT_COMMANDS: &str = "help echo ls cat run wait"; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ConsoleEvent { Input, SuppressedLf, Blank { prompt: u64, }, Help { prompt: u64, }, Echo { prompt: u64, bytes: [u8; CAPACITY], len: usize, }, List { prompt: u64, }, Cat { prompt: u64, bytes: [u8; CAPACITY], len: usize, }, Run { prompt: u64, }, Wait { prompt: u64, }, InvalidUtf8 { prompt: u64, bytes: [u8; CAPACITY], len: usize, }, MalformedArguments { prompt: u64, bytes: [u8; CAPACITY], len: usize, reason: ArgumentError, }, Cancelled { prompt: u64, stored_bytes: usize, overflowed: bool, }, Unhandled([u8; CAPACITY], usize), TooLong, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ArgumentError { UnclosedQuote, DanglingEscape, InvalidRange, } // K416 policy: only ASCII SP/HT separate words; single and double quotes // group bytes, and backslash quotes the next byte. No expansion or globbing. // Offsets refer to copied, unescaped bytes, never the transient input slice. struct Arguments { data: [u8; CAPACITY], starts: [usize; 8], lengths: [usize; 8], count: usize, } impl Arguments { fn word(&self, index: usize) -> Option<&[u8]> { if index >= self.count { return None; } let start = *self.starts.get(index)?; let length = *self.lengths.get(index)?; let end = start.checked_add(length)?; self.data.get(start..end) } } fn parse_arguments(bytes: &[u8]) -> Result { let mut args = Arguments { data: [0; CAPACITY], starts: [0; 8], lengths: [0; 8], count: 0, }; let mut cursor = 0; let mut out = 0; while cursor < bytes.len() { if bytes[cursor] == b' ' || bytes[cursor] == b'\t' { cursor += 1; continue; } // At most eight nonempty one-byte words fit in a 16-byte line. if args.count == args.starts.len() { return Err(ArgumentError::InvalidRange); } let start = out; let mut quote = 0; while cursor < bytes.len() { let byte = bytes[cursor]; if byte == b'\\' { cursor += 1; if cursor == bytes.len() { return Err(ArgumentError::DanglingEscape); } args.data[out] = bytes[cursor]; out += 1; } else if quote != 0 { if byte == quote { quote = 0; } else { args.data[out] = byte; out += 1; } } else if byte == b'\'' || byte == b'"' { quote = byte; } else if byte == b' ' || byte == b'\t' { break; } else { args.data[out] = byte; out += 1; } cursor += 1; } if quote != 0 { return Err(ArgumentError::UnclosedQuote); } args.starts[args.count] = start; args.lengths[args.count] = out - start; args.count += 1; } Ok(args) } pub struct SerialConsole { line: BoundedLine, prompts: u64, blanks: u64, helps: u64, echos: u64, lists: u64, cats: u64, runs: u64, waits: u64, unknowns: u64, invalid_utf8: u64, malformed_arguments: u64, cancellations: u64, command_level: u8, } impl SerialConsole { pub const fn new() -> Self { Self::with_commands(6) } /// K408's historical acceptance image keeps its original help-only set. pub const fn help_only() -> Self { Self::with_commands(1) } /// K409's acceptance image keeps its historical help/echo command set. pub const fn echo_only() -> Self { Self::with_commands(2) } /// K410's acceptance image keeps its historical help/echo/ls set. pub const fn ls_only() -> Self { Self::with_commands(3) } /// K411's acceptance image keeps its historical help/echo/ls/cat set. pub const fn cat_only() -> Self { Self::with_commands(4) } /// K412's acceptance image keeps run without the later wait command. pub const fn run_only() -> Self { Self::with_commands(5) } const fn with_commands(command_level: u8) -> Self { Self { line: BoundedLine::new(), prompts: 1, blanks: 0, helps: 0, echos: 0, lists: 0, cats: 0, runs: 0, waits: 0, unknowns: 0, invalid_utf8: 0, malformed_arguments: 0, cancellations: 0, command_level, } } pub const fn commands(&self) -> &'static str { if self.command_level >= 6 { HELP_ECHO_LS_CAT_RUN_WAIT_COMMANDS } else if self.command_level >= 5 { HELP_ECHO_LS_CAT_RUN_COMMANDS } else if self.command_level >= 4 { HELP_ECHO_LS_CAT_COMMANDS } else if self.command_level >= 3 { HELP_ECHO_LS_COMMANDS } else if self.command_level >= 2 { HELP_ECHO_COMMANDS } else { HELP_COMMANDS } } pub const fn prompts(&self) -> u64 { self.prompts } pub const fn blanks(&self) -> u64 { self.blanks } pub const fn helps(&self) -> u64 { self.helps } pub const fn echos(&self) -> u64 { self.echos } pub const fn lists(&self) -> u64 { self.lists } pub const fn cats(&self) -> u64 { self.cats } pub const fn runs(&self) -> u64 { self.runs } pub const fn waits(&self) -> u64 { self.waits } pub const fn unknowns(&self) -> u64 { self.unknowns } pub const fn invalid_utf8(&self) -> u64 { self.invalid_utf8 } pub const fn malformed_arguments(&self) -> u64 { self.malformed_arguments } pub const fn cancellations(&self) -> u64 { self.cancellations } fn reject_arguments( &mut self, bytes: [u8; CAPACITY], len: usize, reason: ArgumentError, ) -> ConsoleEvent { self.malformed_arguments = self.malformed_arguments.saturating_add(1); self.prompts = self.prompts.saturating_add(1); ConsoleEvent::MalformedArguments { prompt: self.prompts, bytes, len, reason, } } fn dispatch_current(&mut self, bytes: [u8; CAPACITY], len: usize) -> ConsoleEvent { if len > CAPACITY { return self.reject_arguments(bytes, CAPACITY, ArgumentError::InvalidRange); } let args = match parse_arguments(&bytes[..len]) { Ok(args) => args, Err(reason) => return self.reject_arguments(bytes, len, reason), }; if args.count == 0 { self.blanks = self.blanks.saturating_add(1); self.prompts = self.prompts.saturating_add(1); return ConsoleEvent::Blank { prompt: self.prompts, }; } let Some(name) = args.word(0) else { return self.reject_arguments(bytes, len, ArgumentError::InvalidRange); }; if name == b"help" && args.count == 1 { self.helps = self.helps.saturating_add(1); self.prompts = self.prompts.saturating_add(1); return ConsoleEvent::Help { prompt: self.prompts, }; } if name == b"echo" { let mut output = [0; CAPACITY]; let mut used = 0; for index in 1..args.count { if index > 1 { let Some(separator) = output.get_mut(used) else { return self.reject_arguments(bytes, len, ArgumentError::InvalidRange); }; *separator = b' '; used += 1; } let Some(word) = args.word(index) else { return self.reject_arguments(bytes, len, ArgumentError::InvalidRange); }; let Some(end) = used.checked_add(word.len()) else { return self.reject_arguments(bytes, len, ArgumentError::InvalidRange); }; let Some(destination) = output.get_mut(used..end) else { return self.reject_arguments(bytes, len, ArgumentError::InvalidRange); }; destination.copy_from_slice(word); used = end; } self.echos = self.echos.saturating_add(1); self.prompts = self.prompts.saturating_add(1); return ConsoleEvent::Echo { prompt: self.prompts, bytes: output, len: used, }; } if name == b"ls" && args.count == 1 { self.lists = self.lists.saturating_add(1); self.prompts = self.prompts.saturating_add(1); return ConsoleEvent::List { prompt: self.prompts, }; } if name == b"cat" && args.count == 2 { let Some(word) = args.word(1) else { return self.reject_arguments(bytes, len, ArgumentError::InvalidRange); }; let mut output = [0; CAPACITY]; output[..word.len()].copy_from_slice(word); self.cats = self.cats.saturating_add(1); self.prompts = self.prompts.saturating_add(1); return ConsoleEvent::Cat { prompt: self.prompts, bytes: output, len: word.len(), }; } if name == b"run" && args.count == 1 { self.runs = self.runs.saturating_add(1); self.prompts = self.prompts.saturating_add(1); return ConsoleEvent::Run { prompt: self.prompts, }; } if name == b"wait" && args.count == 1 { self.waits = self.waits.saturating_add(1); self.prompts = self.prompts.saturating_add(1); return ConsoleEvent::Wait { prompt: self.prompts, }; } self.unknowns = self.unknowns.saturating_add(1); self.prompts = self.prompts.saturating_add(1); ConsoleEvent::Unhandled(bytes, len) } pub fn feed(&mut self, byte: u8) -> ConsoleEvent { if self.command_level >= 6 && byte == 0x03 { let cancelled = self.line.cancel(); self.cancellations = self.cancellations.saturating_add(1); self.prompts = self.prompts.saturating_add(1); return ConsoleEvent::Cancelled { prompt: self.prompts, stored_bytes: cancelled.stored_bytes, overflowed: cancelled.overflowed, }; } match self.line.feed(byte) { BoundedLineEvent::Complete(_, 0) => { self.blanks = self.blanks.saturating_add(1); self.prompts = self.prompts.saturating_add(1); ConsoleEvent::Blank { prompt: self.prompts, } } BoundedLineEvent::Complete(bytes, len) if self.command_level >= 6 && core::str::from_utf8(&bytes[..len]).is_err() => { self.invalid_utf8 = self.invalid_utf8.saturating_add(1); self.prompts = self.prompts.saturating_add(1); ConsoleEvent::InvalidUtf8 { prompt: self.prompts, bytes, len, } } BoundedLineEvent::Complete(bytes, len) if self.command_level >= 6 => { self.dispatch_current(bytes, len) } BoundedLineEvent::Complete(bytes, len) if &bytes[..len] == b"help" => { self.helps = self.helps.saturating_add(1); self.prompts = self.prompts.saturating_add(1); ConsoleEvent::Help { prompt: self.prompts, } } BoundedLineEvent::Complete(bytes, len) if self.command_level >= 2 && (len == 4 && &bytes[..len] == b"echo" || len >= 5 && &bytes[..5] == b"echo ") => { let start = if len == 4 { 4 } else { 5 }; let arg_len = len - start; let mut arg = [0; CAPACITY]; arg[..arg_len].copy_from_slice(&bytes[start..len]); self.echos = self.echos.saturating_add(1); self.prompts = self.prompts.saturating_add(1); ConsoleEvent::Echo { prompt: self.prompts, bytes: arg, len: arg_len, } } BoundedLineEvent::Complete(bytes, len) if self.command_level >= 3 && &bytes[..len] == b"ls" => { self.lists = self.lists.saturating_add(1); self.prompts = self.prompts.saturating_add(1); ConsoleEvent::List { prompt: self.prompts, } } BoundedLineEvent::Complete(bytes, len) if self.command_level >= 4 && len > 4 && &bytes[..4] == b"cat " => { let arg_len = len - 4; let mut arg = [0; CAPACITY]; arg[..arg_len].copy_from_slice(&bytes[4..len]); self.cats = self.cats.saturating_add(1); self.prompts = self.prompts.saturating_add(1); ConsoleEvent::Cat { prompt: self.prompts, bytes: arg, len: arg_len, } } BoundedLineEvent::Complete(bytes, len) if self.command_level >= 5 && &bytes[..len] == b"run" => { self.runs = self.runs.saturating_add(1); self.prompts = self.prompts.saturating_add(1); ConsoleEvent::Run { prompt: self.prompts, } } BoundedLineEvent::Complete(bytes, len) if self.command_level >= 6 && &bytes[..len] == b"wait" => { self.waits = self.waits.saturating_add(1); self.prompts = self.prompts.saturating_add(1); ConsoleEvent::Wait { prompt: self.prompts, } } BoundedLineEvent::Complete(bytes, len) => { // Historical command sets preserve their exact acceptance // traces. The current console recovers at a fresh prompt. if self.command_level >= 6 { self.unknowns = self.unknowns.saturating_add(1); self.prompts = self.prompts.saturating_add(1); } ConsoleEvent::Unhandled(bytes, len) } BoundedLineEvent::TooLong => { if self.command_level >= 6 { self.prompts = self.prompts.saturating_add(1); } ConsoleEvent::TooLong } BoundedLineEvent::SuppressedLf => ConsoleEvent::SuppressedLf, _ => ConsoleEvent::Input, } } } impl Default for SerialConsole { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn k407_empty_lf_crlf_and_backspace_return_new_prompt_only() { let mut console = SerialConsole::new(); assert_eq!(console.prompts(), 1); assert_eq!(console.feed(b'\n'), ConsoleEvent::Blank { prompt: 2 }); assert_eq!(console.feed(b'\r'), ConsoleEvent::Blank { prompt: 3 }); assert_eq!(console.feed(b'\n'), ConsoleEvent::SuppressedLf); assert_eq!(console.feed(0x08), ConsoleEvent::Input); assert_eq!(console.feed(b'\n'), ConsoleEvent::Blank { prompt: 4 }); assert_eq!(console.prompts(), 4); assert_eq!(console.blanks(), 3); assert_eq!(console.helps(), 0); } #[test] fn k408_help_is_exact_and_lists_only_available_command() { let mut console = SerialConsole::help_only(); for byte in b"help" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } assert_eq!(console.feed(b'\n'), ConsoleEvent::Help { prompt: 2 }); for byte in b"helP" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } let ConsoleEvent::Unhandled(bytes, len) = console.feed(b'\n') else { panic!("unknown command was executed"); }; assert_eq!(&bytes[..len], b"helP"); assert_eq!(HELP_COMMANDS, "help"); assert_eq!(console.prompts(), 2); assert_eq!(console.helps(), 1); assert_eq!(console.blanks(), 0); } #[test] fn k409_echo_preserves_leading_internal_and_trailing_argument_bytes() { let mut console = SerialConsole::echo_only(); assert_eq!(console.commands(), HELP_ECHO_COMMANDS); for byte in b"echo A \tB " { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } let ConsoleEvent::Echo { prompt, bytes, len } = console.feed(b'\n') else { panic!("echo not dispatched"); }; assert_eq!(prompt, 2); assert_eq!(&bytes[..len], b" A \tB "); assert_eq!(console.echos(), 1); } #[test] fn k409_echo_without_argument_is_empty_but_prefix_echo_x_is_unhandled() { let mut console = SerialConsole::echo_only(); for byte in b"echo" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } let ConsoleEvent::Echo { bytes, len, .. } = console.feed(b'\r') else { panic!("empty echo not dispatched"); }; assert_eq!(len, 0); assert_eq!(bytes, [0; CAPACITY]); assert_eq!(console.feed(b'\n'), ConsoleEvent::SuppressedLf); for byte in b"echoX" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } let ConsoleEvent::Unhandled(bytes, len) = console.feed(b'\n') else { panic!("echoX was dispatched"); }; assert_eq!(&bytes[..len], b"echoX"); assert_eq!(console.echos(), 1); } #[test] fn k410_ls_requires_exact_command_and_historical_images_do_not_dispatch_it() { let mut console = SerialConsole::ls_only(); assert_eq!(console.commands(), HELP_ECHO_LS_COMMANDS); for byte in b"ls\n" { if *byte == b'\n' { assert_eq!(console.feed(*byte), ConsoleEvent::List { prompt: 2 }); } else { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } } assert_eq!(console.lists(), 1); for byte in b"lsX\n" { if *byte == b'\n' { let ConsoleEvent::Unhandled(bytes, len) = console.feed(*byte) else { panic!("lsX was dispatched"); }; assert_eq!(&bytes[..len], b"lsX"); } else { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } } let mut old = SerialConsole::echo_only(); assert_eq!(old.feed(b'l'), ConsoleEvent::Input); assert_eq!(old.feed(b's'), ConsoleEvent::Input); assert!(matches!(old.feed(b'\n'), ConsoleEvent::Unhandled(_, 2))); } #[test] fn k411_cat_argument_is_exact_and_ls_image_does_not_dispatch_it() { let mut console = SerialConsole::cat_only(); assert_eq!(console.commands(), HELP_ECHO_LS_CAT_COMMANDS); for byte in b"cat MOTD" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } let ConsoleEvent::Cat { prompt, bytes, len } = console.feed(b'\n') else { panic!("cat was not dispatched"); }; assert_eq!(prompt, 2); assert_eq!(&bytes[..len], b"MOTD"); assert_eq!(console.cats(), 1); for byte in b"cat\n" { console.feed(*byte); } assert_eq!(console.cats(), 1); let mut old = SerialConsole::ls_only(); for byte in b"cat MOTD" { assert_eq!(old.feed(*byte), ConsoleEvent::Input); } assert!(matches!(old.feed(b'\n'), ConsoleEvent::Unhandled(_, 8))); } #[test] fn k412_run_exact_command_does_not_dispatch_in_k411_image() { let mut console = SerialConsole::run_only(); assert_eq!(console.commands(), HELP_ECHO_LS_CAT_RUN_COMMANDS); for byte in b"run" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } assert_eq!(console.feed(b'\n'), ConsoleEvent::Run { prompt: 2 }); assert_eq!(console.runs(), 1); for byte in b"runX" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } assert!(matches!(console.feed(b'\n'), ConsoleEvent::Unhandled(_, 4))); assert_eq!(console.runs(), 1); let mut old = SerialConsole::cat_only(); for byte in b"run" { assert_eq!(old.feed(*byte), ConsoleEvent::Input); } assert!(matches!(old.feed(b'\n'), ConsoleEvent::Unhandled(_, 3))); } #[test] fn k413_wait_exact_command_is_single_and_not_in_k412_image() { let mut console = SerialConsole::new(); assert_eq!(console.commands(), HELP_ECHO_LS_CAT_RUN_WAIT_COMMANDS); for byte in b"wait" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } assert_eq!(console.feed(b'\n'), ConsoleEvent::Wait { prompt: 2 }); assert_eq!(console.waits(), 1); for byte in b"waitX" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } assert!(matches!(console.feed(b'\n'), ConsoleEvent::Unhandled(_, 5))); assert_eq!(console.waits(), 1); let mut old = SerialConsole::run_only(); for byte in b"wait" { assert_eq!(old.feed(*byte), ConsoleEvent::Input); } assert!(matches!(old.feed(b'\n'), ConsoleEvent::Unhandled(_, 4))); } #[test] fn k414_unknown_command_gets_one_new_prompt_and_keeps_next_command() { let mut console = SerialConsole::new(); for byte in b"bogus" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } let ConsoleEvent::Unhandled(bytes, len) = console.feed(b'\n') else { panic!("unknown command was not classified"); }; assert_eq!(&bytes[..len], b"bogus"); assert_eq!((console.unknowns(), console.prompts()), (1, 2)); for byte in b"help" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } assert_eq!(console.feed(b'\r'), ConsoleEvent::Help { prompt: 3 }); assert_eq!(console.feed(b'\n'), ConsoleEvent::SuppressedLf); assert_eq!( (console.unknowns(), console.prompts(), console.helps()), (1, 3, 1) ); for byte in b"waitX" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } assert!(matches!(console.feed(b'\n'), ConsoleEvent::Unhandled(_, 5))); assert_eq!( (console.unknowns(), console.prompts(), console.waits()), (2, 4, 0) ); for byte in b"echo OK" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } let ConsoleEvent::Echo { prompt, bytes, len } = console.feed(b'\n') else { panic!("valid command after errors was lost"); }; assert_eq!((prompt, &bytes[..len]), (5, b"OK".as_slice())); assert_eq!( (console.unknowns(), console.helps(), console.echos()), (2, 1, 1) ); let mut old = SerialConsole::run_only(); for byte in b"bogus\n" { old.feed(*byte); } assert_eq!((old.unknowns(), old.prompts()), (0, 1)); } #[test] fn k415_invalid_utf8_is_not_dispatched_and_valid_utf8_is_not_misclassified() { let mut console = SerialConsole::new(); for byte in [0xff, b'\n'] { if byte == b'\n' { let ConsoleEvent::InvalidUtf8 { prompt, bytes, len } = console.feed(byte) else { panic!("invalid UTF-8 was not classified"); }; assert_eq!((prompt, &bytes[..len]), (2, &[0xff][..])); } else { assert_eq!(console.feed(byte), ConsoleEvent::Input); } } for byte in b"echo \xc3(\n" { if *byte == b'\n' { assert!(matches!( console.feed(*byte), ConsoleEvent::InvalidUtf8 { prompt: 3, len: 7, .. } )); } else { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } } assert_eq!( (console.invalid_utf8(), console.echos(), console.unknowns()), (2, 0, 0) ); for byte in "é\n".as_bytes() { console.feed(*byte); } assert_eq!( ( console.invalid_utf8(), console.unknowns(), console.prompts() ), (2, 1, 4) ); for byte in b"help\n" { if *byte == b'\n' { assert_eq!(console.feed(*byte), ConsoleEvent::Help { prompt: 5 }); } else { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } } let mut historical = SerialConsole::echo_only(); for byte in b"echo \xff" { historical.feed(*byte); } let ConsoleEvent::Echo { bytes, len, .. } = historical.feed(b'\n') else { panic!("historical byte-preserving echo changed"); }; assert_eq!(&bytes[..len], &[0xff]); } #[test] fn k416_quotes_spaces_and_escapes_make_owned_deterministic_arguments() { let mut console = SerialConsole::new(); for (line, expected) in [ (b"echo \"A B\" C\n".as_slice(), b"A B C".as_slice()), (b"echo A\\ B\n", b"A B"), (b"echo 'x y'\n", b"x y"), (b"echo \"\"\n", b""), (b" echo\t A B \n", b"A B"), ] { for byte in &line[..line.len() - 1] { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } let ConsoleEvent::Echo { bytes, len, .. } = console.feed(b'\n') else { panic!("quoted echo did not dispatch"); }; assert_eq!(&bytes[..len], expected); } for byte in b" cat \"MOTD\"\n" { let event = console.feed(*byte); if *byte == b'\n' { let ConsoleEvent::Cat { bytes, len, .. } = event else { panic!("quoted cat path did not dispatch"); }; assert_eq!(&bytes[..len], b"MOTD"); } } for byte in b" help \n" { let event = console.feed(*byte); if *byte == b'\n' { assert!(matches!(event, ConsoleEvent::Help { .. })); } } assert_eq!( (console.echos(), console.cats(), console.helps()), (5, 1, 1) ); } #[test] fn k416_malformed_quotes_and_escape_recover_without_dispatch() { let mut console = SerialConsole::new(); for (line, reason) in [ (b"echo \"open\n".as_slice(), ArgumentError::UnclosedQuote), (b"echo tail\\\n".as_slice(), ArgumentError::DanglingEscape), ] { for byte in line { let event = console.feed(*byte); if *byte == b'\n' { assert!( matches!(event, ConsoleEvent::MalformedArguments { reason: actual, .. } if actual == reason) ); } } } assert_eq!( ( console.malformed_arguments(), console.echos(), console.unknowns(), console.prompts() ), (2, 0, 0, 3) ); for byte in b"echo OK\n" { let event = console.feed(*byte); if *byte == b'\n' { assert!(matches!( event, ConsoleEvent::Echo { len: 2, prompt: 4, .. } )); } } let mut historical = SerialConsole::echo_only(); for byte in b"echo \"A B\"\n" { let event = historical.feed(*byte); if *byte == b'\n' { let ConsoleEvent::Echo { bytes, len, .. } = event else { panic!("historical echo changed"); }; assert_eq!(&bytes[..len], b"\"A B\""); } } } #[test] fn k417_checked_command_spans_reject_overflow_and_outside_without_panic() { let mut args = parse_arguments(b"echo OK").unwrap(); assert_eq!(args.word(0), Some(b"echo".as_slice())); assert_eq!(args.word(1), Some(b"OK".as_slice())); assert_eq!(args.word(2), None); assert_eq!(args.word(8), None); args.starts[1] = usize::MAX; args.lengths[1] = 2; assert_eq!(args.word(1), None, "addition overflow must not wrap"); args.starts[1] = 15; assert_eq!(args.word(1), None, "out-of-buffer span must not be sliced"); let mut console = SerialConsole::new(); let mut bytes = [0; CAPACITY]; bytes[..7].copy_from_slice(b"echo OK"); assert!(matches!( console.dispatch_current(bytes, usize::MAX), ConsoleEvent::MalformedArguments { reason: ArgumentError::InvalidRange, prompt: 2, len: CAPACITY, .. } )); assert_eq!((console.echos(), console.malformed_arguments()), (0, 1)); } #[test] fn k417_overlong_line_recovery_keeps_owned_prior_event_bytes() { let mut console = SerialConsole::new(); let mut saved = None; for byte in b"echo \"A B\"\n" { let event = console.feed(*byte); if let ConsoleEvent::Echo { bytes, len, .. } = event { saved = Some((bytes, len)); } } assert_eq!(&saved.unwrap().0[..3], b"A B"); for byte in b"echo 123456789012\n" { let event = console.feed(*byte); if *byte == b'\n' { assert_eq!(event, ConsoleEvent::TooLong); } } assert_eq!((console.prompts(), console.echos()), (3, 1)); for byte in b"echo OK\n" { let event = console.feed(*byte); if *byte == b'\n' { assert!(matches!( event, ConsoleEvent::Echo { prompt: 4, len: 2, .. } )); } } let (prior_bytes, prior_len) = saved.unwrap(); assert_eq!( &prior_bytes[..prior_len], b"A B", "event must own its argument payload" ); assert_eq!( ( console.echos(), console.unknowns(), console.malformed_arguments() ), (2, 0, 0) ); } #[test] fn k418_ctrl_c_discards_partial_and_overflowed_lines_then_resets_crlf() { let mut console = SerialConsole::new(); for byte in b"echo BAD" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } assert_eq!( console.feed(0x03), ConsoleEvent::Cancelled { prompt: 2, stored_bytes: 8, overflowed: false } ); for byte in b"echo OK\n" { let event = console.feed(*byte); if *byte == b'\n' { assert!(matches!( event, ConsoleEvent::Echo { prompt: 3, len: 2, .. } )); } } for byte in b"echo 123456789012" { assert_eq!(console.feed(*byte), ConsoleEvent::Input); } assert_eq!( console.feed(0x03), ConsoleEvent::Cancelled { prompt: 4, stored_bytes: 16, overflowed: true } ); for byte in b"help\r" { let event = console.feed(*byte); if *byte == b'\r' { assert_eq!(event, ConsoleEvent::Help { prompt: 5 }); } } assert_eq!( console.feed(0x03), ConsoleEvent::Cancelled { prompt: 6, stored_bytes: 0, overflowed: false } ); assert_eq!(console.feed(b'\n'), ConsoleEvent::Blank { prompt: 7 }); for byte in b"echo END\n" { let event = console.feed(*byte); if *byte == b'\n' { assert!(matches!( event, ConsoleEvent::Echo { prompt: 8, len: 3, .. } )); } } assert_eq!( ( console.cancellations(), console.echos(), console.helps(), console.blanks(), console.prompts() ), (3, 2, 1, 1, 8) ); assert_eq!( ( console.unknowns(), console.invalid_utf8(), console.malformed_arguments() ), (0, 0, 0) ); } #[test] fn k418_partial_utf8_cancellation_does_not_change_historical_raw_echo() { let mut console = SerialConsole::new(); for byte in b"echo \xc3" { console.feed(*byte); } assert_eq!( console.feed(0x03), ConsoleEvent::Cancelled { prompt: 2, stored_bytes: 6, overflowed: false } ); for byte in b"help\n" { let event = console.feed(*byte); if *byte == b'\n' { assert_eq!(event, ConsoleEvent::Help { prompt: 3 }); } } assert_eq!( ( console.invalid_utf8(), console.echos(), console.cancellations() ), (0, 0, 1) ); let mut historical = SerialConsole::echo_only(); for byte in b"echo \x03" { historical.feed(*byte); } let ConsoleEvent::Echo { bytes, len, .. } = historical.feed(b'\n') else { panic!("historical Ctrl-C byte must remain raw data"); }; assert_eq!(&bytes[..len], &[0x03]); assert_eq!(historical.cancellations(), 0); } }