Skip to content

Commit

Permalink
image-reference: Add facilities to parse a docker image container string
Browse files Browse the repository at this point in the history
This code has been ported from docker as indicated by the various links in the
commit.
  • Loading branch information
dlespiau committed Apr 28, 2019
1 parent df6ab73 commit 6010801
Show file tree
Hide file tree
Showing 2 changed files with 256 additions and 0 deletions.
218 changes: 218 additions & 0 deletions src/image-reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { disposeEmitNodes } from "typescript";

// Grammar
//
// reference := name [ ":" tag ] [ "@" digest ]
// name := [domain '/'] path-component ['/' path-component]*
// domain := domain-component ['.' domain-component]* [':' port-number]
// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
// port-number := /[0-9]+/
// path-component := alpha-numeric [separator alpha-numeric]*
// alpha-numeric := /[a-z0-9]+/
// separator := /[_.]|__|[-]*/
//
// tag := /[\w][\w.-]{0,127}/
//
// digest := digest-algorithm ":" digest-hex
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
// digest-algorithm-separator := /[+.-_]/
// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
//
// identifier := /[a-f0-9]{64}/
// short-identifier := /[a-f0-9]{6,64}/

// Ref: https://github.com/docker/distribution/blob/master/reference/reference.go
// Ref: https://github.com/docker/distribution/blob/master/reference/regexp.go
// Ref: https://github.com/moby/moby/blob/master/image/spec/v1.2.md

// NameTotalLengthMax is the maximum total number of characters in a repository name.
const NameTotalLengthMax = 255

function match(s: string|RegExp): RegExp {
if (s instanceof RegExp) {
return s;
}
return new RegExp(s);
}

function quoteMeta(s: string): string {
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}

// literal compiles s into a literal regular expression, escaping any regexp
// reserved characters.
function literal(s: string): RegExp {
return match(quoteMeta(s));
}

// expression defines a full expression, where each regular expression must
// follow the previous.
function expression(...res: RegExp[]): RegExp {
let s = '';
for (const re of res) {
s += re.source;
}
return match(s);
}

// optional wraps the expression in a non-capturing group and makes the
// production optional.
function optional(...res: RegExp[]): RegExp {
return match(group(expression(...res)).source + '?');

}

// repeated wraps the regexp in a non-capturing group to get one or more
// matches.
function repeated(...res: RegExp[]): RegExp {
return match(group(expression(...res)).source + '+');
}

// capture wraps the expression in a capturing group.
function capture(...res: RegExp[]): RegExp {
return match(`(` + expression(...res).source + `)`)
}

// anchored anchors the regular expression by adding start and end delimiters.
function anchored(...res: RegExp[]): RegExp {
return match(`^` + expression(...res).source + `$`)
}

// group wraps the regexp in a non-capturing group.
function group(...res: RegExp[]): RegExp {
return match(`(?:${expression(...res).source})`);
}

// alphaNumericRe defines the alpha numeric atom, typically a component of
// names. This only allows lower case characters and digits.
const alphaNumericRegexp = match(/[a-z0-9]+/);

// separatorRegexp defines the separators allowed to be embedded in name components.
// This allow one period, one or two underscore and multiple dashes.
const separatorRegexp = match(/(?:[._]|__|[-]*)/);

// nameComponentRegexp restricts registry path component names 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.
const nameComponentRegexp = expression(
alphaNumericRegexp,
optional(repeated(separatorRegexp, alphaNumericRegexp)));

// domainComponentRegexp restricts the registry domain component of a
// repository name to start with a component as defined by DomainRegexp
// and followed by an optional port.
const domainComponentRegexp = match(/(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/)

// DomainRegexp 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.
const DomainRegexp = expression(
domainComponentRegexp,
optional(repeated(literal(`.`), domainComponentRegexp)),
optional(literal(`:`), match(/[0-9]+/)))

// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
const TagRegexp = match(/[\w][\w.-]{0,127}/)

// anchoredTagRegexp matches valid tag names, anchored at the start and
// end of the matched string.
const anchoredTagRegexp = anchored(TagRegexp)

// DigestRegexp matches valid digests.
const DigestRegexp = match(/[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9a-fA-F]{32,}/)

// anchoredDigestRegexp matches valid digests, anchored at the start and
// end of the matched string.
const anchoredDigestRegexp = anchored(DigestRegexp)

// NameRegexp 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.
const NameRegexp = expression(
optional(DomainRegexp, literal(`/`)),
nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp)))

