1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::errors::OciError;
12
13mod archive;
14mod auth;
15mod build;
16mod pull;
17mod push;
18mod sign;
19mod transport;
20
21pub use archive::{create_tar_gz, extract_tar_gz};
22pub use auth::RegistryAuth;
23pub use build::{build_module, detect_container_runtime};
24pub use pull::{SignaturePolicy, pull_module};
25pub use push::{
26 current_platform, parse_platform_target, push_module, push_module_multiplatform,
27 rust_arch_to_oci,
28};
29pub use sign::{
30 VerifyOptions, attach_attestation, generate_slsa_provenance, sign_artifact, verify_attestation,
31 verify_signature,
32};
33
34pub const MEDIA_TYPE_MODULE_CONFIG: &str = "application/vnd.cfgd.module.config.v1+json";
39pub const MEDIA_TYPE_MODULE_LAYER: &str = "application/vnd.cfgd.module.layer.v1.tar+gzip";
40
41pub(super) const MEDIA_TYPE_OCI_MANIFEST: &str = "application/vnd.oci.image.manifest.v1+json";
43
44#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct OciReference {
51 pub registry: String,
53 pub repository: String,
55 pub reference: ReferenceKind,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum ReferenceKind {
61 Tag(String),
62 Digest(String),
63}
64
65impl std::fmt::Display for OciReference {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match &self.reference {
68 ReferenceKind::Tag(tag) => {
69 write!(f, "{}/{}:{}", self.registry, self.repository, tag)
70 }
71 ReferenceKind::Digest(digest) => {
72 write!(f, "{}/{}@{}", self.registry, self.repository, digest)
73 }
74 }
75 }
76}
77
78impl OciReference {
79 pub fn parse(reference: &str) -> Result<Self, OciError> {
88 if reference.is_empty()
89 || reference
90 .chars()
91 .any(|c| c.is_whitespace() || c.is_control())
92 {
93 return Err(OciError::InvalidReference {
94 reference: reference.to_string(),
95 });
96 }
97
98 let (name_part, ref_kind) = if let Some((name, digest)) = reference.split_once('@') {
100 (name, ReferenceKind::Digest(digest.to_string()))
101 } else if let Some((name, tag)) = reference.rsplit_once(':') {
102 if tag.chars().all(|c| c.is_ascii_digit()) && !name.contains('/') {
110 return Err(OciError::InvalidReference {
112 reference: reference.to_string(),
113 });
114 }
115 if tag.contains('/') {
118 (reference, ReferenceKind::Tag("latest".to_string()))
121 } else {
122 (name, ReferenceKind::Tag(tag.to_string()))
123 }
124 } else {
125 (reference, ReferenceKind::Tag("latest".to_string()))
126 };
127
128 let parts: Vec<&str> = name_part.splitn(2, '/').collect();
132 let (registry, repository) = if parts.len() == 1 {
133 ("docker.io".to_string(), format!("library/{}", parts[0]))
135 } else {
136 let first = parts[0];
137 let rest = parts[1];
138 if first.contains('.') || first.contains(':') || first == "localhost" {
139 (first.to_string(), rest.to_string())
140 } else {
141 ("docker.io".to_string(), name_part.to_string())
143 }
144 };
145
146 if repository.is_empty() {
147 return Err(OciError::InvalidReference {
148 reference: reference.to_string(),
149 });
150 }
151
152 Ok(OciReference {
153 registry,
154 repository,
155 reference: ref_kind,
156 })
157 }
158
159 pub fn reference_str(&self) -> &str {
161 match &self.reference {
162 ReferenceKind::Tag(t) => t,
163 ReferenceKind::Digest(d) => d,
164 }
165 }
166
167 pub(super) fn api_base(&self) -> String {
169 let scheme = if self.registry == "localhost"
170 || self.registry.starts_with("localhost:")
171 || self.registry.starts_with("127.0.0.1")
172 || is_insecure_registry(&self.registry)
173 {
174 "http"
175 } else {
176 "https"
177 };
178 format!("{scheme}://{}/v2", self.registry)
179 }
180}
181
182fn is_insecure_registry(registry: &str) -> bool {
184 std::env::var("OCI_INSECURE_REGISTRIES")
185 .unwrap_or_default()
186 .split(',')
187 .any(|r| r.trim() == registry)
188}
189
190#[derive(Debug, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub(super) struct OciManifest {
197 pub(super) schema_version: u32,
198 pub(super) media_type: String,
199 pub(super) config: OciDescriptor,
200 pub(super) layers: Vec<OciDescriptor>,
201 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
202 pub(super) annotations: HashMap<String, String>,
203}
204
205#[derive(Debug, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub(super) struct OciDescriptor {
208 pub(super) media_type: String,
209 pub(super) digest: String,
210 pub(super) size: u64,
211 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
212 pub(super) annotations: HashMap<String, String>,
213}
214
215#[cfg(test)]
220pub(super) mod test_helpers {
221 pub(crate) fn registry_from_url(url: &str) -> String {
223 url.trim_start_matches("http://")
224 .trim_start_matches("https://")
225 .trim_end_matches('/')
226 .to_string()
227 }
228
229 pub(crate) fn create_test_module_dir() -> tempfile::TempDir {
231 let dir = tempfile::tempdir().unwrap();
232 std::fs::write(
233 dir.path().join("module.yaml"),
234 "apiVersion: cfgd.io/v1alpha1\nkind: Module\nmetadata:\n name: test-mod\nspec:\n packages:\n - name: curl\n",
235 )
236 .unwrap();
237 std::fs::write(dir.path().join("README.md"), "# Test module\n").unwrap();
238 dir
239 }
240}
241
242#[cfg(test)]
247mod tests;