diff --git a/Cargo.toml b/Cargo.toml index c2aa8dee9e..1a7e4259b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,4 +36,5 @@ regex = "~1.10.5" [dev-dependencies] tempfile = "3.2.0" +rstest = "0.22.0" serde_json = { version = "1.0.66", features = ["preserve_order"] } diff --git a/src/distribution/mod.rs b/src/distribution/mod.rs index 420d7916ce..635c701d01 100644 --- a/src/distribution/mod.rs +++ b/src/distribution/mod.rs @@ -12,11 +12,13 @@ //! Guide](https://github.com/opencontainers/artifacts) (a.k.a. "OCI Artifacts"). mod error; +mod reference; mod repository; mod tag; mod version; pub use error::*; +pub use reference::*; pub use repository::*; pub use tag::*; pub use version::*; diff --git a/src/distribution/reference.rs b/src/distribution/reference.rs index 2a44987a42..27ad3e3d8b 100644 --- a/src/distribution/reference.rs +++ b/src/distribution/reference.rs @@ -1,9 +1,9 @@ -use std::convert::TryFrom; use std::error::Error; use std::fmt; use std::str::FromStr; +use std::{convert::TryFrom, sync::OnceLock}; -use crate::regexp; +use regex::{Regex, RegexBuilder}; /// NAME_TOTAL_LENGTH_MAX is the maximum total number of characters in a repository name. const NAME_TOTAL_LENGTH_MAX: usize = 255; @@ -12,6 +12,19 @@ const DOCKER_HUB_DOMAIN_LEGACY: &str = "index.docker.io"; const DOCKER_HUB_DOMAIN: &str = "docker.io"; const DOCKER_HUB_OFFICIAL_REPO_NAME: &str = "library"; const DEFAULT_TAG: &str = "latest"; +/// REFERENCE_REGEXP is the full supported format of a reference. The regexp +/// is anchored and has capturing groups for name, tag, and digest components. +const 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,}))?$"; + +fn reference_regexp() -> &'static Regex { + static RE: OnceLock = OnceLock::new(); + RE.get_or_init(|| { + RegexBuilder::new(REFERENCE_REGEXP) + .size_limit(10 * (1 << 21)) + .build() + .unwrap() + }) +} /// Reasons that parsing a string as a Reference can fail. #[derive(Debug, PartialEq, Eq)] @@ -62,7 +75,7 @@ impl Error for ParseError {} /// Parsing a tagged image reference: /// /// ``` -/// use oci_client::Reference; +/// use oci_spec::distribution::Reference; /// /// let reference: Reference = "docker.io/library/hello-world:latest".parse().unwrap(); /// @@ -228,10 +241,7 @@ impl TryFrom for Reference { if s.is_empty() { return Err(ParseError::NameEmpty); } - lazy_static! { - static ref RE: regex::Regex = regexp::must_compile(regexp::REFERENCE_REGEXP); - }; - let captures = match RE.captures(&s) { + let captures = match reference_regexp().captures(&s) { Some(caps) => caps, None => { return Err(ParseError::ReferenceInvalidFormat);