@@ -189,6 +189,12 @@ class VariantUtil {
189189 // The size (in bytes) of a UUID.
190190 static final int UUID_SIZE = 16 ;
191191
192+ /**
193+ * Maximum permitted nesting depth of a Variant value.
194+ * same limit as in VariantJsonParser.
195+ */
196+ static final int MAX_VARIANT_DEPTH = 500 ;
197+
192198 // header bytes
193199 static final byte HEADER_NULL = primitiveHeader (NULL );
194200 static final byte HEADER_LONG_STRING = primitiveHeader (LONG_STR );
@@ -874,6 +880,160 @@ static HashMap<String, Integer> getMetadataMap(ByteBuffer metadata) {
874880 return result ;
875881 }
876882
883+ /**
884+ * Bounds-checks the metadata buffer: header version, dictionary offset table and string data
885+ * region all fit within the buffer extent. It does not perform any deep checks into
886+ * the metadata itself.
887+ *
888+ * @param metadata the variant metadata buffer
889+ * @return the dictionary size
890+ * @throws IllegalArgumentException if the metadata buffer is not well-formed
891+ */
892+ static int validateMetadata (ByteBuffer metadata ) {
893+ int pos = metadata .position ();
894+ Preconditions .checkArgument (pos >= 0 && pos < metadata .limit (), "variant metadata is empty" );
895+ int header = metadata .get (pos ) & 0xFF ;
896+ Preconditions .checkArgument (
897+ (header & VERSION_MASK ) == VERSION , "Unsupported variant metadata version: %s" , header & VERSION_MASK );
898+ int offsetSize = ((header >> 6 ) & 0x3 ) + 1 ;
899+ long remaining = (long ) metadata .limit () - pos ;
900+ long offsetListStart = 1L + offsetSize ;
901+ Preconditions .checkArgument (offsetListStart <= remaining , "variant metadata truncated" );
902+ int dictSize = readUnsigned (metadata , pos + 1 , offsetSize );
903+ long offsetBytes = (long ) (dictSize + 1 ) * offsetSize ;
904+ long dataStart = offsetListStart + offsetBytes ;
905+ Preconditions .checkArgument (
906+ dataStart <= remaining , "variant metadata dictionary table extends past buffer: dictSize=%s" , dictSize );
907+ return dictSize ;
908+ }
909+
910+ /**
911+ * Bounds-checks a single Variant value node against its buffer slot. Performs no recursion
912+ * into nested children: child nodes are checked on demand when callers descend into them.
913+ *
914+ * <p>Cost: O(1) for primitives and short strings, O(numElements) for objects and arrays.
915+ * Validation of nested structures is deferred so that opening a large well-formed Variant
916+ * is not penalised by sub-trees the caller never inspects.
917+ *
918+ * @param value the variant value buffer (position/limit define the extent of this node's slot)
919+ * @param dictSize the metadata dictionary size, used to bound object field ids
920+ * @throws IllegalArgumentException if the value header or container table does not fit within
921+ * the buffer slot, or if any object field id is out of range
922+ */
923+ static void validateValueShallow (ByteBuffer value , int dictSize ) {
924+ int s = value .position ();
925+ Preconditions .checkArgument (s >= 0 && s < value .limit (), "variant value is empty" );
926+ long slot = (long ) value .limit () - s ;
927+ int header = value .get (s ) & 0xFF ;
928+ int basicType = header & BASIC_TYPE_MASK ;
929+ int typeInfo = (header >> BASIC_TYPE_BITS ) & PRIMITIVE_TYPE_MASK ;
930+ switch (basicType ) {
931+ case SHORT_STR :
932+ Preconditions .checkArgument (1L + typeInfo <= slot , "variant short string extends past buffer" );
933+ return ;
934+ case OBJECT :
935+ validateContainerShallow (value , s , slot , dictSize , true , typeInfo );
936+ return ;
937+ case ARRAY :
938+ validateContainerShallow (value , s , slot , dictSize , false , typeInfo );
939+ return ;
940+ default :
941+ validatePrimitiveShallow (value , s , slot , typeInfo );
942+ }
943+ }
944+
945+ private static void validateContainerShallow (
946+ ByteBuffer value , int s , long slot , int dictSize , boolean isObject , int typeInfo ) {
947+ boolean largeSize ;
948+ int idSize ;
949+ if (isObject ) {
950+ largeSize = ((typeInfo >> 4 ) & 0x1 ) != 0 ;
951+ idSize = ((typeInfo >> 2 ) & 0x3 ) + 1 ;
952+ } else {
953+ largeSize = ((typeInfo >> 2 ) & 0x1 ) != 0 ;
954+ idSize = 0 ;
955+ }
956+ int offsetSize = (typeInfo & 0x3 ) + 1 ;
957+ int sizeBytes = largeSize ? U32_SIZE : 1 ;
958+ Preconditions .checkArgument (1L + sizeBytes <= slot , "variant container header truncated" );
959+ int numElements = readUnsigned (value , s + 1 , sizeBytes );
960+ long idStart = 1L + sizeBytes ;
961+ long idBytes = isObject ? (long ) numElements * idSize : 0L ;
962+ long offsetStart = idStart + idBytes ;
963+ long offsetBytes = (long ) (numElements + 1 ) * offsetSize ;
964+ long dataStart = offsetStart + offsetBytes ;
965+ Preconditions .checkArgument (
966+ dataStart <= slot , "variant container offset table extends past buffer: numElements=%s" , numElements );
967+ long dataLen = slot - dataStart ;
968+ if (isObject ) {
969+ for (int i = 0 ; i < numElements ; i ++) {
970+ int id = readUnsigned (value , s + (int ) idStart + i * idSize , idSize );
971+ Preconditions .checkArgument (
972+ id < dictSize , "variant object key id %s out of range (dictSize=%s)" , id , dictSize );
973+ }
974+ }
975+ // Each child offset must lie within the data region. Children may overlap or leave gaps;
976+ // the trailing terminator offset is range-checked for the same reason.
977+ for (int i = 0 ; i <= numElements ; i ++) {
978+ // O(elements)
979+ int off = readUnsigned (value , s + (int ) offsetStart + i * offsetSize , offsetSize );
980+ Preconditions .checkArgument (
981+ off <= dataLen , "variant child offset out of range: %s (data length %s)" , off , dataLen );
982+ }
983+ }
984+
985+ private static void validatePrimitiveShallow (ByteBuffer value , int s , long slot , int typeInfo ) {
986+ long size ;
987+ switch (typeInfo ) {
988+ case NULL :
989+ case TRUE :
990+ case FALSE :
991+ size = 1 ;
992+ break ;
993+ case INT8 :
994+ size = 2 ;
995+ break ;
996+ case INT16 :
997+ size = 3 ;
998+ break ;
999+ case INT32 :
1000+ case DATE :
1001+ case FLOAT :
1002+ size = 5 ;
1003+ break ;
1004+ case INT64 :
1005+ case DOUBLE :
1006+ case TIMESTAMP_TZ :
1007+ case TIMESTAMP_NTZ :
1008+ case TIME :
1009+ case TIMESTAMP_NANOS_TZ :
1010+ case TIMESTAMP_NANOS_NTZ :
1011+ size = 9 ;
1012+ break ;
1013+ case DECIMAL4 :
1014+ size = 6 ;
1015+ break ;
1016+ case DECIMAL8 :
1017+ size = 10 ;
1018+ break ;
1019+ case DECIMAL16 :
1020+ size = 18 ;
1021+ break ;
1022+ case BINARY :
1023+ case LONG_STR : {
1024+ Preconditions .checkArgument (1L + U32_SIZE <= slot , "variant string/binary length field truncated" );
1025+ size = 1L + U32_SIZE + readUnsigned (value , s + 1 , U32_SIZE );
1026+ break ;
1027+ }
1028+ case UUID :
1029+ size = 1L + UUID_SIZE ;
1030+ break ;
1031+ default :
1032+ throw new IllegalArgumentException (String .format ("Unknown primitive type in variant: %d" , typeInfo ));
1033+ }
1034+ Preconditions .checkArgument (size <= slot , "variant value extends past buffer" );
1035+ }
1036+
8771037 /**
8781038 * Computes the actual size (in bytes) of the Variant value.
8791039 * @param value The Variant value binary
0 commit comments