From f08e69e6eabc5391d434a8e118ebc817721ae4c0 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Thu, 14 May 2026 22:22:48 -0300 Subject: [PATCH 1/5] Fix SQL_C_GUID parameter binding sending corrupted data on the wire When an ODBC application called SQLBindParameter with C type SQL_C_GUID and SQL type SQL_GUID, the driver accepted the call but did not convert the 16-byte UUID into Firebird's wire format. Symptoms reported in #295: * BINARY(16) / CHAR(16) CHARACTER SET OCTETS targets received the ASCII bytes of the canonical UUID string truncated to 16 chars. * Untyped VARCHAR parameters (e.g. inside CHAR_TO_UUID(?)) received a wide-char buffer interpreted as narrow text, so Firebird raised "Human readable UUID argument for CHAR_TO_UUID must have hex digit at position 2 instead of ''". Add two wire-only conversion functions and route SQL_C_GUID input parameters to them: * convGuidToBinary writes 16 raw bytes in canonical UUID byte order (Data1/2/3 swapped from x86 little-endian to big-endian, Data4 as is). Used for BINARY(16) / CHAR(16) OCTETS / FB4+ BINARY targets. * convGuidToVarString stages the 36-char canonical UUID in the DescRecord local buffer and redirects the wire's sqldata via setSqlData(), matching the idiom transferStringToAllowedType already uses. This sidesteps the SQLDA-allocated buffer being only 2 bytes wide for an untyped `?` (VARCHAR(0)) placeholder. The pre-existing convGuidToString / convGuidToStringW remain unchanged; they handle the (currently unreachable) app-side fetch path that will become live with the column-side SQL_GUID mapping work tracked under #287 T5-5. Expose getSqlSubtype() and getSqlLen() as read-only accessors on HeadSqlVar so the dispatch in getAdressFunction can distinguish sqlsubtype == 1 + sqllen == 16 (BINARY/CHAR(16) OCTETS, raw bytes) from text wires of any other charset (canonical UUID string). Add three GuidParamBindingTest acceptance tests covering the issue's exact reproducers: SQL_C_GUID into CHAR(16) OCTETS, into VARCHAR via CHAR_TO_UUID(?), and into BINARY(16) via UUID_TO_CHAR(?). Closes #295. --- IscDbc/Connection.h | 2 + IscDbc/IscHeadSqlVar.h | 2 + OdbcConvert.cpp | 100 +++++++++++++++++++++++ OdbcConvert.h | 2 + tests/test_guid_and_binary.cpp | 141 +++++++++++++++++++++++++++++++++ 5 files changed, 247 insertions(+) diff --git a/IscDbc/Connection.h b/IscDbc/Connection.h index 8c980fb2..8ec28734 100644 --- a/IscDbc/Connection.h +++ b/IscDbc/Connection.h @@ -524,6 +524,8 @@ class HeadSqlVar //virtual void setSqlSubType ( short subtype ) = 0; virtual void setSqlLen ( short len ) = 0; virtual short getSqlMultiple () = 0; + virtual short getSqlSubtype () = 0; + virtual short getSqlLen () = 0; virtual char * getSqlData() = 0; virtual short * getSqlInd() = 0; diff --git a/IscDbc/IscHeadSqlVar.h b/IscDbc/IscHeadSqlVar.h index 38fb53b8..debbe95f 100644 --- a/IscDbc/IscHeadSqlVar.h +++ b/IscDbc/IscHeadSqlVar.h @@ -100,6 +100,8 @@ class IscHeadSqlVar : public HeadSqlVar inline void setSqlData ( char* data ) { sqlvar->sqldata = data; } inline short getSqlMultiple () { return sqlMultiple; } + inline short getSqlSubtype () { return sqlvar->sqlsubtype; } + inline short getSqlLen () { return sqlvar->sqllen; } inline char * getSqlData() { return sqlvar->sqldata; } inline short * getSqlInd() { return sqlvar->sqlind; } diff --git a/OdbcConvert.cpp b/OdbcConvert.cpp index 1f5a8594..3ca9499c 100644 --- a/OdbcConvert.cpp +++ b/OdbcConvert.cpp @@ -1001,12 +1001,38 @@ ADRESS_FUNCTION OdbcConvert::getAdressFunction(DescRecord * from, DescRecord * t } break; case SQL_C_GUID: + // Wire-side (input parameter binding) — Firebird's IPD describes the slot + // as either BINARY(16) / CHAR(16) OCTETS, a text VARCHAR/CHAR, or (FB4+) + // genuine BINARY. Route to dedicated wire-only conversions; leave the + // pre-existing convGuidToString / convGuidToStringW untouched for the + // app-side fetch path below. + if ( to->isIndicatorSqlDa ) + { + // BINARY(16) / CHAR(16) CHARACTER SET OCTETS — 16 raw bytes + if ( to->headSqlVarPtr->getSqlSubtype() == 1 + && to->headSqlVarPtr->getSqlLen() == 16 ) + return &OdbcConvert::convGuidToBinary; + // FB4+ BINARY/VARBINARY described directly as SQL_C_BINARY + if ( to->conciseType == SQL_C_BINARY ) + return &OdbcConvert::convGuidToBinary; + // Text wire (VARCHAR/CHAR, any charset) — 36-char canonical UUID. + // setTypeText() converts SQL_VARYING to SQL_TEXT so convGuidToVarString + // can write the bytes without a length prefix. + to->headSqlVarPtr->setTypeText(); + return &OdbcConvert::convGuidToVarString; + } + // App-side output (fetching a column described as SQL_GUID into the + // application's bound buffer). Currently unreachable because the column + // side of SQL_GUID mapping is task T5-5 (open), but kept intact for when + // that lands. switch(to->conciseType) { case SQL_C_CHAR: return &OdbcConvert::convGuidToString; case SQL_C_WCHAR: return &OdbcConvert::convGuidToStringW; + case SQL_C_BINARY: + return &OdbcConvert::convGuidToBinary; default: return &OdbcConvert::notYetImplemented; } @@ -1688,6 +1714,80 @@ int OdbcConvert::convGuidToStringW(DescRecord * from, DescRecord * to) return SQL_SUCCESS; } +// Write SQLGUID as 16 bytes in canonical UUID byte order (Data1/2/3 big-endian, Data4 unchanged). +// Used when the target is BINARY(16) / CHAR(16) CHARACTER SET OCTETS or SQL_C_BINARY. +int OdbcConvert::convGuidToBinary(DescRecord * from, DescRecord * to) +{ + char* pointer = (char*)getAdressBindDataTo((char*)to->dataPtr); + SQLLEN * indicatorTo = getAdressBindIndTo((char*)to->indicatorPtr); + SQLLEN * indicatorFrom = getAdressBindIndFrom((char*)from->indicatorPtr); + + ODBCCONVERT_CHECKNULL( pointer ); + + SQLGUID *g = (SQLGUID*)getAdressBindDataFrom((char*)from->dataPtr); + + unsigned char buf[16]; + buf[0] = (unsigned char)((g->Data1 >> 24) & 0xFF); + buf[1] = (unsigned char)((g->Data1 >> 16) & 0xFF); + buf[2] = (unsigned char)((g->Data1 >> 8) & 0xFF); + buf[3] = (unsigned char)( g->Data1 & 0xFF); + buf[4] = (unsigned char)((g->Data2 >> 8) & 0xFF); + buf[5] = (unsigned char)( g->Data2 & 0xFF); + buf[6] = (unsigned char)((g->Data3 >> 8) & 0xFF); + buf[7] = (unsigned char)( g->Data3 & 0xFF); + memcpy(buf + 8, g->Data4, 8); + + int outlen = (int)to->length; + int len = outlen < 16 ? outlen : 16; + + if ( len > 0 ) + memcpy(pointer, buf, len); + + if ( to->isIndicatorSqlDa ) { + to->headSqlVarPtr->setSqlLen(len); + } else + if ( indicatorTo ) + setIndicatorPtr(indicatorTo, len, to); + + return SQL_SUCCESS; +} + +// Write SQLGUID as the 36-char canonical UUID string into a Firebird text +// parameter slot (VARCHAR/CHAR of any text charset). The wire-side SQLDA +// buffer for an untyped `?` placeholder is sized for VARCHAR(0) — too small +// for 36 bytes — so we stage the string in the DescRecord's local buffer +// and redirect Firebird to read from there via setSqlData(), mirroring the +// idiom transferStringToAllowedType already uses for app→wire string moves. +// The dispatch in getAdressFunction has already called setTypeText() to +// convert SQL_VARYING wires to SQL_TEXT (no length prefix), so we just +// write 36 raw bytes and set sqllen. +int OdbcConvert::convGuidToVarString(DescRecord * from, DescRecord * to) +{ + SQLLEN * indicatorFrom = getAdressBindIndFrom((char*)from->indicatorPtr); + SQLLEN * indicatorTo = getAdressBindIndTo((char*)to->indicatorPtr); + + ODBCCONVERT_CHECKNULL_SQLDA; + + SQLGUID *g = (SQLGUID*)getAdressBindDataFrom((char*)from->dataPtr); + + char tmp[37]; + int srcLen = snprintf(tmp, sizeof(tmp), + "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", + (unsigned int) g->Data1, g->Data2, g->Data3, + g->Data4[0], g->Data4[1], g->Data4[2], g->Data4[3], + g->Data4[4], g->Data4[5], g->Data4[6], g->Data4[7]); + if ( srcLen < 0 || srcLen > 36 ) srcLen = 36; + + if ( !to->isLocalDataPtr ) + to->allocateLocalDataPtr( srcLen + 1 ); + + memcpy(to->localDataPtr, tmp, srcLen); + to->headSqlVarPtr->setSqlLen((short)srcLen); + to->headSqlVarPtr->setSqlData(to->localDataPtr); + + return SQL_SUCCESS; +} + //////////////////////////////////////////////////////////////////////// // TinyInt diff --git a/OdbcConvert.h b/OdbcConvert.h index 16618b83..a66486cd 100644 --- a/OdbcConvert.h +++ b/OdbcConvert.h @@ -94,6 +94,8 @@ class OdbcConvert // Guid int convGuidToString(DescRecord * from, DescRecord * to); int convGuidToStringW(DescRecord * from, DescRecord * to); + int convGuidToBinary(DescRecord * from, DescRecord * to); + int convGuidToVarString(DescRecord * from, DescRecord * to); // TinyInt int convTinyIntToBoolean(DescRecord * from, DescRecord * to); diff --git a/tests/test_guid_and_binary.cpp b/tests/test_guid_and_binary.cpp index 16f39035..0739596c 100644 --- a/tests/test_guid_and_binary.cpp +++ b/tests/test_guid_and_binary.cpp @@ -467,6 +467,147 @@ TEST_F(Fb4PlusTest, Binary16MapsToGuid) { EXPECT_EQ(ind, 16); } +// ============================================================ +// SQL_C_GUID parameter binding direction (issue #295) +// +// These tests cover SQLBindParameter(SQL_C_GUID, ...) — the input direction. +// They do NOT depend on SQL_GUID column type mapping (T5-5), so they run on +// all Firebird versions that support the underlying SQL types. +// ============================================================ + +class GuidParamBindingTest : public OdbcConnectedTest { +protected: + // The known UUID used by every test — canonical text form and matching + // SQLGUID struct + canonical 16-byte representation. + static constexpr const char* kCanonicalText = + "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"; + + static SQLGUID makeKnownGuid() { + SQLGUID g{}; + g.Data1 = 0xA0EEBC99; + g.Data2 = 0x9C0B; + g.Data3 = 0x4EF8; + const unsigned char d4[8] = {0xBB, 0x6D, 0x6B, 0xB9, 0xBD, 0x38, 0x0A, 0x11}; + memcpy(g.Data4, d4, 8); + return g; + } + + int GetServerMajor() { + return GetServerMajorVersion(hDbc); + } +}; + +// Bind SQL_C_GUID to a CHAR(16) CHARACTER SET OCTETS parameter and verify the +// 16 bytes Firebird stores match the canonical UUID byte order. +TEST_F(GuidParamBindingTest, BindGuidToCharOctets16) { + REQUIRE_FIREBIRD_CONNECTION(); + + TempTable table(this, "TEST_PB_GUID_OCT", + "ID INTEGER NOT NULL PRIMARY KEY, " + "VAL CHAR(16) CHARACTER SET OCTETS"); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + SQLINTEGER id = 1; + SQLLEN idInd = sizeof(id); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_SLONG, SQL_INTEGER, 0, 0, &id, sizeof(id), &idInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"INSERT INTO TEST_PB_GUID_OCT (ID, VAL) VALUES (?, ?)", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + Commit(); + ReallocStmt(); + + // Read back as canonical text via UUID_TO_CHAR — Firebird's UUID_TO_CHAR + // round-trips an OCTETS-string in canonical UUID byte order. + ExecDirect("SELECT UUID_TO_CHAR(VAL) FROM TEST_PB_GUID_OCT WHERE ID = 1"); + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + SQLCHAR buf[64] = {}; + SQLLEN ind = 0; + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + std::string text((char*)buf); + while (!text.empty() && text.back() == ' ') text.pop_back(); + EXPECT_EQ(text, kCanonicalText) + << "BINARY(16) GUID round-trip failed: stored bytes do not decode to " + << "the canonical UUID. Got: '" << text << "'"; +} + +// Bind SQL_C_GUID to a VARCHAR parameter (Firebird infers VARCHAR for an +// untyped `?` slot inside CHAR_TO_UUID(?)). The driver must send the 36-char +// canonical UUID string. +TEST_F(GuidParamBindingTest, BindGuidToVarcharViaCharToUuid) { + REQUIRE_FIREBIRD_CONNECTION(); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + // Round-trip through CHAR_TO_UUID then UUID_TO_CHAR — exercises the + // SQL_C_GUID → VARCHAR wire path on the way in. + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"SELECT UUID_TO_CHAR(CHAR_TO_UUID(?)) FROM rdb$database", + SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "Exec: " << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "Fetch: " << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + SQLCHAR buf[64] = {}; + SQLLEN ind = 0; + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + std::string text((char*)buf); + while (!text.empty() && text.back() == ' ') text.pop_back(); + EXPECT_EQ(text, kCanonicalText) + << "SQL_C_GUID → VARCHAR round-trip failed. Got: '" << text << "'"; +} + +// Bind SQL_C_GUID to a literal `UUID_TO_CHAR(?)` SELECT — Firebird infers +// BINARY(16) for the parameter slot. The wire payload must be the 16 raw +// bytes of the canonical UUID, not a stringified form. +TEST_F(GuidParamBindingTest, BindGuidToUuidToCharRoundtrip) { + REQUIRE_FIREBIRD_CONNECTION(); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"SELECT UUID_TO_CHAR(?) FROM rdb$database", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + SQLCHAR buf[64] = {}; + SQLLEN ind = 0; + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + std::string text((char*)buf); + while (!text.empty() && text.back() == ' ') text.pop_back(); + EXPECT_EQ(text, kCanonicalText) + << "SQL_C_GUID → BINARY(16) (UUID_TO_CHAR(?)) round-trip failed. " + << "Got: '" << text << "' — this is the corruption pattern from issue #295."; +} + // Test: DECFLOAT column insertion and retrieval on Firebird 4+ TEST_F(Fb4PlusTest, DecfloatInsertAndRetrieve) { GTEST_SKIP() << "Requires Phase 8: SQL_GUID type mapping and FB4+ types (not yet merged)"; From 84babbf4660aeaf39be359216934ccaae2310b45 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 25 May 2026 12:13:44 -0300 Subject: [PATCH 2/5] guid: cover VARBINARY wire, fix its varying-prefix crash, drop dead arms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test coverage: - BindGuidToVarcharOctets16 — binds SQL_C_GUID to VARCHAR(16) CHARACTER SET OCTETS (the wire form of native FB4+ VARBINARY(16)). The existing BindGuidToCharOctets16 only covered the fixed CHAR(16) (SQL_TEXT) slot; this exercises the SQL_VARYING (length-prefixed) binary wire, which was previously untested. Bug the new test exposed (and fixes): - For a SQL_VARYING binary wire, convGuidToBinary wrote 16 raw bytes at offset 0 with no 2-byte length prefix. Firebird then read the first GUID bytes as a VARYING length and over-read the buffer — a hard SEGFAULT on the FB6 snapshot (silently tolerated on FB5). The dispatch now calls setTypeText() for the OCTETS subtype-1/len-16 branch, mirroring the text branch: the varying wire becomes fixed SQL_TEXT (OCTETS charset preserved) so the offset-0 write is correct. A fixed CHAR(16) wire is already SQL_TEXT, so this is a no-op there. - The new test is SKIP_ON_FIREBIRD6-guarded: even with the prefix fix, the current FB6 master snapshot aborts this parameterized OCTETS-VARYING insert with a server-side "Stack overflow" — the same parameterized-query incompatibility already guarded across the suite. Exercised on FB 3/4/5. Dead-code removal: - Drop the wire-side `to->conciseType == SQL_C_BINARY` arm. Native FB4+ BINARY/VARBINARY reach the dispatch as subtype-1 / sqllen-16 OCTETS (caught by the branch above); the only type that yields conciseType SQL_C_BINARY is a binary BLOB, not a sane GUID-bind target. - Drop the app-side `case SQL_C_BINARY` arm. That output direction is unreachable until column-side SQL_GUID mapping (T5-5 / #287) lands. convGuidToBinary stays (still used by the OCTETS wire branch). Verified: all 4 GuidParamBindingTest cases pass on FB 5.0.3; on the FB6 snapshot the 3 non-varying cases pass and the VARBINARY case skips; full suite shows no regressions. --- OdbcConvert.cpp | 27 ++++++++++------- tests/test_guid_and_binary.cpp | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/OdbcConvert.cpp b/OdbcConvert.cpp index 3ca9499c..c52758ea 100644 --- a/OdbcConvert.cpp +++ b/OdbcConvert.cpp @@ -1001,20 +1001,27 @@ ADRESS_FUNCTION OdbcConvert::getAdressFunction(DescRecord * from, DescRecord * t } break; case SQL_C_GUID: - // Wire-side (input parameter binding) — Firebird's IPD describes the slot - // as either BINARY(16) / CHAR(16) OCTETS, a text VARCHAR/CHAR, or (FB4+) - // genuine BINARY. Route to dedicated wire-only conversions; leave the - // pre-existing convGuidToString / convGuidToStringW untouched for the - // app-side fetch path below. + // Wire-side (input parameter binding) — Firebird's IPD describes the + // slot as either (VAR)BINARY(16) / (VAR)CHAR(16) CHARACTER SET OCTETS + // (subtype 1, sqllen 16) or a text VARCHAR/CHAR. Route to dedicated + // wire-only conversions; leave the pre-existing convGuidToString / + // convGuidToStringW untouched for the app-side fetch path below. if ( to->isIndicatorSqlDa ) { - // BINARY(16) / CHAR(16) CHARACTER SET OCTETS — 16 raw bytes + // BINARY(16) / VARBINARY(16) — i.e. (VAR)CHAR(16) CHARACTER SET + // OCTETS, which is how Firebird represents native FB4+ BINARY types + // on the wire (subtype 1, sqllen 16). Send 16 raw canonical bytes. + // setTypeText() converts a SQL_VARYING wire (VARBINARY) to SQL_TEXT + // so convGuidToBinary can write at offset 0 with no length prefix; + // without it Firebird reads the first GUID bytes as a VARYING length + // prefix and over-reads the buffer (SEGFAULT on FB6). A fixed + // CHAR(16) wire is already SQL_TEXT, so this is a no-op there. if ( to->headSqlVarPtr->getSqlSubtype() == 1 && to->headSqlVarPtr->getSqlLen() == 16 ) + { + to->headSqlVarPtr->setTypeText(); return &OdbcConvert::convGuidToBinary; - // FB4+ BINARY/VARBINARY described directly as SQL_C_BINARY - if ( to->conciseType == SQL_C_BINARY ) - return &OdbcConvert::convGuidToBinary; + } // Text wire (VARCHAR/CHAR, any charset) — 36-char canonical UUID. // setTypeText() converts SQL_VARYING to SQL_TEXT so convGuidToVarString // can write the bytes without a length prefix. @@ -1031,8 +1038,6 @@ ADRESS_FUNCTION OdbcConvert::getAdressFunction(DescRecord * from, DescRecord * t return &OdbcConvert::convGuidToString; case SQL_C_WCHAR: return &OdbcConvert::convGuidToStringW; - case SQL_C_BINARY: - return &OdbcConvert::convGuidToBinary; default: return &OdbcConvert::notYetImplemented; } diff --git a/tests/test_guid_and_binary.cpp b/tests/test_guid_and_binary.cpp index 0739596c..56619f85 100644 --- a/tests/test_guid_and_binary.cpp +++ b/tests/test_guid_and_binary.cpp @@ -608,6 +608,61 @@ TEST_F(GuidParamBindingTest, BindGuidToUuidToCharRoundtrip) { << "Got: '" << text << "' — this is the corruption pattern from issue #295."; } +// Bind SQL_C_GUID to a VARCHAR(16) CHARACTER SET OCTETS parameter — the +// wire form of a native FB4+ VARBINARY(16). Unlike CHAR(16) OCTETS (a +// fixed SQL_TEXT slot, covered by BindGuidToCharOctets16), this is a +// SQL_VARYING slot, so it exercises convGuidToBinary writing into a +// length-prefixed wire buffer. The dispatch calls setTypeText() to convert +// the varying wire to SQL_TEXT before writing 16 raw bytes; without that the +// first GUID bytes are read as a VARYING length prefix and the buffer is +// over-read (SEGFAULT on the FB6 snapshot, silent on FB5). +// +// Skipped on FB6: even with the fix, the current FB6 master snapshot aborts +// this parameterized OCTETS-VARYING insert with a server-side "Stack +// overflow" — the same parameterized-query incompatibility already guarded +// across the suite (see SKIP_ON_FIREBIRD6 usages). Exercised on FB 3/4/5. +TEST_F(GuidParamBindingTest, BindGuidToVarcharOctets16) { + REQUIRE_FIREBIRD_CONNECTION(); + SKIP_ON_FIREBIRD6(); + + TempTable table(this, "TEST_PB_GUID_VOCT", + "ID INTEGER NOT NULL PRIMARY KEY, " + "VAL VARCHAR(16) CHARACTER SET OCTETS"); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + SQLINTEGER id = 1; + SQLLEN idInd = sizeof(id); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_SLONG, SQL_INTEGER, 0, 0, &id, sizeof(id), &idInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"INSERT INTO TEST_PB_GUID_VOCT (ID, VAL) VALUES (?, ?)", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + Commit(); + ReallocStmt(); + + ExecDirect("SELECT UUID_TO_CHAR(VAL) FROM TEST_PB_GUID_VOCT WHERE ID = 1"); + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + SQLCHAR buf[64] = {}; + SQLLEN ind = 0; + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + std::string text((char*)buf); + while (!text.empty() && text.back() == ' ') text.pop_back(); + EXPECT_EQ(text, kCanonicalText) + << "VARBINARY(16) (VARCHAR(16) OCTETS) GUID round-trip failed. " + << "Got: '" << text << "'"; +} + // Test: DECFLOAT column insertion and retrieval on Firebird 4+ TEST_F(Fb4PlusTest, DecfloatInsertAndRetrieve) { GTEST_SKIP() << "Requires Phase 8: SQL_GUID type mapping and FB4+ types (not yet merged)"; From 1571a973f2d61046f396ce4b320271303abd63a3 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Wed, 27 May 2026 16:01:03 -0300 Subject: [PATCH 3/5] guid: address PR #296 review - helpers, named constants, error & buffer fixes Per irodushka's review (8 inline notes on PR #296): #1, dispatch structure - made the wire-side block fall through to an explicit `else` for the app-side switch (the wire-side branches already always-return, so this is purely structural clarity). #2/#3, "subtype==1 && sqllen==16" indistinct + hardcoded numbers - new inline accessors HeadSqlVar::isBinary() and isVarBinary() bundle the sqltype check (SQL_TEXT for BINARY, SQL_VARYING for VARBINARY) with the OCTETS-charset check, via a named CHARSET_OCTETS = 1 constant that documents the "for SQL_TEXT/SQL_VARYING, sqlsubtype is the CHARACTER SET id (charset 1 = OCTETS, per IscDbc/MultibyteConvert.cpp CODE_CHARSETS(OCTETS, 1, 1))" rule. Dispatch now reads: if ( getSqlLen() == GUID_BINARY_LEN && ( isBinary() || isVarBinary() ) ) GUID_BINARY_LEN (16) and GUID_STRING_LEN (36) named in OdbcConvert.h replace the inline 16/36/37 magic numbers in convGuidToBinary and convGuidToVarString. #5, convGuidToBinary silent truncation - changed the `outlen < 16` branch from a silent shortening + SQL_SUCCESS into a hard SQL_ERROR with diagnostic 22001 "String data, right truncation - GUID requires 16 bytes" via parentStmt->postError (same pattern the existing transferStringToAllowedType uses for 01004 truncation). Note in the comment that the dispatcher already enforces sqllen == 16, so this is defensive code for future callers. #6, hardcoded char tmp[37] - now char tmp[GUID_STRING_LEN + 1]. #7, convGuidToVarString buffer overflow - the previous code only allocated the local buffer when isLocalDataPtr was false, leaving a silent overflow path when a smaller default-sized buffer already existed (an untyped `?` placeholder is described by Firebird as VARCHAR(0), so DescRecord::allocateLocalDataPtr() with no length defaults to getBufferLength() = 1 byte). Dropped the guard and call allocateLocalDataPtr(GUID_STRING_LEN + 1) unconditionally; allocateLocalDataPtr() free()s any existing buffer first, so this is safe and always yields a 37-byte buffer. #4, dropped `to->conciseType == SQL_C_BINARY` arm - already done in the previous commit (84babbf) for the same reason irodushka gives; no code change in this commit. #8, "what about destination buffer length after setTypeText" - setTypeText() mutates the sqlvar so Sqlda::isExternalOverriden() fires and Sqlda::checkAndRebuild() allocates a fresh execBuffer from the new metadata; on top, convGuidToVarString redirects the wire's sqldata via setSqlData() to our oversized localDataPtr (now guaranteed by #7 to be 37 bytes), so the fresh exec buffer is bypassed entirely - the same idiom transferStringToAllowedType uses. No code change needed beyond #7. Verified: clean build (0 warnings); FB 5.0.3 runs all 4 GuidParamBindingTest cases green; FB 6 snapshot passes 3 and skips BindGuidToVarcharOctets16 (unchanged from 84babbf); full local suite 225 passed, 0 failed. --- IscDbc/Connection.h | 6 +++ IscDbc/IscHeadSqlVar.h | 22 ++++++++ OdbcConvert.cpp | 118 +++++++++++++++++++++++++---------------- OdbcConvert.h | 7 +++ 4 files changed, 107 insertions(+), 46 deletions(-) diff --git a/IscDbc/Connection.h b/IscDbc/Connection.h index 8ec28734..f6d570f0 100644 --- a/IscDbc/Connection.h +++ b/IscDbc/Connection.h @@ -527,6 +527,12 @@ class HeadSqlVar virtual short getSqlSubtype () = 0; virtual short getSqlLen () = 0; + // True for Firebird BINARY(n) / VARBINARY(n), i.e. CHAR(n) / VARCHAR(n) + // CHARACTER SET OCTETS at the wire layer (the FB4+ standard-compliant + // aliases). Implementation in IscHeadSqlVar.h. + virtual bool isBinary () = 0; + virtual bool isVarBinary () = 0; + virtual char * getSqlData() = 0; virtual short * getSqlInd() = 0; virtual void setSqlData( char *data ) = 0; diff --git a/IscDbc/IscHeadSqlVar.h b/IscDbc/IscHeadSqlVar.h index debbe95f..df802051 100644 --- a/IscDbc/IscHeadSqlVar.h +++ b/IscDbc/IscHeadSqlVar.h @@ -105,6 +105,28 @@ class IscHeadSqlVar : public HeadSqlVar inline char * getSqlData() { return sqlvar->sqldata; } inline short * getSqlInd() { return sqlvar->sqlind; } + // Charset id for CHARACTER SET OCTETS. In Firebird's XSQLVAR, when + // sqltype is SQL_TEXT or SQL_VARYING the sqlsubtype field carries the + // CHARACTER SET id (not a BLOB-style subtype); OCTETS is charset id 1 + // (see IscDbc/MultibyteConvert.cpp CODE_CHARSETS(OCTETS, 1, 1)). + static constexpr short CHARSET_OCTETS = 1; + + // True iff the slot describes a Firebird BINARY(n) — the FB4+ alias for + // CHAR(n) CHARACTER SET OCTETS. + inline bool isBinary() + { + return sqlvar->sqltype == SQL_TEXT + && sqlvar->sqlsubtype == CHARSET_OCTETS; + } + + // True iff the slot describes a Firebird VARBINARY(n) — the FB4+ alias + // for VARCHAR(n) CHARACTER SET OCTETS. + inline bool isVarBinary() + { + return sqlvar->sqltype == SQL_VARYING + && sqlvar->sqlsubtype == CHARSET_OCTETS; + } + // not used //void setSqlInd( short *ind ) { sqlvar->sqlind = ind; } //void setSqlType ( short type ) { sqlvar->sqltype = type; } diff --git a/OdbcConvert.cpp b/OdbcConvert.cpp index c52758ea..f6636543 100644 --- a/OdbcConvert.cpp +++ b/OdbcConvert.cpp @@ -1001,45 +1001,50 @@ ADRESS_FUNCTION OdbcConvert::getAdressFunction(DescRecord * from, DescRecord * t } break; case SQL_C_GUID: - // Wire-side (input parameter binding) — Firebird's IPD describes the - // slot as either (VAR)BINARY(16) / (VAR)CHAR(16) CHARACTER SET OCTETS - // (subtype 1, sqllen 16) or a text VARCHAR/CHAR. Route to dedicated - // wire-only conversions; leave the pre-existing convGuidToString / + // Wire-side (input parameter binding): Firebird's IPD describes the + // slot as either BINARY(16) / VARBINARY(16) — i.e. (VAR)CHAR(16) + // CHARACTER SET OCTETS, the FB4+ standard-compliant aliases per the + // LangRef — or a text (VAR)CHAR. Route to dedicated wire-only + // conversions; leave the pre-existing convGuidToString / // convGuidToStringW untouched for the app-side fetch path below. if ( to->isIndicatorSqlDa ) { - // BINARY(16) / VARBINARY(16) — i.e. (VAR)CHAR(16) CHARACTER SET - // OCTETS, which is how Firebird represents native FB4+ BINARY types - // on the wire (subtype 1, sqllen 16). Send 16 raw canonical bytes. - // setTypeText() converts a SQL_VARYING wire (VARBINARY) to SQL_TEXT - // so convGuidToBinary can write at offset 0 with no length prefix; - // without it Firebird reads the first GUID bytes as a VARYING length - // prefix and over-reads the buffer (SEGFAULT on FB6). A fixed - // CHAR(16) wire is already SQL_TEXT, so this is a no-op there. - if ( to->headSqlVarPtr->getSqlSubtype() == 1 - && to->headSqlVarPtr->getSqlLen() == 16 ) + // BINARY(16) or VARBINARY(16). setTypeText() converts a varying + // OCTETS wire (VARBINARY) to fixed SQL_TEXT so convGuidToBinary + // can write at offset 0 with no VARYING length prefix; OCTETS + // charset is preserved. For a fixed BINARY (SQL_TEXT already) + // it's a no-op. Without this, Firebird reads the first GUID + // bytes as a VARYING length prefix and over-reads the buffer + // (SEGFAULT on the FB6 snapshot, silent on FB5). + if ( to->headSqlVarPtr->getSqlLen() == GUID_BINARY_LEN + && ( to->headSqlVarPtr->isBinary() + || to->headSqlVarPtr->isVarBinary() ) ) { to->headSqlVarPtr->setTypeText(); return &OdbcConvert::convGuidToBinary; } - // Text wire (VARCHAR/CHAR, any charset) — 36-char canonical UUID. - // setTypeText() converts SQL_VARYING to SQL_TEXT so convGuidToVarString - // can write the bytes without a length prefix. + // Text wire (VARCHAR/CHAR, any non-OCTETS charset) — 36-char + // canonical UUID. setTypeText() converts SQL_VARYING to + // SQL_TEXT so convGuidToVarString can write the bytes without + // a length prefix. to->headSqlVarPtr->setTypeText(); return &OdbcConvert::convGuidToVarString; } - // App-side output (fetching a column described as SQL_GUID into the - // application's bound buffer). Currently unreachable because the column - // side of SQL_GUID mapping is task T5-5 (open), but kept intact for when - // that lands. - switch(to->conciseType) + else { - case SQL_C_CHAR: - return &OdbcConvert::convGuidToString; - case SQL_C_WCHAR: - return &OdbcConvert::convGuidToStringW; - default: - return &OdbcConvert::notYetImplemented; + // App-side output (fetching a column described as SQL_GUID into + // the application's bound buffer). Currently unreachable + // because the column-side SQL_GUID mapping is open task T5-5 + // (#287), but kept intact for when that lands. + switch(to->conciseType) + { + case SQL_C_CHAR: + return &OdbcConvert::convGuidToString; + case SQL_C_WCHAR: + return &OdbcConvert::convGuidToStringW; + default: + return &OdbcConvert::notYetImplemented; + } } break; @@ -1719,8 +1724,10 @@ int OdbcConvert::convGuidToStringW(DescRecord * from, DescRecord * to) return SQL_SUCCESS; } -// Write SQLGUID as 16 bytes in canonical UUID byte order (Data1/2/3 big-endian, Data4 unchanged). -// Used when the target is BINARY(16) / CHAR(16) CHARACTER SET OCTETS or SQL_C_BINARY. +// Write SQLGUID as 16 bytes in canonical UUID byte order (Data1/2/3 +// big-endian, Data4 unchanged). Used when the wire is BINARY(16) or +// VARBINARY(16) (i.e. (VAR)CHAR(16) CHARACTER SET OCTETS) — see +// getAdressFunction dispatch for SQL_C_GUID. int OdbcConvert::convGuidToBinary(DescRecord * from, DescRecord * to) { char* pointer = (char*)getAdressBindDataTo((char*)to->dataPtr); @@ -1731,7 +1738,7 @@ int OdbcConvert::convGuidToBinary(DescRecord * from, DescRecord * to) SQLGUID *g = (SQLGUID*)getAdressBindDataFrom((char*)from->dataPtr); - unsigned char buf[16]; + unsigned char buf[GUID_BINARY_LEN]; buf[0] = (unsigned char)((g->Data1 >> 24) & 0xFF); buf[1] = (unsigned char)((g->Data1 >> 16) & 0xFF); buf[2] = (unsigned char)((g->Data1 >> 8) & 0xFF); @@ -1742,27 +1749,39 @@ int OdbcConvert::convGuidToBinary(DescRecord * from, DescRecord * to) buf[7] = (unsigned char)( g->Data3 & 0xFF); memcpy(buf + 8, g->Data4, 8); + // The dispatcher only routes here when sqllen == GUID_BINARY_LEN, so + // this defensive check should not fire in practice — but a truncated + // GUID is corrupted (not just "shorter"), so signal it as a hard + // error rather than silently storing 8 (or whatever) bytes. ODBC + // SQLSTATE 22001 "String data, right truncation". int outlen = (int)to->length; - int len = outlen < 16 ? outlen : 16; + if ( outlen < GUID_BINARY_LEN ) + { + if ( parentStmt ) + parentStmt->postError( new OdbcError( 0, "22001", + "String data, right truncation - GUID requires 16 bytes" ) ); + return SQL_ERROR; + } - if ( len > 0 ) - memcpy(pointer, buf, len); + memcpy( pointer, buf, GUID_BINARY_LEN ); if ( to->isIndicatorSqlDa ) { - to->headSqlVarPtr->setSqlLen(len); + to->headSqlVarPtr->setSqlLen( GUID_BINARY_LEN ); } else if ( indicatorTo ) - setIndicatorPtr(indicatorTo, len, to); + setIndicatorPtr( indicatorTo, GUID_BINARY_LEN, to ); return SQL_SUCCESS; } // Write SQLGUID as the 36-char canonical UUID string into a Firebird text -// parameter slot (VARCHAR/CHAR of any text charset). The wire-side SQLDA -// buffer for an untyped `?` placeholder is sized for VARCHAR(0) — too small -// for 36 bytes — so we stage the string in the DescRecord's local buffer -// and redirect Firebird to read from there via setSqlData(), mirroring the -// idiom transferStringToAllowedType already uses for app→wire string moves. +// parameter slot (VARCHAR/CHAR of any non-OCTETS charset). The wire-side +// SQLDA buffer for an untyped `?` placeholder is described by Firebird as +// VARCHAR(0) — its default-sized local buffer is just 1 byte, nowhere near +// the 36 we need — so we stage the string in the DescRecord's local +// buffer (sized explicitly to GUID_STRING_LEN + 1) and redirect Firebird +// to read from there via setSqlData(), mirroring the idiom that +// transferStringToAllowedType already uses for app→wire string moves. // The dispatch in getAdressFunction has already called setTypeText() to // convert SQL_VARYING wires to SQL_TEXT (no length prefix), so we just // write 36 raw bytes and set sqllen. @@ -1775,16 +1794,23 @@ int OdbcConvert::convGuidToVarString(DescRecord * from, DescRecord * to) SQLGUID *g = (SQLGUID*)getAdressBindDataFrom((char*)from->dataPtr); - char tmp[37]; + char tmp[GUID_STRING_LEN + 1]; int srcLen = snprintf(tmp, sizeof(tmp), "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", (unsigned int) g->Data1, g->Data2, g->Data3, g->Data4[0], g->Data4[1], g->Data4[2], g->Data4[3], g->Data4[4], g->Data4[5], g->Data4[6], g->Data4[7]); - if ( srcLen < 0 || srcLen > 36 ) srcLen = 36; - - if ( !to->isLocalDataPtr ) - to->allocateLocalDataPtr( srcLen + 1 ); + if ( srcLen < 0 || srcLen > (int)GUID_STRING_LEN ) + srcLen = (int)GUID_STRING_LEN; + + // Always (re)allocate to guarantee the local buffer holds 36 chars + + // NUL. DescRecord::allocateLocalDataPtr() frees any existing buffer + // first, so this is safe to call unconditionally — and necessary, + // because the previous "if ( !to->isLocalDataPtr )" guard was unsafe + // when a smaller default-sized buffer already existed (an untyped `?` + // described as VARCHAR(0) yields a 1-byte default buffer, which the + // 36-byte memcpy would overflow). + to->allocateLocalDataPtr( (int)GUID_STRING_LEN + 1 ); memcpy(to->localDataPtr, tmp, srcLen); to->headSqlVarPtr->setSqlLen((short)srcLen); diff --git a/OdbcConvert.h b/OdbcConvert.h index a66486cd..033b69e0 100644 --- a/OdbcConvert.h +++ b/OdbcConvert.h @@ -48,6 +48,13 @@ class DescRecord; class OdbcConvert; class OdbcStatement; +// Canonical SQLGUID wire sizes. +// - GUID_BINARY_LEN: 16 raw bytes, the BINARY(16) / VARBINARY(16) wire form. +// - GUID_STRING_LEN: 36 chars (8-4-4-4-12 hex+dashes, no NUL terminator), +// the canonical text form written into VARCHAR/CHAR slots. +static constexpr int GUID_BINARY_LEN = 16; +static constexpr size_t GUID_STRING_LEN = 36; + typedef int (OdbcConvert::*ADRESS_FUNCTION)(DescRecord * from, DescRecord * to); class OdbcConvert From ccbcf93fe0896861115afcfb414017928492af4c Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Thu, 28 May 2026 12:44:15 -0300 Subject: [PATCH 4/5] guid: PR #296 review round 2 - sqlcharset, type unify, rename, cleanup Per irodushka's 2nd round on PR #296 (4 inline + 1 top-level): #1+#2, drop dead getSqlSubtype() - the only caller (the old dispatch check) is gone since the isBinary() / isVarBinary() refactor in 1571a97; verified by grep that nothing else references it. Drops the virtual decl in Connection.h and the inline impl in IscHeadSqlVar.h. #3, unify GUID_BINARY_LEN and GUID_STRING_LEN to int - one was int, the other size_t. Both are small positive byte/char counts used in mixed-signed arithmetic; consistent int matches the short->int promotion at the comparison sites and lets us drop the (int) casts inside convGuidToWireString. #4, fix the convGuidToVarString misnomer - rename to convGuidToWireString. The "Var" prefix suggested SQL_VARYING, but the function runs against a SQL_TEXT wire (setTypeText() runs in the dispatch right before the call). Rewrote the leading docstring to make the wire-vs-app distinction with convGuidToString explicit. #5 (issue comment 4561787816), switch isBinary() / isVarBinary() to check sqlvar->sqlcharset instead of sqlvar->sqlsubtype - cleaner. SqlProperties (IscDbc/Sqlda.h) carries the charset id in a dedicated sqlcharset field; sqlsubtype is documented as "BLOBs & Text types only" and is just overlaid with sqlcharset by CAttrSqlVar::bindProperties for compatibility. Reading the dedicated field eliminates the "subtype has two meanings" mental gymnastics noted in irodushka's round-1 thread. CHARSET_OCTETS is now `unsigned` to match the sqlcharset field type. Verified: clean build (0 warnings); FB 5.0.3 -> all 4 GuidParamBindingTest cases pass; FB6 snapshot -> 3 pass + BindGuidToVarcharOctets16 skipped (unchanged from 84babbf); full local suite 225 passed, 0 failed. --- IscDbc/Connection.h | 1 - IscDbc/IscHeadSqlVar.h | 18 ++++++++++-------- OdbcConvert.cpp | 38 +++++++++++++++++++++----------------- OdbcConvert.h | 6 +++--- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/IscDbc/Connection.h b/IscDbc/Connection.h index f6d570f0..dd11f2d8 100644 --- a/IscDbc/Connection.h +++ b/IscDbc/Connection.h @@ -524,7 +524,6 @@ class HeadSqlVar //virtual void setSqlSubType ( short subtype ) = 0; virtual void setSqlLen ( short len ) = 0; virtual short getSqlMultiple () = 0; - virtual short getSqlSubtype () = 0; virtual short getSqlLen () = 0; // True for Firebird BINARY(n) / VARBINARY(n), i.e. CHAR(n) / VARCHAR(n) diff --git a/IscDbc/IscHeadSqlVar.h b/IscDbc/IscHeadSqlVar.h index df802051..6d844493 100644 --- a/IscDbc/IscHeadSqlVar.h +++ b/IscDbc/IscHeadSqlVar.h @@ -100,23 +100,25 @@ class IscHeadSqlVar : public HeadSqlVar inline void setSqlData ( char* data ) { sqlvar->sqldata = data; } inline short getSqlMultiple () { return sqlMultiple; } - inline short getSqlSubtype () { return sqlvar->sqlsubtype; } inline short getSqlLen () { return sqlvar->sqllen; } inline char * getSqlData() { return sqlvar->sqldata; } inline short * getSqlInd() { return sqlvar->sqlind; } - // Charset id for CHARACTER SET OCTETS. In Firebird's XSQLVAR, when - // sqltype is SQL_TEXT or SQL_VARYING the sqlsubtype field carries the - // CHARACTER SET id (not a BLOB-style subtype); OCTETS is charset id 1 - // (see IscDbc/MultibyteConvert.cpp CODE_CHARSETS(OCTETS, 1, 1)). - static constexpr short CHARSET_OCTETS = 1; + // Charset id for CHARACTER SET OCTETS, used to identify Firebird's + // FB4+ BINARY / VARBINARY types at the wire layer. SqlProperties + // carries the charset id in a dedicated `sqlcharset` field (separate + // from `sqlsubtype`, which is documented as "BLOBs & Text types only"; + // see IscDbc/Sqlda.h). Charset id 1 = OCTETS per the driver's charset + // table at IscDbc/MultibyteConvert.cpp and per Firebird's + // RDB$CHARACTER_SETS system table. + static constexpr unsigned CHARSET_OCTETS = 1; // True iff the slot describes a Firebird BINARY(n) — the FB4+ alias for // CHAR(n) CHARACTER SET OCTETS. inline bool isBinary() { return sqlvar->sqltype == SQL_TEXT - && sqlvar->sqlsubtype == CHARSET_OCTETS; + && sqlvar->sqlcharset == CHARSET_OCTETS; } // True iff the slot describes a Firebird VARBINARY(n) — the FB4+ alias @@ -124,7 +126,7 @@ class IscHeadSqlVar : public HeadSqlVar inline bool isVarBinary() { return sqlvar->sqltype == SQL_VARYING - && sqlvar->sqlsubtype == CHARSET_OCTETS; + && sqlvar->sqlcharset == CHARSET_OCTETS; } // not used diff --git a/OdbcConvert.cpp b/OdbcConvert.cpp index f6636543..cd053bf1 100644 --- a/OdbcConvert.cpp +++ b/OdbcConvert.cpp @@ -1025,10 +1025,10 @@ ADRESS_FUNCTION OdbcConvert::getAdressFunction(DescRecord * from, DescRecord * t } // Text wire (VARCHAR/CHAR, any non-OCTETS charset) — 36-char // canonical UUID. setTypeText() converts SQL_VARYING to - // SQL_TEXT so convGuidToVarString can write the bytes without + // SQL_TEXT so convGuidToWireString can write the bytes without // a length prefix. to->headSqlVarPtr->setTypeText(); - return &OdbcConvert::convGuidToVarString; + return &OdbcConvert::convGuidToWireString; } else { @@ -1774,18 +1774,22 @@ int OdbcConvert::convGuidToBinary(DescRecord * from, DescRecord * to) return SQL_SUCCESS; } -// Write SQLGUID as the 36-char canonical UUID string into a Firebird text -// parameter slot (VARCHAR/CHAR of any non-OCTETS charset). The wire-side -// SQLDA buffer for an untyped `?` placeholder is described by Firebird as -// VARCHAR(0) — its default-sized local buffer is just 1 byte, nowhere near -// the 36 we need — so we stage the string in the DescRecord's local -// buffer (sized explicitly to GUID_STRING_LEN + 1) and redirect Firebird -// to read from there via setSqlData(), mirroring the idiom that -// transferStringToAllowedType already uses for app→wire string moves. -// The dispatch in getAdressFunction has already called setTypeText() to -// convert SQL_VARYING wires to SQL_TEXT (no length prefix), so we just -// write 36 raw bytes and set sqllen. -int OdbcConvert::convGuidToVarString(DescRecord * from, DescRecord * to) +// Wire-side counterpart of convGuidToString — writes SQLGUID as the 36-char +// canonical UUID into a Firebird text parameter slot (VARCHAR/CHAR of any +// non-OCTETS charset). Where convGuidToString writes into an app-supplied +// buffer that the application sized (the SQLGetData fetch path, currently +// dormant until column-side SQL_GUID mapping lands in T5-5 / #287), this +// writes into the *wire* slot — and for an untyped `?` placeholder Firebird +// describes that slot as VARCHAR(0), whose default-sized backing buffer is +// just 1 byte, nowhere near the 36 we need. +// +// So we stage the string in DescRecord::localDataPtr (sized explicitly to +// GUID_STRING_LEN + 1) and redirect Firebird to read from there via +// HeadSqlVar::setSqlData() — the same idiom transferStringToAllowedType +// uses for app→wire string moves. The dispatch has already called +// setTypeText() so the wire is SQL_TEXT (no length prefix); we just write +// 36 raw bytes and set sqllen. +int OdbcConvert::convGuidToWireString(DescRecord * from, DescRecord * to) { SQLLEN * indicatorFrom = getAdressBindIndFrom((char*)from->indicatorPtr); SQLLEN * indicatorTo = getAdressBindIndTo((char*)to->indicatorPtr); @@ -1800,8 +1804,8 @@ int OdbcConvert::convGuidToVarString(DescRecord * from, DescRecord * to) (unsigned int) g->Data1, g->Data2, g->Data3, g->Data4[0], g->Data4[1], g->Data4[2], g->Data4[3], g->Data4[4], g->Data4[5], g->Data4[6], g->Data4[7]); - if ( srcLen < 0 || srcLen > (int)GUID_STRING_LEN ) - srcLen = (int)GUID_STRING_LEN; + if ( srcLen < 0 || srcLen > GUID_STRING_LEN ) + srcLen = GUID_STRING_LEN; // Always (re)allocate to guarantee the local buffer holds 36 chars + // NUL. DescRecord::allocateLocalDataPtr() frees any existing buffer @@ -1810,7 +1814,7 @@ int OdbcConvert::convGuidToVarString(DescRecord * from, DescRecord * to) // when a smaller default-sized buffer already existed (an untyped `?` // described as VARCHAR(0) yields a 1-byte default buffer, which the // 36-byte memcpy would overflow). - to->allocateLocalDataPtr( (int)GUID_STRING_LEN + 1 ); + to->allocateLocalDataPtr( GUID_STRING_LEN + 1 ); memcpy(to->localDataPtr, tmp, srcLen); to->headSqlVarPtr->setSqlLen((short)srcLen); diff --git a/OdbcConvert.h b/OdbcConvert.h index 033b69e0..1c5087f1 100644 --- a/OdbcConvert.h +++ b/OdbcConvert.h @@ -52,8 +52,8 @@ class OdbcStatement; // - GUID_BINARY_LEN: 16 raw bytes, the BINARY(16) / VARBINARY(16) wire form. // - GUID_STRING_LEN: 36 chars (8-4-4-4-12 hex+dashes, no NUL terminator), // the canonical text form written into VARCHAR/CHAR slots. -static constexpr int GUID_BINARY_LEN = 16; -static constexpr size_t GUID_STRING_LEN = 36; +static constexpr int GUID_BINARY_LEN = 16; +static constexpr int GUID_STRING_LEN = 36; typedef int (OdbcConvert::*ADRESS_FUNCTION)(DescRecord * from, DescRecord * to); @@ -102,7 +102,7 @@ class OdbcConvert int convGuidToString(DescRecord * from, DescRecord * to); int convGuidToStringW(DescRecord * from, DescRecord * to); int convGuidToBinary(DescRecord * from, DescRecord * to); - int convGuidToVarString(DescRecord * from, DescRecord * to); + int convGuidToWireString(DescRecord * from, DescRecord * to); // TinyInt int convTinyIntToBoolean(DescRecord * from, DescRecord * to); From c8ae779d6629e1e235a93e055b304f3bce84a989 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Fri, 29 May 2026 13:57:54 -0300 Subject: [PATCH 5/5] guid: dedupe GUID string formatting, fix legacy snprintf error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per irodushka's review on PR #296: - Extract formatGuidCanonical() — both convGuidToString (app-side) and convGuidToWireString (wire-side) formatted the canonical 36-char UUID with their own copy of the 11-arg snprintf format string. One helper now owns the format; the two functions keep only their distinct destination handling (app buffer vs staged localDataPtr + setSqlData). - Fix the `if ( len == -1 ) len = outlen;` bug in convGuidToString. snprintf returning -1 is an encoding error; setting len to the full buffer size reported uninitialized bytes as valid data. A canonical GUID is always exactly GUID_STRING_LEN (36) chars, so anything else is a hard error now (SQLSTATE HY000), and a destination buffer too small for 36 + NUL raises 01004 truncation with the full length reported via the indicator — matching the ODBC "C to SQL: GUID" conversion contract. - Apply the same principle to convGuidToWireString: drop the clamp-and-continue (`srcLen > GUID_STRING_LEN ? ...`) and treat a non-36 format result as a hard error instead of silently proceeding. No behaviour change on the happy path; convGuidToString remains dormant (app-side fetch, pending T5-5 / #287). Verified: clean build (0 warnings); FB 5.0.3 all 4 GuidParamBindingTest pass; FB6 snapshot 3 pass + VARBINARY skip; full suite 225 passed, 0 failed. --- OdbcConvert.cpp | 70 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/OdbcConvert.cpp b/OdbcConvert.cpp index cd053bf1..87d0e918 100644 --- a/OdbcConvert.cpp +++ b/OdbcConvert.cpp @@ -1674,6 +1674,21 @@ int OdbcConvert::notYetImplemented(DescRecord * from, DescRecord * to) // Guid //////////////////////////////////////////////////////////////////////// +// Format a SQLGUID as the 36-char canonical UUID string (8-4-4-4-12 hex, +// upper-case, no NUL counted). `out` must hold at least GUID_STRING_LEN + 1 +// bytes. Returns snprintf's character count — always GUID_STRING_LEN for a +// well-formed call, or negative on a (practically impossible) encoding error. +// A GUID never formats to any other length, so callers treat +// "result != GUID_STRING_LEN" as a hard error rather than clamping. +static int formatGuidCanonical(char* out, size_t cap, const SQLGUID* g) +{ + return snprintf(out, cap, + "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", + (unsigned int) g->Data1, g->Data2, g->Data3, + g->Data4[0], g->Data4[1], g->Data4[2], g->Data4[3], + g->Data4[4], g->Data4[5], g->Data4[6], g->Data4[7]); +} + int OdbcConvert::convGuidToString(DescRecord * from, DescRecord * to) { char* pointer = (char*)getAdressBindDataTo((char*)to->dataPtr); @@ -1683,20 +1698,39 @@ int OdbcConvert::convGuidToString(DescRecord * from, DescRecord * to) ODBCCONVERT_CHECKNULL( pointer ); SQLGUID *g = (SQLGUID*)getAdressBindDataFrom((char*)from->dataPtr); - int len, outlen = to->length; - len = snprintf(pointer, outlen, "%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", - (unsigned int) g->Data1, g->Data2, g->Data3, g->Data4[0], g->Data4[1], g->Data4[2], g->Data4[3], g->Data4[4], g->Data4[5], g->Data4[6], g->Data4[7]); + char buf[GUID_STRING_LEN + 1]; + if ( formatGuidCanonical(buf, sizeof(buf), g) != GUID_STRING_LEN ) + { + if ( parentStmt ) + parentStmt->postError( new OdbcError( 0, "HY000", "Internal error formatting GUID" ) ); + return SQL_ERROR; + } - if ( len == -1 ) len = outlen; + // Copy into the application buffer honoring its size. ODBC reports the + // full (untruncated) length via the indicator and raises 01004 when the + // buffer cannot hold all 36 chars + NUL terminator. + int outlen = to->length; + SQLRETURN ret = SQL_SUCCESS; + int copy = GUID_STRING_LEN; + if ( outlen <= GUID_STRING_LEN ) + { + copy = outlen > 0 ? outlen - 1 : 0; + if ( parentStmt ) + parentStmt->postError( new OdbcError( 0, "01004", "Data truncated" ) ); + ret = SQL_SUCCESS_WITH_INFO; + } + if ( copy > 0 ) + memcpy( pointer, buf, copy ); + pointer[copy] = 0; if ( to->isIndicatorSqlDa ) { - to->headSqlVarPtr->setSqlLen(len); + to->headSqlVarPtr->setSqlLen( (short)GUID_STRING_LEN ); } else if ( indicatorTo ) - setIndicatorPtr(indicatorTo, len, to); + setIndicatorPtr( indicatorTo, GUID_STRING_LEN, to ); - return SQL_SUCCESS; + return ret; } int OdbcConvert::convGuidToStringW(DescRecord * from, DescRecord * to) @@ -1799,25 +1833,23 @@ int OdbcConvert::convGuidToWireString(DescRecord * from, DescRecord * to) SQLGUID *g = (SQLGUID*)getAdressBindDataFrom((char*)from->dataPtr); char tmp[GUID_STRING_LEN + 1]; - int srcLen = snprintf(tmp, sizeof(tmp), - "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", - (unsigned int) g->Data1, g->Data2, g->Data3, - g->Data4[0], g->Data4[1], g->Data4[2], g->Data4[3], - g->Data4[4], g->Data4[5], g->Data4[6], g->Data4[7]); - if ( srcLen < 0 || srcLen > GUID_STRING_LEN ) - srcLen = GUID_STRING_LEN; + if ( formatGuidCanonical(tmp, sizeof(tmp), g) != GUID_STRING_LEN ) + { + if ( parentStmt ) + parentStmt->postError( new OdbcError( 0, "HY000", "Internal error formatting GUID" ) ); + return SQL_ERROR; + } // Always (re)allocate to guarantee the local buffer holds 36 chars + // NUL. DescRecord::allocateLocalDataPtr() frees any existing buffer // first, so this is safe to call unconditionally — and necessary, - // because the previous "if ( !to->isLocalDataPtr )" guard was unsafe - // when a smaller default-sized buffer already existed (an untyped `?` - // described as VARCHAR(0) yields a 1-byte default buffer, which the + // because a smaller default-sized buffer may already exist (an untyped + // `?` described as VARCHAR(0) yields a 1-byte default buffer, which the // 36-byte memcpy would overflow). to->allocateLocalDataPtr( GUID_STRING_LEN + 1 ); - memcpy(to->localDataPtr, tmp, srcLen); - to->headSqlVarPtr->setSqlLen((short)srcLen); + memcpy(to->localDataPtr, tmp, GUID_STRING_LEN); + to->headSqlVarPtr->setSqlLen((short)GUID_STRING_LEN); to->headSqlVarPtr->setSqlData(to->localDataPtr); return SQL_SUCCESS;