Skip to content

Version 1.19.0 - #348

Open
tilo wants to merge 22 commits into
mainfrom
version-1.19.0
Open

Version 1.19.0#348
tilo wants to merge 22 commits into
mainfrom
version-1.19.0

Conversation

@tilo

@tilo tilo commented Aug 9, 2026

Copy link
Copy Markdown
Owner
  • removing handling of scientific notation (interferred with normal operations)

tilo and others added 22 commits July 22, 2026 14:00
Values like "12E5" and "0047583311587E590003" are identifiers far more
often than scientific notation; 1.18.0's exponent conversion corrupted
them irreversibly (Infinity). Exponent-shaped values now always stay
Strings on both the C and Ruby paths, as in every version before 1.18.0.
Plain integers and decimals are unaffected, as is decimal_precision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFif67ZYKunZgdhKXJGrcK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed ")

Output written with a custom quote_char (e.g. "'") could not be read back:
escaping doubled the custom quote_char correctly, but the field was then
wrapped in a literal double quote. Round-trip through Writer and Reader
with the same custom quote_char is now tested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ensure clause also ran on the early enum_for return, where
original_chunk_size was never captured, restoring nil — so a later
each_chunk on the same Reader ignored the configured chunk size.
Scoped with an explicit begin/ensure, matching each_chunk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With strip_whitespace: true the C path only stripped space and tab, so a
stray trailing \r survived on CRLF lines in mixed LF/CRLF files, and in
CRLF files read with an explicit row_sep of LF. trim_field now strips
exactly what the Ruby path's String#strip! does: space, \t, \n, \v, \f,
\r, and \0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pattern is written against what's in the file, but the C path
converted values to numbers at parse time, so /\A007\z/ never matched
(the matcher only saw 7). With nil_values_matching set, the C parser now
defers numeric conversion and zero-removal to the Ruby hash
transformations, which apply the pattern to the raw string first — the
same order as the pure-Ruby path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
option_valid? accepted :auto for quote_char although only row_sep and
col_sep have auto-detection; the Reader then crashed with NoMethodError
from '@quote_char * 2'. quote_char must be a non-empty String.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With headers name,name,name2 the second 'name' was renamed to 'name2'
(default suffix + counter), colliding with the real third column, and
check_duplicate_headers then raised DuplicateHeaders — defeating the
disambiguation feature. The duplicate branch now bumps its counter past
taken names, like the blank-header branch always did.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When rows contained more columns than headers, the reader appended
column_N entries directly into the caller's array (and into
options[:user_provided_headers], so a reused options hash silently
changed behavior on the next file). process_headers now adopts a dup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
headers: { only: } / { except: } values were always normalized to
Symbols, but with strings_as_keys / keep_original_headers the row keys
are Strings — nothing matched, and with only: every row came back empty
(then was dropped by remove_empty_hashes): silent total data loss.
Selectors are now normalized to the row-key type on both paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
process_headers ignored the parser's unclosed-quote signal (size -1):
the first header fragment was silently lost and the second fragment was
parsed as a data row. Headers now stitch across physical lines exactly
like the data-row loop; an unclosed quote at EOF raises MalformedCSV.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With strings_as_keys: true and duplicate_header_suffix: nil, an empty
header produces a '' String key. The Ruby path drops it; the C-path
cleanup only deleted the :"" Symbol form. Both cleanup sites now
delete both forms, and @delete_empty_keys also detects the String form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A one-character multi-byte col_sep (e.g. 'é') passed the size == 1 gate
but was then scanned by its first byte only — which also occurs as the
lead byte of other characters — producing mid-character slices and an
ArgumentError on quoted lines. Gating on bytesize sends multi-byte
separators down the character-level path, matching the C parser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Latin-1 data mislabeled as UTF-8 made the Ruby fallback raise
ArgumentError from encoding-aware operations (split, strip!, blank?'s
regex) — an error on_bad_row: :skip couldn't even quarantine. Invalid
lines are now processed as BINARY bytes and the fields re-tagged with
the original encoding — bytes preserved exactly, never transcoded,
matching the C path. blank? treats invalid-byte strings as non-blank.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the coverage move out of the corner-cases scratch file: the
out-of-range parity block there tested 1e400, -1e400, and 1e-400; the
committed contract block only had 1e400.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An unquoted field starting with a multi-byte character followed by a
literal quote (é"x) advanced the byte loop onto a UTF-8 continuation
byte; String#byteindex then raised IndexError (offset not on character
boundary) at the col_sep skip-ahead in both parse_csv_line_ruby and
detect_multiline_strict. The skip-ahead now uses the byte loop when the
scan position is mid-character. Found by differential fuzzing; the C
path parsed these inputs fine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror Ruby's String#chomp("\n") semantics (removes \r\n, \r, or \n)
in chomp_row_sep. The literal chomp left the \r in the line, so a CRLF
line whose last field is quoted raised MalformedCSV even with default
options, and strip_whitespace: false yielded "x\r" / String "1\r"
where Ruby yields "x" / Integer 1. Found by differential fuzzing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes a regression from the raw-string-matching fix: the C parser defers
numeric conversion and zero-removal when nil_values_matching is set, but
the accelerated post-processing only applied the matcher — so numeric
conversion was silently off for the whole file. The acceleration branch
now runs hash_transformations (nil-match on raw strings first, then
zero-removal, numeric conversion, value_converters — pure-Ruby order).

Also guard the transformation regexes (nil_values_matching, ZERO_REGEX,
NUMERIC_REGEX) against invalid-encoding values, which raised
ArgumentError on the Ruby path for values like "1\xFF".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
detect_multiline_strict disagreed with the parser three ways, making the
Ruby path fabricate 'Unclosed quoted field' (MalformedCSV) on rows the
parser closes: no doubled-quote precedence (issue #334 rule), no
backslash-escape awareness (:backslash, and the primary interpretation
of :auto), and it walked the unchomped line so end-of-line close
decisions flipped. The gate now models the parser's rules exactly and,
under :auto, reports still-open only when both the backslash and RFC
interpretations are open (mirroring the dual quote counting).

Adds spec/smarter_csv/parity_fuzz_spec.rb: a seeded, deterministic
differential fuzz asserting C and Ruby paths produce identical results
(same rows, same value classes, or the same error class) across two
alphabets and eleven option sets. 30k randomized inputs run clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The separator comparison loops exited at end-of-buffer with the match
flag still true, so with col_sep '||' a value or header ending in a lone
'|' silently lost that character. A separator now only matches when it
fits completely before endP; the same bound fixes an out-of-bounds read
in is_valid_close for multi-char separators near end-of-line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Empty line yields zero fields on the C path (like Ruby's "".split),
  so remove_empty_values: false pads ALL columns with nil — no more
  {a: "", b: nil, ...} divergence.
- A nil entry in user_provided_headers drops that column on the C path
  too (@delete_nil_keys now detects nil in the headers).
- Extra-column keys are interned as UTF-8 symbols (rb_enc_sprintf +
  rb_str_intern), fixing EncodingError for non-ASCII
  missing_header_prefix.
- col_sep > 7 bytes, row_sep > 15, missing_header_prefix > 63 bytes
  fall back to the pure-Ruby parser instead of being silently truncated
  by the C parse context's fixed-size buffers.
- Fix stale comment: quoting does not suppress numeric conversion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant