chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
Cargo publish reads at most 1 MiB of Cargo.toml and succeeds once made
crates/knotless-serve/src/web/cargo.rs+22−10
| @@ -42,6 +42,10 @@ 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; | ||
| ||
| @@ -153,6 +157,9 @@ fn manifest_of(crate_: &[u8]) -> Option<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); | ||
| @@ -342,7 +349,8 @@ pub(super) async fn publish( | ||
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"))?; | ||
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)); | ||
| @@ -375,22 +383,21 @@ pub(super) async fn publish( | ||
..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 got = got.map_err(upstream)?; | ||
let index = app.index.as_ref().expect("opened with an index"); | ||
let put = got.map(|record| index.put(&did, nsid::RELEASE, &rkey, &record.value)); | ||
let put = put.transpose().map_err(anyhow::Error::from); | ||
put.map_err(upstream)?; | ||
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. | ||
let all = index | ||
.releases(&did, &key) | ||
.map_err(anyhow::Error::from) | ||
.map_err(upstream)?; | ||
// 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) { | ||
| @@ -528,6 +535,11 @@ mod tests { | ||
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(); | ||
docs/design/packages.md+4−1
| @@ -304,7 +304,10 @@ fewer. Only `yanked` is added, as it is read, from the | ||
**What it reads is what the index has,** so a takedown holds and a private | ||
repository has none. `cargo publish` waits up to a minute to find its crate, | ||
and warns if it does not, so a publish and a yank put the record in the index | ||
themselves, without waiting to hear of it. | ||
themselves, without waiting to hear of it. Once its record is made, a publish | ||
succeeds even if that fails: the version is out, the feed indexes it soon | ||
after, and a failure answered then would have a retry told it is published | ||
already. | ||
| ||
**One version, however its build metadata differs.** Cargo's index holds one | ||
of `1.0.0` and `1.0.0+b`, which have different record keys, so creating the | ||
docs/reference/api.md+1−1
| @@ -526,7 +526,7 @@ password>`, which logs in to the owner's PDS. | ||
| `GET config.json` | `{dl, api}`, at the request's origin | | ||
| `GET <index path>` | a crate's file of the index, by Cargo's layout (`1/a`, `2/ab`, `3/a/abc`, `ab/cd/abcd`): a line per version, the oldest first, `no-cache`, with an `ETag`; 304 to one seen, 404 for none | | ||
| `GET dl/<name>/<version>/download` | the `.crate`, as a release's files are sent | | ||
| `PUT api/v1/crates/new` | as Cargo publishes: the crate uploaded to the owner's PDS and its release made, at most 64 MiB. 400 for a body that is not one, a name or version a release does not take, a `Cargo.toml` that says otherwise, an index line over 100,000 bytes, or a version published already | | ||
| `PUT api/v1/crates/new` | as Cargo publishes: the crate uploaded to the owner's PDS and its release made, at most 64 MiB. 400 for a body that is not one, a name or version a release does not take, a `Cargo.toml` missing, over 1 MiB or saying otherwise, an index line over 100,000 bytes, or a version published already. Once the release is made it answers 200, even should reading it back or indexing it fail, as the feed indexes it soon after | | ||
| `DELETE api/v1/crates/<name>/<version>/yank`, `PUT .../unyank` | `{ok: true}`: [yanks](../design/packages.md#yanking) it, or takes that back | | ||
| `GET api/v1/crates?q=&per_page=` | the crates whose names hold `q`, each at its latest: `{crates: [{name, max_version, description}], meta: {total}}`, 10 by default, 100 at most | | ||