mirror of
https://github.com/docker/cli.git
synced 2026-11-09 14:33:06 -04:00
Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
573a46e506 | ||
|
|
433ab01649 | ||
|
|
175b4e0a98 | ||
|
|
00de141abf | ||
|
|
1f46660e37 | ||
|
|
ff3273f3e4 | ||
|
|
6fccf35355 | ||
|
|
511f136927 | ||
|
|
b17df8fea3 | ||
|
|
dc47c19e55 | ||
|
|
3cd22ff935 | ||
|
|
45f589590b | ||
|
|
6b8da05715 | ||
|
|
65ad909cde | ||
|
|
52eb28a512 | ||
|
|
bb63125969 | ||
|
|
2a24c3c77d | ||
|
|
3f096b4ffd | ||
|
|
ebc3d5aeab | ||
|
|
6261c55c56 | ||
|
|
2c36f84eaf | ||
|
|
4f7a1841ba | ||
|
|
ef48c25aed | ||
|
|
f2514827f4 | ||
|
|
f8d6265672 | ||
|
|
744eee4bd4 | ||
|
|
d6041d1abc | ||
|
|
06275f9094 | ||
|
|
bf6f62cc7c | ||
|
|
376f03c858 | ||
|
|
de4a008614 | ||
|
|
f3f7da9f3d | ||
|
|
eed270f656 | ||
|
|
d072fe407f | ||
|
|
3fe73079aa | ||
|
|
28f756087e | ||
|
|
387b8e2af8 | ||
|
|
f7e66aa1ff | ||
|
|
55e4a7eef5 | ||
|
|
58d9cdd2d9 | ||
|
|
1612f4c767 | ||
|
|
cf53a284da | ||
|
|
0da5f45651 | ||
|
|
ce15202968 | ||
|
|
8659aa5d4c | ||
|
|
8d08c75e81 | ||
|
|
24af004cbf | ||
|
|
eb68ed19d2 | ||
|
|
8a375034e2 | ||
|
|
f755f48a12 | ||
|
|
a7c0741fa7 | ||
|
|
12892075d2 | ||
|
|
e1f9b284df | ||
|
|
4f84911bfe | ||
|
|
228f29e810 | ||
|
|
d8c3543407 | ||
|
|
2dd7b7e15b | ||
|
|
a7dcaa6fdb | ||
|
|
400b45f682 | ||
|
|
38887ec4ed | ||
|
|
904aef7f69 | ||
|
|
f08e60eb09 | ||
|
|
abfd89157d | ||
|
|
519eb45d03 | ||
|
|
8717af7168 | ||
|
|
e9452d6e78 | ||
|
|
a6014a702b | ||
|
|
2465fca604 | ||
|
|
52b15cc571 | ||
|
|
13590921a4 | ||
|
|
0b50545471 | ||
|
|
244404379d |
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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) || '' }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, " ")))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,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,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,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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
//go:build !linux && !windows
|
||||
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
|
||||
|
||||
package sockets
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
//go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !windows
|
||||
|
||||
package sockets
|
||||
|
||||
import "syscall"
|
||||
|
||||
func maxListenerBacklog() int {
|
||||
return syscall.SOMAXCONN
|
||||
}
|
||||
-425
@@ -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
@@ -1,59 +1,71 @@
|
||||
# go-metrics [](https://godoc.org/github.com/docker/go-metrics) 
|
||||
# go-metrics
|
||||
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
+16179
File diff suppressed because it is too large
Load Diff
-436
@@ -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
@@ -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
@@ -1,5 +1,4 @@
|
||||
//go:build !go1.21
|
||||
// +build !go1.21
|
||||
|
||||
/*
|
||||
Copyright 2019 The logr Authors.
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build go1.21
|
||||
// +build go1.21
|
||||
|
||||
/*
|
||||
Copyright 2019 The logr Authors.
|
||||
|
||||
+36
-11
@@ -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
@@ -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
@@ -1,5 +1,4 @@
|
||||
//go:build go1.21
|
||||
// +build go1.21
|
||||
|
||||
/*
|
||||
Copyright 2023 The logr Authors.
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build go1.21
|
||||
// +build go1.21
|
||||
|
||||
/*
|
||||
Copyright 2023 The logr Authors.
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build go1.21
|
||||
// +build go1.21
|
||||
|
||||
/*
|
||||
Copyright 2023 The logr Authors.
|
||||
|
||||
+255
-86
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
linters:
|
||||
enable:
|
||||
- nakedret
|
||||
- errname
|
||||
- godot
|
||||
- misspell
|
||||
+7
@@ -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
|
||||
Generated
Vendored
+1
-28
@@ -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
@@ -0,0 +1,88 @@
|
||||
# jsonschema v6.0.2
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v6)
|
||||
[](https://goreportcard.com/report/github.com/santhosh-tekuri/jsonschema/v6)
|
||||
[](https://github.com/santhosh-tekuri/jsonschema/actions/workflows/go.yaml)
|
||||
[](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] [](https://bowtie.report/#/dialects/draft4)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft6)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft7)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft2019-09)
|
||||
- [x] [](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
||||
go 1.21.1
|
||||
|
||||
use (
|
||||
.
|
||||
./cmd/jv
|
||||
)
|
||||
|
||||
// replace github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 => ./
|
||||
+4
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+151
@@ -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": {}
|
||||
}
|
||||
+150
@@ -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": {}
|
||||
}
|
||||
+172
@@ -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
|
||||
}
|
||||
Generated
Vendored
+55
@@ -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": "#" }
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+15
@@ -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": "#" }
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+56
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+13
@@ -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" }
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+35
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+97
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+47
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+15
@@ -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" }
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+50
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+13
@@ -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" }
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+13
@@ -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" }
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+35
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+14
@@ -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" }
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+97
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user