use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::errors::OciError;
mod archive;
mod auth;
mod build;
mod pull;
mod push;
mod sign;
mod transport;
pub use archive::{create_tar_gz, extract_tar_gz};
pub use auth::RegistryAuth;
pub use build::{build_module, detect_container_runtime};
pub use pull::{SignaturePolicy, pull_module};
pub use push::{
current_platform, parse_platform_target, push_module, push_module_multiplatform,
rust_arch_to_oci,
};
pub use sign::{
VerifyOptions, attach_attestation, generate_slsa_provenance, sign_artifact, verify_attestation,
verify_signature,
};
pub const MEDIA_TYPE_MODULE_CONFIG: &str = "application/vnd.cfgd.module.config.v1+json";
pub const MEDIA_TYPE_MODULE_LAYER: &str = "application/vnd.cfgd.module.layer.v1.tar+gzip";
pub(super) const MEDIA_TYPE_OCI_MANIFEST: &str = "application/vnd.oci.image.manifest.v1+json";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OciReference {
pub registry: String,
pub repository: String,
pub reference: ReferenceKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReferenceKind {
Tag(String),
Digest(String),
}
impl std::fmt::Display for OciReference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.reference {
ReferenceKind::Tag(tag) => {
write!(f, "{}/{}:{}", self.registry, self.repository, tag)
}
ReferenceKind::Digest(digest) => {
write!(f, "{}/{}@{}", self.registry, self.repository, digest)
}
}
}
}
impl OciReference {
pub fn parse(reference: &str) -> Result<Self, OciError> {
if reference.is_empty()
|| reference
.chars()
.any(|c| c.is_whitespace() || c.is_control())
{
return Err(OciError::InvalidReference {
reference: reference.to_string(),
});
}
let (name_part, ref_kind) = if let Some((name, digest)) = reference.split_once('@') {
(name, ReferenceKind::Digest(digest.to_string()))
} else if let Some((name, tag)) = reference.rsplit_once(':') {
if tag.chars().all(|c| c.is_ascii_digit()) && !name.contains('/') {
return Err(OciError::InvalidReference {
reference: reference.to_string(),
});
}
if tag.contains('/') {
(reference, ReferenceKind::Tag("latest".to_string()))
} else {
(name, ReferenceKind::Tag(tag.to_string()))
}
} else {
(reference, ReferenceKind::Tag("latest".to_string()))
};
let parts: Vec<&str> = name_part.splitn(2, '/').collect();
let (registry, repository) = if parts.len() == 1 {
("docker.io".to_string(), format!("library/{}", parts[0]))
} else {
let first = parts[0];
let rest = parts[1];
if first.contains('.') || first.contains(':') || first == "localhost" {
(first.to_string(), rest.to_string())
} else {
("docker.io".to_string(), name_part.to_string())
}
};
if repository.is_empty() {
return Err(OciError::InvalidReference {
reference: reference.to_string(),
});
}
Ok(OciReference {
registry,
repository,
reference: ref_kind,
})
}
pub fn reference_str(&self) -> &str {
match &self.reference {
ReferenceKind::Tag(t) => t,
ReferenceKind::Digest(d) => d,
}
}
pub(super) fn api_base(&self) -> String {
let scheme = if self.registry == "localhost"
|| self.registry.starts_with("localhost:")
|| self.registry.starts_with("127.0.0.1")
|| is_insecure_registry(&self.registry)
{
"http"
} else {
"https"
};
format!("{scheme}://{}/v2", self.registry)
}
}
fn is_insecure_registry(registry: &str) -> bool {
std::env::var("OCI_INSECURE_REGISTRIES")
.unwrap_or_default()
.split(',')
.any(|r| r.trim() == registry)
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct OciManifest {
pub(super) schema_version: u32,
pub(super) media_type: String,
pub(super) config: OciDescriptor,
pub(super) layers: Vec<OciDescriptor>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub(super) annotations: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct OciDescriptor {
pub(super) media_type: String,
pub(super) digest: String,
pub(super) size: u64,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub(super) annotations: HashMap<String, String>,
}
#[cfg(test)]
pub(super) mod test_helpers {
pub(crate) fn registry_from_url(url: &str) -> String {
url.trim_start_matches("http://")
.trim_start_matches("https://")
.trim_end_matches('/')
.to_string()
}
pub(crate) fn create_test_module_dir() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("module.yaml"),
"apiVersion: cfgd.io/v1alpha1\nkind: Module\nmetadata:\n name: test-mod\nspec:\n packages:\n - name: curl\n",
)
.unwrap();
std::fs::write(dir.path().join("README.md"), "# Test module\n").unwrap();
dir
}
}
#[cfg(test)]
mod tests;