From eb74ff43af0d213567c4f53db6dc838805b1bf08 Mon Sep 17 00:00:00 2001 From: vasilito Date: Wed, 8 Jul 2026 21:26:25 +0300 Subject: [PATCH] Audit fixes: unsafe scope, unwrap cleanup, goto.rs consolidation, hex_edit tests, Cargo path fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL fixes (F1-F3 from audit): - tlc_pty_login.rs: #![deny(unsafe_code)] with scoped #[allow] + SAFETY doc - handlers.rs:55: unsafe .is_some()+unwrap() → if let Some(menubar) - editor/mod.rs:515: .unwrap() → .expect("system clock before Unix epoch") MEDIUM fixes: - editor/goto.rs: 4 duplicate #[allow(result_large_err)] → 1 module-level #![allow] - viewer/hex_edit.rs: +7 tests (hex_digit parser, nibble styles, HEX_CHARS table) - viewer/mod.rs: pub(crate) mod tests for cross-module test helper access INFRA: - Cargo.toml: fixed redox_syscall path (../../../../→../../../../../) for single canonical source path across all targets 1440 tests passing, zero warnings. --- local/recipes/tui/tlc/source/Cargo.toml | 3 +- .../tui/tlc/source/src/bin/tlc_pty_login.rs | 6 +- .../recipes/tui/tlc/source/src/editor/goto.rs | 9 +-- .../tui/tlc/source/src/editor/handlers.rs | 4 +- .../recipes/tui/tlc/source/src/editor/mod.rs | 2 +- .../tui/tlc/source/src/viewer/hex_edit.rs | 65 +++++++++++++++++++ .../recipes/tui/tlc/source/src/viewer/mod.rs | 4 +- 7 files changed, 82 insertions(+), 11 deletions(-) diff --git a/local/recipes/tui/tlc/source/Cargo.toml b/local/recipes/tui/tlc/source/Cargo.toml index a2a9d93a3c..86d4a9b7e5 100644 --- a/local/recipes/tui/tlc/source/Cargo.toml +++ b/local/recipes/tui/tlc/source/Cargo.toml @@ -76,7 +76,7 @@ suppaftp = { version = "6", optional = true } tar = { version = "0.4", optional = true } zip = { version = "2", default-features = false, optional = true } -redox_syscall = { path = "../../../../local/sources/syscall" } +redox_syscall = { path = "../../../../../local/sources/syscall" } # Syntax highlighting syntect = { version = "5", default-features = false, features = ["default-onig", "regex-onig"], optional = true } @@ -165,3 +165,4 @@ opt-level = 1 [patch.crates-io] libredox = { path = "../../../../../local/sources/libredox" } +redox_syscall = { path = "../../../../../local/sources/syscall" } diff --git a/local/recipes/tui/tlc/source/src/bin/tlc_pty_login.rs b/local/recipes/tui/tlc/source/src/bin/tlc_pty_login.rs index a30376ed36..77842c80d7 100644 --- a/local/recipes/tui/tlc/source/src/bin/tlc_pty_login.rs +++ b/local/recipes/tui/tlc/source/src/bin/tlc_pty_login.rs @@ -1,4 +1,4 @@ -#![allow(unsafe_code)] +#![deny(unsafe_code)] use std::fs::OpenOptions; use std::os::fd::AsRawFd; @@ -18,6 +18,10 @@ fn main() -> anyhow::Result<()> { .write(true) .open(&tty_path)?; + // SAFETY: login_tty(2) has no safe Rust equivalent. The tty fd was + // just opened by us and is guaranteed valid. On error, the return + // code is checked and the last OS error is surfaced. + #[allow(unsafe_code)] let rc = unsafe { libc::login_tty(tty.as_raw_fd()) }; if rc != 0 { return Err(std::io::Error::last_os_error().into()); diff --git a/local/recipes/tui/tlc/source/src/editor/goto.rs b/local/recipes/tui/tlc/source/src/editor/goto.rs index 46748b8758..023e0745a6 100644 --- a/local/recipes/tui/tlc/source/src/editor/goto.rs +++ b/local/recipes/tui/tlc/source/src/editor/goto.rs @@ -10,6 +10,11 @@ //! for clamping the returned offset to `[0, buf.len()]` and moving //! the cursor. //! +//! Several functions return `Result<_, String>` for user-facing error +//! messages; String is a large error type but this is intentional — +//! these errors are displayed directly to the user. +#![allow(clippy::result_large_err)] +//! //! ## Address grammar //! //! [`parse_address`] accepts three input forms: @@ -37,7 +42,6 @@ use crate::editor::buffer::Buffer; /// Returns an error if `s` is empty, contains non-digit characters /// other than the separators `:` and `%`, or has malformed structure /// (e.g. `:` without a column, `%` not at the end). -#[allow(clippy::result_large_err)] pub fn parse_address(s: &str, total_lines: u32) -> Result<(u32, u32), String> { let s = s.trim(); if s.is_empty() { @@ -98,7 +102,6 @@ pub fn parse_address(s: &str, total_lines: u32) -> Result<(u32, u32), String> { /// /// Returns an error if `line == 0` (since 1 is the smallest valid /// 1-based line) or if the line is past the end of the buffer. -#[allow(clippy::result_large_err)] pub fn line_to_offset(buf: &Buffer, line: u32) -> Result { if line == 0 { return Err("line numbers are 1-based; 0 is invalid".to_string()); @@ -124,7 +127,6 @@ pub fn line_to_offset(buf: &Buffer, line: u32) -> Result { /// /// Returns an error if `line` is 0, `col` is 0, `line` is past the /// end of the buffer, or `col` is past the end of the line. -#[allow(clippy::result_large_err)] pub fn col_to_offset(buf: &Buffer, line: u32, col: u32) -> Result { let line_off = line_to_offset(buf, line)?; if col == 0 { @@ -153,7 +155,6 @@ pub fn col_to_offset(buf: &Buffer, line: u32, col: u32) -> Result /// # Errors /// /// Returns an error if `pct > 100`. -#[allow(clippy::result_large_err)] pub fn percent_to_offset(buf: &Buffer, pct: u8) -> Result { if pct > 100 { return Err(format!("percentage out of range: {pct}")); diff --git a/local/recipes/tui/tlc/source/src/editor/handlers.rs b/local/recipes/tui/tlc/source/src/editor/handlers.rs index cf2694b991..d373cf1065 100644 --- a/local/recipes/tui/tlc/source/src/editor/handlers.rs +++ b/local/recipes/tui/tlc/source/src/editor/handlers.rs @@ -52,9 +52,9 @@ impl Editor { } // When the F9 menu bar is open, route the key through it. // Esc / F9 close; arrows / Enter / hotkeys dispatch. - if self.menubar.is_some() { + if let Some(menubar) = self.menubar.as_mut() { use crate::editor::menubar::EditorMenuOutcome; - let outcome = self.menubar.as_mut().unwrap().handle_key(key); + let outcome = menubar.handle_key(key); return match outcome { EditorMenuOutcome::Handled | EditorMenuOutcome::Select(_, _) => { EditorResult::Running diff --git a/local/recipes/tui/tlc/source/src/editor/mod.rs b/local/recipes/tui/tlc/source/src/editor/mod.rs index c9d04c3aa4..6f0a14b20e 100644 --- a/local/recipes/tui/tlc/source/src/editor/mod.rs +++ b/local/recipes/tui/tlc/source/src/editor/mod.rs @@ -512,7 +512,7 @@ impl Editor { "tlc-sort-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .expect("system clock before Unix epoch") .as_nanos() )); let _ = std::fs::create_dir_all(&tmp_dir); diff --git a/local/recipes/tui/tlc/source/src/viewer/hex_edit.rs b/local/recipes/tui/tlc/source/src/viewer/hex_edit.rs index 7f6dd6892e..ef9f224683 100644 --- a/local/recipes/tui/tlc/source/src/viewer/hex_edit.rs +++ b/local/recipes/tui/tlc/source/src/viewer/hex_edit.rs @@ -176,4 +176,69 @@ fn hex_digit(key: Key) -> Option { 'A'..='F' => Some(c as u8 - b'A' + 10), _ => None, } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::key::Modifiers; + + fn plain_key(c: char) -> Key { + Key { code: c as u32, mods: Modifiers::empty() } + } + + #[test] + fn hex_digit_parses_0_to_f() { + for (ch, expected) in [ + ('0', 0), ('1', 1), ('2', 2), ('3', 3), ('4', 4), + ('5', 5), ('6', 6), ('7', 7), ('8', 8), ('9', 9), + ('a', 10), ('b', 11), ('c', 12), ('d', 13), ('e', 14), ('f', 15), + ('A', 10), ('B', 11), ('C', 12), ('D', 13), ('E', 14), ('F', 15), + ] { + assert_eq!(hex_digit(plain_key(ch)), Some(expected), "failed for '{ch}'"); + } + } + + #[test] + fn hex_digit_rejects_non_hex() { + assert_eq!(hex_digit(plain_key('g')), None); + assert_eq!(hex_digit(plain_key('z')), None); + assert_eq!(hex_digit(plain_key(' ')), None); + } + + #[test] + fn hex_digit_rejects_modified_keys() { + let ctrl_f = Key::ctrl('f'); + assert_eq!(hex_digit(ctrl_f), None); + } + + #[test] + fn hex_nibble_oob_returns_question() { + let p = crate::viewer::tests::make_text("hex.bin", &[0x41]); + let v = crate::viewer::Viewer::open(&p).unwrap(); + assert_eq!(hex_nibble_char(&v, 99, true), '?'); + } + + #[test] + fn active_nibble_style_is_bold_reversed() { + let theme = &*crate::terminal::color::DEFAULT_THEME; + let style = active_nibble_style(theme); + assert!(style.add_modifier.contains(Modifier::BOLD)); + assert!(style.add_modifier.contains(Modifier::REVERSED)); + } + + #[test] + fn cursor_off_style_has_no_bold() { + let theme = &*crate::terminal::color::DEFAULT_THEME; + let style = cursor_off_style(theme); + assert!(!style.add_modifier.contains(Modifier::BOLD)); + } + + #[test] + fn hex_chars_table_is_16_entries() { + assert_eq!(HEX_CHARS.len(), 16); + assert_eq!(HEX_CHARS[0], '0'); + assert_eq!(HEX_CHARS[10], 'a'); + assert_eq!(HEX_CHARS[15], 'f'); + } } \ No newline at end of file diff --git a/local/recipes/tui/tlc/source/src/viewer/mod.rs b/local/recipes/tui/tlc/source/src/viewer/mod.rs index d65e29eaae..a7500bd104 100644 --- a/local/recipes/tui/tlc/source/src/viewer/mod.rs +++ b/local/recipes/tui/tlc/source/src/viewer/mod.rs @@ -1539,12 +1539,12 @@ fn format_size(bytes: u64) -> String { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use ratatui::backend::TestBackend; use ratatui::Terminal; - fn make_text(name: &str, data: &[u8]) -> PathBuf { + pub(crate) fn make_text(name: &str, data: &[u8]) -> PathBuf { let dir = std::env::temp_dir().join("tlc-viewer-mod-test"); let _ = std::fs::create_dir_all(&dir); let p = dir.join(name);