#!/bin/sh
# Offline check that the vendored tree is exactly what the manifest describes.
#
# Fails if a vendored file was edited, added or removed without going through
# `tools/vendor/fetch`, or if src/Makevars and the manifest disagree about the
# define set. Safe to run in CI and from a release tarball.
set -eu

ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$ROOT"
MANIFEST=tools/vendor/manifest.tsv
. tools/vendor/common.sh

status=0
sums=tools/vendor/checksums.sha256

if [ ! -f "$sums" ]; then
    echo "FAIL missing $sums; run tools/vendor/record" >&2
    exit 1
fi

# 1. every recorded file still hashes to the recorded value
while read -r want path; do
    [ -n "$want" ] || continue
    if [ ! -f "$path" ]; then
        printf 'FAIL missing vendored file: %s\n' "$path" >&2
        status=1
        continue
    fi
    got=$(sha256_of "$path")
    if [ "$got" != "$want" ]; then
        printf 'FAIL modified vendored file: %s\n      expected %s\n      got      %s\n' \
            "$path" "$want" "$got" >&2
        status=1
    fi
done < "$sums"

# 2. no vendored file that the manifest does not know about
for src in $(awk 'NR > 1 { print $1 }' FS='\t' "$MANIFEST"); do
    for f in $(vendored_files "$src"); do
        if ! grep -q "  $f\$" "$sums"; then
            printf 'FAIL unrecorded vendored file: %s\n' "$f" >&2
            status=1
        fi
    done
done

# 3. Makevars and the manifest agree on the define set
#
# Both directions read Makevars with its comments stripped. The rule is about
# what reaches the compiler, not about prose: the comments there explain what
# each define is for, and one of them has to name -DNDEBUG to say why
# MINIZ_NO_ASSERT exists. Scanning the raw file reported that comment as an
# undeclared define. Same rule as the standalone-header check, which likewise
# targets declarations and lets a comment say anything.
makevars_flags=$(sed 's/#.*//' src/Makevars)

for src in $(awk 'NR > 1 { print $1 }' FS='\t' "$MANIFEST"); do
    for d in $(manifest_field "$src" defines | tr ',' ' '); do
        if ! printf '%s\n' "$makevars_flags" | grep -q -- "-D$d\b"; then
            printf 'FAIL src/Makevars is missing -D%s (declared for %s)\n' "$d" "$src" >&2
            status=1
        fi
    done
done
# The reverse direction compares whole entries of the defines column. A plain
# grep for the name would match a longer define that happens to contain it
# (-DMINIZ_NO_ARCHIVE is "found" by MINIZ_NO_ARCHIVE_APIS) and would match
# anywhere in the row, the repo URL and tag columns included.
declared_defines=$(
    for src in $(awk 'NR > 1 { print $1 }' FS='\t' "$MANIFEST"); do
        manifest_field "$src" defines | tr ',' ' '
    done
)
for d in $(printf '%s\n' "$makevars_flags" | grep -oE '\-D[A-Z0-9_]+' \
             | sed 's/^-D//' | LC_ALL=C sort -u); do
    found=0
    for e in $declared_defines; do
        if [ "$e" = "$d" ]; then
            found=1
            break
        fi
    done
    if [ "$found" -eq 0 ]; then
        printf 'FAIL src/Makevars defines -D%s, which the manifest does not declare\n' "$d" >&2
        status=1
    fi
done

# 4. the version each source reports about itself matches the manifest, and the
#    test suite asserts that same string (tests cannot read this manifest: tools/
#    is not installed, so the literal is duplicated and checked here instead)
mz_declared=$(manifest_field miniz version_string)
mz_header=$(sed -n 's/^#define MZ_VERSION *"\(.*\)"/\1/p' src/vendor/miniz/miniz.h)
if [ "$mz_declared" != "$mz_header" ]; then
    printf 'FAIL miniz.h reports MZ_VERSION %s, manifest declares %s\n' \
        "$mz_header" "$mz_declared" >&2
    status=1
fi
if ! grep -q "\"$mz_declared\"" tests/testthat/test-abi.R; then
    printf 'FAIL tests/testthat/test-abi.R does not assert miniz version %s\n' \
        "$mz_declared" >&2
    status=1
fi

# 5. the patch set agrees across the manifest, tools/patches/ and the shipped
#    attribution. inst/COPYRIGHTS is the record of what was changed in
#    third-party code, and until this check nothing compared it to anything:
#    it went stale the moment a second patch was added, still claiming one
#    patch and "no other modification", and would have shipped saying so.
#    Checked in all three directions, for the same reason the define check is:
#    a one-way check passes happily while the other side rots.
declared_patches=$(
    for src in $(awk 'NR > 1 { print $1 }' FS='\t' "$MANIFEST"); do
        p=$(manifest_field "$src" patches)
        [ "$p" = "-" ] && continue
        echo "$p" | tr ',' '\n'
    done | grep -v '^$' | LC_ALL=C sort -u
)

for p in $declared_patches; do
    # the file has to exist, or fetch would fail halfway through a re-vendor
    if ! find tools/patches -type f -name "$p" | grep -q .; then
        printf 'FAIL manifest declares patch %s, which does not exist under tools/patches/\n' \
            "$p" >&2
        status=1
    fi
    # and it has to be disclosed, since it modifies someone else's source
    if ! grep -qF "$p" inst/COPYRIGHTS; then
        printf 'FAIL inst/COPYRIGHTS does not mention patch %s, which the manifest declares\n' \
            "$p" >&2
        status=1
    fi
done

# A patch file nobody declares is worse than a missing one: it looks applied,
# and fetch would silently leave it out of the vendored tree.
for f in $(find tools/patches -type f -name '*.patch' 2>/dev/null | LC_ALL=C sort); do
    p=$(basename "$f")
    found=0
    for e in $declared_patches; do
        if [ "$e" = "$p" ]; then
            found=1
            break
        fi
    done
    if [ "$found" -eq 0 ]; then
        printf 'FAIL %s is not declared in the manifest, so fetch would not apply it\n' \
            "$f" >&2
        status=1
    fi
done

# And an attribution naming a patch that is gone describes a modification the
# tree no longer carries, which is the same disclosure problem inverted.
for p in $(grep -oE '[0-9]{4}-[A-Za-z0-9._-]+\.patch' inst/COPYRIGHTS \
           | LC_ALL=C sort -u); do
    found=0
    for e in $declared_patches; do
        if [ "$e" = "$p" ]; then
            found=1
            break
        fi
    done
    if [ "$found" -eq 0 ]; then
        printf 'FAIL inst/COPYRIGHTS mentions patch %s, which the manifest does not declare\n' \
            "$p" >&2
        status=1
    fi
done

if [ "$status" -eq 0 ]; then
    printf 'ok  vendored tree matches %s\n' "$MANIFEST"
fi
exit "$status"
