Compare commits

..

164 Commits

Author SHA1 Message Date
Tom Sweeney
9645b282ca Bump Skopeo to v1.23.0
Bump Skopeo to v1.23.0 to go out with Podman 6.0.
It appears that I negelcted to bump to 1.23.0-dev after 1.22.0 was
release as I should have.

Signed-off-by: Tom Sweeney <tsweeney@redhat.com>
2026-05-26 11:03:10 -04:00
Tom Sweeney
09c22b5d2d Bump c/common 0.68.0, c/image 5.40.0, c/storage 1.63.0
Bump the following:

c/common to v0.68.0
c/image to v5.40.0
c/storage to v1.63.0

In preparation for Skopeo v1.23.0 to go out with Podman v6.0

Signed-off-by: Tom Sweeney <tsweeney@redhat.com>
2026-05-26 11:01:20 -04:00
Miloslav Trmač
54eab56a4c Merge pull request #2877 from containers/renovate/common-image-and-storage-deps
Update common, image, and storage deps to 4a820ae
2026-05-18 16:17:31 +02:00
renovate[bot]
e72b1c821b Update common, image, and storage deps to 4a820ae
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-17 20:32:32 +00:00
Miloslav Trmač
e7310406d2 Merge pull request #2874 from aguidirh/add-platform-based-filtering
RUN-4630: copy: add platform-based filtering via --multi-arch flag
2026-05-14 16:55:23 +02:00
Alex Guidi
7a6acb1975 copy: add platform-based filtering via --multi-arch flag
Extends the --multi-arch flag to accept platform specifications,
allowing users to copy specific platforms from multi-architecture
images without requiring digest hashes.

Users can now specify platforms using OS/Architecture pairs:
  skopeo copy --multi-arch=linux/amd64,linux/arm64 docker://src docker://dst

This feature:
- Parses comma-separated platform specifications (e.g., linux/amd64,linux/arm64)
- Copies ALL instances matching each specified platform (including all
  compression variants and other variations)
- Works alongside existing --multi-arch options (system, all, index-only)
- Leverages the InstancePlatforms field added in containers/image

The implementation follows the design from containers/image#1938 and
containers/container-libs#656, providing a more user-friendly alternative
to specifying digest hashes via the Instances field.

Signed-off-by: Alex Guidi <aguidi@redhat.com>
2026-05-13 20:21:39 +02:00
Miloslav Trmač
df5f65813c Merge pull request #2872 from containers/renovate/golang.org-x-term-0.x
Update module golang.org/x/term to v0.43.0
2026-05-11 19:16:42 +02:00
renovate[bot]
1156ffcc43 Update module golang.org/x/term to v0.43.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 16:45:18 +00:00
Miloslav Trmač
9a0399a5b0 Merge pull request #2873 from containers/renovate/common-image-and-storage-deps
Update common, image, and storage deps to abe824d
2026-05-11 18:43:53 +02:00
renovate[bot]
5856c947ce Update common, image, and storage deps to abe824d
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-10 01:16:09 +00:00
Miloslav Trmač
516159d008 Merge pull request #2869 from containers/renovate/golangci-golangci-lint-2.x
Update dependency golangci/golangci-lint to v2.12.2
2026-05-06 16:45:12 +02:00
renovate[bot]
c8831c8bae Update dependency golangci/golangci-lint to v2.12.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 13:58:32 +00:00
Miloslav Trmač
82a71693db Merge pull request #2867 from containers/renovate/golangci-golangci-lint-2.x
Update dependency golangci/golangci-lint to v2.12.1
2026-05-04 14:08:46 +02:00
renovate[bot]
4df9e1df7e Update dependency golangci/golangci-lint to v2.12.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-05-04 13:42:00 +02:00
Miloslav Trmač
bf9d80d9b0 Merge pull request #2868 from containers/renovate/common-image-and-storage-deps
Update common, image, and storage deps to c03a490
2026-05-04 13:29:48 +02:00
renovate[bot]
0157bbaed1 Update common, image, and storage deps to c03a490
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 11:05:37 +00:00
Miloslav Trmač
b71619af45 Merge pull request #2866 from containers/renovate/github.com-masterminds-semver-v3-3.x
Update module github.com/Masterminds/semver/v3 to v3.5.0
2026-04-30 22:19:50 +02:00
renovate[bot]
bb107be5d3 Update module github.com/Masterminds/semver/v3 to v3.5.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 19:51:43 +00:00
Miloslav Trmač
35fcbb8b4a Merge pull request #2865 from lsm5/packit-downstream
Packit: Only create dist-git PRs for rawhide
2026-04-30 21:49:47 +02:00
Lokesh Mandvekar
277f715d8d Packit: Only create dist-git PRs for rawhide
We'll only be releasing breaking changes on rawhide so dist-git PRs
should only be created for rawhide.

Signed-off-by: Lokesh Mandvekar <lsm5@linux.com>
2026-04-30 09:31:10 -04:00
Miloslav Trmač
dc6f60deaf Merge pull request #2861 from timcoding1988/ci/sequoia-no-rawhide
[CI]: switch Sequoia matrix from Rawhide back to stable Fedora
2026-04-29 15:13:46 +02:00
Tim Zhou
f94b8a4338 Cirrus: switch Sequoia matrix from Rawhide back to stable Fedora
The `Skopeo test w/ Sequoia (currently Rawhide)` matrix entry was
introduced in 9753a1a1 ("Also build+test with Sequoia") because
`rust-podman-sequoia` (used by the `containers_image_sequoia` build tag)
shipped only in Rawhide. .cirrus.yml's own comment foreshadowed the
exit condition:

    "once we update to a future Fedora release (or if the package is
    backported), switch back from Rawhide to the latest Fedora release."

Both halves of that condition are now met:

  * Bodhi: rust-podman-sequoia-0.2.0-3.fc43 (stable, F43)
                                  0.3.2-1.fc44 (stable, F44)
  * containers/automation_images@46088a81 (2025-11-21) broadened the
    install condition in cache_images/fedora_packaging.sh from
    "Rawhide only" to `OS_REL_VER -ge 43`. The Fedora cache image and
    the skopeo_cidev container at the current IMAGE_SUFFIX
    (c20260310t170224z-f43f42d14, F43-based) include podman-sequoia.

Concretely:

* .cirrus.yml — drop the multi-line "currently using rawhide" comment
  block and the now-unused RAWHIDE_CACHE_IMAGE_NAME env definition.
* .cirrus.yml — switch the Sequoia matrix entry's VM_IMAGE_NAME from
  ${RAWHIDE_CACHE_IMAGE_NAME} to ${FEDORA_CACHE_IMAGE_NAME} and drop
  the "(currently Rawhide)" suffix from the matrix-entry name.
* .cirrus.yml — drop ${RAWHIDE_CACHE_IMAGE_NAME} from meta_task's
  IMGNAMES keepalive list.
* contrib/cirrus/runner.sh — remove the temporary
  `[[ "$VM_IMAGE_NAME" =~ "rawhide" ]]` compatibility branch in
  _run_setup(); it was specifically annotated as "Eventually, we'll
  stop using Rawhide again" and the .cirrus.yml note pointed here for
  cleanup.

No IMAGE_SUFFIX bump — that's Renovate's responsibility.
No test-script changes — Sequoia tests continue to use the same
`containers_image_sequoia` build tag against the same Fedora target,
just on the regular Fedora cache image instead of the Rawhide one.

Signed-off-by: Tim Zhou <tizhou@redhat.com>
2026-04-28 16:05:11 -04:00
Miloslav Trmač
8f4b42d8cf Merge pull request #2860 from containers/renovate/common-image-and-storage-deps
Update common, image, and storage deps to b9d5b9a
2026-04-27 22:33:31 +02:00
renovate[bot]
e6190508d3 Update common, image, and storage deps to b9d5b9a
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 19:30:43 +00:00
Miloslav Trmač
2d3cbb1207 Merge pull request #2859 from baude/removeowners
Remove OWNERS file
2026-04-24 19:42:20 +02:00
Brent Baude
95471ee7db Remove OWNERS file
The OWNERS file is no longer applicable to this repository as it no
longer uses the openshift merge bot.  References in the MAINTAINERS.md
file are also pruned.

Signed-off-by: Brent Baude <bbaude@redhat.com>
2026-04-24 10:19:21 -05:00
Miloslav Trmač
1a4a0f91ef Merge pull request #2854 from baude/RUN-4548
Move skopeo to go.podman.io
2026-04-20 21:40:33 +02:00
Brent Baude
a1e4d9eccf Additional cleanup for go module changes
Found some additional impact locations for the go module change.

Signed-off-by: Brent Baude <bbaude@redhat.com>
2026-04-20 13:01:59 -05:00
Brent Baude
870378ba16 Move skopeo to go.podman.io
With potential repo moves and cirus EOL, we have decided to begin
preparing by making changes to the skopeo module name.  See
https://github.com/containers/go.podman.io/pull/6 for more information.

Signed-off-by: Brent Baude <bbaude@redhat.com>
2026-04-20 13:00:55 -05:00
Miloslav Trmač
a588269731 Merge pull request #2856 from containers/renovate/common-image-and-storage-deps
Update common, image, and storage deps to 618304d
2026-04-20 16:20:05 +02:00
renovate[bot]
ad331b3fa0 Update common, image, and storage deps to 618304d
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 13:15:57 +00:00
Miloslav Trmač
904ff20c4d Merge pull request #2851 from containers/renovate/github.com-containers-ocicrypt-1.x
Update module github.com/containers/ocicrypt to v1.3.0
2026-04-16 20:57:46 +02:00
renovate[bot]
0113070da2 Update module github.com/containers/ocicrypt to v1.3.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 15:56:45 +00:00
Miloslav Trmač
61fbb9221f Merge pull request #2848 from containers/renovate/common-image-and-storage-deps
Update common, image, and storage deps to 129af75
2026-04-14 20:03:38 +02:00
renovate[bot]
aed4196f22 Update common, image, and storage deps to 129af75
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 16:50:52 +00:00
Miloslav Trmač
4e9bd3d331 Merge pull request #2849 from mtrmac/libs-deps
Update go.podman.io dependencies
2026-04-14 17:36:13 +02:00
Miloslav Trmač
3f4591c246 Update go.podman.io dependencies
Manually update to commits on main, because automation
would find latest tags on a release branch and downgrade
to them instead.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-04-13 17:19:38 +02:00
Miloslav Trmač
da8f4a4ad5 Merge pull request #2844 from containers/renovate/golang.org-x-term-0.x
Update module golang.org/x/term to v0.42.0
2026-04-09 17:38:03 +02:00
renovate[bot]
100826b448 Update module golang.org/x/term to v0.42.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 14:08:45 +00:00
Miloslav Trmač
c91aba2c62 Merge pull request #2843 from TomSweeneyRedHat/dev/tsweeney/cve-2026-34986-1
Bump  google.golang.org/grpc to v1.79.3 - CVE-2026-33186
2026-04-09 16:06:44 +02:00
Tom Sweeney
783a44c9e1 Bump google.golang.org/grpc to v1.79.3 - CVE-2026-33186
Even though it appears that Skopeo is not affected by CVE-2026-33186,
I have been pinged about it already.  Just to keep the CVE sniffers at
bay, bump to the appropriate version of gRPC.

Signed-off-by: Tom Sweeney <tsweeney@redhat.com>
2026-04-08 20:22:26 -04:00
Miloslav Trmač
f752e17ac1 Merge pull request #2838 from containers/renovate/go-github.com-go-jose-go-jose-v4-vulnerability
chore(deps): update module github.com/go-jose/go-jose/v4 to v4.1.4 [security]
2026-04-07 15:46:12 +02:00
renovate[bot]
583d584fbe chore(deps): update module github.com/go-jose/go-jose/v4 to v4.1.4 [security]
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 12:50:42 +00:00
Miloslav Trmač
fb4d72b989 Merge pull request #2837 from containers/renovate/common-image-and-storage-deps
fix(deps): update go.podman.io/storage digest to f0ddf1a
2026-04-07 14:49:25 +02:00
renovate[bot]
bfcd4c7a9d fix(deps): update go.podman.io/storage digest to f0ddf1a
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 21:50:20 +00:00
Miloslav Trmač
e95393154c Merge pull request #2835 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to 8af7873
2026-03-30 17:39:01 +02:00
renovate[bot]
c341f6f786 fix(deps): update common, image, and storage deps to 8af7873
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-30 14:33:33 +00:00
Miloslav Trmač
4ea0d2de5a Merge pull request #2834 from ricardobranco777/forceamd64
integration: Force amd64 on TestProxyMetadata
2026-03-30 15:45:40 +02:00
Ricardo Branco
2b2257a19d integration: Force amd64 on TestProxyMetadata
Otherwise test fails on other architectures due to
https://github.com/containers/skopeo/issues/2829

Signed-off-by: Ricardo Branco <rbranco@suse.de>
2026-03-27 18:40:29 +01:00
Miloslav Trmač
a48862c079 Merge pull request #2831 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to 94ad023
2026-03-24 19:39:26 +01:00
renovate[bot]
1ae60936a5 fix(deps): update common, image, and storage deps to 94ad023
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 19:18:40 +01:00
Miloslav Trmač
b376401a7d Try triggering an ostree image rebuild
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-24 19:18:33 +01:00
Miloslav Trmač
57f5158f59 Merge pull request #2832 from containers/renovate/golangci-golangci-lint-2.x
chore(deps): update dependency golangci/golangci-lint to v2.11.4
2026-03-23 20:55:42 +01:00
renovate[bot]
16aceb4366 chore(deps): update dependency golangci/golangci-lint to v2.11.4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 18:21:29 +00:00
Miloslav Trmač
a9186bf639 Merge pull request #2828 from gounthar/feat/riscv64-cross
ci: add riscv64 to local-cross build target
2026-03-23 19:19:46 +01:00
Bruno Verachten
13c63986ad ci: add riscv64 to local-cross build target
Add bin/skopeo.linux.riscv64 to the local-cross Makefile target, adding
RISC-V cross-compilation validation alongside the existing architectures.

Closes #2827

Signed-off-by: Bruno Verachten <gounthar@gmail.com>
2026-03-20 16:23:07 +01:00
Miloslav Trmač
529952e1a1 Merge pull request #2816 from giuseppe/use-proxy-from-container-libs
Use proxy from container libs
2026-03-19 22:30:54 +01:00
Giuseppe Scrivano
2eb170a288 cmd, proxy: use logic from the container-libs/common package
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
2026-03-19 20:26:35 +01:00
Giuseppe Scrivano
c1c2e83b6a vendor: update go.podman.io/common
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
2026-03-19 17:39:30 +01:00
Miloslav Trmač
37948dcda1 Merge pull request #2825 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to ddaabae
2026-03-16 14:17:20 +01:00
renovate[bot]
993808b53a fix(deps): update common, image, and storage deps to ddaabae
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-15 01:23:33 +00:00
Miloslav Trmač
e394838ec6 Merge pull request #2823 from mtrmac/warnings
Fix misc. warnings
2026-03-12 23:34:26 +01:00
Miloslav Trmač
c468542c7b Use --retry-times 3 for (skopeo sync) tests
... if they contact external registries.

This should, hopefully, decrease the flake rate.
We have already done this for (skopeo copy), sync seems
to have been missed at that time.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-12 19:38:56 +01:00
Miloslav Trmač
6be1904790 Use t.Tempdir() instead of manual os.CreateTemp() in tests
We don't have to worry about cleanup, and also in other respects
the code is shorter.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-12 19:38:56 +01:00
Miloslav Trmač
9de2245b5b Fix references to a wrong err
"err" was known to be nil at that point. Avoid the
writeErr variable name to hopefully simplify.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-12 19:38:56 +01:00
Miloslav Trmač
cad96140b3 Merge pull request #2821 from containers/renovate/golang.org-x-term-0.x
fix(deps): update module golang.org/x/term to v0.41.0
2026-03-12 19:38:24 +01:00
renovate[bot]
b94d15a214 fix(deps): update module golang.org/x/term to v0.41.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 16:15:56 +00:00
Paul Holzinger
71e92396c7 Merge pull request #2822 from mtrmac/go1.25
Update to Go 1.25
2026-03-12 17:14:57 +01:00
Miloslav Trmač
33ea6c63a1 Use fmt.Appendf instead of Sprintf + conversion
Should not change behavior.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-11 18:56:13 +01:00
Miloslav Trmač
8b9f757eb1 Use "any" instead of "interface{}"
Should not change behavior.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-11 18:18:57 +01:00
Miloslav Trmač
c49d55b2a4 Use WaitGroup.Go
Should not change behavior.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-11 18:18:47 +01:00
Miloslav Trmač
3276580fbd Update to Go 1.25
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-11 18:18:26 +01:00
Miloslav Trmač
0dd68eb6f7 Merge pull request #2820 from mtrmac/registry-v2
Update CI images with a dedicated registry that supports both schema1 and schema2
2026-03-11 17:23:00 +01:00
Miloslav Trmač
6a0b6b1c86 Update CI image and tests
registry-v2-schema1 is now registry-v2-schema1-only,
and there is a new registry-v2-schema1-supported because the default
/usr/bin/registry does not support schema1 any more.

Adjust tests.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-10 19:02:36 +01:00
Miloslav Trmač
726479d948 Replace the boolean for schema1 registry with an enum
We will add one more registry.

Should not change behavior.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-10 18:44:00 +01:00
Miloslav Trmač
fcc3aa683b Merge pull request #2817 from containers/renovate/golangci-golangci-lint-2.x
chore(deps): update dependency golangci/golangci-lint to v2.11.3
2026-03-10 16:35:54 +01:00
renovate[bot]
7b622f0dd4 chore(deps): update dependency golangci/golangci-lint to v2.11.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 13:57:46 +00:00
Miloslav Trmač
c43ffaff3f Merge pull request #2819 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to d48bc74
2026-03-09 22:36:50 +01:00
renovate[bot]
ff43ff736a fix(deps): update common, image, and storage deps to d48bc74
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 00:58:39 +00:00
Paul Holzinger
60f1448716 Merge pull request #2815 from mtrmac/llm-policy
Link to Podman's LLM policy
2026-03-03 19:23:29 +01:00
Miloslav Trmač
6da03342b2 Link to Podman's LLM policy
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-03-03 18:47:45 +01:00
Miloslav Trmač
1ab0fedadc Merge pull request #2813 from containers/renovate/github.com-opencontainers-image-spec-digest
fix(deps): update github.com/opencontainers/image-spec digest to a4c6ade
2026-03-02 18:02:48 +01:00
renovate[bot]
ee83783bb9 fix(deps): update github.com/opencontainers/image-spec digest to a4c6ade
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 16:15:46 +00:00
Miloslav Trmač
c280038a95 Merge pull request #2812 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to 854aaaf
2026-03-02 17:14:55 +01:00
renovate[bot]
5e77204385 fix(deps): update common, image, and storage deps to 854aaaf
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-01 01:25:39 +00:00
Miloslav Trmač
0a72ba2e7a Merge pull request #2809 from lsm5/packit-eln
Packit: Re-enable ELN tests
2026-02-27 16:30:56 +01:00
Lokesh Mandvekar
161072f6cc Packit: Re-enable ELN tests
Not certain what exactly fixed the registry container startup, but *something*
changed between 2025-11-17 and 2026-02-26. Probably a mirror / compose
flake, given it only occurred on x86_64. aarch64 wasn't affected but it
was simpler to just disable it entirely in packit config.

Related: #2748

Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
2026-02-27 17:39:39 +05:30
Miloslav Trmač
4c22f4f202 Merge pull request #2797 from mtrmac/tls-behavior
Add --tls-details
2026-02-26 20:34:14 +01:00
Miloslav Trmač
af1b87a955 Add a --tls-details option and integration tests
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-02-26 19:58:04 +01:00
Miloslav Trmač
ade3298955 Add an error return value to globalOptions.newSystemContext
Should not change behavior.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-02-26 19:57:57 +01:00
Miloslav Trmač
4f6fffdd9d Pass a SystemContext to signature.DefaultPolicy
This should not change behavior, but it reinforces the expectation
that the global SystemContext is always used.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-02-26 19:57:50 +01:00
Miloslav Trmač
56bca05e92 Update container-libs after container-libs#623
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-02-26 19:57:38 +01:00
Miloslav Trmač
c72e6a73cf Merge pull request #2810 from lsm5/packit-downstream-action-fix
Packit: fix downstream post-modifications action
2026-02-26 19:40:39 +01:00
Lokesh Mandvekar
1ea0fb4baf Packit: fix downstream post-modifications action
The downstream TMT plan can use a ref value without double quotes[0], so
the `post-modifications` action in packit config can be simplified as in
this patch. Also, switch the single and double quote usage to be
consistent with the usage in `get-current-version` packit action.

This should help to avoid variable substitution issues noticed in the
previous release [1].

Refs:
[0]: https://src.fedoraproject.org/rpms/skopeo/pull-request/98
[1]: fd38aa8a5e/f/plans/main.fmf

Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
2026-02-26 20:35:22 +05:30
Miloslav Trmač
db1feea335 Merge pull request #2803 from containers/renovate/golangci-golangci-lint-2.x
chore(deps): update dependency golangci/golangci-lint to v2.10.1
2026-02-19 14:51:36 +01:00
renovate[bot]
625e3631c4 chore(deps): update dependency golangci/golangci-lint to v2.10.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 20:44:22 +00:00
Miloslav Trmač
abf6bace6a Merge pull request #2801 from containers/renovate/golangci-golangci-lint-2.x
chore(deps): update dependency golangci/golangci-lint to v2.9.0
2026-02-12 21:21:09 +01:00
renovate[bot]
4bf4317042 chore(deps): update dependency golangci/golangci-lint to v2.9.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 01:59:01 +00:00
Miloslav Trmač
9cf7430b10 Merge pull request #2798 from containers/renovate/golang.org-x-term-0.x
fix(deps): update module golang.org/x/term to v0.40.0
2026-02-09 22:37:19 +01:00
renovate[bot]
cfc31fca1b fix(deps): update module golang.org/x/term to v0.40.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 20:29:18 +00:00
Miloslav Trmač
98e4aa395f Merge pull request #2794 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to 0e2aefd
2026-02-03 17:10:16 +01:00
renovate[bot]
800ea987b3 fix(deps): update common, image, and storage deps to 0e2aefd
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 15:28:40 +00:00
Paul Holzinger
12bd9fbb47 Merge pull request #2795 from mtrmac/sq-error
Update tests for a changed error message
2026-02-03 16:24:53 +01:00
Miloslav Trmač
a64f780f83 Update tests for a changed error message
008d971bc0
changed the text we were looking for.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-02-02 13:56:34 +01:00
Miloslav Trmač
5f9e5d79be Merge pull request #2792 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to b5801a6
2026-01-26 14:47:13 +01:00
renovate[bot]
56c4a65ec0 fix(deps): update common, image, and storage deps to b5801a6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-25 01:42:35 +00:00
Miloslav Trmač
7c6e1eb524 Merge pull request #2790 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to b2572af
2026-01-19 17:04:02 +01:00
renovate[bot]
d395f3eb76 fix(deps): update common, image, and storage deps to b2572af
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 00:43:53 +00:00
Miloslav Trmač
fabe041fad Merge pull request #2787 from containers/renovate/github.com-sirupsen-logrus-1.x
fix(deps): update module github.com/sirupsen/logrus to v1.9.4
2026-01-15 17:44:27 +01:00
renovate[bot]
c7e238a4f8 fix(deps): update module github.com/sirupsen/logrus to v1.9.4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 16:26:41 +00:00
Miloslav Trmač
bd93940d5b Merge pull request #2786 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to e7626b7
2026-01-12 18:32:21 +01:00
renovate[bot]
669e21cd77 fix(deps): update common, image, and storage deps to e7626b7
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 17:02:10 +00:00
Miloslav Trmač
94f776ad95 Merge pull request #2785 from containers/renovate/golang.org-x-term-0.x
fix(deps): update module golang.org/x/term to v0.39.0
2026-01-12 18:00:48 +01:00
renovate[bot]
b4516c6eea fix(deps): update module golang.org/x/term to v0.39.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 20:56:44 +00:00
Paul Holzinger
0c1d9730f8 Merge pull request #2783 from containers/renovate/golangci-golangci-lint-2.x
chore(deps): update dependency golangci/golangci-lint to v2.8.0
2026-01-08 14:56:53 +01:00
renovate[bot]
0c04335b21 chore(deps): update dependency golangci/golangci-lint to v2.8.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 13:39:39 +00:00
Paul Holzinger
b3007103d7 Merge pull request #2781 from mtrmac/retry-docs
Document the default of --retry-times
2026-01-08 14:39:05 +01:00
Miloslav Trmač
ef323fcce3 Document the default of --retry-times
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2026-01-07 18:25:26 +01:00
Miloslav Trmač
af43514563 Merge pull request #2780 from promalert/main
chore: fix function name in comment
2026-01-07 16:17:57 +01:00
promalert
f952b7facd chore: fix function name in comment
Signed-off-by: promalert <promalert@outlook.com>
2026-01-07 15:03:46 +08:00
Miloslav Trmač
592464e7c8 Merge pull request #2714 from jlebon/pr/reject-insecure
skopeo: add `--require-signed`
2026-01-05 22:51:51 +01:00
Jonathan Lebon
40f0e16777 skopeo: add --require-signed
In bootc, we want the ability to assert that signature verification is
enforced.

Add a new top-level `--require-signed` switch. When passed, we use the
new `RequireSignatureVerification()` method to ensure that signature
verification is enforced.

Part of https://github.com/containers/skopeo/issues/1829.

Signed-off-by: Jonathan Lebon <jonathan@jlebon.com>
2026-01-05 15:02:18 -05:00
Jonathan Lebon
767d9cb005 integration/signing_test: move findFingerprint to utils_test.go
Prep for another test which will also use this.

Signed-off-by: Jonathan Lebon <jonathan@jlebon.com>
2025-12-27 22:07:41 -05:00
Lokesh Mandvekar
47e615b9a8 Merge pull request #2774 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to b0f86df
2025-12-23 10:31:24 -05:00
renovate[bot]
01c33a7e4b fix(deps): update common, image, and storage deps to b0f86df
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-14 02:56:30 +00:00
Miloslav Trmač
707c470866 Merge pull request #2770 from mtrmac/common-dep
Update c/common to match #2765
2025-12-09 20:52:46 +01:00
Miloslav Trmač
7c747f8220 Update c/common to match #2765
Renovate seems to have had a difficulty with that:
> Could not determine new digest for update (go package go.podman.io/common)

for some reason, let's do it manually now, hoping that
Renovate will catch up next week.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2025-12-09 19:57:44 +01:00
Miloslav Trmač
46b2b95d57 Merge pull request #2765 from containers/renovate/common-image-and-storage-deps
fix(deps): update common, image, and storage deps to afd10d8
2025-12-09 19:52:01 +01:00
renovate[bot]
9efaa1c010 fix(deps): update common, image, and storage deps to afd10d8
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 18:34:31 +00:00
Miloslav Trmač
7e659707da Merge pull request #2766 from containers/renovate/golangci-golangci-lint-2.x
chore(deps): update dependency golangci/golangci-lint to v2.7.2
2025-12-09 19:22:00 +01:00
renovate[bot]
54b4159187 chore(deps): update dependency golangci/golangci-lint to v2.7.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 17:59:12 +00:00
Miloslav Trmač
e0d4b7b8e5 Merge pull request #2768 from containers/renovate/golang.org-x-term-0.x
fix(deps): update module golang.org/x/term to v0.38.0
2025-12-09 18:57:46 +01:00
renovate[bot]
ad431f6d1c fix(deps): update module golang.org/x/term to v0.38.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 18:17:13 +00:00
Miloslav Trmač
2821fe75d0 Merge pull request #2763 from lsm5/packit-fix
Packit: use `post-modifications` hook to update downstream TMT plan
2025-12-05 18:03:38 +01:00
Lokesh Mandvekar
e26a4237fc 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>
2025-12-05 08:50:02 -05:00
Miloslav Trmač
ce4265f9c0 Merge pull request #2747 from lsm5/digest-redux
skopeo inspect: support for sha512 images
2025-12-04 21:33:18 +01:00
Lokesh Mandvekar
420cd29beb docs: manpage update for skopeo inspect --manifest-digest
Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
2025-12-04 15:10:49 -05:00
Lokesh Mandvekar
f85b6db46e inspect: --manifest-digest flag
If this flag is specified, it'll display digest of that type, otherwise
it'll display the original digest.

