yams/tools/fuzzing/fuzz_text_extractors.cpp
trvon 9e44b172ae
test(fuzz): harnesses for search queries, UTF-8, text extraction, compression
- fuzz_fts5_query: sanitized MCP/CLI search text must execute as MATCH on
  an FTS5 table shaped like documents_fts, also after re-sanitizing.
- fuzz_utf8: sanitizer output is well-formed and preserves valid input; a
  UTF-8 verdict implies well-formed bytes; UTF-16/Latin-1 conversions
  produce well-formed UTF-8.
- fuzz_text_extractors: HTML/plain-text extraction, title and section
  detection on arbitrary bytes, with an output-size bound; run with
  -timeout to catch super-linear scans.
- fuzz_compressed_block: stored blocks decode within the header's size and
  -rss_limit_mb; zstd/LZMA round-trip exactly.

Seeds in generate_corpus.sh; the Forgejo fuzz-smoke lane builds and runs
them and triggers on the parsers' sources. Five minutes per harness on a
Linux x86 host after the fixes in this series: no findings.
2026-09-23 18:28:18 -06:00

60 lines
2.1 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright 2026 YAMS Contributors
// Text extraction from ingested file bytes: HTML and plain-text buffer extractors, HTML title
// and document-section detection. Oracles: no crash; extraction succeeds on arbitrary bytes;
// HTML output is not longer than its input plus one newline per tag; valid UTF-8 input yields
// valid UTF-8 text. Run with -timeout to catch super-linear scans (see html_text_extractor).
#include <yams/common/utf8_utils.h>
#include <yams/extraction/html_text_extractor.h>
#include <yams/extraction/plain_text_extractor.h>
#include <yams/extraction/title_util.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
namespace {
void fuzzRequire(bool condition) {
if (!condition) {
__builtin_trap();
}
}
bool wellFormed(std::string_view text) {
return yams::common::sanitizeUtf8Strict(text) == text;
}
} // namespace
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) {
if (data == nullptr || size > std::size_t{1024} * 1024) {
return 0;
}
const auto bytes = std::span<const std::byte>(reinterpret_cast<const std::byte*>(data), size);
const std::string_view text(reinterpret_cast<const char*>(data), size);
const bool inputValid = wellFormed(text);
yams::extraction::ExtractionConfig config;
yams::extraction::HtmlTextExtractor html;
auto htmlResult = html.extractFromBuffer(bytes, config);
fuzzRequire(htmlResult.has_value());
if (htmlResult.value().success) {
const auto tags = static_cast<std::size_t>(std::count(text.begin(), text.end(), '<'));
fuzzRequire(htmlResult.value().text.size() <= size + tags + 1);
fuzzRequire(!inputValid || wellFormed(htmlResult.value().text));
}
yams::extraction::PlainTextExtractor plain;
auto plainResult = plain.extractFromBuffer(bytes, config);
fuzzRequire(plainResult.has_value());
(void)yams::extraction::util::extractHtmlTitle(text);
(void)yams::extraction::util::detectDocumentSections(text);
return 0;
}