diff --git a/build.sh b/build.sh index 45da44de77..0ab9b182c9 100755 --- a/build.sh +++ b/build.sh @@ -238,6 +238,47 @@ check_pdfium_deps() { } # Check pdf_oxide static library. +# pdf_oxide_validate_version +# +# Validates the pdf_oxide version embedded in a lib's string constant pool +# against the pinned . The marker "pdf_oxide " is +# merged into Rust's constant pool and has no fixed length, so a suffixed build +# (e.g. 0.3.73.1) and a clean 0.3.73 whose next pooled constant begins with a +# digit are byte-identical and cannot be told apart. +# +# Capture exactly three dotted segments and reject any trailing [0-9.] as +# ambiguous, rather than silently truncating it into a (wrong) match. +# +# Returns: +# 0 exact match (prints the found version) +# 1 version missing or mismatched (prints the found version, or empty) +# 2 ambiguous: the marker is followed by extra digits/dots in the pool +pdf_oxide_validate_version() { + local pool_text="$1" required="$2" + local found esc + found=$(printf '%s\n' "$pool_text" \ + | grep -oE "pdf_oxide [0-9]+\.[0-9]+\.[0-9]+" | head -1 | cut -d' ' -f2) + if [ -z "$found" ]; then + return 1 + fi + # A digit or dot immediately after the captured "pdf_oxide X.Y.Z" marker + # means a suffixed build (0.3.73.1) or a constant concatenated on to the + # version. Treat it as ambiguous rather than as a wrong version. + # Anchor to the exact captured version (dots escaped) so the third segment + # cannot backtrack and swallow the trailing digit, which would otherwise + # flag a clean 0.3.73 marker as ambiguous. + esc="${found//./\\.}" + if printf '%s\n' "$pool_text" | grep -qE "pdf_oxide ${esc}[0-9.]"; then + return 2 + fi + if [ "$found" != "$required" ]; then + printf '%s\n' "$found" + return 1 + fi + printf '%s\n' "$found" + return 0 +} + check_pdf_oxide_deps() { _seed_from_system "pdf_oxide" || true # Map platform to tarball-internal subdirectory. @@ -268,25 +309,39 @@ check_pdf_oxide_deps() { # lib left over from an earlier pin is reused silently and the upgrade # becomes a no-op. # - # The marker here differs from office_oxide: instead of a standalone - # "0.1.9" line it is "pdf_oxide " merged into Rust's string - # constant pool, so a whole-line match cannot be used. Extract the - # version and compare it exactly — a substring match would let a pin of - # "0.3.7" accept a 0.3.73 lib. The "pdf_oxide " prefix keeps bare - # version numbers of vendored dependencies out of the match. - local found_version - found_version=$(strings "$lib_path" 2>/dev/null \ - | grep -oE "pdf_oxide [0-9]+\.[0-9]+\.[0-9]+" | head -1 | cut -d' ' -f2) - if [ "$found_version" != "$PDF_OXIDE_VERSION" ]; then - echo -e "${RED}Error: pdf_oxide native lib version mismatch${NC}" - echo " Required: v${PDF_OXIDE_VERSION}; found: ${found_version:-unknown}" - echo " A stale lib silently reverts PDF parsing fixes. Refresh:" - echo " rm -rf ${PDF_OXIDE_PREFIX} ragflow_deps/pdf_oxide-go-ffi-linux-amd64.tar.gz" - echo " uv run python3 ragflow_deps/download_go_deps.py" - return 1 - fi - echo " pdf_oxide (static) → ${PDF_OXIDE_PREFIX}" - return 0 + # The version marker "pdf_oxide " is merged into Rust's string + # constant pool (unlike office_oxide's standalone "0.1.9" line), so we + # capture exactly three dotted segments and reject any trailing [0-9.] + # as ambiguous — a suffixed build (0.3.73.1) or a constant that is + # byte-identical to one must not be silently accepted. The "pdf_oxide " + # prefix keeps bare version numbers of vendored dependencies out of the + # match. + local pool_text found_version rc + pool_text=$(strings "$lib_path" 2>/dev/null) + found_version=$(pdf_oxide_validate_version "$pool_text" "$PDF_OXIDE_VERSION"); rc=$? + case "$rc" in + 0) + echo " pdf_oxide (static) → ${PDF_OXIDE_PREFIX}" + return 0 + ;; + 2) + echo -e "${RED}Error: pdf_oxide native lib version ambiguous${NC}" + echo " The version marker is followed by extra digits/dots in the" + echo " string pool; this is usually a suffixed build (e.g. ${PDF_OXIDE_VERSION}.1)" + echo " or a concatenated constant byte-identical to one. Refresh the lib" + echo " to a clean pin." + echo " Required: v${PDF_OXIDE_VERSION}" + return 1 + ;; + *) + echo -e "${RED}Error: pdf_oxide native lib version mismatch${NC}" + echo " Required: v${PDF_OXIDE_VERSION}; found: ${found_version:-unknown}" + echo " A stale lib silently reverts PDF parsing fixes. Refresh:" + echo " rm -rf ${PDF_OXIDE_PREFIX} ragflow_deps/pdf_oxide-go-ffi-linux-amd64.tar.gz" + echo " uv run python3 ragflow_deps/download_go_deps.py" + return 1 + ;; + esac fi echo " pdf_oxide (static) not found" @@ -998,4 +1053,8 @@ main() { esac } -main "$@" +# Only run the build when executed directly. When sourced (e.g. by tests), +# skip main so the version-gate helpers can be unit-tested in isolation. +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + main "$@" +fi diff --git a/test/build_pdf_oxide_version.sh b/test/build_pdf_oxide_version.sh new file mode 100644 index 0000000000..6928011293 --- /dev/null +++ b/test/build_pdf_oxide_version.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# TDD test for the pdf_oxide native-lib version gate in build.sh. +# +# The version marker is embedded in Rust's string constant pool as +# "pdf_oxide " and cannot be anchored to a fixed length: a suffixed +# build (0.3.73.1) and a clean 0.3.73 whose next pooled constant begins with a +# digit are byte-identical. So we capture exactly three dotted segments and +# treat any trailing [0-9.] as ambiguous rather than silently truncating it +# into a (wrong) match. +# +# Run: bash test/build_pdf_oxide_version.sh +set -u + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../build.sh +source "${SCRIPT_DIR}/../build.sh" +# build.sh sets 'set -e'; the test must control its own flow. +set +e + +failures=0 + +# assert_status [] [] +assert_status() { + local desc="$1" expected_rc="$2" actual_rc="$3" expected_out="${4:-}" actual_out="${5:-}" + if [ "$expected_rc" != "$actual_rc" ]; then + echo "FAIL: $desc (expected rc=$expected_rc, got rc=$actual_rc)" + failures=$((failures + 1)) + return + fi + if [ -n "$expected_out" ] && [ "$expected_out" != "$actual_out" ]; then + echo "FAIL: $desc (expected out=[$expected_out], got out=[$actual_out])" + failures=$((failures + 1)) + return + fi + echo "PASS: $desc" +} + +# Helper that runs pdf_oxide_validate_version and captures rc + stdout. +run_validate() { + local pool="$1" required="$2" out rc + out=$(pdf_oxide_validate_version "$pool" "$required"); rc=$? + printf '%s\t%s' "$rc" "$out" +} + +# 1. Exact three-segment match passes (rc 0, prints version). +read -r rc out < <(run_validate "pdf_oxide 0.3.73ForPublicRelease" "0.3.73") +assert_status "exact 0.3.73 match" 0 "$rc" "0.3.73" "$out" + +# 2. Suffixed build 0.3.73.1 is ambiguous (rc 2), never silently accepted. +read -r rc out < <(run_validate "pdf_oxide 0.3.73.1" "0.3.73") +assert_status "suffix 0.3.73.1 is ambiguous" 2 "$rc" + +# 3. A version with an extra appended digit (0.3.731) must be rejected as a +# mismatch (rc 1), NOT silently accepted as 0.3.73. +read -r rc out < <(run_validate "pdf_oxide 0.3.731foobar" "0.3.73") +assert_status "digit-appended 0.3.731 mismatch" 1 "$rc" + +# 4. Truncated pin 0.3.7 must mismatch against a 0.3.73 lib (rc 1). +read -r rc out < <(run_validate "pdf_oxide 0.3.73" "0.3.7") +assert_status "truncated pin 0.3.7 mismatch" 1 "$rc" + +# 5. Wrong three-segment version mismatches (rc 1). +read -r rc out < <(run_validate "pdf_oxide 0.3.72" "0.3.73") +assert_status "wrong version 0.3.72 mismatch" 1 "$rc" + +# 6. No version marker at all mismatches (rc 1). +read -r rc out < <(run_validate "unrelated string pool" "0.3.73") +assert_status "missing marker mismatch" 1 "$rc" + +if [ "$failures" -eq 0 ]; then + echo "ALL TESTS PASSED" + exit 0 +else + echo "$failures TEST(S) FAILED" + exit 1 +fi