use std::cell;
use std::fmt;
use std::io::{self, BufRead};
use std::path::Path;
use serde::de;
use serde::{Deserialize as _, Deserializer as _};
use serde_json::Value;
use super::config::{Index, Schema, TableFormat};
use super::{Cell, Table, TableError};
const QUOTE: u8 = b'"';
const DELIMITER: u8 = b',';
const BOM: &str = "\u{feff}";
type Record = (usize, Vec<String>);
const CELL_OVERHEAD: usize = size_of::<Cell>();
const ROW_OVERHEAD: usize = 4 * size_of::<usize>();
#[derive(Debug)]
pub(super) struct Parsed {
pub(super) columns: Vec<String>,
pub(super) rows: Vec<Vec<Cell>>,
pub(super) held: u64,
}
pub(super) fn read(
reader: impl BufRead,
format: TableFormat,
schema: &Schema,
ceiling: u64,
) -> Result<Parsed, TableError> {
match format {
TableFormat::Csv { header } => read_csv(reader, header, schema, ceiling),
TableFormat::Json => read_json(reader, schema, ceiling),
}
}
pub(crate) fn probe(
path: &Path,
format: TableFormat,
schema: &Schema,
index: &Index,
ceiling: u64,
) -> Result<(), TableError> {
Table::read_file(path, format, schema, index, ceiling).map(drop)
}
fn read_csv(
reader: impl BufRead,
header: bool,
schema: &Schema,
ceiling: u64,
) -> Result<Parsed, TableError> {
let (records, held) = records(reader, ceiling)?;
let mut records = records.into_iter();
let columns = match (header, schema) {
(true, Schema::Auto) => records.next().ok_or(TableError::Empty)?.1,
(true, Schema::Named(named)) => {
records.next().ok_or(TableError::Empty)?;
named.clone()
}
(false, Schema::Auto) => return Err(TableError::NamesRequired),
(false, Schema::Named(named)) => named.clone(),
};
if columns.is_empty() {
return Err(TableError::NoNames);
}
let mut rows = Vec::new();
for (line, fields) in records {
if fields.len() != columns.len() {
return Err(TableError::Malformed {
line,
detail: format!("{} fields against {} columns", fields.len(), columns.len()),
});
}
rows.push(fields.into_iter().map(cell_of_text).collect());
}
if rows.is_empty() {
return Err(TableError::Empty);
}
Ok(Parsed {
columns,
rows,
held,
})
}
fn read_json(reader: impl BufRead, schema: &Schema, ceiling: u64) -> Result<Parsed, TableError> {
let meter = Meter::default();
let mut document = serde_json::Deserializer::from_reader(Metered {
inner: reader,
meter: &meter,
});
let mut stopped = None;
let read = document.deserialize_seq(Elements {
schema,
ceiling,
meter: &meter,
stopped: &mut stopped,
});
if let Some(e) = stopped {
return Err(e);
}
let parsed = read.map_err(|e| not_an_array(&e))?;
document.end().map_err(|e| not_an_array(&e))?;
Ok(parsed)
}
fn not_an_array(e: &serde_json::Error) -> TableError {
TableError::NotAnArrayOfObjects {
detail: e.to_string(),
}
}
#[derive(Default)]
struct Meter {
room: cell::Cell<Option<u64>>,
tripped: cell::Cell<bool>,
}
struct Metered<'m, R> {
inner: R,
meter: &'m Meter,
}
impl<R: io::Read> io::Read for Metered<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let Some(room) = self.meter.room.get() else {
return self.inner.read(buf);
};
if room == 0 {
self.meter.tripped.set(true);
return Err(io::Error::other("an element outgrew the resident ceiling"));
}
let take = buf.len().min(usize::try_from(room).unwrap_or(usize::MAX));
let read = self.inner.read(&mut buf[..take])?;
self.meter
.room
.set(Some(room - u64::try_from(read).unwrap_or(room)));
Ok(read)
}
}
struct Element<'m> {
meter: &'m Meter,
room: u64,
}
impl<'de> de::DeserializeSeed<'de> for Element<'_> {
type Value = Value;
fn deserialize<D: de::Deserializer<'de>>(self, element: D) -> Result<Value, D::Error> {
self.meter.room.set(Some(self.room));
let value = Value::deserialize(element);
self.meter.room.set(None);
value
}
}
fn stop<E: de::Error>(kept: &mut Option<TableError>, reason: TableError) -> E {
let text = reason.to_string();
*kept = Some(reason);
E::custom(text)
}
struct Elements<'a> {
schema: &'a Schema,
ceiling: u64,
meter: &'a Meter,
stopped: &'a mut Option<TableError>,
}
impl<'de> de::Visitor<'de> for Elements<'_> {
type Value = Parsed;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an array of objects")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let stated = matches!(self.schema, Schema::Named(_));
let mut columns: Vec<String> = match self.schema {
Schema::Auto => Vec::new(),
Schema::Named(named) => named.clone(),
};
let mut rows: Vec<Vec<Cell>> = Vec::new();
let mut held: u64 = 0;
let mut seen = 0usize;
loop {
let element = Element {
meter: self.meter,
room: self.ceiling.saturating_sub(held),
};
let value = match seq.next_element_seed(element) {
Ok(Some(value)) => value,
Ok(None) => break,
Err(_) if self.meter.tripped.get() => {
return Err(stop(
self.stopped,
TableError::OverResidentCeiling {
ceiling: self.ceiling,
},
));
}
Err(e) => return Err(e),
};
let Some(object) = value.as_object() else {
return Err(stop(
self.stopped,
TableError::NotAnArrayOfObjects {
detail: format!("element {seen} is not an object"),
},
));
};
if !stated {
for key in object.keys() {
let Err(at) = columns.binary_search(key) else {
continue;
};
if columns.len() >= MAX_FIELDS {
return Err(stop(
self.stopped,
TableError::NotAnArrayOfObjects {
detail: format!("the objects carry more than {MAX_FIELDS} keys"),
},
));
}
columns.insert(at, key.clone());
for row in &mut rows {
row.insert(at, None);
}
held = held.saturating_add(widened(rows.len()));
}
}
let row: Vec<Cell> = columns
.iter()
.map(|column| cell_of_value(object.get(column)))
.collect();
held = held.saturating_add(footprint(&row));
if held > self.ceiling {
return Err(stop(
self.stopped,
TableError::OverResidentCeiling {
ceiling: self.ceiling,
},
));
}
rows.push(row);
seen += 1;
}
if seen == 0 {
return Err(stop(self.stopped, TableError::Empty));
}
if columns.is_empty() {
return Err(stop(self.stopped, TableError::NoNames));
}
Ok(Parsed {
columns,
rows,
held,
})
}
}
fn widened(rows: usize) -> u64 {
u64::try_from(rows * CELL_OVERHEAD).unwrap_or(u64::MAX)
}
fn footprint(row: &[Cell]) -> u64 {
let cells: usize = row
.iter()
.map(|cell| cell.as_deref().map_or(0, str::len) + CELL_OVERHEAD)
.sum();
u64::try_from(cells + ROW_OVERHEAD).unwrap_or(u64::MAX)
}
fn cell_of_text(text: String) -> Cell {
if text.is_empty() {
None
} else {
Some(text.into_boxed_str())
}
}
fn cell_of_value(value: Option<&Value>) -> Cell {
match value {
None | Some(Value::Null) => None,
Some(Value::String(text)) => cell_of_text(text.clone()),
Some(other) => Some(other.to_string().into_boxed_str()),
}
}
fn records(mut reader: impl BufRead, ceiling: u64) -> Result<(Vec<Record>, u64), TableError> {
let mut csv = Csv::new(ceiling);
loop {
let chunk = reader.fill_buf()?;
if chunk.is_empty() {
break;
}
let consumed = chunk.len();
for &byte in chunk {
csv.byte(byte)?;
}
reader.consume(consumed);
}
let (records, held) = csv.finish()?;
Ok((strip_bom(records), held))
}
fn strip_bom(mut records: Vec<Record>) -> Vec<Record> {
if let Some((_, fields)) = records.first_mut()
&& let Some(first) = fields.first_mut()
&& first.starts_with(BOM)
{
first.drain(..BOM.len());
}
records
}
#[derive(Clone, Copy, Default, PartialEq, Eq)]
enum State {
#[default]
FieldStart,
Unquoted,
Quoted,
QuoteInQuoted,
}
struct Csv {
records: Vec<Record>,
fields: Vec<String>,
field: Vec<u8>,
state: State,
line: usize,
record_line: usize,
started: bool,
after_cr: bool,
held: u64,
record: u64,
ceiling: u64,
}
const MAX_FIELD: usize = 8 * 1024 * 1024;
const MAX_FIELDS: usize = 4096;
impl Csv {
fn new(ceiling: u64) -> Self {
Self {
records: Vec::new(),
fields: Vec::new(),
field: Vec::new(),
state: State::FieldStart,
line: 1,
record_line: 1,
started: false,
after_cr: false,
held: 0,
record: 0,
ceiling,
}
}
fn charge(&self, open: usize) -> Result<(), TableError> {
let row = self
.record
.saturating_add(u64::try_from(ROW_OVERHEAD + open).unwrap_or(u64::MAX));
if self.held.saturating_add(row) > self.ceiling {
return Err(TableError::OverResidentCeiling {
ceiling: self.ceiling,
});
}
Ok(())
}
fn push(&mut self, byte: u8) -> Result<(), TableError> {
if self.field.len() >= MAX_FIELD {
return Err(TableError::Malformed {
line: self.record_line,
detail: format!("a field is longer than {MAX_FIELD} bytes"),
});
}
self.charge(self.field.len() + 1 + CELL_OVERHEAD)?;
self.field.push(byte);
Ok(())
}
fn byte(&mut self, byte: u8) -> Result<(), TableError> {
if self.state != State::Quoted {
if byte == b'\n' && std::mem::take(&mut self.after_cr) {
return Ok(());
}
self.after_cr = false;
}
match self.state {
State::FieldStart => match byte {
QUOTE => {
self.begin();
self.state = State::Quoted;
}
DELIMITER => {
self.begin();
self.end_field()?;
}
b'\n' => self.newline()?,
b'\r' => {
self.after_cr = true;
self.newline()?;
}
_ => {
self.begin();
self.push(byte)?;
self.state = State::Unquoted;
}
},
State::Unquoted => match byte {
DELIMITER => self.end_field()?,
b'\n' => self.newline()?,
b'\r' => {
self.after_cr = true;
self.newline()?;
}
_ => self.push(byte)?,
},
State::Quoted => match byte {
QUOTE => self.state = State::QuoteInQuoted,
b'\n' => {
self.line += 1;
self.push(byte)?;
}
_ => self.push(byte)?,
},
State::QuoteInQuoted => match byte {
QUOTE => {
self.push(QUOTE)?;
self.state = State::Quoted;
}
DELIMITER => self.end_field()?,
b'\n' => self.newline()?,
b'\r' => {
self.after_cr = true;
self.newline()?;
}
_ => {
return Err(TableError::Malformed {
line: self.record_line,
detail: "a closing quote is followed by more text".to_string(),
});
}
},
}
Ok(())
}
fn begin(&mut self) {
if !self.started {
self.started = true;
self.record_line = self.line;
}
}
fn end_field(&mut self) -> Result<(), TableError> {
if self.fields.len() >= MAX_FIELDS {
return Err(TableError::Malformed {
line: self.record_line,
detail: format!("a record carries more than {MAX_FIELDS} fields"),
});
}
let bytes = std::mem::take(&mut self.field);
let text = String::from_utf8(bytes).map_err(|_| TableError::Malformed {
line: self.record_line,
detail: "a field is not UTF-8".to_string(),
})?;
let cost = u64::try_from(text.len() + CELL_OVERHEAD).unwrap_or(u64::MAX);
self.record = self.record.saturating_add(cost);
self.fields.push(text);
self.state = State::FieldStart;
self.charge(0)
}
fn newline(&mut self) -> Result<(), TableError> {
self.line += 1;
if !self.started {
return Ok(());
}
self.end_record()
}
fn finish(mut self) -> Result<(Vec<Record>, u64), TableError> {
if self.state == State::Quoted {
return Err(TableError::Malformed {
line: self.record_line,
detail: "a quoted field is never closed".to_string(),
});
}
if self.started {
self.end_record()?;
}
Ok((self.records, self.held))
}
fn end_record(&mut self) -> Result<(), TableError> {
self.end_field()?;
let fields = std::mem::take(&mut self.fields);
let row = std::mem::take(&mut self.record)
.saturating_add(u64::try_from(ROW_OVERHEAD).unwrap_or(u64::MAX));
self.held = self.held.saturating_add(row);
self.records.push((self.record_line, fields));
self.started = false;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
fn cells(parsed: &Parsed) -> Vec<Vec<&str>> {
parsed
.rows
.iter()
.map(|row| {
row.iter()
.map(|cell| cell.as_deref().unwrap_or_default())
.collect()
})
.collect()
}
fn csv(body: &str) -> Result<Parsed, TableError> {
read(
body.as_bytes(),
TableFormat::Csv { header: true },
&Schema::Auto,
u64::MAX,
)
}
#[test]
fn a_header_names_the_columns() {
let parsed = csv("asn,name\n13335,CLOUDFLARENET\n15169,GOOGLE\n").unwrap();
assert_eq!(parsed.columns, ["asn", "name"]);
assert_eq!(
cells(&parsed),
[["13335", "CLOUDFLARENET"], ["15169", "GOOGLE"]]
);
}
#[test]
fn a_quoted_field_holds_the_delimiter_and_the_line_ending() {
let parsed = csv("asn,name\n13335,\"Cloudflare, Inc.\"\n15169,\"Google\nLLC\"\n").unwrap();
assert_eq!(
cells(&parsed),
[["13335", "Cloudflare, Inc."], ["15169", "Google\nLLC"]]
);
}
#[test]
fn a_doubled_quote_is_one_quote() {
let parsed = csv("asn,name\n13335,\"the \"\"net\"\"\"\n").unwrap();
assert_eq!(cells(&parsed), [["13335", "the \"net\""]]);
}
#[test]
fn crlf_endings_and_blank_lines_are_not_rows() {
let parsed = csv("asn,name\r\n13335,CLOUDFLARENET\r\n\r\n").unwrap();
assert_eq!(cells(&parsed), [["13335", "CLOUDFLARENET"]]);
}
#[test]
fn a_final_row_without_a_newline_is_still_a_row() {
let parsed = csv("asn,name\n13335,CLOUDFLARENET").unwrap();
assert_eq!(cells(&parsed), [["13335", "CLOUDFLARENET"]]);
}
#[test]
fn an_empty_field_is_a_value_the_source_did_not_supply() {
let parsed = csv("asn,name,country\n13335,,AU\n").unwrap();
assert_eq!(parsed.rows[0][1], None);
assert_eq!(parsed.rows[0][2].as_deref(), Some("AU"));
}
#[test]
fn a_byte_order_mark_is_not_part_of_the_first_column_name() {
let parsed = csv("\u{feff}asn,name\n13335,CLOUDFLARENET\n").unwrap();
assert_eq!(parsed.columns, ["asn", "name"]);
}
#[test]
fn a_row_of_the_wrong_width_is_refused_by_line() {
let err = csv("asn,name\n13335,Cloudflare, Inc.\n").unwrap_err();
assert!(
matches!(err, TableError::Malformed { line: 2, .. }),
"{err:?}"
);
assert!(
err.to_string().contains("3 fields against 2 columns"),
"{err}"
);
}
#[test]
fn an_unterminated_quote_is_refused() {
let err = csv("asn,name\n13335,\"Cloudflare\n").unwrap_err();
assert!(matches!(err, TableError::Malformed { .. }), "{err:?}");
}
#[test]
fn text_after_a_closing_quote_is_refused() {
let err = csv("asn,name\n13335,\"Cloudflare\" Inc\n").unwrap_err();
assert!(matches!(err, TableError::Malformed { .. }), "{err:?}");
}
#[test]
fn a_headerless_csv_takes_its_names_from_the_config() {
let names = Schema::Named(vec!["asn".to_string(), "name".to_string()]);
let parsed = read(
"13335,CLOUDFLARENET\n15169,GOOGLE\n".as_bytes(),
TableFormat::Csv { header: false },
&names,
u64::MAX,
)
.unwrap();
assert_eq!(parsed.columns, ["asn", "name"]);
assert_eq!(
cells(&parsed),
[["13335", "CLOUDFLARENET"], ["15169", "GOOGLE"]]
);
}
#[test]
fn a_headerless_csv_with_no_names_is_refused() {
let err = read(
"13335,CLOUDFLARENET\n".as_bytes(),
TableFormat::Csv { header: false },
&Schema::Auto,
u64::MAX,
)
.unwrap_err();
assert!(matches!(err, TableError::NamesRequired), "{err:?}");
}
#[test]
fn named_columns_override_a_header_rather_than_shifting_the_rows() {
let names = Schema::Named(vec!["number".to_string(), "operator".to_string()]);
let parsed = read(
"asn,name\n13335,CLOUDFLARENET\n".as_bytes(),
TableFormat::Csv { header: true },
&names,
u64::MAX,
)
.unwrap();
assert_eq!(parsed.columns, ["number", "operator"]);
assert_eq!(cells(&parsed), [["13335", "CLOUDFLARENET"]]);
}
#[test]
fn a_csv_with_only_a_header_holds_no_rows() {
let err = csv("asn,name\n").unwrap_err();
assert!(matches!(err, TableError::Empty), "{err:?}");
}
#[test]
fn json_objects_name_the_columns() {
let body = r#"[{"asn": 13335, "name": "CLOUDFLARENET"}]"#;
let parsed = read(body.as_bytes(), TableFormat::Json, &Schema::Auto, u64::MAX).unwrap();
assert_eq!(parsed.columns, ["asn", "name"]);
assert_eq!(cells(&parsed), [["13335", "CLOUDFLARENET"]]);
}
#[test]
fn ragged_objects_widen_the_table_rather_than_failing_it() {
let body = r#"[
{"ip": "1.1.1.1", "country": "AU"},
{"ip": "8.8.8.8", "asn": 15169}
]"#;
let parsed = read(body.as_bytes(), TableFormat::Json, &Schema::Auto, u64::MAX).unwrap();
assert_eq!(parsed.columns, ["asn", "country", "ip"]);
assert_eq!(parsed.rows[0][0], None);
assert_eq!(parsed.rows[0][1].as_deref(), Some("AU"));
assert_eq!(parsed.rows[1][0].as_deref(), Some("15169"));
assert_eq!(parsed.rows[1][1], None);
}
#[test]
fn named_columns_project_a_json_document() {
let body = r#"[{"ip": "1.1.1.1", "country": "AU", "extra": 1}]"#;
let names = Schema::Named(vec!["ip".to_string(), "country".to_string()]);
let parsed = read(body.as_bytes(), TableFormat::Json, &names, u64::MAX).unwrap();
assert_eq!(parsed.columns, ["ip", "country"]);
assert_eq!(cells(&parsed), [["1.1.1.1", "AU"]]);
}
#[test]
fn json_scalars_are_text_and_nested_values_keep_their_json() {
let body = r#"[{"n": 42, "t": true, "z": null, "a": [1, 2], "o": {"k": "v"}}]"#;
let parsed = read(body.as_bytes(), TableFormat::Json, &Schema::Auto, u64::MAX).unwrap();
assert_eq!(parsed.columns, ["a", "n", "o", "t", "z"]);
assert_eq!(
cells(&parsed),
[["[1,2]", "42", r#"{"k":"v"}"#, "true", ""]]
);
}
#[test]
fn json_that_is_not_an_array_of_objects_is_refused() {
for body in [r#"{"relays": []}"#, "[1, 2]", "not json at all"] {
let err =
read(body.as_bytes(), TableFormat::Json, &Schema::Auto, u64::MAX).unwrap_err();
assert!(
matches!(err, TableError::NotAnArrayOfObjects { .. }),
"{body}: {err:?}"
);
}
}
#[test]
fn a_json_read_stops_at_the_ceiling_rather_than_parsing_the_document() {
use std::fmt::Write as _;
let mut body = String::from("[");
for at in 0..200u32 {
let asn = 64_500 + at;
let _ = write!(body, r#"{{"asn": {asn}, "name": "OPERATOR-{asn}"}},"#);
}
body.push_str(r#"{"asn": ]"#);
let stopped = read(body.as_bytes(), TableFormat::Json, &Schema::Auto, 512).unwrap_err();
assert!(
matches!(stopped, TableError::OverResidentCeiling { ceiling: 512 }),
"{stopped:?}"
);
let read_out =
read(body.as_bytes(), TableFormat::Json, &Schema::Auto, u64::MAX).unwrap_err();
assert!(
matches!(read_out, TableError::NotAnArrayOfObjects { .. }),
"{read_out:?}"
);
}
#[test]
fn one_element_is_held_to_the_ceiling_while_it_is_read() {
let long_string = format!(r#"[{{"name": "{}"#, "x".repeat(1 << 20));
let wide_array = format!(r#"[{{"asn": 1}}, {{"seen": [{}"#, "1,".repeat(1 << 19));
let many_keys: String = std::iter::once("[{".to_string())
.chain((0..1 << 16).map(|key| format!(r#""k{key}": null,"#)))
.collect();
for (shape, body) in [
("one long string", long_string),
("one wide array", wide_array),
("one wide object", many_keys),
] {
let err = read(body.as_bytes(), TableFormat::Json, &Schema::Auto, 4096).unwrap_err();
assert!(
matches!(err, TableError::OverResidentCeiling { ceiling: 4096 }),
"{shape}: {err:?}"
);
let unbounded =
read(body.as_bytes(), TableFormat::Json, &Schema::Auto, u64::MAX).unwrap_err();
assert!(
matches!(unbounded, TableError::NotAnArrayOfObjects { .. }),
"{shape}: {unbounded:?}"
);
}
}
#[test]
fn an_element_that_fits_beside_the_rows_already_read_is_admitted() {
let body = r#"[{"asn": 13335, "name": "CLOUDFLARENET"}, {"asn": 15169, "name": "GOOGLE"}]"#;
let whole = read(body.as_bytes(), TableFormat::Json, &Schema::Auto, u64::MAX).unwrap();
let cost: u64 = whole.rows.iter().map(|row| footprint(row)).sum();
let parsed = read(body.as_bytes(), TableFormat::Json, &Schema::Auto, cost).unwrap();
assert_eq!(cells(&parsed), cells(&whole));
}
#[test]
fn one_record_is_held_to_the_ceiling_while_it_is_read() {
let long_field = format!("asn,name\n1,\"{}", "x".repeat(1 << 20));
let many_fields = format!("asn,name\n{}", "a,".repeat(MAX_FIELDS + 1));
for (shape, body) in [
("one long field", long_field),
("one wide record", many_fields),
] {
let err = read(
body.as_bytes(),
TableFormat::Csv { header: true },
&Schema::Auto,
1024,
)
.unwrap_err();
assert!(
matches!(err, TableError::OverResidentCeiling { ceiling: 1024 }),
"{shape}: {err:?}"
);
let unbounded = csv(&body).unwrap_err();
assert!(
matches!(unbounded, TableError::Malformed { .. }),
"{shape}: {unbounded:?}"
);
}
}
#[test]
fn a_json_element_that_is_not_an_object_is_named_by_its_position() {
let body = r#"[{"ip": "1.1.1.1"}, {"ip": "8.8.8.8"}, 3]"#;
let err = read(body.as_bytes(), TableFormat::Json, &Schema::Auto, u64::MAX).unwrap_err();
assert!(
matches!(&err, TableError::NotAnArrayOfObjects { detail } if detail == "element 2 is not an object"),
"{err:?}"
);
}
#[test]
fn an_empty_json_array_holds_no_rows() {
let err = read("[]".as_bytes(), TableFormat::Json, &Schema::Auto, u64::MAX).unwrap_err();
assert!(matches!(err, TableError::Empty), "{err:?}");
}
fn probe_csv(file: &Path, schema: &Schema, ceiling: u64) -> Result<(), TableError> {
probe(
file,
TableFormat::Csv { header: true },
schema,
&Index::Column("asn".to_string()),
ceiling,
)
}
#[test]
fn a_probe_reads_a_csv_to_the_end() {
use std::fmt::Write as _;
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("rows.csv");
let mut body = String::from("asn,name\n");
for row in 0..5_000 {
let _ = writeln!(body, "{row},operator");
}
probe_csv(file_with(&file, &body), &Schema::Auto, u64::MAX).unwrap();
body.push_str("1,too,many,fields\n");
let err = probe_csv(file_with(&file, &body), &Schema::Auto, u64::MAX).unwrap_err();
assert!(
matches!(err, TableError::Malformed { line: 5002, .. }),
"{err:?}"
);
}
fn file_with<'a>(file: &'a Path, body: &str) -> &'a Path {
fs::write(file, body).unwrap();
file
}
#[test]
fn a_probe_refuses_a_file_the_load_would_refuse() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("rows.csv");
let named = Schema::Named(vec!["asn".to_string(), "name".to_string()]);
for (body, schema, ceiling) in [
("asn,name\n", &Schema::Auto, u64::MAX),
("asn,name\n1,two,three\n", &Schema::Auto, u64::MAX),
("asn,name,extra\n1,one,x\n", &named, u64::MAX),
("number,name\n1,one\n", &Schema::Auto, u64::MAX),
("asn,name\n1,one\n2,two\n", &Schema::Auto, 64),
] {
let result = probe_csv(file_with(&file, body), schema, ceiling);
assert!(result.is_err(), "{body:?} was admitted");
}
fs::write(&file, b"{\"relays\": []}").unwrap();
assert!(matches!(
probe(
&file,
TableFormat::Json,
&Schema::Auto,
&Index::Column("asn".to_string()),
u64::MAX
)
.unwrap_err(),
TableError::NotAnArrayOfObjects { .. }
));
}
#[test]
fn a_lone_carriage_return_ends_a_record_rather_than_vanishing() {
let parsed = csv("asn,name\r1,one\r2,two\r").unwrap();
assert_eq!(parsed.columns, ["asn", "name"]);
assert_eq!(cells(&parsed), [["1", "one"], ["2", "two"]]);
}
#[test]
fn a_crlf_line_ending_is_one_record_and_one_line() {
let err = csv("asn,name\r\n1,one\r\n2,three,too many\r\n").unwrap_err();
assert!(
matches!(err, TableError::Malformed { line: 3, .. }),
"{err:?}"
);
}
#[test]
fn a_carriage_return_inside_a_quoted_field_stays_data() {
let parsed = csv("asn,name\n1,\"a\rb\"\n").unwrap();
assert_eq!(cells(&parsed), [["1", "a\rb"]]);
}
#[test]
fn a_field_that_never_closes_is_refused_rather_than_buffered() {
let mut body = String::from("asn,name\n1,\"");
body.push_str(&"x".repeat(MAX_FIELD + 1));
let err = csv(&body).unwrap_err();
assert!(
matches!(&err, TableError::Malformed { detail, .. } if detail.contains("longer than")),
"{err:?}"
);
}
#[test]
fn a_record_that_never_ends_is_refused_rather_than_buffered() {
let mut body = String::from("asn,name\n");
body.push_str(&"a,".repeat(MAX_FIELDS + 1));
let err = csv(&body).unwrap_err();
assert!(
matches!(&err, TableError::Malformed { detail, .. } if detail.contains("more than")),
"{err:?}"
);
}
}