diff --git a/internal/imageref/imageref.go b/internal/imageref/imageref.go new file mode 100644 index 0000000..cd268b1 --- /dev/null +++ b/internal/imageref/imageref.go @@ -0,0 +1,43 @@ +// Package imageref splits container image references into repository, tag +// and digest. +package imageref + +import "strings" + +// Ref is an image reference split into its parts. Tag and Digest are empty +// when the reference has none. +type Ref struct { + Repo string + Tag string + Digest string +} + +// Parse splits ref into repository, tag and digest. A colon before the last +// "/" belongs to a registry host and port ("localhost:5000/foo"), not a tag. +func Parse(ref string) Ref { + var r Ref + if at := strings.Index(ref, "@"); at >= 0 { + r.Digest = ref[at+1:] + ref = ref[:at] + } + slash := strings.LastIndex(ref, "/") + last := ref[slash+1:] + if colon := strings.Index(last, ":"); colon >= 0 { + r.Tag = last[colon+1:] + ref = ref[:slash+1+colon] + } + r.Repo = ref + return r +} + +// String reassembles the reference as repo[:tag][@digest]. +func (r Ref) String() string { + s := r.Repo + if r.Tag != "" { + s += ":" + r.Tag + } + if r.Digest != "" { + s += "@" + r.Digest + } + return s +} diff --git a/internal/imageref/imageref_test.go b/internal/imageref/imageref_test.go new file mode 100644 index 0000000..c86414b --- /dev/null +++ b/internal/imageref/imageref_test.go @@ -0,0 +1,29 @@ +package imageref + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParse(t *testing.T) { + cases := []struct { + in string + want Ref + }{ + {"postgres", Ref{Repo: "postgres"}}, + {"postgres:17.10", Ref{Repo: "postgres", Tag: "17.10"}}, + {"gitea/gitea:1.27.3@sha256:87a6", Ref{Repo: "gitea/gitea", Tag: "1.27.3", Digest: "sha256:87a6"}}, + {"lscr.io/linuxserver/jellyfin@sha256:0f42", Ref{Repo: "lscr.io/linuxserver/jellyfin", Digest: "sha256:0f42"}}, + {"localhost:5000/foo", Ref{Repo: "localhost:5000/foo"}}, + {"localhost:5000/foo:v1", Ref{Repo: "localhost:5000/foo", Tag: "v1"}}, + {"localhost:5000/foo:v1@sha256:ab", Ref{Repo: "localhost:5000/foo", Tag: "v1", Digest: "sha256:ab"}}, + } + for _, c := range cases { + t.Run(c.in, func(t *testing.T) { + got := Parse(c.in) + assert.Equal(t, c.want, got) + assert.Equal(t, c.in, got.String(), "String must round-trip") + }) + } +}