Doesn't break any existing sha256 workflow.

Example:

1. Default
```
$ ./bin/skopeo inspect docker://docker.io/library/alpine:latest --format "Digest: {{.Digest}}"
Digest: sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
```

2. with --manifest-digest
```
$ ./bin/skopeo inspect --manifest-digest=sha512 docker://docker.io/library/alpine:latest --format "Digest: {{.Digest}}"
Digest: sha512:5acb33fb56a7791bf0c69d5b19a1c70272148e4107be5261d57305d14e9509792bbca53e5277c456181ecfa1c20ad8427f9b8ba46868020584a819de1128dbd2
```

Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
2025-12-04 15:10:42 -05:00
Lokesh Mandvekar
a25bf91823 vendor: container-libs commit 01833ef7b7f1d306205be7fa6fb36d0d6a6e3a33
Includes `manifest.DigestWithAlgorithm` and support for sha512 for
skopeo copy and inspect.

Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
2025-12-04 14:09:27 -05:00
Miloslav Trmač
ecf6e2c79c Merge pull request #2764 from containers/renovate/golangci-golangci-lint-2.x
chore(deps): update dependency golangci/golangci-lint to v2.7.1
2025-12-04 20:05:07 +01:00
renovate[bot]
0291b1e001 chore(deps): update dependency golangci/golangci-lint to v2.7.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 18:09:29 +00:00
Miloslav Trmač
85dc7471fe Merge pull request #2762 from containers/renovate/github.com-spf13-cobra-1.x
fix(deps): update module github.com/spf13/cobra to v1.10.2
2025-12-04 19:09:04 +01:00
renovate[bot]
f7d8ca9876 fix(deps): update module github.com/spf13/cobra to v1.10.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 03:06:26 +00:00
Miloslav Trmač
b440fae236 Merge pull request #2761 from containers/renovate/golangci-golangci-lint-2.x
chore(deps): update dependency golangci/golangci-lint to v2.7.0
2025-12-03 22:51:02 +01:00
renovate[bot]
bad5bd046d chore(deps): update dependency golangci/golangci-lint to v2.7.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 20:51:09 +00:00
Paul Holzinger
53800e09e2 Merge pull request #2759 from mtrmac/1.21-bump
Bump version to 1.22.0-dev
2025-12-03 20:06:47 +01:00
Miloslav Trmač
31d50fd0f9 Bump version to 1.22.0-dev
1.21.0 was released (off an earlier commit).

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2025-12-03 19:35:25 +01:00
Miloslav Trmač
f358adffdd Merge pull request #2757 from containers/renovate/common-image-and-storage-deps
Update common, image, and storage deps to 63be353
2025-12-01 22:40:52 +01:00
renovate[bot]
bd5ec4425d Update common, image, and storage deps to 63be353
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 20:49:31 +00:00
Lokesh Mandvekar
f7e1211a41 Merge pull request #2758 from mtrmac/ostree-base-image
Try triggering an image rebuild
2025-12-01 15:48:28 -05:00
Miloslav Trmač
287045706c Try triggering an image rebuild
https://cirrus-ci.com/task/4601027732701184 shows it using
go 1.24.3, but https://quay.io/repository/coreos-assembler/fcos-buildroot/manifest/sha256:55d7510ee1b15ae4c8c503efaa463c58ce0c3d484ab7ec4fe8b211ca5a5aacf9?tab=packages
shows 1.25.4 already; are we caching an older build?

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2025-12-01 19:44:55 +01:00
Miloslav Trmač
279c831898 Merge pull request #2753 from containers/renovate/common-image-and-storage-deps
Update common, image, and storage deps to 22d50c5
2025-11-25 04:00:44 +01:00
renovate[bot]
3498d8fc77 Update common, image, and storage deps to 22d50c5
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 01:04:16 +00:00
Miloslav Trmač
107b1b1ed2 Merge pull request #2743 from containers/renovate/golangci-golangci-lint-2.x
Update dependency golangci/golangci-lint to v2.6.2
2025-11-19 18:28:35 +01:00
renovate[bot]
ae484462c6 Update dependency golangci/golangci-lint to v2.6.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 17:09:55 +00:00
Miloslav Trmač
733c4d6ad9 Merge pull request #2746 from lsm5/container-libs-vendor
Container libs vendor and Renovate config update
2025-11-19 18:07:42 +01:00
Lokesh Mandvekar
75bc19e334 vendor: Fetch the latest from container-libs main
Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
2025-11-19 10:35:40 -05:00
Miloslav Trmač
c844ecb70c Merge pull request #2744 from Luap99/gofumpt
golangci-lint: enable gofumpt formatter
2025-11-18 23:35:18 +01:00
Paul Holzinger
b625905314 golangci-lint: enable gofumpt formatter
And also remove old, no longer needed validate-gofmt.sh script.
golangci-lint checks the formatting already for us.

And add a fmt make target that just runs golangci-lint fmt for easier
use.

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
2025-11-18 22:46:37 +01:00
Paul Holzinger
7182fecc79 format the code with gofumpt
Use it based on the outcome from our community discussion[1].

[1] https://github.com/containers/podman/discussions/27291

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
2025-11-18 22:46:37 +01:00
Miloslav Trmač
655f2b977b Merge pull request #2749 from lsm5/tmt-disable-eln-x64
Packit: tmp disable ELN tests
2025-11-18 22:33:39 +01:00
Lokesh Mandvekar
2a6fd74207 Packit: tmp disable ELN tests
Ref: https://github.com/containers/skopeo/issues/2748

Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
2025-11-17 13:29:09 -05:00
Miloslav Trmač
52d1fba7a4 Merge pull request #2742 from containers/renovate/golang.org-x-term-0.x
fix(deps): update module golang.org/x/term to v0.37.0
2025-11-11 21:14:50 +01:00
renovate[bot]
4ac321f3bc fix(deps): update module golang.org/x/term to v0.37.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 19:31:08 +00:00
1263 changed files with 101769 additions and 158215 deletions

View File

@@ -11,7 +11,7 @@ env:
GOPATH: &gopath "/var/tmp/go"
GOBIN: "${GOPATH}/bin"
GOCACHE: "${GOPATH}/cache"
GOSRC: &gosrc "/var/tmp/go/src/github.com/containers/skopeo"
GOSRC: &gosrc "/var/tmp/go/src/go.podman.io/skopeo"
# Required for consistency with containers/image CI
SKOPEO_PATH: *gosrc
CIRRUS_WORKING_DIR: *gosrc
@@ -21,14 +21,8 @@ env:
SCRIPT_BASE: "./contrib/cirrus"
# Google-cloud VM Images
# If you are updating IMAGE_SUFFIX: We are currently using rawhide for
# the containers_image_sequoia tests because the rust-podman-sequoia
# package is not available in earlier releases; once we update to a future
# Fedora release (or if the package is backported), switch back from Rawhide
# to the latest Fedora release.
IMAGE_SUFFIX: "c20250910t092246z-f42f41d13"
IMAGE_SUFFIX: "c20260310t170224z-f43f42d14"
FEDORA_CACHE_IMAGE_NAME: "fedora-${IMAGE_SUFFIX}"
RAWHIDE_CACHE_IMAGE_NAME: "rawhide-${IMAGE_SUFFIX}"
# Container FQIN's
FEDORA_CONTAINER_FQIN: "quay.io/libpod/fedora_podman:${IMAGE_SUFFIX}"
@@ -157,7 +151,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: 3
CIRRUS_IMAGE_VERSION: 5
env:
EXT_REPO_NAME: ostree-rs-ext
EXT_REPO_HOME: $CIRRUS_WORKING_DIR/../$EXT_REPO_NAME
@@ -206,11 +200,10 @@ test_skopeo_task:
env:
BUILDTAGS: *withopengpg
VM_IMAGE_NAME: ${FEDORA_CACHE_IMAGE_NAME}
- name: "Skopeo test w/ Sequoia (currently Rawhide)"
- name: "Skopeo test w/ Sequoia"
env:
BUILDTAGS: 'containers_image_sequoia'
# If you are removing the use of rawhide, also remove the VM_IMAGE_NAME condition from runner.sh .
VM_IMAGE_NAME: ${RAWHIDE_CACHE_IMAGE_NAME}
VM_IMAGE_NAME: ${FEDORA_CACHE_IMAGE_NAME}
setup_script: >-
"${GOSRC}/${SCRIPT_BASE}/runner.sh" setup
vendor_script: >-
@@ -239,7 +232,6 @@ meta_task:
# Space-separated list of images used by this repository state
IMGNAMES: |
${FEDORA_CACHE_IMAGE_NAME}
${RAWHIDE_CACHE_IMAGE_NAME}
build-push-${IMAGE_SUFFIX}
BUILDID: "${CIRRUS_BUILD_ID}"
REPOREF: "${CIRRUS_REPO_NAME}"

View File

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

View File

@@ -113,15 +113,19 @@ jobs:
id: https://copr.fedorainfracloud.org/coprs/rhcontainerbot/podman-next/repo/centos-stream-$releasever/rhcontainerbot-podman-next-centos-stream-$releasever.repo
# Sync to Fedora
# NOTE: Breaking changes related to Podman6 will only be shipped to Fedora
# Rawhide (to be Fedora 45) and newer.
# TODO: Update dist_git_branches as and when new Fedora releases are
# available.
- job: propose_downstream
trigger: release
packages: [skopeo-fedora]
update_release: false
dist_git_branches: &fedora_targets
- fedora-all
- fedora-rawhide
actions:
post-modifications: >-
bash -c "sed -i 's/^\(\s*\)ref: .*/\1ref: \"v${PACKIT_PROJECT_VERSION}\"/' ${PACKIT_DOWNSTREAM_REPO}/plans/main.fmf"
bash -c 'sed -i "s/^\(\s*\)ref: .*/\1ref: v${PACKIT_PROJECT_VERSION}/" ${PACKIT_DOWNSTREAM_REPO}/plans/main.fmf'
# Sync to CentOS Stream
# FIXME: Switch trigger whenever we're ready to update CentOS Stream via

View File

