master
John Lauer 2.0.413: missing verbs + behaviors (assisted_click, import, fg-watch, url lint, notices, chrome-stable, eval frame, mov… 0dd55ee 2mo ago
//! URL lint (Node: urlLint): the three ways an AI in a container hands pup a URL that cannot work on
//! the user's desktop: a container file path, localhost, or an owner-less wiki path. Returned as
//! `_urlLint` + `urlLintCode` on open/navigate responses; never blocks the call.

use serde_json::{json, Value};

pub fn url_lint(u: &str, caller_container: &str) -> Option<Value> {
    if u.is_empty() { return None; }
    let low = u.to_ascii_lowercase();
    if low.starts_with("file:") {
        let fpath = u.splitn(2, "file://").nth(1).map(|p| p.trim_start_matches('/').to_string()).unwrap_or_default();
        let fpath = percent_decode(&fpath);
        let containerish = ["home/", "root/", "workspace/", "tmp/", "usr/", "var/", "opt/", "mnt/"].iter().any(|p| fpath.to_ascii_lowercase().starts_with(p));
        if containerish {
            return Some(json!({ "code": "url_file_container_path", "message": format!("CONTAINER PATH HANDED TO PUP. You passed \"{u}\", but pup drives Chrome on the USER'S DESKTOP (Windows/macOS): a file:// URL resolves against THEIR filesystem, not your container's. \"/{fpath}\" is a container path, so it can never exist there and this open is guaranteed to render Chrome's ERR_FILE_NOT_FOUND page. Do ONE of these instead: (1) SERVE it and open the public proxy URL (run `python3 -m http.server <port>` in that directory and open https://<your-container>.adom.cloud/proxy/<port>/<file>); or (2) PUSH it to the user's machine first with adom-bridge push_file, then open the file:// path it actually landed at on THEIR disk.") }));
        }
        let win = fpath.replace('/', "\\");
        if !win.is_empty() && !std::path::Path::new(&win).exists() {
            return Some(json!({ "code": "url_file_not_found", "message": format!("FILE DOES NOT EXIST ON THE USER'S MACHINE. pup pre-flighted \"{win}\" on the desktop before opening it and there is no such file, so this would have rendered Chrome's ERR_FILE_NOT_FOUND page. Check the path, or push the file to the desktop first with adom-bridge push_file and open the path it reports back. (pup's Chrome runs on the USER'S computer, so file:// is THEIR filesystem, not your container's.)") }));
        }
        return None;
    }
    let rest = u.splitn(2, "://").nth(1)?;
    let hostport = rest.split(['/', '?', '#']).next().unwrap_or("");
    let (host, port) = match hostport.rsplit_once(':') { Some((h, p)) if p.chars().all(|c| c.is_ascii_digit()) => (h.to_string(), p.to_string()), _ => (hostport.to_string(), String::new()) };
    let host_l = host.to_ascii_lowercase();
    if ["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "::1"].contains(&host_l.as_str()) {
        let path = rest.get(hostport.len()..).unwrap_or("").trim_start_matches('/');
        let slug = if caller_container.is_empty() { "<your-container-slug>".to_string() } else { caller_container.to_ascii_lowercase().chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '-' }).collect() };
        let p = if port.is_empty() { "<port>".to_string() } else { port.clone() };
        return Some(json!({ "code": "url_localhost", "message": format!("WRONG HOST FOR PUP. You passed \"{host}{}\", but pup drives a browser on the USER'S DESKTOP, not inside your container, so \"{host}\" resolves on THEIR machine and will hit whatever (if anything) is listening there, not your dev server. To show a service running INSIDE your container, use its public proxy URL: https://{slug}-<suffix>.adom.cloud/proxy/{p}/{path} (<suffix> is the 10-12 char id assigned to your container; `adom-cli container list` or your existing container URL has it). If you genuinely meant a server on the USER'S machine, say so explicitly, because that is almost never what an AI in a container intends.", if port.is_empty() { String::new() } else { format!(":{port}") }) }));
    }
    if host_l == "wiki.adom.inc" || host_l.ends_with(".wiki.adom.inc") {
        let path = rest.get(hostport.len()..).unwrap_or("");
        let path_only = path.split(['?', '#']).next().unwrap_or("");
        let segs: Vec<&str> = path_only.split('/').filter(|s| !s.is_empty()).collect();
        if segs.is_empty() { return None; }
        const ROUTES: [&str; 14] = ["apps", "skills", "components", "bootstraps", "discover", "login", "search", "api", "download", "static", "hero", "settings", "new", "readme"];
        if ROUTES.contains(&segs[0].to_ascii_lowercase().as_str()) { return None; }
        let owner_missing = segs.len() == 1 || segs[1].eq_ignore_ascii_case("blob");
        if owner_missing {
            let query = path.get(path_only.len()..).unwrap_or("");
            let fixed = format!("https://{host}/adom/{}{}{query}", segs[0], if segs.len() > 1 { format!("/{}", segs[1..].join("/")) } else { String::new() });
            return Some(json!({ "code": "url_wiki_missing_owner", "message": format!("MALFORMED WIKI URL. wiki.adom.inc pages are OWNER-QUALIFIED (https://wiki.adom.inc/<owner>/<slug>, and /<owner>/<slug>/blob/<path> for a file) and yours has no owner segment, so it will 404 no matter who is logged in. Most pages are owned by \"adom\", so you probably want: {fixed} . Confirm the owner with `adom-wiki search` if unsure.") }));
        }
    }
    None
}

fn percent_decode(s: &str) -> String {
    let b = s.as_bytes(); let mut out = Vec::with_capacity(b.len()); let mut i = 0;
    while i < b.len() { if b[i] == b'%' && i + 2 < b.len() { if let Ok(v) = u8::from_str_radix(&s[i + 1..i + 3], 16) { out.push(v); i += 3; continue; } } out.push(b[i]); i += 1; }
    String::from_utf8_lossy(&out).to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn container_path() { assert_eq!(url_lint("file:///home/adom/x.html", "").unwrap()["code"], "url_file_container_path"); }
    #[test] fn localhost() { let l = url_lint("http://localhost:5173/app?x=1", "my-box").unwrap(); assert_eq!(l["code"], "url_localhost"); assert!(l["message"].as_str().unwrap().contains("my-box-<suffix>.adom.cloud/proxy/5173/app?x=1")); }
    #[test] fn wiki_owner() { assert_eq!(url_lint("https://wiki.adom.inc/pup-bridge", "").unwrap()["code"], "url_wiki_missing_owner"); assert!(url_lint("https://wiki.adom.inc/adom/pup-bridge", "").is_none()); assert!(url_lint("https://wiki.adom.inc/apps/x", "").is_none()); }
    #[test] fn fine() { assert!(url_lint("https://example.com/", "").is_none()); }
}