Skip to content

Commit

Permalink
Fixed parsing of image references without namespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
Madeeks committed Sep 28, 2023
1 parent a450cfa commit 0193d0b
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 91 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed

- Fixed support for image manifests which are provided by registries as multi-line, not indented JSON
- Fixed parsing from the command line of image references which feature registry host and image name, but no namespaces (e.g. `<registry>/<image>`)

### Security

Expand Down
51 changes: 33 additions & 18 deletions src/cli/Utility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include <chrono>

#include <boost/algorithm/string.hpp>
#include <boost/program_options.hpp>

#include "common/Config.hpp"
Expand All @@ -24,30 +25,44 @@ namespace sarus {
namespace cli {
namespace utility {

static bool isDomainLike(const std::string str) {
return ( str.find(".") != std::string::npos
|| str.find(":") != std::string::npos
|| str == std::string("localhost")
|| str == std::string("load")
|| str != boost::algorithm::to_lower_copy(str));
}

/**
* Parse server, namespace and individual image name from a string matching cli::regex::name
*/
static std::tuple<std::string, std::string, std::string> parseNameMatch(const std::string& in) {
static std::tuple<std::string, std::string, std::string> parseNameMatch(const std::string& input) {
auto server = common::ImageReference::DEFAULT_SERVER;
auto repositoryNamespace = common::ImageReference::DEFAULT_REPOSITORY_NAMESPACE;
auto image = std::string{};
auto first_separator = in.find_first_of("/");
auto last_separator = in.find_last_of("/");
auto image = input;

// No separators found: input is short image name
if (last_separator == std::string::npos){
image = in;
}
// Only one separator: input is "namespace/image"
else if (first_separator == last_separator){
repositoryNamespace = in.substr(0, first_separator);
image = in.substr(last_separator+1);
}
// Two or more separators
else {
server = in.substr(0, first_separator);
repositoryNamespace = in.substr(first_separator + 1, last_separator - first_separator - 1);
image = in.substr(last_separator+1);
auto firstSeparator = input.find_first_of("/");
if (firstSeparator != std::string::npos){
auto remainder = input;

auto first_component = input.substr(0, firstSeparator);
if (isDomainLike(first_component)) {
server = first_component;
remainder = input.substr(firstSeparator+1);
}

auto lastSeparator = remainder.find_last_of("/");

// No separators found: remainder is short image name
if (lastSeparator == std::string::npos){
repositoryNamespace = std::string{};
image = remainder;
}
// At least one separator: remainder is "namespace[/namespace]/image"
else {
repositoryNamespace = remainder.substr(0, lastSeparator);
image = remainder.substr(lastSeparator+1);
}
}

return std::tuple<std::string, std::string, std::string>{server, repositoryNamespace, image};
Expand Down
95 changes: 55 additions & 40 deletions src/cli/regex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,61 +20,76 @@ namespace strings {

// alphaNumeric defines the alpha numeric atom, typically a
// component of names. This only allows lower case characters and digits.
std::string alphaNumeric{"[a-z0-9]+"};
const std::string alphaNumeric{"[a-z0-9]+"};

// separator defines the separators allowed to be embedded in name
// components. This allow one period, one or two underscore and multiple
// components. This allows one period, one or two underscore and multiple
// dashes. Repeated dashes and underscores are intentionally treated
// differently. In order to support valid hostnames as name components,
// supporting repeated dash was added. Additionally double underscore is
// now allowed as a separator to loosen the restriction for previously
// supported names.
std::string separator{"(?:[._]|__|[-]*)"};
const std::string separator{"(?:[._]|__|[-]+)"};

// nameComponent restricts registry path component names to start
// pathComponent restricts registry path components to start
// with at least one letter or number, with following parts able to be
// separated by one period, one or two underscore and multiple dashes.
std::string nameComponent = concatenate({ alphaNumeric,
optional(repeated(separator + alphaNumeric))
});
const std::string pathComponent = concatenate({ alphaNumeric,
optional(repeated(separator + alphaNumeric))
});

// domainComponent restricts the registry domain component of a
// domainNameComponent restricts the registry domain component of a
// repository name to start with a component as defined by DomainRegexp
// and followed by an optional port.
std::string domainComponent{"(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])"};
const std::string domainNameComponent{"(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])"};

// domain defines the structure of potential domain components
// Only IPv6 in compressed or uncompressed format are allowed by this expression.
// Other formats like IPv6 zone identifiers or Special addresses are excluded.
// For general recommendations about IPv6 addresses text representations,
// refer to IETF RFC 5952.
const std::string ipv6Address{"\\[(?:[a-fA-F0-9:]+)\\]"};

const std::string port{"\\:[0-9]+"};

// domainName defines the structure of potential domain components
// that may be part of image names. This is purposely a subset of what is
// allowed by DNS to ensure backwards compatibility with Docker image
// names.
std::string domain = concatenate({ domainComponent,
optional(repeated("\\." + domainComponent)),
optional("\\:" + std::string{"[0-9]+"})
});

// name is the format for the name component of references. The
// regexp has capturing groups for the domain and name part omitting
// the separating forward slash from either.
std::string name = concatenate({ optional(domain + "\\/"),
nameComponent,
optional(repeated("\\/" + nameComponent))
});
const std::string domainName = concatenate({ domainNameComponent,
optional(repeated("\\." + domainNameComponent))
});

// host defines the structure of potential domains based on the URI Host
// subcomponent on IETF RFC 3986.
const std::string host = group(concatenate({domainName, "|", ipv6Address}));

const std::string domain = host + optional(port);

// remoteName matches the remote-name of a repository. It consists of one
// or more forward slash (/) delimited path-components (i.e. <namespace>/<repo name>)
const std::string remoteName = concatenate ({ pathComponent,
optional(repeated("\\/" + pathComponent))
});

// name is the format for the name component of references.
const std::string name = concatenate({ optional(domain + "\\/"),
remoteName
});

// tag matches valid tag names. From docker/docker:graph/tags.go.
std::string tag{"[\\w][\\w.-]{0,127}"};
const std::string tag{"[\\w][\\w.-]{0,127}"};

// digest matches valid digests.
std::string digest{"[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9A-Fa-f]{32,}"};
const std::string digest{"[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9A-Fa-f]{32,}"};

// reference is the full supported format of a reference. The regexp
// is anchored and has capturing groups for name, tag, and digest
// components.
std::string reference = anchored(capture(name)
+ optional("\\:" + capture(tag))
+ optional("\\@" + capture(digest))
);
const std::string reference = anchored(capture(name)
+ optional("\\:" + capture(tag))
+ optional("\\@" + capture(digest))
);

std::string concatenate(std::initializer_list<std::string> expr) {
std::string concatenate(const std::initializer_list<std::string> expr) {
auto output = std::stringstream{};
for (const auto& exp : expr) {
output << exp;
Expand All @@ -83,37 +98,37 @@ std::string concatenate(std::initializer_list<std::string> expr) {
}

// Wraps the expression in a non-capturing group and makes the group optional.
std::string optional(std::string expr) {
std::string optional(const std::string& expr) {
return group(expr) + "?";
}

// Wraps the regexp in a non-capturing group to get one or more matches.
std::string repeated(std::string expr) {
std::string repeated(const std::string& expr) {
return group(expr) + "+";
}

// Wraps the regexp in a non-capturing group.
std::string group(std::string expr) {
std::string group(const std::string& expr) {
return "(?:" + expr + ")";
}

// Wraps the expression in a capturing group.
std::string capture(std::string expr) {
std::string capture(const std::string& expr) {
return "(" + expr + ")";
}

// Anchors the regular expression by adding start and end delimiters.
std::string anchored(std::string expr) {
std::string anchored(const std::string& expr) {
return "^" + expr + "$";
}

} //namespace

boost::regex domain(strings::domain);
boost::regex name(strings::name);
boost::regex tag(strings::tag);
boost::regex digest(strings::digest);
boost::regex reference(strings::reference);
const boost::regex domain(strings::domain);
const boost::regex name(strings::name);
const boost::regex tag(strings::tag);
const boost::regex digest(strings::digest);
const boost::regex reference(strings::reference);

} // namespace
} // namespace
Expand Down
48 changes: 27 additions & 21 deletions src/cli/regex.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,36 @@ namespace sarus {
namespace cli {
namespace regex {

extern boost::regex domain;
extern boost::regex name;
extern boost::regex tag;
extern boost::regex digest;
extern boost::regex reference;
extern const boost::regex domain;
extern const boost::regex name;
extern const boost::regex tag;
extern const boost::regex digest;
extern const boost::regex reference;

namespace strings {

extern std::string alphaNumeric;
extern std::string separator;
extern std::string nameComponent;
extern std::string domainComponent;
extern std::string domain;
extern std::string name;
extern std::string tag;
extern std::string digest;
extern std::string reference;

extern std::string concatenate(std::initializer_list<std::string> expr);
extern std::string optional(std::string expr);
extern std::string repeated(std::string expr);
extern std::string group(std::string expr);
extern std::string capture(std::string expr);
extern std::string anchored(std::string expr);
extern const std::string alphaNumeric;
extern const std::string separator;
extern const std::string pathComponent;
extern const std::string domainNameComponent;
extern const std::string ipv6Address;
extern const std::string port;
extern const std::string domainName;
extern const std::string host;
extern const std::string domain;
extern const std::string remoteName;
extern const std::string namePattern;
extern const std::string name;
extern const std::string tag;
extern const std::string digest;
extern const std::string reference;

std::string concatenate(const std::initializer_list<std::string> expr);
std::string optional(const std::string& expr);
std::string repeated(const std::string& expr);
std::string group(const std::string& expr);
std::string capture(const std::string& expr);
std::string anchored(const std::string& expr);

}
}
Expand Down
64 changes: 56 additions & 8 deletions src/cli/test/test_Utility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,32 +65,80 @@ TEST(CLIUtilityTestGroup, parseImageReference) {
CHECK_EQUAL(imageReference.tag, std::string{"tag"});
CHECK_EQUAL(imageReference.digest, std::string{""});

imageReference = cli::utility::parseImageReference("server/namespace/image:tag");
CHECK_EQUAL(imageReference.server, std::string{"server"});
imageReference = cli::utility::parseImageReference("server.io/namespace/image:tag");
CHECK_EQUAL(imageReference.server, std::string{"server.io"});
CHECK_EQUAL(imageReference.repositoryNamespace, std::string{"namespace"});
CHECK_EQUAL(imageReference.image, std::string{"image"});
CHECK_EQUAL(imageReference.tag, std::string{"tag"});
CHECK_EQUAL(imageReference.digest, std::string{""});

// Nested namespaces
imageReference = cli::utility::parseImageReference("server/namespace0/namespace1/image:tag");
CHECK_EQUAL(imageReference.server, std::string{"server"});
imageReference = cli::utility::parseImageReference("server.io/namespace0/namespace1/image:tag");
CHECK_EQUAL(imageReference.server, std::string{"server.io"});
CHECK_EQUAL(imageReference.repositoryNamespace, std::string{"namespace0/namespace1"});
CHECK_EQUAL(imageReference.image, std::string{"image"});
CHECK_EQUAL(imageReference.tag, std::string{"tag"});
CHECK_EQUAL(imageReference.digest, std::string{""});

// Nested namespaces without server
imageReference = cli::utility::parseImageReference("namespace0/namespace1/image:tag");
CHECK_EQUAL(imageReference.server, std::string{"docker.io"});
CHECK_EQUAL(imageReference.repositoryNamespace, std::string{"namespace0/namespace1"});
CHECK_EQUAL(imageReference.image, std::string{"image"});
CHECK_EQUAL(imageReference.tag, std::string{"tag"});
CHECK_EQUAL(imageReference.digest, std::string{""});

// No namespaces
imageReference = cli::utility::parseImageReference("server.io/image:tag");
CHECK_EQUAL(imageReference.server, std::string{"server.io"});
CHECK_EQUAL(imageReference.repositoryNamespace, std::string{""});
CHECK_EQUAL(imageReference.image, std::string{"image"});
CHECK_EQUAL(imageReference.tag, std::string{"tag"});
CHECK_EQUAL(imageReference.digest, std::string{""});

// localhost as server
imageReference = cli::utility::parseImageReference("localhost/namespace/image:tag");
CHECK_EQUAL(imageReference.server, std::string{"localhost"});
CHECK_EQUAL(imageReference.repositoryNamespace, std::string{"namespace"});
CHECK_EQUAL(imageReference.image, std::string{"image"});
CHECK_EQUAL(imageReference.tag, std::string{"tag"});
CHECK_EQUAL(imageReference.digest, std::string{""});

// load as server
imageReference = cli::utility::parseImageReference("load/namespace/image:tag");
CHECK_EQUAL(imageReference.server, std::string{"load"});
CHECK_EQUAL(imageReference.repositoryNamespace, std::string{"namespace"});
CHECK_EQUAL(imageReference.image, std::string{"image"});
CHECK_EQUAL(imageReference.tag, std::string{"tag"});
CHECK_EQUAL(imageReference.digest, std::string{""});

// Server with port
imageReference = cli::utility::parseImageReference("server.io:1234/namespace/image:tag");
CHECK_EQUAL(imageReference.server, std::string{"server.io:1234"});
CHECK_EQUAL(imageReference.repositoryNamespace, std::string{"namespace"});
CHECK_EQUAL(imageReference.image, std::string{"image"});
CHECK_EQUAL(imageReference.tag, std::string{"tag"});
CHECK_EQUAL(imageReference.digest, std::string{""});

// Server with port and no domain dots
imageReference = cli::utility::parseImageReference("server:1234/namespace/image:tag");
CHECK_EQUAL(imageReference.server, std::string{"server:1234"});
CHECK_EQUAL(imageReference.repositoryNamespace, std::string{"namespace"});
CHECK_EQUAL(imageReference.image, std::string{"image"});
CHECK_EQUAL(imageReference.tag, std::string{"tag"});
CHECK_EQUAL(imageReference.digest, std::string{""});

// Image with digest
imageReference = cli::utility::parseImageReference("server/namespace/image@sha256:d4ff818577bc193b309b355b02ebc9220427090057b54a59e73b79bdfe139b83");
CHECK_EQUAL(imageReference.server, std::string{"server"});
imageReference = cli::utility::parseImageReference("server.io/namespace/image@sha256:d4ff818577bc193b309b355b02ebc9220427090057b54a59e73b79bdfe139b83");
CHECK_EQUAL(imageReference.server, std::string{"server.io"});
CHECK_EQUAL(imageReference.repositoryNamespace, std::string{"namespace"});
CHECK_EQUAL(imageReference.image, std::string{"image"});
CHECK_EQUAL(imageReference.tag, std::string{""});
CHECK_EQUAL(imageReference.digest, std::string{"sha256:d4ff818577bc193b309b355b02ebc9220427090057b54a59e73b79bdfe139b83"});

// Image with tag+digest
imageReference = cli::utility::parseImageReference("server/namespace/image:tag@sha256:d4ff818577bc193b309b355b02ebc9220427090057b54a59e73b79bdfe139b83");
CHECK_EQUAL(imageReference.server, std::string{"server"});
imageReference = cli::utility::parseImageReference("server.io/namespace/image:tag@sha256:d4ff818577bc193b309b355b02ebc9220427090057b54a59e73b79bdfe139b83");
CHECK_EQUAL(imageReference.server, std::string{"server.io"});
CHECK_EQUAL(imageReference.repositoryNamespace, std::string{"namespace"});
CHECK_EQUAL(imageReference.image, std::string{"image"});
CHECK_EQUAL(imageReference.tag, std::string{"tag"});
Expand Down
Loading

0 comments on commit 0193d0b

Please sign in to comment.