Skip to content

Commit 8ce78f9

Browse files
committed
csv: handle escaped delimiters with QUOTE_NONE
csv_core applies escapechar while reading quoted fields, but its unquoted field state treats an escaped delimiter as a delimiter. Consequently, QUOTE_NONE with escapechar split 'abc\,def' into 'abc\' and 'def'. Add a small QUOTE_NONE record parser for dialects with escapechar. It keeps the byte following an escape character as field data, so escaped delimiters do not end a field while ordinary delimiters and record terminators retain their usual meaning. Unmark TestEscapedExcel.test_read_escape_fieldsep. Assisted-by: Codex
1 parent abb4bd0 commit 8ce78f9

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

Lib/test/test_csv.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,6 @@ class TestEscapedExcel(TestCsvBase):
857857
def test_escape_fieldsep(self):
858858
self.writerAssertEqual([['abc,def']], 'abc\\,def\r\n')
859859

860-
@unittest.expectedFailure # TODO: RUSTPYTHON
861860
def test_read_escape_fieldsep(self):
862861
self.readerAssertEqual('abc\\,def\r\n', [['abc,def']])
863862

crates/stdlib/src/csv.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,61 @@ mod _csv {
975975

976976
impl SelfIter for Reader {}
977977

978+
fn read_quote_none_record(
979+
input: &[u8],
980+
dialect: PyDialect,
981+
field_limit: isize,
982+
vm: &VirtualMachine,
983+
) -> PyResult<Vec<PyObjectRef>> {
984+
let mut fields = vec![Vec::new()];
985+
let mut escaped = false;
986+
987+
for (index, &byte) in input.iter().enumerate() {
988+
if escaped {
989+
fields.last_mut().unwrap().push(byte);
990+
escaped = false;
991+
} else if dialect.escapechar == Some(byte) {
992+
escaped = true;
993+
} else if byte == dialect.delimiter {
994+
fields.push(Vec::new());
995+
} else if matches!(byte, b'\r' | b'\n') {
996+
if !input[index..]
997+
.iter()
998+
.all(|&byte| matches!(byte, b'\r' | b'\n'))
999+
{
1000+
return Err(new_csv_error(
1001+
vm,
1002+
concat!(
1003+
"new-line character seen in unquoted field",
1004+
" - do you need to open the file in universal-newline mode?"
1005+
),
1006+
));
1007+
}
1008+
break;
1009+
} else {
1010+
fields.last_mut().unwrap().push(byte);
1011+
}
1012+
}
1013+
1014+
// CPython treats an escape character at the end of an iterator item
1015+
// as escaping the implicit newline at the end of that item.
1016+
if escaped {
1017+
fields.last_mut().unwrap().push(b'\n');
1018+
}
1019+
1020+
fields
1021+
.into_iter()
1022+
.map(|field| {
1023+
if field.len() > field_limit as usize {
1024+
return Err(new_csv_error(vm, "filed too long to read"));
1025+
}
1026+
let field = core::str::from_utf8(&field)
1027+
.map_err(|_| vm.new_unicode_decode_error("csv not utf8"))?;
1028+
Ok(vm.ctx.new_str(field).into())
1029+
})
1030+
.collect()
1031+
}
1032+
9781033
impl IterNext for Reader {
9791034
fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
9801035
let string = raise_if_stop!(zelf.iter.next(vm)?);
@@ -1006,6 +1061,12 @@ mod _csv {
10061061
let mut output_ends_offset = 0;
10071062
let field_limit = GLOBAL_FIELD_LIMIT.lock().to_owned();
10081063

1064+
if zelf.dialect.quoting == QuoteStyle::None && zelf.dialect.escapechar.is_some() {
1065+
let out = read_quote_none_record(input, zelf.dialect, field_limit, vm)?;
1066+
*line_num += 1;
1067+
return Ok(PyIterReturn::Return(vm.ctx.new_list(out).into()));
1068+
}
1069+
10091070
#[inline]
10101071
fn trim_spaces(input: &[u8]) -> &[u8] {
10111072
let trimmed_start = input.iter().position(|&x| x != b' ').unwrap_or(input.len());

0 commit comments

Comments
 (0)