Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 148 additions & 5 deletions lib/wasix/src/os/tty/tty_sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ mod sys_terminal_size {
mod sys {
use {
libc::{
ECHO, ECHOCTL, ECHOE, ECHOK, ECHONL, ICANON, ICRNL, IEXTEN, IGNCR, ISIG, IXON, ONLCR,
OPOST, TCSANOW, c_int, tcsetattr, termios,
ECHO, ECHOCTL, ECHOE, ECHOK, ECHONL, ICANON, ICRNL, IEXTEN, IGNCR, INLCR, ISIG, IXON,
ONLCR, OPOST, TCSANOW, c_int, tcsetattr, termios,
},
std::mem,
std::os::unix::io::AsRawFd,
Expand All @@ -96,7 +96,8 @@ mod sys {
io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
let mut termios = unsafe { termios.assume_init() };

termios.c_lflag |= ISIG | ICANON | IEXTEN | ECHO | ECHOE | ECHOK | ECHOCTL;
termios.c_lflag |= ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOCTL;
set_line_buffering(&mut termios, true);

unsafe { tcsetattr(0, TCSANOW, &termios) };
Ok(())
Expand Down Expand Up @@ -185,7 +186,7 @@ mod sys {
io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
let mut termios = unsafe { termios.assume_init() };

termios.c_lflag &= !ICANON;
set_line_buffering(&mut termios, false);

unsafe { tcsetattr(0, TCSANOW, &termios) };
Ok(())
Expand All @@ -196,12 +197,25 @@ mod sys {
io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
let mut termios = unsafe { termios.assume_init() };

termios.c_lflag |= ICANON;
set_line_buffering(&mut termios, true);

unsafe { tcsetattr(0, TCSANOW, &termios) };
Ok(())
}

fn set_line_buffering(termios: &mut termios, enabled: bool) {
if enabled {
termios.c_lflag |= ICANON;
termios.c_iflag |= ICRNL;
termios.c_iflag &= !(INLCR | IGNCR);
} else {
termios.c_lflag &= !ICANON;
// Preserve carriage returns so applications can distinguish Enter (CR)
// from line feed, which is commonly used for Shift+Enter.
termios.c_iflag &= !(ICRNL | INLCR | IGNCR);
}
}

pub fn set_mode_no_line_feeds() -> Result<(), anyhow::Error> {
let mut termios = mem::MaybeUninit::<termios>::uninit();
io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
Expand All @@ -223,6 +237,135 @@ mod sys {
unsafe { tcsetattr(0, TCSANOW, &termios) };
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

fn blank_termios() -> termios {
// SAFETY: libc::termios is a plain C data structure for which an all-zero
// value is valid; the tests only inspect and update its flag fields.
unsafe { mem::zeroed() }
}

#[test]
fn noncanonical_input_preserves_carriage_returns_and_line_feeds() {
let mut state = blank_termios();
state.c_lflag = ICANON | ECHO | ISIG;
state.c_iflag = ICRNL | INLCR | IGNCR | IXON;
state.c_oflag = OPOST;

set_line_buffering(&mut state, false);

assert_eq!(state.c_lflag & ICANON, 0);
assert_eq!(state.c_iflag & (ICRNL | INLCR | IGNCR), 0);
assert_ne!(state.c_lflag & ECHO, 0);
assert_ne!(state.c_lflag & ISIG, 0);
assert_ne!(state.c_iflag & IXON, 0);
assert_ne!(state.c_oflag & OPOST, 0);
}

#[test]
fn cooked_input_translates_carriage_returns_to_newlines() {
let mut state = blank_termios();
state.c_iflag = INLCR | IGNCR | IXON;

set_line_buffering(&mut state, true);

assert_ne!(state.c_lflag & ICANON, 0);
assert_ne!(state.c_iflag & ICRNL, 0);
assert_eq!(state.c_iflag & (INLCR | IGNCR), 0);
assert_ne!(state.c_iflag & IXON, 0);
}

fn read_exact_with_timeout(fd: c_int, output: &mut [u8]) {
let mut offset = 0;
while offset < output.len() {
let mut descriptor = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
assert_eq!(
unsafe { libc::poll(&mut descriptor, 1, 1_000) },
1,
"timed out waiting for PTY input"
);
assert_ne!(descriptor.revents & libc::POLLIN, 0);

let read = unsafe {
libc::read(
fd,
output[offset..].as_mut_ptr().cast(),
output.len() - offset,
)
};
assert!(read > 0, "failed to read PTY input");
offset += read as usize;
}
}

#[test]
fn noncanonical_pty_input_distinguishes_enter_from_line_feed() {
let mut master = -1;
let mut slave = -1;
assert_eq!(
unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null(),
)
},
0
);

struct Pty {
master: c_int,
slave: c_int,
}

impl Drop for Pty {
fn drop(&mut self) {
unsafe {
libc::close(self.master);
libc::close(self.slave);
}
}
}

let pty = Pty { master, slave };
let mut state = blank_termios();
assert_eq!(unsafe { libc::tcgetattr(pty.slave, &mut state) }, 0);
state.c_iflag |= ICRNL | INLCR | IGNCR;
set_line_buffering(&mut state, false);
assert_eq!(unsafe { libc::tcsetattr(pty.slave, TCSANOW, &state) }, 0);

let input = [b'\r', b'\n'];
assert_eq!(
unsafe { libc::write(pty.master, input.as_ptr().cast(), input.len()) },
input.len() as isize
);

let mut output = [0_u8; 2];
read_exact_with_timeout(pty.slave, &mut output);
assert_eq!(output, input);

assert_eq!(unsafe { libc::tcgetattr(pty.slave, &mut state) }, 0);
set_line_buffering(&mut state, true);
assert_eq!(unsafe { libc::tcsetattr(pty.slave, TCSANOW, &state) }, 0);
let input = [b'\r'];
assert_eq!(
unsafe { libc::write(pty.master, input.as_ptr().cast(), input.len()) },
1
);
let mut output = [0_u8];
read_exact_with_timeout(pty.slave, &mut output);
assert_eq!(output, [b'\n']);
}
}
}

#[cfg(any(not(unix), target_os = "ios"))]
Expand Down
Loading