~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
694 lines · 26.0 KB Raw History
//! A Cargo registry of the crates a repository publishes as releases
//! (docs/design/packages.md), one registry for each repository:
//!
//! ```text
//! index = "sparse+https://knotless.dev/cargo/<did>/<repo key>/"
//! cargo login --registry <name>     # <handle>:<app password>
//! cargo publish --registry <name>
//! cargo yank --registry <name> --version 1.0.0 <crate>
//! ```
//!
//! Its address holds no name that can change, as Cargo keeps it in
//! `Cargo.lock`, and in the index lines of crates of other repositories
//! that depend on these. What it answers is what the index has, so a
//! takedown holds, and a publish or a yank puts its record there too, as
//! `cargo publish` waits to find its crate.
//!
//! A write is the owner's alone, by the app password its token holds, which
//! logs in to their PDS for that request and is kept nowhere.

// A handler's error is the answer it sends, made once per request.
#![allow(clippy::result_large_err)]

use super::releases::sending;
use super::*;
use crate::releases::Records;
use axum::body::Body;
use flate2::read::GzDecoder;
use knotless::records::{CargoCrate, RELEASE_MIME, ReleaseFile, ReleasePart, ReleaseRecord, nsid};
use knotless::release::{self, Made, Target};
use knotless::remote::{self, Remote};
use knotless::{keys, push};
use serde_json::{Value, json};
use std::collections::{BTreeMap, HashSet};
use std::io::Read;

/// The largest publish taken, its crate and what is said of it.
const MOST_PUBLISH: usize = 64 << 20;

/// The most of a crate read, unpacked, looking for its `Cargo.toml`.
const MOST_UNPACKED: u64 = 1 << 30;

/// The longest index line a release holds, as its lexicon has it.
const MOST_LINE: usize = 100_000;

/// The most of a crate's `Cargo.toml` read: what `cargo package` writes is
/// a few KiB.
const MOST_MANIFEST: u64 = 1 << 20;

/// The most characters of a crate's description a release holds.
const MOST_DESCRIPTION: usize = 1000;

type Answer = std::result::Result<Response, Response>;

fn refusal(status: StatusCode, detail: impl std::fmt::Display) -> Response {
    let body = json!({"errors": [{"detail": detail.to_string()}]});
    (status, axum::Json(body)).into_response()
}

fn refused(detail: impl std::fmt::Display) -> Response {
    refusal(StatusCode::BAD_REQUEST, detail)
}

fn upstream(detail: anyhow::Error) -> Response {
    refusal(StatusCode::BAD_GATEWAY, format!("{detail:#}"))
}

/// A crate as `cargo publish` describes it.
#[derive(Deserialize, Debug)]
struct Published {
    name: String,
    vers: String,
    #[serde(default)]
    deps: Vec<Dep>,
    #[serde(default)]
    features: BTreeMap<String, Vec<String>>,
    links: Option<String>,
    rust_version: Option<String>,
    description: Option<String>,
}

/// A dependency as `cargo publish` describes it.
#[derive(Deserialize, Debug)]
struct Dep {
    name: String,
    version_req: String,
    #[serde(default)]
    features: Vec<String>,
    optional: Option<bool>,
    default_features: Option<bool>,
    target: Option<String>,
    kind: Option<String>,
    registry: Option<String>,
    explicit_name_in_toml: Option<String>,
}

/// The line of Cargo's index for `crate_`, of SHA-256 `cksum`. Features a
/// Cargo before namespaced features cannot read go in `features2`, as the
/// index asks. Whether it is yanked is said as it is read.
fn index_line(crate_: &Published, cksum: &str) -> String {
    let namespaced = |f: &String| f.starts_with("dep:") || f.contains("?/");
    let (second, first): (BTreeMap<_, _>, BTreeMap<_, _>) = crate_
        .features
        .iter()
        .partition(|(_, wants)| wants.iter().any(namespaced));
    let deps = crate_.deps.iter().map(|d| {
        let renamed = d.explicit_name_in_toml.as_ref();
        json!({
            "name": renamed.unwrap_or(&d.name),
            "req": d.version_req,
            "features": d.features,
            "optional": d.optional.unwrap_or(false),
            "default_features": d.default_features.unwrap_or(true),
            "target": d.target,
            "kind": d.kind.as_deref().unwrap_or("normal"),
            "registry": d.registry,
            "package": renamed.map(|_| &d.name),
        })
    });
    let mut line = json!({
        "name": crate_.name,
        "vers": crate_.vers,
        "deps": deps.collect::<Vec<_>>(),
        "cksum": cksum,
        "features": first,
        "links": crate_.links,
        "rust_version": crate_.rust_version,
        "v": 1,
    });
    if !second.is_empty() {
        line["v"] = json!(2);
        line["features2"] = json!(second);
    }
    line.to_string()
}

/// The JSON and the crate of a publish's body: each its length, four bytes
/// little-endian, and then itself.
fn framed(body: &[u8]) -> Option<(&[u8], &[u8])> {
    let (length, rest) = body.split_first_chunk::<4>()?;
    let (said, rest) = rest.split_at_checked(u32::from_le_bytes(*length) as usize)?;
    let (length, rest) = rest.split_first_chunk::<4>()?;
    (rest.len() == u32::from_le_bytes(*length) as usize).then_some((said, rest))
}

/// The text of the file at `<anything>/Cargo.toml` in a gzipped tar, read
/// as far as it, and so no further than [`MOST_UNPACKED`].
fn manifest_of(crate_: &[u8]) -> Option<String> {
    let mut tar = GzDecoder::new(crate_).take(MOST_UNPACKED);
    let mut header = [0u8; 512];
    loop {
        tar.read_exact(&mut header).ok()?;
        let field = |from: usize, to: usize| {
            let bytes = &header[from..to];
            let end = bytes.iter().position(|b| *b == 0).unwrap_or(bytes.len());
            String::from_utf8_lossy(&bytes[..end]).trim().to_string()
        };
        let (name, size) = (field(0, 100), field(124, 136));
        let size = u64::from_str_radix(&size, 8).ok()?;
        if name.split('/').count() == 2 && name.ends_with("/Cargo.toml") {
            if size > MOST_MANIFEST {
                return None;
            }
            let mut text = String::new();
            tar.by_ref().take(size).read_to_string(&mut text).ok()?;
            return Some(text);
        }
        let padded = size.div_ceil(512) * 512;
        std::io::copy(&mut tar.by_ref().take(padded), &mut std::io::sink()).ok()?;
    }
}

/// The name and version `manifest`'s `[package]` says.
fn package_of(manifest: &str) -> Option<(&str, &str)> {
    let (_, package) = manifest.split_once("[package]\n")?;
    let package = package.split("\n[").next()?;
    let said = |key: &str| {
        package.lines().find_map(|line| {
            let (k, v) = line.split_once('=')?;
            (k.trim() == key).then(|| v.trim().trim_matches('"'))
        })
    };
    Some((said("name")?, said("version")?))
}

/// A version with no build metadata, as Cargo's index tells versions apart.
fn base(version: &str) -> &str {
    version.split('+').next().unwrap_or(version)
}

/// `name`'s releases as crates, the oldest first, each version once however
/// its build metadata differs: a record can always be made that a publish
/// here would refuse, and the first is what was offered first.
fn crates<'a>(releases: &'a [(String, ReleaseRecord)], name: &str) -> Vec<&'a ReleaseRecord> {
    let mut seen = HashSet::new();
    let oldest = releases.iter().rev().map(|(_, r)| r);
    oldest
        .filter(|r| r.package == name && r.cargo.is_some())
        .filter(|r| seen.insert(base(&r.version)))
        .collect()
}

/// `r`'s line of the index, saying whether it is yanked: none if the line
/// is not of `r`.
fn line_of(r: &ReleaseRecord) -> Option<String> {
    let mut line: Value = serde_json::from_str(&r.cargo.as_ref()?.index).ok()?;
    let fields = line.as_object_mut()?;
    let name = fields.get("name")?.as_str()?;
    let version = fields.get("vers")?.as_str()?;
    if name != r.package || version != r.version {
        return None;
    }
    fields.insert("yanked".into(), json!(r.yanked));
    Some(line.to_string())
}

/// Where the index keeps `name`'s file.
fn index_path(name: &str) -> String {
    match name.len() {
        1 => format!("1/{name}"),
        2 => format!("2/{name}"),
        3 => format!("3/{}/{name}", &name[..1]),
        _ => format!("{}/{}/{name}", &name[..2], &name[2..4]),
    }
}

/// `did`'s repository `key` and its releases, by what the index has: else
/// that there is none, its or its owner's taken down among them.
fn opened(app: &App, did: &str, key: &str) -> std::result::Result<Opened, Response> {
    let none = || refusal(StatusCode::NOT_FOUND, "no such repository");
    let index = app.index.as_ref().ok_or_else(none)?;
    let listed = index
        .listed(did, key)
        .map_err(anyhow::Error::from)
        .map_err(upstream)?;
    let listed = listed.ok_or_else(none)?;
    let url = RepoUrl {
        authority: did.into(),
        name: listed.name,
    };
    let releases = index
        .releases(did, key)
        .map_err(anyhow::Error::from)
        .map_err(upstream)?;
    Ok(Opened { url, releases })
}

struct Opened {
    url: RepoUrl,
    releases: Vec<(String, ReleaseRecord)>,
}

/// The owner `did`'s session, by the app password `headers` hold.
async fn logged_in(
    app: &App,
    headers: &HeaderMap,
    did: &str,
) -> std::result::Result<session::Session, Response> {
    let denied = |says: String| refusal(StatusCode::FORBIDDEN, says);
    let token = headers
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok());
    let token = token.and_then(|t| t.split_once(':'));
    let Some((user, password)) = token else {
        return Err(denied(
            "log in: cargo login, with <handle>:<app password>".into(),
        ));
    };
    let session = session::create(&app.ctx, user, password).await;
    let session = session.map_err(|e| denied(format!("{user} could not log in: {e:#}")))?;
    if session.did != did {
        return Err(denied(format!("only {did} writes to this registry")));
    }
    Ok(session)
}

/// `config.json`: where crates are read from and published to.
pub(super) async fn config(
    State(app): State<Arc<App>>,
    headers: HeaderMap,
    Path((did, key)): Path<(String, String)>,
) -> Answer {
    opened(&app, &did, &key)?;
    let here = format!("{}/cargo/{did}/{key}", origin_of(&headers));
    let body = json!({"dl": format!("{here}/dl/{{crate}}/{{version}}/download"), "api": here});
    Ok(axum::Json(body).into_response())
}

/// `me`, where `cargo login` sends its user for a token: what one is.
pub(super) async fn me(
    State(app): State<Arc<App>>,
    Path((did, key)): Path<(String, String)>,
) -> Answer {
    opened(&app, &did, &key)?;
    let body = format!(
        "The token for this registry is a handle and an app password, \
         written <handle>:<app password>, such as alice.example.com:abcd-efgh-ijkl-mnop.\n\n\
         Only {did}, the repository's owner, publishes and yanks here. \
         Make an app password in your PDS's settings (in Bluesky, \
         Settings, then Privacy and security, then App passwords), \
         then give the token to cargo login.\n\n\
         Reading the registry needs no token.\n"
    );
    let kind = (header::CONTENT_TYPE, "text/plain; charset=utf-8");
    Ok(([kind], body).into_response())
}

