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"
);
}
}
}