@@ -5,6 +5,7 @@ that we follow.
## Topics
* [LLM ("AI") Policy](#llm-ai-policy)
* [Reporting Issues](#reporting-issues)
* [Submitting Pull Requests](#submitting-pull-requests)
* [Communications](#communications)
@@ -12,6 +13,11 @@ that we follow.
* [Becoming a Maintainer](#becoming-a-maintainer)
-->
## LLM ("AI") Policy
If your contribution is aided by LLMs or other AI tools, please read the [LLM Policy in the Podman project](https://github.com/containers/podman/blob/main/LLM_POLICY.md).
This project also follows this LLM policy, which includes comments, issues, PRs, and any other interactions with the team.
## Reporting Issues
Before reporting an issue, check our backlog of

View File

@@ -7,6 +7,5 @@ except sections found in this document, which override those found in Podman's G
# Maintainers File
The definitive source of truth for maintainers of this repository is the local [MAINTAINERS.md](./MAINTAINERS.md) file. The [MAINTAINERS.md](https://github.com/containers/podman/blob/main/MAINTAINERS.md) file in the main Podman repository is used for project-spanning roles, including Core Maintainer and Community Manager. Some repositories in the project will also have a local [OWNERS](./OWNERS) file, which the CI system uses to map users to roles. Any changes to the [OWNERS](./OWNERS) file must make a corresponding change to the [MAINTAINERS.md](./MAINTAINERS.md) file to ensure that the file remains up to date. Most changes to [MAINTAINERS.md](./MAINTAINERS.md) will require a change to the repositorys [OWNERS](.OWNERS) file (e.g., adding a Reviewer), but some will not (e.g., promoting a Maintainer to a Core Maintainer, which comes with no additional CI-related privileges).
The definitive source of truth for maintainers of this repository is the local [MAINTAINERS.md](./MAINTAINERS.md) file. The [MAINTAINERS.md](https://github.com/containers/podman/blob/main/MAINTAINERS.md) file in the main Podman repository is used for project-spanning roles, including Core Maintainer and Community Manager.
Any Core Maintainers listed in Podmans [MAINTAINERS.md](https://github.com/containers/podman/blob/main/MAINTAINERS.md) file should also be added to the list of “approvers” in the local [OWNERS](./OWNERS) file and as a Core Maintainer in the list of “Maintainers” in the local [MAINTAINERS.md](./MAINTAINERS.md) file.

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.6.1
GOLANGCI_LINT_VERSION := 2.12.2
ifeq ($(GOBIN),)
GOBIN := $(GOPATH)/bin
@@ -80,7 +80,7 @@ INTERACTIVE := $(shell [ -t 0 ] && echo 1 || echo 0)
ifeq ($(INTERACTIVE), 1)
CONTAINER_CMD += -t
endif
CONTAINER_GOSRC = /src/github.com/containers/skopeo
CONTAINER_GOSRC = /src/go.podman.io/skopeo
CONTAINER_RUN ?= $(CONTAINER_CMD) --security-opt label=disable -v $(CURDIR):$(CONTAINER_GOSRC) -w $(CONTAINER_GOSRC) $(SKOPEO_CIDEV_CONTAINER_FQIN)
EXTRA_LDFLAGS ?=
@@ -134,7 +134,7 @@ bin/skopeo:
$(GO) build ${GO_DYN_FLAGS} ${SKOPEO_LDFLAGS} -gcflags "$(GOGCFLAGS)" -tags "$(BUILDTAGS)" -o $@ ./cmd/skopeo
bin/skopeo.%:
GOOS=$(word 2,$(subst ., ,$@)) GOARCH=$(word 3,$(subst ., ,$@)) $(GO) build ${SKOPEO_LDFLAGS} -tags "containers_image_openpgp $(BUILDTAGS)" -o $@ ./cmd/skopeo
local-cross: bin/skopeo.darwin.amd64 bin/skopeo.linux.arm bin/skopeo.linux.arm64 bin/skopeo.windows.386.exe bin/skopeo.windows.amd64.exe
local-cross: bin/skopeo.darwin.amd64 bin/skopeo.linux.arm bin/skopeo.linux.arm64 bin/skopeo.linux.riscv64 bin/skopeo.windows.386.exe bin/skopeo.windows.amd64.exe
$(MANPAGES): %: %.md
ifneq ($(DISABLE_DOCS), 1)
@@ -188,7 +188,7 @@ shell:
tools:
if [ ! -x "$(GOBIN)/golangci-lint" ]; then \
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v$(GOLANGCI_LINT_VERSION) ; \
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(GOBIN) v$(GOLANGCI_LINT_VERSION) ; \
fi
check: validate test-unit test-integration test-system
@@ -242,10 +242,13 @@ 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.
@@ -260,7 +263,7 @@ validate-docs: bin/skopeo
hack/xref-helpmsgs-manpages
test-unit-local:
$(GO) test $(SKOPEO_LDFLAGS) -tags "$(BUILDTAGS)" $$($(GO) list -tags "$(BUILDTAGS)" -e ./... | grep -v '^github\.com/containers/skopeo/\(integration\|vendor/.*\)$$')
$(GO) test $(SKOPEO_LDFLAGS) -tags "$(BUILDTAGS)" $$($(GO) list -tags "$(BUILDTAGS)" -e ./... | grep -v '^go\.podman\.io/skopeo/\(integration\|vendor/.*\)$$')
vendor:
$(GO) mod tidy

23
OWNERS
View File

@@ -1,23 +0,0 @@
approvers:
- baude
- giuseppe
- lsm5
- Luap99
- mheon
- mtrmac
- nalind
- rhatdan
- TomSweeneyRedHat
reviewers:
- ashley-cui
- baude
- cgwalters
- giuseppe
- lsm5
- Luap99
- mheon
- mtrmac
- nalind
- rhatdan
- TomSweeneyRedHat
- vrothberg

View File

@@ -5,7 +5,6 @@
----
![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/containers/skopeo)
[![Go Report Card](https://goreportcard.com/badge/github.com/containers/skopeo)](https://goreportcard.com/report/github.com/containers/skopeo)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10516/badge)](https://www.bestpractices.dev/projects/10516)
`skopeo` is a command line utility that performs various operations on container images and image repositories.

View File

@@ -45,7 +45,8 @@ 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,
@@ -77,7 +78,7 @@ See skopeo(1) section "IMAGE NAMES" for the expected format
flags.StringSliceVar(&opts.additionalTags, "additional-tag", []string{}, "additional tags (supports docker-archive)")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress output information when copying images")
flags.BoolVarP(&opts.all, "all", "a", false, "Copy all images if SOURCE-IMAGE is a list")
flags.Var(commonFlag.NewOptionalStringValue(&opts.multiArch), "multi-arch", `How to handle multi-architecture images (system, all, or index-only)`)
flags.Var(commonFlag.NewOptionalStringValue(&opts.multiArch), "multi-arch", `How to handle multi-architecture images (system, all, index-only, or comma-separated platform list like linux/amd64,linux/arm64)`)
flags.StringVar(&opts.signIdentity, "sign-identity", "", "Identity of signed image, must be a fully specified docker reference. Defaults to the target docker reference.")
flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digest of the pushed image to the specified file")
flags.StringSliceVar(&opts.encryptionKeys, "encryption-key", []string{}, "*Experimental* key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)")
@@ -88,23 +89,37 @@ See skopeo(1) section "IMAGE NAMES" for the expected format
}
// parseMultiArch parses the list processing selection
// It returns the copy.ImageListSelection to use with image.Copy option
func parseMultiArch(multiArch string) (copy.ImageListSelection, error) {
// It returns the copy.ImageListSelection to use with image.Copy option,
// and optionally a list of platform filters if specific platforms were requested
func parseMultiArch(multiArch string) (copy.ImageListSelection, []copy.InstancePlatformFilter, error) {
switch multiArch {
case "system":
return copy.CopySystemImage, nil
return copy.CopySystemImage, nil, nil
case "all":
return copy.CopyAllImages, nil
return copy.CopyAllImages, nil, nil
// There is no CopyNoImages value in copy.ImageListSelection, but because we
// don't provide an option to select a set of images to copy, we can use
// CopySpecificImages.
case "index-only":
return copy.CopySpecificImages, nil
// We don't expose CopySpecificImages other than index-only above, because
// we currently don't provide an option to choose the images to copy. That
// could be added in the future.
return copy.CopySpecificImages, nil, nil
default:
return copy.CopySystemImage, fmt.Errorf("unknown multi-arch option %q. Choose one of the supported options: 'system', 'all', or 'index-only'", multiArch)
if !strings.Contains(multiArch, "/") {
return copy.CopySystemImage, nil, fmt.Errorf("unknown multi-arch option %q. Choose one of the supported options: 'system', 'all', 'index-only', or a comma-separated platform list like 'linux/amd64,linux/arm64'", multiArch)
}
// Parse comma-separated platform list
var platforms []copy.InstancePlatformFilter
for platform := range strings.SplitSeq(multiArch, ",") {
platform = strings.TrimSpace(platform)
parts := strings.Split(platform, "/")
if len(parts) != 2 {
return copy.CopySystemImage, nil, fmt.Errorf("invalid platform format %q in --multi-arch, expected OS/Architecture (e.g., linux/amd64)", platform)
}
platforms = append(platforms, copy.InstancePlatformFilter{
OS: parts[0],
Architecture: parts[1],
})
}
return copy.CopySpecificImages, platforms, nil
}
}
@@ -167,11 +182,12 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) {
}
imageListSelection := copy.CopySystemImage
var instancePlatforms []copy.InstancePlatformFilter
if opts.multiArch.Present() && opts.all {
return fmt.Errorf("Cannot use --all and --multi-arch flags together")
}
if opts.multiArch.Present() {
imageListSelection, err = parseMultiArch(opts.multiArch.Value())
imageListSelection, instancePlatforms, err = parseMultiArch(opts.multiArch.Value())
if err != nil {
return err
}
@@ -235,6 +251,7 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) {
copyOpts.SourceCtx = sourceCtx
copyOpts.DestinationCtx = destinationCtx
copyOpts.ImageListSelection = imageListSelection
copyOpts.InstancePlatforms = instancePlatforms
copyOpts.OciDecryptConfig = decConfig
copyOpts.OciEncryptLayers = encLayers
copyOpts.OciEncryptConfig = encConfig
@@ -251,7 +268,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()), 0644); err != nil {
if err = os.WriteFile(opts.digestFile, []byte(manifestDigest.String()), 0o644); err != nil {
return fmt.Errorf("Failed to write digest to file %q: %w", opts.digestFile, err)
}
}

View File

@@ -1,6 +1,12 @@
package main
import "testing"
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.podman.io/image/v5/copy"
)
func TestCopy(t *testing.T) {
// Invalid command-line arguments
@@ -16,3 +22,95 @@ func TestCopy(t *testing.T) {
// FIXME: Much more test coverage
// Actual feature tests exist in integration and systemtest
}
func TestParseMultiArch(t *testing.T) {
tests := []struct {
name string
input string
expectedSelection copy.ImageListSelection
expectedPlatforms []copy.InstancePlatformFilter
expectError bool
}{
{
name: "system option",
input: "system",
expectedSelection: copy.CopySystemImage,
expectedPlatforms: nil,
expectError: false,
},
{
name: "all option",
input: "all",
expectedSelection: copy.CopyAllImages,
expectedPlatforms: nil,
expectError: false,
},
{
name: "index-only option",
input: "index-only",
expectedSelection: copy.CopySpecificImages,
expectedPlatforms: nil,
expectError: false,
},
{
name: "single platform",
input: "linux/amd64",
expectedSelection: copy.CopySpecificImages,
expectedPlatforms: []copy.InstancePlatformFilter{
{OS: "linux", Architecture: "amd64"},
},
expectError: false,
},
{
name: "multiple platforms",
input: "linux/amd64,linux/arm64",
expectedSelection: copy.CopySpecificImages,
expectedPlatforms: []copy.InstancePlatformFilter{
{OS: "linux", Architecture: "amd64"},
{OS: "linux", Architecture: "arm64"},
},
expectError: false,
},
{
name: "platforms with whitespace",
input: "linux/amd64, linux/arm64 , windows/amd64",
expectedSelection: copy.CopySpecificImages,
expectedPlatforms: []copy.InstancePlatformFilter{
{OS: "linux", Architecture: "amd64"},
{OS: "linux", Architecture: "arm64"},
{OS: "windows", Architecture: "amd64"},
},
expectError: false,
},
{
name: "invalid option",
input: "invalid",
expectError: true,
},
{
name: "invalid platform format - no slash",
input: "linux-amd64",
expectError: true,
},
{
name: "invalid platform format - too many parts",
input: "linux/amd64/extra",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
selection, platforms, err := parseMultiArch(tt.input)
if tt.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expectedSelection, selection)
assert.Equal(t, tt.expectedPlatforms, platforms)
})
}
}

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, 0600); err != nil {
if err := os.WriteFile(privateKeyPath, keys.PrivateKey, 0o600); err != nil {
return fmt.Errorf("Error writing private key to %q: %w", privateKeyPath, err)
}
if err := os.WriteFile(pubKeyPath, keys.PublicKey, 0644); err != nil {
if err := os.WriteFile(pubKeyPath, keys.PublicKey, 0o644); 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{}, 0600)
err := os.WriteFile(prefix+suffix, []byte{}, 0o600)
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{}, 0600)
err := os.WriteFile(nonDirectory, []byte{}, 0o600)
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"), 0600)
err = os.WriteFile(passphraseFile, []byte("some passphrase"), 0o600)
require.NoError(t, err)
out, err = runSkopeo("generate-sigstore-key",
"--output-prefix", prefix, "--passphrase-file", passphraseFile,
@@ -75,5 +75,4 @@ func TestGenerateSigstoreKey(t *testing.T) {
for _, suffix := range outputSuffixes {
assert.Contains(t, out, prefix+suffix)
}
}

View File

@@ -7,8 +7,8 @@ import (
"io"
"strings"
"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"
@@ -19,16 +19,18 @@ import (
"go.podman.io/image/v5/manifest"
"go.podman.io/image/v5/transports"
"go.podman.io/image/v5/types"
"go.podman.io/skopeo/cmd/skopeo/inspect"
)
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
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
}
func inspectCmd(global *globalOptions) *cobra.Command {
@@ -64,6 +66,7 @@ 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
}
@@ -176,7 +179,7 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error)
LayersData: imgInspect.LayersData,
Env: imgInspect.Env,
}
outputData.Digest, err = manifest.Digest(rawManifest)
outputData.Digest, err = manifestDigestFromManifest(rawManifest, img, opts.manifestDigest)
if err != nil {
return fmt.Errorf("Error computing manifest digest: %w", err)
}
@@ -235,3 +238,48 @@ 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

@@ -40,7 +40,10 @@ func (opts *loginOptions) run(args []string, stdout io.Writer) error {
opts.loginOpts.Stdout = stdout
opts.loginOpts.Stdin = os.Stdin
opts.loginOpts.AcceptRepositories = true
sys := opts.global.newSystemContext()
sys, err := opts.global.newSystemContext()
if err != nil {
return err
}
if opts.tlsVerify.Present() {
sys.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.tlsVerify.Value())
}

View File

@@ -36,7 +36,10 @@ func logoutCmd(global *globalOptions) *cobra.Command {
func (opts *logoutOptions) run(args []string, stdout io.Writer) error {
opts.logoutOpts.Stdout = stdout
opts.logoutOpts.AcceptRepositories = true
sys := opts.global.newSystemContext()
sys, err := opts.global.newSystemContext()
if err != nil {
return err
}
if opts.tlsVerify.Present() {
sys.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.tlsVerify.Value())
}

View File

@@ -7,12 +7,13 @@ import (
"strings"
"time"
"github.com/containers/skopeo/version"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
commonFlag "go.podman.io/common/pkg/flag"
"go.podman.io/image/v5/pkg/cli/basetls/tlsdetails"
"go.podman.io/image/v5/signature"
"go.podman.io/image/v5/types"
"go.podman.io/skopeo/version"
"go.podman.io/storage/pkg/reexec"
)
@@ -21,6 +22,7 @@ var defaultUserAgent = "skopeo/" + version.Version
type globalOptions struct {
debug bool // Enable debug output
tlsVerify commonFlag.OptionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:)
tlsDetailsPath string // Path to a containers-tls-details.yaml(5) file
policyPath string // Path to a signature verification policy file
insecurePolicy bool // Use an "allow everything" signature verification policy
registriesDirPath string // Path to a "registries.d" registry configuration directory
@@ -31,6 +33,7 @@ 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
@@ -79,8 +82,10 @@ func createApp() (*cobra.Command, *globalOptions) {
var dummyVersion bool
rootCommand.Flags().BoolVarP(&dummyVersion, "version", "v", false, "Version for Skopeo")
rootCommand.PersistentFlags().BoolVar(&opts.debug, "debug", false, "enable debug output")
rootCommand.PersistentFlags().StringVar(&opts.tlsDetailsPath, "tls-details", "", "path to a containers-tls-details.yaml(5) file")
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")
@@ -135,6 +140,9 @@ 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
}
@@ -155,18 +163,34 @@ func main() {
// getPolicyContext returns a *signature.PolicyContext based on opts.
func (opts *globalOptions) getPolicyContext() (*signature.PolicyContext, error) {
var policy *signature.Policy // This could be cached across calls in opts.
var err error
if opts.insecurePolicy {
policy = &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}
} else if opts.policyPath == "" {
policy, err = signature.DefaultPolicy(nil)
sys, err := opts.newSystemContext()
if err != nil {
return nil, err
}
p, err := signature.DefaultPolicy(sys)
if err != nil {
return nil, err
}
policy = p
} else {
policy, err = signature.NewPolicyFromFile(opts.policyPath)
p, err := signature.NewPolicyFromFile(opts.policyPath)
if err != nil {
return nil, err
}
policy = p
}
pc, err := signature.NewPolicyContext(policy)
if err != nil {
return nil, err
}
return signature.NewPolicyContext(policy)
if opts.requireSigned {
pc.RequireSignatureVerification(true)
}
return pc, nil
}
// commandTimeoutContext returns a context.Context and a cancellation callback based on opts.
@@ -182,11 +206,15 @@ func (opts *globalOptions) commandTimeoutContext() (context.Context, context.Can
// newSystemContext returns a *types.SystemContext corresponding to opts.
// It is guaranteed to return a fresh instance, so it is safe to make additional updates to it.
func (opts *globalOptions) newSystemContext() *types.SystemContext {
func (opts *globalOptions) newSystemContext() (*types.SystemContext, error) {
userAgent := defaultUserAgent
if opts.userAgentPrefix != "" {
userAgent = opts.userAgentPrefix + " " + defaultUserAgent
}
baseTLSConfig, err := tlsdetails.BaseTLSFromOptionalFile(opts.tlsDetailsPath)
if err != nil {
return nil, err
}
ctx := &types.SystemContext{
RegistriesDirPath: opts.registriesDirPath,
ArchitectureChoice: opts.overrideArch,
@@ -194,11 +222,12 @@ func (opts *globalOptions) newSystemContext() *types.SystemContext {
VariantChoice: opts.overrideVariant,
SystemRegistriesConfPath: opts.registriesConfPath,
BigFilesTemporaryDir: opts.tmpDir,
BaseTLSConfig: baseTLSConfig.TLSConfig(),
DockerRegistryUserAgent: userAgent,
}
// DEPRECATED: We support this for backward compatibility, but override it if a per-image flag is provided.
if opts.tlsVerify.Present() {
ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.tlsVerify.Value())
}
return ctx
return ctx, nil
}

View File

@@ -2,9 +2,11 @@ package main
import (
"bytes"
"crypto/tls"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.podman.io/image/v5/types"
)
@@ -22,7 +24,8 @@ func runSkopeo(args ...string) (string, error) {
func TestGlobalOptionsNewSystemContext(t *testing.T) {
// Default state
opts, _ := fakeGlobalOptions(t, []string{})
res := opts.newSystemContext()
res, err := opts.newSystemContext()
require.NoError(t, err)
assert.Equal(t, &types.SystemContext{
// User-Agent is set by default.
DockerRegistryUserAgent: defaultUserAgent,
@@ -33,16 +36,22 @@ func TestGlobalOptionsNewSystemContext(t *testing.T) {
"--override-arch", "overridden-arch",
"--override-os", "overridden-os",
"--override-variant", "overridden-variant",
"--tls-details", "../../integration/fixtures/tls-details-pqc-only.yaml",
"--tmpdir", "/srv",
"--registries-conf", "/srv/registries.conf",
"--tls-verify=false",
})
res = opts.newSystemContext()
res, err = opts.newSystemContext()
require.NoError(t, err)
assert.Equal(t, &types.SystemContext{
RegistriesDirPath: "/srv/registries.d",
ArchitectureChoice: "overridden-arch",
OSChoice: "overridden-os",
VariantChoice: "overridden-variant",
RegistriesDirPath: "/srv/registries.d",
ArchitectureChoice: "overridden-arch",
OSChoice: "overridden-os",
VariantChoice: "overridden-variant",
BaseTLSConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519MLKEM768},
},
BigFilesTemporaryDir: "/srv",
SystemRegistriesConfPath: "/srv/registries.conf",
DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,

View File

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

View File

@@ -9,879 +9,13 @@ package main
*/
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"sync"
"syscall"
"github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.podman.io/common/pkg/retry"
"go.podman.io/image/v5/image"
"go.podman.io/image/v5/manifest"
"go.podman.io/image/v5/pkg/blobinfocache"
"go.podman.io/image/v5/transports"
"go.podman.io/image/v5/transports/alltransports"
"go.podman.io/image/v5/types"
"go.podman.io/common/pkg/json-proxy"
)
// protocolVersion is semantic version of the protocol used by this proxy.
// The first version of the protocol has major version 0.2 to signify a
// departure from the original code which used HTTP.
//
// When bumping this, please also update the man page.
const protocolVersion = "0.2.8"
// maxMsgSize is the current limit on a packet size.
// Note that all non-metadata (i.e. payload data) is sent over a pipe.
const maxMsgSize = 32 * 1024
// maxJSONFloat is ECMA Number.MAX_SAFE_INTEGER
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
// We hard error if the input JSON numbers we expect to be
// integers are above this.
const maxJSONFloat = float64(uint64(1)<<53 - 1)
// sentinelImageID represents "image not found" on the wire
const sentinelImageID = 0
// request is the JSON serialization of a function call
type request struct {
// Method is the name of the function
Method string `json:"method"`
// Args is the arguments (parsed inside the function)
Args []any `json:"args"`
}
type proxyErrorCode string
const (
// proxyErrPipe means we got EPIPE writing to a pipe owned by the client
proxyErrPipe proxyErrorCode = "EPIPE"
// proxyErrRetryable can be used by clients to automatically retry operations
proxyErrRetryable proxyErrorCode = "retryable"
// All other errors
proxyErrOther proxyErrorCode = "other"
)
// proxyError is serialized over the errfd channel for GetRawBlob
type proxyError struct {
Code proxyErrorCode `json:"code"`
Message string `json:"message"`
}
// reply is serialized to JSON as the return value from a function call.
type reply struct {
// Success is true if and only if the call succeeded.
Success bool `json:"success"`
// Value is an arbitrary value (or values, as array/map) returned from the call.
Value any `json:"value"`
// PipeID is an index into open pipes, and should be passed to FinishPipe
PipeID uint32 `json:"pipeid"`
// ErrorCode will be non-empty if error is set (new in 0.2.8)
ErrorCode proxyErrorCode `json:"error_code"`
// Error should be non-empty if Success == false
Error string `json:"error"`
}
// replyBuf is our internal deserialization of reply plus optional fd
type replyBuf struct {
// value will be converted to a reply Value
value any
// fd is the read half of a pipe, passed back to the client for additional data
fd *os.File
// errfd will be a serialization of error state. This is optional and is currently
// only used by GetRawBlob.
errfd *os.File
// pipeid will be provided to the client as PipeID, an index into our open pipes
pipeid uint32
}
// activePipe is an open pipe to the client.
// It contains an error value
type activePipe struct {
// w is the write half of the pipe
w *os.File
// wg is completed when our worker goroutine is done
wg sync.WaitGroup
// err may be set in our worker goroutine
err error
}
// openImage is an opened image reference
type openImage struct {
// id is an opaque integer handle
id uint64
src types.ImageSource
cachedimg types.Image
}
// proxyHandler is the state associated with our socket.
type proxyHandler struct {
// lock protects everything else in this structure.
lock sync.Mutex
// opts is CLI options
opts *proxyOptions
sysctx *types.SystemContext
cache types.BlobInfoCache
// imageSerial is a counter for open images
imageSerial uint64
// images holds our opened images
images map[uint64]*openImage
// activePipes maps from "pipeid" to a pipe + goroutine pair
activePipes map[uint32]*activePipe
}
// convertedLayerInfo is the reduced form of the OCI type BlobInfo
// Used in the return value of GetLayerInfo
type convertedLayerInfo struct {
Digest digest.Digest `json:"digest"`
Size int64 `json:"size"`
MediaType string `json:"media_type"`
}
// mapProxyErrorCode turns an error into a known string value.
func mapProxyErrorCode(err error) proxyErrorCode {
switch {
case err == nil:
return ""
case errors.Is(err, syscall.EPIPE):
return proxyErrPipe
case retry.IsErrorRetryable(err):
return proxyErrRetryable
default:
return proxyErrOther
}
}
// newProxyError creates a serializable structure for
// the client containing a mapped error code based
// on the error type, plus its value as a string.
func newProxyError(err error) proxyError {
return proxyError{
Code: mapProxyErrorCode(err),
Message: fmt.Sprintf("%v", err),
}
}
// Initialize performs one-time initialization, and returns the protocol version
func (h *proxyHandler) Initialize(args []any) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
if len(args) != 0 {
return ret, fmt.Errorf("invalid request, expecting zero arguments")
}
if h.sysctx != nil {
return ret, fmt.Errorf("already initialized")
}
sysctx, err := h.opts.imageOpts.newSystemContext()
if err != nil {
return ret, err
}
h.sysctx = sysctx
h.cache = blobinfocache.DefaultCache(sysctx)
r := replyBuf{
value: protocolVersion,
}
return r, nil
}
// OpenImage accepts a string image reference i.e. TRANSPORT:REF - like `skopeo copy`.
// The return value is an opaque integer handle.
func (h *proxyHandler) OpenImage(args []any) (replyBuf, error) {
return h.openImageImpl(args, false)
}
func (h *proxyHandler) openImageImpl(args []any, allowNotFound bool) (retReplyBuf replyBuf, retErr error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
if h.sysctx == nil {
return ret, fmt.Errorf("client error: must invoke Initialize")
}
if len(args) != 1 {
return ret, fmt.Errorf("invalid request, expecting one argument")
}
imageref, ok := args[0].(string)
if !ok {
return ret, fmt.Errorf("expecting string imageref, not %T", args[0])
}
imgRef, err := alltransports.ParseImageName(imageref)
if err != nil {
return ret, err
}
imgsrc, err := imgRef.NewImageSource(context.Background(), h.sysctx)
if err != nil {
if allowNotFound && isNotFoundImageError(err) {
ret.value = sentinelImageID
return ret, nil
}
return ret, err
}
policyContext, err := h.opts.global.getPolicyContext()
if err != nil {
return ret, err
}
defer func() {
if err := policyContext.Destroy(); err != nil {
retErr = noteCloseFailure(retErr, "tearing down policy context", err)
}
}()
unparsedTopLevel := image.UnparsedInstance(imgsrc, nil)
allowed, err := policyContext.IsRunningImageAllowed(context.Background(), unparsedTopLevel)
if err != nil {
return ret, err
}
if !allowed {
return ret, fmt.Errorf("internal inconsistency: policy verification failed without returning an error")
}
// Note that we never return zero as an imageid; this code doesn't yet
// handle overflow though.
h.imageSerial++
openimg := &openImage{
id: h.imageSerial,
src: imgsrc,
}
h.images[openimg.id] = openimg
ret.value = openimg.id
return ret, nil
}
// OpenImageOptional accepts a string image reference i.e. TRANSPORT:REF - like `skopeo copy`.
// The return value is an opaque integer handle. If the image does not exist, zero
// is returned.
func (h *proxyHandler) OpenImageOptional(args []any) (replyBuf, error) {
return h.openImageImpl(args, true)
}
func (h *proxyHandler) CloseImage(args []any) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
if h.sysctx == nil {
return ret, fmt.Errorf("client error: must invoke Initialize")
}
if len(args) != 1 {
return ret, fmt.Errorf("invalid request, expecting one argument")
}
imgref, err := h.parseImageFromID(args[0])
if err != nil {
return ret, err
}
imgref.src.Close()
delete(h.images, imgref.id)
return ret, nil
}
// parseUint64 validates that a number fits inside a JavaScript safe integer
func parseUint64(v any) (uint64, error) {
f, ok := v.(float64)
if !ok {
return 0, fmt.Errorf("expecting numeric, not %T", v)
}
if f > maxJSONFloat {
return 0, fmt.Errorf("out of range integer for numeric %f", f)
}
return uint64(f), nil
}
func (h *proxyHandler) parseImageFromID(v any) (*openImage, error) {
imgid, err := parseUint64(v)
if err != nil {
return nil, err
}
if imgid == sentinelImageID {
return nil, fmt.Errorf("Invalid imageid value of zero")
}
imgref, ok := h.images[imgid]
if !ok {
return nil, fmt.Errorf("no image %v", imgid)
}
return imgref, nil
}
func (h *proxyHandler) allocPipe() (*os.File, *activePipe, error) {
piper, pipew, err := os.Pipe()
if err != nil {
return nil, nil, err
}
f := activePipe{
w: pipew,
}
h.activePipes[uint32(pipew.Fd())] = &f
f.wg.Add(1)
return piper, &f, nil
}
// returnBytes generates a return pipe() from a byte array
// In the future it might be nicer to return this via memfd_create()
func (h *proxyHandler) returnBytes(retval any, buf []byte) (replyBuf, error) {
var ret replyBuf
piper, f, err := h.allocPipe()
if err != nil {
return ret, err
}
go func() {
// Signal completion when we return
defer f.wg.Done()
_, err = io.Copy(f.w, bytes.NewReader(buf))
if err != nil {
f.err = err
}
}()
ret.value = retval
ret.fd = piper
ret.pipeid = uint32(f.w.Fd())
return ret, nil
}
// cacheTargetManifest is invoked when GetManifest or GetConfig is invoked
// the first time for a given image. If the requested image is a manifest
// list, this function resolves it to the image matching the calling process'
// operating system and architecture.
//
// TODO: Add GetRawManifest or so that exposes manifest lists
func (h *proxyHandler) cacheTargetManifest(img *openImage) error {
ctx := context.Background()
if img.cachedimg != nil {
return nil
}
unparsedToplevel := image.UnparsedInstance(img.src, nil)
mfest, manifestType, err := unparsedToplevel.Manifest(ctx)
if err != nil {
return err
}
var target *image.UnparsedImage
if manifest.MIMETypeIsMultiImage(manifestType) {
manifestList, err := manifest.ListFromBlob(mfest, manifestType)
if err != nil {
return err
}
instanceDigest, err := manifestList.ChooseInstance(h.sysctx)
if err != nil {
return err
}
target = image.UnparsedInstance(img.src, &instanceDigest)
} else {
target = unparsedToplevel
}
cachedimg, err := image.FromUnparsedImage(ctx, h.sysctx, target)
if err != nil {
return err
}
img.cachedimg = cachedimg
return nil
}
// GetManifest returns a copy of the manifest, converted to OCI format, along with the original digest.
// Manifest lists are resolved to the current operating system and architecture.
func (h *proxyHandler) GetManifest(args []any) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
if h.sysctx == nil {
return ret, fmt.Errorf("client error: must invoke Initialize")
}
if len(args) != 1 {
return ret, fmt.Errorf("invalid request, expecting one argument")
}
imgref, err := h.parseImageFromID(args[0])
if err != nil {
return ret, err
}
err = h.cacheTargetManifest(imgref)
if err != nil {
return ret, err
}
img := imgref.cachedimg
ctx := context.Background()
rawManifest, manifestType, err := img.Manifest(ctx)
if err != nil {
return ret, err
}
// We only support OCI and docker2schema2. We know docker2schema2 can be easily+cheaply
// converted into OCI, so consumers only need to see OCI.
switch manifestType {
case imgspecv1.MediaTypeImageManifest, manifest.DockerV2Schema2MediaType:
break
// Explicitly reject e.g. docker schema 1 type with a "legacy" note
case manifest.DockerV2Schema1MediaType, manifest.DockerV2Schema1SignedMediaType:
return ret, fmt.Errorf("unsupported legacy manifest MIME type: %s", manifestType)
default:
return ret, fmt.Errorf("unsupported manifest MIME type: %s", manifestType)
}
// We always return the original digest, as that's what clients need to do pull-by-digest
// and in general identify the image.
digest, err := manifest.Digest(rawManifest)
if err != nil {
return ret, err
}
var serialized []byte
// But, we convert to OCI format on the wire if it's not already. The idea here is that by reusing the containers/image
// stack, clients to this proxy can pretend the world is OCI only, and not need to care about e.g.
// docker schema and MIME types.
if manifestType != imgspecv1.MediaTypeImageManifest {
manifestUpdates := types.ManifestUpdateOptions{ManifestMIMEType: imgspecv1.MediaTypeImageManifest}
ociImage, err := img.UpdatedImage(ctx, manifestUpdates)
if err != nil {
return ret, err
}
ociSerialized, _, err := ociImage.Manifest(ctx)
if err != nil {
return ret, err
}
serialized = ociSerialized
} else {
serialized = rawManifest
}
return h.returnBytes(digest, serialized)
}
// GetFullConfig returns a copy of the image configuration, converted to OCI format.
// https://github.com/opencontainers/image-spec/blob/main/config.md
func (h *proxyHandler) GetFullConfig(args []any) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
if h.sysctx == nil {
return ret, fmt.Errorf("client error: must invoke Initialize")
}
if len(args) != 1 {
return ret, fmt.Errorf("invalid request, expecting: [imgid]")
}
imgref, err := h.parseImageFromID(args[0])
if err != nil {
return ret, err
}
err = h.cacheTargetManifest(imgref)
if err != nil {
return ret, err
}
img := imgref.cachedimg
ctx := context.TODO()
config, err := img.OCIConfig(ctx)
if err != nil {
return ret, err
}
serialized, err := json.Marshal(&config)
if err != nil {
return ret, err
}
return h.returnBytes(nil, serialized)
}
// GetConfig returns a copy of the container runtime configuration, converted to OCI format.
// Note that due to a historical mistake, this returns not the full image configuration,
// but just the container runtime configuration. You should use GetFullConfig instead.
func (h *proxyHandler) GetConfig(args []any) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
if h.sysctx == nil {
return ret, fmt.Errorf("client error: must invoke Initialize")
}
if len(args) != 1 {
return ret, fmt.Errorf("invalid request, expecting: [imgid]")
}
imgref, err := h.parseImageFromID(args[0])
if err != nil {
return ret, err
}
err = h.cacheTargetManifest(imgref)
if err != nil {
return ret, err
}
img := imgref.cachedimg
ctx := context.TODO()
config, err := img.OCIConfig(ctx)
if err != nil {
return ret, err
}
serialized, err := json.Marshal(&config.Config)
if err != nil {
return ret, err
}
return h.returnBytes(nil, serialized)
}
// GetBlob fetches a blob, performing digest verification.
func (h *proxyHandler) GetBlob(args []any) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
if h.sysctx == nil {
return ret, fmt.Errorf("client error: must invoke Initialize")
}
if len(args) != 3 {
return ret, fmt.Errorf("found %d args, expecting (imgid, digest, size)", len(args))
}
imgref, err := h.parseImageFromID(args[0])
if err != nil {
return ret, err
}
digestStr, ok := args[1].(string)
if !ok {
return ret, fmt.Errorf("expecting string blobid")
}
size, err := parseUint64(args[2])
if err != nil {
return ret, err
}
ctx := context.TODO()
d, err := digest.Parse(digestStr)
if err != nil {
return ret, err
}
blobr, blobSize, err := imgref.src.GetBlob(ctx, types.BlobInfo{Digest: d, Size: int64(size)}, h.cache)
if err != nil {
return ret, err
}
piper, f, err := h.allocPipe()
if err != nil {
blobr.Close()
return ret, err
}
go func() {
// Signal completion when we return
defer blobr.Close()
defer f.wg.Done()
verifier := d.Verifier()
tr := io.TeeReader(blobr, verifier)
n, err := io.Copy(f.w, tr)
if err != nil {
f.err = err
return
}
if n != int64(size) {
f.err = fmt.Errorf("expected %d bytes in blob, got %d", size, n)
}
if !verifier.Verified() {
f.err = fmt.Errorf("corrupted blob, expecting %s", d.String())
}
}()
ret.value = blobSize
ret.fd = piper
ret.pipeid = uint32(f.w.Fd())
return ret, nil
}
// GetRawBlob can be viewed as a more general purpose successor
// to GetBlob. First, it does not verify the digest, which in
// some cases is unnecessary as the client would prefer to do it.
//
// It also does not use the "FinishPipe" API call, but instead
// returns *two* file descriptors, one for errors and one for data.
//
// On (initial) success, the return value provided to the client is the size of the blob.
func (h *proxyHandler) GetRawBlob(args []any) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
if h.sysctx == nil {
return ret, fmt.Errorf("client error: must invoke Initialize")
}
if len(args) != 2 {
return ret, fmt.Errorf("found %d args, expecting (imgid, digest)", len(args))
}
imgref, err := h.parseImageFromID(args[0])
if err != nil {
return ret, err
}
digestStr, ok := args[1].(string)
if !ok {
return ret, fmt.Errorf("expecting string blobid")
}
ctx := context.TODO()
d, err := digest.Parse(digestStr)
if err != nil {
return ret, err
}
blobr, blobSize, err := imgref.src.GetBlob(ctx, types.BlobInfo{Digest: d, Size: int64(-1)}, h.cache)
if err != nil {
return ret, err
}
// Note this doesn't call allocPipe; we're not using the FinishPipe infrastructure.
piper, pipew, err := os.Pipe()
if err != nil {
blobr.Close()
return ret, err
}
errpipeR, errpipeW, err := os.Pipe()
if err != nil {
piper.Close()
pipew.Close()
blobr.Close()
return ret, err
}
// Asynchronous worker doing a copy
go func() {
// We own the read from registry, and write pipe objects
defer blobr.Close()
defer pipew.Close()
defer errpipeW.Close()
logrus.Debugf("Copying blob to client: %d bytes", blobSize)
_, err := io.Copy(pipew, blobr)
// Handle errors here by serializing a JSON error back over
// the error channel. In either case, both file descriptors
// will be closed, signaling the completion of the operation.
if err != nil {
logrus.Debugf("Sending error to client: %v", err)
serializedErr := newProxyError(err)
buf, err := json.Marshal(serializedErr)
if err != nil {
// Should never happen
panic(err)
}
_, writeErr := errpipeW.Write(buf)
if writeErr != nil && !errors.Is(err, syscall.EPIPE) {
logrus.Debugf("Writing to client: %v", err)
}
}
logrus.Debugf("Completed GetRawBlob operation")
}()
ret.value = blobSize
ret.fd = piper
ret.errfd = errpipeR
return ret, nil
}
// GetLayerInfo returns data about the layers of an image, useful for reading the layer contents.
//
// This is the same as GetLayerInfoPiped, but returns its contents inline. This is subject to
// failure for large images (because we use SOCK_SEQPACKET which has a maximum buffer size)
// and is hence only retained for backwards compatibility. Callers are expected to use
// the semver to know whether they can call the new API.
func (h *proxyHandler) GetLayerInfo(args []any) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
if h.sysctx == nil {
return ret, fmt.Errorf("client error: must invoke Initialize")
}
if len(args) != 1 {
return ret, fmt.Errorf("found %d args, expecting (imgid)", len(args))
}
imgref, err := h.parseImageFromID(args[0])
if err != nil {
return ret, err
}
ctx := context.TODO()
err = h.cacheTargetManifest(imgref)
if err != nil {
return ret, err
}
img := imgref.cachedimg
layerInfos, err := img.LayerInfosForCopy(ctx)
if err != nil {
return ret, err
}
if layerInfos == nil {
layerInfos = img.LayerInfos()
}
layers := make([]convertedLayerInfo, 0, len(layerInfos))
for _, layer := range layerInfos {
layers = append(layers, convertedLayerInfo{layer.Digest, layer.Size, layer.MediaType})
}
ret.value = layers
return ret, nil
}
// GetLayerInfoPiped returns data about the layers of an image, useful for reading the layer contents.
//
// This needs to be called since the data returned by GetManifest() does not allow to correctly
// calling GetBlob() for the containers-storage: transport (which doesnt store the original compressed
// representations referenced in the manifest).
func (h *proxyHandler) GetLayerInfoPiped(args []any) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
if h.sysctx == nil {
return ret, fmt.Errorf("client error: must invoke Initialize")
}
if len(args) != 1 {
return ret, fmt.Errorf("found %d args, expecting (imgid)", len(args))
}
imgref, err := h.parseImageFromID(args[0])
if err != nil {
return ret, err
}
ctx := context.TODO()
err = h.cacheTargetManifest(imgref)
if err != nil {
return ret, err
}
img := imgref.cachedimg
layerInfos, err := img.LayerInfosForCopy(ctx)
if err != nil {
return ret, err
}
if layerInfos == nil {
layerInfos = img.LayerInfos()
}
layers := make([]convertedLayerInfo, 0, len(layerInfos))
for _, layer := range layerInfos {
layers = append(layers, convertedLayerInfo{layer.Digest, layer.Size, layer.MediaType})
}
serialized, err := json.Marshal(&layers)
if err != nil {
return ret, err
}
return h.returnBytes(nil, serialized)
}
// FinishPipe waits for the worker goroutine to finish, and closes the write side of the pipe.
func (h *proxyHandler) FinishPipe(args []any) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
var ret replyBuf
pipeidv, err := parseUint64(args[0])
if err != nil {
return ret, err
}
pipeid := uint32(pipeidv)
f, ok := h.activePipes[pipeid]
if !ok {
return ret, fmt.Errorf("finishpipe: no active pipe %d", pipeid)
}
// Wait for the goroutine to complete
f.wg.Wait()
logrus.Debug("Completed pipe goroutine")
// And only now do we close the write half; this forces the client to call this API
f.w.Close()
// Propagate any errors from the goroutine worker
err = f.err
delete(h.activePipes, pipeid)
return ret, err
}
// close releases all resources associated with this proxy backend
func (h *proxyHandler) close() {
for _, image := range h.images {
err := image.src.Close()
if err != nil {
// This shouldn't be fatal
logrus.Warnf("Failed to close image %s: %v", transports.ImageName(image.cachedimg.Reference()), err)
}
}
}
// send writes a reply buffer to the socket
func (buf replyBuf) send(conn *net.UnixConn, err error) error {
logrus.Debugf("Sending reply: err=%v value=%v pipeid=%v datafd=%v errfd=%v", err, buf.value, buf.pipeid, buf.fd, buf.errfd)
// We took ownership of these FDs, so close when we're done sending them or on error
defer func() {
if buf.fd != nil {
buf.fd.Close()
}
if buf.errfd != nil {
buf.errfd.Close()
}
}()
replyToSerialize := reply{
Success: err == nil,
Value: buf.value,
PipeID: buf.pipeid,
}
if err != nil {
replyToSerialize.ErrorCode = mapProxyErrorCode(err)
replyToSerialize.Error = err.Error()
}
serializedReply, err := json.Marshal(&replyToSerialize)
if err != nil {
return err
}
// Copy the FD number(s) to the socket ancillary buffer
fds := make([]int, 0)
if buf.fd != nil {
fds = append(fds, int(buf.fd.Fd()))
}
if buf.errfd != nil {
fds = append(fds, int(buf.errfd.Fd()))
}
oob := syscall.UnixRights(fds...)
n, oobn, err := conn.WriteMsgUnix(serializedReply, oob, nil)
if err != nil {
return err
}
// Validate that we sent the full packet
if n != len(serializedReply) || oobn != len(oob) {
return io.ErrShortWrite
}
return nil
}
type proxyOptions struct {
global *globalOptions
imageOpts *imageOptions
@@ -910,92 +44,15 @@ func proxyCmd(global *globalOptions) *cobra.Command {
return cmd
}
// processRequest dispatches a remote request.
// replyBuf is the result of the invocation.
// terminate should be true if processing of requests should halt.
func (h *proxyHandler) processRequest(readBytes []byte) (rb replyBuf, terminate bool, err error) {
var req request
// Parse the request JSON
if err = json.Unmarshal(readBytes, &req); err != nil {
err = fmt.Errorf("invalid request: %v", err)
return
}
logrus.Debugf("Executing method %s", req.Method)
// Dispatch on the method
switch req.Method {
case "Initialize":
rb, err = h.Initialize(req.Args)
case "OpenImage":
rb, err = h.OpenImage(req.Args)
case "OpenImageOptional":
rb, err = h.OpenImageOptional(req.Args)
case "CloseImage":
rb, err = h.CloseImage(req.Args)
case "GetManifest":
rb, err = h.GetManifest(req.Args)
case "GetConfig":
rb, err = h.GetConfig(req.Args)
case "GetFullConfig":
rb, err = h.GetFullConfig(req.Args)
case "GetBlob":
rb, err = h.GetBlob(req.Args)
case "GetRawBlob":
rb, err = h.GetRawBlob(req.Args)
case "GetLayerInfo":
rb, err = h.GetLayerInfo(req.Args)
case "GetLayerInfoPiped":
rb, err = h.GetLayerInfoPiped(req.Args)
case "FinishPipe":
rb, err = h.FinishPipe(req.Args)
case "Shutdown":
terminate = true
// NOTE: If you add a method here, you should very likely be bumping the
// const protocolVersion above.
default:
err = fmt.Errorf("unknown method: %s", req.Method)
}
return
}
// Implementation of podman experimental-image-proxy
// Implementation of podman experimental-image-proxy using the library
func (opts *proxyOptions) run(args []string, stdout io.Writer) error {
handler := &proxyHandler{
opts: opts,
images: make(map[uint64]*openImage),
activePipes: make(map[uint32]*activePipe),
}
defer handler.close()
// Convert the socket FD passed by client into a net.FileConn
fd := os.NewFile(uintptr(opts.sockFd), "sock")
fconn, err := net.FileConn(fd)
manager, err := jsonproxy.NewManager(
jsonproxy.WithSystemContext(opts.imageOpts.newSystemContext),
jsonproxy.WithPolicyContext(opts.global.getPolicyContext),
)
if err != nil {
return err
}
conn := fconn.(*net.UnixConn)
// Allocate a buffer to copy the packet into
buf := make([]byte, maxMsgSize)
for {
n, _, err := conn.ReadFrom(buf)
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("reading socket: %v", err)
}
readbuf := buf[0:n]
rb, terminate, err := handler.processRequest(readbuf)
if terminate {
logrus.Debug("terminating")
return nil
}
if err := rb.send(conn, err); err != nil {
return fmt.Errorf("writing to socket: %w", err)
}
}
defer manager.Close()
return manager.Serve(context.Background(), opts.sockFd)
}

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, 0644); err != nil {
if err := os.WriteFile(opts.output, signature, 0o644); err != nil {
return fmt.Errorf("Error writing signature to %s: %w", opts.output, err)
}
return nil
@@ -118,7 +118,6 @@ 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()
@@ -147,8 +146,7 @@ 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

@@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
@@ -75,15 +76,13 @@ func TestStandaloneSign(t *testing.T) {
assertTestFailed(t, out, err, "/dev/full")
// Success
sigOutput, err := os.CreateTemp("", "sig")
require.NoError(t, err)
defer os.Remove(sigOutput.Name())
out, err = runSkopeo("standalone-sign", "-o", sigOutput.Name(),
sigOutput := filepath.Join(t.TempDir(), "sig")
out, err = runSkopeo("standalone-sign", "-o", sigOutput,
manifestPath, dockerReference, fixturesTestKeyFingerprint)
require.NoError(t, err)
assert.Empty(t, out)
sig, err := os.ReadFile(sigOutput.Name())
sig, err := os.ReadFile(sigOutput)
require.NoError(t, err)
manifest, err := os.ReadFile(manifestPath)
require.NoError(t, err)

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, 0755); err != nil {
if err = os.MkdirAll(destination, 0o755); err != nil {
return nil, fmt.Errorf("Error creating directory for image %s: %w", destination, err)
}
imageTransport = directory.Transport
@@ -270,7 +270,6 @@ func imagesToCopyFromDir(dirPath string) ([]types.ImageReference, error) {
}
return nil
})
if err != nil {
return sourceReferences,
fmt.Errorf("Error walking the path %q: %w", dirPath, err)
@@ -367,7 +366,8 @@ 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, 0644)
digestFile, err = os.OpenFile(opts.digestFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return fmt.Errorf("Error creating digest file: %w", err)
}

View File

@@ -200,7 +200,10 @@ func retryFlags() (pflag.FlagSet, *retry.Options) {
func (opts *imageOptions) newSystemContext() (*types.SystemContext, error) {
// *types.SystemContext instance from globalOptions
// imageOptions option overrides the instance if both are present.
ctx := opts.global.newSystemContext()
ctx, err := opts.global.newSystemContext()
if err != nil {
return nil, err
}
ctx.DockerCertPath = opts.dockerCertPath
ctx.OCISharedBlobDirPath = opts.sharedBlobDir
ctx.AuthFilePath = opts.shared.authFilePath

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"os"
"path/filepath"
"testing"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -49,7 +50,8 @@ 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
@@ -124,7 +126,8 @@ 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
@@ -366,10 +369,8 @@ func fakeSharedCopyOptions(t *testing.T, cmdFlags []string) *sharedCopyOptions {
func TestSharedCopyOptionsCopyOptions(t *testing.T) {
someStdout := bytes.Buffer{}
passphraseFile, err := os.CreateTemp("", "passphrase") // Eventually we could refer to a passphrase fixture instead
require.NoError(t, err)
defer os.Remove(passphraseFile.Name())
_, err = passphraseFile.WriteString("test-passphrase")
passphraseFile := filepath.Join(t.TempDir(), "passphrase") // Eventually we could refer to a passphrase fixture instead
err := os.WriteFile(passphraseFile, []byte("test-passphrase"), 0o600)
require.NoError(t, err)
type tc struct {
@@ -389,7 +390,8 @@ 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",
@@ -405,7 +407,7 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) {
{ // --sign-passphrase-file + --sign-by work
options: []string{
"--sign-by", "gpgFingerprint",
"--sign-passphrase-file", passphraseFile.Name(),
"--sign-passphrase-file", passphraseFile,
},
expected: copy.Options{
SignBy: "gpgFingerprint",
@@ -417,7 +419,7 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) {
{ // --sign-passphrase-file + --sign-by-sigstore-private-key work
options: []string{
"--sign-by-sigstore-private-key", "/some/key/path.private",
"--sign-passphrase-file", passphraseFile.Name(),
"--sign-passphrase-file", passphraseFile,
},
expected: copy.Options{
SignPassphrase: "test-passphrase",
@@ -444,7 +446,7 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) {
c = append(c, tc{
options: []string{
"--sign-by-sq-fingerprint", "sqFingerprint",
"--sign-passphrase-file", passphraseFile.Name(),
"--sign-passphrase-file", passphraseFile,
},
expected: copy.Options{
SignPassphrase: "test-passphrase",
@@ -470,9 +472,9 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) {
{"--format", "invalid"}, // Invalid --format
// More --sign-by-sigstore-private-key, --sign-by-sigstore failure cases should be tested here.
// --sign-passphrase-file + more than one key option
{"--sign-by", "gpgFingerprint", "--sign-by-sq-fingerprint", "sqFingerprint", "--sign-passphrase-file", passphraseFile.Name()},
{"--sign-by", "gpgFingerprint", "--sign-by-sigstore-private-key", "sigstorePrivateKey", "--sign-passphrase-file", passphraseFile.Name()},
{"--sign-by-sq-fingerprint", "sqFingerprint", "--sign-by-sigstore-private-key", "sigstorePrivateKey", "--sign-passphrase-file", passphraseFile.Name()},
{"--sign-by", "gpgFingerprint", "--sign-by-sq-fingerprint", "sqFingerprint", "--sign-passphrase-file", passphraseFile},
{"--sign-by", "gpgFingerprint", "--sign-by-sigstore-private-key", "sigstorePrivateKey", "--sign-passphrase-file", passphraseFile},
{"--sign-by-sq-fingerprint", "sqFingerprint", "--sign-by-sigstore-private-key", "sigstorePrivateKey", "--sign-passphrase-file", passphraseFile},
{"--sign-by", "gpgFingerprint", "--sign-passphrase-file", "/dev/null/this/does/not/exist"}, // --sign-passphrase-file not found
{"--sign-by-sigstore", "/dev/null/this/does/not/exist"}, // --sign-by-sigstore file not found
} {
@@ -488,21 +490,31 @@ 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},
true,
},
{
"badValue",
"",
true,
},
} {
manifestType, err := parseManifestFormat(testCase.formatParam)
if testCase.expectErr {
@@ -523,28 +535,37 @@ 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

@@ -71,10 +71,8 @@ _run_setup() {
# automation, but the sources are in different directories. It's
# possible for a mismatch to happen, but should (hopefully) be unlikely.
# Double-check to make sure.
# Temporarily, allow running on Rawhide VMs and consuming older binaries:
# that should be compatible enough. Eventually, well stop using Rawhide again.
if ! grep -Fqx "ID=$OS_RELEASE_ID" $mnt/etc/os-release || \
{ ! [[ "$VM_IMAGE_NAME" =~ "rawhide" ]] && ! grep -Fqx "VERSION_ID=$OS_RELEASE_VER" $mnt/etc/os-release; } then
! grep -Fqx "VERSION_ID=$OS_RELEASE_VER" $mnt/etc/os-release; then
die "Somehow $SKOPEO_CIDEV_CONTAINER_FQIN is not based on $OS_REL_VER."
fi
msg "Copying test binaries from $SKOPEO_CIDEV_CONTAINER_FQIN /usr/local/bin/"

View File

@@ -83,8 +83,11 @@ Options:
- system: Copy only the image that matches the system architecture
- all: Copy the full multi-architecture image
- index-only: Copy only the index
- _platform-list_: Copy only specific platforms (comma-separated list of OS/Architecture pairs, e.g., `linux/amd64,linux/arm64`)
The index-only option usually fails unless the referenced per-architecture images are already present in the destination, or the target registry supports sparse indexes.
The index-only option and platform-list both create sparse manifest lists, which usually fail unless the referenced per-architecture images are already present in the destination, or the target registry supports sparse indexes.
When specifying a platform list, all compression variants and other variations for each platform are copied.
**--quiet**, **-q**
@@ -214,7 +217,7 @@ Precompute digests to ensure layers are not uploaded that already exist on the d
**--retry-times**
The number of times to retry.
The number of times to retry. By default, no retries are attempted.
**--retry-delay**
@@ -267,6 +270,11 @@ To copy and sign an image:
$ skopeo copy --sign-by dev@example.com containers-storage:example/busybox:streaming docker://example/busybox:gold
```
To copy only specific platforms from a multi-architecture image (creates a sparse manifest list):
```console
$ skopeo copy --multi-arch=linux/amd64,linux/arm64 docker://quay.io/skopeo/stable:latest docker://registry.example.com/skopeo:latest
```
To encrypt an image:
```console
$ skopeo copy docker://docker.io/library/nginx:1.17.8 oci:local_nginx:1.17.8

View File

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

View File

@@ -69,7 +69,7 @@ Registry token for accessing the registry.
**--retry-times**
The number of times to retry.
The number of times to retry. By default, no retries are attempted.
**--retry-delay**
@@ -95,6 +95,12 @@ 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:
@@ -186,6 +192,12 @@ $ /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.
The number of times to retry. By default, no retries are attempted.
**--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.
The number of times to retry. By default, no retries are attempted.
**--retry-delay**

View File

@@ -92,6 +92,19 @@ 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.
**--tls-details** _path_
Path to a containers-tls-details(5) file, affecting TLS behavior throughout the program.
If not set, defaults to a reasonable default that may change over time (depending on systems global policy,
version of the program, version of the Go language, and the like).
Users should generally not use this option unless they have a process to ensure that the configuration will be kept up to date.
**--tmpdir** _dir_
Directory used to store temporary files. Defaults to /var/tmp.

110
go.mod
View File

@@ -1,55 +1,57 @@
module github.com/containers/skopeo
module go.podman.io/skopeo
// Minimum required golang version
go 1.24.2
go 1.25.6
// Warning: Ensure the "go" and "toolchain" versions match exactly to prevent unwanted auto-updates
require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/containers/ocicrypt v1.2.1
github.com/Masterminds/semver/v3 v3.5.0
github.com/containers/ocicrypt v1.3.0
github.com/docker/distribution v2.8.3+incompatible
github.com/moby/sys/capability v0.4.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.2-0.20251016170850-26647a49f642
github.com/opencontainers/image-spec v1.1.2-0.20260226102121-a4c6ade7bb82
github.com/opencontainers/image-tools v1.0.0-rc3
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.10.1
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
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
go.podman.io/common v0.68.0
go.podman.io/image/v5 v5.40.0
go.podman.io/storage v1.63.0
golang.org/x/term v0.43.0
gopkg.in/yaml.v3 v3.0.1
)
require (
cyphar.com/go-pathrs v0.2.4 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/BurntSushi/toml v1.6.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/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.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.17.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/cyphar/filepath-securejoin v0.6.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 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/docker-credential-helpers v0.9.7 // indirect
github.com/docker/go-connections v0.7.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.0.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // 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
github.com/google/go-containerregistry v0.20.6 // indirect
github.com/google/go-containerregistry v0.21.1 // indirect
github.com/google/go-intervals v0.0.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
@@ -58,53 +60,51 @@ 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.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/pgzip v1.2.6 // 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/mattn/go-runewidth v0.0.23 // indirect
github.com/mattn/go-sqlite3 v1.14.44 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mistifyio/go-zfs/v3 v3.1.0 // indirect
github.com/mistifyio/go-zfs/v4 v4.0.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/moby/moby/client v0.4.1 // 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.2 // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/opencontainers/image-spec/schema v0.0.0-20250717171153-ab80ff15c2dd // indirect
github.com/opencontainers/runtime-spec v1.2.1 // indirect
github.com/opencontainers/selinux v1.12.0 // indirect
github.com/opencontainers/runtime-spec v1.3.0 // indirect
github.com/opencontainers/selinux v1.14.1 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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.5 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/proglottis/gpgme v0.1.6 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // 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/secure-systems-lab/go-securesystemslib v0.11.0 // indirect
github.com/sigstore/fulcio v1.8.5 // indirect
github.com/sigstore/protobuf-specs v0.5.0 // indirect
github.com/sigstore/sigstore v1.10.6 // 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/sylabs/sif/v2 v2.24.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.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
github.com/vbatts/tar-split v0.12.3 // indirect
github.com/vbauerster/mpb/v8 v8.12.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

314
go.sum
View File

@@ -1,42 +1,38 @@
cyphar.com/go-pathrs v0.2.4 h1:iD/mge36swa1UFKdINkr1Frkpp6wZsy3YYEildj9cLY=
cyphar.com/go-pathrs v0.2.4/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/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/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
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/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/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
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/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/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/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.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/containers/ocicrypt v1.3.0 h1:ps3St6ZWNWhOQ/Kqld6K2wPHt01Mj3AqRTNCZLIWOfo=
github.com/containers/ocicrypt v1.3.0/go.mod h1:PmfuGFpBwnGLnbqBm+QIy2nc8noDJ1Wt6B19la7VBFo=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
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.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
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/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=
@@ -45,29 +41,25 @@ 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 v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs=
github.com/docker/cli v29.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 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/docker-credential-helpers v0.9.7 h1:jaPIxEIDz5bQeghNAdzz0ETwMMnM4vzjZlxz3pWP4JA=
github.com/docker/docker-credential-helpers v0.9.7/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
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.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
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/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.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
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=
@@ -77,28 +69,24 @@ 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=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
github.com/google/go-containerregistry v0.21.1 h1:sOt/o9BS2b87FnR7wxXPvRKU1XVJn2QCwOS5g8zQXlc=
github.com/google/go-containerregistry v0.21.1/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM=
github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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=
@@ -109,13 +97,11 @@ 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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
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=
@@ -123,105 +109,85 @@ 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/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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
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.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/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
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/v3 v3.1.0 h1:FZaylcg0hjUp27i23VcJJQiuBeAZjrC8lPqCGM1CopY=
github.com/mistifyio/go-zfs/v3 v3.1.0/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k=
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/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/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
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/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/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.29.0 h1:rfh+ZFjgJhYWRoIqVf3Uwx/W20yLrcrE2h2GmYVRaag=
github.com/onsi/ginkgo/v2 v2.29.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/gomega v1.41.0 h1:OwKp4pXNgVxf6sCplzYo794OFNuoL2q2SBMU5NSWOjA=
github.com/onsi/gomega v1.41.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
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=
github.com/opencontainers/image-spec v1.1.2-0.20251016170850-26647a49f642/go.mod h1:GRy5q9c6/vsqXmQ1I6TL1PkhA64F6eXG9fUOQ9tFvm8=
github.com/opencontainers/image-spec v1.1.2-0.20260226102121-a4c6ade7bb82 h1:E+Fkd2gXu1H0wGsF/81ghtqacuzl+OkuS4rEnGOsTCw=
github.com/opencontainers/image-spec v1.1.2-0.20260226102121-a4c6ade7bb82/go.mod h1:GRy5q9c6/vsqXmQ1I6TL1PkhA64F6eXG9fUOQ9tFvm8=
github.com/opencontainers/image-spec/schema v0.0.0-20250717171153-ab80ff15c2dd h1:demTtfPH+DsqagnumQZv8nQrFoUqCJDNVrw+6LsGpm4=
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.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/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.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ=
github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
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/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.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/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/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.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/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs=
github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q=
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.5 h1:HYTD1/L5wlBp8JxsWxUf8hmfaNBBF/x3r3p5l6tZwbA=
github.com/sigstore/fulcio v1.8.5/go.mod h1:tSLYK3JsKvJpDW1BsIsVHZgHj+f8TjXARzqIUWSsSPQ=
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.10.6 h1:YWhMQfTrJSK80QB1pbxjYeAwGKx+5UwWPPAY9hrPPZg=
github.com/sigstore/sigstore v1.10.6/go.mod h1:k/mcVVXw3I87dYG/iCVTSW2xTrW7vPzxxGic4KqsqXs=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
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.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
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=
@@ -229,22 +195,19 @@ 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/sylabs/sif/v2 v2.24.0 h1:1wB5uMDUQYjk8AckTySaDcP9YnpMb1LyDRr1Jt9A10w=
github.com/sylabs/sif/v2 v2.24.0/go.mod h1:DbXWqWZ1hdLSU+K9ipdds5AmZeHWsyxCOj/oQakBa88=
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.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/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw=
github.com/vbatts/tar-split v0.12.3/go.mod h1:sQOc6OlqGCr7HkGx/IDBeKiTIvqhmj8KffNhEXG4Nq0=
github.com/vbauerster/mpb/v8 v8.12.0 h1:+gneY3ifzc88tKDzOtfG8k8gfngCx615S2ZmFM4liWg=
github.com/vbauerster/mpb/v8 v8.12.0/go.mod h1:V02YIuMVo301Y1VE9VtZlD8s84OMsk+EKN6mwvf/588=
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=
@@ -256,34 +219,26 @@ 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.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.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.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.podman.io/common v0.68.0 h1:6V8nZS33vLTPC047RfSGxARgS/Ui6CQtgdZXLo18qAc=
go.podman.io/common v0.68.0/go.mod h1:zVzufHkRpLueF6NW6N+fAs1C2METdzYcfD9zuw+oJKA=
go.podman.io/image/v5 v5.40.0 h1:gNQvj343Eb4juCitUBkuDz1T82Zpp6nhgMEXzNfCges=
go.podman.io/image/v5 v5.40.0/go.mod h1:qgXf1abXJ+2l01pL8+CljaMKryeo6ahaHO7H51ooKIc=
go.podman.io/storage v1.63.0 h1:bj/pAWFhChbuBmejzno0iQLhU7FevGVXepRXm5pFGeA=
go.podman.io/storage v1.63.0/go.mod h1:z4Z9K+7GhKjWL/Y1O17+4f8a1KGijVeC9hr3tymhSOs=
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=
@@ -292,15 +247,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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
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=
@@ -309,10 +264,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.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/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
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=
@@ -320,22 +275,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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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=
@@ -345,8 +300,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.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
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=
@@ -356,33 +311,34 @@ 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.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/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
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=
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=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=

View File

@@ -29,7 +29,7 @@ in_get_ci_vm() {
if [[ "$1" == "--config" ]]; then
in_get_ci_vm "$1"
cat <<EOF
DESTDIR="/var/tmp/go/src/github.com/containers/skopeo"
DESTDIR="/var/tmp/go/src/go.podman.io/skopeo"
UPSTREAM_REPO="https://github.com/containers/skopeo.git"
GCLOUD_PROJECT="skopeo"
GCLOUD_IMGPROJECT="libpod-218412"

View File

@@ -1,27 +0,0 @@
#!/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

@@ -183,8 +183,8 @@ sudo pacman -S base-devel gpgme btrfs-progs
Make sure to clone this repository in your `GOPATH` - otherwise compilation fails.
```bash
git clone https://github.com/containers/skopeo $GOPATH/src/github.com/containers/skopeo
cd $GOPATH/src/github.com/containers/skopeo && make bin/skopeo
git clone https://github.com/containers/skopeo $GOPATH/src/go.podman.io/skopeo
cd $GOPATH/src/go.podman.io/skopeo && make bin/skopeo
```
By default the `make` command (make all) will build bin/skopeo and the documentation locally.

View File

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

View File

@@ -5,10 +5,10 @@ import (
"os/exec"
"testing"
"github.com/containers/skopeo/version"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.podman.io/skopeo/version"
)
const (
@@ -26,15 +26,17 @@ type skopeoSuite struct {
regV2WithAuth *testRegistryV2
}
var _ = suite.SetupAllSuite(&skopeoSuite{})
var _ = suite.TearDownAllSuite(&skopeoSuite{})
var (
_ = suite.SetupAllSuite(&skopeoSuite{})
_ = suite.TearDownAllSuite(&skopeoSuite{})
)
func (s *skopeoSuite) SetupSuite() {
t := s.T()
_, err := exec.LookPath(skopeoBinary)
require.NoError(t, err)
s.regV2 = setupRegistryV2At(t, privateRegistryURL0, false, false)
s.regV2WithAuth = setupRegistryV2At(t, privateRegistryURL1, true, false)
s.regV2 = setupRegistryV2At(t, privateRegistryURL0, false, registryVersionModern)
s.regV2WithAuth = setupRegistryV2At(t, privateRegistryURL1, true, registryVersionModern)
}
func (s *skopeoSuite) TearDownSuite() {

View File

@@ -12,6 +12,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
@@ -29,11 +30,12 @@ import (
)
const (
v2DockerRegistryURL = "localhost:5555" // Update also policy.json
v2s1DockerRegistryURL = "localhost:5556"
knownWindowsOnlyImage = "docker://mcr.microsoft.com/windows/nanoserver:1909"
knownListImageRepo = "docker://registry.fedoraproject.org/fedora-minimal"
knownListImage = knownListImageRepo + ":38"
v2DockerRegistryURL = "localhost:5555" // Update also policy.json
v2s1OnlyDockerRegistryURL = "localhost:5556"
v2s1SupportedDockerRegistryURL = "localhost:5557"
knownWindowsOnlyImage = "docker://mcr.microsoft.com/windows/nanoserver:1909"
knownListImageRepo = "docker://registry.fedoraproject.org/fedora-minimal"
knownListImage = knownListImageRepo + ":38"
)
func TestCopy(t *testing.T) {
@@ -42,14 +44,18 @@ func TestCopy(t *testing.T) {
type copySuite struct {
suite.Suite
cluster *openshiftCluster
registry *testRegistryV2
s1Registry *testRegistryV2
gpgHome string
cluster *openshiftCluster
registry *testRegistryV2
s1OnlyRegistry *testRegistryV2
s1SupportedRegistry *testRegistryV2
gpgHome string
fingerprint string
}
var _ = suite.SetupAllSuite(&copySuite{})
var _ = suite.TearDownAllSuite(&copySuite{})
var (
_ = suite.SetupAllSuite(&copySuite{})
_ = suite.TearDownAllSuite(&copySuite{})
)
func (s *copySuite) SetupSuite() {
t := s.T()
@@ -72,8 +78,9 @@ func (s *copySuite) SetupSuite() {
}
// FIXME: Set up TLS for the docker registry port instead of using "--tls-verify=false" all over the place.
s.registry = setupRegistryV2At(t, v2DockerRegistryURL, false, false)
s.s1Registry = setupRegistryV2At(t, v2s1DockerRegistryURL, false, true)
s.registry = setupRegistryV2At(t, v2DockerRegistryURL, false, registryVersionModern)
s.s1OnlyRegistry = setupRegistryV2At(t, v2s1OnlyDockerRegistryURL, false, registryVersionSchema1Only)
s.s1SupportedRegistry = setupRegistryV2At(t, v2s1SupportedDockerRegistryURL, false, registryVersionSchema1Supported)
s.gpgHome = t.TempDir()
t.Setenv("GNUPGHOME", s.gpgHome)
@@ -85,9 +92,15 @@ 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), 0600)
[]byte(out), 0o600)
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() {
@@ -95,8 +108,11 @@ func (s *copySuite) TearDownSuite() {
if s.registry != nil {
s.registry.tearDown()
}
if s.s1Registry != nil {
s.s1Registry.tearDown()
if s.s1OnlyRegistry != nil {
s.s1OnlyRegistry.tearDown()
}
if s.s1SupportedRegistry != nil {
s.s1SupportedRegistry.tearDown()
}
if s.cluster != nil {
s.cluster.tearDown(t)
@@ -171,6 +187,23 @@ func (s *copySuite) TestCopyNoneWithManifestList() {
assert.Equal(t, "manifest.json\nversion\n", out)
}
func (s *copySuite) TestCopyWithPlatformList() {
t := s.T()
dir1 := t.TempDir()
assertSkopeoSucceeds(t, "", "copy", "--retry-times", "3", "--multi-arch=linux/amd64,linux/arm64", knownListImage, "dir:"+dir1)
manifestPath := filepath.Join(dir1, "manifest.json")
readManifest, err := os.ReadFile(manifestPath)
require.NoError(t, err)
mimeType := manifest.GuessMIMEType(readManifest)
assert.Equal(t, "application/vnd.docker.distribution.manifest.list.v2+json", mimeType)
// Verify that we copied exactly 2 platform manifests (manifest list + 2 platforms = 3 manifest files)
manifestFiles, err := filepath.Glob(filepath.Join(dir1, "*manifest.json"))
require.NoError(t, err)
assert.Equal(t, 3, len(manifestFiles), "Expected manifest list + 2 platform manifests")
}
func (s *copySuite) TestCopyWithManifestListConverge() {
t := s.T()
oci1 := t.TempDir()
@@ -541,9 +574,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, 0644)
err = os.WriteFile(keysDir+"/private.key", privateKeyBytes, 0o644)
require.NoError(t, err)
err = os.WriteFile(keysDir+"/public.key", publicKeyBytes, 0644)
err = os.WriteFile(keysDir+"/public.key", publicKeyBytes, 0o644)
require.NoError(t, err)
// We can either perform encryption or decryption on the image.
@@ -568,7 +601,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, 0644)
err = os.WriteFile(keysDir+"/invalid_private.key", invalidPrivateKeyBytes, 0o644)
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",
@@ -604,7 +637,6 @@ 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) {
@@ -780,10 +812,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" by Sequoia.
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*",
// "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).*",
"--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.
@@ -796,8 +828,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" by Sequoia.
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*",
// "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).*",
"--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")
@@ -818,7 +850,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, 0755)
err := os.MkdirAll(topDir+suffix, 0o755)
require.NoError(t, err)
}
@@ -842,8 +874,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" by Sequoia.
assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*",
// "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).*",
"--policy", policy, "copy", topDirDest+"/restricted/personal", topDirDest+"/dest")
// Verify that the signed identity is verified.
@@ -894,13 +926,13 @@ func (s *copySuite) TestCopyCompression() {
topDir := t.TempDir()
for i, c := range []struct{ fixture, remote string }{
{"uncompressed-image-s1", "docker://" + v2DockerRegistryURL + "/compression/compression:s1"},
{"uncompressed-image-s1", "docker://" + v2s1SupportedDockerRegistryURL + "/compression/compression:s1"},
{"uncompressed-image-s2", "docker://" + v2DockerRegistryURL + "/compression/compression:s2"},
{"uncompressed-image-s1", "atomic:localhost:5000/myns/compression:s1"},
{"uncompressed-image-s2", "atomic:localhost:5000/myns/compression:s2"},
} {
dir := filepath.Join(topDir, fmt.Sprintf("case%d", i))
err := os.MkdirAll(dir, 0755)
err := os.MkdirAll(dir, 0o755)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "--tls-verify=false", "copy", "dir:fixtures/"+c.fixture, c.remote)
@@ -953,7 +985,7 @@ func (s *copySuite) TestCopyDockerLookaside() {
tmpDir := t.TempDir()
copyDest := filepath.Join(tmpDir, "dest")
err = os.Mkdir(copyDest, 0755)
err = os.Mkdir(copyDest, 0o755)
require.NoError(t, err)
dirDest := "dir:" + copyDest
plainLookaside := filepath.Join(tmpDir, "lookaside")
@@ -967,7 +999,7 @@ func (s *copySuite) TestCopyDockerLookaside() {
policy := s.policyFixture(nil)
registriesDir := filepath.Join(tmpDir, "registries.d")
err = os.Mkdir(registriesDir, 0755)
err = os.Mkdir(registriesDir, 0o755)
require.NoError(t, err)
registriesFile := fileFromFixture(t, "fixtures/registries.yaml",
map[string]string{"@lookaside@": plainLookaside, "@split-staging@": splitLookasideStaging, "@split-read@": splitLookasideReadServer.URL})
@@ -1020,7 +1052,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), 0755)
err := os.MkdirAll(filepath.Join(topDir, subdir), 0o755)
require.NoError(t, err)
}
registriesDir := filepath.Join(topDir, "registries.d")
@@ -1174,7 +1206,7 @@ func (s *copySuite) TestCopySchemaConversion() {
// Test conversion / schema autodetection both for the OpenShift embedded registry…
s.testCopySchemaConversionRegistries(t, "docker://localhost:5005/myns/schema1", "docker://localhost:5006/myns/schema2")
// … and for various docker/distribution registry versions.
s.testCopySchemaConversionRegistries(t, "docker://"+v2s1DockerRegistryURL+"/schema1", "docker://"+v2DockerRegistryURL+"/schema2")
s.testCopySchemaConversionRegistries(t, "docker://"+v2s1OnlyDockerRegistryURL+"/schema1", "docker://"+v2s1SupportedDockerRegistryURL+"/schema2")
}
func (s *copySuite) TestCopyManifestConversion() {
@@ -1213,7 +1245,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), 0755)
err := os.MkdirAll(filepath.Join(topDir, subdir), 0o755)
require.NoError(t, err)
}
input1Dir := filepath.Join(topDir, "input1")
@@ -1284,3 +1316,83 @@ 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 := filepath.Join(t.TempDir(), "policy.json")
err = os.WriteFile(policyFile, policyJSON, 0o600)
require.NoError(t, err)
// now copying with --require-signed should pass
destDir3 := t.TempDir()
assertSkopeoSucceeds(t, "", "--policy", policyFile, "--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, "--require-signed", "copy",
"dir:"+srcDir, "dir:"+destDir4)
}

View File

@@ -0,0 +1 @@
minVersion: "1.3"

View File

@@ -0,0 +1 @@
{} # No fields

View File

@@ -0,0 +1,3 @@
minVersion: "1.3"
namedGroups:
- "X25519MLKEM768"

View File

@@ -207,7 +207,7 @@ func (cluster *openshiftCluster) startRegistry(t *testing.T) {
cluster.processes = append(cluster.processes, cluster.startRegistryProcess(t, 5006, schema2Config))
}
// ocLogin runs (oc login) and (oc new-project) on the cluster, or terminates on failure.
// ocLoginToProject 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, 0700)
err := os.MkdirAll(cluster.dockerDir, 0o700)
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), 0600)
err = os.WriteFile(filepath.Join(cluster.dockerDir, "config.json"), []byte(configJSON), 0o600)
require.NoError(t, err)
}

View File

@@ -214,9 +214,7 @@ func (p *proxy) callGetRawBlob(args []any) (rval any, buf []byte, err error) {
var wg sync.WaitGroup
fetchchan := make(chan byteFetch, 1)
errchan := make(chan proxyError, 1)
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
defer close(fetchchan)
defer fd.datafd.Close()
buf, err := io.ReadAll(fd.datafd)
@@ -224,11 +222,8 @@ func (p *proxy) callGetRawBlob(args []any) (rval any, buf []byte, err error) {
content: buf,
err: err,
}
}()
wg.Add(1)
go func() {
defer wg.Done()
})
wg.Go(func() {
defer fd.errfd.Close()
defer close(errchan)
buf, err := io.ReadAll(fd.errfd)
@@ -249,7 +244,7 @@ func (p *proxy) callGetRawBlob(args []any) (rval any, buf []byte, err error) {
panic(unmarshalErr)
}
errchan <- proxyErr
}()
})
wg.Wait()
errMsg := <-errchan
@@ -266,7 +261,7 @@ func (p *proxy) callGetRawBlob(args []any) (rval any, buf []byte, err error) {
return
}
func newProxy() (*proxy, error) {
func newProxy(extraArgs ...string) (*proxy, error) {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_SEQPACKET, 0)
if err != nil {
return nil, err
@@ -282,7 +277,8 @@ func newProxy() (*proxy, error) {
}
// Note ExtraFiles starts at 3
proc := exec.Command(skopeoBinary, "experimental-image-proxy", "--sockfd", "3")
args := append(append([]string{}, extraArgs...), "experimental-image-proxy", "--sockfd", "3")
proc := exec.Command(skopeoBinary, args...)
proc.Stderr = os.Stderr
cmdLifecycleToParentIfPossible(proc)
proc.ExtraFiles = append(proc.ExtraFiles, theirfd)
@@ -389,7 +385,7 @@ func runTestMetadataAPIs(p *proxy, img string) error {
if err != nil {
return err
}
var layerInfoBytesData []interface{}
var layerInfoBytesData []any
err = json.Unmarshal(layerInfoBytes, &layerInfoBytesData)
if err != nil {
return err
@@ -480,7 +476,8 @@ func runTestGetBlob(p *proxy, img string) error {
func (s *proxySuite) TestProxyMetadata() {
t := s.T()
p, err := newProxy()
// The test image quay.io/coreos/11bot is only available for amd64
p, err := newProxy("--override-arch", "amd64")
require.NoError(t, err)
err = runTestMetadataAPIs(p, knownNotManifestListedImageX8664)

View File

@@ -13,8 +13,18 @@ import (
)
const (
binaryV2 = "registry"
binaryV2Schema1 = "registry-v2-schema1"
binaryV2 = "registry"
binaryV2Schema1Only = "registry-v2-schema1-only"
binaryV2Schema1Supported = "registry-v2-schema1-supported"
)
type registryVersion int
const (
registryVersionInvalid registryVersion = iota
registryVersionModern // Whatever comes from a packaged docker-distribution; as of 2026-03 supports schema2, not schema1.
registryVersionSchema1Only // Supports only schema1
registryVersionSchema1Supported // Supports both schema1 and schema2
)
type testRegistryV2 struct {
@@ -25,8 +35,8 @@ type testRegistryV2 struct {
email string
}
func setupRegistryV2At(t *testing.T, url string, auth, schema1 bool) *testRegistryV2 {
reg, err := newTestRegistryV2At(t, url, auth, schema1)
func setupRegistryV2At(t *testing.T, url string, auth bool, version registryVersion) *testRegistryV2 {
reg, err := newTestRegistryV2At(t, url, auth, version)
require.NoError(t, err)
// Wait for registry to be ready to serve requests.
@@ -43,7 +53,7 @@ func setupRegistryV2At(t *testing.T, url string, auth, schema1 bool) *testRegist
return reg
}
func newTestRegistryV2At(t *testing.T, url string, auth, schema1 bool) (*testRegistryV2, error) {
func newTestRegistryV2At(t *testing.T, url string, auth bool, version registryVersion) (*testRegistryV2, error) {
tmp := t.TempDir()
template := `version: 0.1
loglevel: debug
@@ -70,7 +80,7 @@ compatibility:
username = "testuser"
password = "testpassword"
email = "test@test.org"
if err := os.WriteFile(htpasswdPath, []byte(userpasswd), os.FileMode(0644)); err != nil {
if err := os.WriteFile(htpasswdPath, []byte(userpasswd), os.FileMode(0o644)); err != nil {
return nil, err
}
htpasswd = fmt.Sprintf(`auth:
@@ -89,10 +99,15 @@ compatibility:
}
var cmd *exec.Cmd
if schema1 {
cmd = exec.Command(binaryV2Schema1, confPath)
} else {
switch version {
case registryVersionModern:
cmd = exec.Command(binaryV2, "serve", confPath)
case registryVersionSchema1Only:
cmd = exec.Command(binaryV2Schema1Only, confPath)
case registryVersionSchema1Supported:
cmd = exec.Command(binaryV2Schema1Supported, "serve", confPath)
default:
return nil, fmt.Errorf("invalid registry version: %v", version)
}
consumeAndLogOutputs(t, fmt.Sprintf("registry-%s", url), cmd)

View File

@@ -1,12 +1,9 @@
package main
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
@@ -29,16 +26,6 @@ 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)
@@ -67,13 +54,11 @@ func (s *signingSuite) TestSignVerifySmoke() {
manifestPath := "fixtures/image.manifest.json"
dockerReference := "testing/smoketest"
sigOutput, err := os.CreateTemp("", "sig")
require.NoError(t, err)
defer os.Remove(sigOutput.Name())
assertSkopeoSucceeds(t, "^$", "standalone-sign", "-o", sigOutput.Name(),
sigOutput := filepath.Join(t.TempDir(), "sig")
assertSkopeoSucceeds(t, "^$", "standalone-sign", "-o", sigOutput,
manifestPath, dockerReference, s.fingerprint)
expected := fmt.Sprintf("^Signature verified using fingerprint %s, digest %s\n$", s.fingerprint, TestImageManifestDigest)
assertSkopeoSucceeds(t, expected, "standalone-verify", manifestPath,
dockerReference, s.fingerprint, sigOutput.Name())
dockerReference, s.fingerprint, sigOutput)
}

View File

@@ -46,18 +46,17 @@ type syncSuite struct {
registry *testRegistryV2
}
var _ = suite.SetupAllSuite(&syncSuite{})
var _ = suite.TearDownAllSuite(&syncSuite{})
var (
_ = suite.SetupAllSuite(&syncSuite{})
_ = suite.TearDownAllSuite(&syncSuite{})
)
func (s *syncSuite) SetupSuite() {
t := s.T()
const registryAuth = false
const registrySchema1 = false
if os.Getenv("SKOPEO_LOCAL_TESTS") == "1" {
t.Log("Running tests without a container")
fmt.Printf("NOTE: tests requires a V2 registry at url=%s, with auth=%t, schema1=%t \n", v2DockerRegistryURL, registryAuth, registrySchema1)
fmt.Printf("NOTE: tests requires a V2 registry at url=%s\n", v2DockerRegistryURL)
return
}
@@ -80,7 +79,7 @@ func (s *syncSuite) SetupSuite() {
}
// FIXME: Set up TLS for the docker registry port instead of using "--tls-verify=false" all over the place.
s.registry = setupRegistryV2At(t, v2DockerRegistryURL, registryAuth, registrySchema1)
s.registry = setupRegistryV2At(t, v2DockerRegistryURL, false, registryVersionModern)
gpgHome := t.TempDir()
t.Setenv("GNUPGHOME", gpgHome)
@@ -92,7 +91,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), 0600)
[]byte(out), 0o600)
require.NoError(t, err)
}
}
@@ -141,7 +140,7 @@ func (s *syncSuite) TestDocker2DirTagged() {
dir2 := path.Join(tmpDir, "dir2")
// sync docker => dir
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
_, err = os.Stat(path.Join(dir1, imagePath, "manifest.json"))
require.NoError(t, err)
@@ -168,7 +167,7 @@ func (s *syncSuite) TestDocker2DirTaggedAll() {
dir2 := path.Join(tmpDir, "dir2")
// sync docker => dir
assertSkopeoSucceeds(t, "", "sync", "--all", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--all", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
_, err = os.Stat(path.Join(dir1, imagePath, "manifest.json"))
require.NoError(t, err)
@@ -206,11 +205,11 @@ func (s *syncSuite) TestScoped() {
imagePath := imageRef.DockerReference().String()
dir1 := t.TempDir()
assertSkopeoSucceeds(t, "", "sync", "--src", "docker", "--dest", "dir", image, dir1)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--src", "docker", "--dest", "dir", image, dir1)
_, err = os.Stat(path.Join(dir1, path.Base(imagePath), "manifest.json"))
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
_, err = os.Stat(path.Join(dir1, imagePath, "manifest.json"))
require.NoError(t, err)
}
@@ -226,16 +225,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)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--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()
@@ -255,7 +254,7 @@ func (s *syncSuite) TestDocker2DirUntagged() {
imagePath := imageRef.DockerReference().String()
dir1 := path.Join(tmpDir, "dir1")
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
sysCtx := types.SystemContext{}
tags, err := docker.GetRepositoryTags(context.Background(), &sysCtx, imageRef)
@@ -290,9 +289,9 @@ func (s *syncSuite) TestYamlUntagged() {
// sync to the local registry
yamlFile := path.Join(tmpDir, "registries.yaml")
err = os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
err = os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "yaml", "--dest", "docker", "--dest-tls-verify=false", yamlFile, v2DockerRegistryURL)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--scoped", "--src", "yaml", "--dest", "docker", "--dest-tls-verify=false", yamlFile, v2DockerRegistryURL)
// sync back from local registry to a folder
os.Remove(yamlFile)
yamlConfig = fmt.Sprintf(`
@@ -302,7 +301,7 @@ func (s *syncSuite) TestYamlUntagged() {
%s: []
`, v2DockerRegistryURL, imagePath)
err = os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
err = os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
@@ -329,13 +328,13 @@ registry.k8s.io:
pause: ^[12]\.0$ # regex string test
`
// the ↑ regex strings always matches only 2 images
var nTags = 2
nTags := 2
assert.NotZero(t, nTags)
yamlFile := path.Join(tmpDir, "registries.yaml")
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
assertNumberOfManifestsInSubdirs(t, dir1, nTags)
}
@@ -351,9 +350,9 @@ registry.k8s.io:
- sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610
`
yamlFile := path.Join(tmpDir, "registries.yaml")
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
assertNumberOfManifestsInSubdirs(t, dir1, 1)
}
@@ -390,9 +389,9 @@ quay.io:
assert.NotZero(t, nTags)
yamlFile := path.Join(tmpDir, "registries.yaml")
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0644)
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
require.NoError(t, err)
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
assertNumberOfManifestsInSubdirs(t, dir1, nTags)
}
@@ -441,14 +440,13 @@ 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), 0644)
err := os.WriteFile(yamlFile, []byte(yamlConfig), 0o644)
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() {
@@ -459,14 +457,14 @@ 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)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--format=oci", "--all", "--src", "docker", "--dest", "dir", pullableTaggedImage, destDir1)
verifyManifestMIMEType(t, filepath.Join(destDir1, imageDir), imgspecv1.MediaTypeImageManifest)
assertSkopeoSucceeds(t, "", "sync", "--format=v2s2", "--all", "--src", "docker", "--dest", "dir", pullableTaggedImage, destDir2)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--format=v2s2", "--all", "--src", "docker", "--dest", "dir", pullableTaggedImage, destDir2)
verifyManifestMIMEType(t, filepath.Join(destDir2, imageDir), manifest.DockerV2Schema2MediaType)
assertSkopeoSucceeds(t, "", "sync", "--format=v2s1", "--all", "--src", "docker", "--dest", "dir", pullableTaggedImage, destDir3)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--format=v2s1", "--all", "--src", "docker", "--dest", "dir", pullableTaggedImage, destDir3)
verifyManifestMIMEType(t, filepath.Join(destDir3, imageDir), manifest.DockerV2Schema1SignedMediaType)
}
@@ -486,7 +484,7 @@ func (s *syncSuite) TestDocker2DockerTagged() {
dir2 := path.Join(tmpDir, "dir2")
// sync docker => docker
assertSkopeoSucceeds(t, "", "sync", "--scoped", "--dest-tls-verify=false", "--src", "docker", "--dest", "docker", image, v2DockerRegistryURL)
assertSkopeoSucceeds(t, "", "sync", "--retry-times", "3", "--scoped", "--dest-tls-verify=false", "--src", "docker", "--dest", "docker", image, v2DockerRegistryURL)
// copy docker => dir
assertSkopeoSucceeds(t, "", "copy", "--retry-times", "3", "docker://"+image, "dir:"+dir1)
@@ -512,14 +510,14 @@ func (s *syncSuite) TestDir2DockerTagged() {
image := pullableRepoWithLatestTag
dir1 := path.Join(tmpDir, "dir1")
err := os.Mkdir(dir1, 0755)
err := os.Mkdir(dir1, 0o755)
require.NoError(t, err)
dir2 := path.Join(tmpDir, "dir2")
err = os.Mkdir(dir2, 0755)
err = os.Mkdir(dir2, 0o755)
require.NoError(t, err)
// create leading dirs
err = os.MkdirAll(path.Dir(path.Join(dir1, image)), 0755)
err = os.MkdirAll(path.Dir(path.Join(dir1, image)), 0o755)
require.NoError(t, err)
// copy docker => dir
@@ -531,7 +529,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)), 0755)
err = os.MkdirAll(path.Dir(path.Join(dir2, image)), 0o755)
require.NoError(t, err)
// copy docker => dir
@@ -571,11 +569,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)
}
@@ -585,11 +583,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)
}
@@ -599,11 +597,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)
}

302
integration/tls_test.go Normal file
View File

@@ -0,0 +1,302 @@
package main
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/opencontainers/image-spec/specs-go"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.podman.io/image/v5/oci/layout"
)
func TestTLS(t *testing.T) {
suite.Run(t, &tlsSuite{})
}
type tlsSuite struct {
suite.Suite
defaultServer *tlsConfigServer
tls12Server *tlsConfigServer
nonPQCserver *tlsConfigServer
pqcServer *tlsConfigServer
expected []expectedBehavior
}
var (
_ = suite.SetupAllSuite(&tlsSuite{})
_ = suite.TearDownAllSuite(&tlsSuite{})
)
type expectedBehavior struct {
server *tlsConfigServer
tlsDetails string
expected string
}
func (s *tlsSuite) SetupSuite() {
t := s.T()
s.defaultServer = newServer(t, &tls.Config{})
s.tls12Server = newServer(t, &tls.Config{
MaxVersion: tls.VersionTLS12,
})
s.nonPQCserver = newServer(t, &tls.Config{
MinVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384, tls.CurveP521},
})
s.pqcServer = newServer(t, &tls.Config{
MinVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519MLKEM768},
})
s.expected = []expectedBehavior{
{
server: s.defaultServer,
tlsDetails: "fixtures/tls-details-anything.yaml",
expected: `\b418\b`, // "I'm a teapot"
},
{
server: s.tls12Server,
tlsDetails: "fixtures/tls-details-anything.yaml",
expected: `\b418\b`, // "I'm a teapot"
},
{
server: s.nonPQCserver,
tlsDetails: "fixtures/tls-details-anything.yaml",
expected: `\b418\b`, // "I'm a teapot"
},
{
server: s.pqcServer,
tlsDetails: "fixtures/tls-details-anything.yaml",
expected: `\b418\b`, // "I'm a teapot"
},
{
server: s.defaultServer,
tlsDetails: "fixtures/tls-details-1.3.yaml",
expected: `\b418\b`, // "I'm a teapot"
},
{
server: s.tls12Server,
tlsDetails: "fixtures/tls-details-1.3.yaml",
expected: `protocol version not supported`,
},
{
server: s.nonPQCserver,
tlsDetails: "fixtures/tls-details-1.3.yaml",
expected: `\b418\b`, // "I'm a teapot"
},
{
server: s.pqcServer,
tlsDetails: "fixtures/tls-details-1.3.yaml",
expected: `\b418\b`, // "I'm a teapot"
},
{
server: s.defaultServer,
tlsDetails: "fixtures/tls-details-pqc-only.yaml",
expected: `\b418\b`, // "I'm a teapot"
},
{
server: s.tls12Server,
tlsDetails: "fixtures/tls-details-pqc-only.yaml",
expected: `protocol version not supported`,
},
{
server: s.nonPQCserver,
tlsDetails: "fixtures/tls-details-pqc-only.yaml",
expected: `handshake failure`,
},
{
server: s.pqcServer,
tlsDetails: "fixtures/tls-details-pqc-only.yaml",
expected: `\b418\b`, // "I'm a teapot"
},
}
}
func (s *tlsSuite) TearDownSuite() {
}
func (s *tlsSuite) TestDockerDaemon() {
t := s.T()
// Our server doesnt perform client authentication, but the docker-daemon: option semantics
// requires us to provide a certificate if we want to specify a CA.
dockerCertPath := t.TempDir()
caPath := filepath.Join(dockerCertPath, "ca.pem")
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
require.NoError(t, err)
publicKey := &privateKey.PublicKey
err = os.WriteFile(filepath.Join(dockerCertPath, "key.pem"), pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}), 0o644)
require.NoError(t, err)
referenceTime := time.Now()
template := &x509.Certificate{
Subject: pkix.Name{
CommonName: "client",
},
NotBefore: referenceTime.Add(-1 * time.Minute),
NotAfter: referenceTime.Add(1 * time.Hour),
}
certDER, err := x509.CreateCertificate(rand.Reader, template, template, publicKey, privateKey)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(dockerCertPath, "cert.pem"), pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
}), 0o644)
require.NoError(t, err)
for _, e := range s.expected {
err := os.WriteFile(caPath, e.server.certBytes, 0o644)
require.NoError(t, err)
assertSkopeoFails(t, e.expected, "--tls-details", e.tlsDetails, "inspect", "--daemon-host", e.server.server.URL, "--cert-dir", dockerCertPath, "docker-daemon:repo:tag")
}
}
func (s *tlsSuite) TestRegistry() {
t := s.T()
caDir := t.TempDir()
caPath := filepath.Join(caDir, "ca.crt")
for _, e := range s.expected {
err := os.WriteFile(caPath, e.server.certBytes, 0o644)
require.NoError(t, err)
assertSkopeoFails(t, e.expected, "--tls-details", e.tlsDetails, "inspect", "--cert-dir", caDir, "docker://"+e.server.hostPort+"/repo")
}
}
func (s *tlsSuite) TestOCILayout() {
t := s.T()
caDir := t.TempDir()
caPath := filepath.Join(caDir, "ca.crt")
for _, e := range s.expected {
err := os.WriteFile(caPath, e.server.certBytes, 0o644)
require.NoError(t, err)
ociLayoutDir := t.TempDir()
destRef, err := layout.NewReference(ociLayoutDir, "repo:tag")
require.NoError(t, err)
dest, err := destRef.NewImageDestination(context.Background(), nil)
require.NoError(t, err)
manifestBytes, err := json.Marshal(imgspecv1.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: imgspecv1.MediaTypeImageManifest,
Config: imgspecv1.Descriptor{
MediaType: imgspecv1.MediaTypeImageConfig,
Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Size: 42,
URLs: []string{e.server.server.URL + "/config.json"},
},
Layers: []imgspecv1.Descriptor{},
ArtifactType: "",
Subject: &imgspecv1.Descriptor{},
Annotations: map[string]string{},
})
require.NoError(t, err)
err = dest.PutManifest(context.Background(), manifestBytes, nil)
require.NoError(t, err)
err = dest.Commit(context.Background(), nil) // nil is technically invalid, but works here
require.NoError(t, err)
err = dest.Close()
require.NoError(t, err)
// We dont expose types.OCICertPath in the CLI. But if we get far enough to be worrying about certificates,
// we already negotiated the TLS version and named group.
expected := e.expected
if expected == `\b418\b` {
expected = `certificate signed by unknown authority`
}
assertSkopeoFails(t, expected, "--tls-details", e.tlsDetails, "inspect", "oci:"+ociLayoutDir)
}
}
func (s *tlsSuite) TestOpenShift() {
t := s.T()
configDir := t.TempDir()
configPath := filepath.Join(configDir, "kubeconfig")
t.Setenv("KUBECONFIG", configPath)
for _, e := range s.expected {
err := os.WriteFile(configPath, fmt.Appendf(nil,
`apiVersion: v1
clusters:
- cluster:
certificate-authority: "%s"
server: "%s"
name: our-cluster
contexts:
- context:
cluster: our-cluster
namespace: default
name: our-context
current-context: our-context
kind: Config
`, e.server.certPath, e.server.server.URL), 0o644)
require.NoError(t, err)
// The atomic: image access starts with resolving the tag in a k8s API (and that will always fail, one way or another),
// so we never actually contact registry.example.
assertSkopeoFails(t, e.expected, "--tls-details", e.tlsDetails, "inspect", "atomic:registry.example/namespace/repo:tag")
}
}
// tlsConfigServer serves TLS with a specific configuration.
// It returns StatusTeapot on all requests; we use that to detect that the TLS negotiation succeeded,
// without bothering to actually implement any of the protocols.
type tlsConfigServer struct {
server *httptest.Server
hostPort string
certBytes []byte
certPath string
}
func newServer(t *testing.T, config *tls.Config) *tlsConfigServer {
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
}))
t.Cleanup(server.Close)
server.TLS = config.Clone()
server.StartTLS()
certBytes := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: server.Certificate().Raw,
})
certDir := t.TempDir()
certPath := filepath.Join(certDir, "cert.pem")
err := os.WriteFile(certPath, certBytes, 0o644)
require.NoError(t, err)
return &tlsConfigServer{
server: server,
hostPort: server.Listener.Addr().String(),
certBytes: certBytes,
certPath: certPath,
}
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"io"
"net"
"net/netip"
@@ -29,9 +30,22 @@ var skopeoBinary = func() string {
return "skopeo"
}()
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
// 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
)
// 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) {
@@ -192,14 +206,9 @@ func fileFromFixture(t *testing.T, inputPath string, edits map[string]string) st
contents = updated
}
file, err := os.CreateTemp("", "policy.json")
require.NoError(t, err)
path := file.Name()
t.Cleanup(func() { os.Remove(path) })
path := filepath.Join(t.TempDir(), "policy.json")
_, err = file.Write(contents)
require.NoError(t, err)
err = file.Close()
err = os.WriteFile(path, contents, 0o600)
require.NoError(t, err)
return path
}

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" by Sequoia.
# "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" or "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

43
vendor/cyphar.com/go-pathrs/.golangci.yml generated vendored Normal file
View File

@@ -0,0 +1,43 @@
# SPDX-License-Identifier: MPL-2.0
#
# libpathrs: safe path resolution on Linux
# Copyright (C) 2019-2025 SUSE LLC
# Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
#
# 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

View File

@@ -1,5 +1,3 @@
Copyright 2016 ISRG. All rights reserved.
Mozilla Public License Version 2.0
==================================
@@ -37,7 +35,7 @@ Mozilla Public License Version 2.0
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
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
@@ -359,7 +357,7 @@ 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 http://mozilla.org/MPL/2.0/.
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

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

@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 SUSE LLC
* Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
*
* 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

110
vendor/cyphar.com/go-pathrs/handle_linux.go generated vendored Normal file
View File

@@ -0,0 +1,110 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 SUSE LLC
* Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
*
* 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 underlying [os.File] yourself with [Handle.IntoFile]),
// because the security properties of libpathrs depend on users doing all
// relevant filesystem operations through libpathrs.
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.IntoFile], 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].
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

@@ -0,0 +1,75 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 SUSE LLC
* Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
*
* 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

@@ -0,0 +1,40 @@
//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 SUSE LLC
* Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
*
* 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

@@ -0,0 +1,337 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 SUSE LLC
* Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
*
* 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.size_t(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.size_t(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)
}

