Skip to content

Commit c5add15

Browse files
authored
fix: don't fail importing unreadable history lines (reubeno#710)
Don't treat unparseable lines in history files as fatal.
1 parent 1f821c0 commit c5add15

3 files changed

Lines changed: 39 additions & 9 deletions

File tree

brush-core/src/history.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ pub struct History {
2222

2323
impl History {
2424
/// Constructs a new `History` instance, with its contents initialized from the given readable
25-
/// stream.
25+
/// stream. If errors are encountered reading lines from the stream, unreadable lines will
26+
/// be skipped but the call will still return successfully, with a warning logged. An error
27+
/// result will be returned only if an internal error occurs updating the history.
2628
///
2729
/// # Arguments
2830
///
@@ -33,9 +35,18 @@ impl History {
3335
let buf_reader = std::io::BufReader::new(reader);
3436

3537
let mut next_timestamp = None;
36-
for line in buf_reader.lines() {
37-
let line = line?;
38+
for line_result in buf_reader.lines() {
39+
// If we couldn't decode the line (perhaps it wasn't valid UTF8?), skip it and make
40+
// a best-effort attempt to proceed on. We'll later warn the user.
41+
let line = match line_result {
42+
Ok(line) => line,
43+
Err(err) => {
44+
tracing::warn!("unreadable history line; {err}");
45+
continue;
46+
}
47+
};
3848

49+
// Look for timestamp comments; ignore other comment lines.
3950
if let Some(comment) = line.strip_prefix("#") {
4051
if let Ok(seconds_since_epoch) = comment.trim().parse() {
4152
next_timestamp =

brush-shell/tests/cases/builtins/history.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,9 @@ cases:
185185
: cmd1
186186
: cmd2
187187
history
188+
189+
- name: "history file with unreadable lines"
190+
ignore_stderr: true # Ignore brush's warnings about unreadable lines
191+
stdin: |
192+
bash -c 'printf "\xff\xfe\xfd" >history-file.txt'
193+
HISTFILE=history-file.txt $0 -o history -c 'echo hi'

brush-shell/tests/compat_tests.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use clap::Parser;
1010
use colored::Colorize;
1111
use descape::UnescapeExt;
1212
use serde::{Deserialize, Serialize};
13-
use std::fs;
1413
#[cfg(unix)]
1514
use std::os::unix::{fs::PermissionsExt, process::ExitStatusExt};
1615
use std::{
@@ -19,6 +18,7 @@ use std::{
1918
path::{Path, PathBuf},
2019
process::ExitStatus,
2120
};
21+
use std::{fs, io::Read};
2222

2323
#[derive(Clone)]
2424
struct ShellConfig {
@@ -1375,15 +1375,28 @@ fn diff_dirs(oracle_path: &Path, test_path: &Path) -> Result<DirComparison> {
13751375
let test_file_path = test_path.join(filename);
13761376

13771377
if file_type.is_file() {
1378-
let oracle_contents = fs::read_to_string(&oracle_file_path)?;
1379-
let test_contents = fs::read_to_string(&test_file_path)?;
1378+
let mut oracle_file = std::fs::OpenOptions::new()
1379+
.read(true)
1380+
.open(&oracle_file_path)?;
1381+
let mut oracle_bytes = vec![];
1382+
oracle_file.read_to_end(&mut oracle_bytes)?;
1383+
1384+
let mut test_file = std::fs::OpenOptions::new()
1385+
.read(true)
1386+
.open(&test_file_path)?;
1387+
let mut test_bytes = vec![];
1388+
test_file.read_to_end(&mut test_bytes)?;
1389+
1390+
if oracle_bytes != test_bytes {
1391+
// Convert using lossy conversion to avoid issues with invalid UTF-8.
1392+
let oracle_display_text = String::from_utf8_lossy(&oracle_bytes);
1393+
let test_display_text = String::from_utf8_lossy(&test_bytes);
13801394

1381-
if oracle_contents != test_contents {
13821395
entries.push(DirComparisonEntry::Different(
13831396
oracle_file_path,
1384-
oracle_contents,
1397+
oracle_display_text.to_string(),
13851398
test_file_path,
1386-
test_contents,
1399+
test_display_text.to_string(),
13871400
));
13881401
}
13891402
} else if file_type.is_dir() {

0 commit comments

Comments
 (0)