blob: f1431e49d4c4e3fc5df5ca3d51d59db7e9da8527 [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.
*/
#pragma once
/// \file iceberg/util/string_util.h
/// \brief Provide string utility helpers.
#include <algorithm>
#include <cerrno>
#include <charconv>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <typeinfo>
#include <utility>
#include <vector>
#include "iceberg/compat/unreachable.h"
#include "iceberg/iceberg_export.h"
#include "iceberg/result.h"
namespace iceberg {
template <typename T>
concept FromChars = requires(const char* p, T& v) { std::from_chars(p, p, v); };
class ICEBERG_EXPORT StringUtils {
public:
/// \brief Lower-case a UTF-8 string using Unicode simple (1:1) case mapping.
///
/// Intended for case-insensitive name matching, similar to Iceberg Java's
/// toLowerCase(Locale.ROOT). The mapping is locale-independent, matching the intent
/// of Locale.ROOT. It uses simple (1:1) case mapping rather than Java's full case
/// mapping, so results differ for a few code points; e.g. U+0130 (capital I with dot
/// above) maps to U+0069 ("i") here, but to U+0069 U+0307 ("i" + combining dot above)
/// in Java. For ASCII and the large majority of letters the two agree.
///
/// Pure-ASCII input takes a byte-wise fast path; utf8proc is only invoked when a
/// non-ASCII byte (>= 0x80) is present. The function is total: it never fails, and
/// input need not be valid UTF-8. A byte that does not begin a valid UTF-8 sequence
/// is copied through unchanged and decoding resumes at the next byte, so the valid
/// code points around it are still lower-cased.
/// See https://github.com/apache/iceberg-cpp/issues/613.
static std::string ToLower(std::string_view str);
/// \brief Upper-case the ASCII letters (a-z) in a string; all other bytes, including
/// multi-byte UTF-8 sequences, are left unchanged.
///
/// Deliberately ASCII-only and, unlike ToLower, not Unicode-aware. It is only used to
/// normalize ASCII enum/codec strings (e.g. "gzip" -> "GZIP", "all" -> "ALL") for
/// case-insensitive comparison. A Unicode upper-case is intentionally not provided:
/// simple case mapping would be wrong for some letters (e.g. "ß" (U+00DF) would stay
/// unchanged instead of becoming "SS"), and no caller needs it.
static std::string ToUpper(std::string_view str);
/// \brief Case-insensitive equality using Unicode simple (1:1) case mapping.
///
/// Equal when the ToLower forms of both operands are equal, so folding follows
/// ToLower's rules (e.g. "İ" (U+0130) folds to "i"). Defined for any byte sequence:
/// ToLower passes invalid UTF-8 bytes through unchanged, so they compare verbatim.
static bool EqualsIgnoreCase(std::string_view lhs, std::string_view rhs) {
const std::optional<bool> fast = AsciiEqualsIgnoreCase(lhs, rhs);
return fast.has_value() ? *fast : (ToLower(lhs) == ToLower(rhs));
}
/// \brief Case-insensitive prefix test using Unicode simple (1:1) case mapping.
///
/// True when the ToLower form of str starts with the ToLower form of prefix, so folding
/// follows ToLower's rules (e.g. "İ" (U+0130) folds to "i"). Defined for any byte
/// sequence: ToLower passes invalid UTF-8 bytes through unchanged, so they compare
/// verbatim.
static bool StartsWithIgnoreCase(std::string_view str, std::string_view prefix) {
if (prefix.size() <= str.size()) {
const std::optional<bool> fast =
AsciiEqualsIgnoreCase(str.substr(0, prefix.size()), prefix);
if (fast.has_value()) {
return *fast;
}
}
return ToLower(str).starts_with(ToLower(prefix));
}
/// \brief Count the number of code points in a UTF-8 string.
static size_t CodePointCount(std::string_view str) {
size_t count = 0;
for (char i : str) {
if ((i & 0xC0) != 0x80) {
count++;
}
}
return count;
}
template <typename T>
requires std::is_arithmetic_v<T> && FromChars<T> && (!std::same_as<T, bool>)
static Result<T> ParseNumber(std::string_view str) {
T value = 0;
auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), value);
if (ec == std::errc()) [[likely]] {
if (ptr != str.data() + str.size()) {
return InvalidArgument("Failed to parse {} from string '{}': trailing characters",
typeid(T).name(), str);
}
return value;
}
if (ec == std::errc::invalid_argument) {
return InvalidArgument("Failed to parse {} from string '{}': invalid argument",
typeid(T).name(), str);
}
if (ec == std::errc::result_out_of_range) {
return InvalidArgument("Failed to parse {} from string '{}': value out of range",
typeid(T).name(), str);
}
::iceberg::unreachable();
}
/// \brief Decode a hex string (upper or lower case) into bytes.
/// Returns an error if the string has odd length or contains invalid hex characters.
static Result<std::vector<uint8_t>> HexStringToBytes(std::string_view hex);
template <typename T>
requires std::is_floating_point_v<T> && (!FromChars<T>)
static Result<T> ParseNumber(std::string_view str) {
T value{};
// strto* require null-terminated input; string_view does not guarantee it.
std::string owned(str);
const char* start = owned.c_str();
char* end = nullptr;
errno = 0;
if constexpr (std::same_as<T, float>) {
value = std::strtof(start, &end);
} else if constexpr (std::same_as<T, double>) {
value = std::strtod(start, &end);
} else {
value = std::strtold(start, &end);
}
if (end == start || end != start + static_cast<std::ptrdiff_t>(owned.size())) {
return InvalidArgument("Failed to parse {} from string '{}': invalid argument",
typeid(T).name(), str);
}
if (errno == ERANGE) {
return InvalidArgument("Failed to parse {} from string '{}': value out of range",
typeid(T).name(), str);
}
return value;
}
/// \brief Parse a boolean from its string representation, ignoring case.
///
/// Mirrors Iceberg Java's Boolean.parseBoolean, which every boolean property in the
/// Java implementation is read through: "true" in any case reads as true, and every
/// other value reads as false, including "yes", "1" and typos such as "ture". Parsing
/// therefore cannot fail, so a caller that wants to reject a malformed value has to
/// check for it separately.
static bool ParseBoolean(std::string_view str) { return EqualsIgnoreCase(str, "true"); }
private:
// ASCII-only case mappings. These avoid std::toupper/std::tolower, which are
// locale-dependent and have undefined behavior for negative char values.
static constexpr char ToUpperAscii(char c) noexcept {
return (c >= 'a' && c <= 'z') ? static_cast<char>(c - 'a' + 'A') : c;
}
static constexpr char ToLowerAscii(char c) noexcept {
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
}
// True if c is a 7-bit ASCII byte (< 0x80). The cast is required because char may be
// signed, which would make bytes >= 0x80 compare as negative.
static constexpr bool IsAsciiByte(char c) noexcept {
return (static_cast<unsigned char>(c) & 0x80) == 0;
}
// Case-insensitive equality decided in a single byte-wise pass, without allocating.
// Returns nullopt once a byte of either operand is non-ASCII, because folding can then
// be non-ASCII and length-changing (e.g. "İ" (U+0130) -> "i"), which only ToLower
// knows.
static std::optional<bool> AsciiEqualsIgnoreCase(std::string_view a,
std::string_view b) {
const size_t n = std::min(a.size(), b.size());
for (size_t i = 0; i < n; ++i) {
if (!IsAsciiByte(a[i]) || !IsAsciiByte(b[i])) {
return std::nullopt;
}
if (ToLowerAscii(a[i]) != ToLowerAscii(b[i])) {
return false;
}
}
return a.size() == b.size();
}
};
/// \brief Transparent hash function that supports std::string_view as lookup key
///
/// Enables std::unordered_map to directly accept std::string_view lookup keys
/// without creating temporary std::string objects, using C++20's transparent lookup.
struct ICEBERG_EXPORT StringHash {
using hash_type = std::hash<std::string_view>;
using is_transparent = void;
std::size_t operator()(std::string_view str) const { return hash_type{}(str); }
std::size_t operator()(const char* str) const { return hash_type{}(str); }
std::size_t operator()(const std::string& str) const { return hash_type{}(str); }
};
/// \brief Transparent equality function that supports std::string_view as lookup key
struct ICEBERG_EXPORT StringEqual {
using is_transparent = void;
bool operator()(std::string_view lhs, std::string_view rhs) const { return lhs == rhs; }
bool operator()(const std::string& lhs, const std::string& rhs) const {
return lhs == rhs;
}
};
/// \brief Transparent less function that supports std::string_view as lookup key
struct ICEBERG_EXPORT StringLess {
using is_transparent = void;
bool operator()(std::string_view lhs, std::string_view rhs) const { return lhs < rhs; }
};
} // namespace iceberg