mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
fix(cli): sanitize errors and diagnostics at their choke points
From the narrow Codex confirmation: provider, KSEI, and manifest text still reached the terminal through error messages (a Yahoo error description, a manifest's `kind`), idx-core diagnostics, the ownership heading's ticker code and Bing date, the technical heading and comparison symbol, and printed paths. Sanitize where every error (emit_error) and every diagnostic (runtime) is printed instead of at each caller, plus the remaining direct prints. terminal_safe moves to idx_core::text so the runtime can use it; the CLI re-exports it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
75b8819d95
commit
927d4224e9
8 changed files with 67 additions and 49 deletions
|
|
@ -34,7 +34,10 @@ pub fn handle(cmd: &CacheCmd, format: &OutputFormat) -> Result<(), IdxError> {
|
|||
"newest": info.newest,
|
||||
}));
|
||||
}
|
||||
println!("path: {}", info.path.display());
|
||||
println!(
|
||||
"path: {}",
|
||||
crate::output::text::terminal_safe(&info.path.display().to_string())
|
||||
);
|
||||
println!("files: {}", info.files);
|
||||
println!("size_bytes: {}", info.total_size);
|
||||
println!(
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ pub fn handle(cmd: &ConfigCmd, format: &OutputFormat) -> Result<(), IdxError> {
|
|||
if format.is_json() {
|
||||
json::print_json(&document)
|
||||
} else {
|
||||
println!("{text}");
|
||||
println!("{}", crate::output::text::terminal_safe(&text));
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ fn handle_sync(investor: &Investor, args: &SyncArgs, config: &IdxConfig) -> Resu
|
|||
result.latest_as_of_date.format("%Y-%m-%d"),
|
||||
result.release_count,
|
||||
result.ticker_count,
|
||||
result.db_path
|
||||
terminal_safe(&result.db_path)
|
||||
);
|
||||
}
|
||||
snapshot::OwnershipSyncAction::Updated => {
|
||||
|
|
@ -237,14 +237,14 @@ fn handle_sync(investor: &Investor, args: &SyncArgs, config: &IdxConfig) -> Resu
|
|||
result.latest_as_of_date.format("%Y-%m-%d"),
|
||||
result.release_count,
|
||||
result.ticker_count,
|
||||
result.db_path
|
||||
terminal_safe(&result.db_path)
|
||||
);
|
||||
}
|
||||
snapshot::OwnershipSyncAction::Refreshed => {
|
||||
println!(
|
||||
"Refreshed ownership snapshot {} in {}.",
|
||||
result.latest_as_of_date.format("%Y-%m-%d"),
|
||||
result.db_path
|
||||
terminal_safe(&result.db_path)
|
||||
);
|
||||
}
|
||||
snapshot::OwnershipSyncAction::NoChange
|
||||
|
|
@ -318,11 +318,11 @@ fn handle_ticker(
|
|||
println!(
|
||||
"{} {} KSEI={} Bing={}",
|
||||
"Ownership:".bold(),
|
||||
data.ticker.code.bold(),
|
||||
terminal_safe(&data.ticker.code).bold(),
|
||||
data.ksei_as_of
|
||||
.map(|d| d.format("%Y-%m-%d").to_string())
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
data.bing_as_of.unwrap_or_else(|| "-".to_string())
|
||||
terminal_safe(&data.bing_as_of.unwrap_or_else(|| "-".to_string()))
|
||||
);
|
||||
|
||||
let mut table = Table::new();
|
||||
|
|
|
|||
|
|
@ -211,7 +211,8 @@ pub fn render_screener(
|
|||
|
||||
pub fn emit_error(err: &IdxError, format: &OutputFormat) {
|
||||
if !format.is_json() {
|
||||
eprintln!("Error: {err}");
|
||||
// Errors can carry provider, KSEI, or manifest text.
|
||||
eprintln!("Error: {}", text::terminal_safe(&err.to_string()));
|
||||
return;
|
||||
}
|
||||
let payload = serde_json::json!({
|
||||
|
|
|
|||
|
|
@ -135,7 +135,8 @@ pub fn print_technical(report: &TechnicalReport, no_color: bool) -> Result<(), I
|
|||
"{}",
|
||||
format!(
|
||||
"Technical Analysis for {} ({})",
|
||||
report.symbol, report.as_of
|
||||
terminal_safe(&report.symbol),
|
||||
report.as_of
|
||||
)
|
||||
.bold()
|
||||
);
|
||||
|
|
@ -363,7 +364,7 @@ pub fn print_compare(reports: &[FundamentalReport], no_color: bool) -> Result<()
|
|||
"Symbol",
|
||||
reports
|
||||
.iter()
|
||||
.map(|report| report.symbol.clone())
|
||||
.map(|report| terminal_safe(&report.symbol))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
add_compare_row(
|
||||
|
|
|
|||
|
|
@ -1,24 +1,8 @@
|
|||
//! Text from providers and KSEI files is untrusted: before it reaches a
|
||||
//! terminal, control characters (which can start escape sequences that clear
|
||||
//! the screen, rewrite output, or set the window title) and bidirectional
|
||||
//! overrides (which can reorder what is displayed) are replaced.
|
||||
//! Terminal-safe table cells; see [`idx_core::text`].
|
||||
|
||||
use comfy_table::Cell;
|
||||
|
||||
/// `text` with control characters other than newline, and bidirectional
|
||||
/// override characters, replaced by U+FFFD.
|
||||
pub fn terminal_safe(text: &str) -> String {
|
||||
text.chars()
|
||||
.map(|ch| {
|
||||
let bidi = matches!(ch, '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}');
|
||||
if (ch.is_control() && ch != '\n') || bidi {
|
||||
'\u{FFFD}'
|
||||
} else {
|
||||
ch
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
pub use idx_core::text::terminal_safe;
|
||||
|
||||
/// A table cell whose content is made [`terminal_safe`].
|
||||
pub fn cell(value: impl ToString) -> Cell {
|
||||
|
|
@ -42,26 +26,7 @@ pub fn truncate_chars(text: &str, max: usize) -> String {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{painted, terminal_safe, truncate_chars};
|
||||
|
||||
#[test]
|
||||
fn escape_sequences_and_bidi_overrides_are_neutralized() {
|
||||
assert_eq!(
|
||||
terminal_safe("\u{1b}[2J\u{1b}[Hfake"),
|
||||
"\u{FFFD}[2J\u{FFFD}[Hfake"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal_safe("\u{1b}]0;title\u{7}x"),
|
||||
"\u{FFFD}]0;title\u{FFFD}x"
|
||||
);
|
||||
assert_eq!(terminal_safe("a\u{9b}31mb"), "a\u{FFFD}31mb");
|
||||
assert_eq!(terminal_safe("ab\u{202E}cd"), "ab\u{FFFD}cd");
|
||||
assert_eq!(terminal_safe("line one\nline two"), "line one\nline two");
|
||||
assert_eq!(
|
||||
terminal_safe("Saham Grup Djarum’s 14,81%"),
|
||||
"Saham Grup Djarum’s 14,81%"
|
||||
);
|
||||
}
|
||||
use super::{painted, truncate_chars};
|
||||
|
||||
#[test]
|
||||
fn painted_cells_keep_idx_color_codes() {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ pub mod investor;
|
|||
pub mod market;
|
||||
#[cfg(feature = "ownership")]
|
||||
pub mod ownership;
|
||||
pub mod text;
|
||||
|
||||
pub use client::{CachePolicy, Fetched, Idx};
|
||||
|
||||
|
|
@ -47,7 +48,10 @@ pub mod runtime {
|
|||
let line = serde_json::json!({ "level": level, "message": message.to_string() });
|
||||
eprintln!("{line}");
|
||||
} else {
|
||||
eprintln!("{level}: {message}");
|
||||
eprintln!(
|
||||
"{level}: {}",
|
||||
crate::text::terminal_safe(&message.to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
44
crates/idx-core/src/text.rs
Normal file
44
crates/idx-core/src/text.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
//! Text from providers, KSEI files, and snapshot manifests is untrusted.
|
||||
//! Before it reaches a terminal, control characters (which can start escape
|
||||
//! sequences that clear the screen, rewrite output, or set the window title)
|
||||
//! and bidirectional overrides (which can reorder what is displayed) are
|
||||
//! replaced.
|
||||
|
||||
/// `text` with control characters other than newline, and bidirectional
|
||||
/// override characters, replaced by U+FFFD.
|
||||
pub fn terminal_safe(text: &str) -> String {
|
||||
text.chars()
|
||||
.map(|ch| {
|
||||
let bidi = matches!(ch, '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}');
|
||||
if (ch.is_control() && ch != '\n') || bidi {
|
||||
'\u{FFFD}'
|
||||
} else {
|
||||
ch
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::terminal_safe;
|
||||
|
||||
#[test]
|
||||
fn escape_sequences_and_bidi_overrides_are_neutralized() {
|
||||
assert_eq!(
|
||||
terminal_safe("\u{1b}[2J\u{1b}[Hfake"),
|
||||
"\u{FFFD}[2J\u{FFFD}[Hfake"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal_safe("\u{1b}]0;title\u{7}x"),
|
||||
"\u{FFFD}]0;title\u{FFFD}x"
|
||||
);
|
||||
assert_eq!(terminal_safe("a\u{9b}31mb"), "a\u{FFFD}31mb");
|
||||
assert_eq!(terminal_safe("ab\u{202E}cd"), "ab\u{FFFD}cd");
|
||||
assert_eq!(terminal_safe("line one\nline two"), "line one\nline two");
|
||||
assert_eq!(
|
||||
terminal_safe("Saham Grup Djarum’s 14,81%"),
|
||||
"Saham Grup Djarum’s 14,81%"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue