Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/repository/znippy-python/tests/plugin_read_view.rs

//! Holger python backend serve test through a REAL compressed archive built with
//! the python plugin. Proves `fetch` resolves `(name, version)` coords via the
//! typed view — a wheel and an sdist resolve by their authoritative coords, where
//! the old `.tar.gz` filename guess / substring-match would mis-resolve.

use std::fs;

use holger_znippy_python_repository::PipRepoZnippy;
use traits::{ArtifactId, RepositoryBackendTrait};
use znippy_common::plugin::PluginRegistry;
use znippy_compress::compress_dir;
use znippy_plugin_python::NativePythonPlugin;

fn crc32(data: &[u8]) -> u32 {
    let mut crc = 0xFFFF_FFFFu32;
    for &b in data {
        crc ^= b as u32;
        for _ in 0..8 {
            crc = if crc & 1 != 0 {
                (crc >> 1) ^ 0xEDB8_8320
            } else {
                crc >> 1
            };
        }
    }
    !crc
}

fn build_zip_stored(name: &str, data: &[u8]) -> Vec<u8> {
    let nb = name.as_bytes();
    let crc = crc32(data);
    let size = data.len() as u32;
    let mut out = Vec::new();
    let local = out.len() as u32;
    out.extend_from_slice(b"PK\x03\x04");
    out.extend_from_slice(&20u16.to_le_bytes());
    for _ in 0..3 {
        out.extend_from_slice(&0u16.to_le_bytes());
    }
    out.extend_from_slice(&0u16.to_le_bytes());
    out.extend_from_slice(&crc.to_le_bytes());
    out.extend_from_slice(&size.to_le_bytes());
    out.extend_from_slice(&size.to_le_bytes());
    out.extend_from_slice(&(nb.len() as u16).to_le_bytes());
    out.extend_from_slice(&0u16.to_le_bytes());
    out.extend_from_slice(nb);
    out.extend_from_slice(data);
    let cd = out.len() as u32;
    out.extend_from_slice(b"PK\x01\x02");
    out.extend_from_slice(&20u16.to_le_bytes());
    out.extend_from_slice(&20u16.to_le_bytes());
    for _ in 0..4 {
        out.extend_from_slice(&0u16.to_le_bytes());
    }
    out.extend_from_slice(&crc.to_le_bytes());
    out.extend_from_slice(&size.to_le_bytes());
    out.extend_from_slice(&size.to_le_bytes());
    out.extend_from_slice(&(nb.len() as u16).to_le_bytes());
    for _ in 0..3 {
        out.extend_from_slice(&0u16.to_le_bytes());
    }
    out.extend_from_slice(&0u16.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&local.to_le_bytes());
    out.extend_from_slice(nb);
    let cd_size = out.len() as u32 - cd;
    out.extend_from_slice(b"PK\x05\x06");
    out.extend_from_slice(&0u16.to_le_bytes());
    out.extend_from_slice(&0u16.to_le_bytes());
    out.extend_from_slice(&1u16.to_le_bytes());
    out.extend_from_slice(&1u16.to_le_bytes());
    out.extend_from_slice(&cd_size.to_le_bytes());
    out.extend_from_slice(&cd.to_le_bytes());
    out.extend_from_slice(&0u16.to_le_bytes());
    out
}

#[test]
fn python_backend_serves_wheel_and_sdist_via_view() {
    let src = tempfile::tempdir().unwrap();
    let out = tempfile::tempdir().unwrap();

    // A real-zip wheel (so the plugin parses tags) + an sdist tarball.
    let wheel = build_zip_stored("numpy-1.26.0.dist-info/METADATA", b"Name: numpy\n");
    fs::create_dir_all(src.path().join("packages/numpy")).unwrap();
    fs::write(
        src.path()
            .join("packages/numpy/numpy-1.26.0-cp311-cp311-manylinux_x86_64.whl"),
        &wheel,
    )
    .unwrap();

    let sdist = b"requests sdist tarball bytes".to_vec();
    fs::create_dir_all(src.path().join("packages/requests")).unwrap();
    fs::write(
        src.path().join("packages/requests/requests-2.31.0.tar.gz"),
        &sdist,
    )
    .unwrap();

    let archive = out.path().join("py.znippy");
    compress_dir(
        &src.path().to_path_buf(),
        &archive,
        false,
        Some(&PluginRegistry::with_plugin(Box::new(NativePythonPlugin))),
        None,
        None,
    )
    .expect("compress_dir with python plugin");

    let repo = PipRepoZnippy::with_archive("pip".into(), archive).unwrap();

    // fetch via the view → exact wheel bytes by (name, version).
    let wid = ArtifactId {
        namespace: None,
        name: "numpy".into(),
        version: "1.26.0".into(),
    };
    assert_eq!(repo.fetch(&wid).unwrap().as_deref(), Some(wheel.as_slice()));

    // sdist resolves too — the old canonical `{name}-{version}.tar.gz` guess would
    // have worked here, but the view path is coord-authoritative regardless.
    let sid = ArtifactId {
        namespace: None,
        name: "requests".into(),
        version: "2.31.0".into(),
    };
    assert_eq!(repo.fetch(&sid).unwrap().as_deref(), Some(sdist.as_slice()));

    // Absent → None.
    let absent = ArtifactId {
        namespace: None,
        name: "numpy".into(),
        version: "9.9.9".into(),
    };
    assert_eq!(repo.fetch(&absent).unwrap(), None);
}

// ───────────────────────── the change token ─────────────────────────

/// `coordinate -> change_token` for one listing, refusing a row without one.
fn token_map(entries: &[traits::ArtifactEntry]) -> std::collections::BTreeMap<String, String> {
    entries
        .iter()
        .map(|e| {
            let key = format!(
                "{}/{}@{}",
                e.id.namespace.clone().unwrap_or_default(),
                e.id.name,
                e.id.version
            );
            let tok = e
                .change_token
                .clone()
                .unwrap_or_else(|| panic!("{key}: no change token"));
            (key, tok)
        })
        .collect()
}

/// **Every python row carries a change token, and it moves only when the bytes do.**
///
/// Two independent seals of the same three distributions, the second with ONE
/// sdist rewritten (same name, same version, same archive path, different
/// bytes). The token is folded from the per-chunk checksums the archive index
/// carries for that row, so the two unchanged distributions fold to the same
/// token across the seals and the rewritten one does not. Before
/// `PythonPackage::relative_path` this backend listed through the typed view
/// and could not name its row, so it stamped nothing.
#[test]
fn python_entries_carry_a_change_token_that_moves_only_when_the_bytes_do() {
    let seal = |rewrite: bool| -> (tempfile::TempDir, std::path::PathBuf) {
        let src = tempfile::tempdir().unwrap();
        let out = tempfile::tempdir().unwrap();
        let lay = |dir: &str, file: &str, bytes: &[u8]| {
            let d = src.path().join("packages").join(dir);
            fs::create_dir_all(&d).unwrap();
            fs::write(d.join(file), bytes).unwrap();
        };
        lay(
            "numpy",
            "numpy-1.26.0-cp311-cp311-manylinux_x86_64.whl",
            &build_zip_stored("numpy-1.26.0.dist-info/METADATA", b"Name: numpy\n"),
        );
        lay(
            "requests",
            "requests-2.31.0.tar.gz",
            b"requests sdist tarball bytes",
        );
        let sdist: &[u8] = if rewrite {
            b"urllib3 sdist REWRITTEN bytes"
        } else {
            b"urllib3 sdist tarball bytes"
        };
        lay("urllib3", "urllib3-2.1.0.tar.gz", sdist);
        let archive = out.path().join("py.znippy");
        compress_dir(
            &src.path().to_path_buf(),
            &archive,
            false,
            Some(&PluginRegistry::with_plugin(Box::new(NativePythonPlugin))),
            None,
            None,
        )
        .expect("compress_dir with python plugin");
        (out, archive)
    };

    let (_a, first) = seal(false);
    let (_b, second) = seal(true);
    let before = token_map(
        &PipRepoZnippy::with_archive("pip".into(), first)
            .unwrap()
            .list(None, 0)
            .unwrap(),
    );
    let after = token_map(
        &PipRepoZnippy::with_archive("pip".into(), second)
            .unwrap()
            .list(None, 0)
            .unwrap(),
    );

    assert_eq!(
        before.len(),
        3,
        "three rows, every one with a token: {before:?}"
    );
    assert_eq!(
        before.keys().collect::<Vec<_>>(),
        after.keys().collect::<Vec<_>>(),
        "the same coordinates on both sides"
    );
    let moved = "/urllib3@2.1.0";
    assert!(
        before.contains_key(moved),
        "the rewritten row is listed: {before:?}"
    );
    for (k, t) in &before {
        if k == moved {
            assert_ne!(
                t, &after[k],
                "{k}: the bytes changed, so the token must move"
            );
        } else {
            assert_eq!(
                t, &after[k],
                "{k}: the bytes did not change, so the token must not move"
            );
        }
    }
}