ff-rdp-cli 0.2.0

CLI for Firefox Remote Debugging Protocol
use crate::error::AppError;

const ALLOWED_SCHEMES: &[&str] = &["http", "https", "file", "about"];

pub fn validate_url(url: &str) -> Result<(), AppError> {
    let colon_pos = url
        .find(':')
        .ok_or_else(|| AppError::User(format!("invalid URL (no scheme): {url}")))?;

    let scheme = url[..colon_pos].to_ascii_lowercase();

    if scheme.is_empty() {
        return Err(AppError::User(format!("invalid URL (empty scheme): {url}")));
    }

    if !ALLOWED_SCHEMES.contains(&scheme.as_str()) {
        return Err(AppError::User(format!(
            "URL scheme '{scheme}:' is not allowed; permitted schemes: http, https, file, about. Use --allow-unsafe-urls to allow javascript: and data: schemes"
        )));
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn allows_http() {
        assert!(validate_url("http://example.com").is_ok());
    }

    #[test]
    fn allows_https() {
        assert!(validate_url("https://example.com/path?q=1").is_ok());
    }

    #[test]
    fn allows_file() {
        assert!(validate_url("file:///tmp/index.html").is_ok());
    }

    #[test]
    fn allows_about_blank() {
        assert!(validate_url("about:blank").is_ok());
    }

    #[test]
    fn allows_about_newtab() {
        assert!(validate_url("about:newtab").is_ok());
    }

    #[test]
    fn rejects_javascript() {
        let err = validate_url("javascript:alert(1)").unwrap_err();
        assert!(matches!(err, AppError::User(_)));
        assert!(err.to_string().contains("javascript"));
    }

    #[test]
    fn rejects_data() {
        let err = validate_url("data:text/html,<h1>hi</h1>").unwrap_err();
        assert!(matches!(err, AppError::User(_)));
        assert!(err.to_string().contains("data"));
    }

    #[test]
    fn rejects_vbscript() {
        let err = validate_url("vbscript:MsgBox(1)").unwrap_err();
        assert!(matches!(err, AppError::User(_)));
    }

    #[test]
    fn rejects_no_scheme() {
        let err = validate_url("example.com").unwrap_err();
        assert!(matches!(err, AppError::User(_)));
    }

    #[test]
    fn scheme_comparison_is_case_insensitive() {
        assert!(validate_url("HTTP://example.com").is_ok());
        assert!(validate_url("HTTPS://example.com").is_ok());
        let err = validate_url("Javascript:alert(1)").unwrap_err();
        assert!(matches!(err, AppError::User(_)));
    }

    #[test]
    fn rejects_empty_scheme() {
        let err = validate_url(":foo").unwrap_err();
        assert!(err.to_string().contains("empty scheme"));
    }

    #[test]
    fn rejects_leading_whitespace() {
        let err = validate_url(" http://example.com").unwrap_err();
        assert!(matches!(err, AppError::User(_)));
    }

    #[test]
    fn rejects_empty_string() {
        let err = validate_url("").unwrap_err();
        assert!(matches!(err, AppError::User(_)));
    }
}