wick_oci_utils/
utils.rs

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
10// Originally borrowed from oci-distribution who doesn't export it...
11// pub static REFERENCE_REGEXP: &str = r"^((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?/)?[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\w][\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$";
12
13// ... with some minor changes that reduce the cost of the regex by >80% at the risk of some outlier false positives:
14pub 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
25/// Check if a &str is an OCI reference.
26pub fn is_oci_reference(reference: &str) -> bool {
27  RE.is_match(reference)
28}
29
30/// Parse a `&str` as an OCI Reference.
31pub 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
50// Also borrowed from oci-distribution who borrowed it from the go docker implementation.
51fn 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
64/// Parse a `&str` as a Reference and return the protocol to use.
65pub 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  // Create the wick_components directory if it doesn't exist
97  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}