// LinkHandler factories — URL parsing + URL building for YouTube // resource categories. Mirrors NPE // services/youtube/linkHandler/Youtube*LinkHandlerFactory.java. // // PORT SCOPE (per SPEC §6.6): we keep youtube.com / youtube-nocookie.com // / youtu.be / m.youtube.com / music.youtube.com. The 27-host Invidious // mirror list in NPE is dropped — Sulkta isn't an Invidious mirror. pub mod channel; pub mod search; pub mod stream; use thiserror::Error; #[derive(Debug, Error)] pub enum LinkError { #[error("invalid url: {0}")] InvalidUrl(String), #[error("unsupported host: {0}")] UnsupportedHost(String), #[error("missing id in url: {0}")] MissingId(String), #[error("malformed id: {0}")] MalformedId(String), } /// The acceptable hosts for first-party YT links. Audit Track D §6. pub const ACCEPTED_HOSTS: &[&str] = &[ "youtube.com", "www.youtube.com", "m.youtube.com", "music.youtube.com", "youtu.be", "www.youtube-nocookie.com", ]; pub fn host_is_youtube(host: &str) -> bool { let h = host.to_ascii_lowercase(); let h = h.strip_prefix("www.").unwrap_or(&h); ACCEPTED_HOSTS .iter() .any(|allowed| { let allowed = allowed.strip_prefix("www.").unwrap_or(allowed); allowed == h }) } #[cfg(test)] mod tests { use super::*; #[test] fn accepts_first_party_hosts() { assert!(host_is_youtube("www.youtube.com")); assert!(host_is_youtube("youtube.com")); assert!(host_is_youtube("m.youtube.com")); assert!(host_is_youtube("music.youtube.com")); assert!(host_is_youtube("youtu.be")); assert!(host_is_youtube("WWW.YouTube.COM")); // case-insensitive } #[test] fn rejects_invidious_and_random() { assert!(!host_is_youtube("invidious.io")); assert!(!host_is_youtube("yewtu.be")); assert!(!host_is_youtube("piped.video")); assert!(!host_is_youtube("evil.com")); } }