A repository's release files as a page: /<name>/latest/ (the latest
release) and /<name>/releases/ (the recent ones), each file with its
size and a link to download it. Pure: releases in, markup out. Every word
from GitHub (tags, titles, file names) is untrusted and reaches the page
only through maud's escaping; a link is made only from a name release::segment
accepts, so what is not a route is shown and not linked.
8use maud::{DOCTYPE, Markup, PreEscaped, html};
Where a file is downloaded from. On the latest page a file is linked by its kind, which does not change with the version, when that kind is one file's and a route; otherwise (and on the page of many) by its exact name.
18fn link(bare: &str, release: &Release, name: &str, stable: bool) -> Option<String> { 19 if stable { 20 let kind = kind_of(name, &release.tag); 21 let same = release.assets.iter().filter(|asset| kind_of(&asset.name, &release.tag) == kind).count(); 22 if same == 1 && segment(&kind) { 23 return Some(format!("/{bare}/latest/{kind}")); 24 } 25 } 26 (segment(&release.tag) && segment(name)).then(|| format!("/{bare}/releases/{}/{name}", release.tag)) 27}
many is the page of recent releases; otherwise it is the latest one.
30pub fn page(origin: &str, bare: &str, project: Project, many: bool, releases: &[Release]) -> Markup { 31 let title = if many { "Releases" } else { "Latest release" }; 32 let about = format!("The release files of {bare}, to download without a GitHub account."); 33 html! { 34 (DOCTYPE) 35 html lang="en" { 36 (head(preview(origin, project.served, &format!("LMJTFY · {bare} {}", title.to_lowercase()), &about, project.branch))) 37 body { 38 div .sheet { 39 div .wrap { 40 (nav(Host::Code)) 41 div .headline { 42 p .eyebrow { "download ............. " (bare) } 43 h1 { (title) } 44 } 45 div .stack { 46 @if releases.is_empty() { 47 p .meta { "No release yet." } 48 } 49 @for release in releases { 50 (card(bare, release, !many)) 51 } 52 p .meta { 53 a href={ "/" (project.served) } { "Browse the code" } 54 " · " 55 @if many { 56 a href={ "/" (bare) "/latest/" } { "Latest release" } 57 } @else { 58 a href={ "/" (bare) "/releases/" } { "All releases" } 59 } 60 } 61 } 62 } 63 } 64 script { (PreEscaped(JS)) } 65 } 66 } 67 } 68}
70fn card(bare: &str, release: &Release, stable: bool) -> Markup { 71 html! { 72 div .win { 73 div .bar-title { 74 span { (release.tag) } 75 span .meta { 76 (release.date()) 77 @if release.prerelease { " · pre-release" } 78 } 79 } 80 div .body { 81 @if let Some(name) = release.name.as_deref().filter(|name| !name.is_empty() && *name != release.tag) { 82 p { (name) } 83 } 84 @if release.assets.is_empty() { 85 p .meta { "No files." } 86 } 87 @for asset in &release.assets { 88 p { 89 @match link(bare, release, &asset.name, stable) { 90 Some(href) => a href=(href) download=(asset.name) { (asset.name) }, 91 None => span { (asset.name) }, 92 } 93 " " span .meta { (human(asset.size)) } 94 } 95 } 96 } 97 } 98 } 99} 100 101#[cfg(test)] 102mod tests { 103 use super::*; 104 use crate::clone::Repo; 105 106 fn release(tag: &str, names: &[(&str, u64)]) -> Release { 107 let assets: Vec<_> = names.iter().enumerate().map(|(id, (name, size))| format!(r#"{{"id":{id},"name":{name:?},"size":{size},"state":"uploaded"}}"#)).collect(); 108 Release::parse(&format!(r#"{{"tag_name":{tag:?},"name":null,"published_at":"2026-10-05T00:00:00Z","assets":[{}]}}"#, assets.join(","))).unwrap() 109 } 110 111 #[test] 112 fn the_latest_page_links_each_file_by_its_kind() { 113 let r = release("2026.10.5", &[("whiskers-2026.10.5-arm64.apk", 80_400_000), ("SHA256SUMS", 300)]); 114 let html = page("https://code.x.test", "whiskers", Repo::Whiskers.project(), false, &[r]).into_string(); 115 assert!(html.contains(r#"href="/whiskers/latest/arm64.apk""#), "{html}"); 116 assert!(html.contains(r#"href="/whiskers/latest/SHA256SUMS""#), "{html}"); 117 assert!(html.contains("80.4 MB"), "{html}"); 118 assert!(html.contains("2026-10-05"), "{html}"); 119 assert!(html.contains(r#"href="/whiskers/releases/""#), "{html}"); 120 } 121 122 #[test] 123 fn the_page_of_many_links_exact_names_and_an_ambiguous_kind_is_too() { 124 let r = release("1.0", &[("a-1.0-x.zip", 1), ("b-1.0-x.zip", 2)]); 125 let html = page("https://code.x.test", "whiskers", Repo::Whiskers.project(), true, &[r.clone()]).into_string(); 126 assert!(html.contains(r#"href="/whiskers/releases/1.0/a-1.0-x.zip""#), "{html}"); 127 let html = page("https://code.x.test", "whiskers", Repo::Whiskers.project(), false, &[r]).into_string(); 128 assert!(!html.contains("/latest/x.zip"), "{html}"); 129 assert!(html.contains(r#"href="/whiskers/releases/1.0/b-1.0-x.zip""#), "{html}"); 130 } 131 132 #[test] 133 fn github_words_are_escaped_and_not_linked_when_they_are_not_routes() { 134 let evil = "<script>alert(1)</script>"; 135 let r = release(evil, &[("\"><img src=x onerror=alert(1)>", 5), ("fine-<b>.txt", 1)]); 136 let html = page("https://code.x.test", "whiskers", Repo::Whiskers.project(), true, &[r]).into_string(); 137 assert!(!html.contains("<script>alert"), "{html}"); 138 assert!(!html.contains("<img src=x"), "{html}"); 139 assert!(!html.contains("<b>"), "{html}"); 140 assert!(html.contains("<script>"), "{html}"); 141 // Neither the tag nor the names are route segments, so nothing links to them. 142 assert!(!html.contains("/releases/<"), "{html}"); 143 assert!(!html.contains("download="), "{html}"); 144 } 145 146 #[test] 147 fn no_release_is_said_and_the_page_opens_no_socket() { 148 let html = page("https://code.x.test", "whiskers", Repo::Whiskers.project(), true, &[]).into_string(); 149 assert!(html.contains("No release yet."), "{html}"); 150 assert!(!html.contains("id=\"online\""), "{html}"); 151 } 152}