#!/usr/bin/env bash
# Maintainer script: documents (and re-runs) how i2pd-sys/vendor/{i2pd-src,boost-src,zlib-src}
# were produced. Not run automatically by `cargo build` -- this is for refreshing the committed
# snapshots when upstream i2pd/Boost/zlib move, not a build-time dependency fetcher (a git
# submodule/build-time network fetch can't survive `cargo package`/crates.io -- see the plan doc
# this crate's build.rs module comment references for why everything here is a committed source
# snapshot instead).
#
# Usage: run each section's commands from a scratch directory, then copy/diff the result into
# the paths named below. This script is deliberately not "curl | bash"-able end to end: each step
# needs a human to review the diff (especially the six i2pd patches, applied automatically by
# i2pd_snapshot from scripts/patches/*.patch) before committing.

set -euo pipefail

SCRATCH="${SCRATCH:-$(mktemp -d)}"
# One level up from scripts/ is the repo root, wherever the checkout happens to be named --
# deliberately NOT "two levels up + append i2pd-sys/" (that only worked by coincidence when the
# checkout directory happened to be named exactly `i2pd-sys`; cloned under any other name, e.g. a
# fork, it silently resolved to a nonexistent sibling path and `mkdir -p` would create a stray
# vendor tree there instead of erroring).
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VENDOR="$REPO_ROOT/vendor"

if ! grep -q '^name = "i2pd-sys"' "$REPO_ROOT/Cargo.toml" 2>/dev/null; then
  echo "error: expected $REPO_ROOT/Cargo.toml to be i2pd-sys's own manifest -- is this script" >&2
  echo "still at <i2pd-sys checkout>/scripts/update-vendor.sh?" >&2
  exit 1
fi

echo "Scratch directory: $SCRATCH"
echo "Vendor directory:  $VENDOR"

# ---------------------------------------------------------------------------
# 1. i2pd — trim a fresh checkout of the pinned tag down to what the CMake build needs
# ---------------------------------------------------------------------------
i2pd_snapshot() {
  local tag="${1:?usage: i2pd_snapshot <tag, e.g. 2.61.0>}"
  local src="$SCRATCH/i2pd"
  rm -rf "$src"
  git clone --depth 1 --branch "$tag" https://github.com/PurpleI2P/i2pd.git "$src"

  local dst="$VENDOR/i2pd-src"
  rm -rf "$dst"
  mkdir -p "$dst"
  # Only libi2pd/libi2pd_client/build/i18n are needed for a WITH_LIBRARY-only build (no daemon
  # binary) -- drop contrib/daemon/debian/docs/libi2pd_wrapper/tests/Win32/Makefile.* entirely.
  cp -r "$src/libi2pd" "$dst/libi2pd"
  cp -r "$src/libi2pd_client" "$dst/libi2pd_client"
  cp -r "$src/build" "$dst/build"
  cp -r "$src/i18n" "$dst/i18n"
  cp "$src/LICENSE" "$dst/LICENSE"
  cp "$src/README.md" "$dst/README.md"
  git -C "$src" rev-parse HEAD > "$dst/SNAPSHOT_COMMIT.txt"

  # AWS-LC compatibility patches -- see 'i2pd AWS-LC compatibility patches' further down in this
  # script for what each one does and why. Applied automatically (verified to `git apply` cleanly
  # against a pristine checkout of the tag this snapshot was originally cut from) so a re-run
  # can't silently ship without them; still worth reviewing `git diff` against upstream afterwards
  # since these touch crypto code, and re-verifying they still apply cleanly if upstream i2pd has
  # since touched the same lines (a rejected hunk means the patch needs updating for the new tag,
  # not force-applying).
  local patch
  for patch in "$REPO_ROOT/scripts/patches"/*.patch; do
    echo "applying $(basename "$patch")..."
    (cd "$dst" && git apply "$patch")
  done

  echo "i2pd $tag snapshotted to $dst, with the six AWS-LC/AWS-LC-FIPS compatibility patches applied --"
  echo "review the diff against a pristine checkout, then re-verify with:"
  echo "  cargo test -p i2pd-sys --test roundtrip -- --ignored"
}

# ---------------------------------------------------------------------------
# 2. zlib — trim to just the source files needed to compile libz directly via cc::Build
# ---------------------------------------------------------------------------
zlib_snapshot() {
  local version="${1:?usage: zlib_snapshot <version, e.g. 1.3.1>}"
  local tarball="$SCRATCH/zlib-$version.tar.gz"
  # zlib.net's own download links have moved/404'd before; the GitHub release asset is reliable.
  curl -fsSL -o "$tarball" \
    "https://github.com/madler/zlib/releases/download/v$version/zlib-$version.tar.gz"
  tar -xzf "$tarball" -C "$SCRATCH"
  local src="$SCRATCH/zlib-$version"

  local dst="$VENDOR/zlib-src"
  rm -rf "$dst"
  mkdir -p "$dst"
  local files=(
    adler32.c compress.c crc32.c deflate.c gzclose.c gzlib.c gzread.c gzwrite.c infback.c
    inflate.c inftrees.c inffast.c trees.c uncompr.c zutil.c
    zlib.h zconf.h.in crc32.h deflate.h gzguts.h inffast.h inffixed.h inflate.h inftrees.h
    trees.h zutil.h LICENSE README
  )
  for f in "${files[@]}"; do cp "$src/$f" "$dst/$f"; done
  # zconf.h.in needs no template substitution for any platform this crate supports (only an
  # `@(#)` SCCS tag comment references the `@` marker autotools/CMake would otherwise process) --
  # just use it directly as zconf.h.
  cp "$dst/zconf.h.in" "$dst/zconf.h"

  echo "zlib $version snapshotted to $dst"
}

# ---------------------------------------------------------------------------
# 3. Boost — extract a minimal CMake-buildable subset with `bcp`
# ---------------------------------------------------------------------------
# Boost's classic release tarball (boost.org downloads) CANNOT be built with Boost's own CMake
# support: release tarballs consolidate all headers into a flat `boost/` directory, but the CMake
# build expects per-library `libs/<name>/include/` layout (see
# <boost-checkout>/tools/cmake/README.md). Only the git superproject (or a GitHub release's
# auto-generated source archive) has that layout -- hence cloning the full superproject below.
boost_snapshot() {
  local tag="${1:?usage: boost_snapshot <tag, e.g. boost-1.86.0>}"
  local super="$SCRATCH/boost-superproject"
  # If $SCRATCH is reused across calls (e.g. re-running this function against a different tag in
  # the same shell session), a stale checkout for a *different* tag would otherwise be silently
  # reused as-is -- `git describe` confirms HEAD is actually at the requested tag before trusting
  # an existing checkout, and re-clones from scratch if not.
  if [ -d "$super" ] && [ "$(git -C "$super" describe --tags --exact-match 2>/dev/null)" != "$tag" ]; then
    echo "existing checkout at $super is not tag $tag -- removing and re-cloning"
    rm -rf "$super"
  fi
  if [ ! -d "$super" ]; then
    git clone --branch "$tag" --recurse-submodules --shallow-submodules --depth 1 \
      https://github.com/boostorg/boost.git "$super"
  fi

  # bcp itself needs a couple of mechanical fixes before it'll even build against the same
  # Boost version it ships with: it still calls Filesystem's removed v2 API
  # (.leaf()/.branch_path()/.normalize()) and is missing an explicit
  # <boost/filesystem/directory.hpp> include that operations.hpp used to pull in transitively.
  # This is a known bcp bit-rot issue, not something specific to this vendoring -- if a future
  # Boost release fixes bcp upstream, these seds become no-ops (harmless) or can be dropped.
  local bcp_dir="$super/tools/bcp"
  sed -i 's/\.leaf()/.filename()/g; s/\.branch_path()/.parent_path()/g' \
    "$bcp_dir"/add_dependent_lib.cpp "$bcp_dir"/add_path.cpp "$bcp_dir"/copy_path.cpp \
    "$bcp_dir"/file_types.cpp
  sed -i 's/normalized_path\.normalize();/normalized_path = normalized_path.lexically_normal();/' \
    "$bcp_dir"/add_path.cpp
  sed -i 's|#include <boost/filesystem/operations.hpp>|#include <boost/filesystem/operations.hpp>\n#include <boost/filesystem/directory.hpp>|' \
    "$bcp_dir"/add_dependent_lib.cpp "$bcp_dir"/add_path.cpp

  ( cd "$super" && ./bootstrap.sh && ./b2 headers -j"$(nproc)" && ./b2 tools/bcp -j"$(nproc)" )

  local out="$SCRATCH/boost-bcp-out"
  rm -rf "$out"
  mkdir -p "$out"
  # --scan (not the plain "library name" mode) finds the TRUE dependency closure by grepping
  # i2pd's actual #include usage, rather than guessing from i2pd's declared find_package
  # COMPONENTS (which only lists the libraries with compiled .cpp sources, missing header-only
  # ones i2pd uses directly like Asio/PropertyTree/DynamicBitset).
  "$super/dist/bin/bcp" --boost="$super" --scan \
    "$VENDOR/i2pd-src"/libi2pd/*.cpp "$VENDOR/i2pd-src"/libi2pd/*.h \
    "$VENDOR/i2pd-src"/libi2pd_client/*.cpp "$VENDOR/i2pd-src"/libi2pd_client/*.h \
    "$VENDOR/i2pd-src"/i18n/*.cpp "$VENDOR/i2pd-src"/i18n/*.h \
    "$out"

  # bcp's --scan mode is used above only to discover which headers are actually referenced (its
  # own dependency-following works file-by-file, not library-by-library) -- but *whether* it then
  # emits a real libs/<name>/{include,CMakeLists.txt} for a given library, vs. only flattening
  # that library's headers into the throwaway top-level boost/ tree bcp also produces, has been
  # observed (2026-07-23) to be unreliable even for i2pd's own *direct* dependencies (filesystem,
  # asio, property_tree, etc. -- the exact BOOST_INCLUDE_LIBRARIES list build.rs passes to CMake),
  # not just transitive header-only ones. Since that full list is already known statically (it's
  # build.rs's own BOOST_INCLUDE_LIBRARIES), there's no reason to depend on bcp's per-library
  # heuristic for it at all -- copy every library i2pd needs (direct or header-only-transitive)
  # in from the superproject by hand, the same way, unconditionally. bcp's --scan invocation above
  # is kept anyway since its own copied files still validate that these libraries are genuinely
  # referenced by i2pd's actual #includes, not just guessed. The current full list (found by
  # iteratively re-running the CMake configure step and copying in whatever it reports missing)
  # is below. If bumping the Boost version surfaces a *different* missing-target error during
  # `cmake -S vendor/boost-src -B <dir> -DBOOST_INCLUDE_LIBRARIES=...` (see build.rs for the
  # current list), add the missing library name to this list.
  local direct_deps=(filesystem program_options atomic system date_time asio property_tree dynamic_bitset)
  local header_only_deps=(
    algorithm align any assert bind concept_check container container_hash core describe detail
    function functional integer intrusive io iterator lexical_cast move mp11 mpl optional
    preprocessor range scope smart_ptr tuple type_index type_traits utility variant2
    winapi static_assert throw_exception function_types tokenizer array unordered fusion
    conversion typeof variant pool multi_index
  )
  for name in "${direct_deps[@]}" "${header_only_deps[@]}"; do
    mkdir -p "$out/libs/$name"
    for sub in include src CMakeLists.txt; do
      # rm -rf the destination first: for direct_deps in particular, bcp's own --scan may have
      # already partially populated $out/libs/$name/$sub (e.g. just a handful of .cpp files it
      # decided it needed) before this unconditional copy runs -- cp -r into an *existing*
      # directory nests the source inside it (.../src/src) instead of replacing it. Since our own
      # copy from $super is always the complete, authoritative version, always let it win outright
      # rather than trying to preserve or merge with whatever bcp left behind.
      if [ -e "$super/libs/$name/$sub" ]; then
        rm -rf "$out/libs/$name/$sub"
        cp -r "$super/libs/$name/$sub" "$out/libs/$name/$sub"
      fi
    done
  done
  mkdir -p "$out/libs/numeric/conversion"
  for sub in include src CMakeLists.txt; do
    [ -e "$super/libs/numeric/conversion/$sub" ] && \
      cp -r "$super/libs/numeric/conversion/$sub" "$out/libs/numeric/conversion/$sub"
  done
  # First-batch libraries bcp only partially populated (CMakeLists.txt only, no include/) --
  # ensure their headers are present too. (Deliberately NOT `test` -- Boost.Test is a unit-test
  # framework with no legitimate runtime dependency from i2pd; verified nothing in i2pd-src or
  # the rest of this vendored subset references `boost/test`, so it's excluded here rather than
  # vendoring a multi-MB framework that would just sit unused.)
  for name in config exception predef regex timer; do
    # Unlike the header_only_deps loop above, this doesn't assume bcp left nothing behind for
    # these -- but it also mustn't assume bcp left *something* behind (a directory to fill
    # gaps in): observed (2026-07-23) to vary run-to-run whether bcp's --scan populates any
    # libs/<name>/ stub here at all, vs. only flattening these into the throwaway boost/ tree.
    # mkdir -p first makes this loop correct either way.
    mkdir -p "$out/libs/$name"
    for sub in include src CMakeLists.txt; do
      if [ -e "$super/libs/$name/$sub" ] && [ ! -e "$out/libs/$name/$sub" ]; then
        cp -r "$super/libs/$name/$sub" "$out/libs/$name/$sub"
      fi
    done
  done
  mkdir -p "$out/libs" "$out/tools"
  cp -r "$super/libs/headers" "$out/libs/headers"   # special Boost::headers meta-target
  cp "$super/CMakeLists.txt" "$out/CMakeLists.txt"   # superproject root -- release tarballs omit this
  cp -r "$super/tools/cmake" "$out/tools/cmake"      # BoostRoot.cmake and its support modules

  # Sanity-check the actual output *before* trusting it enough to overwrite the live vendor tree
  # below. direct_deps/header_only_deps are now copied in unconditionally above specifically to
  # not depend on bcp's own (observed 2026-07-23 to be unreliable run-to-run) per-library decision
  # of whether to emit a real libs/<name>/{include,CMakeLists.txt} vs. only flatten a library's
  # headers into the throwaway top-level boost/ tree this function deletes further down -- but
  # numeric/conversion and the first-batch libraries just below still depend on $super (the
  # superproject checkout) having what's expected at the paths this function assumes, and a
  # silently incomplete result here would otherwise only surface much later as a confusing CMake
  # "target not found" error against the *already-overwritten* vendor tree. Check every library
  # both build.rs and this function need, and abort loudly, before that overwrite, if any are
  # missing.
  local all_expected_libs=(
    "${direct_deps[@]}" "${header_only_deps[@]}" numeric/conversion config exception predef regex
    timer headers
  )
  local missing_libs=()
  for lib in "${all_expected_libs[@]}"; do
    if [ ! -e "$out/libs/$lib/CMakeLists.txt" ] && [ ! -d "$out/libs/$lib/include" ]; then
      missing_libs+=("$lib")
    fi
  done
  if [ "${#missing_libs[@]}" -gt 0 ]; then
    echo "error: bcp's output has no usable libs/<name>/ for: ${missing_libs[*]}" >&2
    echo "(only flattened into the throwaway $out/boost/<name> tree instead, if even that --" >&2
    echo "inspect $out/boost/<name>, then copy the real libs/<name>/{include,src,CMakeLists.txt}" >&2
    echo "in from \$super by hand, the same way the header_only_deps loop above does)" >&2
    echo "NOT touching \$VENDOR/boost-src -- the previous snapshot there is untouched." >&2
    return 1
  fi

  # property_tree's own upstream CMakeLists.txt unconditionally declares a `Boost::serialization`
  # link dependency, for an optional ptree_serialization.hpp feature i2pd's actual headers never
  # touch (confirmed: zero `#include`s of boost/serialization or boost/archive anywhere in
  # i2pd-src). Left in place, that one edge drags in serialization -> spirit -> {thread (hence
  # chrono, hence ratio), proto, phoenix} -- none of which i2pd needs either, ~26 MiB / ~2000
  # files of dead weight. See SNAPSHOT_NOTES.txt for the full dependency-chain verification.
  sed -i '/Boost::serialization/d' "$out/libs/property_tree/CMakeLists.txt"
  rm -rf "$out/libs"/{serialization,spirit,phoenix,proto,thread,chrono,ratio}
  # Re-run the "who still depends on X" check below against the *new* tag before trusting this
  # list is unchanged -- Boost's internal dependency graph isn't guaranteed stable across
  # versions: `for l in serialization spirit phoenix proto thread chrono ratio; do echo "=== $l
  # ==="; grep -l "Boost::$l\b" "$out"/libs/*/CMakeLists.txt; done` should print nothing but each
  # library's own self-reference (already removed, so nothing at all) for every name still safe
  # to omit.

  # Drop everything not needed to actually compile the libraries (tests, docs, examples, the b2
  # Jamfiles/Jamroot since we only use the CMake path, and the flat top-level boost/ header tree
  # bcp also produces, which -- confirmed empirically -- BoostRoot.cmake's CMake build never adds
  # to any include path; only each library's own libs/<name>/include is used).
  find "$out/libs" -maxdepth 2 -type d \
    \( -name test -o -name doc -o -name example -o -name examples -o -name xmldoc -o -name data \
       -o -name build -o -name meta -o -name check \) -exec rm -rf {} +
  find "$out" -iname "Jamfile*" -delete
  find "$out" -name "*.jam" -not -path "*/tools/cmake/*" -delete
  rm -rf "$out/boost" "$out/boost.png" "$out/Jamroot"
  # tools/cmake and libs/headers were copied wholesale (cp -r, not the trimmed-copy loops above),
  # so they carry along whatever the superproject submodule checkout had: a test/ suite CMake
  # doesn't need, and a dangling `.git` gitlink file (pointing at this *function's own* $super
  # checkout's .git/modules/<name> -- meaningless, and would be flat-out broken, once copied
  # anywhere else).
  rm -rf "$out/tools/cmake/test"
  find "$out" -name ".git" -delete

  rm -rf "$VENDOR/boost-src"
  cp -r "$out" "$VENDOR/boost-src"
  # SNAPSHOT_NOTES.txt is maintained by hand, not regenerated here -- update it manually with
  # anything that changed (new tag, different trimmed-library set, etc.) after running this.

  echo "Boost $tag subset snapshotted to $VENDOR/boost-src -- re-verify with:"
  echo "  cmake -S $VENDOR/boost-src -B /tmp/boost-verify -DBOOST_INCLUDE_LIBRARIES=filesystem\;program_options\;atomic\;system\;date_time\;asio\;property_tree\;dynamic_bitset -DBUILD_TESTING=OFF"
  echo "  cmake --build /tmp/boost-verify -j\$(nproc)"
}

# ---------------------------------------------------------------------------
# i2pd AWS-LC compatibility patches -- applied automatically by i2pd_snapshot from
# scripts/patches/*.patch (regenerate those files if a new upstream tag causes one to stop
# applying cleanly -- see the comment above the `git apply` loop in i2pd_snapshot).
# ---------------------------------------------------------------------------
# Patches 1-4 are needed because AWS-LC (i2pd-sys's default crypto backend -- see build.rs)
# spoofs OPENSSL_VERSION_NUMBER as OpenSSL 1.1.1 for broad API compatibility, but doesn't
# implement several specific pieces of surface i2pd's already-existing ">= 3.0.0" / legacy
# dual-path code assumes are present whenever the reported version isn't exactly 3.0.0:
#
# 1. libi2pd/Crypto.h: guard OPENSSL_SIPHASH's auto-detection with `&& !defined(OPENSSL_IS_AWSLC)`
#    (AWS-LC has no EVP_PKEY_SIPHASH; i2pd falls back to its own bundled Siphash.h correctly once
#    this macro is left unset).
# 2. libi2pd/Gost.cpp: guard the EC_GROUP_set_curve_name() call with `#ifndef OPENSSL_IS_AWSLC`
#    (AWS-LC's EC_GROUP_new_curve_GFp-created groups can't have a curve name set; the tag is
#    write-only cosmetic metadata -- nothing in i2pd reads it back via
#    EC_GROUP_get_curve_name).
# 3. libi2pd/Crypto.cpp: the raw (non-AEAD) ChaCha20 helper uses EVP_chacha20(), which AWS-LC
#    doesn't expose as an EVP_CIPHER at all (only the combined chacha20-poly1305 AEAD) -- swap to
#    AWS-LC's low-level CRYPTO_chacha_20() under `#ifdef OPENSSL_IS_AWSLC`.
# 4. libi2pd/Gost.cpp: EC_POINT_set_compressed_coordinates() (the generic OpenSSL dispatcher) has
#    no equivalent in AWS-LC -- swap to AWS-LC's GFp-specific EC_POINT_set_compressed_coordinates_GFp()
#    under `#ifdef OPENSSL_IS_AWSLC` (exact same signature; the Gost curve is always GFp).
#
# Patch 5 is unconditional (not AWS-LC-specific, doesn't need OPENSSL_IS_AWSLC) and needed for a
# *different* reason -- see scripts/patches/0004-awslc-fips-hmac-include.patch for the full
# rationale, short version: libi2pd/Crypto.cpp calls plain HMAC() without ever directly including
# <openssl/hmac.h>, relying on it arriving transitively; that transitive path exists under
# regular AWS-LC's header graph but not AWS-LC-FIPS's (see the optional `fips` feature in
# Cargo.toml / README.md's "FIPS" section), so building against `--features fips` fails without
# this patch even though patches 1-4 alone are enough for the default `aws-lc` feature.
#
# Patch 6 (0005-awslc-mlkem-compat.patch) is the newest and largest: it reimplements i2pd's
# post-quantum ML-KEM support (Crypto.h/PostQuantum.h/PostQuantum.cpp) against AWS-LC's own KEM
# API, since upstream's OPENSSL_PQ-gated code targets OpenSSL 3.5's "provider" API, which AWS-LC
# doesn't implement at all -- see the README.md "Post-quantum (ML-KEM) support" section for the
# full rationale, what's been verified (a real generate/encapsulate/decapsulate round-trip,
# and end-to-end reachability of a destination publishing the resulting hybrid LeaseSet2), and
# what hasn't (wire-level interop of the ratchet handshake itself against other live peers).
#
# All six are small, self-contained, and reviewable directly as scripts/patches/*.patch (five
# files -- #2 and #4 above both touch Gost.cpp, so they're combined into one patch file for that
# file). Verified (2026-07-23) to `git apply` cleanly in sequence against a pristine `2.61.0`
# checkout; a full `cargo build` (default `aws-lc` feature) compiles and links i2pd successfully
# against the patched result, including a real ML-KEM-768 round-trip and a live I2P connection
# to a destination publishing the resulting MLKEM768_X25519/ECIES_X25519/ELGAMAL_2048 LeaseSet2.
# `cargo build --no-default-features --features fips` compiled and linked successfully against
# patches 1-5 as of that same date; re-verify patch 6 there too before relying on `fips` + PQ
# together, since it wasn't specifically re-tested under the `fips` backend.

echo "This script defines functions (i2pd_snapshot, zlib_snapshot, boost_snapshot) -- source it"
echo "and call them individually, e.g.:"
echo "  source i2pd-sys/scripts/update-vendor.sh"
echo "  i2pd_snapshot 2.61.0   # applies the 6 AWS-LC/AWS-LC-FIPS patches automatically, review the diff"
echo "  zlib_snapshot 1.3.1"
echo "  boost_snapshot boost-1.86.0   # NOTE: verify its output carefully -- see the warning"
echo "                                 # printed by boost_snapshot itself before trusting it"
