@@ -271,6 +271,142 @@ TEST_CASE("unterminated quote captures rest of input")
271271 CHECK (cells[0 ].value == " start,here,no-close" );
272272}
273273
274+ TEST_CASE (" unterminated_quote flag flips only on the trailing open cell" )
275+ {
276+ auto input = std::string (R"( a,"unterminated cell)" );
277+ separated_string ss (input.data (), input.length ());
278+ std::vector<bool > flags;
279+ for (auto iter = ss.begin (); iter != ss.end (); ++iter) {
280+ flags.push_back (iter.unterminated_quote ());
281+ }
282+ REQUIRE (flags.size () == 2 );
283+ CHECK_FALSE (flags[0 ]);
284+ CHECK (flags[1 ]);
285+ }
286+
287+ TEST_CASE (" unterminated_quote stays false when every quote is closed" )
288+ {
289+ auto input = std::string (R"( "one","two","three")" );
290+ separated_string ss (input.data (), input.length ());
291+ for (auto iter = ss.begin (); iter != ss.end (); ++iter) {
292+ CHECK_FALSE (iter.unterminated_quote ());
293+ }
294+ }
295+
296+ TEST_CASE (" unterminated_quote sees through `\"\" ` escapes" )
297+ {
298+ // The `""` is an embedded literal, not a close-quote — so the
299+ // outer quoted region is still open at end-of-buffer.
300+ auto input = std::string (R"( "a""b,rest)" );
301+ separated_string ss (input.data (), input.length ());
302+ auto iter = ss.begin ();
303+ REQUIRE (iter != ss.end ());
304+ CHECK (iter.unterminated_quote ());
305+ ++iter;
306+ CHECK (iter == ss.end ());
307+ }
308+
309+ TEST_CASE (" ss_resume: continuation closes and emits remaining cells" )
310+ {
311+ // Caller previously parsed `"line one` (open quote), is now
312+ // feeding the rest after a `\n` glue: `line two",x,y`.
313+ auto input = std::string (R"( line two",x,y)" );
314+ separated_string ss (input.data (), input.length ());
315+ ss.ss_resume = separated_string::resume_state{0 , true };
316+ auto cells = std::vector<parsed_cell>{};
317+ for (auto iter = ss.begin (); iter != ss.end (); ++iter) {
318+ cells.push_back (parsed_cell{(*iter).to_string (), iter.kind ()});
319+ CHECK_FALSE (iter.unterminated_quote ());
320+ }
321+ REQUIRE (cells.size () == 3 );
322+ CHECK (cells[0 ].value == " line two" );
323+ CHECK (cells[1 ].value == " x" );
324+ CHECK (cells[2 ].value == " y" );
325+ }
326+
327+ TEST_CASE (" ss_resume: continuation still unterminated re-flags" )
328+ {
329+ // Open-quote chunk N+1: still no closing `"` in this buffer
330+ // either. Caller will need to keep stitching.
331+ auto input = std::string (" more middle text" );
332+ separated_string ss (input.data (), input.length ());
333+ ss.ss_resume = separated_string::resume_state{0 , true };
334+ auto iter = ss.begin ();
335+ REQUIRE (iter != ss.end ());
336+ CHECK ((*iter).to_string () == " more middle text" );
337+ CHECK (iter.unterminated_quote ());
338+ ++iter;
339+ CHECK (iter == ss.end ());
340+ }
341+
342+ TEST_CASE (" ss_resume: `\"\" ` inside a continuation stays embedded" )
343+ {
344+ // `""` in a continuation chunk is still a literal `"`, not a
345+ // close-quote; the surrounding region remains open until a
346+ // lone `"`.
347+ auto input = std::string (R"( has ""embedded"" still open)" );
348+ separated_string ss (input.data (), input.length ());
349+ ss.ss_resume = separated_string::resume_state{0 , true };
350+ auto iter = ss.begin ();
351+ REQUIRE (iter != ss.end ());
352+ CHECK ((*iter).to_string () == R"( has ""embedded"" still open)" );
353+ CHECK (iter.unterminated_quote ());
354+ }
355+
356+ TEST_CASE (" ss_resume: continuation that closes immediately" )
357+ {
358+ // Edge case: the buffer is just the close-quote, separator, next
359+ // cell. The leading character closes the open region and yields
360+ // an empty cell value (no bytes from this chunk belonged to the
361+ // quoted run).
362+ auto input = std::string (R"( ",tail)" );
363+ separated_string ss (input.data (), input.length ());
364+ ss.ss_resume = separated_string::resume_state{0 , true };
365+ auto cells = std::vector<parsed_cell>{};
366+ for (auto iter = ss.begin (); iter != ss.end (); ++iter) {
367+ cells.push_back (parsed_cell{(*iter).to_string (), iter.kind ()});
368+ }
369+ REQUIRE (cells.size () == 2 );
370+ CHECK (cells[0 ].value .empty ());
371+ CHECK (cells[1 ].value == " tail" );
372+ }
373+
374+ TEST_CASE (" ss_resume: suspend() preserves row-level cell index" )
375+ {
376+ // Buffer 1: 3 closed cells, then a 4th that opens a quote and
377+ // never closes. suspend() snapshot should report index 3 +
378+ // in-quote.
379+ auto buf1 = std::string (R"( a,b,c,"open and unfinished)" );
380+ separated_string ss1 (buf1.data (), buf1.length ());
381+ std::optional<separated_string::resume_state> snap;
382+ for (auto iter = ss1.begin (); iter != ss1.end (); ++iter) {
383+ if (iter.unterminated_quote ()) {
384+ snap = iter.suspend ();
385+ }
386+ }
387+ REQUIRE (snap.has_value ());
388+ CHECK (snap->rs_index == 3 );
389+ CHECK (snap->rs_in_quote );
390+
391+ // Buffer 2: continuation closes the cell, then two more. Indices
392+ // emitted should be 3, 4, 5 — the snapshot's row-level position
393+ // carried across the buffer boundary.
394+ auto buf2 = std::string (R"( rest of cell",d,e)" );
395+ separated_string ss2 (buf2.data (), buf2.length ());
396+ ss2.ss_resume = *snap;
397+ std::vector<size_t > indices;
398+ std::vector<std::string> values;
399+ for (auto iter = ss2.begin (); iter != ss2.end (); ++iter) {
400+ indices.push_back (iter.index ());
401+ values.push_back ((*iter).to_string ());
402+ }
403+ REQUIRE (indices.size () == 3 );
404+ CHECK (indices == std::vector<size_t >{3 , 4 , 5 });
405+ CHECK (values[0 ] == " rest of cell" );
406+ CHECK (values[1 ] == " d" );
407+ CHECK (values[2 ] == " e" );
408+ }
409+
274410TEST_CASE (" newlines inside a quoted cell are preserved verbatim" )
275411{
276412 auto cells = tokenize (" \" line1\n line2\" ,next" );
0 commit comments