Skip to main content

max / makenotwork

server: type-gate CDN cover behind visible_cover_url + build seal All 6 conversion sites route through visible_cover_url, which parses FileScanStatus and shows the cover only on Ok(Clean); any other or unrecognized status fails closed. Build-time seal cover_scan_status_never_compared_to_a_literal (mirrors the S3-delete seal) fails the build on a bare string compare. 5 unit tests + guard, proven non-vacuous. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-05 15:49 UTC
Commit: 94ac0bd71604d0d9bffab75cf9428038cb849d06
Parent: aec6aa8
1 file changed, +120 insertions, -30 deletions
@@ -9,6 +9,27 @@ use crate::helpers::{format_file_size, format_price, get_initials};
9 9
10 10 use super::*;
11 11
12 + /// Fail-closed cover gate: render the CDN-served cover URL only when the raw
13 + /// `cover_scan_status` column parses to exactly [`db::FileScanStatus::Clean`].
14 + ///
15 + /// Every other status (pending/scanning/quarantined/held_for_review/error) and
16 + /// every *unrecognized* string — a typo like `"Clean"`/`"clean "`, or a future
17 + /// variant not yet handled — fails closed to `None`, hiding the cover from the
18 + /// CDN. Routing the decision through `FileScanStatus::from_str` means the
19 + /// `"clean"` sentinel lives in exactly one place (`impl_str_enum!(FileScanStatus …)`),
20 + /// not re-typed at each conversion site.
21 + ///
22 + /// This is the *sole* sanctioned cover-scan gate. The build-time guard
23 + /// [`cover_gate::cover_scan_status_never_compared_to_a_literal`] fails the build
24 + /// if any file reintroduces a bare `cover_scan_status == "…"` compare, so a 7th
25 + /// conversion cannot silently serve an unscanned cover.
26 + fn visible_cover_url(cover_scan_status: &str, cover_image_url: &Option<String>) -> Option<String> {
27 + match cover_scan_status.parse::<db::FileScanStatus>() {
28 + Ok(db::FileScanStatus::Clean) => cover_image_url.clone(),
29 + _ => None,
30 + }
31 + }
32 +
12 33 /// Format a duration in seconds as `H:MM:SS` or `M:SS`.
13 34 fn format_duration(seconds: i32) -> String {
14 35 let hours = seconds / 3600;
@@ -281,11 +302,7 @@ impl Item {
281 302 // Fail-closed cover gate: render the CDN-served cover only once its
282 303 // scan cleared. Pending/held covers are hidden everywhere this view
283 304 // is used, OG cards included.
284 - cover_image_url: if db_item.cover_scan_status == "clean" {
285 - db_item.cover_image_url.clone()
286 - } else {
287 - None
288 - },
305 + cover_image_url: visible_cover_url(&db_item.cover_scan_status, &db_item.cover_image_url),
289 306 is_free,
290 307 can_access,
291 308 enable_license_keys: db_item.enable_license_keys,
@@ -325,11 +342,10 @@ impl Item {
325 342 ItemContent::Audio {
326 343 duration,
327 344 duration_seconds: db_item.duration_seconds,
328 - cover_url: if db_item.cover_scan_status == "clean" {
329 - db_item.cover_image_url.clone()
330 - } else {
331 - None
332 - },
345 + cover_url: visible_cover_url(
346 + &db_item.cover_scan_status,
347 + &db_item.cover_image_url,
348 + ),
333 349 episode_number: db_item.episode_number.map(|n| n as u32),
334 350 audio_s3_key: db_item.audio_s3_key.clone(),
335 351 }
@@ -339,11 +355,10 @@ impl Item {
339 355 ItemContent::Video {
340 356 duration,
341 357 duration_seconds: db_item.video_duration_seconds,
342 - cover_url: if db_item.cover_scan_status == "clean" {
343 - db_item.cover_image_url.clone()
344 - } else {
345 - None
346 - },
358 + cover_url: visible_cover_url(
359 + &db_item.cover_scan_status,
360 + &db_item.cover_image_url,
361 + ),
347 362 video_s3_key: db_item.video_s3_key.clone(),
348 363 width: db_item.video_width,
349 364 height: db_item.video_height,
@@ -364,11 +379,7 @@ impl Item {
364 379 sales_count: db_item.sales_count as u32,
365 380 tags: tags.iter().map(TagView::from).collect(),
366 381 content,
367 - cover_image_url: if db_item.cover_scan_status == "clean" {
368 - db_item.cover_image_url.clone()
369 - } else {
370 - None
371 - },
382 + cover_image_url: visible_cover_url(&db_item.cover_scan_status, &db_item.cover_image_url),
372 383 is_free,
373 384 can_access,
374 385 enable_license_keys: db_item.enable_license_keys,
@@ -399,11 +410,10 @@ impl Project {
399 410 item_count,
400 411 project_type: db_project.project_type.to_string(),
401 412 // Fail-closed cover gate: hide the CDN-served cover until its scan clears.
402 - cover_image_url: if db_project.cover_scan_status == "clean" {
403 - db_project.cover_image_url.clone()
404 - } else {
405 - None
406 - },
413 + cover_image_url: visible_cover_url(
414 + &db_project.cover_scan_status,
415 + &db_project.cover_image_url,
416 + ),
407 417 }
408 418 }
409 419 }
@@ -420,11 +430,7 @@ impl From<&db::DbProjectWithItemCount> for Project {
420 430 item_count: p.item_count.clamp(0, u32::MAX as i64) as u32,
421 431 project_type: p.project_type.to_string(),
422 432 // Fail-closed cover gate: hide the CDN-served cover until its scan clears.
423 - cover_image_url: if p.cover_scan_status == "clean" {
424 - p.cover_image_url.clone()
425 - } else {
426 - None
427 - },
433 + cover_image_url: visible_cover_url(&p.cover_scan_status, &p.cover_image_url),
428 434 }
429 435 }
430 436 }
@@ -840,6 +846,35 @@ mod tests {
840 846 use super::*;
841 847
842 848 #[test]
849 + fn visible_cover_url_shows_cover_only_when_clean() {
850 + let url = Some("https://cdn.makenot.work/covers/abc.jpg".to_string());
851 + assert_eq!(visible_cover_url("clean", &url), url);
852 + }
853 +
854 + #[test]
855 + fn visible_cover_url_hides_for_every_non_clean_status() {
856 + let url = Some("https://cdn.makenot.work/covers/abc.jpg".to_string());
857 + for status in ["pending", "scanning", "quarantined", "held_for_review", "error"] {
858 + assert_eq!(visible_cover_url(status, &url), None, "leaked cover for {status}");
859 + }
860 + }
861 +
862 + #[test]
863 + fn visible_cover_url_fails_closed_on_unrecognized_or_typo_status() {
864 + let url = Some("https://cdn.makenot.work/covers/abc.jpg".to_string());
865 + // Capitalization / whitespace / future variants must not parse to Clean.
866 + for status in ["Clean", "clean ", " clean", "CLEAN", "", "approved", "ok"] {
867 + assert_eq!(visible_cover_url(status, &url), None, "leaked cover for {status:?}");
868 + }
869 + }
870 +
871 + #[test]
872 + fn visible_cover_url_never_invents_a_url() {
873 + // A clean status with no stored cover still yields None.
874 + assert_eq!(visible_cover_url("clean", &None), None);
875 + }
876 +
877 + #[test]
843 878 fn format_duration_zero() {
844 879 assert_eq!(format_duration(0), "0:00");
845 880 }
@@ -899,3 +934,58 @@ mod tests {
899 934 assert_eq!(format_duration(303), "5:03");
900 935 }
901 936 }
937 +
938 + /// Build-time seal for the cover-scan gate (Run #23, finding F1). The CDN-served
939 + /// cover URL must be gated *only* through [`visible_cover_url`], which routes the
940 + /// decision through the [`db::FileScanStatus`] enum. A bare
941 + /// `cover_scan_status == "clean"` string compare at a conversion site is the
942 + /// exact failure the enum removes: a `"Clean"`/`"clean "` typo, or a 7th site
943 + /// added without the check, silently serves an unscanned/quarantined cover.
944 + ///
945 + /// This mirrors the `routes_never_delete_s3_directly` seal: the build fails if
946 + /// any file under `src/` compares `cover_scan_status` to a string literal.
947 + #[cfg(test)]
948 + mod cover_gate {
949 + use std::path::Path;
950 +
951 + #[test]
952 + fn cover_scan_status_never_compared_to_a_literal() {
953 + // Built at runtime so this guard file does not match its own needle.
954 + let needle = format!("cover_scan_status {}", "== \"");
955 + let src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
956 + let mut offenders = Vec::new();
957 + walk(&src_dir, &mut |path, contents| {
958 + for (i, line) in contents.lines().enumerate() {
959 + if line.trim_start().starts_with("//") {
960 + continue; // doc/comment lines legitimately mention the pattern
961 + }
962 + if line.contains(&needle) {
963 + offenders.push(format!("{}:{}: {}", path.display(), i + 1, line.trim()));
964 + }
965 + }
966 + });
967 + assert!(
968 + offenders.is_empty(),
969 + "cover-scan gate seal violated — gate the CDN cover through \
970 + types::conversions::visible_cover_url (which parses FileScanStatus), \
971 + never a bare cover_scan_status string compare. Offending lines:\n{}",
972 + offenders.join("\n")
973 + );
974 + }
975 +
976 + fn walk(dir: &Path, f: &mut impl FnMut(&Path, &str)) {
977 + let Ok(entries) = std::fs::read_dir(dir) else {
978 + return;
979 + };
980 + for entry in entries.flatten() {
981 + let path = entry.path();
982 + if path.is_dir() {
983 + walk(&path, f);
984 + } else if path.extension().is_some_and(|e| e == "rs")
985 + && let Ok(contents) = std::fs::read_to_string(&path)
986 + {
987 + f(&path, &contents);
988 + }
989 + }
990 + }
991 + }