Audit fixes: unsafe scope, unwrap cleanup, goto.rs consolidation, hex_edit tests, Cargo path fix

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.
This commit is contained in:
2026-07-08 21:26:25 +03:00
parent 39cfa4a706
commit eb74ff43af
7 changed files with 82 additions and 11 deletions
+2 -1
View File
@@ -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" }
@@ -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());
@@ -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<usize, String> {
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<usize, String> {
///
/// 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<usize, String> {
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<usize, String>
/// # Errors
///
/// Returns an error if `pct > 100`.
#[allow(clippy::result_large_err)]
pub fn percent_to_offset(buf: &Buffer, pct: u8) -> Result<usize, String> {
if pct > 100 {
return Err(format!("percentage out of range: {pct}"));
@@ -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
@@ -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);
@@ -176,4 +176,69 @@ fn hex_digit(key: Key) -> Option<u8> {
'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');
}
}
@@ -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);