Skip to content

Commit a776d6e

Browse files
committed
Add widechar ODBC call tests (SQLGetDiagRecW / SQLErrorW / SQLExecDirectW / SQLPrepareW)
Exercises both ConvertingString constructor paths in MainUnicode.cpp: output buffer (SQLGetDiagRecW / SQLErrorW) and input buffer (SQLExecDirectW / SQLPrepareW). Linux tests skip with a pointer to the follow-up Unicode rewrite PR; they are meant to drive that PR's implementation. Windows runs them for real. Per the plan in #289 comment 4279111183. Tracked as issue #287 Tier 1b.
1 parent eb7072b commit a776d6e

3 files changed

Lines changed: 291 additions & 0 deletions

File tree

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ add_executable(firebird_odbc_tests
8686
test_catalogfunctions.cpp
8787
test_server_version.cpp
8888
test_scrollable_cursor.cpp
89+
test_wide_errors.cpp
8990

9091
# Category C — all tests SKIP'd (features not yet on upstream master)
9192
test_null_handles.cpp

tests/test_helpers.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,38 @@ inline std::string GetSqlState(SQLSMALLINT handleType, SQLHANDLE handle) {
6060
return "";
6161
}
6262

63+
// Convert an ASCII C-string to a null-terminated SQLWCHAR vector. Cannot use
64+
// L"..." literals — sizeof(wchar_t) != sizeof(SQLWCHAR) on Linux.
65+
inline std::vector<SQLWCHAR> ToSqlWchar(const char* s) {
66+
std::vector<SQLWCHAR> out;
67+
while (*s) {
68+
out.push_back((SQLWCHAR)(unsigned char)*s++);
69+
}
70+
out.push_back(0);
71+
return out;
72+
}
73+
74+
// Convert a null-terminated SQLWCHAR string to a narrow std::string (low byte
75+
// only, so ASCII round-trips correctly; non-ASCII is lossy but these helpers
76+
// are for tests, not production data).
77+
inline std::string FromSqlWchar(const SQLWCHAR* s) {
78+
std::string out;
79+
if (!s) return out;
80+
while (*s) {
81+
out.push_back((char)(*s & 0xFF));
82+
++s;
83+
}
84+
return out;
85+
}
86+
87+
// Length of a null-terminated SQLWCHAR string, in SQLWCHAR units.
88+
inline size_t SqlWcharLen(const SQLWCHAR* s) {
89+
size_t n = 0;
90+
if (!s) return 0;
91+
while (s[n]) ++n;
92+
return n;
93+
}
94+
6395
// Base test fixture: ODBC environment + connection + auto-cleanup
6496
class OdbcConnectedTest : public ::testing::Test {
6597
public:

tests/test_wide_errors.cpp

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// tests/test_wide_errors.cpp — Widechar ODBC call tests
2+
//
3+
// Drives the fix for the Linux widechar conversion bugs in MainUnicode.cpp.
4+
// Covers both ConvertingString constructor scenarios:
5+
//
6+
// (a) Output path used by SQLGetDiagRecW / SQLErrorW
7+
// ConvertingString(length, sqlState) -> exercised by
8+
// GetDiagRecW_* and ErrorW_* tests below.
9+
//
10+
// (b) Input path used by SQLExecDirectW / SQLPrepareW
11+
// ConvertingString(connection, wcString, length) +
12+
// convUnicodeToString -> exercised by ExecDirectW_* /
13+
// PrepareW_* tests below.
14+
//
15+
// On Linux the current driver mis-handles both paths — the widechar
16+
// read-back is truncated to one SQLWCHAR, and a small output buffer can
17+
// smash the caller's stack. These tests are therefore SKIP'd on Linux
18+
// with a pointer to the follow-up Unicode-fix PR. Remove the skips once
19+
// that PR lands.
20+
21+
#include "test_helpers.h"
22+
#include <cstring>
23+
#include <string>
24+
#include <vector>
25+
26+
#ifndef _WIN32
27+
// Shared skip body — all tests in this file depend on the same Linux
28+
// widechar bug. Keep the message pointing at issue #287 Tier 1b so
29+
// grepping finds every affected test at once.
30+
#define SKIP_ON_LINUX_WIDECHAR() \
31+
do { \
32+
GTEST_SKIP() << "Widechar conversion paths broken on Linux — " \
33+
"see issue #287 Tier 1b. Un-skip once the " \
34+
"Unicode-fix PR lands."; \
35+
} while (0)
36+
#else
37+
#define SKIP_ON_LINUX_WIDECHAR() do {} while (0)
38+
#endif
39+
40+
class WideErrorsTest : public OdbcConnectedTest {};
41+
42+
// ============================================================================
43+
// (a) Output path — SQLGetDiagRecW on an error
44+
// ============================================================================
45+
46+
// Force a parse error, then read the SQLSTATE back via the widechar variant
47+
// with a tight 12-byte (6-SQLWCHAR) output buffer. This is the exact shape of
48+
// the ConvertingString(12, sqlState) construction inside SQLGetDiagRecW.
49+
TEST_F(WideErrorsTest, GetDiagRecW_SqlState) {
50+
SKIP_ON_LINUX_WIDECHAR();
51+
52+
// Trigger an error (column doesn't exist)
53+
SQLRETURN ret = SQLExecDirect(hStmt,
54+
(SQLCHAR*)"SELECT doesnotexist FROM RDB$DATABASE", SQL_NTS);
55+
ASSERT_FALSE(SQL_SUCCEEDED(ret));
56+
57+
SQLWCHAR sqlState[6] = {}; // 12 bytes, fits "HY000\0"
58+
SQLINTEGER nativeError = 0;
59+
SQLWCHAR messageBuf[SQL_MAX_MESSAGE_LENGTH] = {};
60+
SQLSMALLINT messageLen = 0;
61+
62+
ret = SQLGetDiagRecW(SQL_HANDLE_STMT, hStmt, 1,
63+
sqlState, &nativeError,
64+
messageBuf, SQL_MAX_MESSAGE_LENGTH, &messageLen);
65+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
66+
<< "SQLGetDiagRecW failed: "
67+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
68+
69+
// SQLSTATE is exactly 5 characters.
70+
EXPECT_EQ(SqlWcharLen(sqlState), 5u)
71+
<< "State decoded as: '" << FromSqlWchar(sqlState) << "'";
72+
73+
// Firebird returns 42S22 (column not found) or 42000 or HY000.
74+
std::string state = FromSqlWchar(sqlState);
75+
EXPECT_TRUE(state == "42S22" || state == "42000" || state == "HY000")
76+
<< "Unexpected SQLSTATE: '" << state << "'";
77+
}
78+
79+
// Same as above but checks the message buffer, which uses the other
80+
// ConvertingString size (bufferLength can be large). This exercises the
81+
// widechar path with a reasonable output buffer.
82+
TEST_F(WideErrorsTest, GetDiagRecW_Message) {
83+
SKIP_ON_LINUX_WIDECHAR();
84+
85+
SQLRETURN ret = SQLExecDirect(hStmt,
86+
(SQLCHAR*)"SELECT doesnotexist FROM RDB$DATABASE", SQL_NTS);
87+
ASSERT_FALSE(SQL_SUCCEEDED(ret));
88+
89+
SQLWCHAR sqlState[6] = {};
90+
SQLINTEGER nativeError = 0;
91+
SQLWCHAR messageBuf[512] = {};
92+
SQLSMALLINT messageLen = 0;
93+
94+
ret = SQLGetDiagRecW(SQL_HANDLE_STMT, hStmt, 1,
95+
sqlState, &nativeError,
96+
messageBuf, 512, &messageLen);
97+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
98+
99+
// Message must be non-empty and readable as ASCII.
100+
std::string msg = FromSqlWchar(messageBuf);
101+
EXPECT_FALSE(msg.empty()) << "Error message was empty";
102+
103+
// And the driver must report the length correctly (in SQLWCHAR units
104+
// according to the spec).
105+
EXPECT_GT(messageLen, 0);
106+
}
107+
108+
// Deliberately tight output buffer — the driver must not overrun it.
109+
// With sqlState sized at 6 SQLWCHARs (12 bytes) this is exactly the shape
110+
// that originally triggered the heap-buffer-overflow / stack-smash pair.
111+
TEST_F(WideErrorsTest, GetDiagRecW_SmallBuffer) {
112+
SKIP_ON_LINUX_WIDECHAR();
113+
114+
SQLRETURN ret = SQLExecDirect(hStmt,
115+
(SQLCHAR*)"SELECT doesnotexist FROM RDB$DATABASE", SQL_NTS);
116+
ASSERT_FALSE(SQL_SUCCEEDED(ret));
117+
118+
// Guard bytes either side; any write outside sqlState[] will trip them.
119+
SQLWCHAR guardBefore[4];
120+
SQLWCHAR sqlState[6] = {};
121+
SQLWCHAR guardAfter[4];
122+
for (int i = 0; i < 4; ++i) {
123+
guardBefore[i] = (SQLWCHAR)0xBEEF;
124+
guardAfter[i] = (SQLWCHAR)0xBEEF;
125+
}
126+
127+
SQLINTEGER nativeError = 0;
128+
SQLWCHAR messageBuf[16] = {}; // intentionally tiny
129+
SQLSMALLINT messageLen = 0;
130+
131+
ret = SQLGetDiagRecW(SQL_HANDLE_STMT, hStmt, 1,
132+
sqlState, &nativeError,
133+
messageBuf, 16, &messageLen);
134+
ASSERT_TRUE(SQL_SUCCEEDED(ret) || ret == SQL_SUCCESS_WITH_INFO);
135+
136+
// Guards must be untouched.
137+
for (int i = 0; i < 4; ++i) {
138+
EXPECT_EQ(guardBefore[i], (SQLWCHAR)0xBEEF) << "guardBefore[" << i << "]";
139+
EXPECT_EQ(guardAfter[i], (SQLWCHAR)0xBEEF) << "guardAfter[" << i << "]";
140+
}
141+
142+
// State still decodes to 5 characters.
143+
EXPECT_EQ(SqlWcharLen(sqlState), 5u)
144+
<< "State decoded as: '" << FromSqlWchar(sqlState) << "'";
145+
}
146+
147+
// Legacy ODBC 2.x SQLErrorW — same underlying ConvertingString(12, sqlState)
148+
// shape. Kept so the fix also covers that call site.
149+
TEST_F(WideErrorsTest, ErrorW_SqlState) {
150+
SKIP_ON_LINUX_WIDECHAR();
151+
152+
SQLRETURN ret = SQLExecDirect(hStmt,
153+
(SQLCHAR*)"SELECT doesnotexist FROM RDB$DATABASE", SQL_NTS);
154+
ASSERT_FALSE(SQL_SUCCEEDED(ret));
155+
156+
SQLWCHAR sqlState[6] = {};
157+
SQLINTEGER nativeError = 0;
158+
SQLWCHAR messageBuf[512] = {};
159+
SQLSMALLINT messageLen = 0;
160+
161+
ret = SQLErrorW(hEnv, hDbc, hStmt,
162+
sqlState, &nativeError,
163+
messageBuf, 512, &messageLen);
164+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
165+
166+
EXPECT_EQ(SqlWcharLen(sqlState), 5u)
167+
<< "State decoded as: '" << FromSqlWchar(sqlState) << "'";
168+
}
169+
170+
// ============================================================================
171+
// (b) Input path — SQLExecDirectW / SQLPrepareW
172+
// ============================================================================
173+
174+
class WideExecTest : public OdbcConnectedTest {};
175+
176+
// Pass an ASCII query as SQLWCHAR with SQL_NTS — exercises the convUnicodeToString
177+
// path's sqlwcharLen-equivalent branch (currently wcslen on a SQLWCHAR pointer,
178+
// which is wrong on Linux).
179+
TEST_F(WideExecTest, ExecDirectW_Ascii_Nts) {
180+
SKIP_ON_LINUX_WIDECHAR();
181+
182+
auto query = ToSqlWchar("SELECT 1 FROM RDB$DATABASE");
183+
SQLRETURN ret = SQLExecDirectW(hStmt, query.data(), SQL_NTS);
184+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
185+
<< "SQLExecDirectW failed: "
186+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
187+
188+
SQLINTEGER val = 0;
189+
SQLLEN ind = 0;
190+
SQLBindCol(hStmt, 1, SQL_C_SLONG, &val, 0, &ind);
191+
ret = SQLFetch(hStmt);
192+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
193+
EXPECT_EQ(val, 1);
194+
}
195+
196+
// Same but with an explicit SQLWCHAR-unit length — exercises the other branch
197+
// of convUnicodeToString where we temporarily NUL-terminate input.
198+
TEST_F(WideExecTest, ExecDirectW_Ascii_ExplicitLength) {
199+
SKIP_ON_LINUX_WIDECHAR();
200+
201+
auto query = ToSqlWchar("SELECT 1 FROM RDB$DATABASE");
202+
// Length is in characters (SQLWCHAR units), per the spec.
203+
SQLINTEGER len = (SQLINTEGER)(query.size() - 1); // exclude trailing NUL
204+
205+
SQLRETURN ret = SQLExecDirectW(hStmt, query.data(), len);
206+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
207+
<< "SQLExecDirectW(len=" << len << ") failed: "
208+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
209+
210+
SQLINTEGER val = 0;
211+
SQLLEN ind = 0;
212+
SQLBindCol(hStmt, 1, SQL_C_SLONG, &val, 0, &ind);
213+
ret = SQLFetch(hStmt);
214+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
215+
EXPECT_EQ(val, 1);
216+
}
217+
218+
// SQLPrepareW twin — same input path as ExecDirectW.
219+
TEST_F(WideExecTest, PrepareW_Ascii_Nts) {
220+
SKIP_ON_LINUX_WIDECHAR();
221+
222+
auto query = ToSqlWchar("SELECT 1 FROM RDB$DATABASE");
223+
SQLRETURN ret = SQLPrepareW(hStmt, query.data(), SQL_NTS);
224+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
225+
<< "SQLPrepareW failed: "
226+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
227+
228+
ret = SQLExecute(hStmt);
229+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
230+
231+
SQLINTEGER val = 0;
232+
SQLLEN ind = 0;
233+
SQLBindCol(hStmt, 1, SQL_C_SLONG, &val, 0, &ind);
234+
ret = SQLFetch(hStmt);
235+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
236+
EXPECT_EQ(val, 1);
237+
}
238+
239+
// A query long enough that the narrow byteString allocation has to grow.
240+
// If the length accounting is wrong in SQLWCHAR vs wchar_t units, this is
241+
// where truncation/overflow bugs surface.
242+
TEST_F(WideExecTest, ExecDirectW_Ascii_LongQuery) {
243+
SKIP_ON_LINUX_WIDECHAR();
244+
245+
// 200+ byte query so the byteString buffer is a meaningful size.
246+
std::string q = "SELECT ";
247+
for (int i = 0; i < 40; ++i) q += "1+";
248+
q += "0 FROM RDB$DATABASE";
249+
250+
auto query = ToSqlWchar(q.c_str());
251+
SQLRETURN ret = SQLExecDirectW(hStmt, query.data(), SQL_NTS);
252+
ASSERT_TRUE(SQL_SUCCEEDED(ret))
253+
<< "SQLExecDirectW(long) failed: "
254+
<< GetOdbcError(SQL_HANDLE_STMT, hStmt);
255+
256+
ret = SQLFetch(hStmt);
257+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
258+
}

0 commit comments

Comments
 (0)