/// A crate's file of the index, asked again each time, at the cost of a 304.
pub(super) async fn index(
    State(app): State<Arc<App>>,
    headers: HeaderMap,
    Path((did, key, path)): Path<(String, String, String)>,
) -> Answer {
    let opened = opened(&app, &did, &key)?;
    let name = path.rsplit('/').next().unwrap_or_default();
    let found = crates(&opened.releases, name);
    let lines = found.into_iter().filter_map(line_of);
    let mut body = lines.collect::<Vec<_>>().join("\n");
    if body.is_empty() || index_path(name) != path {
        return Err(refusal(StatusCode::NOT_FOUND, format!("no crate {name}")));
    }
    body.push('\n');
    let etag = format!("\"{}\"", release::sha256(body.as_bytes()));
    let cache = (header::CACHE_CONTROL, "no-cache".to_string());
    let tag = (header::ETAG, etag.clone());
    let seen = headers.get(header::IF_NONE_MATCH);
    if seen.is_some_and(|v| v.as_bytes() == etag.as_bytes()) {
        return Ok((StatusCode::NOT_MODIFIED, [cache, tag]).into_response());
    }
    Ok(([cache, tag], body).into_response())
}

/// A crate, read from its owner's PDS once and kept, as a release's files are.
pub(super) async fn download(
    State(app): State<Arc<App>>,
    Path((did, key, name, version)): Path<(String, String, String, String)>,
) -> Answer {
    let opened = opened(&app, &did, &key)?;
    let found = crates(&opened.releases, &name);
    let found = found.into_iter().find(|r| r.version == version);
    let file = format!("{name}-{version}.crate");
    let found = found.and_then(|r| r.files.iter().find(|f| f.name == file));
    let Some(f) = found else {
        return Err(refusal(StatusCode::NOT_FOUND, format!("no {file}")));
    };
    let records = Records::new(app.ctx.clone(), opened.url, name);
    let sent = records
        .send(f, sending(f, format!("\"{}\"", f.sha256)))
        .await;
    sent.map_err(upstream)
}

/// `cargo publish`: the crate uploaded to its owner's PDS, its release made.
pub(super) async fn publish(
    State(app): State<Arc<App>>,
    headers: HeaderMap,
    Path((did, key)): Path<(String, String)>,
    body: Body,
) -> Answer {
    let session = logged_in(&app, &headers, &did).await?;
    let opened = opened(&app, &did, &key)?;
    let large = format!("a publish is at most {MOST_PUBLISH} bytes");
    let body = axum::body::to_bytes(body, MOST_PUBLISH).await;
    let body = body.map_err(|_| refusal(StatusCode::PAYLOAD_TOO_LARGE, large))?;
    let Some((said, crate_)) = framed(&body) else {
        return Err(refused("this is no publish: lengths and sizes differ"));
    };
    let said: Published = serde_json::from_slice(said).map_err(refused)?;
    let (name, version) = (said.name.as_str(), said.vers.as_str());
    let file = format!("{name}-{version}.crate");
    release::check_names(name, version, &[&file]).map_err(refused)?;
    let manifest = manifest_of(crate_)
        .ok_or_else(|| refused("the crate holds no Cargo.toml of at most 1 MiB"))?;
    if package_of(&manifest) != Some((name, version)) {
        let says = format!("what was said is {name} {version}, which its Cargo.toml is not");
        return Err(refused(says));
    }
    let cksum = release::sha256(crate_);
    let line = index_line(&said, &cksum);
    if line.len() > MOST_LINE {
        return Err(refused(format!(
            "{name} says more than {MOST_LINE} bytes of its index line"
        )));
    }
    let target = Target::open(&app.ctx, &opened.url, name, version).await;
    let target = target.map_err(refused)?;
    let auth = session.auth();
    let blobs = push::upload(&app.ctx, &target.remote, &auth, crate_, RELEASE_MIME).await;
    let parts = blobs.map_err(upstream)?;
    let files = vec![ReleaseFile {
        name: file,
        size: crate_.len() as u64,
        sha256: cksum,
        parts: parts.into_iter().map(|blob| ReleasePart { blob }).collect(),
    }];
    let description = said.description.as_ref();
    let cargo = CargoCrate {
        index: line,
        description: description.map(|d| d.chars().take(MOST_DESCRIPTION).collect()),
    };
    let made = Made {
        cargo: Some(cargo),
        ..Made::default()
    };
    target.create(&auth, files, made).await.map_err(upstream)?;
    // It is published now, so what follows only hastens what the feed
    // brings: a failure to read it back or index it is not the publish's,
    // and answering one would have a retry told it is published already.
    let rkey = keys::release_rkey(&key, name, version);
    let got = target
        .remote
        .get_record::<Value>(nsid::RELEASE, &rkey)
        .await;
    let index = app.index.as_ref().expect("opened with an index");
    let got = got.ok().flatten();
    let _ = got.map(|record| index.put(&did, nsid::RELEASE, &rkey, &record.value));
    // Build metadata makes another record's key, so only now can a version
    // be seen to have been published already under another's. Should the
    // index not say, both stay, and it answers the oldest.
    let all = index.releases(&did, &key).unwrap_or_default();
    let found = crates(&all, name);
    let first = found.iter().find(|r| base(&r.version) == base(version));
    if let Some(first) = first.filter(|r| r.version != version) {
        let says = format!("{name} {} is published already", first.version);
        let gone = target
            .remote
            .apply(vec![remote::delete(nsid::RELEASE, &rkey)], &auth);
        gone.await.map_err(upstream)?;
        index
            .delete(&did, nsid::RELEASE, &rkey)
            .map_err(anyhow::Error::from)
            .map_err(upstream)?;
        return Err(refused(says));
    }
    let warnings = json!({"invalid_categories": [], "invalid_badges": [], "other": []});
    Ok(axum::Json(json!({"warnings": warnings})).into_response())
}

