use std::time::Duration;
use crate::error::{AsynError, AsynResult, AsynStatus};
use crate::interpose::{EomReason, OctetInterpose, OctetNext, OctetReadResult};
use crate::trace::TraceMask;
use crate::user::AsynUser;
pub const IAC: u8 = 255;
pub const DONT: u8 = 254;
pub const DO: u8 = 253;
pub const WONT: u8 = 252;
pub const WILL: u8 = 251;
pub const SB: u8 = 250;
pub const SE: u8 = 240;
const WD_TRANSMIT_BINARY: u8 = 0;
const SB_COM_PORT_OPTION: u8 = 44;
const CPO_SET_BAUDRATE: u8 = 1;
const CPO_SET_DATASIZE: u8 = 2;
const CPO_SET_PARITY: u8 = 3;
const CPO_PARITY_NONE: u8 = 1;
const CPO_PARITY_ODD: u8 = 2;
const CPO_PARITY_EVEN: u8 = 3;
const CPO_PARITY_MARK: u8 = 4;
const CPO_PARITY_SPACE: u8 = 5;
const CPO_SET_STOPSIZE: u8 = 4;
const CPO_SET_CONTROL: u8 = 5;
const CPO_CONTROL_NOFLOW: u8 = 1;
const CPO_CONTROL_IXON: u8 = 2;
const CPO_CONTROL_HWFLOW: u8 = 3;
const CPO_CONTROL_BREAK_ON: u8 = 5;
const CPO_CONTROL_BREAK_OFF: u8 = 6;
const CPO_SET_MODEMSTATE_MASK: u8 = 11;
const CPO_SERVER_NOTIFY_LINESTATE: u8 = 106;
const CPO_SERVER_NOTIFY_MODEMSTATE: u8 = 107;
const CPO_REPLY_OFFSET: i32 = 100;
const EOF: i32 = -1;
const INTERPOSE_USER_TIMEOUT: Duration = Duration::from_secs(2);
fn interpose_user() -> AsynUser {
AsynUser::new(0).with_timeout(INTERPOSE_USER_TIMEOUT)
}
const COM_OPTION_KEYS: [&str; 7] = ["baud", "bits", "parity", "stop", "crtscts", "ixon", "break"];
fn hash_hex_lower(v: i32) -> String {
if v == 0 {
"0".to_string()
} else {
format!("0x{v:x}")
}
}
fn hash_hex_upper(v: i32) -> String {
if v == 0 {
"0".to_string()
} else {
format!("0X{v:X}")
}
}
fn scan_int(s: &str) -> Option<i32> {
let b = s.as_bytes();
let mut i = 0;
while i < b.len() && b[i].is_ascii_whitespace() {
i += 1;
}
let start = i;
if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
i += 1;
}
let first_digit = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
if i == first_digit {
return None;
}
s[start..i].parse::<i32>().ok()
}
fn scan_uint(s: &str) -> Option<u32> {
let b = s.as_bytes();
let mut i = 0;
while i < b.len() && b[i].is_ascii_whitespace() {
i += 1;
}
let start = i;
if i < b.len() && b[i] == b'+' {
i += 1;
}
let first_digit = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
if i == first_digit {
return None;
}
s[start..i].parse::<u32>().ok()
}
fn scan_float(s: &str) -> Option<f32> {
let t = s.trim_start();
let mut end = t.len();
while end > 0 {
if t.is_char_boundary(end) {
if let Ok(v) = t[..end].parse::<f32>() {
return Some(v);
}
}
end -= 1;
}
None
}
fn asyn_error(message: impl Into<String>) -> AsynError {
AsynError::Status {
status: AsynStatus::Error,
message: message.into(),
}
}
pub struct ComInterpose;
impl ComInterpose {
pub fn new() -> Self {
Self
}
}
impl Default for ComInterpose {
fn default() -> Self {
Self::new()
}
}
fn next_char(next: &mut dyn OctetNext, user: &AsynUser) -> Result<u8, String> {
let mut c = [0u8; 1];
match next.read(user, &mut c) {
Ok(r) if r.nbytes_transferred >= 1 => Ok(c[0]),
Ok(_) => Err("no data".into()),
Err(e) => Err(e.message()),
}
}
impl OctetInterpose for ComInterpose {
fn read(
&mut self,
user: &AsynUser,
buf: &mut [u8],
next: &mut dyn OctetNext,
) -> AsynResult<OctetReadResult> {
let maxchars = buf.len();
let r = next.read(user, buf)?;
let mut n_read = r.nbytes_transferred;
let mut eom = r.eom_reason;
let mut d: isize = 0;
let mut n_check: isize = n_read as isize;
let mut unstuffed = false;
while n_check > 0 {
let span = &buf[d as usize..(d + n_check) as usize];
let Some(rel) = span.iter().position(|&b| b == IAC) else {
break;
};
let mut iac = d + rel as isize;
unstuffed = true;
eom.remove(EomReason::CNT);
let c = if iac == d + n_check - 1 {
let c = next_char(next, user).map_or(EOF, i32::from);
iac -= 1;
c
} else {
let c = i32::from(buf[(iac + 1) as usize]);
n_read -= 1;
c
};
if c != i32::from(IAC) {
return Err(asyn_error("Missing IAC"));
}
n_check -= (iac - d) + 2;
d = iac + 1;
if n_check == 0 {
break;
}
let dst = d as usize;
buf.copy_within(dst + 1..dst + 1 + n_check as usize, dst);
}
if unstuffed {
user.print_io(
TraceMask::IO_FILTER,
&buf[..n_read],
&format!("nRead {n_read} after IAC unstuffing"),
);
}
if n_read == maxchars {
eom.insert(EomReason::CNT);
}
Ok(OctetReadResult {
nbytes_transferred: n_read,
eom_reason: eom,
})
}
fn write(
&mut self,
user: &mut AsynUser,
data: &[u8],
next: &mut dyn OctetNext,
) -> AsynResult<usize> {
let n_iac = data.iter().filter(|&&b| b == IAC).count();
if n_iac == 0 {
return next.write(user, data);
}
let mut stuffed = Vec::with_capacity(data.len() + n_iac);
for &b in data {
stuffed.push(b);
if b == IAC {
stuffed.push(IAC);
}
}
let n = next.write(user, &stuffed)?;
Ok(if n == stuffed.len() { n - n_iac } else { n })
}
fn flush(&mut self, user: &mut AsynUser, next: &mut dyn OctetNext) -> AsynResult<()> {
next.flush(user)
}
}
struct TelnetLink<'a> {
next: &'a mut dyn OctetNext,
user: &'a mut AsynUser,
}
impl<'a> TelnetLink<'a> {
fn new(next: &'a mut dyn OctetNext, user: &'a mut AsynUser) -> Self {
Self { next, user }
}
fn next_char(&mut self) -> i32 {
match next_char(self.next, self.user) {
Ok(b) => i32::from(b),
Err(msg) => {
self.user.error_message = msg;
EOF
}
}
}
fn advise(&mut self, msg: &str) {
self.user.error_message = msg.to_string();
}
fn expect_char(&mut self, expect: u8) -> bool {
let c = self.next_char();
if c == EOF {
return false;
}
if c != i32::from(expect) {
self.user.error_message = format!(
"Expected {}, got {}",
hash_hex_upper(i32::from(expect)),
hash_hex_upper(c)
);
return false;
}
true
}
fn next_payload_char(&mut self) -> i32 {
let c = self.next_char();
if c != i32::from(IAC) {
return c;
}
let c2 = self.next_char();
if c2 == i32::from(IAC) {
return i32::from(IAC);
}
if c2 != EOF {
self.user.error_message = format!(
"Unescaped IAC in a COM-PORT-OPTION payload, followed by {}",
hash_hex_upper(c2)
);
}
EOF
}
fn error(&self) -> AsynError {
asyn_error(self.user.error_message.clone())
}
fn write_subnegotiation(&mut self, payload: &[u8]) -> AsynResult<usize> {
let mut cbuf = Vec::with_capacity(5 + payload.len());
cbuf.extend_from_slice(&[IAC, SB, SB_COM_PORT_OPTION]);
for &b in payload {
cbuf.push(b);
if b == IAC {
cbuf.push(IAC);
}
}
cbuf.extend_from_slice(&[IAC, SE]);
self.write(&cbuf)
}
fn write(&mut self, bytes: &[u8]) -> AsynResult<usize> {
self.next.write(self.user, bytes)
}
fn willdo(&mut self, command: u8, code: u8) -> AsynResult<()> {
self.write(&[IAC, command, code])?;
loop {
loop {
let c = self.next_char();
if c == EOF {
return Err(self.error());
}
if c == i32::from(IAC) {
break;
}
}
let c = self.next_char();
if c == EOF {
return Err(self.error());
}
match c as u8 {
IAC | SE => {}
DO | DONT => {
let wd = c as u8;
let opt = self.next_char();
if opt == EOF {
return Err(self.error());
}
if opt != i32::from(code) {
continue;
}
if command == DO {
self.user.error_message = format!(
"Received response {} in response to DO.",
hash_hex_lower(opt)
);
return Err(self.error());
}
if wd == DONT {
self.user.error_message =
format!("Device says DON'T {}.", hash_hex_lower(opt));
return Err(self.error());
}
return Ok(());
}
WILL | WONT => {
let wd = c as u8;
let opt = self.next_char();
if opt == EOF {
return Err(self.error());
}
if opt != i32::from(code) {
continue;
}
if command == WILL {
self.user.error_message = format!(
"Received response {} in response to WILL.",
hash_hex_lower(opt)
);
return Err(self.error());
}
if wd == WONT {
self.user.error_message =
format!("Device says WON'T {}.", hash_hex_lower(opt));
return Err(self.error());
}
return Ok(());
}
SB => {
if self.next_char() != i32::from(SB_COM_PORT_OPTION) {
continue;
}
let c = self.next_char();
if c != i32::from(CPO_SERVER_NOTIFY_LINESTATE)
&& c != i32::from(CPO_SERVER_NOTIFY_MODEMSTATE)
{
if c == EOF {
return Err(self.error());
}
continue;
}
if self.next_payload_char() == EOF {
return Err(self.error());
}
}
_ => {
self.user.error_message =
format!("Unexpected character {} in TELNET reply", hash_hex_lower(c));
return Err(self.error());
}
}
}
}
fn sb_com_port_option(&mut self, x: &[u8], r: &mut [u8]) -> AsynResult<()> {
debug_assert!(!x.is_empty() && r.len() >= x.len() - 1);
self.write_subnegotiation(x)?;
loop {
loop {
let c = self.next_char();
if c == EOF {
return Err(self.error());
}
if c == i32::from(IAC) {
break;
}
}
if !self.expect_char(SB) || !self.expect_char(SB_COM_PORT_OPTION) {
return Err(self.error());
}
let c = self.next_char();
if c == i32::from(CPO_SERVER_NOTIFY_LINESTATE)
|| c == i32::from(CPO_SERVER_NOTIFY_MODEMSTATE)
{
if self.next_payload_char() == EOF
|| !self.expect_char(IAC)
|| !self.expect_char(SE)
{
return Err(self.error());
}
} else if c == i32::from(x[0]) + CPO_REPLY_OFFSET {
for slot in r.iter_mut().take(x.len() - 1) {
let b = self.next_payload_char();
if b == EOF {
return Err(self.error());
}
*slot = b as u8;
}
if !self.expect_char(IAC) || !self.expect_char(SE) {
return Err(self.error());
}
return Ok(());
} else {
self.user.error_message =
format!("Sent COM-PORT-OPTION {} but got reply {}", x[0], c);
return Err(self.error());
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComPortOptions {
baud: i32,
bits: i32,
parity: u8,
stop: i32,
flow: u8,
break_active: bool,
}
impl Default for ComPortOptions {
fn default() -> Self {
Self {
baud: 9600,
bits: 8,
parity: CPO_PARITY_NONE,
stop: 1,
flow: CPO_CONTROL_NOFLOW,
break_active: false,
}
}
}
impl ComPortOptions {
pub fn new() -> Self {
Self::default()
}
pub fn owns_key(key: &str) -> bool {
COM_OPTION_KEYS.iter().any(|k| key.eq_ignore_ascii_case(k))
}
pub fn get_option(&self, key: &str) -> AsynResult<String> {
if key.eq_ignore_ascii_case("baud") {
Ok(self.baud.to_string())
} else if key.eq_ignore_ascii_case("bits") {
Ok(self.bits.to_string())
} else if key.eq_ignore_ascii_case("parity") {
Ok(match self.parity {
CPO_PARITY_NONE => "none",
CPO_PARITY_EVEN => "even",
CPO_PARITY_ODD => "odd",
CPO_PARITY_MARK => "mark",
CPO_PARITY_SPACE => "space",
_ => "",
}
.to_string())
} else if key.eq_ignore_ascii_case("stop") {
Ok(self.stop.to_string())
} else if key.eq_ignore_ascii_case("crtscts") {
match self.flow {
CPO_CONTROL_NOFLOW | CPO_CONTROL_IXON => Ok("N".to_string()),
CPO_CONTROL_HWFLOW => Ok("Y".to_string()),
other => Err(asyn_error(format!(
"Unknown flow control code {}",
hash_hex_upper(i32::from(other))
))),
}
} else if key.eq_ignore_ascii_case("ixon") {
match self.flow {
CPO_CONTROL_NOFLOW | CPO_CONTROL_HWFLOW => Ok("N".to_string()),
CPO_CONTROL_IXON => Ok("Y".to_string()),
other => Err(asyn_error(format!(
"Unknown flow control code {}",
hash_hex_upper(i32::from(other))
))),
}
} else if key.eq_ignore_ascii_case("break") {
Ok(if self.break_active { "on" } else { "off" }.to_string())
} else {
Err(AsynError::OptionNotFound(key.to_string()))
}
}
fn flow_mode_off(&self, key_mode: u8) -> u8 {
if self.flow == key_mode {
CPO_CONTROL_NOFLOW
} else {
self.flow
}
}
pub fn set_option(
&mut self,
user: &mut AsynUser,
next: &mut dyn OctetNext,
key: &str,
val: &str,
) -> AsynResult<()> {
let mut link = TelnetLink::new(next, user);
self.set_option_on(&mut link, key, val)
}
pub fn restore_settings(&mut self, next: &mut dyn OctetNext) -> AsynResult<()> {
let mut user = interpose_user();
let mut link = TelnetLink::new(next, &mut user);
self.restore_settings_on(&mut link)
}
fn restore_settings_on(&mut self, link: &mut TelnetLink) -> AsynResult<()> {
link.willdo(DO, WD_TRANSMIT_BINARY)?;
link.willdo(WILL, WD_TRANSMIT_BINARY)?;
link.willdo(WILL, SB_COM_PORT_OPTION)?;
let mut r = [0u8; 1];
link.sb_com_port_option(&[CPO_SET_MODEMSTATE_MASK, 0], &mut r)?;
for key in ["baud", "bits", "parity", "stop", "crtscts", "ixon"] {
let val = self.get_option(key)?;
self.set_option_on(link, key, &val)?;
}
Ok(())
}
fn set_option_on(&mut self, link: &mut TelnetLink, key: &str, val: &str) -> AsynResult<()> {
if key.eq_ignore_ascii_case("baud") {
let Some(b) = scan_int(val) else {
return Err(asyn_error("Bad number"));
};
let baud = b as u32;
let x = [
CPO_SET_BAUDRATE,
(baud >> 24) as u8,
(baud >> 16) as u8,
(baud >> 8) as u8,
baud as u8,
];
let mut r = [0u8; 4];
link.sb_com_port_option(&x, &mut r)?;
self.baud = i32::from_be_bytes(r);
if self.baud != b {
return Err(asyn_error(format!(
"Tried to set {b} baud, actually set {} baud.",
self.baud
)));
}
Ok(())
} else if key.eq_ignore_ascii_case("bits") {
let Some(b) = scan_int(val) else {
return Err(asyn_error("Bad number"));
};
let mut r = [0u8; 1];
link.sb_com_port_option(&[CPO_SET_DATASIZE, b as u8], &mut r)?;
self.bits = i32::from(r[0]);
if self.bits != b {
return Err(asyn_error(format!(
"Tried to set {b} bits, actually set {} bits.",
self.bits
)));
}
Ok(())
} else if key.eq_ignore_ascii_case("parity") {
let code = if val.eq_ignore_ascii_case("none") {
CPO_PARITY_NONE
} else if val.eq_ignore_ascii_case("even") {
CPO_PARITY_EVEN
} else if val.eq_ignore_ascii_case("odd") {
CPO_PARITY_ODD
} else if val.eq_ignore_ascii_case("mark") {
CPO_PARITY_MARK
} else if val.eq_ignore_ascii_case("space") {
CPO_PARITY_SPACE
} else {
return Err(asyn_error("Invalid parity selection"));
};
let mut r = [0u8; 1];
link.sb_com_port_option(&[CPO_SET_PARITY, code], &mut r)?;
self.parity = r[0];
Ok(())
} else if key.eq_ignore_ascii_case("stop") {
let Some(b) = scan_float(val) else {
return Err(asyn_error("Bad number"));
};
if b != 1.0 && b != 2.0 {
return Err(asyn_error("Bad stop bit count"));
}
let mut r = [0u8; 1];
link.sb_com_port_option(&[CPO_SET_STOPSIZE, b as u8], &mut r)?;
self.stop = i32::from(r[0]);
if self.stop as f32 != b {
return Err(asyn_error(format!(
"Tried to set {b} stop bits, actually set {} stop bits.",
self.stop
)));
}
Ok(())
} else if key.eq_ignore_ascii_case("crtscts") {
if self.flow == CPO_CONTROL_IXON {
link.advise("XON/XOFF already set. Now using RTS/CTS.");
}
let mode = if val.eq_ignore_ascii_case("n") {
self.flow_mode_off(CPO_CONTROL_HWFLOW)
} else if val.eq_ignore_ascii_case("y") {
CPO_CONTROL_HWFLOW
} else {
return Err(asyn_error("Bad value"));
};
let mut r = [0u8; 1];
link.sb_com_port_option(&[CPO_SET_CONTROL, mode], &mut r)?;
self.flow = r[0];
Ok(())
} else if key.eq_ignore_ascii_case("ixon") {
if self.flow == CPO_CONTROL_HWFLOW {
link.advise("RTS/CTS already set. Now using XON/XOFF.");
}
let mode = if val.eq_ignore_ascii_case("n") {
self.flow_mode_off(CPO_CONTROL_IXON)
} else if val.eq_ignore_ascii_case("y") {
CPO_CONTROL_IXON
} else {
return Err(asyn_error("Bad option value"));
};
let mut r = [0u8; 1];
match link.sb_com_port_option(&[CPO_SET_CONTROL, mode], &mut r) {
Ok(()) => {
self.flow = r[0];
Ok(())
}
Err(e) => {
println!("XON/XOFF not set.");
Err(e)
}
}
} else if key.eq_ignore_ascii_case("break") {
self.set_break(link, val)
} else {
Err(AsynError::OptionNotFound(key.to_string()))
}
}
fn set_break(&mut self, link: &mut TelnetLink, val: &str) -> AsynResult<()> {
let (on, off, sleep_for) = if val.eq_ignore_ascii_case("on") {
(!self.break_active, false, None)
} else if val.eq_ignore_ascii_case("off") {
(false, self.break_active, None)
} else {
let break_len = if val.is_empty() {
0
} else {
let Some(n) = scan_uint(val) else {
return Err(asyn_error("Bad number"));
};
n
};
let ms = if break_len == 0 { 250 } else { break_len };
(!self.break_active, true, Some(ms))
};
if on {
let mut r = [0u8; 1];
link.sb_com_port_option(&[CPO_SET_CONTROL, CPO_CONTROL_BREAK_ON], &mut r)?;
self.break_active = r[0] == CPO_CONTROL_BREAK_ON;
}
if let Some(ms) = sleep_for {
std::thread::sleep(Duration::from_millis(u64::from(ms)));
}
if off {
let mut r = [0u8; 1];
link.sb_com_port_option(&[CPO_SET_CONTROL, CPO_CONTROL_BREAK_OFF], &mut r)?;
self.break_active = r[0] == CPO_CONTROL_BREAK_ON;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interpose::OctetInterposeStack;
use std::collections::VecDeque;
struct FakeServer {
written: Vec<u8>,
to_read: VecDeque<u8>,
}
impl FakeServer {
fn new(reply: &[u8]) -> Self {
Self {
written: Vec::new(),
to_read: reply.iter().copied().collect(),
}
}
}
impl OctetNext for FakeServer {
fn read(&mut self, _user: &AsynUser, buf: &mut [u8]) -> AsynResult<OctetReadResult> {
if buf.is_empty() {
return Ok(OctetReadResult {
nbytes_transferred: 0,
eom_reason: EomReason::empty(),
});
}
let mut n = 0;
while n < buf.len() {
match self.to_read.pop_front() {
Some(b) => {
buf[n] = b;
n += 1;
}
None => break,
}
}
if n == 0 {
return Err(AsynError::Status {
status: AsynStatus::Timeout,
message: "read timeout".into(),
});
}
Ok(OctetReadResult {
nbytes_transferred: n,
eom_reason: if n == buf.len() {
EomReason::CNT
} else {
EomReason::empty()
},
})
}
fn write(&mut self, _user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
self.written.extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self, _user: &mut AsynUser) -> AsynResult<()> {
Ok(())
}
}
fn ack(subcmd: u8, values: &[u8]) -> Vec<u8> {
let mut v = vec![IAC, SB, SB_COM_PORT_OPTION, subcmd + 100];
v.extend_from_slice(values);
v.extend_from_slice(&[IAC, SE]);
v
}
#[test]
fn write_doubles_iac_and_reports_the_unstuffed_count() {
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = FakeServer::new(&[]);
let mut user = AsynUser::default();
let n = stack
.dispatch_write(&mut user, &[b'A', IAC, b'B'], &mut base)
.unwrap();
assert_eq!(base.written, vec![b'A', IAC, IAC, b'B']);
assert_eq!(n, 3);
}
#[test]
fn write_without_iac_is_verbatim_passthrough() {
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = FakeServer::new(&[]);
let mut user = AsynUser::default();
let n = stack
.dispatch_write(&mut user, b"HELLO", &mut base)
.unwrap();
assert_eq!(base.written, b"HELLO");
assert_eq!(n, 5);
}
#[test]
fn write_stuffs_every_iac_in_a_run() {
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = FakeServer::new(&[]);
let mut user = AsynUser::default();
let n = stack
.dispatch_write(&mut user, &[IAC, IAC, IAC], &mut base)
.unwrap();
assert_eq!(base.written, vec![IAC, IAC, IAC, IAC, IAC, IAC]);
assert_eq!(n, 3);
}
#[test]
fn short_write_reports_the_stuffed_count_c_reports() {
struct ShortWrite;
impl OctetNext for ShortWrite {
fn read(&mut self, _u: &AsynUser, _b: &mut [u8]) -> AsynResult<OctetReadResult> {
unreachable!()
}
fn write(&mut self, _u: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
Ok(data.len() - 1)
}
fn flush(&mut self, _u: &mut AsynUser) -> AsynResult<()> {
Ok(())
}
}
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = ShortWrite;
let mut user = AsynUser::default();
let n = stack
.dispatch_write(&mut user, &[b'A', IAC, b'B'], &mut base)
.unwrap();
assert_eq!(n, 3);
}
#[test]
fn read_unstuffs_a_doubled_iac_inside_the_buffer() {
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = FakeServer::new(&[b'A', IAC, IAC, b'B']);
let user = AsynUser::default();
let mut buf = [0u8; 8];
let r = stack.dispatch_read(&user, &mut buf, &mut base).unwrap();
assert_eq!(r.nbytes_transferred, 3);
assert_eq!(&buf[..3], &[b'A', IAC, b'B']);
}
#[test]
fn read_pulls_the_partner_from_the_device_when_iac_lands_last() {
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = FakeServer::new(&[b'A', IAC, IAC]);
let user = AsynUser::default();
let mut buf = [0u8; 2];
let r = stack.dispatch_read(&user, &mut buf, &mut base).unwrap();
assert_eq!(r.nbytes_transferred, 2);
assert_eq!(&buf[..2], &[b'A', IAC]);
assert!(base.to_read.is_empty(), "the partner byte was consumed");
}
#[test]
fn read_handles_a_lone_iac_as_the_only_byte() {
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = FakeServer::new(&[IAC, IAC]);
let user = AsynUser::default();
let mut buf = [0u8; 1];
let r = stack.dispatch_read(&user, &mut buf, &mut base).unwrap();
assert_eq!(r.nbytes_transferred, 1);
assert_eq!(buf[0], IAC);
}
#[test]
fn read_unstuffs_consecutive_escapes() {
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = FakeServer::new(&[IAC, IAC, IAC, IAC, b'Z']);
let user = AsynUser::default();
let mut buf = [0u8; 8];
let r = stack.dispatch_read(&user, &mut buf, &mut base).unwrap();
assert_eq!(r.nbytes_transferred, 3);
assert_eq!(&buf[..3], &[IAC, IAC, b'Z']);
}
#[test]
fn read_rejects_an_unescaped_iac() {
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = FakeServer::new(&[b'A', IAC, WILL, 0]);
let user = AsynUser::default();
let mut buf = [0u8; 8];
let err = stack.dispatch_read(&user, &mut buf, &mut base).unwrap_err();
assert_eq!(err.status(), AsynStatus::Error);
assert_eq!(err.message(), "Missing IAC");
}
#[test]
fn unstuffing_clears_the_count_eom_reason() {
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = FakeServer::new(&[b'A', IAC, IAC, b'B']);
let user = AsynUser::default();
let mut buf = [0u8; 4];
let r = stack.dispatch_read(&user, &mut buf, &mut base).unwrap();
assert_eq!(r.nbytes_transferred, 3);
assert!(!r.eom_reason.contains(EomReason::CNT));
}
#[test]
fn read_without_iac_keeps_the_base_eom_reason() {
let mut stack = OctetInterposeStack::new(false);
stack.install(-1, Box::new(ComInterpose::new()));
let mut base = FakeServer::new(b"ABCD");
let user = AsynUser::default();
let mut buf = [0u8; 4];
let r = stack.dispatch_read(&user, &mut buf, &mut base).unwrap();
assert_eq!(r.nbytes_transferred, 4);
assert!(r.eom_reason.contains(EomReason::CNT));
}
#[test]
fn restore_settings_emits_c_s_exact_handshake() {
let mut reply = Vec::new();
reply.extend_from_slice(&[IAC, WILL, WD_TRANSMIT_BINARY]);
reply.extend_from_slice(&[IAC, DO, WD_TRANSMIT_BINARY]);
reply.extend_from_slice(&[IAC, DO, SB_COM_PORT_OPTION]);
reply.extend_from_slice(&ack(CPO_SET_MODEMSTATE_MASK, &[0]));
reply.extend_from_slice(&ack(CPO_SET_BAUDRATE, &[0x00, 0x00, 0x25, 0x80]));
reply.extend_from_slice(&ack(CPO_SET_DATASIZE, &[8]));
reply.extend_from_slice(&ack(CPO_SET_PARITY, &[CPO_PARITY_NONE]));
reply.extend_from_slice(&ack(CPO_SET_STOPSIZE, &[1]));
reply.extend_from_slice(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_NOFLOW]));
reply.extend_from_slice(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_NOFLOW]));
let mut server = FakeServer::new(&reply);
let mut com = ComPortOptions::new();
com.restore_settings(&mut server).unwrap();
#[rustfmt::skip]
let expected: Vec<u8> = vec![
IAC, DO, WD_TRANSMIT_BINARY, IAC, WILL, WD_TRANSMIT_BINARY, IAC, WILL, SB_COM_PORT_OPTION, IAC, SB, SB_COM_PORT_OPTION, CPO_SET_MODEMSTATE_MASK, 0, IAC, SE, IAC, SB, SB_COM_PORT_OPTION, CPO_SET_BAUDRATE, 0x00, 0x00, 0x25, 0x80, IAC, SE,
IAC, SB, SB_COM_PORT_OPTION, CPO_SET_DATASIZE, 8, IAC, SE,
IAC, SB, SB_COM_PORT_OPTION, CPO_SET_PARITY, CPO_PARITY_NONE, IAC, SE,
IAC, SB, SB_COM_PORT_OPTION, CPO_SET_STOPSIZE, 1, IAC, SE,
IAC, SB, SB_COM_PORT_OPTION, CPO_SET_CONTROL, CPO_CONTROL_NOFLOW, IAC, SE,
IAC, SB, SB_COM_PORT_OPTION, CPO_SET_CONTROL, CPO_CONTROL_NOFLOW, IAC, SE,
];
assert_eq!(server.written, expected);
assert_eq!(com, ComPortOptions::default());
}
#[test]
fn set_baud_emits_a_big_endian_four_byte_subnegotiation() {
let mut server = FakeServer::new(&ack(CPO_SET_BAUDRATE, &[0x00, 0x01, 0xC2, 0x00]));
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "baud", "115200")
.unwrap();
assert_eq!(
server.written,
vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SET_BAUDRATE,
0x00,
0x01,
0xC2,
0x00,
IAC,
SE
]
);
assert_eq!(com.get_option("baud").unwrap(), "115200");
}
#[test]
fn b8_a_payload_byte_of_0xff_is_escaped_both_ways() {
let reply = vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SET_BAUDRATE + 100,
0x00,
0x00,
0x00,
IAC,
IAC, IAC,
SE,
];
let mut server = FakeServer::new(&reply);
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "baud", "255")
.unwrap();
assert_eq!(
server.written,
vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SET_BAUDRATE,
0x00,
0x00,
0x00,
0xFF,
0xFF, IAC,
SE
]
);
assert_eq!(com.get_option("baud").unwrap(), "255");
}
#[test]
fn b8_an_unescaped_iac_in_a_reply_payload_is_a_framing_error() {
let reply = vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SET_BAUDRATE + 100,
0x00,
0x00,
0x00,
IAC,
SE, ];
let mut server = FakeServer::new(&reply);
let mut com = ComPortOptions::new();
let err = com
.set_option(&mut AsynUser::default(), &mut server, "baud", "255")
.unwrap_err();
assert_eq!(err.status(), AsynStatus::Error);
assert_eq!(
err.message(),
"Unescaped IAC in a COM-PORT-OPTION payload, followed by 0XF0"
);
}
#[test]
fn set_baud_fails_when_the_server_applies_a_different_rate() {
let mut server = FakeServer::new(&ack(CPO_SET_BAUDRATE, &[0x00, 0x00, 0x25, 0x80]));
let mut com = ComPortOptions::new();
let err = com
.set_option(&mut AsynUser::default(), &mut server, "baud", "115200")
.unwrap_err();
assert_eq!(err.status(), AsynStatus::Error);
assert_eq!(
err.message(),
"Tried to set 115200 baud, actually set 9600 baud."
);
assert_eq!(com.get_option("baud").unwrap(), "9600");
}
#[test]
fn set_bits_and_stop_check_the_echo() {
let mut server = FakeServer::new(&ack(CPO_SET_DATASIZE, &[7]));
let mut com = ComPortOptions::new();
let err = com
.set_option(&mut AsynUser::default(), &mut server, "bits", "8")
.unwrap_err();
assert_eq!(err.message(), "Tried to set 8 bits, actually set 7 bits.");
let mut server = FakeServer::new(&ack(CPO_SET_STOPSIZE, &[2]));
let mut com = ComPortOptions::new();
let err = com
.set_option(&mut AsynUser::default(), &mut server, "stop", "1")
.unwrap_err();
assert_eq!(
err.message(),
"Tried to set 1 stop bits, actually set 2 stop bits."
);
}
#[test]
fn set_parity_does_not_check_the_echo() {
let mut server = FakeServer::new(&ack(CPO_SET_PARITY, &[CPO_PARITY_ODD]));
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "parity", "even")
.unwrap();
assert_eq!(
server.written,
vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SET_PARITY,
CPO_PARITY_EVEN,
IAC,
SE
]
);
assert_eq!(com.get_option("parity").unwrap(), "odd");
}
#[test]
fn parity_names_map_to_the_rfc2217_codes() {
for (name, code) in [
("none", CPO_PARITY_NONE),
("odd", CPO_PARITY_ODD),
("even", CPO_PARITY_EVEN),
("mark", CPO_PARITY_MARK),
("space", CPO_PARITY_SPACE),
] {
let mut server = FakeServer::new(&ack(CPO_SET_PARITY, &[code]));
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "parity", name)
.unwrap();
assert_eq!(server.written[4], code, "parity {name}");
assert_eq!(com.get_option("parity").unwrap(), name);
}
}
#[test]
fn turning_off_the_mode_that_is_not_in_effect_leaves_the_other_running() {
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_IXON]));
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "ixon", "y")
.unwrap();
assert_eq!(com.get_option("ixon").unwrap(), "Y");
assert_eq!(com.get_option("crtscts").unwrap(), "N");
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_IXON]));
com.set_option(&mut AsynUser::default(), &mut server, "crtscts", "n")
.unwrap();
assert_eq!(
server.written,
vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SET_CONTROL,
CPO_CONTROL_IXON,
IAC,
SE
]
);
assert_eq!(com.get_option("ixon").unwrap(), "Y");
}
#[test]
fn each_key_can_turn_its_own_flow_control_back_off() {
for (key, on_mode) in [("crtscts", CPO_CONTROL_HWFLOW), ("ixon", CPO_CONTROL_IXON)] {
let mut com = ComPortOptions::new();
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[on_mode]));
com.set_option(&mut AsynUser::default(), &mut server, key, "y")
.unwrap();
assert_eq!(com.get_option(key).unwrap(), "Y", "{key} on");
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_NOFLOW]));
com.set_option(&mut AsynUser::default(), &mut server, key, "n")
.unwrap();
assert_eq!(
server.written,
vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SET_CONTROL,
CPO_CONTROL_NOFLOW,
IAC,
SE
],
"{key} off must transmit NOFLOW"
);
assert_eq!(com.get_option(key).unwrap(), "N", "{key} reads back off");
assert_eq!(com.get_option("crtscts").unwrap(), "N");
assert_eq!(com.get_option("ixon").unwrap(), "N");
}
}
#[test]
fn turning_flow_control_off_when_it_is_already_off_sends_noflow() {
let mut com = ComPortOptions::new();
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_NOFLOW]));
com.set_option(&mut AsynUser::default(), &mut server, "crtscts", "n")
.unwrap();
assert_eq!(server.written[4], CPO_CONTROL_NOFLOW);
assert_eq!(com.get_option("crtscts").unwrap(), "N");
}
#[test]
fn crtscts_y_sets_hardware_flow_control() {
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_HWFLOW]));
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "crtscts", "y")
.unwrap();
assert_eq!(
server.written,
vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SET_CONTROL,
CPO_CONTROL_HWFLOW,
IAC,
SE
]
);
assert_eq!(com.get_option("crtscts").unwrap(), "Y");
assert_eq!(com.get_option("ixon").unwrap(), "N");
}
#[test]
fn break_on_and_off_emit_set_control_break() {
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_BREAK_ON]));
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "break", "on")
.unwrap();
assert_eq!(
server.written,
vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SET_CONTROL,
CPO_CONTROL_BREAK_ON,
IAC,
SE
]
);
assert_eq!(com.get_option("break").unwrap(), "on");
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_BREAK_OFF]));
com.set_option(&mut AsynUser::default(), &mut server, "break", "off")
.unwrap();
assert_eq!(
server.written,
vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SET_CONTROL,
CPO_CONTROL_BREAK_OFF,
IAC,
SE
]
);
assert_eq!(com.get_option("break").unwrap(), "off");
}
#[test]
fn break_is_edge_triggered_against_the_cached_state() {
let mut server = FakeServer::new(&[]);
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "break", "off")
.unwrap();
assert!(server.written.is_empty());
}
#[test]
fn break_with_a_duration_asserts_then_releases() {
let mut reply = ack(CPO_SET_CONTROL, &[CPO_CONTROL_BREAK_ON]);
reply.extend_from_slice(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_BREAK_OFF]));
let mut server = FakeServer::new(&reply);
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "break", "1")
.unwrap();
#[rustfmt::skip]
let expected: Vec<u8> = vec![
IAC, SB, SB_COM_PORT_OPTION, CPO_SET_CONTROL, CPO_CONTROL_BREAK_ON, IAC, SE,
IAC, SB, SB_COM_PORT_OPTION, CPO_SET_CONTROL, CPO_CONTROL_BREAK_OFF, IAC, SE,
];
assert_eq!(server.written, expected);
assert_eq!(com.get_option("break").unwrap(), "off");
}
#[test]
fn a_volunteered_modemstate_notification_is_skipped() {
let mut reply = vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SERVER_NOTIFY_MODEMSTATE,
0x30,
IAC,
SE,
];
reply.extend_from_slice(&ack(CPO_SET_DATASIZE, &[8]));
let mut server = FakeServer::new(&reply);
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "bits", "8")
.unwrap();
assert_eq!(com.get_option("bits").unwrap(), "8");
}
#[test]
fn a_volunteered_linestate_notification_is_skipped() {
let mut reply = vec![
IAC,
SB,
SB_COM_PORT_OPTION,
CPO_SERVER_NOTIFY_LINESTATE,
0x60,
IAC,
SE,
];
reply.extend_from_slice(&ack(CPO_SET_DATASIZE, &[8]));
let mut server = FakeServer::new(&reply);
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "bits", "8")
.unwrap();
assert_eq!(com.get_option("bits").unwrap(), "8");
}
#[test]
fn a_reply_for_the_wrong_subcommand_is_an_error() {
let mut server = FakeServer::new(&ack(CPO_SET_PARITY, &[CPO_PARITY_NONE]));
let mut com = ComPortOptions::new();
let err = com
.set_option(&mut AsynUser::default(), &mut server, "bits", "8")
.unwrap_err();
assert_eq!(err.status(), AsynStatus::Error);
assert_eq!(err.message(), "Sent COM-PORT-OPTION 2 but got reply 103");
}
#[test]
fn a_malformed_subnegotiation_frame_reports_c_s_expected_got() {
let mut server = FakeServer::new(&[IAC, b'A']);
let mut com = ComPortOptions::new();
let err = com
.set_option(&mut AsynUser::default(), &mut server, "bits", "8")
.unwrap_err();
assert_eq!(err.message(), "Expected 0XFA, got 0X41");
}
#[test]
fn willdo_reports_a_refusal() {
let mut server = FakeServer::new(&[IAC, WONT, WD_TRANSMIT_BINARY]);
let mut com = ComPortOptions::new();
let err = com.restore_settings(&mut server).unwrap_err();
assert_eq!(err.status(), AsynStatus::Error);
assert_eq!(err.message(), "Device says WON'T 0.");
}
#[test]
fn willdo_reports_a_dont_refusal_for_a_will_request() {
let mut reply = vec![IAC, WILL, WD_TRANSMIT_BINARY, IAC, DO, WD_TRANSMIT_BINARY];
reply.extend_from_slice(&[IAC, DONT, SB_COM_PORT_OPTION]);
let mut server = FakeServer::new(&reply);
let mut com = ComPortOptions::new();
let err = com.restore_settings(&mut server).unwrap_err();
assert_eq!(err.message(), "Device says DON'T 0x2c.");
}
#[test]
fn willdo_rejects_a_do_echoed_in_response_to_do() {
let mut server = FakeServer::new(&[IAC, DO, WD_TRANSMIT_BINARY]);
let mut com = ComPortOptions::new();
let err = com.restore_settings(&mut server).unwrap_err();
assert_eq!(err.message(), "Received response 0 in response to DO.");
}
#[test]
fn willdo_skips_replies_about_other_option_codes() {
let mut reply = Vec::new();
reply.extend_from_slice(&[IAC, WILL, 1, IAC, WONT, 3]);
reply.extend_from_slice(&[IAC, WILL, WD_TRANSMIT_BINARY]);
reply.extend_from_slice(&[IAC, DO, WD_TRANSMIT_BINARY]);
reply.extend_from_slice(&[IAC, DO, SB_COM_PORT_OPTION]);
reply.extend_from_slice(&ack(CPO_SET_MODEMSTATE_MASK, &[0]));
reply.extend_from_slice(&ack(CPO_SET_BAUDRATE, &[0x00, 0x00, 0x25, 0x80]));
reply.extend_from_slice(&ack(CPO_SET_DATASIZE, &[8]));
reply.extend_from_slice(&ack(CPO_SET_PARITY, &[CPO_PARITY_NONE]));
reply.extend_from_slice(&ack(CPO_SET_STOPSIZE, &[1]));
reply.extend_from_slice(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_NOFLOW]));
reply.extend_from_slice(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_NOFLOW]));
let mut server = FakeServer::new(&reply);
let mut com = ComPortOptions::new();
com.restore_settings(&mut server).unwrap();
assert_eq!(com, ComPortOptions::default());
}
#[test]
fn willdo_rejects_an_unexpected_command_byte() {
let mut server = FakeServer::new(&[IAC, b'A']);
let mut com = ComPortOptions::new();
let err = com.restore_settings(&mut server).unwrap_err();
assert_eq!(err.message(), "Unexpected character 0x41 in TELNET reply");
}
#[test]
fn a_silent_server_surfaces_the_lower_layers_message_as_asyn_error() {
let mut server = FakeServer::new(&[]);
let mut com = ComPortOptions::new();
let err = com.restore_settings(&mut server).unwrap_err();
assert_eq!(err.status(), AsynStatus::Error);
assert_eq!(err.message(), "read timeout");
}
#[test]
fn owns_exactly_c_s_seven_keys_case_insensitively() {
for key in ["baud", "BITS", "Parity", "stop", "CRTSCTS", "ixon", "Break"] {
assert!(ComPortOptions::owns_key(key), "{key}");
}
for key in ["hostInfo", "disconnectOnReadTimeout", "clocal", "ixoff"] {
assert!(!ComPortOptions::owns_key(key), "{key}");
}
}
#[test]
fn the_negotiation_runs_under_the_callers_timeout() {
struct TimeoutSpy {
inner: FakeServer,
seen: Vec<Duration>,
}
impl OctetNext for TimeoutSpy {
fn read(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<OctetReadResult> {
self.seen.push(user.timeout);
self.inner.read(user, buf)
}
fn write(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
self.seen.push(user.timeout);
self.inner.write(user, data)
}
fn flush(&mut self, user: &mut AsynUser) -> AsynResult<()> {
self.inner.flush(user)
}
}
let mut spy = TimeoutSpy {
inner: FakeServer::new(&ack(CPO_SET_DATASIZE, &[8])),
seen: Vec::new(),
};
let mut com = ComPortOptions::new();
let caller = Duration::from_millis(250);
com.set_option(
&mut AsynUser::default().with_timeout(caller),
&mut spy,
"bits",
"8",
)
.unwrap();
assert!(!spy.seen.is_empty(), "the negotiation reached the wire");
assert!(
spy.seen.iter().all(|t| *t == caller),
"every wire operation runs under the caller's timeout, got {:?}",
spy.seen
);
let mut replies = Vec::new();
replies.extend(vec![IAC, WILL, WD_TRANSMIT_BINARY]);
replies.extend(vec![IAC, DO, WD_TRANSMIT_BINARY]);
replies.extend(vec![IAC, DO, SB_COM_PORT_OPTION]);
replies.extend(ack(CPO_SET_MODEMSTATE_MASK, &[0]));
replies.extend(ack(CPO_SET_BAUDRATE, &9600i32.to_be_bytes()));
replies.extend(ack(CPO_SET_DATASIZE, &[8]));
replies.extend(ack(CPO_SET_PARITY, &[CPO_PARITY_NONE]));
replies.extend(ack(CPO_SET_STOPSIZE, &[1]));
replies.extend(ack(CPO_SET_CONTROL, &[CPO_CONTROL_NOFLOW]));
replies.extend(ack(CPO_SET_CONTROL, &[CPO_CONTROL_NOFLOW]));
let mut spy = TimeoutSpy {
inner: FakeServer::new(&replies),
seen: Vec::new(),
};
let mut com = ComPortOptions::new();
com.restore_settings(&mut spy).unwrap();
assert!(!spy.seen.is_empty());
assert!(
spy.seen.iter().all(|t| *t == INTERPOSE_USER_TIMEOUT),
"restoreSettings runs on the interpose's own 2 s asynUser, got {:?}",
spy.seen
);
}
#[test]
fn an_unstuffed_read_is_traced_at_traceio_filter() {
use crate::trace::{TraceFile, TraceInfoMask, TraceIoMask, TraceManager};
use crate::user::UserTrace;
use std::sync::{Arc, Mutex};
let mgr = Arc::new(TraceManager::new());
mgr.set_trace_mask(Some("comtrace"), TraceMask::IO_FILTER);
mgr.set_trace_info_mask(Some("comtrace"), TraceInfoMask::PORT);
mgr.set_trace_io_mask(Some("comtrace"), TraceIoMask::ESCAPE);
let temp = std::env::temp_dir().join("asyn_com_filter_trace.txt");
let file = std::fs::File::create(&temp).unwrap();
mgr.set_trace_file(
Some("comtrace"),
TraceFile::File(Arc::new(Mutex::new(file))),
);
let user = AsynUser {
trace: Some(UserTrace {
manager: mgr.clone(),
port: "comtrace".into(),
}),
..AsynUser::default()
};
let mut server = FakeServer::new(&[b'A', IAC, IAC, b'B']);
let mut com = ComInterpose::new();
let mut buf = [0u8; 8];
let r = com.read(&user, &mut buf, &mut server).unwrap();
assert_eq!(&buf[..r.nbytes_transferred], &[b'A', IAC, b'B']);
let contents = std::fs::read_to_string(&temp).unwrap();
assert!(
contents.contains("nRead 3 after IAC unstuffing"),
"C :238 label, got {contents:?}"
);
assert!(contents.contains("IO_FILTER"), "at ASYN_TRACEIO_FILTER");
let _ = std::fs::remove_file(&temp);
let temp2 = std::env::temp_dir().join("asyn_com_filter_trace_quiet.txt");
let file = std::fs::File::create(&temp2).unwrap();
mgr.set_trace_file(
Some("comtrace"),
TraceFile::File(Arc::new(Mutex::new(file))),
);
let mut server = FakeServer::new(b"AB");
com.read(&user, &mut buf, &mut server).unwrap();
assert_eq!(std::fs::read_to_string(&temp2).unwrap(), "");
let _ = std::fs::remove_file(&temp2);
}
#[test]
fn switching_flow_control_leaves_c_s_advisory_in_the_users_message() {
let mut user = AsynUser::default();
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_HWFLOW]));
let mut com = ComPortOptions::new();
com.flow = CPO_CONTROL_IXON;
com.set_option(&mut user, &mut server, "crtscts", "y")
.expect("the advisory does not fail the call");
assert_eq!(
user.error_message,
"XON/XOFF already set. Now using RTS/CTS."
);
assert_eq!(com.flow, CPO_CONTROL_HWFLOW);
let mut user = AsynUser::default();
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_IXON]));
let mut com = ComPortOptions::new();
com.flow = CPO_CONTROL_HWFLOW;
com.set_option(&mut user, &mut server, "ixon", "y")
.expect("the advisory does not fail the call");
assert_eq!(
user.error_message,
"RTS/CTS already set. Now using XON/XOFF."
);
assert_eq!(com.flow, CPO_CONTROL_IXON);
let mut user = AsynUser::default();
let mut server = FakeServer::new(&ack(CPO_SET_CONTROL, &[CPO_CONTROL_HWFLOW]));
let mut com = ComPortOptions::new();
com.set_option(&mut user, &mut server, "crtscts", "y")
.unwrap();
assert_eq!(user.error_message, "");
}
#[test]
fn get_option_reports_the_defaults_asyn_interpose_com_installs() {
let com = ComPortOptions::new();
assert_eq!(com.get_option("baud").unwrap(), "9600");
assert_eq!(com.get_option("bits").unwrap(), "8");
assert_eq!(com.get_option("parity").unwrap(), "none");
assert_eq!(com.get_option("stop").unwrap(), "1");
assert_eq!(com.get_option("crtscts").unwrap(), "N");
assert_eq!(com.get_option("ixon").unwrap(), "N");
assert_eq!(com.get_option("break").unwrap(), "off");
}
#[test]
fn bad_option_values_are_rejected_with_c_s_messages() {
let mut server = FakeServer::new(&[]);
let mut com = ComPortOptions::new();
assert_eq!(
com.set_option(&mut AsynUser::default(), &mut server, "baud", "fast")
.unwrap_err()
.message(),
"Bad number"
);
assert_eq!(
com.set_option(&mut AsynUser::default(), &mut server, "parity", "sideways")
.unwrap_err()
.message(),
"Invalid parity selection"
);
assert_eq!(
com.set_option(&mut AsynUser::default(), &mut server, "stop", "1.5")
.unwrap_err()
.message(),
"Bad stop bit count"
);
assert_eq!(
com.set_option(&mut AsynUser::default(), &mut server, "crtscts", "maybe")
.unwrap_err()
.message(),
"Bad value"
);
assert!(server.written.is_empty());
}
#[test]
fn baud_accepts_a_trailing_tail_the_way_sscanf_does() {
let mut server = FakeServer::new(&ack(CPO_SET_BAUDRATE, &[0x00, 0x00, 0x25, 0x80]));
let mut com = ComPortOptions::new();
com.set_option(&mut AsynUser::default(), &mut server, "baud", "9600 bps")
.unwrap();
assert_eq!(com.get_option("baud").unwrap(), "9600");
}
#[test]
fn scan_helpers_match_sscanf() {
assert_eq!(scan_int("9600"), Some(9600));
assert_eq!(scan_int(" -12xyz"), Some(-12));
assert_eq!(scan_int("+7"), Some(7));
assert_eq!(scan_int("abc"), None);
assert_eq!(scan_int(""), None);
assert_eq!(scan_uint("250ms"), Some(250));
assert_eq!(scan_uint("x"), None);
assert_eq!(scan_float("2"), Some(2.0));
assert_eq!(scan_float("1.5abc"), Some(1.5));
assert_eq!(scan_float("nope"), None);
}
#[test]
fn hash_hex_matches_printf() {
assert_eq!(hash_hex_lower(0), "0");
assert_eq!(hash_hex_lower(44), "0x2c");
assert_eq!(hash_hex_upper(0), "0");
assert_eq!(hash_hex_upper(250), "0XFA");
assert_eq!(hash_hex_upper(0x41), "0X41");
}
}