239
vendor/cyphar.com/go-pathrs/procfs/procfs_linux.go generated vendored Normal file
View File

@@ -0,0 +1,239 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 SUSE LLC
* Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
*
* 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 || uint64(pid) >= 1<<31 {
panic("invalid ProcBasePid value") // TODO: should this be an error?
}
pid32 := uint32(pid) //nolint:gosec // G115 false positive <https://github.com/securego/gosec/issues/1212>
return ProcBase{inner: libpathrs.ProcPid(pid32)}
}
// ThreadCloser is a callback that needs to be called when you are done
// operating on an [os.File] fetched using [Handle.OpenThreadSelf].
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.
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.
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)
}

341
vendor/cyphar.com/go-pathrs/root_linux.go generated vendored Normal file
View File

@@ -0,0 +1,341 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 SUSE LLC
* Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
*
* 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].
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].
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.
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.
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].
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].
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].
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].
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 [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].
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].
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].
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].
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()
}

56
vendor/cyphar.com/go-pathrs/utils_linux.go generated vendored Normal file
View File

@@ -0,0 +1,56 @@
//go:build linux
// SPDX-License-Identifier: MPL-2.0
/*
* libpathrs: safe path resolution on Linux
* Copyright (C) 2019-2025 SUSE LLC
* Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
*
* 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.0.0](https://toml.io/en/v1.0.0).
Compatible with TOML version [v1.1.0](https://toml.io/en/v1.1.0).
Documentation: https://pkg.go.dev/github.com/BurntSushi/toml

View File

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

View File

@@ -13,7 +13,6 @@ type itemType int
const (
itemError itemType = iota
itemNIL // used in the parser to indicate no type
itemEOF
itemText
itemString
@@ -47,14 +46,13 @@ func (p Position) String() string {
}
type lexer struct {
input string
start int
pos int
line int
state stateFn
items chan item
tomlNext bool
esc bool
input string
start int
pos int
line int
state stateFn
items chan item
esc bool
// Allow for backing up up to 4 runes. This is necessary because TOML
// contains 3-rune tokens (""" and ''').
@@ -90,14 +88,13 @@ func (lx *lexer) nextItem() item {
}
}
func lex(input string, tomlNext bool) *lexer {
func lex(input string) *lexer {
lx := &lexer{
input: input,
state: lexTop,
items: make(chan item, 10),
stack: make([]stateFn, 0, 10),
line: 1,
tomlNext: tomlNext,
input: input,
state: lexTop,
items: make(chan item, 10),
stack: make([]stateFn, 0, 10),
line: 1,
}
return lx
}
@@ -108,7 +105,7 @@ func (lx *lexer) push(state stateFn) {
func (lx *lexer) pop() stateFn {
if len(lx.stack) == 0 {
return lx.errorf("BUG in lexer: no states to pop")
panic("BUG in lexer: no states to pop")
}
last := lx.stack[len(lx.stack)-1]
lx.stack = lx.stack[0 : len(lx.stack)-1]
@@ -305,6 +302,8 @@ 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)
@@ -392,8 +391,6 @@ 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
@@ -412,7 +409,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, lx.tomlNext) {
if isBareKeyChar(r) {
return lexBareName
}
lx.backup()
@@ -420,23 +417,23 @@ func lexBareName(lx *lexer) stateFn {
return lx.pop()
}
// lexBareName lexes one part of a key or table.
//
// It assumes that at least one valid character for the table has already been
// read.
// lexQuotedName lexes one part of a quoted key or table name. It assumes that
// it starts lexing at the quote itself (" or ').
//
// 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:
@@ -464,17 +461,19 @@ func lexKeyStart(lx *lexer) stateFn {
func lexKeyNameStart(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.peek(); {
case r == '=' || r == eof:
return lx.errorf("unexpected '='")
case r == '.':
return lx.errorf("unexpected '.'")
default:
lx.push(lexKeyEnd)
return lexBareName
case r == '"' || r == '\'':
lx.ignore()
lx.push(lexKeyEnd)
return lexQuotedName
default:
lx.push(lexKeyEnd)
return lexBareName
// TODO: I think these can never be reached?
case r == '=' || r == eof:
return lx.errorf("unexpected '='")
case r == '.':
return lx.errorf("unexpected '.'")
}
}
@@ -485,7 +484,7 @@ func lexKeyEnd(lx *lexer) stateFn {
switch r := lx.next(); {
case isWhitespace(r):
return lexSkip(lx, lexKeyEnd)
case r == eof:
case r == eof: // TODO: never reached
return lx.errorf("unexpected EOF; expected key separator '='")
case r == '.':
lx.ignore()
@@ -628,10 +627,7 @@ func lexInlineTableValue(lx *lexer) stateFn {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValue)
case isNL(r):
if lx.tomlNext {
return lexSkip(lx, lexInlineTableValue)
}
return lx.errorPrevLine(errLexInlineTableNL{})
return lexSkip(lx, lexInlineTableValue)
case r == '#':
lx.push(lexInlineTableValue)
return lexCommentStart
@@ -653,10 +649,7 @@ func lexInlineTableValueEnd(lx *lexer) stateFn {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValueEnd)
case isNL(r):
if lx.tomlNext {
return lexSkip(lx, lexInlineTableValueEnd)
}
return lx.errorPrevLine(errLexInlineTableNL{})
return lexSkip(lx, lexInlineTableValueEnd)
case r == '#':
lx.push(lexInlineTableValueEnd)
return lexCommentStart
@@ -664,10 +657,7 @@ func lexInlineTableValueEnd(lx *lexer) stateFn {
lx.ignore()
lx.skip(isWhitespace)
if lx.peek() == '}' {
if lx.tomlNext {
return lexInlineTableValueEnd
}
return lx.errorf("trailing comma not allowed in inline tables")
return lexInlineTableValueEnd
}
return lexInlineTableValue
case r == '}':
@@ -855,9 +845,6 @@ func lexStringEscape(lx *lexer) stateFn {
r := lx.next()
switch r {
case 'e':
if !lx.tomlNext {
return lx.error(errLexEscape{r})
}
fallthrough
case 'b':
fallthrough
@@ -878,9 +865,6 @@ 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
@@ -928,19 +912,9 @@ func lexLongUnicodeEscape(lx *lexer) stateFn {
// lexBaseNumberOrDate can differentiate base prefixed integers from other
// types.
func lexNumberOrDateStart(lx *lexer) stateFn {
r := lx.next()
switch r {
case '0':
if lx.next() == '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
}
@@ -1196,13 +1170,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 + "()"
}
@@ -1210,8 +1184,6 @@ func (itype itemType) String() string {
switch itype {
case itemError:
return "Error"
case itemNIL:
return "NIL"
case itemEOF:
return "EOF"
case itemText:
@@ -1226,18 +1198,22 @@ func (itype itemType) String() string {
return "Float"
case itemDatetime:
return "DateTime"
case itemTableStart:
return "TableStart"
case itemTableEnd:
return "TableEnd"
case itemKeyStart:
return "KeyStart"
case itemKeyEnd:
return "KeyEnd"
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 itemCommentStart:
return "CommentStart"
case itemInlineTableStart:
@@ -1266,7 +1242,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, tomlNext bool) bool {
func isBareKeyChar(r rune) bool {
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') || r == '_' || r == '-'
}

View File

@@ -3,7 +3,6 @@ package toml
import (
"fmt"
"math"
"os"
"strconv"
"strings"
"time"
@@ -17,7 +16,6 @@ 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.
@@ -32,8 +30,6 @@ 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 {
@@ -73,10 +69,9 @@ func parse(data string) (p *parser, err error) {
p = &parser{
keyInfo: make(map[string]keyInfo),
mapping: make(map[string]any),
lx: lex(data, tomlNext),
lx: lex(data),
ordered: make([]Key, 0),
implicits: make(map[string]struct{}),
tomlNext: tomlNext,
}
for {
item := p.next()
@@ -350,17 +345,14 @@ func (p *parser) valueFloat(it item) (any, tomlType) {
var dtTypes = []struct {
fmt string
zone *time.Location
next bool
}{
{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},
{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},
}
func (p *parser) valueDatetime(it item) (any, tomlType) {
@@ -371,9 +363,6 @@ 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) {
@@ -644,6 +633,11 @@ 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
@@ -802,10 +796,8 @@ func (p *parser) replaceEscapes(it item, str string) string {
b.WriteByte(0x0d)
skip = 1
case 'e':
if p.tomlNext {
b.WriteByte(0x1b)
skip = 1
}
b.WriteByte(0x1b)
skip = 1
case '"':
b.WriteByte(0x22)
skip = 1
@@ -815,11 +807,9 @@ 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':
if p.tomlNext {
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+4])
b.WriteRune(escaped)
skip = 3
}
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 +1,2 @@
_fuzz/
_fuzz/
.devcontainer/

View File

@@ -1,27 +1,42 @@
run:
deadline: 2m
version: "2"
linters:
disable-all: true
default: none
enable:
- misspell
- govet
- staticcheck
- errcheck
- unparam
- ineffassign
- nakedret
- gocyclo
- dupl
- goimports
- revive
- errcheck
- gocyclo
- gosec
- gosimple
- typecheck
- govet
- ineffassign
- misspell
- nakedret
- revive
- staticcheck
- unparam
- unused
linters-settings:
gofmt:
simplify: true
dupl:
threshold: 600
settings:
dupl:
threshold: 600
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- goimports
settings:
gofmt:
simplify: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -21,21 +21,43 @@ type Constraints struct {
IncludePrerelease bool
}
// MaxConstraintLen is the maximum allowed length of a constraint string.
const MaxConstraintLen = 512
// MaxConstraintGroups is the maximum number of OR groups allowed in a
// constraint string.
const MaxConstraintGroups = 32
// ErrConstraintTooLong is returned when a constraint string exceeds the
// maximum allowed length.
var ErrConstraintTooLong = fmt.Errorf("constraint string is too long (max %d bytes)", MaxConstraintLen)
// ErrTooManyConstraintGroups is returned when a constraint string contains
// too many OR groups.
var ErrTooManyConstraintGroups = fmt.Errorf("too many constraint groups (max %d)", MaxConstraintGroups)
// NewConstraint returns a Constraints instance that a Version instance can
// be checked against. If there is a parse error it will be returned.
func NewConstraint(c string) (*Constraints, error) {
if len(c) > MaxConstraintLen {
return nil, ErrConstraintTooLong
}
// Rewrite - ranges into a comparison operation.
c = rewriteRange(c)
ors := strings.Split(c, "||")
if len(ors) > MaxConstraintGroups {
return nil, ErrTooManyConstraintGroups
}
lenors := len(ors)
or := make([][]*constraint, lenors)
hasPre := make([]bool, lenors)
for k, v := range ors {
// Validate the segment
if !validConstraintRegex.MatchString(v) {
return nil, fmt.Errorf("improper constraint: %s", v)
return nil, fmt.Errorf("improper constraint: %q", v)
}
cs := findConstraintRegex.FindAllString(v, -1)
@@ -104,9 +126,9 @@ func (cs Constraints) Validate(v *Version) (bool, []error) {
for _, c := range o {
// Before running the check handle the case there the version is
// a prerelease and the check is not searching for prereleases.
if !(cs.IncludePrerelease || cs.containsPre[i]) && v.pre != "" {
if !cs.IncludePrerelease && !cs.containsPre[i] && v.pre != "" {
if !prerelesase {
em := fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
em := fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
e = append(e, em)
prerelesase = true
}
@@ -258,7 +280,7 @@ func parseConstraint(c string) (*constraint, error) {
if len(c) > 0 {
m := constraintRegex.FindStringSubmatch(c)
if m == nil {
return nil, fmt.Errorf("improper constraint: %s", c)
return nil, fmt.Errorf("improper constraint: %q", c)
}
cs := &constraint{
@@ -325,7 +347,7 @@ func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
}
if c.dirty {
@@ -335,7 +357,7 @@ func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error
if c.con.Minor() != v.Minor() && !c.minorDirty {
return true, nil
} else if c.minorDirty {
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
return false, fmt.Errorf("%q is equal to %q", v, c.orig)
} else if c.con.Patch() != v.Patch() && !c.patchDirty {
return true, nil
} else if c.patchDirty {
@@ -345,15 +367,15 @@ func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error
if eq {
return true, nil
}
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
return false, fmt.Errorf("%q is equal to %q", v, c.orig)
}
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
return false, fmt.Errorf("%q is equal to %q", v, c.orig)
}
}
eq := v.Equal(c.con)
if eq {
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
return false, fmt.Errorf("%q is equal to %q", v, c.orig)
}
return true, nil
@@ -364,7 +386,7 @@ func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, er
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
}
var eq bool
@@ -374,17 +396,17 @@ func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, er
if eq {
return true, nil
}
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig)
}
if v.Major() > c.con.Major() {
return true, nil
} else if v.Major() < c.con.Major() {
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig)
} else if c.minorDirty {
// This is a range case such as >11. When the version is something like
// 11.1.0 is it not > 11. For that we would need 12 or higher
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig)
} else if c.patchDirty {
// This is for ranges such as >11.1. A version of 11.1.1 is not greater
// which one of 11.2.1 is greater
@@ -392,7 +414,7 @@ func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, er
if eq {
return true, nil
}
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig)
}
// If we have gotten here we are not comparing pre-preleases and can use the
@@ -401,21 +423,21 @@ func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, er
if eq {
return true, nil
}
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig)
}
func constraintLessThan(v *Version, c *constraint, includePre bool) (bool, error) {
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
}
eq := v.Compare(c.con) < 0
if eq {
return true, nil
}
return false, fmt.Errorf("%s is greater than or equal to %s", v, c.orig)
return false, fmt.Errorf("%q is greater than or equal to %q", v, c.orig)
}
func constraintGreaterThanEqual(v *Version, c *constraint, includePre bool) (bool, error) {
@@ -423,21 +445,21 @@ func constraintGreaterThanEqual(v *Version, c *constraint, includePre bool) (boo
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
}
eq := v.Compare(c.con) >= 0
if eq {
return true, nil
}
return false, fmt.Errorf("%s is less than %s", v, c.orig)
return false, fmt.Errorf("%q is less than %q", v, c.orig)
}
func constraintLessThanEqual(v *Version, c *constraint, includePre bool) (bool, error) {
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
}
var eq bool
@@ -447,13 +469,13 @@ func constraintLessThanEqual(v *Version, c *constraint, includePre bool) (bool,
if eq {
return true, nil
}
return false, fmt.Errorf("%s is greater than %s", v, c.orig)
return false, fmt.Errorf("%q is greater than %q", v, c.orig)
}
if v.Major() > c.con.Major() {
return false, fmt.Errorf("%s is greater than %s", v, c.orig)
return false, fmt.Errorf("%q is greater than %q", v, c.orig)
} else if v.Major() == c.con.Major() && v.Minor() > c.con.Minor() && !c.minorDirty {
return false, fmt.Errorf("%s is greater than %s", v, c.orig)
return false, fmt.Errorf("%q is greater than %q", v, c.orig)
}
return true, nil
@@ -469,11 +491,11 @@ func constraintTilde(v *Version, c *constraint, includePre bool) (bool, error) {
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
}
if v.LessThan(c.con) {
return false, fmt.Errorf("%s is less than %s", v, c.orig)
return false, fmt.Errorf("%q is less than %q", v, c.orig)
}
// ~0.0.0 is a special case where all constraints are accepted. It's
@@ -484,11 +506,11 @@ func constraintTilde(v *Version, c *constraint, includePre bool) (bool, error) {
}
if v.Major() != c.con.Major() {
return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig)
return false, fmt.Errorf("%q does not have same major version as %q", v, c.orig)
}
if v.Minor() != c.con.Minor() && !c.minorDirty {
return false, fmt.Errorf("%s does not have same major and minor version as %s", v, c.orig)
return false, fmt.Errorf("%q does not have same major and minor version as %q", v, c.orig)
}
return true, nil
@@ -500,7 +522,7 @@ func constraintTildeOrEqual(v *Version, c *constraint, includePre bool) (bool, e
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
}
if c.dirty {
@@ -512,7 +534,7 @@ func constraintTildeOrEqual(v *Version, c *constraint, includePre bool) (bool, e
return true, nil
}
return false, fmt.Errorf("%s is not equal to %s", v, c.orig)
return false, fmt.Errorf("%q is not equal to %q", v, c.orig)
}
// ^* --> (any)
@@ -528,12 +550,12 @@ func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) {
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
}
// This less than handles prereleases
if v.LessThan(c.con) {
return false, fmt.Errorf("%s is less than %s", v, c.orig)
return false, fmt.Errorf("%q is less than %q", v, c.orig)
}
var eq bool
@@ -548,12 +570,12 @@ func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) {
if eq {
return true, nil
}
return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig)
return false, fmt.Errorf("%q does not have same major version as %q", v, c.orig)
}
// ^ when the major is 0 and minor > 0 is >=0.y.z < 0.y+1
if c.con.Major() == 0 && v.Major() > 0 {
return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig)
return false, fmt.Errorf("%q does not have same major version as %q", v, c.orig)
}
// If the con Minor is > 0 it is not dirty
if c.con.Minor() > 0 || c.patchDirty {
@@ -561,11 +583,11 @@ func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) {
if eq {
return true, nil
}
return false, fmt.Errorf("%s does not have same minor version as %s. Expected minor versions to match when constraint major version is 0", v, c.orig)
return false, fmt.Errorf("%q does not have same minor version as %q. Expected minor versions to match when constraint major version is 0", v, c.orig)
}
// ^ when the minor is 0 and minor > 0 is =0.0.z
if c.con.Minor() == 0 && v.Minor() > 0 {
return false, fmt.Errorf("%s does not have same minor version as %s", v, c.orig)
return false, fmt.Errorf("%q does not have same minor version as %q", v, c.orig)
}
// At this point the major is 0 and the minor is 0 and not dirty. The patch
@@ -574,7 +596,7 @@ func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) {
if eq {
return true, nil
}
return false, fmt.Errorf("%s does not equal %s. Expect version and constraint to equal when major and minor versions are 0", v, c.orig)
return false, fmt.Errorf("%q does not equal %q. Expect version and constraint to equal when major and minor versions are 0", v, c.orig)
}
func isX(x string) bool {

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
@@ -48,8 +49,16 @@ var (
// ErrInvalidPrerelease is returned when the pre-release is an invalid format
ErrInvalidPrerelease = errors.New("invalid prerelease string")
// ErrVersionTooLong is returned when a version string exceeds the
// maximum allowed length.
ErrVersionTooLong = fmt.Errorf("version string is too long (max %d bytes)", MaxVersionLen)
)
// MaxVersionLen is the maximum allowed length of a version string. This guards
// against unbounded input causing excessive memory allocations during parsing.
const MaxVersionLen = 256
// semVerRegex is the regular expression used to parse a semantic version.
// This is not the official regex from the semver spec. It has been modified to allow for loose handling
// where versions like 2.1 are detected.
@@ -94,6 +103,10 @@ func StrictNewVersion(v string) (*Version, error) {
return nil, ErrEmptyString
}
if len(v) > MaxVersionLen {
return nil, ErrVersionTooLong
}
// Split the parts into [0]major, [1]minor, and [2]patch,prerelease,build
parts := strings.SplitN(v, ".", 3)
if len(parts) != 3 {
@@ -161,6 +174,9 @@ func StrictNewVersion(v string) (*Version, error) {
// attempts to convert it to SemVer. If you want to validate it was a strict
// semantic version at parse time see StrictNewVersion().
func NewVersion(v string) (*Version, error) {
if len(v) > MaxVersionLen {
return nil, ErrVersionTooLong
}
if CoerceNewVersion {
return coerceNewVersion(v)
}
@@ -289,6 +305,8 @@ func coerceNewVersion(v string) (*Version, error) {
// New creates a new instance of Version with each of the parts passed in as
// arguments instead of parsing a version string.
// Note, New does not validate prerelease or metadata. Incorrect information can
// be passed in.
func New(major, minor, patch uint64, pre, metadata string) *Version {
v := Version{
major: major,
@@ -301,6 +319,7 @@ func New(major, minor, patch uint64, pre, metadata string) *Version {
v.original = v.String()
// TODO: In the next semver major version validate the pre and metadata. Return error if there is one.
return &v
}
@@ -388,6 +407,9 @@ func (v Version) IncPatch() Version {
} else {
vNext.metadata = ""
vNext.pre = ""
if v.patch == math.MaxUint64 {
panic("patch version increment would overflow uint64")
}
vNext.patch = v.patch + 1
}
vNext.original = v.originalVPrefix() + "" + vNext.String()
@@ -404,6 +426,9 @@ func (v Version) IncMinor() Version {
vNext.metadata = ""
vNext.pre = ""
vNext.patch = 0
if v.minor == math.MaxUint64 {
panic("minor version increment would overflow uint64")
}
vNext.minor = v.minor + 1
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext
@@ -421,6 +446,9 @@ func (v Version) IncMajor() Version {
vNext.pre = ""
vNext.patch = 0
vNext.minor = 0
if v.major == math.MaxUint64 {
panic("major version increment would overflow uint64")
}
vNext.major = v.major + 1
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext
@@ -568,7 +596,16 @@ func (v Version) MarshalText() ([]byte, error) {
// Scan implements the SQL.Scanner interface.
func (v *Version) Scan(value interface{}) error {
var s string
s, _ = value.(string)
switch t := value.(type) {
case string:
s = t
case []byte:
s = string(t)
case nil:
return fmt.Errorf("cannot scan nil into Version")
default:
return fmt.Errorf("unsupported Scan type %T", value)
}
temp, err := NewVersion(s)
if err != nil {
return err

22
vendor/github.com/cespare/xxhash/v2/LICENSE.txt generated vendored Normal file
View File

@@ -0,0 +1,22 @@
Copyright (c) 2016 Caleb Spare
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

74
vendor/github.com/cespare/xxhash/v2/README.md generated vendored Normal file
View File

@@ -0,0 +1,74 @@
# xxhash
[![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2)
[![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml)
xxhash is a Go implementation of the 64-bit [xxHash] algorithm, XXH64. This is a
high-quality hashing algorithm that is much faster than anything in the Go
standard library.
This package provides a straightforward API:
```
func Sum64(b []byte) uint64
func Sum64String(s string) uint64
type Digest struct{ ... }
func New() *Digest
```
The `Digest` type implements hash.Hash64. Its key methods are:
```
func (*Digest) Write([]byte) (int, error)
func (*Digest) WriteString(string) (int, error)
func (*Digest) Sum64() uint64
```
The package is written with optimized pure Go and also contains even faster
assembly implementations for amd64 and arm64. If desired, the `purego` build tag
opts into using the Go code even on those architectures.
[xxHash]: http://cyan4973.github.io/xxHash/
## Compatibility
This package is in a module and the latest code is in version 2 of the module.
You need a version of Go with at least "minimal module compatibility" to use
github.com/cespare/xxhash/v2:
* 1.9.7+ for Go 1.9
* 1.10.3+ for Go 1.10
* Go 1.11 or later
I recommend using the latest release of Go.
## Benchmarks
Here are some quick benchmarks comparing the pure-Go and assembly
implementations of Sum64.
| input size | purego | asm |
| ---------- | --------- | --------- |
| 4 B | 1.3 GB/s | 1.2 GB/s |
| 16 B | 2.9 GB/s | 3.5 GB/s |
| 100 B | 6.9 GB/s | 8.1 GB/s |
| 4 KB | 11.7 GB/s | 16.7 GB/s |
| 10 MB | 12.0 GB/s | 17.3 GB/s |
These numbers were generated on Ubuntu 20.04 with an Intel Xeon Platinum 8252C
CPU using the following commands under Go 1.19.2:
```
benchstat <(go test -tags purego -benchtime 500ms -count 15 -bench 'Sum64$')
benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$')
```
## Projects using this package
- [InfluxDB](https://github.com/influxdata/influxdb)
- [Prometheus](https://github.com/prometheus/prometheus)
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
- [FreeCache](https://github.com/coocood/freecache)
- [FastCache](https://github.com/VictoriaMetrics/fastcache)
- [Ristretto](https://github.com/dgraph-io/ristretto)
- [Badger](https://github.com/dgraph-io/badger)

10
vendor/github.com/cespare/xxhash/v2/testall.sh generated vendored Normal file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
set -eu -o pipefail
# Small convenience script for running the tests with various combinations of
# arch/tags. This assumes we're running on amd64 and have qemu available.
go test ./...
go test -tags purego ./...
GOARCH=arm64 go test
GOARCH=arm64 go test -tags purego

243
vendor/github.com/cespare/xxhash/v2/xxhash.go generated vendored Normal file
View File

@@ -0,0 +1,243 @@
// Package xxhash implements the 64-bit variant of xxHash (XXH64) as described
// at http://cyan4973.github.io/xxHash/.
package xxhash
import (
"encoding/binary"
"errors"
"math/bits"
)
const (
prime1 uint64 = 11400714785074694791
prime2 uint64 = 14029467366897019727
prime3 uint64 = 1609587929392839161
prime4 uint64 = 9650029242287828579
prime5 uint64 = 2870177450012600261
)
// Store the primes in an array as well.
//
// The consts are used when possible in Go code to avoid MOVs but we need a
// contiguous array for the assembly code.
var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5}
// Digest implements hash.Hash64.
//
// Note that a zero-valued Digest is not ready to receive writes.
// Call Reset or create a Digest using New before calling other methods.
type Digest struct {
v1 uint64
v2 uint64
v3 uint64
v4 uint64
total uint64
mem [32]byte
n int // how much of mem is used
}
// New creates a new Digest with a zero seed.
func New() *Digest {
return NewWithSeed(0)
}
// NewWithSeed creates a new Digest with the given seed.
func NewWithSeed(seed uint64) *Digest {
var d Digest
d.ResetWithSeed(seed)
return &d
}
// Reset clears the Digest's state so that it can be reused.
// It uses a seed value of zero.
func (d *Digest) Reset() {
d.ResetWithSeed(0)
}
// ResetWithSeed clears the Digest's state so that it can be reused.
// It uses the given seed to initialize the state.
func (d *Digest) ResetWithSeed(seed uint64) {
d.v1 = seed + prime1 + prime2
d.v2 = seed + prime2
d.v3 = seed
d.v4 = seed - prime1
d.total = 0
d.n = 0
}
// Size always returns 8 bytes.
func (d *Digest) Size() int { return 8 }
// BlockSize always returns 32 bytes.
func (d *Digest) BlockSize() int { return 32 }
// Write adds more data to d. It always returns len(b), nil.
func (d *Digest) Write(b []byte) (n int, err error) {
n = len(b)
d.total += uint64(n)
memleft := d.mem[d.n&(len(d.mem)-1):]
if d.n+n < 32 {
// This new data doesn't even fill the current block.
copy(memleft, b)
d.n += n
return
}
if d.n > 0 {
// Finish off the partial block.
c := copy(memleft, b)
d.v1 = round(d.v1, u64(d.mem[0:8]))
d.v2 = round(d.v2, u64(d.mem[8:16]))
d.v3 = round(d.v3, u64(d.mem[16:24]))
d.v4 = round(d.v4, u64(d.mem[24:32]))
b = b[c:]
d.n = 0
}
if len(b) >= 32 {
// One or more full blocks left.
nw := writeBlocks(d, b)
b = b[nw:]
}
// Store any remaining partial block.
copy(d.mem[:], b)
d.n = len(b)
return
}
// Sum appends the current hash to b and returns the resulting slice.
func (d *Digest) Sum(b []byte) []byte {
s := d.Sum64()
return append(
b,
byte(s>>56),
byte(s>>48),
byte(s>>40),
byte(s>>32),
byte(s>>24),
byte(s>>16),
byte(s>>8),
byte(s),
)
}
// Sum64 returns the current hash.
func (d *Digest) Sum64() uint64 {
var h uint64
if d.total >= 32 {
v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4
h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4)
h = mergeRound(h, v1)
h = mergeRound(h, v2)
h = mergeRound(h, v3)
h = mergeRound(h, v4)
} else {
h = d.v3 + prime5
}
h += d.total
b := d.mem[:d.n&(len(d.mem)-1)]
for ; len(b) >= 8; b = b[8:] {
k1 := round(0, u64(b[:8]))
h ^= k1
h = rol27(h)*prime1 + prime4
}
if len(b) >= 4 {
h ^= uint64(u32(b[:4])) * prime1
h = rol23(h)*prime2 + prime3
b = b[4:]
}
for ; len(b) > 0; b = b[1:] {
h ^= uint64(b[0]) * prime5
h = rol11(h) * prime1
}
h ^= h >> 33
h *= prime2
h ^= h >> 29
h *= prime3
h ^= h >> 32
return h
}
const (
magic = "xxh\x06"
marshaledSize = len(magic) + 8*5 + 32
)
// MarshalBinary implements the encoding.BinaryMarshaler interface.
func (d *Digest) MarshalBinary() ([]byte, error) {
b := make([]byte, 0, marshaledSize)
b = append(b, magic...)
b = appendUint64(b, d.v1)
b = appendUint64(b, d.v2)
b = appendUint64(b, d.v3)
b = appendUint64(b, d.v4)
b = appendUint64(b, d.total)
b = append(b, d.mem[:d.n]...)
b = b[:len(b)+len(d.mem)-d.n]
return b, nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
func (d *Digest) UnmarshalBinary(b []byte) error {
if len(b) < len(magic) || string(b[:len(magic)]) != magic {
return errors.New("xxhash: invalid hash state identifier")
}
if len(b) != marshaledSize {
return errors.New("xxhash: invalid hash state size")
}
b = b[len(magic):]
b, d.v1 = consumeUint64(b)
b, d.v2 = consumeUint64(b)
b, d.v3 = consumeUint64(b)
b, d.v4 = consumeUint64(b)
b, d.total = consumeUint64(b)
copy(d.mem[:], b)
d.n = int(d.total % uint64(len(d.mem)))
return nil
}
func appendUint64(b []byte, x uint64) []byte {
var a [8]byte
binary.LittleEndian.PutUint64(a[:], x)
return append(b, a[:]...)
}
func consumeUint64(b []byte) ([]byte, uint64) {
x := u64(b)
return b[8:], x
}
func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) }
func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) }
func round(acc, input uint64) uint64 {
acc += input * prime2
acc = rol31(acc)
acc *= prime1
return acc
}
func mergeRound(acc, val uint64) uint64 {
val = round(0, val)
acc ^= val
acc = acc*prime1 + prime4
return acc
}
func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) }
func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) }
func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) }
func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) }
func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) }
func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) }
func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) }
func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) }

209
vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s generated vendored Normal file
View File

@@ -0,0 +1,209 @@
//go:build !appengine && gc && !purego
// +build !appengine
// +build gc
// +build !purego
#include "textflag.h"
// Registers:
#define h AX
#define d AX
#define p SI // pointer to advance through b
#define n DX
#define end BX // loop end
#define v1 R8
#define v2 R9
#define v3 R10
#define v4 R11
#define x R12
#define prime1 R13
#define prime2 R14
#define prime4 DI
#define round(acc, x) \
IMULQ prime2, x \
ADDQ x, acc \
ROLQ $31, acc \
IMULQ prime1, acc
// round0 performs the operation x = round(0, x).
#define round0(x) \
IMULQ prime2, x \
ROLQ $31, x \
IMULQ prime1, x
// mergeRound applies a merge round on the two registers acc and x.
// It assumes that prime1, prime2, and prime4 have been loaded.
#define mergeRound(acc, x) \
round0(x) \
XORQ x, acc \
IMULQ prime1, acc \
ADDQ prime4, acc
// blockLoop processes as many 32-byte blocks as possible,
// updating v1, v2, v3, and v4. It assumes that there is at least one block
// to process.
#define blockLoop() \
loop: \
MOVQ +0(p), x \
round(v1, x) \
MOVQ +8(p), x \
round(v2, x) \
MOVQ +16(p), x \
round(v3, x) \
MOVQ +24(p), x \
round(v4, x) \
ADDQ $32, p \
CMPQ p, end \
JLE loop
// func Sum64(b []byte) uint64
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32
// Load fixed primes.
MOVQ ·primes+0(SB), prime1
MOVQ ·primes+8(SB), prime2
MOVQ ·primes+24(SB), prime4
// Load slice.
MOVQ b_base+0(FP), p
MOVQ b_len+8(FP), n
LEAQ (p)(n*1), end
// The first loop limit will be len(b)-32.
SUBQ $32, end
// Check whether we have at least one block.
CMPQ n, $32
JLT noBlocks
// Set up initial state (v1, v2, v3, v4).
MOVQ prime1, v1
ADDQ prime2, v1
MOVQ prime2, v2
XORQ v3, v3
XORQ v4, v4
SUBQ prime1, v4
blockLoop()
MOVQ v1, h
ROLQ $1, h
MOVQ v2, x
ROLQ $7, x
ADDQ x, h
MOVQ v3, x
ROLQ $12, x
ADDQ x, h
MOVQ v4, x
ROLQ $18, x
ADDQ x, h
mergeRound(h, v1)
mergeRound(h, v2)
mergeRound(h, v3)
mergeRound(h, v4)
JMP afterBlocks
noBlocks:
MOVQ ·primes+32(SB), h
afterBlocks:
ADDQ n, h
ADDQ $24, end
CMPQ p, end
JG try4
loop8:
MOVQ (p), x
ADDQ $8, p
round0(x)
XORQ x, h
ROLQ $27, h
IMULQ prime1, h
ADDQ prime4, h
CMPQ p, end
JLE loop8
try4:
ADDQ $4, end
CMPQ p, end
JG try1
MOVL (p), x
ADDQ $4, p
IMULQ prime1, x
XORQ x, h
ROLQ $23, h
IMULQ prime2, h
ADDQ ·primes+16(SB), h
try1:
ADDQ $4, end
CMPQ p, end
JGE finalize
loop1:
MOVBQZX (p), x
ADDQ $1, p
IMULQ ·primes+32(SB), x
XORQ x, h
ROLQ $11, h
IMULQ prime1, h
CMPQ p, end
JL loop1
finalize:
MOVQ h, x
SHRQ $33, x
XORQ x, h
IMULQ prime2, h
MOVQ h, x
SHRQ $29, x
XORQ x, h
IMULQ ·primes+16(SB), h
MOVQ h, x
SHRQ $32, x
XORQ x, h
MOVQ h, ret+24(FP)
RET
// func writeBlocks(d *Digest, b []byte) int
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
// Load fixed primes needed for round.
MOVQ ·primes+0(SB), prime1
MOVQ ·primes+8(SB), prime2
// Load slice.
MOVQ b_base+8(FP), p
MOVQ b_len+16(FP), n
LEAQ (p)(n*1), end
SUBQ $32, end
// Load vN from d.
MOVQ s+0(FP), d
MOVQ 0(d), v1
MOVQ 8(d), v2
MOVQ 16(d), v3
MOVQ 24(d), v4
// We don't need to check the loop condition here; this function is
// always called with at least one block of data to process.
blockLoop()
// Copy vN back to d.
MOVQ v1, 0(d)
MOVQ v2, 8(d)
MOVQ v3, 16(d)
MOVQ v4, 24(d)
// The number of bytes written is p minus the old base pointer.
SUBQ b_base+8(FP), p
MOVQ p, ret+32(FP)
RET

183
vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s generated vendored Normal file
View File

@@ -0,0 +1,183 @@
//go:build !appengine && gc && !purego
// +build !appengine
// +build gc
// +build !purego
#include "textflag.h"
// Registers:
#define digest R1
#define h R2 // return value
#define p R3 // input pointer
#define n R4 // input length
#define nblocks R5 // n / 32
#define prime1 R7
#define prime2 R8
#define prime3 R9
#define prime4 R10
#define prime5 R11
#define v1 R12
#define v2 R13
#define v3 R14
#define v4 R15
#define x1 R20
#define x2 R21
#define x3 R22
#define x4 R23
#define round(acc, x) \
MADD prime2, acc, x, acc \
ROR $64-31, acc \
MUL prime1, acc
// round0 performs the operation x = round(0, x).
#define round0(x) \
MUL prime2, x \
ROR $64-31, x \
MUL prime1, x
#define mergeRound(acc, x) \
round0(x) \
EOR x, acc \
MADD acc, prime4, prime1, acc
// blockLoop processes as many 32-byte blocks as possible,
// updating v1, v2, v3, and v4. It assumes that n >= 32.
#define blockLoop() \
LSR $5, n, nblocks \
PCALIGN $16 \
loop: \
LDP.P 16(p), (x1, x2) \
LDP.P 16(p), (x3, x4) \
round(v1, x1) \
round(v2, x2) \
round(v3, x3) \
round(v4, x4) \
SUB $1, nblocks \
CBNZ nblocks, loop
// func Sum64(b []byte) uint64
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32
LDP b_base+0(FP), (p, n)
LDP ·primes+0(SB), (prime1, prime2)
LDP ·primes+16(SB), (prime3, prime4)
MOVD ·primes+32(SB), prime5
CMP $32, n
CSEL LT, prime5, ZR, h // if n < 32 { h = prime5 } else { h = 0 }
BLT afterLoop
ADD prime1, prime2, v1
MOVD prime2, v2
MOVD $0, v3
NEG prime1, v4
blockLoop()
ROR $64-1, v1, x1
ROR $64-7, v2, x2
ADD x1, x2
ROR $64-12, v3, x3
ROR $64-18, v4, x4
ADD x3, x4
ADD x2, x4, h
mergeRound(h, v1)
mergeRound(h, v2)
mergeRound(h, v3)
mergeRound(h, v4)
afterLoop:
ADD n, h
TBZ $4, n, try8
LDP.P 16(p), (x1, x2)
round0(x1)
// NOTE: here and below, sequencing the EOR after the ROR (using a
// rotated register) is worth a small but measurable speedup for small
// inputs.
ROR $64-27, h
EOR x1 @> 64-27, h, h
MADD h, prime4, prime1, h
round0(x2)
ROR $64-27, h
EOR x2 @> 64-27, h, h
MADD h, prime4, prime1, h
try8:
TBZ $3, n, try4
MOVD.P 8(p), x1
round0(x1)
ROR $64-27, h
EOR x1 @> 64-27, h, h
MADD h, prime4, prime1, h
try4:
TBZ $2, n, try2
MOVWU.P 4(p), x2
MUL prime1, x2
ROR $64-23, h
EOR x2 @> 64-23, h, h
MADD h, prime3, prime2, h
try2:
TBZ $1, n, try1
MOVHU.P 2(p), x3
AND $255, x3, x1
LSR $8, x3, x2
MUL prime5, x1
ROR $64-11, h
EOR x1 @> 64-11, h, h
MUL prime1, h
MUL prime5, x2
ROR $64-11, h
EOR x2 @> 64-11, h, h
MUL prime1, h
try1:
TBZ $0, n, finalize
MOVBU (p), x4
MUL prime5, x4
ROR $64-11, h
EOR x4 @> 64-11, h, h
MUL prime1, h
finalize:
EOR h >> 33, h
MUL prime2, h
EOR h >> 29, h
MUL prime3, h
EOR h >> 32, h
MOVD h, ret+24(FP)
RET
// func writeBlocks(d *Digest, b []byte) int
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
LDP ·primes+0(SB), (prime1, prime2)
// Load state. Assume v[1-4] are stored contiguously.
MOVD d+0(FP), digest
LDP 0(digest), (v1, v2)
LDP 16(digest), (v3, v4)
LDP b_base+8(FP), (p, n)
blockLoop()
// Store updated state.
STP (v1, v2), 0(digest)
STP (v3, v4), 16(digest)
BIC $31, n
MOVD n, ret+32(FP)
RET

15
vendor/github.com/cespare/xxhash/v2/xxhash_asm.go generated vendored Normal file
View File

@@ -0,0 +1,15 @@
//go:build (amd64 || arm64) && !appengine && gc && !purego
// +build amd64 arm64
// +build !appengine
// +build gc
// +build !purego
package xxhash
// Sum64 computes the 64-bit xxHash digest of b with a zero seed.
//
//go:noescape
func Sum64(b []byte) uint64
//go:noescape
func writeBlocks(d *Digest, b []byte) int

76
vendor/github.com/cespare/xxhash/v2/xxhash_other.go generated vendored Normal file
View File

@@ -0,0 +1,76 @@
//go:build (!amd64 && !arm64) || appengine || !gc || purego
// +build !amd64,!arm64 appengine !gc purego
package xxhash
// Sum64 computes the 64-bit xxHash digest of b with a zero seed.
func Sum64(b []byte) uint64 {
// A simpler version would be
// d := New()
// d.Write(b)
// return d.Sum64()
// but this is faster, particularly for small inputs.
n := len(b)
var h uint64
if n >= 32 {
v1 := primes[0] + prime2
v2 := prime2
v3 := uint64(0)
v4 := -primes[0]
for len(b) >= 32 {
v1 = round(v1, u64(b[0:8:len(b)]))
v2 = round(v2, u64(b[8:16:len(b)]))
v3 = round(v3, u64(b[16:24:len(b)]))
v4 = round(v4, u64(b[24:32:len(b)]))
b = b[32:len(b):len(b)]
}
h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4)
h = mergeRound(h, v1)
h = mergeRound(h, v2)
h = mergeRound(h, v3)
h = mergeRound(h, v4)
} else {
h = prime5
}
h += uint64(n)
for ; len(b) >= 8; b = b[8:] {
k1 := round(0, u64(b[:8]))
h ^= k1
h = rol27(h)*prime1 + prime4
}
if len(b) >= 4 {
h ^= uint64(u32(b[:4])) * prime1
h = rol23(h)*prime2 + prime3
b = b[4:]
}
for ; len(b) > 0; b = b[1:] {
h ^= uint64(b[0]) * prime5
h = rol11(h) * prime1
}
h ^= h >> 33
h *= prime2
h ^= h >> 29
h *= prime3
h ^= h >> 32
return h
}
func writeBlocks(d *Digest, b []byte) int {
v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4
n := len(b)
for len(b) >= 32 {
v1 = round(v1, u64(b[0:8:len(b)]))
v2 = round(v2, u64(b[8:16:len(b)]))
v3 = round(v3, u64(b[16:24:len(b)]))
v4 = round(v4, u64(b[24:32:len(b)]))
b = b[32:len(b):len(b)]
}
d.v1, d.v2, d.v3, d.v4 = v1, v2, v3, v4
return n - len(b)
}

16
vendor/github.com/cespare/xxhash/v2/xxhash_safe.go generated vendored Normal file
View File

@@ -0,0 +1,16 @@
//go:build appengine
// +build appengine
// This file contains the safe implementations of otherwise unsafe-using code.
package xxhash
// Sum64String computes the 64-bit xxHash digest of s with a zero seed.
func Sum64String(s string) uint64 {
return Sum64([]byte(s))
}
// WriteString adds more data to d. It always returns len(s), nil.
func (d *Digest) WriteString(s string) (n int, err error) {
return d.Write([]byte(s))
}

58
vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go generated vendored Normal file
View File

@@ -0,0 +1,58 @@
//go:build !appengine
// +build !appengine
// This file encapsulates usage of unsafe.
// xxhash_safe.go contains the safe implementations.
package xxhash
import (
"unsafe"
)
// In the future it's possible that compiler optimizations will make these
// XxxString functions unnecessary by realizing that calls such as
// Sum64([]byte(s)) don't need to copy s. See https://go.dev/issue/2205.
// If that happens, even if we keep these functions they can be replaced with
// the trivial safe code.
// NOTE: The usual way of doing an unsafe string-to-[]byte conversion is:
//
// var b []byte
// bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
// bh.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
// bh.Len = len(s)
// bh.Cap = len(s)
//
// Unfortunately, as of Go 1.15.3 the inliner's cost model assigns a high enough
// weight to this sequence of expressions that any function that uses it will
// not be inlined. Instead, the functions below use a different unsafe
// conversion designed to minimize the inliner weight and allow both to be
// inlined. There is also a test (TestInlining) which verifies that these are
// inlined.
//
// See https://github.com/golang/go/issues/42739 for discussion.
// Sum64String computes the 64-bit xxHash digest of s with a zero seed.
// It may be faster than Sum64([]byte(s)) by avoiding a copy.
func Sum64String(s string) uint64 {
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))
return Sum64(b)
}
// WriteString adds more data to d. It always returns len(s), nil.
// It may be faster than Write([]byte(s)) by avoiding a copy.
func (d *Digest) WriteString(s string) (n int, err error) {
d.Write(*(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)})))
// d.Write always returns len(s), nil.
// Ignoring the return output and returning these fixed values buys a
// savings of 6 in the inliner's cost model.
return len(s), nil
}
// sliceHeader is similar to reflect.SliceHeader, but it assumes that the layout
// of the first two words is the same as the layout of a string.
type sliceHeader struct {
s string
cap int
}

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017 Segment.io
Copyright (c) 2020 Matt Sherman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -0,0 +1,120 @@
An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode 17.
[![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! 👍🐶"
g := graphemes.FromString(text)
for g.Next() { // Next() returns true until end of data
fmt.Println(g.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-36.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! 👍🐶"
g := graphemes.FromString(text)
for g.Next() { // Next() returns true until end of data
fmt.Println(g.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
g := graphemes.FromReader(r)
for g.Scan() { // Scan() returns true until error or EOF
fmt.Println(g.Text()) // Do something with the current grapheme
}
if g.Err() != nil { // Check the error
log.Fatal(g.Err())
}
```
### If you have a `[]byte`
```go
b := []byte("Hello, 世界. Nice dog! 👍🐶")
g := graphemes.FromBytes(b)
for g.Next() { // Next() returns true until end of data
fmt.Println(g.Value()) // Do something with the current grapheme
}
```
### ANSI escape sequences
By the UAX 29 specification, ANSI escape sequences are not grapheme clusters. To treat 7-bit ANSI escape sequences as a single cluster, set `AnsiEscapeSequences` to true.
```go
text := "Hello, \x1b[31mworld\x1b[0m!"
g := graphemes.FromString(text)
g.AnsiEscapeSequences = true
for g.Next() {
fmt.Println(g.Value())
}
```
To also parse 8-bit C1 controls (non-UTF-8 bytes), set `AnsiEscapeSequences8Bit` to true.
```go
g.AnsiEscapeSequences = true // 7-bit forms (ESC ...)
g.AnsiEscapeSequences8Bit = true // 8-bit C1 forms (0x80-0x9F), not valid UTF-8
```
For ESC-initiated (7-bit) control strings, only 7-bit terminators are recognized.
For C1-initiated (8-bit) control strings, only C1 ST (`0x9C`) is recognized as ST.
We implement [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/) control codes in both 7-bit and 8-bit representations. 8-bit control codes are not UTF-8 encoded and are not valid UTF-8, caveat emptor.
### Benchmarks
```
goos: darwin
goarch: arm64
pkg: github.com/clipperhouse/uax29/graphemes/comparative
cpu: Apple M2
BenchmarkGraphemesMixed/clipperhouse/uax29-8 142635 ns/op 245.12 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemesMixed/rivo/uniseg-8 2018284 ns/op 17.32 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemesASCII/clipperhouse/uax29-8 8846 ns/op 508.73 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemesASCII/rivo/uniseg-8 366760 ns/op 12.27 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

@@ -0,0 +1,138 @@
package graphemes
// ansiEscapeLength returns the byte length of a valid 7-bit ANSI escape
// sequence at the start of data, or 0 if none.
//
// Recognized forms (ECMA-48 / ISO 6429):
// - CSI: ESC [ then parameter bytes (0x30-0x3F), intermediate (0x20-0x2F), final (0x40-0x7E)
// - OSC: ESC ] then payload until BEL (0x07), 7-bit ST (ESC \), CAN (0x18), or SUB (0x1A)
// - DCS, SOS, PM, APC: ESC P/X/^/_ then payload until 7-bit ST (ESC \), CAN, or SUB
// - Two-byte: ESC + Fe/Fs (0x40-0x7E excluding above), or Fp (0x30-0x3F), or nF (0x20-0x2F then final)
func ansiEscapeLength[T ~string | ~[]byte](data T) int {
n := len(data)
if n < 2 || data[0] != esc {
return 0
}
b1 := data[1]
switch b1 {
case '[': // CSI
body := csiBodyLength(data[2:])
if body == 0 {
return 0
}
return 2 + body
case ']': // OSC - allows BEL or 7-bit ST terminator
body := oscLength(data[2:])
if body < 0 {
return 0
}
return 2 + body
case 'P', 'X', '^', '_': // DCS, SOS, PM, APC
body := stSequenceLength(data[2:])
if body < 0 {
return 0
}
return 2 + body
}
if b1 >= 0x40 && b1 <= 0x7E {
// Fe/Fs two-byte; [ ] P X ^ _ handled above
return 2
}
if b1 >= 0x30 && b1 <= 0x3F {
// Fp (private) two-byte
return 2
}
if b1 >= 0x20 && b1 <= 0x2F {
// nF: intermediates then one final (0x30-0x7E)
i := 2
for i < n && data[i] >= 0x20 && data[i] <= 0x2F {
i++
}
if i < n && data[i] >= 0x30 && data[i] <= 0x7E {
return i + 1
}
return 0
}
return 0
}
// csiBodyLength returns the length of the CSI body (param/intermediate/final bytes).
// data is the slice after "ESC [".
// Per ECMA-48, the CSI body has the form:
//
// parameters (0x300x3F)*, intermediates (0x200x2F)*, final (0x400x7E)
//
// Once an intermediate byte is seen, subsequent parameter bytes are invalid.
func csiBodyLength[T ~string | ~[]byte](data T) int {
seenIntermediate := false
for i := 0; i < len(data); i++ {
b := data[i]
if b >= 0x30 && b <= 0x3F {
if seenIntermediate {
return 0
}
continue
}
if b >= 0x20 && b <= 0x2F {
seenIntermediate = true
continue
}
if b >= 0x40 && b <= 0x7E {
return i + 1
}
return 0
}
return 0
}
// oscLength returns the length of the OSC body.
// data is the slice after "ESC ]".
//
// Returns:
// - n >= 0: consumed body length (includes BEL/ST terminator when present)
// - -1: not terminated in the provided data
//
// OSC accepts BEL (0x07) or 7-bit ST (ESC \) as terminators by widespread convention.
// Per ECMA-48, CAN (0x18) and SUB (0x1A) cancel the control string; in that
// case they are not part of the OSC sequence length.
func oscLength[T ~string | ~[]byte](data T) int {
for i := 0; i < len(data); i++ {
b := data[i]
if b == bel {
return i + 1
}
if b == can || b == sub {
return i
}
if b == esc && i+1 < len(data) && data[i+1] == '\\' {
return i + 2
}
}
return -1
}
// stSequenceLength returns the length of a control-string body.
// data is the slice after "ESC x".
//
// Returns:
// - n >= 0: consumed body length (includes ST terminator when present)
// - -1: not terminated in the provided data
//
// Used for DCS, SOS, PM, and APC, which per ECMA-48 terminate with ST.
// ST here is the 7-bit form (ESC \).
// CAN (0x18) and SUB (0x1A) cancel the control string; in that case they are
// not part of the sequence length.
func stSequenceLength[T ~string | ~[]byte](data T) int {
for i := 0; i < len(data); i++ {
if data[i] == can || data[i] == sub {
return i
}
if data[i] == esc && i+1 < len(data) && data[i+1] == '\\' {
return i + 2
}
}
return -1
}

View File

@@ -0,0 +1,79 @@
package graphemes
// ansiEscapeLength8Bit returns the byte length of a valid 8-bit C1 ANSI
// sequence at the start of data, or 0 if none.
//
// Recognized forms (ECMA-48 / ISO 6429):
// - C1 CSI (0x9B) body as parameter/intermediate/final bytes
// - C1 OSC (0x9D) body terminated by BEL, C1 ST, CAN, or SUB
// - C1 DCS/SOS/PM/APC (0x90/0x98/0x9E/0x9F) body terminated by C1 ST, CAN, or SUB
// - Standalone C1 controls (0x80..0x9F not listed above): single byte
func ansiEscapeLength8Bit[T ~string | ~[]byte](data T) int {
if len(data) == 0 {
return 0
}
switch data[0] {
case 0x9B: // C1 CSI
body := csiBodyLength(data[1:])
if body == 0 {
return 0
}
return 1 + body
case 0x9D: // C1 OSC
body := oscLengthC1(data[1:])
if body < 0 {
return 0
}
return 1 + body
case 0x90, 0x98, 0x9E, 0x9F: // C1 DCS, SOS, PM, APC
body := stSequenceLengthC1(data[1:])
if body < 0 {
return 0
}
return 1 + body
default:
if data[0] >= 0x80 && data[0] <= 0x9F {
return 1
}
}
return 0
}
// oscLengthC1 returns the length of a C1 OSC body.
// data is the slice after the C1 OSC initiator (0x9D).
//
// Returns:
// - n >= 0: consumed body length (includes BEL/ST terminator when present)
// - -1: not terminated in the provided data
//
// Terminators: BEL (0x07) or C1 ST (0x9C).
// CAN (0x18) and SUB (0x1A) cancel the control string.
func oscLengthC1[T ~string | ~[]byte](data T) int {
for i := 0; i < len(data); i++ {
b := data[i]
if b == bel || b == st {
return i + 1
}
if b == can || b == sub {
return i
}
}
return -1
}
// stSequenceLengthC1 parses DCS/SOS/PM/APC bodies that terminate with C1 ST
// (0x9C), or are canceled by CAN/SUB.
func stSequenceLengthC1[T ~string | ~[]byte](data T) int {
for i := 0; i < len(data); i++ {
b := data[i]
if b == can || b == sub {
return i
}
if b == st {
return i + 1
}
}
return -1
}

View File

@@ -0,0 +1,144 @@
package graphemes
import "unicode/utf8"
// 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]{
split: splitFuncString,
data: 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]{
split: splitFuncBytes,
data: b,
}
}
// Iterator is a generic iterator for grapheme clusters in strings or byte slices,
// with an ASCII hot path optimization.
type Iterator[T ~string | ~[]byte] struct {
split func(T, bool) (int, T, error)
data T
pos int
start int
// AnsiEscapeSequences treats 7-bit ANSI escape sequences (ECMA-48) as
// single grapheme clusters when true. The default is false.
//
// 8-bit controls are not enabled by this option. See [AnsiEscapeSequences8Bit].
AnsiEscapeSequences bool
// AnsiEscapeSequences8Bit treats 8-bit C1 ANSI escape sequences (ECMA-48) as single
// grapheme clusters when true. The default is false.
//
// 8-bit control bytes are not UTF-8 encoded, i.e. not valid UTF-8. If you
// choose this option, you are choosing to interpret non-UTF-8 data, caveat
// emptor.
AnsiEscapeSequences8Bit bool
}
var (
splitFuncString = splitFunc[string]
splitFuncBytes = splitFunc[[]byte]
)
const (
esc = 0x1B
cr = 0x0D
bel = 0x07
can = 0x18
sub = 0x1A
st = 0x9C
)
// Next advances the iterator to the next grapheme cluster.
// Returns false when there are no more grapheme clusters.
func (iter *Iterator[T]) Next() bool {
if iter.pos >= len(iter.data) {
return false
}
iter.start = iter.pos
b := iter.data[iter.pos]
if iter.AnsiEscapeSequences && b == esc {
if a := ansiEscapeLength(iter.data[iter.pos:]); a > 0 {
iter.pos += a
return true
}
}
if iter.AnsiEscapeSequences8Bit && b >= 0x80 && b <= 0x9F {
if a := ansiEscapeLength8Bit(iter.data[iter.pos:]); a > 0 {
iter.pos += a
return true
}
}
// ASCII hot path: any ASCII is one grapheme when next byte is ASCII or end.
if b < utf8.RuneSelf && b != cr {
if iter.pos+1 >= len(iter.data) || iter.data[iter.pos+1] < utf8.RuneSelf {
iter.pos++
return true
}
}
// Fall back to UAX29 grapheme parsing
remaining := iter.data[iter.pos:]
advance, _, err := iter.split(remaining, 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 end of data")
}
return true
}
// Value returns the current grapheme cluster.
func (iter *Iterator[T]) Value() T {
return iter.data[iter.start:iter.pos]
}
// Start returns the byte position of the current grapheme in the original data.
func (iter *Iterator[T]) Start() int {
return iter.start
}
// End returns the byte position after the current grapheme 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
}
// SetText sets the data 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
}
// First returns the first grapheme cluster without advancing the iterator.
func (iter *Iterator[T]) First() T {
if len(iter.data) == 0 {
return iter.data
}
// Use a copy to leverage Next()'s ASCII optimization
cp := *iter
cp.pos = 0
cp.start = 0
cp.Next()
return cp.Value()
}

View File

@@ -0,0 +1,25 @@
// 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

@@ -0,0 +1,205 @@
package graphemes
import (
"bufio"
)
// is determines if lookup intersects propert(ies)
func (lookup property) is(properties property) bool {
return (lookup & properties) != 0
}
const _Ignore = _Extend
// incbState tracks state for GB9c rule (Indic conjunct clusters)
// Pattern: Consonant (Extend|Linker)* Linker (Extend|Linker)* × Consonant
type incbState int
const (
incbNone incbState = iota // initial/reset
incbConsonant // seen Consonant, awaiting Linker
incbLinker // seen Consonant and Linker (conjunct ready)
)
// 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 ~string | ~[]byte](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
// GB9c state: tracking Indic conjunct clusters
var incb incbState
// 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
}
// Update GB9c state based on what we just advanced past
if last.is(_InCBConsonant | _InCBLinker | _InCBExtend) {
switch {
case last.is(_InCBConsonant):
if incb != incbLinker {
incb = incbConsonant
}
case last.is(_InCBLinker):
if incb >= incbConsonant {
incb = incbLinker
}
// case last.is(_InCBExtend): stay in current state
}
} else {
incb = incbNone
}
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
// Do not break within certain combinations with Indic_Conjunct_Break (InCB)=Linker.
if incb == incbLinker && current.is(_InCBConsonant) {
// After matching the pattern, reset state to start tracking a new pattern
// The current Consonant becomes the start of the new pattern
incb = incbConsonant
pos += w
continue
}
// 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
}

1717
vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@ import (
"runtime"
"strings"
"sync"
"sync/atomic"
"github.com/containerd/stargz-snapshotter/estargz/errorutil"
"github.com/klauspost/compress/zstd"
@@ -42,6 +43,8 @@ import (
"golang.org/x/sync/errgroup"
)
type GzipHelperFunc func(io.Reader) (io.ReadCloser, error)
type options struct {
chunkSize int
compressionLevel int
@@ -50,6 +53,7 @@ type options struct {
compression Compression
ctx context.Context
minChunkSize int
gzipHelperFunc GzipHelperFunc
}
type Option func(o *options) error
@@ -127,11 +131,25 @@ 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
diffID digest.Digester
tocDigest digest.Digest
readCompleted *atomic.Bool
uncompressedSize *atomic.Int64
}
// DiffID returns the digest of uncompressed blob.
@@ -145,6 +163,19 @@ 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
@@ -186,7 +217,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)
tarBlob, err := decompressBlob(tarBlob, layerFiles, opts.gzipHelperFunc)
if err != nil {
return nil, err
}
@@ -252,17 +283,28 @@ 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() {
r, err := opts.compression.Reader(io.TeeReader(io.MultiReader(append(rs, tocAndFooter)...), pw))
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))
if err != nil {
pw.CloseWithError(err)
return
}
defer r.Close()
if _, err := io.Copy(diffID.Hash(), r); err != nil {
defer decompressR.Close()
if size, err = io.Copy(diffID.Hash(), decompressR); err != nil {
pw.CloseWithError(err)
return
}
uncompressedSize.Store(size)
readCompleted.Store(true)
pw.Close()
}()
return &Blob{
@@ -270,8 +312,10 @@ func Build(tarBlob *io.SectionReader, opt ...Option) (_ *Blob, rErr error) {
Reader: pr,
closeFunc: layerFiles.CleanupAll,
},
tocDigest: tocDgst,
diffID: diffID,
tocDigest: tocDgst,
diffID: diffID,
readCompleted: readCompleted,
uncompressedSize: uncompressedSize,
}, nil
}
@@ -366,8 +410,9 @@ 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); err != nil {
if err := moveRec(l, intar, sorted, picked); err != nil {
if errors.Is(err, errNotFound) && missedPrioritized != nil {
*missedPrioritized = append(*missedPrioritized, l)
continue // allow not found
@@ -395,8 +440,8 @@ func sortEntries(in io.ReaderAt, prioritized []string, missedPrioritized *[]stri
})
}
// Dump all entry and concatinate them.
return append(sorted.dump(), intar.dump()...), nil
// Dump prioritized entries followed by the rest entries while skipping picked ones.
return append(sorted.dump(nil), intar.dump(picked)...), nil
}
// readerFromEntries returns a reader of tar archive that contains entries passed
@@ -458,36 +503,42 @@ func importTar(in io.ReaderAt) (*tarFile, error) {
return tf, nil
}
func moveRec(name string, in *tarFile, out *tarFile) error {
func moveRec(name string, in *tarFile, out *tarFile, picked map[string]struct{}) 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.
out.add(e)
in.remove(name)
if _, done := picked[name]; !done {
out.add(e)
picked[name] = struct{}{}
}
}
return nil
}
_, okIn := in.get(name)
_, okOut := out.get(name)
if !okIn && !okOut {
_, okPicked := picked[name]
if !okIn && !okOut && !okPicked {
return fmt.Errorf("file: %q: %w", name, errNotFound)
}
parent, _ := path.Split(strings.TrimSuffix(name, "/"))
if err := moveRec(parent, in, out); err != nil {
if err := moveRec(parent, in, out, picked); err != nil {
return err
}
if e, ok := in.get(name); ok && e.header.Typeflag == tar.TypeLink {
if err := moveRec(e.header.Linkname, in, out); err != nil {
if err := moveRec(e.header.Linkname, in, out, picked); err != nil {
return err
}
}
if _, done := picked[name]; done {
return nil
}
if e, ok := in.get(name); ok {
out.add(e)
in.remove(name)
picked[name] = struct{}{}
}
return nil
}
@@ -533,8 +584,18 @@ func (f *tarFile) get(name string) (e *entry, ok bool) {
return
}
func (f *tarFile) dump() []*entry {
return f.stream
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
}
type readCloser struct {
@@ -649,7 +710,7 @@ func (cr *countReadSeeker) currentPos() int64 {
return *cr.cPos
}
func decompressBlob(org *io.SectionReader, tmp *tempFiles) (*io.SectionReader, error) {
func decompressBlob(org *io.SectionReader, tmp *tempFiles, gzipHelperFunc GzipHelperFunc) (*io.SectionReader, error) {
if org.Size() < 4 {
return org, nil
}
@@ -660,7 +721,13 @@ func decompressBlob(org *io.SectionReader, tmp *tempFiles) (*io.SectionReader, e
var dR io.Reader
if bytes.Equal([]byte{0x1F, 0x8B, 0x08}, src[:3]) {
// gzip
dgR, err := gzip.NewReader(io.NewSectionReader(org, 0, org.Size()))
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()))
}
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 = uname[ent.GID]
ent.Gname = gname[ent.GID]
}
ent.modTime, _ = time.Parse(time.RFC3339, ent.ModTime3339)
@@ -307,6 +307,15 @@ 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,7 +38,6 @@ import (
"reflect"
"sort"
"strings"
"testing"
"time"
"github.com/containerd/stargz-snapshotter/estargz/errorutil"
@@ -49,16 +48,48 @@ import (
// TestingController is Compression with some helper methods necessary for testing.
type TestingController interface {
Compression
TestStreams(t *testing.T, b []byte, streams []int64)
DiffIDOf(*testing.T, []byte) string
TestStreams(t TestingT, b []byte, streams []int64)
DiffIDOf(TestingT, []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 *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...) })
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...) })
}
type TestingControllerFactory func() TestingController
@@ -79,7 +110,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 *testing.T, controllers ...TestingControllerFactory) {
func testBuild(t *TestRunner, controllers ...TestingControllerFactory) {
tests := []struct {
name string
chunkSize int
@@ -165,7 +196,7 @@ func testBuild(t *testing.T, 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 *testing.T) {
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,src=%d,format=%s,minChunkSize=%d", newCL(), prefix, srcCompression, srcTarFormat, minChunkSize), func(t *TestRunner) {
tarBlob := buildTar(t, tt.in, prefix, srcTarFormat)
// Test divideEntries()
entries, err := sortEntries(tarBlob, nil, nil) // identical order
@@ -265,7 +296,7 @@ func testBuild(t *testing.T, controllers ...TestingControllerFactory) {
}
}
func isSameTarGz(t *testing.T, cla TestingController, a []byte, clb TestingController, b []byte) bool {
func isSameTarGz(t TestingT, 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")
@@ -325,7 +356,7 @@ func isSameTarGz(t *testing.T, cla TestingController, a []byte, clb TestingContr
return true
}
func isSameVersion(t *testing.T, cla TestingController, a []byte, clb TestingController, b []byte) bool {
func isSameVersion(t TestingT, 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)
@@ -339,7 +370,7 @@ func isSameVersion(t *testing.T, cla TestingController, a []byte, clb TestingCon
return aJTOC.Version == bJTOC.Version
}
func isSameEntries(t *testing.T, a, b *Reader) bool {
func isSameEntries(t TestingT, a, b *Reader) bool {
aroot, ok := a.Lookup("")
if !ok {
t.Fatalf("failed to get root of A")
@@ -353,7 +384,7 @@ func isSameEntries(t *testing.T, a, b *Reader) bool {
return contains(t, aEntry, bEntry) && contains(t, bEntry, aEntry)
}
func compressBlob(t *testing.T, src *io.SectionReader, srcCompression int) *io.SectionReader {
func compressBlob(t TestingT, src *io.SectionReader, srcCompression int) *io.SectionReader {
buf := new(bytes.Buffer)
var w io.WriteCloser
var err error
@@ -387,7 +418,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 *testing.T, a, b stargzEntry) bool {
func contains(t TestingT, 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)
@@ -498,7 +529,7 @@ func equalEntry(a, b *TOCEntry) bool {
a.Digest == b.Digest
}
func readOffset(t *testing.T, r *io.SectionReader, offset int64, e stargzEntry) ([]byte, int64, bool) {
func readOffset(t TestingT, 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
@@ -517,7 +548,7 @@ func readOffset(t *testing.T, r *io.SectionReader, offset int64, e stargzEntry)
return data[:n], offset + ce.ChunkSize, true
}
func dumpTOCJSON(t *testing.T, tocJSON *JTOC) string {
func dumpTOCJSON(t TestingT, tocJSON *JTOC) string {
jtocData, err := json.Marshal(*tocJSON)
if err != nil {
t.Fatalf("failed to marshal TOC JSON: %v", err)
@@ -531,20 +562,19 @@ func dumpTOCJSON(t *testing.T, tocJSON *JTOC) string {
const chunkSize = 3
// 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)
type check func(t *TestRunner, 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 *testing.T, controllers ...TestingControllerFactory) {
func testDigestAndVerify(t *TestRunner, controllers ...TestingControllerFactory) {
tests := []struct {
name string
tarInit func(t *testing.T, dgstMap map[string]digest.Digest) (blob []tarEntry)
tarInit func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry)
checks []check
minChunkSize []int
}{
{
name: "no-regfile",
tarInit: func(t *testing.T, dgstMap map[string]digest.Digest) (blob []tarEntry) {
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
dir("test/"),
)
@@ -559,7 +589,7 @@ func testDigestAndVerify(t *testing.T, controllers ...TestingControllerFactory)
},
{
name: "small-files",
tarInit: func(t *testing.T, dgstMap map[string]digest.Digest) (blob []tarEntry) {
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
regDigest(t, "baz.txt", "", dgstMap),
regDigest(t, "foo.txt", "a", dgstMap),
@@ -583,7 +613,7 @@ func testDigestAndVerify(t *testing.T, controllers ...TestingControllerFactory)
},
{
name: "big-files",
tarInit: func(t *testing.T, dgstMap map[string]digest.Digest) (blob []tarEntry) {
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
regDigest(t, "baz.txt", "bazbazbazbazbazbazbaz", dgstMap),
regDigest(t, "foo.txt", "a", dgstMap),
@@ -607,7 +637,7 @@ func testDigestAndVerify(t *testing.T, controllers ...TestingControllerFactory)
{
name: "with-non-regfiles",
minChunkSize: []int{0, 64000},
tarInit: func(t *testing.T, dgstMap map[string]digest.Digest) (blob []tarEntry) {
tarInit: func(t TestingT, dgstMap map[string]digest.Digest) (blob []tarEntry) {
return tarOf(
regDigest(t, "baz.txt", "bazbazbazbazbazbazbaz", dgstMap),
regDigest(t, "foo.txt", "a", dgstMap),
@@ -654,7 +684,7 @@ func testDigestAndVerify(t *testing.T, 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 *testing.T) {
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,format=%s,minChunkSize=%d", newCL(), prefix, srcTarFormat, minChunkSize), func(t *TestRunner) {
// Get original tar file and chunk digests
dgstMap := make(map[string]digest.Digest)
tarBlob := buildTar(t, tt.tarInit(t, dgstMap), prefix, srcTarFormat)
@@ -690,7 +720,7 @@ func testDigestAndVerify(t *testing.T, 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 *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
func checkStargzTOC(t *TestRunner, 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),
@@ -801,7 +831,7 @@ func checkStargzTOC(t *testing.T, sgzData []byte, tocDigest digest.Digest, dgstM
// 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 *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
func checkVerifyTOC(t *TestRunner, 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),
@@ -882,9 +912,9 @@ func checkVerifyTOC(t *testing.T, sgzData []byte, tocDigest digest.Digest, dgstM
// 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 *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
return func(t *TestRunner, 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 *testing.T, toc *JTOC, sgz *io.SectionReader) {
"lost digest in a entry": func(t TestingT, toc *JTOC, sgz *io.SectionReader) {
var found bool
for _, e := range toc.Entries {
if cleanEntryName(e.Name) == filename {
@@ -902,7 +932,7 @@ func checkVerifyInvalidTOCEntryFail(filename string) check {
t.Fatalf("rewrite target not found")
}
},
"duplicated entry offset": func(t *testing.T, toc *JTOC, sgz *io.SectionReader) {
"duplicated entry offset": func(t TestingT, toc *JTOC, sgz *io.SectionReader) {
var (
sampleEntry *TOCEntry
targetEntry *TOCEntry
@@ -929,7 +959,7 @@ func checkVerifyInvalidTOCEntryFail(filename string) check {
}
for name, rFunc := range funcs {
t.Run(name, func(t *testing.T) {
t.Run(name, func(t *TestRunner) {
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 {
@@ -958,7 +988,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 *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
return func(t *TestRunner, 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 {
@@ -990,7 +1020,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 *testing.T, sgzData []byte, tocDigest digest.Digest, dgstMap map[string]digest.Digest, controller TestingController, newController TestingControllerFactory) {
return func(t *TestRunner, 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))),
@@ -1047,9 +1077,9 @@ func chunkID(name string, offset, size int64) string {
return fmt.Sprintf("%s-%d-%d", cleanEntryName(name), offset, size)
}
type rewriteFunc func(t *testing.T, toc *JTOC, sgz *io.SectionReader)
type rewriteFunc func(t TestingT, toc *JTOC, sgz *io.SectionReader)
func rewriteTOCJSON(t *testing.T, sgz *io.SectionReader, rewrite rewriteFunc, controller TestingController) (newSgz io.Reader, tocDigest digest.Digest) {
func rewriteTOCJSON(t TestingT, 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)
@@ -1120,7 +1150,7 @@ func parseStargz(sgz *io.SectionReader, controller TestingController) (decodedJT
return decodedJTOC, tocOffset, nil
}
func testWriteAndOpen(t *testing.T, controllers ...TestingControllerFactory) {
func testWriteAndOpen(t *TestRunner, controllers ...TestingControllerFactory) {
const content = "Some contents"
invalidUtf8 := "\xff\xfe\xfd"
@@ -1464,7 +1494,7 @@ func testWriteAndOpen(t *testing.T, 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 *testing.T) {
t.Run(tt.name+"-"+fmt.Sprintf("compression=%v,prefix=%q,lossless=%v,format=%s", newCL(), prefix, lossless, srcTarFormat), func(t *TestRunner) {
var tr io.Reader = buildTar(t, tt.in, prefix, srcTarFormat)
origTarDgstr := digest.Canonical.Digester()
tr = io.TeeReader(tr, origTarDgstr.Hash())
@@ -1530,6 +1560,9 @@ func testWriteAndOpen(t *testing.T, 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
@@ -1628,7 +1661,7 @@ func digestFor(content string) string {
type numTOCEntries int
func (n numTOCEntries) check(t *testing.T, r *Reader) {
func (n numTOCEntries) check(t TestingT, r *Reader) {
if r.toc == nil {
t.Fatal("nil TOC")
}
@@ -1648,15 +1681,15 @@ func (n numTOCEntries) check(t *testing.T, r *Reader) {
func checks(s ...stargzCheck) []stargzCheck { return s }
type stargzCheck interface {
check(t *testing.T, r *Reader)
check(t TestingT, r *Reader)
}
type stargzCheckFn func(*testing.T, *Reader)
type stargzCheckFn func(TestingT, *Reader)
func (f stargzCheckFn) check(t *testing.T, r *Reader) { f(t, r) }
func (f stargzCheckFn) check(t TestingT, r *Reader) { f(t, r) }
func maxDepth(max int) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
e, ok := r.Lookup("")
if !ok {
t.Fatal("root directory not found")
@@ -1673,7 +1706,7 @@ func maxDepth(max int) stargzCheck {
})
}
func getMaxDepth(t *testing.T, e *TOCEntry, current, limit int) (max int, rErr error) {
func getMaxDepth(t TestingT, 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)
@@ -1695,7 +1728,7 @@ func getMaxDepth(t *testing.T, e *TOCEntry, current, limit int) (max int, rErr e
}
func hasFileLen(file string, wantLen int) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == file {
if ent.Type != "reg" {
@@ -1711,7 +1744,7 @@ func hasFileLen(file string, wantLen int) stargzCheck {
}
func hasFileXattrs(file, name, value string) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == file {
if ent.Type != "reg" {
@@ -1738,7 +1771,7 @@ func hasFileXattrs(file, name, value string) stargzCheck {
}
func hasFileDigest(file string, digest string) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
ent, ok := r.Lookup(file)
if !ok {
t.Fatalf("didn't find TOCEntry for file %q", file)
@@ -1750,7 +1783,7 @@ func hasFileDigest(file string, digest string) stargzCheck {
}
func hasFileContentsWithPreRead(file string, offset int, want string, extra ...chunkInfo) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
extraMap := make(map[string]chunkInfo)
for _, e := range extra {
extraMap[e.name] = e
@@ -1797,7 +1830,7 @@ func hasFileContentsWithPreRead(file string, offset int, want string, extra ...c
}
func hasFileContentsRange(file string, offset int, want string) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
f, err := r.OpenFile(file)
if err != nil {
t.Fatal(err)
@@ -1814,7 +1847,7 @@ func hasFileContentsRange(file string, offset int, want string) stargzCheck {
}
func hasChunkEntries(file string, wantChunks int) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
ent, ok := r.Lookup(file)
if !ok {
t.Fatalf("no file for %q", file)
@@ -1858,7 +1891,7 @@ func hasChunkEntries(file string, wantChunks int) stargzCheck {
}
func entryHasChildren(dir string, want ...string) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
want := append([]string(nil), want...)
var got []string
ent, ok := r.Lookup(dir)
@@ -1877,7 +1910,7 @@ func entryHasChildren(dir string, want ...string) stargzCheck {
}
func hasDir(file string) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == cleanEntryName(file) {
if ent.Type != "dir" {
@@ -1891,7 +1924,7 @@ func hasDir(file string) stargzCheck {
}
func hasDirLinkCount(file string, count int) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == cleanEntryName(file) {
if ent.Type != "dir" {
@@ -1909,7 +1942,7 @@ func hasDirLinkCount(file string, count int) stargzCheck {
}
func hasMode(file string, mode os.FileMode) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == cleanEntryName(file) {
if ent.Stat().Mode() != mode {
@@ -1924,7 +1957,7 @@ func hasMode(file string, mode os.FileMode) stargzCheck {
}
func hasSymlink(file, target string) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
for _, ent := range r.toc.Entries {
if ent.Name == file {
if ent.Type != "symlink" {
@@ -1940,7 +1973,7 @@ func hasSymlink(file, target string) stargzCheck {
}
func lookupMatch(name string, want *TOCEntry) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
e, ok := r.Lookup(name)
if !ok {
t.Fatalf("failed to Lookup entry %q", name)
@@ -1953,7 +1986,7 @@ func lookupMatch(name string, want *TOCEntry) stargzCheck {
}
func hasEntryOwner(entry string, owner owner) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
ent, ok := r.Lookup(strings.TrimSuffix(entry, "/"))
if !ok {
t.Errorf("entry %q not found", entry)
@@ -1967,7 +2000,7 @@ func hasEntryOwner(entry string, owner owner) stargzCheck {
}
func mustSameEntry(files ...string) stargzCheck {
return stargzCheckFn(func(t *testing.T, r *Reader) {
return stargzCheckFn(func(t TestingT, r *Reader) {
var first *TOCEntry
for _, f := range files {
if first == nil {
@@ -2039,7 +2072,7 @@ func (f tarEntryFunc) appendTar(tw *tar.Writer, prefix string, format tar.Format
return f(tw, prefix, format)
}
func buildTar(t *testing.T, ents []tarEntry, prefix string, opts ...interface{}) *io.SectionReader {
func buildTar(t TestingT, ents []tarEntry, prefix string, opts ...interface{}) *io.SectionReader {
format := tar.FormatUnknown
for _, opt := range opts {
switch v := opt.(type) {
@@ -2248,7 +2281,7 @@ func noPrefetchLandmark() tarEntry {
})
}
func regDigest(t *testing.T, name string, contentStr string, digestMap map[string]digest.Digest) tarEntry {
func regDigest(t TestingT, name string, contentStr string, digestMap map[string]digest.Digest) tarEntry {
if digestMap == nil {
t.Fatalf("digest map mustn't be nil")
}
@@ -2318,7 +2351,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 *testing.T, b []byte, streams []int64) {
func CheckGzipHasStreams(t TestingT, b []byte, streams []int64) {
if len(streams) == 0 {
return // nop
}
@@ -2356,7 +2389,7 @@ func CheckGzipHasStreams(t *testing.T, b []byte, streams []int64) {
}
}
func GzipDiffIDOf(t *testing.T, b []byte) string {
func GzipDiffIDOf(t TestingT, b []byte) string {
h := sha256.New()
zr, err := gzip.NewReader(bytes.NewReader(b))
if err != nil {

View File

@@ -1,35 +1,47 @@
version: "2"
linters:
enable:
- depguard
- staticcheck
- misspell
- revive
- unconvert
settings:
depguard:
rules:
main:
files:
- $all
deny:
- pkg: io/ioutil
revive:
severity: error
rules:
- name: indent-error-flow
severity: warning
disabled: false
- name: error-strings
disabled: false
staticcheck:
checks:
- -SA1019
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
- revive
- ineffassign
- govet
- unused
- misspell
linters-settings:
depguard:
rules:
main:
files:
- $all
deny:
- pkg: "io/ioutil"
revive:
severity: error
rules:
- name: indent-error-flow
severity: warning
disabled: false
- name: error-strings
disabled: false
staticcheck:
# Suppress reports of deprecated packages
checks: ["-SA1019"]
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -29,10 +29,39 @@ type Command struct {
Args []string `json:"args,omitempty"`
}
// GrpcTLS describes the structure of TLS configuration for gRPC connection, it consist of CA certificate,
// client certificate and client key
type GrpcTLS struct {
// RootCAFile defines path to the PEM file with the set of root certificate authorities
// that clients use when verifying server certificates.
// If RootCAs is nil, TLS uses the host's root CA set.
RootCAFile string `json:"root-ca-file,omitempty"`
// CertFile contains the path to the x509 PEM encoded client certificate.
CertFile string `json:"cert-file,omitempty"`
// KeyFile contains the path to the PEM encoded client key.
KeyFile string `json:"key-file,omitempty"`
// ServerName is used to verify the hostname on the returned
// certificates unless InsecureSkipVerify is given. It is also included
// in the client's handshake to support virtual hosting unless it is
// an IP address.
ServerName string `json:"server-name,omitempty"`
// InsecureSkipVerify controls whether a client verifies the
// server's certificate chain and host name.
// If InsecureSkipVerify is true, TLS accepts any certificate
// presented by the server and any host name in that certificate.
// In this mode, TLS is susceptible to man-in-the-middle attacks.
// This should be used only for testing.
InsecureSkipVerify bool `json:"insecure-skip-verify,omitempty"`
}
// KeyProviderAttrs describes the structure of key provider, it defines the way of invocation to key provider
type KeyProviderAttrs struct {
Command *Command `json:"cmd,omitempty"`
Grpc string `json:"grpc,omitempty"`
GrpcTLS *GrpcTLS `json:"grpc-tls,omitempty"`
}
// OcicryptConfig represents the format of an ocicrypt_provider.conf config file

View File

@@ -18,9 +18,12 @@ package keyprovider
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"os"
"github.com/containers/ocicrypt/config"
keyproviderconfig "github.com/containers/ocicrypt/config/keyprovider-config"
@@ -29,6 +32,7 @@ import (
keyproviderpb "github.com/containers/ocicrypt/utils/keyprovider"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
type keyProviderKeyWrapper struct {
@@ -118,7 +122,7 @@ func (kw *keyProviderKeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []b
}
return protocolOuput.KeyWrapResults.Annotation, nil
} else if kw.attrs.Grpc != "" {
protocolOuput, err := getProviderGRPCOutput(input, kw.attrs.Grpc, OpKeyWrap)
protocolOuput, err := getProviderGRPCOutput(input, kw.attrs.Grpc, kw.attrs.GrpcTLS, OpKeyWrap)
if err != nil {
return nil, fmt.Errorf("error while retrieving keyprovider protocol grpc output: %w", err)
}
@@ -154,7 +158,7 @@ func (kw *keyProviderKeyWrapper) UnwrapKey(dc *config.DecryptConfig, jsonString
return protocolOuput.KeyUnwrapResults.OptsData, nil
} else if kw.attrs.Grpc != "" {
protocolOuput, err := getProviderGRPCOutput(input, kw.attrs.Grpc, OpKeyUnwrap)
protocolOuput, err := getProviderGRPCOutput(input, kw.attrs.Grpc, kw.attrs.GrpcTLS, OpKeyUnwrap)
if err != nil {
// If err is not nil, then ignore it and continue with rest of the given keyproviders
return nil, err
@@ -165,12 +169,56 @@ func (kw *keyProviderKeyWrapper) UnwrapKey(dc *config.DecryptConfig, jsonString
return nil, errors.New("Unsupported keyprovider invocation. Supported invocation methods are grpc and cmd")
}
func getProviderGRPCOutput(input []byte, connString string, operation KeyProviderKeyWrapProtocolOperation) (*KeyProviderKeyWrapProtocolOutput, error) {
func getProviderGRPCOutput(input []byte, connString string, grpcTls *keyproviderconfig.GrpcTLS, operation KeyProviderKeyWrapProtocolOperation) (*KeyProviderKeyWrapProtocolOutput, error) {
var protocolOuput KeyProviderKeyWrapProtocolOutput
var grpcOutput *keyproviderpb.KeyProviderKeyWrapProtocolOutput
cc, err := grpc.Dial(connString, grpc.WithInsecure())
if err != nil {
return nil, fmt.Errorf("error while dialing rpc server: %w", err)
var cc *grpc.ClientConn
var err error
if grpcTls != nil {
var rootCAs *x509.CertPool
if grpcTls.RootCAFile != "" {
pem, err := os.ReadFile(grpcTls.RootCAFile)
if err != nil {
return nil, fmt.Errorf("failed to load root CA certificates error=%v", err)
}
rootCAs = x509.NewCertPool()
if !rootCAs.AppendCertsFromPEM(pem) {
return nil, fmt.Errorf("no root CA certs parsed from file ")
}
} else {
rootCAs, err = x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("error reading SystemCertPool error=%v", err)
}
}
var clientCerts []tls.Certificate
if grpcTls.CertFile != "" && grpcTls.KeyFile != "" {
cert, err := tls.LoadX509KeyPair(grpcTls.CertFile, grpcTls.KeyFile)
if err != nil {
return nil, fmt.Errorf("failed to load client certificate and key: %v", err)
}
clientCerts = []tls.Certificate{cert}
}
tlsConfig := &tls.Config{
RootCAs: rootCAs,
ServerName: grpcTls.ServerName,
InsecureSkipVerify: grpcTls.InsecureSkipVerify,
Certificates: clientCerts,
}
creds := credentials.NewTLS(tlsConfig)
cc, err = grpc.Dial(connString, grpc.WithTransportCredentials(creds))
if err != nil {
return nil, fmt.Errorf("error while dialing TLS rpc server: %w", err)
}
} else {
cc, err = grpc.Dial(connString, grpc.WithInsecure())
if err != nil {
return nil, fmt.Errorf("error while dialing rpc server: %w", err)
}
}
defer func() {
derr := cc.Close()

View File

@@ -11,7 +11,6 @@ import (
"io"
"net/http"
"sync"
"time"
jose "github.com/go-jose/go-jose/v4"
)
@@ -57,16 +56,12 @@ 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, time.Now)
return newRemoteKeySet(ctx, jwksURL)
}
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *RemoteKeySet {
if now == nil {
now = time.Now
}
func newRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet {
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.
//
@@ -81,7 +76,6 @@ func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time)
// a jwks_uri endpoint.
type RemoteKeySet struct {
jwksURL string
now func() time.Time
// Used for configuration. Cancelation is ignored.
ctx context.Context

View File

@@ -162,7 +162,7 @@ var supportedAlgorithms = map[string]bool{
// parsing.
//
// // Directly fetch the metadata document.
// resp, err := http.Get("https://login.example.com/custom-metadata-path")
// resp, err := http.Get("https://login.example.com/custom-metadata-path")
// if err != nil {
// // ...
// }
@@ -267,7 +267,7 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
issuerURL = issuer
}
if p.Issuer != issuerURL && !skipIssuerValidation {
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer)
return nil, fmt.Errorf("oidc: issuer URL provided to client (%q) did not match the issuer URL returned by provider (%q)", issuer, p.Issuer)
}
var algs []string
for _, a := range p.Algorithms {

View File

@@ -1,15 +1,11 @@
package oidc
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
jose "github.com/go-jose/go-jose/v4"
@@ -145,18 +141,6 @@ 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 {
@@ -219,12 +203,49 @@ 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) {
// 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)
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)
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)
@@ -254,6 +275,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
AccessTokenHash: token.AtHash,
claims: payload,
distributedClaims: distributedClaims,
sigAlgorithm: sig.Header.Algorithm,
}
// Check issuer.
@@ -306,45 +328,6 @@ 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

@@ -0,0 +1,60 @@
# 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,6 +6,198 @@ 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 ###
@@ -173,7 +365,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/openSUSE/libpathrs
[libpathrs]: https://github.com/cyphar/libpathrs
[open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html
## [0.2.5] - 2024-05-03 ##
@@ -238,7 +430,12 @@ 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.4.1...HEAD
[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
[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

447
vendor/github.com/cyphar/filepath-securejoin/COPYING.md generated vendored Normal file
View File

@@ -0,0 +1,447 @@
## 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.
```

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