1use std::path::{Path, PathBuf};
2
3use oci_distribution::Reference;
4use once_cell::sync::Lazy;
5use regex::Regex;
6use tokio::fs;
7
8use crate::Error;
9
10pub static REFERENCE_REGEXP: &str = r"^((?:(?:[[:alnum:]]+)(?:(?:\.(?:[[:alnum:]]+))+)?(?::[[:digit:]]+)?/)?[[:lower:][:digit:]]+(?:(?:(?:[._]|__|[-]*)[[:lower:][:digit:]]+)+)?(?:(?:/[[:lower:][:digit:]]+(?:(?:(?:[._]|__|[-]*)[[:lower:][:digit:]]+)+)?)+)?)(?::([\w][\w.-]*))?(?:@([[:alpha:]][[:alnum:]]*(?:[-_+.][[:alpha:]][[:alnum:]]*)*[:][[:xdigit:]]+))?$";
15
16static RE: Lazy<Regex> = Lazy::new(|| {
17 regex::RegexBuilder::new(REFERENCE_REGEXP)
18 .size_limit(10 * (1 << 21))
19 .build()
20 .unwrap()
21});
22
23pub const DEFAULT_REGISTRY: &str = "registry.candle.dev";
24
25pub fn is_oci_reference(reference: &str) -> bool {
27 RE.is_match(reference)
28}
29
30pub fn parse_reference(reference: &str) -> Result<Reference, Error> {
32 let captures = RE
33 .captures(reference)
34 .ok_or(Error::InvalidReferenceFormat(reference.to_owned()))?;
35 let name = &captures[1];
36 let tag = captures.get(2).map(|m| m.as_str().to_owned());
37 let digest = captures.get(3).map(|m| m.as_str().to_owned());
38
39 let (registry, repository) = split_domain(name);
40
41 if let Some(tag) = tag {
42 Ok(oci_distribution::Reference::with_tag(registry, repository, tag))
43 } else if let Some(digest) = digest {
44 Ok(oci_distribution::Reference::with_digest(registry, repository, digest))
45 } else {
46 Err(Error::NoTagOrDigest(reference.to_owned()))
47 }
48}
49
50fn split_domain(name: &str) -> (String, String) {
52 match name.split_once('/') {
53 None => (DEFAULT_REGISTRY.to_owned(), name.to_owned()),
54 Some((left, right)) => {
55 if !(left.contains('.') || left.contains(':')) && left != "localhost" {
56 (DEFAULT_REGISTRY.to_owned(), name.to_owned())
57 } else {
58 (left.to_owned(), right.to_owned())
59 }
60 }
61 }
62}
63
64pub fn parse_reference_and_protocol(
66 reference: &str,
67 allowed_insecure: &[String],
68) -> Result<(Reference, oci_distribution::client::ClientProtocol), Error> {
69 let reference = parse_reference(reference)?;
70
71 let insecure = allowed_insecure.contains(&reference.registry().to_owned());
72 Ok((
73 reference,
74 if insecure {
75 oci_distribution::client::ClientProtocol::Http
76 } else {
77 oci_distribution::client::ClientProtocol::Https
78 },
79 ))
80}
81
82pub fn get_cache_directory<T: AsRef<Path>>(input: &str, basedir: T) -> Result<PathBuf, Error> {
83 let image_ref = parse_reference(input)?;
84
85 let registry = image_ref
86 .registry()
87 .split_once(':')
88 .map_or(image_ref.registry(), |(reg, _port)| reg);
89 let (org, repo) = image_ref.repository().split_once('/').ok_or(Error::OCIParseError(
90 input.to_owned(),
91 "repository was not in org/repo format".to_owned(),
92 ))?;
93
94 let version = image_ref.tag().ok_or(Error::NoName)?;
95
96 let target_dir = basedir.as_ref().join(registry).join(org).join(repo).join(version);
98 Ok(target_dir)
99}
100
101pub(crate) async fn create_directory_structure(dir: &Path) -> Result<(), Error> {
102 fs::create_dir_all(&dir)
103 .await
104 .map_err(|e| Error::CreateDir(dir.to_path_buf(), e))?;
105
106 debug!(path = %dir.display(), "Directory created");
107
108 Ok(())
109}
110
111static WICK_REF_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\w+/\w+:(\d+\.\d+\.\d+(-\w+)?|latest)?$").unwrap());
112
113pub fn is_wick_package_reference(loc: &str) -> bool {
114 WICK_REF_REGEX.is_match(loc)
115}
116
117#[cfg(test)]
118mod tests {
119 use std::path::Path;
120
121 use anyhow::Result;
122
123 use super::*;
124
125 #[rstest::rstest]
126 #[case("localhost:5555/test/integration:0.0.3", "", "localhost/test/integration/0.0.3")]
127 #[case(
128 "example.com/myorg/myrepo:1.0.0",
129 "/foo/bar",
130 "/foo/bar/example.com/myorg/myrepo/1.0.0"
131 )]
132 #[case("org/myrepo:1.0.1", "", "registry.candle.dev/org/myrepo/1.0.1")]
133 fn directory_structure_positive(#[case] input: &str, #[case] basedir: &str, #[case] expected: &str) {
134 let expected_dir = Path::new(expected);
135 let result = get_cache_directory(input, basedir).unwrap();
136 assert_eq!(result, expected_dir);
137 }
138 #[rstest::rstest]
139 #[case("example.com/myrepo:1.0.0")]
140 #[case("example.com/org/myrepo")]
141 #[case("example.com/myrepo")]
142 #[case("example.com:5000/myrepo:1.0.0")]
143 #[case("example.com:5000/org/myrepo")]
144 #[case("example.com:5000/myrepo")]
145 #[case("myrepo:1.0.0")]
146 #[case("org/myrepo")]
147 #[case("myrepo")]
148 fn directory_structure_negative(#[case] input: &str) {
149 let result = get_cache_directory(input, "");
150 println!("{:?}", result);
151 assert!(result.is_err());
152 }
153
154 #[test]
155 fn test_good_wickref() -> Result<()> {
156 assert!(is_wick_package_reference("this/that:1.2.3"));
157 assert!(is_wick_package_reference("this/that:1.2.3"));
158 assert!(is_wick_package_reference("1alpha/2alpha:0000.2222.9999"));
159 assert!(is_wick_package_reference("a_b_c_1/1_2_3_a:1.2.999-alpha"));
160 assert!(is_wick_package_reference("this/that:latest"));
161
162 Ok(())
163 }
164
165 #[test]
166 fn test_bad_wickref() -> Result<()> {
167 assert!(!is_wick_package_reference("not/this:bad_tag"));
168
169 Ok(())
170 }
171}