server/lib/tests/repository_tokens_with_real_clients.rs
//! ★ **Real package clients carry a repository token** (lane P3): `cargo`, `twine` with `pip`,
//! and `npm`, each configured the way its own documentation says, against a REAL holger whose
//! repositories all demand a credential — and each is refused once the token is revoked.
//!
//! * **cargo**: `cargo publish` to the sparse registry `crates` (the token from
//! `CARGO_REGISTRIES_HOLGER_TOKEN`, sent as the bare `Authorization` header), then a consumer
//! resolves and fetches it (`"auth-required": true` in `config.json` makes cargo send the
//! token on index and download requests). Revoked: the resolve fails with the token's code.
//! * **twine and pip**: Ivan, an Importer, uploads a wheel with `twine upload -u __token__`
//! into the intake chain; it is walked to AVAILABLE; `pip download` reads it from the
//! production PEP 503 index with `__token__` in the index URL. Anna, the admin, may mint a
//! write token for the intake's door, but the chain still refuses her upload: a token never
//! holds more than its owner's duties.
//! * **npm**: a token in `.npmrc` (`//host/npm/:_authToken=`) is sent as Bearer: `npm view`
//! and `npm install` read a packument and its tarball from a raw repository; at the chain's
//! npm door (lane P5) Ivan's token publishes into the intake chain, Anna's reads the
//! AVAILABLE version, a read token is refused a publish (`holger-token-5`), and a revoked
//! one publishes nothing.
//!
//! A client that is not on `PATH` is skipped with a line saying so; the toolchain container
//! carries cargo, and the lane's client image adds pip, twine and npm.
use std::io::Write;
use std::path::Path;
use std::process::Command;
use traits::test_support::hold_port;
use holger_server_lib::grpc::holger_token_proto::token_service_client::TokenServiceClient;
use holger_server_lib::grpc::holger_token_proto::{
ListTokensRequest, MintTokenRequest, RevokeTokenRequest, TokenScope,
};
use holger_server_lib::{read_ron_config, wire_holger, Holger};
use nornir_auth_webauthn::softkey::SoftKey;
const RP_ID: &str = "holger.estate.test";
const ORIGIN: &str = "https://holger.estate.test";
const CONSOLE: &str = "c0nsole-bearer-0123456789abcdefghij";
const WHEEL: &str = "demo_pkg-1.0.0-py3-none-any.whl";
struct Running {
_data: tempfile::TempDir,
_cfg: tempfile::NamedTempFile,
_holger: Holger,
base: String,
grpc: String,
http: u16,
}
fn start() -> Running {
let mut last = String::new();
for _ in 0..8 {
let (grpc, http) = (hold_port(), hold_port());
match try_start(grpc, http) {
Ok(r) => return r,
Err(e) => last = e,
}
}
panic!("server never started; last error: {last}");
}
fn try_start(grpc: u16, http: u16) -> Result<Running, String> {
let data = tempfile::tempdir().unwrap();
let d = data.path().display().to_string();
let raw = |name: &str| {
format!(
r#"(ron_name: "{name}", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")),
ron_data_dir: Some("{d}/{name}"), ron_require_auth: true),"#
)
};
let ron = format!(
r#"(
repositories: [
(ron_name: "crates", ron_repo_type: "rust", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")),
ron_writable_archive: Some("{d}/crates.znippy"), ron_base_url: Some("http://127.0.0.1:{http}"),
ron_require_auth: true),
{incoming} {quarantine} {approved} {production} {npm}
(ron_name: "npm", ron_repo_type: "npm", ron_base_url: Some("http://127.0.0.1:{http}"), ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door"))),
],
exposed_endpoints: [
(ron_name: "door", ron_url: "127.0.0.1:{grpc}", ron_http_url: Some("127.0.0.1:{http}")),
],
storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{d}") ],
audit: (dir: "{d}/audit", enabled: true),
auth: Some((
methods: [
StaticToken(subject: "console", token: Some("{CONSOLE}")),
Passkey(rp_id: "{RP_ID}", rp_origin: "{ORIGIN}"),
],
roles: {{"console": Admin}},
group_roles: {{"holger-admins": Admin, "holger-writers": Writer, "holger-readers": Reader}},
)),
promotion: Some((
ladder_name: "intake",
stages: [
(name: "incoming", repo: "incoming"),
(name: "quarantine", repo: "quarantine"),
(name: "approved", repo: "approved"),
(name: "production", repo: "production"),
],
intake: Some((rules: (sandbox: off),
incoming: "incoming",
quarantine: "quarantine",
approved: "approved",
production: "production",
approvals: 1,
npm: Some("npm"),
)),
)),
)"#,
incoming = raw("incoming"),
quarantine = raw("quarantine"),
approved = raw("approved"),
production = raw("production"),
npm = raw("npm-raw"),
);
let mut cfg = tempfile::NamedTempFile::new().unwrap();
cfg.write_all(ron.as_bytes()).unwrap();
let mut holger: Holger = read_ron_config(cfg.path()).map_err(|e| format!("config: {e}"))?;
holger_server_lib::auth::estate_duties(holger.auth.as_mut().expect("an auth block"));
holger
.instantiate_backends()
.map_err(|e| format!("backends: {e}"))?;
wire_holger(&mut holger).map_err(|e| format!("wire: {e}"))?;
holger.start().map_err(|e| format!("start: {e}"))?;
Ok(Running {
_data: data,
_cfg: cfg,
_holger: holger,
base: format!("http://127.0.0.1:{http}"),
grpc: format!("http://127.0.0.1:{grpc}"),
http,
})
}
async fn wait_healthy(c: &reqwest::Client, base: &str) {
for _ in 0..200 {
if let Ok(r) = c.get(format!("{base}/healthz")).send().await {
if r.status() == 200 {
return;
}
}
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
panic!("gateway never became healthy at {base}");
}
async fn post(
c: &reqwest::Client,
base: &str,
path: &str,
bearer: Option<&str>,
body: serde_json::Value,
) -> (u16, serde_json::Value) {
let mut req = c.post(format!("{base}{path}")).json(&body);
if let Some(b) = bearer {
req = req.bearer_auth(b);
}
let r = req.send().await.expect("the door answers");
let status = r.status().as_u16();
let text = r.text().await.unwrap_or_default();
(
status,
serde_json::from_str(&text).unwrap_or(serde_json::Value::String(text)),
)
}
async fn register(
c: &reqwest::Client,
base: &str,
key: &mut SoftKey,
bearer: Option<&str>,
body: serde_json::Value,
) -> (u16, serde_json::Value) {
let (status, start) = post(c, base, "/-/auth/passkey/register/start", bearer, body).await;
if status != 200 {
return (status, start);
}
let ceremony = start["ceremony_id"]
.as_str()
.expect("a ceremony id")
.to_owned();
let challenge = start["publicKey"]["challenge"]
.as_str()
.expect("a challenge");
let (client_data, attestation) = key.register(RP_ID, ORIGIN, challenge, true);
post(
c,
base,
"/-/auth/passkey/register/finish",
bearer,
serde_json::json!({
"ceremony_id": ceremony,
"id": key.credential_id_b64(),
"response": { "clientDataJSON": client_data, "attestationObject": attestation },
}),
)
.await
}
async fn login(c: &reqwest::Client, base: &str, key: &mut SoftKey) -> String {
let (status, start) = post(
c,
base,
"/-/auth/passkey/login/start",
None,
serde_json::json!({ "uv": "required" }),
)
.await;
assert_eq!(status, 200, "{start}");
let ceremony = start["ceremony_id"]
.as_str()
.expect("a ceremony id")
.to_owned();
let challenge = start["publicKey"]["challenge"]
.as_str()
.expect("a challenge");
let (client_data, auth_data, signature) = key.authenticate(RP_ID, ORIGIN, challenge, true);
let (status, out) = post(
c,
base,
"/-/auth/passkey/login/finish",
None,
serde_json::json!({
"ceremony_id": ceremony,
"id": key.credential_id_b64(),
"response": { "clientDataJSON": client_data, "authenticatorData": auth_data, "signature": signature, "userHandle": null },
}),
)
.await;
assert_eq!(status, 200, "logs in: {out}");
out["token"].as_str().expect("a token").to_owned()
}
fn authed<T>(msg: T, bearer: &str) -> tonic::Request<T> {
let mut r = tonic::Request::new(msg);
r.metadata_mut()
.insert("authorization", format!("Bearer {bearer}").parse().unwrap());
r
}
async fn tokens(grpc: &str) -> TokenServiceClient<tonic::transport::Channel> {
for _ in 0..200 {
if let Ok(c) = TokenServiceClient::connect(grpc.to_string()).await {
return c;
}
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
panic!("gRPC never came up at {grpc}");
}
async fn mint(
ts: &mut TokenServiceClient<tonic::transport::Channel>,
by: &str,
label: &str,
repos: &[&str],
scope: TokenScope,
) -> String {
let m = MintTokenRequest {
label: label.into(),
repositories: repos.iter().map(|s| s.to_string()).collect(),
scope: scope as i32,
days: 1,
};
ts.mint(authed(m, by))
.await
.unwrap_or_else(|e| panic!("{label}: {e}"))
.into_inner()
.secret
}
async fn revoke(ts: &mut TokenServiceClient<tonic::transport::Channel>, by: &str, label: &str) {
let all = ts
.list(authed(ListTokensRequest {}, by))
.await
.unwrap()
.into_inner()
.tokens;
let id = all
.iter()
.find(|t| t.label == label)
.unwrap_or_else(|| panic!("{label} is listed"))
.id
.clone();
ts.revoke(authed(RevokeTokenRequest { id }, by))
.await
.expect("revoked");
}
/// Is `bin` on PATH (it answers `--version`)?
fn have(bin: &str, arg: &str) -> bool {
Command::new(bin)
.arg(arg)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Run `cmd`; its exit and its stdout and stderr together.
fn run(cmd: &mut Command) -> (bool, String) {
let out = cmd.output().unwrap_or_else(|e| panic!("{cmd:?}: {e}"));
(
out.status.success(),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
fn write(path: &Path, body: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, body).unwrap();
}
/// A cargo home of its own: the registry, and no other credential.
fn cargo_home(dir: &Path, http: u16) {
write(
&dir.join("config.toml"),
&format!(
"[registries.holger]\nindex = \"sparse+http://127.0.0.1:{http}/crates/index/\"\ncredential-provider = \"cargo:token\"\n\n[net]\nretry = 0\n\n[publish]\ntimeout = 30\n"
),
);
}
fn cargo(home: &Path, token: &str, dir: &Path, args: &[&str]) -> (bool, String) {
run(Command::new("cargo")
.args(args)
.current_dir(dir)
.env("CARGO_HOME", home)
.env("CARGO_REGISTRIES_HOLGER_TOKEN", token)
.env("CARGO_TARGET_DIR", dir.join("target"))
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_JOBS"))
}
#[tokio::test(flavor = "multi_thread")]
async fn cargo_twine_pip_and_npm_carry_a_repository_token_and_stop_when_it_is_revoked() {
let r = start();
let c = reqwest::Client::builder().build().expect("reqwest client");
wait_healthy(&c, &r.base).await;
let base = r.base.as_str();
let mut ts = tokens(&r.grpc).await;
let work = tempfile::tempdir().unwrap();
let mut anna_key = SoftKey::new(0x41, b"cred-anna-0001");
let (status, reg) = register(
&c,
base,
&mut anna_key,
Some(CONSOLE),
serde_json::json!({ "display": "anna", "role": "admin" }),
)
.await;
assert_eq!(status, 200, "{reg}");
let anna = login(&c, base, &mut anna_key).await;
// Ivan, the Importer: what twine and npm publish as.
let (status, minted) = post(
&c,
base,
"/-/auth/passkey/invites",
Some(&anna),
serde_json::json!({ "display": "ivan", "duty": "importer" }),
)
.await;
assert_eq!(status, 200, "{minted}");
let mut ivan_key = SoftKey::new(0x49, b"cred-ivan-0001");
let (status, reg) = register(
&c,
base,
&mut ivan_key,
None,
serde_json::json!({ "display": "ivan", "invite": minted["invite"].as_str().unwrap() }),
)
.await;
assert_eq!(status, 200, "{reg}");
let ivan = login(&c, base, &mut ivan_key).await;
// ── cargo: publish, resolve and fetch with the token; refused once revoked ──
if have("cargo", "--version") {
let token = mint(&mut ts, &anna, "cargo", &["crates"], TokenScope::ReadWrite).await;
let home = work.path().join("cargo-home");
cargo_home(&home, r.http);
let lib = work.path().join("tokdemo");
write(&lib.join("Cargo.toml"), "[package]\nname = \"tokdemo\"\nversion = \"0.1.0\"\nedition = \"2021\"\ndescription = \"a crate published with a repository token\"\nlicense = \"MIT\"\npublish = [\"holger\"]\n");
write(&lib.join("src/lib.rs"), "pub fn answer() -> u32 { 42 }\n");
let (ok, out) = cargo(
&home,
&token,
&lib,
&[
"publish",
"--registry",
"holger",
"--no-verify",
"--allow-dirty",
],
);
assert!(ok, "cargo publish with the token: {out}");
let app = work.path().join("app");
write(&app.join("Cargo.toml"), "[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2021\"\npublish = false\n\n[dependencies]\ntokdemo = { version = \"0.1.0\", registry = \"holger\" }\n");
write(
&app.join("src/main.rs"),
"fn main() { println!(\"{}\", tokdemo::answer()); }\n",
);
let (ok, out) = cargo(&home, &token, &app, &["fetch"]);
assert!(
ok,
"cargo resolves and fetches from the gated registry with the token: {out}"
);
revoke(&mut ts, &anna, "cargo").await;
let fresh = work.path().join("cargo-home-2");
cargo_home(&fresh, r.http);
let _ = std::fs::remove_file(app.join("Cargo.lock"));
let (ok, out) = cargo(&fresh, &token, &app, &["fetch"]);
assert!(!ok, "a revoked token resolves nothing: {out}");
assert!(out.contains("401"), "cargo says the server refused: {out}");
println!(
"cargo, revoked: {}",
out.lines()
.filter(|l| l.contains("401") || l.contains("holger-token"))
.collect::<Vec<_>>()
.join(" | ")
);
} else {
println!("SKIPPED cargo: not on PATH");
}
// ── twine and pip: through the intake chain, as the Importer ──
if have("twine", "--version")
&& Command::new("python3")
.args(["-m", "pip", "--version"])
.output()
.is_ok_and(|o| o.status.success())
{
let wheel = work.path().join(WHEEL);
std::fs::write(&wheel, wheel_bytes()).unwrap();
let twine = |token: &str| {
run(Command::new("twine")
.args([
"upload",
"--non-interactive",
"--disable-progress-bar",
"--repository-url",
&format!("{base}/incoming/legacy/"),
"-u",
"__token__",
"-p",
token,
])
.arg(&wheel)
.env("TWINE_CERT", "")
.env_remove("TWINE_PASSWORD"))
};
// Anna holds write on the door, so she may mint this; the chain still refuses her.
let annas = mint(
&mut ts,
&anna,
"anna twine",
&["incoming"],
TokenScope::ReadWrite,
)
.await;
let (ok, out) = twine(&annas);
assert!(
!ok && out.contains("403"),
"an admin never imports, token or not: {out}"
);
let up = mint(
&mut ts,
&ivan,
"ivan twine",
&["incoming"],
TokenScope::ReadWrite,
)
.await;
let (ok, out) = twine(&up);
assert!(ok, "twine uploads with __token__: {out}");
for (who, bearer) in [
("ivan", &ivan),
("ivan", &ivan),
("anna", &anna),
("anna", &anna),
("anna", &anna),
("anna", &anna),
] {
let res = c
.post(format!("{base}/-/intake/advance?name={WHEEL}&verb=advance"))
.bearer_auth(bearer)
.send()
.await
.unwrap();
let status = res.status().as_u16();
assert_eq!(
status,
200,
"{who} advances: {}",
res.text().await.unwrap_or_default()
);
}
let read = mint(
&mut ts,
&ivan,
"ivan pip",
&["production"],
TokenScope::Read,
)
.await;
let pip = |token: &str, into: &Path| {
run(Command::new("python3")
.args([
"-m",
"pip",
"download",
"--no-deps",
"--no-cache-dir",
"--disable-pip-version-check",
"--retries",
"0",
"--index-url",
])
.arg(format!(
"http://__token__:{token}@127.0.0.1:{}/production/simple/",
r.http
))
.arg("-d")
.arg(into)
.arg("demo-pkg")
.env("PIP_CONFIG_FILE", "/dev/null"))
};
let got = work.path().join("pip-out");
let (ok, out) = pip(&read, &got);
assert!(ok, "pip reads the production index with __token__: {out}");
assert!(got.join(WHEEL).is_file(), "the wheel is downloaded: {out}");
revoke(&mut ts, &ivan, "ivan pip").await;
let (ok, out) = pip(&read, &work.path().join("pip-out-2"));
assert!(
!ok && out.contains("401"),
"a revoked token reads nothing: {out}"
);
} else {
println!("SKIPPED twine and pip: not on PATH");
}
// ── npm: Bearer from .npmrc, for a packument and its tarball in a raw repository ──
if have("npm", "--version") {
let tarball = npm_tarball(work.path());
let integrity = {
use base64::Engine as _;
use sha2::Digest as _;
format!(
"sha512-{}",
base64::engine::general_purpose::STANDARD.encode(sha2::Sha512::digest(&tarball))
)
};
let packument = serde_json::json!({
"name": "tokdemo",
"dist-tags": { "latest": "1.0.0" },
"versions": { "1.0.0": {
"name": "tokdemo", "version": "1.0.0",
"dist": { "tarball": format!("{base}/npm-raw/-/tokdemo-1.0.0.tgz"), "integrity": integrity },
}},
});
for (path, body) in [
("tokdemo", serde_json::to_vec(&packument).unwrap()),
("-/tokdemo-1.0.0.tgz", tarball),
] {
let res = c
.put(format!("{base}/npm-raw/{path}"))
.bearer_auth(CONSOLE)
.body(body)
.send()
.await
.unwrap();
assert_eq!(
res.status(),
201,
"{path}: {}",
res.text().await.unwrap_or_default()
);
}
let token = mint(&mut ts, &anna, "npm", &["npm-raw"], TokenScope::Read).await;
let npmrc = work.path().join("npmrc");
write(&npmrc, &format!("registry={base}/npm-raw/\n//127.0.0.1:{}/npm-raw/:_authToken={token}\naudit=false\nfund=false\n", r.http));
let npm = |args: &[&str]| {
run(Command::new("npm")
.args(args)
.arg("--userconfig")
.arg(&npmrc)
.arg("--cache")
.arg(work.path().join("npm-cache"))
.env("NO_UPDATE_NOTIFIER", "1"))
};
let (ok, out) = npm(&["view", "tokdemo", "version"]);
assert!(
ok && out.contains("1.0.0"),
"npm view with the token from .npmrc: {out}"
);
let prefix = work.path().join("npm-app");
std::fs::create_dir_all(&prefix).unwrap();
let (ok, out) = npm(&["install", "tokdemo", "--prefix", prefix.to_str().unwrap()]);
assert!(ok, "npm install with the token: {out}");
assert!(
prefix.join("node_modules/tokdemo/package.json").is_file(),
"installed: {out}"
);
revoke(&mut ts, &anna, "npm").await;
let (ok, out) = npm(&["view", "tokdemo", "version", "--prefer-online"]);
assert!(
!ok && (out.contains("401") || out.contains("E401")),
"a revoked token reads nothing: {out}"
);
} else {
println!("SKIPPED npm: not on PATH");
}
// ── the npm door (lane P5): a read token never publishes there ──
let ivan_reads = mint(&mut ts, &ivan, "ivan npm read", &["npm"], TokenScope::Read).await;
let res = c
.put(format!("{base}/npm/tokdoor"))
.bearer_auth(&ivan_reads)
.body("{}")
.send()
.await
.unwrap();
let status = res.status().as_u16();
let said = res.text().await.unwrap_or_default();
assert_eq!(status, 403, "{said}");
assert!(
said.starts_with("[holger-token-5]"),
"a read token publishes nothing at the npm door: {said}"
);
// ── the npm door: `npm publish` with Ivan's token, walked to AVAILABLE, `npm install` with Anna's ──
if have("npm", "--version") {
let publish = mint(
&mut ts,
&ivan,
"ivan npm publish",
&["npm"],
TokenScope::ReadWrite,
)
.await;
let pkg = work.path().join("tokdoor");
write(&pkg.join("package.json"), "{\"name\":\"tokdoor\",\"version\":\"1.0.0\",\"main\":\"index.js\",\"license\":\"MIT\"}\n");
write(&pkg.join("index.js"), "module.exports = 7;\n");
let rc = |name: &str, token: &str| {
let f = work.path().join(name);
write(&f, &format!("registry={base}/npm/\n//127.0.0.1:{}/npm/:_authToken={token}\naudit=false\nfund=false\n", r.http));
f
};
let npm_with = |rcfile: &Path, dir: &Path, args: &[&str]| {
run(Command::new("npm")
.args(args)
.current_dir(dir)
.arg("--userconfig")
.arg(rcfile)
.arg("--cache")
.arg(work.path().join("npm-cache-door"))
.env("NO_UPDATE_NOTIFIER", "1"))
};
let ivan_rc = rc("npmrc-ivan", &publish);
let (ok, out) = npm_with(&ivan_rc, &pkg, &["publish"]);
assert!(
ok,
"npm publish with the Importer's token, into the chain: {out}"
);
let name = "tokdoor/-/tokdoor-1.0.0.tgz";
let mut state = String::from("INGESTED");
'walk: for _ in 0..12 {
if state == "AVAILABLE" {
break;
}
for bearer in [&ivan, &anna] {
let res = c
.post(format!("{base}/-/intake/advance?name={name}&verb=advance"))
.bearer_auth(bearer)
.send()
.await
.unwrap();
if res.status() == 200 {
let v: serde_json::Value =
serde_json::from_str(&res.text().await.unwrap_or_default())
.unwrap_or_default();
state = v["state"].as_str().unwrap_or("?").to_owned();
continue 'walk;
}
}
panic!("held at {state}");
}
assert_eq!(state, "AVAILABLE");
let reads = mint(&mut ts, &anna, "anna npm door", &["npm"], TokenScope::Read).await;
let anna_rc = rc("npmrc-anna", &reads);
let (ok, out) = npm_with(&anna_rc, work.path(), &["view", "tokdoor", "version"]);
assert!(
ok && out.contains("1.0.0"),
"npm view at the npm door with a token: {out}"
);
let prefix = work.path().join("npm-door-app");
std::fs::create_dir_all(&prefix).unwrap();
let (ok, out) = npm_with(
&anna_rc,
work.path(),
&["install", "tokdoor", "--prefix", prefix.to_str().unwrap()],
);
assert!(
ok && prefix.join("node_modules/tokdoor/package.json").is_file(),
"npm install at the npm door with a token: {out}"
);
revoke(&mut ts, &ivan, "ivan npm publish").await;
write(&pkg.join("package.json"), "{\"name\":\"tokdoor\",\"version\":\"1.0.1\",\"main\":\"index.js\",\"license\":\"MIT\"}\n");
let (ok, out) = npm_with(&ivan_rc, &pkg, &["publish"]);
assert!(
!ok && (out.contains("401") || out.contains("E401")),
"a revoked token publishes nothing: {out}"
);
} else {
println!("SKIPPED npm at the npm door: not on PATH");
}
}
/// `package/package.json` and `package/index.js`, as `npm pack` lays them, gzipped.
fn npm_tarball(work: &Path) -> Vec<u8> {
let dir = work.join("npm-src");
write(
&dir.join("package/package.json"),
"{\"name\":\"tokdemo\",\"version\":\"1.0.0\",\"main\":\"index.js\"}\n",
);
write(&dir.join("package/index.js"), "module.exports = 42;\n");
let out = work.join("tokdemo-1.0.0.tgz");
let (ok, said) = run(Command::new("tar")
.arg("czf")
.arg(&out)
.arg("-C")
.arg(&dir)
.arg("package"));
assert!(ok, "{said}");
std::fs::read(out).unwrap()
}
/// A small but well-formed wheel: a stored-only zip with the dist-info METADATA and WHEEL members.
fn wheel_bytes() -> Vec<u8> {
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 member(out: &mut Vec<u8>, central: &mut Vec<u8>, name: &str, body: &[u8]) {
let crc = crc32(body);
let offset = out.len() as u32;
out.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
out.extend_from_slice(&[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&(name.len() as u16).to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(name.as_bytes());
out.extend_from_slice(body);
central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
central.extend_from_slice(&[20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
central.extend_from_slice(&crc.to_le_bytes());
central.extend_from_slice(&(body.len() as u32).to_le_bytes());
central.extend_from_slice(&(body.len() as u32).to_le_bytes());
central.extend_from_slice(&(name.len() as u16).to_le_bytes());
central.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
central.extend_from_slice(&offset.to_le_bytes());
central.extend_from_slice(name.as_bytes());
}
let mut out = Vec::new();
let mut central = Vec::new();
member(
&mut out,
&mut central,
"demo_pkg/__init__.py",
b"__version__ = \"1.0.0\"\n",
);
member(
&mut out,
&mut central,
"demo_pkg-1.0.0.dist-info/METADATA",
b"Metadata-Version: 2.4\nName: demo-pkg\nVersion: 1.0.0\nLicense-Expression: MIT\n",
);
member(
&mut out,
&mut central,
"demo_pkg-1.0.0.dist-info/WHEEL",
b"Wheel-Version: 1.0\nGenerator: holger-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
);
let cd_offset = out.len() as u32;
let cd_len = central.len() as u32;
out.extend_from_slice(¢ral);
out.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
out.extend_from_slice(&[0, 0, 0, 0, 3, 0, 3, 0]);
out.extend_from_slice(&cd_len.to_le_bytes());
out.extend_from_slice(&cd_offset.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out
}