async fn yanking(
    app: &App,
    headers: &HeaderMap,
    (did, key, name, version): (String, String, String, String),
    undo: bool,
) -> Answer {
    let session = logged_in(app, headers, &did).await?;
    let opened = opened(app, &did, &key)?;
    let remote = Remote::open(&app.ctx, &opened.url)
        .await
        .map_err(upstream)?;
    let yanked = release::yank_as(&remote, &session.auth(), &name, &version, undo).await;
    yanked.map_err(refused)?;
    let rkey = keys::release_rkey(&key, &name, &version);
    let index = app.index.as_ref().expect("opened with an index");
    let said = if undo {
        index.delete(&did, nsid::RELEASE_YANK, &rkey)
    } else {
        let record = json!({"repo": key, "createdAt": knotless::records::now()});
        index
            .put(&did, nsid::RELEASE_YANK, &rkey, &record)
            .map(|_| ())
    };
    said.map_err(anyhow::Error::from).map_err(upstream)?;
    Ok(axum::Json(json!({"ok": true})).into_response())
}

/// `cargo yank`.
pub(super) async fn yank(
    State(app): State<Arc<App>>,
    headers: HeaderMap,
    Path(asked): Path<(String, String, String, String)>,
) -> Answer {
    yanking(&app, &headers, asked, false).await
}

/// `cargo yank --undo`.
pub(super) async fn unyank(
    State(app): State<Arc<App>>,
    headers: HeaderMap,
    Path(asked): Path<(String, String, String, String)>,
) -> Answer {
    yanking(&app, &headers, asked, true).await
}

#[derive(Deserialize)]
pub(super) struct SearchParams {
    q: Option<String>,
    per_page: Option<usize>,
}

