Skip to content

Commit 9276290

Browse files
committed
Support negative numbers in writeVLong (#22314)
We don't *want* to use negative numbers with `writeVLong` so throw an exception when we try. On the other hand unforeseen bugs might cause us to write negative numbers (some versions of Elasticsearch don't have the exception, only an assertion) so this fixes `readVLong` so that instead of reading a wrong value and corrupting the stream it reads the negative value.
1 parent eead2ff commit 9276290

3 files changed

Lines changed: 61 additions & 10 deletions

File tree

core/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,8 @@ public long readLong() throws IOException {
214214
}
215215

216216
/**
217-
* Reads a long stored in variable-length format. Reads between one and
218-
* nine bytes. Smaller values take fewer bytes. Negative numbers are not
219-
* supported.
217+
* Reads a long stored in variable-length format. Reads between one and ten bytes. Smaller values take fewer bytes. Negative numbers
218+
* are encoded in ten bytes so prefer {@link #readLong()} or {@link #readZLong()} for negative numbers.
220219
*/
221220
public long readVLong() throws IOException {
222221
byte b = readByte();
@@ -260,8 +259,16 @@ public long readVLong() throws IOException {
260259
return i;
261260
}
262261
b = readByte();
263-
assert (b & 0x80) == 0;
264-
return i | ((b & 0x7FL) << 56);
262+
i |= ((b & 0x7FL) << 56);
263+
if ((b & 0x80) == 0) {
264+
return i;
265+
}
266+
b = readByte();
267+
if (b != 0 && b != 1) {
268+
throw new IOException("Invalid vlong (" + Integer.toHexString(b) + " << 63) | " + Long.toHexString(i));
269+
}
270+
i |= ((long) b) << 63;
271+
return i;
265272
}
266273

267274
public long readZLong() throws IOException {

core/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,22 @@ public void writeLong(long i) throws IOException {
210210
}
211211

212212
/**
213-
* Writes a non-negative long in a variable-length format.
214-
* Writes between one and nine bytes. Smaller values take fewer bytes.
215-
* Negative numbers are not supported.
213+
* Writes a non-negative long in a variable-length format. Writes between one and ten bytes. Smaller values take fewer bytes. Negative
214+
* numbers use ten bytes and trip assertions (if running in tests) so prefer {@link #writeLong(long)} or {@link #writeZLong(long)} for
215+
* negative numbers.
216216
*/
217217
public void writeVLong(long i) throws IOException {
218-
assert i >= 0;
218+
if (i < 0) {
219+
throw new IllegalStateException("Negative longs unsupported, use writeLong or writeZLong for negative numbers [" + i + "]");
220+
}
221+
writeVLongNoCheck(i);
222+
}
223+
224+
/**
225+
* Writes a long in a variable-length format without first checking if it is negative. Package private for testing. Use
226+
* {@link #writeVLong(long)} instead.
227+
*/
228+
void writeVLongNoCheck(long i) throws IOException {
219229
while ((i & ~0x7F) != 0) {
220230
writeByte((byte) ((i & 0x7f) | 0x80));
221231
i >>>= 7;

core/src/test/java/org/elasticsearch/common/io/stream/BytesStreamsTests.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import org.apache.lucene.util.BytesRef;
2323
import org.apache.lucene.util.Constants;
24-
import org.apache.lucene.util.UnicodeUtil;
24+
import org.elasticsearch.Version;
2525
import org.elasticsearch.common.bytes.BytesArray;
2626
import org.elasticsearch.common.bytes.BytesReference;
2727
import org.elasticsearch.common.geo.GeoPoint;
@@ -33,6 +33,7 @@
3333
import java.io.EOFException;
3434
import java.io.IOException;
3535
import java.util.ArrayList;
36+
import java.util.Base64;
3637
import java.util.Collections;
3738
import java.util.HashMap;
3839
import java.util.LinkedHashMap;
@@ -778,4 +779,37 @@ public void testReadNegativeArraySize() throws IOException {
778779
}
779780
}
780781
}
782+
783+
public void testVInt() throws IOException {
784+
final int value = randomInt();
785+
BytesStreamOutput output = new BytesStreamOutput();
786+
output.writeVInt(value);
787+
StreamInput input = output.bytes().streamInput();
788+
assertEquals(value, input.readVInt());
789+
}
790+
791+
public void testVLong() throws IOException {
792+
final long value = randomLong();
793+
{
794+
// Read works for positive and negative numbers
795+
BytesStreamOutput output = new BytesStreamOutput();
796+
output.writeVLongNoCheck(value); // Use NoCheck variant so we can write negative numbers
797+
StreamInput input = output.bytes().streamInput();
798+
assertEquals(value, input.readVLong());
799+
}
800+
if (value < 0) {
801+
// Write doesn't work for negative numbers
802+
BytesStreamOutput output = new BytesStreamOutput();
803+
Exception e = expectThrows(IllegalStateException.class, () -> output.writeVLong(value));
804+
assertEquals("Negative longs unsupported, use writeLong or writeZLong for negative numbers [" + value + "]", e.getMessage());
805+
}
806+
807+
assertTrue("If we're not compatible with 5.1.1 we can drop the assertion below",
808+
Version.CURRENT.minimumCompatibilityVersion().onOrBefore(Version.V_5_1_1_UNRELEASED));
809+
/* Read -1 as serialized by a version of Elasticsearch that supported writing negative numbers with writeVLong. Note that this
810+
* should be the same test as the first case (when value is negative) but we've kept some bytes so no matter what we do to
811+
* writeVLong in the future we can be sure we can read bytes as written by Elasticsearch before 5.1.2 */
812+
StreamInput in = new BytesArray(Base64.getDecoder().decode("////////////AQAAAAAAAA==")).streamInput();
813+
assertEquals(-1, in.readVLong());
814+
}
781815
}

0 commit comments

Comments
 (0)