https://code.lmjtfy.fun/whiskers/latest/arm64.apk: the release files of
the repositories served here, for anyone, though the repositories are
private.
GitHub keeps a release's files beside its tag. This forwards them the way
clone.rs forwards a clone: the read-only token (Contents: read, which
covers releases) looks the release up and asks for the file, GitHub
answers with a redirect to a short-lived signed address, and that address
is fetched without the token and streamed to the visitor. Nothing is
kept but the release's metadata, a minute per isolate, and nothing is
spent: a download reaches neither Jev nor the LLM.
The addresses
<name> is a served repository's name without .git (whiskers).
| Path | What |
|---|---|
/<name>/latest/<kind> | The file of the latest release whose kind is <kind>. It moves, so it is cached five minutes. |
/<name>/releases/<tag>/<asset> | That exact file of that exact release. A tag does not move, so it is cached for good. |
/<name>/latest/, /<name>/releases/ | A page listing the latest release, or the recent ones, with each file's size and link. |
Anything else under /<name>/latest or /<name>/releases is 404, and
so is a <name> that is not in Repo::ALL.
A file's kind
The kind is the file's name with the release's version taken out, so that
the address does not change when the version does. It is everything after
the first -<tag>- in the name (whiskers-2026.10.5-arm64.apk is
arm64.apk; whiskersd-2026.10.5-x86_64-linux.tar.gz is
x86_64-linux.tar.gz; a tag of v1.2 is also looked for as 1.2). A
file whose name has no -<tag>- in it (SHA256SUMS) is its own kind. Two
files of one release with the same kind is not guessed at: that kind is
404 for that release and the log names the release (Pick::Ambiguous).
The pinned form is always unambiguous, since names are unique in a release.
The pure parts (routes, kinds, headers, sizes) are tested natively; only
serve and the two fetches use the Worker.
How long an isolate keeps a release's metadata. The same minute as the code pages' other caches.
53const KEEP_MS: f64 = 60_000.0;
How many releases the page lists.
56const LISTED: usize = 20;
The longest name or tag taken as a path segment.
59const LONGEST: usize = 200;
The most entries one isolate keeps.
62const KEPT_MOST: usize = 64;
Cache-Control for the latest release's files: they move.
65pub const LATEST_CACHE: &str = "public, max-age=300";
And for a file of a named release, which does not.
67pub const PINNED_CACHE: &str = "public, max-age=86400, immutable";
---------------------------------------------------------------- routes
What a download address asks for.
/<name>/latest/<kind>.
75 Latest { repo: Repo, kind: &'a str },
/<name>/releases/<tag>/<asset>.
77 Pinned { repo: Repo, tag: &'a str, asset: &'a str },
/<name>/latest or /<name>/latest/.
79 LatestPage(Repo),
Whether text may be a path segment here: letters, digits and . _ + ~ @ -
and nothing else, not . or .., not empty, not long. No %, so there is
no encoded slash or dot-dot to be decoded later, and no space or quote to
reach a header. GitHub's own names for files and tags are of this kind.
The repository whose downloads are under path, if path is under one:
/whiskers/latest... or /whiskers/releases..., of a served repository.
What host::gate sends to the code host, and what route reads further.
The route for path, or None for anything the downloads do not serve.
107pub fn route(path: &str) -> Option<Route<'_>> { 108 let repo = under(path)?; 109 let rest = path.strip_prefix('/')?.split_once('/')?.1; 110 let parts: Vec<&str> = rest.split('/').collect(); 111 match parts.as_slice() { 112 ["latest"] | ["latest", ""] => Some(Route::LatestPage(repo)), 113 ["releases"] | ["releases", ""] => Some(Route::ReleasesPage(repo)), 114 ["latest", kind] if segment(kind) => Some(Route::Latest { repo, kind }), 115 ["releases", tag, asset] if segment(tag) && segment(asset) => Some(Route::Pinned { repo, tag, asset }), 116 _ => None, 117 } 118}
---------------------------------------------------------------- releases
A file of a release, as GitHub lists it.
A release, as far as the downloads read it.
135#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] 136pub struct Release { 137 #[serde(rename = "tag_name")] 138 pub tag: String, 139 #[serde(default)] 140 pub name: Option<String>, 141 #[serde(default)] 142 pub published_at: Option<String>, 143 #[serde(default)] 144 pub draft: bool, 145 #[serde(default)] 146 pub prerelease: bool, 147 #[serde(default)] 148 pub assets: Vec<Asset>, 149}
GitHub's list of releases, newest first, without drafts.
159 pub fn parse_list(body: &str) -> Option<Vec<Release>> { 160 let mut releases: Vec<Release> = serde_json::from_str(body).ok()?; 161 releases.retain(|release| !release.draft); 162 for release in &mut releases { 163 release.assets.retain(|asset| asset.state == "uploaded"); 164 } 165 Some(releases) 166 }
The date it was published, 2026-10-05, or nothing.
The kind of each asset, in order.
The file's name with the version taken out: everything after the first
-<tag>- (or -<tag without a leading v>-), else the whole name.
181pub fn kind_of(name: &str, tag: &str) -> String { 182 let bare = tag.strip_prefix(['v', 'V']).filter(|bare| !bare.is_empty()); 183 for version in std::iter::once(tag).chain(bare) { 184 let marker = format!("-{version}-"); 185 if let Some((before, after)) = name.split_once(&marker) 186 && !before.is_empty() 187 && !after.is_empty() 188 { 189 return after.to_owned(); 190 } 191 } 192 name.to_owned() 193}
Which file of a release a kind is.
204pub fn pick<'a>(release: &'a Release, kind: &str) -> Pick<'a> { 205 let mut found = release.assets.iter().filter(|asset| kind_of(&asset.name, &release.tag) == kind); 206 match (found.next(), found.next()) { 207 (None, _) => Pick::Missing, 208 (Some(asset), None) => Pick::Found(asset), 209 (Some(_), Some(_)) => Pick::Ambiguous, 210 } 211}
The file of a release with exactly this name.
---------------------------------------------------------------- headers
The content-type a file is given, by its name.
221pub fn content_type(name: &str) -> &'static str { 222 let lower = name.to_ascii_lowercase(); 223 if lower.ends_with(".apk") { 224 "application/vnd.android.package-archive" 225 } else if lower.ends_with(".tar.gz") || lower.ends_with(".tgz") { 226 "application/gzip" 227 } else if lower == "sha256sums" || lower.ends_with(".sha256") || lower.ends_with(".txt") { 228 "text/plain; charset=utf-8" 229 } else { 230 "application/octet-stream" 231 } 232}
attachment; filename="<name>". A name is a GitHub asset's, which may
hold a quote, a backslash or a control character: each of those, and
anything that is not ASCII, is an underscore, so the name cannot end the
quotes or the header.
A Range header worth forwarding: one bytes= with digits, commas and
dashes. Anything else is left out, and the whole file is sent.
Whether a request starts the file over: no range, or one from byte 0. What counts as a download in the archive; a resumed one does not.
A size as people read it: 76.7 MB, in GitHub's decimal megabytes.
257pub fn human(bytes: u64) -> String { 258 const UNITS: [&str; 4] = ["B", "kB", "MB", "GB"]; 259 let mut size = bytes as f64; 260 let mut unit = 0; 261 while size >= 1000.0 && unit < UNITS.len() - 1 { 262 size /= 1000.0; 263 unit += 1; 264 } 265 if unit == 0 { format!("{bytes} B") } else { format!("{size:.1} {}", UNITS[unit]) } 266}
---------------------------------------------------------------- reading GitHub
What looking a release up came to.
What is asked of GitHub, and the cache's key for it.
284thread_local! {
What GitHub said, by (repository, what was asked), and when. Only
good answers.
290async fn lookup(env: &worker::Env, repo: Repo, ask: Ask<'_>) -> Found { 291 let (key, url) = match ask { 292 Ask::Latest => (format!("{}:latest", repo.github()), format!("https://api.github.com/repos/{}/releases/latest", repo.github())), 293 Ask::Tag(tag) => (format!("{}:tag:{tag}", repo.github()), format!("https://api.github.com/repos/{}/releases/tags/{tag}", repo.github())), 294 Ask::List => (format!("{}:list", repo.github()), format!("https://api.github.com/repos/{}/releases?per_page={LISTED}", repo.github())), 295 }; 296 let now = js_sys::Date::now(); 297 let kept = KEPT.with(|kept| kept.borrow().iter().find(|(seen, at, _)| *seen == key && now - at < KEEP_MS).map(|(_, _, releases)| releases.clone())); 298 if let Some(releases) = kept { 299 return Found::Releases(releases); 300 } 301 let listing = matches!(ask, Ask::List); 302 let read = match clone::github(env, &url).await { 303 Some((200, body, _)) if listing => Release::parse_list(&body), 304 Some((200, body, _)) => Release::parse(&body).map(|release| vec![release]), 305 Some((404, _, _)) => { 306 // GitHub answers 404 for a repository the token cannot read as 307 // well as for a release that is not there, so say which it was. 308 worker::console_log!("release: {} {} has no such release (404)", repo.github(), key.split_once(':').map_or("", |(_, what)| what)); 309 return Found::Missing; 310 } 311 Some((status, _, _)) => { 312 worker::console_error!("release: GitHub answered {status} for {} {}", repo.github(), key.split_once(':').map_or("", |(_, what)| what)); 313 return Found::Unreachable; 314 } 315 None => { 316 worker::console_error!("release: GitHub could not be reached for {} (or the token is not set)", repo.github()); 317 return Found::Unreachable; 318 } 319 }; 320 match read { 321 Some(releases) => { 322 KEPT.with(|kept| { 323 let mut kept = kept.borrow_mut(); 324 kept.retain(|(seen, at, _)| *seen != key && now - at < KEEP_MS); 325 if kept.len() >= KEPT_MOST { 326 kept.remove(0); 327 } 328 kept.push((key, now, releases.clone())); 329 }); 330 Found::Releases(releases) 331 } 332 None => { 333 worker::console_error!("release: GitHub's answer for {} could not be read", repo.github()); 334 Found::Unreachable 335 } 336 } 337}
---------------------------------------------------------------- serving
341use worker::worker_sys::web_sys;
What the Worker answers with: a page or refusal as an http response, or
a file as the fetch response it is. A file is not an http response so
that its body stays the runtime's own stream from GitHub's storage, which
is what keeps its Content-Length: an http body is read through Rust
and written out again as a plain stream, and a plain stream is sent
chunked, with no length for a download bar. (Measured on staging,
2026-10-05.) It also keeps 200 MB out of the Worker's own memory and CPU.
355impl worker::IntoResponse for Served { 356 fn into_raw(self) -> Result<web_sys::Response, impl Into<Box<dyn std::error::Error>>> { 357 match self { 358 Served::Http(response) => worker::IntoResponse::into_raw(response).map_err(|error| -> Box<dyn std::error::Error> { error.into() }), 359 Served::File(response) => Ok(response), 360 } 361 } 362} 363 364impl Route<'_> {
A file, as opposed to a listing page.
The two listing pages. The file routes are answered by file, before the
router, so one that reaches here (and anything that is no route) is a 404.
373#[worker::send] 374pub async fn serve(State(app): State<App>, headers: HeaderMap, uri: Uri) -> Response<Body> { 375 match route(uri.path()) { 376 Some(Route::LatestPage(repo)) => page(&app, &headers, repo, Ask::Latest, false).await, 377 Some(Route::ReleasesPage(repo)) => page(&app, &headers, repo, Ask::List, true).await, 378 _ => clone::refused(StatusCode::NOT_FOUND, "No such file."), 379 } 380}
The page listing one release (latest) or the recent ones.
387async fn page(app: &App, headers: &HeaderMap, repo: Repo, ask: Ask<'_>, many: bool) -> Response<Body> { 388 let releases = match lookup(&app.env, repo, ask).await { 389 Found::Releases(releases) => releases, 390 // A repository with no release yet has an empty page, not a 404. 391 Found::Missing if many => Vec::new(), 392 Found::Missing => return clone::refused(StatusCode::NOT_FOUND, "No release yet."), 393 Found::Unreachable => return unreachable(), 394 }; 395 let origin = super::origin(headers); 396 let html = crate::view::releases::page(&origin, repo.bare(), repo.project(), many, &releases); 397 Response::builder() 398 .header(header::CONTENT_TYPE, "text/html; charset=utf-8") 399 .header(header::CACHE_CONTROL, "no-cache") 400 .body(Body::from(html.into_string())) 401 .expect("static headers are valid") 402}
Which file a route is, and how long it may be cached.
405async fn find(env: &worker::Env, route: &Route<'_>) -> Result<(Repo, Release, Asset, &'static str), Response<Body>> { 406 let missing = |why: &str| clone::refused(StatusCode::NOT_FOUND, why); 407 match route { 408 Route::Latest { repo, kind } => { 409 let release = match lookup(env, *repo, Ask::Latest).await { 410 Found::Releases(mut releases) => releases.swap_remove(0), 411 Found::Missing => return Err(missing("No such release.")), 412 Found::Unreachable => return Err(unreachable()), 413 }; 414 match pick(&release, kind) { 415 Pick::Found(asset) => Ok((*repo, release.clone(), asset.clone(), LATEST_CACHE)), 416 Pick::Missing => Err(missing("No such file.")), 417 Pick::Ambiguous => { 418 worker::console_error!("release: {} {}: two files have one kind, so none is sent", repo.github(), release.tag); 419 Err(missing("No such file.")) 420 } 421 } 422 } 423 Route::Pinned { repo, tag, asset } => { 424 let release = match lookup(env, *repo, Ask::Tag(tag)).await { 425 Found::Releases(mut releases) => releases.swap_remove(0), 426 Found::Missing => return Err(missing("No such release.")), 427 Found::Unreachable => return Err(unreachable()), 428 }; 429 // GitHub's `tags/<tag>` is exact; hold it to the tag asked for anyway. 430 if release.tag != *tag { 431 return Err(missing("No such release.")); 432 } 433 match named(&release, asset) { 434 Some(found) => Ok((*repo, release.clone(), found.clone(), PINNED_CACHE)), 435 None => Err(missing("No such file.")), 436 } 437 } 438 Route::LatestPage(_) | Route::ReleasesPage(_) => Err(missing("No such file.")), 439 } 440}
A file route (GET or HEAD), answered before the router sees it
(fetch in lib.rs). Its headers come from the release's own record; for a
GET its body is GitHub's signed address's, passed on as it streams.
445pub async fn file(env: &worker::Env, event: crate::events::Event, route: Route<'_>, method: &Method, headers: &HeaderMap) -> Served { 446 let (repo, release, asset, cache) = match find(env, &route).await { 447 Ok(found) => found, 448 Err(response) => return Served::Http(response), 449 }; 450 let wanted = headers.get(header::RANGE).and_then(|value| value.to_str().ok()).and_then(range); 451 if *method == Method::HEAD { 452 let response = Response::builder() 453 .header(header::CONTENT_TYPE, content_type(&asset.name)) 454 .header(header::CONTENT_DISPOSITION, disposition(&asset.name)) 455 .header("x-content-type-options", "nosniff") 456 .header(header::CACHE_CONTROL, cache) 457 .header(header::ACCEPT_RANGES, "bytes") 458 .header(header::CONTENT_LENGTH, asset.size) 459 .body(Body::empty()) 460 .expect("static headers are valid"); 461 return Served::Http(response); 462 } 463 let (status, upstream) = match fetch(env, repo, asset.id, wanted).await { 464 Ok(answer) => answer, 465 Err(why) => { 466 worker::console_error!("release: {} {} asset {}: {why}", repo.github(), release.tag, asset.id); 467 return Served::Http(unreachable()); 468 } 469 }; 470 let upstream: web_sys::Response = upstream.into(); 471 let sent = worker::Headers::new(); 472 let set = |name: &str, value: &str| sent.set(name, value).is_ok(); 473 let mut ok = set("content-type", content_type(&asset.name)) 474 && set("content-disposition", &disposition(&asset.name)) 475 && set("x-content-type-options", "nosniff") 476 && set("cache-control", cache) 477 && set("accept-ranges", "bytes"); 478 // As GitHub's storage sent them: how long the body is, and, for a range, 479 // which part of the file it is. 480 let length = upstream.headers().get("content-length").ok().flatten(); 481 for name in ["content-length", "content-range"] { 482 if let Some(value) = upstream.headers().get(name).ok().flatten() { 483 ok &= set(name, &value); 484 } 485 } 486 let Some(body) = upstream.body().filter(|_| ok) else { 487 worker::console_error!("release: {} {} asset {}: the answer had no body to send", repo.github(), release.tag, asset.id); 488 return Served::Http(unreachable()); 489 }; 490 worker::console_log!("release: {} {} asset {} sent, status {status}, {} bytes long", repo.github(), release.tag, asset.id, length.as_deref().unwrap_or("?")); 491 if from_start(wanted) { 492 let mut event = event.named("download").with(format!("{}/{}", repo.bare(), asset.name)); 493 event.status = f64::from(status); 494 crate::events::record(env, event).await; 495 } 496 Served::File(worker::ResponseBuilder::new().with_status(status).with_headers(sent).stream(body).into()) 497}
GitHub's asset, streamed. The API answers a request for
application/octet-stream with a redirect to a signed address; the token
is sent to the API and never to that address, which is fetched with only
the range. Err carries fixed words for the log, never GitHub's text.
503async fn fetch(env: &worker::Env, repo: Repo, id: u64, wanted: Option<&str>) -> Result<(u16, worker::Response), &'static str> { 504 let token = env.secret(clone::TOKEN).map_err(|_| "the token is not set")?.to_string(); 505 let url = format!("https://api.github.com/repos/{}/releases/assets/{id}", repo.github()); 506 let asked = worker::Headers::new(); 507 asked.set("authorization", &format!("Bearer {token}")).map_err(|_| "a header was refused")?; 508 asked.set("accept", "application/octet-stream").map_err(|_| "a header was refused")?; 509 asked.set("user-agent", "lmjtfy").map_err(|_| "a header was refused")?; 510 let mut init = worker::RequestInit::new(); 511 init.with_headers(asked).with_redirect(worker::RequestRedirect::Manual); 512 let request = worker::Request::new_with_init(&url, &init).map_err(|_| "the request could not be made")?; 513 let first = worker::Fetch::Request(request).send().await.map_err(|_| "GitHub did not answer")?; 514 let signed = match first.status_code() { 515 // GitHub answers a file with a redirect. 516 301 | 302 | 303 | 307 | 308 => first.headers().get("location").ok().flatten().filter(|to| to.starts_with("https://")).ok_or("the redirect had no https address")?, 517 status @ (200 | 206) => return Ok((status, first)), 518 401 | 403 | 404 => return Err("GitHub refused the token for the asset (does it have Contents: read on this repository?)"), 519 _ => return Err("GitHub answered the asset request with an unexpected status"), 520 }; 521 // No credentials past this point: the signed address is another host. 522 let second = worker::Headers::new(); 523 second.set("user-agent", "lmjtfy").map_err(|_| "a header was refused")?; 524 if let Some(wanted) = wanted { 525 second.set("range", wanted).map_err(|_| "a header was refused")?; 526 } 527 let mut init = worker::RequestInit::new(); 528 init.with_headers(second); 529 let request = worker::Request::new_with_init(&signed, &init).map_err(|_| "the signed address could not be used")?; 530 let response = worker::Fetch::Request(request).send().await.map_err(|_| "the signed address did not answer")?; 531 match response.status_code() { 532 status @ (200 | 206) => Ok((status, response)), 533 _ => Err("the signed address answered with an unexpected status"), 534 } 535}
537#[cfg(test)] 538mod tests { 539 use super::*; 540 use axum::http::HeaderValue; 541 542 fn asset(name: &str) -> Asset { 543 Asset { id: 1, name: name.to_owned(), size: 10, state: "uploaded".into() } 544 } 545 546 fn release(tag: &str, names: &[&str]) -> Release { 547 Release { tag: tag.to_owned(), name: None, published_at: Some("2026-10-05T01:02:03Z".into()), draft: false, prerelease: false, assets: names.iter().map(|name| asset(name)).collect() } 548 } 549 550 fn whiskers() -> Release { 551 release( 552 "2026.10.5", 553 &[ 554 "whiskers-2026.10.5-arm64.apk", 555 "whiskers-2026.10.5-arm64-lite.apk", 556 "whiskers-2026.10.5-universal.apk", 557 "whiskersd-2026.10.5-x86_64-linux.tar.gz", 558 "SHA256SUMS", 559 ], 560 ) 561 } 562 563 #[test] 564 fn a_kind_is_the_name_without_the_version() { 565 let kinds = whiskers().kinds(); 566 assert_eq!(kinds, ["arm64.apk", "arm64-lite.apk", "universal.apk", "x86_64-linux.tar.gz", "SHA256SUMS"]); 567 // A leading v on the tag is not in the file names. 568 assert_eq!(kind_of("app-1.2-x.zip", "v1.2"), "x.zip"); 569 assert_eq!(kind_of("app-v1.2-x.zip", "v1.2"), "x.zip"); 570 // No version in the name: its own kind. So is a name where the 571 // version is not between dashes. 572 assert_eq!(kind_of("app-2026.10.5.apk", "2026.10.5"), "app-2026.10.5.apk"); 573 assert_eq!(kind_of("-2026.10.5-x", "2026.10.5"), "-2026.10.5-x"); 574 assert_eq!(kind_of("a-2026.10.5-", "2026.10.5"), "a-2026.10.5-"); 575 } 576 577 #[test] 578 fn the_first_version_marker_splits_the_name() { 579 assert_eq!(kind_of("a-1.0-b-1.0-c", "1.0"), "b-1.0-c"); 580 } 581 582 #[test] 583 fn a_tag_with_regex_characters_is_matched_as_text() { 584 let tag = "v1.0+(x)[a]*"; 585 assert_eq!(kind_of("app-1.0+(x)[a]*-linux.tar.gz", tag), "linux.tar.gz"); 586 assert_eq!(kind_of(&format!("app-{tag}-linux.tar.gz"), tag), "linux.tar.gz"); 587 // The dot is a dot, not any character. 588 assert_eq!(kind_of("app-1x0-linux", "1.0"), "app-1x0-linux"); 589 assert_eq!(kind_of("app-1.0-linux", ".*"), "app-1.0-linux"); 590 } 591 592 #[test] 593 fn a_kind_finds_its_file() { 594 let release = whiskers(); 595 let found = |kind: &str| match pick(&release, kind) { 596 Pick::Found(asset) => Some(asset.name.clone()), 597 _ => None, 598 }; 599 assert_eq!(found("arm64.apk").as_deref(), Some("whiskers-2026.10.5-arm64.apk")); 600 assert_eq!(found("arm64-lite.apk").as_deref(), Some("whiskers-2026.10.5-arm64-lite.apk")); 601 assert_eq!(found("x86_64-linux.tar.gz").as_deref(), Some("whiskersd-2026.10.5-x86_64-linux.tar.gz")); 602 assert_eq!(found("SHA256SUMS").as_deref(), Some("SHA256SUMS")); 603 // A file is not found by its versioned name, or by part of a kind. 604 assert_eq!(pick(&release, "whiskers-2026.10.5-arm64.apk"), Pick::Missing); 605 assert_eq!(pick(&release, "apk"), Pick::Missing); 606 assert_eq!(pick(&release, ""), Pick::Missing); 607 } 608 609 #[test] 610 fn two_files_of_one_kind_are_not_guessed_between() { 611 let release = release("1.0", &["a-1.0-x.zip", "b-1.0-x.zip", "a-1.0-y.zip"]); 612 assert_eq!(pick(&release, "x.zip"), Pick::Ambiguous); 613 assert!(matches!(pick(&release, "y.zip"), Pick::Found(_))); 614 // The pinned form names the file exactly. 615 assert_eq!(named(&release, "b-1.0-x.zip").map(|asset| asset.name.as_str()), Some("b-1.0-x.zip")); 616 assert_eq!(named(&release, "x.zip"), None); 617 } 618 619 #[test] 620 fn the_routes_are_read() { 621 let w = Repo::Whiskers; 622 assert_eq!(route("/whiskers/latest/arm64.apk"), Some(Route::Latest { repo: w, kind: "arm64.apk" })); 623 assert_eq!(route("/whiskers/latest/SHA256SUMS"), Some(Route::Latest { repo: w, kind: "SHA256SUMS" })); 624 assert_eq!( 625 route("/whiskers/releases/2026.10.5/whiskers-2026.10.5-arm64.apk"), 626 Some(Route::Pinned { repo: w, tag: "2026.10.5", asset: "whiskers-2026.10.5-arm64.apk" }) 627 ); 628 assert_eq!(route("/whiskers/latest"), Some(Route::LatestPage(w))); 629 assert_eq!(route("/whiskers/latest/"), Some(Route::LatestPage(w))); 630 assert_eq!(route("/whiskers/releases"), Some(Route::ReleasesPage(w))); 631 assert_eq!(route("/whiskers/releases/"), Some(Route::ReleasesPage(w))); 632 assert_eq!(route("/lmjtfy/latest/x"), Some(Route::Latest { repo: Repo::Lmjtfy, kind: "x" })); 633 } 634 635 #[test] 636 fn what_is_not_a_route_is_not_one() { 637 for path in [ 638 // Not served, or not a name of one: `.git` is the clone's name. 639 "/nixos-config/latest/x", 640 "/whiskers.git/latest/x", 641 "/Whiskers/latest/x", 642 "/latest/x", 643 "/", 644 "", 645 // Path tricks. 646 "/whiskers/latest/..", 647 "/whiskers/latest/.", 648 "/whiskers/latest/../x", 649 "/whiskers/releases/../latest/x", 650 "/whiskers/releases/1.0/..", 651 "/whiskers/latest/%2e%2e", 652 "/whiskers/latest/a%2fb", 653 "/whiskers/latest/a%2Fb", 654 "/whiskers/latest/a\\b", 655 "/whiskers/latest/a b", 656 "/whiskers/latest/a\"b", 657 "/whiskers/latest/a\nb", 658 // Empty parts, too many parts, too few. 659 "/whiskers/latest//x", 660 "/whiskers/latest/a/b", 661 "/whiskers/releases/1.0", 662 "/whiskers/releases/1.0/", 663 "/whiskers/releases//x", 664 "/whiskers/releases/1.0/a/b", 665 "/whiskers/releases/1.0//", 666 // Other things under the repository's name. 667 "/whiskers/other/x", 668 "/whiskers/latestx/x", 669 "//whiskers/latest/x", 670 ] { 671 assert_eq!(route(path), None, "{path:?}"); 672 } 673 let long = format!("/whiskers/latest/{}", "a".repeat(LONGEST + 1)); 674 assert_eq!(route(&long), None); 675 } 676 677 #[test] 678 fn a_path_under_the_prefix_is_the_codes_even_if_malformed() { 679 // What the apex sends on to the code host, which then says 404. 680 assert_eq!(under("/whiskers/latest/../x"), Some(Repo::Whiskers)); 681 assert_eq!(under("/whiskers/releases"), Some(Repo::Whiskers)); 682 assert_eq!(under("/whiskers/latest"), Some(Repo::Whiskers)); 683 assert_eq!(under("/whiskers"), None); 684 assert_eq!(under("/whiskers/"), None); 685 assert_eq!(under("/whiskers.git/latest"), None); 686 assert_eq!(under("/nope/latest"), None); 687 assert_eq!(under("/lmjtfy.git/info/refs"), None); 688 } 689 690 #[test] 691 fn every_served_repository_has_downloads_under_its_bare_name() { 692 for repo in Repo::ALL { 693 let path = format!("/{}/latest/x", repo.bare()); 694 assert_eq!(route(&path), Some(Route::Latest { repo, kind: "x" }), "{path}"); 695 } 696 } 697 698 #[test] 699 fn a_file_is_given_its_type_by_its_name() { 700 assert_eq!(content_type("whiskers-2026.10.5-arm64.apk"), "application/vnd.android.package-archive"); 701 assert_eq!(content_type("X.APK"), "application/vnd.android.package-archive"); 702 assert_eq!(content_type("whiskersd-2026.10.5-x86_64-linux.tar.gz"), "application/gzip"); 703 assert_eq!(content_type("SHA256SUMS"), "text/plain; charset=utf-8"); 704 assert_eq!(content_type("thing.zip"), "application/octet-stream"); 705 assert_eq!(content_type("apk"), "application/octet-stream"); 706 } 707 708 #[test] 709 fn the_filename_cannot_leave_its_quotes() { 710 assert_eq!(disposition("whiskers-2026.10.5-arm64.apk"), "attachment; filename=\"whiskers-2026.10.5-arm64.apk\""); 711 assert_eq!(disposition("a\"b\\c\r\nd"), "attachment; filename=\"a_b_c__d\""); 712 assert_eq!(disposition("é.apk"), "attachment; filename=\"_.apk\""); 713 assert!(HeaderValue::from_str(&disposition("x\ny\u{7f}")).is_ok()); 714 } 715 716 #[test] 717 fn only_a_plain_range_is_forwarded() { 718 assert_eq!(range("bytes=0-1048575"), Some("bytes=0-1048575")); 719 assert_eq!(range("bytes=100-"), Some("bytes=100-")); 720 assert_eq!(range("bytes=-500"), Some("bytes=-500")); 721 assert_eq!(range("bytes=0-1,5-9"), Some("bytes=0-1,5-9")); 722 assert_eq!(range("items=0-1"), None); 723 assert_eq!(range("bytes="), None); 724 assert_eq!(range("bytes=a-b"), None); 725 assert_eq!(range("bytes=0-1\r\nx: y"), None); 726 assert!(from_start(None)); 727 assert!(from_start(Some("bytes=0-1048575"))); 728 assert!(!from_start(Some("bytes=100-"))); 729 } 730 731 #[test] 732 fn sizes_are_read_by_people() { 733 assert_eq!(human(0), "0 B"); 734 assert_eq!(human(999), "999 B"); 735 assert_eq!(human(1500), "1.5 kB"); 736 assert_eq!(human(80_400_000), "80.4 MB"); 737 assert_eq!(human(2_500_000_000), "2.5 GB"); 738 } 739 740 #[test] 741 fn githubs_release_is_read_without_drafts_or_unfinished_files() { 742 let body = r#"{"tag_name":"2026.10.5","name":"Oct","published_at":"2026-10-05T01:02:03Z","draft":false,"prerelease":false, 743 "assets":[{"id":7,"name":"a-2026.10.5-x.apk","size":12,"state":"uploaded"},{"id":8,"name":"b","size":1,"state":"starter"}]}"#; 744 let release = Release::parse(body).unwrap(); 745 assert_eq!(release.assets.len(), 1); 746 assert_eq!(release.assets[0].id, 7); 747 assert_eq!(release.date(), "2026-10-05"); 748 assert!(Release::parse(&body.replace("\"draft\":false", "\"draft\":true")).is_none()); 749 assert!(Release::parse("not json").is_none()); 750 let list = format!("[{body},{}]", body.replace("\"draft\":false", "\"draft\":true")); 751 assert_eq!(Release::parse_list(&list).unwrap().len(), 1); 752 } 753}