/// `cargo search`: the crates whose names hold what was asked, by name,
/// each at its latest.
pub(super) async fn search(
    State(app): State<Arc<App>>,
    Path((did, key)): Path<(String, String)>,
    Query(p): Query<SearchParams>,
) -> Answer {
    let opened = opened(&app, &did, &key)?;
    let records: Vec<ReleaseRecord> = opened.releases.into_iter().map(|(_, r)| r).collect();
    let q = p.q.unwrap_or_default();
    let names: std::collections::BTreeSet<&str> = records
        .iter()
        .filter(|r| r.cargo.is_some() && r.package.contains(&q))
        .map(|r| r.package.as_str())
        .collect();
    let found = names
        .into_iter()
        .filter_map(|name| release::latest(&records, name));
    let found: Vec<Value> = found
        .map(|r| {
            let said = r.cargo.as_ref().and_then(|c| c.description.as_deref());
            json!({"name": r.package, "max_version": r.version, "description": said})
        })
        .collect();
    let total = found.len();
    let shown: Vec<Value> = found
        .into_iter()
        .take(p.per_page.unwrap_or(10).clamp(1, 100))
        .collect();
    Ok(axum::Json(json!({"crates": shown, "meta": {"total": total}})).into_response())
}

#[cfg(test)]
mod tests {
    use super::*;
    use flate2::Compression;
    use flate2::write::GzEncoder;
    use std::io::Write;

    /// A gzipped tar of each named file, as `cargo package` makes one.
    fn tarball(files: &[(&str, &str)]) -> Vec<u8> {
        let mut gz = GzEncoder::new(Vec::new(), Compression::default());
        for (name, text) in files {
            let mut header = [0u8; 512];
            header[..name.len()].copy_from_slice(name.as_bytes());
            let size = format!("{:011o}\0", text.len());
            header[124..136].copy_from_slice(size.as_bytes());
            gz.write_all(&header).unwrap();
            gz.write_all(text.as_bytes()).unwrap();
            let pad = text.len().div_ceil(512) * 512 - text.len();
            gz.write_all(&vec![0; pad]).unwrap();
        }
        gz.write_all(&[0; 1024]).unwrap();
        gz.finish().unwrap()
    }

    #[test]
    fn a_crates_manifest_is_found_in_it() {
        let toml = "# made\n[package]\nname = \"a\"\nversion = \"1.0.0\"\n\n[lib]\nname = \"b\"\n";
        let crate_ = tarball(&[
            ("a-1.0.0/.cargo_vcs_info.json", "{}"),
            ("a-1.0.0/sub/Cargo.toml", "no"),
            ("a-1.0.0/Cargo.toml", toml),
        ]);
        let found = manifest_of(&crate_).unwrap();
        assert_eq!(package_of(&found), Some(("a", "1.0.0")));
        assert_eq!(manifest_of(&tarball(&[("a-1.0.0/x", "x")])), None);
        assert_eq!(manifest_of(b"not gzip"), None);
        let large = "#".repeat(MOST_MANIFEST as usize + 1);
        assert_eq!(
            manifest_of(&tarball(&[("a-1.0.0/Cargo.toml", &large)])),
            None
        );
        let bad = tarball(&[("a-1.0.0/x", "x")]);
        let mut tar = Vec::new();
        GzDecoder::new(&bad[..]).read_to_end(&mut tar).unwrap();
        tar[124..136].copy_from_slice(b"not a size\0\0");
        let mut gz = GzEncoder::new(Vec::new(), Compression::default());
        gz.write_all(&tar).unwrap();
        assert_eq!(manifest_of(&gz.finish().unwrap()), None);
        assert_eq!(package_of("no package"), None);
        assert_eq!(package_of("[package]\nname = \"a\"\n"), None);
    }

    #[test]
    fn a_publish_is_framed() {
        let body = [
            &2u32.to_le_bytes()[..],
            b"{}",
            &3u32.to_le_bytes()[..],
            b"abc",
        ]
        .concat();
        assert_eq!(framed(&body), Some((&b"{}"[..], &b"abc"[..])));
        assert_eq!(framed(&body[..body.len() - 1]), None);
        assert_eq!(framed(&body[..3]), None);
        assert_eq!(framed(&body[..5]), None);
        assert_eq!(framed(&body[..7]), None);
    }

