diff --git a/crates/rattler_networking/Cargo.toml b/crates/rattler_networking/Cargo.toml index 929f3322d..c949b4dfe 100644 --- a/crates/rattler_networking/Cargo.toml +++ b/crates/rattler_networking/Cargo.toml @@ -21,6 +21,7 @@ async-trait = { workspace = true } base64 = { workspace = true } dirs = { workspace = true } fslock = { workspace = true } +http = "1.1.0" itertools = { workspace = true } keyring = { workspace = true } lazy_static = { workspace = true } diff --git a/crates/rattler_networking/src/mirror_middleware.rs b/crates/rattler_networking/src/mirror_middleware.rs index 8f43d3d7d..210b09725 100644 --- a/crates/rattler_networking/src/mirror_middleware.rs +++ b/crates/rattler_networking/src/mirror_middleware.rs @@ -171,22 +171,68 @@ fn oci_url_with_hash(url: &Url, hash: &str) -> Url { .unwrap() } +#[derive(Debug)] +struct OciTagMediaType { + url: Url, + tag: String, + media_type: String, +} + +#[allow(dead_code)] +fn reverse_version_build_tag(tag: &str) -> String { + tag.replace("__p__", "+") + .replace("__e__", "!") + .replace("__eq__", "=") +} + +fn version_build_tag(tag: &str) -> String { + tag.replace("+", "__p__") + .replace("!", "__e__") + .replace("=", "__eq__") +} + /// We reimplement some logic from rattler here because we don't want to introduce cyclic dependencies -fn package_to_tag(url: &Url) -> (Url, String) { +fn package_to_tag(url: &Url) -> OciTagMediaType { // get filename (last segment of path) let filename = url.path_segments().unwrap().last().unwrap(); - if let Some(archive_name) = filename - .strip_suffix(".conda") - .or(filename.strip_suffix(".tar.bz2")) - { - let parts = archive_name.splitn(3, '-').collect::>(); - let name = parts[0]; - let tag = format!("{}-{}", parts[1], parts[2]); - // TODO we need to adjust the tag for OCI compatibility - (url.join(name).unwrap(), tag) - } else { - (url.clone(), "latest".to_string()) + + let mut res = OciTagMediaType { + url: url.clone(), + tag: "latest".to_string(), + media_type: "".to_string(), + }; + + let mut computed_filename = filename.to_string(); + + if let Some(archive_name) = filename.strip_suffix(".conda") { + let parts = archive_name.rsplitn(3, '-').collect::>(); + computed_filename = parts[2].to_string(); + res.tag = version_build_tag(&format!("{}-{}", parts[1], parts[0])); + res.media_type = "application/vnd.conda.package.v2".to_string(); + } else if let Some(archive_name) = filename.strip_suffix(".tar.bz2") { + let parts = archive_name.rsplitn(3, '-').collect::>(); + computed_filename = parts[2].to_string(); + res.tag = version_build_tag(&format!("{}-{}", parts[1], parts[0])); + res.media_type = "application/vnd.conda.package.v1".to_string(); + } else if filename.starts_with("repodata.json") { + computed_filename = "repodata.json".to_string(); + if filename == "repodata.json" { + res.media_type = "application/vnd.conda.repodata.v1+json".to_string(); + } else if filename.ends_with(".gz") { + res.media_type = "application/vnd.conda.repodata.v1+json+gzip".to_string(); + } else if filename.ends_with(".bz2") { + res.media_type = "application/vnd.conda.repodata.v1+json+bz2".to_string(); + } else if filename.ends_with(".zst") { + res.media_type = "application/vnd.conda.repodata.v1+json+zst".to_string(); + } } + + if computed_filename.starts_with("_") { + computed_filename = format!("zzz{computed_filename}"); + } + + res.url = url.join(&computed_filename).unwrap(); + res } #[allow(dead_code)] @@ -223,8 +269,9 @@ impl Middleware for OciMiddleware { return next.run(req, extensions).await; } - let (url, tag) = package_to_tag(req.url()); - let token = self.token_cache.lock().unwrap().get(&url).cloned(); + let oci_info = package_to_tag(req.url()); + let url = &oci_info.url; + let token = self.token_cache.lock().unwrap().get(url).cloned(); let token = if let Some(token) = token { token @@ -257,7 +304,7 @@ impl Middleware for OciMiddleware { "https://{}/v2{}/manifests/{}", url.host_str().unwrap(), url.path(), - tag + &oci_info.tag ); let manifest = reqwest::Client::new() @@ -268,25 +315,23 @@ impl Middleware for OciMiddleware { .await .map_err(reqwest_middleware::Error::Reqwest)?; - let manifest_string = manifest.text().await.unwrap(); - + let manifest_string = manifest.text().await?; + tracing::info!("{}", manifest_string); let manifest: Manifest = serde_json::from_str(&manifest_string).unwrap(); - let layer = if url.as_str().ends_with(".json") { - manifest - .layers - .iter() - .find(|l| { - l.media_type - .starts_with("application/vnd.conda.repodata.v1+json") - }) - .unwrap() + + tracing::info!("{:?}", oci_info); + + let layer = if let Some(layer) = manifest + .layers + .iter() + .find(|l| l.media_type == oci_info.media_type) + { + layer } else { - // find the layer with the correct media type - manifest - .layers - .iter() - .find(|l| l.media_type.starts_with("application/vnd.conda.package")) - .unwrap() + // TODO it would be much better to return a 404 here + return Err(reqwest_middleware::Error::Middleware(anyhow::anyhow!( + "No layer found with the expected media type".to_string() + ))); }; let layer_url = format!(