blob: e4bd16a657d0d76beeea2fcbb63d05cb31eecb0f [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "variant_test_utils.h"
#include <algorithm>
#include <array>
#include <bit>
#include <cstdint>
#include <cstring>
#include <limits>
#include <vector>
#include "common/exception.h"
#include "core/value/variant/variant_parquet_encoding.h"
#include "util/utf8_check.h"
namespace doris {
namespace {
constexpr unsigned __int128 max_decimal38() {
unsigned __int128 value = 1;
for (uint8_t digit = 0; digit < 38; ++digit) {
value *= 10;
}
return value - 1;
}
constexpr unsigned __int128 MAX_DECIMAL38 = max_decimal38();
constexpr unsigned __int128 DECIMAL4_MAX = 999'999'999;
constexpr unsigned __int128 DECIMAL8_MAX = 999'999'999'999'999'999;
[[noreturn]] void fail(const char* reason) {
throw Exception(ErrorCode::CORRUPTION, "Non-canonical Variant encoding: {}", reason);
}
void require_bytes(size_t available, size_t required) {
if (available < required) {
fail("truncated bytes");
}
}
uint64_t read_unsigned(const char* data, uint8_t width) {
uint64_t result = 0;
for (uint8_t byte = 0; byte < width; ++byte) {
result |= static_cast<uint64_t>(static_cast<uint8_t>(data[byte])) << (byte * 8);
}
return result;
}
unsigned __int128 magnitude(__int128 value) {
const auto unsigned_value = static_cast<unsigned __int128>(value);
return value < 0 ? ~unsigned_value + 1 : unsigned_value;
}
uint8_t minimum_unsigned_width(uint64_t value) {
if (value <= std::numeric_limits<uint8_t>::max()) {
return 1;
}
if (value <= std::numeric_limits<uint16_t>::max()) {
return 2;
}
if (value <= 0xFFFFFFU) {
return 3;
}
return 4;
}
VariantPrimitiveId minimum_integer_id(int64_t value) {
if (value >= std::numeric_limits<int8_t>::min() &&
value <= std::numeric_limits<int8_t>::max()) {
return VariantPrimitiveId::INT8;
}
if (value >= std::numeric_limits<int16_t>::min() &&
value <= std::numeric_limits<int16_t>::max()) {
return VariantPrimitiveId::INT16;
}
if (value >= std::numeric_limits<int32_t>::min() &&
value <= std::numeric_limits<int32_t>::max()) {
return VariantPrimitiveId::INT32;
}
return VariantPrimitiveId::INT64;
}
uint8_t minimum_decimal_width(__int128 value) {
const unsigned __int128 absolute = magnitude(value);
if (absolute <= DECIMAL4_MAX) {
return 4;
}
if (absolute <= DECIMAL8_MAX) {
return 8;
}
return 16;
}
void require_valid_utf8(StringRef value, const char* description) {
if (value.size != 0 && !validate_utf8_naive(value.data, value.size)) {
fail(description);
}
}
bool unsigned_bytes_less(StringRef left, StringRef right) {
return std::ranges::lexicographical_compare(left, right, [](char left_byte, char right_byte) {
return static_cast<uint8_t>(left_byte) < static_cast<uint8_t>(right_byte);
});
}
void validate_node(VariantRef value, std::vector<bool>& referenced_keys);
void validate_primitive(VariantRef value) {
const VariantPrimitiveId id = value.primitive_id();
switch (id) {
case VariantPrimitiveId::INT8:
case VariantPrimitiveId::INT16:
case VariantPrimitiveId::INT32:
case VariantPrimitiveId::INT64:
if (id != minimum_integer_id(value.get_int())) {
fail("integer does not use its minimum width");
}
return;
case VariantPrimitiveId::DECIMAL4:
case VariantPrimitiveId::DECIMAL8:
case VariantPrimitiveId::DECIMAL16: {
const VariantDecimal decimal = value.get_decimal();
if (magnitude(decimal.unscaled) > MAX_DECIMAL38) {
fail("decimal exceeds precision 38");
}
if (decimal.width < minimum_decimal_width(decimal.unscaled)) {
fail("decimal width is below its implied precision");
}
return;
}
case VariantPrimitiveId::STRING: {
const StringRef string = value.get_string();
if (string.size <= VARIANT_MAX_SHORT_STRING_SIZE) {
fail("long string form used for a short string");
}
require_valid_utf8(string, "string is not valid UTF-8");
return;
}
case VariantPrimitiveId::NULL_VALUE:
case VariantPrimitiveId::TRUE_VALUE:
case VariantPrimitiveId::FALSE_VALUE:
case VariantPrimitiveId::DOUBLE:
case VariantPrimitiveId::DATE:
case VariantPrimitiveId::TIMESTAMP_MICROS:
case VariantPrimitiveId::TIMESTAMP_NTZ_MICROS:
case VariantPrimitiveId::FLOAT:
case VariantPrimitiveId::BINARY:
case VariantPrimitiveId::TIME_NTZ_MICROS:
case VariantPrimitiveId::TIMESTAMP_NANOS:
case VariantPrimitiveId::TIMESTAMP_NTZ_NANOS:
case VariantPrimitiveId::UUID:
return;
}
fail("unknown primitive id");
}
// NOLINTNEXTLINE(readability-function-size): Kept contiguous to mirror the encoded container layout.
void validate_container(VariantRef value, std::vector<bool>& referenced_keys, bool is_object) {
const uint8_t value_header =
static_cast<uint8_t>(value.value.data[0]) >> VARIANT_VALUE_HEADER_SHIFT;
if ((is_object && (value_header & 0x20U) != 0) || (!is_object && (value_header & 0x38U) != 0)) {
fail("container reserved bits are nonzero");
}
const auto offset_width = static_cast<uint8_t>((value_header & 0x03U) + 1);
const uint8_t id_width =
is_object ? static_cast<uint8_t>(((value_header >> 2) & 0x03U) + 1) : 0;
const bool is_large = (value_header &
(is_object ? VARIANT_OBJECT_LARGE_MASK : VARIANT_ARRAY_LARGE_MASK)) != 0;
const uint8_t count_width = is_large ? sizeof(uint32_t) : sizeof(uint8_t);
require_bytes(value.value.size - 1, count_width);
const auto count = static_cast<uint32_t>(read_unsigned(value.value.data + 1, count_width));
if (is_large != (count > std::numeric_limits<uint8_t>::max())) {
fail("container count does not use its minimum form");
}
size_t position = 1 + count_width;
if (is_object) {
if (count > (value.value.size - position) / id_width) {
fail("truncated object id table");
}
}
const size_t ids_offset = position;
position += static_cast<size_t>(count) * id_width;
if (static_cast<uint64_t>(count) + 1 > (value.value.size - position) / offset_width) {
fail("truncated container offset table");
}
const size_t offsets_offset = position;
position += (static_cast<size_t>(count) + 1) * offset_width;
const size_t values_offset = position;
uint32_t maximum_id = 0;
uint32_t previous_id = 0;
for (uint32_t index = 0; index < count && is_object; ++index) {
const auto id = static_cast<uint32_t>(read_unsigned(
value.value.data + ids_offset + static_cast<size_t>(index) * id_width, id_width));
if (id >= referenced_keys.size()) {
fail("object field id is outside metadata");
}
if (index != 0 && id <= previous_id) {
fail("object field ids are not strictly increasing");
}
referenced_keys[id] = true;
previous_id = id;
maximum_id = id;
}
if (is_object && id_width != minimum_unsigned_width(maximum_id)) {
fail("object field ids do not use their minimum width");
}
const auto final_offset = static_cast<uint32_t>(read_unsigned(
value.value.data + offsets_offset + static_cast<size_t>(count) * offset_width,
offset_width));
if (offset_width != minimum_unsigned_width(final_offset)) {
fail("container offsets do not use their minimum width");
}
if (final_offset != value.value.size - values_offset) {
fail("container final offset does not end at the value boundary");
}
uint32_t expected_offset = 0;
for (uint32_t index = 0; index < count; ++index) {
const auto offset = static_cast<uint32_t>(read_unsigned(
value.value.data + offsets_offset + static_cast<size_t>(index) * offset_width,
offset_width));
const auto next_offset = static_cast<uint32_t>(read_unsigned(
value.value.data + offsets_offset + (static_cast<size_t>(index) + 1) * offset_width,
offset_width));
if (offset != expected_offset || next_offset <= offset || next_offset > final_offset) {
fail("container offsets are not tightly increasing");
}
VariantRef child {
.metadata = value.metadata,
.value = {value.value.data + values_offset + offset, next_offset - offset}};
validate_node(child, referenced_keys);
expected_offset = next_offset;
}
if (expected_offset != final_offset) {
fail("container values contain an unreferenced gap");
}
}
void validate_node(VariantRef value, std::vector<bool>& referenced_keys) {
if (value.value_size() != value.value.size) {
fail("value contains trailing bytes");
}
switch (value.basic_type()) {
case VariantBasicType::PRIMITIVE:
validate_primitive(value);
return;
case VariantBasicType::SHORT_STRING:
require_valid_utf8(value.get_string(), "short string is not valid UTF-8");
return;
case VariantBasicType::OBJECT:
validate_container(value, referenced_keys, true);
return;
case VariantBasicType::ARRAY:
validate_container(value, referenced_keys, false);
return;
}
fail("unknown basic type");
}
} // namespace
void validate_canonical(VariantMetadataRef metadata, std::span<const VariantRef> rows) {
metadata.validate();
if (metadata.version() != VARIANT_ENCODING_VERSION || !metadata.sorted_strings()) {
fail("metadata must use version 1 and a sorted dictionary");
}
if ((static_cast<uint8_t>(metadata.data[0]) & 0x20U) != 0) {
fail("metadata reserved bit is nonzero");
}
const uint32_t key_count = metadata.dict_size();
uint64_t strings_size = 0;
StringRef previous;
for (uint32_t id = 0; id < key_count; ++id) {
const StringRef key = metadata.key_at(id);
require_valid_utf8(key, "metadata key is not valid UTF-8");
strings_size += key.size;
if (strings_size > std::numeric_limits<uint32_t>::max()) {
fail("metadata dictionary strings exceed uint32");
}
if (id != 0 && !unsigned_bytes_less(previous, key)) {
fail("metadata dictionary is not unsigned-byte sorted and unique");
}
previous = key;
}
if (metadata.offset_size() !=
minimum_unsigned_width(std::max<uint64_t>(key_count, strings_size))) {
fail("metadata does not use its minimum offset width");
}
std::vector<bool> referenced_keys(key_count, false);
for (VariantRef row : rows) {
if (row.metadata.size != metadata.size ||
std::memcmp(row.metadata.data, metadata.data, metadata.size) != 0) {
fail("rows do not share the encoding unit metadata");
}
validate_node(row, referenced_keys);
}
if (std::ranges::find(referenced_keys, false) != referenced_keys.end()) {
fail("metadata contains an unreferenced key");
}
}
void validate_canonical(VariantRef row) {
const std::array<VariantRef, 1> rows {row};
validate_canonical(row.metadata, rows);
}
} // namespace doris