1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
use std::path::{Path, PathBuf};

use oci_distribution::Reference;
use once_cell::sync::Lazy;
use regex::Regex;
use tokio::fs;

use crate::Error;

// Originally borrowed from oci-distribution who doesn't export it...
// 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,}))?$";

// ... with some minor changes that reduce the cost of the regex by >80% at the risk of some outlier false positives:
pub static REFERENCE_REGEXP: &str = r"^((?:(?:[[:alnum:]]+)(?:(?:\.(?:[[:alnum:]]+))+)?(?::[[:digit:]]+)?/)?[[:lower:][:digit:]]+(?:(?:(?:[._]|__|[-]*)[[:lower:][:digit:]]+)+)?(?:(?:/[[:lower:][:digit:]]+(?:(?:(?:[._]|__|[-]*)[[:lower:][:digit:]]+)+)?)+)?)(?::([\w][\w.-]*))?(?:@([[:alpha:]][[:alnum:]]*(?:[-_+.][[:alpha:]][[:alnum:]]*)*[:][[:xdigit:]]+))?$";

static RE: Lazy<Regex> = Lazy::new(|| {
  regex::RegexBuilder::new(REFERENCE_REGEXP)
    .size_limit(10 * (1 << 21))
    .build()
    .unwrap()
});

pub const DEFAULT_REGISTRY: &str = "registry.candle.dev";

/// Check if a &str is an OCI reference.
pub fn is_oci_reference(reference: &str) -> bool {
  RE.is_match(reference)
}

/// Parse a `&str` as an OCI Reference.
pub fn parse_reference(reference: &str) -> Result<Reference, Error> {
  let captures = RE
    .captures(reference)
    .ok_or(Error::InvalidReferenceFormat(reference.to_owned()))?;
  let name = &captures[1];
  let tag = captures.get(2).map(|m| m.as_str().to_owned());
  let digest = captures.get(3).map(|m| m.as_str().to_owned());

  let (registry, repository) = split_domain(name);

  if let Some(tag) = tag {
    Ok(oci_distribution::Reference::with_tag(registry, repository, tag))
  } else if let Some(digest) = digest {
    Ok(oci_distribution::Reference::with_digest(registry, repository, digest))
  } else {
    Err(Error::NoTagOrDigest(reference.to_owned()))
  }
}

// Also borrowed from oci-distribution who borrowed it from the go docker implementation.
fn split_domain(name: &str) -> (String, String) {
  match name.split_once('/') {
    None => (DEFAULT_REGISTRY.to_owned(), name.to_owned()),
    Some((left, right)) => {
      if !(left.contains('.') || left.contains(':')) && left != "localhost" {
        (DEFAULT_REGISTRY.to_owned(), name.to_owned())
      } else {
        (left.to_owned(), right.to_owned())
      }
    }
  }
}

/// Parse a `&str` as a Reference and return the protocol to use.
pub fn parse_reference_and_protocol(
  reference: &str,
  allowed_insecure: &[String],
) -> Result<(Reference, oci_distribution::client::ClientProtocol), Error> {
  let reference = parse_reference(reference)?;

  let insecure = allowed_insecure.contains(&reference.registry().to_owned());
  Ok((
    reference,
    if insecure {
      oci_distribution::client::ClientProtocol::Http
    } else {
      oci_distribution::client::ClientProtocol::Https
    },
  ))
}

pub fn get_cache_directory<T: AsRef<Path>>(input: &str, basedir: T) -> Result<PathBuf, Error> {
  let image_ref = parse_reference(input)?;

  let registry = image_ref
    .registry()
    .split_once(':')
    .map_or(image_ref.registry(), |(reg, _port)| reg);
  let (org, repo) = image_ref.repository().split_once('/').ok_or(Error::OCIParseError(
    input.to_owned(),
    "repository was not in org/repo format".to_owned(),
  ))?;

  let version = image_ref.tag().ok_or(Error::NoName)?;

  // Create the wick_components directory if it doesn't exist
  let target_dir = basedir.as_ref().join(registry).join(org).join(repo).join(version);
  Ok(target_dir)
}

pub(crate) async fn create_directory_structure(dir: &Path) -> Result<(), Error> {
  fs::create_dir_all(&dir)
    .await
    .map_err(|e| Error::CreateDir(dir.to_path_buf(), e))?;

  debug!(path = %dir.display(), "Directory created");

  Ok(())
}

static WICK_REF_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\w+/\w+:(\d+\.\d+\.\d+(-\w+)?|latest)?$").unwrap());

pub fn is_wick_package_reference(loc: &str) -> bool {
  WICK_REF_REGEX.is_match(loc)
}

#[cfg(test)]
mod tests {
  use std::path::Path;

  use anyhow::Result;

  use super::*;

  #[rstest::rstest]
  #[case("localhost:5555/test/integration:0.0.3", "", "localhost/test/integration/0.0.3")]
  #[case(
    "example.com/myorg/myrepo:1.0.0",
    "/foo/bar",
    "/foo/bar/example.com/myorg/myrepo/1.0.0"
  )]
  #[case("org/myrepo:1.0.1", "", "registry.candle.dev/org/myrepo/1.0.1")]
  fn directory_structure_positive(#[case] input: &str, #[case] basedir: &str, #[case] expected: &str) {
    let expected_dir = Path::new(expected);
    let result = get_cache_directory(input, basedir).unwrap();
    assert_eq!(result, expected_dir);
  }
  #[rstest::rstest]
  #[case("example.com/myrepo:1.0.0")]
  #[case("example.com/org/myrepo")]
  #[case("example.com/myrepo")]
  #[case("example.com:5000/myrepo:1.0.0")]
  #[case("example.com:5000/org/myrepo")]
  #[case("example.com:5000/myrepo")]
  #[case("myrepo:1.0.0")]
  #[case("org/myrepo")]
  #[case("myrepo")]
  fn directory_structure_negative(#[case] input: &str) {
    let result = get_cache_directory(input, "");
    println!("{:?}", result);
    assert!(result.is_err());
  }

  #[test]
  fn test_good_wickref() -> Result<()> {
    assert!(is_wick_package_reference("this/that:1.2.3"));
    assert!(is_wick_package_reference("this/that:1.2.3"));
    assert!(is_wick_package_reference("1alpha/2alpha:0000.2222.9999"));
    assert!(is_wick_package_reference("a_b_c_1/1_2_3_a:1.2.999-alpha"));
    assert!(is_wick_package_reference("this/that:latest"));

    Ok(())
  }

  #[test]
  fn test_bad_wickref() -> Result<()> {
    assert!(!is_wick_package_reference("not/this:bad_tag"));

    Ok(())
  }
}