Skip to content

Commit a161c33

Browse files
authored
Fixed a bug in reading varbinary max fields (#1209)
2 parents 537ae2d + 6b1923c commit a161c33

File tree

5 files changed

+495
-63
lines changed

5 files changed

+495
-63
lines changed

source/shared/core_results.cpp

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,7 @@ SQLRETURN binary_to_string( _Inout_ SQLCHAR* field_data, _Inout_ SQLLEN& read_so
882882
_In_ SQLLEN buffer_length, _Inout_ SQLLEN* out_buffer_length,
883883
_Inout_ sqlsrv_error_auto_ptr& out_error )
884884
{
885-
// hex characters for the conversion loop below
885+
// The hex characters for the conversion loop below
886886
static char hex_chars[] = "0123456789ABCDEF";
887887

888888
SQLSRV_ASSERT( out_error == 0, "Pending error for sqlsrv_buffered_results_set::binary_to_string" );
@@ -892,17 +892,19 @@ SQLRETURN binary_to_string( _Inout_ SQLCHAR* field_data, _Inout_ SQLLEN& read_so
892892
// Set the amount of space necessary for null characters at the end of the data.
893893
SQLSMALLINT extra = sizeof(Char);
894894

895-
SQLSRV_ASSERT( ((buffer_length - extra) % (extra * 2)) == 0, "Must be multiple of 2 for binary to system string or "
896-
"multiple of 4 for binary to wide string" );
895+
// TO convert a binary to a system string or a binary to a wide string, the buffer size minus
896+
// 'extra' is ideally multiples of 2 or 4 (depending on Char), but calculating to_copy_hex below
897+
// takes care of this.
897898

898-
// all fields will be treated as ODBC returns varchar(max) fields:
899+
// All fields will be treated as ODBC returns varchar(max) fields:
899900
// the entire length of the string is returned the first
900901
// call in out_buffer_len. Successive calls return how much is
901902
// left minus how much has already been read by previous reads
902-
// *2 is for each byte to hex conversion and * extra is for either system or wide string allocation
903+
// *2 is for each byte to hex conversion and * extra is for either system
904+
// or wide string allocation
903905
*out_buffer_length = (*reinterpret_cast<SQLLEN*>( field_data - sizeof( SQLULEN )) - read_so_far) * 2 * extra;
904906

905-
// copy as much as we can into the buffer
907+
// Will copy as much as we can into the buffer
906908
SQLLEN to_copy;
907909
if( buffer_length < *out_buffer_length + extra ) {
908910
to_copy = (buffer_length - extra);
@@ -915,22 +917,22 @@ SQLRETURN binary_to_string( _Inout_ SQLCHAR* field_data, _Inout_ SQLLEN& read_so
915917
to_copy = *out_buffer_length;
916918
}
917919

918-
// if there are bytes to copy as hex
920+
// If there are bytes to copy as hex
919921
if( to_copy > 0 ) {
920922
// quick hex conversion routine
921-
Char* h = reinterpret_cast<Char*>( buffer );
922-
BYTE* b = reinterpret_cast<BYTE*>( field_data );
923+
Char* h = reinterpret_cast<Char*>(buffer);
924+
BYTE* b = reinterpret_cast<BYTE*>(field_data + read_so_far);
923925
// to_copy contains the number of bytes to copy, so we divide the number in half (or quarter)
924-
// to get the number of hex digits we can copy
925-
SQLLEN to_copy_hex = to_copy / (2 * extra);
926+
// to get the maximum number of hex digits to copy
927+
SQLLEN to_copy_hex = static_cast<SQLLEN>(floor(to_copy / (2 * extra)));
926928
for( SQLLEN i = 0; i < to_copy_hex; ++i ) {
927929
*h = hex_chars[(*b & 0xf0) >> 4];
928930
h++;
929931
*h = hex_chars[(*b++ & 0x0f)];
930932
h++;
931933
}
932934
read_so_far += to_copy_hex;
933-
*h = static_cast<Char>( 0 );
935+
*h = static_cast<Char>(0);
934936
}
935937
else {
936938
reinterpret_cast<char*>( buffer )[0] = '\0';

source/shared/core_stream.cpp

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,18 @@ size_t sqlsrv_stream_read(_Inout_ php_stream* stream, _Out_writes_bytes_(count)
101101
throw core::CoreException();
102102
}
103103

104-
// if the stream returns either no data, NULL data, or returns data < than the count requested then
105-
// we are at the "end of the stream" so we mark it
106-
if( r == SQL_NO_DATA || read == SQL_NULL_DATA || ( static_cast<size_t>( read ) <= count && read != SQL_NO_TOTAL )) {
104+
// If the stream returns no data or NULL data, mark the "end of the stream" and return
105+
if( r == SQL_NO_DATA || read == SQL_NULL_DATA) {
106+
stream->eof = 1;
107+
return 0;
108+
}
109+
110+
// If the stream returns data less than the count requested then we are at the "end of the stream" but continue processing
111+
if (static_cast<size_t>(read) <= count && read != SQL_NO_TOTAL) {
107112
stream->eof = 1;
108113
}
109114

110-
// if ODBC returns the 01004 (truncated string) warning, then we return the count minus the null terminator
115+
// If ODBC returns the 01004 (truncated string) warning, then we return the count minus the null terminator
111116
// if it's not a binary encoded field
112117
if( r == SQL_SUCCESS_WITH_INFO ) {
113118

@@ -120,26 +125,42 @@ size_t sqlsrv_stream_read(_Inout_ php_stream* stream, _Out_writes_bytes_(count)
120125
SQLSRV_ASSERT( is_truncated_warning( state ), "sqlsrv_stream_read: truncation warning was expected but it "
121126
"did not occur." );
122127
}
123-
124-
// with unixODBC connection pooling enabled the truncated state may not be returned so check the actual length read
125-
// with buffer length.
128+
129+
// As per SQLGetData documentation, if the length of character data exceeds the BufferLength,
130+
// SQLGetData truncates the data to BufferLength less the length of null-termination character.
131+
// But when fetching binary fields as chars (wide chars), each byte is represented as 2 hex characters,
132+
// each takes the size of a char (wide char). Note that BufferLength may not be multiples of 2 or 4.
133+
bool is_binary = (ss->sql_type == SQL_BINARY || ss->sql_type == SQL_VARBINARY || ss->sql_type == SQL_LONGVARBINARY);
134+
135+
// With unixODBC connection pooling enabled the truncated state may not be returned so check the actual length read
136+
// with buffer length.
126137
#ifndef _WIN32
127138
if( is_truncated_warning( state ) || count < read) {
128139
#else
129140
if( is_truncated_warning( state ) ) {
130141
#endif // !_WIN32
142+
size_t char_size = sizeof(SQLCHAR);
143+
131144
switch( c_type ) {
132-
133-
// As per SQLGetData documentation, if the length of character data exceeds the BufferLength,
134-
// SQLGetData truncates the data to BufferLength less the length of null-termination character.
135145
case SQL_C_BINARY:
136146
read = count;
137147
break;
138148
case SQL_C_WCHAR:
139-
read = ( count % 2 == 0 ? count - 2 : count - 3 );
149+
char_size = sizeof(SQLWCHAR);
150+
if (is_binary) {
151+
// Each binary byte read will be 2 hex wide chars in the buffer
152+
SQLLEN num_bytes_read = static_cast<SQLLEN>(floor((count - char_size) / (2 * char_size)));
153+
read = num_bytes_read * char_size * 2 ;
154+
} else {
155+
read = (count % 2 == 0 ? count - 2 : count - 3);
156+
}
140157
break;
141158
case SQL_C_CHAR:
142-
read = count - 1;
159+
if (is_binary) {
160+
read = ((count - char_size) % 2 == 0 ? count - char_size : count - char_size - 1);
161+
} else {
162+
read = count - 1;
163+
}
143164
break;
144165
default:
145166
DIE( "sqlsrv_stream_read: should have never reached in this switch case.");
@@ -151,10 +172,10 @@ size_t sqlsrv_stream_read(_Inout_ php_stream* stream, _Out_writes_bytes_(count)
151172
}
152173
}
153174

154-
// if the encoding is UTF-8
175+
// If the encoding is UTF-8
155176
if( c_type == SQL_C_WCHAR ) {
156177
count *= 2;
157-
// undo the shift to use the full buffer
178+
// Undo the shift to use the full buffer
158179
// flags set to 0 by default, which means that any invalid characters are dropped rather than causing
159180
// an error. This happens only on XP.
160181
// convert to UTF-8
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
--TEST--
2+
Test fetching varbinary, varchar, nvarchar max fields with client buffer
3+
--DESCRIPTION--
4+
Similar to sqlsrv_fetch_large_stream test but fetching varbinary, varchar, nvarchar max fields as strings with or without client buffer
5+
--SKIPIF--
6+
<?php require_once('skipif_mid-refactor.inc'); ?>
7+
--ENV--
8+
PHPT_EXEC=true
9+
--FILE--
10+
<?php
11+
require_once("MsCommon_mid-refactor.inc");
12+
13+
$tableName = 'pdoFetchLobTest';
14+
$binaryColumn = 'varbinary_max';
15+
$strColumn = 'varchar_max';
16+
$nstrColumn = 'nvarchar_max';
17+
18+
$bin = 'abcdefghijklmnopqrstuvwxyz';
19+
$binaryValue = str_repeat($bin, 100);
20+
$hexValue = str_repeat(strtoupper(bin2hex($bin)), 100);
21+
$strValue = str_repeat("stuvwxyz", 400);
22+
$nstrValue = str_repeat("ÃÜðßZZýA©", 200);
23+
24+
function checkData($actual, $expected)
25+
{
26+
trace("Actual:\n$actual\n");
27+
28+
$success = true;
29+
$pos = strpos($actual, $expected);
30+
if (($pos === false) || ($pos > 1)) {
31+
$success = false;
32+
}
33+
34+
return ($success);
35+
}
36+
37+
function fetchBinary($conn, $buffered)
38+
{
39+
global $tableName, $binaryColumn, $binaryValue, $hexValue;
40+
41+
try {
42+
$query = "SELECT $binaryColumn FROM $tableName";
43+
if ($buffered) {
44+
$stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED));
45+
} else {
46+
$stmt = $conn->prepare($query);
47+
}
48+
$stmt->bindColumn($binaryColumn, $value, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY);
49+
$stmt->execute();
50+
51+
$row = $stmt->fetch(PDO::FETCH_BOUND);
52+
53+
if (!checkData($value, $binaryValue)) {
54+
echo "Fetched binary value unexpected ($buffered): $value\n";
55+
}
56+
57+
$stmt->bindColumn($binaryColumn, $value, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_SYSTEM);
58+
$stmt->execute();
59+
60+
$row = $stmt->fetch(PDO::FETCH_BOUND);
61+
62+
if (!checkData($value, $hexValue)) {
63+
echo "Fetched binary value a char string ($buffered): $value\n";
64+
}
65+
66+
$stmt->bindColumn($binaryColumn, $value, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_UTF8);
67+
$stmt->execute();
68+
69+
$row = $stmt->fetch(PDO::FETCH_BOUND);
70+
71+
if (!checkData($value, $hexValue)) {
72+
echo "Fetched binary value as UTF-8 string ($buffered): $value\n";
73+
}
74+
} catch (PdoException $e) {
75+
echo "Caught exception in fetchBinary ($buffered):\n";
76+
echo $e->getMessage() . PHP_EOL;
77+
}
78+
}
79+
80+
function fetchAsString($conn, $buffered)
81+
{
82+
global $tableName, $strColumn, $strValue;
83+
global $nstrColumn, $nstrValue;
84+
85+
try {
86+
$query = "SELECT $strColumn, $nstrColumn FROM $tableName";
87+
if ($buffered) {
88+
$stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED));
89+
} else {
90+
$stmt = $conn->prepare($query);
91+
}
92+
$stmt->execute();
93+
94+
$stmt->bindColumn($strColumn, $value1, PDO::PARAM_STR);
95+
$stmt->bindColumn($nstrColumn, $value2, PDO::PARAM_STR);
96+
$row = $stmt->fetch(PDO::FETCH_BOUND);
97+
98+
if (!checkData($value1, $strValue)) {
99+
echo "Fetched string value ($buffered): $value1\n";
100+
}
101+
102+
if (!checkData($value2, $nstrValue)) {
103+
echo "Fetched string value ($buffered): $value2\n";
104+
}
105+
$stmt->execute();
106+
107+
$stmt->bindColumn($strColumn, $value, PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_SYSTEM);
108+
$row = $stmt->fetch(PDO::FETCH_BOUND);
109+
110+
if (!checkData($value, $strValue)) {
111+
echo "Fetched string value: $value\n";
112+
}
113+
} catch (PdoException $e) {
114+
echo "Caught exception in fetchBinary ($buffered):\n";
115+
echo $e->getMessage() . PHP_EOL;
116+
}
117+
}
118+
119+
try {
120+
$conn = connect();
121+
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
122+
123+
// Create table of one max column
124+
$colMeta = array(new ColumnMeta('varbinary(max)', $binaryColumn),
125+
new ColumnMeta('varchar(max)', $strColumn),
126+
new ColumnMeta('nvarchar(max)', $nstrColumn));
127+
createTable($conn, $tableName, $colMeta);
128+
129+
// Insert one row
130+
$query = "INSERT INTO $tableName ($binaryColumn, $strColumn, $nstrColumn) VALUES (?, ?, ?)";
131+
$stmt = $conn->prepare($query);
132+
$stmt->bindParam(1, $binaryValue, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY);
133+
$stmt->bindParam(2, $strValue, PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_SYSTEM);
134+
$stmt->bindParam(3, $nstrValue, PDO::PARAM_STR);
135+
$stmt->execute();
136+
unset($stmt);
137+
138+
// Starting fetching with or without client buffer
139+
fetchBinary($conn, false);
140+
fetchBinary($conn, true);
141+
142+
fetchAsString($conn, false);
143+
fetchAsString($conn, true);
144+
145+
dropTable($conn, $tableName);
146+
echo "Done\n";
147+
unset($conn);
148+
} catch (PdoException $e) {
149+
echo $e->getMessage() . PHP_EOL;
150+
}
151+
?>
152+
--EXPECT--
153+
Done

0 commit comments

Comments
 (0)