// anchoredNameRegexp is used to parse a name value, capturing the
// domain and trailing components.
const anchoredNameRegexp = anchored(
optional(capture(DomainRegexp), literal(`/`)),
capture(nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp))))

// ReferenceRegexp is the full supported format of a reference. The regexp
// is anchored and has capturing groups for name, tag, and digest
// components.
const ReferenceRegexp = anchored(capture(NameRegexp),
optional(literal(":"), capture(TagRegexp)),
optional(literal("@"), capture(DigestRegexp)))

// IdentifierRegexp is the format for string identifier used as a
// content addressable identifier using sha256. These identifiers
// are like digests without the algorithm, since sha256 is used.
const IdentifierRegexp = match(/([a-f0-9]{64})/)

// ShortIdentifierRegexp is the format used to represent a prefix
// of an identifier. A prefix may be used to match a sha256 identifier
// within a list of trusted identifiers.
const ShortIdentifierRegexp = match(/([a-f0-9]{6,64})/)

// anchoredIdentifierRegexp is used to check or match an
// identifier value, anchored at start and end of string.
const anchoredIdentifierRegexp = anchored(IdentifierRegexp)

// anchoredShortIdentifierRegexp is used to check if a value
// is a possible identifier prefix, anchored at start and end
// of string.
const anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp)

interface VersionInfo {
tag?: string;
digest?: string;
}

class ImageReference {
domain?: string;
path: string;
tag?: string;
digest?: string;

constructor(domain: string|undefined, path: string, version?: VersionInfo) {
this.domain = domain;
this.path = path;
if (version) {
this.tag = version.tag;
this.digest = version.digest;
}
}

static fromString(s: string): ImageReference {
const matches = s.match(ReferenceRegexp);
if (matches == null) {
throw new Error(`invalid image reference`);
}

const name = matches[1],
tag = matches[2],
digest = matches[3];

if (name.length > NameTotalLengthMax) {
throw new Error(`repository name must not be more than ${NameTotalLengthMax} characters`);
}

const nameMatches = name.match(anchoredNameRegexp);
if (nameMatches == null) {
throw new Error(`invalid image reference`);
}
const domain = nameMatches[1],
path = nameMatches[2];

return new ImageReference(domain, path, { tag, digest });
}
}

export {
ImageReference,
};
38 changes: 38 additions & 0 deletions tests/image-reference.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ImageReference } from '../src/image-reference';

// from https://github.com/docker/distribution/blob/master/reference/reference_test.go
[
['test_com', { path: 'test_com' }],
['test.com:tag', { path: 'test.com', tag: 'tag' }],
['test.com:5000', { path: 'test.com', tag: '5000' }],
['test.com/repo:tag', { domain: 'test.com', path: 'repo', tag: 'tag' }],
['test.com:5000/repo', { domain: 'test.com:5000', path: 'repo' } ],
['test.com:5000/repo:tag', { domain: 'test.com:5000', path: 'repo', tag: 'tag' } ],
['test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', {
domain: 'test:5000',
path: 'repo',
digest: 'sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
}],
['test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', {
domain: 'test:5000',
path: 'repo',
tag: 'tag',
digest: 'sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
}],
['test:5000/repo', { domain: 'test:5000', path: 'repo' } ],
[':justtag', { err: 'invalid' }],
['b.gcr.io/test.example.com/my-app:test.example.com', {
domain: 'b.gcr.io',
path: 'test.example.com/my-app',
tag: 'test.example.com',
}],
].forEach(([input, expected]) => {
test(input, () => {
const f = s => ImageReference.fromString(s);
if (expected.err) {
expect(() => f(input)).toThrow(expected.err);
} else {
expect(f(input)).toEqual(expected);
}
});
});

0 comments on commit 6010801

Please sign in to comment.