From 306f5e1104dbf707cf00ea1f0dbd9c390752f7ac Mon Sep 17 00:00:00 2001 From: Brian Cosgrove Date: Wed, 11 Jun 2025 13:12:20 -0500 Subject: fix(search): prevent panic on malformed format strings (#2776) (#2777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(search): prevent panic on malformed format strings (#2776) - Wrap format operations in panic catcher for graceful error handling - Improve error messages with context-aware guidance for common issues - Let runtime-format parser handle validation to avoid blocking valid formats Fixes crash when using malformed format strings by catching formatting errors gracefully and providing actionable guidance without restricting legitimate format patterns like {command} or {time}. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Satisfy cargo fmt * test(search): add regression tests for format string panic (#2776) - Add test for malformed JSON format strings that previously caused panics - Add test to ensure valid format strings continue to work - Prevent future regressions of the format string panic issue 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- crates/atuin/src/command/client/history.rs | 65 +++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/crates/atuin/src/command/client/history.rs b/crates/atuin/src/command/client/history.rs index 80d40f58..d4ed22f6 100644 --- a/crates/atuin/src/command/client/history.rs +++ b/crates/atuin/src/command/client/history.rs @@ -197,12 +197,38 @@ pub fn print_list( tz: &tz, }; let args = parsed_fmt.with_args(&fh); - let write = write!(w, "{args}{entry_terminator}"); + + // Check for formatting errors before attempting to write if let Err(err) = args.status() { eprintln!("ERROR: history output failed with: {err}"); std::process::exit(1); } - check_for_write_errors(write); + + let write_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + write!(w, "{args}{entry_terminator}") + })); + + match write_result { + Ok(Ok(())) => { + // Write succeeded + } + Ok(Err(err)) => { + if err.kind() != io::ErrorKind::BrokenPipe { + eprintln!("ERROR: Failed to write history output: {err}"); + std::process::exit(1); + } + } + Err(_) => { + eprintln!("ERROR: Format string caused a formatting error."); + eprintln!( + "This may be due to an unsupported format string containing special characters." + ); + eprintln!( + "Please check your format string syntax and ensure literal braces are properly escaped." + ); + std::process::exit(1); + } + } if flush_each_line { check_for_write_errors(w.flush()); } @@ -300,14 +326,43 @@ fn parse_fmt(format: &str) -> ParsedFmt { Ok(fmt) => fmt, Err(err) => { eprintln!("ERROR: History formatting failed with the following error: {err}"); - println!( - "If your formatting string contains curly braces (eg: {{var}}) you need to escape them this way: {{{{var}}." - ); + + if format.contains('"') && (format.contains(":{") || format.contains(",{")) { + eprintln!("It looks like you're trying to create JSON output."); + eprintln!("For JSON, you need to escape literal braces by doubling them:"); + eprintln!("Example: '{{\"command\":\"{{command}}\",\"time\":\"{{time}}\"}}'"); + } else { + eprintln!( + "If your formatting string contains literal curly braces, you need to escape them by doubling:" + ); + eprintln!("Use {{{{ for literal {{ and }}}} for literal }}"); + } std::process::exit(1) } } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_string_no_panic() { + // Don't panic but provide helpful output (issue #2776) + let malformed_json = r#"{"command":"{command}","key":"value"}"#; + + let result = std::panic::catch_unwind(|| parse_fmt(malformed_json)); + + assert!(result.is_ok()); + } + + #[test] + fn test_valid_formats_still_work() { + assert!(std::panic::catch_unwind(|| parse_fmt("{command}")).is_ok()); + assert!(std::panic::catch_unwind(|| parse_fmt("{time} - {command}")).is_ok()); + } +} + impl Cmd { #[allow(clippy::too_many_lines, clippy::cast_possible_truncation)] async fn handle_start( -- cgit v1.3.1