Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 55 additions & 18 deletions csv-core/src/writer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use core::fmt;
use core::str;

use memchr::memchr;
use memchr::{memchr, memchr2};

use crate::{QuoteStyle, Terminator};

Expand Down Expand Up @@ -547,38 +547,46 @@ pub fn quote(
) -> (WriteResult, usize, usize) {
let (mut nin, mut nout) = (0, 0);
loop {
match memchr(quote, input) {
// Find the next byte that must be escaped. When double quoting is
// enabled, only the quote byte needs escaping (by doubling it). When
// it is disabled, the escape byte itself must also be escaped (by
// preceding it with another escape byte); otherwise an escape byte
// occurring in the field data would be indistinguishable from the
// start of an escape sequence when the data is read back.
let next = if double_quote {
memchr(quote, input)
} else {
memchr2(quote, escape, input)
};
match next {
None => {
let (res, i, o) = write_optimistic(input, output);
nin += i;
nout += o;
return (res, nin, nout);
}
Some(next_quote) => {
let (res, i, o) =
write_optimistic(&input[..next_quote], output);
Some(next) => {
let (res, i, o) = write_optimistic(&input[..next], output);
input = &input[i..];
output = &mut moving(output)[o..];
nin += i;
nout += o;
if let WriteResult::OutputFull = res {
return (res, nin, nout);
}
if double_quote {
let (res, o) = write_pessimistic(&[quote, quote], output);
if let WriteResult::OutputFull = res {
return (res, nin, nout);
}
nout += o;
output = &mut moving(output)[o..];
let escaped = if double_quote {
[quote, quote]
} else {
let (res, o) = write_pessimistic(&[escape, quote], output);
if let WriteResult::OutputFull = res {
return (res, nin, nout);
}
nout += o;
output = &mut moving(output)[o..];
// `input[0]` is the byte we found: either the quote byte or
// the escape byte. Both are escaped with the escape byte.
[escape, input[0]]
};
let (res, o) = write_pessimistic(&escaped, output);
if let WriteResult::OutputFull = res {
return (res, nin, nout);
}
nout += o;
output = &mut moving(output)[o..];
nin += 1;
input = &input[1..];
}
Expand Down Expand Up @@ -1014,6 +1022,35 @@ mod tests {
assert_quote!(inp, out, 2, 4, InputEmpty, r#"\"\""#, false);
}

// When double quoting is disabled, the escape byte itself must be escaped
// (by doubling it), otherwise it could not be distinguished from the start
// of an escape sequence when the data is read back.
#[test]
fn quote_escaped_escape_one() {
let inp = b(r#"\"#);
let out = &mut [0; 1024];

assert_quote!(inp, out, 1, 2, InputEmpty, r#"\\"#, false);
}

#[test]
fn quote_escaped_escape_and_quote() {
let inp = b(r#"\""#);
let out = &mut [0; 1024];

assert_quote!(inp, out, 2, 4, InputEmpty, r#"\\\""#, false);
}

// With double quoting enabled (the default), the escape byte is not special
// and must be written verbatim.
#[test]
fn quote_double_quote_leaves_escape_byte() {
let inp = b(r#"a\b"#);
let out = &mut [0; 1024];

assert_quote!(inp, out, 3, 3, InputEmpty, r#"a\b"#, true);
}

#[test]
fn quote_misc() {
let inp = b(r#"foo "bar" baz "quux"?"#);
Expand Down
36 changes: 36 additions & 0 deletions src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,42 @@ mod tests {
assert_eq!(wtr_as_string(wtr), "\"\"\n\"\"\n");
}

// Regression test: when `double_quote` is disabled, the writer escapes
// quotes with the escape character. It must also escape the escape
// character itself, otherwise fields containing it (e.g. Windows paths
// like `C:\path`) are silently corrupted when read back.
#[test]
fn escape_escape_char_when_not_double_quoting() {
let mut wtr =
WriterBuilder::new().double_quote(false).from_writer(vec![]);
wtr.write_record([r#"C:\path"#, "END"]).unwrap();

// The backslash is doubled so the reader can recover it.
assert_eq!(wtr_as_string(wtr), "\"C:\\\\path\",END\n");
}

#[test]
fn roundtrip_escape_char_when_not_double_quoting() {
use crate::ReaderBuilder;

let mut wtr =
WriterBuilder::new().double_quote(false).from_writer(vec![]);
let record = [r#"a\b"#, r#"C:\path\to"#, r#"\"#, "plain"];
wtr.write_record(record).unwrap();
let data = wtr.into_inner().unwrap();

let mut rdr = ReaderBuilder::new()
.has_headers(false)
.double_quote(false)
.escape(Some(b'\\'))
.from_reader(&*data);
let got = rdr.records().next().unwrap().unwrap();
assert_eq!(
got.iter().collect::<Vec<_>>(),
vec![r#"a\b"#, r#"C:\path\to"#, r#"\"#, "plain"],
);
}

#[test]
fn unequal_records_bad() {
let mut wtr = WriterBuilder::new().from_writer(vec![]);
Expand Down
Loading