|
1 | 1 | //! History API
|
2 | 2 |
|
| 3 | +use log::warn; |
3 | 4 | use std::collections::vec_deque;
|
4 | 5 | use std::collections::VecDeque;
|
5 | 6 | use std::fs::File;
|
@@ -141,7 +142,18 @@ impl History {
|
141 | 142 | wtr.write_all(Self::FILE_VERSION_V2.as_bytes())?;
|
142 | 143 | for entry in &self.entries {
|
143 | 144 | 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 \ |
145 | 157 | }
|
146 | 158 | wtr.write_all(b"\n")?;
|
147 | 159 | // https://github.com/rust-lang/rust/issues/32677#issuecomment-204833485
|
@@ -169,14 +181,46 @@ impl History {
|
169 | 181 | }
|
170 | 182 | }
|
171 | 183 | 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?; |
177 | 185 | if line.is_empty() {
|
178 | 186 | continue;
|
179 | 187 | }
|
| 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 | + } |
180 | 224 | self.add(line); // TODO truncate to MAX_LINE
|
181 | 225 | }
|
182 | 226 | Ok(())
|
@@ -349,6 +393,12 @@ mod tests {
|
349 | 393 | check_save("line\nfour \\ abc")
|
350 | 394 | }
|
351 | 395 |
|
| 396 | + #[test] |
| 397 | + fn save_windows_path() -> Result<()> { |
| 398 | + let path = "cd source\\repos\\forks\\nushell\\"; |
| 399 | + check_save(path) |
| 400 | + } |
| 401 | + |
352 | 402 | #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled
|
353 | 403 | fn check_save(line: &str) -> Result<()> {
|
354 | 404 | let mut history = init();
|
|
0 commit comments