RustyCSV Compliance & Validation

Copy Markdown View Source

RustyCSV takes correctness seriously. With 494 ExUnit tests plus 125 Rust tests, including industry-standard validation suites used by CSV parsers across multiple languages, RustyCSV is one of the most thoroughly tested CSV libraries available for Elixir.

This document describes RFC 4180 compliance and the validation methodology.

RFC 4180 Compliance

RustyCSV.RFC4180 is fully compliant with RFC 4180 (Common Format and MIME Type for Comma-Separated Values).

RFC 4180 Requirements

SectionRequirementStatus
2.1Records separated by line breaks (CRLF)✅ Accepts CRLF and LF; outputs CRLF
2.2Last record may or may not have trailing line break
2.3Optional header line✅ Via skip_headers and headers: options
2.4Each record should have same number of fields✅ Parses variable-width rows
2.5Spaces are part of the field✅ Preserved exactly
2.6Fields may be enclosed in double quotes
2.6Fields containing CRLF must be quoted
2.6Fields containing double quotes must be quoted
2.6Fields containing commas must be quoted
2.7Double quotes escaped by doubling ("")

Line Ending Behavior

Parsing:

  • Accepts both CRLF (\r\n) and LF (\n) as record separators
  • Preserves embedded CRLF/LF inside quoted fields exactly as-is

Dumping:

  • Uses CRLF (\r\n) as the record separator (RFC 4180 compliant)
  • Matches NimbleCSV.RFC4180 output exactly

Differences from Strict RFC 4180

RustyCSV makes one practical concession shared by most CSV implementations:

  1. Accepts LF line endings - RFC 4180 specifies CRLF, but LF-only files are common on Unix systems. RustyCSV parses both.

Bare Carriage Return (\r)

A bare \r not followed by \n is treated as field data, not a line ending. This matches:

  • The RFC 4180 ABNF grammar (bare \r is not in TEXTDATA, only valid inside quoted fields)
  • NimbleCSV (only \r\n and \n are line endings)
  • Go encoding/csv, Ruby CSV, PostgreSQL COPY

Python's csv module differs — it treats bare \r as a line ending via universal newline handling.


Industry Test Suites

RustyCSV validates correctness against two industry-standard CSV test suites.

csv-spectrum (Acid Test)

Source: https://github.com/max-mapper/csv-spectrum

The csv-spectrum suite is a widely-used "acid test" for CSV parsers, providing CSV files with JSON expected outputs for verification.

Note: The csv-spectrum repository's raw files have LF line endings due to git normalization. Our test fixtures match the actual content served by GitHub, and we verify that both RustyCSV and NimbleCSV produce identical output for these files.

