From a6b756504aa3fd29c7560f49454c4443d9d50bdb Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Thu, 6 Aug 2026 17:25:02 +0200 Subject: [PATCH 1/2] fix(wasix): preserve carriage returns in raw tty mode --- lib/wasix/src/os/tty/tty_sys.rs | 124 +++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/lib/wasix/src/os/tty/tty_sys.rs b/lib/wasix/src/os/tty/tty_sys.rs index 6c73c9dadbcc..f532c21f398b 100644 --- a/lib/wasix/src/os/tty/tty_sys.rs +++ b/lib/wasix/src/os/tty/tty_sys.rs @@ -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 &= !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 | 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,110 @@ 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 raw_input_preserves_carriage_returns() { + let mut state = blank_termios(); + state.c_lflag = ICANON | ECHO; + state.c_iflag = ICRNL | IGNCR | IXON; + + set_line_buffering(&mut state, false); + + assert_eq!(state.c_lflag & ICANON, 0); + assert_eq!(state.c_iflag & (ICRNL | IGNCR), 0); + assert_ne!(state.c_lflag & ECHO, 0); + assert_ne!(state.c_iflag & IXON, 0); + } + + #[test] + fn cooked_input_translates_carriage_returns_to_newlines() { + let mut state = blank_termios(); + state.c_iflag = 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 & IGNCR, 0); + assert_ne!(state.c_iflag & IXON, 0); + } + + #[test] + fn raw_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; + state.c_iflag &= !IGNCR; + set_line_buffering(&mut state, false); + 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]; + assert_eq!( + unsafe { libc::read(pty.slave, output.as_mut_ptr().cast(), output.len()) }, + 1 + ); + assert_eq!(output, [b'\r']); + + 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); + assert_eq!( + unsafe { libc::write(pty.master, input.as_ptr().cast(), input.len()) }, + 1 + ); + assert_eq!( + unsafe { libc::read(pty.slave, output.as_mut_ptr().cast(), output.len()) }, + 1 + ); + assert_eq!(output, [b'\n']); + } + } } #[cfg(any(not(unix), target_os = "ios"))] From b78da63b89c1f313eadb041544798dcf8b3d0407 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Fri, 7 Aug 2026 01:45:42 +0200 Subject: [PATCH 2/2] fix(wasix): preserve CR and LF in noncanonical tty mode --- lib/wasix/src/os/tty/tty_sys.rs | 75 ++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/lib/wasix/src/os/tty/tty_sys.rs b/lib/wasix/src/os/tty/tty_sys.rs index f532c21f398b..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, @@ -207,12 +207,12 @@ mod sys { if enabled { termios.c_lflag |= ICANON; termios.c_iflag |= ICRNL; - termios.c_iflag &= !IGNCR; + 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 | IGNCR); + termios.c_iflag &= !(ICRNL | INLCR | IGNCR); } } @@ -249,34 +249,64 @@ mod sys { } #[test] - fn raw_input_preserves_carriage_returns() { + fn noncanonical_input_preserves_carriage_returns_and_line_feeds() { let mut state = blank_termios(); - state.c_lflag = ICANON | ECHO; - state.c_iflag = ICRNL | IGNCR | IXON; + 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 | IGNCR), 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 = IGNCR | IXON; + 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 & IGNCR, 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 raw_pty_input_distinguishes_enter_from_line_feed() { + fn noncanonical_pty_input_distinguishes_enter_from_line_feed() { let mut master = -1; let mut slave = -1; assert_eq!( @@ -309,35 +339,30 @@ mod sys { let pty = Pty { master, slave }; let mut state = blank_termios(); assert_eq!(unsafe { libc::tcgetattr(pty.slave, &mut state) }, 0); - state.c_iflag |= ICRNL; - state.c_iflag &= !IGNCR; + 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']; + let input = [b'\r', b'\n']; assert_eq!( unsafe { libc::write(pty.master, input.as_ptr().cast(), input.len()) }, - 1 + input.len() as isize ); - let mut output = [0_u8]; - assert_eq!( - unsafe { libc::read(pty.slave, output.as_mut_ptr().cast(), output.len()) }, - 1 - ); - assert_eq!(output, [b'\r']); + 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 ); - assert_eq!( - unsafe { libc::read(pty.slave, output.as_mut_ptr().cast(), output.len()) }, - 1 - ); + let mut output = [0_u8]; + read_exact_with_timeout(pty.slave, &mut output); assert_eq!(output, [b'\n']); } }