//! Two grep-based lints over `src/routes/`, not unit tests: one proves every //! bucket delete goes through the authority, the other that no served key is //! built by hand. Both resolve their target from `CARGO_MANIFEST_DIR`, so //! moving this file does not move what they scan. use std::path::Path; /// Hand every `.rs` file under `dir` to `f`, with its path and its contents. /// Both seals below are a grep over the same tree, so they share the walk. fn walk(dir: &Path, f: &mut impl FnMut(&Path, &str)) { let Ok(entries) = std::fs::read_dir(dir) else { return; }; for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { walk(&path, f); } else if path.extension().is_some_and(|e| e == "rs") && let Ok(contents) = std::fs::read_to_string(&path) { f(&path, &contents); } } } /// Build-time enforcement: route handlers must never /// delete S3 objects directly, nor mint an [`S3DeleteAuthority`]. Direct /// deletion is for the sanctioned durable-deletion paths (`scheduler/cleanup.rs`, /// `scanning/worker.rs`) only; handlers enqueue through `pending_s3_deletions`. /// /// The type system already makes the accidental `s3.delete_object(key)` /// uncompilable (the delete methods require an authority handlers can't reach). /// This test closes the deliberate-circumvention gap: it fails the build if any /// file under `src/routes/` names a delete method or the authority type, so the /// seal cannot silently erode in a future handler. #[cfg(test)] mod delete_seal_guard { use super::walk; use std::path::Path; #[test] fn routes_never_delete_s3_directly() { let routes_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/routes"); let mut offenders = Vec::new(); walk(&routes_dir, &mut |path, contents| { for (i, line) in contents.lines().enumerate() { // Skip comment/doc lines (they legitimately mention the API). if line.trim_start().starts_with("//") { continue; } if line.contains(".delete_object(") || line.contains(".delete_objects(") || line.contains(".delete_prefix(") || line.contains("S3DeleteAuthority") { offenders.push(format!("{}:{}: {}", path.display(), i + 1, line.trim())); } } }); assert!( offenders.is_empty(), "CHRONIC B' seal violated, route code must enqueue via \ routes::storage::enqueue_s3_orphan, never delete S3 directly or mint an \ S3DeleteAuthority. Offending lines:\n{}", offenders.join("\n") ); } } /// C1 scan-then-promote seal: route handlers must never mint a *served* S3 key. /// /// A presigned client upload can only ever land at a `staging/{uuid}` key /// ([`S3Client::generate_staging_key`]); the served, content-addressed key /// ([`S3Client::content_key`]) is created in exactly one place, the scan /// worker's promote step, after a Clean verdict, so the bytes a buyer is served /// are provably the bytes that were scanned. The mutable-served-key class (a /// presign minting `{user}/{item}/type/filename`, then the owner re-PUTting to it /// after it goes Clean) is what this closes. /// /// This guard fails the build if any file under `src/routes/` names a served-key /// generator or `content_key`. It is stronger than `pub(crate)` visibility, /// route code lives in the same crate, so `pub(crate)` would not stop it from /// calling these, and it is the same grep-proof discipline as the delete seal /// above. (The build runner uploads OTA artifacts server-side to a deterministic /// key via `generate_ota_artifact_key`; it lives outside `src/routes/`, so it is /// legitimately unaffected.) #[cfg(test)] mod served_key_seal_guard { use super::walk; use std::path::Path; #[test] fn routes_never_mint_served_keys() { let routes_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/routes"); // Every served-key generator, plus the content-key minter. `staging` // keys are the ONLY key a route may mint, so `generate_staging_key` is // deliberately absent from this list. // Anchored to the `S3Client::` call prefix so an unrelated `generate_key` // (e.g. `license_keys::generate_key`, `helpers::generate_key_code`) is not // a false positive, only the storage generators are S3Client methods. const FORBIDDEN: &[&str] = &[ "S3Client::generate_key(", "S3Client::generate_version_key(", "S3Client::generate_insertion_key(", "S3Client::generate_media_key(", "S3Client::generate_project_image_key(", "S3Client::generate_ota_artifact_key(", "S3Client::generate_item_gallery_key(", "S3Client::generate_project_gallery_key(", "S3Client::content_key(", ]; let mut offenders = Vec::new(); walk(&routes_dir, &mut |path, contents| { for (i, line) in contents.lines().enumerate() { if line.trim_start().starts_with("//") { continue; } for needle in FORBIDDEN { if line.contains(needle) { offenders.push(format!("{}:{}: {}", path.display(), i + 1, line.trim())); } } } }); assert!( offenders.is_empty(), "C1 seal violated, route handlers must presign only `generate_staging_key`; \ the served/content key is minted solely by the scan worker's promote step. \ Offending lines:\n{}", offenders.join("\n") ); } }