Skip to content

Commit 82a2a8a

Browse files
authored
Correct unsigned long handling (#161)
Motivation: Length-encoded integers should be handled correctly, especially those larger than `Long.MAX_VALUE`. See also #39. Modification: - Add code comments to explain how to handle the length encoded integer. - Correct `LargeFieldReader.readSlice()` method, it should use the length as an unsigned long. Result: - `LargeFieldReader.readSlice()` should now work correctly for fields those size is greater than 2<sup>63</sup>-1
1 parent 2b6e378 commit 82a2a8a

File tree

6 files changed

+33
-10
lines changed

6 files changed

+33
-10
lines changed

src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ public final class VarIntUtils {
4747

4848
private static final int MEDIUM_SIZE = MEDIUM_BYTES * Byte.SIZE;
4949

50+
/**
51+
* Reads a length encoded integer from the given buffers. Notice that a length encoded integer can be
52+
* greater than {@link Long#MAX_VALUE}. In this case it should be used as an unsigned long. If we need
53+
* assume the result as a smaller integer, add code comment to explain it.
54+
* <p>
55+
* Note: it will change {@code firstPart} and {@code secondPart} readerIndex if necessary.
56+
*
57+
* @param firstPart the first part of a readable buffer include a part of the var integer.
58+
* @param secondPart the second part of a readable buffer include subsequent part of the var integer.
59+
* @return A var integer read from buffer.
60+
*/
5061
public static long crossReadVarInt(ByteBuf firstPart, ByteBuf secondPart) {
5162
requireNonNull(firstPart, "firstPart must not be null");
5263
requireNonNull(secondPart, "secondPart must not be null");
@@ -87,6 +98,10 @@ public static long crossReadVarInt(ByteBuf firstPart, ByteBuf secondPart) {
8798
}
8899

89100
/**
101+
* Reads a length encoded integer from the given buffer. Notice that a length encoded integer can be
102+
* greater than {@link Long#MAX_VALUE}. In this case it should be used as an unsigned long. If we need
103+
* assume the result as a smaller integer, add code comment to explain it.
104+
* <p>
90105
* Note: it will change {@code buf} readerIndex.
91106
*
92107
* @param buf a readable buffer include a var integer.

src/main/java/io/asyncer/r2dbc/mysql/message/server/ColumnCountMessage.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public int getTotalColumns() {
4040
}
4141

4242
static ColumnCountMessage decode(ByteBuf buf) {
43+
// JVM does NOT support arrays longer than Integer.MAX_VALUE
4344
return new ColumnCountMessage(Math.toIntExact(VarIntUtils.readVarInt(buf)));
4445
}
4546

src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,8 @@ private static DefinitionMetadataMessage decode41(ByteBuf buf, ConnectionContext
173173
String column = readVarIntSizedString(buf, charset);
174174
String originColumn = readVarIntSizedString(buf, charset);
175175

176-
VarIntUtils.readVarInt(buf); // skip constant 0x0c encoded by var integer
176+
// Skip constant 0x0c encoded by var integer
177+
VarIntUtils.readVarInt(buf);
177178

178179
int collationId = buf.readUnsignedShortLE();
179180
long size = buf.readUnsignedIntLE();
@@ -185,7 +186,7 @@ private static DefinitionMetadataMessage decode41(ByteBuf buf, ConnectionContext
185186
}
186187

187188
private static String readVarIntSizedString(ByteBuf buf, Charset charset) {
188-
// JVM can NOT support string which length upper than maximum of int32
189+
// JVM does NOT support strings longer than Integer.MAX_VALUE
189190
int bytes = (int) VarIntUtils.readVarInt(buf);
190191

191192
if (bytes == 0) {

src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ public FieldValue readVarIntSizedField() {
9999
fieldSize = VarIntUtils.readVarInt(currentBuf);
100100
}
101101

102-
// Refresh non empty buffer because current buffer has been read.
102+
// Refresh non-empty buffer because current buffer has been read.
103103
currentBuf = nonEmptyBuffer();
104104

105105
List<ByteBuf> results = readSlice(currentBuf, fieldSize);
106106

107-
if (fieldSize > Integer.MAX_VALUE) {
107+
if (Long.compareUnsigned(fieldSize, Integer.MAX_VALUE) > 0) {
108108
return retainedLargeField(results);
109109
}
110110

@@ -130,26 +130,30 @@ protected void deallocate() {
130130
* list instead of a single buffer.
131131
*
132132
* @param current the current {@link ByteBuf} in {@link #buffers}.
133-
* @param length the length of read.
133+
* @param length the length of read, it can be an unsigned long.
134134
* @return result buffer list, should NEVER retain any buffer.
135135
*/
136136
private List<ByteBuf> readSlice(ByteBuf current, long length) {
137137
ByteBuf buf = current;
138138
List<ByteBuf> results = new ArrayList<>(Math.max(
139-
(int) Math.min((length / Envelopes.MAX_ENVELOPE_SIZE) + 2, Byte.MAX_VALUE), 10));
139+
(int) Math.min(Long.divideUnsigned(length, Envelopes.MAX_ENVELOPE_SIZE) + 2, Byte.MAX_VALUE),
140+
10
141+
));
140142
long totalSize = 0;
141143
int bufReadable;
142144

143145
// totalSize + bufReadable <= length
144-
while (totalSize <= length - (bufReadable = buf.readableBytes())) {
146+
while (Long.compareUnsigned(totalSize, length - (bufReadable = buf.readableBytes())) <= 0) {
145147
totalSize += bufReadable;
146148
// No need readSlice because currentBufIndex will be increment after List pushed.
147149
results.add(buf);
148150
buf = this.buffers[++this.currentBufIndex];
149151
}
150152

151-
if (length > totalSize) {
152-
// need bytes = length - `results` real length = length - (totalSize - `buf` length)
153+
// totalSize < length
154+
if (Long.compareUnsigned(length, totalSize) > 0) {
155+
// need bytes = length - `results` length = length - totalSize
156+
// length - totalSize should be an int due to while loop above.
153157
results.add(buf.readSlice((int) (length - totalSize)));
154158
} // else results has filled by prev buffer, and currentBufIndex is unread for now.
155159

src/main/java/io/asyncer/r2dbc/mysql/message/server/NormalFieldReader.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public boolean release(int decrement) {
105105
}
106106

107107
private static ByteBuf readVarIntSizedRetained(ByteBuf buf) {
108+
// Normal field will NEVER be greater than Integer.MAX_VALUE.
108109
int size = (int) VarIntUtils.readVarInt(buf);
109110
if (size == 0) {
110111
// Use EmptyByteBuf, new buffer no need to be retained.

src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,11 @@ static OkMessage decode(ByteBuf buf, ConnectionContext context) {
160160
if (size > sizeAfterVarInt) {
161161
information = buf.toString(readerIndex, buf.writerIndex() - readerIndex, charset);
162162
} else {
163+
// JVM does NOT support strings longer than Integer.MAX_VALUE
163164
information = buf.toString(buf.readerIndex(), (int) size, charset);
164165
}
165166

166-
// Ignore session track, it is not human readable and useless for R2DBC client.
167+
// Ignore session track, it is not human-readable and useless for R2DBC client.
167168
return new OkMessage(affectedRows, lastInsertId, serverStatuses, warnings, information);
168169
}
169170

0 commit comments

Comments
 (0)