Skip to content

Commit f173e65

Browse files
authored
Fix Column::operator<< to stream the exact column bytes (#556)
Fixes COL-01 from the code review. `Column::operator<<` passed `getText()` and `getBytes()` as two separate call arguments: their evaluation order is unspecified, and the two SQLite accessors have an order-dependent contract (the text pointer can be invalidated by a later bytes call). Reading the value once through `getString()` pairs the pointer with its length and behaves consistently for TEXT, BLOB, embedded NULs and NULL. Adds UTF-8 regression tests for the stream inserter (multibyte text, an embedded NUL, and the same bytes stored as a BLOB). Note these pass against the old code too on GCC, since for TEXT/BLOB the accessors happen to be idempotent; the fix is about removing the unspecified-order UB and honoring SQLite's call-order contract.
2 parents e52087e + b4851d2 commit f173e65

3 files changed

Lines changed: 85 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,5 @@ Version 3.4.0 - 2026 ???
304304
- Add a GitHub Actions workflow computing unit test & example code coverage (GCov) and publishing it to Coveralls (#549)
305305
- Add unit tests covering error and edge-case paths to raise code coverage (#551)
306306
- Guard against null C pointers in Exception, Database, and Statement to avoid undefined behavior (#552)
307-
- Fix Database::isUnencrypted() to compare the full 16-byte header in binary mode (#553)
307+
- Fix Database::isUnencrypted() to compare the full 16-byte header in binary mode (#553)
308+
- Fix Column::operator<< to stream the exact column bytes via getString() (#556)

src/Column.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ int Column::getBytes() const noexcept
116116
// Standard std::ostream inserter
117117
std::ostream& operator<<(std::ostream& aStream, const Column& aColumn)
118118
{
119-
aStream.write(aColumn.getText(), aColumn.getBytes());
119+
const std::string value = aColumn.getString();
120+
aStream.write(value.data(), static_cast<std::streamsize>(value.size()));
120121
return aStream;
121122
}
122123

tests/Column_test.cpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,87 @@ TEST(Column, stream)
262262
EXPECT_EQ(content, str);
263263
}
264264

265+
// COL-01 probe: try to break operator<< with multibyte UTF-8 TEXT.
266+
// Bytes are written with \x escapes so the test does not depend on the source file encoding.
267+
TEST(Column, streamUtf8Text)
268+
{
269+
SQLite::Database db(":memory:", SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE);
270+
EXPECT_EQ(0, db.exec("CREATE TABLE test (msg TEXT)"));
271+
SQLite::Statement insert(db, "INSERT INTO test VALUES (?)");
272+
273+
const std::string utf8 =
274+
"caf\xC3\xA9" // cafe with e-acute (U+00E9)
275+
" \xE4\xB8\x96\xE7\x95\x8C" // CJK "world" (U+4E16 U+754C)
276+
" \xF0\x9F\x98\x80"; // grinning face emoji (U+1F600)
277+
278+
insert.bind(1, utf8);
279+
EXPECT_EQ(1, insert.exec());
280+
281+
SQLite::Statement query(db, "SELECT msg FROM test");
282+
ASSERT_TRUE(query.executeStep());
283+
284+
std::stringstream ss;
285+
ss << query.getColumn(0);
286+
const std::string streamed = ss.str();
287+
288+
// operator<< must reproduce the exact UTF-8 byte sequence and agree with getString()
289+
EXPECT_EQ(streamed.size(), utf8.size());
290+
EXPECT_EQ(streamed, utf8);
291+
EXPECT_EQ(streamed, query.getColumn(0).getString());
292+
}
293+
294+
// COL-01 probe: multibyte UTF-8 TEXT on both sides of an embedded NUL byte.
295+
TEST(Column, streamUtf8EmbeddedNul)
296+
{
297+
SQLite::Database db(":memory:", SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE);
298+
EXPECT_EQ(0, db.exec("CREATE TABLE test (msg TEXT)"));
299+
SQLite::Statement insert(db, "INSERT INTO test VALUES (?)");
300+
301+
const char raw[] = "caf\xC3\xA9\x00\xF0\x9F\x98\x80"; // "cafe" + NUL + emoji
302+
const std::string utf8(raw, sizeof(raw) - 1); // keep the embedded NUL, drop the literal terminator
303+
304+
insert.bind(1, utf8);
305+
EXPECT_EQ(1, insert.exec());
306+
307+
SQLite::Statement query(db, "SELECT msg FROM test");
308+
ASSERT_TRUE(query.executeStep());
309+
310+
std::stringstream ss;
311+
ss << query.getColumn(0);
312+
const std::string streamed = ss.str();
313+
314+
EXPECT_EQ(streamed.size(), utf8.size());
315+
EXPECT_EQ(streamed, utf8);
316+
}
317+
318+
// COL-01 probe: the same UTF-8 bytes stored as a BLOB (the case the review flags).
319+
TEST(Column, streamUtf8Blob)
320+
{
321+
SQLite::Database db(":memory:", SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE);
322+
EXPECT_EQ(0, db.exec("CREATE TABLE test (data BLOB)"));
323+
SQLite::Statement insert(db, "INSERT INTO test VALUES (?)");
324+
325+
const std::string utf8 =
326+
"caf\xC3\xA9"
327+
" \xE4\xB8\x96\xE7\x95\x8C"
328+
" \xF0\x9F\x98\x80";
329+
330+
insert.bind(1, utf8.data(), static_cast<int>(utf8.size())); // store as a BLOB
331+
EXPECT_EQ(1, insert.exec());
332+
333+
SQLite::Statement query(db, "SELECT data FROM test");
334+
ASSERT_TRUE(query.executeStep());
335+
EXPECT_TRUE(query.getColumn(0).isBlob());
336+
337+
std::stringstream ss;
338+
ss << query.getColumn(0);
339+
const std::string streamed = ss.str();
340+
341+
EXPECT_EQ(streamed.size(), utf8.size());
342+
EXPECT_EQ(streamed, utf8);
343+
EXPECT_EQ(streamed, query.getColumn(0).getString());
344+
}
345+
265346
TEST(Column, shared_ptr)
266347
{
267348
// Create a new database

0 commit comments

Comments
 (0)