Compare commits

..
72 Commits
Author SHA1 Message Date
Sebastiaan van Stijn 573a46e506 Merge pull request #7223 from vvoland/fix-review
gha: Grant PR review permissions required by v2.0.3
2026-08-28 12:41:48 +02:00
Sebastiaan van Stijn 433ab01649 Merge pull request #7252 from docker/dependabot/github_actions/codeql-actions-f442528237
build(deps): bump the codeql-actions group with 3 updates
2026-08-28 12:40:53 +02:00
Sebastiaan van Stijn 175b4e0a98 Merge pull request #7253 from docker/dependabot/github_actions/docker-actions-93f31bce83
build(deps): bump docker/github-builder/.github/workflows/bake.yml from 1.16.0 to 1.17.0 in the docker-actions group
2026-08-28 12:40:07 +02:00
dependabot[bot] 00de141abf build(deps): bump docker/github-builder/.github/workflows/bake.yml
Bumps the docker-actions group with 1 update: [docker/github-builder/.github/workflows/bake.yml](https://github.com/docker/github-builder).


Updates `docker/github-builder/.github/workflows/bake.yml` from 1.16.0 to 1.17.0
- [Release notes](https://github.com/docker/github-builder/releases)
- [Commits](https://github.com/docker/github-builder/compare/a492c6d04fd3315f67230809b44d60cc0acd50b3...58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a)

---
updated-dependencies:
- dependency-name: docker/github-builder/.github/workflows/bake.yml
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 08:45:50 +00:00
dependabot[bot] 1f46660e37 build(deps): bump the codeql-actions group with 3 updates
Bumps the codeql-actions group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.37.7 to 4.37.8
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28)

Updates `github/codeql-action/autobuild` from 4.37.7 to 4.37.8
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28)

Updates `github/codeql-action/analyze` from 4.37.7 to 4.37.8
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 08:45:25 +00:00
Paweł Gronowski ff3273f3e4 Merge pull request #7243 from thaJeztah/bump_go_metrics
vendor: github.com/docker/go-metrics v0.1.0
2026-08-27 18:50:25 +02:00
Paweł Gronowski 6fccf35355 Merge pull request #7246 from thaJeztah/bump_userns
vendor: github.com/moby/sys/userns v0.2.0
2026-08-27 18:50:15 +02:00
Paweł Gronowski 511f136927 Merge pull request #7247 from thaJeztah/bump_connections
vendor: github.com/docker/go-connections v0.8.1
2026-08-27 18:50:05 +02:00
Sebastiaan van Stijn b17df8fea3 vendor: github.com/docker/go-connections v0.8.1
fixes support for BSD variants

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-27 17:06:54 +02:00
Sebastiaan van Stijn dc47c19e55 vendor: github.com/moby/sys/userns v0.2.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-27 17:06:09 +02:00
Sebastiaan van Stijn 3cd22ff935 vendor: github.com/docker/go-metrics v0.1.0
- Add support for creating timers with custom histogram buckets.
- Update `github.com/prometheus/client_golang` to v1.20.5.
- Update the minimum supported Go version to Go 1.21.
- Improve HTTP handler instrumentation, including minor performance improvements and cleanup.
- Avoid mutating shared label maps when creating namespaces and metrics.
- Encapsulate the Prometheus collector used by `HTTPMetric`.
- Improve package and API documentation.
- Update `golang.org/x/sys` and other dependencies.

full diff: https://github.com/docker/go-metrics/compare/v0.0.1...v0.1.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-27 17:05:24 +02:00
Paweł Gronowski 45f589590b Merge pull request #7248 from thaJeztah/bump_runewidth
vendor: github.com/mattn/go-runewidth v0.0.28
2026-08-27 14:10:47 +02:00
Paweł Gronowski 6b8da05715 Merge pull request #7245 from thaJeztah/bump_otels
vendor: go.opentelemetry.io/otel v1.46.0, go.opentelemetry.io/contrib v0.71.0
2026-08-27 14:10:18 +02:00
Paweł Gronowski 65ad909cde Merge pull request #7244 from thaJeztah/bump_logrus
vendor: github.com/sirupsen/logrus v1.10.2
2026-08-27 14:10:06 +02:00
Paweł Gronowski 52eb28a512 Merge pull request #7242 from thaJeztah/bump_platforms
vendor: github.com/containerd/platforms v1.0.0-rc.5
2026-08-27 14:09:02 +02:00
Sebastiaan van Stijn bb63125969 vendor: go.opentelemetry.io/otel v1.46.0, go.opentelemetry.io/contrib v0.71.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-27 01:56:22 +02:00
Sebastiaan van Stijn 2a24c3c77d vendor: google.golang.org/genproto/* da73d73af1c5
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-27 01:56:22 +02:00
Paweł Gronowski 3f096b4ffd Merge pull request #7241 from thaJeztah/bump_protobuf
vendor: google.golang.org/protobuf v1.36.12
2026-08-26 20:18:16 +02:00
Sebastiaan van Stijn ebc3d5aeab Merge pull request #7055 from docker/dependabot/github_actions/codecov/codecov-action-7.0.0
build(deps): bump codecov/codecov-action from 6.0.1 to 7.0.0
2026-08-26 17:16:22 +02:00
Sebastiaan van Stijn 6261c55c56 Merge pull request #7187 from docker/dependabot/github_actions/crazy-max/dot-github/dot-github/workflows/zizmor.yml-1.11.0
build(deps): bump crazy-max/.github/.github/workflows/zizmor.yml from 1.10.1 to 1.11.0
2026-08-26 17:15:31 +02:00
Sebastiaan van Stijn 2c36f84eaf Merge pull request #7225 from docker/dependabot/github_actions/codeql-actions-79dea28a82
build(deps): bump the codeql-actions group across 1 directory with 3 updates
2026-08-26 17:15:00 +02:00
Sebastiaan van Stijn 4f7a1841ba vendor: github.com/mattn/go-runewidth v0.0.28
full diff: https://github.com/mattn/go-runewidth/compare/v0.0.24...v0.0.28

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-26 17:08:29 +02:00
Sebastiaan van Stijn ef48c25aed vendor: github.com/sirupsen/logrus v1.10.2
removes some transitive dependencies

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-26 16:48:02 +02:00
Sebastiaan van Stijn f2514827f4 vendor: github.com/containerd/platforms v1.0.0-rc.5
full diff: https://github.com/containerd/platforms/compare/v1.0.0-rc.4...v1.0.0-rc.5

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-26 16:41:30 +02:00
Sebastiaan van Stijn f8d6265672 vendor: google.golang.org/protobuf v1.36.12
full diff: https://github.com/protocolbuffers/protobuf-go/compare/v1.36.11...v1.36.12

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-26 16:20:43 +02:00
Paweł Gronowski 744eee4bd4 Merge pull request #7240 from thaJeztah/bump_grpc
vendor: google.golang.org/grpc v1.83.2
2026-08-26 13:06:45 +02:00
Sebastiaan van Stijn d6041d1abc vendor: google.golang.org/grpc v1.83.2
contains a fix for [GHSA-2v4p-qf9q-27wj] and other vulnerabilities
fixed in v1.83.0, v1.83.1

full diff: https://github.com/grpc/grpc-go/compare/v1.82.1...v1.83.2

[GHSA-2v4p-qf9q-27wj]: https://github.com/grpc/grpc-go/security/advisories/GHSA-2v4p-qf9q-27wj

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-26 12:25:17 +02:00
dependabot[bot] 06275f9094 build(deps): bump the codeql-actions group across 1 directory with 3 updates
Bumps the codeql-actions group with 3 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.37.6 to 4.37.7
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd)

Updates `github/codeql-action/autobuild` from 4.37.6 to 4.37.7
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd)

Updates `github/codeql-action/analyze` from 4.37.6 to 4.37.7
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 08:46:02 +00:00
Sebastiaan van Stijn bf6f62cc7c Merge pull request #7195 from thaJeztah/modern_jsonschema
cli/compose/schema: migrate to github.com/santhosh-tekuri/jsonschema/v6
2026-08-25 21:55:32 +02:00
Paweł Gronowski 376f03c858 Merge pull request #7192 from thaJeztah/bump_x_deps
vendor: update golang.org/x/* dependencies
2026-08-25 21:50:33 +02:00
Sebastiaan van Stijn de4a008614 Merge pull request #7233 from docker/dependabot/github_actions/docker-actions-1a2b39a443
build(deps): bump the docker-actions group across 1 directory with 2 updates
2026-08-25 14:54:22 +02:00
dependabot[bot] f3f7da9f3d build(deps): bump the docker-actions group across 1 directory with 2 updates
Bumps the docker-actions group with 2 updates in the / directory: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) and [docker/docker-agent-action/.github/workflows/review-pr.yml](https://github.com/docker/docker-agent-action).


Updates `docker/setup-buildx-action` from 4.2.0 to 4.3.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e)

Updates `docker/docker-agent-action/.github/workflows/review-pr.yml` from 2.0.3 to 2.0.4
- [Release notes](https://github.com/docker/docker-agent-action/releases)
- [Commits](https://github.com/docker/docker-agent-action/compare/baf90543d81f5de59751dfd10e6cf45e21a5a982...b08367e7e9a471822464be2bb9fdaa1c0fb6061b)

---
updated-dependencies:
- dependency-name: docker/docker-agent-action/.github/workflows/review-pr.yml
  dependency-version: 2.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 08:43:06 +00:00
Sebastiaan van Stijn eed270f656 cli/compose/schema: migrate to github.com/santhosh-tekuri/jsonschema/v6
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-24 12:53:45 +02:00
Sebastiaan van Stijn d072fe407f Merge pull request #7234 from vvoland/update-dco
docs: Update DCO identity guidance
2026-08-24 12:16:16 +02:00
Paweł Gronowski 3fe73079aa docs: Clarify DCO identity guidance
The DCO policy used here appears to have been derived from the Linux
kernel's guidance. In 2023, that guidance changed to relax the "real
name" requirement to a "known identity".

The DCO sign-off identifies the person certifying that they have the
right to submit a contribution. The previous wording required a "real
name" and rejected pseudonyms, which could discourage contributors
whose preferred or community name is the identity by which they can be
known.

The requirement is not anonymity: the identity in the sign-off must be
known enough for the project to follow up about the contribution if a
question arises. It does not require a legal name or a government-issued
identity. Use the clarified wording from the Linux kernel so known
pseudonyms and preferred names remain acceptable while anonymous
contributions remain excluded.

This also reflects concerns raised in the CNCF community about whether
"real name" means a legal name, a government-issued identity, or a
public identity that is sufficient to identify and contact a
contributor. The discussion emphasized that attribution and future
accountability do not require publishing a legal identity, and that
strict public-name rules can exclude people who cannot safely contribute
under that name.

Source: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d4563201f33a022fc035

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-24 11:41:24 +02:00
Paweł Gronowski 28f756087e Merge pull request #7224 from vvoland/update-go
update to go1.26.7
2026-08-20 12:38:49 +02:00
Paweł Gronowski 387b8e2af8 update to go1.26.7
This release includes a fix to address a breakage affecting unencrypted
HTTP/2 (h2c) connections caused by a security patch included in last
patch release.

See go.dev/issue/80876 for details.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-19 20:23:24 +02:00
Paweł Gronowski f7e66aa1ff gha: Grant PR review permissions required by v2.0.3
`docker-agent-action` v2.0.3 requires callers of its reusable PR
review workflow to grant `actions: write` for review-lock cache
cleanup and feedback artifacts. Earlier releases required only
`actions: read`. See:

https://github.com/docker/docker-agent-action#pr-review-workflow

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-19 17:45:14 +02:00
Paweł Gronowski 55e4a7eef5 Merge pull request #7222 from thaJeztah/bump_logrus
vendor: github.com/sirupsen/logrus v1.10.1
2026-08-19 12:34:56 +02:00
Sebastiaan van Stijn 58d9cdd2d9 vendor: github.com/sirupsen/logrus v1.10.1
Notable changes:

- Add bidirectional `log/slog` integration with a Logrus hook and `slog.Handler`.
- Add minimal, composable logging interfaces for individual log levels.
- Fix reentrant logging deadlocks and improve concurrency safety around formatters, hooks, and entries.
- Fix generic `Log`, `Logf`, `Logln`, and `LogFn` methods unexpectedly panicking at `PanicLevel`.
- Allow `Entry.Caller` to be set explicitly and preserved across derived entries.
- Improve `TextFormatter` performance and reduce allocations significantly.
- Improve common Logger and Entry hot paths and caller-reporting performance.
- Update `TextFormatter` handling for `[]byte`, debug/trace colors, and Windows ANSI terminals.
- Raise the minimum supported Go version to Go 1.23.
- Deprecate `Entry.HasCaller` and `MutexWrap`.

release-notes: https://github.com/sirupsen/logrus/releases/tag/v1.10.0
release-notes: https://github.com/sirupsen/logrus/releases/tag/v1.10.1
full diff: https://github.com/sirupsen/logrus/compare/v1.9.4...v1.10.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-19 11:34:53 +02:00
Sebastiaan van Stijn 1612f4c767 vendor: golang.org/x/mod v0.40.0
full diff: https://github.com/golang/mod/compare/v0.39.0...v0.40.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-15 14:44:44 +02:00
Sebastiaan van Stijn cf53a284da vendor: golang.org/x/net v0.58.0
full diff: https://github.com/golang/net/compare/v0.57.0...v0.58.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-15 14:43:34 +02:00
Sebastiaan van Stijn 0da5f45651 vendor: golang.org/x/text v0.41.0
full diff: https://github.com/golang/text/compare/v0.40.0...v0.41.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-15 14:42:34 +02:00
Sebastiaan van Stijn ce15202968 vendor: golang.org/x/mod v0.39.0
full diff: https://github.com/golang/mod/compare/v0.38.0...v0.39.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-15 14:41:51 +02:00
Sebastiaan van Stijn 8659aa5d4c Merge pull request #7185 from thaJeztah/duplicate_normalize
cli/command/manifest: remove redundant normalization
2026-08-14 18:42:04 +02:00
Paweł Gronowski 8d08c75e81 Merge pull request #7191 from thaJeztah/bump_go
update to go1.26.6
2026-08-14 14:52:56 +02:00
Sebastiaan van Stijn 24af004cbf update to go1.26.6
This release includes 10 security fixes following the security policy:

- x/mod/sumdb/tlog: fix transparency log tile verification bypass

    A malicious GOPROXY was previously capable of forging
    up to two sumdb tiles that allow for a requested module
    to bypass the GOSUMDB check and persist attacker-controlled
    module content to a local Go module cache.

    This attack allows for a malicious GOPROXY to serve
    malicious module content that cannot be detected
    by evaluating the transparency log.

    All tiles are now correctly verified against their parents.

    In order to determine if you have been affected:

    rm -r go.sum go.work.sum vendor/ && go mod tidy

    Thanks to Filippo Valsorda (Geomys) for reporting this issue.

    This is CVE-2026-56865 and Go issue https://go.dev/issue/80744.

- x/mod/sumdb: ignore unrelated, unauthenticated hashes in Lookup

    A malicious GOSUMDB was capable of serving arbitrary
    module content not contained within the transparency
    log.

    This attack allows for a coordinating GOPROXY and
    GOSUMDB to serve a client malicious module content
    that cannot be detected by evaluating the transparency
    log.

    In order to determine if you have been affected:

    rm -r go.sum go.work.sum vendor/ && go mod tidy

    Thanks to mundur for reporting this issue.

    This is CVE-2026-56864 and Go issue https://go.dev/issue/80745.

- encoding/xml: add recursion depth guard during decode

    Previously, DecodeElement would reset the depth counter
    causing it to never fire; this could lead to stack
    exhaustion.

    This is CVE-2026-56859 and Go issue https://go.dev/issue/80481.

 net/http: apply ReadHeaderTimeout when doing unencrypted HTTP/2 check

    When a server is configured to support unencrypted HTTP/2, it reads a
    few bytes from each new connection to see if they contain the HTTP/2
    client preface. Previously, this was being done with no timeout applied.
    ReadHeaderTimeout is now applied for this.

    This is CVE-2026-56853 and Go issue https://go.dev/issue/80205.

- net/url: avoid quadratic complexity in resolvePath

    Previously, resolving relative paths containing parent directory (..) segments performed string conversions and buffer rewrites on each step, resulting in quadratic time complexity and high memory allocation overhead.

    Now, path resolution operates on a byte buffer using index-based backtracking for .. segments, eliminating the quadratic time complexity and significantly reducing memory allocations.

    This is CVE-2026-56860 and Go issue https://go.dev/issue/80494.

- golang.org/x/net/dns/dnsmessage: panic when parsing invalid SVCB record

    Parsing an invalid SVCB or HTTPS RR can panic when
    the size of a parameter value overflows the message buffer.

    Thanks to Mundur (https://github.com/M0nd0R) for reporting this issue.

    This is CVE-2026-46600 and Go issue https://go.dev/issue/79795.

- crypto/tls: limit handshake messages we are willing to accept post-handshake

    Previously, we always counted handshake messages, such as KeyUpdate, as
    state-advancing, regardless of whether a handshake has been completed or
    not. As a result, a malicious client can keep sending KeyUpdate messages
    to force the server to keep performing key derivation operations
    indefinitely.

    Thanks to Qi Deng of Aurascape.ai for reporting this issue.

    This is CVE-2026-56862 and Go issue https://go.dev/issue/80528.

- html/template: fix Javascript regexp context tracking

    Previously, pathological inputs could close an
    unescaped / early, allowing for attack-controlled
    data to inject arbitrary content, potentially
    leading to XSS.

    Thanks to Ali Sherif for reporting this issue.

    This is CVE-2026-56858 and Go issue https://go.dev/issue/80435.

- x/net/idna: failure to reject ASCII-only Punycode-encoded labels

    The ToASCII and ToUnicode functions incorrectly accepted Punycode-encoded labels
    that decode to an ASCII-only label. For example, ToUnicode("xn--example-.com")
    incorrectly returned the name "example.com" rather than an error.

    The idna package implements the processing algorithm from UTS 46.
    Older versions of UTS 46 included a specification bug which permitted
    multiple ASCII labels to decode to the same Unicode label.
    UTS 46 revision 33 fixed the specification bug.
    The idna package now implements the updated specification.

    This behavior can lead to privilege escalation in programs using the idna package.
    For example, a program which performs privilege checks on the ASCII hostname
    may reject "example.com" but permit "xn--example-.com". If that program subsequently
    converts the ASCII hostname to Unicode, it will inadvertently permits access
    to the Unicode name "example.com".

    Thanks to KC1zs4 (https://github.com/KC1zs4) for reporting this issue.

    This is CVE-2026-39821 and Go issue https://go.dev/issue/78760.

- encoding/asn1: enforce maximum recursion depth

    Enforce a recursion limit in Unmarshal to prevent stack exhaustion
    when parsing deeply-nested, recursive structures.

    Thanks to Marwan Atia (marwans...@gmail.com) for reporting this issue.

    This is CVE-2026-33818 and Go issue https://go.dev/issue/80405.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-14 14:02:40 +02:00
Sebastiaan van Stijn eb68ed19d2 Merge pull request #7157 from docker/dependabot/github_actions/codeql-actions-8417568871
build(deps): bump the codeql-actions group across 1 directory with 3 updates
2026-08-13 22:42:40 +02:00
dependabot[bot] 8a375034e2 build(deps): bump the codeql-actions group across 1 directory with 3 updates
Bumps the codeql-actions group with 3 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.37.3 to 4.37.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81...5595ccaf912efad79be6eef63a5619ff05969be3)

Updates `github/codeql-action/autobuild` from 4.37.3 to 4.37.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81...5595ccaf912efad79be6eef63a5619ff05969be3)

Updates `github/codeql-action/analyze` from 4.37.3 to 4.37.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81...5595ccaf912efad79be6eef63a5619ff05969be3)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 08:46:20 +00:00
dependabot[bot] f755f48a12 build(deps): bump crazy-max/.github/.github/workflows/zizmor.yml
Bumps [crazy-max/.github/.github/workflows/zizmor.yml](https://github.com/crazy-max/.github) from 1.10.1 to 1.11.0.
- [Release notes](https://github.com/crazy-max/.github/releases)
- [Commits](https://github.com/crazy-max/.github/compare/46267a6e61cd56aac2fc79943df180152f4c89d6...f91220cba053bd54299ba645a599ee8ed436f222)

---
updated-dependencies:
- dependency-name: crazy-max/.github/.github/workflows/zizmor.yml
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 08:43:11 +00:00
Sebastiaan van Stijn a7c0741fa7 cli/command/manifest: remove redundant normalization
commit 55fcffe743 added normalization
in config/configfile, so it's no longer needed to normalize here as
well.

updates 55fcffe743

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-12 16:38:26 +02:00
Sebastiaan van Stijn 12892075d2 Merge pull request #7180 from docker/dependabot/github_actions/docker-actions-ebe691cc85
build(deps): bump docker/github-builder/.github/workflows/bake.yml from 1.15.0 to 1.16.0 in the docker-actions group
2026-08-12 15:37:56 +02:00
dependabot[bot] e1f9b284df build(deps): bump docker/github-builder/.github/workflows/bake.yml
Bumps the docker-actions group with 1 update: [docker/github-builder/.github/workflows/bake.yml](https://github.com/docker/github-builder).


Updates `docker/github-builder/.github/workflows/bake.yml` from 1.15.0 to 1.16.0
- [Release notes](https://github.com/docker/github-builder/releases)
- [Commits](https://github.com/docker/github-builder/compare/27ade872c1e2296e62ef15ab3b10d37665e57cf7...a492c6d04fd3315f67230809b44d60cc0acd50b3)

---
updated-dependencies:
- dependency-name: docker/github-builder/.github/workflows/bake.yml
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-12 08:43:00 +00:00
Sebastiaan van Stijn 4f84911bfe Merge pull request #7144 from hirehamir/fix/ps-port-sort
cli/command/formatter: sort published ports numerically by IP
2026-08-06 15:50:28 +02:00
Paweł Gronowski 228f29e810 Merge pull request #7159 from thaJeztah/version_29.8
VERSION: 29.8.0
2026-08-06 15:47:41 +02:00
Sebastiaan van Stijn d8c3543407 VERSION: 29.8.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-08-06 15:34:41 +02:00
Hamir 2dd7b7e15b cli/command/formatter: sort published ports numerically by IP
`comparePorts` compared host IPs with `i.IP.String() < j.IP.String()`, which
sorts them lexicographically, so "10.0.0.2" ordered before "9.0.0.1" in
`docker ps` output.

This also avoids formatting both addresses as strings on every comparison.

ports    before               after
64       1540 allocs/op       394 allocs/op
256      5364 allocs/op       1548 allocs/op

That's a ~74% (3.9x) reduction in allocations.

Signed-off-by: Hamir <hirehamir@gmail.com>
2026-08-05 11:38:20 -07:00
Paweł Gronowski a7dcaa6fdb Merge pull request #7151 from vvoland/update-archive
vendor: github.com/moby/go-archive v0.3.3
2026-08-05 19:34:15 +02:00
Paweł Gronowski 400b45f682 vendor: github.com/moby/go-archive v0.3.3
full diff: https://github.com/moby/go-archive/compare/v0.3.2...v0.3.3

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-05 19:11:05 +02:00
Sebastiaan van Stijn 38887ec4ed Merge pull request #7145 from winklemad/fix-service-duplicate-removal-panic
cli/command/service: fix panic when removing duplicate values
2026-08-05 19:10:29 +02:00
Sebastiaan van Stijn 904aef7f69 Merge pull request #7147 from docker/dependabot/github_actions/docker-actions-269857e486
build(deps): bump docker/docker-agent-action/.github/workflows/review-pr.yml from 2.0.2 to 2.0.3 in the docker-actions group across 1 directory
2026-08-04 13:11:45 +02:00
dependabot[bot] f08e60eb09 build(deps): bump docker/docker-agent-action/.github/workflows/review-pr.yml
Bumps the docker-actions group with 1 update in the / directory: [docker/docker-agent-action/.github/workflows/review-pr.yml](https://github.com/docker/docker-agent-action).


Updates `docker/docker-agent-action/.github/workflows/review-pr.yml` from 2.0.2 to 2.0.3
- [Release notes](https://github.com/docker/docker-agent-action/releases)
- [Commits](https://github.com/docker/docker-agent-action/compare/774b6e0e60d6c648b0f2dc43bd5221377a0a7420...baf90543d81f5de59751dfd10e6cf45e21a5a982)

---
updated-dependencies:
- dependency-name: docker/docker-agent-action/.github/workflows/review-pr.yml
  dependency-version: 2.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 08:42:59 +00:00
Paweł Gronowski abfd89157d Merge pull request #7149 from vvoland/update-docker
VERSION: 29.7.2
2026-08-03 18:24:10 +02:00
Paweł Gronowski 519eb45d03 VERSION: 29.7.2
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-03 18:18:56 +02:00
Madan Kumar 8717af7168 cli/command/service: fix panic when removing duplicate values
Both makeEnv and updateHosts removed elements from a slice while ranging
over that same slice. The range expression is evaluated once, so after a
removal the loop keeps using the original length: it reads stale elements
that shifted down, skips a live element, and can slice past the end of the
shrunken slice, which panics.

In makeEnv, the "no update required" continue also applied to the inner
loop instead of skipping the re-append, so an env-var passed twice with
the same value was stored twice. A third occurrence with a different value
then tried to remove both entries and panicked:

    docker service create --env A=1 --env A=1 --env A=2 --name repro nginx
    panic: runtime error: slice bounds out of range [2:1]

The same happens with an env-file that lists a variable twice, and the
panic occurs before any API call, so no daemon is needed to hit it.

updateHosts has the same problem when a hostname is listed more than once
in a single entry: --host-rm either leaves a copy behind or panics with
"slice bounds out of range". That needs a spec written through the API or
swarmkit directly, as the CLI does not produce such entries itself, so it
is less likely to be hit in practice.

Use slices.DeleteFunc for both, which removes every match in one pass, and
add tests for makeEnv, which had no coverage.

Signed-off-by: Madan Kumar <winklemad@outlook.com>
2026-08-02 07:13:16 +05:30
Paweł Gronowski e9452d6e78 Merge pull request #7142 from thaJeztah/bump_go_archive_0.3.2
vendor: github.com/moby/go-archive v0.3.2
2026-07-31 19:03:37 +02:00
Sebastiaan van Stijn a6014a702b vendor: github.com/moby/go-archive v0.3.2
full diff: https://github.com/moby/go-archive/compare/v0.3.1...v0.3.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-31 18:53:47 +02:00
Paweł Gronowski 2465fca604 Merge pull request #7140 from thaJeztah/bump_go_archive_0.3.1
vendor: github.com/moby/go-archive v0.3.1
2026-07-31 18:33:04 +02:00
Sebastiaan van Stijn 52b15cc571 Merge pull request #7141 from thaJeztah/bump_version2
update version to 29.7.1
2026-07-31 18:20:09 +02:00
Sebastiaan van Stijn 13590921a4 update version to 29.7.1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-31 18:11:45 +02:00
Sebastiaan van Stijn 0b50545471 vendor: github.com/moby/go-archive v0.3.1
full diff: https://github.com/moby/go-archive/compare/v0.3.0...v0.3.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-31 18:10:04 +02:00
dependabot[bot] 244404379d build(deps): bump codecov/codecov-action from 6.0.1 to 7.0.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6.0.1 to 7.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/e79a6962e0d4c0c17b229090214935d2e33f8354...fb8b3582c8e4def4969c97caa2f19720cb33a72f)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 08:44:21 +00:00
511 changed files with 48075 additions and 12909 deletions
+3 -3
View File
@@ -65,7 +65,7 @@ jobs:
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
-
name: Build
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
@@ -99,7 +99,7 @@ jobs:
bin-image:
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
uses: docker/github-builder/.github/workflows/bake.yml@27ade872c1e2296e62ef15ab3b10d37665e57cf7 # v1.15.0
uses: docker/github-builder/.github/workflows/bake.yml@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a # v1.17.0
permissions:
contents: read # same as global permission
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
@@ -156,7 +156,7 @@ jobs:
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
-
name: Build
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
+4 -4
View File
@@ -64,18 +64,18 @@ jobs:
name: Update Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.5"
go-version: "1.26.7"
cache: false
-
name: Initialize CodeQL
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/autobuild@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
category: "/language:go"
+2 -2
View File
@@ -65,7 +65,7 @@ jobs:
docker info
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
-
name: Run ${{ matrix.target }}
run: |
@@ -76,7 +76,7 @@ jobs:
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
-
name: Send to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
+2 -2
View File
@@ -11,13 +11,13 @@ permissions:
jobs:
review:
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@774b6e0e60d6c648b0f2dc43bd5221377a0a7420 # v2.0.2
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@b08367e7e9a471822464be2bb9fdaa1c0fb6061b # v2.0.4
permissions:
contents: read # Read repository files and PR diffs
pull-requests: write # Post review comments
issues: write # Create security incident issues if secrets detected
checks: write # (Optional) Show review progress as a check run
id-token: write # Required for OIDC authentication to AWS Secrets Manager
actions: read # Download artifacts from trigger workflow
actions: write # Required since v2.0.3 — review-lock cache cleanup and feedback artifacts
with:
trigger-run-id: ${{ github.event_name == 'workflow_run' && format('{0}', github.event.workflow_run.id) || '' }}
+4 -4
View File
@@ -30,7 +30,7 @@ jobs:
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
-
name: Test
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
@@ -38,7 +38,7 @@ jobs:
targets: test-coverage
-
name: Send to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
@@ -68,7 +68,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.5"
go-version: "1.26.7"
cache: false
-
name: Test
@@ -82,7 +82,7 @@ jobs:
shell: bash
-
name: Send to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
+1 -1
View File
@@ -101,7 +101,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.5"
go-version: "1.26.7"
cache: false
-
name: Run gocompat check
+1 -1
View File
@@ -21,7 +21,7 @@ on:
jobs:
run:
uses: crazy-max/.github/.github/workflows/zizmor.yml@46267a6e61cd56aac2fc79943df180152f4c89d6 # v1.10.1
uses: crazy-max/.github/.github/workflows/zizmor.yml@f91220cba053bd54299ba645a599ee8ed436f222 # v1.11.0
permissions:
contents: read
security-events: write
+1 -1
View File
@@ -5,7 +5,7 @@ run:
# which causes it to fallback to go1.17 semantics.
#
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.26.5"
go: "1.26.7"
timeout: 5m
+1 -1
View File
@@ -267,7 +267,7 @@ Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use your real name (sorry, no pseudonyms or anonymous contributions.)
Use a known identity (sorry, no anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.
+1 -1
View File
@@ -8,7 +8,7 @@ ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.23
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.26.5
ARG GO_VERSION=1.26.7
# XX_VERSION specifies the version of the xx utility to use.
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
+1 -1
View File
@@ -1 +1 @@
29.7.0
29.8.0
+1 -1
View File
@@ -458,7 +458,7 @@ func comparePorts(i, j container.PortSummary) bool {
}
if i.IP != j.IP {
return i.IP.String() < j.IP.String()
return i.IP.Less(j.IP)
}
if i.PublicPort != j.PublicPort {
+18
View File
@@ -946,6 +946,24 @@ func TestDisplayablePorts(t *testing.T) {
},
expected: "80/tcp, 80/udp, 1024/tcp, 1024/udp, 12345/sctp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp", //nolint:revive // ignore line-length-limit (revive)
},
{
// host IPs are ordered numerically, not lexicographically:
// "10.0.0.2" sorts as a string before "9.0.0.1".
ports: []container.PortSummary{
{
IP: netip.MustParseAddr("10.0.0.2"),
PublicPort: 8080,
PrivatePort: 80,
Type: "tcp",
}, {
IP: netip.MustParseAddr("9.0.0.1"),
PublicPort: 8081,
PrivatePort: 80,
Type: "tcp",
},
},
expected: "9.0.0.1:8081->80/tcp, 10.0.0.2:8080->80/tcp",
},
}
for _, port := range cases {
+1 -23
View File
@@ -48,27 +48,6 @@ func newManifestStore(dockerCLI command.Cli) store.Store {
return store.NewStore(filepath.Join(config.Dir(), "manifests"))
}
// authConfigKey is the key used to store credentials for Docker Hub. It is
// a copy of [registry.IndexServer].
//
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer
const authConfigKey = "https://index.docker.io/v1/"
// getAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
//
// It is similar to [registry.GetAuthConfigKey], but does not require on
// [registrytypes.IndexInfo] as intermediate.
//
// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#GetAuthConfigKey
// [registrytypes.IndexInfo]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/api/types/registry#IndexInfo
func getAuthConfigKey(domainName string) string {
if domainName == "docker.io" || domainName == "index.docker.io" {
return authConfigKey
}
return domainName
}
// newRegistryClient returns a client for communicating with a Docker distribution
// registry
func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient.RegistryClient {
@@ -78,8 +57,7 @@ func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient
}
cfg := dockerCLI.ConfigFile()
resolver := func(ctx context.Context, domainName string) registry.AuthConfig {
configKey := getAuthConfigKey(domainName)
a, _ := cfg.GetAuthConfig(configKey)
a, _ := cfg.GetAuthConfig(domainName)
return registry.AuthConfig{
Username: a.Username,
Password: a.Password,
+7 -8
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
@@ -687,15 +688,13 @@ func (options *serviceOptions) makeEnv() ([]string, error) {
}
currentEnv := make([]string, 0, len(envVariables))
for _, env := range envVariables { // need to process each var, in order
k, _, _ := strings.Cut(env, "=")
for i, current := range currentEnv { // remove duplicates
if current == env {
continue // no update required, may hide this behind flag to preserve order of envVariables
}
if strings.HasPrefix(current, k+"=") {
currentEnv = append(currentEnv[:i], currentEnv[i+1:]...)
}
if slices.Contains(currentEnv, env) {
continue // no update required, may hide this behind flag to preserve order of envVariables
}
k, _, _ := strings.Cut(env, "=")
currentEnv = slices.DeleteFunc(currentEnv, func(current string) bool { // remove duplicates
return strings.HasPrefix(current, k+"=")
})
currentEnv = append(currentEnv, env)
}
+46
View File
@@ -373,3 +373,49 @@ func TestToServiceSysCtls(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(service.TaskTemplate.ContainerSpec.Sysctls, expected))
}
func TestMakeEnv(t *testing.T) {
tests := []struct {
doc string
env []string
expected []string
}{
{
doc: "no duplicates",
env: []string{"one=1", "two=2"},
expected: []string{"one=1", "two=2"},
},
{
doc: "same variable repeated",
env: []string{"one=1", "one=1"},
expected: []string{"one=1"},
},
{
doc: "same variable repeated, then overridden",
env: []string{"one=1", "one=1", "one=2"},
expected: []string{"one=2"},
},
{
doc: "repeated variable last",
env: []string{"one=1", "two=2", "two=2"},
expected: []string{"one=1", "two=2"},
},
{
doc: "last value wins",
env: []string{"one=1", "two=2", "one=3"},
expected: []string{"two=2", "one=3"},
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
o := newServiceOptions()
for _, env := range tc.env {
assert.NilError(t, o.env.Set(env))
}
actual, err := o.makeEnv()
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(tc.expected, actual))
})
}
}
+3 -5
View File
@@ -1213,11 +1213,9 @@ func updateHosts(flags *pflag.FlagSet, hosts *[]string) error {
if rm.IPAddr != "" && rm.IPAddr != ip {
continue
}
for i, h := range hostNames {
if h == rm.Host {
hostNames = append(hostNames[:i], hostNames[i+1:]...)
}
}
hostNames = slices.DeleteFunc(hostNames, func(h string) bool {
return h == rm.Host
})
}
if len(hostNames) > 0 {
newHosts = append(newHosts, fmt.Sprintf("%s %s", ip, strings.Join(hostNames, " ")))
+15
View File
@@ -1727,3 +1727,18 @@ func TestUpdateUlimits(t *testing.T) {
})
}
}
func TestUpdateHostsRemoveRepeatedHost(t *testing.T) {
flags := newUpdateCommand(nil).Flags()
flags.Set("host-rm", "host1")
//nolint:dupword // ignore "Duplicate words (host1) found"
hosts := []string{"127.0.0.1 host1 host1 host2", "127.0.0.2 host2 host1 host1"}
err := updateHosts(flags, &hosts)
assert.NilError(t, err)
// All occurrences of `host1` should be removed, also if the same host
// is listed multiple times in the same entry.
assert.Check(t, is.DeepEqual([]string{"127.0.0.1 host2", "127.0.0.2 host2"}, hosts))
}
+15 -15
View File
@@ -419,7 +419,7 @@ services:
foo:
image: busybox
`)
assert.Check(t, is.ErrorContains(err, "version must be a string"))
assert.Check(t, is.ErrorContains(err, "version: must be a string"))
}
func TestV1Unsupported(t *testing.T) {
@@ -427,7 +427,7 @@ func TestV1Unsupported(t *testing.T) {
foo:
image: busybox
`)
assert.Check(t, is.ErrorContains(err, "(root) Additional property foo is not allowed"))
assert.Check(t, is.ErrorContains(err, "(root): additional property 'foo' is not allowed"))
_, err = loadYAML(`
version: "1.0"
@@ -445,14 +445,14 @@ services:
- foo:
image: busybox
`)
assert.Check(t, is.ErrorContains(err, "services must be a mapping"))
assert.Check(t, is.ErrorContains(err, "services: must be a mapping"))
_, err = loadYAML(`
version: "3"
services:
foo: busybox
`)
assert.Check(t, is.ErrorContains(err, "services.foo must be a mapping"))
assert.Check(t, is.ErrorContains(err, "services.foo: must be a mapping"))
_, err = loadYAML(`
version: "3"
@@ -460,14 +460,14 @@ networks:
- default:
driver: bridge
`)
assert.Check(t, is.ErrorContains(err, "networks must be a mapping"))
assert.Check(t, is.ErrorContains(err, "networks: must be a mapping"))
_, err = loadYAML(`
version: "3"
networks:
default: bridge
`)
assert.Check(t, is.ErrorContains(err, "networks.default must be a mapping"))
assert.Check(t, is.ErrorContains(err, "networks.default: must be null or a mapping"))
_, err = loadYAML(`
version: "3"
@@ -475,14 +475,14 @@ volumes:
- data:
driver: local
`)
assert.Check(t, is.ErrorContains(err, "volumes must be a mapping"))
assert.Check(t, is.ErrorContains(err, "volumes: must be a mapping"))
_, err = loadYAML(`
version: "3"
volumes:
data: local
`)
assert.Check(t, is.ErrorContains(err, "volumes.data must be a mapping"))
assert.Check(t, is.ErrorContains(err, "volumes.data: must be null or a mapping"))
}
func TestNonStringImage(t *testing.T) {
@@ -492,7 +492,7 @@ services:
foo:
image: ["busybox", "latest"]
`)
assert.Check(t, is.ErrorContains(err, "services.foo.image must be a string"))
assert.Check(t, is.ErrorContains(err, "services.foo.image: must be a string"))
}
func TestIgnoreBuildProperties(t *testing.T) {
@@ -553,7 +553,7 @@ services:
environment:
FOO: ["1"]
`)
assert.Check(t, is.ErrorContains(err, "services.dict-env.environment.FOO must be a string, number or null"))
assert.Check(t, is.ErrorContains(err, "services.dict-env.environment.FOO: must be null or a number or a string"))
}
func TestInvalidEnvironmentObject(t *testing.T) {
@@ -564,7 +564,7 @@ services:
image: busybox
environment: "FOO=1"
`)
assert.Check(t, is.ErrorContains(err, "services.dict-env.environment must be a mapping"))
assert.Check(t, is.ErrorContains(err, "services.dict-env.environment: must be a mapping"))
}
func TestLoadWithEnvironmentInterpolation(t *testing.T) {
@@ -899,7 +899,7 @@ func TestInvalidResource(t *testing.T) {
impossible:
x: 1
`)
assert.Check(t, is.ErrorContains(err, "Additional property impossible is not allowed"))
assert.Check(t, is.ErrorContains(err, "additional property 'impossible' is not allowed"))
}
func TestInvalidExternalAndDriverCombination(t *testing.T) {
@@ -1033,7 +1033,7 @@ services:
tmpfs:
size: 10000
`)
assert.Check(t, is.ErrorContains(err, "services.tmpfs.volumes.0 Additional property tmpfs is not allowed"))
assert.Check(t, is.ErrorContains(err, "services.tmpfs.volumes.0: additional property 'tmpfs' is not allowed"))
}
func TestLoadBindMountSourceMustNotBeEmpty(t *testing.T) {
@@ -1191,7 +1191,7 @@ services:
tmpfs:
size: -1
`)
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0")
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size: must be greater than or equal to 0")
}
func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) {
@@ -1206,7 +1206,7 @@ services:
tmpfs:
size: 0.0001
`)
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size must be a integer")
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size: must be an integer")
}
func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
+167 -67
View File
@@ -4,14 +4,20 @@
package schema
import (
"bytes"
"embed"
"encoding/json"
"errors"
"fmt"
"math/big"
"reflect"
"strings"
"time"
"github.com/docker/go-connections/nat"
"github.com/xeipuuv/gojsonschema"
"github.com/santhosh-tekuri/jsonschema/v6"
"github.com/santhosh-tekuri/jsonschema/v6/kind"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
const (
@@ -21,34 +27,29 @@ const (
type portsFormatChecker struct{}
func (portsFormatChecker) IsFormat(input any) bool {
var portSpec string
switch p := input.(type) {
case string:
portSpec = p
case *big.Rat:
portSpec = strings.Split(p.String(), "/")[0]
func (portsFormatChecker) Validate(input any) error {
switch input.(type) {
case string, json.Number,
int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64,
float32, float64:
_, err := nat.ParsePortSpec(fmt.Sprint(input))
return err
default:
// Formats only apply to supported string/number values.
return nil
}
_, err := nat.ParsePortSpec(portSpec)
return err == nil
}
type durationFormatChecker struct{}
func (durationFormatChecker) IsFormat(input any) bool {
func (durationFormatChecker) Validate(input any) error {
value, ok := input.(string)
if !ok {
return false
return nil
}
_, err := time.ParseDuration(value)
return err == nil
}
func init() {
gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{})
gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{})
return err
}
// Version returns the version of the config, defaulting to the latest "3.x"
@@ -82,44 +83,84 @@ func Validate(config map[string]any, version string) error {
return fmt.Errorf("unsupported Compose file version: %s", version)
}
schemaLoader := gojsonschema.NewStringLoader(string(schemaData))
dataLoader := gojsonschema.NewGoLoader(config)
result, err := gojsonschema.Validate(schemaLoader, dataLoader)
schemaDoc, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaData))
if err != nil {
return err
}
if !result.Valid() {
return toError(result)
compiler := jsonschema.NewCompiler()
compiler.RegisterFormat(&jsonschema.Format{Name: "ports", Validate: portsFormatChecker{}.Validate})
compiler.RegisterFormat(&jsonschema.Format{Name: "duration", Validate: durationFormatChecker{}.Validate})
if err := compiler.AddResource("schema.json", schemaDoc); err != nil {
return err
}
schema, err := compiler.Compile("schema.json")
if err != nil {
return err
}
if err := schema.Validate(toJSONValue(config)); err != nil {
var validationErr *jsonschema.ValidationError
if errors.As(err, &validationErr) {
return getMostSpecificError(validationErr)
}
return err
}
return nil
}
func toError(result *gojsonschema.Result) error {
err := getMostSpecificError(result.Errors())
return err
func toJSONValue(v any) any {
if v == nil {
return nil
}
switch value := reflect.ValueOf(v); value.Kind() { //nolint: exhaustive // only need to handle maps and slices.
case reflect.Map:
result := make(map[string]any, value.Len())
iter := value.MapRange()
for iter.Next() {
result[iter.Key().String()] = toJSONValue(iter.Value().Interface())
}
return result
case reflect.Slice:
result := make([]any, value.Len())
for i := range value.Len() {
result[i] = toJSONValue(value.Index(i).Interface())
}
return result
default:
return v
}
}
const (
jsonschemaOneOf = "number_one_of"
jsonschemaAnyOf = "number_any_of"
)
var printer = message.NewPrinter(language.English)
func getDescription(err validationError) string {
switch err.parent.Type() {
case "invalid_type":
if expectedType, ok := err.parent.Details()["expected"].(string); ok {
return "must be a " + humanReadableType(expectedType)
switch parent := err.parent.ErrorKind.(type) {
case *kind.Type:
types := make([]string, len(parent.Want))
for i, typ := range parent.Want {
types[i] = humanReadableType(typ)
}
case jsonschemaOneOf, jsonschemaAnyOf:
if err.child == nil {
return err.parent.Description()
return "must be " + strings.Join(types, " or ")
case *kind.AnyOf, *kind.OneOf:
if err.child != nil {
return getDescription(validationError{parent: err.child})
}
return err.child.Description()
return err.parent.ErrorKind.LocalizedString(printer)
case *kind.AdditionalProperties:
if len(parent.Properties) == 1 {
return fmt.Sprintf("additional property '%s' is not allowed", parent.Properties[0])
}
return err.parent.ErrorKind.LocalizedString(printer)
case *kind.Minimum:
want, _ := parent.Want.Float64()
return fmt.Sprintf("must be greater than or equal to %v", want)
default:
return err.parent.ErrorKind.LocalizedString(printer)
}
return err.parent.Description()
}
func humanReadableType(definition string) string {
@@ -134,26 +175,39 @@ func humanReadableType(definition string) string {
allTypes[len(allTypes)-1],
)
}
if definition == "object" {
return "mapping"
switch definition {
case "object":
return "a mapping"
case "array":
return "a list"
case "integer":
return "an integer"
case "null":
return "null"
default:
return "a " + definition
}
if definition == "array" {
return "list"
}
return definition
}
type validationError struct {
parent gojsonschema.ResultError
child gojsonschema.ResultError
parent *jsonschema.ValidationError
child *jsonschema.ValidationError
}
func (err validationError) Error() string {
description := getDescription(err)
return fmt.Sprintf("%s %s", err.parent.Field(), description)
validationErr := err.parent
if err.child != nil {
validationErr = err.child
}
field := strings.Join(validationErr.InstanceLocation, ".")
if field == "" {
field = "(root)"
}
return field + ": " + getDescription(err)
}
func getMostSpecificError(errs []gojsonschema.ResultError) validationError {
func getMostSpecificError(result *jsonschema.ValidationError) validationError {
errs := flattenErrors(result)
mostSpecificError := 0
for i, err := range errs {
if specificity(err) > specificity(errs[mostSpecificError]) {
@@ -162,28 +216,74 @@ func getMostSpecificError(errs []gojsonschema.ResultError) validationError {
}
if specificity(err) == specificity(errs[mostSpecificError]) {
// Invalid type errors win in a tie-breaker for most specific field name
if err.Type() == "invalid_type" && errs[mostSpecificError].Type() != "invalid_type" {
// Invalid type errors win in a tie-breaker for most specific field name.
if isTypeError(err) && !isTypeError(errs[mostSpecificError]) {
mostSpecificError = i
}
}
}
if mostSpecificError+1 == len(errs) {
return validationError{parent: errs[mostSpecificError]}
err := validationError{parent: errs[mostSpecificError]}
switch err.parent.ErrorKind.(type) {
case *kind.OneOf, *kind.AnyOf:
err.child = mostSpecificCause(err.parent)
}
return err
}
switch errs[mostSpecificError].Type() {
case "number_one_of", "number_any_of":
return validationError{
parent: errs[mostSpecificError],
child: errs[mostSpecificError+1],
func mostSpecificCause(err *jsonschema.ValidationError) *jsonschema.ValidationError {
var best *jsonschema.ValidationError
for _, cause := range err.Causes {
for _, candidate := range flattenErrors(cause) {
switch candidate.ErrorKind.(type) {
case *kind.OneOf, *kind.AnyOf:
if nested := mostSpecificCause(candidate); nested != nil {
candidate = nested
}
}
if best == nil || specificity(candidate) > specificity(best) {
best = candidate
continue
}
if specificity(candidate) == specificity(best) {
// Within oneOf/anyOf, an error from an alternative whose
// type matched is more useful than a type mismatch from
// another alternative.
if !isTypeError(candidate) && isTypeError(best) {
best = candidate
}
}
}
default:
return validationError{parent: errs[mostSpecificError]}
}
return best
}
func specificity(err gojsonschema.ResultError) int {
return len(strings.Split(err.Field(), "."))
func flattenErrors(err *jsonschema.ValidationError) []*jsonschema.ValidationError {
var errs []*jsonschema.ValidationError
var walk func(*jsonschema.ValidationError)
walk = func(err *jsonschema.ValidationError) {
switch err.ErrorKind.(type) {
case *kind.Schema, *kind.Group, *kind.Reference:
for _, cause := range err.Causes {
walk(cause)
}
default:
errs = append(errs, err)
}
}
walk(err)
return errs
}
func isTypeError(err *jsonschema.ValidationError) bool {
_, ok := err.ErrorKind.(*kind.Type)
return ok
}
func specificity(err *jsonschema.ValidationError) int {
return len(err.InstanceLocation)
}
+16 -3
View File
@@ -4,6 +4,7 @@
package schema
import (
"encoding/json"
"fmt"
"testing"
@@ -114,6 +115,18 @@ func TestValidatePorts(t *testing.T) {
ports: []string{"8006-8000:8005-8000"},
hasError: true,
},
{
ports: []any{json.Number("8080")},
hasError: false,
},
{
ports: []any{json.Number("65536")},
hasError: true,
},
{
ports: []any{json.Number("1.5")},
hasError: true,
},
}
for _, tc := range testcases {
@@ -127,7 +140,7 @@ func TestValidatePorts(t *testing.T) {
},
}
if tc.hasError {
assert.ErrorContains(t, Validate(config, "3"), "services.foo.ports.0 Does not match format 'ports'")
assert.ErrorContains(t, Validate(config, "3"), "is not valid ports")
} else {
assert.NilError(t, Validate(config, "3"))
}
@@ -146,7 +159,7 @@ func TestValidateUndefinedTopLevelOption(t *testing.T) {
}
err := Validate(config, "3.0")
assert.ErrorContains(t, err, "Additional property helicopters is not allowed")
assert.ErrorContains(t, err, "additional property 'helicopters' is not allowed")
}
func TestValidateAllowsXTopLevelFields(t *testing.T) {
@@ -230,7 +243,7 @@ func TestValidateCredentialSpecs(t *testing.T) {
}
err := Validate(config, tc.version)
if tc.expectedErr != "" {
assert.ErrorContains(t, err, fmt.Sprintf("Additional property %s is not allowed", tc.expectedErr))
assert.ErrorContains(t, err, fmt.Sprintf("additional property '%s' is not allowed", tc.expectedErr))
} else {
assert.NilError(t, err)
}
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.5
ARG GO_VERSION=1.26.7
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.5
ARG GO_VERSION=1.26.7
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.5
ARG GO_VERSION=1.26.7
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+28 -30
View File
@@ -15,13 +15,13 @@ require (
dario.cat/mergo v1.0.2
github.com/containerd/errdefs v1.0.0
github.com/containerd/log v0.1.0
github.com/containerd/platforms v1.0.0-rc.4
github.com/containerd/platforms v1.0.0-rc.5
github.com/creack/pty v1.1.24
github.com/distribution/reference v0.6.0
github.com/docker/cli-docs-tool v0.11.0
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker-credential-helpers v0.9.8
github.com/docker/go-connections v0.8.0
github.com/docker/go-connections v0.8.1
github.com/docker/go-units v0.5.0
github.com/fvbommel/sortorder v1.1.0
github.com/go-jose/go-jose/v4 v4.1.4
@@ -30,8 +30,8 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/mattn/go-runewidth v0.0.24
github.com/moby/go-archive v0.3.0
github.com/mattn/go-runewidth v0.0.28
github.com/moby/go-archive v0.3.3
github.com/moby/moby/api v1.55.0
github.com/moby/moby/client v0.5.1
github.com/moby/patternmatcher v0.6.1
@@ -46,24 +46,24 @@ require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/sirupsen/logrus v1.9.4
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
github.com/sirupsen/logrus v1.10.2
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
github.com/xeipuuv/gojsonschema v1.2.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/metric v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0
go.opentelemetry.io/otel v1.46.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0
go.opentelemetry.io/otel/metric v1.46.0
go.opentelemetry.io/otel/sdk v1.46.0
go.opentelemetry.io/otel/sdk/metric v1.46.0
go.opentelemetry.io/otel/trace v1.46.0
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0
golang.org/x/text v0.40.0
golang.org/x/text v0.41.0
gotest.tools/v3 v3.5.2
tags.cncf.io/container-device-interface v1.1.0
)
@@ -78,36 +78,34 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-metrics v0.1.0 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/user v0.4.1 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/sys/userns v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
go.etcd.io/raft/v3 v3.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.57.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.12 // indirect
)
+66 -123
View File
@@ -6,10 +6,6 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
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/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -24,91 +20,74 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X
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/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=
github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU=
github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
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-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE=
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q=
github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.8.0 h1:T9UlP76qPLA/HaLrcC+s4Doqqv5XsWMMUGPF5Aih/k0=
github.com/docker/go-connections v0.8.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4 h1:Bj+mzWc7MJqqD0UzTaPmwszW3ttOVjSFi84ZU5l+2I0=
github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw=
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/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
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=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
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.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
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/go-archive v0.3.0 h1:nos4BtzzUIqB406BgQnWGMI4qib9BZ8XUHU+ucv/n1c=
github.com/moby/go-archive v0.3.0/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
@@ -121,6 +100,10 @@ github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
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.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
@@ -129,132 +112,96 @@ github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrN
github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/sys/userns v0.2.0 h1:nEtDtp7NCV/6dutSklNe8FrENPwFdc4mXnZqC/JWgXM=
github.com/moby/sys/userns v0.2.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
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.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
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.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
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/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/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
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=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346 h1:TvtdmeYsYEij78hS4oxnwikoiLdIrgav3BA+CbhaDAI=
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346/go.mod h1:xKQhd7snlzKFuUi1taTGWjpRE8iFTA06DeacYi3CVFQ=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
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.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY=
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU=
go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4=
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -264,8 +211,8 @@ golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -278,19 +225,15 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 h1:izFU9hz7aeLI/Mi1J0991ae+xcwRLr7hTqWnB/9aIIU=
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5/go.mod h1:3LhxRw4YYkf+ylAfgaY9JlVLFKhokkCV8duhLLe7+t0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
+16 -9
View File
@@ -87,19 +87,26 @@ func checkWindowsHostAndContainerCompat(host, ctr windowsOSVersion) bool {
return host.Build == ctr.Build
}
// Find the latest LTSC version that is earlier than the host version.
// This is the earliest version of container that the host can run.
// Find the floor of the compatible container range. Per the Windows stable
// ABI policy, every host from LTSC N up to (but not including) LTSC N+1 can
// run containers from LTSC N-1 up to the host build.
//
// If the host version is an LTSC, then it supports compatibility with
// everything from the previous LTSC up to itself, so we want supportedLTSCRelease
// to be the previous entry.
// So we find the largest LTSC <= host.Build, then step one entry back to
// get the floor. If host.Build is past the latest LTSC in the list
// (e.g. a 26200 host, which is in the WS2025 generation), the floor is
// still the previous LTSC (20348), not the latest LTSC itself.
//
// If no match is found, then we know that the host is LTSC2022 exactly,
// since we already checked that it's not less than LTSC2022.
// If host is the very first LTSC (or no entry matches, which is impossible
// here since we already checked host.Build >= ltsc2022), use that LTSC as
// the floor.
var supportedLTSCRelease uint16 = ltsc2022
for i := len(compatLTSCReleases) - 1; i >= 0; i-- {
if host.Build > compatLTSCReleases[i] {
supportedLTSCRelease = compatLTSCReleases[i]
if host.Build >= compatLTSCReleases[i] {
if i == 0 {
supportedLTSCRelease = compatLTSCReleases[i]
} else {
supportedLTSCRelease = compatLTSCReleases[i-1]
}
break
}
}
@@ -1,4 +1,4 @@
//go:build !linux && !windows
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
package sockets
+9
View File
@@ -0,0 +1,9 @@
//go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !windows
package sockets
import "syscall"
func maxListenerBacklog() int {
return syscall.SOMAXCONN
}
-425
View File
@@ -1,425 +0,0 @@
Attribution-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More_considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-ShareAlike 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-ShareAlike 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
l. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
m. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public licenses.
Notwithstanding, Creative Commons may elect to apply one of its public
licenses to material it publishes and in those instances will be
considered the "Licensor." Except for the limited purpose of indicating
that material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the public
licenses.
Creative Commons may be contacted at creativecommons.org.
+42 -32
View File
@@ -1,59 +1,71 @@
# go-metrics [![GoDoc](https://godoc.org/github.com/docker/go-metrics?status.svg)](https://godoc.org/github.com/docker/go-metrics) ![Badge Badge](http://doyouevenbadge.com/github.com/docker/go-metrics)
# go-metrics
[![Go Reference](https://pkg.go.dev/badge/github.com/docker/go-metrics.svg)](https://pkg.go.dev/github.com/docker/go-metrics)
This package is small wrapper around the prometheus go client to help enforce convention and best practices for metrics collection in Docker projects.
## Best Practices
This packages is meant to be used for collecting metrics in Docker projects.
It is not meant to be used as a replacement for the prometheus client but to help enforce consistent naming across metrics collected.
If you have not already read the prometheus best practices around naming and labels you can read the page [here](https://prometheus.io/docs/practices/naming/).
This packages is meant to be used for collecting metrics. It is not meant to be
used as a replacement for the prometheus client but to help enforce consistent
naming across metrics collected. If you have not already read the prometheus
best practices around naming and labels you can read the naming section in the
[prometheus documentation](https://prometheus.io/docs/practices/naming/).
The following are a few Docker specific rules that will help you name and work with metrics in your project.
The following are a few conventions that help naming metrics in your project.
1. Namespace and Subsystem
### Namespace and Subsystem
This package provides you with a namespace type that allows you to specify the same namespace and subsystem for your metrics.
This package provides you with a namespace type that allows you to specify the
same namespace and subsystem for your metrics.
```go
ns := metrics.NewNamespace("engine", "daemon", metrics.Labels{
"version": dockerversion.Version,
"commit": dockerversion.GitCommit,
"version": daemon.Version,
"commit": daemon.GitCommit,
})
```
In the example above we are creating metrics for the Docker engine's daemon package.
`engine` would be the namespace in this example where `daemon` is the subsystem or package where we are collecting the metrics.
In the example above we are creating metrics for the daemon package.
`engine` would be the namespace in this example where `daemon` is the subsystem
or package where we are collecting the metrics.
A namespace also allows you to attach constant labels to the metrics such as the git commit and version that it is collecting.
A namespace also allows you to attach constant labels to the metrics such as
the git commit and version that it is collecting.
2. Declaring your Metrics
### Declaring your Metrics
Try to keep all your metric declarations in one file.
This makes it easy for others to see what constant labels are defined on the namespace and what labels are defined on the metrics when they are created.
Try to keep all your metric declarations in one file. This makes it easy for
others to see what constant labels are defined on the namespace and what
labels are defined on the metrics when they are created.
3. Use labels instead of multiple metrics
### Use labels instead of multiple metrics
Labels allow you to define one metric such as the time it takes to perform a certain action on an object.
If we wanted to collect timings on various container actions such as create, start, and delete then we can define one metric called `container_actions` and use labels to specify the type of action.
Labels allow you to define one metric such as the time it takes to perform a
certain action on an object. If we wanted to collect timings on various container
actions such as create, start, and delete then we can define one metric called
`container_actions` and use labels to specify the type of action.
```go
containerActions = ns.NewLabeledTimer("container_actions", "The number of milliseconds it takes to process each container action", "action")
```
The last parameter is the label name or key.
When adding a data point to the metric you will use the `WithValues` function to specify the `action` that you are collecting for.
The last parameter is the label name or key. When adding a data point to the
metric you can use the `WithValues` function to specify the `action` that you
are collecting for.
```go
containerActions.WithValues("create").UpdateSince(start)
```
4. Always use a unit
### Always use a unit
The metric name should describe what you are measuring but you also need to provide the unit that it is being measured with.
For a timer, the standard unit is seconds and a counter's standard unit is a total.
For gauges you must provide the unit.
This package provides a standard set of units for use within the Docker projects.
The metric name should describe what you are measuring but you also need to
provide the unit that it is being measured with. For a timer, the standard
unit is seconds and a counter's standard unit is a total. For gauges you must
provide the unit. This package provides a standard set of units for use within
your projects.
```go
Nanoseconds Unit = "nanoseconds"
@@ -62,18 +74,16 @@ Bytes Unit = "bytes"
Total Unit = "total"
```
If you need to use a unit but it is not defined in the package please open a PR to add it but first try to see if one of the already created units will work for your metric, i.e. seconds or nanoseconds vs adding milliseconds.
## Docs
Package documentation can be found [here](https://godoc.org/github.com/docker/go-metrics).
If you need to use a unit but it is not defined in the package please open
a PR to add it but first try to see if one of the existing units work for
your metric, i.e., seconds or nanoseconds vs adding milliseconds.
## HTTP Metrics
To instrument a http handler, you can wrap the code like this:
```go
namespace := metrics.NewNamespace("docker_distribution", "http", metrics.Labels{"handler": "your_http_handler_name"})
namespace := metrics.NewNamespace("api_server", "http", metrics.Labels{"handler": "your_http_handler_name"})
httpMetrics := namespace.NewDefaultHttpMetrics()
metrics.Register(namespace)
instrumentedHandler = metrics.InstrumentHandler(httpMetrics, unInstrumentedHandler)
@@ -88,4 +98,4 @@ If you need a custom metrics and it is generic enough to be used by multiple pro
## Copyright and license
Copyright © 2016 Docker, Inc. All rights reserved, except as follows. Code is released under the Apache 2.0 license. The README.md file, and files in the "docs" folder are licensed under the Creative Commons Attribution 4.0 International License under the terms and conditions set forth in the file "LICENSE.docs". You may obtain a duplicate copy of the same license, titled CC-BY-SA-4.0, at http://creativecommons.org/licenses/by/4.0/.
Copyright © 2016 Docker, Inc. All rights reserved, except as follows. Code is released under the Apache 2.0 license.
+4
View File
@@ -23,10 +23,12 @@ func (lc *labeledCounter) WithValues(vs ...string) Counter {
return &counter{pc: lc.pc.WithLabelValues(vs...)}
}
// Describe implements [prometheus.Collector].
func (lc *labeledCounter) Describe(ch chan<- *prometheus.Desc) {
lc.pc.Describe(ch)
}
// Collect implements [prometheus.Collector].
func (lc *labeledCounter) Collect(ch chan<- prometheus.Metric) {
lc.pc.Collect(ch)
}
@@ -43,10 +45,12 @@ func (c *counter) Inc(vs ...float64) {
c.pc.Add(sumFloat64(vs...))
}
// Describe implements [prometheus.Collector].
func (c *counter) Describe(ch chan<- *prometheus.Desc) {
c.pc.Describe(ch)
}
// Collect implements [prometheus.Collector].
func (c *counter) Collect(ch chan<- prometheus.Metric) {
c.pc.Collect(ch)
}
+2 -1
View File
@@ -1,3 +1,4 @@
// This package is small wrapper around the prometheus go client to help enforce convention and best practices for metrics collection in Docker projects.
// Package metrics is small wrapper around the prometheus go client to help
// enforce convention and best practices for metrics collection.
package metrics
+4
View File
@@ -27,10 +27,12 @@ func (lg *labeledGauge) WithValues(labels ...string) Gauge {
return &gauge{pg: lg.pg.WithLabelValues(labels...)}
}
// Describe implements [prometheus.Collector].
func (lg *labeledGauge) Describe(c chan<- *prometheus.Desc) {
lg.pg.Describe(c)
}
// Collect implements [prometheus.Collector].
func (lg *labeledGauge) Collect(c chan<- prometheus.Metric) {
lg.pg.Collect(c)
}
@@ -63,10 +65,12 @@ func (g *gauge) Set(v float64) {
g.pg.Set(v)
}
// Describe implements [prometheus.Collector].
func (g *gauge) Describe(c chan<- *prometheus.Desc) {
g.pg.Describe(c)
}
// Collect implements [prometheus.Collector].
func (g *gauge) Collect(c chan<- prometheus.Metric) {
g.pg.Collect(c)
}
+33 -39
View File
@@ -7,68 +7,62 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// HTTPHandlerOpts describes a set of configurable options of http metrics
// HTTPHandlerOpts configures HTTP instrumentation metrics.
type HTTPHandlerOpts struct {
DurationBuckets []float64
RequestSizeBuckets []float64
ResponseSizeBuckets []float64
}
const (
InstrumentHandlerResponseSize = iota
InstrumentHandlerRequestSize
InstrumentHandlerDuration
InstrumentHandlerCounter
InstrumentHandlerInFlight
)
// HTTPMetric describes a metric used to instrument an HTTP handler.
//
// HTTPMetric values must be created using the HTTP metric methods on
// [Namespace], such as [Namespace.NewDefaultHttpMetrics] or
// [Namespace.NewHttpMetricsWithOpts].
type HTTPMetric struct {
prometheus.Collector
handlerType int
collector prometheus.Collector
wrap func(http.Handler) http.Handler
}
var _ prometheus.Collector = (*HTTPMetric)(nil)
// Describe implements [prometheus.Collector].
func (m *HTTPMetric) Describe(ch chan<- *prometheus.Desc) {
m.collector.Describe(ch)
}
// Collect implements [prometheus.Collector].
func (m *HTTPMetric) Collect(ch chan<- prometheus.Metric) {
m.collector.Collect(ch)
}
var (
defaultDurationBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 60}
defaultRequestSizeBuckets = prometheus.ExponentialBuckets(1024, 2, 22) //1K to 4G
defaultRequestSizeBuckets = prometheus.ExponentialBuckets(1024, 2, 22) // 1K to 4G
defaultResponseSizeBuckets = defaultRequestSizeBuckets
)
// Handler returns the global http.Handler that provides the prometheus
// metrics format on GET requests. This handler is no longer instrumented.
// Handler is a convenience wrapper around [promhttp.Handler] that returns an
// HTTP handler serving metrics from the default Prometheus gatherer.
func Handler() http.Handler {
return promhttp.Handler()
}
// InstrumentHandler returns an HTTP handler function that instruments handler
// with the provided HTTP metrics.
func InstrumentHandler(metrics []*HTTPMetric, handler http.Handler) http.HandlerFunc {
return InstrumentHandlerFunc(metrics, handler.ServeHTTP)
return instrumentHandler(metrics, handler)
}
// InstrumentHandlerFunc returns an HTTP handler function that instruments
// handlerFunc with the provided HTTP metrics.
func InstrumentHandlerFunc(metrics []*HTTPMetric, handlerFunc http.HandlerFunc) http.HandlerFunc {
var handler http.Handler
handler = http.HandlerFunc(handlerFunc)
return instrumentHandler(metrics, handlerFunc)
}
func instrumentHandler(metrics []*HTTPMetric, handler http.Handler) http.HandlerFunc {
for _, metric := range metrics {
switch metric.handlerType {
case InstrumentHandlerResponseSize:
if collector, ok := metric.Collector.(prometheus.ObserverVec); ok {
handler = promhttp.InstrumentHandlerResponseSize(collector, handler)
}
case InstrumentHandlerRequestSize:
if collector, ok := metric.Collector.(prometheus.ObserverVec); ok {
handler = promhttp.InstrumentHandlerRequestSize(collector, handler)
}
case InstrumentHandlerDuration:
if collector, ok := metric.Collector.(prometheus.ObserverVec); ok {
handler = promhttp.InstrumentHandlerDuration(collector, handler)
}
case InstrumentHandlerCounter:
if collector, ok := metric.Collector.(*prometheus.CounterVec); ok {
handler = promhttp.InstrumentHandlerCounter(collector, handler)
}
case InstrumentHandlerInFlight:
if collector, ok := metric.Collector.(prometheus.Gauge); ok {
handler = promhttp.InstrumentHandlerInFlight(collector, handler)
}
}
handler = metric.wrap(handler)
}
return handler.ServeHTTP
}
+129 -85
View File
@@ -1,27 +1,25 @@
package metrics
import (
"fmt"
"maps"
"net/http"
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
type Labels map[string]string
// Labels represents a collection of metric label names and values.
type Labels = prometheus.Labels
// NewNamespace returns a namespaces that is responsible for managing a collection of
// metrics for a particual namespace and subsystem
//
// labels allows const labels to be added to all metrics created in this namespace
// and are commonly used for data like application version and git commit
// NewNamespace returns a namespace for metrics that share the given namespace
// and subsystem. labels are added as constant labels to metrics created by the
// namespace.
func NewNamespace(name, subsystem string, labels Labels) *Namespace {
if labels == nil {
labels = make(map[string]string)
}
return &Namespace{
name: name,
subsystem: subsystem,
labels: labels,
labels: maps.Clone(labels),
}
}
@@ -29,7 +27,7 @@ func NewNamespace(name, subsystem string, labels Labels) *Namespace {
type Namespace struct {
name string
subsystem string
labels Labels
labels prometheus.Labels
mu sync.Mutex
metrics []prometheus.Collector
}
@@ -37,8 +35,8 @@ type Namespace struct {
// WithConstLabels returns a namespace with the provided set of labels merged
// with the existing constant labels on the namespace.
//
// Only metrics created with the returned namespace will get the new constant
// labels. The returned namespace must be registered separately.
// Only metrics created with the returned namespace will get the new constant
// labels. The returned namespace must be registered separately.
func (n *Namespace) WithConstLabels(labels Labels) *Namespace {
n.mu.Lock()
ns := &Namespace{
@@ -50,12 +48,15 @@ func (n *Namespace) WithConstLabels(labels Labels) *Namespace {
return ns
}
// NewCounter creates and adds a counter to the namespace.
func (n *Namespace) NewCounter(name, help string) Counter {
c := &counter{pc: prometheus.NewCounter(n.newCounterOpts(name, help))}
n.Add(c)
return c
}
// NewLabeledCounter creates and adds a counter with the given variable labels
// to the namespace.
func (n *Namespace) NewLabeledCounter(name, help string, labels ...string) LabeledCounter {
c := &labeledCounter{pc: prometheus.NewCounterVec(n.newCounterOpts(name, help), labels)}
n.Add(c)
@@ -68,36 +69,64 @@ func (n *Namespace) newCounterOpts(name, help string) prometheus.CounterOpts {
Subsystem: n.subsystem,
Name: makeName(name, Total),
Help: help,
ConstLabels: prometheus.Labels(n.labels),
ConstLabels: n.labels,
}
}
// NewTimer creates and adds a timer to the namespace.
func (n *Namespace) NewTimer(name, help string) Timer {
t := &timer{
m: prometheus.NewHistogram(n.newTimerOpts(name, help)),
m: prometheus.NewHistogram(n.newTimerOpts(name, help, []float64{})),
}
n.Add(t)
return t
}
// NewTimerWithBuckets creates and adds a timer with the given histogram buckets
// to the namespace.
func (n *Namespace) NewTimerWithBuckets(name, help string, buckets []float64) Timer {
t := &timer{
m: prometheus.NewHistogram(n.newTimerOpts(name, help, buckets)),
}
n.Add(t)
return t
}
// NewLabeledTimer creates and adds a timer with the given variable labels to
// the namespace.
func (n *Namespace) NewLabeledTimer(name, help string, labels ...string) LabeledTimer {
t := &labeledTimer{
m: prometheus.NewHistogramVec(n.newTimerOpts(name, help), labels),
m: prometheus.NewHistogramVec(n.newTimerOpts(name, help, []float64{}), labels),
}
n.Add(t)
return t
}
func (n *Namespace) newTimerOpts(name, help string) prometheus.HistogramOpts {
return prometheus.HistogramOpts{
// NewLabeledTimerWithBuckets creates and adds a timer with the given histogram
// buckets and variable labels to the namespace.
func (n *Namespace) NewLabeledTimerWithBuckets(name, help string, buckets []float64, labels ...string) LabeledTimer {
t := &labeledTimer{
m: prometheus.NewHistogramVec(n.newTimerOpts(name, help, buckets), labels),
}
n.Add(t)
return t
}
func (n *Namespace) newTimerOpts(name, help string, buckets []float64) prometheus.HistogramOpts {
opts := prometheus.HistogramOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: makeName(name, Seconds),
Help: help,
ConstLabels: prometheus.Labels(n.labels),
ConstLabels: n.labels,
}
if len(buckets) > 0 {
opts.Buckets = buckets
}
return opts
}
// NewGauge creates and adds a gauge with the given unit to the namespace.
func (n *Namespace) NewGauge(name, help string, unit Unit) Gauge {
g := &gauge{
pg: prometheus.NewGauge(n.newGaugeOpts(name, help, unit)),
@@ -106,6 +135,8 @@ func (n *Namespace) NewGauge(name, help string, unit Unit) Gauge {
return g
}
// NewLabeledGauge creates and adds a gauge with the given unit and variable
// labels to the namespace.
func (n *Namespace) NewLabeledGauge(name, help string, unit Unit, labels ...string) LabeledGauge {
g := &labeledGauge{
pg: prometheus.NewGaugeVec(n.newGaugeOpts(name, help, unit), labels),
@@ -120,10 +151,13 @@ func (n *Namespace) newGaugeOpts(name, help string, unit Unit) prometheus.GaugeO
Subsystem: n.subsystem,
Name: makeName(name, unit),
Help: help,
ConstLabels: prometheus.Labels(n.labels),
ConstLabels: n.labels,
}
}
// Describe sends the descriptions of all metrics in the namespace to ch.
//
// Describe implements [prometheus.Collector].
func (n *Namespace) Describe(ch chan<- *prometheus.Desc) {
n.mu.Lock()
defer n.mu.Unlock()
@@ -133,6 +167,9 @@ func (n *Namespace) Describe(ch chan<- *prometheus.Desc) {
}
}
// Collect sends all metrics in the namespace to ch.
//
// Collect implements [prometheus.Collector].
func (n *Namespace) Collect(ch chan<- prometheus.Metric) {
n.mu.Lock()
defer n.mu.Unlock()
@@ -142,33 +179,44 @@ func (n *Namespace) Collect(ch chan<- prometheus.Metric) {
}
}
// Add adds collector to the namespace.
func (n *Namespace) Add(collector prometheus.Collector) {
n.mu.Lock()
n.metrics = append(n.metrics, collector)
n.mu.Unlock()
}
// NewDesc returns a Prometheus metric descriptor using the namespace,
// subsystem, constant labels, and given variable labels.
func (n *Namespace) NewDesc(name, help string, unit Unit, labels ...string) *prometheus.Desc {
name = makeName(name, unit)
namespace := n.name
if n.subsystem != "" {
namespace = fmt.Sprintf("%s_%s", namespace, n.subsystem)
namespace += "_" + n.subsystem
}
name = fmt.Sprintf("%s_%s", namespace, name)
return prometheus.NewDesc(name, help, labels, prometheus.Labels(n.labels))
name = namespace + "_" + name
return prometheus.NewDesc(name, help, labels, n.labels)
}
// mergeLabels merges two or more labels objects into a single map, favoring
// the later labels.
func mergeLabels(lbs ...Labels) Labels {
merged := make(Labels)
for _, target := range lbs {
for k, v := range target {
merged[k] = v
}
// withHandlerLabel returns a copy of labels with the "handler" label set
// to handlerName.
func withHandlerLabel(labels prometheus.Labels, handlerName string) prometheus.Labels {
out := maps.Clone(labels)
if out == nil {
out = make(prometheus.Labels)
}
out["handler"] = handlerName
return out
}
// mergeLabels returns a new label map containing base and overrides.
// Labels in overrides take precedence over labels in base.
func mergeLabels(base prometheus.Labels, overrides Labels) prometheus.Labels {
merged := maps.Clone(base)
if merged == nil {
merged = make(prometheus.Labels, len(overrides))
}
maps.Copy(merged, overrides)
return merged
}
@@ -176,10 +224,11 @@ func makeName(name string, unit Unit) string {
if unit == "" {
return name
}
return fmt.Sprintf("%s_%s", name, unit)
return name + "_" + string(unit)
}
// NewDefaultHttpMetrics creates and adds the default set of HTTP metrics for
// handlerName using the default histogram buckets.
func (n *Namespace) NewDefaultHttpMetrics(handlerName string) []*HTTPMetric {
return n.NewHttpMetricsWithOpts(handlerName, HTTPHandlerOpts{
DurationBuckets: defaultDurationBuckets,
@@ -188,6 +237,8 @@ func (n *Namespace) NewDefaultHttpMetrics(handlerName string) []*HTTPMetric {
})
}
// NewHttpMetrics creates and adds the default set of HTTP metrics for
// handlerName using the given histogram buckets.
func (n *Namespace) NewHttpMetrics(handlerName string, durationBuckets, requestSizeBuckets, responseSizeBuckets []float64) []*HTTPMetric {
return n.NewHttpMetricsWithOpts(handlerName, HTTPHandlerOpts{
DurationBuckets: durationBuckets,
@@ -196,6 +247,8 @@ func (n *Namespace) NewHttpMetrics(handlerName string, durationBuckets, requestS
})
}
// NewHttpMetricsWithOpts creates and adds the configured set of HTTP metrics
// for handlerName.
func (n *Namespace) NewHttpMetricsWithOpts(handlerName string, opts HTTPHandlerOpts) []*HTTPMetric {
var httpMetrics []*HTTPMetric
inFlightMetric := n.NewInFlightGaugeMetric(handlerName)
@@ -207,109 +260,100 @@ func (n *Namespace) NewHttpMetricsWithOpts(handlerName string, opts HTTPHandlerO
return httpMetrics
}
func (n *Namespace) newHTTPMetric(collector prometheus.Collector, wrap func(http.Handler) http.Handler) *HTTPMetric {
n.Add(collector)
return &HTTPMetric{collector: collector, wrap: wrap}
}
// NewInFlightGaugeMetric creates and adds a metric that tracks the number of
// in-flight HTTP requests for handlerName.
func (n *Namespace) NewInFlightGaugeMetric(handlerName string) *HTTPMetric {
labels := prometheus.Labels(n.labels)
labels["handler"] = handlerName
metric := prometheus.NewGauge(prometheus.GaugeOpts{
collector := prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: "in_flight_requests",
Help: "The in-flight HTTP requests",
ConstLabels: prometheus.Labels(labels),
ConstLabels: withHandlerLabel(n.labels, handlerName),
})
return n.newHTTPMetric(collector, func(next http.Handler) http.Handler {
return promhttp.InstrumentHandlerInFlight(collector, next)
})
httpMetric := &HTTPMetric{
Collector: metric,
handlerType: InstrumentHandlerInFlight,
}
n.Add(httpMetric)
return httpMetric
}
// NewRequestTotalMetric creates and adds a metric that counts HTTP requests for
// handlerName by response code and method.
func (n *Namespace) NewRequestTotalMetric(handlerName string) *HTTPMetric {
labels := prometheus.Labels(n.labels)
labels["handler"] = handlerName
metric := prometheus.NewCounterVec(
collector := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: "requests_total",
Help: "Total number of HTTP requests made.",
ConstLabels: prometheus.Labels(labels),
ConstLabels: withHandlerLabel(n.labels, handlerName),
},
[]string{"code", "method"},
)
httpMetric := &HTTPMetric{
Collector: metric,
handlerType: InstrumentHandlerCounter,
}
n.Add(httpMetric)
return httpMetric
return n.newHTTPMetric(collector, func(next http.Handler) http.Handler {
return promhttp.InstrumentHandlerCounter(collector, next)
})
}
// NewRequestDurationMetric creates and adds a metric that records HTTP request
// durations for handlerName using the given histogram buckets.
func (n *Namespace) NewRequestDurationMetric(handlerName string, buckets []float64) *HTTPMetric {
if len(buckets) == 0 {
panic("DurationBuckets must be provided")
}
labels := prometheus.Labels(n.labels)
labels["handler"] = handlerName
opts := prometheus.HistogramOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: "request_duration_seconds",
Help: "The HTTP request latencies in seconds.",
Buckets: buckets,
ConstLabels: prometheus.Labels(labels),
ConstLabels: withHandlerLabel(n.labels, handlerName),
}
metric := prometheus.NewHistogramVec(opts, []string{"method"})
httpMetric := &HTTPMetric{
Collector: metric,
handlerType: InstrumentHandlerDuration,
}
n.Add(httpMetric)
return httpMetric
collector := prometheus.NewHistogramVec(opts, []string{"method"})
return n.newHTTPMetric(collector, func(next http.Handler) http.Handler {
return promhttp.InstrumentHandlerDuration(collector, next)
})
}
// NewRequestSizeMetric creates and adds a metric that records HTTP request
// sizes for handlerName using the given histogram buckets.
func (n *Namespace) NewRequestSizeMetric(handlerName string, buckets []float64) *HTTPMetric {
if len(buckets) == 0 {
panic("RequestSizeBuckets must be provided")
}
labels := prometheus.Labels(n.labels)
labels["handler"] = handlerName
opts := prometheus.HistogramOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: "request_size_bytes",
Help: "The HTTP request sizes in bytes.",
Buckets: buckets,
ConstLabels: prometheus.Labels(labels),
ConstLabels: withHandlerLabel(n.labels, handlerName),
}
metric := prometheus.NewHistogramVec(opts, []string{})
httpMetric := &HTTPMetric{
Collector: metric,
handlerType: InstrumentHandlerRequestSize,
}
n.Add(httpMetric)
return httpMetric
collector := prometheus.NewHistogramVec(opts, []string{})
return n.newHTTPMetric(collector, func(next http.Handler) http.Handler {
return promhttp.InstrumentHandlerRequestSize(collector, next)
})
}
// NewResponseSizeMetric creates and adds a metric that records HTTP response
// sizes for handlerName using the given histogram buckets.
func (n *Namespace) NewResponseSizeMetric(handlerName string, buckets []float64) *HTTPMetric {
if len(buckets) == 0 {
panic("ResponseSizeBuckets must be provided")
}
labels := prometheus.Labels(n.labels)
labels["handler"] = handlerName
opts := prometheus.HistogramOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: "response_size_bytes",
Help: "The HTTP response sizes in bytes.",
Buckets: buckets,
ConstLabels: prometheus.Labels(labels),
ConstLabels: withHandlerLabel(n.labels, handlerName),
}
metrics := prometheus.NewHistogramVec(opts, []string{})
httpMetric := &HTTPMetric{
Collector: metrics,
handlerType: InstrumentHandlerResponseSize,
}
n.Add(httpMetric)
return httpMetric
collector := prometheus.NewHistogramVec(opts, []string{})
return n.newHTTPMetric(collector, func(next http.Handler) http.Handler {
return promhttp.InstrumentHandlerResponseSize(collector, next)
})
}
+4 -4
View File
@@ -2,14 +2,14 @@ package metrics
import "github.com/prometheus/client_golang/prometheus"
// Register adds all the metrics in the provided namespace to the global
// metrics registry
// Register is a convenience wrapper around [prometheus.MustRegister] that
// registers all metrics in n with the default Prometheus registry.
func Register(n *Namespace) {
prometheus.MustRegister(n)
}
// Deregister removes all the metrics in the provided namespace from the
// global metrics registry
// Deregister is a convenience wrapper around [prometheus.Unregister] that
// unregisters all metrics in n from the default Prometheus registry.
func Deregister(n *Namespace) {
prometheus.Unregister(n)
}
+4
View File
@@ -51,10 +51,12 @@ func (lt *labeledTimer) WithValues(labels ...string) *labeledTimerObserver {
return &labeledTimerObserver{m: lt.m.WithLabelValues(labels...)}
}
// Describe implements [prometheus.Collector].
func (lt *labeledTimer) Describe(c chan<- *prometheus.Desc) {
lt.m.Describe(c)
}
// Collect implements [prometheus.Collector].
func (lt *labeledTimer) Collect(c chan<- prometheus.Metric) {
lt.m.Collect(c)
}
@@ -71,10 +73,12 @@ func (t *timer) UpdateSince(since time.Time) {
t.m.Observe(time.Since(since).Seconds())
}
// Describe implements [prometheus.Collector].
func (t *timer) Describe(c chan<- *prometheus.Desc) {
c <- t.m.(prometheus.Metric).Desc()
}
// Collect implements [prometheus.Collector].
func (t *timer) Collect(c chan<- prometheus.Metric) {
// Are there any observers that don't implement Collector? It is really
// unclear what the point of the upstream change was, but we'll let this
+36 -1
View File
@@ -69,6 +69,16 @@ func (m *Metrics) CaptureMetrics(w http.ResponseWriter, fn func(http.ResponseWri
}
},
WriteString: func(next WriteStringFunc) WriteStringFunc {
return func(s string) (int, error) {
n, err := next(s)
m.Written += int64(n)
headerWritten = true
return n, err
}
},
ReadFrom: func(next ReadFromFunc) ReadFromFunc {
return func(src io.Reader) (int64, error) {
n, err := next(src)
@@ -81,6 +91,31 @@ func (m *Metrics) CaptureMetrics(w http.ResponseWriter, fn func(http.ResponseWri
}
)
// defer to ensure duration is updated even if the handler panics
defer func() {
m.Duration += time.Since(start)
}()
fn(Wrap(w, hooks))
m.Duration += time.Since(start)
}
// deadliner defines two methods introduced in go 1.20. The standard library
// seems not to provide an interface we can import, hence its definition here.
type deadliner interface {
SetReadDeadline(deadline time.Time) error
SetWriteDeadline(deadline time.Time) error
}
// fullDuplexEnabler defines a method introduced in go 1.21. The standard
// library seems not to provide an interface we can import, hence its definition
// here.
type fullDuplexEnabler interface {
EnableFullDuplex() error
}
// httpFlushError defines a method introduced in go 1.20. The standard
// library seems not to provide an interface we can import, hence its definition
// here.
// See https://github.com/golang/go/blob/go1.20/src/net/http/responsecontroller.go#L50
type httpFlushError interface {
FlushError() error
}
File diff suppressed because it is too large Load Diff
-436
View File
@@ -1,436 +0,0 @@
// +build go1.8
// Code generated by "httpsnoop/codegen"; DO NOT EDIT.
package httpsnoop
import (
"bufio"
"io"
"net"
"net/http"
)
// HeaderFunc is part of the http.ResponseWriter interface.
type HeaderFunc func() http.Header
// WriteHeaderFunc is part of the http.ResponseWriter interface.
type WriteHeaderFunc func(code int)
// WriteFunc is part of the http.ResponseWriter interface.
type WriteFunc func(b []byte) (int, error)
// FlushFunc is part of the http.Flusher interface.
type FlushFunc func()
// CloseNotifyFunc is part of the http.CloseNotifier interface.
type CloseNotifyFunc func() <-chan bool
// HijackFunc is part of the http.Hijacker interface.
type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
// ReadFromFunc is part of the io.ReaderFrom interface.
type ReadFromFunc func(src io.Reader) (int64, error)
// PushFunc is part of the http.Pusher interface.
type PushFunc func(target string, opts *http.PushOptions) error
// Hooks defines a set of method interceptors for methods included in
// http.ResponseWriter as well as some others. You can think of them as
// middleware for the function calls they target. See Wrap for more details.
type Hooks struct {
Header func(HeaderFunc) HeaderFunc
WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
Write func(WriteFunc) WriteFunc
Flush func(FlushFunc) FlushFunc
CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
Hijack func(HijackFunc) HijackFunc
ReadFrom func(ReadFromFunc) ReadFromFunc
Push func(PushFunc) PushFunc
}
// Wrap returns a wrapped version of w that provides the exact same interface
// as w. Specifically if w implements any combination of:
//
// - http.Flusher
// - http.CloseNotifier
// - http.Hijacker
// - io.ReaderFrom
// - http.Pusher
//
// The wrapped version will implement the exact same combination. If no hooks
// are set, the wrapped version also behaves exactly as w. Hooks targeting
// methods not supported by w are ignored. Any other hooks will intercept the
// method they target and may modify the call's arguments and/or return values.
// The CaptureMetrics implementation serves as a working example for how the
// hooks can be used.
func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
rw := &rw{w: w, h: hooks}
_, i0 := w.(http.Flusher)
_, i1 := w.(http.CloseNotifier)
_, i2 := w.(http.Hijacker)
_, i3 := w.(io.ReaderFrom)
_, i4 := w.(http.Pusher)
switch {
// combination 1/32
case !i0 && !i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
}{rw, rw}
// combination 2/32
case !i0 && !i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Pusher
}{rw, rw, rw}
// combination 3/32
case !i0 && !i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
}{rw, rw, rw}
// combination 4/32
case !i0 && !i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw}
// combination 5/32
case !i0 && !i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
}{rw, rw, rw}
// combination 6/32
case !i0 && !i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
http.Pusher
}{rw, rw, rw, rw}
// combination 7/32
case !i0 && !i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 8/32
case !i0 && !i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 9/32
case !i0 && i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
}{rw, rw, rw}
// combination 10/32
case !i0 && i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Pusher
}{rw, rw, rw, rw}
// combination 11/32
case !i0 && i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 12/32
case !i0 && i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 13/32
case !i0 && i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw}
// combination 14/32
case !i0 && i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 15/32
case !i0 && i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 16/32
case !i0 && i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 17/32
case i0 && !i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
}{rw, rw, rw}
// combination 18/32
case i0 && !i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Pusher
}{rw, rw, rw, rw}
// combination 19/32
case i0 && !i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 20/32
case i0 && !i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 21/32
case i0 && !i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
}{rw, rw, rw, rw}
// combination 22/32
case i0 && !i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 23/32
case i0 && !i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 24/32
case i0 && !i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 25/32
case i0 && i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
}{rw, rw, rw, rw}
// combination 26/32
case i0 && i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 27/32
case i0 && i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 28/32
case i0 && i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 29/32
case i0 && i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw, rw}
// combination 30/32
case i0 && i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 31/32
case i0 && i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw, rw}
// combination 32/32
case i0 && i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw, rw}
}
panic("unreachable")
}
type rw struct {
w http.ResponseWriter
h Hooks
}
func (w *rw) Unwrap() http.ResponseWriter {
return w.w
}
func (w *rw) Header() http.Header {
f := w.w.(http.ResponseWriter).Header
if w.h.Header != nil {
f = w.h.Header(f)
}
return f()
}
func (w *rw) WriteHeader(code int) {
f := w.w.(http.ResponseWriter).WriteHeader
if w.h.WriteHeader != nil {
f = w.h.WriteHeader(f)
}
f(code)
}
func (w *rw) Write(b []byte) (int, error) {
f := w.w.(http.ResponseWriter).Write
if w.h.Write != nil {
f = w.h.Write(f)
}
return f(b)
}
func (w *rw) Flush() {
f := w.w.(http.Flusher).Flush
if w.h.Flush != nil {
f = w.h.Flush(f)
}
f()
}
func (w *rw) CloseNotify() <-chan bool {
f := w.w.(http.CloseNotifier).CloseNotify
if w.h.CloseNotify != nil {
f = w.h.CloseNotify(f)
}
return f()
}
func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
f := w.w.(http.Hijacker).Hijack
if w.h.Hijack != nil {
f = w.h.Hijack(f)
}
return f()
}
func (w *rw) ReadFrom(src io.Reader) (int64, error) {
f := w.w.(io.ReaderFrom).ReadFrom
if w.h.ReadFrom != nil {
f = w.h.ReadFrom(f)
}
return f(src)
}
func (w *rw) Push(target string, opts *http.PushOptions) error {
f := w.w.(http.Pusher).Push
if w.h.Push != nil {
f = w.h.Push(f)
}
return f(target, opts)
}
type Unwrapper interface {
Unwrap() http.ResponseWriter
}
// Unwrap returns the underlying http.ResponseWriter from within zero or more
// layers of httpsnoop wrappers.
func Unwrap(w http.ResponseWriter) http.ResponseWriter {
if rw, ok := w.(Unwrapper); ok {
// recurse until rw.Unwrap() returns a non-Unwrapper
return Unwrap(rw.Unwrap())
} else {
return w
}
}
-278
View File
@@ -1,278 +0,0 @@
// +build !go1.8
// Code generated by "httpsnoop/codegen"; DO NOT EDIT.
package httpsnoop
import (
"bufio"
"io"
"net"
"net/http"
)
// HeaderFunc is part of the http.ResponseWriter interface.
type HeaderFunc func() http.Header
// WriteHeaderFunc is part of the http.ResponseWriter interface.
type WriteHeaderFunc func(code int)
// WriteFunc is part of the http.ResponseWriter interface.
type WriteFunc func(b []byte) (int, error)
// FlushFunc is part of the http.Flusher interface.
type FlushFunc func()
// CloseNotifyFunc is part of the http.CloseNotifier interface.
type CloseNotifyFunc func() <-chan bool
// HijackFunc is part of the http.Hijacker interface.
type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
// ReadFromFunc is part of the io.ReaderFrom interface.
type ReadFromFunc func(src io.Reader) (int64, error)
// Hooks defines a set of method interceptors for methods included in
// http.ResponseWriter as well as some others. You can think of them as
// middleware for the function calls they target. See Wrap for more details.
type Hooks struct {
Header func(HeaderFunc) HeaderFunc
WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
Write func(WriteFunc) WriteFunc
Flush func(FlushFunc) FlushFunc
CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
Hijack func(HijackFunc) HijackFunc
ReadFrom func(ReadFromFunc) ReadFromFunc
}
// Wrap returns a wrapped version of w that provides the exact same interface
// as w. Specifically if w implements any combination of:
//
// - http.Flusher
// - http.CloseNotifier
// - http.Hijacker
// - io.ReaderFrom
//
// The wrapped version will implement the exact same combination. If no hooks
// are set, the wrapped version also behaves exactly as w. Hooks targeting
// methods not supported by w are ignored. Any other hooks will intercept the
// method they target and may modify the call's arguments and/or return values.
// The CaptureMetrics implementation serves as a working example for how the
// hooks can be used.
func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
rw := &rw{w: w, h: hooks}
_, i0 := w.(http.Flusher)
_, i1 := w.(http.CloseNotifier)
_, i2 := w.(http.Hijacker)
_, i3 := w.(io.ReaderFrom)
switch {
// combination 1/16
case !i0 && !i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
}{rw, rw}
// combination 2/16
case !i0 && !i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
}{rw, rw, rw}
// combination 3/16
case !i0 && !i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
}{rw, rw, rw}
// combination 4/16
case !i0 && !i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 5/16
case !i0 && i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
}{rw, rw, rw}
// combination 6/16
case !i0 && i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 7/16
case !i0 && i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw}
// combination 8/16
case !i0 && i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 9/16
case i0 && !i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
}{rw, rw, rw}
// combination 10/16
case i0 && !i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 11/16
case i0 && !i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
}{rw, rw, rw, rw}
// combination 12/16
case i0 && !i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 13/16
case i0 && i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
}{rw, rw, rw, rw}
// combination 14/16
case i0 && i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 15/16
case i0 && i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw, rw}
// combination 16/16
case i0 && i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw, rw}
}
panic("unreachable")
}
type rw struct {
w http.ResponseWriter
h Hooks
}
func (w *rw) Unwrap() http.ResponseWriter {
return w.w
}
func (w *rw) Header() http.Header {
f := w.w.(http.ResponseWriter).Header
if w.h.Header != nil {
f = w.h.Header(f)
}
return f()
}
func (w *rw) WriteHeader(code int) {
f := w.w.(http.ResponseWriter).WriteHeader
if w.h.WriteHeader != nil {
f = w.h.WriteHeader(f)
}
f(code)
}
func (w *rw) Write(b []byte) (int, error) {
f := w.w.(http.ResponseWriter).Write
if w.h.Write != nil {
f = w.h.Write(f)
}
return f(b)
}
func (w *rw) Flush() {
f := w.w.(http.Flusher).Flush
if w.h.Flush != nil {
f = w.h.Flush(f)
}
f()
}
func (w *rw) CloseNotify() <-chan bool {
f := w.w.(http.CloseNotifier).CloseNotify
if w.h.CloseNotify != nil {
f = w.h.CloseNotify(f)
}
return f()
}
func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
f := w.w.(http.Hijacker).Hijack
if w.h.Hijack != nil {
f = w.h.Hijack(f)
}
return f()
}
func (w *rw) ReadFrom(src io.Reader) (int64, error) {
f := w.w.(io.ReaderFrom).ReadFrom
if w.h.ReadFrom != nil {
f = w.h.ReadFrom(f)
}
return f(src)
}
type Unwrapper interface {
Unwrap() http.ResponseWriter
}
// Unwrap returns the underlying http.ResponseWriter from within zero or more
// layers of httpsnoop wrappers.
func Unwrap(w http.ResponseWriter) http.ResponseWriter {
if rw, ok := w.(Unwrapper); ok {
// recurse until rw.Unwrap() returns a non-Unwrapper
return Unwrap(rw.Unwrap())
} else {
return w
}
}
-1
View File
@@ -1,5 +1,4 @@
//go:build !go1.21
// +build !go1.21
/*
Copyright 2019 The logr Authors.
-1
View File
@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2019 The logr Authors.
+36 -11
View File
@@ -426,7 +426,7 @@ func (f Formatter) colon() byte {
}
func (f Formatter) pretty(value any) string {
return f.prettyWithFlags(value, 0, 0)
return f.prettyWithFlags(value, 0, 0, 0, nil)
}
const (
@@ -434,7 +434,13 @@ const (
)
// TODO: This is not fast. Most of the overhead goes here.
func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
// value: The value to render
// flags: Bitmask of flags (see above)
// depth: The current depth of nested structs, slices, arrays, and maps
// ptrDepth: The current depth of including pointer dereferences
// ptrMap: A map of pointers already seen, to avoid infinite recursion (usually
// nil unless ptrDepth is large)
func (f Formatter) prettyWithFlags(value any, flags uint32, depth int, ptrDepth int, ptrMap map[uintptr]bool) string {
if depth > f.opts.MaxLogDepth {
return `"<max-log-depth-exceeded>"`
}
@@ -504,7 +510,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
// arbitrary keys might need escaping
buf.WriteString(prettyString(k))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1))
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1, ptrDepth+1, ptrMap))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
@@ -576,7 +582,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
}
printComma = true // if we got here, we are rendering a field
if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" {
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1))
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1, ptrDepth+1, ptrMap))
continue
}
if name == "" {
@@ -585,7 +591,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
// field names can't contain characters which need escaping
buf.WriteString(f.quoted(name, false))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1))
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1, ptrDepth+1, ptrMap))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
@@ -612,7 +618,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
buf.WriteByte(f.comma())
}
e := v.Index(i)
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1))
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1, ptrDepth+1, ptrMap))
}
buf.WriteByte(']')
return buf.String()
@@ -637,7 +643,8 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
keystr = prettyString(keystr)
} else {
// prettyWithFlags will produce already-escaped values
keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1)
// key depth is unrelated to overall depth
keystr = f.prettyWithFlags(it.Key().Interface(), 0, 0, ptrDepth, ptrMap)
if t.Key().Kind() != reflect.String {
// JSON only does string keys. Unlike Go's standard JSON, we'll
// convert just about anything to a string.
@@ -646,16 +653,34 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
}
buf.WriteString(keystr)
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1))
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1, ptrDepth+1, ptrMap))
i++
}
buf.WriteByte('}')
return buf.String()
case reflect.Ptr, reflect.Interface:
case reflect.Pointer, reflect.Interface:
if v.IsNil() {
return "null"
}
return f.prettyWithFlags(v.Elem().Interface(), 0, depth)
// Special case: recursive pointers. For normal use we do not want to
// count pointer dereferences as depth, but if we see the same pointer
// again we have a recursion and need to stop. After a large number of
// pointer dereferences we will start tracking pointers to avoid the
// perf hit of doing it in the normal path.
//
// This should not happen accidentally (e.g. json decoding should never
// do this) but we can handle it gracefully.
if ptrMap != nil && ptrMap[uintptr(v.Pointer())] {
depth = f.opts.MaxLogDepth + 1 // force a depth error
}
const maxDepthFactor = 4 // arbitrary, but we want it large enough to not false-alert
if ptrDepth > f.opts.MaxLogDepth*maxDepthFactor && ptrMap == nil {
ptrMap = map[uintptr]bool{}
}
if ptrMap != nil {
ptrMap[(uintptr)(v.Pointer())] = true
}
return f.prettyWithFlags(v.Elem().Interface(), 0, depth, ptrDepth+1, ptrMap)
}
return fmt.Sprintf(`"<unhandled-%s>"`, t.Kind().String())
}
@@ -697,7 +722,7 @@ func isEmpty(v reflect.Value) bool {
return v.Float() == 0
case reflect.Complex64, reflect.Complex128:
return v.Complex() == 0
case reflect.Interface, reflect.Ptr:
case reflect.Interface, reflect.Pointer:
return v.IsNil()
}
return false
+16 -6
View File
@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
@@ -33,7 +32,7 @@ const extraSlogSinkDepth = 3 // 2 for slog, 1 for SlogSink
func (l fnlogger) Handle(_ context.Context, record slog.Record) error {
kvList := make([]any, 0, 2*record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
kvList = attrToKVs(attr, kvList)
kvList = attrToKVs(attr, kvList, l.opts.MaxLogDepth)
return true
})
@@ -49,7 +48,7 @@ func (l fnlogger) Handle(_ context.Context, record slog.Record) error {
func (l fnlogger) WithAttrs(attrs []slog.Attr) logr.SlogSink {
kvList := make([]any, 0, 2*len(attrs))
for _, attr := range attrs {
kvList = attrToKVs(attr, kvList)
kvList = attrToKVs(attr, kvList, l.opts.MaxLogDepth)
}
l.AddValues(kvList)
return &l
@@ -61,14 +60,25 @@ func (l fnlogger) WithGroup(name string) logr.SlogSink {
}
// attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups
// and other details of slog.
func attrToKVs(attr slog.Attr, kvList []any) []any {
// and other details of slog. maxDepth bounds recursion into nested groups so a
// deeply-nested slog.Group cannot exhaust the stack; it is decremented per group
// level and starts at the Formatter's MaxLogDepth (past which the formatter would
// truncate the rendering anyway).
func attrToKVs(attr slog.Attr, kvList []any, maxDepth int) []any {
attrVal := attr.Value.Resolve()
if attrVal.Kind() == slog.KindGroup {
if maxDepth <= 0 {
// Nesting is too deep to build without risking a stack overflow.
// Stop here; the formatter truncates below MaxLogDepth regardless.
if attr.Key != "" {
kvList = append(kvList, attr.Key, "<max-log-depth-exceeded>")
}
return kvList
}
groupVal := attrVal.Group()
grpKVs := make([]any, 0, 2*len(groupVal))
for _, attr := range groupVal {
grpKVs = attrToKVs(attr, grpKVs)
grpKVs = attrToKVs(attr, grpKVs, maxDepth-1)
}
if attr.Key == "" {
// slog says we have to inline these
-1
View File
@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
-1
View File
@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
-1
View File
@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
+255 -86
View File
@@ -4,6 +4,8 @@ import (
"os"
"sort"
"strings"
"sync"
"sync/atomic"
"unicode/utf8"
"github.com/clipperhouse/uax29/v2/graphemes"
@@ -18,6 +20,14 @@ var (
// StrictEmojiNeutral should be set false if handle broken fonts
StrictEmojiNeutral bool = true
// ZeroWidthJoiner is flag to set to use UTR#51 ZWJ.
//
// Deprecated: ZWJ sequences are always handled through Unicode
// grapheme cluster segmentation now, so this flag has no effect.
// It is kept only for compatibility with code written against
// v0.0.9 and earlier.
ZeroWidthJoiner bool
// DefaultCondition is a condition in current locale
DefaultCondition = &Condition{
EastAsianWidth: false,
@@ -26,20 +36,73 @@ var (
)
var (
zerowidth table // combining + nonprint merged for faster zero-width lookup
widewidth table // ambiguous + doublewidth merged for EA path
eastAsianWidth widthTable
eastAsianWidth0 [0x300]byte
zerowidth table // combining + nonprint merged for faster zero-width lookup
widewidth table // ambiguous + doublewidth merged for EA path
eastAsianWidth widthTable
tablesOnce sync.Once
// strictWidthLUT is mostly built lazily on the first width lookup so
// that importing the package costs neither the build time nor the 2 MB
// of resident memory; see issue #104. Only the entries below
// strictWidthLUTLimit are valid: init fills the first 0x300 entries of
// both planes, and the lazy build fills the rest — never rewriting the
// low region, so readers of it cannot race with the build. The limit
// is loaded with acquire semantics, which makes the non-atomic reads
// of the high region safe once it reports 0x110000. Keeping the whole
// check down to one compare-and-branch matters: RuneWidth is only a
// dozen instructions long.
strictWidthLUT [2][0x110000]byte
strictWidthLUTLimit atomic.Int32
strictWidthLUTOnce sync.Once
)
func init() {
initStrictWidthLUTLow()
strictWidthLUTLimit.Store(0x300)
handleEnv()
}
// initStrictWidthLUTLow paints the first 0x300 entries of strictWidthLUT
// from the static interval tables. The result must stay identical to
// runeWidthNoLUT for runes below 0x300, which TestStrictWidthLUT verifies.
func initStrictWidthLUTLow() {
for i := 0; i < 0x300; i++ {
r := rune(i)
w := byte(1)
if r < 0x20 || (r >= 0x7F && r <= 0x9F) || r == 0xAD { // nonprint
w = 0
}
strictWidthLUT[0][i] = w
}
ea := strictWidthLUT[1][:0x300]
fillBytes(ea, 1)
paint := func(t table, w byte) {
for _, iv := range t {
if iv.first >= 0x300 {
break
}
last := iv.last
if last > 0x2FF {
last = 0x2FF
}
fillBytes(ea[iv.first:last+1], w)
}
}
paint(ambiguous, 2)
paint(doublewidth, 2)
// zero-width wins over wide on overlap, so paint it last.
paint(combining, 0)
paint(nonprint, 0)
}
// initTables builds the merged lookup tables. It runs lazily through
// tablesOnce so that merely importing the package stays cheap; see issue
// #104.
func initTables() {
zerowidth = mergeIntervals(combining, nonprint)
widewidth = mergeIntervals(ambiguous, doublewidth)
eastAsianWidth = makeWidthTable(zerowidth, widewidth)
for r := range eastAsianWidth0 {
eastAsianWidth0[r] = byte(runeWidthEastAsian(rune(r)))
}
handleEnv()
}
func mergeIntervals(t1, t2 table) table {
@@ -189,50 +252,9 @@ func inWidthTable(r rune, t widthTable) (int, bool) {
return 0, false
}
func runeWidthEastAsian(r rune) int {
if w, ok := inWidthTable(r, eastAsianWidth); ok {
return w
}
return 1
}
var private = table{
{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
}
var nonprint = table{
{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
}
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
type Condition struct {
combinedLut []byte
EastAsianWidth bool
StrictEmojiNeutral bool
}
// NewCondition return new instance of Condition which is current locale.
func NewCondition() *Condition {
return &Condition{
EastAsianWidth: EastAsianWidth,
StrictEmojiNeutral: StrictEmojiNeutral,
}
}
// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
if r < 0 || r > 0x10FFFF {
return 0
}
if len(c.combinedLut) > 0 {
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
}
// optimized version, verified by TestRuneWidthChecksums()
if !c.EastAsianWidth {
func runeWidthNoLUT(r rune, eastAsian, strictEmojiNeutral bool) int {
tablesOnce.Do(initTables)
if !eastAsian {
if r < 0x20 {
return 0
}
@@ -253,17 +275,145 @@ func (c *Condition) RuneWidth(r rune) int {
}
if r < 0x300 {
return int(eastAsianWidth0[r])
return int(strictWidthLUT[1][r])
}
if w, ok := inWidthTable(r, eastAsianWidth); ok {
return w
}
if !c.StrictEmojiNeutral && inTable(r, emoji) {
if !strictEmojiNeutral && inTable(r, emoji) {
return 2
}
return 1
}
// fillBytes sets every byte of b to v. It doubles the copied region on each
// iteration so large slices are filled at memcpy speed instead of one byte
// per loop iteration.
func fillBytes(b []byte, v byte) {
if len(b) == 0 {
return
}
b[0] = v
for i := 1; i < len(b); i *= 2 {
copy(b[i:], b[:i])
}
}
// buildStrictWidthLUT builds the strict-width lookup table above 0x300
// exactly once. It paints whole intervals instead of computing every rune
// through the binary searches in runeWidthNoLUT. It must not write below
// 0x300: that region was filled by init and may be read concurrently. The
// result must stay identical to runeWidthNoLUT(r, eastAsian, true), which
// TestStrictWidthLUT verifies.
func buildStrictWidthLUT() {
strictWidthLUTOnce.Do(func() {
tablesOnce.Do(initTables)
// paintHigh fills lut with w over each interval, clipped to 0x300+.
paintHigh := func(lut []byte, first, last rune, w byte) {
if first < 0x300 {
if last < 0x300 {
return
}
first = 0x300
}
fillBytes(lut[first:last+1], w)
}
// EastAsianWidth=false, StrictEmojiNeutral=true
lut := strictWidthLUT[0][:]
fillBytes(lut[0x300:], 1)
for _, iv := range doublewidth {
paintHigh(lut, iv.first, iv.last, 2)
}
// zerowidth is checked before doublewidth, so it wins on overlap.
for _, iv := range zerowidth {
paintHigh(lut, iv.first, iv.last, 0)
}
// EastAsianWidth=true, StrictEmojiNeutral=true
lut = strictWidthLUT[1][:]
fillBytes(lut[0x300:], 1)
for _, iv := range eastAsianWidth {
paintHigh(lut, iv.first, iv.last, iv.width)
}
strictWidthLUTLimit.Store(0x110000)
})
}
var private = table{
{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
}
var nonprint = table{
{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
}
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
type Condition struct {
combinedLut []byte
EastAsianWidth bool
StrictEmojiNeutral bool
// Deprecated: ZWJ sequences are always handled through Unicode
// grapheme cluster segmentation now, so this flag has no effect.
// It is kept only for compatibility with code written against
// v0.0.9 and earlier.
ZeroWidthJoiner bool
}
// NewCondition return new instance of Condition which is current locale.
func NewCondition() *Condition {
return &Condition{
EastAsianWidth: EastAsianWidth,
StrictEmojiNeutral: StrictEmojiNeutral,
ZeroWidthJoiner: ZeroWidthJoiner,
}
}
// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
// This one compare doubles as the range check and the lazy-LUT check:
// out-of-range runes and runes above the built portion of
// strictWidthLUT both take the slow path. Once the LUT is fully built
// the limit is 0x110000 and only invalid runes go slow.
if uint32(r) >= uint32(strictWidthLUTLimit.Load()) {
return c.runeWidthSlow(r)
}
if len(c.combinedLut) > 0 {
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
}
if c.StrictEmojiNeutral {
if c.EastAsianWidth {
return int(strictWidthLUT[1][r])
}
return int(strictWidthLUT[0][r])
}
return runeWidthNoLUT(r, c.EastAsianWidth, c.StrictEmojiNeutral)
}
func (c *Condition) runeWidthSlow(r rune) int {
if r < 0 || r > 0x10FFFF {
return 0
}
buildStrictWidthLUT()
if len(c.combinedLut) > 0 {
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
}
if c.StrictEmojiNeutral {
if c.EastAsianWidth {
return int(strictWidthLUT[1][r])
}
return int(strictWidthLUT[0][r])
}
return runeWidthNoLUT(r, c.EastAsianWidth, c.StrictEmojiNeutral)
}
// CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
// This should not be called concurrently with other operations on c.
// If options in c is changed, CreateLUT should be called again.
@@ -285,6 +435,21 @@ func (c *Condition) CreateLUT() {
c.combinedLut = lut
}
// graphemeWidth returns the width of a single grapheme cluster: the sum of
// the widths of its runes, capped at 2 cells. The cap keeps multi-rune
// sequences that render as a single glyph (ZWJ emoji, flags, Hangul jamo)
// from being counted wider than the two cells terminals give them.
func (c *Condition) graphemeWidth(cluster string) int {
width := 0
for _, r := range cluster {
width += c.RuneWidth(r)
}
if width > 2 {
width = 2
}
return width
}
// StringWidth return width as you can see
func (c *Condition) StringWidth(s string) (width int) {
if len(s) == 1 {
@@ -316,14 +481,7 @@ graphemes:
width = 0
g := graphemes.FromString(s)
for g.Next() {
var chWidth int
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // Our best guess at this point is to use the width of the first non-zero-width rune.
}
}
width += chWidth
width += c.graphemeWidth(g.Value())
}
return
}
@@ -338,13 +496,7 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
pos := len(s)
g := graphemes.FromString(s)
for g.Next() {
var chWidth int
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // See StringWidth() for details.
}
}
chWidth := c.graphemeWidth(g.Value())
if width+chWidth > w {
pos = g.Start()
break
@@ -365,13 +517,7 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
g := graphemes.FromString(s)
for g.Next() {
var chWidth int
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // See StringWidth() for details.
}
}
chWidth := c.graphemeWidth(g.Value())
if width+chWidth > w {
if width < w {
@@ -390,6 +536,32 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
return prefix + s[pos:]
}
// TruncatePrefix cuts the beginning of `s` so the result fits in w cells, with prefix prepended
func (c *Condition) TruncatePrefix(s string, w int, prefix string) string {
if c.StringWidth(prefix) >= w {
return prefix
}
sw := c.StringWidth(s)
if sw <= w {
return s
}
w -= c.StringWidth(prefix)
var width int
var pos int
g := graphemes.FromString(s)
for g.Next() {
chWidth := c.graphemeWidth(g.Value())
if sw-(width+chWidth) <= w {
pos = g.End()
break
}
width += chWidth
}
return prefix + s[pos:]
}
// Wrap return string wrapped with w cells
func (c *Condition) Wrap(s string, w int) string {
width := 0
@@ -419,11 +591,7 @@ func (c *Condition) FillLeft(s string, w int) string {
width := c.StringWidth(s)
count := w - width
if count > 0 {
b := make([]byte, count)
for i := range b {
b[i] = ' '
}
return string(b) + s
return strings.Repeat(" ", count) + s
}
return s
}
@@ -433,11 +601,7 @@ func (c *Condition) FillRight(s string, w int) string {
width := c.StringWidth(s)
count := w - width
if count > 0 {
b := make([]byte, count)
for i := range b {
b[i] = ' '
}
return s + string(b)
return s + strings.Repeat(" ", count)
}
return s
}
@@ -478,6 +642,11 @@ func TruncateLeft(s string, w int, prefix string) string {
return DefaultCondition.TruncateLeft(s, w, prefix)
}
// TruncatePrefix cuts the beginning of `s` so the result fits in w cells, with prefix prepended
func TruncatePrefix(s string, w int, prefix string) string {
return DefaultCondition.TruncatePrefix(s, w, prefix)
}
// Wrap return string wrapped with w cells
func Wrap(s string, w int) string {
return DefaultCondition.Wrap(s, w)
+37 -25
View File
@@ -5,35 +5,50 @@ package runewidth
import (
"os"
"regexp"
"strings"
)
var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`)
func mblen(charset string) int {
switch charset {
case "utf-8", "utf8":
return 6
case "jis":
return 8
case "eucjp":
return 3
case "euckr", "euccn", "sjis", "cp932", "cp51932", "cp936", "cp949", "cp950", "big5", "gbk", "gb2312":
return 2
}
return 1
}
var mblenTable = map[string]int{
"utf-8": 6,
"utf8": 6,
"jis": 8,
"eucjp": 3,
"euckr": 2,
"euccn": 2,
"sjis": 2,
"cp932": 2,
"cp51932": 2,
"cp936": 2,
"cp949": 2,
"cp950": 2,
"big5": 2,
"gbk": 2,
"gb2312": 2,
// localeCharset extracts the charset part of a locale name of the form
// "ll.CHARSET" or "ll_CC.CHARSET" (two- or three-letter language code,
// optional uppercase country code). It returns "" if locale does not have
// that shape.
func localeCharset(locale string) string {
n := 0
for n < len(locale) && locale[n] >= 'a' && locale[n] <= 'z' {
n++
}
if n < 2 || n > 3 {
return ""
}
rest := locale[n:]
if len(rest) >= 3 && rest[0] == '_' &&
rest[1] >= 'A' && rest[1] <= 'Z' && rest[2] >= 'A' && rest[2] <= 'Z' {
rest = rest[3:]
}
if len(rest) >= 2 && rest[0] == '.' {
return rest[1:]
}
return ""
}
func isEastAsian(locale string) bool {
charset := strings.ToLower(locale)
r := reLoc.FindStringSubmatch(locale)
if len(r) == 2 {
charset = strings.ToLower(r[1])
if cs := localeCharset(locale); cs != "" {
charset = strings.ToLower(cs)
}
if strings.HasSuffix(charset, "@cjk_narrow") {
@@ -46,10 +61,7 @@ func isEastAsian(locale string) bool {
break
}
}
max := 1
if m, ok := mblenTable[charset]; ok {
max = m
}
max := mblen(charset)
if max > 1 && (charset[0] != 'u' ||
strings.HasPrefix(locale, "ja") ||
strings.HasPrefix(locale, "ko") ||
+121 -17
View File
@@ -3,23 +3,127 @@
package runewidth
var combining = table{
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x07EB, 0x07F3},
{0x0C00, 0x0C00}, {0x0C04, 0x0C04}, {0x0CF3, 0x0CF3},
{0x0D00, 0x0D01}, {0x135D, 0x135F}, {0x180B, 0x180D},
{0x180F, 0x180F}, {0x1A7F, 0x1A7F}, {0x1AB0, 0x1ADD},
{0x1AE0, 0x1AEB}, {0x1B6B, 0x1B73}, {0x1DC0, 0x1DFF},
{0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, {0x2DE0, 0x2DFF},
{0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D},
{0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA8E0, 0xA8F1},
{0xFE00, 0xFE0F}, {0xFE20, 0xFE2F}, {0x101FD, 0x101FD},
{0x10376, 0x1037A}, {0x10EAB, 0x10EAC}, {0x10F46, 0x10F50},
{0x10F82, 0x10F85}, {0x11300, 0x11301}, {0x1133B, 0x1133C},
{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x16AF0, 0x16AF4},
{0x1CF00, 0x1CF2D}, {0x1CF30, 0x1CF46}, {0x1D165, 0x1D169},
{0x1D16D, 0x1D172}, {0x1D17B, 0x1D182}, {0x1D185, 0x1D18B},
{0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0x1E000, 0x1E006},
{0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024},
{0x1E026, 0x1E02A}, {0x1E08F, 0x1E08F}, {0x1E8D0, 0x1E8D6},
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x0591, 0x05BD},
{0x05BF, 0x05BF}, {0x05C1, 0x05C2}, {0x05C4, 0x05C5},
{0x05C7, 0x05C7}, {0x0610, 0x061A}, {0x064B, 0x065F},
{0x0670, 0x0670}, {0x06D6, 0x06DC}, {0x06DF, 0x06E4},
{0x06E7, 0x06E8}, {0x06EA, 0x06ED}, {0x0711, 0x0711},
{0x0730, 0x074A}, {0x07A6, 0x07B0}, {0x07EB, 0x07F3},
{0x07FD, 0x07FD}, {0x0816, 0x0819}, {0x081B, 0x0823},
{0x0825, 0x0827}, {0x0829, 0x082D}, {0x0859, 0x085B},
{0x0897, 0x089F}, {0x08CA, 0x08E1}, {0x08E3, 0x0902},
{0x093A, 0x093A}, {0x093C, 0x093C}, {0x0941, 0x0948},
{0x094D, 0x094D}, {0x0951, 0x0957}, {0x0962, 0x0963},
{0x0981, 0x0981}, {0x09BC, 0x09BC}, {0x09C1, 0x09C4},
{0x09CD, 0x09CD}, {0x09E2, 0x09E3}, {0x09FE, 0x09FE},
{0x0A01, 0x0A02}, {0x0A3C, 0x0A3C}, {0x0A41, 0x0A42},
{0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51},
{0x0A70, 0x0A71}, {0x0A75, 0x0A75}, {0x0A81, 0x0A82},
{0x0ABC, 0x0ABC}, {0x0AC1, 0x0AC5}, {0x0AC7, 0x0AC8},
{0x0ACD, 0x0ACD}, {0x0AE2, 0x0AE3}, {0x0AFA, 0x0AFF},
{0x0B01, 0x0B01}, {0x0B3C, 0x0B3C}, {0x0B3F, 0x0B3F},
{0x0B41, 0x0B44}, {0x0B4D, 0x0B4D}, {0x0B55, 0x0B56},
{0x0B62, 0x0B63}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0},
{0x0BCD, 0x0BCD}, {0x0C00, 0x0C00}, {0x0C04, 0x0C04},
{0x0C3C, 0x0C3C}, {0x0C3E, 0x0C40}, {0x0C46, 0x0C48},
{0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0C62, 0x0C63},
{0x0C81, 0x0C81}, {0x0CBC, 0x0CBC}, {0x0CBF, 0x0CBF},
{0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD}, {0x0CE2, 0x0CE3},
{0x0CF3, 0x0CF3}, {0x0D00, 0x0D01}, {0x0D3B, 0x0D3C},
{0x0D41, 0x0D44}, {0x0D4D, 0x0D4D}, {0x0D62, 0x0D63},
{0x0D81, 0x0D81}, {0x0DCA, 0x0DCA}, {0x0DD2, 0x0DD4},
{0x0DD6, 0x0DD6}, {0x0E31, 0x0E31}, {0x0E34, 0x0E3A},
{0x0E47, 0x0E4E}, {0x0EB1, 0x0EB1}, {0x0EB4, 0x0EBC},
{0x0EC8, 0x0ECE}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35},
{0x0F37, 0x0F37}, {0x0F39, 0x0F39}, {0x0F71, 0x0F7E},
{0x0F80, 0x0F84}, {0x0F86, 0x0F87}, {0x0F8D, 0x0F97},
{0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102D, 0x1030},
{0x1032, 0x1037}, {0x1039, 0x103A}, {0x103D, 0x103E},
{0x1058, 0x1059}, {0x105E, 0x1060}, {0x1071, 0x1074},
{0x1082, 0x1082}, {0x1085, 0x1086}, {0x108D, 0x108D},
{0x109D, 0x109D}, {0x135D, 0x135F}, {0x1712, 0x1714},
{0x1732, 0x1733}, {0x1752, 0x1753}, {0x1772, 0x1773},
{0x17B4, 0x17B5}, {0x17B7, 0x17BD}, {0x17C6, 0x17C6},
{0x17C9, 0x17D3}, {0x17DD, 0x17DD}, {0x180B, 0x180D},
{0x180F, 0x180F}, {0x1885, 0x1886}, {0x18A9, 0x18A9},
{0x1920, 0x1922}, {0x1927, 0x1928}, {0x1932, 0x1932},
{0x1939, 0x193B}, {0x1A17, 0x1A18}, {0x1A1B, 0x1A1B},
{0x1A56, 0x1A56}, {0x1A58, 0x1A5E}, {0x1A60, 0x1A60},
{0x1A62, 0x1A62}, {0x1A65, 0x1A6C}, {0x1A73, 0x1A7C},
{0x1A7F, 0x1A7F}, {0x1AB0, 0x1ADD}, {0x1AE0, 0x1AEB},
{0x1B00, 0x1B03}, {0x1B34, 0x1B34}, {0x1B36, 0x1B3A},
{0x1B3C, 0x1B3C}, {0x1B42, 0x1B42}, {0x1B6B, 0x1B73},
{0x1B80, 0x1B81}, {0x1BA2, 0x1BA5}, {0x1BA8, 0x1BA9},
{0x1BAB, 0x1BAD}, {0x1BE6, 0x1BE6}, {0x1BE8, 0x1BE9},
{0x1BED, 0x1BED}, {0x1BEF, 0x1BF1}, {0x1C2C, 0x1C33},
{0x1C36, 0x1C37}, {0x1CD0, 0x1CD2}, {0x1CD4, 0x1CE0},
{0x1CE2, 0x1CE8}, {0x1CED, 0x1CED}, {0x1CF4, 0x1CF4},
{0x1CF8, 0x1CF9}, {0x1DC0, 0x1DFF}, {0x20D0, 0x20F0},
{0x2CEF, 0x2CF1}, {0x2D7F, 0x2D7F}, {0x2DE0, 0x2DFF},
{0x302A, 0x302D}, {0x3099, 0x309A}, {0xA66F, 0xA672},
{0xA674, 0xA67D}, {0xA69E, 0xA69F}, {0xA6F0, 0xA6F1},
{0xA802, 0xA802}, {0xA806, 0xA806}, {0xA80B, 0xA80B},
{0xA825, 0xA826}, {0xA82C, 0xA82C}, {0xA8C4, 0xA8C5},
{0xA8E0, 0xA8F1}, {0xA8FF, 0xA8FF}, {0xA926, 0xA92D},
{0xA947, 0xA951}, {0xA980, 0xA982}, {0xA9B3, 0xA9B3},
{0xA9B6, 0xA9B9}, {0xA9BC, 0xA9BD}, {0xA9E5, 0xA9E5},
{0xAA29, 0xAA2E}, {0xAA31, 0xAA32}, {0xAA35, 0xAA36},
{0xAA43, 0xAA43}, {0xAA4C, 0xAA4C}, {0xAA7C, 0xAA7C},
{0xAAB0, 0xAAB0}, {0xAAB2, 0xAAB4}, {0xAAB7, 0xAAB8},
{0xAABE, 0xAABF}, {0xAAC1, 0xAAC1}, {0xAAEC, 0xAAED},
{0xAAF6, 0xAAF6}, {0xABE5, 0xABE5}, {0xABE8, 0xABE8},
{0xABED, 0xABED}, {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F},
{0xFE20, 0xFE2F}, {0x101FD, 0x101FD}, {0x102E0, 0x102E0},
{0x10376, 0x1037A}, {0x10A01, 0x10A03}, {0x10A05, 0x10A06},
{0x10A0C, 0x10A0F}, {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F},
{0x10AE5, 0x10AE6}, {0x10D24, 0x10D27}, {0x10D69, 0x10D6D},
{0x10EAB, 0x10EAC}, {0x10EFA, 0x10EFF}, {0x10F46, 0x10F50},
{0x10F82, 0x10F85}, {0x11001, 0x11001}, {0x11038, 0x11046},
{0x11070, 0x11070}, {0x11073, 0x11074}, {0x1107F, 0x11081},
{0x110B3, 0x110B6}, {0x110B9, 0x110BA}, {0x110C2, 0x110C2},
{0x11100, 0x11102}, {0x11127, 0x1112B}, {0x1112D, 0x11134},
{0x11173, 0x11173}, {0x11180, 0x11181}, {0x111B6, 0x111BE},
{0x111C9, 0x111CC}, {0x111CF, 0x111CF}, {0x1122F, 0x11231},
{0x11234, 0x11234}, {0x11236, 0x11237}, {0x1123E, 0x1123E},
{0x11241, 0x11241}, {0x112DF, 0x112DF}, {0x112E3, 0x112EA},
{0x11300, 0x11301}, {0x1133B, 0x1133C}, {0x11340, 0x11340},
{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x113BB, 0x113C0},
{0x113CE, 0x113CE}, {0x113D0, 0x113D0}, {0x113D2, 0x113D2},
{0x113E1, 0x113E2}, {0x11438, 0x1143F}, {0x11442, 0x11444},
{0x11446, 0x11446}, {0x1145E, 0x1145E}, {0x114B3, 0x114B8},
{0x114BA, 0x114BA}, {0x114BF, 0x114C0}, {0x114C2, 0x114C3},
{0x115B2, 0x115B5}, {0x115BC, 0x115BD}, {0x115BF, 0x115C0},
{0x115DC, 0x115DD}, {0x11633, 0x1163A}, {0x1163D, 0x1163D},
{0x1163F, 0x11640}, {0x116AB, 0x116AB}, {0x116AD, 0x116AD},
{0x116B0, 0x116B5}, {0x116B7, 0x116B7}, {0x1171D, 0x1171D},
{0x1171F, 0x1171F}, {0x11722, 0x11725}, {0x11727, 0x1172B},
{0x1182F, 0x11837}, {0x11839, 0x1183A}, {0x1193B, 0x1193C},
{0x1193E, 0x1193E}, {0x11943, 0x11943}, {0x119D4, 0x119D7},
{0x119DA, 0x119DB}, {0x119E0, 0x119E0}, {0x11A01, 0x11A0A},
{0x11A33, 0x11A38}, {0x11A3B, 0x11A3E}, {0x11A47, 0x11A47},
{0x11A51, 0x11A56}, {0x11A59, 0x11A5B}, {0x11A8A, 0x11A96},
{0x11A98, 0x11A99}, {0x11B60, 0x11B60}, {0x11B62, 0x11B64},
{0x11B66, 0x11B66}, {0x11C30, 0x11C36}, {0x11C38, 0x11C3D},
{0x11C3F, 0x11C3F}, {0x11C92, 0x11CA7}, {0x11CAA, 0x11CB0},
{0x11CB2, 0x11CB3}, {0x11CB5, 0x11CB6}, {0x11D31, 0x11D36},
{0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D}, {0x11D3F, 0x11D45},
{0x11D47, 0x11D47}, {0x11D90, 0x11D91}, {0x11D95, 0x11D95},
{0x11D97, 0x11D97}, {0x11EF3, 0x11EF4}, {0x11F00, 0x11F01},
{0x11F36, 0x11F3A}, {0x11F40, 0x11F40}, {0x11F42, 0x11F42},
{0x11F5A, 0x11F5A}, {0x13440, 0x13440}, {0x13447, 0x13455},
{0x1611E, 0x16129}, {0x1612D, 0x1612F}, {0x16AF0, 0x16AF4},
{0x16B30, 0x16B36}, {0x16F4F, 0x16F4F}, {0x16F8F, 0x16F92},
{0x16FE4, 0x16FE4}, {0x1BC9D, 0x1BC9E}, {0x1CF00, 0x1CF2D},
{0x1CF30, 0x1CF46}, {0x1D165, 0x1D169}, {0x1D16D, 0x1D172},
{0x1D17B, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD},
{0x1D242, 0x1D244}, {0x1DA00, 0x1DA36}, {0x1DA3B, 0x1DA6C},
{0x1DA75, 0x1DA75}, {0x1DA84, 0x1DA84}, {0x1DA9B, 0x1DA9F},
{0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006}, {0x1E008, 0x1E018},
{0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, {0x1E026, 0x1E02A},
{0x1E08F, 0x1E08F}, {0x1E130, 0x1E136}, {0x1E2AE, 0x1E2AE},
{0x1E2EC, 0x1E2EF}, {0x1E4EC, 0x1E4EF}, {0x1E5EE, 0x1E5EF},
{0x1E6E3, 0x1E6E3}, {0x1E6E6, 0x1E6E6}, {0x1E6EE, 0x1E6EF},
{0x1E6F5, 0x1E6F5}, {0x1E8D0, 0x1E8D6}, {0x1E944, 0x1E94A},
{0xE0100, 0xE01EF},
}
+183 -78
View File
@@ -17,6 +17,7 @@ import (
"time"
"github.com/containerd/log"
"github.com/moby/go-archive/internal/archiveoptions"
"github.com/moby/patternmatcher"
"github.com/moby/sys/sequential"
"github.com/moby/sys/user"
@@ -81,9 +82,22 @@ type (
// were probably in the archive for a reason, so set this option at
// your own peril.
BestEffortXattrs bool
// internalOptions contains options for use by packages within this module.
internalOptions *archiveoptions.Options
}
)
// WithProcSelfFD returns a copy of opts prepared for extraction in a
// filesystem context where /proc/self/fd may not be accessible by path.
//
// The caller must invoke the returned cleanup function after extraction
// completes. On platforms that do not use /proc/self/fd for extraction,
// the returned cleanup function is a no-op.
func WithProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
return withProcSelfFD(opts)
}
// Archiver implements the Archiver interface and allows the reuse of most utility functions of
// this package with a pluggable Untar function. Also, to facilitate the passing of specific id
// mappings for untar, an Archiver can be created with maps which will then be passed to Untar operations.
@@ -439,6 +453,90 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
return nil
}
// resolveArchivePath resolves intermediate symlinks in name using chroot-like
// semantics when os.Root cannot traverse them. The final path component is
// intentionally preserved because archive extraction may create or replace it.
//
// This is a compatibility workaround rather than the preferred long-term
// implementation. It resolves the path separately before the actual operation,
// so a concurrent filesystem change may cause the operation to affect a
// different path within root. The subsequent os.Root operation still confines
// the operation to root and prevents such a change from escaping it.
//
// Paths with missing components are supported. Existing symlinks are resolved,
// and any remaining nonexistent components are retained for later creation.
//
// This helper should eventually be replaced by handle-relative resolution and
// operations with resolve-in-root semantics, avoiding the resolution/use race
// and repeated path traversal.
func resolveArchivePath(root *os.Root, name string) (string, error) {
parent, base := filepath.Split(name)
if parent == "" {
return name, nil
}
parent = filepath.Clean(parent)
// Follow the final parent component: it is an intermediate component of name,
// and an absolute symlink there must trigger the resolve-in-root fallback.
_, statErr := root.Stat(parent)
switch {
case statErr == nil:
return name, nil
case !os.IsNotExist(statErr) && !isPathEscapes(statErr):
return "", statErr
}
// Resolve the parent both to handle ENOENT from missing components or dangling
// symlinks, and to determine whether an os.Root breakout was caused by an
// absolute symlink. Relative symlink escapes preserve the original Stat error.
resolved, err := resolveFSRootPath(root.Name(), parent)
if err != nil {
return "", err
}
if isPathEscapes(statErr) && (!resolved.followedAbsoluteLink || resolved.relativeEscapeBeforeAbsolute) {
return "", statErr
}
relParent, err := filepath.Rel(root.Name(), resolved.path)
if err != nil {
return "", breakoutError(fmt.Errorf(
"could not make resolved parent %q relative to root %q: %w",
resolved.path,
root.Name(),
err,
))
}
if relParent != "." && !filepath.IsLocal(relParent) {
return "", breakoutError(fmt.Errorf(
"resolved parent %q escapes root %q",
resolved.path,
root.Name(),
))
}
return filepath.Join(relParent, base), nil
}
// resolveHardlinkTarget validates a POSIX hardlink target and resolves it to
// the native, root-relative filesystem path used for extraction.
func resolveHardlinkTarget(root *os.Root, linkname string) (string, error) {
cleaned := path.Clean(linkname)
if strings.HasPrefix(cleaned, "/") {
// Some image builders (e.g. kaniko) write hardlink targets as absolute
// paths. Resolve those relative to the extraction root, with chroot-like
// semantics matching absolute symlink targets. Strip the root from the
// original linkname rather than the cleaned one so that ".." components
// are not collapsed against "/" but instead rejected below.
cleaned = path.Clean(strings.TrimLeft(linkname, "/"))
}
if cleaned == "." || !filepath.IsLocal(cleaned) {
return "", breakoutError(fmt.Errorf("invalid hardlink target %q", linkname))
}
return resolveArchivePath(root, filepath.FromSlash(cleaned))
}
// createTarFile extracts a single tar entry into the given root. dstPath is the
// root-relative path of the entry being extracted, in native (host-separator)
// form so it can be passed directly to os.Root methods and fsRootPath.
@@ -447,6 +545,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
Lchown = true
inUserns, bestEffortXattrs bool
chownOpts *ChownOpts
internalOpts *archiveoptions.Options
)
// TODO(thaJeztah): make opts a required argument.
@@ -455,6 +554,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
inUserns = opts.InUserNS // TODO(thaJeztah): consider deprecating opts.InUserNS and detect locally.
chownOpts = opts.ChownOpts
bestEffortXattrs = opts.BestEffortXattrs
internalOpts = opts.internalOptions
}
// hdr.Mode is in linux format, which we can use for sycalls,
@@ -462,6 +562,15 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
// so use hdrInfo.Mode() (they differ for e.g. setuid bits)
hdrInfo := hdr.FileInfo()
var hardlinkTarget string
if hdr.Typeflag == tar.TypeLink {
var err error
hardlinkTarget, err = resolveHardlinkTarget(root, hdr.Linkname)
if err != nil {
return err
}
}
switch hdr.Typeflag {
case tar.TypeDir:
// Create directory unless it already exists as one; merge in that case.
@@ -511,13 +620,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
}
case tar.TypeLink:
// Defence in depth: root.Link's containment is limited when
// dest is a volume root.
linkname := path.Clean(hdr.Linkname)
if linkname == "." || !filepath.IsLocal(linkname) {
return breakoutError(fmt.Errorf("invalid hardlink target %q", hdr.Linkname))
}
if err := root.Link(filepath.FromSlash(linkname), dstPath); err != nil {
if err := root.Link(hardlinkTarget, dstPath); err != nil {
return err
}
@@ -593,7 +696,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
// There is no LChmod, so ignore mode for symlink. Also, this
// must happen after chown, as that can modify the file mode
if err := handleLChmod(root, dstPath, hdr, hdrInfo); err != nil {
if err := handleLChmod(root, dstPath, hardlinkTarget, hdr, hdrInfo, internalOpts); err != nil {
return err
}
@@ -608,7 +711,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
}
case tar.TypeLink:
// Follow the hardlink only when its target is not itself a symlink.
fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname)))
fi, err := root.Lstat(hardlinkTarget)
if err == nil && fi.Mode()&os.ModeSymlink == 0 {
if err := chtimes(root, dstPath, aTime, mTime); err != nil {
return err
@@ -698,13 +801,13 @@ func (t *Tarballer) Do() {
defer func() {
// Make sure to check the error on Close.
if err := ta.TarWriter.Close(); err != nil {
if err := ta.TarWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.G(context.TODO()).Errorf("Can't close tar writer: %s", err)
}
if err := t.compressWriter.Close(); err != nil {
if err := t.compressWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.G(context.TODO()).Errorf("Can't close compress writer: %s", err)
}
if err := t.pipeWriter.Close(); err != nil {
if err := t.pipeWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.G(context.TODO()).Errorf("Can't close pipe writer: %s", err)
}
}()
@@ -931,7 +1034,10 @@ loop:
// dstPath is the native (host-separator) form of the entry name,
// used at all filesystem boundaries (os.Root methods, fsRootPath).
// hdr.Name stays POSIX (forward-slash) for logical string checks.
dstPath := filepath.FromSlash(hdr.Name)
dstPath, err := resolveArchivePath(root, filepath.FromSlash(hdr.Name))
if err != nil {
return err
}
// If dstPath exists we almost always just want to remove and replace it.
// The only exception is when it is a directory *and* the file from
@@ -969,7 +1075,7 @@ loop:
//
// This must be done before whiteoutConverter.ConvertRead, which
// may set xattrs on the directory or create whiteout files.
if err := createImpliedDirectories(root, hdr, options); err != nil {
if err := createImpliedDirectories(root, dstPath, options); err != nil {
return err
}
@@ -1024,81 +1130,80 @@ func unrepresentableOnWindows(hdr *tar.Header) error {
return nil
}
// createImpliedDirectories will create all parent directories of the current path with default permissions, if they do
// not already exist. This is possible as the tar format supports 'implicit' directories, where their existence is
// defined by the paths of files in the tar, but there are no header entries for the directories themselves, and thus
// we most both create them and choose metadata like permissions.
// createImpliedDirectories creates all parent directories of dstPath with
// default permissions if they do not already exist. This is necessary because
// the tar format permits implicit directories whose existence is defined only
// by file paths, without corresponding directory headers from which metadata
// could be restored.
//
// The caller must have normalized hdr.Name (no leading ".." components).
// All directory creation is performed via root so it is bounded within the
// destination at the OS level (openat(2) semantics), preventing escape via
// symlinks in the destination tree.
func createImpliedDirectories(root *os.Root, hdr *tar.Header, options *TarOptions) error {
// For non-directory entries, ensure that the parent directory exists.
if hdr.Typeflag != tar.TypeDir {
parent := filepath.FromSlash(path.Dir(strings.TrimSuffix(hdr.Name, "/")))
// Skip when the parent is the root itself; nothing to create.
if parent == "." || parent == "" {
return nil
}
if _, err := root.Lstat(parent); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
// RootPair() is confined inside this loop as most cases will not require a call, so we can spend some
// unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche
// usage that reduces the portability of an image.
uid, gid := options.IDMap.RootPair()
// The caller must pass a normalized, root-relative local path. Any archive-path
// conversion and resolve-in-root handling must already have been applied.
// Directory creation is performed through root, so it remains confined to the
// extraction destination even if the destination tree changes concurrently.
func createImpliedDirectories(root *os.Root, dstPath string, options *TarOptions) error {
parent := filepath.Dir(dstPath)
// Similar to [user.MkdirAllAndChown]
//
// [user.MkdirAllAndChown]: https://pkg.go.dev/github.com/moby/sys/user#MkdirAllAndChown
var cur string
for c := range strings.SplitSeq(parent, string(os.PathSeparator)) {
if c == "" {
continue
}
cur = filepath.Join(cur, c)
if err := root.Mkdir(cur, ImpliedDirectoryMode); err != nil {
if !errors.Is(err, os.ErrExist) {
return err
}
// Skip when the parent is the root itself; nothing to create.
if parent == "." || parent == "" {
return nil
}
if _, err := root.Lstat(parent); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
// RootPair() is confined inside this loop as most cases will not require a call, so we can spend some
// unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche
// usage that reduces the portability of an image.
uid, gid := options.IDMap.RootPair()
fi, err := root.Stat(cur)
if err != nil {
return err
}
if fi.IsDir() {
continue
}
return &os.PathError{Op: "mkdir", Path: cur, Err: syscall.ENOTDIR}
// Similar to [user.MkdirAllAndChown]
//
// [user.MkdirAllAndChown]: https://pkg.go.dev/github.com/moby/sys/user#MkdirAllAndChown
var cur string
for c := range strings.SplitSeq(parent, string(os.PathSeparator)) {
if c == "" {
continue
}
cur = filepath.Join(cur, c)
if err := root.Mkdir(cur, ImpliedDirectoryMode); err != nil {
if !errors.Is(err, os.ErrExist) {
return err
}
if options.NoLchown {
continue
}
// Only the successful Mkdir case is newly-created.
dir, err := root.Open(cur)
fi, err := root.Stat(cur)
if err != nil {
return err
}
if uid != 0 || gid != 0 {
if err := dir.Chown(uid, gid); err != nil {
_ = dir.Close()
return err
}
if fi.IsDir() {
continue
}
// root.Mkdir applies the mode subject to the process umask, so
// re-apply it with Chmod to guarantee ImpliedDirectoryMode
// independent of umask, matching the previous MkdirAllAndChown
// behavior.
if err := dir.Chmod(ImpliedDirectoryMode); err != nil {
return &os.PathError{Op: "mkdir", Path: cur, Err: syscall.ENOTDIR}
}
if options.NoLchown {
continue
}
// Only the successful Mkdir case is newly-created.
dir, err := root.Open(cur)
if err != nil {
return err
}
if uid != 0 || gid != 0 {
if err := dir.Chown(uid, gid); err != nil {
_ = dir.Close()
return err
}
if err := dir.Close(); err != nil {
return err
}
}
// root.Mkdir applies the mode subject to the process umask, so
// re-apply it with Chmod to guarantee ImpliedDirectoryMode
// independent of umask, matching the previous MkdirAllAndChown
// behavior.
if err := dir.Chmod(ImpliedDirectoryMode); err != nil {
_ = dir.Close()
return err
}
if err := dir.Close(); err != nil {
return err
}
}
+18
View File
@@ -8,10 +8,28 @@ import (
"path/filepath"
"strings"
"github.com/moby/go-archive/internal/archiveoptions"
"github.com/moby/sys/userns"
"golang.org/x/sys/unix"
)
func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
procSelfFD, err := os.Open("/proc/self/fd")
if err != nil {
return nil, nil, err
}
var prepared TarOptions
if opts != nil {
prepared = *opts
}
prepared.internalOptions = &archiveoptions.Options{
ProcSelfFD: procSelfFD,
}
return &prepared, func() { _ = procSelfFD.Close() }, nil
}
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
if format == OverlayWhiteoutFormat {
return newOverlayWhiteoutConverter()
+8
View File
@@ -2,6 +2,14 @@
package archive
func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
var prepared TarOptions
if opts != nil {
prepared = *opts
}
return &prepared, func() {}, nil
}
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
return nil
}
+7 -19
View File
@@ -8,11 +8,11 @@ import (
"fmt"
"math"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
@@ -88,7 +88,7 @@ func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, dstPath string)
// handleLChmod applies the mode from hdrInfo to dstPath within root, skipping
// symlinks (there is no lchmod). For hardlinks, the mode is applied only when
// the link target is itself not a symlink.
func handleLChmod(root *os.Root, dstPath string, hdr *tar.Header, hdrInfo os.FileInfo) error {
func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo, opts *archiveoptions.Options) error {
switch hdr.Typeflag {
case tar.TypeSymlink:
return nil
@@ -96,21 +96,21 @@ func handleLChmod(root *os.Root, dstPath string, hdr *tar.Header, hdrInfo os.Fil
case tar.TypeLink:
// If the target is a symlink, there is no way to chmod the hardlink
// without following it.
fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname)))
fi, err := root.Lstat(hardlinkTarget)
if err != nil || fi.Mode()&os.ModeSymlink != 0 {
return nil
}
return chmodNoSymlink(root, dstPath, hdrInfo.Mode())
return chmodNoSymlink(root, dstPath, hdrInfo.Mode(), opts)
default:
return chmodNoSymlink(root, dstPath, hdrInfo.Mode())
return chmodNoSymlink(root, dstPath, hdrInfo.Mode(), opts)
}
}
// chmodNoSymlink applies mode to a non-symlink entry.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlink(root *os.Root, name string, mode os.FileMode) error {
func chmodNoSymlink(root *os.Root, name string, mode os.FileMode, opts *archiveoptions.Options) error {
parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0)
if err != nil {
return err
@@ -127,19 +127,7 @@ func chmodNoSymlink(root *os.Root, name string, mode os.FileMode) error {
}
// Fallback for systems that cannot perform fchmodat with AT_SYMLINK_NOFOLLOW.
// Open the entry without following symlinks and apply the mode through the
// resulting file descriptor.
// #nosec G115 -- ignore integer overflow conversion for parent.Fd
fd, err := unix.Openat(int(parent.Fd()), base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
if err != nil {
return &os.PathError{Op: "openat", Path: name, Err: err}
}
defer unix.Close(fd)
if err := unix.Fchmod(fd, perm); err != nil {
return &os.PathError{Op: "fchmod", Path: name, Err: err}
}
return nil
return chmodNoSymlinkFallback(int(parent.Fd()), base, name, perm, opts) // #nosec G115 -- ignore integer overflow conversion for parent.Fd
}
// fileModeToPerm returns the subset of an os.FileMode that can be applied
+1 -1
View File
@@ -53,7 +53,7 @@ func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, path string) err
}
// handleLChmod is a no-op on Windows because chmod is not supported.
func handleLChmod(root *os.Root, path string, hdr *tar.Header, hdrInfo os.FileInfo) error {
func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo, opts any) error {
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package archive
import (
"fmt"
"os"
"runtime"
"strconv"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
// chmodNoSymlinkFallback applies mode without following the final path
// component on systems without fchmodat2 support.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32, opts *archiveoptions.Options) error {
fd, err := unix.Openat(parentFD, base, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
if err != nil {
return &os.PathError{Op: "openat", Path: name, Err: err}
}
defer unix.Close(fd)
if opts != nil && opts.ProcSelfFD != nil {
err := unix.Fchmodat(int(opts.ProcSelfFD.Fd()), strconv.Itoa(fd), perm, 0)
// Keep the os.File alive until fchmodat has finished using its descriptor.
runtime.KeepAlive(opts.ProcSelfFD)
if err != nil {
return &os.PathError{
Op: "fchmodat",
Path: name,
Err: fmt.Errorf("via pre-opened /proc/self/fd/%d: %w", fd, err),
}
}
} else {
procPath := "/proc/self/fd/" + strconv.Itoa(fd)
if err := unix.Chmod(procPath, perm); err != nil {
return &os.PathError{
Op: "chmod",
Path: name,
Err: fmt.Errorf("via %s: %w", procPath, err),
}
}
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
//go:build !linux && !windows
package archive
import (
"os"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
// chmodNoSymlinkFallback applies mode without following the final path
// component on systems without fchmodat2 support.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32, _ *archiveoptions.Options) error {
fd, err := unix.Openat(parentFD, base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0)
if err != nil {
return &os.PathError{Op: "openat", Path: name, Err: err}
}
defer unix.Close(fd)
if err := unix.Fchmod(fd, perm); err != nil {
return &os.PathError{Op: "fchmod", Path: name, Err: err}
}
return nil
}
+18 -18
View File
@@ -29,8 +29,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
tr := tar.NewReader(layer)
var dirs []unpackedDir
// unpackedPaths tracks root-relative paths already written in this layer
// so that the AUFS opaque-whiteout walk knows which paths to preserve.
// unpackedPaths tracks resolved, native-separator, root-relative paths
// already written in this layer so that the AUFS opaque-whiteout walk
// knows which paths to preserve.
unpackedPaths := make(map[string]struct{})
if options == nil {
@@ -71,12 +72,6 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
continue
}
// Ensure that the parent directory exists.
err = createImpliedDirectories(root, hdr, options)
if err != nil {
return 0, err
}
// Skip AUFS metadata dirs
if strings.HasPrefix(hdr.Name, WhiteoutMetaPrefix) {
// Regular files inside /.wh..wh.plnk can be used as hardlink targets
@@ -109,10 +104,15 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
// dstPath is the native (host-separator) form of the entry name,
// used at all filesystem boundaries (os.Root methods, fsRootPath).
// The tar-header name (hdr.Name) is POSIX, so convert it here.
dstPath := filepath.FromSlash(hdr.Name)
base := filepath.Base(dstPath)
if strings.HasPrefix(base, WhiteoutPrefix) {
dstPath, err := resolveArchivePath(root, filepath.FromSlash(hdr.Name))
if err != nil {
return 0, err
}
// Ensure that the parent directory exists.
if err := createImpliedDirectories(root, dstPath, options); err != nil {
return 0, err
}
if base := filepath.Base(dstPath); strings.HasPrefix(base, WhiteoutPrefix) {
dir := filepath.Dir(dstPath)
if base == WhiteoutOpaqueDir {
_, err := root.Lstat(dir)
@@ -144,9 +144,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
return err
}
// unpackedPaths is keyed by root-relative slash paths; convert
// filepath.WalkDir's native path before looking it up.
if _, exists := unpackedPaths[filepath.ToSlash(rel)]; !exists {
// unpackedPaths is keyed by resolved, native-separator,
// root-relative paths, matching filepath.WalkDir's paths.
if _, exists := unpackedPaths[rel]; !exists {
return root.RemoveAll(rel)
}
return nil
@@ -206,9 +206,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
if hdr.Typeflag == tar.TypeDir {
dirs = append(dirs, unpackedDir{hdr: hdr, name: dstPath})
}
// unpackedPaths is keyed by the POSIX (forward-slash) name so it
// matches the ToSlash'd lookup in the opaque-whiteout walk above.
unpackedPaths[hdr.Name] = struct{}{}
// Record the resolved, native-separator, root-relative path so it
// matches the paths produced by the opaque-whiteout walk.
unpackedPaths[dstPath] = struct{}{}
}
}
+12
View File
@@ -0,0 +1,12 @@
// Package archiveoptions defines internal options shared between archive and
// chrootarchive.
package archiveoptions
import "os"
// Options contains extraction resources supplied by internal callers.
type Options struct {
// ProcSelfFD references /proc/self/fd as opened before entering a chroot.
// The caller retains ownership of the file.
ProcSelfFD *os.File
}
+45 -15
View File
@@ -24,31 +24,47 @@ import (
var errTooManyLinks = errors.New("too many links")
type fsRootPathResult struct {
path string
followedAbsoluteLink bool
relativeEscapeBeforeAbsolute bool
}
// fsRootPath joins a path with a root, evaluating and bounding any
// symlink to the root directory.
func fsRootPath(root, path string) (string, error) {
result, err := resolveFSRootPath(root, path)
if err != nil {
return "", err
}
return result.path, nil
}
func resolveFSRootPath(root, path string) (fsRootPathResult, error) {
result := fsRootPathResult{path: root}
if path == "" {
return root, nil
return result, nil
}
var linksWalked int // to protect against cycles
for {
i := linksWalked
newpath, err := walkLinks(root, path, &linksWalked)
newpath, err := walkLinks(root, path, &linksWalked, &result)
if err != nil {
return "", err
return fsRootPathResult{}, err
}
path = newpath
if i == linksWalked {
newpath = filepath.Join(string(os.PathSeparator), newpath)
if path == newpath {
return filepath.Join(root, newpath), nil
result.path = filepath.Join(root, newpath)
return result, nil
}
path = newpath
}
}
}
func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) {
func walkLink(root, path string, linksWalked *int, result *fsRootPathResult) (newpath string, islink bool, err error) {
if *linksWalked > 255 {
return "", false, errTooManyLinks
}
@@ -74,37 +90,51 @@ func walkLink(root, path string, linksWalked *int) (newpath string, islink bool,
if err != nil {
return "", false, err
}
if filepath.IsAbs(newpath) {
result.followedAbsoluteLink = true
} else if !result.followedAbsoluteLink {
// Record an escape before a later absolute link can make the original
// os.Root error appear eligible for resolve-in-root fallback.
relativeDir, err := filepath.Rel(string(os.PathSeparator), filepath.Dir(path))
if err != nil {
return "", false, err
}
resolved := filepath.Join(relativeDir, newpath)
if resolved != "." && !filepath.IsLocal(resolved) {
result.relativeEscapeBeforeAbsolute = true
}
}
*linksWalked++
return newpath, true, nil
}
func walkLinks(root, path string, linksWalked *int) (string, error) {
func walkLinks(root, path string, linksWalked *int, result *fsRootPathResult) (string, error) {
switch dir, file := filepath.Split(path); {
case dir == "":
newpath, _, err := walkLink(root, file, linksWalked)
newpath, _, err := walkLink(root, file, linksWalked, result)
return newpath, err
case file == "":
if os.IsPathSeparator(dir[len(dir)-1]) {
if dir == string(os.PathSeparator) {
return dir, nil
}
return walkLinks(root, dir[:len(dir)-1], linksWalked)
return walkLinks(root, dir[:len(dir)-1], linksWalked, result)
}
newpath, _, err := walkLink(root, dir, linksWalked)
newpath, _, err := walkLink(root, dir, linksWalked, result)
return newpath, err
default:
newdir, err := walkLinks(root, dir, linksWalked)
newdir, err := walkLinks(root, dir, linksWalked, result)
if err != nil {
return "", err
}
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked)
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked, result)
if err != nil {
return "", err
}
if !islink {
return newpath, nil
}
if filepath.IsAbs(newpath) {
if !islink || filepath.IsAbs(newpath) {
return newpath, nil
}
return filepath.Join(newdir, newpath), nil
+18
View File
@@ -5,8 +5,12 @@ import (
"fmt"
"os"
"sync"
"syscall"
)
// See PROC_USER_INIT_INO in https://github.com/torvalds/linux/blob/v7.1/include/uapi/linux/nsfs.h#L50.
const procUserInitIno = 0xEFFFFFFD
var inUserNS = sync.OnceValue(runningInUserNS)
// runningInUserNS detects whether we are currently running in a user namespace.
@@ -17,6 +21,20 @@ var inUserNS = sync.OnceValue(runningInUserNS)
// [libcontainer/runc]: https://github.com/opencontainers/runc/blob/3778ae603c706494fd1e2c2faf83b406e38d687d/libcontainer/userns/userns_linux.go#L12-L49
// [lcx/incus]: https://github.com/lxc/incus/blob/e45085dd42f826b3c8c3228e9733c0b6f998eafe/shared/util.go#L678-L700
func runningInUserNS() bool {
var st syscall.Stat_t
if err := syscall.Stat("/proc/self/ns/user", &st); err == nil {
return st.Ino != procUserInitIno
} else if !os.IsNotExist(err) {
// As long as /proc/self/ns/user exists, we are on a modern kernel.
// Other errors indicate an unexpected procfs state, where assuming the
// init namespace would be unsafe.
return false
}
// Only fall back for older kernels that do not expose the user namespace
// through procfs at /proc/self/ns/user.
// TODO: Remove this fallback once Linux kernels older than 3.8 are no
// longer supported.
file, err := os.Open("/proc/self/uid_map")
if err != nil {
// This kernel-provided file only exists if user namespaces are supported.
@@ -0,0 +1,4 @@
[submodule "testdata/JSON-Schema-Test-Suite"]
path = testdata/JSON-Schema-Test-Suite
url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git
branch = main
+6
View File
@@ -0,0 +1,6 @@
linters:
enable:
- nakedret
- errname
- godot
- misspell
@@ -0,0 +1,7 @@
- id: jsonschema-validate
name: Validate JSON against JSON Schema
description: ensure json files follow specified JSON Schema
entry: jv
language: golang
additional_dependencies:
- ./cmd/jv
@@ -172,31 +172,4 @@
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 xeipuuv
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
of your accepting any such warranty or additional liability.
+88
View File
@@ -0,0 +1,88 @@
# jsonschema v6.0.2
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![GoDoc](https://godoc.org/github.com/santhosh-tekuri/jsonschema?status.svg)](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v6)
[![Go Report Card](https://goreportcard.com/badge/github.com/santhosh-tekuri/jsonschema/v6)](https://goreportcard.com/report/github.com/santhosh-tekuri/jsonschema/v6)
[![Build Status](https://github.com/santhosh-tekuri/jsonschema/actions/workflows/go.yaml/badge.svg?branch=boon)](https://github.com/santhosh-tekuri/jsonschema/actions/workflows/go.yaml)
[![codecov](https://codecov.io/gh/santhosh-tekuri/jsonschema/branch/boon/graph/badge.svg?token=JMVj1pFT2l)](https://codecov.io/gh/santhosh-tekuri/jsonschema/tree/boon)
see [godoc](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v6) for examples
## Library Features
- [x] pass [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) excluding optional(compare with other impls at [bowtie](https://bowtie-json-schema.github.io/bowtie/#))
- [x] [![draft-04](https://img.shields.io/endpoint?url=https://bowtie.report/badges/go-jsonschema/compliance/draft4.json)](https://bowtie.report/#/dialects/draft4)
- [x] [![draft-06](https://img.shields.io/endpoint?url=https://bowtie.report/badges/go-jsonschema/compliance/draft6.json)](https://bowtie.report/#/dialects/draft6)
- [x] [![draft-07](https://img.shields.io/endpoint?url=https://bowtie.report/badges/go-jsonschema/compliance/draft7.json)](https://bowtie.report/#/dialects/draft7)
- [x] [![draft/2019-09](https://img.shields.io/endpoint?url=https://bowtie.report/badges/go-jsonschema/compliance/draft2019-09.json)](https://bowtie.report/#/dialects/draft2019-09)
- [x] [![draft/2020-12](https://img.shields.io/endpoint?url=https://bowtie.report/badges/go-jsonschema/compliance/draft2020-12.json)](https://bowtie.report/#/dialects/draft2020-12)
- [x] detect infinite loop traps
- [x] `$schema` cycle
- [x] validation cycle
- [x] custom `$schema` url
- [x] vocabulary based validation
- [x] custom regex engine
- [x] format assertions
- [x] flag to enable in draft >= 2019-09
- [x] custom format registration
- [x] built-in formats
- [x] regex, uuid
- [x] ipv4, ipv6
- [x] hostname, email
- [x] date, time, date-time, duration
- [x] json-pointer, relative-json-pointer
- [x] uri, uri-reference, uri-template
- [x] iri, iri-reference
- [x] period, semver
- [x] content assertions
- [x] flag to enable in draft >= 7
- [x] contentEncoding
- [x] base64
- [x] custom
- [x] contentMediaType
- [x] application/json
- [x] custom
- [x] contentSchema
- [x] errors
- [x] introspectable
- [x] hierarchy
- [x] alternative display with `#`
- [x] output
- [x] flag
- [x] basic
- [x] detailed
- [x] custom vocabulary
- enable via `$vocabulary` for draft >=2019-19
- enable via flag for draft <= 7
- [x] mixed dialect support
## CLI v0.7.0
to install: `go install github.com/santhosh-tekuri/jsonschema/cmd/jv@latest`
Note that the cli is versioned independently. you can see it in git tags `cmd/jv/v0.7.0`
```
Usage: jv [OPTIONS] SCHEMA [INSTANCE...]
Options:
-c, --assert-content Enable content assertions with draft >= 7
-f, --assert-format Enable format assertions with draft >= 2019
--cacert pem-file Use the specified pem-file to verify the peer. The file may contain multiple CA certificates
-d, --draft version Draft version used when '$schema' is missing. Valid values 4, 6, 7, 2019, 2020 (default 2020)
-h, --help Print help information
-k, --insecure Use insecure TLS connection
-o, --output format Output format. Valid values simple, alt, flag, basic, detailed (default "simple")
-q, --quiet Do not print errors
-v, --version Print build information
```
- [x] exit code `1` for validation errors, `2` for usage errors
- [x] validate both schema and multiple instances
- [x] support both json and yaml files
- [x] support standard input, use `-`
- [x] quite mode with parsable output
- [x] http(s) url support
- [x] custom certs for validation, use `--cacert`
- [x] flag to skip certificate verification, use `--insecure`
+332
View File
@@ -0,0 +1,332 @@
package jsonschema
import (
"fmt"
"regexp"
"slices"
)
// Compiler compiles json schema into *Schema.
type Compiler struct {
schemas map[urlPtr]*Schema
roots *roots
formats map[string]*Format
decoders map[string]*Decoder
mediaTypes map[string]*MediaType
assertFormat bool
assertContent bool
}
// NewCompiler create Compiler Object.
func NewCompiler() *Compiler {
return &Compiler{
schemas: map[urlPtr]*Schema{},
roots: newRoots(),
formats: map[string]*Format{},
decoders: map[string]*Decoder{},
mediaTypes: map[string]*MediaType{},
assertFormat: false,
assertContent: false,
}
}
// DefaultDraft overrides the draft used to
// compile schemas without `$schema` field.
//
// By default, this library uses the latest
// draft supported.
//
// The use of this option is HIGHLY encouraged
// to ensure continued correct operation of your
// schema. The current default value will not stay
// the same overtime.
func (c *Compiler) DefaultDraft(d *Draft) {
c.roots.defaultDraft = d
}
// AssertFormat always enables format assertions.
//
// Default Behavior:
// for draft-07: enabled.
// for draft/2019-09: disabled unless metaschema says `format` vocabulary is required.
// for draft/2020-12: disabled unless metaschema says `format-assertion` vocabulary is required.
func (c *Compiler) AssertFormat() {
c.assertFormat = true
}
// AssertContent enables content assertions.
//
// Content assertions include keywords:
// - contentEncoding
// - contentMediaType
// - contentSchema
//
// Default behavior is always disabled.
func (c *Compiler) AssertContent() {
c.assertContent = true
}
// RegisterFormat registers custom format.
//
// NOTE:
// - "regex" format can not be overridden
// - format assertions are disabled for draft >= 2019-09
// see [Compiler.AssertFormat]
func (c *Compiler) RegisterFormat(f *Format) {
if f.Name != "regex" {
c.formats[f.Name] = f
}
}
// RegisterContentEncoding registers custom contentEncoding.
//
// NOTE: content assertions are disabled by default.
// see [Compiler.AssertContent].
func (c *Compiler) RegisterContentEncoding(d *Decoder) {
c.decoders[d.Name] = d
}
// RegisterContentMediaType registers custom contentMediaType.
//
// NOTE: content assertions are disabled by default.
// see [Compiler.AssertContent].
func (c *Compiler) RegisterContentMediaType(mt *MediaType) {
c.mediaTypes[mt.Name] = mt
}
// RegisterVocabulary registers custom vocabulary.
//
// NOTE:
// - vocabularies are disabled for draft >= 2019-09
// see [Compiler.AssertVocabs]
func (c *Compiler) RegisterVocabulary(vocab *Vocabulary) {
c.roots.vocabularies[vocab.URL] = vocab
}
// AssertVocabs always enables user-defined vocabularies assertions.
//
// Default Behavior:
// for draft-07: enabled.
// for draft/2019-09: disabled unless metaschema enables a vocabulary.
// for draft/2020-12: disabled unless metaschema enables a vocabulary.
func (c *Compiler) AssertVocabs() {
c.roots.assertVocabs = true
}
// AddResource adds schema resource which gets used later in reference
// resolution.
//
// The argument url can be file path or url. Any fragment in url is ignored.
// The argument doc must be valid json value.
func (c *Compiler) AddResource(url string, doc any) error {
uf, err := absolute(url)
if err != nil {
return err
}
if isMeta(string(uf.url)) {
return &ResourceExistsError{string(uf.url)}
}
if !c.roots.loader.add(uf.url, doc) {
return &ResourceExistsError{string(uf.url)}
}
return nil
}
// UseLoader overrides the default [URLLoader] used
// to load schema resources.
func (c *Compiler) UseLoader(loader URLLoader) {
c.roots.loader.loader = loader
}
// UseRegexpEngine changes the regexp-engine used.
// By default it uses regexp package from go standard
// library.
//
// NOTE: must be called before compiling any schemas.
func (c *Compiler) UseRegexpEngine(engine RegexpEngine) {
if engine == nil {
engine = goRegexpCompile
}
c.roots.regexpEngine = engine
}
func (c *Compiler) enqueue(q *queue, up urlPtr) *Schema {
if sch, ok := c.schemas[up]; ok {
// already got compiled
return sch
}
if sch := q.get(up); sch != nil {
return sch
}
sch := newSchema(up)
q.append(sch)
return sch
}
// MustCompile is like [Compile] but panics if compilation fails.
// It simplifies safe initialization of global variables holding
// compiled schema.
func (c *Compiler) MustCompile(loc string) *Schema {
sch, err := c.Compile(loc)
if err != nil {
panic(fmt.Sprintf("jsonschema: Compile(%q): %v", loc, err))
}
return sch
}
// Compile compiles json-schema at given loc.
func (c *Compiler) Compile(loc string) (*Schema, error) {
uf, err := absolute(loc)
if err != nil {
return nil, err
}
up, err := c.roots.resolveFragment(*uf)
if err != nil {
return nil, err
}
return c.doCompile(up)
}
func (c *Compiler) doCompile(up urlPtr) (*Schema, error) {
q := &queue{}
compiled := 0
c.enqueue(q, up)
for q.len() > compiled {
sch := q.at(compiled)
if err := c.roots.ensureSubschema(sch.up); err != nil {
return nil, err
}
r := c.roots.roots[sch.up.url]
v, err := sch.up.lookup(r.doc)
if err != nil {
return nil, err
}
if err := c.compileValue(v, sch, r, q); err != nil {
return nil, err
}
compiled++
}
for _, sch := range *q {
c.schemas[sch.up] = sch
}
return c.schemas[up], nil
}
func (c *Compiler) compileValue(v any, sch *Schema, r *root, q *queue) error {
res := r.resource(sch.up.ptr)
sch.DraftVersion = res.dialect.draft.version
base := urlPtr{sch.up.url, res.ptr}
sch.resource = c.enqueue(q, base)
// if resource, enqueue dynamic anchors for compilation
if sch.DraftVersion >= 2020 && sch.up == sch.resource.up {
res := r.resource(sch.up.ptr)
for anchor, anchorPtr := range res.anchors {
if slices.Contains(res.dynamicAnchors, anchor) {
up := urlPtr{sch.up.url, anchorPtr}
danchorSch := c.enqueue(q, up)
if sch.dynamicAnchors == nil {
sch.dynamicAnchors = map[string]*Schema{}
}
sch.dynamicAnchors[string(anchor)] = danchorSch
}
}
}
switch v := v.(type) {
case bool:
sch.Bool = &v
case map[string]any:
if err := c.compileObject(v, sch, r, q); err != nil {
return err
}
}
sch.allPropsEvaluated = sch.AdditionalProperties != nil
if sch.DraftVersion < 2020 {
sch.allItemsEvaluated = sch.AdditionalItems != nil
switch items := sch.Items.(type) {
case *Schema:
sch.allItemsEvaluated = true
case []*Schema:
sch.numItemsEvaluated = len(items)
}
} else {
sch.allItemsEvaluated = sch.Items2020 != nil
sch.numItemsEvaluated = len(sch.PrefixItems)
}
return nil
}
func (c *Compiler) compileObject(obj map[string]any, sch *Schema, r *root, q *queue) error {
if len(obj) == 0 {
b := true
sch.Bool = &b
return nil
}
oc := objCompiler{
c: c,
obj: obj,
up: sch.up,
r: r,
res: r.resource(sch.up.ptr),
q: q,
}
return oc.compile(sch)
}
// queue --
type queue []*Schema
func (q *queue) append(sch *Schema) {
*q = append(*q, sch)
}
func (q *queue) at(i int) *Schema {
return (*q)[i]
}
func (q *queue) len() int {
return len(*q)
}
func (q *queue) get(up urlPtr) *Schema {
i := slices.IndexFunc(*q, func(sch *Schema) bool { return sch.up == up })
if i != -1 {
return (*q)[i]
}
return nil
}
// regexp --
// Regexp is the representation of compiled regular expression.
type Regexp interface {
fmt.Stringer
// MatchString reports whether the string s contains
// any match of the regular expression.
MatchString(string) bool
}
// RegexpEngine parses a regular expression and returns,
// if successful, a Regexp object that can be used to
// match against text.
type RegexpEngine func(string) (Regexp, error)
func (re RegexpEngine) validate(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
_, err := re(s)
return err
}
func goRegexpCompile(s string) (Regexp, error) {
return regexp.Compile(s)
}
+51
View File
@@ -0,0 +1,51 @@
package jsonschema
import (
"bytes"
"encoding/base64"
"encoding/json"
)
// Decoder specifies how to decode specific contentEncoding.
type Decoder struct {
// Name of contentEncoding.
Name string
// Decode given string to byte array.
Decode func(string) ([]byte, error)
}
var decoders = map[string]*Decoder{
"base64": {
Name: "base64",
Decode: func(s string) ([]byte, error) {
return base64.StdEncoding.DecodeString(s)
},
},
}
// MediaType specified how to validate bytes against specific contentMediaType.
type MediaType struct {
// Name of contentMediaType.
Name string
// Validate checks whether bytes conform to this mediatype.
Validate func([]byte) error
// UnmarshalJSON unmarshals bytes into json value.
// This must be nil if this mediatype is not compatible
// with json.
UnmarshalJSON func([]byte) (any, error)
}
var mediaTypes = map[string]*MediaType{
"application/json": {
Name: "application/json",
Validate: func(b []byte) error {
var v any
return json.Unmarshal(b, &v)
},
UnmarshalJSON: func(b []byte) (any, error) {
return UnmarshalJSON(bytes.NewReader(b))
},
},
}
+360
View File
@@ -0,0 +1,360 @@
package jsonschema
import (
"fmt"
"slices"
"strings"
)
// A Draft represents json-schema specification.
type Draft struct {
version int
url string
sch *Schema
id string // property name used to represent id
subschemas []SchemaPath // locations of subschemas
vocabPrefix string // prefix used for vocabulary
allVocabs map[string]*Schema // names of supported vocabs with its schemas
defaultVocabs []string // names of default vocabs
}
// String returns the specification url.
func (d *Draft) String() string {
return d.url
}
var (
Draft4 = &Draft{
version: 4,
url: "http://json-schema.org/draft-04/schema",
id: "id",
subschemas: []SchemaPath{
// type agonistic
schemaPath("definitions/*"),
schemaPath("not"),
schemaPath("allOf/[]"),
schemaPath("anyOf/[]"),
schemaPath("oneOf/[]"),
// object
schemaPath("properties/*"),
schemaPath("additionalProperties"),
schemaPath("patternProperties/*"),
// array
schemaPath("items"),
schemaPath("items/[]"),
schemaPath("additionalItems"),
schemaPath("dependencies/*"),
},
vocabPrefix: "",
allVocabs: map[string]*Schema{},
defaultVocabs: []string{},
}
Draft6 = &Draft{
version: 6,
url: "http://json-schema.org/draft-06/schema",
id: "$id",
subschemas: joinSubschemas(Draft4.subschemas,
schemaPath("propertyNames"),
schemaPath("contains"),
),
vocabPrefix: "",
allVocabs: map[string]*Schema{},
defaultVocabs: []string{},
}
Draft7 = &Draft{
version: 7,
url: "http://json-schema.org/draft-07/schema",
id: "$id",
subschemas: joinSubschemas(Draft6.subschemas,
schemaPath("if"),
schemaPath("then"),
schemaPath("else"),
),
vocabPrefix: "",
allVocabs: map[string]*Schema{},
defaultVocabs: []string{},
}
Draft2019 = &Draft{
version: 2019,
url: "https://json-schema.org/draft/2019-09/schema",
id: "$id",
subschemas: joinSubschemas(Draft7.subschemas,
schemaPath("$defs/*"),
schemaPath("dependentSchemas/*"),
schemaPath("unevaluatedProperties"),
schemaPath("unevaluatedItems"),
schemaPath("contentSchema"),
),
vocabPrefix: "https://json-schema.org/draft/2019-09/vocab/",
allVocabs: map[string]*Schema{
"core": nil,
"applicator": nil,
"validation": nil,
"meta-data": nil,
"format": nil,
"content": nil,
},
defaultVocabs: []string{"core", "applicator", "validation"},
}
Draft2020 = &Draft{
version: 2020,
url: "https://json-schema.org/draft/2020-12/schema",
id: "$id",
subschemas: joinSubschemas(Draft2019.subschemas,
schemaPath("prefixItems/[]"),
),
vocabPrefix: "https://json-schema.org/draft/2020-12/vocab/",
allVocabs: map[string]*Schema{
"core": nil,
"applicator": nil,
"unevaluated": nil,
"validation": nil,
"meta-data": nil,
"format-annotation": nil,
"format-assertion": nil,
"content": nil,
},
defaultVocabs: []string{"core", "applicator", "unevaluated", "validation"},
}
draftLatest = Draft2020
)
func init() {
c := NewCompiler()
c.AssertFormat()
for _, d := range []*Draft{Draft4, Draft6, Draft7, Draft2019, Draft2020} {
d.sch = c.MustCompile(d.url)
for name := range d.allVocabs {
d.allVocabs[name] = c.MustCompile(strings.TrimSuffix(d.url, "schema") + "meta/" + name)
}
}
}
func draftFromURL(url string) *Draft {
u, frag := split(url)
if frag != "" {
return nil
}
u, ok := strings.CutPrefix(u, "http://")
if !ok {
u, _ = strings.CutPrefix(u, "https://")
}
switch u {
case "json-schema.org/schema":
return draftLatest
case "json-schema.org/draft/2020-12/schema":
return Draft2020
case "json-schema.org/draft/2019-09/schema":
return Draft2019
case "json-schema.org/draft-07/schema":
return Draft7
case "json-schema.org/draft-06/schema":
return Draft6
case "json-schema.org/draft-04/schema":
return Draft4
default:
return nil
}
}
func (d *Draft) getID(obj map[string]any) string {
if d.version < 2019 {
if _, ok := obj["$ref"]; ok {
// All other properties in a "$ref" object MUST be ignored
return ""
}
}
id, ok := strVal(obj, d.id)
if !ok {
return ""
}
id, _ = split(id) // ignore fragment
return id
}
func (d *Draft) getVocabs(url url, doc any, vocabularies map[string]*Vocabulary) ([]string, error) {
if d.version < 2019 {
return nil, nil
}
obj, ok := doc.(map[string]any)
if !ok {
return nil, nil
}
v, ok := obj["$vocabulary"]
if !ok {
return nil, nil
}
obj, ok = v.(map[string]any)
if !ok {
return nil, nil
}
var vocabs []string
for vocab, reqd := range obj {
if reqd, ok := reqd.(bool); !ok || !reqd {
continue
}
name, ok := strings.CutPrefix(vocab, d.vocabPrefix)
if ok {
if _, ok := d.allVocabs[name]; ok {
if !slices.Contains(vocabs, name) {
vocabs = append(vocabs, name)
continue
}
}
}
if _, ok := vocabularies[vocab]; !ok {
return nil, &UnsupportedVocabularyError{url.String(), vocab}
}
if !slices.Contains(vocabs, vocab) {
vocabs = append(vocabs, vocab)
}
}
if !slices.Contains(vocabs, "core") {
vocabs = append(vocabs, "core")
}
return vocabs, nil
}
// --
type dialect struct {
draft *Draft
vocabs []string // nil means use draft.defaultVocabs
}
func (d *dialect) hasVocab(name string) bool {
if name == "core" || d.draft.version < 2019 {
return true
}
if d.vocabs != nil {
return slices.Contains(d.vocabs, name)
}
return slices.Contains(d.draft.defaultVocabs, name)
}
func (d *dialect) activeVocabs(assertVocabs bool, vocabularies map[string]*Vocabulary) []string {
if len(vocabularies) == 0 {
return d.vocabs
}
if d.draft.version < 2019 {
assertVocabs = true
}
if !assertVocabs {
return d.vocabs
}
var vocabs []string
if d.vocabs == nil {
vocabs = slices.Clone(d.draft.defaultVocabs)
} else {
vocabs = slices.Clone(d.vocabs)
}
for vocab := range vocabularies {
if !slices.Contains(vocabs, vocab) {
vocabs = append(vocabs, vocab)
}
}
return vocabs
}
func (d *dialect) getSchema(assertVocabs bool, vocabularies map[string]*Vocabulary) *Schema {
vocabs := d.activeVocabs(assertVocabs, vocabularies)
if vocabs == nil {
return d.draft.sch
}
var allOf []*Schema
for _, vocab := range vocabs {
sch := d.draft.allVocabs[vocab]
if sch == nil {
if v, ok := vocabularies[vocab]; ok {
sch = v.Schema
}
}
if sch != nil {
allOf = append(allOf, sch)
}
}
if !slices.Contains(vocabs, "core") {
sch := d.draft.allVocabs["core"]
if sch == nil {
sch = d.draft.sch
}
allOf = append(allOf, sch)
}
sch := &Schema{
Location: "urn:mem:metaschema",
up: urlPtr{url("urn:mem:metaschema"), ""},
DraftVersion: d.draft.version,
AllOf: allOf,
}
sch.resource = sch
if sch.DraftVersion >= 2020 {
sch.DynamicAnchor = "meta"
sch.dynamicAnchors = map[string]*Schema{
"meta": sch,
}
}
return sch
}
// --
type ParseIDError struct {
URL string
}
func (e *ParseIDError) Error() string {
return fmt.Sprintf("error in parsing id at %q", e.URL)
}
// --
type ParseAnchorError struct {
URL string
}
func (e *ParseAnchorError) Error() string {
return fmt.Sprintf("error in parsing anchor at %q", e.URL)
}
// --
type DuplicateIDError struct {
ID string
URL string
Ptr1 string
Ptr2 string
}
func (e *DuplicateIDError) Error() string {
return fmt.Sprintf("duplicate id %q in %q at %q and %q", e.ID, e.URL, e.Ptr1, e.Ptr2)
}
// --
type DuplicateAnchorError struct {
Anchor string
URL string
Ptr1 string
Ptr2 string
}
func (e *DuplicateAnchorError) Error() string {
return fmt.Sprintf("duplicate anchor %q in %q at %q and %q", e.Anchor, e.URL, e.Ptr1, e.Ptr2)
}
// --
func joinSubschemas(a1 []SchemaPath, a2 ...SchemaPath) []SchemaPath {
var a []SchemaPath
a = append(a, a1...)
a = append(a, a2...)
return a
}
+708
View File
@@ -0,0 +1,708 @@
package jsonschema
import (
"net/netip"
gourl "net/url"
"strconv"
"strings"
"time"
)
// Format defined specific format.
type Format struct {
// Name of format.
Name string
// Validate checks if given value is of this format.
Validate func(v any) error
}
var formats = map[string]*Format{
"json-pointer": {"json-pointer", validateJSONPointer},
"relative-json-pointer": {"relative-json-pointer", validateRelativeJSONPointer},
"uuid": {"uuid", validateUUID},
"duration": {"duration", validateDuration},
"period": {"period", validatePeriod},
"ipv4": {"ipv4", validateIPV4},
"ipv6": {"ipv6", validateIPV6},
"hostname": {"hostname", validateHostname},
"email": {"email", validateEmail},
"date": {"date", validateDate},
"time": {"time", validateTime},
"date-time": {"date-time", validateDateTime},
"uri": {"uri", validateURI},
"iri": {"iri", validateURI},
"uri-reference": {"uri-reference", validateURIReference},
"iri-reference": {"iri-reference", validateURIReference},
"uri-template": {"uri-template", validateURITemplate},
"semver": {"semver", validateSemver},
}
// see https://www.rfc-editor.org/rfc/rfc6901#section-3
func validateJSONPointer(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
if s == "" {
return nil
}
if !strings.HasPrefix(s, "/") {
return LocalizableError("not starting with /")
}
for _, tok := range strings.Split(s, "/")[1:] {
escape := false
for _, ch := range tok {
if escape {
escape = false
if ch != '0' && ch != '1' {
return LocalizableError("~ must be followed by 0 or 1")
}
continue
}
if ch == '~' {
escape = true
continue
}
switch {
case ch >= '\x00' && ch <= '\x2E':
case ch >= '\x30' && ch <= '\x7D':
case ch >= '\x7F' && ch <= '\U0010FFFF':
default:
return LocalizableError("invalid character %q", ch)
}
}
if escape {
return LocalizableError("~ must be followed by 0 or 1")
}
}
return nil
}
// see https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
func validateRelativeJSONPointer(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// start with non-negative-integer
numDigits := 0
for _, ch := range s {
if ch >= '0' && ch <= '9' {
numDigits++
} else {
break
}
}
if numDigits == 0 {
return LocalizableError("must start with non-negative integer")
}
if numDigits > 1 && strings.HasPrefix(s, "0") {
return LocalizableError("starts with zero")
}
s = s[numDigits:]
// followed by either json-pointer or '#'
if s == "#" {
return nil
}
return validateJSONPointer(s)
}
// see https://datatracker.ietf.org/doc/html/rfc4122#page-4
func validateUUID(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
hexGroups := []int{8, 4, 4, 4, 12}
groups := strings.Split(s, "-")
if len(groups) != len(hexGroups) {
return LocalizableError("must have %d elements", len(hexGroups))
}
for i, group := range groups {
if len(group) != hexGroups[i] {
return LocalizableError("element %d must be %d characters long", i+1, hexGroups[i])
}
for _, ch := range group {
switch {
case ch >= '0' && ch <= '9':
case ch >= 'a' && ch <= 'f':
case ch >= 'A' && ch <= 'F':
default:
return LocalizableError("non-hex character %q", ch)
}
}
}
return nil
}
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
func validateDuration(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// must start with 'P'
s, ok = strings.CutPrefix(s, "P")
if !ok {
return LocalizableError("must start with P")
}
if s == "" {
return LocalizableError("nothing after P")
}
// dur-week
if s, ok := strings.CutSuffix(s, "W"); ok {
if s == "" {
return LocalizableError("no number in week")
}
for _, ch := range s {
if ch < '0' || ch > '9' {
return LocalizableError("invalid week")
}
}
return nil
}
allUnits := []string{"YMD", "HMS"}
for i, s := range strings.Split(s, "T") {
if i != 0 && s == "" {
return LocalizableError("no time elements")
}
if i >= len(allUnits) {
return LocalizableError("more than one T")
}
units := allUnits[i]
for s != "" {
digitCount := 0
for _, ch := range s {
if ch >= '0' && ch <= '9' {
digitCount++
} else {
break
}
}
if digitCount == 0 {
return LocalizableError("missing number")
}
s = s[digitCount:]
if s == "" {
return LocalizableError("missing unit")
}
unit := s[0]
j := strings.IndexByte(units, unit)
if j == -1 {
if strings.IndexByte(allUnits[i], unit) != -1 {
return LocalizableError("unit %q out of order", unit)
}
return LocalizableError("invalid unit %q", unit)
}
units = units[j+1:]
s = s[1:]
}
}
return nil
}
func validateIPV4(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
groups := strings.Split(s, ".")
if len(groups) != 4 {
return LocalizableError("expected four decimals")
}
for _, group := range groups {
if len(group) > 1 && group[0] == '0' {
return LocalizableError("leading zeros")
}
n, err := strconv.Atoi(group)
if err != nil {
return err
}
if n < 0 || n > 255 {
return LocalizableError("decimal must be between 0 and 255")
}
}
return nil
}
func validateIPV6(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
if !strings.Contains(s, ":") {
return LocalizableError("missing colon")
}
addr, err := netip.ParseAddr(s)
if err != nil {
return err
}
if addr.Zone() != "" {
return LocalizableError("zone id is not a part of ipv6 address")
}
return nil
}
// see https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
func validateHostname(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// entire hostname (including the delimiting dots but not a trailing dot) has a maximum of 253 ASCII characters
s = strings.TrimSuffix(s, ".")
if len(s) > 253 {
return LocalizableError("more than 253 characters long")
}
// Hostnames are composed of series of labels concatenated with dots, as are all domain names
for _, label := range strings.Split(s, ".") {
// Each label must be from 1 to 63 characters long
if len(label) < 1 || len(label) > 63 {
return LocalizableError("label must be 1 to 63 characters long")
}
// labels must not start or end with a hyphen
if strings.HasPrefix(label, "-") {
return LocalizableError("label starts with hyphen")
}
if strings.HasSuffix(label, "-") {
return LocalizableError("label ends with hyphen")
}
// labels may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner),
// the digits '0' through '9', and the hyphen ('-')
for _, ch := range label {
switch {
case ch >= 'a' && ch <= 'z':
case ch >= 'A' && ch <= 'Z':
case ch >= '0' && ch <= '9':
case ch == '-':
default:
return LocalizableError("invalid character %q", ch)
}
}
}
return nil
}
// see https://en.wikipedia.org/wiki/Email_address
func validateEmail(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// entire email address to be no more than 254 characters long
if len(s) > 254 {
return LocalizableError("more than 255 characters long")
}
// email address is generally recognized as having two parts joined with an at-sign
at := strings.LastIndexByte(s, '@')
if at == -1 {
return LocalizableError("missing @")
}
local, domain := s[:at], s[at+1:]
// local part may be up to 64 characters long
if len(local) > 64 {
return LocalizableError("local part more than 64 characters long")
}
if len(local) > 1 && strings.HasPrefix(local, `"`) && strings.HasSuffix(local, `"`) {
// quoted
local := local[1 : len(local)-1]
if strings.IndexByte(local, '\\') != -1 || strings.IndexByte(local, '"') != -1 {
return LocalizableError("backslash and quote are not allowed within quoted local part")
}
} else {
// unquoted
if strings.HasPrefix(local, ".") {
return LocalizableError("starts with dot")
}
if strings.HasSuffix(local, ".") {
return LocalizableError("ends with dot")
}
// consecutive dots not allowed
if strings.Contains(local, "..") {
return LocalizableError("consecutive dots")
}
// check allowed chars
for _, ch := range local {
switch {
case ch >= 'a' && ch <= 'z':
case ch >= 'A' && ch <= 'Z':
case ch >= '0' && ch <= '9':
case strings.ContainsRune(".!#$%&'*+-/=?^_`{|}~", ch):
default:
return LocalizableError("invalid character %q", ch)
}
}
}
// domain if enclosed in brackets, must match an IP address
if strings.HasPrefix(domain, "[") && strings.HasSuffix(domain, "]") {
domain = domain[1 : len(domain)-1]
if rem, ok := strings.CutPrefix(domain, "IPv6:"); ok {
if err := validateIPV6(rem); err != nil {
return LocalizableError("invalid ipv6 address: %v", err)
}
return nil
}
if err := validateIPV4(domain); err != nil {
return LocalizableError("invalid ipv4 address: %v", err)
}
return nil
}
// domain must match the requirements for a hostname
if err := validateHostname(domain); err != nil {
return LocalizableError("invalid domain: %v", err)
}
return nil
}
// see see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
func validateDate(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
_, err := time.Parse("2006-01-02", s)
return err
}
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
// NOTE: golang time package does not support leap seconds.
func validateTime(v any) error {
str, ok := v.(string)
if !ok {
return nil
}
// min: hh:mm:ssZ
if len(str) < 9 {
return LocalizableError("less than 9 characters long")
}
if str[2] != ':' || str[5] != ':' {
return LocalizableError("missing colon in correct place")
}
// parse hh:mm:ss
var hms []int
for _, tok := range strings.SplitN(str[:8], ":", 3) {
i, err := strconv.Atoi(tok)
if err != nil {
return LocalizableError("invalid hour/min/sec")
}
if i < 0 {
return LocalizableError("non-positive hour/min/sec")
}
hms = append(hms, i)
}
if len(hms) != 3 {
return LocalizableError("missing hour/min/sec")
}
h, m, s := hms[0], hms[1], hms[2]
if h > 23 || m > 59 || s > 60 {
return LocalizableError("hour/min/sec out of range")
}
str = str[8:]
// parse sec-frac if present
if rem, ok := strings.CutPrefix(str, "."); ok {
numDigits := 0
for _, ch := range rem {
if ch >= '0' && ch <= '9' {
numDigits++
} else {
break
}
}
if numDigits == 0 {
return LocalizableError("no digits in second fraction")
}
str = rem[numDigits:]
}
if str != "z" && str != "Z" {
// parse time-numoffset
if len(str) != 6 {
return LocalizableError("offset must be 6 characters long")
}
var sign int
switch str[0] {
case '+':
sign = -1
case '-':
sign = +1
default:
return LocalizableError("offset must begin with plus/minus")
}
str = str[1:]
if str[2] != ':' {
return LocalizableError("missing colon in offset in correct place")
}
var zhm []int
for _, tok := range strings.SplitN(str, ":", 2) {
i, err := strconv.Atoi(tok)
if err != nil {
return LocalizableError("invalid hour/min in offset")
}
if i < 0 {
return LocalizableError("non-positive hour/min in offset")
}
zhm = append(zhm, i)
}
zh, zm := zhm[0], zhm[1]
if zh > 23 || zm > 59 {
return LocalizableError("hour/min in offset out of range")
}
// apply timezone
hm := (h*60 + m) + sign*(zh*60+zm)
if hm < 0 {
hm += 24 * 60
}
h, m = hm/60, hm%60
}
// check leap second
if s >= 60 && (h != 23 || m != 59) {
return LocalizableError("invalid leap second")
}
return nil
}
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
func validateDateTime(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// min: yyyy-mm-ddThh:mm:ssZ
if len(s) < 20 {
return LocalizableError("less than 20 characters long")
}
if s[10] != 't' && s[10] != 'T' {
return LocalizableError("11th character must be t or T")
}
if err := validateDate(s[:10]); err != nil {
return LocalizableError("invalid date element: %v", err)
}
if err := validateTime(s[11:]); err != nil {
return LocalizableError("invalid time element: %v", err)
}
return nil
}
func parseURL(s string) (*gourl.URL, error) {
u, err := gourl.Parse(s)
if err != nil {
return nil, err
}
// gourl does not validate ipv6 host address
hostName := u.Hostname()
if strings.Contains(hostName, ":") {
if !strings.Contains(u.Host, "[") || !strings.Contains(u.Host, "]") {
return nil, LocalizableError("ipv6 address not enclosed in brackets")
}
if err := validateIPV6(hostName); err != nil {
return nil, LocalizableError("invalid ipv6 address: %v", err)
}
}
return u, nil
}
func validateURI(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
u, err := parseURL(s)
if err != nil {
return err
}
if !u.IsAbs() {
return LocalizableError("relative url")
}
return nil
}
func validateURIReference(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
if strings.Contains(s, `\`) {
return LocalizableError(`contains \`)
}
_, err := parseURL(s)
return err
}
func validateURITemplate(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
u, err := parseURL(s)
if err != nil {
return err
}
for _, tok := range strings.Split(u.RawPath, "/") {
tok, err = decode(tok)
if err != nil {
return LocalizableError("percent decode failed: %v", err)
}
want := true
for _, ch := range tok {
var got bool
switch ch {
case '{':
got = true
case '}':
got = false
default:
continue
}
if got != want {
return LocalizableError("nested curly braces")
}
want = !want
}
if !want {
return LocalizableError("no matching closing brace")
}
}
return nil
}
func validatePeriod(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
slash := strings.IndexByte(s, '/')
if slash == -1 {
return LocalizableError("missing slash")
}
start, end := s[:slash], s[slash+1:]
if strings.HasPrefix(start, "P") {
if err := validateDuration(start); err != nil {
return LocalizableError("invalid start duration: %v", err)
}
if err := validateDateTime(end); err != nil {
return LocalizableError("invalid end date-time: %v", err)
}
} else {
if err := validateDateTime(start); err != nil {
return LocalizableError("invalid start date-time: %v", err)
}
if strings.HasPrefix(end, "P") {
if err := validateDuration(end); err != nil {
return LocalizableError("invalid end duration: %v", err)
}
} else if err := validateDateTime(end); err != nil {
return LocalizableError("invalid end date-time: %v", err)
}
}
return nil
}
// see https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions
func validateSemver(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// build --
if i := strings.IndexByte(s, '+'); i != -1 {
build := s[i+1:]
if build == "" {
return LocalizableError("build is empty")
}
for _, buildID := range strings.Split(build, ".") {
if buildID == "" {
return LocalizableError("build identifier is empty")
}
for _, ch := range buildID {
switch {
case ch >= '0' && ch <= '9':
case (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-':
default:
return LocalizableError("invalid character %q in build identifier", ch)
}
}
}
s = s[:i]
}
// pre-release --
if i := strings.IndexByte(s, '-'); i != -1 {
preRelease := s[i+1:]
for _, preReleaseID := range strings.Split(preRelease, ".") {
if preReleaseID == "" {
return LocalizableError("pre-release identifier is empty")
}
allDigits := true
for _, ch := range preReleaseID {
switch {
case ch >= '0' && ch <= '9':
case (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-':
allDigits = false
default:
return LocalizableError("invalid character %q in pre-release identifier", ch)
}
}
if allDigits && len(preReleaseID) > 1 && preReleaseID[0] == '0' {
return LocalizableError("pre-release numeric identifier starts with zero")
}
}
s = s[:i]
}
// versionCore --
versions := strings.Split(s, ".")
if len(versions) != 3 {
return LocalizableError("versionCore must have 3 numbers separated by dot")
}
names := []string{"major", "minor", "patch"}
for i, version := range versions {
if version == "" {
return LocalizableError("%s is empty", names[i])
}
if len(version) > 1 && version[0] == '0' {
return LocalizableError("%s starts with zero", names[i])
}
for _, ch := range version {
if ch < '0' || ch > '9' {
return LocalizableError("%s contains non-digit", names[i])
}
}
}
return nil
}
+8
View File
@@ -0,0 +1,8 @@
go 1.21.1
use (
.
./cmd/jv
)
// replace github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 => ./
+4
View File
@@ -0,0 +1,4 @@
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+651
View File
@@ -0,0 +1,651 @@
package kind
import (
"fmt"
"math/big"
"strings"
"golang.org/x/text/message"
)
// --
type InvalidJsonValue struct {
Value any
}
func (*InvalidJsonValue) KeywordPath() []string {
return nil
}
func (k *InvalidJsonValue) LocalizedString(p *message.Printer) string {
return p.Sprintf("invalid jsonType %T", k.Value)
}
// --
type Schema struct {
Location string
}
func (*Schema) KeywordPath() []string {
return nil
}
func (k *Schema) LocalizedString(p *message.Printer) string {
return p.Sprintf("jsonschema validation failed with %s", quote(k.Location))
}
// --
type Group struct{}
func (*Group) KeywordPath() []string {
return nil
}
func (*Group) LocalizedString(p *message.Printer) string {
return p.Sprintf("validation failed")
}
// --
type Not struct{}
func (*Not) KeywordPath() []string {
return nil
}
func (*Not) LocalizedString(p *message.Printer) string {
return p.Sprintf("'not' failed")
}
// --
type AllOf struct{}
func (*AllOf) KeywordPath() []string {
return []string{"allOf"}
}
func (*AllOf) LocalizedString(p *message.Printer) string {
return p.Sprintf("'allOf' failed")
}
// --
type AnyOf struct{}
func (*AnyOf) KeywordPath() []string {
return []string{"anyOf"}
}
func (*AnyOf) LocalizedString(p *message.Printer) string {
return p.Sprintf("'anyOf' failed")
}
// --
type OneOf struct {
// Subschemas gives indexes of Subschemas that have matched.
// Value nil, means none of the subschemas matched.
Subschemas []int
}
func (*OneOf) KeywordPath() []string {
return []string{"oneOf"}
}
func (k *OneOf) LocalizedString(p *message.Printer) string {
if len(k.Subschemas) == 0 {
return p.Sprintf("'oneOf' failed, none matched")
}
return p.Sprintf("'oneOf' failed, subschemas %d, %d matched", k.Subschemas[0], k.Subschemas[1])
}
//--
type FalseSchema struct{}
func (*FalseSchema) KeywordPath() []string {
return nil
}
func (*FalseSchema) LocalizedString(p *message.Printer) string {
return p.Sprintf("false schema")
}
// --
type RefCycle struct {
URL string
KeywordLocation1 string
KeywordLocation2 string
}
func (*RefCycle) KeywordPath() []string {
return nil
}
func (k *RefCycle) LocalizedString(p *message.Printer) string {
return p.Sprintf("both %s and %s resolve to %q causing reference cycle", k.KeywordLocation1, k.KeywordLocation2, k.URL)
}
// --
type Type struct {
Got string
Want []string
}
func (*Type) KeywordPath() []string {
return []string{"type"}
}
func (k *Type) LocalizedString(p *message.Printer) string {
want := strings.Join(k.Want, " or ")
return p.Sprintf("got %s, want %s", k.Got, want)
}
// --
type Enum struct {
Got any
Want []any
}
// KeywordPath implements jsonschema.ErrorKind.
func (*Enum) KeywordPath() []string {
return []string{"enum"}
}
func (k *Enum) LocalizedString(p *message.Printer) string {
allPrimitive := true
loop:
for _, item := range k.Want {
switch item.(type) {
case []any, map[string]any:
allPrimitive = false
break loop
}
}
if allPrimitive {
if len(k.Want) == 1 {
return p.Sprintf("value must be %s", display(k.Want[0]))
}
var want []string
for _, v := range k.Want {
want = append(want, display(v))
}
return p.Sprintf("value must be one of %s", strings.Join(want, ", "))
}
return p.Sprintf("'enum' failed")
}
// --
type Const struct {
Got any
Want any
}
func (*Const) KeywordPath() []string {
return []string{"const"}
}
func (k *Const) LocalizedString(p *message.Printer) string {
switch want := k.Want.(type) {
case []any, map[string]any:
return p.Sprintf("'const' failed")
default:
return p.Sprintf("value must be %s", display(want))
}
}
// --
type Format struct {
Got any
Want string
Err error
}
func (*Format) KeywordPath() []string {
return []string{"format"}
}
func (k *Format) LocalizedString(p *message.Printer) string {
return p.Sprintf("%s is not valid %s: %v", display(k.Got), k.Want, localizedError(k.Err, p))
}
// --
type Reference struct {
Keyword string
URL string
}
func (k *Reference) KeywordPath() []string {
return []string{k.Keyword}
}
func (*Reference) LocalizedString(p *message.Printer) string {
return p.Sprintf("validation failed")
}
// --
type MinProperties struct {
Got, Want int
}
func (*MinProperties) KeywordPath() []string {
return []string{"minProperties"}
}
func (k *MinProperties) LocalizedString(p *message.Printer) string {
return p.Sprintf("minProperties: got %d, want %d", k.Got, k.Want)
}
// --
type MaxProperties struct {
Got, Want int
}
func (*MaxProperties) KeywordPath() []string {
return []string{"maxProperties"}
}
func (k *MaxProperties) LocalizedString(p *message.Printer) string {
return p.Sprintf("maxProperties: got %d, want %d", k.Got, k.Want)
}
// --
type MinItems struct {
Got, Want int
}
func (*MinItems) KeywordPath() []string {
return []string{"minItems"}
}
func (k *MinItems) LocalizedString(p *message.Printer) string {
return p.Sprintf("minItems: got %d, want %d", k.Got, k.Want)
}
// --
type MaxItems struct {
Got, Want int
}
func (*MaxItems) KeywordPath() []string {
return []string{"maxItems"}
}
func (k *MaxItems) LocalizedString(p *message.Printer) string {
return p.Sprintf("maxItems: got %d, want %d", k.Got, k.Want)
}
// --
type AdditionalItems struct {
Count int
}
func (*AdditionalItems) KeywordPath() []string {
return []string{"additionalItems"}
}
func (k *AdditionalItems) LocalizedString(p *message.Printer) string {
return p.Sprintf("last %d additionalItem(s) not allowed", k.Count)
}
// --
type Required struct {
Missing []string
}
func (*Required) KeywordPath() []string {
return []string{"required"}
}
func (k *Required) LocalizedString(p *message.Printer) string {
if len(k.Missing) == 1 {
return p.Sprintf("missing property %s", quote(k.Missing[0]))
}
return p.Sprintf("missing properties %s", joinQuoted(k.Missing, ", "))
}
// --
type Dependency struct {
Prop string // dependency of prop that failed
Missing []string // missing props
}
func (k *Dependency) KeywordPath() []string {
return []string{"dependency", k.Prop}
}
func (k *Dependency) LocalizedString(p *message.Printer) string {
return p.Sprintf("properties %s required, if %s exists", joinQuoted(k.Missing, ", "), quote(k.Prop))
}
// --
type DependentRequired struct {
Prop string // dependency of prop that failed
Missing []string // missing props
}
func (k *DependentRequired) KeywordPath() []string {
return []string{"dependentRequired", k.Prop}
}
func (k *DependentRequired) LocalizedString(p *message.Printer) string {
return p.Sprintf("properties %s required, if %s exists", joinQuoted(k.Missing, ", "), quote(k.Prop))
}
// --
type AdditionalProperties struct {
Properties []string
}
func (*AdditionalProperties) KeywordPath() []string {
return []string{"additionalProperties"}
}
func (k *AdditionalProperties) LocalizedString(p *message.Printer) string {
return p.Sprintf("additional properties %s not allowed", joinQuoted(k.Properties, ", "))
}
// --
type PropertyNames struct {
Property string
}
func (*PropertyNames) KeywordPath() []string {
return []string{"propertyNames"}
}
func (k *PropertyNames) LocalizedString(p *message.Printer) string {
return p.Sprintf("invalid propertyName %s", quote(k.Property))
}
// --
type UniqueItems struct {
Duplicates [2]int
}
func (*UniqueItems) KeywordPath() []string {
return []string{"uniqueItems"}
}
func (k *UniqueItems) LocalizedString(p *message.Printer) string {
return p.Sprintf("items at %d and %d are equal", k.Duplicates[0], k.Duplicates[1])
}
// --
type Contains struct{}
func (*Contains) KeywordPath() []string {
return []string{"contains"}
}
func (*Contains) LocalizedString(p *message.Printer) string {
return p.Sprintf("no items match contains schema")
}
// --
type MinContains struct {
Got []int
Want int
}
func (*MinContains) KeywordPath() []string {
return []string{"minContains"}
}
func (k *MinContains) LocalizedString(p *message.Printer) string {
if len(k.Got) == 0 {
return p.Sprintf("min %d items required to match contains schema, but none matched", k.Want)
} else {
got := fmt.Sprintf("%v", k.Got)
return p.Sprintf("min %d items required to match contains schema, but matched %d items at %v", k.Want, len(k.Got), got[1:len(got)-1])
}
}
// --
type MaxContains struct {
Got []int
Want int
}
func (*MaxContains) KeywordPath() []string {
return []string{"maxContains"}
}
func (k *MaxContains) LocalizedString(p *message.Printer) string {
got := fmt.Sprintf("%v", k.Got)
return p.Sprintf("max %d items required to match contains schema, but matched %d items at %v", k.Want, len(k.Got), got[1:len(got)-1])
}
// --
type MinLength struct {
Got, Want int
}
func (*MinLength) KeywordPath() []string {
return []string{"minLength"}
}
func (k *MinLength) LocalizedString(p *message.Printer) string {
return p.Sprintf("minLength: got %d, want %d", k.Got, k.Want)
}
// --
type MaxLength struct {
Got, Want int
}
func (*MaxLength) KeywordPath() []string {
return []string{"maxLength"}
}
func (k *MaxLength) LocalizedString(p *message.Printer) string {
return p.Sprintf("maxLength: got %d, want %d", k.Got, k.Want)
}
// --
type Pattern struct {
Got string
Want string
}
func (*Pattern) KeywordPath() []string {
return []string{"pattern"}
}
func (k *Pattern) LocalizedString(p *message.Printer) string {
return p.Sprintf("%s does not match pattern %s", quote(k.Got), quote(k.Want))
}
// --
type ContentEncoding struct {
Want string
Err error
}
func (*ContentEncoding) KeywordPath() []string {
return []string{"contentEncoding"}
}
func (k *ContentEncoding) LocalizedString(p *message.Printer) string {
return p.Sprintf("value is not %s encoded: %v", quote(k.Want), localizedError(k.Err, p))
}
// --
type ContentMediaType struct {
Got []byte
Want string
Err error
}
func (*ContentMediaType) KeywordPath() []string {
return []string{"contentMediaType"}
}
func (k *ContentMediaType) LocalizedString(p *message.Printer) string {
return p.Sprintf("value is not of mediatype %s: %v", quote(k.Want), k.Err)
}
// --
type ContentSchema struct{}
func (*ContentSchema) KeywordPath() []string {
return []string{"contentSchema"}
}
func (*ContentSchema) LocalizedString(p *message.Printer) string {
return p.Sprintf("'contentSchema' failed")
}
// --
type Minimum struct {
Got *big.Rat
Want *big.Rat
}
func (*Minimum) KeywordPath() []string {
return []string{"minimum"}
}
func (k *Minimum) LocalizedString(p *message.Printer) string {
got, _ := k.Got.Float64()
want, _ := k.Want.Float64()
return p.Sprintf("minimum: got %v, want %v", got, want)
}
// --
type Maximum struct {
Got *big.Rat
Want *big.Rat
}
func (*Maximum) KeywordPath() []string {
return []string{"maximum"}
}
func (k *Maximum) LocalizedString(p *message.Printer) string {
got, _ := k.Got.Float64()
want, _ := k.Want.Float64()
return p.Sprintf("maximum: got %v, want %v", got, want)
}
// --
type ExclusiveMinimum struct {
Got *big.Rat
Want *big.Rat
}
func (*ExclusiveMinimum) KeywordPath() []string {
return []string{"exclusiveMinimum"}
}
func (k *ExclusiveMinimum) LocalizedString(p *message.Printer) string {
got, _ := k.Got.Float64()
want, _ := k.Want.Float64()
return p.Sprintf("exclusiveMinimum: got %v, want %v", got, want)
}
// --
type ExclusiveMaximum struct {
Got *big.Rat
Want *big.Rat
}
func (*ExclusiveMaximum) KeywordPath() []string {
return []string{"exclusiveMaximum"}
}
func (k *ExclusiveMaximum) LocalizedString(p *message.Printer) string {
got, _ := k.Got.Float64()
want, _ := k.Want.Float64()
return p.Sprintf("exclusiveMaximum: got %v, want %v", got, want)
}
// --
type MultipleOf struct {
Got *big.Rat
Want *big.Rat
}
func (*MultipleOf) KeywordPath() []string {
return []string{"multipleOf"}
}
func (k *MultipleOf) LocalizedString(p *message.Printer) string {
got, _ := k.Got.Float64()
want, _ := k.Want.Float64()
return p.Sprintf("multipleOf: got %v, want %v", got, want)
}
// --
func quote(s string) string {
s = fmt.Sprintf("%q", s)
s = strings.ReplaceAll(s, `\"`, `"`)
s = strings.ReplaceAll(s, `'`, `\'`)
return "'" + s[1:len(s)-1] + "'"
}
func joinQuoted(arr []string, sep string) string {
var sb strings.Builder
for _, s := range arr {
if sb.Len() > 0 {
sb.WriteString(sep)
}
sb.WriteString(quote(s))
}
return sb.String()
}
// to be used only for primitive.
func display(v any) string {
switch v := v.(type) {
case string:
return quote(v)
case []any, map[string]any:
return "value"
default:
return fmt.Sprintf("%v", v)
}
}
func localizedError(err error, p *message.Printer) string {
if err, ok := err.(interface{ LocalizedError(*message.Printer) string }); ok {
return err.LocalizedError(p)
}
return err.Error()
}
+266
View File
@@ -0,0 +1,266 @@
package jsonschema
import (
"embed"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
gourl "net/url"
"os"
"path/filepath"
"runtime"
"strings"
)
// URLLoader knows how to load json from given url.
type URLLoader interface {
// Load loads json from given absolute url.
Load(url string) (any, error)
}
// --
// FileLoader loads json file url.
type FileLoader struct{}
func (l FileLoader) Load(url string) (any, error) {
path, err := l.ToFile(url)
if err != nil {
return nil, err
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return UnmarshalJSON(f)
}
// ToFile is helper method to convert file url to file path.
func (l FileLoader) ToFile(url string) (string, error) {
u, err := gourl.Parse(url)
if err != nil {
return "", err
}
if u.Scheme != "file" {
return "", fmt.Errorf("invalid file url: %s", u)
}
path := u.Path
if runtime.GOOS == "windows" {
path = strings.TrimPrefix(path, "/")
path = filepath.FromSlash(path)
}
return path, nil
}
// --
// SchemeURLLoader delegates to other [URLLoaders]
// based on url scheme.
type SchemeURLLoader map[string]URLLoader
func (l SchemeURLLoader) Load(url string) (any, error) {
u, err := gourl.Parse(url)
if err != nil {
return nil, err
}
ll, ok := l[u.Scheme]
if !ok {
return nil, &UnsupportedURLSchemeError{u.String()}
}
return ll.Load(url)
}
// --
//go:embed metaschemas
var metaFS embed.FS
func openMeta(url string) (fs.File, error) {
u, meta := strings.CutPrefix(url, "http://json-schema.org/")
if !meta {
u, meta = strings.CutPrefix(url, "https://json-schema.org/")
}
if meta {
if u == "schema" {
return openMeta(draftLatest.url)
}
f, err := metaFS.Open("metaschemas/" + u)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil
}
return nil, err
}
return f, err
}
return nil, nil
}
func isMeta(url string) bool {
f, err := openMeta(url)
if err != nil {
return true
}
if f != nil {
f.Close()
return true
}
return false
}
func loadMeta(url string) (any, error) {
f, err := openMeta(url)
if err != nil {
return nil, err
}
if f == nil {
return nil, nil
}
defer f.Close()
return UnmarshalJSON(f)
}
// --
type defaultLoader struct {
docs map[url]any // docs loaded so far
loader URLLoader
}
func (l *defaultLoader) add(url url, doc any) bool {
if _, ok := l.docs[url]; ok {
return false
}
l.docs[url] = doc
return true
}
func (l *defaultLoader) load(url url) (any, error) {
if doc, ok := l.docs[url]; ok {
return doc, nil
}
doc, err := loadMeta(url.String())
if err != nil {
return nil, err
}
if doc != nil {
l.add(url, doc)
return doc, nil
}
if l.loader == nil {
return nil, &LoadURLError{url.String(), errors.New("no URLLoader set")}
}
doc, err = l.loader.Load(url.String())
if err != nil {
return nil, &LoadURLError{URL: url.String(), Err: err}
}
l.add(url, doc)
return doc, nil
}
func (l *defaultLoader) getDraft(up urlPtr, doc any, defaultDraft *Draft, cycle map[url]struct{}) (*Draft, error) {
obj, ok := doc.(map[string]any)
if !ok {
return defaultDraft, nil
}
sch, ok := strVal(obj, "$schema")
if !ok {
return defaultDraft, nil
}
if draft := draftFromURL(sch); draft != nil {
return draft, nil
}
sch, _ = split(sch)
if _, err := gourl.Parse(sch); err != nil {
return nil, &InvalidMetaSchemaURLError{up.String(), err}
}
schUrl := url(sch)
if up.ptr.isEmpty() && schUrl == up.url {
return nil, &UnsupportedDraftError{schUrl.String()}
}
if _, ok := cycle[schUrl]; ok {
return nil, &MetaSchemaCycleError{schUrl.String()}
}
cycle[schUrl] = struct{}{}
doc, err := l.load(schUrl)
if err != nil {
return nil, err
}
return l.getDraft(urlPtr{schUrl, ""}, doc, defaultDraft, cycle)
}
func (l *defaultLoader) getMetaVocabs(doc any, draft *Draft, vocabularies map[string]*Vocabulary) ([]string, error) {
obj, ok := doc.(map[string]any)
if !ok {
return nil, nil
}
sch, ok := strVal(obj, "$schema")
if !ok {
return nil, nil
}
if draft := draftFromURL(sch); draft != nil {
return nil, nil
}
sch, _ = split(sch)
if _, err := gourl.Parse(sch); err != nil {
return nil, &ParseURLError{sch, err}
}
schUrl := url(sch)
doc, err := l.load(schUrl)
if err != nil {
return nil, err
}
return draft.getVocabs(schUrl, doc, vocabularies)
}
// --
type LoadURLError struct {
URL string
Err error
}
func (e *LoadURLError) Error() string {
return fmt.Sprintf("failing loading %q: %v", e.URL, e.Err)
}
// --
type UnsupportedURLSchemeError struct {
url string
}
func (e *UnsupportedURLSchemeError) Error() string {
return fmt.Sprintf("no URLLoader registered for %q", e.url)
}
// --
type ResourceExistsError struct {
url string
}
func (e *ResourceExistsError) Error() string {
return fmt.Sprintf("resource for %q already exists", e.url)
}
// --
// UnmarshalJSON unmarshals into [any] without losing
// number precision using [json.Number].
func UnmarshalJSON(r io.Reader) (any, error) {
decoder := json.NewDecoder(r)
decoder.UseNumber()
var doc any
if err := decoder.Decode(&doc); err != nil {
return nil, err
}
if _, err := decoder.Token(); err == nil || err != io.EOF {
return nil, fmt.Errorf("invalid character after top-level value")
}
return doc, nil
}
@@ -0,0 +1,151 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"positiveInteger": {
"type": "integer",
"minimum": 0
},
"positiveIntegerDefault0": {
"allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
},
"simpleTypes": {
"enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
},
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uriref"
},
"$schema": {
"type": "string",
"format": "uri"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "boolean",
"default": false
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "boolean",
"default": false
},
"maxLength": { "$ref": "#/definitions/positiveInteger" },
"minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/positiveInteger" },
"minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxProperties": { "$ref": "#/definitions/positiveInteger" },
"minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" },
"format": { "type": "string" },
"$ref": { "type": "string" }
},
"dependencies": {
"exclusiveMaximum": [ "maximum" ],
"exclusiveMinimum": [ "minimum" ]
},
"default": {}
}
@@ -0,0 +1,150 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"$id": "http://json-schema.org/draft-06/schema#",
"title": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"allOf": [
{ "$ref": "#/definitions/nonNegativeInteger" },
{ "default": 0 }
]
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
},
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": { "$ref": "#" },
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"contains": { "$ref": "#" },
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": { "$ref": "#" },
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"propertyNames": { "$ref": "#" },
"const": {},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"default": {}
}
@@ -0,0 +1,172 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://json-schema.org/draft-07/schema#",
"title": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"allOf": [
{ "$ref": "#/definitions/nonNegativeInteger" },
{ "default": 0 }
]
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
},
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"$comment": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": { "$ref": "#" },
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": true
},
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"contains": { "$ref": "#" },
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": { "$ref": "#" },
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"propertyNames": { "$ref": "#" },
"const": true,
"enum": {
"type": "array",
"items": true,
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"contentMediaType": { "type": "string" },
"contentEncoding": { "type": "string" },
"if": { "$ref": "#" },
"then": { "$ref": "#" },
"else": { "$ref": "#" },
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"default": true
}
@@ -0,0 +1,55 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/applicator",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/applicator": true
},
"$recursiveAnchor": true,
"title": "Applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"additionalItems": { "$recursiveRef": "#" },
"unevaluatedItems": { "$recursiveRef": "#" },
"items": {
"anyOf": [
{ "$recursiveRef": "#" },
{ "$ref": "#/$defs/schemaArray" }
]
},
"contains": { "$recursiveRef": "#" },
"additionalProperties": { "$recursiveRef": "#" },
"unevaluatedProperties": { "$recursiveRef": "#" },
"properties": {
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependentSchemas": {
"type": "object",
"additionalProperties": {
"$recursiveRef": "#"
}
},
"propertyNames": { "$recursiveRef": "#" },
"if": { "$recursiveRef": "#" },
"then": { "$recursiveRef": "#" },
"else": { "$recursiveRef": "#" },
"allOf": { "$ref": "#/$defs/schemaArray" },
"anyOf": { "$ref": "#/$defs/schemaArray" },
"oneOf": { "$ref": "#/$defs/schemaArray" },
"not": { "$recursiveRef": "#" }
},
"$defs": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$recursiveRef": "#" }
}
}
}
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/content",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/content": true
},
"$recursiveAnchor": true,
"title": "Content vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"contentMediaType": { "type": "string" },
"contentEncoding": { "type": "string" },
"contentSchema": { "$recursiveRef": "#" }
}
}
@@ -0,0 +1,56 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/core",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/core": true
},
"$recursiveAnchor": true,
"title": "Core vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference",
"$comment": "Non-empty fragments not allowed.",
"pattern": "^[^#]*#?$"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$anchor": {
"type": "string",
"pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"$recursiveRef": {
"type": "string",
"format": "uri-reference"
},
"$recursiveAnchor": {
"type": "boolean",
"default": false
},
"$vocabulary": {
"type": "object",
"propertyNames": {
"type": "string",
"format": "uri"
},
"additionalProperties": {
"type": "boolean"
}
},
"$comment": {
"type": "string"
},
"$defs": {
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"default": {}
}
}
}
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/format",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/format": true
},
"$recursiveAnchor": true,
"title": "Format vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}
@@ -0,0 +1,35 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/meta-data",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/meta-data": true
},
"$recursiveAnchor": true,
"title": "Meta-data vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"deprecated": {
"type": "boolean",
"default": false
},
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
}
}
}
@@ -0,0 +1,97 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/validation",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/validation": true
},
"$recursiveAnchor": true,
"title": "Validation vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/$defs/nonNegativeInteger" },
"minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"maxItems": { "$ref": "#/$defs/nonNegativeInteger" },
"minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxContains": { "$ref": "#/$defs/nonNegativeInteger" },
"minContains": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 1
},
"maxProperties": { "$ref": "#/$defs/nonNegativeInteger" },
"minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/$defs/stringArray" },
"dependentRequired": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/stringArray"
}
},
"const": true,
"enum": {
"type": "array",
"items": true
},
"type": {
"anyOf": [
{ "$ref": "#/$defs/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/$defs/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
}
},
"$defs": {
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 0
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
}
}
@@ -0,0 +1,41 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/schema",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/core": true,
"https://json-schema.org/draft/2019-09/vocab/applicator": true,
"https://json-schema.org/draft/2019-09/vocab/validation": true,
"https://json-schema.org/draft/2019-09/vocab/meta-data": true,
"https://json-schema.org/draft/2019-09/vocab/format": false,
"https://json-schema.org/draft/2019-09/vocab/content": true
},
"$recursiveAnchor": true,
"title": "Core and Validation specifications meta-schema",
"allOf": [
{"$ref": "meta/core"},
{"$ref": "meta/applicator"},
{"$ref": "meta/validation"},
{"$ref": "meta/meta-data"},
{"$ref": "meta/format"},
{"$ref": "meta/content"}
],
"type": ["object", "boolean"],
"properties": {
"definitions": {
"$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.",
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"default": {}
},
"dependencies": {
"$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"",
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$recursiveRef": "#" },
{ "$ref": "meta/validation#/$defs/stringArray" }
]
}
}
}
}
@@ -0,0 +1,47 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/applicator",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/applicator": true
},
"$dynamicAnchor": "meta",
"title": "Applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"prefixItems": { "$ref": "#/$defs/schemaArray" },
"items": { "$dynamicRef": "#meta" },
"contains": { "$dynamicRef": "#meta" },
"additionalProperties": { "$dynamicRef": "#meta" },
"properties": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependentSchemas": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"default": {}
},
"propertyNames": { "$dynamicRef": "#meta" },
"if": { "$dynamicRef": "#meta" },
"then": { "$dynamicRef": "#meta" },
"else": { "$dynamicRef": "#meta" },
"allOf": { "$ref": "#/$defs/schemaArray" },
"anyOf": { "$ref": "#/$defs/schemaArray" },
"oneOf": { "$ref": "#/$defs/schemaArray" },
"not": { "$dynamicRef": "#meta" }
},
"$defs": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$dynamicRef": "#meta" }
}
}
}
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/content",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/content": true
},
"$dynamicAnchor": "meta",
"title": "Content vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"contentEncoding": { "type": "string" },
"contentMediaType": { "type": "string" },
"contentSchema": { "$dynamicRef": "#meta" }
}
}
@@ -0,0 +1,50 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/core",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true
},
"$dynamicAnchor": "meta",
"title": "Core vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"$id": {
"$ref": "#/$defs/uriReferenceString",
"$comment": "Non-empty fragments not allowed.",
"pattern": "^[^#]*#?$"
},
"$schema": { "$ref": "#/$defs/uriString" },
"$ref": { "$ref": "#/$defs/uriReferenceString" },
"$anchor": { "$ref": "#/$defs/anchorString" },
"$dynamicRef": { "$ref": "#/$defs/uriReferenceString" },
"$dynamicAnchor": { "$ref": "#/$defs/anchorString" },
"$vocabulary": {
"type": "object",
"propertyNames": { "$ref": "#/$defs/uriString" },
"additionalProperties": {
"type": "boolean"
}
},
"$comment": {
"type": "string"
},
"$defs": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" }
}
},
"$defs": {
"anchorString": {
"type": "string",
"pattern": "^[A-Za-z_][-A-Za-z0-9._]*$"
},
"uriString": {
"type": "string",
"format": "uri"
},
"uriReferenceString": {
"type": "string",
"format": "uri-reference"
}
}
}
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/format-annotation",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true
},
"$dynamicAnchor": "meta",
"title": "Format vocabulary meta-schema for annotation results",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/format-assertion",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/format-assertion": true
},
"$dynamicAnchor": "meta",
"title": "Format vocabulary meta-schema for assertion results",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}
@@ -0,0 +1,35 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/meta-data",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/meta-data": true
},
"$dynamicAnchor": "meta",
"title": "Meta-data vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"deprecated": {
"type": "boolean",
"default": false
},
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
}
}
}
@@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/unevaluated",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true
},
"$dynamicAnchor": "meta",
"title": "Unevaluated applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"unevaluatedItems": { "$dynamicRef": "#meta" },
"unevaluatedProperties": { "$dynamicRef": "#meta" }
}
}
@@ -0,0 +1,97 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/validation",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/validation": true
},
"$dynamicAnchor": "meta",
"title": "Validation vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"type": {
"anyOf": [
{ "$ref": "#/$defs/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/$defs/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"const": true,
"enum": {
"type": "array",
"items": true
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/$defs/nonNegativeInteger" },
"minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"maxItems": { "$ref": "#/$defs/nonNegativeInteger" },
"minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxContains": { "$ref": "#/$defs/nonNegativeInteger" },
"minContains": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 1
},
"maxProperties": { "$ref": "#/$defs/nonNegativeInteger" },
"minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/$defs/stringArray" },
"dependentRequired": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/stringArray"
}
}
},
"$defs": {
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 0
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
}
}
@@ -0,0 +1,57 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/schema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://json-schema.org/draft/2020-12/vocab/meta-data": true,
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
"https://json-schema.org/draft/2020-12/vocab/content": true
},
"$dynamicAnchor": "meta",
"title": "Core and Validation specifications meta-schema",
"allOf": [
{"$ref": "meta/core"},
{"$ref": "meta/applicator"},
{"$ref": "meta/unevaluated"},
{"$ref": "meta/validation"},
{"$ref": "meta/meta-data"},
{"$ref": "meta/format-annotation"},
{"$ref": "meta/content"}
],
"type": ["object", "boolean"],
"$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.",
"properties": {
"definitions": {
"$comment": "\"definitions\" has been replaced by \"$defs\".",
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"deprecated": true,
"default": {}
},
"dependencies": {
"$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.",
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$dynamicRef": "#meta" },
{ "$ref": "meta/validation#/$defs/stringArray" }
]
},
"deprecated": true,
"default": {}
},
"$recursiveAnchor": {
"$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".",
"$ref": "meta/core#/$defs/anchorString",
"deprecated": true
},
"$recursiveRef": {
"$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".",
"$ref": "meta/core#/$defs/uriReferenceString",
"deprecated": true
}
}
}
+553
View File
@@ -0,0 +1,553 @@
package jsonschema
import (
"encoding/json"
"fmt"
"math/big"
"strconv"
)
type objCompiler struct {
c *Compiler
obj map[string]any
up urlPtr
r *root
res *resource
q *queue
}
func (c *objCompiler) compile(s *Schema) error {
// id --
if id := c.res.dialect.draft.getID(c.obj); id != "" {
s.ID = id
}
// anchor --
if s.DraftVersion < 2019 {
// anchor is specified in id
id := c.string(c.res.dialect.draft.id)
if id != "" {
_, f := split(id)
if f != "" {
var err error
s.Anchor, err = decode(f)
if err != nil {
return &ParseAnchorError{URL: s.Location}
}
}
}
} else {
s.Anchor = c.string("$anchor")
}
if err := c.compileDraft4(s); err != nil {
return err
}
if s.DraftVersion >= 6 {
if err := c.compileDraft6(s); err != nil {
return err
}
}
if s.DraftVersion >= 7 {
if err := c.compileDraft7(s); err != nil {
return err
}
}
if s.DraftVersion >= 2019 {
if err := c.compileDraft2019(s); err != nil {
return err
}
}
if s.DraftVersion >= 2020 {
if err := c.compileDraft2020(s); err != nil {
return err
}
}
// vocabularies
vocabs := c.res.dialect.activeVocabs(c.c.roots.assertVocabs, c.c.roots.vocabularies)
for _, vocab := range vocabs {
v := c.c.roots.vocabularies[vocab]
if v == nil {
continue
}
ext, err := v.Compile(&CompilerContext{c}, c.obj)
if err != nil {
return err
}
if ext != nil {
s.Extensions = append(s.Extensions, ext)
}
}
return nil
}
func (c *objCompiler) compileDraft4(s *Schema) error {
var err error
if c.hasVocab("core") {
if s.Ref, err = c.enqueueRef("$ref"); err != nil {
return err
}
if s.DraftVersion < 2019 && s.Ref != nil {
// All other properties in a "$ref" object MUST be ignored
return nil
}
}
if c.hasVocab("applicator") {
s.AllOf = c.enqueueArr("allOf")
s.AnyOf = c.enqueueArr("anyOf")
s.OneOf = c.enqueueArr("oneOf")
s.Not = c.enqueueProp("not")
if s.DraftVersion < 2020 {
if items, ok := c.obj["items"]; ok {
if _, ok := items.([]any); ok {
s.Items = c.enqueueArr("items")
s.AdditionalItems = c.enqueueAdditional("additionalItems")
} else {
s.Items = c.enqueueProp("items")
}
}
}
s.Properties = c.enqueueMap("properties")
if m := c.enqueueMap("patternProperties"); m != nil {
s.PatternProperties = map[Regexp]*Schema{}
for pname, sch := range m {
re, err := c.c.roots.regexpEngine(pname)
if err != nil {
return &InvalidRegexError{c.up.format("patternProperties"), pname, err}
}
s.PatternProperties[re] = sch
}
}
s.AdditionalProperties = c.enqueueAdditional("additionalProperties")
if m := c.objVal("dependencies"); m != nil {
s.Dependencies = map[string]any{}
for pname, pvalue := range m {
if arr, ok := pvalue.([]any); ok {
s.Dependencies[pname] = toStrings(arr)
} else {
ptr := c.up.ptr.append2("dependencies", pname)
s.Dependencies[pname] = c.enqueuePtr(ptr)
}
}
}
}
if c.hasVocab("validation") {
if t, ok := c.obj["type"]; ok {
s.Types = newTypes(t)
}
if arr := c.arrVal("enum"); arr != nil {
s.Enum = newEnum(arr)
}
s.MultipleOf = c.numVal("multipleOf")
s.Maximum = c.numVal("maximum")
if c.boolean("exclusiveMaximum") {
s.ExclusiveMaximum = s.Maximum
s.Maximum = nil
} else {
s.ExclusiveMaximum = c.numVal("exclusiveMaximum")
}
s.Minimum = c.numVal("minimum")
if c.boolean("exclusiveMinimum") {
s.ExclusiveMinimum = s.Minimum
s.Minimum = nil
} else {
s.ExclusiveMinimum = c.numVal("exclusiveMinimum")
}
s.MinLength = c.intVal("minLength")
s.MaxLength = c.intVal("maxLength")
if pat := c.strVal("pattern"); pat != nil {
s.Pattern, err = c.c.roots.regexpEngine(*pat)
if err != nil {
return &InvalidRegexError{c.up.format("pattern"), *pat, err}
}
}
s.MinItems = c.intVal("minItems")
s.MaxItems = c.intVal("maxItems")
s.UniqueItems = c.boolean("uniqueItems")
s.MaxProperties = c.intVal("maxProperties")
s.MinProperties = c.intVal("minProperties")
if arr := c.arrVal("required"); arr != nil {
s.Required = toStrings(arr)
}
}
// format --
if c.assertFormat(s.DraftVersion) {
if f := c.strVal("format"); f != nil {
if *f == "regex" {
s.Format = &Format{
Name: "regex",
Validate: c.c.roots.regexpEngine.validate,
}
} else {
s.Format = c.c.formats[*f]
if s.Format == nil {
s.Format = formats[*f]
}
}
}
}
// annotations --
s.Title = c.string("title")
s.Description = c.string("description")
if v, ok := c.obj["default"]; ok {
s.Default = &v
}
return nil
}
func (c *objCompiler) compileDraft6(s *Schema) error {
if c.hasVocab("applicator") {
s.Contains = c.enqueueProp("contains")
s.PropertyNames = c.enqueueProp("propertyNames")
}
if c.hasVocab("validation") {
if v, ok := c.obj["const"]; ok {
s.Const = &v
}
}
return nil
}
func (c *objCompiler) compileDraft7(s *Schema) error {
if c.hasVocab("applicator") {
s.If = c.enqueueProp("if")
if s.If != nil {
b := c.boolVal("if")
if b == nil || *b {
s.Then = c.enqueueProp("then")
}
if b == nil || !*b {
s.Else = c.enqueueProp("else")
}
}
}
if c.c.assertContent {
if ce := c.strVal("contentEncoding"); ce != nil {
s.ContentEncoding = c.c.decoders[*ce]
if s.ContentEncoding == nil {
s.ContentEncoding = decoders[*ce]
}
}
if cm := c.strVal("contentMediaType"); cm != nil {
s.ContentMediaType = c.c.mediaTypes[*cm]
if s.ContentMediaType == nil {
s.ContentMediaType = mediaTypes[*cm]
}
}
}
// annotations --
s.Comment = c.string("$comment")
s.ReadOnly = c.boolean("readOnly")
s.WriteOnly = c.boolean("writeOnly")
if arr, ok := c.obj["examples"].([]any); ok {
s.Examples = arr
}
return nil
}
func (c *objCompiler) compileDraft2019(s *Schema) error {
var err error
if c.hasVocab("core") {
if s.RecursiveRef, err = c.enqueueRef("$recursiveRef"); err != nil {
return err
}
s.RecursiveAnchor = c.boolean("$recursiveAnchor")
}
if c.hasVocab("validation") {
if s.Contains != nil {
s.MinContains = c.intVal("minContains")
s.MaxContains = c.intVal("maxContains")
}
if m := c.objVal("dependentRequired"); m != nil {
s.DependentRequired = map[string][]string{}
for pname, pvalue := range m {
if arr, ok := pvalue.([]any); ok {
s.DependentRequired[pname] = toStrings(arr)
}
}
}
}
if c.hasVocab("applicator") {
s.DependentSchemas = c.enqueueMap("dependentSchemas")
}
var unevaluated bool
if s.DraftVersion == 2019 {
unevaluated = c.hasVocab("applicator")
} else {
unevaluated = c.hasVocab("unevaluated")
}
if unevaluated {
s.UnevaluatedItems = c.enqueueProp("unevaluatedItems")
s.UnevaluatedProperties = c.enqueueProp("unevaluatedProperties")
}
if c.c.assertContent {
if s.ContentMediaType != nil && s.ContentMediaType.UnmarshalJSON != nil {
s.ContentSchema = c.enqueueProp("contentSchema")
}
}
// annotations --
s.Deprecated = c.boolean("deprecated")
return nil
}
func (c *objCompiler) compileDraft2020(s *Schema) error {
if c.hasVocab("core") {
sch, err := c.enqueueRef("$dynamicRef")
if err != nil {
return err
}
if sch != nil {
dref := c.strVal("$dynamicRef")
_, frag, err := splitFragment(*dref)
if err != nil {
return err
}
var anch string
if anchor, ok := frag.convert().(anchor); ok {
anch = string(anchor)
}
s.DynamicRef = &DynamicRef{sch, anch}
}
s.DynamicAnchor = c.string("$dynamicAnchor")
}
if c.hasVocab("applicator") {
s.PrefixItems = c.enqueueArr("prefixItems")
s.Items2020 = c.enqueueProp("items")
}
return nil
}
// enqueue helpers --
func (c *objCompiler) enqueuePtr(ptr jsonPointer) *Schema {
up := urlPtr{c.up.url, ptr}
return c.c.enqueue(c.q, up)
}
func (c *objCompiler) enqueueRef(pname string) (*Schema, error) {
ref := c.strVal(pname)
if ref == nil {
return nil, nil
}
return c.enqueueRefVal(*ref)
}
func (c *objCompiler) enqueueRefVal(ref string) (*Schema, error) {
baseURL := c.res.id
// baseURL := c.r.baseURL(c.up.ptr)
uf, err := baseURL.join(ref)
if err != nil {
return nil, err
}
up, err := c.r.resolve(*uf)
if err != nil {
return nil, err
}
if up != nil {
// local ref
return c.enqueuePtr(up.ptr), nil
}
// remote ref
up_, err := c.c.roots.resolveFragment(*uf)
if err != nil {
return nil, err
}
return c.c.enqueue(c.q, up_), nil
}
func (c *objCompiler) enqueueProp(pname string) *Schema {
if _, ok := c.obj[pname]; !ok {
return nil
}
ptr := c.up.ptr.append(pname)
return c.enqueuePtr(ptr)
}
func (c *objCompiler) enqueueArr(pname string) []*Schema {
arr := c.arrVal(pname)
if arr == nil {
return nil
}
sch := make([]*Schema, len(arr))
for i := range arr {
ptr := c.up.ptr.append2(pname, strconv.Itoa(i))
sch[i] = c.enqueuePtr(ptr)
}
return sch
}
func (c *objCompiler) enqueueMap(pname string) map[string]*Schema {
obj := c.objVal(pname)
if obj == nil {
return nil
}
sch := make(map[string]*Schema)
for k := range obj {
ptr := c.up.ptr.append2(pname, k)
sch[k] = c.enqueuePtr(ptr)
}
return sch
}
func (c *objCompiler) enqueueAdditional(pname string) any {
if b := c.boolVal(pname); b != nil {
return *b
}
if sch := c.enqueueProp(pname); sch != nil {
return sch
}
return nil
}
// --
func (c *objCompiler) hasVocab(name string) bool {
return c.res.dialect.hasVocab(name)
}
func (c *objCompiler) assertFormat(draftVersion int) bool {
if c.c.assertFormat || draftVersion < 2019 {
return true
}
if draftVersion == 2019 {
return c.hasVocab("format")
} else {
return c.hasVocab("format-assertion")
}
}
// value helpers --
func (c *objCompiler) boolVal(pname string) *bool {
v, ok := c.obj[pname]
if !ok {
return nil
}
b, ok := v.(bool)
if !ok {
return nil
}
return &b
}
func (c *objCompiler) boolean(pname string) bool {
b := c.boolVal(pname)
return b != nil && *b
}
func (c *objCompiler) strVal(pname string) *string {
v, ok := c.obj[pname]
if !ok {
return nil
}
s, ok := v.(string)
if !ok {
return nil
}
return &s
}
func (c *objCompiler) string(pname string) string {
if s := c.strVal(pname); s != nil {
return *s
}
return ""
}
func (c *objCompiler) numVal(pname string) *big.Rat {
v, ok := c.obj[pname]
if !ok {
return nil
}
switch v.(type) {
case json.Number, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
if n, ok := new(big.Rat).SetString(fmt.Sprint(v)); ok {
return n
}
}
return nil
}
func (c *objCompiler) intVal(pname string) *int {
if n := c.numVal(pname); n != nil && n.IsInt() {
n := int(n.Num().Int64())
return &n
}
return nil
}
func (c *objCompiler) objVal(pname string) map[string]any {
v, ok := c.obj[pname]
if !ok {
return nil
}
obj, ok := v.(map[string]any)
if !ok {
return nil
}
return obj
}
func (c *objCompiler) arrVal(pname string) []any {
v, ok := c.obj[pname]
if !ok {
return nil
}
arr, ok := v.([]any)
if !ok {
return nil
}
return arr
}
// --
type InvalidRegexError struct {
URL string
Regex string
Err error
}
func (e *InvalidRegexError) Error() string {
return fmt.Sprintf("invalid regex %q at %q: %v", e.Regex, e.URL, e.Err)
}
// --
func toStrings(arr []any) []string {
var strings []string
for _, item := range arr {
if s, ok := item.(string); ok {
strings = append(strings, s)
}
}
return strings
}
+216
View File
@@ -0,0 +1,216 @@
package jsonschema
import (
"encoding/json"
"fmt"
"strings"
"github.com/santhosh-tekuri/jsonschema/v6/kind"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
var defaultPrinter = message.NewPrinter(language.English)
// format ---
func (e *ValidationError) schemaURL() string {
if ref, ok := e.ErrorKind.(*kind.Reference); ok {
return ref.URL
} else {
return e.SchemaURL
}
}
func (e *ValidationError) absoluteKeywordLocation() string {
var schemaURL string
var keywordPath []string
if ref, ok := e.ErrorKind.(*kind.Reference); ok {
schemaURL = ref.URL
keywordPath = nil
} else {
schemaURL = e.SchemaURL
keywordPath = e.ErrorKind.KeywordPath()
}
return fmt.Sprintf("%s%s", schemaURL, encode(jsonPtr(keywordPath)))
}
func (e *ValidationError) skip() bool {
if len(e.Causes) == 1 {
_, ok := e.ErrorKind.(*kind.Reference)
return ok
}
return false
}
func (e *ValidationError) display(sb *strings.Builder, verbose bool, indent int, absKwLoc string, p *message.Printer) {
if !e.skip() {
if indent > 0 {
sb.WriteByte('\n')
for i := 0; i < indent-1; i++ {
sb.WriteString(" ")
}
sb.WriteString("- ")
}
indent = indent + 1
prevAbsKwLoc := absKwLoc
absKwLoc = e.absoluteKeywordLocation()
if _, ok := e.ErrorKind.(*kind.Schema); ok {
sb.WriteString(e.ErrorKind.LocalizedString(p))
} else {
sb.WriteString(p.Sprintf("at %s", quote(jsonPtr(e.InstanceLocation))))
if verbose {
schLoc := absKwLoc
if prevAbsKwLoc != "" {
pu, _ := split(prevAbsKwLoc)
u, f := split(absKwLoc)
if u == pu {
schLoc = fmt.Sprintf("S#%s", f)
}
}
fmt.Fprintf(sb, " [%s]", schLoc)
}
fmt.Fprintf(sb, ": %s", e.ErrorKind.LocalizedString(p))
}
}
for _, cause := range e.Causes {
cause.display(sb, verbose, indent, absKwLoc, p)
}
}
func (e *ValidationError) Error() string {
return e.LocalizedError(defaultPrinter)
}
func (e *ValidationError) LocalizedError(p *message.Printer) string {
var sb strings.Builder
e.display(&sb, false, 0, "", p)
return sb.String()
}
func (e *ValidationError) GoString() string {
return e.LocalizedGoString(defaultPrinter)
}
func (e *ValidationError) LocalizedGoString(p *message.Printer) string {
var sb strings.Builder
e.display(&sb, true, 0, "", p)
return sb.String()
}
func jsonPtr(tokens []string) string {
var sb strings.Builder
for _, tok := range tokens {
sb.WriteByte('/')
sb.WriteString(escape(tok))
}
return sb.String()
}
// --
// Flag is output format with simple boolean property valid.
type FlagOutput struct {
Valid bool `json:"valid"`
}
// The `Flag` output format, merely the boolean result.
func (e *ValidationError) FlagOutput() *FlagOutput {
return &FlagOutput{Valid: false}
}
// --
type OutputUnit struct {
Valid bool `json:"valid"`
KeywordLocation string `json:"keywordLocation"`
AbsoluteKeywordLocation string `json:"AbsoluteKeywordLocation,omitempty"`
InstanceLocation string `json:"instanceLocation"`
Error *OutputError `json:"error,omitempty"`
Errors []OutputUnit `json:"errors,omitempty"`
}
type OutputError struct {
Kind ErrorKind
p *message.Printer
}
func (k OutputError) String() string {
return k.Kind.LocalizedString(k.p)
}
func (k OutputError) MarshalJSON() ([]byte, error) {
return json.Marshal(k.Kind.LocalizedString(k.p))
}
// The `Basic` structure, a flat list of output units.
func (e *ValidationError) BasicOutput() *OutputUnit {
return e.LocalizedBasicOutput(defaultPrinter)
}
func (e *ValidationError) LocalizedBasicOutput(p *message.Printer) *OutputUnit {
out := e.output(true, false, "", "", p)
return &out
}
// The `Detailed` structure, based on the schema.
func (e *ValidationError) DetailedOutput() *OutputUnit {
return e.LocalizedDetailedOutput(defaultPrinter)
}
func (e *ValidationError) LocalizedDetailedOutput(p *message.Printer) *OutputUnit {
out := e.output(false, false, "", "", p)
return &out
}
func (e *ValidationError) output(flatten, inRef bool, schemaURL, kwLoc string, p *message.Printer) OutputUnit {
if !inRef {
if _, ok := e.ErrorKind.(*kind.Reference); ok {
inRef = true
}
}
if schemaURL != "" {
kwLoc += e.SchemaURL[len(schemaURL):]
if ref, ok := e.ErrorKind.(*kind.Reference); ok {
kwLoc += jsonPtr(ref.KeywordPath())
}
}
schemaURL = e.schemaURL()
keywordLocation := kwLoc
if _, ok := e.ErrorKind.(*kind.Reference); !ok {
keywordLocation += jsonPtr(e.ErrorKind.KeywordPath())
}
out := OutputUnit{
Valid: false,
InstanceLocation: jsonPtr(e.InstanceLocation),
KeywordLocation: keywordLocation,
}
if inRef {
out.AbsoluteKeywordLocation = e.absoluteKeywordLocation()
}
for _, cause := range e.Causes {
causeOut := cause.output(flatten, inRef, schemaURL, kwLoc, p)
if cause.skip() {
causeOut = causeOut.Errors[0]
}
if flatten {
errors := causeOut.Errors
causeOut.Errors = nil
causeOut.Error = &OutputError{cause.ErrorKind, p}
out.Errors = append(out.Errors, causeOut)
if len(errors) > 0 {
out.Errors = append(out.Errors, errors...)
}
} else {
out.Errors = append(out.Errors, causeOut)
}
}
if len(out.Errors) == 0 {
out.Error = &OutputError{e.ErrorKind, p}
}
return out
}
+142
View File
@@ -0,0 +1,142 @@
package jsonschema
import (
"strconv"
"strings"
)
// Position tells possible tokens in json.
type Position interface {
collect(v any, ptr jsonPointer) map[jsonPointer]any
}
// --
type AllProp struct{}
func (AllProp) collect(v any, ptr jsonPointer) map[jsonPointer]any {
obj, ok := v.(map[string]any)
if !ok {
return nil
}
m := map[jsonPointer]any{}
for pname, pvalue := range obj {
m[ptr.append(pname)] = pvalue
}
return m
}
// --
type AllItem struct{}
func (AllItem) collect(v any, ptr jsonPointer) map[jsonPointer]any {
arr, ok := v.([]any)
if !ok {
return nil
}
m := map[jsonPointer]any{}
for i, item := range arr {
m[ptr.append(strconv.Itoa(i))] = item
}
return m
}
// --
type Prop string
func (p Prop) collect(v any, ptr jsonPointer) map[jsonPointer]any {
obj, ok := v.(map[string]any)
if !ok {
return nil
}
pvalue, ok := obj[string(p)]
if !ok {
return nil
}
return map[jsonPointer]any{
ptr.append(string(p)): pvalue,
}
}
// --
type Item int
func (i Item) collect(v any, ptr jsonPointer) map[jsonPointer]any {
arr, ok := v.([]any)
if !ok {
return nil
}
if i < 0 || int(i) >= len(arr) {
return nil
}
return map[jsonPointer]any{
ptr.append(strconv.Itoa(int(i))): arr[int(i)],
}
}
// --
// SchemaPath tells where to look for subschema inside keyword.
type SchemaPath []Position
func schemaPath(path string) SchemaPath {
var sp SchemaPath
for _, tok := range strings.Split(path, "/") {
var pos Position
switch tok {
case "*":
pos = AllProp{}
case "[]":
pos = AllItem{}
default:
if i, err := strconv.Atoi(tok); err == nil {
pos = Item(i)
} else {
pos = Prop(tok)
}
}
sp = append(sp, pos)
}
return sp
}
func (sp SchemaPath) collect(v any, ptr jsonPointer) map[jsonPointer]any {
if len(sp) == 0 {
return map[jsonPointer]any{
ptr: v,
}
}
p, sp := sp[0], sp[1:]
m := p.collect(v, ptr)
mm := map[jsonPointer]any{}
for ptr, v := range m {
m = sp.collect(v, ptr)
for k, v := range m {
mm[k] = v
}
}
return mm
}
func (sp SchemaPath) String() string {
var sb strings.Builder
for _, pos := range sp {
if sb.Len() != 0 {
sb.WriteByte('/')
}
switch pos := pos.(type) {
case AllProp:
sb.WriteString("*")
case AllItem:
sb.WriteString("[]")
case Prop:
sb.WriteString(string(pos))
case Item:
sb.WriteString(strconv.Itoa(int(pos)))
}
}
return sb.String()
}
+202
View File
@@ -0,0 +1,202 @@
package jsonschema
import (
"fmt"
"slices"
"strings"
)
type root struct {
url url
doc any
resources map[jsonPointer]*resource
subschemasProcessed map[jsonPointer]struct{}
}
func (r *root) rootResource() *resource {
return r.resources[""]
}
func (r *root) resource(ptr jsonPointer) *resource {
for {
if res, ok := r.resources[ptr]; ok {
return res
}
slash := strings.LastIndexByte(string(ptr), '/')
if slash == -1 {
break
}
ptr = ptr[:slash]
}
return r.rootResource()
}
func (r *root) resolveFragmentIn(frag fragment, res *resource) (urlPtr, error) {
var ptr jsonPointer
switch f := frag.convert().(type) {
case jsonPointer:
ptr = res.ptr.concat(f)
case anchor:
aptr, ok := res.anchors[f]
if !ok {
return urlPtr{}, &AnchorNotFoundError{
URL: r.url.String(),
Reference: (&urlFrag{res.id, frag}).String(),
}
}
ptr = aptr
}
return urlPtr{r.url, ptr}, nil
}
func (r *root) resolveFragment(frag fragment) (urlPtr, error) {
return r.resolveFragmentIn(frag, r.rootResource())
}
// resolves urlFrag to urlPtr from root.
// returns nil if it is external.
func (r *root) resolve(uf urlFrag) (*urlPtr, error) {
var res *resource
if uf.url == r.url {
res = r.rootResource()
} else {
// look for resource with id==uf.url
for _, v := range r.resources {
if v.id == uf.url {
res = v
break
}
}
if res == nil {
return nil, nil // external url
}
}
up, err := r.resolveFragmentIn(uf.frag, res)
return &up, err
}
func (r *root) collectAnchors(sch any, schPtr jsonPointer, res *resource) error {
obj, ok := sch.(map[string]any)
if !ok {
return nil
}
addAnchor := func(anchor anchor) error {
ptr1, ok := res.anchors[anchor]
if ok {
if ptr1 == schPtr {
// anchor with same root_ptr already exists
return nil
}
return &DuplicateAnchorError{
string(anchor), r.url.String(), string(ptr1), string(schPtr),
}
}
res.anchors[anchor] = schPtr
return nil
}
if res.dialect.draft.version < 2019 {
if _, ok := obj["$ref"]; ok {
// All other properties in a "$ref" object MUST be ignored
return nil
}
// anchor is specified in id
if id, ok := strVal(obj, res.dialect.draft.id); ok {
_, frag, err := splitFragment(id)
if err != nil {
loc := urlPtr{r.url, schPtr}
return &ParseAnchorError{loc.String()}
}
if anchor, ok := frag.convert().(anchor); ok {
if err := addAnchor(anchor); err != nil {
return err
}
}
}
}
if res.dialect.draft.version >= 2019 {
if s, ok := strVal(obj, "$anchor"); ok {
if err := addAnchor(anchor(s)); err != nil {
return err
}
}
}
if res.dialect.draft.version >= 2020 {
if s, ok := strVal(obj, "$dynamicAnchor"); ok {
if err := addAnchor(anchor(s)); err != nil {
return err
}
res.dynamicAnchors = append(res.dynamicAnchors, anchor(s))
}
}
return nil
}
func (r *root) clone() *root {
processed := map[jsonPointer]struct{}{}
for k := range r.subschemasProcessed {
processed[k] = struct{}{}
}
resources := map[jsonPointer]*resource{}
for k, v := range r.resources {
resources[k] = v.clone()
}
return &root{
url: r.url,
doc: r.doc,
resources: resources,
subschemasProcessed: processed,
}
}
// --
type resource struct {
ptr jsonPointer
id url
dialect dialect
anchors map[anchor]jsonPointer
dynamicAnchors []anchor
}
func newResource(ptr jsonPointer, id url) *resource {
return &resource{ptr: ptr, id: id, anchors: make(map[anchor]jsonPointer)}
}
func (res *resource) clone() *resource {
anchors := map[anchor]jsonPointer{}
for k, v := range res.anchors {
anchors[k] = v
}
return &resource{
ptr: res.ptr,
id: res.id,
dialect: res.dialect,
anchors: anchors,
dynamicAnchors: slices.Clone(res.dynamicAnchors),
}
}
//--
type UnsupportedVocabularyError struct {
URL string
Vocabulary string
}
func (e *UnsupportedVocabularyError) Error() string {
return fmt.Sprintf("unsupported vocabulary %q in %q", e.Vocabulary, e.URL)
}
// --
type AnchorNotFoundError struct {
URL string
Reference string
}
func (e *AnchorNotFoundError) Error() string {
return fmt.Sprintf("anchor in %q not found in schema %q", e.Reference, e.URL)
}
+286
View File
@@ -0,0 +1,286 @@
package jsonschema
import (
"fmt"
"strings"
)
type roots struct {
defaultDraft *Draft
roots map[url]*root
loader defaultLoader
regexpEngine RegexpEngine
vocabularies map[string]*Vocabulary
assertVocabs bool
}
func newRoots() *roots {
return &roots{
defaultDraft: draftLatest,
roots: map[url]*root{},
loader: defaultLoader{
docs: map[url]any{},
loader: FileLoader{},
},
regexpEngine: goRegexpCompile,
vocabularies: map[string]*Vocabulary{},
}
}
func (rr *roots) orLoad(u url) (*root, error) {
if r, ok := rr.roots[u]; ok {
return r, nil
}
doc, err := rr.loader.load(u)
if err != nil {
return nil, err
}
return rr.addRoot(u, doc)
}
func (rr *roots) addRoot(u url, doc any) (*root, error) {
r := &root{
url: u,
doc: doc,
resources: map[jsonPointer]*resource{},
subschemasProcessed: map[jsonPointer]struct{}{},
}
if err := rr.collectResources(r, doc, u, "", dialect{rr.defaultDraft, nil}); err != nil {
return nil, err
}
if !strings.HasPrefix(u.String(), "http://json-schema.org/") &&
!strings.HasPrefix(u.String(), "https://json-schema.org/") {
if err := rr.validate(r, doc, ""); err != nil {
return nil, err
}
}
rr.roots[u] = r
return r, nil
}
func (rr *roots) resolveFragment(uf urlFrag) (urlPtr, error) {
r, err := rr.orLoad(uf.url)
if err != nil {
return urlPtr{}, err
}
return r.resolveFragment(uf.frag)
}
func (rr *roots) collectResources(r *root, sch any, base url, schPtr jsonPointer, fallback dialect) error {
if _, ok := r.subschemasProcessed[schPtr]; ok {
return nil
}
if err := rr._collectResources(r, sch, base, schPtr, fallback); err != nil {
return err
}
r.subschemasProcessed[schPtr] = struct{}{}
return nil
}
func (rr *roots) _collectResources(r *root, sch any, base url, schPtr jsonPointer, fallback dialect) error {
obj, ok := sch.(map[string]any)
if !ok {
if schPtr.isEmpty() {
// root resource
res := newResource(schPtr, base)
res.dialect = fallback
r.resources[schPtr] = res
}
return nil
}
hasSchema := false
if sch, ok := obj["$schema"]; ok {
if _, ok := sch.(string); ok {
hasSchema = true
}
}
draft, err := rr.loader.getDraft(urlPtr{r.url, schPtr}, sch, fallback.draft, map[url]struct{}{})
if err != nil {
return err
}
id := draft.getID(obj)
if id == "" && !schPtr.isEmpty() {
// ignore $schema
draft = fallback.draft
hasSchema = false
id = draft.getID(obj)
}
var res *resource
if id != "" {
uf, err := base.join(id)
if err != nil {
loc := urlPtr{r.url, schPtr}
return &ParseIDError{loc.String()}
}
base = uf.url
res = newResource(schPtr, base)
} else if schPtr.isEmpty() {
// root resource
res = newResource(schPtr, base)
}
if res != nil {
found := false
for _, res := range r.resources {
if res.id == base {
found = true
if res.ptr != schPtr {
return &DuplicateIDError{base.String(), r.url.String(), string(schPtr), string(res.ptr)}
}
}
}
if !found {
if hasSchema {
vocabs, err := rr.loader.getMetaVocabs(sch, draft, rr.vocabularies)
if err != nil {
return err
}
res.dialect = dialect{draft, vocabs}
} else {
res.dialect = fallback
}
r.resources[schPtr] = res
}
}
var baseRes *resource
for _, res := range r.resources {
if res.id == base {
baseRes = res
break
}
}
if baseRes == nil {
panic("baseres is nil")
}
// found base resource
if err := r.collectAnchors(sch, schPtr, baseRes); err != nil {
return err
}
// process subschemas
subschemas := map[jsonPointer]any{}
for _, sp := range draft.subschemas {
ss := sp.collect(obj, schPtr)
for k, v := range ss {
subschemas[k] = v
}
}
for _, vocab := range baseRes.dialect.activeVocabs(true, rr.vocabularies) {
if v := rr.vocabularies[vocab]; v != nil {
for _, sp := range v.Subschemas {
ss := sp.collect(obj, schPtr)
for k, v := range ss {
subschemas[k] = v
}
}
}
}
for ptr, v := range subschemas {
if err := rr.collectResources(r, v, base, ptr, baseRes.dialect); err != nil {
return err
}
}
return nil
}
func (rr *roots) ensureSubschema(up urlPtr) error {
r, err := rr.orLoad(up.url)
if err != nil {
return err
}
if _, ok := r.subschemasProcessed[up.ptr]; ok {
return nil
}
v, err := up.lookup(r.doc)
if err != nil {
return err
}
rClone := r.clone()
if err := rr.addSubschema(rClone, up.ptr); err != nil {
return err
}
if err := rr.validate(rClone, v, up.ptr); err != nil {
return err
}
rr.roots[r.url] = rClone
return nil
}
func (rr *roots) addSubschema(r *root, ptr jsonPointer) error {
v, err := (&urlPtr{r.url, ptr}).lookup(r.doc)
if err != nil {
return err
}
base := r.resource(ptr)
baseURL := base.id
if err := rr.collectResources(r, v, baseURL, ptr, base.dialect); err != nil {
return err
}
// collect anchors
if _, ok := r.resources[ptr]; !ok {
res := r.resource(ptr)
if err := r.collectAnchors(v, ptr, res); err != nil {
return err
}
}
return nil
}
func (rr *roots) validate(r *root, v any, ptr jsonPointer) error {
dialect := r.resource(ptr).dialect
meta := dialect.getSchema(rr.assertVocabs, rr.vocabularies)
if err := meta.validate(v, rr.regexpEngine, meta, r.resources, rr.assertVocabs, rr.vocabularies); err != nil {
up := urlPtr{r.url, ptr}
return &SchemaValidationError{URL: up.String(), Err: err}
}
return nil
}
// --
type InvalidMetaSchemaURLError struct {
URL string
Err error
}
func (e *InvalidMetaSchemaURLError) Error() string {
return fmt.Sprintf("invalid $schema in %q: %v", e.URL, e.Err)
}
// --
type UnsupportedDraftError struct {
URL string
}
func (e *UnsupportedDraftError) Error() string {
return fmt.Sprintf("draft %q is not supported", e.URL)
}
// --
type MetaSchemaCycleError struct {
URL string
}
func (e *MetaSchemaCycleError) Error() string {
return fmt.Sprintf("cycle in resolving $schema in %q", e.URL)
}
// --
type MetaSchemaMismatchError struct {
URL string
}
func (e *MetaSchemaMismatchError) Error() string {
return fmt.Sprintf("$schema in %q does not match with $schema in root", e.URL)
}

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