Files
Alano Terblanche 0c2d0f700c vendor: github.com/docker/secrets-engine/client v0.2.2
Add the Docker secrets engine client, used to read the Docker Desktop session for Docker Hub. It brings in github.com/docker/secrets-engine/x v0.8.5 and connectrpc.com/connect v1.19.1.

The client module requires go1.26.10, which raises the go directive in vendor.mod from 1.26.0.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2026-10-22 11:10:40 +02:00

296 lines
8.0 KiB
Go

// Copyright 2025-2026 Docker, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secrets
import (
"errors"
"fmt"
"strings"
)
var ErrInvalidPattern = errors.New("invalid pattern")
// validPattern reports whether s is a valid pattern: non-empty '/'-separated
// components of A-Z, a-z, 0-9, '.', '-', '_', ':', where a component may
// instead be '*' or '**'.
func validPattern(s string) bool {
if len(s) == 0 {
return false
}
componentLen := 0
wildcardLen := 0
for _, r := range s {
switch {
case r == '/':
if !isValidComponentMatcher(componentLen, wildcardLen) {
return false
}
componentLen = 0
wildcardLen = 0
case isValidPatternRune(r):
componentLen++
if r == '*' {
wildcardLen++
}
default:
return false
}
}
// Final component
return isValidComponentMatcher(componentLen, wildcardLen)
}
func isValidComponentMatcher(componentLen, wildcardLen int) bool {
if wildcardLen > 2 {
// No more than two wildcards per component
return false
}
if wildcardLen > 0 && wildcardLen != componentLen {
// Wildcard can't be mixed with other characters in the same component
return false
}
// Component must not be empty
return componentLen > 0
}
func isValidPatternRune(c rune) bool {
return isValidRune(c) || c == '*'
}
// Pattern matches secret IDs. It follows the ID validation rules, except
// that '*' matches one component and '**' matches zero or more.
// Below, matches(p) denotes the set of IDs a pattern p matches.
type Pattern interface {
// Match reports whether the pattern matches id.
// Complexity: O(n*m^k), where id has n components and the pattern has
// m components and k occurrences of '**'.
Match(id ID) bool
// Contains reports whether every ID that [other] matches can also be
// matched by the pattern: the set of IDs [other] matches is contained
// in the set the pattern matches, i.e., matches(other) ⊆ matches(p).
//
// Examples:
// - docker/** contains docker/*/mcp/*: '**' is more general than
// '*/mcp/*'.
// - docker/proj1/** does not contain docker/*/mcp/*: docker/*/mcp/*
// matches docker/proj2/mcp/x, but docker/proj1/** does not.
//
// Complexity: O(n*m)
Contains(other Pattern) bool
// Overlaps reports whether the pattern and [other] match at least one
// ID in common, i.e., matches(p) ∩ matches(other) ≠ ∅.
//
// Examples:
// - docker/*/mcp/* and docker/proj1/** overlap: both match e.g. docker/proj1/mcp/x.
// - bar/** and foo/** do not overlap: an ID cannot begin with both bar and foo.
//
// Complexity: O(n*m)
Overlaps(other Pattern) bool
// String returns the pattern text.
String() string
ExpandID(other ID) (ID, error)
ExpandPattern(other Pattern) (Pattern, error)
}
type pattern string
func (p pattern) Match(id ID) bool {
pathParts := split(id.String())
patternParts := split(string(p))
return match(patternParts, pathParts)
}
func (p pattern) Contains(other Pattern) bool {
return covers(canonicalize(string(p)), canonicalize(other.String()))
}
func (p pattern) Overlaps(other Pattern) bool {
return compatible(canonicalize(string(p)), canonicalize(other.String()))
}
type tokenKind uint8
const (
tokenLit tokenKind = iota
tokenStar
tokenGap
)
type token struct {
kind tokenKind
lit string
}
// canonicalize tokenizes a pattern so that two patterns match the same set
// of IDs exactly when their canonical tokens are equal. Two rewrites give
// that property:
//
// - In a run of consecutive wildcard components, the '*' move to the
// front and the '**' merge into one at the end: order within a run
// does not affect what it matches, and one '**' already matches any
// number of components.
// - A pattern consisting only of '**' becomes "*/**": an ID always has
// at least one component, so both match the same IDs, and the first
// rewrite already turns the equivalent spelling "**/*" into "*/**".
//
// Examples:
//
// a/a/** and a/a/**/** -> [a a **]
// a/**/*/**/b -> [a * ** b]
// ** and **/* -> [* **]
func canonicalize(s string) []token {
parts := split(s)
toks := make([]token, 0, len(parts))
stars, gap := 0, false
flush := func() {
for range stars {
toks = append(toks, token{kind: tokenStar})
}
if gap {
toks = append(toks, token{kind: tokenGap})
}
stars, gap = 0, false
}
for _, part := range parts {
switch part {
case "*":
stars++
case "**":
gap = true
default:
flush()
toks = append(toks, token{kind: tokenLit, lit: part})
}
}
flush()
if len(toks) == 1 && toks[0].kind == tokenGap {
toks = []token{{kind: tokenStar}, {kind: tokenGap}}
}
return toks
}
// covers reports whether p matches every ID q matches, for canonical tokens.
// The component alphabet is unbounded, so a literal in p never covers an '*'
// or '**' in q.
func covers(p, q []token) bool {
np, nq := len(p), len(q)
prev := make([]bool, nq+1)
cur := make([]bool, nq+1)
prev[nq] = true
for i := np - 1; i >= 0; i-- {
cur[nq] = p[i].kind == tokenGap && prev[nq]
for j := nq - 1; j >= 0; j-- {
switch {
case p[i].kind == tokenGap:
cur[j] = prev[j] || cur[j+1]
case q[j].kind == tokenGap:
cur[j] = p[i].kind == tokenStar && cur[j+1] && prev[j]
case p[i].kind == tokenStar:
cur[j] = prev[j+1]
default:
cur[j] = q[j].kind == tokenLit && p[i].lit == q[j].lit && prev[j+1]
}
}
prev, cur = cur, prev
}
return prev[0]
}
// compatible reports whether p and q match at least one ID in common, for
// canonical tokens. A common ID aligns both token lists over its
// components: wildcards accept anything, so only differing literals or
// mismatched lengths (* vs */foo) rule one out.
func compatible(p, q []token) bool {
np, nq := len(p), len(q)
prev := make([]bool, nq+1)
cur := make([]bool, nq+1)
prev[nq] = true
for j := nq - 1; j >= 0; j-- {
prev[j] = q[j].kind == tokenGap && prev[j+1]
}
for i := np - 1; i >= 0; i-- {
cur[nq] = p[i].kind == tokenGap && prev[nq]
for j := nq - 1; j >= 0; j-- {
switch {
case p[i].kind == tokenGap || q[j].kind == tokenGap:
cur[j] = prev[j] || cur[j+1]
case p[i].kind == tokenLit && q[j].kind == tokenLit && p[i].lit != q[j].lit:
cur[j] = false
default:
cur[j] = prev[j+1]
}
}
prev, cur = cur, prev
}
return prev[0]
}
func (p pattern) String() string {
return string(p)
}
// ParsePattern parses s into a [Pattern]: non-empty '/'-separated components
// of A-Z, a-z, 0-9, '.', '-', '_', ':', where a component may instead be '*'
// or '**'. It returns [ErrInvalidPattern] for anything else.
func ParsePattern(s string) (Pattern, error) {
if !validPattern(s) {
return nil, ErrInvalidPattern
}
return pattern(s), nil
}
// MustParsePattern is like [ParsePattern] but panics on an invalid pattern.
func MustParsePattern(s string) Pattern {
if !validPattern(s) {
panic(ErrInvalidPattern)
}
return pattern(s)
}
func (p pattern) ExpandID(other ID) (ID, error) {
val, err := replace1(string(p), other.String())
if err != nil {
return nil, err
}
return ParseID(val)
}
func (p pattern) ExpandPattern(other Pattern) (Pattern, error) {
val, err := replace1(string(p), other.String())
if err != nil {
return nil, err
}
return pattern(val), err
}
func replace1(original, other string) (string, error) {
components := split(original)
var candidates []int
for idx, val := range components {
if val == "**" {
candidates = append(candidates, idx)
}
}
if len(candidates) != 1 {
return "", fmt.Errorf("expand only supports one expansion glob, pattern %s has %d", original, len(candidates))
}
components[candidates[0]] = other
return strings.Join(components, "/"), nil
}