diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java index 94592fc787..d159ac4945 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java @@ -48,10 +48,7 @@ public abstract class AbstractType implements Comparator } /* validate that the byte array is a valid sequence for the type we are supposed to be comparing */ - public void validate(ByteBuffer bytes) - { - getString(bytes); - } + public abstract void validate(ByteBuffer bytes) throws MarshalException; public Comparator getReverseComparator() { diff --git a/src/java/org/apache/cassandra/db/marshal/AsciiType.java b/src/java/org/apache/cassandra/db/marshal/AsciiType.java index a84f5a607e..422b6a3676 100644 --- a/src/java/org/apache/cassandra/db/marshal/AsciiType.java +++ b/src/java/org/apache/cassandra/db/marshal/AsciiType.java @@ -43,4 +43,15 @@ public class AsciiType extends BytesType { return ByteBuffer.wrap(source.getBytes(Charsets.US_ASCII)); } + + public void validate(ByteBuffer bytes) throws MarshalException + { + // 0-127 + for (int i = 0; i < bytes.remaining(); i++) + { + byte b = bytes.array()[bytes.arrayOffset() + bytes.position() + i]; + if (b < 0 || b > 127) + throw new MarshalException("Invalid byte for ascii: " + Byte.toString(b)); + } + } } diff --git a/src/java/org/apache/cassandra/db/marshal/BytesType.java b/src/java/org/apache/cassandra/db/marshal/BytesType.java index 56823eb96b..e6c46bea6a 100644 --- a/src/java/org/apache/cassandra/db/marshal/BytesType.java +++ b/src/java/org/apache/cassandra/db/marshal/BytesType.java @@ -51,4 +51,9 @@ public class BytesType extends AbstractType { return ByteBuffer.wrap(source.getBytes()); } + + public void validate(ByteBuffer bytes) throws MarshalException + { + // all bytes are legal. + } } diff --git a/src/java/org/apache/cassandra/db/marshal/IntegerType.java b/src/java/org/apache/cassandra/db/marshal/IntegerType.java index 528bb553c1..64365f217e 100644 --- a/src/java/org/apache/cassandra/db/marshal/IntegerType.java +++ b/src/java/org/apache/cassandra/db/marshal/IntegerType.java @@ -139,4 +139,9 @@ public final class IntegerType extends AbstractType return ByteBuffer.wrap(integerType.toByteArray()); } + + public void validate(ByteBuffer bytes) throws MarshalException + { + // no invalid integers. + } } diff --git a/src/java/org/apache/cassandra/db/marshal/LexicalUUIDType.java b/src/java/org/apache/cassandra/db/marshal/LexicalUUIDType.java index 33cafcc19f..6aff641132 100644 --- a/src/java/org/apache/cassandra/db/marshal/LexicalUUIDType.java +++ b/src/java/org/apache/cassandra/db/marshal/LexicalUUIDType.java @@ -63,4 +63,11 @@ public class LexicalUUIDType extends AbstractType { return ByteBuffer.wrap(UUIDGen.decompose(UUID.fromString(source))); } + + public void validate(ByteBuffer bytes) throws MarshalException + { + if (bytes.remaining() != 16 && bytes.remaining() != 0) + throw new MarshalException(String.format("LexicalUUID should be 16 or 0 bytes (%d)", bytes.remaining())); + // not sure what the version should be for this. + } } diff --git a/src/java/org/apache/cassandra/db/marshal/LocalByPartionerType.java b/src/java/org/apache/cassandra/db/marshal/LocalByPartionerType.java index f518fbcab2..1bf822feaa 100644 --- a/src/java/org/apache/cassandra/db/marshal/LocalByPartionerType.java +++ b/src/java/org/apache/cassandra/db/marshal/LocalByPartionerType.java @@ -51,4 +51,9 @@ public class LocalByPartionerType extends AbstractType { return partitioner.decorateKey(o1).compareTo(partitioner.decorateKey(o2)); } + + public void validate(ByteBuffer bytes) throws MarshalException + { + throw new IllegalStateException("You shouldn't be validating this."); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/LongType.java b/src/java/org/apache/cassandra/db/marshal/LongType.java index f9bf574432..43f9f91e5c 100644 --- a/src/java/org/apache/cassandra/db/marshal/LongType.java +++ b/src/java/org/apache/cassandra/db/marshal/LongType.java @@ -81,4 +81,10 @@ public class LongType extends AbstractType return FBUtilities.toByteBuffer(longType); } + + public void validate(ByteBuffer bytes) throws MarshalException + { + if (bytes.remaining() != 8 && bytes.remaining() != 0) + throw new MarshalException(String.format("Expected 8 or 0 byte long (%d)", bytes.remaining())); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/TimeUUIDType.java b/src/java/org/apache/cassandra/db/marshal/TimeUUIDType.java index 3a5b9fbd77..4f95327d57 100644 --- a/src/java/org/apache/cassandra/db/marshal/TimeUUIDType.java +++ b/src/java/org/apache/cassandra/db/marshal/TimeUUIDType.java @@ -98,4 +98,18 @@ public class TimeUUIDType extends AbstractType return ByteBuffer.wrap(UUIDGen.decompose(uuid)); } + + public void validate(ByteBuffer bytes) throws MarshalException + { + if (bytes.remaining() != 16 && bytes.remaining() != 0) + throw new MarshalException(String.format("TimeUUID should be 16 or 0 bytes (%d)", bytes.remaining())); + ByteBuffer slice = bytes.slice(); + // version is bits 4-7 of byte 6. + if (bytes.remaining() > 0) + { + slice.position(6); + if ((slice.get() & 0x0f) != 1) + throw new MarshalException("Invalid version for TimeUUID type."); + } + } } diff --git a/src/java/org/apache/cassandra/db/marshal/UTF8Type.java b/src/java/org/apache/cassandra/db/marshal/UTF8Type.java index fc98c13264..8cb747b01a 100644 --- a/src/java/org/apache/cassandra/db/marshal/UTF8Type.java +++ b/src/java/org/apache/cassandra/db/marshal/UTF8Type.java @@ -49,4 +49,127 @@ public class UTF8Type extends BytesType { return ByteBuffer.wrap(source.getBytes(Charsets.UTF_8)); } + + public void validate(ByteBuffer bytes) throws MarshalException + { + if (!UTF8Validator.validate(bytes.slice())) + throw new MarshalException("String didn't validate."); + } + + static class UTF8Validator + { + enum State { + START, + TWO, + TWO_80, + THREE_a0bf, + THREE_80bf_1, + THREE_80bf_2, + FOUR_90bf, + FOUR_80bf_3, + }; + + // since we're not converting to java strings, we don't need to worry about converting to surrogates. + // buf has already been sliced/duplicated. + static boolean validate(ByteBuffer buf) + { + int b = 0; + State state = State.START; + while (buf.remaining() > 0) + { + b = buf.get(); + switch (state) + { + case START: + if (b >= 0) + { + // ascii, state stays start. + if (b > 127) + return false; + } + else if ((b >> 5) == -2) + { + // validate first byte of 2-byte char, 0xc2-0xdf + if (b == (byte) 0xc0) + // speical case: modified utf8 null is 0xc080. + state = State.TWO_80; + else if ((b & 0x1e) == 0) + return false; + state = State.TWO; + } + else if ((b >> 4) == -2) + { + // 3 bytes. first byte will be 0xe0 or 0xe1-0xef. handling of second byte will differ. + // so 0xe0,0xa0-0xbf,0x80-0xbf or 0xe1-0xef,0x80-0xbf,0x80-0xbf. + if (b == (byte)0xe0) + state = State.THREE_a0bf; + else + state = State.THREE_80bf_2; + break; + } + else if ((b >> 3) == -2) + { + // 4 bytes. this is where the fun starts. + if (b == (byte)0xf0) + // 0xf0, 0x90-0xbf, 0x80-0xbf, 0x80-0xbf + state = State.FOUR_90bf; + else if (b == (byte)0xf4) + // 0xf4, 0x80-0xbf, 0x80-0xbf, 0x80-0xbf + state = State.FOUR_80bf_3; + else + // 0xf1-0xf3, 0x80-0xbf, 0x80-0xbf, 0x80-0xbf + state = State.FOUR_80bf_3; + break; + } + else + return false; // malformed. + break; + case TWO: + // validate second byte of 2-byte char, 0x80-0xbf + if ((b & 0xc0) != 0x80) + return false; + state = State.START; + break; + case TWO_80: + if (b != (byte)0x80) + return false; + state = State.START; + break; + case THREE_a0bf: + if ((b & 0xe0) == 0x80) + return false; + state = State.THREE_80bf_1; + break; + case THREE_80bf_1: + // expecting 0x80-0xbf + if ((b & 0xc0) != 0x80) + return false; + state = State.START; + break; + case THREE_80bf_2: + // expecting 0x80-bf and then another of the same. + if ((b & 0xc0) != 0x80) + return false; + state = State.THREE_80bf_1; + break; + case FOUR_90bf: + // expecting 0x90-bf. 2nd byte of 4byte sequence. after that it should degrade to 80-bf,80-bf (like 3byte seq). + if ((b & 0x30) == 0) + return false; + state = State.THREE_80bf_2; + break; + case FOUR_80bf_3: + // expecting 0x80-bf 3 times. degenerates to THREE_80bf_2. + if ((b & 0xc0) != 0x80) + return false; + state = State.THREE_80bf_2; + break; + default: + return false; // invalid state. + } + } + // if state != start, we've got underflow. that's an error. + return state == State.START; + } + } } diff --git a/test/unit/org/apache/cassandra/db/marshal/TimeUUIDTypeTest.java b/test/unit/org/apache/cassandra/db/marshal/TimeUUIDTypeTest.java index f6461adb6b..6eca9549d4 100644 --- a/test/unit/org/apache/cassandra/db/marshal/TimeUUIDTypeTest.java +++ b/test/unit/org/apache/cassandra/db/marshal/TimeUUIDTypeTest.java @@ -40,6 +40,8 @@ public class TimeUUIDTypeTest UUID a = generator.generateTimeBasedUUID(); UUID b = new UUID(a.asByteArray()); + timeUUIDType.validate(ByteBuffer.wrap(a.asByteArray())); + timeUUIDType.validate(ByteBuffer.wrap(b.asByteArray())); assertEquals(0, timeUUIDType.compare(ByteBuffer.wrap(a.asByteArray()), ByteBuffer.wrap(b.asByteArray()))); } @@ -50,6 +52,10 @@ public class TimeUUIDTypeTest UUID b = generator.generateTimeBasedUUID(); UUID c = generator.generateTimeBasedUUID(); + timeUUIDType.validate(ByteBuffer.wrap(a.asByteArray())); + timeUUIDType.validate(ByteBuffer.wrap(b.asByteArray())); + timeUUIDType.validate(ByteBuffer.wrap(c.asByteArray())); + assert timeUUIDType.compare(ByteBuffer.wrap(a.asByteArray()), ByteBuffer.wrap(b.asByteArray())) < 0; assert timeUUIDType.compare(ByteBuffer.wrap(b.asByteArray()), ByteBuffer.wrap(c.asByteArray())) < 0; assert timeUUIDType.compare(ByteBuffer.wrap(a.asByteArray()), ByteBuffer.wrap(c.asByteArray())) < 0; @@ -61,6 +67,10 @@ public class TimeUUIDTypeTest UUID a = generator.generateTimeBasedUUID(); UUID b = generator.generateTimeBasedUUID(); UUID c = generator.generateTimeBasedUUID(); + + timeUUIDType.validate(ByteBuffer.wrap(a.asByteArray())); + timeUUIDType.validate(ByteBuffer.wrap(b.asByteArray())); + timeUUIDType.validate(ByteBuffer.wrap(c.asByteArray())); assert timeUUIDType.compare(ByteBuffer.wrap(c.asByteArray()), ByteBuffer.wrap(b.asByteArray())) > 0; assert timeUUIDType.compare(ByteBuffer.wrap(b.asByteArray()), ByteBuffer.wrap(a.asByteArray())) > 0;