Compare commits

..

3 Commits

Author SHA1 Message Date
Miloslav Trmač
48a05d71f0 Merge pull request #2767 from lsm5/release-1.21-packit-backport
[release-1.21] Packit: use `post-modifications` hook to update downstream TMT plan
2025-12-08 19:15:56 +01:00
Lokesh Mandvekar
dec587b480 Packit: use post-modifications hook to update downstream TMT plan
`prepare-files` action was interfering with spec file update which caused
https://github.com/containers/skopeo/issues/2760 .

`post-modifications` needs to be limited to the propose_downstream job or
else it will interfere with upstream PR copr builds.

Also, s/PACKIT_PROJECT_TAG/PACKIT_PROJECT_VERSION/ .

Co-authored-by: Nikola Forró <nforro@redhat.com>
Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>

(cherry picked from commit e26a4237fc)
Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
2025-12-08 09:03:51 -05:00
Miloslav Trmač
8bd9c541f0 Bump to 1.21.0
- New support for creating "simple signing" signatures using Sequoia-PGP,
  dependent on a build tag that enables it
- New option (skopeo copy --force-compression-format)
- New option --user-agent-prefix
- TLS options on the command line of (skopeo sync) take precedence
  over options in YAML

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2025-11-28 11:28:17 +01:00
956 changed files with 74524 additions and 45728 deletions

View File

@@ -157,7 +157,7 @@ ostree-rs-ext_task:
dockerfile: contrib/cirrus/ostree_ext.dockerfile
docker_arguments: # required build-args
BASE_FQIN: quay.io/coreos-assembler/fcos-buildroot:testing-devel
CIRRUS_IMAGE_VERSION: 4
CIRRUS_IMAGE_VERSION: 3
env:
EXT_REPO_NAME: ostree-rs-ext
EXT_REPO_HOME: $CIRRUS_WORKING_DIR/../$EXT_REPO_NAME

View File

@@ -1,9 +1,4 @@
version: "2"
formatters:
enable:
- gofumpt
linters:
settings:
staticcheck:

View File

@@ -90,8 +90,7 @@ jobs:
# Tests on ELN for main branch
- job: tests
# FIXME: https://github.com/containers/skopeo/issues/2748
trigger: ignore
trigger: pull_request
packages: [skopeo-eln]
notifications: *test_failure_notification
targets: *eln_copr_targets

View File

@@ -29,7 +29,7 @@ SEQUOIA_SONAME_DIR =
# N/B: This value is managed by Renovate, manual changes are
# possible, as long as they don't disturb the formatting
# (i.e. DO NOT ADD A 'v' prefix!)
GOLANGCI_LINT_VERSION := 2.8.0
GOLANGCI_LINT_VERSION := 2.6.1
ifeq ($(GOBIN),)
GOBIN := $(GOPATH)/bin
@@ -242,13 +242,10 @@ validate:
# This target is only intended for development, e.g. executing it from an IDE. Use (make test) for CI or pre-release testing.
test-all-local: validate-local validate-docs test-unit-local
.PHONY: fmt
fmt: tools
$(GOBIN)/golangci-lint fmt
.PHONY: validate-local
validate-local: tools
hack/validate-git-marks.sh
hack/validate-gofmt.sh
$(GOBIN)/golangci-lint run --build-tags "${BUILDTAGS}"
# An extra run with --tests=false allows detecting code unused outside of tests;
# ideally the linter should be able to find this automatically.

View File

