diff --git a/lib/wasix/src/os/tty/tty_sys.rs b/lib/wasix/src/os/tty/tty_sys.rs index 6c73c9dadbcc..d5fc94b6b8ba 100644 --- a/lib/wasix/src/os/tty/tty_sys.rs +++ b/lib/wasix/src/os/tty/tty_sys.rs @@ -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, @@ -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(()) @@ -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(()) @@ -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::::uninit(); io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?; @@ -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"))]