Skip to content

Commit 7fe1925

Browse files
authored
Merge pull request #412 from gwenn/escape_history_fix
Fix History::load
2 parents 03f981e + 441ed1b commit 7fe1925

File tree

1 file changed

+56
-6
lines changed

1 file changed

+56
-6
lines changed

src/history.rs

+56-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! History API
22
3+
use log::warn;
34
use std::collections::vec_deque;
45
use std::collections::VecDeque;
56
use std::fs::File;
@@ -141,7 +142,18 @@ impl History {
141142
wtr.write_all(Self::FILE_VERSION_V2.as_bytes())?;
142143
for entry in &self.entries {
143144
wtr.write_all(b"\n")?;
144-
wtr.write_all(entry.replace('\\', "\\\\").replace('\n', "\\n").as_bytes())?;
145+
let mut bytes = entry.as_bytes();
146+
while let Some(i) = memchr::memchr2(b'\\', b'\n', bytes) {
147+
wtr.write_all(&bytes[..i])?;
148+
if bytes[i] == b'\n' {
149+
wtr.write_all(b"\\n")?; // escaped line feed
150+
} else {
151+
debug_assert_eq!(bytes[i], b'\\');
152+
wtr.write_all(b"\\\\")?; // escaped backslash
153+
}
154+
bytes = &bytes[i + 1..];
155+
}
156+
wtr.write_all(bytes)?; // remaining bytes with no \n or \
145157
}
146158
wtr.write_all(b"\n")?;
147159
// https://github.com/rust-lang/rust/issues/32677#issuecomment-204833485
@@ -169,14 +181,46 @@ impl History {
169181
}
170182
}
171183
for line in lines {
172-
let line = if v2 {
173-
line?.replace("\\n", "\n").replace("\\\\", "\\")
174-
} else {
175-
line?
176-
};
184+
let mut line = line?;
177185
if line.is_empty() {
178186
continue;
179187
}
188+
if v2 {
189+
let mut copy = None; // lazily copy line if unescaping is needed
190+
let mut str = line.as_str();
191+
while let Some(i) = str.find('\\') {
192+
if copy.is_none() {
193+
copy = Some(String::with_capacity(line.len()));
194+
}
195+
let s = copy.as_mut().unwrap();
196+
s.push_str(&str[..i]);
197+
let j = i + 1; // escaped char idx
198+
let b = if j < str.len() {
199+
str.as_bytes()[j]
200+
} else {
201+
0 // unexpected if History::save works properly
202+
};
203+
match b {
204+
b'n' => {
205+
s.push('\n'); // unescaped line feed
206+
}
207+
b'\\' => {
208+
s.push('\\'); // unescaped back slash
209+
}
210+
_ => {
211+
// only line feed and back slash should have been escaped
212+
warn!(target: "rustyline", "bad escaped line: {}", line);
213+
copy = None;
214+
break;
215+
}
216+
}
217+
str = &str[j + 1..];
218+
}
219+
if let Some(mut s) = copy {
220+
s.push_str(str); // remaining bytes with no escaped char
221+
line = s;
222+
}
223+
}
180224
self.add(line); // TODO truncate to MAX_LINE
181225
}
182226
Ok(())
@@ -349,6 +393,12 @@ mod tests {
349393
check_save("line\nfour \\ abc")
350394
}
351395

396+
#[test]
397+
fn save_windows_path() -> Result<()> {
398+
let path = "cd source\\repos\\forks\\nushell\\";
399+
check_save(path)
400+
}
401+
352402
#[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled
353403
fn check_save(line: &str) -> Result<()> {
354404
let mut history = init();

0 commit comments

Comments
 (0)