//! Going the other way: a stored URL back to a key, and a key forward to a //! public URL. /// Extract the S3 key from a CDN or presigned URL. /// /// Accepts two URL shapes: /// - **CDN**: `https://cdn.example.com/{s3_key}`, caller supplies the /// CDN base; the function strips it verbatim. /// - **Path-style S3**: `https://{host}/{bucket}/{s3_key}?...`, caller /// supplies the bucket name; the function strips host + bucket prefix. /// /// Returns `None` if neither prefix matches. Query strings (presigned URL /// signatures) are stripped before returning. /// /// **Why explicit prefixes**: the prior implementation used /// `find("projects/")` as a heuristic, which would silently mis-key any URL /// whose path happened to contain the literal substring (e.g. a key with a /// `projects/` suffix inside a user folder). Passing the known CDN base and /// bucket eliminates the heuristic entirely. pub fn extract_s3_key_from_url( url: &str, cdn_base: &str, bucket: Option<&str>, s3_endpoint: Option<&str>, ) -> Option { let no_query = url.split('?').next()?; // Try CDN-base prefix first. An empty base matches nothing rather than // matching everything: `strip_prefix("")` succeeds on any input, so the // guard is what keeps a caller that passes "" from harvesting a key out of // an arbitrary host. if !cdn_base.is_empty() { let base = cdn_base.trim_end_matches('/'); if let Some(rest) = no_query.strip_prefix(base) && let Some(key) = rest.strip_prefix('/') && !key.is_empty() { return Some(key.to_string()); } } // Path-style S3: must match the configured `{endpoint}/{bucket}/` exactly. // Without the endpoint pin, the prior implementation accepted any // `https://{any-host}/{bucket}/{key}`, so an attacker-controlled URL like // `https://attacker.example/my-bucket/poisoned` would extract a real-looking // key and direct downstream code at attacker-chosen storage paths. if let (Some(bucket), Some(endpoint)) = (bucket, s3_endpoint) { let endpoint = endpoint.trim_end_matches('/'); let prefix = format!("{endpoint}/{bucket}/"); if let Some(key) = no_query.strip_prefix(&prefix) && !key.is_empty() { return Some(key.to_string()); } } None } /// Build a permanent URL for a project image. /// /// Permanent is the whole contract: callers persist the result into /// `projects.cover_image_url`, which is read forever. There is deliberately no /// presigned fallback — an expiring URL in a durable column is the bug this /// signature exists to make unrepresentable. `cdn_base` is required config /// (`Config::cdn_base_url`), so there is nothing to fall back to. pub fn build_project_image_url(cdn_base: &str, s3_key: &str) -> String { format!("{cdn_base}/{s3_key}") }