* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed 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 <algorithm>
#include <cctype>
#include <cstdint>
#include <cstring>
#include <string>
#include <string_view>
#include <vector>
#include "selection_errors.h"
#include "selection_log.h"
namespace OHOS {
namespace SelectionFwk {
namespace {
constexpr uint32_t CONTENT_MAX_BYTES = 6000;
constexpr uint8_t UTF8_CONTINUATION_MIN = 0x80;
constexpr uint8_t UTF8_CONTINUATION_MAX = 0xBF;
constexpr uint8_t UTF8_LEAD_MASK_TWO = 0xE0;
constexpr uint8_t UTF8_LEAD_VAL_TWO = 0xC0;
constexpr uint8_t UTF8_LEAD_MASK_THREE = 0xF0;
constexpr uint8_t UTF8_LEAD_VAL_THREE = 0xE0;
constexpr uint8_t UTF8_LEAD_MASK_FOUR = 0xF8;
constexpr uint8_t UTF8_LEAD_VAL_FOUR = 0xF0;
constexpr uint8_t UTF8_TWO_BYTE_PAYLOAD_MASK = 0x1F;
constexpr uint8_t UTF8_THREE_BYTE_PAYLOAD_MASK = 0x0F;
constexpr uint8_t UTF8_FOUR_BYTE_PAYLOAD_MASK = 0x07;
constexpr uint8_t UTF8_CONTINUATION_PAYLOAD_MASK = 0x3F;
constexpr uint8_t UTF8_CONTINUATION_BITS = 6;
constexpr uint8_t UTF8_CONT_SHIFT_DOUBLE = UTF8_CONTINUATION_BITS * 2;
constexpr uint8_t UTF8_CONT_SHIFT_TRIPLE = UTF8_CONTINUATION_BITS * 3;
constexpr uint8_t UTF8_SEQ_LEN_INVALID = 0;
constexpr uint8_t UTF8_SEQ_LEN_ONE = 1;
constexpr uint8_t UTF8_SEQ_LEN_TWO = 2;
constexpr uint8_t UTF8_SEQ_LEN_THREE = 3;
constexpr uint8_t UTF8_SEQ_LEN_FOUR = 4;
constexpr uint8_t UTF8_CONT_OFFSET_FIRST = 1;
constexpr uint8_t UTF8_CONT_OFFSET_SECOND = 2;
constexpr uint8_t UTF8_CONT_OFFSET_THIRD = 3;
constexpr uint8_t UTF8_TWO_BYTE_MIN = 0xC2;
constexpr uint8_t UTF8_TWO_BYTE_MAX = 0xDF;
constexpr uint8_t UTF8_THREE_BYTE_MIN = 0xE0;
constexpr uint8_t UTF8_THREE_BYTE_MAX = 0xEF;
constexpr uint8_t UTF8_FOUR_BYTE_MIN = 0xF0;
constexpr uint8_t UTF8_FOUR_BYTE_MAX = 0xF4;
constexpr uint8_t ALLOWED_HT = 0x09;
constexpr uint8_t ALLOWED_LF = 0x0A;
constexpr uint8_t ALLOWED_CR = 0x0D;
constexpr uint8_t DEL_BYTE = 0x7F;
constexpr uint8_t ASCII_CONTROL_BOUND = 0x20;
constexpr uint8_t ASCII_SPACE = 0x20;
constexpr uint8_t BOM_BYTE0 = 0xEF;
constexpr uint8_t BOM_BYTE1 = 0xBB;
constexpr uint8_t BOM_BYTE2 = 0xBF;
constexpr uint8_t BOM_LEN = 3;
constexpr size_t BOM_BYTE_INDEX_0 = 0;
constexpr size_t BOM_BYTE_INDEX_1 = 1;
constexpr size_t BOM_BYTE_INDEX_2 = 2;
constexpr uint64_t FNV_OFFSET_BASIS = 14695981039346656037ULL;
constexpr uint64_t FNV_PRIME = 1099511628211ULL;
constexpr uint32_t ASCII_PUNCT_RANGE1_MIN = 0x21;
constexpr uint32_t ASCII_PUNCT_RANGE1_MAX = 0x2F;
constexpr uint32_t ASCII_PUNCT_RANGE2_MIN = 0x3A;
constexpr uint32_t ASCII_PUNCT_RANGE2_MAX = 0x40;
constexpr uint32_t ASCII_PUNCT_RANGE3_MIN = 0x5B;
constexpr uint32_t ASCII_PUNCT_RANGE3_MAX = 0x60;
constexpr uint32_t ASCII_PUNCT_RANGE4_MIN = 0x7B;
constexpr uint32_t ASCII_PUNCT_RANGE4_MAX = 0x7E;
constexpr uint32_t ASCII_UPPER_A = 0x41;
constexpr uint32_t ASCII_UPPER_Z = 0x5A;
constexpr uint32_t ASCII_LOWER_A = 0x61;
constexpr uint32_t ASCII_LOWER_Z = 0x7A;
constexpr uint32_t ASCII_DIGIT_ZERO = 0x30;
constexpr uint32_t ASCII_DIGIT_NINE = 0x39;
constexpr uint32_t CP_HAN_MIN = 0x4E00;
constexpr uint32_t CP_HAN_MAX = 0x9FFF;
constexpr uint32_t CP_HAN_EXT_A_MIN = 0x3400;
constexpr uint32_t CP_HAN_EXT_A_MAX = 0x4DBF;
constexpr uint32_t CP_HAN_COMPAT_MIN = 0xF900;
constexpr uint32_t CP_HAN_COMPAT_MAX = 0xFAFF;
constexpr uint32_t CP_ZWSP = 0x200B;
constexpr uint32_t CP_ZWNJ = 0x200C;
constexpr uint32_t CP_ZWJ = 0x200D;
constexpr uint32_t CP_LRM = 0x200E;
constexpr uint32_t CP_RLM = 0x200F;
constexpr uint32_t CP_WJ = 0x2060;
constexpr uint32_t CP_ZWNBSP = 0xFEFF;
constexpr uint32_t CP_INVALID = 0;
constexpr uint32_t CONFIG_DISABLED = 0;
constexpr uint32_t REPEAT_COUNT_RESET = 0;
constexpr uint32_t REPEAT_COUNT_INITIAL = 1;
}
struct SelectionContentValidationResult {
SelectionServiceError code = INVALID_DATA;
uint32_t byteLen = 0;
uint32_t charLen = 0;
bool truncated = false;
};
struct CharCategoryStat {
uint32_t letters = 0;
uint32_t digits = 0;
uint32_t han = 0;
uint32_t punctuation = 0;
uint32_t whitespace = 0;
uint32_t control = 0;
uint32_t zeroWidth = 0;
uint32_t other = 0;
uint32_t total = 0;
};
struct ValidatorConfig {
uint32_t maxBytes = CONTENT_MAX_BYTES;
uint32_t maxChars = CONFIG_DISABLED;
bool stripBom = true;
bool dropZeroWidth = true;
bool dropControlChars = true;
bool normalizeWhitespace = false;
uint32_t maxRepeat = CONFIG_DISABLED;
};
enum class SanitizeAction {
KEEP,
DROP,
REPLACE_SPACE,
REPLACE_NEWLINE,
};
class SelectionContentValidator {
public:
static SelectionContentValidator& GetInstance();
SelectionContentValidationResult Validate(std::string_view content) const;
SelectionContentValidationResult ValidateWithConfig(std::string_view content,
const ValidatorConfig& cfg) const;
bool CheckByteLength(std::string_view content) const;
uint32_t CountUtf8Chars(std::string_view content) const;
std::string TruncateUtf8Safe(std::string_view content, uint32_t maxBytes) const;
std::string Sanitize(std::string_view content) const;
std::string SanitizeWithConfig(std::string_view content, const ValidatorConfig& cfg) const;
bool IsBlankOnly(std::string_view content) const;
bool ContainsInvalidUtf8(std::string_view content) const;
bool HasBom(std::string_view content) const;
std::string StripBom(std::string_view content) const;
std::string DropZeroWidth(std::string_view content) const;
CharCategoryStat CountCharCategories(std::string_view content) const;
std::string CompressRepeats(std::string_view content, uint32_t maxRepeat) const;
std::string NormalizeWhitespace(std::string_view content) const;
size_t AlignToCharBoundary(std::string_view content, size_t byteOffset) const;
size_t ByteOffsetToCharOffset(std::string_view content, size_t byteOffset) const;
size_t CharOffsetToByteOffset(std::string_view content, size_t charOffset) const;
std::string ExtractFirstNChars(std::string_view content, uint32_t n) const;
uint64_t ComputeHash(std::string_view content) const;
std::vector<SelectionContentValidationResult> ValidateBatch(
const std::vector<std::string_view>& contents) const;
private:
uint8_t GetUtf8SequenceLength(uint8_t firstByte) const;
bool IsUtf8ContinuationByte(uint8_t b) const;
bool IsControlChar(uint8_t b) const;
bool ValidateUtf8Sequence(std::string_view content, size_t pos, uint8_t len) const;
uint32_t DecodeCodePoint(std::string_view content, size_t pos, uint8_t len) const;
bool IsZeroWidthCodePoint(uint32_t cp) const;
bool IsHanCodePoint(uint32_t cp) const;
bool IsPunctuationCodePoint(uint32_t cp) const;
static bool IsAsciiLetterCodePoint(uint32_t cp);
static bool IsDigitCodePoint(uint32_t cp);
SanitizeAction DecideSanitizeAction(uint32_t cp, uint8_t lead, uint8_t len,
const ValidatorConfig& cfg) const;
};
SelectionContentValidator& SelectionContentValidator::GetInstance()
{
static SelectionContentValidator instance;
return instance;
}
uint8_t SelectionContentValidator::GetUtf8SequenceLength(uint8_t firstByte) const
{
if (firstByte < UTF8_CONTINUATION_MIN) {
return UTF8_SEQ_LEN_ONE;
}
if ((firstByte & UTF8_LEAD_MASK_TWO) == UTF8_LEAD_VAL_TWO) {
return UTF8_SEQ_LEN_TWO;
}
if ((firstByte & UTF8_LEAD_MASK_THREE) == UTF8_LEAD_VAL_THREE) {
return UTF8_SEQ_LEN_THREE;
}
if ((firstByte & UTF8_LEAD_MASK_FOUR) == UTF8_LEAD_VAL_FOUR) {
return UTF8_SEQ_LEN_FOUR;
}
return UTF8_SEQ_LEN_INVALID;
}
bool SelectionContentValidator::IsUtf8ContinuationByte(uint8_t b) const
{
return b >= UTF8_CONTINUATION_MIN && b <= UTF8_CONTINUATION_MAX;
}
bool SelectionContentValidator::IsControlChar(uint8_t b) const
{
if (b == ALLOWED_HT || b == ALLOWED_LF || b == ALLOWED_CR) {
return false;
}
return b < ASCII_CONTROL_BOUND || b == DEL_BYTE;
}
bool SelectionContentValidator::ValidateUtf8Sequence(std::string_view content, size_t pos, uint8_t len) const
{
if (len == UTF8_SEQ_LEN_INVALID || pos + len > content.size()) {
return false;
}
uint8_t lead = static_cast<uint8_t>(content[pos]);
if (len == UTF8_SEQ_LEN_ONE) {
return lead < UTF8_CONTINUATION_MIN;
}
if (len == UTF8_SEQ_LEN_TWO && (lead < UTF8_TWO_BYTE_MIN || lead > UTF8_TWO_BYTE_MAX)) {
return false;
}
if (len == UTF8_SEQ_LEN_THREE && (lead < UTF8_THREE_BYTE_MIN || lead > UTF8_THREE_BYTE_MAX)) {
return false;
}
if (len == UTF8_SEQ_LEN_FOUR && (lead < UTF8_FOUR_BYTE_MIN || lead > UTF8_FOUR_BYTE_MAX)) {
return false;
}
for (uint8_t i = UTF8_CONT_OFFSET_FIRST; i < len; ++i) {
uint8_t b = static_cast<uint8_t>(content[pos + i]);
if (!IsUtf8ContinuationByte(b)) {
return false;
}
}
return true;
}
uint32_t SelectionContentValidator::DecodeCodePoint(std::string_view content, size_t pos, uint8_t len) const
{
if (!ValidateUtf8Sequence(content, pos, len)) {
return CP_INVALID;
}
uint8_t b0 = static_cast<uint8_t>(content[pos]);
if (len == UTF8_SEQ_LEN_ONE) {
return b0;
}
if (len == UTF8_SEQ_LEN_TWO) {
return (static_cast<uint32_t>(b0 & UTF8_TWO_BYTE_PAYLOAD_MASK) << UTF8_CONTINUATION_BITS) |
static_cast<uint32_t>(static_cast<uint8_t>(content[pos + UTF8_CONT_OFFSET_FIRST]) &
UTF8_CONTINUATION_PAYLOAD_MASK);
}
if (len == UTF8_SEQ_LEN_THREE) {
return (static_cast<uint32_t>(b0 & UTF8_THREE_BYTE_PAYLOAD_MASK) << UTF8_CONT_SHIFT_DOUBLE) |
(static_cast<uint32_t>(static_cast<uint8_t>(content[pos + UTF8_CONT_OFFSET_FIRST]) &
UTF8_CONTINUATION_PAYLOAD_MASK) << UTF8_CONTINUATION_BITS) |
static_cast<uint32_t>(static_cast<uint8_t>(content[pos + UTF8_CONT_OFFSET_SECOND]) &
UTF8_CONTINUATION_PAYLOAD_MASK);
}
return (static_cast<uint32_t>(b0 & UTF8_FOUR_BYTE_PAYLOAD_MASK) << UTF8_CONT_SHIFT_TRIPLE) |
(static_cast<uint32_t>(static_cast<uint8_t>(content[pos + UTF8_CONT_OFFSET_FIRST]) &
UTF8_CONTINUATION_PAYLOAD_MASK) << UTF8_CONT_SHIFT_DOUBLE) |
(static_cast<uint32_t>(static_cast<uint8_t>(content[pos + UTF8_CONT_OFFSET_SECOND]) &
UTF8_CONTINUATION_PAYLOAD_MASK) << UTF8_CONTINUATION_BITS) |
static_cast<uint32_t>(static_cast<uint8_t>(content[pos + UTF8_CONT_OFFSET_THIRD]) &
UTF8_CONTINUATION_PAYLOAD_MASK);
}
bool SelectionContentValidator::IsZeroWidthCodePoint(uint32_t cp) const
{
return cp == CP_ZWSP || cp == CP_ZWNJ || cp == CP_ZWJ || cp == CP_LRM || cp == CP_RLM ||
cp == CP_WJ || cp == CP_ZWNBSP;
}
bool SelectionContentValidator::IsHanCodePoint(uint32_t cp) const
{
return (cp >= CP_HAN_MIN && cp <= CP_HAN_MAX) ||
(cp >= CP_HAN_EXT_A_MIN && cp <= CP_HAN_EXT_A_MAX) ||
(cp >= CP_HAN_COMPAT_MIN && cp <= CP_HAN_COMPAT_MAX);
}
bool SelectionContentValidator::IsPunctuationCodePoint(uint32_t cp) const
{
return (cp >= ASCII_PUNCT_RANGE1_MIN && cp <= ASCII_PUNCT_RANGE1_MAX) ||
(cp >= ASCII_PUNCT_RANGE2_MIN && cp <= ASCII_PUNCT_RANGE2_MAX) ||
(cp >= ASCII_PUNCT_RANGE3_MIN && cp <= ASCII_PUNCT_RANGE3_MAX) ||
(cp >= ASCII_PUNCT_RANGE4_MIN && cp <= ASCII_PUNCT_RANGE4_MAX);
}
bool SelectionContentValidator::IsAsciiLetterCodePoint(uint32_t cp)
{
return (cp >= ASCII_UPPER_A && cp <= ASCII_UPPER_Z) ||
(cp >= ASCII_LOWER_A && cp <= ASCII_LOWER_Z);
}
bool SelectionContentValidator::IsDigitCodePoint(uint32_t cp)
{
return cp >= ASCII_DIGIT_ZERO && cp <= ASCII_DIGIT_NINE;
}
bool SelectionContentValidator::CheckByteLength(std::string_view content) const
{
return content.size() <= CONTENT_MAX_BYTES;
}
uint32_t SelectionContentValidator::CountUtf8Chars(std::string_view content) const
{
uint32_t count = 0;
size_t pos = 0;
while (pos < content.size()) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID) {
++pos;
continue;
}
if (!ValidateUtf8Sequence(content, pos, len)) {
++pos;
continue;
}
++count;
pos += len;
}
return count;
}
std::string SelectionContentValidator::TruncateUtf8Safe(std::string_view content, uint32_t maxBytes) const
{
if (maxBytes >= content.size()) {
return std::string(content);
}
size_t end = static_cast<size_t>(maxBytes);
while (end > 0 && IsUtf8ContinuationByte(static_cast<uint8_t>(content[end]))) {
--end;
}
return std::string(content.data(), end);
}
std::string SelectionContentValidator::Sanitize(std::string_view content) const
{
std::string out;
out.reserve(content.size());
size_t pos = 0;
while (pos < content.size()) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
++pos;
continue;
}
if (len == UTF8_SEQ_LEN_ONE && IsControlChar(lead)) {
++pos;
continue;
}
out.append(content.data() + pos, len);
pos += len;
}
return out;
}
SanitizeAction SelectionContentValidator::DecideSanitizeAction(uint32_t cp, uint8_t lead, uint8_t len,
const ValidatorConfig& cfg) const
{
if (cfg.dropZeroWidth && IsZeroWidthCodePoint(cp)) {
return SanitizeAction::DROP;
}
if (cfg.dropControlChars && len == UTF8_SEQ_LEN_ONE && IsControlChar(lead)) {
return SanitizeAction::DROP;
}
if (cfg.normalizeWhitespace && (cp == ASCII_SPACE || cp == ALLOWED_HT)) {
return SanitizeAction::REPLACE_SPACE;
}
if (cfg.normalizeWhitespace && (cp == ALLOWED_LF || cp == ALLOWED_CR)) {
return SanitizeAction::REPLACE_NEWLINE;
}
return SanitizeAction::KEEP;
}
std::string SelectionContentValidator::SanitizeWithConfig(std::string_view content,
const ValidatorConfig& cfg) const
{
std::string bomStripped;
if (cfg.stripBom && HasBom(content)) {
bomStripped = content.substr(BOM_LEN);
content = bomStripped;
}
std::string out;
out.reserve(content.size());
size_t pos = 0;
bool prevIsWs = false;
while (pos < content.size()) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
++pos;
prevIsWs = false;
continue;
}
uint32_t cp = DecodeCodePoint(content, pos, len);
switch (DecideSanitizeAction(cp, lead, len, cfg)) {
case SanitizeAction::DROP:
break;
case SanitizeAction::REPLACE_SPACE:
if (!prevIsWs) {
out.push_back(static_cast<char>(ASCII_SPACE));
prevIsWs = true;
}
break;
case SanitizeAction::REPLACE_NEWLINE:
if (!prevIsWs) {
out.push_back(static_cast<char>(ALLOWED_LF));
prevIsWs = true;
}
break;
default:
out.append(content.data() + pos, len);
prevIsWs = false;
break;
}
pos += len;
}
if (cfg.maxRepeat > CONFIG_DISABLED) {
out = CompressRepeats(out, cfg.maxRepeat);
}
return out;
}
bool SelectionContentValidator::IsBlankOnly(std::string_view content) const
{
if (content.empty()) {
return true;
}
size_t pos = 0;
while (pos < content.size()) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
++pos;
continue;
}
if (len == UTF8_SEQ_LEN_ONE) {
if (!std::isspace(static_cast<int>(lead)) && lead != ALLOWED_LF && lead != ALLOWED_CR) {
return false;
}
} else {
return false;
}
pos += len;
}
return true;
}
bool SelectionContentValidator::ContainsInvalidUtf8(std::string_view content) const
{
size_t pos = 0;
while (pos < content.size()) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
return true;
}
pos += len;
}
return false;
}
bool SelectionContentValidator::HasBom(std::string_view content) const
{
return content.size() >= BOM_LEN &&
static_cast<uint8_t>(content[BOM_BYTE_INDEX_0]) == BOM_BYTE0 &&
static_cast<uint8_t>(content[BOM_BYTE_INDEX_1]) == BOM_BYTE1 &&
static_cast<uint8_t>(content[BOM_BYTE_INDEX_2]) == BOM_BYTE2;
}
std::string SelectionContentValidator::StripBom(std::string_view content) const
{
if (HasBom(content)) {
return std::string(content.substr(BOM_LEN));
}
return std::string(content);
}
std::string SelectionContentValidator::DropZeroWidth(std::string_view content) const
{
std::string out;
out.reserve(content.size());
size_t pos = 0;
while (pos < content.size()) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
++pos;
continue;
}
uint32_t cp = DecodeCodePoint(content, pos, len);
if (IsZeroWidthCodePoint(cp)) {
pos += len;
continue;
}
out.append(content.data() + pos, len);
pos += len;
}
return out;
}
CharCategoryStat SelectionContentValidator::CountCharCategories(std::string_view content) const
{
CharCategoryStat stat;
size_t pos = 0;
while (pos < content.size()) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
++pos;
continue;
}
uint32_t cp = DecodeCodePoint(content, pos, len);
if (IsZeroWidthCodePoint(cp)) {
++stat.zeroWidth;
} else if (len == UTF8_SEQ_LEN_ONE && IsControlChar(lead)) {
++stat.control;
} else if (IsAsciiLetterCodePoint(cp)) {
++stat.letters;
} else if (IsDigitCodePoint(cp)) {
++stat.digits;
} else if (IsHanCodePoint(cp)) {
++stat.han;
} else if (IsPunctuationCodePoint(cp)) {
++stat.punctuation;
} else if (cp == ASCII_SPACE || cp == ALLOWED_HT || cp == ALLOWED_LF || cp == ALLOWED_CR) {
++stat.whitespace;
} else {
++stat.other;
}
++stat.total;
pos += len;
}
return stat;
}
std::string SelectionContentValidator::CompressRepeats(std::string_view content, uint32_t maxRepeat) const
{
if (maxRepeat == CONFIG_DISABLED) {
return std::string(content);
}
std::string out;
out.reserve(content.size());
size_t pos = 0;
uint32_t consecutive = REPEAT_COUNT_RESET;
uint32_t lastCp = UINT32_MAX;
uint8_t lastLen = UTF8_SEQ_LEN_INVALID;
while (pos < content.size()) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
++pos;
consecutive = REPEAT_COUNT_RESET;
lastCp = UINT32_MAX;
continue;
}
uint32_t cp = DecodeCodePoint(content, pos, len);
if (cp == lastCp && len == lastLen) {
++consecutive;
} else {
consecutive = REPEAT_COUNT_INITIAL;
lastCp = cp;
lastLen = len;
}
if (consecutive <= maxRepeat) {
out.append(content.data() + pos, len);
}
pos += len;
}
return out;
}
std::string SelectionContentValidator::NormalizeWhitespace(std::string_view content) const
{
std::string out;
out.reserve(content.size());
size_t pos = 0;
bool prevIsWs = false;
while (pos < content.size()) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
++pos;
prevIsWs = false;
continue;
}
uint32_t cp = DecodeCodePoint(content, pos, len);
if (cp == ASCII_SPACE || cp == ALLOWED_HT) {
if (!prevIsWs) {
out.push_back(static_cast<char>(ASCII_SPACE));
prevIsWs = true;
}
} else if (cp == ALLOWED_LF || cp == ALLOWED_CR) {
if (!prevIsWs) {
out.push_back(static_cast<char>(ALLOWED_LF));
prevIsWs = true;
}
} else {
out.append(content.data() + pos, len);
prevIsWs = false;
}
pos += len;
}
return out;
}
size_t SelectionContentValidator::AlignToCharBoundary(std::string_view content, size_t byteOffset) const
{
if (byteOffset >= content.size()) {
return content.size();
}
while (byteOffset > 0 && IsUtf8ContinuationByte(static_cast<uint8_t>(content[byteOffset]))) {
--byteOffset;
}
return byteOffset;
}
size_t SelectionContentValidator::ByteOffsetToCharOffset(std::string_view content, size_t byteOffset) const
{
size_t charOffset = 0;
size_t pos = 0;
size_t target = std::min(byteOffset, content.size());
while (pos < target) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
++pos;
++charOffset;
continue;
}
pos += len;
++charOffset;
}
return charOffset;
}
size_t SelectionContentValidator::CharOffsetToByteOffset(std::string_view content, size_t charOffset) const
{
size_t pos = 0;
size_t count = 0;
while (pos < content.size() && count < charOffset) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
++pos;
++count;
continue;
}
pos += len;
++count;
}
return pos;
}
std::string SelectionContentValidator::ExtractFirstNChars(std::string_view content, uint32_t n) const
{
if (n == CONFIG_DISABLED) {
return std::string();
}
size_t pos = 0;
uint32_t count = 0;
while (pos < content.size() && count < n) {
uint8_t lead = static_cast<uint8_t>(content[pos]);
uint8_t len = GetUtf8SequenceLength(lead);
if (len == UTF8_SEQ_LEN_INVALID || !ValidateUtf8Sequence(content, pos, len)) {
++pos;
++count;
continue;
}
pos += len;
++count;
}
return std::string(content.data(), pos);
}
uint64_t SelectionContentValidator::ComputeHash(std::string_view content) const
{
uint64_t hash = FNV_OFFSET_BASIS;
for (size_t i = 0; i < content.size(); ++i) {
hash ^= static_cast<uint64_t>(static_cast<uint8_t>(content[i]));
hash *= FNV_PRIME;
}
return hash;
}
std::vector<SelectionContentValidationResult> SelectionContentValidator::ValidateBatch(
const std::vector<std::string_view>& contents) const
{
std::vector<SelectionContentValidationResult> results;
results.reserve(contents.size());
for (const auto& item : contents) {
results.push_back(Validate(item));
}
return results;
}
SelectionContentValidationResult SelectionContentValidator::Validate(std::string_view content) const
{
SelectionContentValidationResult result;
result.byteLen = static_cast<uint32_t>(content.size());
result.charLen = CountUtf8Chars(content);
if (content.empty() || IsBlankOnly(content)) {
SELECTION_HILOGW("content is empty or blank-only, byteLen=%{public}u", result.byteLen);
result.code = CANNOT_GET_CONTENT;
return result;
}
if (ContainsInvalidUtf8(content)) {
SELECTION_HILOGW("content contains invalid utf8 sequence, byteLen=%{public}u", result.byteLen);
result.code = INVALID_DATA;
return result;
}
if (!CheckByteLength(content)) {
SELECTION_HILOGW("content exceeds max bytes, byteLen=%{public}u max=%{public}u",
result.byteLen, CONTENT_MAX_BYTES);
result.code = CONTENT_OUT_OF_RANGE;
result.truncated = true;
return result;
}
result.code = INVALID_DATA;
return result;
}
SelectionContentValidationResult SelectionContentValidator::ValidateWithConfig(std::string_view content,
const ValidatorConfig& cfg) const
{
SelectionContentValidationResult result;
result.byteLen = static_cast<uint32_t>(content.size());
result.charLen = CountUtf8Chars(content);
if (content.empty() || IsBlankOnly(content)) {
SELECTION_HILOGW("content is empty or blank-only, byteLen=%{public}u", result.byteLen);
result.code = CANNOT_GET_CONTENT;
return result;
}
if (ContainsInvalidUtf8(content)) {
SELECTION_HILOGW("content contains invalid utf8 sequence, byteLen=%{public}u", result.byteLen);
result.code = INVALID_DATA;
return result;
}
if (cfg.maxBytes != CONFIG_DISABLED && content.size() > cfg.maxBytes) {
SELECTION_HILOGW("content exceeds configured max bytes, byteLen=%{public}u max=%{public}u",
result.byteLen, cfg.maxBytes);
result.code = CONTENT_OUT_OF_RANGE;
result.truncated = true;
return result;
}
if (cfg.maxChars != CONFIG_DISABLED && result.charLen > cfg.maxChars) {
SELECTION_HILOGW("content exceeds configured max chars, charLen=%{public}u max=%{public}u",
result.charLen, cfg.maxChars);
result.code = CONTENT_OUT_OF_RANGE;
result.truncated = true;
return result;
}
result.code = INVALID_DATA;
return result;
}
}
}