diff --git a/Cargo.lock b/Cargo.lock index e7f711b..d6c12ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2120,6 +2120,7 @@ dependencies = [ "rustls", "serde", "serde_json", + "sha1", "socket2", "tempfile", "thiserror 2.0.17", diff --git a/pkg6depotd/Cargo.toml b/pkg6depotd/Cargo.toml index 2569b7f..0e9e9a4 100644 --- a/pkg6depotd/Cargo.toml +++ b/pkg6depotd/Cargo.toml @@ -30,6 +30,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" dirs = "6" nix = { version = "0.30", features = ["signal", "process", "user", "fs"] } +sha1 = "0.10" # Telemetry tracing = "0.1" diff --git a/pkg6depotd/src/http/handlers/catalog.rs b/pkg6depotd/src/http/handlers/catalog.rs index 3d9b6af..fe84203 100644 --- a/pkg6depotd/src/http/handlers/catalog.rs +++ b/pkg6depotd/src/http/handlers/catalog.rs @@ -7,6 +7,7 @@ use crate::repo::DepotRepo; use crate::errors::DepotError; use tower_http::services::ServeFile; use tower::ServiceExt; +use axum::http::header; pub async fn get_catalog_v1( State(repo): State>, @@ -19,7 +20,14 @@ pub async fn get_catalog_v1( let result = service.oneshot(req).await; match result { - Ok(res) => Ok(res.into_response()), + Ok(mut res) => { + // Ensure correct content-type for JSON catalog artifacts regardless of file extension + let is_catalog_json = filename == "catalog.attrs" || filename.starts_with("catalog."); + if is_catalog_json { + res.headers_mut().insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); + } + Ok(res.into_response()) + }, Err(e) => Err(DepotError::Server(e.to_string())), } } diff --git a/pkg6depotd/src/http/handlers/manifest.rs b/pkg6depotd/src/http/handlers/manifest.rs index e661d87..42250b4 100644 --- a/pkg6depotd/src/http/handlers/manifest.rs +++ b/pkg6depotd/src/http/handlers/manifest.rs @@ -8,6 +8,7 @@ use crate::repo::DepotRepo; use crate::errors::DepotError; use libips::fmri::Fmri; use std::str::FromStr; +use sha1::Digest as _; pub async fn get_manifest( State(repo): State>, @@ -16,9 +17,16 @@ pub async fn get_manifest( let fmri = Fmri::from_str(&fmri_str).map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?; let content = repo.get_manifest_text(&publisher, &fmri)?; + // Compute weak ETag from SHA-1 of manifest content (legacy friendly) + let mut hasher = sha1::Sha1::new(); + hasher.update(content.as_bytes()); + let etag = format!("\"{}\"", format!("{:x}", hasher.finalize())); Ok(( - [(header::CONTENT_TYPE, "text/plain")], - content + [ + (header::CONTENT_TYPE, "text/plain"), + (header::ETAG, etag.as_str()), + ], + content, ).into_response()) } diff --git a/pkg6depotd/tests/integration_tests.rs b/pkg6depotd/tests/integration_tests.rs index 6f1ab96..c20e5d7 100644 --- a/pkg6depotd/tests/integration_tests.rs +++ b/pkg6depotd/tests/integration_tests.rs @@ -230,4 +230,21 @@ async fn test_ini_only_repo_serving_catalog() { let body = resp.text().await.unwrap(); assert!(body.contains("package-count")); assert!(body.contains("parts")); + + // Also fetch individual catalog parts + for part in ["catalog.base.C", "catalog.dependency.C", "catalog.summary.C"].iter() { + let url = format!("{}/{}/catalog/1/{}", base_url, publisher, part); + let resp = client.get(&url).send().await.unwrap(); + assert!(resp.status().is_success(), "{} status: {:?}", part, resp.status()); + let ct = resp.headers().get("content-type").unwrap().to_str().unwrap().to_string(); + assert!(ct.contains("application/json"), "content-type for {} was {}", part, ct); + let txt = resp.text().await.unwrap(); + assert!(!txt.is_empty(), "{} should not be empty", part); + if *part == "catalog.base.C" { + assert!(txt.contains(&publisher) && txt.contains("version"), "base part should contain publisher and version"); + } else { + // dependency/summary may be empty for this test package; at least ensure signature is present + assert!(txt.contains("_SIGNATURE"), "{} should contain a signature field", part); + } + } }