Skip to content

Commit 8799770

Browse files
committed
JAVA-1900: Ensure that BSON binary values of subtype 3 or 4 (UUID_LEGACY and UUID_STANDARD) can be decoded even if they have an unexpected size.
BSON binary representations of UUID should be 16 bytes, but the server doesn't enforce that constraint. Rather than throw an exception when decoding a UUID value with length other than 16, the driver should instead decode it to an instance of org.bson.Binary rather than the expected java.util.UUID. This way, clients expecting a UUID instance will still fail if casting to UUID, but at least they have the ability to control this behavior by doing type tests. And clients that don't care one way or the other will continue to function in the face of bad data.
1 parent e33ca0c commit 8799770

15 files changed

+152
-21
lines changed

bson/src/main/org/bson/AbstractBsonReader.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ protected boolean isClosed() {
120120
*/
121121
protected abstract byte doPeekBinarySubType();
122122

123+
/**
124+
* Handles the logic to peek at the binary size.
125+
*
126+
* @return the binary size
127+
* @since 3.4
128+
*/
129+
protected abstract int doPeekBinarySize();
130+
123131
/**
124132
* Handles the logic to read booleans
125133
*
@@ -283,6 +291,12 @@ public byte peekBinarySubType() {
283291
return doPeekBinarySubType();
284292
}
285293

294+
@Override
295+
public int peekBinarySize() {
296+
checkPreconditions("readBinaryData", BsonType.BINARY);
297+
return doPeekBinarySize();
298+
}
299+
286300
@Override
287301
public boolean readBoolean() {
288302
checkPreconditions("readBoolean", BsonType.BOOLEAN);

bson/src/main/org/bson/BsonBinaryReader.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,14 @@ protected byte doPeekBinarySubType() {
153153
return type;
154154
}
155155

156+
@Override
157+
protected int doPeekBinarySize() {
158+
mark();
159+
int size = readSize();
160+
reset();
161+
return size;
162+
}
163+
156164
@Override
157165
protected boolean doReadBoolean() {
158166
byte booleanByte = bsonInput.readByte();

bson/src/main/org/bson/BsonBinarySubType.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ public enum BsonBinarySubType {
5959

6060
private final byte value;
6161

62+
/**
63+
* Returns true if the given value is a UUID subtype
64+
*
65+
* @param value the subtype value as a byte
66+
* @return true if value is a UUID subtype
67+
* @since 3.4
68+
*/
69+
public static boolean isUuid(final byte value) {
70+
return value == UUID_LEGACY.getValue() || value == UUID_STANDARD.getValue();
71+
}
72+
6273
BsonBinarySubType(final byte value) {
6374
this.value = value;
6475
}
@@ -71,5 +82,4 @@ public enum BsonBinarySubType {
7182
public byte getValue() {
7283
return value;
7384
}
74-
7585
}

bson/src/main/org/bson/BsonDocumentReader.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ protected byte doPeekBinarySubType() {
5858
return currentValue.asBinary().getType();
5959
}
6060

61+
@Override
62+
protected int doPeekBinarySize() {
63+
return currentValue.asBinary().getData().length;
64+
}
65+
6166
@Override
6267
protected boolean doReadBoolean() {
6368
return currentValue.asBoolean().getValue();

bson/src/main/org/bson/BsonReader.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ public interface BsonReader {
5252
*/
5353
byte peekBinarySubType();
5454

55+
/**
56+
* Peeks the size of the binary data that the reader is positioned at. This operation is not permitted if the mark is already set.
57+
*
58+
* @return the size of the binary data
59+
* @see #mark()
60+
* @since 3.4
61+
*/
62+
int peekBinarySize();
63+
5564
/**
5665
* Reads a BSON Binary data element from the reader.
5766
*

bson/src/main/org/bson/codecs/DocumentCodec.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,9 @@ private Object readValue(final BsonReader reader, final DecoderContext decoderCo
205205
reader.readNull();
206206
return null;
207207
} else if (bsonType == BsonType.ARRAY) {
208-
return readList(reader, decoderContext);
209-
} else if (bsonType == BsonType.BINARY) {
210-
byte bsonSubType = reader.peekBinarySubType();
211-
if (bsonSubType == BsonBinarySubType.UUID_STANDARD.getValue() || bsonSubType == BsonBinarySubType.UUID_LEGACY.getValue()) {
212-
return registry.get(UUID.class).decode(reader, decoderContext);
213-
}
208+
return readList(reader, decoderContext);
209+
} else if (bsonType == BsonType.BINARY && BsonBinarySubType.isUuid(reader.peekBinarySubType()) && reader.peekBinarySize() == 16) {
210+
return registry.get(UUID.class).decode(reader, decoderContext);
214211
}
215212
return valueTransformer.transform(bsonTypeCodecMap.get(bsonType).decode(reader, decoderContext));
216213
}

bson/src/main/org/bson/json/JsonReader.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ protected byte doPeekBinarySubType() {
8686
return doReadBinaryData().getType();
8787
}
8888

89+
@Override
90+
protected int doPeekBinarySize() {
91+
return doReadBinaryData().getData().length;
92+
}
93+
8994
@Override
9095
protected boolean doReadBoolean() {
9196
return (Boolean) currentValue;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2016 MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package org.bson
19+
20+
import spock.lang.Specification
21+
22+
class BsonBinarySubTypeSpecification extends Specification {
23+
24+
def 'should be uuid only for legacy and uuid types'() {
25+
expect:
26+
BsonBinarySubType.isUuid(value as byte) == isUuid
27+
28+
where:
29+
value | isUuid
30+
1 | false
31+
2 | false
32+
3 | true
33+
4 | true
34+
5 | false
35+
}
36+
}

bson/src/test/unit/org/bson/LimitedLookaheadMarkSpecification.groovy

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ class LimitedLookaheadMarkSpecification extends Specification {
242242
]
243243
}
244244

245-
def 'should peek binary subtype'(BsonWriter writer) {
245+
def 'should peek binary subtype and size'(BsonWriter writer) {
246246
given:
247247
writer.with {
248248
writeStartDocument()
@@ -265,12 +265,14 @@ class LimitedLookaheadMarkSpecification extends Specification {
265265
reader.readStartDocument()
266266
reader.readName()
267267
def subType = reader.peekBinarySubType()
268+
def size = reader.peekBinarySize()
268269
def binary = reader.readBinaryData()
269270
def longValue = reader.readInt64('int64')
270271
reader.readEndDocument()
271272

272273
then:
273274
subType == BsonBinarySubType.UUID_LEGACY.value
275+
size == 16
274276
binary == new BsonBinary(BsonBinarySubType.UUID_LEGACY, new byte[16])
275277
longValue == 52L
276278

bson/src/test/unit/org/bson/codecs/DocumentCodecSpecification.groovy

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,25 @@ class DocumentCodecSpecification extends Specification {
139139
]
140140
}
141141

142+
@SuppressWarnings(['LineLength'])
143+
def 'should decode binary subtypes for UUID'() {
144+
given:
145+
def reader = new BsonBinaryReader(ByteBuffer.wrap(bytes as byte[]))
146+
147+
when:
148+
def document = new DocumentCodec().decode(reader, DecoderContext.builder().build())
149+
150+
then:
151+
value == document.get('f')
152+
153+
where:
154+
value | bytes
155+
new Binary((byte) 0x03, (byte[]) [115, 116, 11]) | [16, 0, 0, 0, 5, 102, 0, 3, 0, 0, 0, 3, 115, 116, 11, 0]
156+
new Binary((byte) 0x04, (byte[]) [115, 116, 11]) | [16, 0, 0, 0, 5, 102, 0, 3, 0, 0, 0, 4, 115, 116, 11, 0]
157+
UUID.fromString('08070605-0403-0201-100f-0e0d0c0b0a09') | [29, 0, 0, 0, 5, 102, 0, 16, 0, 0, 0, 3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0]
158+
UUID.fromString('01020304-0506-0708-090a-0b0c0d0e0f10') | [29, 0, 0, 0, 5, 102, 0, 16, 0, 0, 0, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0]
159+
}
160+
142161
def 'should respect encodeIdFirst property in encoder context'() {
143162
given:
144163
def originalDocument = new Document('x', 2)

0 commit comments

Comments
 (0)