diff options
| author | Brian Cosgrove <cosgroveb@gmail.com> | 2025-06-11 13:12:20 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-11 19:12:20 +0100 |
| commit | 306f5e1104dbf707cf00ea1f0dbd9c390752f7ac (patch) | |
| tree | 3f570c8021947c4af2814bf2cc59cede401606e9 | |
| parent | fix: `atuin.nu` enchancements (#2778) (diff) | |
| download | atuin-306f5e1104dbf707cf00ea1f0dbd9c390752f7ac.zip | |
fix(search): prevent panic on malformed format strings (#2776) (#2777)
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Diffstat (limited to '')
| -rw-r--r-- | crates/atuin/src/command/client/history.rs | 65 |
1 files 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( |