@@ -45,8 +45,7 @@ func copyCmd(global *globalOptions) *cobra.Command {
destFlags, destOpts := imageDestFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "dest-", "dcreds")
retryFlags, retryOpts := retryFlags()
copyFlags, copyOpts := sharedCopyFlags()
opts := copyOptions{
global: global,
opts := copyOptions{global: global,
deprecatedTLSVerify: deprecatedTLSVerifyOpt,
srcImage: srcOpts,
destImage: destOpts,
@@ -252,7 +251,7 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) {
if err != nil {
return err
}
if err = os.WriteFile(opts.digestFile, []byte(manifestDigest.String()), 0o644); err != nil {
if err = os.WriteFile(opts.digestFile, []byte(manifestDigest.String()), 0644); err != nil {
return fmt.Errorf("Failed to write digest to file %q: %w", opts.digestFile, err)
}
}

View File

@@ -79,10 +79,10 @@ func (opts *generateSigstoreKeyOptions) run(args []string, stdout io.Writer) err
return fmt.Errorf("Error generating key pair: %w", err)
}
if err := os.WriteFile(privateKeyPath, keys.PrivateKey, 0o600); err != nil {
if err := os.WriteFile(privateKeyPath, keys.PrivateKey, 0600); err != nil {
return fmt.Errorf("Error writing private key to %q: %w", privateKeyPath, err)
}
if err := os.WriteFile(pubKeyPath, keys.PublicKey, 0o644); err != nil {
if err := os.WriteFile(pubKeyPath, keys.PublicKey, 0644); err != nil {
return fmt.Errorf("Error writing private key to %q: %w", pubKeyPath, err)
}
fmt.Fprintf(stdout, "Key written to %q and %q\n", privateKeyPath, pubKeyPath)

View File

@@ -24,7 +24,7 @@ func TestGenerateSigstoreKey(t *testing.T) {
for _, suffix := range outputSuffixes {
dir := t.TempDir()
prefix := filepath.Join(dir, "prefix")
err := os.WriteFile(prefix+suffix, []byte{}, 0o600)
err := os.WriteFile(prefix+suffix, []byte{}, 0600)
require.NoError(t, err)
out, err := runSkopeo("generate-sigstore-key",
"--output-prefix", prefix, "--passphrase-file", "/dev/null",
@@ -37,7 +37,7 @@ func TestGenerateSigstoreKey(t *testing.T) {
for _, suffix := range outputSuffixes {
dir := t.TempDir()
nonDirectory := filepath.Join(dir, "nondirectory")
err := os.WriteFile(nonDirectory, []byte{}, 0o600)
err := os.WriteFile(nonDirectory, []byte{}, 0600)
require.NoError(t, err)
prefix := filepath.Join(dir, "prefix")
err = os.Symlink(filepath.Join(nonDirectory, "unaccessible"), prefix+suffix)
@@ -66,7 +66,7 @@ func TestGenerateSigstoreKey(t *testing.T) {
dir := t.TempDir()
prefix := filepath.Join(dir, "prefix")
passphraseFile := filepath.Join(dir, "passphrase")
err = os.WriteFile(passphraseFile, []byte("some passphrase"), 0o600)
err = os.WriteFile(passphraseFile, []byte("some passphrase"), 0600)
require.NoError(t, err)
out, err = runSkopeo("generate-sigstore-key",
"--output-prefix", prefix, "--passphrase-file", passphraseFile,
@@ -75,4 +75,5 @@ func TestGenerateSigstoreKey(t *testing.T) {
for _, suffix := range outputSuffixes {
assert.Contains(t, out, prefix+suffix)
}
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/containers/skopeo/cmd/skopeo/inspect"
"github.com/docker/distribution/registry/api/errcode"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -23,14 +22,13 @@ import (
)
type inspectOptions struct {
global *globalOptions
image *imageOptions
retryOpts *retry.Options
format string
raw bool // Output the raw manifest instead of parsing information about the image
config bool // Output the raw config blob instead of parsing information about the image
doNotListTags bool // Do not list all tags available in the same repository
manifestDigest digest.Algorithm // Algorithm to use for computing manifest digest
global *globalOptions
image *imageOptions
retryOpts *retry.Options
format string
raw bool // Output the raw manifest instead of parsing information about the image
config bool // Output the raw config blob instead of parsing information about the image
doNotListTags bool // Do not list all tags available in the same repository
}
func inspectCmd(global *globalOptions) *cobra.Command {
@@ -66,7 +64,6 @@ skopeo inspect --format "Name: {{.Name}} Digest: {{.Digest}}" docker://registry.
flags.BoolVar(&opts.config, "config", false, "output configuration")
flags.StringVarP(&opts.format, "format", "f", "", "Format the output to a Go template")
flags.BoolVarP(&opts.doNotListTags, "no-tags", "n", false, "Do not list the available tags from the repository in the output")
flags.Var(newAlgorithmValue(&opts.manifestDigest), "manifest-digest", "Algorithm to use for computing manifest digest (sha256, sha512); defaults to algorithm used in config digest")
return cmd
}
@@ -179,7 +176,7 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error)
LayersData: imgInspect.LayersData,
Env: imgInspect.Env,
}
outputData.Digest, err = manifestDigestFromManifest(rawManifest, img, opts.manifestDigest)
outputData.Digest, err = manifest.Digest(rawManifest)
if err != nil {
return fmt.Errorf("Error computing manifest digest: %w", err)
}
@@ -238,48 +235,3 @@ func (opts *inspectOptions) writeOutput(stdout io.Writer, data any) error {
defer rpt.Flush()
return rpt.Execute([]any{data})
}
func manifestDigestFromManifest(manifestBlob []byte, img types.Image, userAlgorithm digest.Algorithm) (digest.Digest, error) {
if userAlgorithm != "" {
if !userAlgorithm.Available() {
return "", fmt.Errorf("digest algorithm %q is not available", userAlgorithm)
}
return manifest.DigestWithAlgorithm(manifestBlob, userAlgorithm)
}
configInfo := img.ConfigInfo()
if configInfo.Digest != "" {
alg := configInfo.Digest.Algorithm()
if !alg.Available() {
return "", fmt.Errorf("config digest algorithm %q is not available", alg)
}
return manifest.DigestWithAlgorithm(manifestBlob, alg)
}
return manifest.Digest(manifestBlob)
}
type algorithmValue digest.Algorithm
func newAlgorithmValue(alg *digest.Algorithm) *algorithmValue {
return (*algorithmValue)(alg)
}
func (a *algorithmValue) Set(value string) error {
algorithm := digest.Algorithm(value)
*a = algorithmValue(algorithm)
if algorithm == "" {
*a = algorithmValue(digest.Canonical)
}
return nil
}
func (a *algorithmValue) String() string {
return digest.Algorithm(*a).String()
}
func (a *algorithmValue) Type() string {
return "algorithm"
}

View File

@@ -11,8 +11,8 @@ import (
// Tests the kinds of inputs allowed and expected to the command
func TestDockerRepositoryReferenceParser(t *testing.T) {
for _, test := range [][]string{
{"docker://myhost.com:1000/nginx"}, // no tag
{"docker://myhost.com/nginx"}, // no port or tag
{"docker://myhost.com:1000/nginx"}, //no tag
{"docker://myhost.com/nginx"}, //no port or tag
{"docker://somehost.com"}, // Valid default expansion
{"docker://nginx"}, // Valid default expansion
} {
@@ -31,8 +31,8 @@ func TestDockerRepositoryReferenceParser(t *testing.T) {
{"docker-daemon:myhost.com/someimage"},
{"docker://myhost.com:1000/nginx:foobar:foobar"}, // Invalid repository ref
{"docker://somehost.com:5000/"}, // no repo
{"docker://myhost.com:1000/nginx:latest"}, // tag not allowed
{"docker://myhost.com:1000/nginx@sha256:abcdef1234567890"}, // digest not allowed
{"docker://myhost.com:1000/nginx:latest"}, //tag not allowed
{"docker://myhost.com:1000/nginx@sha256:abcdef1234567890"}, //digest not allowed
} {
_, err := parseDockerRepositoryReference(test[0])
assert.Error(t, err, test[0])
@@ -41,8 +41,8 @@ func TestDockerRepositoryReferenceParser(t *testing.T) {
func TestDockerRepositoryReferenceParserDrift(t *testing.T) {
for _, test := range [][]string{
{"docker://myhost.com:1000/nginx", "myhost.com:1000/nginx"}, // no tag
{"docker://myhost.com/nginx", "myhost.com/nginx"}, // no port or tag
{"docker://myhost.com:1000/nginx", "myhost.com:1000/nginx"}, //no tag
{"docker://myhost.com/nginx", "myhost.com/nginx"}, //no port or tag
{"docker://somehost.com", "docker.io/library/somehost.com"}, // Valid default expansion
{"docker://nginx", "docker.io/library/nginx"}, // Valid default expansion
} {

View File

@@ -31,7 +31,6 @@ type globalOptions struct {
registriesConfPath string // Path to the "registries.conf" file
tmpDir string // Path to use for big temporary files
userAgentPrefix string // Prefix to add to the user agent string
requireSigned bool // Require any pulled image to be signed
}
// requireSubcommand returns an error if no sub command is provided
@@ -82,7 +81,6 @@ func createApp() (*cobra.Command, *globalOptions) {
rootCommand.PersistentFlags().BoolVar(&opts.debug, "debug", false, "enable debug output")
rootCommand.PersistentFlags().StringVar(&opts.policyPath, "policy", "", "Path to a trust policy file")
rootCommand.PersistentFlags().BoolVar(&opts.insecurePolicy, "insecure-policy", false, "run the tool without any policy check")
rootCommand.PersistentFlags().BoolVar(&opts.requireSigned, "require-signed", false, "require any pulled image to be signed")
rootCommand.PersistentFlags().StringVar(&opts.registriesDirPath, "registries.d", "", "use registry configuration files in `DIR` (e.g. for container signature storage)")
rootCommand.PersistentFlags().StringVar(&opts.overrideArch, "override-arch", "", "use `ARCH` instead of the architecture of the machine for choosing images")
rootCommand.PersistentFlags().StringVar(&opts.overrideOS, "override-os", "", "use `OS` instead of the running OS for choosing images")
@@ -137,9 +135,6 @@ func (opts *globalOptions) before(cmd *cobra.Command, args []string) error {
if opts.tlsVerify.Present() {
logrus.Warn("'--tls-verify' is deprecated, please set this on the specific subcommand")
}
if opts.insecurePolicy && opts.requireSigned {
return fmt.Errorf("--insecure-policy and --require-signed are mutually exclusive")
}
return nil
}
@@ -171,14 +166,7 @@ func (opts *globalOptions) getPolicyContext() (*signature.PolicyContext, error)
if err != nil {
return nil, err
}
pc, err := signature.NewPolicyContext(policy)
if err != nil {
return nil, err
}
if opts.requireSigned {
pc.RequireSignatureVerification(true)
}
return pc, nil
return signature.NewPolicyContext(policy)
}
// commandTimeoutContext returns a context.Context and a cancellation callback based on opts.

View File

@@ -10,7 +10,8 @@ import (
"go.podman.io/image/v5/manifest"
)
type manifestDigestOptions struct{}
type manifestDigestOptions struct {
}
func manifestDigestCmd() *cobra.Command {
var opts manifestDigestOptions

View File

@@ -61,7 +61,7 @@ func (opts *standaloneSignOptions) run(args []string, stdout io.Writer) error {
return fmt.Errorf("Error creating signature: %w", err)
}
if err := os.WriteFile(opts.output, signature, 0o644); err != nil {
if err := os.WriteFile(opts.output, signature, 0644); err != nil {
return fmt.Errorf("Error writing signature to %s: %w", opts.output, err)
}
return nil
@@ -118,6 +118,7 @@ func (opts *standaloneVerifyOptions) run(args []string, stdout io.Writer) error
mech, publicKeyfingerprints, err = signature.NewEphemeralGPGSigningMechanism(publicKeys)
if err != nil {
return fmt.Errorf("Error initializing GPG: %w", err)
}
} else {
mech, err = signature.NewGPGSigningMechanism()
@@ -146,7 +147,8 @@ func (opts *standaloneVerifyOptions) run(args []string, stdout io.Writer) error
// (including things like “✅ Verified by $authority”)
//
// The subcommand is undocumented, and it may be renamed or entirely disappear in the future.
type untrustedSignatureDumpOptions struct{}
type untrustedSignatureDumpOptions struct {
}
func untrustedSignatureDumpCmd() *cobra.Command {
opts := untrustedSignatureDumpOptions{}

View File

@@ -182,7 +182,7 @@ func destinationReference(destination string, transport string) (types.ImageRefe
return nil, fmt.Errorf("Destination directory could not be used: %w", err)
}
// the directory holding the image must be created here
if err = os.MkdirAll(destination, 0o755); err != nil {
if err = os.MkdirAll(destination, 0755); err != nil {
return nil, fmt.Errorf("Error creating directory for image %s: %w", destination, err)
}
imageTransport = directory.Transport
@@ -270,6 +270,7 @@ func imagesToCopyFromDir(dirPath string) ([]types.ImageReference, error) {
}
return nil
})
if err != nil {
return sourceReferences,
fmt.Errorf("Error walking the path %q: %w", dirPath, err)
@@ -366,8 +367,7 @@ func imagesToCopyFromRegistry(registryName string, cfg registrySyncConfig, sourc
}
repoDescList = append(repoDescList, repoDescriptor{
ImageRefs: sourceReferences,
Context: serverCtx,
})
Context: serverCtx})
}
// include repository descriptors for cfg.ImagesByTagRegex
@@ -667,7 +667,7 @@ func (opts *syncOptions) run(args []string, stdout io.Writer) (retErr error) {
var digestFile *os.File
if opts.digestFile != "" && !opts.dryRun {
digestFile, err = os.OpenFile(opts.digestFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
digestFile, err = os.OpenFile(opts.digestFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("Error creating digest file: %w", err)
}

View File

@@ -49,8 +49,7 @@ func fakeGlobalOptions(t *testing.T, flags []string) (*globalOptions, *cobra.Com
// fakeImageOptions creates imageOptions and sets it according to globalFlags/cmdFlags.
func fakeImageOptions(t *testing.T, flagPrefix string, useDeprecatedTLSVerify bool,
globalFlags []string, cmdFlags []string,
) *imageOptions {
globalFlags []string, cmdFlags []string) *imageOptions {
globalOpts, cmd := fakeGlobalOptions(t, globalFlags)
sharedFlags, sharedOpts := sharedImageFlags()
var deprecatedTLSVerifyFlag pflag.FlagSet
@@ -125,8 +124,7 @@ func TestImageOptionsNewSystemContext(t *testing.T) {
// fakeImageDestOptions creates imageDestOptions and sets it according to globalFlags/cmdFlags.
func fakeImageDestOptions(t *testing.T, flagPrefix string, useDeprecatedTLSVerify bool,
globalFlags []string, cmdFlags []string,
) *imageDestOptions {
globalFlags []string, cmdFlags []string) *imageDestOptions {
globalOpts, cmd := fakeGlobalOptions(t, globalFlags)
sharedFlags, sharedOpts := sharedImageFlags()
var deprecatedTLSVerifyFlag pflag.FlagSet
@@ -391,8 +389,7 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) {
// to create test keys for that.
// This does not test --sign-by-sq-fingerprint, because that needs to be conditional based on buildWithSequoia.
{
options: []string{
"--remove-signatures",
options: []string{"--remove-signatures",
"--sign-by", "gpgFingerprint",
"--format", "oci",
"--preserve-digests",
@@ -491,31 +488,21 @@ func TestParseManifestFormat(t *testing.T) {
expectedManifestType string
expectErr bool
}{
{
"oci",
{"oci",
imgspecv1.MediaTypeImageManifest,
false,
},
{
"v2s1",
false},
{"v2s1",
manifest.DockerV2Schema1SignedMediaType,
false,
},
{
"v2s2",
false},
{"v2s2",
manifest.DockerV2Schema2MediaType,
false,
},
{
false},
{"",
"",
true},
{"badValue",
"",
true,
},
{
"badValue",
"",
true,
},
true},
} {
manifestType, err := parseManifestFormat(testCase.formatParam)
if testCase.expectErr {
@@ -536,37 +523,28 @@ func TestImageOptionsAuthfileOverride(t *testing.T) {
expectedAuthfilePath string
}{
// if there is no prefix, only authfile is allowed.
{
"",
{"",
[]string{
"--authfile", "/srv/authfile",
},
"/srv/authfile",
},
}, "/srv/authfile"},
// if authfile and dest-authfile is provided, dest-authfile wins
{
"dest-",
{"dest-",
[]string{
"--authfile", "/srv/authfile",
"--dest-authfile", "/srv/dest-authfile",
},
"/srv/dest-authfile",
}, "/srv/dest-authfile",
},
// if only the shared authfile is provided, authfile must be present in system context
{
"dest-",
{"dest-",
[]string{
"--authfile", "/srv/authfile",
},
"/srv/authfile",
}, "/srv/authfile",
},
// if only the dest authfile is provided, dest-authfile must be present in system context
{
"dest-",
{"dest-",
[]string{
"--dest-authfile", "/srv/dest-authfile",
},
"/srv/dest-authfile",
}, "/srv/dest-authfile",
},
} {
opts := fakeImageOptions(t, testCase.flagPrefix, false, []string{}, testCase.cmdFlags)

View File

@@ -214,7 +214,7 @@ Precompute digests to ensure layers are not uploaded that already exist on the d
**--retry-times**
The number of times to retry. By default, no retries are attempted.
The number of times to retry.
**--retry-delay**

View File

@@ -70,7 +70,7 @@ Bearer token for accessing the registry.
**--retry-times**
The number of times to retry. By default, no retries are attempted.
The number of times to retry.
**--retry-delay**

View File

@@ -69,7 +69,7 @@ Registry token for accessing the registry.
**--retry-times**
The number of times to retry. By default, no retries are attempted.
The number of times to retry.
**--retry-delay**
@@ -95,12 +95,6 @@ The password to access the registry.
Do not list the available tags from the repository in the output. When `true`, the `RepoTags` array will be empty. Defaults to `false`, which includes all available tags.
**--manifest-digest**=_algorithm_ **EXPERIMENTAL**
Algorithm to use for computing manifest digest (sha256, sha512); defaults to algorithm used in config digest.
**Note:** This flag is experimental and its behavior may change in future releases.
## EXAMPLES
To review information for the image fedora from the docker.io registry:
@@ -192,12 +186,6 @@ $ /bin/skopeo inspect --format '{{ .Env }}' docker://registry.access.redhat.com/
[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin container=oci]
```
To get the digest using a specific algorithm:
```console
$ skopeo inspect --manifest-digest=sha512 docker://docker.io/library/alpine:latest --format "Digest: {{.Digest}}"
Digest: sha512:5acb33fb56a7791bf0c69d5b19a1c70272148e4107be5261d57305d14e9509792bbca53e5277c456181ecfa1c20ad8427f9b8ba46868020584a819de1128dbd2
```
# SEE ALSO
skopeo(1), skopeo-login(1), docker-login(1), containers-auth.json(5)

View File

@@ -43,7 +43,7 @@ Bearer token for accessing the registry.
**--retry-times**
The number of times to retry. By default, no retries are attempted.
The number of times to retry.
**--retry-delay**

View File

@@ -134,7 +134,7 @@ Only the first line will be read. A passphrase stored in a file is of questionab
**--retry-times**
The number of times to retry. By default, no retries are attempted.
The number of times to retry.
**--retry-delay**

View File

@@ -92,10 +92,6 @@ Path to a policy.json file to use for verifying signatures and deciding whether
Use registry configuration files in _dir_ (e.g. for container signature storage), overriding the default path.
**--require-signed**
Require that any pulled image must be signed regardless of what the default or provided trust policy file says.
**--tmpdir** _dir_
Directory used to store temporary files. Defaults to /var/tmp.

98
go.mod
View File

@@ -1,9 +1,7 @@
module github.com/containers/skopeo
// Minimum required golang version
go 1.24.6
toolchain go1.24.10
go 1.24.2
// Warning: Ensure the "go" and "toolchain" versions match exactly to prevent unwanted auto-updates
@@ -15,41 +13,39 @@ require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.2-0.20251016170850-26647a49f642
github.com/opencontainers/image-tools v1.0.0-rc3
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
go.podman.io/common v0.66.2-0.20260202154637-0e2aefda57c9
go.podman.io/image/v5 v5.38.1-0.20260202154637-0e2aefda57c9
go.podman.io/storage v1.61.1-0.20260202154637-0e2aefda57c9
golang.org/x/term v0.39.0
go.podman.io/common v0.66.0
go.podman.io/image/v5 v5.38.0
go.podman.io/storage v1.61.0
golang.org/x/term v0.36.0
gopkg.in/yaml.v3 v3.0.1
)
require (
cyphar.com/go-pathrs v0.2.1 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.17.0 // indirect
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/coreos/go-oidc/v3 v3.16.0 // indirect
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/docker v28.5.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.4 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
@@ -62,51 +58,53 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.33 // indirect
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mistifyio/go-zfs/v4 v4.0.0 // indirect
github.com/mistifyio/go-zfs/v3 v3.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.53.0 // indirect
github.com/moby/moby/client v0.2.2 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/image-spec/schema v0.0.0-20250717171153-ab80ff15c2dd // indirect
github.com/opencontainers/runtime-spec v1.3.0 // indirect
github.com/opencontainers/selinux v1.13.1 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/opencontainers/runtime-spec v1.2.1 // indirect
github.com/opencontainers/selinux v1.12.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/proglottis/gpgme v0.1.6 // indirect
github.com/proglottis/gpgme v0.1.5 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect
github.com/sigstore/fulcio v1.8.1 // indirect
github.com/sigstore/protobuf-specs v0.5.0 // indirect
github.com/sigstore/sigstore v1.9.6-0.20251111174640-d8ab8afb1326 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/sigstore/fulcio v1.7.1 // indirect
github.com/sigstore/protobuf-specs v0.4.1 // indirect
github.com/sigstore/sigstore v1.9.5 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/smallstep/pkcs7 v0.1.1 // indirect
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect
github.com/sylabs/sif/v2 v2.22.0 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/vbatts/tar-split v0.12.2 // indirect
github.com/vbauerster/mpb/v8 v8.11.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect
github.com/vbauerster/mpb/v8 v8.10.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
google.golang.org/grpc v1.72.2 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

286
go.sum
View File

@@ -1,9 +1,9 @@
cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@@ -12,27 +12,31 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=
github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE=
github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM=
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA=
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM=
github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ=
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -41,25 +45,29 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI=
github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -69,6 +77,8 @@ github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -87,6 +97,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
@@ -97,11 +109,13 @@ github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVU
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -109,40 +123,47 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2TSgRbAhD7yjZzTQmcN25sDRPEeinR51yQ=
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs/v4 v4.0.0 h1:sU0+5dX45tdDK5xNZ3HBi95nxUc48FS92qbIZEvpAg4=
github.com/mistifyio/go-zfs/v4 v4.0.0/go.mod h1:weotFtXTHvBwhr9Mv96KYnDkTPBOHFUbm9cBmQpesL0=
github.com/mistifyio/go-zfs/v3 v3.1.0 h1:FZaylcg0hjUp27i23VcJJQiuBeAZjrC8lPqCGM1CopY=
github.com/mistifyio/go-zfs/v3 v3.1.0/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=
github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.27.5 h1:ZeVgZMx2PDMdJm/+w5fE/OyG6ILo1Y3e+QX4zSR0zTE=
github.com/onsi/ginkgo/v2 v2.27.5/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE=
github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.2-0.20251016170850-26647a49f642 h1:BNZwTO1e0QJV7HVGz/Qw/tyOE/GnooRmuy6qZnhNGCE=
@@ -151,43 +172,56 @@ github.com/opencontainers/image-spec/schema v0.0.0-20250717171153-ab80ff15c2dd h
github.com/opencontainers/image-spec/schema v0.0.0-20250717171153-ab80ff15c2dd/go.mod h1:vPOv9cXqxB6ycHY5iVwqL4rkYbwRh46GZj13CfkZ6As=
github.com/opencontainers/image-tools v1.0.0-rc3 h1:ZR837lBIxq6mmwEqfYrbLMuf75eBSHhccVHy6lsBeM4=
github.com/opencontainers/image-tools v1.0.0-rc3/go.mod h1:A9btVpZLzttF4iFaKNychhPyrhfOjJ1OF5KrA8GcLj4=
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/proglottis/gpgme v0.1.6 h1:8WpQ8VWggLdxkuTnW+sZ1r1t92XBNd8GZNDhQ4Rz+98=
github.com/proglottis/gpgme v0.1.6/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/proglottis/gpgme v0.1.5 h1:KCGyOw8sQ+SI96j6G8D8YkOGn+1TwbQTT9/zQXoVlz0=
github.com/proglottis/gpgme v0.1.5/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/secure-systems-lab/go-securesystemslib v0.10.0 h1:l+H5ErcW0PAehBNrBxoGv1jjNpGYdZ9RcheFkB2WI14=
github.com/secure-systems-lab/go-securesystemslib v0.10.0/go.mod h1:MRKONWmRoFzPNQ9USRF9i1mc7MvAVvF1LlW8X5VWDvk=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sigstore/fulcio v1.8.1 h1:PmoQv3XmhjR2BWFWw5LcMUXJPmhyizOIL7HeYnpio58=
github.com/sigstore/fulcio v1.8.1/go.mod h1:7tP3KW9eCGlPYRj5N4MSuUOat7CkeIHuXZ2jAUQ+Rwc=
github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY=
github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
github.com/sigstore/sigstore v1.9.6-0.20251111174640-d8ab8afb1326 h1:s39MsSDVn8LhePV5adidcOjjKHaplLxpHM1mvbC24l4=
github.com/sigstore/sigstore v1.9.6-0.20251111174640-d8ab8afb1326/go.mod h1:xSCb7eki7lCdi+mNh4I4MVpKPP2cWGtDYmSPPmX/K70=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g=
github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sigstore/fulcio v1.7.1 h1:RcoW20Nz49IGeZyu3y9QYhyyV3ZKQ85T+FXPKkvE+aQ=
github.com/sigstore/fulcio v1.7.1/go.mod h1:7lYY+hsd8Dt+IvKQRC+KEhWpCZ/GlmNvwIa5JhypMS8=
github.com/sigstore/protobuf-specs v0.4.1 h1:5SsMqZbdkcO/DNHudaxuCUEjj6x29tS2Xby1BxGU7Zc=
github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
github.com/sigstore/sigstore v1.9.5 h1:Wm1LT9yF4LhQdEMy5A2JeGRHTrAWGjT3ubE5JUSrGVU=
github.com/sigstore/sigstore v1.9.5/go.mod h1:VtxgvGqCmEZN9X2zhFSOkfXxvKUjpy8RpUW39oCtoII=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU=
github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -195,19 +229,22 @@ github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLy
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/sylabs/sif/v2 v2.22.0 h1:Y+xXufp4RdgZe02SR3nWEg7S6q4tPWN237WHYzkDSKA=
github.com/sylabs/sif/v2 v2.22.0/go.mod h1:W1XhWTmG1KcG7j5a3KSYdMcUIFvbs240w/MMVW627hs=
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vbauerster/mpb/v8 v8.11.3 h1:iniBmO4ySXCl4gVdmJpgrtormH5uvjpxcx/dMyVU9Jw=
github.com/vbauerster/mpb/v8 v8.11.3/go.mod h1:n9M7WbP0NFjpgKS5XdEC3tMRgZTNM/xtC8zWGkiMuy0=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vbauerster/mpb/v8 v8.10.2 h1:2uBykSHAYHekE11YvJhKxYmLATKHAGorZwFlyNw4hHM=
github.com/vbauerster/mpb/v8 v8.10.2/go.mod h1:+Ja4P92E3/CorSZgfDtK46D7AVbDqmBQRTmyTqPElo0=
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
@@ -219,26 +256,34 @@ github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3R
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.podman.io/common v0.66.2-0.20260202154637-0e2aefda57c9 h1:iLJYwHeJ548eWlrOgFFnE2QW7A4SHgTHjw+4QJqJ0kY=
go.podman.io/common v0.66.2-0.20260202154637-0e2aefda57c9/go.mod h1:+4bMKfBbfK+qgURkCj6vUtghP5ASjsWyYDI/udLOKxk=
go.podman.io/image/v5 v5.38.1-0.20260202154637-0e2aefda57c9 h1:RFtNtYD33WvYJKAoCzONX2AjP7Ey1MtikfKfJ+dcWCk=
go.podman.io/image/v5 v5.38.1-0.20260202154637-0e2aefda57c9/go.mod h1:imQIBRN6114qH01ttrueVkVCHj28jhsiN7Yubh0CzGc=
go.podman.io/storage v1.61.1-0.20260202154637-0e2aefda57c9 h1:ab5KO2VjxG/VsARN5gBsQoCuQvJr1MYSYf50hpn1ROI=
go.podman.io/storage v1.61.1-0.20260202154637-0e2aefda57c9/go.mod h1:yuLB1ikwsdGrGqSGBWv7fMbOeHupCaMn5iJ1biqxrpI=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.podman.io/common v0.66.0 h1:KElE3HKLFdMdJL+jv5ExBiX2Dh4Qcv8ovmzaBGRsyZM=
go.podman.io/common v0.66.0/go.mod h1:aNd2a0S7pY+fx1X5kpQYuF4hbwLU8ZOccuVrhu7h1Xc=
go.podman.io/image/v5 v5.38.0 h1:aUKrCANkPvze1bnhLJsaubcfz0d9v/bSDLnwsXJm6G4=
go.podman.io/image/v5 v5.38.0/go.mod h1:hSIoIUzgBnmc4DjoIdzk63aloqVbD7QXDMkSE/cvG90=
go.podman.io/storage v1.61.0 h1:5hD/oyRYt1f1gxgvect+8syZBQhGhV28dCw2+CZpx0Q=
go.podman.io/storage v1.61.0/go.mod h1:A3UBK0XypjNZ6pghRhuxg62+2NIm5lcUGv/7XyMhMUI=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -247,15 +292,15 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -264,10 +309,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -275,22 +320,22 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -300,8 +345,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -311,34 +356,33 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY=
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=

27
hack/validate-gofmt.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
IFS=$'\n'
files=( $(find . -name '*.go' | grep -v '^./vendor/' | sort || true) )
unset IFS
badFiles=()
for f in "${files[@]}"; do
if [ "$(gofmt -s -l < $f)" ]; then
badFiles+=( "$f" )
fi
done
if [ ${#badFiles[@]} -eq 0 ]; then
echo 'Congratulations! All Go source files are properly formatted.'
else
{
echo "These files are not properly gofmt'd:"
for f in "${badFiles[@]}"; do
echo " - $f"
done
echo
echo 'Please reformat the above files using "gofmt -s -w" and commit the result.'
echo
} >&2
exit 1
fi

View File

@@ -1,9 +1,7 @@
package main
const (
blockedRegistriesConf = "./fixtures/blocked-registries.conf"
blockedErrorRegex = `.*registry registry-blocked.com is blocked in .*`
)
const blockedRegistriesConf = "./fixtures/blocked-registries.conf"
const blockedErrorRegex = `.*registry registry-blocked.com is blocked in .*`
func (s *skopeoSuite) TestCopyBlockedSource() {
t := s.T()

View File

@@ -26,10 +26,8 @@ type skopeoSuite struct {
regV2WithAuth *testRegistryV2
}
var (
_ = suite.SetupAllSuite(&skopeoSuite{})
_ = suite.TearDownAllSuite(&skopeoSuite{})
)
var _ = suite.SetupAllSuite(&skopeoSuite{})
var _ = suite.TearDownAllSuite(&skopeoSuite{})
func (s *skopeoSuite) SetupSuite() {
t := s.T()

View File

@@ -12,7 +12,6 @@ import (
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
@@ -43,17 +42,14 @@ func TestCopy(t *testing.T) {
type copySuite struct {
suite.Suite
cluster *openshiftCluster
registry *testRegistryV2
s1Registry *testRegistryV2
gpgHome string
fingerprint string
cluster *openshiftCluster
registry *testRegistryV2
s1Registry *testRegistryV2
gpgHome string
}
var (
_ = suite.SetupAllSuite(&copySuite{})
_ = suite.TearDownAllSuite(&copySuite{})
)
var _ = suite.SetupAllSuite(&copySuite{})
var _ = suite.TearDownAllSuite(&copySuite{})
func (s *copySuite) SetupSuite() {
t := s.T()
@@ -89,15 +85,9 @@ func (s *copySuite) SetupSuite() {
out := combinedOutputOfCommand(t, gpgBinary, "--armor", "--export", fmt.Sprintf("%s@example.com", key))
err := os.WriteFile(filepath.Join(s.gpgHome, fmt.Sprintf("%s-pubkey.gpg", key)),
[]byte(out), 0o600)
[]byte(out), 0600)
require.NoError(t, err)
}
// Get fingerprint for the personal key (used by some tests)
lines, err := exec.Command(gpgBinary, "--homedir", s.gpgHome, "--with-colons", "--no-permission-warning", "--fingerprint", "personal@example.com").Output()
require.NoError(t, err)
s.fingerprint, err = findFingerprint(lines)
require.NoError(t, err)
}
func (s *copySuite) TearDownSuite() {
@@ -551,9 +541,9 @@ func (s *copySuite) TestCopyEncryption() {
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
require.NoError(t, err)
err = os.WriteFile(keysDir+"/private.key", privateKeyBytes, 0o644)
err = os.WriteFile(keysDir+"/private.key", privateKeyBytes, 0644)
require.NoError(t, err)
err = os.WriteFile(keysDir+"/public.key", publicKeyBytes, 0o644)
err = os.WriteFile(keysDir+"/public.key", publicKeyBytes, 0644)
require.NoError(t, err)
// We can either perform encryption or decryption on the image.
@@ -578,7 +568,7 @@ func (s *copySuite) TestCopyEncryption() {
invalidPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096)
require.NoError(t, err)
invalidPrivateKeyBytes := x509.MarshalPKCS1PrivateKey(invalidPrivateKey)
err = os.WriteFile(keysDir+"/invalid_private.key", invalidPrivateKeyBytes, 0o644)
err = os.WriteFile(keysDir+"/invalid_private.key", invalidPrivateKeyBytes, 0644)
require.NoError(t, err)
assertSkopeoFails(t, ".*no suitable key unwrapper found or none of the private keys could be used for decryption.*",
"copy", "--decryption-key", keysDir+"/invalid_private.key",
@@ -614,6 +604,7 @@ func (s *copySuite) TestCopyEncryption() {
// After successful decryption we should find the gzipped layers from the nginx image
matchLayerBlobBinaryType(t, partiallyDecryptedImgDir+"/blobs/sha256", "application/x-gzip", 3)
}
func matchLayerBlobBinaryType(t *testing.T, ociImageDirPath string, contentType string, matchCount int) {
@@ -789,10 +780,10 @@ func (s *copySuite) TestCopySignatures() {
// Verify that mis-signed images are rejected
assertSkopeoSucceeds(t, "", "--tls-verify=false", "copy", "atomic:localhost:5006/myns/personal:personal", "atomic:localhost:5006/myns/official:attack")
assertSkopeoSucceeds(t, "", "--tls-verify=false", "copy", "atomic:localhost:5006/myns/official:official", "atomic:localhost:5006/myns/personal:attack")
// "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" or "Missing key $fingerprint" by Sequoia.
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key).*",
// "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" by Sequoia.
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*",
"--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5006/myns/personal:attack", dirDest)
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key).*",
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*",
"--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5006/myns/official:attack", dirDest)
// Verify that signed identity is verified.
@@ -805,8 +796,8 @@ func (s *copySuite) TestCopySignatures() {
// Verify that cosigning requirements are enforced
assertSkopeoSucceeds(t, "", "--tls-verify=false", "copy", "atomic:localhost:5006/myns/official:official", "atomic:localhost:5006/myns/cosigned:cosigned")
// "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" or "Missing key $fingerprint" by Sequoia.
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key).*",
// "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" by Sequoia.
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*",
"--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5006/myns/cosigned:cosigned", dirDest)
assertSkopeoSucceeds(t, "", "--tls-verify=false", "copy", "--sign-by", "personal@example.com", "atomic:localhost:5006/myns/official:official", "atomic:localhost:5006/myns/cosigned:cosigned")
@@ -827,7 +818,7 @@ func (s *copySuite) TestCopyDirSignatures() {
topDirDest := "dir:" + topDir
for _, suffix := range []string{"/dir1", "/dir2", "/restricted/personal", "/restricted/official", "/restricted/badidentity", "/dest"} {
err := os.MkdirAll(topDir+suffix, 0o755)
err := os.MkdirAll(topDir+suffix, 0755)
require.NoError(t, err)
}
@@ -851,8 +842,8 @@ func (s *copySuite) TestCopyDirSignatures() {
// Verify that correct images are accepted
assertSkopeoSucceeds(t, "", "--policy", policy, "copy", topDirDest+"/restricted/official", topDirDest+"/dest")
// ... and that mis-signed images are rejected.
// "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" or "Missing key $fingerprint" by Sequoia.
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key).*",
// "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" by Sequoia.
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*",
"--policy", policy, "copy", topDirDest+"/restricted/personal", topDirDest+"/dest")
// Verify that the signed identity is verified.
@@ -909,7 +900,7 @@ func (s *copySuite) TestCopyCompression() {
{"uncompressed-image-s2", "atomic:localhost:5000/myns/compression:s2"},
} {
dir := filepath.Join(topDir, fmt.Sprintf("case%d", i))
err := os.MkdirAll(dir, 0o755)
err := os.MkdirAll(dir, 0755)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "--tls-verify=false", "copy", "dir:fixtures/"+c.fixture, c.remote)
@@ -962,7 +953,7 @@ func (s *copySuite) TestCopyDockerLookaside() {
tmpDir := t.TempDir()
copyDest := filepath.Join(tmpDir, "dest")
err = os.Mkdir(copyDest, 0o755)
err = os.Mkdir(copyDest, 0755)
require.NoError(t, err)
dirDest := "dir:" + copyDest
plainLookaside := filepath.Join(tmpDir, "lookaside")
@@ -976,7 +967,7 @@ func (s *copySuite) TestCopyDockerLookaside() {
policy := s.policyFixture(nil)
registriesDir := filepath.Join(tmpDir, "registries.d")
err = os.Mkdir(registriesDir, 0o755)
err = os.Mkdir(registriesDir, 0755)
require.NoError(t, err)
registriesFile := fileFromFixture(t, "fixtures/registries.yaml",
map[string]string{"@lookaside@": plainLookaside, "@split-staging@": splitLookasideStaging, "@split-read@": splitLookasideReadServer.URL})
@@ -1029,7 +1020,7 @@ func (s *copySuite) TestCopyAtomicExtension() {
topDir := t.TempDir()
for _, subdir := range []string{"dirAA", "dirAD", "dirDA", "dirDD", "registries.d"} {
err := os.MkdirAll(filepath.Join(topDir, subdir), 0o755)
err := os.MkdirAll(filepath.Join(topDir, subdir), 0755)
require.NoError(t, err)
}
registriesDir := filepath.Join(topDir, "registries.d")
@@ -1222,7 +1213,7 @@ func (s *copySuite) TestCopyPreserveDigests() {
func (s *copySuite) testCopySchemaConversionRegistries(t *testing.T, schema1Registry, schema2Registry string) {
topDir := t.TempDir()
for _, subdir := range []string{"input1", "input2", "dest2"} {
err := os.MkdirAll(filepath.Join(topDir, subdir), 0o755)
err := os.MkdirAll(filepath.Join(topDir, subdir), 0755)
require.NoError(t, err)
}
input1Dir := filepath.Join(topDir, "input1")
@@ -1293,87 +1284,3 @@ func (s *copySuite) TestCopyFailsWhenReferenceIsInvalid() {
t := s.T()
assertSkopeoFails(t, `.*Invalid image name.*`, "copy", "unknown:transport", "unknown:test")
}
func (s *copySuite) TestInsecurePolicyAndRequireSignedConflict() {
t := s.T()
assertSkopeoFails(t, ".*--insecure-policy and --require-signed are mutually exclusive.*",
"--insecure-policy", "--require-signed", "inspect", "dir:/nonexistent")
}
func (s *copySuite) TestRequireSignedAcceptsSignedImage() {
t := s.T()
mech, err := signature.NewGPGSigningMechanism()
require.NoError(t, err)
defer mech.Close()
if err := mech.SupportsSigning(); err != nil {
t.Skipf("Signing not supported: %v", err)
}
srcDir := t.TempDir()
// get an image to work with
assertSkopeoSucceeds(t, "", "copy", "--retry-times", "3",
testFQIN64, "dir:"+srcDir)
// first, sanity-check that without --require-signed, we can copy it since by default, `dir:` is insecureAcceptAnything
destDir1 := t.TempDir()
assertSkopeoSucceeds(t, "", "copy", "dir:"+srcDir, "dir:"+destDir1)
// now verify that copying fails with --require-signed
destDir2 := t.TempDir()
assertSkopeoFails(t, ".*Source image rejected: No signature verification policy found for image.*",
"--require-signed", "copy",
"dir:"+srcDir, "dir:"+destDir2)
// sign the image
manifestPath := filepath.Join(srcDir, "manifest.json")
signaturePath := filepath.Join(srcDir, "signature-1")
dockerReference := "localhost/test:latest"
assertSkopeoSucceeds(t, "", "standalone-sign",
"-o", signaturePath,
manifestPath, dockerReference, s.fingerprint)
// sanity-check signature file is there
_, err = os.Stat(signaturePath)
require.NoError(t, err)
// create a basic policy that requires signatures
policy := map[string]any{
"default": []map[string]any{{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": filepath.Join(s.gpgHome, "personal-pubkey.gpg"),
"signedIdentity": map[string]any{
"type": "exactRepository",
"dockerRepository": dockerReference,
},
}},
}
policyJSON, err := json.Marshal(policy)
require.NoError(t, err)
policyFile, err := os.CreateTemp("", "policy-*.json")
require.NoError(t, err)
t.Cleanup(func() { os.Remove(policyFile.Name()) })
_, err = policyFile.Write(policyJSON)
require.NoError(t, err)
err = policyFile.Close()
require.NoError(t, err)
// now copying with --require-signed should pass
destDir3 := t.TempDir()
assertSkopeoSucceeds(t, "", "--policy", policyFile.Name(), "--require-signed", "copy",
"dir:"+srcDir, "dir:"+destDir3)
// Delete the signature and sanity-check that copying fails. This doesn't
// strictly test --require-signed, but rather the PolicyRequirements logic, but
// it makes the test feel complete.
err = os.Remove(signaturePath)
require.NoError(t, err)
destDir4 := t.TempDir()
assertSkopeoFails(t, ".*Source image rejected: A signature was required, but no signature exists.*",
"--policy", policyFile.Name(), "--require-signed", "copy",
"dir:"+srcDir, "dir:"+destDir4)
}

View File

@@ -207,7 +207,7 @@ func (cluster *openshiftCluster) startRegistry(t *testing.T) {
cluster.processes = append(cluster.processes, cluster.startRegistryProcess(t, 5006, schema2Config))
}
// ocLoginToProject runs (oc login) and (oc new-project) on the cluster, or terminates on failure.
// ocLogin runs (oc login) and (oc new-project) on the cluster, or terminates on failure.
func (cluster *openshiftCluster) ocLoginToProject(t *testing.T) {
t.Logf("oc login")
cmd := cluster.clusterCmd(nil, "oc", "login", "--certificate-authority=openshift.local.config/master/ca.crt", "-u", "myuser", "-p", "mypw", "https://localhost:8443")
@@ -223,7 +223,7 @@ func (cluster *openshiftCluster) ocLoginToProject(t *testing.T) {
// We do not run (docker login) directly, because that requires a running daemon and a docker package.
func (cluster *openshiftCluster) dockerLogin(t *testing.T) {
cluster.dockerDir = filepath.Join(homedir.Get(), ".docker")
err := os.MkdirAll(cluster.dockerDir, 0o700)
err := os.MkdirAll(cluster.dockerDir, 0700)
require.NoError(t, err)
out := combinedOutputOfCommand(t, "oc", "config", "view", "-o", "json", "-o", "jsonpath={.users[*].user.token}")
@@ -237,7 +237,7 @@ func (cluster *openshiftCluster) dockerLogin(t *testing.T) {
}`, port, authValue))
}
configJSON := `{"auths": {` + strings.Join(auths, ",") + `}}`
err = os.WriteFile(filepath.Join(cluster.dockerDir, "config.json"), []byte(configJSON), 0o600)
err = os.WriteFile(filepath.Join(cluster.dockerDir, "config.json"), []byte(configJSON), 0600)
require.NoError(t, err)
}

View File

@@ -224,6 +224,7 @@ func (p *proxy) callGetRawBlob(args []any) (rval any, buf []byte, err error) {
content: buf,
err: err,
}
}()
wg.Add(1)
go func() {

View File

@@ -70,7 +70,7 @@ compatibility:
username = "testuser"
password = "testpassword"
email = "test@test.org"
if err := os.WriteFile(htpasswdPath, []byte(userpasswd), os.FileMode(0o644)); err != nil {
if err := os.WriteFile(htpasswdPath, []byte(userpasswd), os.FileMode(0644)); err != nil {
return nil, err
}
htpasswd = fmt.Sprintf(`auth:

View File

@@ -1,9 +1,12 @@
package main
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -26,6 +29,16 @@ type signingSuite struct {
var _ = suite.SetupAllSuite(&signingSuite{})
func findFingerprint(lineBytes []byte) (string, error) {
for line := range bytes.SplitSeq(lineBytes, []byte{'\n'}) {
fields := strings.Split(string(line), ":")
if len(fields) >= 10 && fields[0] == "fpr" {
return fields[9], nil
}
}
return "", errors.New("No fingerprint found")
}
func (s *signingSuite) SetupSuite() {
t := s.T()
_, err := exec.LookPath(skopeoBinary)

View File

@@ -46,10 +46,8 @@ type syncSuite struct {
registry *testRegistryV2
}
var (
_ = suite.SetupAllSuite(&syncSuite{})
_ = suite.TearDownAllSuite(&syncSuite{})
)
var _ = suite.SetupAllSuite(&syncSuite{})
var _ = suite.TearDownAllSuite(&syncSuite{})
func (s *syncSuite) SetupSuite() {
t := s.T()
@@ -94,7 +92,7 @@ func (s *syncSuite) SetupSuite() {
out := combinedOutputOfCommand(t, gpgBinary, "--armor", "--export", fmt.Sprintf("%s@example.com", key))
err := os.WriteFile(filepath.Join(gpgHome, fmt.Sprintf("%s-pubkey.gpg", key)),
[]byte(out), 0o600)
[]byte(out), 0600)
require.NoError(t, err)
}
}
@@ -228,16 +226,16 @@ func (s *syncSuite) TestDirIsNotOverwritten() {
// make a copy of the image in the local registry
assertSkopeoSucceeds(t, "", "copy", "--retry-times", "3", "--dest-tls-verify=false", "docker://"+image, "docker://"+path.Join(v2DockerRegistryURL, reference.Path(imageRef.DockerReference())))
// sync upstream image to dir, not scoped
//sync upstream image to dir, not scoped
dir1 := t.TempDir()
assertSkopeoSucceeds(t, "", "sync", "--src", "docker", "--dest", "dir", image, dir1)
_, err = os.Stat(path.Join(dir1, path.Base(imagePath), "manifest.json"))
require.NoError(t, err)
// sync local registry image to dir, not scoped
//sync local registry image to dir, not scoped
assertSkopeoFails(t, ".*Refusing to overwrite destination directory.*", "sync", "--src-tls-verify=false", "--src", "docker", "--dest", "dir", path.Join(v2DockerRegistryURL, reference.Path(imageRef.DockerReference())), dir1)
// sync local registry image to dir, scoped
//sync local registry image to dir, scoped
imageRef, err = docker.ParseReference(fmt.Sprintf("//%s", path.Join(v2DockerRegistryURL, reference.Path(imageRef.DockerReference()))))
require.NoError(t, err)
imagePath = imageRef.DockerReference().String()
@@ -292,7 +290,7 @@ func (s *syncSuite) TestYamlUntagged() {
// sync to the local registry
yamlFile := path.Join(tmpDir, "registries.yaml")
err = os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
err = os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "yaml", "--dest", "docker", "--dest-tls-verify=false", yamlFile, v2DockerRegistryURL)
// sync back from local registry to a folder
@@ -304,7 +302,7 @@ func (s *syncSuite) TestYamlUntagged() {
%s: []
`, v2DockerRegistryURL, imagePath)
err = os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
err = os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
@@ -331,11 +329,11 @@ registry.k8s.io:
pause: ^[12]\.0$ # regex string test
`
// the ↑ regex strings always matches only 2 images
nTags := 2
var nTags = 2
assert.NotZero(t, nTags)
yamlFile := path.Join(tmpDir, "registries.yaml")
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
assertNumberOfManifestsInSubdirs(t, dir1, nTags)
@@ -353,7 +351,7 @@ registry.k8s.io:
- sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610
`
yamlFile := path.Join(tmpDir, "registries.yaml")
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
assertNumberOfManifestsInSubdirs(t, dir1, 1)
@@ -392,7 +390,7 @@ quay.io:
assert.NotZero(t, nTags)
yamlFile := path.Join(tmpDir, "registries.yaml")
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
assertNumberOfManifestsInSubdirs(t, dir1, nTags)
@@ -443,13 +441,14 @@ func (s *syncSuite) TestYamlTLSVerify() {
for _, cfg := range testCfg {
yamlConfig := fmt.Sprintf(yamlTemplate, v2DockerRegistryURL, cfg.tlsVerify, image, tag)
yamlFile := path.Join(tmpDir, "registries.yaml")
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
require.NoError(t, err)
cfg.checker(t, cfg.msg, "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
os.Remove(yamlFile)
os.RemoveAll(dir1)
}
}
func (s *syncSuite) TestSyncManifestOutput() {
@@ -460,7 +459,7 @@ func (s *syncSuite) TestSyncManifestOutput() {
destDir2 := filepath.Join(tmpDir, "dest2")
destDir3 := filepath.Join(tmpDir, "dest3")
// Split image:tag path from image URI for manifest comparison
//Split image:tag path from image URI for manifest comparison
imageDir := pullableTaggedImage[strings.LastIndex(pullableTaggedImage, "/")+1:]
assertSkopeoSucceeds(t, "", "sync", "--format=oci", "--all", "--src", "docker", "--dest", "dir", pullableTaggedImage, destDir1)
@@ -513,14 +512,14 @@ func (s *syncSuite) TestDir2DockerTagged() {
image := pullableRepoWithLatestTag
dir1 := path.Join(tmpDir, "dir1")
err := os.Mkdir(dir1, 0o755)
err := os.Mkdir(dir1, 0755)
require.NoError(t, err)
dir2 := path.Join(tmpDir, "dir2")
err = os.Mkdir(dir2, 0o755)
err = os.Mkdir(dir2, 0755)
require.NoError(t, err)
// create leading dirs
err = os.MkdirAll(path.Dir(path.Join(dir1, image)), 0o755)
err = os.MkdirAll(path.Dir(path.Join(dir1, image)), 0755)
require.NoError(t, err)
// copy docker => dir
@@ -532,7 +531,7 @@ func (s *syncSuite) TestDir2DockerTagged() {
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--dest-tls-verify=false", "--src", "dir", "--dest", "docker", dir1, v2DockerRegistryURL)
// create leading dirs
err = os.MkdirAll(path.Dir(path.Join(dir2, image)), 0o755)
err = os.MkdirAll(path.Dir(path.Join(dir2, image)), 0755)
require.NoError(t, err)
// copy docker => dir
@@ -572,11 +571,11 @@ func (s *syncSuite) TestFailsWithDockerSourceNoRegistry() {
tmpDir := t.TempDir()
// untagged
//untagged
assertSkopeoFails(t, ".*StatusCode: 404.*",
"sync", "--scoped", "--src", "docker", "--dest", "dir", regURL, tmpDir)
// tagged
//tagged
assertSkopeoFails(t, ".*StatusCode: 404.*",
"sync", "--scoped", "--src", "docker", "--dest", "dir", regURL+":thetag", tmpDir)
}
@@ -586,11 +585,11 @@ func (s *syncSuite) TestFailsWithDockerSourceUnauthorized() {
const repo = "privateimagenamethatshouldnotbepublic"
tmpDir := t.TempDir()
// untagged
//untagged
assertSkopeoFails(t, ".*requested access to the resource is denied.*",
"sync", "--scoped", "--src", "docker", "--dest", "dir", repo, tmpDir)
// tagged
//tagged
assertSkopeoFails(t, ".*requested access to the resource is denied.*",
"sync", "--scoped", "--src", "docker", "--dest", "dir", repo+":thetag", tmpDir)
}
@@ -600,11 +599,11 @@ func (s *syncSuite) TestFailsWithDockerSourceNotExisting() {
repo := path.Join(v2DockerRegistryURL, "imagedoesnotexist")
tmpDir := t.TempDir()
// untagged
//untagged
assertSkopeoFails(t, ".*repository name not known to registry.*",
"sync", "--scoped", "--src-tls-verify=false", "--src", "docker", "--dest", "dir", repo, tmpDir)
// tagged
//tagged
assertSkopeoFails(t, ".*reading manifest.*",
"sync", "--scoped", "--src-tls-verify=false", "--src", "docker", "--dest", "dir", repo+":thetag", tmpDir)
}

View File

@@ -4,7 +4,6 @@ import (
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"io"
"net"
"net/netip"
@@ -30,22 +29,9 @@ var skopeoBinary = func() string {
return "skopeo"
}()
// findFingerprint extracts the GPG key fingerprint from gpg --with-colons output.
func findFingerprint(lineBytes []byte) (string, error) {
for line := range bytes.SplitSeq(lineBytes, []byte{'\n'}) {
fields := strings.Split(string(line), ":")
if len(fields) >= 10 && fields[0] == "fpr" {
return fields[9], nil
}
}
return "", errors.New("No fingerprint found")
}
const (
testFQIN = "docker://quay.io/libpod/busybox" // tag left off on purpose, some tests need to add a special one
testFQIN64 = "docker://quay.io/libpod/busybox:amd64"
testFQINMultiLayer = "docker://quay.io/libpod/alpine_nginx:latest" // multi-layer
)
const testFQIN = "docker://quay.io/libpod/busybox" // tag left off on purpose, some tests need to add a special one
const testFQIN64 = "docker://quay.io/libpod/busybox:amd64"
const testFQINMultiLayer = "docker://quay.io/libpod/alpine_nginx:latest" // multi-layer
// consumeAndLogOutputStream takes (f, err) from an exec.*Pipe(), and causes all output to it to be logged to t.
func consumeAndLogOutputStream(t *testing.T, id string, f io.ReadCloser, err error) {

View File

@@ -137,7 +137,7 @@ END_PUSH
# The table below lists the paths to fetch, and the expected errors (or
# none, if we expect them to pass).
#
# "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" or "Missing key $fingerprint" by Sequoia.
# "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" by Sequoia.
while read path expected_error; do
expected_rc=
if [[ -n $expected_error ]]; then
@@ -156,7 +156,7 @@ END_PUSH
fi
done <<END_TESTS
/myns/alice:signed
/myns/bob:signedbyalice (Invalid GPG signature|Missing key)
/myns/bob:signedbyalice (Invalid GPG signature|Missing key:)
/myns/alice:unsigned Signature for identity \\\\\\\\"localhost:5000/myns/alice:signed\\\\\\\\" is not accepted
/myns/carol:latest Running image docker://localhost:5000/myns/carol:latest is rejected by policy.
/open/forall:latest

View File

@@ -1,43 +0,0 @@
# SPDX-License-Identifier: MPL-2.0
#
# libpathrs: safe path resolution on Linux
# Copyright (C) 2019-2025 Aleksa Sarai <cyphar@cyphar.com>
# Copyright (C) 2019-2025 SUSE LLC
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
version: "2"
linters:
enable:
- bidichk
- cyclop
- errname
- errorlint
- exhaustive
- goconst
- godot
- gomoddirectives
- gosec
- mirror
- misspell
- mnd
- nilerr
- nilnil
- perfsprint
- prealloc
- reassign
- revive
- unconvert
- unparam
- usestdlibvars
- wastedassign
formatters:
enable:
- gofumpt
- goimports
settings:
goimports:
local-prefixes:
- cyphar.com/go-pathrs

14
vendor/cyphar.com/go-pathrs/doc.go generated vendored
View File

@@ -1,14 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2019-2025 SUSE LLC
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// Package pathrs provides bindings for libpathrs, a library for safe path
// resolution on Linux.
package pathrs

View File

@@ -1,114 +0,0 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2019-2025 SUSE LLC
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package pathrs
import (
"fmt"
"os"
"cyphar.com/go-pathrs/internal/fdutils"
"cyphar.com/go-pathrs/internal/libpathrs"
)
// Handle is a handle for a path within a given [Root]. This handle references
// an already-resolved path which can be used for only one purpose -- to
// "re-open" the handle and get an actual [os.File] which can be used for
// ordinary operations.
//
// If you wish to open a file without having an intermediate [Handle] object,
// you can try to use [Root.Open] or [Root.OpenFile].
//
// It is critical that perform all relevant operations through this [Handle]
// (rather than fetching the file descriptor yourself with [Handle.IntoRaw]),
// because the security properties of libpathrs depend on users doing all
// relevant filesystem operations through libpathrs.
//
// [os.File]: https://pkg.go.dev/os#File
type Handle struct {
inner *os.File
}
// HandleFromFile creates a new [Handle] from an existing file handle. The
// handle will be copied by this method, so the original handle should still be
// freed by the caller.
//
// This is effectively the inverse operation of [Handle.IntoRaw], and is used
// for "deserialising" pathrs root handles.
func HandleFromFile(file *os.File) (*Handle, error) {
newFile, err := fdutils.DupFile(file)
if err != nil {
return nil, fmt.Errorf("duplicate handle fd: %w", err)
}
return &Handle{inner: newFile}, nil
}
// Open creates an "upgraded" file handle to the file referenced by the
// [Handle]. Note that the original [Handle] is not consumed by this operation,
// and can be opened multiple times.
//
// The handle returned is only usable for reading, and this is method is
// shorthand for [Handle.OpenFile] with os.O_RDONLY.
//
// TODO: Rename these to "Reopen" or something.
func (h *Handle) Open() (*os.File, error) {
return h.OpenFile(os.O_RDONLY)
}
// OpenFile creates an "upgraded" file handle to the file referenced by the
// [Handle]. Note that the original [Handle] is not consumed by this operation,
// and can be opened multiple times.
//
// The provided flags indicate which open(2) flags are used to create the new
// handle.
//
// TODO: Rename these to "Reopen" or something.
func (h *Handle) OpenFile(flags int) (*os.File, error) {
return fdutils.WithFileFd(h.inner, func(fd uintptr) (*os.File, error) {
newFd, err := libpathrs.Reopen(fd, flags)
if err != nil {
return nil, err
}
return os.NewFile(newFd, h.inner.Name()), nil
})
}
// IntoFile unwraps the [Handle] into its underlying [os.File].
//
// You almost certainly want to use [Handle.OpenFile] to get a non-O_PATH
// version of this [Handle].
//
// This operation returns the internal [os.File] of the [Handle] directly, so
// calling [Handle.Close] will also close any copies of the returned [os.File].
// If you want to get an independent copy, use [Handle.Clone] followed by
// [Handle.IntoFile] on the cloned [Handle].
//
// [os.File]: https://pkg.go.dev/os#File
func (h *Handle) IntoFile() *os.File {
// TODO: Figure out if we really don't want to make a copy.
// TODO: We almost certainly want to clear r.inner here, but we can't do
// that easily atomically (we could use atomic.Value but that'll make
// things quite a bit uglier).
return h.inner
}
// Clone creates a copy of a [Handle], such that it has a separate lifetime to
// the original (while referring to the same underlying file).
func (h *Handle) Clone() (*Handle, error) {
return HandleFromFile(h.inner)
}
// Close frees all of the resources used by the [Handle].
func (h *Handle) Close() error {
return h.inner.Close()
}

View File

@@ -1,75 +0,0 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2019-2025 SUSE LLC
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// Package fdutils contains a few helper methods when dealing with *os.File and
// file descriptors.
package fdutils
import (
"fmt"
"os"
"golang.org/x/sys/unix"
"cyphar.com/go-pathrs/internal/libpathrs"
)
// DupFd makes a duplicate of the given fd.
func DupFd(fd uintptr, name string) (*os.File, error) {
newFd, err := unix.FcntlInt(fd, unix.F_DUPFD_CLOEXEC, 0)
if err != nil {
return nil, fmt.Errorf("fcntl(F_DUPFD_CLOEXEC): %w", err)
}
return os.NewFile(uintptr(newFd), name), nil
}
// WithFileFd is a more ergonomic wrapper around file.SyscallConn().Control().
func WithFileFd[T any](file *os.File, fn func(fd uintptr) (T, error)) (T, error) {
conn, err := file.SyscallConn()
if err != nil {
return *new(T), err
}
var (
ret T
innerErr error
)
if err := conn.Control(func(fd uintptr) {
ret, innerErr = fn(fd)
}); err != nil {
return *new(T), err
}
return ret, innerErr
}
// DupFile makes a duplicate of the given file.
func DupFile(file *os.File) (*os.File, error) {
return WithFileFd(file, func(fd uintptr) (*os.File, error) {
return DupFd(fd, file.Name())
})
}
// MkFile creates a new *os.File from the provided file descriptor. However,
// unlike os.NewFile, the file's Name is based on the real path (provided by
// /proc/self/fd/$n).
func MkFile(fd uintptr) (*os.File, error) {
fdPath := fmt.Sprintf("fd/%d", fd)
fdName, err := libpathrs.ProcReadlinkat(libpathrs.ProcDefaultRootFd, libpathrs.ProcThreadSelf, fdPath)
if err != nil {
_ = unix.Close(int(fd))
return nil, fmt.Errorf("failed to fetch real name of fd %d: %w", fd, err)
}
// TODO: Maybe we should prefix this name with something to indicate to
// users that they must not use this path as a "safe" path. Something like
// "//pathrs-handle:/foo/bar"?
return os.NewFile(fd, fdName), nil
}

View File

@@ -1,40 +0,0 @@
//go:build linux
// TODO: Use "go:build unix" once we bump the minimum Go version 1.19.
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2019-2025 SUSE LLC
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package libpathrs
import (
"syscall"
)
// Error represents an underlying libpathrs error.
type Error struct {
description string
errno syscall.Errno
}
// Error returns a textual description of the error.
func (err *Error) Error() string {
return err.description
}
// Unwrap returns the underlying error which was wrapped by this error (if
// applicable).
func (err *Error) Unwrap() error {
if err.errno != 0 {
return err.errno
}
return nil
}

View File

@@ -1,337 +0,0 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2019-2025 SUSE LLC
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// Package libpathrs is an internal thin wrapper around the libpathrs C API.
package libpathrs
import (
"fmt"
"syscall"
"unsafe"
)
/*
// TODO: Figure out if we need to add support for linking against libpathrs
// statically even if in dynamically linked builds in order to make
// packaging a bit easier (using "-Wl,-Bstatic -lpathrs -Wl,-Bdynamic" or
// "-l:pathrs.a").
#cgo pkg-config: pathrs
#include <pathrs.h>
// This is a workaround for unsafe.Pointer() not working for non-void pointers.
char *cast_ptr(void *ptr) { return ptr; }
*/
import "C"
func fetchError(errID C.int) error {
if errID >= C.__PATHRS_MAX_ERR_VALUE {
return nil
}
cErr := C.pathrs_errorinfo(errID)
defer C.pathrs_errorinfo_free(cErr)
var err error
if cErr != nil {
err = &Error{
errno: syscall.Errno(cErr.saved_errno),
description: C.GoString(cErr.description),
}
}
return err
}
// OpenRoot wraps pathrs_open_root.
func OpenRoot(path string) (uintptr, error) {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
fd := C.pathrs_open_root(cPath)
return uintptr(fd), fetchError(fd)
}
// Reopen wraps pathrs_reopen.
func Reopen(fd uintptr, flags int) (uintptr, error) {
newFd := C.pathrs_reopen(C.int(fd), C.int(flags))
return uintptr(newFd), fetchError(newFd)
}
// InRootResolve wraps pathrs_inroot_resolve.
func InRootResolve(rootFd uintptr, path string) (uintptr, error) {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
fd := C.pathrs_inroot_resolve(C.int(rootFd), cPath)
return uintptr(fd), fetchError(fd)
}
// InRootResolveNoFollow wraps pathrs_inroot_resolve_nofollow.
func InRootResolveNoFollow(rootFd uintptr, path string) (uintptr, error) {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
fd := C.pathrs_inroot_resolve_nofollow(C.int(rootFd), cPath)
return uintptr(fd), fetchError(fd)
}
// InRootOpen wraps pathrs_inroot_open.
func InRootOpen(rootFd uintptr, path string, flags int) (uintptr, error) {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
fd := C.pathrs_inroot_open(C.int(rootFd), cPath, C.int(flags))
return uintptr(fd), fetchError(fd)
}
// InRootReadlink wraps pathrs_inroot_readlink.
func InRootReadlink(rootFd uintptr, path string) (string, error) {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
size := 128
for {
linkBuf := make([]byte, size)
n := C.pathrs_inroot_readlink(C.int(rootFd), cPath, C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.ulong(len(linkBuf)))
switch {
case int(n) < C.__PATHRS_MAX_ERR_VALUE:
return "", fetchError(n)
case int(n) <= len(linkBuf):
return string(linkBuf[:int(n)]), nil
default:
// The contents were truncated. Unlike readlinkat, pathrs returns
// the size of the link when it checked. So use the returned size
// as a basis for the reallocated size (but in order to avoid a DoS
// where a magic-link is growing by a single byte each iteration,
// make sure we are a fair bit larger).
size += int(n)
}
}
}
// InRootRmdir wraps pathrs_inroot_rmdir.
func InRootRmdir(rootFd uintptr, path string) error {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
err := C.pathrs_inroot_rmdir(C.int(rootFd), cPath)
return fetchError(err)
}
// InRootUnlink wraps pathrs_inroot_unlink.
func InRootUnlink(rootFd uintptr, path string) error {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
err := C.pathrs_inroot_unlink(C.int(rootFd), cPath)
return fetchError(err)
}
// InRootRemoveAll wraps pathrs_inroot_remove_all.
func InRootRemoveAll(rootFd uintptr, path string) error {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
err := C.pathrs_inroot_remove_all(C.int(rootFd), cPath)
return fetchError(err)
}
// InRootCreat wraps pathrs_inroot_creat.
func InRootCreat(rootFd uintptr, path string, flags int, mode uint32) (uintptr, error) {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
fd := C.pathrs_inroot_creat(C.int(rootFd), cPath, C.int(flags), C.uint(mode))
return uintptr(fd), fetchError(fd)
}
// InRootRename wraps pathrs_inroot_rename.
func InRootRename(rootFd uintptr, src, dst string, flags uint) error {
cSrc := C.CString(src)
defer C.free(unsafe.Pointer(cSrc))
cDst := C.CString(dst)
defer C.free(unsafe.Pointer(cDst))
err := C.pathrs_inroot_rename(C.int(rootFd), cSrc, cDst, C.uint(flags))
return fetchError(err)
}
// InRootMkdir wraps pathrs_inroot_mkdir.
func InRootMkdir(rootFd uintptr, path string, mode uint32) error {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
err := C.pathrs_inroot_mkdir(C.int(rootFd), cPath, C.uint(mode))
return fetchError(err)
}
// InRootMkdirAll wraps pathrs_inroot_mkdir_all.
func InRootMkdirAll(rootFd uintptr, path string, mode uint32) (uintptr, error) {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
fd := C.pathrs_inroot_mkdir_all(C.int(rootFd), cPath, C.uint(mode))
return uintptr(fd), fetchError(fd)
}
// InRootMknod wraps pathrs_inroot_mknod.
func InRootMknod(rootFd uintptr, path string, mode uint32, dev uint64) error {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
err := C.pathrs_inroot_mknod(C.int(rootFd), cPath, C.uint(mode), C.dev_t(dev))
return fetchError(err)
}
// InRootSymlink wraps pathrs_inroot_symlink.
func InRootSymlink(rootFd uintptr, path, target string) error {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
cTarget := C.CString(target)
defer C.free(unsafe.Pointer(cTarget))
err := C.pathrs_inroot_symlink(C.int(rootFd), cPath, cTarget)
return fetchError(err)
}
// InRootHardlink wraps pathrs_inroot_hardlink.
func InRootHardlink(rootFd uintptr, path, target string) error {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
cTarget := C.CString(target)
defer C.free(unsafe.Pointer(cTarget))
err := C.pathrs_inroot_hardlink(C.int(rootFd), cPath, cTarget)
return fetchError(err)
}
// ProcBase is pathrs_proc_base_t (uint64_t).
type ProcBase C.pathrs_proc_base_t
// FIXME: We need to open-code the constants because CGo unfortunately will
// implicitly convert any non-literal constants (i.e. those resolved using gcc)
// to signed integers. See <https://github.com/golang/go/issues/39136> for some
// more information on the underlying issue (though.
const (
// ProcRoot is PATHRS_PROC_ROOT.
ProcRoot ProcBase = 0xFFFF_FFFE_7072_6F63 // C.PATHRS_PROC_ROOT
// ProcSelf is PATHRS_PROC_SELF.
ProcSelf ProcBase = 0xFFFF_FFFE_091D_5E1F // C.PATHRS_PROC_SELF
// ProcThreadSelf is PATHRS_PROC_THREAD_SELF.
ProcThreadSelf ProcBase = 0xFFFF_FFFE_3EAD_5E1F // C.PATHRS_PROC_THREAD_SELF
// ProcBaseTypeMask is __PATHRS_PROC_TYPE_MASK.
ProcBaseTypeMask ProcBase = 0xFFFF_FFFF_0000_0000 // C.__PATHRS_PROC_TYPE_MASK
// ProcBaseTypePid is __PATHRS_PROC_TYPE_PID.
ProcBaseTypePid ProcBase = 0x8000_0000_0000_0000 // C.__PATHRS_PROC_TYPE_PID
// ProcDefaultRootFd is PATHRS_PROC_DEFAULT_ROOTFD.
ProcDefaultRootFd = -int(syscall.EBADF) // C.PATHRS_PROC_DEFAULT_ROOTFD
)
func assertEqual[T comparable](a, b T, msg string) {
if a != b {
panic(fmt.Sprintf("%s ((%T) %#v != (%T) %#v)", msg, a, a, b, b))
}
}
// Verify that the values above match the actual C values. Unfortunately, Go
// only allows us to forcefully cast int64 to uint64 if you use a temporary
// variable, which means we cannot do it in a const context and thus need to do
// it at runtime (even though it is a check that fundamentally could be done at
// compile-time)...
func init() {
var (
actualProcRoot int64 = C.PATHRS_PROC_ROOT
actualProcSelf int64 = C.PATHRS_PROC_SELF
actualProcThreadSelf int64 = C.PATHRS_PROC_THREAD_SELF
)
assertEqual(ProcRoot, ProcBase(actualProcRoot), "PATHRS_PROC_ROOT")
assertEqual(ProcSelf, ProcBase(actualProcSelf), "PATHRS_PROC_SELF")
assertEqual(ProcThreadSelf, ProcBase(actualProcThreadSelf), "PATHRS_PROC_THREAD_SELF")
var (
actualProcBaseTypeMask uint64 = C.__PATHRS_PROC_TYPE_MASK
actualProcBaseTypePid uint64 = C.__PATHRS_PROC_TYPE_PID
)
assertEqual(ProcBaseTypeMask, ProcBase(actualProcBaseTypeMask), "__PATHRS_PROC_TYPE_MASK")
assertEqual(ProcBaseTypePid, ProcBase(actualProcBaseTypePid), "__PATHRS_PROC_TYPE_PID")
assertEqual(ProcDefaultRootFd, int(C.PATHRS_PROC_DEFAULT_ROOTFD), "PATHRS_PROC_DEFAULT_ROOTFD")
}
// ProcPid reimplements the PROC_PID(x) conversion.
func ProcPid(pid uint32) ProcBase { return ProcBaseTypePid | ProcBase(pid) }
// ProcOpenat wraps pathrs_proc_openat.
func ProcOpenat(procRootFd int, base ProcBase, path string, flags int) (uintptr, error) {
cBase := C.pathrs_proc_base_t(base)
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
fd := C.pathrs_proc_openat(C.int(procRootFd), cBase, cPath, C.int(flags))
return uintptr(fd), fetchError(fd)
}
// ProcReadlinkat wraps pathrs_proc_readlinkat.
func ProcReadlinkat(procRootFd int, base ProcBase, path string) (string, error) {
// TODO: See if we can unify this code with InRootReadlink.
cBase := C.pathrs_proc_base_t(base)
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
size := 128
for {
linkBuf := make([]byte, size)
n := C.pathrs_proc_readlinkat(
C.int(procRootFd), cBase, cPath,
C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.ulong(len(linkBuf)))
switch {
case int(n) < C.__PATHRS_MAX_ERR_VALUE:
return "", fetchError(n)
case int(n) <= len(linkBuf):
return string(linkBuf[:int(n)]), nil
default:
// The contents were truncated. Unlike readlinkat, pathrs returns
// the size of the link when it checked. So use the returned size
// as a basis for the reallocated size (but in order to avoid a DoS
// where a magic-link is growing by a single byte each iteration,
// make sure we are a fair bit larger).
size += int(n)
}
}
}
// ProcfsOpenHow is pathrs_procfs_open_how (struct).
type ProcfsOpenHow C.pathrs_procfs_open_how
const (
// ProcfsNewUnmasked is PATHRS_PROCFS_NEW_UNMASKED.
ProcfsNewUnmasked = C.PATHRS_PROCFS_NEW_UNMASKED
)
// Flags returns a pointer to the internal flags field to allow other packages
// to modify structure fields that are internal due to Go's visibility model.
func (how *ProcfsOpenHow) Flags() *C.uint64_t { return &how.flags }
// ProcfsOpen is pathrs_procfs_open (sizeof(*how) is passed automatically).
func ProcfsOpen(how *ProcfsOpenHow) (uintptr, error) {
fd := C.pathrs_procfs_open((*C.pathrs_procfs_open_how)(how), C.size_t(unsafe.Sizeof(*how)))
return uintptr(fd), fetchError(fd)
}

View File

@@ -1,246 +0,0 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2019-2025 SUSE LLC
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// Package procfs provides a safe API for operating on /proc on Linux.
package procfs
import (
"os"
"runtime"
"cyphar.com/go-pathrs/internal/fdutils"
"cyphar.com/go-pathrs/internal/libpathrs"
)
// ProcBase is used with [ProcReadlink] and related functions to indicate what
// /proc subpath path operations should be done relative to.
type ProcBase struct {
inner libpathrs.ProcBase
}
var (
// ProcRoot indicates to use /proc. Note that this mode may be more
// expensive because we have to take steps to try to avoid leaking unmasked
// procfs handles, so you should use [ProcBaseSelf] if you can.
ProcRoot = ProcBase{inner: libpathrs.ProcRoot}
// ProcSelf indicates to use /proc/self. For most programs, this is the
// standard choice.
ProcSelf = ProcBase{inner: libpathrs.ProcSelf}
// ProcThreadSelf indicates to use /proc/thread-self. In multi-threaded
// programs where one thread has a different CLONE_FS, it is possible for
// /proc/self to point the wrong thread and so /proc/thread-self may be
// necessary.
ProcThreadSelf = ProcBase{inner: libpathrs.ProcThreadSelf}
)
// ProcPid returns a ProcBase which indicates to use /proc/$pid for the given
// PID (or TID). Be aware that due to PID recycling, using this is generally
// not safe except in certain circumstances. Namely:
//
// - PID 1 (the init process), as that PID cannot ever get recycled.
// - Your current PID (though you should just use [ProcBaseSelf]).
// - Your current TID if you have used [runtime.LockOSThread] (though you
// should just use [ProcBaseThreadSelf]).
// - PIDs of child processes (as long as you are sure that no other part of
// your program incorrectly catches or ignores SIGCHLD, and that you do it
// *before* you call wait(2)or any equivalent method that could reap
// zombies).
func ProcPid(pid int) ProcBase {
if pid < 0 || pid >= 1<<31 {
panic("invalid ProcBasePid value") // TODO: should this be an error?
}
return ProcBase{inner: libpathrs.ProcPid(uint32(pid))}
}
// ThreadCloser is a callback that needs to be called when you are done
// operating on an [os.File] fetched using [Handle.OpenThreadSelf].
//
// [os.File]: https://pkg.go.dev/os#File
type ThreadCloser func()
// Handle is a wrapper around an *os.File handle to "/proc", which can be
// used to do further procfs-related operations in a safe way.
type Handle struct {
inner *os.File
}
// Close releases all internal resources for this [Handle].
//
// Note that if the handle is actually the global cached handle, this operation
// is a no-op.
func (proc *Handle) Close() error {
var err error
if proc.inner != nil {
err = proc.inner.Close()
}
return err
}
// OpenOption is a configuration function passed as an argument to [Open].
type OpenOption func(*libpathrs.ProcfsOpenHow) error
// UnmaskedProcRoot can be passed to [Open] to request an unmasked procfs
// handle be created.
//
// procfs, err := procfs.OpenRoot(procfs.UnmaskedProcRoot)
func UnmaskedProcRoot(how *libpathrs.ProcfsOpenHow) error {
*how.Flags() |= libpathrs.ProcfsNewUnmasked
return nil
}
// Open creates a new [Handle] to a safe "/proc", based on the passed
// configuration options (in the form of a series of [OpenOption]s).
func Open(opts ...OpenOption) (*Handle, error) {
var how libpathrs.ProcfsOpenHow
for _, opt := range opts {
if err := opt(&how); err != nil {
return nil, err
}
}
fd, err := libpathrs.ProcfsOpen(&how)
if err != nil {
return nil, err
}
var procFile *os.File
if int(fd) >= 0 {
procFile = os.NewFile(fd, "/proc")
}
// TODO: Check that fd == PATHRS_PROC_DEFAULT_ROOTFD in the <0 case?
return &Handle{inner: procFile}, nil
}
// TODO: Switch to something fdutils.WithFileFd-like.
func (proc *Handle) fd() int {
if proc.inner != nil {
return int(proc.inner.Fd())
}
return libpathrs.ProcDefaultRootFd
}
// TODO: Should we expose open?
func (proc *Handle) open(base ProcBase, path string, flags int) (_ *os.File, Closer ThreadCloser, Err error) {
var closer ThreadCloser
if base == ProcThreadSelf {
runtime.LockOSThread()
closer = runtime.UnlockOSThread
}
defer func() {
if closer != nil && Err != nil {
closer()
Closer = nil
}
}()
fd, err := libpathrs.ProcOpenat(proc.fd(), base.inner, path, flags)
if err != nil {
return nil, nil, err
}
file, err := fdutils.MkFile(fd)
return file, closer, err
}
// OpenRoot safely opens a given path from inside /proc/.
//
// This function must only be used for accessing global information from procfs
// (such as /proc/cpuinfo) or information about other processes (such as
// /proc/1). Accessing your own process information should be done using
// [Handle.OpenSelf] or [Handle.OpenThreadSelf].
func (proc *Handle) OpenRoot(path string, flags int) (*os.File, error) {
file, closer, err := proc.open(ProcRoot, path, flags)
if closer != nil {
// should not happen
panic("non-zero closer returned from procOpen(ProcRoot)")
}
return file, err
}
// OpenSelf safely opens a given path from inside /proc/self/.
//
// This method is recommend for getting process information about the current
// process for almost all Go processes *except* for cases where there are
// [runtime.LockOSThread] threads that have changed some aspect of their state
// (such as through unshare(CLONE_FS) or changing namespaces).
//
// For such non-heterogeneous processes, /proc/self may reference to a task
// that has different state from the current goroutine and so it may be
// preferable to use [Handle.OpenThreadSelf]. The same is true if a user
// really wants to inspect the current OS thread's information (such as
// /proc/thread-self/stack or /proc/thread-self/status which is always uniquely
// per-thread).
//
// Unlike [Handle.OpenThreadSelf], this method does not involve locking
// the goroutine to the current OS thread and so is simpler to use and
// theoretically has slightly less overhead.
//
// [runtime.LockOSThread]: https://pkg.go.dev/runtime#LockOSThread
func (proc *Handle) OpenSelf(path string, flags int) (*os.File, error) {
file, closer, err := proc.open(ProcSelf, path, flags)
if closer != nil {
// should not happen
panic("non-zero closer returned from procOpen(ProcSelf)")
}
return file, err
}
// OpenPid safely opens a given path from inside /proc/$pid/, where pid can be
// either a PID or TID.
//
// This is effectively equivalent to calling [Handle.OpenRoot] with the
// pid prefixed to the subpath.
//
// Be aware that due to PID recycling, using this is generally not safe except
// in certain circumstances. See the documentation of [ProcPid] for more
// details.
func (proc *Handle) OpenPid(pid int, path string, flags int) (*os.File, error) {
file, closer, err := proc.open(ProcPid(pid), path, flags)
if closer != nil {
// should not happen
panic("non-zero closer returned from procOpen(ProcPidOpen)")
}
return file, err
}
// OpenThreadSelf safely opens a given path from inside /proc/thread-self/.
//
// Most Go processes have heterogeneous threads (all threads have most of the
// same kernel state such as CLONE_FS) and so [Handle.OpenSelf] is
// preferable for most users.
//
// For non-heterogeneous threads, or users that actually want thread-specific
// information (such as /proc/thread-self/stack or /proc/thread-self/status),
// this method is necessary.
//
// Because Go can change the running OS thread of your goroutine without notice
// (and then subsequently kill the old thread), this method will lock the
// current goroutine to the OS thread (with [runtime.LockOSThread]) and the
// caller is responsible for unlocking the the OS thread with the
// [ThreadCloser] callback once they are done using the returned file. This
// callback MUST be called AFTER you have finished using the returned
// [os.File]. This callback is completely separate to [os.File.Close], so it
// must be called regardless of how you close the handle.
//
// [runtime.LockOSThread]: https://pkg.go.dev/runtime#LockOSThread
// [os.File]: https://pkg.go.dev/os#File
// [os.File.Close]: https://pkg.go.dev/os#File.Close
func (proc *Handle) OpenThreadSelf(path string, flags int) (*os.File, ThreadCloser, error) {
return proc.open(ProcThreadSelf, path, flags)
}
// Readlink safely reads the contents of a symlink from the given procfs base.
//
// This is effectively equivalent to doing an Open*(O_PATH|O_NOFOLLOW) of the
// path and then doing unix.Readlinkat(fd, ""), but with the benefit that
// thread locking is not necessary for [ProcThreadSelf].
func (proc *Handle) Readlink(base ProcBase, path string) (string, error) {
return libpathrs.ProcReadlinkat(proc.fd(), base.inner, path)
}

View File

@@ -1,367 +0,0 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2019-2025 SUSE LLC
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package pathrs
import (
"errors"
"fmt"
"os"
"syscall"
"cyphar.com/go-pathrs/internal/fdutils"
"cyphar.com/go-pathrs/internal/libpathrs"
)
// Root is a handle to the root of a directory tree to resolve within. The only
// purpose of this "root handle" is to perform operations within the directory
// tree, or to get a [Handle] to inodes within the directory tree.
//
// At time of writing, it is considered a *VERY BAD IDEA* to open a [Root]
// inside a possibly-attacker-controlled directory tree. While we do have
// protections that should defend against it, it's far more dangerous than just
// opening a directory tree which is not inside a potentially-untrusted
// directory.
type Root struct {
inner *os.File
}
// OpenRoot creates a new [Root] handle to the directory at the given path.
func OpenRoot(path string) (*Root, error) {
fd, err := libpathrs.OpenRoot(path)
if err != nil {
return nil, err
}
file, err := fdutils.MkFile(fd)
if err != nil {
return nil, err
}
return &Root{inner: file}, nil
}
// RootFromFile creates a new [Root] handle from an [os.File] referencing a
// directory. The provided file will be duplicated, so the original file should
// still be closed by the caller.
//
// This is effectively the inverse operation of [Root.IntoFile].
//
// [os.File]: https://pkg.go.dev/os#File
func RootFromFile(file *os.File) (*Root, error) {
newFile, err := fdutils.DupFile(file)
if err != nil {
return nil, fmt.Errorf("duplicate root fd: %w", err)
}
return &Root{inner: newFile}, nil
}
// Resolve resolves the given path within the [Root]'s directory tree, and
// returns a [Handle] to the resolved path. The path must already exist,
// otherwise an error will occur.
//
// All symlinks (including trailing symlinks) are followed, but they are
// resolved within the rootfs. If you wish to open a handle to the symlink
// itself, use [ResolveNoFollow].
func (r *Root) Resolve(path string) (*Handle, error) {
return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) {
handleFd, err := libpathrs.InRootResolve(rootFd, path)
if err != nil {
return nil, err
}
handleFile, err := fdutils.MkFile(handleFd)
if err != nil {
return nil, err
}
return &Handle{inner: handleFile}, nil
})
}
// ResolveNoFollow is effectively an O_NOFOLLOW version of [Resolve]. Their
// behaviour is identical, except that *trailing* symlinks will not be
// followed. If the final component is a trailing symlink, an O_PATH|O_NOFOLLOW
// handle to the symlink itself is returned.
func (r *Root) ResolveNoFollow(path string) (*Handle, error) {
return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) {
handleFd, err := libpathrs.InRootResolveNoFollow(rootFd, path)
if err != nil {
return nil, err
}
handleFile, err := fdutils.MkFile(handleFd)
if err != nil {
return nil, err
}
return &Handle{inner: handleFile}, nil
})
}
// Open is effectively shorthand for [Resolve] followed by [Handle.Open], but
// can be slightly more efficient (it reduces CGo overhead and the number of
// syscalls used when using the openat2-based resolver) and is arguably more
// ergonomic to use.
//
// This is effectively equivalent to [os.Open].
//
// [os.Open]: https://pkg.go.dev/os#Open
func (r *Root) Open(path string) (*os.File, error) {
return r.OpenFile(path, os.O_RDONLY)
}
// OpenFile is effectively shorthand for [Resolve] followed by
// [Handle.OpenFile], but can be slightly more efficient (it reduces CGo
// overhead and the number of syscalls used when using the openat2-based
// resolver) and is arguably more ergonomic to use.
//
// However, if flags contains os.O_NOFOLLOW and the path is a symlink, then
// OpenFile's behaviour will match that of openat2. In most cases an error will
// be returned, but if os.O_PATH is provided along with os.O_NOFOLLOW then a
// file equivalent to [ResolveNoFollow] will be returned instead.
//
// This is effectively equivalent to [os.OpenFile], except that os.O_CREAT is
// not supported.
//
// [os.OpenFile]: https://pkg.go.dev/os#OpenFile
func (r *Root) OpenFile(path string, flags int) (*os.File, error) {
return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*os.File, error) {
fd, err := libpathrs.InRootOpen(rootFd, path, flags)
if err != nil {
return nil, err
}
return fdutils.MkFile(fd)
})
}
// Create creates a file within the [Root]'s directory tree at the given path,
// and returns a handle to the file. The provided mode is used for the new file
// (the process's umask applies).
//
// Unlike [os.Create], if the file already exists an error is created rather
// than the file being opened and truncated.
//
// [os.Create]: https://pkg.go.dev/os#Create
func (r *Root) Create(path string, flags int, mode os.FileMode) (*os.File, error) {
unixMode, err := toUnixMode(mode, false)
if err != nil {
return nil, err
}
return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*os.File, error) {
handleFd, err := libpathrs.InRootCreat(rootFd, path, flags, unixMode)
if err != nil {
return nil, err
}
return fdutils.MkFile(handleFd)
})
}
// Rename two paths within a [Root]'s directory tree. The flags argument is
// identical to the RENAME_* flags to the renameat2(2) system call.
func (r *Root) Rename(src, dst string, flags uint) error {
_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
err := libpathrs.InRootRename(rootFd, src, dst, flags)
return struct{}{}, err
})
return err
}
// RemoveDir removes the named empty directory within a [Root]'s directory
// tree.
func (r *Root) RemoveDir(path string) error {
_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
err := libpathrs.InRootRmdir(rootFd, path)
return struct{}{}, err
})
return err
}
// RemoveFile removes the named file within a [Root]'s directory tree.
func (r *Root) RemoveFile(path string) error {
_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
err := libpathrs.InRootUnlink(rootFd, path)
return struct{}{}, err
})
return err
}
// Remove removes the named file or (empty) directory within a [Root]'s
// directory tree.
//
// This is effectively equivalent to [os.Remove].
//
// [os.Remove]: https://pkg.go.dev/os#Remove
func (r *Root) Remove(path string) error {
// In order to match os.Remove's implementation we need to also do both
// syscalls unconditionally and adjust the error based on whether
// pathrs_inroot_rmdir() returned ENOTDIR.
unlinkErr := r.RemoveFile(path)
if unlinkErr == nil {
return nil
}
rmdirErr := r.RemoveDir(path)
if rmdirErr == nil {
return nil
}
// Both failed, adjust the error in the same way that os.Remove does.
err := rmdirErr
if errors.Is(err, syscall.ENOTDIR) {
err = unlinkErr
}
return err
}
// RemoveAll recursively deletes a path and all of its children.
//
// This is effectively equivalent to [os.RemoveAll].
//
// [os.RemoveAll]: https://pkg.go.dev/os#RemoveAll
func (r *Root) RemoveAll(path string) error {
_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
err := libpathrs.InRootRemoveAll(rootFd, path)
return struct{}{}, err
})
return err
}
// Mkdir creates a directory within a [Root]'s directory tree. The provided
// mode is used for the new directory (the process's umask applies).
//
// This is effectively equivalent to [os.Mkdir].
//
// [os.Mkdir]: https://pkg.go.dev/os#Mkdir
func (r *Root) Mkdir(path string, mode os.FileMode) error {
unixMode, err := toUnixMode(mode, false)
if err != nil {
return err
}
_, err = fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
err := libpathrs.InRootMkdir(rootFd, path, unixMode)
return struct{}{}, err
})
return err
}
// MkdirAll creates a directory (and any parent path components if they don't
// exist) within a [Root]'s directory tree. The provided mode is used for any
// directories created by this function (the process's umask applies).
//
// This is effectively equivalent to [os.MkdirAll].
//
// [os.MkdirAll]: https://pkg.go.dev/os#MkdirAll
func (r *Root) MkdirAll(path string, mode os.FileMode) (*Handle, error) {
unixMode, err := toUnixMode(mode, false)
if err != nil {
return nil, err
}
return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) {
handleFd, err := libpathrs.InRootMkdirAll(rootFd, path, unixMode)
if err != nil {
return nil, err
}
handleFile, err := fdutils.MkFile(handleFd)
if err != nil {
return nil, err
}
return &Handle{inner: handleFile}, err
})
}
// Mknod creates a new device inode of the given type within a [Root]'s
// directory tree. The provided mode is used for the new directory (the
// process's umask applies).
//
// This is effectively equivalent to [unix.Mknod].
//
// [unix.Mknod]: https://pkg.go.dev/golang.org/x/sys/unix#Mknod
func (r *Root) Mknod(path string, mode os.FileMode, dev uint64) error {
unixMode, err := toUnixMode(mode, true)
if err != nil {
return err
}
_, err = fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
err := libpathrs.InRootMknod(rootFd, path, unixMode, dev)
return struct{}{}, err
})
return err
}
// Symlink creates a symlink within a [Root]'s directory tree. The symlink is
// created at path and is a link to target.
//
// This is effectively equivalent to [os.Symlink].
//
// [os.Symlink]: https://pkg.go.dev/os#Symlink
func (r *Root) Symlink(path, target string) error {
_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
err := libpathrs.InRootSymlink(rootFd, path, target)
return struct{}{}, err
})
return err
}
// Hardlink creates a hardlink within a [Root]'s directory tree. The hardlink
// is created at path and is a link to target. Both paths are within the
// [Root]'s directory tree (you cannot hardlink to a different [Root] or the
// host).
//
// This is effectively equivalent to [os.Link].
//
// [os.Link]: https://pkg.go.dev/os#Link
func (r *Root) Hardlink(path, target string) error {
_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
err := libpathrs.InRootHardlink(rootFd, path, target)
return struct{}{}, err
})
return err
}
// Readlink returns the target of a symlink with a [Root]'s directory tree.
//
// This is effectively equivalent to [os.Readlink].
//
// [os.Readlink]: https://pkg.go.dev/os#Readlink
func (r *Root) Readlink(path string) (string, error) {
return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (string, error) {
return libpathrs.InRootReadlink(rootFd, path)
})
}
// IntoFile unwraps the [Root] into its underlying [os.File].
//
// It is critical that you do not operate on this file descriptor yourself,
// because the security properties of libpathrs depend on users doing all
// relevant filesystem operations through libpathrs.
//
// This operation returns the internal [os.File] of the [Root] directly, so
// calling [Root.Close] will also close any copies of the returned [os.File].
// If you want to get an independent copy, use [Root.Clone] followed by
// [Root.IntoFile] on the cloned [Root].
//
// [os.File]: https://pkg.go.dev/os#File
func (r *Root) IntoFile() *os.File {
// TODO: Figure out if we really don't want to make a copy.
// TODO: We almost certainly want to clear r.inner here, but we can't do
// that easily atomically (we could use atomic.Value but that'll make
// things quite a bit uglier).
return r.inner
}
// Clone creates a copy of a [Root] handle, such that it has a separate
// lifetime to the original (while referring to the same underlying directory).
func (r *Root) Clone() (*Root, error) {
return RootFromFile(r.inner)
}
// Close frees all of the resources used by the [Root] handle.
func (r *Root) Close() error {
return r.inner.Close()
}

View File

@@ -1,56 +0,0 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2019-2025 SUSE LLC
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package pathrs
import (
"fmt"
"os"
"golang.org/x/sys/unix"
)
//nolint:cyclop // this function needs to handle a lot of cases
func toUnixMode(mode os.FileMode, needsType bool) (uint32, error) {
sysMode := uint32(mode.Perm())
switch mode & os.ModeType { //nolint:exhaustive // we only care about ModeType bits
case 0:
if needsType {
sysMode |= unix.S_IFREG
}
case os.ModeDir:
sysMode |= unix.S_IFDIR
case os.ModeSymlink:
sysMode |= unix.S_IFLNK
case os.ModeCharDevice | os.ModeDevice:
sysMode |= unix.S_IFCHR
case os.ModeDevice:
sysMode |= unix.S_IFBLK
case os.ModeNamedPipe:
sysMode |= unix.S_IFIFO
case os.ModeSocket:
sysMode |= unix.S_IFSOCK
default:
return 0, fmt.Errorf("invalid mode filetype %+o", mode)
}
if mode&os.ModeSetuid != 0 {
sysMode |= unix.S_ISUID
}
if mode&os.ModeSetgid != 0 {
sysMode |= unix.S_ISGID
}
if mode&os.ModeSticky != 0 {
sysMode |= unix.S_ISVTX
}
return sysMode, nil
}

View File

@@ -1,7 +1,7 @@
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
reflection interface similar to Go's standard library `json` and `xml` packages.
Compatible with TOML version [v1.1.0](https://toml.io/en/v1.1.0).
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
Documentation: https://pkg.go.dev/github.com/BurntSushi/toml

View File

@@ -206,13 +206,6 @@ func markDecodedRecursive(md *MetaData, tmap map[string]any) {
markDecodedRecursive(md, tmap)
md.context = md.context[0 : len(md.context)-1]
}
if tarr, ok := tmap[key].([]map[string]any); ok {
for _, elm := range tarr {
md.context = append(md.context, key)
markDecodedRecursive(md, elm)
md.context = md.context[0 : len(md.context)-1]
}
}
}
}
@@ -430,7 +423,7 @@ func (md *MetaData) unifyString(data any, rv reflect.Value) error {
if i, ok := data.(int64); ok {
rv.SetString(strconv.FormatInt(i, 10))
} else if f, ok := data.(float64); ok {
rv.SetString(strconv.FormatFloat(f, 'g', -1, 64))
rv.SetString(strconv.FormatFloat(f, 'f', -1, 64))
} else {
return md.badtype("string", data)
}

View File

@@ -228,9 +228,9 @@ func (enc *Encoder) eElement(rv reflect.Value) {
}
switch v.Location() {
default:
enc.write(v.Format(format))
enc.wf(v.Format(format))
case internal.LocalDatetime, internal.LocalDate, internal.LocalTime:
enc.write(v.In(time.UTC).Format(format))
enc.wf(v.In(time.UTC).Format(format))
}
return
case Marshaler:
@@ -279,40 +279,40 @@ func (enc *Encoder) eElement(rv reflect.Value) {
case reflect.String:
enc.writeQuoted(rv.String())
case reflect.Bool:
enc.write(strconv.FormatBool(rv.Bool()))
enc.wf(strconv.FormatBool(rv.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
enc.write(strconv.FormatInt(rv.Int(), 10))
enc.wf(strconv.FormatInt(rv.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
enc.write(strconv.FormatUint(rv.Uint(), 10))
enc.wf(strconv.FormatUint(rv.Uint(), 10))
case reflect.Float32:
f := rv.Float()
if math.IsNaN(f) {
if math.Signbit(f) {
enc.write("-")
enc.wf("-")
}
enc.write("nan")
enc.wf("nan")
} else if math.IsInf(f, 0) {
if math.Signbit(f) {
enc.write("-")
enc.wf("-")
}
enc.write("inf")
enc.wf("inf")
} else {
enc.write(floatAddDecimal(strconv.FormatFloat(f, 'g', -1, 32)))
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
}
case reflect.Float64:
f := rv.Float()
if math.IsNaN(f) {
if math.Signbit(f) {
enc.write("-")
enc.wf("-")
}
enc.write("nan")
enc.wf("nan")
} else if math.IsInf(f, 0) {
if math.Signbit(f) {
enc.write("-")
enc.wf("-")
}
enc.write("inf")
enc.wf("inf")
} else {
enc.write(floatAddDecimal(strconv.FormatFloat(f, 'g', -1, 64)))
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
}
case reflect.Array, reflect.Slice:
enc.eArrayOrSliceElement(rv)
@@ -330,32 +330,27 @@ func (enc *Encoder) eElement(rv reflect.Value) {
// By the TOML spec, all floats must have a decimal with at least one number on
// either side.
func floatAddDecimal(fstr string) string {
for _, c := range fstr {
if c == 'e' { // Exponent syntax
return fstr
}
if c == '.' {
return fstr
}
if !strings.Contains(fstr, ".") {
return fstr + ".0"
}
return fstr + ".0"
return fstr
}
func (enc *Encoder) writeQuoted(s string) {
enc.write(`"` + dblQuotedReplacer.Replace(s) + `"`)
enc.wf("\"%s\"", dblQuotedReplacer.Replace(s))
}
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
length := rv.Len()
enc.write("[")
enc.wf("[")
for i := 0; i < length; i++ {
elem := eindirect(rv.Index(i))
enc.eElement(elem)
if i != length-1 {
enc.write(", ")
enc.wf(", ")
}
}
enc.write("]")
enc.wf("]")
}
func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
@@ -368,7 +363,7 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
continue
}
enc.newline()
enc.writef("%s[[%s]]", enc.indentStr(key), key)
enc.wf("%s[[%s]]", enc.indentStr(key), key)
enc.newline()
enc.eMapOrStruct(key, trv, false)
}
@@ -381,7 +376,7 @@ func (enc *Encoder) eTable(key Key, rv reflect.Value) {
enc.newline()
}
if len(key) > 0 {
enc.writef("%s[%s]", enc.indentStr(key), key)
enc.wf("%s[%s]", enc.indentStr(key), key)
enc.newline()
}
enc.eMapOrStruct(key, rv, false)
@@ -427,7 +422,7 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
if inline {
enc.writeKeyValue(Key{mapKey.String()}, val, true)
if trailC || i != len(mapKeys)-1 {
enc.write(", ")
enc.wf(", ")
}
} else {
enc.encode(key.add(mapKey.String()), val)
@@ -436,12 +431,12 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
}
if inline {
enc.write("{")
enc.wf("{")
}
writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0)
writeMapKeys(mapKeysSub, false)
if inline {
enc.write("}")
enc.wf("}")
}
}
@@ -539,7 +534,7 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
if inline {
enc.writeKeyValue(Key{keyName}, fieldVal, true)
if fieldIndex[0] != totalFields-1 {
enc.write(", ")
enc.wf(", ")
}
} else {
enc.encode(key.add(keyName), fieldVal)
@@ -548,14 +543,14 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
}
if inline {
enc.write("{")
enc.wf("{")
}
l := len(fieldsDirect) + len(fieldsSub)
writeFields(fieldsDirect, l)
writeFields(fieldsSub, l)
if inline {
enc.write("}")
enc.wf("}")
}
}
@@ -705,7 +700,7 @@ func isEmpty(rv reflect.Value) bool {
func (enc *Encoder) newline() {
if enc.hasWritten {
enc.write("\n")
enc.wf("\n")
}
}
@@ -727,22 +722,14 @@ func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
enc.eElement(val)
return
}
enc.writef("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.eElement(val)
if !inline {
enc.newline()
}
}
func (enc *Encoder) write(s string) {
_, err := enc.w.WriteString(s)
if err != nil {
encPanic(err)
}
enc.hasWritten = true
}
func (enc *Encoder) writef(format string, v ...any) {
func (enc *Encoder) wf(format string, v ...any) {
_, err := fmt.Fprintf(enc.w, format, v...)
if err != nil {
encPanic(err)

View File

@@ -13,6 +13,7 @@ type itemType int
const (
itemError itemType = iota
itemNIL // used in the parser to indicate no type
itemEOF
itemText
itemString
@@ -46,13 +47,14 @@ func (p Position) String() string {
}
type lexer struct {
input string
start int
pos int
line int
state stateFn
items chan item
esc bool
input string
start int
pos int
line int
state stateFn
items chan item
tomlNext bool
esc bool
// Allow for backing up up to 4 runes. This is necessary because TOML
// contains 3-rune tokens (""" and ''').
@@ -88,13 +90,14 @@ func (lx *lexer) nextItem() item {
}
}
func lex(input string) *lexer {
func lex(input string, tomlNext bool) *lexer {
lx := &lexer{
input: input,
state: lexTop,
items: make(chan item, 10),
stack: make([]stateFn, 0, 10),
line: 1,
input: input,
state: lexTop,
items: make(chan item, 10),
stack: make([]stateFn, 0, 10),
line: 1,
tomlNext: tomlNext,
}
return lx
}
@@ -105,7 +108,7 @@ func (lx *lexer) push(state stateFn) {
func (lx *lexer) pop() stateFn {
if len(lx.stack) == 0 {
panic("BUG in lexer: no states to pop")
return lx.errorf("BUG in lexer: no states to pop")
}
last := lx.stack[len(lx.stack)-1]
lx.stack = lx.stack[0 : len(lx.stack)-1]
@@ -302,8 +305,6 @@ func lexTop(lx *lexer) stateFn {
return lexTableStart
case eof:
if lx.pos > lx.start {
// TODO: never reached? I think this can only occur on a bug in the
// lexer(?)
return lx.errorf("unexpected EOF")
}
lx.emit(itemEOF)
@@ -391,6 +392,8 @@ func lexTableNameStart(lx *lexer) stateFn {
func lexTableNameEnd(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.next(); {
case isWhitespace(r):
return lexTableNameEnd
case r == '.':
lx.ignore()
return lexTableNameStart
@@ -409,7 +412,7 @@ func lexTableNameEnd(lx *lexer) stateFn {
// Lexes only one part, e.g. only 'a' inside 'a.b'.
func lexBareName(lx *lexer) stateFn {
r := lx.next()
if isBareKeyChar(r) {
if isBareKeyChar(r, lx.tomlNext) {
return lexBareName
}
lx.backup()
@@ -417,23 +420,23 @@ func lexBareName(lx *lexer) stateFn {
return lx.pop()
}
// lexQuotedName lexes one part of a quoted key or table name. It assumes that
// it starts lexing at the quote itself (" or ').
// lexBareName lexes one part of a key or table.
//
// It assumes that at least one valid character for the table has already been
// read.
//
// Lexes only one part, e.g. only '"a"' inside '"a".b'.
func lexQuotedName(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexValue)
case r == '"':
lx.ignore() // ignore the '"'
return lexString
case r == '\'':
lx.ignore() // ignore the "'"
return lexRawString
// TODO: I don't think any of the below conditions can ever be reached?
case isWhitespace(r):
return lexSkip(lx, lexValue)
case r == eof:
return lx.errorf("unexpected EOF; expected value")
default:
@@ -461,19 +464,17 @@ func lexKeyStart(lx *lexer) stateFn {
func lexKeyNameStart(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.peek(); {
default:
lx.push(lexKeyEnd)
return lexBareName
case r == '"' || r == '\'':
lx.ignore()
lx.push(lexKeyEnd)
return lexQuotedName
// TODO: I think these can never be reached?
case r == '=' || r == eof:
return lx.errorf("unexpected '='")
case r == '.':
return lx.errorf("unexpected '.'")
case r == '"' || r == '\'':
lx.ignore()
lx.push(lexKeyEnd)
return lexQuotedName
default:
lx.push(lexKeyEnd)
return lexBareName
}
}
@@ -484,7 +485,7 @@ func lexKeyEnd(lx *lexer) stateFn {
switch r := lx.next(); {
case isWhitespace(r):
return lexSkip(lx, lexKeyEnd)
case r == eof: // TODO: never reached
case r == eof:
return lx.errorf("unexpected EOF; expected key separator '='")
case r == '.':
lx.ignore()
@@ -627,7 +628,10 @@ func lexInlineTableValue(lx *lexer) stateFn {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValue)
case isNL(r):
return lexSkip(lx, lexInlineTableValue)
if lx.tomlNext {
return lexSkip(lx, lexInlineTableValue)
}
return lx.errorPrevLine(errLexInlineTableNL{})
case r == '#':
lx.push(lexInlineTableValue)
return lexCommentStart
@@ -649,7 +653,10 @@ func lexInlineTableValueEnd(lx *lexer) stateFn {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValueEnd)
case isNL(r):
return lexSkip(lx, lexInlineTableValueEnd)
if lx.tomlNext {
return lexSkip(lx, lexInlineTableValueEnd)
}
return lx.errorPrevLine(errLexInlineTableNL{})
case r == '#':
lx.push(lexInlineTableValueEnd)
return lexCommentStart
@@ -657,7 +664,10 @@ func lexInlineTableValueEnd(lx *lexer) stateFn {
lx.ignore()
lx.skip(isWhitespace)
if lx.peek() == '}' {
return lexInlineTableValueEnd
if lx.tomlNext {
return lexInlineTableValueEnd
}
return lx.errorf("trailing comma not allowed in inline tables")
}
return lexInlineTableValue
case r == '}':
@@ -845,6 +855,9 @@ func lexStringEscape(lx *lexer) stateFn {
r := lx.next()
switch r {
case 'e':
if !lx.tomlNext {
return lx.error(errLexEscape{r})
}
fallthrough
case 'b':
fallthrough
@@ -865,6 +878,9 @@ func lexStringEscape(lx *lexer) stateFn {
case '\\':
return lx.pop()
case 'x':
if !lx.tomlNext {
return lx.error(errLexEscape{r})
}
return lexHexEscape
case 'u':
return lexShortUnicodeEscape
@@ -912,9 +928,19 @@ func lexLongUnicodeEscape(lx *lexer) stateFn {
// lexBaseNumberOrDate can differentiate base prefixed integers from other
// types.
func lexNumberOrDateStart(lx *lexer) stateFn {
if lx.next() == '0' {
r := lx.next()
switch r {
case '0':
return lexBaseNumberOrDate
}
if !isDigit(r) {
// The only way to reach this state is if the value starts
// with a digit, so specifically treat anything else as an
// error.
return lx.errorf("expected a digit but got %q", r)
}
return lexNumberOrDate
}
@@ -1170,13 +1196,13 @@ func lexSkip(lx *lexer, nextState stateFn) stateFn {
}
func (s stateFn) String() string {
if s == nil {
return "<nil>"
}
name := runtime.FuncForPC(reflect.ValueOf(s).Pointer()).Name()
if i := strings.LastIndexByte(name, '.'); i > -1 {
name = name[i+1:]
}
if s == nil {
name = "<nil>"
}
return name + "()"
}
@@ -1184,6 +1210,8 @@ func (itype itemType) String() string {
switch itype {
case itemError:
return "Error"
case itemNIL:
return "NIL"
case itemEOF:
return "EOF"
case itemText:
@@ -1198,22 +1226,18 @@ func (itype itemType) String() string {
return "Float"
case itemDatetime:
return "DateTime"
case itemArray:
return "Array"
case itemArrayEnd:
return "ArrayEnd"
case itemTableStart:
return "TableStart"
case itemTableEnd:
return "TableEnd"
case itemArrayTableStart:
return "ArrayTableStart"
case itemArrayTableEnd:
return "ArrayTableEnd"
case itemKeyStart:
return "KeyStart"
case itemKeyEnd:
return "KeyEnd"
case itemArray:
return "Array"
case itemArrayEnd:
return "ArrayEnd"
case itemCommentStart:
return "CommentStart"
case itemInlineTableStart:
@@ -1242,7 +1266,7 @@ func isDigit(r rune) bool { return r >= '0' && r <= '9' }
func isBinary(r rune) bool { return r == '0' || r == '1' }
func isOctal(r rune) bool { return r >= '0' && r <= '7' }
func isHex(r rune) bool { return (r >= '0' && r <= '9') || (r|0x20 >= 'a' && r|0x20 <= 'f') }
func isBareKeyChar(r rune) bool {
func isBareKeyChar(r rune, tomlNext bool) bool {
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') || r == '_' || r == '-'
}

View File

@@ -3,6 +3,7 @@ package toml
import (
"fmt"
"math"
"os"
"strconv"
"strings"
"time"
@@ -16,6 +17,7 @@ type parser struct {
context Key // Full key for the current hash in scope.
currentKey string // Base key name for everything except hashes.
pos Position // Current position in the TOML file.
tomlNext bool
ordered []Key // List of keys in the order that they appear in the TOML data.
@@ -30,6 +32,8 @@ type keyInfo struct {
}
func parse(data string) (p *parser, err error) {
_, tomlNext := os.LookupEnv("BURNTSUSHI_TOML_110")
defer func() {
if r := recover(); r != nil {
if pErr, ok := r.(ParseError); ok {
@@ -69,9 +73,10 @@ func parse(data string) (p *parser, err error) {
p = &parser{
keyInfo: make(map[string]keyInfo),
mapping: make(map[string]any),
lx: lex(data),
lx: lex(data, tomlNext),
ordered: make([]Key, 0),
implicits: make(map[string]struct{}),
tomlNext: tomlNext,
}
for {
item := p.next()
@@ -345,14 +350,17 @@ func (p *parser) valueFloat(it item) (any, tomlType) {
var dtTypes = []struct {
fmt string
zone *time.Location
next bool
}{
{time.RFC3339Nano, time.Local},
{"2006-01-02T15:04:05.999999999", internal.LocalDatetime},
{"2006-01-02", internal.LocalDate},
{"15:04:05.999999999", internal.LocalTime},
{"2006-01-02T15:04Z07:00", time.Local},
{"2006-01-02T15:04", internal.LocalDatetime},
{"15:04", internal.LocalTime},
{time.RFC3339Nano, time.Local, false},
{"2006-01-02T15:04:05.999999999", internal.LocalDatetime, false},
{"2006-01-02", internal.LocalDate, false},
{"15:04:05.999999999", internal.LocalTime, false},
// tomlNext
{"2006-01-02T15:04Z07:00", time.Local, true},
{"2006-01-02T15:04", internal.LocalDatetime, true},
{"15:04", internal.LocalTime, true},
}
func (p *parser) valueDatetime(it item) (any, tomlType) {
@@ -363,6 +371,9 @@ func (p *parser) valueDatetime(it item) (any, tomlType) {
err error
)
for _, dt := range dtTypes {
if dt.next && !p.tomlNext {
continue
}
t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone)
if err == nil {
if missingLeadingZero(it.val, dt.fmt) {
@@ -633,11 +644,6 @@ func (p *parser) setValue(key string, value any) {
// Note that since it has already been defined (as a hash), we don't
// want to overwrite it. So our business is done.
if p.isArray(keyContext) {
if !p.isImplicit(keyContext) {
if _, ok := hash[key]; ok {
p.panicf("Key '%s' has already been defined.", keyContext)
}
}
p.removeImplicit(keyContext)
hash[key] = value
return
@@ -796,8 +802,10 @@ func (p *parser) replaceEscapes(it item, str string) string {
b.WriteByte(0x0d)
skip = 1
case 'e':
b.WriteByte(0x1b)
skip = 1
if p.tomlNext {
b.WriteByte(0x1b)
skip = 1
}
case '"':
b.WriteByte(0x22)
skip = 1
@@ -807,9 +815,11 @@ func (p *parser) replaceEscapes(it item, str string) string {
// The lexer guarantees the correct number of characters are present;
// don't need to check here.
case 'x':
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+4])
b.WriteRune(escaped)
skip = 3
if p.tomlNext {
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+4])
b.WriteRune(escaped)
skip = 3
}
case 'u':
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+6])
b.WriteRune(escaped)

View File

@@ -1,2 +0,0 @@
.DS_Store
*.test

View File

@@ -1,64 +0,0 @@
# stringish
A small Go module that provides a generic type constraint for “string-like”
data, and a utf8 package that works with both strings and byte slices
without conversions.
```go
type Interface interface {
~[]byte | ~string
}
```
[![Go Reference](https://pkg.go.dev/badge/github.com/clipperhouse/stringish/utf8.svg)](https://pkg.go.dev/github.com/clipperhouse/stringish/utf8)
[![Test Status](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml/badge.svg)](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml)
## Install
```
go get github.com/clipperhouse/stringish
```
## Examples
```go
import (
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/stringish/utf8"
)
s := "Hello, 世界"
r, size := utf8.DecodeRune(s) // not DecodeRuneInString 🎉
b := []byte("Hello, 世界")
r, size = utf8.DecodeRune(b) // same API!
func MyFoo[T stringish.Interface](s T) T {
// pass a string or a []byte
// iterate, slice, transform, whatever
}
```
## Motivation
Sometimes we want APIs to accept `string` or `[]byte` without having to convert
between those types. That conversion usually allocates!
By implementing with `stringish.Interface`, we can have a single API, and
single implementation for both types: one `Foo` instead of `Foo` and
`FooString`.
We have converted the
[`unicode/utf8` package](https://github.com/clipperhouse/stringish/blob/main/utf8/utf8.go)
as an example -- note the absence of`*InString` funcs. We might look at `x/text`
next.
## Used by
- clipperhouse/uax29: [stringish trie](https://github.com/clipperhouse/uax29/blob/master/graphemes/trie.go#L27), [stringish iterator](https://github.com/clipperhouse/uax29/blob/master/internal/iterators/iterator.go#L9), [stringish SplitFunc](https://github.com/clipperhouse/uax29/blob/master/graphemes/splitfunc.go#L21)
- [clipperhouse/displaywidth](https://github.com/clipperhouse/displaywidth)
## Prior discussion
- [Consideration of similar by the Go team](https://github.com/golang/go/issues/48643)

View File

@@ -1,5 +0,0 @@
package stringish
type Interface interface {
~[]byte | ~string
}

View File

@@ -1,94 +0,0 @@
An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode version 15.0.0.
[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/uax29/v2/graphemes.svg)](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg)
## Quick start
```
go get "github.com/clipperhouse/uax29/v2/graphemes"
```
```go
import "github.com/clipperhouse/uax29/v2/graphemes"
text := "Hello, 世界. Nice dog! 👍🐶"
tokens := graphemes.FromString(text)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
_A grapheme is a “single visible character”, which might be a simple as a single letter, or a complex emoji that consists of several Unicode code points._
## Conformance
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29).
![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg)
## APIs
### If you have a `string`
```go
text := "Hello, 世界. Nice dog! 👍🐶"
tokens := graphemes.FromString(text)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
### If you have an `io.Reader`
`FromReader` embeds a [`bufio.Scanner`](https://pkg.go.dev/bufio#Scanner), so just use those methods.
```go
r := getYourReader() // from a file or network maybe
tokens := graphemes.FromReader(r)
for tokens.Scan() { // Scan() returns true until error or EOF
fmt.Println(tokens.Text()) // Do something with the current grapheme
}
if tokens.Err() != nil { // Check the error
log.Fatal(tokens.Err())
}
```
### If you have a `[]byte`
```go
b := []byte("Hello, 世界. Nice dog! 👍🐶")
tokens := graphemes.FromBytes(b)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
### Benchmarks
On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second, and no allocations.
```
goos: darwin
goarch: arm64
pkg: github.com/clipperhouse/uax29/graphemes/comparative
cpu: Apple M2
BenchmarkGraphemes/clipperhouse/uax29-8 173805 ns/op 201.16 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemes/rivo/uniseg-8 2045128 ns/op 17.10 MB/s 0 B/op 0 allocs/op
```
### Invalid inputs
Invalid UTF-8 input is considered undefined behavior. We test to ensure that bad inputs will not cause pathological outcomes, such as a panic or infinite loop. Callers should expect “garbage-in, garbage-out”.
Your pipeline should probably include a call to [`utf8.Valid()`](https://pkg.go.dev/unicode/utf8#Valid).

View File

@@ -1,31 +0,0 @@
package graphemes
import (
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/uax29/v2/internal/iterators"
)
type Iterator[T stringish.Interface] struct {
*iterators.Iterator[T]
}
var (
splitFuncString = splitFunc[string]
splitFuncBytes = splitFunc[[]byte]
)
// FromString returns an iterator for the grapheme clusters in the input string.
// Iterate while Next() is true, and access the grapheme via Value().
func FromString(s string) Iterator[string] {
return Iterator[string]{
iterators.New(splitFuncString, s),
}
}
// FromBytes returns an iterator for the grapheme clusters in the input bytes.
// Iterate while Next() is true, and access the grapheme via Value().
func FromBytes(b []byte) Iterator[[]byte] {
return Iterator[[]byte]{
iterators.New(splitFuncBytes, b),
}
}

View File

@@ -1,25 +0,0 @@
// Package graphemes implements Unicode grapheme cluster boundaries: https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
package graphemes
import (
"bufio"
"io"
)
type Scanner struct {
*bufio.Scanner
}
// FromReader returns a Scanner, to split graphemes per
// https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
//
// It embeds a [bufio.Scanner], so you can use its methods.
//
// Iterate through graphemes by calling Scan() until false, then check Err().
func FromReader(r io.Reader) *Scanner {
sc := bufio.NewScanner(r)
sc.Split(SplitFunc)
return &Scanner{
Scanner: sc,
}
}

View File

@@ -1,174 +0,0 @@
package graphemes
import (
"bufio"
"github.com/clipperhouse/stringish"
)
// is determines if lookup intersects propert(ies)
func (lookup property) is(properties property) bool {
return (lookup & properties) != 0
}
const _Ignore = _Extend
// SplitFunc is a bufio.SplitFunc implementation of Unicode grapheme cluster segmentation, for use with bufio.Scanner.
//
// See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
var SplitFunc bufio.SplitFunc = splitFunc[[]byte]
func splitFunc[T stringish.Interface](data T, atEOF bool) (advance int, token T, err error) {
var empty T
if len(data) == 0 {
return 0, empty, nil
}
// These vars are stateful across loop iterations
var pos int
var lastExIgnore property = 0 // "last excluding ignored categories"
var lastLastExIgnore property = 0 // "last one before that"
var regionalIndicatorCount int
// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
// to the right of the ×, from which we look back or forward
current, w := lookup(data[pos:])
if w == 0 {
if !atEOF {
// Rune extends past current data, request more
return 0, empty, nil
}
pos = len(data)
return pos, data[:pos], nil
}
// https://unicode.org/reports/tr29/#GB1
// Start of text always advances
pos += w
for {
eot := pos == len(data) // "end of text"
if eot {
if !atEOF {
// Token extends past current data, request more
return 0, empty, nil
}
// https://unicode.org/reports/tr29/#GB2
break
}
/*
We've switched the evaluation order of GB1↓ and GB2↑. It's ok:
because we've checked for len(data) at the top of this function,
sot and eot are mutually exclusive, order doesn't matter.
*/
// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
// to the right of the ×, from which we look back or forward
// Remember previous properties to avoid lookups/lookbacks
last := current
if !last.is(_Ignore) {
lastLastExIgnore = lastExIgnore
lastExIgnore = last
}
current, w = lookup(data[pos:])
if w == 0 {
if atEOF {
// Just return the bytes, we can't do anything with them
pos = len(data)
break
}
// Rune extends past current data, request more
return 0, empty, nil
}
// Optimization: no rule can possibly apply
if current|last == 0 { // i.e. both are zero
break
}
// https://unicode.org/reports/tr29/#GB3
if current.is(_LF) && last.is(_CR) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB4
// https://unicode.org/reports/tr29/#GB5
if (current | last).is(_Control | _CR | _LF) {
break
}
// https://unicode.org/reports/tr29/#GB6
if current.is(_L|_V|_LV|_LVT) && last.is(_L) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB7
if current.is(_V|_T) && last.is(_LV|_V) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB8
if current.is(_T) && last.is(_LVT|_T) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9
if current.is(_Extend | _ZWJ) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9a
if current.is(_SpacingMark) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9b
if last.is(_Prepend) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9c
// TODO(clipperhouse):
// It appears to be added in Unicode 15.1.0:
// https://unicode.org/versions/Unicode15.1.0/#Migration
// This package currently supports Unicode 15.0.0, so
// out of scope for now
// https://unicode.org/reports/tr29/#GB11
if current.is(_ExtendedPictographic) && last.is(_ZWJ) && lastLastExIgnore.is(_ExtendedPictographic) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB12
// https://unicode.org/reports/tr29/#GB13
if (current & last).is(_RegionalIndicator) {
regionalIndicatorCount++
odd := regionalIndicatorCount%2 == 1
if odd {
pos += w
continue
}
}
// If we fall through all the above rules, it's a grapheme cluster break
break
}
// Return token
return pos, data[:pos], nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,100 +0,0 @@
package iterators
import "github.com/clipperhouse/stringish"
type SplitFunc[T stringish.Interface] func(T, bool) (int, T, error)
// Iterator is a generic iterator for words that are either []byte or string.
// Iterate while Next() is true, and access the word via Value().
type Iterator[T stringish.Interface] struct {
split SplitFunc[T]
data T
start int
pos int
}
// New creates a new Iterator for the given data and SplitFunc.
func New[T stringish.Interface](split SplitFunc[T], data T) *Iterator[T] {
return &Iterator[T]{
split: split,
data: data,
}
}
// SetText sets the text for the iterator to operate on, and resets all state.
func (iter *Iterator[T]) SetText(data T) {
iter.data = data
iter.start = 0
iter.pos = 0
}
// Split sets the SplitFunc for the Iterator.
func (iter *Iterator[T]) Split(split SplitFunc[T]) {
iter.split = split
}
// Next advances the iterator to the next token. It returns false when there
// are no remaining tokens or an error occurred.
func (iter *Iterator[T]) Next() bool {
if iter.pos == len(iter.data) {
return false
}
if iter.pos > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
iter.start = iter.pos
advance, _, err := iter.split(iter.data[iter.pos:], true)
if err != nil {
panic(err)
}
if advance <= 0 {
panic("SplitFunc returned a zero or negative advance")
}
iter.pos += advance
if iter.pos > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
return true
}
// Value returns the current token.
func (iter *Iterator[T]) Value() T {
return iter.data[iter.start:iter.pos]
}
// Start returns the byte position of the current token in the original data.
func (iter *Iterator[T]) Start() int {
return iter.start
}
// End returns the byte position after the current token in the original data.
func (iter *Iterator[T]) End() int {
return iter.pos
}
// Reset resets the iterator to the beginning of the data.
func (iter *Iterator[T]) Reset() {
iter.start = 0
iter.pos = 0
}
func (iter *Iterator[T]) First() T {
if len(iter.data) == 0 {
return iter.data
}
advance, _, err := iter.split(iter.data, true)
if err != nil {
panic(err)
}
if advance <= 0 {
panic("SplitFunc returned a zero or negative advance")
}
if advance > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
return iter.data[:advance]
}

View File

@@ -35,7 +35,6 @@ import (
"runtime"
"strings"
"sync"
"sync/atomic"
"github.com/containerd/stargz-snapshotter/estargz/errorutil"
"github.com/klauspost/compress/zstd"
@@ -43,8 +42,6 @@ import (
"golang.org/x/sync/errgroup"
)
type GzipHelperFunc func(io.Reader) (io.ReadCloser, error)
type options struct {
chunkSize int
compressionLevel int
@@ -53,7 +50,6 @@ type options struct {
compression Compression
ctx context.Context
minChunkSize int
gzipHelperFunc GzipHelperFunc
}
type Option func(o *options) error
@@ -131,25 +127,11 @@ func WithMinChunkSize(minChunkSize int) Option {
}
}
// WithGzipHelperFunc option specifies a custom function to decompress gzip-compressed layers.
// When a gzip-compressed layer is detected, this function will be used instead of the
// Go standard library gzip decompression for better performance.
// The function should take an io.Reader as input and return an io.ReadCloser.
// If nil, the Go standard library gzip.NewReader will be used.
func WithGzipHelperFunc(gzipHelperFunc GzipHelperFunc) Option {
return func(o *options) error {
o.gzipHelperFunc = gzipHelperFunc
return nil
}
}
// Blob is an eStargz blob.
type Blob struct {
io.ReadCloser
diffID digest.Digester
tocDigest digest.Digest
readCompleted *atomic.Bool
uncompressedSize *atomic.Int64
diffID digest.Digester
tocDigest digest.Digest
}
// DiffID returns the digest of uncompressed blob.
@@ -163,19 +145,6 @@ func (b *Blob) TOCDigest() digest.Digest {
return b.tocDigest
}
// UncompressedSize returns the size of uncompressed blob.
// UncompressedSize should only be called after the blob has been fully read.
func (b *Blob) UncompressedSize() (int64, error) {
switch {
case b.uncompressedSize == nil || b.readCompleted == nil:
return -1, fmt.Errorf("readCompleted or uncompressedSize is not initialized")
case !b.readCompleted.Load():
return -1, fmt.Errorf("called UncompressedSize before the blob has been fully read")
default:
return b.uncompressedSize.Load(), nil
}
}
// Build builds an eStargz blob which is an extended version of stargz, from a blob (gzip, zstd
// or plain tar) passed through the argument. If there are some prioritized files are listed in
// the option, these files are grouped as "prioritized" and can be used for runtime optimization
@@ -217,7 +186,7 @@ func Build(tarBlob *io.SectionReader, opt ...Option) (_ *Blob, rErr error) {
rErr = fmt.Errorf("error from context %q: %w", cErr, rErr)
}
}()
tarBlob, err := decompressBlob(tarBlob, layerFiles, opts.gzipHelperFunc)
tarBlob, err := decompressBlob(tarBlob, layerFiles)
if err != nil {
return nil, err
}
@@ -283,28 +252,17 @@ func Build(tarBlob *io.SectionReader, opt ...Option) (_ *Blob, rErr error) {
}
diffID := digest.Canonical.Digester()
pr, pw := io.Pipe()
readCompleted := new(atomic.Bool)
uncompressedSize := new(atomic.Int64)
go func() {
var size int64
var decompressFunc func(io.Reader) (io.ReadCloser, error)
if _, ok := opts.compression.(*gzipCompression); ok && opts.gzipHelperFunc != nil {
decompressFunc = opts.gzipHelperFunc
} else {
decompressFunc = opts.compression.Reader
}
decompressR, err := decompressFunc(io.TeeReader(io.MultiReader(append(rs, tocAndFooter)...), pw))
r, err := opts.compression.Reader(io.TeeReader(io.MultiReader(append(rs, tocAndFooter)...), pw))
if err != nil {
pw.CloseWithError(err)
return
}
defer decompressR.Close()
if size, err = io.Copy(diffID.Hash(), decompressR); err != nil {
defer r.Close()
if _, err := io.Copy(diffID.Hash(), r); err != nil {
pw.CloseWithError(err)
return
}
uncompressedSize.Store(size)
readCompleted.Store(true)
pw.Close()
}()
return &Blob{
@@ -312,10 +270,8 @@ func Build(tarBlob *io.SectionReader, opt ...Option) (_ *Blob, rErr error) {
Reader: pr,
closeFunc: layerFiles.CleanupAll,
},
tocDigest: tocDgst,
diffID: diffID,
readCompleted: readCompleted,
uncompressedSize: uncompressedSize,
tocDigest: tocDgst,
diffID: diffID,
}, nil
}
@@ -410,9 +366,8 @@ func sortEntries(in io.ReaderAt, prioritized []string, missedPrioritized *[]stri
// Sort the tar file respecting to the prioritized files list.
sorted := &tarFile{}
picked := make(map[string]struct{})
for _, l := range prioritized {
if err := moveRec(l, intar, sorted, picked); err != nil {
if err := moveRec(l, intar, sorted); err != nil {
if errors.Is(err, errNotFound) && missedPrioritized != nil {
*missedPrioritized = append(*missedPrioritized, l)
continue // allow not found
@@ -440,8 +395,8 @@ func sortEntries(in io.ReaderAt, prioritized []string, missedPrioritized *[]stri
})
}
// Dump prioritized entries followed by the rest entries while skipping picked ones.
return append(sorted.dump(nil), intar.dump(picked)...), nil
// Dump all entry and concatinate them.
return append(sorted.dump(), intar.dump()...), nil
}
// readerFromEntries returns a reader of tar archive that contains entries passed
@@ -503,42 +458,36 @@ func importTar(in io.ReaderAt) (*tarFile, error) {
return tf, nil
}
func moveRec(name string, in *tarFile, out *tarFile, picked map[string]struct{}) error {
func moveRec(name string, in *tarFile, out *tarFile) error {
name = cleanEntryName(name)
if name == "" { // root directory. stop recursion.
if e, ok := in.get(name); ok {
// entry of the root directory exists. we should move it as well.
// this case will occur if tar entries are prefixed with "./", "/", etc.
if _, done := picked[name]; !done {
out.add(e)
picked[name] = struct{}{}
}
out.add(e)
in.remove(name)
}
return nil
}
_, okIn := in.get(name)
_, okOut := out.get(name)
_, okPicked := picked[name]
if !okIn && !okOut && !okPicked {
if !okIn && !okOut {
return fmt.Errorf("file: %q: %w", name, errNotFound)
}
parent, _ := path.Split(strings.TrimSuffix(name, "/"))
if err := moveRec(parent, in, out, picked); err != nil {
if err := moveRec(parent, in, out); err != nil {
return err
}
if e, ok := in.get(name); ok && e.header.Typeflag == tar.TypeLink {
if err := moveRec(e.header.Linkname, in, out, picked); err != nil {
if err := moveRec(e.header.Linkname, in, out); err != nil {
return err
}
}
if _, done := picked[name]; done {
return nil
}
if e, ok := in.get(name); ok {
out.add(e)
picked[name] = struct{}{}
in.remove(name)
}
return nil
}
@@ -584,18 +533,8 @@ func (f *tarFile) get(name string) (e *entry, ok bool) {
return
}
func (f *tarFile) dump(skip map[string]struct{}) []*entry {
if len(skip) == 0 {
return f.stream
}
var out []*entry
for _, e := range f.stream {
if _, ok := skip[cleanEntryName(e.header.Name)]; ok {
continue
}
out = append(out, e)
}
return out
func (f *tarFile) dump() []*entry {
return f.stream
}
type readCloser struct {
@@ -710,7 +649,7 @@ func (cr *countReadSeeker) currentPos() int64 {
return *cr.cPos
}
func decompressBlob(org *io.SectionReader, tmp *tempFiles, gzipHelperFunc GzipHelperFunc) (*io.SectionReader, error) {
func decompressBlob(org *io.SectionReader, tmp *tempFiles) (*io.SectionReader, error) {
if org.Size() < 4 {
return org, nil
}
@@ -721,13 +660,7 @@ func decompressBlob(org *io.SectionReader, tmp *tempFiles, gzipHelperFunc GzipHe
var dR io.Reader
if bytes.Equal([]byte{0x1F, 0x8B, 0x08}, src[:3]) {
// gzip
var dgR io.ReadCloser
var err error
if gzipHelperFunc != nil {
dgR, err = gzipHelperFunc(io.NewSectionReader(org, 0, org.Size()))
} else {
dgR, err = gzip.NewReader(io.NewSectionReader(org, 0, org.Size()))
}
dgR, err := gzip.NewReader(io.NewSectionReader(org, 0, org.Size()))
if err != nil {
return nil, err
}

View File

@@ -237,7 +237,7 @@ func (r *Reader) initFields() error {
if ent.Gname != "" {
gname[ent.GID] = ent.Gname
} else {
ent.Gname = gname[ent.GID]
ent.Gname = uname[ent.GID]
}
ent.modTime, _ = time.Parse(time.RFC3339, ent.ModTime3339)
@@ -307,15 +307,6 @@ func (r *Reader) initFields() error {
}
}
if len(r.m) == 0 {
r.m[""] = &TOCEntry{
Name: "",
Type: "dir",
Mode: 0755,
NumLink: 1,
}
}
return nil
}

View File

@@ -38,6 +38,7 @@ import (
"reflect"
"sort"
"strings"
"testing"
"time"
"github.com/containerd/stargz-snapshotter/estargz/errorutil"
@@ -48,48 +49,16 @@ import (
// TestingController is Compression with some helper methods necessary for testing.
type TestingController interface {
Compression
TestStreams(t TestingT, b []byte, streams []int64)
DiffIDOf(TestingT, []byte) string
TestStreams(t *testing.T, b []byte, streams []int64)
DiffIDOf(*testing.T, []byte) string
String() string
}
// TestingT is the minimal set of testing.T required to run the
// tests defined in CompressionTestSuite. This interface exists to prevent
// leaking the testing package from being exposed outside tests.
type TestingT interface {
Errorf(format string, args ...any)
FailNow()
Failed() bool
Fatal(args ...any)
Fatalf(format string, args ...any)
Logf(format string, args ...any)
Parallel()
}
// Runner allows running subtests of TestingT. This exists instead of adding
// a Run method to TestingT interface because the Run implementation of
// testing.T would not satisfy the interface.
type Runner func(t TestingT, name string, fn func(t TestingT))
type TestRunner struct {
TestingT
Runner Runner
}
func (r *TestRunner) Run(name string, run func(*TestRunner)) {
r.Runner(r.TestingT, name, func(t TestingT) {
run(&TestRunner{TestingT: t, Runner: r.Runner})
})
}
// CompressionTestSuite tests this pkg with controllers can build valid eStargz blobs and parse them.
func CompressionTestSuite(t *TestRunner, controllers ...TestingControllerFactory) {
t.Run("testBuild", func(t *TestRunner) { t.Parallel(); testBuild(t, controllers...) })
t.Run("testDigestAndVerify", func(t *TestRunner) {
t.Parallel()
testDigestAndVerify(t, controllers...)
})
t.Run("testWriteAndOpen", func(t *TestRunner) { t.Parallel(); testWriteAndOpen(t, controllers...) })
func CompressionTestSuite(t *testing.T, controllers ...TestingControllerFactory) {
t.Run("testBuild", func(t *testing.T) { t.Parallel(); testBuild(t, controllers...) })
t.Run("testDigestAndVerify", func(t *testing.T) { t.Parallel(); testDigestAndVerify(t, controllers...) })
t.Run("testWriteAndOpen", func(t *testing.T) { t.Parallel(); testWriteAndOpen(t, controllers...) })
}
type TestingControllerFactory func() TestingController
@@ -110,7 +79,7 @@ var allowedPrefix = [4]string{"", "./", "/", "../"}
// testBuild tests the resulting stargz blob built by this pkg has the same
// contents as the normal stargz blob.
func testBuild(t *TestRunner, controllers ...TestingControllerFactory) {
func testBuild(t *testing.T, controllers ...TestingControllerFactory) {
tests := []struct {
name string
chunkSize int
@@ -196,7 +165,7 @@ func testBuild(t *TestRunner, controllers ...TestingControllerFactory) {
prefix := prefix
for _, minChunkSize := range tt.minChunkSize {
minChunkSize := minChunkSize
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,src=%d,format=%s,minChunkSize=%d", newCL(), prefix, srcCompression, srcTarFormat, minChunkSize), func(t *TestRunner) {
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,src=%d,format=%s,minChunkSize=%d", newCL(), prefix, srcCompression, srcTarFormat, minChunkSize), func(t *testing.T) {
tarBlob := buildTar(t, tt.in, prefix, srcTarFormat)
// Test divideEntries()
entries, err := sortEntries(tarBlob, nil, nil) // identical order
@@ -296,7 +265,7 @@ func testBuild(t *TestRunner, controllers ...TestingControllerFactory) {
}
}
func isSameTarGz(t TestingT, cla TestingController, a []byte, clb TestingController, b []byte) bool {
func isSameTarGz(t *testing.T, cla TestingController, a []byte, clb TestingController, b []byte) bool {
aGz, err := cla.Reader(bytes.NewReader(a))
if err != nil {
t.Fatalf("failed to read A")
@@ -356,7 +325,7 @@ func isSameTarGz(t TestingT, cla TestingController, a []byte, clb TestingControl
return true
}
func isSameVersion(t TestingT, cla TestingController, a []byte, clb TestingController, b []byte) bool {
func isSameVersion(t *testing.T, cla TestingController, a []byte, clb TestingController, b []byte) bool {
aJTOC, _, err := parseStargz(io.NewSectionReader(bytes.NewReader(a), 0, int64(len(a))), cla)
if err != nil {
t.Fatalf("failed to parse A: %v", err)
@@ -370,7 +339,7 @@ func isSameVersion(t TestingT, cla TestingController, a []byte, clb TestingContr
return aJTOC.Version == bJTOC.Version
}
func isSameEntries(t TestingT, a, b *Reader) bool {
func isSameEntries(t *testing.T, a, b *Reader) bool {
aroot, ok := a.Lookup("")
if !ok {
t.Fatalf("failed to get root of A")
@@ -384,7 +353,7 @@ func isSameEntries(t TestingT, a, b *Reader) bool {
return contains(t, aEntry, bEntry) && contains(t, bEntry, aEntry)
}
func compressBlob(t TestingT, src *io.SectionReader, srcCompression int) *io.SectionReader {
func compressBlob(t *testing.T, src *io.SectionReader, srcCompression int) *io.SectionReader {
buf := new(bytes.Buffer)
var w io.WriteCloser
var err error
@@ -418,7 +387,7 @@ type stargzEntry struct {
// contains checks if all child entries in "b" are also contained in "a".
// This function also checks if the files/chunks contain the same contents among "a" and "b".
func contains(t TestingT, a, b stargzEntry) bool {
func contains(t *testing.T, a, b stargzEntry) bool {
ae, ar := a.e, a.r
be, br := b.e, b.r
t.Logf("Comparing: %q vs %q", ae.Name, be.Name)
@@ -529,7 +498,7 @@ func equalEntry(a, b *TOCEntry) bool {
a.Digest == b.Digest
}
func readOffset(t TestingT, r *io.SectionReader, offset int64, e stargzEntry) ([]byte, int64, bool) {
func readOffset(t *testing.T, r *io.SectionReader, offset int64, e stargzEntry) ([]byte, int64, bool) {
ce, ok := e.r.ChunkEntryForOffset(e.e.Name, offset)
if !ok {
return nil, 0, false
@@ -548,7 +517,7 @@ func readOffset(t TestingT, r *io.SectionReader, offset int64, e stargzEntry) ([
return data[:n], offset + ce.ChunkSize, true
}
func dumpTOCJSON(t TestingT, tocJSON *JTOC) string {
func dumpTOCJSON(t *testing.T, tocJSON *JTOC) string {
jtocData, err := json.Marshal(*tocJSON)
if err != nil {
t.Fatalf("failed to marshal TOC JSON: %v", err)
@@ -562,19 +531,20 @@ func dumpTOCJSON(t TestingT, tocJSON *JTOC) string {
const chunkSize = 3
type check func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory)
// type check func(t *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, compressionLevel int)
type check func(t *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory)
// testDigestAndVerify runs specified checks against sample stargz blobs.
func testDigestAndVerify(t *TestRunner, controllers ...TestingControllerFactory) {
func testDigestAndVerify(t *testing.T, controllers ...TestingControllerFactory) {
tests := []struct {
name string
tarInit func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry)
tarInit func(t *testing.T, dgstMap map[string]digest.Digest) (blob []tarEntry)
checks []check
minChunkSize []int
}{
{
name: "no-regfile",
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
tarInit: func(t *testing.T, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
dir("test/"),
)
@@ -589,7 +559,7 @@ func testDigestAndVerify(t *TestRunner, controllers ...TestingControllerFactory)
},
{
name: "small-files",
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
tarInit: func(t *testing.T, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
regDigest(t, "baz.txt", "", dgstMap),
regDigest(t, "foo.txt", "a", dgstMap),
@@ -613,7 +583,7 @@ func testDigestAndVerify(t *TestRunner, controllers ...TestingControllerFactory)
},
{
name: "big-files",
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
tarInit: func(t *testing.T, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
regDigest(t, "baz.txt", "bazbazbazbazbazbazbaz", dgstMap),
regDigest(t, "foo.txt", "a", dgstMap),
@@ -637,7 +607,7 @@ func testDigestAndVerify(t *TestRunner, controllers ...TestingControllerFactory)
{
name: "with-non-regfiles",
minChunkSize: []int{0, 64000},
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
tarInit: func(t *testing.T, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
regDigest(t, "baz.txt", "bazbazbazbazbazbazbaz", dgstMap),
regDigest(t, "foo.txt", "a", dgstMap),
@@ -684,7 +654,7 @@ func testDigestAndVerify(t *TestRunner, controllers ...TestingControllerFactory)
srcTarFormat := srcTarFormat
for _, minChunkSize := range tt.minChunkSize {
minChunkSize := minChunkSize
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,format=%s,minChunkSize=%d", newCL(), prefix, srcTarFormat, minChunkSize), func(t *TestRunner) {
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,format=%s,minChunkSize=%d", newCL(), prefix, srcTarFormat, minChunkSize), func(t *testing.T) {
// Get original tar file and chunk digests
dgstMap := make(map[string]digest.Digest)
tarBlob := buildTar(t, tt.tarInit(t, dgstMap), prefix, srcTarFormat)
@@ -720,7 +690,7 @@ func testDigestAndVerify(t *TestRunner, controllers ...TestingControllerFactory)
// checkStargzTOC checks the TOC JSON of the passed stargz has the expected
// digest and contains valid chunks. It walks all entries in the stargz and
// checks all chunk digests stored to the TOC JSON match the actual contents.
func checkStargzTOC(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
func checkStargzTOC(t *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
sgz, err := Open(
io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))),
WithDecompressors(controller),
@@ -831,7 +801,7 @@ func checkStargzTOC(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgst
// checkVerifyTOC checks the verification works for the TOC JSON of the passed
// stargz. It walks all entries in the stargz and checks the verifications for
// all chunks work.
func checkVerifyTOC(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
func checkVerifyTOC(t *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
sgz, err := Open(
io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))),
WithDecompressors(controller),
@@ -912,9 +882,9 @@ func checkVerifyTOC(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgst
// checkVerifyInvalidTOCEntryFail checks if misconfigured TOC JSON can be
// detected during the verification and the verification returns an error.
func checkVerifyInvalidTOCEntryFail(filename string) check {
return func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
return func(t *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
funcs := map[string]rewriteFunc{
"lost digest in a entry": func(t TestingT, toc *JTOC, sgz *io.SectionReader) {
"lost digest in a entry": func(t *testing.T, toc *JTOC, sgz *io.SectionReader) {
var found bool
for _, e := range toc.Entries {
if cleanEntryName(e.Name) == filename {
@@ -932,7 +902,7 @@ func checkVerifyInvalidTOCEntryFail(filename string) check {
t.Fatalf("rewrite target not found")
}
},
"duplicated entry offset": func(t TestingT, toc *JTOC, sgz *io.SectionReader) {
"duplicated entry offset": func(t *testing.T, toc *JTOC, sgz *io.SectionReader) {
var (
sampleEntry *TOCEntry
targetEntry *TOCEntry
@@ -959,7 +929,7 @@ func checkVerifyInvalidTOCEntryFail(filename string) check {
}
for name, rFunc := range funcs {
t.Run(name, func(t *TestRunner) {
t.Run(name, func(t *testing.T) {
newSgz, newTocDigest := rewriteTOCJSON(t, io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))), rFunc, controller)
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, newSgz); err != nil {
@@ -988,7 +958,7 @@ func checkVerifyInvalidTOCEntryFail(filename string) check {
// checkVerifyInvalidStargzFail checks if the verification detects that the
// given stargz file doesn't match to the expected digest and returns error.
func checkVerifyInvalidStargzFail(invalid *io.SectionReader) check {
return func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
return func(t *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
cl := newController()
rc, err := Build(invalid, WithChunkSize(chunkSize), WithCompression(cl))
if err != nil {
@@ -1020,7 +990,7 @@ func checkVerifyInvalidStargzFail(invalid *io.SectionReader) check {
// checkVerifyBrokenContentFail checks if the verifier detects broken contents
// that doesn't match to the expected digest and returns error.
func checkVerifyBrokenContentFail(filename string) check {
return func(t *TestRunner, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
return func(t *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
// Parse stargz file
sgz, err := Open(
io.NewSectionReader(bytes.NewReader(sgzData), 0, int64(len(sgzData))),
@@ -1077,9 +1047,9 @@ func chunkID(name string, offset, size int64) string {
return fmt.Sprintf("%s-%d-%d", cleanEntryName(name), offset, size)
}
type rewriteFunc func(t TestingT, toc *JTOC, sgz *io.SectionReader)
type rewriteFunc func(t *testing.T, toc *JTOC, sgz *io.SectionReader)
func rewriteTOCJSON(t TestingT, sgz *io.SectionReader, rewrite rewriteFunc, controller TestingController) (newSgz io.Reader, tocDigest digest.Digest) {
func rewriteTOCJSON(t *testing.T, sgz *io.SectionReader, rewrite rewriteFunc, controller TestingController) (newSgz io.Reader, tocDigest digest.Digest) {
decodedJTOC, jtocOffset, err := parseStargz(sgz, controller)
if err != nil {
t.Fatalf("failed to extract TOC JSON: %v", err)
@@ -1150,7 +1120,7 @@ func parseStargz(sgz *io.SectionReader, controller TestingController) (decodedJT
return decodedJTOC, tocOffset, nil
}
func testWriteAndOpen(t *TestRunner, controllers ...TestingControllerFactory) {
func testWriteAndOpen(t *testing.T, controllers ...TestingControllerFactory) {
const content = "Some contents"
invalidUtf8 := "\xff\xfe\xfd"
@@ -1494,7 +1464,7 @@ func testWriteAndOpen(t *TestRunner, controllers ...TestingControllerFactory) {
for _, srcTarFormat := range []tar.Format{tar.FormatUSTAR, tar.FormatPAX, tar.FormatGNU} {
srcTarFormat := srcTarFormat
for _, lossless := range []bool{true, false} {
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,lossless=%v,format=%s", newCL(), prefix, lossless, srcTarFormat), func(t *TestRunner) {
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,lossless=%v,format=%s", newCL(), prefix, lossless, srcTarFormat), func(t *testing.T) {
var tr io.Reader = buildTar(t, tt.in, prefix, srcTarFormat)
origTarDgstr := digest.Canonical.Digester()
tr = io.TeeReader(tr, origTarDgstr.Hash())
@@ -1560,9 +1530,6 @@ func testWriteAndOpen(t *TestRunner, controllers ...TestingControllerFactory) {
if err != nil {
t.Fatalf("stargz.Open: %v", err)
}
if _, ok := r.Lookup(""); !ok {
t.Fatalf("failed to lookup rootdir: %v", err)
}
wantTOCVersion := 1
if tt.wantTOCVersion > 0 {
wantTOCVersion = tt.wantTOCVersion
@@ -1661,7 +1628,7 @@ func digestFor(content string) string {
type numTOCEntries int
func (n numTOCEntries) check(t TestingT, r *Reader) {
func (n numTOCEntries) check(t *testing.T, r *Reader) {
if r.toc == nil {
t.Fatal("nil TOC")
}
@@ -1681,15 +1648,15 @@ func (n numTOCEntries) check(t TestingT, r *Reader) {
func checks(s ...stargzCheck) []stargzCheck { return s }
type stargzCheck interface {
check(t TestingT, r *Reader)
check(t *testing.T, r *Reader)
}
type stargzCheckFn func(TestingT, *Reader)
type stargzCheckFn func(*testing.T, *Reader)
func (f stargzCheckFn) check(t TestingT, r *Reader) { f(t, r) }
func (f stargzCheckFn) check(t *testing.T, r *Reader) { f(t, r) }
func maxDepth(max int) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
e, ok := r.Lookup("")
if !ok {
t.Fatal("root directory not found")
@@ -1706,7 +1673,7 @@ func maxDepth(max int) stargzCheck {
})
}
func getMaxDepth(t TestingT, e *TOCEntry, current, limit int) (max int, rErr error) {
func getMaxDepth(t *testing.T, e *TOCEntry, current, limit int) (max int, rErr error) {
if current > limit {
return -1, fmt.Errorf("walkMaxDepth: exceeds limit: current:%d > limit:%d",
current, limit)
@@ -1728,7 +1695,7 @@ func getMaxDepth(t TestingT, e *TOCEntry, current, limit int) (max int, rErr err
}
func hasFileLen(file string, wantLen int) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == file {
if ent.Type != "reg" {
@@ -1744,7 +1711,7 @@ func hasFileLen(file string, wantLen int) stargzCheck {
}
func hasFileXattrs(file, name, value string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == file {
if ent.Type != "reg" {
@@ -1771,7 +1738,7 @@ func hasFileXattrs(file, name, value string) stargzCheck {
}
func hasFileDigest(file string, digest string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
ent, ok := r.Lookup(file)
if !ok {
t.Fatalf("didn't find TOCEntry for file %q", file)
@@ -1783,7 +1750,7 @@ func hasFileDigest(file string, digest string) stargzCheck {
}
func hasFileContentsWithPreRead(file string, offset int, want string, extra ...chunkInfo) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
extraMap := make(map[string]chunkInfo)
for _, e := range extra {
extraMap[e.name] = e
@@ -1830,7 +1797,7 @@ func hasFileContentsWithPreRead(file string, offset int, want string, extra ...c
}
func hasFileContentsRange(file string, offset int, want string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
f, err := r.OpenFile(file)
if err != nil {
t.Fatal(err)
@@ -1847,7 +1814,7 @@ func hasFileContentsRange(file string, offset int, want string) stargzCheck {
}
func hasChunkEntries(file string, wantChunks int) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
ent, ok := r.Lookup(file)
if !ok {
t.Fatalf("no file for %q", file)
@@ -1891,7 +1858,7 @@ func hasChunkEntries(file string, wantChunks int) stargzCheck {
}
func entryHasChildren(dir string, want ...string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
want := append([]string(nil), want...)
var got []string
ent, ok := r.Lookup(dir)
@@ -1910,7 +1877,7 @@ func entryHasChildren(dir string, want ...string) stargzCheck {
}
func hasDir(file string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == cleanEntryName(file) {
if ent.Type != "dir" {
@@ -1924,7 +1891,7 @@ func hasDir(file string) stargzCheck {
}
func hasDirLinkCount(file string, count int) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == cleanEntryName(file) {
if ent.Type != "dir" {
@@ -1942,7 +1909,7 @@ func hasDirLinkCount(file string, count int) stargzCheck {
}
func hasMode(file string, mode os.FileMode) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == cleanEntryName(file) {
if ent.Stat().Mode() != mode {
@@ -1957,7 +1924,7 @@ func hasMode(file string, mode os.FileMode) stargzCheck {
}
func hasSymlink(file, target string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == file {
if ent.Type != "symlink" {
@@ -1973,7 +1940,7 @@ func hasSymlink(file, target string) stargzCheck {
}
func lookupMatch(name string, want *TOCEntry) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
e, ok := r.Lookup(name)
if !ok {
t.Fatalf("failed to Lookup entry %q", name)
@@ -1986,7 +1953,7 @@ func lookupMatch(name string, want *TOCEntry) stargzCheck {
}
func hasEntryOwner(entry string, owner owner) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
ent, ok := r.Lookup(strings.TrimSuffix(entry, "/"))
if !ok {
t.Errorf("entry %q not found", entry)
@@ -2000,7 +1967,7 @@ func hasEntryOwner(entry string, owner owner) stargzCheck {
}
func mustSameEntry(files ...string) stargzCheck {
return stargzCheckFn(func(t TestingT, r *Reader) {
return stargzCheckFn(func(t *testing.T, r *Reader) {
var first *TOCEntry
for _, f := range files {
if first == nil {
@@ -2072,7 +2039,7 @@ func (f tarEntryFunc) appendTar(tw *tar.Writer, prefix string, format tar.Format
return f(tw, prefix, format)
}
func buildTar(t TestingT, ents []tarEntry, prefix string, opts ...interface{}) *io.SectionReader {
func buildTar(t *testing.T, ents []tarEntry, prefix string, opts ...interface{}) *io.SectionReader {
format := tar.FormatUnknown
for _, opt := range opts {
switch v := opt.(type) {
@@ -2281,7 +2248,7 @@ func noPrefetchLandmark() tarEntry {
})
}
func regDigest(t TestingT, name string, contentStr string, digestMap map[string]digest.Digest) tarEntry {
func regDigest(t *testing.T, name string, contentStr string, digestMap map[string]digest.Digest) tarEntry {
if digestMap == nil {
t.Fatalf("digest map mustn't be nil")
}
@@ -2351,7 +2318,7 @@ func (f fileInfoOnlyMode) ModTime() time.Time { return time.Now() }
func (f fileInfoOnlyMode) IsDir() bool { return os.FileMode(f).IsDir() }
func (f fileInfoOnlyMode) Sys() interface{} { return nil }
func CheckGzipHasStreams(t TestingT, b []byte, streams []int64) {
func CheckGzipHasStreams(t *testing.T, b []byte, streams []int64) {
if len(streams) == 0 {
return // nop
}
@@ -2389,7 +2356,7 @@ func CheckGzipHasStreams(t TestingT, b []byte, streams []int64) {
}
}
func GzipDiffIDOf(t TestingT, b []byte) string {
func GzipDiffIDOf(t *testing.T, b []byte) string {
h := sha256.New()
zr, err := gzip.NewReader(bytes.NewReader(b))
if err != nil {

View File

@@ -11,6 +11,7 @@ import (
"io"
"net/http"
"sync"
"time"
jose "github.com/go-jose/go-jose/v4"
)
@@ -56,12 +57,16 @@ func (s *StaticKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte,
// The returned KeySet is a long lived verifier that caches keys based on any
// keys change. Reuse a common remote key set instead of creating new ones as needed.
func NewRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet {
return newRemoteKeySet(ctx, jwksURL)
return newRemoteKeySet(ctx, jwksURL, time.Now)
}
func newRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet {
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *RemoteKeySet {
if now == nil {
now = time.Now
}
return &RemoteKeySet{
jwksURL: jwksURL,
now: now,
// For historical reasons, this package uses contexts for configuration, not just
// cancellation. In hindsight, this was a bad idea.
//
@@ -76,6 +81,7 @@ func newRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet {
// a jwks_uri endpoint.
type RemoteKeySet struct {
jwksURL string
now func() time.Time
// Used for configuration. Cancelation is ignored.
ctx context.Context

View File

@@ -1,11 +1,15 @@
package oidc
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
jose "github.com/go-jose/go-jose/v4"
@@ -141,6 +145,18 @@ func (p *Provider) newVerifier(keySet KeySet, config *Config) *IDTokenVerifier {
return NewVerifier(p.issuer, keySet, config)
}
func parseJWT(p string) ([]byte, error) {
parts := strings.Split(p, ".")
if len(parts) < 2 {
return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
}
return payload, nil
}
func contains(sli []string, ele string) bool {
for _, s := range sli {
if s == ele {
@@ -203,49 +219,12 @@ func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src
//
// token, err := verifier.Verify(ctx, rawIDToken)
func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) {
var supportedSigAlgs []jose.SignatureAlgorithm
for _, alg := range v.config.SupportedSigningAlgs {
supportedSigAlgs = append(supportedSigAlgs, jose.SignatureAlgorithm(alg))
}
if len(supportedSigAlgs) == 0 {
// If no algorithms were specified by both the config and discovery, default
// to the one mandatory algorithm "RS256".
supportedSigAlgs = []jose.SignatureAlgorithm{jose.RS256}
}
if v.config.InsecureSkipSignatureCheck {
// "none" is a required value to even parse a JWT with the "none" algorithm
// using go-jose.
supportedSigAlgs = append(supportedSigAlgs, "none")
}
// Parse and verify the signature first. This at least forces the user to have
// a valid, signed ID token before we do any other processing.
jws, err := jose.ParseSigned(rawIDToken, supportedSigAlgs)
// Throw out tokens with invalid claims before trying to verify the token. This lets
// us do cheap checks before possibly re-syncing keys.
payload, err := parseJWT(rawIDToken)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
switch len(jws.Signatures) {
case 0:
return nil, fmt.Errorf("oidc: id token not signed")
case 1:
default:
return nil, fmt.Errorf("oidc: multiple signatures on id token not supported")
}
sig := jws.Signatures[0]
var payload []byte
if v.config.InsecureSkipSignatureCheck {
// Yolo mode.
payload = jws.UnsafePayloadWithoutVerification()
} else {
// The JWT is attached here for the happy path to avoid the verifier from
// having to parse the JWT twice.
ctx = context.WithValue(ctx, parsedJWTKey, jws)
payload, err = v.keySet.VerifySignature(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("failed to verify signature: %v", err)
}
}
var token idToken
if err := json.Unmarshal(payload, &token); err != nil {
return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
@@ -275,7 +254,6 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
AccessTokenHash: token.AtHash,
claims: payload,
distributedClaims: distributedClaims,
sigAlgorithm: sig.Header.Algorithm,
}
// Check issuer.
@@ -328,6 +306,45 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
}
}
if v.config.InsecureSkipSignatureCheck {
return t, nil
}
var supportedSigAlgs []jose.SignatureAlgorithm
for _, alg := range v.config.SupportedSigningAlgs {
supportedSigAlgs = append(supportedSigAlgs, jose.SignatureAlgorithm(alg))
}
if len(supportedSigAlgs) == 0 {
// If no algorithms were specified by both the config and discovery, default
// to the one mandatory algorithm "RS256".
supportedSigAlgs = []jose.SignatureAlgorithm{jose.RS256}
}
jws, err := jose.ParseSigned(rawIDToken, supportedSigAlgs)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
switch len(jws.Signatures) {
case 0:
return nil, fmt.Errorf("oidc: id token not signed")
case 1:
default:
return nil, fmt.Errorf("oidc: multiple signatures on id token not supported")
}
sig := jws.Signatures[0]
t.sigAlgorithm = sig.Header.Algorithm
ctx = context.WithValue(ctx, parsedJWTKey, jws)
gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("failed to verify signature: %v", err)
}
// Ensure that the payload returned by the square actually matches the payload parsed earlier.
if !bytes.Equal(gotPayload, payload) {
return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
}
return t, nil
}

View File

@@ -1,60 +0,0 @@
# SPDX-License-Identifier: MPL-2.0
# Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
# Copyright (C) 2025 SUSE LLC
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
version: "2"
run:
build-tags:
- libpathrs
linters:
enable:
- asasalint
- asciicheck
- containedctx
- contextcheck
- errcheck
- errorlint
- exhaustive
- forcetypeassert
- godot
- goprintffuncname
- govet
- importas
- ineffassign
- makezero
- misspell
- musttag
- nilerr
- nilnesserr
- nilnil
- noctx
- prealloc
- revive
- staticcheck
- testifylint
- unconvert
- unparam
- unused
- usetesting
settings:
govet:
enable:
- nilness
testifylint:
enable-all: true
formatters:
enable:
- gofumpt
- goimports
settings:
goimports:
local-prefixes:
- github.com/cyphar/filepath-securejoin

View File

@@ -6,198 +6,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] ##
## [0.6.1] - 2025-11-19 ##
> At last up jumped the cunning spider, and fiercely held her fast.
### Fixed ###
- Our logic for deciding whether to use `openat2(2)` or fallback to an `O_PATH`
resolver would cache the result to avoid doing needless test runs of
`openat2(2)`. However, this causes issues when `pathrs-lite` is being used by
a program that applies new seccomp-bpf filters onto itself -- if the filter
denies `openat2(2)` then we would return that error rather than falling back
to the `O_PATH` resolver. To resolve this issue, we no longer cache the
result if `openat2(2)` was successful, only if there was an error.
- A file descriptor leak in our `openat2` wrapper (when doing the necessary
`dup` for `RESOLVE_IN_ROOT`) has been removed.
## [0.5.2] - 2025-11-19 ##
> "Will you walk into my parlour?" said a spider to a fly.
### Fixed ###
- Our logic for deciding whether to use `openat2(2)` or fallback to an `O_PATH`
resolver would cache the result to avoid doing needless test runs of
`openat2(2)`. However, this causes issues when `pathrs-lite` is being used by
a program that applies new seccomp-bpf filters onto itself -- if the filter
denies `openat2(2)` then we would return that error rather than falling back
to the `O_PATH` resolver. To resolve this issue, we no longer cache the
result if `openat2(2)` was successful, only if there was an error.
- A file descriptor leak in our `openat2` wrapper (when doing the necessary
`dup` for `RESOLVE_IN_ROOT`) has been removed.
## [0.6.0] - 2025-11-03 ##
> By the Power of Greyskull!
### Breaking ###
- The deprecated `MkdirAll`, `MkdirAllHandle`, `OpenInRoot`, `OpenatInRoot` and
`Reopen` wrappers have been removed. Please switch to using `pathrs-lite`
directly.
### Added ###
- `pathrs-lite` now has support for using libpathrs as a backend. This is
opt-in and can be enabled at build time with the `libpathrs` build tag. The
intention is to allow for downstream libraries and other projects to make use
of the pure-Go `github.com/cyphar/filepath-securejoin/pathrs-lite` package
and distributors can then opt-in to using `libpathrs` for the entire binary
if they wish.
## [0.5.1] - 2025-10-31 ##
> Spooky scary skeletons send shivers down your spine!
### Changed ###
- `openat2` can return `-EAGAIN` if it detects a possible attack in certain
scenarios (namely if there was a rename or mount while walking a path with a
`..` component). While this is necessary to avoid a denial-of-service in the
kernel, it does require retry loops in userspace.
In previous versions, `pathrs-lite` would retry `openat2` 32 times before
returning an error, but we've received user reports that this limit can be
hit on systems with very heavy load. In some synthetic benchmarks (testing
the worst-case of an attacker doing renames in a tight loop on every core of
a 16-core machine) we managed to get a ~3% failure rate in runc. We have
improved this situation in two ways:
* We have now increased this limit to 128, which should be good enough for
most use-cases without becoming a denial-of-service vector (the number of
syscalls called by the `O_PATH` resolver in a typical case is within the
same ballpark). The same benchmarks show a failure rate of ~0.12% which
(while not zero) is probably sufficient for most users.
* In addition, we now return a `unix.EAGAIN` error that is bubbled up and can
be detected by callers. This means that callers with stricter requirements
to avoid spurious errors can choose to do their own infinite `EAGAIN` retry
loop (though we would strongly recommend users use time-based deadlines in
such retry loops to avoid potentially unbounded denials-of-service).
## [0.5.0] - 2025-09-26 ##
> Let the past die. Kill it if you have to.
> **NOTE**: With this release, some parts of
> `github.com/cyphar/filepath-securejoin` are now licensed under the Mozilla
> Public License (version 2). Please see [COPYING.md][] as well as the the
> license header in each file for more details.
[COPYING.md]: ./COPYING.md
### Breaking ###
- The new API introduced in the [0.3.0][] release has been moved to a new
subpackage called `pathrs-lite`. This was primarily done to better indicate
the split between the new and old APIs, as well as indicate to users the
purpose of this subpackage (it is a less complete version of [libpathrs][]).
We have added some wrappers to the top-level package to ease the transition,
but those are deprecated and will be removed in the next minor release of
filepath-securejoin. Users should update their import paths.
This new subpackage has also been relicensed under the Mozilla Public License
(version 2), please see [COPYING.md][] for more details.
### Added ###
- Most of the key bits the safe `procfs` API have now been exported and are
available in `github.com/cyphar/filepath-securejoin/pathrs-lite/procfs`. At
the moment this primarily consists of a new `procfs.Handle` API:
* `OpenProcRoot` returns a new handle to `/proc`, endeavouring to make it
safe if possible (`subset=pid` to protect against mistaken write attacks
and leaks, as well as using `fsopen(2)` to avoid racing mount attacks).
`OpenUnsafeProcRoot` returns a handle without attempting to create one
with `subset=pid`, which makes it more dangerous to leak. Most users
should use `OpenProcRoot` (even if you need to use `ProcRoot` as the base
of an operation, as filepath-securejoin will internally open a handle when
necessary).
* The `(*procfs.Handle).Open*` family of methods lets you get a safe
`O_PATH` handle to subpaths within `/proc` for certain subpaths.
For `OpenThreadSelf`, the returned `ProcThreadSelfCloser` needs to be
called after you completely finish using the handle (this is necessary
because Go is multi-threaded and `ProcThreadSelf` references
`/proc/thread-self` which may disappear if we do not
`runtime.LockOSThread` -- `ProcThreadSelfCloser` is currently equivalent
to `runtime.UnlockOSThread`).
Note that you cannot open any `procfs` symlinks (most notably magic-links)
using this API. At the moment, filepath-securejoin does not support this
feature (but [libpathrs][] does).
* `ProcSelfFdReadlink` lets you get the in-kernel path representation of a
file descriptor (think `readlink("/proc/self/fd/...")`), except that we
verify that there aren't any tricky overmounts that could fool the
process.
Please be aware that the returned string is simply a snapshot at that
particular moment, and an attacker could move the file being pointed to.
In addition, complex namespace configurations could result in non-sensical
or confusing paths to be returned. The value received from this function
should only be used as secondary verification of some security property,
not as proof that a particular handle has a particular path.
The procfs handle used internally by the API is the same as the rest of
`filepath-securejoin` (for privileged programs this is usually a private
in-process `procfs` instance created with `fsopen(2)`).
As before, this is intended as a stop-gap before users migrate to
[libpathrs][], which provides a far more extensive safe `procfs` API and is
generally more robust.
- Previously, the hardened procfs implementation (used internally within
`Reopen` and `Open(at)InRoot`) only protected against overmount attacks on
systems with `openat2(2)` (Linux 5.6) or systems with `fsopen(2)` or
`open_tree(2)` (Linux 5.2) and programs with privileges to use them (with
some caveats about locked mounts that probably affect very few users). For
other users, an attacker with the ability to create malicious mounts (on most
systems, a sysadmin) could trick you into operating on files you didn't
expect. This attack only really makes sense in the context of container
runtime implementations.
This was considered a reasonable trade-off, as the long-term intention was to
get all users to just switch to [libpathrs][] if they wanted to use the safe
`procfs` API (which had more extensive protections, and is what these new
protections in `filepath-securejoin` are based on). However, as the API
is now being exported it seems unwise to advertise the API as "safe" if we do
not protect against known attacks.
The procfs API is now more protected against attackers on systems lacking the
aforementioned protections. However, the most comprehensive of these
protections effectively rely on [`statx(STATX_MNT_ID)`][statx.2] (Linux 5.8).
On older kernel versions, there is no effective protection (there is some
minimal protection against non-`procfs` filesystem components but a
sufficiently clever attacker can work around those). In addition,
`STATX_MNT_ID` is vulnerable to mount ID reuse attacks by sufficiently
motivated and privileged attackers -- this problem is mitigated with
`STATX_MNT_ID_UNIQUE` (Linux 6.8) but that raises the minimum kernel version
for more protection.
The fact that these protections are quite limited despite needing a fair bit
of extra code to handle was one of the primary reasons we did not initially
implement this in `filepath-securejoin` ([libpathrs][] supports all of this,
of course).
### Fixed ###
- RHEL 8 kernels have backports of `fsopen(2)` but in some testing we've found
that it has very bad (and very difficult to debug) performance issues, and so
we will explicitly refuse to use `fsopen(2)` if the running kernel version is
pre-5.2 and will instead fallback to `open("/proc")`.
[CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
[libpathrs]: https://github.com/cyphar/libpathrs
[statx.2]: https://www.man7.org/linux/man-pages/man2/statx.2.html
## [0.4.1] - 2025-01-28 ##
### Fixed ###
@@ -365,7 +173,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
safe to start migrating to as we have extensive tests ensuring they behave
correctly and are safe against various races and other attacks.
[libpathrs]: https://github.com/cyphar/libpathrs
[libpathrs]: https://github.com/openSUSE/libpathrs
[open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html
## [0.2.5] - 2024-05-03 ##
@@ -430,12 +238,7 @@ This is our first release of `github.com/cyphar/filepath-securejoin`,
containing a full implementation with a coverage of 93.5% (the only missing
cases are the error cases, which are hard to mocktest at the moment).
[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.6.1...HEAD
[0.6.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.6.0
[0.5.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.5.0
[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...HEAD
[0.4.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.6...v0.4.0
[0.3.6]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.5...v0.3.6

View File

@@ -1,447 +0,0 @@
## COPYING ##
`SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0`
This project is made up of code licensed under different licenses. Which code
you use will have an impact on whether only one or both licenses apply to your
usage of this library.
Note that **each file** in this project individually has a code comment at the
start describing the license of that particular file -- this is the most
accurate license information of this project; in case there is any conflict
between this document and the comment at the start of a file, the comment shall
take precedence. The only purpose of this document is to work around [a known
technical limitation of pkg.go.dev's license checking tool when dealing with
non-trivial project licenses][go75067].
[go75067]: https://go.dev/issue/75067
### `BSD-3-Clause` ###
At time of writing, the following files and directories are licensed under the
BSD-3-Clause license:
* `doc.go`
* `join*.go`
* `vfs.go`
* `internal/consts/*.go`
* `pathrs-lite/internal/gocompat/*.go`
* `pathrs-lite/internal/kernelversion/*.go`
The text of the BSD-3-Clause license used by this project is the following (the
text is also available from the [`LICENSE.BSD`](./LICENSE.BSD) file):
```
Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
### `MPL-2.0` ###
All other files (unless otherwise marked) are licensed under the Mozilla Public
License (version 2.0).
The text of the Mozilla Public License (version 2.0) is the following (the text
is also available from the [`LICENSE.MPL-2.0`](./LICENSE.MPL-2.0) file):
```
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
```

View File

@@ -1,373 +0,0 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -67,8 +67,7 @@ func SecureJoin(root, unsafePath string) (string, error) {
[libpathrs]: https://github.com/openSUSE/libpathrs
[go#20126]: https://github.com/golang/go/issues/20126
### <a name="new-api" /> New API ###
[#new-api]: #new-api
### New API ###
While we recommend users switch to [libpathrs][libpathrs] as soon as it has a
stable release, some methods implemented by libpathrs have been ported to this
@@ -166,19 +165,5 @@ after `MkdirAll`).
### License ###
`SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0`
Some of the code in this project is derived from Go, and is licensed under a
BSD 3-clause license (available in `LICENSE.BSD`). Other files (many of which
are derived from [libpathrs][libpathrs]) are licensed under the Mozilla Public
License version 2.0 (available in `LICENSE.MPL-2.0`). If you are using the
["New API" described above][#new-api], you are probably using code from files
released under this license.
Every source file in this project has a copyright header describing its
license. Please check the license headers of each file to see what license
applies to it.
See [COPYING.md](./COPYING.md) for some more details.
[umoci]: https://github.com/opencontainers/umoci
The license of this project is the same as Go, which is a BSD 3-clause license
available in the `LICENSE` file.

View File

@@ -1 +1 @@
0.6.1
0.4.1

View File

@@ -1,29 +0,0 @@
# SPDX-License-Identifier: MPL-2.0
# Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
# Copyright (C) 2025 SUSE LLC
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
comment:
layout: "condensed_header, reach, diff, components, condensed_files, condensed_footer"
require_changes: true
branches:
- main
coverage:
range: 60..100
status:
project:
default:
target: 85%
threshold: 0%
patch:
default:
target: auto
informational: true
github_checks:
annotations: false

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
@@ -16,13 +14,14 @@
// **not** safe against race conditions where an attacker changes the
// filesystem after (or during) the [SecureJoin] operation.
//
// The new API is available in the [pathrs-lite] subpackage, and provide
// protections against racing attackers as well as several other key
// protections against attacks often seen by container runtimes. As the name
// suggests, [pathrs-lite] is a stripped down (pure Go) reimplementation of
// [libpathrs]. The main APIs provided are [OpenInRoot], [MkdirAll], and
// [procfs.Handle] -- other APIs are not planned to be ported. The long-term
// goal is for users to migrate to [libpathrs] which is more fully-featured.
// The new API is made up of [OpenInRoot] and [MkdirAll] (and derived
// functions). These are safe against racing attackers and have several other
// protections that are not provided by the legacy API. There are many more
// operations that most programs expect to be able to do safely, but we do not
// provide explicit support for them because we want to encourage users to
// switch to [libpathrs](https://github.com/openSUSE/libpathrs) which is a
// cross-language next-generation library that is entirely designed around
// operating on paths safely.
//
// securejoin has been used by several container runtimes (Docker, runc,
// Kubernetes, etc) for quite a few years as a de-facto standard for operating
@@ -32,16 +31,9 @@
// API as soon as possible (or even better, switch to libpathrs).
//
// This project was initially intended to be included in the Go standard
// library, but it was rejected (see https://go.dev/issue/20126). Much later,
// [os.Root] was added to the Go stdlib that shares some of the goals of
// filepath-securejoin. However, its design is intended to work like
// openat2(RESOLVE_BENEATH) which does not fit the usecase of container
// runtimes and most system tools.
//
// [pathrs-lite]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite
// [libpathrs]: https://github.com/openSUSE/libpathrs
// [OpenInRoot]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite#OpenInRoot
// [MkdirAll]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite#MkdirAll
// [procfs.Handle]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle
// [os.Root]: https:///pkg.go.dev/os#Root
// library, but [it was rejected](https://go.dev/issue/20126). There is now a
// [new Go proposal](https://go.dev/issue/67002) for a safe path resolution API
// that shares some of the goals of filepath-securejoin. However, that design
// is intended to work like `openat2(RESOLVE_BENEATH)` which does not fit the
// usecase of container runtimes and most system tools.
package securejoin

View File

@@ -1,19 +1,18 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && go1.20
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocompat
package securejoin
import (
"fmt"
)
// WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
// wrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
// is only guaranteed to give you baseErr.
func WrapBaseError(baseErr, extraErr error) error {
func wrapBaseError(baseErr, extraErr error) error {
return fmt.Errorf("%w: %w", extraErr, baseErr)
}

View File

@@ -1,12 +1,10 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !go1.20
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocompat
package securejoin
import (
"fmt"
@@ -29,10 +27,10 @@ func (err wrappedError) Error() string {
return fmt.Sprintf("%v: %v", err.isError, err.inner)
}
// WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
// wrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
// is only guaranteed to give you baseErr.
func WrapBaseError(baseErr, extraErr error) error {
func wrapBaseError(baseErr, extraErr error) error {
return wrappedError{
inner: baseErr,
isError: extraErr,

View File

@@ -0,0 +1,32 @@
//go:build linux && go1.21
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package securejoin
import (
"slices"
"sync"
)
func slices_DeleteFunc[S ~[]E, E any](slice S, delFn func(E) bool) S {
return slices.DeleteFunc(slice, delFn)
}
func slices_Contains[S ~[]E, E comparable](slice S, val E) bool {
return slices.Contains(slice, val)
}
func slices_Clone[S ~[]E, E any](slice S) S {
return slices.Clone(slice)
}
func sync_OnceValue[T any](f func() T) func() T {
return sync.OnceValue(f)
}
func sync_OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
return sync.OnceValues(f)
}

View File

@@ -0,0 +1,124 @@
//go:build linux && !go1.21
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package securejoin
import (
"sync"
)
// These are very minimal implementations of functions that appear in Go 1.21's
// stdlib, included so that we can build on older Go versions. Most are
// borrowed directly from the stdlib, and a few are modified to be "obviously
// correct" without needing to copy too many other helpers.
// clearSlice is equivalent to the builtin clear from Go 1.21.
// Copied from the Go 1.24 stdlib implementation.
func clearSlice[S ~[]E, E any](slice S) {
var zero E
for i := range slice {
slice[i] = zero
}
}
// Copied from the Go 1.24 stdlib implementation.
func slices_IndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
for i := range s {
if f(s[i]) {
return i
}
}
return -1
}
// Copied from the Go 1.24 stdlib implementation.
func slices_DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
i := slices_IndexFunc(s, del)
if i == -1 {
return s
}
// Don't start copying elements until we find one to delete.
for j := i + 1; j < len(s); j++ {
if v := s[j]; !del(v) {
s[i] = v
i++
}
}
clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC
return s[:i]
}
// Similar to the stdlib slices.Contains, except that we don't have
// slices.Index so we need to use slices.IndexFunc for this non-Func helper.
func slices_Contains[S ~[]E, E comparable](s S, v E) bool {
return slices_IndexFunc(s, func(e E) bool { return e == v }) >= 0
}
// Copied from the Go 1.24 stdlib implementation.
func slices_Clone[S ~[]E, E any](s S) S {
// Preserve nil in case it matters.
if s == nil {
return nil
}
return append(S([]E{}), s...)
}
// Copied from the Go 1.24 stdlib implementation.
func sync_OnceValue[T any](f func() T) func() T {
var (
once sync.Once
valid bool
p any
result T
)
g := func() {
defer func() {
p = recover()
if !valid {
panic(p)
}
}()
result = f()
f = nil
valid = true
}
return func() T {
once.Do(g)
if !valid {
panic(p)
}
return result
}
}
// Copied from the Go 1.24 stdlib implementation.
func sync_OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
var (
once sync.Once
valid bool
p any
r1 T1
r2 T2
)
g := func() {
defer func() {
p = recover()
if !valid {
panic(p)
}
}()
r1, r2 = f()
f = nil
valid = true
}
return func() (T1, T2) {
once.Do(g)
if !valid {
panic(p)
}
return r1, r2
}
}

View File

@@ -1,15 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
// Copyright (C) 2017-2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package consts contains the definitions of internal constants used
// throughout filepath-securejoin.
package consts
// MaxSymlinkLimit is the maximum number of symlinks that can be encountered
// during a single lookup before returning -ELOOP. At time of writing, Linux
// has an internal limit of 40.
const MaxSymlinkLimit = 255

View File

@@ -1,5 +1,3 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
// Copyright (C) 2017-2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
@@ -13,10 +11,10 @@ import (
"path/filepath"
"strings"
"syscall"
"github.com/cyphar/filepath-securejoin/internal/consts"
)
const maxSymlinkLimit = 255
// IsNotExist tells you if err is an error that implies that either the path
// accessed does not exist (or path components don't exist). This is
// effectively a more broad version of [os.IsNotExist].
@@ -51,13 +49,12 @@ func hasDotDot(path string) bool {
return strings.Contains("/"+path+"/", "/../")
}
// SecureJoinVFS joins the two given path components (similar to
// [filepath.Join]) except that the returned path is guaranteed to be scoped
// inside the provided root path (when evaluated). Any symbolic links in the
// path are evaluated with the given root treated as the root of the
// filesystem, similar to a chroot. The filesystem state is evaluated through
// the given [VFS] interface (if nil, the standard [os].* family of functions
// are used).
// SecureJoinVFS joins the two given path components (similar to [filepath.Join]) except
// that the returned path is guaranteed to be scoped inside the provided root
// path (when evaluated). Any symbolic links in the path are evaluated with the
// given root treated as the root of the filesystem, similar to a chroot. The
// filesystem state is evaluated through the given [VFS] interface (if nil, the
// standard [os].* family of functions are used).
//
// Note that the guarantees provided by this function only apply if the path
// components in the returned string are not modified (in other words are not
@@ -81,7 +78,7 @@ func hasDotDot(path string) bool {
// fully resolved using [filepath.EvalSymlinks] or otherwise constructed to
// avoid containing symlink components. Of course, the root also *must not* be
// attacker-controlled.
func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { //nolint:revive // name is part of public API
func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) {
// The root path must not contain ".." components, otherwise when we join
// the subpath we will end up with a weird path. We could work around this
// in other ways but users shouldn't be giving us non-lexical root paths in
@@ -141,7 +138,7 @@ func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { //nolint:
// It's a symlink, so get its contents and expand it by prepending it
// to the yet-unparsed path.
linksWalked++
if linksWalked > consts.MaxSymlinkLimit {
if linksWalked > maxSymlinkLimit {
return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP}
}

View File

@@ -1,15 +1,10 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gopathrs
package securejoin
import (
"errors"
@@ -20,12 +15,6 @@ import (
"strings"
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/internal/consts"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
)
type symlinkStackEntry struct {
@@ -123,12 +112,12 @@ func (s *symlinkStack) push(dir *os.File, remainingPath, linkTarget string) erro
return nil
}
// Split the link target and clean up any "" parts.
linkTargetParts := gocompat.SlicesDeleteFunc(
linkTargetParts := slices_DeleteFunc(
strings.Split(linkTarget, "/"),
func(part string) bool { return part == "" || part == "." })
// Copy the directory so the caller doesn't close our copy.
dirCopy, err := fd.Dup(dir)
dirCopy, err := dupFile(dir)
if err != nil {
return err
}
@@ -166,15 +155,15 @@ func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) {
return tailEntry.dir, tailEntry.remainingPath, true
}
// PartialLookupInRoot tries to lookup as much of the request path as possible
// partialLookupInRoot tries to lookup as much of the request path as possible
// within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing
// component of the requested path, returning a file handle to the final
// existing component and a string containing the remaining path components.
func PartialLookupInRoot(root fd.Fd, unsafePath string) (*os.File, string, error) {
func partialLookupInRoot(root *os.File, unsafePath string) (*os.File, string, error) {
return lookupInRoot(root, unsafePath, true)
}
func completeLookupInRoot(root fd.Fd, unsafePath string) (*os.File, error) {
func completeLookupInRoot(root *os.File, unsafePath string) (*os.File, error) {
handle, remainingPath, err := lookupInRoot(root, unsafePath, false)
if remainingPath != "" && err == nil {
// should never happen
@@ -185,7 +174,7 @@ func completeLookupInRoot(root fd.Fd, unsafePath string) (*os.File, error) {
return handle, err
}
func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) {
func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) {
unsafePath = filepath.ToSlash(unsafePath) // noop
// This is very similar to SecureJoin, except that we operate on the
@@ -193,25 +182,20 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
// managed open, along with the remaining path components not opened.
// Try to use openat2 if possible.
//
// NOTE: If openat2(2) works normally but fails for this lookup, it is
// probably not a good idea to fall-back to the O_PATH resolver. An
// attacker could find a bug in the O_PATH resolver and uncontionally
// falling back to the O_PATH resolver would form a downgrade attack.
if handle, remainingPath, err := lookupOpenat2(root, unsafePath, partial); err == nil || linux.HasOpenat2() {
return handle, remainingPath, err
if hasOpenat2() {
return lookupOpenat2(root, unsafePath, partial)
}
// Get the "actual" root path from /proc/self/fd. This is necessary if the
// root is some magic-link like /proc/$pid/root, in which case we want to
// make sure when we do procfs.CheckProcSelfFdPath that we are using the
// correct root path.
logicalRootPath, err := procfs.ProcSelfFdReadlink(root)
// make sure when we do checkProcSelfFdPath that we are using the correct
// root path.
logicalRootPath, err := procSelfFdReadlink(root)
if err != nil {
return nil, "", fmt.Errorf("get real root path: %w", err)
}
currentDir, err := fd.Dup(root)
currentDir, err := dupFile(root)
if err != nil {
return nil, "", fmt.Errorf("clone root fd: %w", err)
}
@@ -276,7 +260,7 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err)
}
// Jump to root.
rootClone, err := fd.Dup(root)
rootClone, err := dupFile(root)
if err != nil {
return nil, "", fmt.Errorf("clone root fd: %w", err)
}
@@ -287,21 +271,21 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
}
// Try to open the next component.
nextDir, err := fd.Openat(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
switch err {
case nil:
nextDir, err := openatFile(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
switch {
case err == nil:
st, err := nextDir.Stat()
if err != nil {
_ = nextDir.Close()
return nil, "", fmt.Errorf("stat component %q: %w", part, err)
}
switch st.Mode() & os.ModeType { //nolint:exhaustive // just a glorified if statement
switch st.Mode() & os.ModeType {
case os.ModeSymlink:
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See
// Linux commit 65cfc6722361 ("readlinkat(), fchownat() and
// fstatat() with empty relative pathnames").
linkDest, err := fd.Readlinkat(nextDir, "")
linkDest, err := readlinkatFile(nextDir, "")
// We don't need the handle anymore.
_ = nextDir.Close()
if err != nil {
@@ -309,7 +293,7 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
}
linksWalked++
if linksWalked > consts.MaxSymlinkLimit {
if linksWalked > maxSymlinkLimit {
return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP}
}
@@ -323,7 +307,7 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
// Absolute symlinks reset any work we've already done.
if path.IsAbs(linkDest) {
// Jump to root.
rootClone, err := fd.Dup(root)
rootClone, err := dupFile(root)
if err != nil {
return nil, "", fmt.Errorf("clone root fd: %w", err)
}
@@ -351,12 +335,12 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
// rename or mount on the system.
if part == ".." {
// Make sure the root hasn't moved.
if err := procfs.CheckProcSelfFdPath(logicalRootPath, root); err != nil {
if err := checkProcSelfFdPath(logicalRootPath, root); err != nil {
return nil, "", fmt.Errorf("root path moved during lookup: %w", err)
}
// Make sure the path is what we expect.
fullPath := logicalRootPath + nextPath
if err := procfs.CheckProcSelfFdPath(fullPath, currentDir); err != nil {
if err := checkProcSelfFdPath(fullPath, currentDir); err != nil {
return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err)
}
}
@@ -387,7 +371,7 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
// context of openat2, a trailing slash and a trailing "/." are completely
// equivalent.
if strings.HasSuffix(unsafePath, "/") {
nextDir, err := fd.Openat(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
nextDir, err := openatFile(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
if err != nil {
if !partial {
_ = currentDir.Close()

View File

@@ -1,15 +1,10 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gopathrs
package securejoin
import (
"errors"
@@ -19,16 +14,12 @@ import (
"strings"
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
)
// ErrInvalidMode is returned from [MkdirAll] when the requested mode is
// invalid.
var ErrInvalidMode = errors.New("invalid permission mode")
var (
errInvalidMode = errors.New("invalid permission mode")
errPossibleAttack = errors.New("possible attack detected")
)
// modePermExt is like os.ModePerm except that it also includes the set[ug]id
// and sticky bits.
@@ -48,11 +39,11 @@ func toUnixMode(mode os.FileMode) (uint32, error) {
}
// We don't allow file type bits.
if mode&os.ModeType != 0 {
return 0, fmt.Errorf("%w %+.3o (%s): type bits not permitted", ErrInvalidMode, mode, mode)
return 0, fmt.Errorf("%w %+.3o (%s): type bits not permitted", errInvalidMode, mode, mode)
}
// We don't allow other unknown modes.
if mode&^modePermExt != 0 || sysMode&unix.S_IFMT != 0 {
return 0, fmt.Errorf("%w %+.3o (%s): unknown mode bits", ErrInvalidMode, mode, mode)
return 0, fmt.Errorf("%w %+.3o (%s): unknown mode bits", errInvalidMode, mode, mode)
}
return sysMode, nil
}
@@ -75,8 +66,6 @@ func toUnixMode(mode os.FileMode) (uint32, error) {
// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after
// doing [MkdirAll]. If you intend to open the directory after creating it, you
// should use MkdirAllHandle.
//
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.File, Err error) {
unixMode, err := toUnixMode(mode)
if err != nil {
@@ -87,11 +76,11 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
// users it seems more prudent to return an error so users notice that
// these bits will not be set.
if unixMode&^0o1777 != 0 {
return nil, fmt.Errorf("%w for mkdir %+.3o: suid and sgid are ignored by mkdir", ErrInvalidMode, mode)
return nil, fmt.Errorf("%w for mkdir %+.3o: suid and sgid are ignored by mkdir", errInvalidMode, mode)
}
// Try to open as much of the path as possible.
currentDir, remainingPath, err := PartialLookupInRoot(root, unsafePath)
currentDir, remainingPath, err := partialLookupInRoot(root, unsafePath)
defer func() {
if Err != nil {
_ = currentDir.Close()
@@ -113,24 +102,24 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
//
// This is mostly a quality-of-life check, because mkdir will simply fail
// later if the attacker deletes the tree after this check.
if err := fd.IsDeadInode(currentDir); err != nil {
if err := isDeadInode(currentDir); err != nil {
return nil, fmt.Errorf("finding existing subpath of %q: %w", unsafePath, err)
}
// Re-open the path to match the O_DIRECTORY reopen loop later (so that we
// always return a non-O_PATH handle). We also check that we actually got a
// directory.
if reopenDir, err := procfs.ReopenFd(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) {
if reopenDir, err := Reopen(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) {
return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR)
} else if err != nil {
return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err)
} else { //nolint:revive // indent-error-flow lint doesn't make sense here
} else {
_ = currentDir.Close()
currentDir = reopenDir
}
remainingParts := strings.Split(remainingPath, string(filepath.Separator))
if gocompat.SlicesContains(remainingParts, "..") {
if slices_Contains(remainingParts, "..") {
// The path contained ".." components after the end of the "real"
// components. We could try to safely resolve ".." here but that would
// add a bunch of extra logic for something that it's not clear even
@@ -161,12 +150,12 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
if err := unix.Mkdirat(int(currentDir.Fd()), part, unixMode); err != nil && !errors.Is(err, unix.EEXIST) {
err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err}
// Make the error a bit nicer if the directory is dead.
if deadErr := fd.IsDeadInode(currentDir); deadErr != nil {
if deadErr := isDeadInode(currentDir); deadErr != nil {
// TODO: Once we bump the minimum Go version to 1.20, we can use
// multiple %w verbs for this wrapping. For now we need to use a
// compatibility shim for older Go versions.
// err = fmt.Errorf("%w (%w)", err, deadErr)
err = gocompat.WrapBaseError(err, deadErr)
//err = fmt.Errorf("%w (%w)", err, deadErr)
err = wrapBaseError(err, deadErr)
}
return nil, err
}
@@ -174,13 +163,13 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
// Get a handle to the next component. O_DIRECTORY means we don't need
// to use O_PATH.
var nextDir *os.File
if linux.HasOpenat2() {
nextDir, err = openat2(currentDir, part, &unix.OpenHow{
if hasOpenat2() {
nextDir, err = openat2File(currentDir, part, &unix.OpenHow{
Flags: unix.O_NOFOLLOW | unix.O_DIRECTORY | unix.O_CLOEXEC,
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV,
})
} else {
nextDir, err = fd.Openat(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
nextDir, err = openatFile(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
}
if err != nil {
return nil, err
@@ -210,3 +199,38 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
}
return currentDir, nil
}
// MkdirAll is a race-safe alternative to the [os.MkdirAll] function,
// where the new directory is guaranteed to be within the root directory (if an
// attacker can move directories from inside the root to outside the root, the
// created directory tree might be outside of the root but the key constraint
// is that at no point will we walk outside of the directory tree we are
// creating).
//
// Effectively, MkdirAll(root, unsafePath, mode) is equivalent to
//
// path, _ := securejoin.SecureJoin(root, unsafePath)
// err := os.MkdirAll(path, mode)
//
// But is much safer. The above implementation is unsafe because if an attacker
// can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is
// possible for MkdirAll to resolve unsafe symlink components and create
// directories outside of the root.
//
// If you plan to open the directory after you have created it or want to use
// an open directory handle as the root, you should use [MkdirAllHandle] instead.
// This function is a wrapper around [MkdirAllHandle].
func MkdirAll(root, unsafePath string, mode os.FileMode) error {
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
if err != nil {
return err
}
defer rootDir.Close()
f, err := MkdirAllHandle(rootDir, unsafePath, mode)
if err != nil {
return err
}
_ = f.Close()
return nil
}

View File

@@ -0,0 +1,103 @@
//go:build linux
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package securejoin
import (
"fmt"
"os"
"strconv"
"golang.org/x/sys/unix"
)
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
// using an *[os.File] handle, to ensure that the correct root directory is used.
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
handle, err := completeLookupInRoot(root, unsafePath)
if err != nil {
return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err}
}
return handle, nil
}
// OpenInRoot safely opens the provided unsafePath within the root.
// Effectively, OpenInRoot(root, unsafePath) is equivalent to
//
// path, _ := securejoin.SecureJoin(root, unsafePath)
// handle, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)
//
// But is much safer. The above implementation is unsafe because if an attacker
// can modify the filesystem tree between [SecureJoin] and [os.OpenFile], it is
// possible for the returned file to be outside of the root.
//
// Note that the returned handle is an O_PATH handle, meaning that only a very
// limited set of operations will work on the handle. This is done to avoid
// accidentally opening an untrusted file that could cause issues (such as a
// disconnected TTY that could cause a DoS, or some other issue). In order to
// use the returned handle, you can "upgrade" it to a proper handle using
// [Reopen].
func OpenInRoot(root, unsafePath string) (*os.File, error) {
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
if err != nil {
return nil, err
}
defer rootDir.Close()
return OpenatInRoot(rootDir, unsafePath)
}
// Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd.
// Reopen(file, flags) is effectively equivalent to
//
// fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd())
// os.OpenFile(fdPath, flags|unix.O_CLOEXEC)
//
// But with some extra hardenings to ensure that we are not tricked by a
// maliciously-configured /proc mount. While this attack scenario is not
// common, in container runtimes it is possible for higher-level runtimes to be
// tricked into configuring an unsafe /proc that can be used to attack file
// operations. See [CVE-2019-19921] for more details.
//
// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
func Reopen(handle *os.File, flags int) (*os.File, error) {
procRoot, err := getProcRoot()
if err != nil {
return nil, err
}
// We can't operate on /proc/thread-self/fd/$n directly when doing a
// re-open, so we need to open /proc/thread-self/fd and then open a single
// final component.
procFdDir, closer, err := procThreadSelf(procRoot, "fd/")
if err != nil {
return nil, fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err)
}
defer procFdDir.Close()
defer closer()
// Try to detect if there is a mount on top of the magic-link we are about
// to open. If we are using unsafeHostProcRoot(), this could change after
// we check it (and there's nothing we can do about that) but for
// privateProcRoot() this should be guaranteed to be safe (at least since
// Linux 5.12[1], when anonymous mount namespaces were completely isolated
// from external mounts including mount propagation events).
//
// [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts
// onto targets that reside on shared mounts").
fdStr := strconv.Itoa(int(handle.Fd()))
if err := checkSymlinkOvermount(procRoot, procFdDir, fdStr); err != nil {
return nil, fmt.Errorf("check safety of /proc/thread-self/fd/%s magiclink: %w", fdStr, err)
}
flags |= unix.O_CLOEXEC
// Rather than just wrapping openatFile, open-code it so we can copy
// handle.Name().
reopenFd, err := unix.Openat(int(procFdDir.Fd()), fdStr, flags, 0)
if err != nil {
return nil, fmt.Errorf("reopen fd %d: %w", handle.Fd(), err)
}
return os.NewFile(uintptr(reopenFd), handle.Name()), nil
}

View File

@@ -0,0 +1,127 @@
//go:build linux
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package securejoin
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/sys/unix"
)
var hasOpenat2 = sync_OnceValue(func() bool {
fd, err := unix.Openat2(unix.AT_FDCWD, ".", &unix.OpenHow{
Flags: unix.O_PATH | unix.O_CLOEXEC,
Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_IN_ROOT,
})
if err != nil {
return false
}
_ = unix.Close(fd)
return true
})
func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool {
// RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve
// ".." while a mount or rename occurs anywhere on the system. This could
// happen spuriously, or as the result of an attacker trying to mess with
// us during lookup.
//
// In addition, scoped lookups have a "safety check" at the end of
// complete_walk which will return -EXDEV if the final path is not in the
// root.
return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 &&
(errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV))
}
const scopedLookupMaxRetries = 10
func openat2File(dir *os.File, path string, how *unix.OpenHow) (*os.File, error) {
fullPath := dir.Name() + "/" + path
// Make sure we always set O_CLOEXEC.
how.Flags |= unix.O_CLOEXEC
var tries int
for tries < scopedLookupMaxRetries {
fd, err := unix.Openat2(int(dir.Fd()), path, how)
if err != nil {
if scopedLookupShouldRetry(how, err) {
// We retry a couple of times to avoid the spurious errors, and
// if we are being attacked then returning -EAGAIN is the best
// we can do.
tries++
continue
}
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err}
}
// If we are using RESOLVE_IN_ROOT, the name we generated may be wrong.
// NOTE: The procRoot code MUST NOT use RESOLVE_IN_ROOT, otherwise
// you'll get infinite recursion here.
if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT {
if actualPath, err := rawProcSelfFdReadlink(fd); err == nil {
fullPath = actualPath
}
}
return os.NewFile(uintptr(fd), fullPath), nil
}
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: errPossibleAttack}
}
func lookupOpenat2(root *os.File, unsafePath string, partial bool) (*os.File, string, error) {
if !partial {
file, err := openat2File(root, unsafePath, &unix.OpenHow{
Flags: unix.O_PATH | unix.O_CLOEXEC,
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
})
return file, "", err
}
return partialLookupOpenat2(root, unsafePath)
}
// partialLookupOpenat2 is an alternative implementation of
// partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a
// handle to the deepest existing child of the requested path within the root.
func partialLookupOpenat2(root *os.File, unsafePath string) (*os.File, string, error) {
// TODO: Implement this as a git-bisect-like binary search.
unsafePath = filepath.ToSlash(unsafePath) // noop
endIdx := len(unsafePath)
var lastError error
for endIdx > 0 {
subpath := unsafePath[:endIdx]
handle, err := openat2File(root, subpath, &unix.OpenHow{
Flags: unix.O_PATH | unix.O_CLOEXEC,
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
})
if err == nil {
// Jump over the slash if we have a non-"" remainingPath.
if endIdx < len(unsafePath) {
endIdx += 1
}
// We found a subpath!
return handle, unsafePath[endIdx:], lastError
}
if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) {
// That path doesn't exist, let's try the next directory up.
endIdx = strings.LastIndexByte(subpath, '/')
lastError = err
continue
}
return nil, "", fmt.Errorf("open subpath: %w", err)
}
// If we couldn't open anything, the whole subpath is missing. Return a
// copy of the root fd so that the caller doesn't close this one by
// accident.
rootClone, err := dupFile(root)
if err != nil {
return nil, "", err
}
return rootClone, unsafePath, lastError
}

View File

@@ -0,0 +1,59 @@
//go:build linux
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package securejoin
import (
"os"
"path/filepath"
"golang.org/x/sys/unix"
)
func dupFile(f *os.File) (*os.File, error) {
fd, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0)
if err != nil {
return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err)
}
return os.NewFile(uintptr(fd), f.Name()), nil
}
func openatFile(dir *os.File, path string, flags int, mode int) (*os.File, error) {
// Make sure we always set O_CLOEXEC.
flags |= unix.O_CLOEXEC
fd, err := unix.Openat(int(dir.Fd()), path, flags, uint32(mode))
if err != nil {
return nil, &os.PathError{Op: "openat", Path: dir.Name() + "/" + path, Err: err}
}
// All of the paths we use with openatFile(2) are guaranteed to be
// lexically safe, so we can use path.Join here.
fullPath := filepath.Join(dir.Name(), path)
return os.NewFile(uintptr(fd), fullPath), nil
}
func fstatatFile(dir *os.File, path string, flags int) (unix.Stat_t, error) {
var stat unix.Stat_t
if err := unix.Fstatat(int(dir.Fd()), path, &stat, flags); err != nil {
return stat, &os.PathError{Op: "fstatat", Path: dir.Name() + "/" + path, Err: err}
}
return stat, nil
}
func readlinkatFile(dir *os.File, path string) (string, error) {
size := 4096
for {
linkBuf := make([]byte, size)
n, err := unix.Readlinkat(int(dir.Fd()), path, linkBuf)
if err != nil {
return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err}
}
if n != size {
return string(linkBuf[:n]), nil
}
// Possible truncation, resize the buffer.
size *= 2
}
}

View File

@@ -1,35 +0,0 @@
## `pathrs-lite` ##
`github.com/cyphar/filepath-securejoin/pathrs-lite` provides a minimal **pure
Go** implementation of the core bits of [libpathrs][]. This is not intended to
be a complete replacement for libpathrs, instead it is mainly intended to be
useful as a transition tool for existing Go projects.
`pathrs-lite` also provides a very easy way to switch to `libpathrs` (even for
downstreams where `pathrs-lite` is being used in a third-party package and is
not interested in using CGo). At build time, if you use the `libpathrs` build
tag then `pathrs-lite` will use `libpathrs` directly instead of the pure Go
implementation. The two backends are functionally equivalent (and we have
integration tests to verify this), so this migration should be very easy with
no user-visible impact.
[libpathrs]: https://github.com/cyphar/libpathrs
### License ###
Most of this subpackage is licensed under the Mozilla Public License (version
2.0). For more information, see the top-level [COPYING.md][] and
[LICENSE.MPL-2.0][] files, as well as the individual license headers for each
file.
```
Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
Copyright (C) 2024-2025 SUSE LLC
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
```
[COPYING.md]: ../COPYING.md
[LICENSE.MPL-2.0]: ../LICENSE.MPL-2.0

View File

@@ -1,16 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Package pathrs (pathrs-lite) is a less complete pure Go implementation of
// some of the APIs provided by [libpathrs].
//
// [libpathrs]: https://github.com/cyphar/libpathrs
package pathrs

View File

@@ -1,30 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Package assert provides some basic assertion helpers for Go.
package assert
import (
"fmt"
)
// Assert panics if the predicate is false with the provided argument.
func Assert(predicate bool, msg any) {
if !predicate {
panic(msg)
}
}
// Assertf panics if the predicate is false and formats the message using the
// same formatting as [fmt.Printf].
//
// [fmt.Printf]: https://pkg.go.dev/fmt#Printf
func Assertf(predicate bool, fmtMsg string, args ...any) {
Assert(predicate, fmt.Sprintf(fmtMsg, args...))
}

View File

@@ -1,41 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Package internal contains unexported common code for filepath-securejoin.
package internal
import (
"errors"
"golang.org/x/sys/unix"
)
type xdevErrorish struct {
description string
}
func (err xdevErrorish) Error() string { return err.description }
func (err xdevErrorish) Is(target error) bool { return target == unix.EXDEV }
var (
// ErrPossibleAttack indicates that some attack was detected.
ErrPossibleAttack error = xdevErrorish{"possible attack detected"}
// ErrPossibleBreakout indicates that during an operation we ended up in a
// state that could be a breakout but we detected it.
ErrPossibleBreakout error = xdevErrorish{"possible breakout detected"}
// ErrInvalidDirectory indicates an unlinked directory.
ErrInvalidDirectory = errors.New("wandered into deleted directory")
// ErrDeletedInode indicates an unlinked file (non-directory).
ErrDeletedInode = errors.New("cannot verify path of deleted inode")
)

View File

@@ -1,148 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package fd
import (
"fmt"
"os"
"path/filepath"
"runtime"
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
)
// prepareAtWith returns -EBADF (an invalid fd) if dir is nil, otherwise using
// the dir.Fd(). We use -EBADF because in filepath-securejoin we generally
// don't want to allow relative-to-cwd paths. The returned path is an
// *informational* string that describes a reasonable pathname for the given
// *at(2) arguments. You must not use the full path for any actual filesystem
// operations.
func prepareAt(dir Fd, path string) (dirFd int, unsafeUnmaskedPath string) {
dirFd, dirPath := -int(unix.EBADF), "."
if dir != nil {
dirFd, dirPath = int(dir.Fd()), dir.Name()
}
if !filepath.IsAbs(path) {
// only prepend the dirfd path for relative paths
path = dirPath + "/" + path
}
// NOTE: If path is "." or "", the returned path won't be filepath.Clean,
// but that's okay since this path is either used for errors (in which case
// a trailing "/" or "/." is important information) or will be
// filepath.Clean'd later (in the case of fd.Openat).
return dirFd, path
}
// Openat is an [Fd]-based wrapper around unix.Openat.
func Openat(dir Fd, path string, flags int, mode int) (*os.File, error) { //nolint:unparam // wrapper func
dirFd, fullPath := prepareAt(dir, path)
// Make sure we always set O_CLOEXEC.
flags |= unix.O_CLOEXEC
fd, err := unix.Openat(dirFd, path, flags, uint32(mode))
if err != nil {
return nil, &os.PathError{Op: "openat", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
// openat is only used with lexically-safe paths so we can use
// filepath.Clean here, and also the path itself is not going to be used
// for actual path operations.
fullPath = filepath.Clean(fullPath)
return os.NewFile(uintptr(fd), fullPath), nil
}
// Fstatat is an [Fd]-based wrapper around unix.Fstatat.
func Fstatat(dir Fd, path string, flags int) (unix.Stat_t, error) {
dirFd, fullPath := prepareAt(dir, path)
var stat unix.Stat_t
if err := unix.Fstatat(dirFd, path, &stat, flags); err != nil {
return stat, &os.PathError{Op: "fstatat", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
return stat, nil
}
// Faccessat is an [Fd]-based wrapper around unix.Faccessat.
func Faccessat(dir Fd, path string, mode uint32, flags int) error {
dirFd, fullPath := prepareAt(dir, path)
err := unix.Faccessat(dirFd, path, mode, flags)
if err != nil {
err = &os.PathError{Op: "faccessat", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
return err
}
// Readlinkat is an [Fd]-based wrapper around unix.Readlinkat.
func Readlinkat(dir Fd, path string) (string, error) {
dirFd, fullPath := prepareAt(dir, path)
size := 4096
for {
linkBuf := make([]byte, size)
n, err := unix.Readlinkat(dirFd, path, linkBuf)
if err != nil {
return "", &os.PathError{Op: "readlinkat", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
if n != size {
return string(linkBuf[:n]), nil
}
// Possible truncation, resize the buffer.
size *= 2
}
}
const (
// STATX_MNT_ID_UNIQUE is provided in golang.org/x/sys@v0.20.0, but in order to
// avoid bumping the requirement for a single constant we can just define it
// ourselves.
_STATX_MNT_ID_UNIQUE = 0x4000 //nolint:revive // unix.* name
// We don't care which mount ID we get. The kernel will give us the unique
// one if it is supported. If the kernel doesn't support
// STATX_MNT_ID_UNIQUE, the bit is ignored and the returned request mask
// will only contain STATX_MNT_ID (if supported).
wantStatxMntMask = _STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID
)
var hasStatxMountID = gocompat.SyncOnceValue(func() bool {
var stx unix.Statx_t
err := unix.Statx(-int(unix.EBADF), "/", 0, wantStatxMntMask, &stx)
return err == nil && stx.Mask&wantStatxMntMask != 0
})
// GetMountID gets the mount identifier associated with the fd and path
// combination. It is effectively a wrapper around fetching
// STATX_MNT_ID{,_UNIQUE} with unix.Statx, but with a fallback to 0 if the
// kernel doesn't support the feature.
func GetMountID(dir Fd, path string) (uint64, error) {
// If we don't have statx(STATX_MNT_ID*) support, we can't do anything.
if !hasStatxMountID() {
return 0, nil
}
dirFd, fullPath := prepareAt(dir, path)
var stx unix.Statx_t
err := unix.Statx(dirFd, path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, wantStatxMntMask, &stx)
if stx.Mask&wantStatxMntMask == 0 {
// It's not a kernel limitation, for some reason we couldn't get a
// mount ID. Assume it's some kind of attack.
err = fmt.Errorf("could not get mount id: %w", err)
}
if err != nil {
return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
return stx.Mnt_id, nil
}

View File

@@ -1,55 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Package fd provides a drop-in interface-based replacement of [*os.File] that
// allows for things like noop-Close wrappers to be used.
//
// [*os.File]: https://pkg.go.dev/os#File
package fd
import (
"io"
"os"
)
// Fd is an interface that mirrors most of the API of [*os.File], allowing you
// to create wrappers that can be used in place of [*os.File].
//
// [*os.File]: https://pkg.go.dev/os#File
type Fd interface {
io.Closer
Name() string
Fd() uintptr
}
// Compile-time interface checks.
var (
_ Fd = (*os.File)(nil)
_ Fd = noClose{}
)
type noClose struct{ inner Fd }
func (f noClose) Name() string { return f.inner.Name() }
func (f noClose) Fd() uintptr { return f.inner.Fd() }
func (f noClose) Close() error { return nil }
// NopCloser returns an [*os.File]-like object where the [Close] method is now
// a no-op.
//
// Note that for [*os.File] and similar objects, the Go garbage collector will
// still call [Close] on the underlying file unless you use
// [runtime.SetFinalizer] to disable this behaviour. This is up to the caller
// to do (if necessary).
//
// [*os.File]: https://pkg.go.dev/os#File
// [Close]: https://pkg.go.dev/io#Closer
// [runtime.SetFinalizer]: https://pkg.go.dev/runtime#SetFinalizer
func NopCloser(f Fd) Fd { return noClose{inner: f} }

View File

@@ -1,78 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package fd
import (
"fmt"
"os"
"runtime"
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal"
)
// DupWithName creates a new file descriptor referencing the same underlying
// file, but with the provided name instead of fd.Name().
func DupWithName(fd Fd, name string) (*os.File, error) {
fd2, err := unix.FcntlInt(fd.Fd(), unix.F_DUPFD_CLOEXEC, 0)
if err != nil {
return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err)
}
runtime.KeepAlive(fd)
return os.NewFile(uintptr(fd2), name), nil
}
// Dup creates a new file description referencing the same underlying file.
func Dup(fd Fd) (*os.File, error) {
return DupWithName(fd, fd.Name())
}
// Fstat is an [Fd]-based wrapper around unix.Fstat.
func Fstat(fd Fd) (unix.Stat_t, error) {
var stat unix.Stat_t
if err := unix.Fstat(int(fd.Fd()), &stat); err != nil {
return stat, &os.PathError{Op: "fstat", Path: fd.Name(), Err: err}
}
runtime.KeepAlive(fd)
return stat, nil
}
// Fstatfs is an [Fd]-based wrapper around unix.Fstatfs.
func Fstatfs(fd Fd) (unix.Statfs_t, error) {
var statfs unix.Statfs_t
if err := unix.Fstatfs(int(fd.Fd()), &statfs); err != nil {
return statfs, &os.PathError{Op: "fstatfs", Path: fd.Name(), Err: err}
}
runtime.KeepAlive(fd)
return statfs, nil
}
// IsDeadInode detects whether the file has been unlinked from a filesystem and
// is thus a "dead inode" from the kernel's perspective.
func IsDeadInode(file Fd) error {
// If the nlink of a file drops to 0, there is an attacker deleting
// directories during our walk, which could result in weird /proc values.
// It's better to error out in this case.
stat, err := Fstat(file)
if err != nil {
return fmt.Errorf("check for dead inode: %w", err)
}
if stat.Nlink == 0 {
err := internal.ErrDeletedInode
if stat.Mode&unix.S_IFMT == unix.S_IFDIR {
err = internal.ErrInvalidDirectory
}
return fmt.Errorf("%w %q", err, file.Name())
}
return nil
}

View File

@@ -1,54 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package fd
import (
"os"
"runtime"
"golang.org/x/sys/unix"
)
// Fsopen is an [Fd]-based wrapper around unix.Fsopen.
func Fsopen(fsName string, flags int) (*os.File, error) {
// Make sure we always set O_CLOEXEC.
flags |= unix.FSOPEN_CLOEXEC
fd, err := unix.Fsopen(fsName, flags)
if err != nil {
return nil, os.NewSyscallError("fsopen "+fsName, err)
}
return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil
}
// Fsmount is an [Fd]-based wrapper around unix.Fsmount.
func Fsmount(ctx Fd, flags, mountAttrs int) (*os.File, error) {
// Make sure we always set O_CLOEXEC.
flags |= unix.FSMOUNT_CLOEXEC
fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs)
if err != nil {
return nil, os.NewSyscallError("fsmount "+ctx.Name(), err)
}
return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil
}
// OpenTree is an [Fd]-based wrapper around unix.OpenTree.
func OpenTree(dir Fd, path string, flags uint) (*os.File, error) {
dirFd, fullPath := prepareAt(dir, path)
// Make sure we always set O_CLOEXEC.
flags |= unix.OPEN_TREE_CLOEXEC
fd, err := unix.OpenTree(dirFd, path, flags)
if err != nil {
return nil, &os.PathError{Op: "open_tree", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
return os.NewFile(uintptr(fd), fullPath), nil
}

View File

@@ -1,64 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package fd
import (
"errors"
"os"
"runtime"
"golang.org/x/sys/unix"
)
func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool {
// RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve
// ".." while a mount or rename occurs anywhere on the system. This could
// happen spuriously, or as the result of an attacker trying to mess with
// us during lookup.
//
// In addition, scoped lookups have a "safety check" at the end of
// complete_walk which will return -EXDEV if the final path is not in the
// root.
return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 &&
(errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV))
}
// This is a fairly arbitrary limit we have just to avoid an attacker being
// able to make us spin in an infinite retry loop -- callers can choose to
// retry on EAGAIN if they prefer.
const scopedLookupMaxRetries = 128
// Openat2 is an [Fd]-based wrapper around unix.Openat2, but with some retry
// logic in case of EAGAIN errors.
//
// NOTE: This is a variable so that the lookup tests can force openat2 to fail.
var Openat2 = func(dir Fd, path string, how *unix.OpenHow) (*os.File, error) {
dirFd, fullPath := prepareAt(dir, path)
// Make sure we always set O_CLOEXEC.
how.Flags |= unix.O_CLOEXEC
var tries int
for {
fd, err := unix.Openat2(dirFd, path, how)
if err != nil {
if scopedLookupShouldRetry(how, err) && tries < scopedLookupMaxRetries {
// We retry a couple of times to avoid the spurious errors, and
// if we are being attacked then returning -EAGAIN is the best
// we can do.
tries++
continue
}
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
return os.NewFile(uintptr(fd), fullPath), nil
}
}

View File

@@ -1,10 +0,0 @@
## gocompat ##
This directory contains backports of stdlib functions from later Go versions so
the filepath-securejoin can continue to be used by projects that are stuck with
Go 1.18 support. Note that often filepath-securejoin is added in security
patches for old releases, so avoiding the need to bump Go compiler requirements
is a huge plus to downstreams.
The source code is licensed under the same license as the Go stdlib. See the
source files for the precise license information.

View File

@@ -1,13 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && go1.20
// Copyright (C) 2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package gocompat includes compatibility shims (backported from future Go
// stdlib versions) to permit filepath-securejoin to be used with older Go
// versions (often filepath-securejoin is added in security patches for old
// releases, so avoiding the need to bump Go compiler requirements is a huge
// plus to downstreams).
package gocompat

View File

@@ -1,19 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && go1.19
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocompat
import (
"sync/atomic"
)
// A Bool is an atomic boolean value.
// The zero value is false.
//
// Bool must not be copied after first use.
type Bool = atomic.Bool

View File

@@ -1,48 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !go1.19
// Copyright (C) 2024-2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocompat
import (
"sync/atomic"
)
// noCopy may be added to structs which must not be copied
// after the first use.
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
//
// Note that it must not be embedded, due to the Lock and Unlock methods.
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
// b32 returns a uint32 0 or 1 representing b.
func b32(b bool) uint32 {
if b {
return 1
}
return 0
}
// A Bool is an atomic boolean value.
// The zero value is false.
//
// Bool must not be copied after first use.
type Bool struct {
_ noCopy
v uint32
}
// Load atomically loads and returns the value stored in x.
func (x *Bool) Load() bool { return atomic.LoadUint32(&x.v) != 0 }
// Store atomically stores val into x.
func (x *Bool) Store(val bool) { atomic.StoreUint32(&x.v, b32(val)) }

View File

@@ -1,53 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && go1.21
// Copyright (C) 2024-2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocompat
import (
"cmp"
"slices"
"sync"
)
// SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc.
func SlicesDeleteFunc[S ~[]E, E any](slice S, delFn func(E) bool) S {
return slices.DeleteFunc(slice, delFn)
}
// SlicesContains is equivalent to Go 1.21's slices.Contains.
func SlicesContains[S ~[]E, E comparable](slice S, val E) bool {
return slices.Contains(slice, val)
}
// SlicesClone is equivalent to Go 1.21's slices.Clone.
func SlicesClone[S ~[]E, E any](slice S) S {
return slices.Clone(slice)
}
// SyncOnceValue is equivalent to Go 1.21's sync.OnceValue.
func SyncOnceValue[T any](f func() T) func() T {
return sync.OnceValue(f)
}
// SyncOnceValues is equivalent to Go 1.21's sync.OnceValues.
func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
return sync.OnceValues(f)
}
// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition.
type CmpOrdered = cmp.Ordered
// CmpCompare is equivalent to Go 1.21's cmp.Compare.
func CmpCompare[T CmpOrdered](x, y T) int {
return cmp.Compare(x, y)
}
// Max2 is equivalent to Go 1.21's max builtin (but only for two parameters).
func Max2[T CmpOrdered](x, y T) T {
return max(x, y)
}

View File

@@ -1,187 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !go1.21
// Copyright (C) 2021, 2022 The Go Authors. All rights reserved.
// Copyright (C) 2024-2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.BSD file.
package gocompat
import (
"sync"
)
// These are very minimal implementations of functions that appear in Go 1.21's
// stdlib, included so that we can build on older Go versions. Most are
// borrowed directly from the stdlib, and a few are modified to be "obviously
// correct" without needing to copy too many other helpers.
// clearSlice is equivalent to Go 1.21's builtin clear.
// Copied from the Go 1.24 stdlib implementation.
func clearSlice[S ~[]E, E any](slice S) {
var zero E
for i := range slice {
slice[i] = zero
}
}
// slicesIndexFunc is equivalent to Go 1.21's slices.IndexFunc.
// Copied from the Go 1.24 stdlib implementation.
func slicesIndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
for i := range s {
if f(s[i]) {
return i
}
}
return -1
}
// SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc.
// Copied from the Go 1.24 stdlib implementation.
func SlicesDeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
i := slicesIndexFunc(s, del)
if i == -1 {
return s
}
// Don't start copying elements until we find one to delete.
for j := i + 1; j < len(s); j++ {
if v := s[j]; !del(v) {
s[i] = v
i++
}
}
clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC
return s[:i]
}
// SlicesContains is equivalent to Go 1.21's slices.Contains.
// Similar to the stdlib slices.Contains, except that we don't have
// slices.Index so we need to use slices.IndexFunc for this non-Func helper.
func SlicesContains[S ~[]E, E comparable](s S, v E) bool {
return slicesIndexFunc(s, func(e E) bool { return e == v }) >= 0
}
// SlicesClone is equivalent to Go 1.21's slices.Clone.
// Copied from the Go 1.24 stdlib implementation.
func SlicesClone[S ~[]E, E any](s S) S {
// Preserve nil in case it matters.
if s == nil {
return nil
}
return append(S([]E{}), s...)
}
// SyncOnceValue is equivalent to Go 1.21's sync.OnceValue.
// Copied from the Go 1.25 stdlib implementation.
func SyncOnceValue[T any](f func() T) func() T {
// Use a struct so that there's a single heap allocation.
d := struct {
f func() T
once sync.Once
valid bool
p any
result T
}{
f: f,
}
return func() T {
d.once.Do(func() {
defer func() {
d.f = nil
d.p = recover()
if !d.valid {
panic(d.p)
}
}()
d.result = d.f()
d.valid = true
})
if !d.valid {
panic(d.p)
}
return d.result
}
}
// SyncOnceValues is equivalent to Go 1.21's sync.OnceValues.
// Copied from the Go 1.25 stdlib implementation.
func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
// Use a struct so that there's a single heap allocation.
d := struct {
f func() (T1, T2)
once sync.Once
valid bool
p any
r1 T1
r2 T2
}{
f: f,
}
return func() (T1, T2) {
d.once.Do(func() {
defer func() {
d.f = nil
d.p = recover()
if !d.valid {
panic(d.p)
}
}()
d.r1, d.r2 = d.f()
d.valid = true
})
if !d.valid {
panic(d.p)
}
return d.r1, d.r2
}
}
// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition.
// Copied from the Go 1.25 stdlib implementation.
type CmpOrdered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
// isNaN reports whether x is a NaN without requiring the math package.
// This will always return false if T is not floating-point.
// Copied from the Go 1.25 stdlib implementation.
func isNaN[T CmpOrdered](x T) bool {
return x != x
}
// CmpCompare is equivalent to Go 1.21's cmp.Compare.
// Copied from the Go 1.25 stdlib implementation.
func CmpCompare[T CmpOrdered](x, y T) int {
xNaN := isNaN(x)
yNaN := isNaN(y)
if xNaN {
if yNaN {
return 0
}
return -1
}
if yNaN {
return +1
}
if x < y {
return -1
}
if x > y {
return +1
}
return 0
}
// Max2 is equivalent to Go 1.21's max builtin for two parameters.
func Max2[T CmpOrdered](x, y T) T {
m := x
if y > m {
m = y
}
return m
}

View File

@@ -1,16 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Package gopathrs is a less complete pure Go implementation of some of the
// APIs provided by [libpathrs].
//
// [libpathrs]: https://github.com/cyphar/libpathrs
package gopathrs

View File

@@ -1,26 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package gopathrs
import (
"os"
)
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
// using an *[os.File] handle, to ensure that the correct root directory is used.
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
handle, err := completeLookupInRoot(root, unsafePath)
if err != nil {
return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err}
}
return handle, nil
}

View File

@@ -1,102 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package gopathrs
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
"github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
)
func openat2(dir fd.Fd, path string, how *unix.OpenHow) (*os.File, error) {
file, err := fd.Openat2(dir, path, how)
if err != nil {
return nil, err
}
// If we are using RESOLVE_IN_ROOT, the name we generated may be wrong.
if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT {
if actualPath, err := procfs.ProcSelfFdReadlink(file); err == nil {
// TODO: Ideally we would not need to dup the fd, but you cannot
// easily just swap an *os.File with one from the same fd
// (the GC will close the old one, and you cannot clear the
// finaliser easily because it is associated with an internal
// field of *os.File not *os.File itself).
newFile, err := fd.DupWithName(file, actualPath)
if err != nil {
return nil, err
}
_ = file.Close()
file = newFile
}
}
return file, nil
}
func lookupOpenat2(root fd.Fd, unsafePath string, partial bool) (*os.File, string, error) {
if !partial {
file, err := openat2(root, unsafePath, &unix.OpenHow{
Flags: unix.O_PATH | unix.O_CLOEXEC,
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
})
return file, "", err
}
return partialLookupOpenat2(root, unsafePath)
}
// partialLookupOpenat2 is an alternative implementation of
// partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a
// handle to the deepest existing child of the requested path within the root.
func partialLookupOpenat2(root fd.Fd, unsafePath string) (*os.File, string, error) {
// TODO: Implement this as a git-bisect-like binary search.
unsafePath = filepath.ToSlash(unsafePath) // noop
endIdx := len(unsafePath)
var lastError error
for endIdx > 0 {
subpath := unsafePath[:endIdx]
handle, err := openat2(root, subpath, &unix.OpenHow{
Flags: unix.O_PATH | unix.O_CLOEXEC,
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
})
if err == nil {
// Jump over the slash if we have a non-"" remainingPath.
if endIdx < len(unsafePath) {
endIdx++
}
// We found a subpath!
return handle, unsafePath[endIdx:], lastError
}
if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) {
// That path doesn't exist, let's try the next directory up.
endIdx = strings.LastIndexByte(subpath, '/')
lastError = err
continue
}
return nil, "", fmt.Errorf("open subpath: %w", err)
}
// If we couldn't open anything, the whole subpath is missing. Return a
// copy of the root fd so that the caller doesn't close this one by
// accident.
rootClone, err := fd.Dup(root)
if err != nil {
return nil, "", err
}
return rootClone, unsafePath, lastError
}

View File

@@ -1,123 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (C) 2022 The Go Authors. All rights reserved.
// Copyright (C) 2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.BSD file.
// The parsing logic is very loosely based on the Go stdlib's
// src/internal/syscall/unix/kernel_version_linux.go but with an API that looks
// a bit like runc's libcontainer/system/kernelversion.
//
// TODO(cyphar): This API has been copied around to a lot of different projects
// (Docker, containerd, runc, and now filepath-securejoin) -- maybe we should
// put it in a separate project?
// Package kernelversion provides a simple mechanism for checking whether the
// running kernel is at least as new as some baseline kernel version. This is
// often useful when checking for features that would be too complicated to
// test support for (or in cases where we know that some kernel features in
// backport-heavy kernels are broken and need to be avoided).
package kernelversion
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
)
// KernelVersion is a numeric representation of the key numerical elements of a
// kernel version (for instance, "4.1.2-default-1" would be represented as
// KernelVersion{4, 1, 2}).
type KernelVersion []uint64
func (kver KernelVersion) String() string {
var str strings.Builder
for idx, elem := range kver {
if idx != 0 {
_, _ = str.WriteRune('.')
}
_, _ = str.WriteString(strconv.FormatUint(elem, 10))
}
return str.String()
}
var errInvalidKernelVersion = errors.New("invalid kernel version")
// parseKernelVersion parses a string and creates a KernelVersion based on it.
func parseKernelVersion(kverStr string) (KernelVersion, error) {
kver := make(KernelVersion, 1, 3)
for idx, ch := range kverStr {
if '0' <= ch && ch <= '9' {
v := &kver[len(kver)-1]
*v = (*v * 10) + uint64(ch-'0')
} else {
if idx == 0 || kverStr[idx-1] < '0' || '9' < kverStr[idx-1] {
// "." must be preceded by a digit while in version section
return nil, fmt.Errorf("%w %q: kernel version has dot(s) followed by non-digit in version section", errInvalidKernelVersion, kverStr)
}
if ch != '.' {
break
}
kver = append(kver, 0)
}
}
if len(kver) < 2 {
return nil, fmt.Errorf("%w %q: kernel versions must contain at least two components", errInvalidKernelVersion, kverStr)
}
return kver, nil
}
// getKernelVersion gets the current kernel version.
var getKernelVersion = gocompat.SyncOnceValues(func() (KernelVersion, error) {
var uts unix.Utsname
if err := unix.Uname(&uts); err != nil {
return nil, err
}
// Remove the \x00 from the release.
release := uts.Release[:]
return parseKernelVersion(string(release[:bytes.IndexByte(release, 0)]))
})
// GreaterEqualThan returns true if the the host kernel version is greater than
// or equal to the provided [KernelVersion]. When doing this comparison, any
// non-numerical suffixes of the host kernel version are ignored.
//
// If the number of components provided is not equal to the number of numerical
// components of the host kernel version, any missing components are treated as
// 0. This means that GreaterEqualThan(KernelVersion{4}) will be treated the
// same as GreaterEqualThan(KernelVersion{4, 0, 0, ..., 0, 0}), and that if the
// host kernel version is "4" then GreaterEqualThan(KernelVersion{4, 1}) will
// return false (because the host version will be treated as "4.0").
func GreaterEqualThan(wantKver KernelVersion) (bool, error) {
hostKver, err := getKernelVersion()
if err != nil {
return false, err
}
// Pad out the kernel version lengths to match one another.
cmpLen := gocompat.Max2(len(hostKver), len(wantKver))
hostKver = append(hostKver, make(KernelVersion, cmpLen-len(hostKver))...)
wantKver = append(wantKver, make(KernelVersion, cmpLen-len(wantKver))...)
for i := 0; i < cmpLen; i++ {
switch gocompat.CmpCompare(hostKver[i], wantKver[i]) {
case -1:
// host < want
return false, nil
case +1:
// host > want
return true, nil
case 0:
continue
}
}
// equal version values
return true, nil
}

Some files were not shown because too many files have changed in this diff Show More