    #[test]
    fn a_crate_makes_its_index_line() {
        let said: Published = serde_json::from_value(json!({
            "name": "a", "vers": "1.0.0", "links": "z", "rust_version": "1.80",
            "features": {"plain": ["other"], "named": ["dep:b"], "weak": ["b?/x"]},
            "deps": [
                {"name": "b", "version_req": "^1", "explicit_name_in_toml": "c", "optional": true,
                 "default_features": false, "features": ["f"], "target": "cfg(unix)",
                 "kind": "dev", "registry": "https://elsewhere"},
                {"name": "d", "version_req": "^2"},
            ],
        }))
        .unwrap();
        let line: Value = serde_json::from_str(&index_line(&said, "ff")).unwrap();
        assert_eq!(line["cksum"], "ff");
        assert_eq!(line["v"], 2);
        assert_eq!(line["features"], json!({"plain": ["other"]}));
        assert_eq!(
            line["features2"],
            json!({"named": ["dep:b"], "weak": ["b?/x"]})
        );
        assert_eq!(
            line["deps"],
            json!([
                {"name": "c", "req": "^1", "features": ["f"], "optional": true,
                 "default_features": false, "target": "cfg(unix)", "kind": "dev",
                 "registry": "https://elsewhere", "package": "b"},
                {"name": "d", "req": "^2", "features": [], "optional": false,
                 "default_features": true, "target": null, "kind": "normal",
                 "registry": null, "package": null},
            ])
        );
        let plain: Published =
            serde_json::from_value(json!({"name": "a", "vers": "1.0.0"})).unwrap();
        let line: Value = serde_json::from_str(&index_line(&plain, "ff")).unwrap();
        assert_eq!(line["v"], 1);
        assert!(line.get("features2").is_none());
    }

    #[test]
    fn the_index_has_each_version_once_as_cargo_reads_them() {
        for (name, path) in [
            ("a", "1/a"),
            ("ab", "2/ab"),
            ("abc", "3/a/abc"),
            ("abcd", "ab/cd/abcd"),
            ("abcde", "ab/cd/abcde"),
        ] {
            assert_eq!(index_path(name), path);
        }
        let release = |version: &str, line: Option<String>, yanked| {
            let cargo = line.map(|index| CargoCrate {
                index,
                description: None,
            });
            let record = ReleaseRecord {
                repo: "k".into(),
                package: "a".into(),
                version: version.into(),
                commit: None,
                check: None,
                notes: None,
                files: Vec::new(),
                created_at: String::new(),
                cargo,
                yanked,
            };
            (String::new(), record)
        };
        let of = |v: &str| Some(json!({"name": "a", "vers": v}).to_string());
        // Newest first, as the index gives them: the oldest of a version wins.
        let all = [
            release("2.0.0", None, false),
            release("1.1.0+later", of("1.1.0+later"), false),
            release("1.1.0", of("1.1.0"), true),
            release("1.0.0", of("1.0.0"), false),
        ];
        let found: Vec<&str> = crates(&all, "a")
            .iter()
            .map(|r| r.version.as_str())
            .collect();
        assert_eq!(found, ["1.0.0", "1.1.0"]);
        assert!(crates(&all, "b").is_empty());
        let line: Value = serde_json::from_str(&line_of(&all[2].1).unwrap()).unwrap();
        assert_eq!(line["yanked"], true);

        // A line that is not of its release, or none, is passed by.
        for bad in [
            of("9.9.9"),
            Some(json!({"name": "b", "vers": "1.0.0"}).to_string()),
            Some("[]".to_string()),
            Some("not json".to_string()),
            Some(json!({"vers": "1.0.0"}).to_string()),
            Some(json!({"name": "a"}).to_string()),
            None,
        ] {
            assert_eq!(line_of(&release("1.0.0", bad, false).1), None);
        }
    }

    #[test]
    fn a_version_is_told_apart_without_its_build() {
        assert_eq!(base("1.0.0+b.1"), "1.0.0");
        assert_eq!(base("1.0.0-rc.1"), "1.0.0-rc.1");
    }
}