Test FileEdge CaseStatus
simple.csvBasic parsing
simple_crlf.csvCRLF line endings
comma_in_quotes.csvCommas inside quoted fields
escaped_quotes.csvDoubled quotes (""")
newlines.csvLF inside quoted fields
newlines_crlf.csvCRLF inside quoted fields
quotes_and_newlines.csvCombined edge cases
empty.csvHeaders only (LF)
empty_crlf.csvHeaders only (CRLF)
utf8.csvUnicode content
json.csvJSON-like content in fields
location_coordinates.csvNumeric/coordinate data

Test file: test/csv_spectrum_test.exs

csv-test-data (RFC 4180 Focused)

Source: https://github.com/sineemore/csv-test-data

A comprehensive RFC 4180-focused test suite with both valid and invalid CSV cases.

Valid Cases

Test FileEdge CaseStatus
simple-lf.csvBasic with LF endings
simple-crlf.csvBasic with CRLF endings
quotes-with-comma.csvCommas in quoted fields
quotes-with-escaped-quote.csvEscaped quotes
quotes-with-newline.csvNewlines in quoted fields
quotes-with-space.csvSpaces in quoted fields
quotes-empty.csvEmpty quoted fields
empty-field.csvEmpty unquoted fields
one-column.csvSingle column
empty-one-column.csvSingle empty column
leading-space.csvLeading spaces preserved
trailing-space.csvTrailing spaces preserved
trailing-newline.csvFile ends with newline
utf8.csvUTF-8 encoded content
header-simple.csvBasic with header row
header-no-rows.csvHeaders only, no data
all-empty.csvAll empty fields

Test file: test/rfc4180_test_data_test.exs


Edge Case Tests (PapaParse-inspired)

Source: https://github.com/mholt/PapaParse/blob/master/tests/test-cases.js

A comprehensive edge case test suite inspired by PapaParse, covering malformed input, unusual delimiters, and stress testing.

CategoryTest Cases
Basic parsingEmpty input, single field, delimiter-only
WhitespaceEdges, tabs, quoted whitespace
Quoted fieldsDelimiters, newlines, escaped quotes
Empty fieldsLeading, trailing, consecutive
Line endingsLF, CRLF, mixed, no trailing
Field countsRagged rows, single/many columns
UnicodeUTF-8, emoji, mixed scripts, BOM
Special charsNull bytes, control chars, backslash
Large data100K char fields, 1000 rows, 500 columns
Strategy consistencyAll strategies produce identical output

Test file: test/edge_cases_test.exs


Cross-Strategy Validation

All public batch strategy atoms must produce identical output for the same input. This is verified by running the shared batch suites across all five public batch atoms. parse_stream/2 is validated separately against batch parsing because it uses a different stateful parser.

Entry PointDescriptionValidates Against
:basicPublic alias of the SIMD batch pathShared batch suites
:simdSIMD batch path (default)Shared batch suites
:indexedPublic alias of the SIMD batch pathShared batch suites
:parallelParallel batch path via rayonShared batch suites
:zero_copyPublic alias of the SIMD batch pathShared batch suites
parse_stream/2Stateful streaming parserStream-vs-batch consistency tests
# Shared batch-strategy atom matrix
for strategy <- [:basic, :simd, :indexed, :parallel, :zero_copy] do
  test "all tests pass with #{strategy} strategy" do
    for name <- test_files do
      result = CSV.parse_string(csv, strategy: strategy)
      assert result == expected
    end
  end
end

NimbleCSV Compatibility

RustyCSV targets the latest published NimbleCSV release, currently v1.3.0. Compatibility is checked against both the v1.3.0 source tests and current master, with the upstream changelog reviewed for behavior changes.

Compatibility is verified through:

  1. API compatibility tests - NimbleCSV's public parser/dumper functions and original options/0 values.
  2. Output matching - Encoded bytes, top-level row count, row order, BOM, and list-shaped row iodata.
  3. Round-trip tests - Parse → dump → parse produces identical data.
  4. Full-file validation - 100K-row CSV parsed through both libraries produces identical row-by-row output.

Test file: test/nimble_csv_compat_test.exs

# Verify public dump behavior without depending on private iodata nesting
test "dump output matches NimbleCSV" do
  data = [["a", "b"], ["1", "2"]]
  rusty = RustyCSV.RFC4180.dump_to_iodata(data)
  nimble = NimbleCSV.RFC4180.dump_to_iodata(data)

  assert length(rusty) == length(nimble)
  assert IO.iodata_to_binary(rusty) == IO.iodata_to_binary(nimble)
end

Upstream Suite Verification

The upstream suite is loaded from the Git tag or commit, its NimbleCSV namespace is mechanically replaced with RustyCSV, and it is compiled in memory against the force-rebuilt local NIF.

Two runs are recorded:

  • Literal keeps every upstream assertion unchanged.
  • Semantic removes only the expected message string argument from six assert_raise calls. The CSV inputs, functions called, exception type, and production RustyCSV code remain unchanged.
Upstream sourceLiteralSemantic
v1.3.0 tag17/2121/21
master at 8cc4e68151975e5ff6eb1ad4a738a728bcb17a1e19/2323/23

The four literal failures are message-string differences only. The temporary semantic transformation exists only in the test process; no compatibility shim or raw-input formatter is written to the repository or shipped.

Parse Error Data Policy

NimbleCSV includes the offending CSV line in some ParseError messages. RustyCSV intentionally does not. CSV may contain credentials, personal data, or very large fields, and exceptions are commonly forwarded to logs and error trackers.

RustyCSV errors therefore contain a stable category and byte position, never field contents. Synthetic CSV content may appear in tests, but real user fixtures must be scrubbed. This is an intentional security difference, not an unfinished parity item.

Temporarily changing production errors to include raw CSV would test behavior that will not ship and risks committing a data leak. Future parity checks should repeat the in-memory semantic run instead.

Iodata Shape

NimbleCSV builds nested iodata per field. RustyCSV returns one list-wrapped binary per row. The documented/public behavior matches:

  • the outer list has one element per input row (plus a BOM element when enabled),
  • each row is list-shaped iodata,
  • row order and encoded bytes are identical,
  • length/1, row zipping, and IO.iodata_to_binary/1 behave compatibly.

The deeper per-field term tree is an implementation detail of iodata and is not replicated; doing so would add one BEAM term per field and separator without changing the callback contract.

Other Intentional Extensions

parse_stream/2 with non-line-delimited chunks

The two libraries use different streaming architectures. NimbleCSV's parse_stream expects each element of the input enumerable to be a complete line, but arbitrary chunks are supported by first calling to_line_stream/1. RustyCSV's streaming parser accepts arbitrary chunk boundaries directly because the Rust NIF maintains parse state across feed() calls.

This difference is invisible for the standard use case (File.stream! |> parse_stream), where both produce identical output. For non-line-delimited chunks, NimbleCSV requires the explicit line-normalization step while RustyCSV does not.

RustyCSV also exposes strategy selection, headers-to-maps, and strict: false as extensions. Strict parsing remains the default.


Running Compliance Tests

# Run all tests including compliance suites
mix test

# Run only compliance tests
mix test test/csv_spectrum_test.exs test/rfc4180_test_data_test.exs

# Run with specific strategy
mix test --only strategy:parallel

Test Fixtures

Test fixtures are stored in test/fixtures/:

test/fixtures/
 csv-spectrum/           # csv-spectrum acid test suite
    *.csv              # CSV test files
    *.json             # Expected JSON outputs
 csv-test-data/         # RFC 4180 test suite
     *.csv              # Valid/invalid CSV files
     *.json             # Expected outputs

Test Summary

GateResult
Rust unit tests114 passed
Rust conformance tests11 passed
ExUnit494 passed, including 5 properties
NimbleCSV v1.3.0 semantic suite21/21 passed
NimbleCSV master semantic suite23/23 passed
cargo clippy -D warningsPassed
mix credo --strictPassed
mix dialyzerPassed

Additional Test Resources

The following resources provide additional CSV test cases that may be valuable for future validation:

W3C CSVW Test Suite

The W3C CSV on the Web (CSVW) test suite contains 550+ tests for CSV validation and conversion to JSON/RDF. While focused on metadata and semantic representation, the parsing tests are valuable.

csv-fuzz (Fuzzing)

Fuzzing-based testing using Jazzer to find crashes, exceptions, and memory issues.


References