Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy

plugins/native/znippy-plugin-maven/src/resolver.rs

//! Transitive Maven dependency resolver.
//!
//! Fetches POMs from Maven Central, parses dependencies, and recursively
//! resolves the full dependency tree. Requires the `resolve` feature.

use crate::pom::{parse_pom_dependencies, MavenCoord};
use std::collections::{HashSet, VecDeque};
use std::io::Read;

/// Resolve all transitive dependencies starting from a root POM.
/// Returns a deduplicated list of all coords (including the root's direct deps).
pub fn resolve_transitive(root_pom: &[u8], max_depth: usize) -> Vec<MavenCoord> {
    let direct = parse_pom_dependencies(root_pom);
    let mut resolved: Vec<MavenCoord> = Vec::new();
    let mut visited: HashSet<String> = HashSet::new();
    let mut queue: VecDeque<(MavenCoord, usize)> = VecDeque::new();

    for coord in direct {
        let key = coord_key(&coord);
        if visited.insert(key) {
            queue.push_back((coord, 0));
        }
    }

    while let Some((coord, depth)) = queue.pop_front() {
        resolved.push(coord.clone());

        if depth >= max_depth {
            continue;
        }

        // Fetch this dep's POM to get its transitive deps
        let pom_url = pom_url(&coord);
        match fetch_pom(&pom_url) {
            Some(pom_bytes) => {
                let transitive = parse_pom_dependencies(&pom_bytes);
                for dep in transitive {
                    // Skip deps without version (managed by parent — we can't resolve those without parent POM)
                    if dep.version.is_empty() || dep.version.starts_with('$') {
                        continue;
                    }
                    let key = coord_key(&dep);
                    if visited.insert(key) {
                        queue.push_back((dep, depth + 1));
                    }
                }
            }
            None => {
                // POM not found — skip transitives for this one
            }
        }
    }

    resolved
}

/// Resolve direct dependencies only (no recursion), just parse + return coords.
pub fn resolve_direct(root_pom: &[u8]) -> Vec<MavenCoord> {
    parse_pom_dependencies(root_pom)
}

/// Download a JAR/artifact from Maven Central. Returns bytes or None.
pub fn download_artifact(coord: &MavenCoord) -> Option<Vec<u8>> {
    let url = coord.central_url();
    let resp = http_agent().get(&url).call().ok()?;
    if resp.status() != 200 {
        return None;
    }
    let mut body = Vec::new();
    resp.into_body().into_reader().read_to_end(&mut body).ok()?;
    Some(body)
}

/// Download a JAR to a file path. Returns true on success.
pub fn download_artifact_to_file(coord: &MavenCoord, path: &std::path::Path) -> bool {
    let url = coord.central_url();
    match http_agent().get(&url).call() {
        Ok(resp) if resp.status() == 200 => {
            let mut body = Vec::new();
            if resp.into_body().into_reader().read_to_end(&mut body).is_ok() {
                std::fs::write(path, &body).is_ok()
            } else {
                false
            }
        }
        _ => false,
    }
}

fn coord_key(c: &MavenCoord) -> String {
    format!("{}:{}:{}", c.group_id, c.artifact_id, c.version)
}

fn pom_url(coord: &MavenCoord) -> String {
    let group_path = coord.group_id.replace('.', "/");
    format!(
        "https://repo1.maven.org/maven2/{}/{}/{}/{}-{}.pom",
        group_path, coord.artifact_id, coord.version,
        coord.artifact_id, coord.version
    )
}

fn fetch_pom(url: &str) -> Option<Vec<u8>> {
    let resp = http_agent().get(url).call().ok()?;
    if resp.status() != 200 {
        return None;
    }
    let mut body = Vec::new();
    resp.into_body().into_reader().read_to_end(&mut body).ok()?;
    Some(body)
}

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

    #[test]
    fn test_resolve_guava_transitive() {
        let pom = br#"<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>test</groupId>
    <artifactId>test</artifactId>
    <version>1.0</version>
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>33.2.0-jre</version>
        </dependency>
    </dependencies>
</project>"#;

        let all = resolve_transitive(pom, 2);
        assert!(!all.is_empty());
        // Guava pulls in failureaccess, checker-qual, etc.
        println!("Resolved {} deps from guava:", all.len());
        for c in &all {
            println!("  {}:{}:{}", c.group_id, c.artifact_id, c.version);
        }
        assert!(all.len() > 1);
    }
}

/// A ureq 3 agent with ureq 2's defaults, so the bump changes no behaviour:
/// ureq 2 (built without `proxy-from-env`) never read `HTTP(S)_PROXY` and
/// followed at most 5 redirects; ureq 3 reads the environment and allows 10.
/// (ureq 2 -> 3 bump, 2026-10-02.)
fn http_agent() -> ureq::Agent {
    ureq::Agent::config_builder().proxy(None).max_redirects(5).build().new_agent()
}