Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
// Copyright 2020 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package bundle
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/bundle"
"github.com/open-policy-agent/opa/v1/resolver/wasm"
"github.com/open-policy-agent/opa/v1/storage"
)
// LoadWasmResolversFromStore will lookup all Wasm modules from the store along with the
// associated bundle manifest configuration and instantiate the respective resolvers.
func LoadWasmResolversFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, otherBundles map[string]*bundle.Bundle) ([]*wasm.Resolver, error) {
bundleNames, err := bundle.ReadBundleNamesFromStore(ctx, store, txn)
if err != nil && !storage.IsNotFound(err) {
return nil, err
}
var resolversToLoad []*bundle.WasmModuleFile
for _, bundleName := range bundleNames {
var wasmResolverConfigs []bundle.WasmResolver
rawModules := map[string][]byte{}
// Save round-tripping the bundle that was just activated
if _, ok := otherBundles[bundleName]; ok {
wasmResolverConfigs = otherBundles[bundleName].Manifest.WasmResolvers
for _, wmf := range otherBundles[bundleName].WasmModules {
rawModules[wmf.Path] = wmf.Raw
}
} else {
wasmResolverConfigs, err = bundle.ReadWasmMetadataFromStore(ctx, store, txn, bundleName)
if err != nil && !storage.IsNotFound(err) {
return nil, fmt.Errorf("failed to read wasm module manifest from store: %s", err)
}
rawModules, err = bundle.ReadWasmModulesFromStore(ctx, store, txn, bundleName)
if err != nil && !storage.IsNotFound(err) {
return nil, fmt.Errorf("failed to read wasm modules from store: %s", err)
}
}
for path, raw := range rawModules {
wmf := &bundle.WasmModuleFile{
URL: path,
Path: path,
Raw: raw,
}
for _, resolverConf := range wasmResolverConfigs {
if resolverConf.Module == path {
ref, err := ast.PtrRef(ast.DefaultRootDocument, resolverConf.Entrypoint)
if err != nil {
return nil, fmt.Errorf("failed to parse wasm module entrypoint '%s': %s", resolverConf.Entrypoint, err)
}
wmf.Entrypoints = append(wmf.Entrypoints, ref)
}
}
if len(wmf.Entrypoints) > 0 {
resolversToLoad = append(resolversToLoad, wmf)
}
}
}
var resolvers []*wasm.Resolver
if len(resolversToLoad) > 0 {
// Get a full snapshot of the current data (including any from "outside" the bundles)
data, err := store.Read(ctx, txn, storage.RootPath)
if err != nil {
return nil, fmt.Errorf("failed to initialize wasm runtime: %s", err)
}
for _, wmf := range resolversToLoad {
resolver, err := wasm.New(wmf.Entrypoints, wmf.Raw, data)
if err != nil {
return nil, fmt.Errorf("failed to initialize wasm module for entrypoints '%s': %s", wmf.Entrypoints, err)
}
resolvers = append(resolvers, resolver)
}
}
return resolvers, nil
}
// LoadBundleFromDisk loads a previously persisted activated bundle from disk
func LoadBundleFromDisk(path, name string, bvc *bundle.VerificationConfig) (*bundle.Bundle, error) {
return LoadBundleFromDiskForRegoVersion(ast.RegoV0, path, name, bvc)
}
func LoadBundleFromDiskForRegoVersion(regoVersion ast.RegoVersion, path, name string, bvc *bundle.VerificationConfig) (*bundle.Bundle, error) {
bundlePath := filepath.Join(path, name, "bundle.tar.gz")
_, err := os.Stat(bundlePath)
if err == nil {
f, err := os.Open(bundlePath)
if err != nil {
return nil, err
}
defer f.Close()
r := bundle.NewCustomReader(bundle.NewTarballLoaderWithBaseURL(f, "")).
WithRegoVersion(regoVersion)
if bvc != nil {
r = r.WithBundleVerificationConfig(bvc)
}
b, err := r.Read()
if err != nil {
return nil, err
}
return &b, nil
} else if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
// SaveBundleToDisk saves the given raw bytes representing the bundle's content to disk
func SaveBundleToDisk(path string, raw io.Reader) (string, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
err = os.MkdirAll(path, os.ModePerm)
if err != nil {
return "", err
}
}
if raw == nil {
return "", errors.New("no raw bundle bytes to persist to disk")
}
dest, err := os.CreateTemp(path, ".bundle.tar.gz.*.tmp")
if err != nil {
return "", err
}
defer dest.Close()
_, err = io.Copy(dest, raw)
return dest.Name(), err
}
@@ -0,0 +1,367 @@
// Copyright 2017-2020 Authors of Cilium
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package merge provides helper functions for merging a list of
// IP addresses and subnets into the smallest possible list of CIDRs.
// Original Implementation: https://github.com/cilium/cilium
package merge
import (
"bytes"
"encoding/binary"
"math/big"
"net"
)
const (
ipv4BitLen = 8 * net.IPv4len
ipv6BitLen = 8 * net.IPv6len
)
var (
v4Mappedv6Prefix = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff}
defaultIPv4 = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0}
defaultIPv6 = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
upperIPv4 = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 255, 255, 255, 255}
upperIPv6 = []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
ipv4LeadingZeroes = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
)
// RangeToCIDRs converts the range of IPs covered by firstIP and lastIP to
// a list of CIDRs that contains all of the IPs covered by the range.
func RangeToCIDRs(firstIP, lastIP net.IP) []*net.IPNet {
// First, create a CIDR that spans both IPs.
spanningCIDR := createSpanningCIDR(&firstIP, &lastIP)
firstIPSpanning, lastIPSpanning := GetAddressRange(spanningCIDR)
cidrList := []*net.IPNet{}
// If the first IP of the spanning CIDR passes the lower bound (firstIP),
// we need to split the spanning CIDR and only take the IPs that are
// greater than the value which we split on, as we do not want the lesser
// values since they are less than the lower-bound (firstIP).
if bytes.Compare(firstIPSpanning, firstIP) < 0 {
// Split on the previous IP of the first IP so that the right list of IPs
// of the partition includes the firstIP.
prevFirstRangeIP := GetPreviousIP(firstIP)
var bitLen int
if prevFirstRangeIP.To4() != nil {
bitLen = ipv4BitLen
} else {
bitLen = ipv6BitLen
}
_, _, right := partitionCIDR(spanningCIDR, net.IPNet{IP: prevFirstRangeIP, Mask: net.CIDRMask(bitLen, bitLen)})
// Append all CIDRs but the first, as this CIDR includes the upper
// bound of the spanning CIDR, which we still need to partition on.
cidrList = append(cidrList, right...)
spanningCIDR = *right[0]
cidrList = cidrList[1:]
}
// Conversely, if the last IP of the spanning CIDR passes the upper bound
// (lastIP), we need to split the spanning CIDR and only take the IPs that
// are greater than the value which we split on, as we do not want the greater
// values since they are greater than the upper-bound (lastIP).
if bytes.Compare(lastIPSpanning, lastIP) > 0 {
// Split on the next IP of the last IP so that the left list of IPs
// of the partition include the lastIP.
nextFirstRangeIP := getNextIP(lastIP)
var bitLen int
if nextFirstRangeIP.To4() != nil {
bitLen = ipv4BitLen
} else {
bitLen = ipv6BitLen
}
left, _, _ := partitionCIDR(spanningCIDR, net.IPNet{IP: nextFirstRangeIP, Mask: net.CIDRMask(bitLen, bitLen)})
cidrList = append(cidrList, left...)
} else {
// Otherwise, there is no need to partition; just use add the spanning
// CIDR to the list of networks.
cidrList = append(cidrList, &spanningCIDR)
}
return cidrList
}
// GetAddressRange returns the first and last addresses in the given CIDR range.
func GetAddressRange(ipNet net.IPNet) (net.IP, net.IP) {
firstIP := make(net.IP, len(ipNet.IP))
lastIP := make(net.IP, len(ipNet.IP))
copy(firstIP, ipNet.IP)
copy(lastIP, ipNet.IP)
firstIP = firstIP.Mask(ipNet.Mask)
lastIP = lastIP.Mask(ipNet.Mask)
if firstIP.To4() != nil {
firstIP = append(v4Mappedv6Prefix, firstIP...)
lastIP = append(v4Mappedv6Prefix, lastIP...)
}
lastIPMask := make(net.IPMask, len(ipNet.Mask))
copy(lastIPMask, ipNet.Mask)
for i := range lastIPMask {
lastIPMask[len(lastIPMask)-i-1] = ^lastIPMask[len(lastIPMask)-i-1]
lastIP[net.IPv6len-i-1] |= lastIPMask[len(lastIPMask)-i-1]
}
return firstIP, lastIP
}
// GetPreviousIP returns the previous IP from the given IP address.
func GetPreviousIP(ip net.IP) net.IP {
// Cannot go lower than zero!
if ip.Equal(net.IP(defaultIPv4)) || ip.Equal(net.IP(defaultIPv6)) {
return ip
}
previousIP := make(net.IP, len(ip))
copy(previousIP, ip)
var overflow bool
var lowerByteBound int
if ip.To4() != nil {
lowerByteBound = net.IPv6len - net.IPv4len
} else {
lowerByteBound = 0
}
for i := len(ip) - 1; i >= lowerByteBound; i-- {
if overflow || i == len(ip)-1 {
previousIP[i]--
}
// Track if we have overflowed and thus need to continue subtracting.
if ip[i] == 0 && previousIP[i] == 255 {
overflow = true
} else {
overflow = false
}
}
return previousIP
}
// createSpanningCIDR returns a single IP network spanning the
// the lower and upper bound IP addresses.
func createSpanningCIDR(firstIP, lastIP *net.IP) net.IPNet {
// Don't want to modify the values of the provided range, so make copies.
lowest := *firstIP
highest := *lastIP
var isIPv4 bool
var spanningMaskSize, bitLen, byteLen int
if lowest.To4() != nil {
isIPv4 = true
bitLen = ipv4BitLen
byteLen = net.IPv4len
} else {
bitLen = ipv6BitLen
byteLen = net.IPv6len
}
if isIPv4 {
spanningMaskSize = ipv4BitLen
} else {
spanningMaskSize = ipv6BitLen
}
// Convert to big Int so we can easily do bitshifting on the IP addresses,
// since golang only provides up to 64-bit unsigned integers.
lowestBig := big.NewInt(0).SetBytes(lowest)
highestBig := big.NewInt(0).SetBytes(highest)
// Starting from largest mask / smallest range possible, apply a mask one bit
// larger in each iteration to the upper bound in the range until we have
// masked enough to pass the lower bound in the range. This
// gives us the size of the prefix for the spanning CIDR to return as
// well as the IP for the CIDR prefix of the spanning CIDR.
for spanningMaskSize > 0 && lowestBig.Cmp(highestBig) < 0 {
spanningMaskSize--
mask := big.NewInt(1)
mask = mask.Lsh(mask, uint(bitLen-spanningMaskSize))
mask = mask.Mul(mask, big.NewInt(-1))
highestBig = highestBig.And(highestBig, mask)
}
// If ipv4, need to append 0s because math.Big gets rid of preceding zeroes.
if isIPv4 {
highest = append(ipv4LeadingZeroes, highestBig.Bytes()...)
} else {
highest = highestBig.Bytes()
}
// Int does not store leading zeroes.
if len(highest) == 0 {
highest = make([]byte, byteLen)
}
newNet := net.IPNet{IP: highest, Mask: net.CIDRMask(spanningMaskSize, bitLen)}
return newNet
}
// partitionCIDR returns a list of IP Networks partitioned upon excludeCIDR.
// The first list contains the networks to the left of the excludeCIDR in the
// partition, the second is a list containing the excludeCIDR itself if it is
// contained within the targetCIDR (nil otherwise), and the
// third is a list containing the networks to the right of the excludeCIDR in
// the partition.
func partitionCIDR(targetCIDR net.IPNet, excludeCIDR net.IPNet) ([]*net.IPNet, []*net.IPNet, []*net.IPNet) {
var targetIsIPv4 bool
if targetCIDR.IP.To4() != nil {
targetIsIPv4 = true
}
targetFirstIP, targetLastIP := GetAddressRange(targetCIDR)
excludeFirstIP, excludeLastIP := GetAddressRange(excludeCIDR)
targetMaskSize, _ := targetCIDR.Mask.Size()
excludeMaskSize, _ := excludeCIDR.Mask.Size()
if bytes.Compare(excludeLastIP, targetFirstIP) < 0 {
return nil, nil, []*net.IPNet{&targetCIDR}
} else if bytes.Compare(targetLastIP, excludeFirstIP) < 0 {
return []*net.IPNet{&targetCIDR}, nil, nil
}
if targetMaskSize >= excludeMaskSize {
return nil, []*net.IPNet{&targetCIDR}, nil
}
left := []*net.IPNet{}
right := []*net.IPNet{}
newPrefixLen := targetMaskSize + 1
targetFirstCopy := make(net.IP, len(targetFirstIP))
copy(targetFirstCopy, targetFirstIP)
iLowerOld := make(net.IP, len(targetFirstCopy))
copy(iLowerOld, targetFirstCopy)
// Since golang only supports up to unsigned 64-bit integers, and we need
// to perform addition on addresses, use math/big library, which allows
// for manipulation of large integers.
// Used to track the current lower and upper bounds of the ranges to compare
// to excludeCIDR.
iLower := big.NewInt(0)
iUpper := big.NewInt(0)
iLower = iLower.SetBytes(targetFirstCopy)
var bitLen int
if targetIsIPv4 {
bitLen = ipv4BitLen
} else {
bitLen = ipv6BitLen
}
shiftAmount := (uint)(bitLen - newPrefixLen)
targetIPInt := big.NewInt(0)
targetIPInt.SetBytes(targetFirstIP.To16())
exp := big.NewInt(0)
// Use left shift for exponentiation
exp = exp.Lsh(big.NewInt(1), shiftAmount)
iUpper = iUpper.Add(targetIPInt, exp)
matched := big.NewInt(0)
for excludeMaskSize >= newPrefixLen {
// Append leading zeros to IPv4 addresses, as math.Big.Int does not
// append them when the IP address is copied from a byte array to
// math.Big.Int. Leading zeroes are required for parsing IPv4 addresses
// for use with net.IP / net.IPNet.
var iUpperBytes, iLowerBytes []byte
if targetIsIPv4 {
iUpperBytes = append(ipv4LeadingZeroes, iUpper.Bytes()...)
iLowerBytes = append(ipv4LeadingZeroes, iLower.Bytes()...)
} else {
iUpperBytesLen := len(iUpper.Bytes())
// Make sure that the number of bytes in the array matches what net
// package expects, as big package doesn't append leading zeroes.
if iUpperBytesLen != net.IPv6len {
numZeroesToAppend := net.IPv6len - iUpperBytesLen
zeroBytes := make([]byte, numZeroesToAppend)
iUpperBytes = append(zeroBytes, iUpper.Bytes()...)
} else {
iUpperBytes = iUpper.Bytes()
}
iLowerBytesLen := len(iLower.Bytes())
if iLowerBytesLen != net.IPv6len {
numZeroesToAppend := net.IPv6len - iLowerBytesLen
zeroBytes := make([]byte, numZeroesToAppend)
iLowerBytes = append(zeroBytes, iLower.Bytes()...)
} else {
iLowerBytes = iLower.Bytes()
}
}
// If the IP we are excluding over is of a higher value than the current
// CIDR prefix we are generating, add the CIDR prefix to the set of IPs
// to the left of the exclude CIDR
if bytes.Compare(excludeFirstIP, iUpperBytes) >= 0 {
left = append(left, &net.IPNet{IP: iLowerBytes, Mask: net.CIDRMask(newPrefixLen, bitLen)})
matched = matched.Set(iUpper)
} else {
// Same as above, but opposite.
right = append(right, &net.IPNet{IP: iUpperBytes, Mask: net.CIDRMask(newPrefixLen, bitLen)})
matched = matched.Set(iLower)
}
newPrefixLen++
if newPrefixLen > bitLen {
break
}
iLower = iLower.Set(matched)
iUpper = iUpper.Add(matched, big.NewInt(0).Lsh(big.NewInt(1), uint(bitLen-newPrefixLen)))
}
excludeList := []*net.IPNet{&excludeCIDR}
return left, excludeList, right
}
func getNextIP(ip net.IP) net.IP {
if ip.Equal(upperIPv4) || ip.Equal(upperIPv6) {
return ip
}
nextIP := make(net.IP, len(ip))
switch len(ip) {
case net.IPv4len:
ipU32 := binary.BigEndian.Uint32(ip)
ipU32++
binary.BigEndian.PutUint32(nextIP, ipU32)
return nextIP
case net.IPv6len:
ipU64 := binary.BigEndian.Uint64(ip[net.IPv6len/2:])
ipU64++
binary.BigEndian.PutUint64(nextIP[net.IPv6len/2:], ipU64)
if ipU64 == 0 {
ipU64 = binary.BigEndian.Uint64(ip[:net.IPv6len/2])
ipU64++
binary.BigEndian.PutUint64(nextIP[:net.IPv6len/2], ipU64)
} else {
copy(nextIP[:net.IPv6len/2], ip[:net.IPv6len/2])
}
return nextIP
default:
return ip
}
}
@@ -0,0 +1,95 @@
// Copyright 2023 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package compiler
import (
"errors"
"sync"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/schemas"
"github.com/open-policy-agent/opa/v1/util"
)
type SchemaFile string
const (
AuthorizationPolicySchema SchemaFile = "authorizationPolicy.json"
)
var schemaDefinitions = map[SchemaFile]any{}
var loadOnce = sync.OnceValue(func() error {
cont, err := schemas.FS.ReadFile(string(AuthorizationPolicySchema))
if err != nil {
return err
}
if len(cont) == 0 {
return errors.New("expected authorization policy schema file to be present")
}
var schema any
if err := util.Unmarshal(cont, &schema); err != nil {
return err
}
schemaDefinitions[AuthorizationPolicySchema] = schema
return nil
})
// VerifyAuthorizationPolicySchema performs type checking on rules against the schema for the Authorization Policy
// Input document.
// NOTE: The provided compiler should have already run the compilation process on the input modules
func VerifyAuthorizationPolicySchema(compiler *ast.Compiler, ref ast.Ref) error {
if err := loadOnce(); err != nil {
panic(err)
}
rules := getRulesWithDependencies(compiler, ref)
if len(rules) == 0 {
return nil
}
schemaSet := ast.NewSchemaSet()
schemaSet.Put(ast.SchemaRootRef, schemaDefinitions[AuthorizationPolicySchema])
errs := ast.NewCompiler().
WithDefaultRegoVersion(compiler.DefaultRegoVersion()).
WithSchemas(schemaSet).
PassesTypeCheckRules(rules)
if len(errs) > 0 {
return errs
}
return nil
}
// getRulesWithDependencies returns a slice of rules that are referred to by ref along with their dependencies
func getRulesWithDependencies(compiler *ast.Compiler, ref ast.Ref) []*ast.Rule {
allRules := compiler.GetRules(ref)
deps := map[*ast.Rule]struct{}{}
for _, rule := range allRules {
transitiveDependencies(compiler, rule, deps)
}
for dep := range deps {
allRules = append(allRules, dep)
}
return allRules
}
func transitiveDependencies(compiler *ast.Compiler, rule *ast.Rule, deps map[*ast.Rule]struct{}) {
for x := range compiler.Graph.Dependencies(rule) {
other := x.(*ast.Rule)
deps[other] = struct{}{}
transitiveDependencies(compiler, other, deps)
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
// Copyright 2021 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package opa contains bytecode for the OPA-WASM library.
package opa
import (
_ "embed"
)
//go:embed opa.wasm
var wasmBase []byte
//go:embed callgraph.csv
var callGraphCSV []byte
// Bytes returns the OPA-WASM bytecode.
func Bytes() []byte {
return wasmBase
}
// CallGraphCSV returns a CSV representation of the
// OPA-WASM bytecode's call graph: 'caller,callee'
func CallGraphCSV() []byte {
return callGraphCSV
}
@@ -0,0 +1,271 @@
package wasm
import (
"bytes"
"context"
"encoding/csv"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/open-policy-agent/opa/internal/compiler/wasm/opa"
"github.com/open-policy-agent/opa/internal/wasm/encoding"
"github.com/open-policy-agent/opa/internal/wasm/instruction"
"github.com/open-policy-agent/opa/internal/wasm/module"
)
const warning = `---------------------------------------------------------------
WARNING: Using EXPERIMENTAL, unsupported wasm-opt optimization.
It is not supported, and may go away in the future.
---------------------------------------------------------------`
// optimizeBinaryen passes the encoded module into wasm-opt, and replaces
// the compiler's module with the decoding of the process' output.
func (c *Compiler) optimizeBinaryen() error {
if os.Getenv("EXPERIMENTAL_WASM_OPT") == "" && os.Getenv("EXPERIMENTAL_WASM_OPT_ARGS") == "" {
c.debug.Printf("not opted in, skipping wasm-opt optimization")
return nil
}
if !woptFound() {
c.debug.Printf("wasm-opt binary not found, skipping optimization")
return nil
}
if os.Getenv("EXPERIMENTAL_WASM_OPT") != "silent" { // for benchmarks
fmt.Fprintln(os.Stderr, warning)
}
args := []string{ // WARNING: flags with typos are ignored!
"-O2",
"--debuginfo", // don't strip name section
}
// allow overriding the options
if env := os.Getenv("EXPERIMENTAL_WASM_OPT_ARGS"); env != "" {
args = strings.Split(env, " ")
}
args = append(args, "-o", "-") // always output to stdout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
wopt := exec.CommandContext(ctx, "wasm-opt", args...)
stdin, err := wopt.StdinPipe()
if err != nil {
return fmt.Errorf("get stdin: %w", err)
}
defer stdin.Close()
var stdout, stderr bytes.Buffer
wopt.Stdout = &stdout
if err := wopt.Start(); err != nil {
return fmt.Errorf("start wasm-opt: %w", err)
}
if err := encoding.WriteModule(stdin, c.module); err != nil {
return fmt.Errorf("encode module: %w", err)
}
if err := stdin.Close(); err != nil {
return fmt.Errorf("write to wasm-opt: %w", err)
}
if err := wopt.Wait(); err != nil {
return fmt.Errorf("wait for wasm-opt: %w", err)
}
if d := stderr.String(); d != "" {
c.debug.Printf("wasm-opt debug output: %s", d)
}
mod, err := encoding.ReadModule(&stdout)
if err != nil {
return fmt.Errorf("decode module: %w", err)
}
c.module = mod
return nil
}
func woptFound() bool {
_, err := exec.LookPath("wasm-opt")
return err == nil
}
// NOTE(sr): Yes, there are more control instructions than these two,
// but we haven't made use of them yet. So this function only checks
// for the control instructions we're possibly emitting, and which are
// relevant for block nesting.
func withControlInstr(is []instruction.Instruction) bool {
for _, i := range is {
switch i := i.(type) {
case instruction.Br, instruction.BrIf:
return true
case instruction.StructuredInstruction:
// NOTE(sr): We could attempt to further flatten the nested blocks
// here, but I believe we'd then have to correct block labels.
if withControlInstr(i.Instructions()) {
return true
}
}
}
return false
}
func unquote(s string) (string, error) {
return strconv.Unquote("\"" + strings.ReplaceAll(s, `\`, `\x`) + "\"")
}
func (c *Compiler) removeUnusedCode() error {
cgCSV := opa.CallGraphCSV()
r := csv.NewReader(bytes.NewReader(cgCSV))
r.LazyQuotes = true
cg, err := r.ReadAll()
if err != nil {
return fmt.Errorf("csv read: %w", err)
}
cgIdx := map[uint32][]uint32{}
for i := range cg {
callerName, err := unquote(cg[i][0])
if err != nil {
return fmt.Errorf("unquote caller name %s: %w", cg[i][0], err)
}
calleeName, err := unquote(cg[i][1])
if err != nil {
return fmt.Errorf("unquote callee name %s: %w", cg[i][1], err)
}
caller, ok := c.funcs[callerName]
if !ok {
continue // without a caller, it should get removed anyways (right?)
}
callee, ok := c.funcs[calleeName]
if !ok {
return fmt.Errorf("callee not found: %s (%s)", cg[i][1], calleeName)
}
cgIdx[caller] = append(cgIdx[caller], callee)
}
// add the calls from planned functions
for _, f := range c.funcsCode {
fidx := c.funcs[f.name]
cgIdx[fidx] = findCallees(f.code.Func.Expr.Instrs)
}
keepFuncs := map[uint32]struct{}{}
// we'll keep
// - what's referenced in a table (these could be called indirectly)
// - what's exported or imported
// - what's been compiled by us
// - anything transitively called from those
for _, imp := range c.module.Import.Imports {
if _, ok := imp.Descriptor.(module.FunctionImport); ok {
reach(cgIdx, keepFuncs, c.funcs[imp.Name])
}
}
for _, exp := range c.module.Export.Exports {
if exp.Descriptor.Type == module.FunctionExportType {
reach(cgIdx, keepFuncs, c.funcs[exp.Name])
}
}
for _, f := range c.funcsCode {
reach(cgIdx, keepFuncs, c.funcs[f.name])
}
// anything referenced in a table
for _, seg := range c.module.Element.Segments {
for _, idx := range seg.Indices {
if c.skipElemRE2(keepFuncs, idx) {
c.debug.Printf("dropping element %d because policy does not depend on re2", idx)
} else {
reach(cgIdx, keepFuncs, idx)
}
}
}
// remove all that's not needed, update index for remaining ones
funcNames := []module.NameMap{}
for _, nm := range c.module.Names.Functions {
if _, ok := keepFuncs[nm.Index]; ok {
funcNames = append(funcNames, nm)
}
}
c.module.Names.Functions = funcNames
// For anything that we don't want, replace the function code entries'
// expressions with `unreachable`.
// We do this because it lets the resulting wasm module pass `wasm-validate`,
// empty bodies would not.
nopEntry := module.Function{
Expr: module.Expr{
Instrs: []instruction.Instruction{instruction.Unreachable{}},
},
}
var buf bytes.Buffer
if err := encoding.WriteCodeEntry(&buf, &module.CodeEntry{Func: nopEntry}); err != nil {
return fmt.Errorf("write code entry: %w", err)
}
for i := range c.module.Code.Segments {
idx := i + c.functionImportCount()
if _, ok := keepFuncs[uint32(idx)]; !ok {
c.module.Code.Segments[i].Code = buf.Bytes()
}
}
return nil
}
func findCallees(instrs []instruction.Instruction) []uint32 {
var ret []uint32
for _, expr := range instrs {
switch expr := expr.(type) {
case instruction.Call:
ret = append(ret, expr.Index)
case instruction.StructuredInstruction:
ret = append(ret, findCallees(expr.Instructions())...)
}
}
return ret
}
func reach(cg map[uint32][]uint32, keep map[uint32]struct{}, node uint32) {
if _, ok := keep[node]; !ok {
keep[node] = struct{}{}
for _, v := range cg[node] {
reach(cg, keep, v)
}
}
}
// skipElemRE2 determines if a function in the table is really required:
// We'll exclude anything with a prefix of "re2::" if none of the known
// entrypoints into re2 are used.
func (c *Compiler) skipElemRE2(keep map[uint32]struct{}, idx uint32) bool {
if c.usesRE2(keep) {
return false
}
return c.nameContains(idx, "re2::", "lexer::", "std::", "__cxa_pure_virtual", "operator", "parser_")
}
func (c *Compiler) usesRE2(keep map[uint32]struct{}) bool {
for _, fn := range builtinsUsingRE2 {
if _, ok := keep[c.function(fn)]; ok {
return true
}
}
return false
}
func (c *Compiler) nameContains(idx uint32, hs ...string) bool {
// TODO(sr): keep reverse mapping (idx -> name) in Compiler struct
for _, nm := range c.module.Names.Functions {
if nm.Index == idx {
for _, h := range hs {
if strings.Contains(nm.Name, h) {
return true
}
}
return false
}
}
return false
}
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
package debug
import (
"io"
"log"
)
// Debug allows printing debug messages.
type Debug interface {
// Printf prints, with a short file:line-number prefix
Printf(format string, args ...any)
// Writer returns the writer being written to, which may be
// `io.Discard` if no debug output is requested.
Writer() io.Writer
// Output allows tweaking the calldepth used for figuring
// out which Go source file location is the interesting one,
// i.e., which is included in the debug message. Useful for
// setting up local helper methods.
Output(calldepth int, s string) error
}
// New returns a new `Debug` outputting to the passed `sink`.
func New(sink io.Writer) Debug {
flags := log.Lshortfile
return log.New(sink, "", flags)
}
// Discard returns a new `Debug` that doesn't output anything.
// Note: We're not implementing the methods here with noop stubs
// since doing this way, we can propagate the "discarding" via
// `(Debug).Writer()`.
func Discard() Debug {
return New(io.Discard)
}
@@ -0,0 +1,31 @@
// Copyright 2020 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package deepcopy
// DeepCopy performs a recursive deep copy for nested slices/maps and
// returns the copied object. Supports []any
// and map[string]any only
func DeepCopy(val any) any {
switch val := val.(type) {
case []any:
cpy := make([]any, len(val))
for i := range cpy {
cpy[i] = DeepCopy(val[i])
}
return cpy
case map[string]any:
return Map(val)
default:
return val
}
}
func Map(val map[string]any) map[string]any {
cpy := make(map[string]any, len(val))
for k := range val {
cpy[k] = DeepCopy(val[k])
}
return cpy
}
@@ -0,0 +1,15 @@
# godropbox [![GoDoc](https://godoc.org/github.com/dropbox/godropbox?status.svg)](https://godoc.org/github.com/dropbox/godropbox) [![Actions Status](https://github.com/dropbox/godropbox/workflows/Test/badge.svg)](https://github.com/dropbox/godropbox/actions) [![Actions Status](https://github.com/dropbox/godropbox/workflows/Lint/badge.svg)](https://github.com/dropbox/godropbox/actions)
Common libraries for writing go services/applications on Linux servers.
### Requirements
* Go 1.13+
* Linux/x64
### Installation
``go get github.com/dropbox/godropbox``
### Documentation
See https://pkg.go.dev/github.com/dropbox/godropbox for modules documentation.
@@ -0,0 +1,223 @@
// Package bitvector provides the implementation of a variable sized compact vector of bits
// which supports lookups, sets, appends, insertions, and deletions.
package bitvector
import "slices"
// A BitVector is a variable sized vector of bits. It supports
// lookups, sets, appends, insertions, and deletions.
//
// Operations are not thread safe.
type BitVector struct {
data []byte
length int
}
// NewBitVector creates and initializes a new bit vector with length
// elements, using data as its initial contents.
func NewBitVector(data []byte, length int) *BitVector {
return &BitVector{data: data, length: length}
}
func (vector *BitVector) Clear() *BitVector {
if vector == nil {
return nil
}
clear(vector.data)
vector.length = 0
return vector
}
func (vector *BitVector) Reset(size, length int) *BitVector {
clear(vector.data)
vector.data = slices.Grow(vector.data, size)[:size]
vector.length = length
return vector
}
// Bytes returns a slice of the contents of the bit vector. If the caller changes the returned slice,
// the contents of the bit vector may change.
func (vector *BitVector) Bytes() []byte {
return vector.data
}
// Length returns the current number of elements in the bit vector.
func (vector *BitVector) Length() int {
return vector.length
}
// This function shifts a byte slice one bit lower (less significant).
// bit (either 1 or 0) contains the bit to put in the most significant
// position of the last byte in the slice.
// This returns the bit that was shifted off of the last byte.
func shiftLower(bit byte, b []byte) byte {
bit <<= 7
for i := len(b) - 1; i >= 0; i-- {
newByte := b[i] >> 1
newByte |= bit
bit = (b[i] & 1) << 7
b[i] = newByte
}
return bit >> 7
}
// This function shifts a byte slice one bit higher (more significant).
// bit (either 1 or 0) contains the bit to put in the least significant
// position of the first byte in the slice.
// This returns the bit that was shifted off the last byte.
func shiftHigher(bit byte, b []byte) byte {
for i := range b {
newByte := b[i] << 1
newByte |= bit
bit = (b[i] & 0x80) >> 7
b[i] = newByte
}
return bit
}
// Returns the minimum number of bytes needed for storing the bit vector.
func (vector *BitVector) bytesLength() int {
lastBitIndex := vector.length - 1
lastByteIndex := lastBitIndex >> 3
return lastByteIndex + 1
}
// Panics if the given index is not within the bounds of the bit vector.
func (vector *BitVector) indexAssert(i int) {
if i < 0 || i >= vector.length {
panic("Attempted to access element outside buffer")
}
}
// Append adds a bit to the end of a bit vector.
func (vector *BitVector) Append(bit byte) {
index := uint32(vector.length)
vector.length++
if vector.bytesLength() > len(vector.data) {
vector.data = append(vector.data, 0)
}
byteIndex := index >> 3
byteOffset := index % 8
oldByte := vector.data[byteIndex]
var newByte byte
if bit == 1 {
newByte = oldByte | 1<<byteOffset
} else {
// Set all bits except the byteOffset
mask := byte(^(1 << byteOffset))
newByte = oldByte & mask
}
vector.data[byteIndex] = newByte
}
// Element returns the bit in the ith index of the bit vector.
// Returned value is either 1 or 0.
func (vector *BitVector) Element(i int) byte {
vector.indexAssert(i)
byteIndex := i >> 3
byteOffset := uint32(i % 8)
b := vector.data[byteIndex]
// Check the offset bit
return (b >> byteOffset) & 1
}
// Set changes the bit in the ith index of the bit vector to the value specified in
// bit.
func (vector *BitVector) Set(bit byte, index int) {
vector.indexAssert(index)
byteIndex := uint32(index >> 3)
byteOffset := uint32(index % 8)
oldByte := vector.data[byteIndex]
var newByte byte
if bit == 1 {
// turn on the byteOffset'th bit
newByte = oldByte | 1<<byteOffset
} else {
// turn off the byteOffset'th bit
removeMask := byte(^(1 << byteOffset))
newByte = oldByte & removeMask
}
vector.data[byteIndex] = newByte
}
// Insert inserts bit into the supplied index of the bit vector. All
// bits in positions greater than or equal to index before the call will
// be shifted up by one.
func (vector *BitVector) Insert(bit byte, index int) {
vector.indexAssert(index)
vector.length++
// Append an additional byte if necessary.
if vector.bytesLength() > len(vector.data) {
vector.data = append(vector.data, 0)
}
byteIndex := uint32(index >> 3)
byteOffset := uint32(index % 8)
var bitToInsert byte
if bit == 1 {
bitToInsert = 1 << byteOffset
}
oldByte := vector.data[byteIndex]
// This bit will need to be shifted into the next byte
leftoverBit := (oldByte & 0x80) >> 7
// Make masks to pull off the bits below and above byteOffset
// This mask has the byteOffset lowest bits set.
bottomMask := byte((1 << byteOffset) - 1)
// This mask has the 8 - byteOffset top bits set.
topMask := ^bottomMask
top := (oldByte & topMask) << 1
newByte := bitToInsert | (oldByte & bottomMask) | top
vector.data[byteIndex] = newByte
// Shift the rest of the bytes in the slice one higher, append
// the leftoverBit obtained above.
shiftHigher(leftoverBit, vector.data[byteIndex+1:])
}
// Delete removes the bit in the supplied index of the bit vector. All
// bits in positions greater than or equal to index before the call will
// be shifted down by one.
func (vector *BitVector) Delete(index int) {
vector.indexAssert(index)
vector.length--
byteIndex := uint32(index >> 3)
byteOffset := uint32(index % 8)
oldByte := vector.data[byteIndex]
// Shift all the bytes above the byte we're modifying, return the
// leftover bit to include in the byte we're modifying.
bit := shiftLower(0, vector.data[byteIndex+1:])
// Modify oldByte.
// At a high level, we want to select the bits above byteOffset,
// and shift them down by one, removing the bit at byteOffset.
// This selects the bottom bits
bottomMask := byte((1 << byteOffset) - 1)
// This selects the top (8 - byteOffset - 1) bits
topMask := byte(^((1 << (byteOffset + 1)) - 1))
// newTop is the top bits, shifted down one, combined with the leftover bit from shifting
// the other bytes.
newTop := (oldByte&topMask)>>1 | (bit << 7)
// newByte takes the bottom bits and combines with the new top.
newByte := (bottomMask & oldByte) | newTop
vector.data[byteIndex] = newByte
// The desired length is the byte index of the last element plus one,
// where the byte index of the last element is the bit index of the last
// element divided by 8.
byteLength := vector.bytesLength()
if byteLength < len(vector.data) {
vector.data = vector.data[:byteLength]
}
}
@@ -0,0 +1,27 @@
Copyright (c) 2014 Dropbox, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,75 @@
package archive
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"io"
"strings"
)
type TarGzWriter struct {
*tar.Writer
gw *gzip.Writer
}
func NewTarGzWriter(w io.Writer) *TarGzWriter {
gw := gzip.NewWriter(w)
tw := tar.NewWriter(gw)
return &TarGzWriter{
Writer: tw,
gw: gw,
}
}
func (tgw *TarGzWriter) WriteFile(path string, bs []byte) (err error) {
hdr := &tar.Header{
Name: path,
Mode: 0600,
Typeflag: tar.TypeReg,
Size: int64(len(bs)),
}
if err = tgw.WriteHeader(hdr); err == nil {
_, err = tgw.Write(bs)
}
return err
}
func (tgw *TarGzWriter) WriteJSONFile(path string, v any) error {
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(v); err != nil {
return err
}
return tgw.WriteFile(path, buf.Bytes())
}
func (tgw *TarGzWriter) Close() error {
return errors.Join(tgw.Writer.Close(), tgw.gw.Close())
}
// MustWriteTarGz writes the list of file names and content into a tarball.
// Paths are prefixed with "/".
func MustWriteTarGz(files [][2]string) *bytes.Buffer {
buf := &bytes.Buffer{}
tgw := NewTarGzWriter(buf)
defer tgw.Close()
for _, file := range files {
if !strings.HasPrefix(file[0], "/") {
file[0] = "/" + file[0]
}
if err := tgw.WriteFile(file[0], []byte(file[1])); err != nil {
panic(err)
}
}
return buf
}
@@ -0,0 +1,42 @@
// Copyright 2019 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package url contains helpers for dealing with file paths and URLs.
package url
import (
"fmt"
"net/url"
"runtime"
"strings"
)
var goos = runtime.GOOS
// Clean returns a cleaned file path that may or may not be a URL.
func Clean(path string) (string, error) {
if strings.Contains(path, "://") {
url, err := url.Parse(path)
if err != nil {
return "", err
}
if url.Scheme != "file" {
return "", fmt.Errorf("unsupported URL scheme: %v", path)
}
path = url.Path
// Trim leading slash on Windows if present. The url.Path field returned
// by url.Parse has leading slash that causes CreateFile() calls to fail
// on Windows. See https://github.com/golang/go/issues/6027 for details.
if goos == "windows" && len(path) >= 1 && path[0] == '/' {
path = path[1:]
}
}
return path, nil
}
@@ -0,0 +1,49 @@
// Copyright 2021 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package future
import "github.com/open-policy-agent/opa/v1/ast"
// FilterFutureImports filters OUT any future imports from the passed slice of
// `*ast.Import`s.
func FilterFutureImports(imps []*ast.Import) []*ast.Import {
ret := []*ast.Import{}
for _, imp := range imps {
path := imp.Path.Value.(ast.Ref)
if !ast.FutureRootDocument.Equal(path[0]) {
ret = append(ret, imp)
}
}
return ret
}
// IsAllFutureKeywords returns true if the passed *ast.Import is `future.keywords`
func IsAllFutureKeywords(imp *ast.Import) bool {
path := imp.Path.Value.(ast.Ref)
return len(path) == 2 &&
ast.FutureRootDocument.Equal(path[0]) &&
path[1].Equal(ast.InternedTerm("keywords"))
}
// IsFutureKeyword returns true if the passed *ast.Import is `future.keywords.{kw}`
func IsFutureKeyword(imp *ast.Import, kw string) bool {
path := imp.Path.Value.(ast.Ref)
return len(path) == 3 &&
ast.FutureRootDocument.Equal(path[0]) &&
path[1].Equal(ast.InternedTerm("keywords")) &&
path[2].Equal(ast.StringTerm(kw))
}
func WhichFutureKeyword(imp *ast.Import) (string, bool) {
path := imp.Path.Value.(ast.Ref)
if len(path) == 3 &&
ast.FutureRootDocument.Equal(path[0]) &&
path[1].Equal(ast.InternedTerm("keywords")) {
if str, ok := path[2].Value.(ast.String); ok {
return string(str), true
}
}
return "", false
}
@@ -0,0 +1,43 @@
// Copyright 2021 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package future
import (
"errors"
"fmt"
"github.com/open-policy-agent/opa/v1/ast"
)
// ParserOptionsFromFutureImports transforms a slice of `ast.Import`s into the
// `ast.ParserOptions` that can be used to parse a statement according to the
// included "future.keywords" and "future.keywords.xyz" imports.
func ParserOptionsFromFutureImports(imports []*ast.Import) (ast.ParserOptions, error) {
popts := ast.ParserOptions{
FutureKeywords: []string{},
}
for _, imp := range imports {
path := imp.Path.Value.(ast.Ref)
if !ast.FutureRootDocument.Equal(path[0]) {
continue
}
if len(path) >= 2 {
if string(path[1].Value.(ast.String)) != "keywords" {
return popts, fmt.Errorf("unknown future import: %v", imp)
}
if len(path) == 2 {
// retun, one "future.keywords" import means we can disregard any others
return ast.ParserOptions{AllFutureKeywords: true}, nil
}
}
if len(path) == 3 {
if imp.Alias != "" {
return popts, errors.New("alias not supported")
}
popts.FutureKeywords = append(popts.FutureKeywords, string(path[2].Value.(ast.String)))
}
}
return popts, nil
}
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
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.
@@ -0,0 +1,482 @@
# gojsonschema (Details of library residing in OPA's internal)
## Description
https://github.com/xeipuuv/gojsonschema was duplicated into `internal/gojsonschema` folder and modified to make it possible
to set Rego types in OPA, using schemas returned from `gojsonschema`s `Compile` utility.
The modifications done are as below:
1. Some of the private fields in `gojsonschema`'s structs, `schema` and `subSchema` have been exported (publicized) so that OPA's type checking code can access and manipulate the values returned by `gojsonschema`s `Compile` method,
2. Also, other changes in `gojsonschema`'s code include fixes to satisfy OPA's lint and format checker scripts. Hence, this `internal/gojsonschema` is in conformance with OPA's code style.
3. Modernize usage of Go in `gojsonschema`, using conventions like type switching and language-built-in map accessing rather than helper methods.
[![GoDoc](https://godoc.org/github.com/xeipuuv/gojsonschema?status.svg)](https://godoc.org/github.com/xeipuuv/gojsonschema)
[![Build Status](https://travis-ci.org/xeipuuv/gojsonschema.svg)](https://travis-ci.org/xeipuuv/gojsonschema)
[![Go Report Card](https://goreportcard.com/badge/github.com/xeipuuv/gojsonschema)](https://goreportcard.com/report/github.com/xeipuuv/gojsonschema)
# gojsonschema (Details of original parent repository from README)
## Description
An implementation of JSON Schema for the Go programming language. Supports draft-04, draft-06 and draft-07.
References :
* http://json-schema.org
* http://json-schema.org/latest/json-schema-core.html
* http://json-schema.org/latest/json-schema-validation.html
## Installation
```
go get github.com/xeipuuv/gojsonschema
```
Dependencies :
* [github.com/xeipuuv/gojsonpointer](https://github.com/xeipuuv/gojsonpointer)
* [github.com/xeipuuv/gojsonreference](https://github.com/xeipuuv/gojsonreference)
* [github.com/stretchr/testify/assert](https://github.com/stretchr/testify#assert-package) - (Note, this dependency has
been removed to reduce dependencies in OPA)
## Usage
### Example
```go
package main
import (
"fmt"
"github.com/xeipuuv/gojsonschema"
)
func main() {
schemaLoader := gojsonschema.NewReferenceLoader("file:///home/me/schema.json")
documentLoader := gojsonschema.NewReferenceLoader("file:///home/me/document.json")
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
panic(err.Error())
}
if result.Valid() {
fmt.Printf("The document is valid\n")
} else {
fmt.Printf("The document is not valid. see errors :\n")
for _, desc := range result.Errors() {
fmt.Printf("- %s\n", desc)
}
}
}
```
#### Loaders
There are various ways to load your JSON data.
In order to load your schemas and documents,
first declare an appropriate loader :
* Web / HTTP, using a reference :
```go
loader := gojsonschema.NewReferenceLoader("http://www.some_host.com/schema.json")
```
* Local file, using a reference :
```go
loader := gojsonschema.NewReferenceLoader("file:///home/me/schema.json")
```
References use the URI scheme, the prefix (file://) and a full path to the file are required.
* JSON strings :
```go
loader := gojsonschema.NewStringLoader(`{"type": "string"}`)
```
* Custom Go types :
```go
m := map[string]interface{}{"type": "string"}
loader := gojsonschema.NewGoLoader(m)
```
And
```go
type Root struct {
Users []User `json:"users"`
}
type User struct {
Name string `json:"name"`
}
...
data := Root{}
data.Users = append(data.Users, User{"John"})
data.Users = append(data.Users, User{"Sophia"})
data.Users = append(data.Users, User{"Bill"})
loader := gojsonschema.NewGoLoader(data)
```
#### Validation
Once the loaders are set, validation is easy :
```go
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
```
Alternatively, you might want to load a schema only once and process to multiple validations :
```go
schema, err := gojsonschema.NewSchema(schemaLoader)
...
result1, err := schema.Validate(documentLoader1)
...
result2, err := schema.Validate(documentLoader2)
...
// etc ...
```
To check the result :
```go
if result.Valid() {
fmt.Printf("The document is valid\n")
} else {
fmt.Printf("The document is not valid. see errors :\n")
for _, err := range result.Errors() {
// Err implements the ResultError interface
fmt.Printf("- %s\n", err)
}
}
```
## Loading local schemas
By default `file` and `http(s)` references to external schemas are loaded automatically via the file system or via http(s). An external schema can also be loaded using a `SchemaLoader`.
```go
sl := gojsonschema.NewSchemaLoader()
loader1 := gojsonschema.NewStringLoader(`{ "type" : "string" }`)
err := sl.AddSchema("http://some_host.com/string.json", loader1)
```
Alternatively if your schema already has an `$id` you can use the `AddSchemas` function
```go
loader2 := gojsonschema.NewStringLoader(`{
"$id" : "http://some_host.com/maxlength.json",
"maxLength" : 5
}`)
err = sl.AddSchemas(loader2)
```
The main schema should be passed to the `Compile` function. This main schema can then directly reference the added schemas without needing to download them.
```go
loader3 := gojsonschema.NewStringLoader(`{
"$id" : "http://some_host.com/main.json",
"allOf" : [
{ "$ref" : "http://some_host.com/string.json" },
{ "$ref" : "http://some_host.com/maxlength.json" }
]
}`)
schema, err := sl.Compile(loader3)
documentLoader := gojsonschema.NewStringLoader(`"hello world"`)
result, err := schema.Validate(documentLoader)
```
It's also possible to pass a `ReferenceLoader` to the `Compile` function that references a loaded schema.
```go
err = sl.AddSchemas(loader3)
schema, err := sl.Compile(gojsonschema.NewReferenceLoader("http://some_host.com/main.json"))
```
Schemas added by `AddSchema` and `AddSchemas` are only validated when the entire schema is compiled, unless meta-schema validation is used.
## Using a specific draft
By default `gojsonschema` will try to detect the draft of a schema by using the `$schema` keyword and parse it in a strict draft-04, draft-06 or draft-07 mode. If `$schema` is missing, or the draft version is not explicitely set, a hybrid mode is used which merges together functionality of all drafts into one mode.
Autodectection can be turned off with the `AutoDetect` property. Specific draft versions can be specified with the `Draft` property.
```go
sl := gojsonschema.NewSchemaLoader()
sl.Draft = gojsonschema.Draft7
sl.AutoDetect = false
```
If autodetection is on (default), a draft-07 schema can savely reference draft-04 schemas and vice-versa, as long as `$schema` is specified in all schemas.
## Meta-schema validation
Schemas that are added using the `AddSchema`, `AddSchemas` and `Compile` can be validated against their meta-schema by setting the `Validate` property.
The following example will produce an error as `multipleOf` must be a number. If `Validate` is off (default), this error is only returned at the `Compile` step.
```go
sl := gojsonschema.NewSchemaLoader()
sl.Validate = true
err := sl.AddSchemas(gojsonschema.NewStringLoader(`{
"$id" : "http://some_host.com/invalid.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"multipleOf" : true
}`))
```
```
```
Errors returned by meta-schema validation are more readable and contain more information, which helps significantly if you are developing a schema.
Meta-schema validation also works with a custom `$schema`. In case `$schema` is missing, or `AutoDetect` is set to `false`, the meta-schema of the used draft is used.
## Working with Errors
The library handles string error codes which you can customize by creating your own gojsonschema.locale and setting it
```go
gojsonschema.Locale = YourCustomLocale{}
```
However, each error contains additional contextual information.
Newer versions of `gojsonschema` may have new additional errors, so code that uses a custom locale will need to be updated when this happens.
**err.Type()**: *string* Returns the "type" of error that occurred. Note you can also type check. See below
Note: An error of RequiredType has an err.Type() return value of "required"
"required": RequiredError
"invalid_type": InvalidTypeError
"number_any_of": NumberAnyOfError
"number_one_of": NumberOneOfError
"number_all_of": NumberAllOfError
"number_not": NumberNotError
"missing_dependency": MissingDependencyError
"internal": InternalError
"const": ConstEror
"enum": EnumError
"array_no_additional_items": ArrayNoAdditionalItemsError
"array_min_items": ArrayMinItemsError
"array_max_items": ArrayMaxItemsError
"unique": ItemsMustBeUniqueError
"contains" : ArrayContainsError
"array_min_properties": ArrayMinPropertiesError
"array_max_properties": ArrayMaxPropertiesError
"additional_property_not_allowed": AdditionalPropertyNotAllowedError
"invalid_property_pattern": InvalidPropertyPatternError
"invalid_property_name": InvalidPropertyNameError
"string_gte": StringLengthGTEError
"string_lte": StringLengthLTEError
"pattern": DoesNotMatchPatternError
"multiple_of": MultipleOfError
"number_gte": NumberGTEError
"number_gt": NumberGTError
"number_lte": NumberLTEError
"number_lt": NumberLTError
"condition_then" : ConditionThenError
"condition_else" : ConditionElseError
**err.Value()**: *interface{}* Returns the value given
**err.Context()**: *gojsonschema.JsonContext* Returns the context. This has a String() method that will print something like this: (root).firstName
**err.Field()**: *string* Returns the fieldname in the format firstName, or for embedded properties, person.firstName. This returns the same as the String() method on *err.Context()* but removes the (root). prefix.
**err.Description()**: *string* The error description. This is based on the locale you are using. See the beginning of this section for overwriting the locale with a custom implementation.
**err.DescriptionFormat()**: *string* The error description format. This is relevant if you are adding custom validation errors afterwards to the result.
**err.Details()**: *gojsonschema.ErrorDetails* Returns a map[string]interface{} of additional error details specific to the error. For example, GTE errors will have a "min" value, LTE will have a "max" value. See errors.go for a full description of all the error details. Every error always contains a "field" key that holds the value of *err.Field()*
Note in most cases, the err.Details() will be used to generate replacement strings in your locales, and not used directly. These strings follow the text/template format i.e.
```
{{.field}} must be greater than or equal to {{.min}}
```
The library allows you to specify custom template functions, should you require more complex error message handling.
```go
gojsonschema.ErrorTemplateFuncs = map[string]interface{}{
"allcaps": func(s string) string {
return strings.ToUpper(s)
},
}
```
Given the above definition, you can use the custom function `"allcaps"` in your localization templates:
```
{{allcaps .field}} must be greater than or equal to {{.min}}
```
The above error message would then be rendered with the `field` value in capital letters. For example:
```
"PASSWORD must be greater than or equal to 8"
```
Learn more about what types of template functions you can use in `ErrorTemplateFuncs` by referring to Go's [text/template FuncMap](https://golang.org/pkg/text/template/#FuncMap) type.
## Formats
JSON Schema allows for optional "format" property to validate instances against well-known formats. gojsonschema ships with all of the formats defined in the spec that you can use like this:
````json
{"type": "string", "format": "email"}
````
Not all formats defined in draft-07 are available. Implemented formats are:
* `date`
* `time`
* `date-time`
* `hostname`. Subdomains that start with a number are also supported, but this means that it doesn't strictly follow [RFC1034](http://tools.ietf.org/html/rfc1034#section-3.5) and has the implication that ipv4 addresses are also recognized as valid hostnames.
* `email`. Go's email parser deviates slightly from [RFC5322](https://tools.ietf.org/html/rfc5322). Includes unicode support.
* `idn-email`. Same caveat as `email`.
* `ipv4`
* `ipv6`
* `uri`. Includes unicode support.
* `uri-reference`. Includes unicode support.
* `iri`
* `iri-reference`
* `uri-template`
* `uuid`
* `regex`. Go uses the [RE2](https://github.com/google/re2/wiki/Syntax) engine and is not [ECMA262](http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf) compatible.
* `json-pointer`
* `relative-json-pointer`
`email`, `uri` and `uri-reference` use the same validation code as their unicode counterparts `idn-email`, `iri` and `iri-reference`. If you rely on unicode support you should use the specific
unicode enabled formats for the sake of interoperability as other implementations might not support unicode in the regular formats.
The validation code for `uri`, `idn-email` and their relatives use mostly standard library code.
For repetitive or more complex formats, you can create custom format checkers and add them to gojsonschema like this:
```go
// Define the format checker
type RoleFormatChecker struct {}
// Ensure it meets the gojsonschema.FormatChecker interface
func (f RoleFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if ok == false {
return false
}
return strings.HasPrefix("ROLE_", asString)
}
// Add it to the library
gojsonschema.FormatCheckers.Add("role", RoleFormatChecker{})
````
Now to use in your json schema:
````json
{"type": "string", "format": "role"}
````
Another example would be to check if the provided integer matches an id on database:
JSON schema:
```json
{"type": "integer", "format": "ValidUserId"}
```
```go
// Define the format checker
type ValidUserIdFormatChecker struct {}
// Ensure it meets the gojsonschema.FormatChecker interface
func (f ValidUserIdFormatChecker) IsFormat(input interface{}) bool {
asFloat64, ok := input.(float64) // Numbers are always float64 here
if ok == false {
return false
}
// XXX
// do the magic on the database looking for the int(asFloat64)
return true
}
// Add it to the library
gojsonschema.FormatCheckers.Add("ValidUserId", ValidUserIdFormatChecker{})
````
Formats can also be removed, for example if you want to override one of the formats that is defined by default.
```go
gojsonschema.FormatCheckers.Remove("hostname")
```
## Additional custom validation
After the validation has run and you have the results, you may add additional
errors using `Result.AddError`. This is useful to maintain the same format within the resultset instead
of having to add special exceptions for your own errors. Below is an example.
```go
type AnswerInvalidError struct {
gojsonschema.ResultErrorFields
}
func newAnswerInvalidError(context *gojsonschema.JsonContext, value interface{}, details gojsonschema.ErrorDetails) *AnswerInvalidError {
err := AnswerInvalidError{}
err.SetContext(context)
err.SetType("custom_invalid_error")
// it is important to use SetDescriptionFormat() as this is used to call SetDescription() after it has been parsed
// using the description of err will be overridden by this.
err.SetDescriptionFormat("Answer to the Ultimate Question of Life, the Universe, and Everything is {{.answer}}")
err.SetValue(value)
err.SetDetails(details)
return &err
}
func main() {
// ...
schema, err := gojsonschema.NewSchema(schemaLoader)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if true { // some validation
jsonContext := gojsonschema.NewJsonContext("question", nil)
errDetail := gojsonschema.ErrorDetails{
"answer": 42,
}
result.AddError(
newAnswerInvalidError(
gojsonschema.NewJsonContext("answer", jsonContext),
52,
errDetail,
),
errDetail,
)
}
return result, err
}
```
This is especially useful if you want to add validation beyond what the
json schema drafts can provide such business specific logic.
## Uses
gojsonschema uses the following test suite :
https://github.com/json-schema/JSON-Schema-Test-Suite
@@ -0,0 +1,122 @@
// Copyright 2018 johandorland ( https://github.com/johandorland )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gojsonschema
import (
"errors"
"math"
"github.com/xeipuuv/gojsonreference"
)
// Draft is a JSON-schema draft version
type Draft int
// Supported Draft versions
const (
Draft4 Draft = 4
Draft6 Draft = 6
Draft7 Draft = 7
Hybrid Draft = math.MaxInt32
)
type draftConfig struct {
Version Draft
MetaSchemaURL string
MetaSchema string
}
type draftConfigs []draftConfig
var drafts draftConfigs
func init() {
drafts = []draftConfig{
{
Version: Draft4,
MetaSchemaURL: "http://json-schema.org/draft-04/schema",
MetaSchema: `{"id":"http://json-schema.org/draft-04/schema#","$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"},"$schema":{"type":"string"},"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}]},"format":{"type":"string"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"dependencies":{"exclusiveMaximum":["maximum"],"exclusiveMinimum":["minimum"]},"default":{}}`,
},
{
Version: Draft6,
MetaSchemaURL: "http://json-schema.org/draft-06/schema",
MetaSchema: `{"$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":{},"examples":{"type":"array","items":{}},"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":{}}`,
},
{
Version: Draft7,
MetaSchemaURL: "http://json-schema.org/draft-07/schema",
MetaSchema: `{"$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},"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}`,
},
}
}
func (dc draftConfigs) GetMetaSchema(url string) string {
for _, config := range dc {
if config.MetaSchemaURL == url {
return config.MetaSchema
}
}
return ""
}
func (dc draftConfigs) GetDraftVersion(url string) *Draft {
for _, config := range dc {
if config.MetaSchemaURL == url {
return &config.Version
}
}
return nil
}
func (dc draftConfigs) GetSchemaURL(draft Draft) string {
for _, config := range dc {
if config.Version == draft {
return config.MetaSchemaURL
}
}
return ""
}
func parseSchemaURL(documentNode any) (string, *Draft, error) {
if _, ok := documentNode.(bool); ok {
return "", nil, nil
}
m, ok := documentNode.(map[string]any)
if !ok {
return "", nil, errors.New("schema is invalid")
}
if v, ok := m[KeySchema]; ok {
s, ok := v.(string)
if !ok {
return "", nil, errors.New(formatErrorDescription(
Locale.MustBeOfType(),
ErrorDetails{
"key": KeySchema,
"type": TypeString,
}))
}
schemaReference, err := gojsonreference.NewJsonReference(s)
if err != nil {
return "", nil, err
}
schema := schemaReference.String()
return schema, drafts.GetDraftVersion(schema), nil
}
return "", nil, nil
}
@@ -0,0 +1,366 @@
// nolint: goconst // String duplication will be handled later by using errors.Is.
package gojsonschema
import (
"bytes"
"sync"
"text/template"
)
var errorTemplates = errorTemplate{template.New("errors-new"), sync.RWMutex{}}
// template.Template is not thread-safe for writing, so some locking is done
// sync.RWMutex is used for efficiently locking when new templates are created
type errorTemplate struct {
*template.Template
sync.RWMutex
}
type (
// FalseError indicates that -
// ErrorDetails: -
FalseError struct {
ResultErrorFields
}
// RequiredError indicates that a required field is missing
// ErrorDetails: property string
RequiredError struct {
ResultErrorFields
}
// InvalidTypeError indicates that a field has the incorrect type
// ErrorDetails: expected, given
InvalidTypeError struct {
ResultErrorFields
}
// NumberAnyOfError is produced in case of a failing "anyOf" validation
// ErrorDetails: -
NumberAnyOfError struct {
ResultErrorFields
}
// NumberOneOfError is produced in case of a failing "oneOf" validation
// ErrorDetails: -
NumberOneOfError struct {
ResultErrorFields
}
// NumberAllOfError is produced in case of a failing "allOf" validation
// ErrorDetails: -
NumberAllOfError struct {
ResultErrorFields
}
// NumberNotError is produced if a "not" validation failed
// ErrorDetails: -
NumberNotError struct {
ResultErrorFields
}
// MissingDependencyError is produced in case of a "missing dependency" problem
// ErrorDetails: dependency
MissingDependencyError struct {
ResultErrorFields
}
// InternalError indicates an internal error
// ErrorDetails: error
InternalError struct {
ResultErrorFields
}
// ConstError indicates a const error
// ErrorDetails: allowed
ConstError struct {
ResultErrorFields
}
// EnumError indicates an enum error
// ErrorDetails: allowed
EnumError struct {
ResultErrorFields
}
// ArrayNoAdditionalItemsError is produced if additional items were found, but not allowed
// ErrorDetails: -
ArrayNoAdditionalItemsError struct {
ResultErrorFields
}
// ArrayMinItemsError is produced if an array contains less items than the allowed minimum
// ErrorDetails: min
ArrayMinItemsError struct {
ResultErrorFields
}
// ArrayMaxItemsError is produced if an array contains more items than the allowed maximum
// ErrorDetails: max
ArrayMaxItemsError struct {
ResultErrorFields
}
// ItemsMustBeUniqueError is produced if an array requires unique items, but contains non-unique items
// ErrorDetails: type, i, j
ItemsMustBeUniqueError struct {
ResultErrorFields
}
// ArrayContainsError is produced if an array contains invalid items
// ErrorDetails:
ArrayContainsError struct {
ResultErrorFields
}
// ArrayMinPropertiesError is produced if an object contains less properties than the allowed minimum
// ErrorDetails: min
ArrayMinPropertiesError struct {
ResultErrorFields
}
// ArrayMaxPropertiesError is produced if an object contains more properties than the allowed maximum
// ErrorDetails: max
ArrayMaxPropertiesError struct {
ResultErrorFields
}
// AdditionalPropertyNotAllowedError is produced if an object has additional properties, but not allowed
// ErrorDetails: property
AdditionalPropertyNotAllowedError struct {
ResultErrorFields
}
// InvalidPropertyPatternError is produced if an pattern was found
// ErrorDetails: property, pattern
InvalidPropertyPatternError struct {
ResultErrorFields
}
// InvalidPropertyNameError is produced if an invalid-named property was found
// ErrorDetails: property
InvalidPropertyNameError struct {
ResultErrorFields
}
// StringLengthGTEError is produced if a string is shorter than the minimum required length
// ErrorDetails: min
StringLengthGTEError struct {
ResultErrorFields
}
// StringLengthLTEError is produced if a string is longer than the maximum allowed length
// ErrorDetails: max
StringLengthLTEError struct {
ResultErrorFields
}
// DoesNotMatchPatternError is produced if a string does not match the defined pattern
// ErrorDetails: pattern
DoesNotMatchPatternError struct {
ResultErrorFields
}
// DoesNotMatchFormatError is produced if a string does not match the defined format
// ErrorDetails: format
DoesNotMatchFormatError struct {
ResultErrorFields
}
// MultipleOfError is produced if a number is not a multiple of the defined multipleOf
// ErrorDetails: multiple
MultipleOfError struct {
ResultErrorFields
}
// NumberGTEError is produced if a number is lower than the allowed minimum
// ErrorDetails: min
NumberGTEError struct {
ResultErrorFields
}
// NumberGTError is produced if a number is lower than, or equal to the specified minimum, and exclusiveMinimum is set
// ErrorDetails: min
NumberGTError struct {
ResultErrorFields
}
// NumberLTEError is produced if a number is higher than the allowed maximum
// ErrorDetails: max
NumberLTEError struct {
ResultErrorFields
}
// NumberLTError is produced if a number is higher than, or equal to the specified maximum, and exclusiveMaximum is set
// ErrorDetails: max
NumberLTError struct {
ResultErrorFields
}
// ConditionThenError is produced if a condition's "then" validation is invalid
// ErrorDetails: -
ConditionThenError struct {
ResultErrorFields
}
// ConditionElseError is produced if a condition's "else" condition is invalid
// ErrorDetails: -
ConditionElseError struct {
ResultErrorFields
}
)
// newError takes a ResultError type and sets the type, context, description, details, value, and field
func newError(err ResultError, context *JSONContext, value any, locale locale, details ErrorDetails) {
var t string
var d string
switch err.(type) {
case *FalseError:
t = "false"
d = locale.False()
case *RequiredError:
t = "required"
d = locale.Required()
case *InvalidTypeError:
t = "invalid_type"
d = locale.InvalidType()
case *NumberAnyOfError:
t = "number_any_of"
d = locale.NumberAnyOf()
case *NumberOneOfError:
t = "number_one_of"
d = locale.NumberOneOf()
case *NumberAllOfError:
t = "number_all_of"
d = locale.NumberAllOf()
case *NumberNotError:
t = "number_not"
d = locale.NumberNot()
case *MissingDependencyError:
t = "missing_dependency"
d = locale.MissingDependency()
case *InternalError:
t = "internal"
d = locale.Internal()
case *ConstError:
t = "const"
d = locale.Const()
case *EnumError:
t = "enum"
d = locale.Enum()
case *ArrayNoAdditionalItemsError:
t = "array_no_additional_items"
d = locale.ArrayNoAdditionalItems()
case *ArrayMinItemsError:
t = "array_min_items"
d = locale.ArrayMinItems()
case *ArrayMaxItemsError:
t = "array_max_items"
d = locale.ArrayMaxItems()
case *ItemsMustBeUniqueError:
t = "unique"
d = locale.Unique()
case *ArrayContainsError:
t = "contains"
d = locale.ArrayContains()
case *ArrayMinPropertiesError:
t = "array_min_properties"
d = locale.ArrayMinProperties()
case *ArrayMaxPropertiesError:
t = "array_max_properties"
d = locale.ArrayMaxProperties()
case *AdditionalPropertyNotAllowedError:
t = "additional_property_not_allowed"
d = locale.AdditionalPropertyNotAllowed()
case *InvalidPropertyPatternError:
t = "invalid_property_pattern"
d = locale.InvalidPropertyPattern()
case *InvalidPropertyNameError:
t = "invalid_property_name"
d = locale.InvalidPropertyName()
case *StringLengthGTEError:
t = "string_gte"
d = locale.StringGTE()
case *StringLengthLTEError:
t = "string_lte"
d = locale.StringLTE()
case *DoesNotMatchPatternError:
t = "pattern"
d = locale.DoesNotMatchPattern()
case *DoesNotMatchFormatError:
t = "format"
d = locale.DoesNotMatchFormat()
case *MultipleOfError:
t = "multiple_of"
d = locale.MultipleOf()
case *NumberGTEError:
t = "number_gte"
d = locale.NumberGTE()
case *NumberGTError:
t = "number_gt"
d = locale.NumberGT()
case *NumberLTEError:
t = "number_lte"
d = locale.NumberLTE()
case *NumberLTError:
t = "number_lt"
d = locale.NumberLT()
case *ConditionThenError:
t = "condition_then"
d = locale.ConditionThen()
case *ConditionElseError:
t = "condition_else"
d = locale.ConditionElse()
}
err.SetType(t)
err.SetContext(context)
err.SetValue(value)
err.SetDetails(details)
err.SetDescriptionFormat(d)
details["field"] = err.Field()
if _, exists := details["context"]; !exists && context != nil {
details["context"] = context.String()
}
err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details))
}
// formatErrorDescription takes a string in the default text/template
// format and converts it to a string with replacements. The fields come
// from the ErrorDetails struct and vary for each type of error.
func formatErrorDescription(s string, details ErrorDetails) string {
var tpl *template.Template
var descrAsBuffer bytes.Buffer
var err error
errorTemplates.RLock()
tpl = errorTemplates.Lookup(s)
errorTemplates.RUnlock()
if tpl == nil {
errorTemplates.Lock()
tpl = errorTemplates.New(s)
if ErrorTemplateFuncs != nil {
tpl.Funcs(ErrorTemplateFuncs)
}
tpl, err = tpl.Parse(s)
errorTemplates.Unlock()
if err != nil {
return err.Error()
}
}
err = tpl.Execute(&descrAsBuffer, details)
if err != nil {
return err.Error()
}
return descrAsBuffer.String()
}
@@ -0,0 +1,368 @@
package gojsonschema
import (
"net"
"net/mail"
"net/url"
"regexp"
"strings"
"sync"
"time"
)
type (
// FormatChecker is the interface all formatters added to FormatCheckerChain must implement
FormatChecker interface {
// IsFormat checks if input has the correct format
IsFormat(input any) bool
}
// FormatCheckerChain holds the formatters
FormatCheckerChain struct {
formatters map[string]FormatChecker
}
// EmailFormatChecker verifies email address formats
EmailFormatChecker struct{}
// IPV4FormatChecker verifies IP addresses in the IPv4 format
IPV4FormatChecker struct{}
// IPV6FormatChecker verifies IP addresses in the IPv6 format
IPV6FormatChecker struct{}
// DateTimeFormatChecker verifies date/time formats per RFC3339 5.6
//
// Valid formats:
// Partial Time: HH:MM:SS
// Full Date: YYYY-MM-DD
// Full Time: HH:MM:SSZ-07:00
// Date Time: YYYY-MM-DDTHH:MM:SSZ-0700
//
// Where
// YYYY = 4DIGIT year
// MM = 2DIGIT month ; 01-12
// DD = 2DIGIT day-month ; 01-28, 01-29, 01-30, 01-31 based on month/year
// HH = 2DIGIT hour ; 00-23
// MM = 2DIGIT ; 00-59
// SS = 2DIGIT ; 00-58, 00-60 based on leap second rules
// T = Literal
// Z = Literal
//
// Note: Nanoseconds are also suported in all formats
//
// http://tools.ietf.org/html/rfc3339#section-5.6
DateTimeFormatChecker struct{}
// DateFormatChecker verifies date formats
//
// Valid format:
// Full Date: YYYY-MM-DD
//
// Where
// YYYY = 4DIGIT year
// MM = 2DIGIT month ; 01-12
// DD = 2DIGIT day-month ; 01-28, 01-29, 01-30, 01-31 based on month/year
DateFormatChecker struct{}
// TimeFormatChecker verifies time formats
//
// Valid formats:
// Partial Time: HH:MM:SS
// Full Time: HH:MM:SSZ-07:00
//
// Where
// HH = 2DIGIT hour ; 00-23
// MM = 2DIGIT ; 00-59
// SS = 2DIGIT ; 00-58, 00-60 based on leap second rules
// T = Literal
// Z = Literal
TimeFormatChecker struct{}
// URIFormatChecker validates a URI with a valid Scheme per RFC3986
URIFormatChecker struct{}
// URIReferenceFormatChecker validates a URI or relative-reference per RFC3986
URIReferenceFormatChecker struct{}
// URITemplateFormatChecker validates a URI template per RFC6570
URITemplateFormatChecker struct{}
// HostnameFormatChecker validates a hostname is in the correct format
HostnameFormatChecker struct{}
// UUIDFormatChecker validates a UUID is in the correct format
UUIDFormatChecker struct{}
// RegexFormatChecker validates a regex is in the correct format
RegexFormatChecker struct{}
// JSONPointerFormatChecker validates a JSON Pointer per RFC6901
JSONPointerFormatChecker struct{}
// RelativeJSONPointerFormatChecker validates a relative JSON Pointer is in the correct format
RelativeJSONPointerFormatChecker struct{}
)
var (
// FormatCheckers holds the valid formatters, and is a public variable
// so library users can add custom formatters
FormatCheckers = FormatCheckerChain{
formatters: map[string]FormatChecker{
"date": DateFormatChecker{},
"time": TimeFormatChecker{},
"date-time": DateTimeFormatChecker{},
"hostname": HostnameFormatChecker{},
"email": EmailFormatChecker{},
"idn-email": EmailFormatChecker{},
"ipv4": IPV4FormatChecker{},
"ipv6": IPV6FormatChecker{},
"uri": URIFormatChecker{},
"uri-reference": URIReferenceFormatChecker{},
"iri": URIFormatChecker{},
"iri-reference": URIReferenceFormatChecker{},
"uri-template": URITemplateFormatChecker{},
"uuid": UUIDFormatChecker{},
"regex": RegexFormatChecker{},
"json-pointer": JSONPointerFormatChecker{},
"relative-json-pointer": RelativeJSONPointerFormatChecker{},
},
}
// Regex credit: https://www.socketloop.com/tutorials/golang-validate-hostname
rxHostname = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$`)
// Use a regex to make sure curly brackets are balanced properly after validating it as a AURI
rxURITemplate = regexp.MustCompile("^([^{]*({[^}]*})?)*$")
rxUUID = regexp.MustCompile("^(?i)[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$")
rxJSONPointer = regexp.MustCompile("^(?:/(?:[^~/]|~0|~1)*)*$")
rxRelJSONPointer = regexp.MustCompile("^(?:0|[1-9][0-9]*)(?:#|(?:/(?:[^~/]|~0|~1)*)*)$")
lock = new(sync.RWMutex)
)
// Add adds a FormatChecker to the FormatCheckerChain
// The name used will be the value used for the format key in your json schema
func (c *FormatCheckerChain) Add(name string, f FormatChecker) *FormatCheckerChain {
lock.Lock()
c.formatters[name] = f
lock.Unlock()
return c
}
// Remove deletes a FormatChecker from the FormatCheckerChain (if it exists)
func (c *FormatCheckerChain) Remove(name string) *FormatCheckerChain {
lock.Lock()
delete(c.formatters, name)
lock.Unlock()
return c
}
// Has checks to see if the FormatCheckerChain holds a FormatChecker with the given name
func (c *FormatCheckerChain) Has(name string) bool {
lock.RLock()
_, ok := c.formatters[name]
lock.RUnlock()
return ok
}
// IsFormat will check an input against a FormatChecker with the given name
// to see if it is the correct format
func (c *FormatCheckerChain) IsFormat(name string, input any) bool {
lock.RLock()
f, ok := c.formatters[name]
lock.RUnlock()
// If a format is unrecognized it should always pass validation
if !ok {
return true
}
return f.IsFormat(input)
}
// IsFormat checks if input is a correctly formatted e-mail address
func (f EmailFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
_, err := mail.ParseAddress(asString)
return err == nil
}
// IsFormat checks if input is a correctly formatted IPv4-address
func (f IPV4FormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
// Credit: https://github.com/asaskevich/govalidator
ip := net.ParseIP(asString)
return ip != nil && strings.Contains(asString, ".")
}
// IsFormat checks if input is a correctly formatted IPv6=address
func (f IPV6FormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
// Credit: https://github.com/asaskevich/govalidator
ip := net.ParseIP(asString)
return ip != nil && strings.Contains(asString, ":")
}
// IsFormat checks if input is a correctly formatted date/time per RFC3339 5.6
func (f DateTimeFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
formats := []string{
"15:04:05",
"15:04:05Z07:00",
"2006-01-02",
time.RFC3339,
time.RFC3339Nano,
}
for _, format := range formats {
if _, err := time.Parse(format, asString); err == nil {
return true
}
}
return false
}
// IsFormat checks if input is a correctly formatted date (YYYY-MM-DD)
func (f DateFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
_, err := time.Parse("2006-01-02", asString)
return err == nil
}
// IsFormat checks if input correctly formatted time (HH:MM:SS or HH:MM:SSZ-07:00)
func (f TimeFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
if _, err := time.Parse("15:04:05Z07:00", asString); err == nil {
return true
}
_, err := time.Parse("15:04:05", asString)
return err == nil
}
// IsFormat checks if input is correctly formatted URI with a valid Scheme per RFC3986
func (f URIFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
u, err := url.Parse(asString)
if err != nil || u.Scheme == "" {
return false
}
return !strings.Contains(asString, `\`)
}
// IsFormat checks if input is a correctly formatted URI or relative-reference per RFC3986
func (f URIReferenceFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
_, err := url.Parse(asString)
return err == nil && !strings.Contains(asString, `\`)
}
// IsFormat checks if input is a correctly formatted URI template per RFC6570
func (f URITemplateFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
u, err := url.Parse(asString)
if err != nil || strings.Contains(asString, `\`) {
return false
}
return rxURITemplate.MatchString(u.Path)
}
// IsFormat checks if input is a correctly formatted hostname
func (f HostnameFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
return rxHostname.MatchString(asString) && len(asString) < 256
}
// IsFormat checks if input is a correctly formatted UUID
func (f UUIDFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
return rxUUID.MatchString(asString)
}
// IsFormat checks if input is a correctly formatted regular expression
func (f RegexFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
if asString == "" {
return true
}
_, err := regexp.Compile(asString)
return err == nil
}
// IsFormat checks if input is a correctly formatted JSON Pointer per RFC6901
func (f JSONPointerFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
return rxJSONPointer.MatchString(asString)
}
// IsFormat checks if input is a correctly formatted relative JSON Pointer
func (f RelativeJSONPointerFormatChecker) IsFormat(input any) bool {
asString, ok := input.(string)
if !ok {
return true
}
return rxRelJSONPointer.MatchString(asString)
}
@@ -0,0 +1,37 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Very simple log wrapper.
// Used for debugging/testing purposes.
//
// created 01-01-2015
package gojsonschema
import (
"log"
)
const internalLogEnabled = false
func internalLog(format string, v ...any) {
log.Printf(format, v...)
}
@@ -0,0 +1,73 @@
// Copyright 2013 MongoDB, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author tolsen
// author-github https://github.com/tolsen
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Implements a persistent (immutable w/ shared structure) singly-linked list of strings for the purpose of storing a json context
//
// created 04-09-2013
package gojsonschema
import "bytes"
// JSONContext implements a persistent linked-list of strings
type JSONContext struct {
head string
tail *JSONContext
}
// NewJSONContext creates a new JSONContext
func NewJSONContext(head string, tail *JSONContext) *JSONContext {
return &JSONContext{head, tail}
}
// String displays the context in reverse.
// This plays well with the data structure's persistent nature with
// Cons and a json document's tree structure.
func (c *JSONContext) String(del ...string) string {
byteArr := make([]byte, 0, c.stringLen())
buf := bytes.NewBuffer(byteArr)
c.writeStringToBuffer(buf, del)
return buf.String()
}
func (c *JSONContext) stringLen() int {
length := 0
if c.tail != nil {
length = c.tail.stringLen() + 1 // add 1 for "."
}
length += len(c.head)
return length
}
func (c *JSONContext) writeStringToBuffer(buf *bytes.Buffer, del []string) {
if c.tail != nil {
c.tail.writeStringToBuffer(buf, del)
if len(del) > 0 {
buf.WriteString(del[0])
} else {
buf.WriteString(".")
}
}
buf.WriteString(c.head)
}
@@ -0,0 +1,410 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Different strategies to load JSON files.
// Includes References (file and HTTP), JSON strings and Go types.
//
// created 01-02-2015
package gojsonschema
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/xeipuuv/gojsonreference"
)
// NOTE(sr): We need to control from which hosts remote references are
// allowed to be resolved via HTTP requests. It's quite cumbersome to
// add extra parameters to all calls and interfaces involved, so we're
// using a global variable instead:
var allowNet map[string]struct{}
var netMut sync.RWMutex
func SetAllowNet(hosts []string) {
netMut.Lock()
defer netMut.Unlock()
if hosts == nil {
allowNet = nil // resetting the global
return
}
allowNet = make(map[string]struct{}, len(hosts))
for _, host := range hosts {
allowNet[host] = struct{}{}
}
}
func isAllowed(ref *url.URL) bool {
netMut.RLock()
defer netMut.RUnlock()
if allowNet == nil {
return true
}
_, ok := allowNet[ref.Hostname()]
return ok
}
var osFS = osFileSystem(os.Open)
// JSONLoader defines the JSON loader interface
type JSONLoader interface {
JSONSource() any
LoadJSON() (any, error)
JSONReference() (gojsonreference.JsonReference, error)
LoaderFactory() JSONLoaderFactory
}
// JSONLoaderFactory defines the JSON loader factory interface
type JSONLoaderFactory interface {
// New creates a new JSON loader for the given source
New(source string) JSONLoader
}
// DefaultJSONLoaderFactory is the default JSON loader factory
type DefaultJSONLoaderFactory struct {
}
// FileSystemJSONLoaderFactory is a JSON loader factory that uses http.FileSystem
type FileSystemJSONLoaderFactory struct {
fs http.FileSystem
}
// New creates a new JSON loader for the given source
func (d DefaultJSONLoaderFactory) New(source string) JSONLoader {
return &jsonReferenceLoader{
fs: osFS,
source: source,
}
}
// New creates a new JSON loader for the given source
func (f FileSystemJSONLoaderFactory) New(source string) JSONLoader {
return &jsonReferenceLoader{
fs: f.fs,
source: source,
}
}
// osFileSystem is a functional wrapper for os.Open that implements http.FileSystem.
type osFileSystem func(string) (*os.File, error)
// Opens a file with the given name
func (o osFileSystem) Open(name string) (http.File, error) {
return o(name)
}
// JSON Reference loader
// references are used to load JSONs from files and HTTP
type jsonReferenceLoader struct {
fs http.FileSystem
source string
}
func (l *jsonReferenceLoader) JSONSource() any {
return l.source
}
func (l *jsonReferenceLoader) JSONReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference(l.JSONSource().(string))
}
func (l *jsonReferenceLoader) LoaderFactory() JSONLoaderFactory {
return &FileSystemJSONLoaderFactory{
fs: l.fs,
}
}
// NewReferenceLoader returns a JSON reference loader using the given source and the local OS file system.
func NewReferenceLoader(source string) JSONLoader {
return &jsonReferenceLoader{
fs: osFS,
source: source,
}
}
// NewReferenceLoaderFileSystem returns a JSON reference loader using the given source and file system.
func NewReferenceLoaderFileSystem(source string, fs http.FileSystem) JSONLoader {
return &jsonReferenceLoader{
fs: fs,
source: source,
}
}
func (l *jsonReferenceLoader) LoadJSON() (any, error) {
var err error
reference, err := gojsonreference.NewJsonReference(l.JSONSource().(string))
if err != nil {
return nil, err
}
refToURL := reference
refToURL.GetUrl().Fragment = ""
if reference.HasFileScheme {
filename := strings.TrimPrefix(refToURL.String(), "file://")
filename, err = url.QueryUnescape(filename)
if err != nil {
return nil, err
}
if runtime.GOOS == "windows" {
// on Windows, a file URL may have an extra leading slash, use slashes
// instead of backslashes, and have spaces escaped
filename = strings.TrimPrefix(filename, "/")
filename = filepath.FromSlash(filename)
}
return l.loadFromFile(filename)
}
// NOTE(sr): hardcoded metaschema references are not subject to allow_net
// checking; their contents are hardcoded in the library!
//
// returned cached versions for metaschemas for drafts 4, 6 and 7
// for performance and allow for easier offline use
if metaSchema := drafts.GetMetaSchema(refToURL.String()); metaSchema != "" {
return decodeJSONUsingNumber(strings.NewReader(metaSchema))
}
if isAllowed(refToURL.GetUrl()) {
return l.loadFromHTTP(refToURL.String())
}
return nil, fmt.Errorf("remote reference loading disabled: %s", reference.String())
}
func (l *jsonReferenceLoader) loadFromHTTP(address string) (any, error) {
resp, err := http.Get(address)
if err != nil {
return nil, err
}
// must return HTTP Status 200 OK
if resp.StatusCode != http.StatusOK {
return nil, errors.New(formatErrorDescription(Locale.HTTPBadStatus(), ErrorDetails{"status": resp.Status}))
}
bodyBuff, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return decodeJSONUsingNumber(bytes.NewReader(bodyBuff))
}
func (l *jsonReferenceLoader) loadFromFile(path string) (any, error) {
f, err := l.fs.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
bodyBuff, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return decodeJSONUsingNumber(bytes.NewReader(bodyBuff))
}
// JSON string loader
type jsonStringLoader struct {
source string
}
func (l *jsonStringLoader) JSONSource() any {
return l.source
}
func (l *jsonStringLoader) JSONReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonStringLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
// NewStringLoader creates a new JSONLoader, taking a string as source
func NewStringLoader(source string) JSONLoader {
return &jsonStringLoader{source: source}
}
func (l *jsonStringLoader) LoadJSON() (any, error) {
return decodeJSONUsingNumber(strings.NewReader(l.JSONSource().(string)))
}
// JSON bytes loader
type jsonBytesLoader struct {
source []byte
}
func (l *jsonBytesLoader) JSONSource() any {
return l.source
}
func (l *jsonBytesLoader) JSONReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonBytesLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
// NewBytesLoader creates a new JSONLoader, taking a `[]byte` as source
func NewBytesLoader(source []byte) JSONLoader {
return &jsonBytesLoader{source: source}
}
func (l *jsonBytesLoader) LoadJSON() (any, error) {
return decodeJSONUsingNumber(bytes.NewReader(l.JSONSource().([]byte)))
}
// JSON Go (types) loader
// used to load JSONs from the code as maps, any, structs ...
type jsonGoLoader struct {
source any
}
func (l *jsonGoLoader) JSONSource() any {
return l.source
}
func (l *jsonGoLoader) JSONReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonGoLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
// NewGoLoader creates a new JSONLoader from a given Go struct
func NewGoLoader(source any) JSONLoader {
return &jsonGoLoader{source: source}
}
func (l *jsonGoLoader) LoadJSON() (any, error) {
// convert it to a compliant JSON first to avoid types "mismatches"
jsonBytes, err := json.Marshal(l.JSONSource())
if err != nil {
return nil, err
}
return decodeJSONUsingNumber(bytes.NewReader(jsonBytes))
}
type jsonIOLoader struct {
buf *bytes.Buffer
}
// NewReaderLoader creates a new JSON loader using the provided io.Reader
func NewReaderLoader(source io.Reader) (JSONLoader, io.Reader) {
buf := &bytes.Buffer{}
return &jsonIOLoader{buf: buf}, io.TeeReader(source, buf)
}
// NewWriterLoader creates a new JSON loader using the provided io.Writer
func NewWriterLoader(source io.Writer) (JSONLoader, io.Writer) {
buf := &bytes.Buffer{}
return &jsonIOLoader{buf: buf}, io.MultiWriter(source, buf)
}
func (l *jsonIOLoader) JSONSource() any {
return l.buf.String()
}
func (l *jsonIOLoader) LoadJSON() (any, error) {
return decodeJSONUsingNumber(l.buf)
}
func (l *jsonIOLoader) JSONReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonIOLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
// JSON raw loader
// In case the JSON is already marshalled to any use this loader
// This is used for testing as otherwise there is no guarantee the JSON is marshalled
// "properly" by using https://golang.org/pkg/encoding/json/#Decoder.UseNumber
type jsonRawLoader struct {
source any
}
// NewRawLoader creates a new JSON raw loader for the given source
func NewRawLoader(source any) JSONLoader {
return &jsonRawLoader{source: source}
}
func (l *jsonRawLoader) JSONSource() any {
return l.source
}
func (l *jsonRawLoader) LoadJSON() (any, error) {
return l.source, nil
}
func (l *jsonRawLoader) JSONReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonRawLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
func decodeJSONUsingNumber(r io.Reader) (any, error) {
var document any
decoder := json.NewDecoder(r)
decoder.UseNumber()
err := decoder.Decode(&document)
if err != nil {
return nil, err
}
return document, nil
}
@@ -0,0 +1,472 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Contains const string and messages.
//
// created 01-01-2015
package gojsonschema
type (
// locale is an interface for defining custom error strings
locale interface {
// False returns a format-string for "false" schema validation errors
False() string
// Required returns a format-string for "required" schema validation errors
Required() string
// InvalidType returns a format-string for "invalid type" schema validation errors
InvalidType() string
// NumberAnyOf returns a format-string for "anyOf" schema validation errors
NumberAnyOf() string
// NumberOneOf returns a format-string for "oneOf" schema validation errors
NumberOneOf() string
// NumberAllOf returns a format-string for "allOf" schema validation errors
NumberAllOf() string
// NumberNot returns a format-string to format a NumberNotError
NumberNot() string
// MissingDependency returns a format-string for "missing dependency" schema validation errors
MissingDependency() string
// Internal returns a format-string for internal errors
Internal() string
// Const returns a format-string to format a ConstError
Const() string
// Enum returns a format-string to format an EnumError
Enum() string
// ArrayNotEnoughItems returns a format-string to format an error for arrays having not enough items to match positional list of schema
ArrayNotEnoughItems() string
// ArrayNoAdditionalItems returns a format-string to format an ArrayNoAdditionalItemsError
ArrayNoAdditionalItems() string
// ArrayMinItems returns a format-string to format an ArrayMinItemsError
ArrayMinItems() string
// ArrayMaxItems returns a format-string to format an ArrayMaxItemsError
ArrayMaxItems() string
// Unique returns a format-string to format an ItemsMustBeUniqueError
Unique() string
// ArrayContains returns a format-string to format an ArrayContainsError
ArrayContains() string
// ArrayMinProperties returns a format-string to format an ArrayMinPropertiesError
ArrayMinProperties() string
// ArrayMaxProperties returns a format-string to format an ArrayMaxPropertiesError
ArrayMaxProperties() string
// AdditionalPropertyNotAllowed returns a format-string to format an AdditionalPropertyNotAllowedError
AdditionalPropertyNotAllowed() string
// InvalidPropertyPattern returns a format-string to format an InvalidPropertyPatternError
InvalidPropertyPattern() string
// InvalidPropertyName returns a format-string to format an InvalidPropertyNameError
InvalidPropertyName() string
// StringGTE returns a format-string to format an StringLengthGTEError
StringGTE() string
// StringLTE returns a format-string to format an StringLengthLTEError
StringLTE() string
// DoesNotMatchPattern returns a format-string to format an DoesNotMatchPatternError
DoesNotMatchPattern() string
// DoesNotMatchFormat returns a format-string to format an DoesNotMatchFormatError
DoesNotMatchFormat() string
// MultipleOf returns a format-string to format an MultipleOfError
MultipleOf() string
// NumberGTE returns a format-string to format an NumberGTEError
NumberGTE() string
// NumberGT returns a format-string to format an NumberGTError
NumberGT() string
// NumberLTE returns a format-string to format an NumberLTEError
NumberLTE() string
// NumberLT returns a format-string to format an NumberLTError
NumberLT() string
// Schema validations
// RegexPattern returns a format-string to format a regex-pattern error
RegexPattern() string
// GreaterThanZero returns a format-string to format an error where a number must be greater than zero
GreaterThanZero() string
// MustBeOfA returns a format-string to format an error where a value is of the wrong type
MustBeOfA() string
// MustBeOfAn returns a format-string to format an error where a value is of the wrong type
MustBeOfAn() string
// CannotBeUsedWithout returns a format-string to format a "cannot be used without" error
CannotBeUsedWithout() string
// CannotBeGT returns a format-string to format an error where a value are greater than allowed
CannotBeGT() string
// MustBeOfType returns a format-string to format an error where a value does not match the required type
MustBeOfType() string
// MustBeValidRegex returns a format-string to format an error where a regex is invalid
MustBeValidRegex() string
// MustBeValidFormat returns a format-string to format an error where a value does not match the expected format
MustBeValidFormat() string
// MustBeGTEZero returns a format-string to format an error where a value must be greater or equal than 0
MustBeGTEZero() string
// KeyCannotBeGreaterThan returns a format-string to format an error where a key is greater than the maximum allowed
KeyCannotBeGreaterThan() string
// KeyItemsMustBeOfType returns a format-string to format an error where a key is of the wrong type
KeyItemsMustBeOfType() string
// KeyItemsMustBeUnique returns a format-string to format an error where keys are not unique
KeyItemsMustBeUnique() string
// ReferenceMustBeCanonical returns a format-string to format a "reference must be canonical" error
ReferenceMustBeCanonical() string
// NotAValidType returns a format-string to format an invalid type error
NotAValidType() string
// Duplicated returns a format-string to format an error where types are duplicated
Duplicated() string
// HTTPBadStatus returns a format-string for errors when loading a schema using HTTP
HTTPBadStatus() string
// ParseError returns a format-string for JSON parsing errors
ParseError() string
// ConditionThen returns a format-string for ConditionThenError errors
ConditionThen() string
// ConditionElse returns a format-string for ConditionElseError errors
ConditionElse() string
// ErrorFormat returns a format string for errors
ErrorFormat() string
}
// DefaultLocale is the default locale for this package
DefaultLocale struct{}
)
// False returns a format-string for "false" schema validation errors
func (l DefaultLocale) False() string {
return "False always fails validation"
}
// Required returns a format-string for "required" schema validation errors
func (l DefaultLocale) Required() string {
return `{{.property}} is required`
}
// InvalidType returns a format-string for "invalid type" schema validation errors
func (l DefaultLocale) InvalidType() string {
return `Invalid type. Expected: {{.expected}}, given: {{.given}}`
}
// NumberAnyOf returns a format-string for "anyOf" schema validation errors
func (l DefaultLocale) NumberAnyOf() string {
return `Must validate at least one schema (anyOf)`
}
// NumberOneOf returns a format-string for "oneOf" schema validation errors
func (l DefaultLocale) NumberOneOf() string {
return `Must validate one and only one schema (oneOf)`
}
// NumberAllOf returns a format-string for "allOf" schema validation errors
func (l DefaultLocale) NumberAllOf() string {
return `Must validate all the schemas (allOf)`
}
// NumberNot returns a format-string to format a NumberNotError
func (l DefaultLocale) NumberNot() string {
return `Must not validate the schema (not)`
}
// MissingDependency returns a format-string for "missing dependency" schema validation errors
func (l DefaultLocale) MissingDependency() string {
return `Has a dependency on {{.dependency}}`
}
// Internal returns a format-string for internal errors
func (l DefaultLocale) Internal() string {
return `Internal Error {{.error}}`
}
// Const returns a format-string to format a ConstError
func (l DefaultLocale) Const() string {
return `{{.field}} does not match: {{.allowed}}`
}
// Enum returns a format-string to format an EnumError
func (l DefaultLocale) Enum() string {
return `{{.field}} must be one of the following: {{.allowed}}`
}
// ArrayNoAdditionalItems returns a format-string to format an ArrayNoAdditionalItemsError
func (l DefaultLocale) ArrayNoAdditionalItems() string {
return `No additional items allowed on array`
}
// ArrayNotEnoughItems returns a format-string to format an error for arrays having not enough items to match positional list of schema
func (l DefaultLocale) ArrayNotEnoughItems() string {
return `Not enough items on array to match positional list of schema`
}
// ArrayMinItems returns a format-string to format an ArrayMinItemsError
func (l DefaultLocale) ArrayMinItems() string {
return `Array must have at least {{.min}} items`
}
// ArrayMaxItems returns a format-string to format an ArrayMaxItemsError
func (l DefaultLocale) ArrayMaxItems() string {
return `Array must have at most {{.max}} items`
}
// Unique returns a format-string to format an ItemsMustBeUniqueError
func (l DefaultLocale) Unique() string {
return `{{.type}} items[{{.i}},{{.j}}] must be unique`
}
// ArrayContains returns a format-string to format an ArrayContainsError
func (l DefaultLocale) ArrayContains() string {
return `At least one of the items must match`
}
// ArrayMinProperties returns a format-string to format an ArrayMinPropertiesError
func (l DefaultLocale) ArrayMinProperties() string {
return `Must have at least {{.min}} properties`
}
// ArrayMaxProperties returns a format-string to format an ArrayMaxPropertiesError
func (l DefaultLocale) ArrayMaxProperties() string {
return `Must have at most {{.max}} properties`
}
// AdditionalPropertyNotAllowed returns a format-string to format an AdditionalPropertyNotAllowedError
func (l DefaultLocale) AdditionalPropertyNotAllowed() string {
return `Additional property {{.property}} is not allowed`
}
// InvalidPropertyPattern returns a format-string to format an InvalidPropertyPatternError
func (l DefaultLocale) InvalidPropertyPattern() string {
return `Property "{{.property}}" does not match pattern {{.pattern}}`
}
// InvalidPropertyName returns a format-string to format an InvalidPropertyNameError
func (l DefaultLocale) InvalidPropertyName() string {
return `Property name of "{{.property}}" does not match`
}
// StringGTE returns a format-string to format an StringLengthGTEError
func (l DefaultLocale) StringGTE() string {
return `String length must be greater than or equal to {{.min}}`
}
// StringLTE returns a format-string to format an StringLengthLTEError
func (l DefaultLocale) StringLTE() string {
return `String length must be less than or equal to {{.max}}`
}
// DoesNotMatchPattern returns a format-string to format an DoesNotMatchPatternError
func (l DefaultLocale) DoesNotMatchPattern() string {
return `Does not match pattern '{{.pattern}}'`
}
// DoesNotMatchFormat returns a format-string to format an DoesNotMatchFormatError
func (l DefaultLocale) DoesNotMatchFormat() string {
return `Does not match format '{{.format}}'`
}
// MultipleOf returns a format-string to format an MultipleOfError
func (l DefaultLocale) MultipleOf() string {
return `Must be a multiple of {{.multiple}}`
}
// NumberGTE returns the format string to format a NumberGTEError
func (l DefaultLocale) NumberGTE() string {
return `Must be greater than or equal to {{.min}}`
}
// NumberGT returns the format string to format a NumberGTError
func (l DefaultLocale) NumberGT() string {
return `Must be greater than {{.min}}`
}
// NumberLTE returns the format string to format a NumberLTEError
func (l DefaultLocale) NumberLTE() string {
return `Must be less than or equal to {{.max}}`
}
// NumberLT returns the format string to format a NumberLTError
func (l DefaultLocale) NumberLT() string {
return `Must be less than {{.max}}`
}
// Schema validators
// RegexPattern returns a format-string to format a regex-pattern error
func (l DefaultLocale) RegexPattern() string {
return `Invalid regex pattern '{{.pattern}}'`
}
// GreaterThanZero returns a format-string to format an error where a number must be greater than zero
func (l DefaultLocale) GreaterThanZero() string {
return `{{.number}} must be strictly greater than 0`
}
// MustBeOfA returns a format-string to format an error where a value is of the wrong type
func (l DefaultLocale) MustBeOfA() string {
return `{{.x}} must be of a {{.y}}`
}
// MustBeOfAn returns a format-string to format an error where a value is of the wrong type
func (l DefaultLocale) MustBeOfAn() string {
return `{{.x}} must be of an {{.y}}`
}
// CannotBeUsedWithout returns a format-string to format a "cannot be used without" error
func (l DefaultLocale) CannotBeUsedWithout() string {
return `{{.x}} cannot be used without {{.y}}`
}
// CannotBeGT returns a format-string to format an error where a value are greater than allowed
func (l DefaultLocale) CannotBeGT() string {
return `{{.x}} cannot be greater than {{.y}}`
}
// MustBeOfType returns a format-string to format an error where a value does not match the required type
func (l DefaultLocale) MustBeOfType() string {
return `{{.key}} must be of type {{.type}}`
}
// MustBeValidRegex returns a format-string to format an error where a regex is invalid
func (l DefaultLocale) MustBeValidRegex() string {
return `{{.key}} must be a valid regex`
}
// MustBeValidFormat returns a format-string to format an error where a value does not match the expected format
func (l DefaultLocale) MustBeValidFormat() string {
return `{{.key}} must be a valid format {{.given}}`
}
// MustBeGTEZero returns a format-string to format an error where a value must be greater or equal than 0
func (l DefaultLocale) MustBeGTEZero() string {
return `{{.key}} must be greater than or equal to 0`
}
// KeyCannotBeGreaterThan returns a format-string to format an error where a value is greater than the maximum allowed
func (l DefaultLocale) KeyCannotBeGreaterThan() string {
return `{{.key}} cannot be greater than {{.y}}`
}
// KeyItemsMustBeOfType returns a format-string to format an error where a key is of the wrong type
func (l DefaultLocale) KeyItemsMustBeOfType() string {
return `{{.key}} items must be {{.type}}`
}
// KeyItemsMustBeUnique returns a format-string to format an error where keys are not unique
func (l DefaultLocale) KeyItemsMustBeUnique() string {
return `{{.key}} items must be unique`
}
// ReferenceMustBeCanonical returns a format-string to format a "reference must be canonical" error
func (l DefaultLocale) ReferenceMustBeCanonical() string {
return `Reference {{.reference}} must be canonical`
}
// NotAValidType returns a format-string to format an invalid type error
func (l DefaultLocale) NotAValidType() string {
return `has a primitive type that is NOT VALID -- given: {{.given}} Expected valid values are:{{.expected}}`
}
// Duplicated returns a format-string to format an error where types are duplicated
func (l DefaultLocale) Duplicated() string {
return `{{.type}} type is duplicated`
}
// HTTPBadStatus returns a format-string for errors when loading a schema using HTTP
func (l DefaultLocale) HTTPBadStatus() string {
return `Could not read schema from HTTP, response status is {{.status}}`
}
// ErrorFormat returns a format string for errors
// Replacement options: field, description, context, value
func (l DefaultLocale) ErrorFormat() string {
return `{{.field}}: {{.description}}`
}
// ParseError returns a format-string for JSON parsing errors
func (l DefaultLocale) ParseError() string {
return `Expected: {{.expected}}, given: Invalid JSON`
}
// ConditionThen returns a format-string for ConditionThenError errors
// If/Else
func (l DefaultLocale) ConditionThen() string {
return `Must validate "then" as "if" was valid`
}
// ConditionElse returns a format-string for ConditionElseError errors
func (l DefaultLocale) ConditionElse() string {
return `Must validate "else" as "if" was not valid`
}
// constants
const (
StringNumber = "Number"
StringArrayOfStrings = "Array Of Strings"
StringArrayOfSchemas = "Array Of Schemas"
StringSchema = "Valid Schema"
StringSchemaOrArrayOfStrings = "Schema Or Array Of Strings"
StringProperties = "Properties"
StringDependency = "Dependency"
StringProperty = "Property"
StringUndefined = "Undefined"
StringContextRoot = "(Root)"
StringRootSchemaProperty = "(Root)"
)
@@ -0,0 +1,220 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Result and ResultError implementations.
//
// created 01-01-2015
package gojsonschema
import (
"fmt"
"strings"
)
type (
// ErrorDetails is a map of details specific to each error.
// While the values will vary, every error will contain a "field" value
ErrorDetails map[string]any
// ResultError is the interface that library errors must implement
ResultError interface {
// Field returns the field name without the root context
// i.e. firstName or person.firstName instead of (root).firstName or (root).person.firstName
Field() string
// SetType sets the error-type
SetType(string)
// Type returns the error-type
Type() string
// SetContext sets the JSON-context for the error
SetContext(*JSONContext)
// Context returns the JSON-context of the error
Context() *JSONContext
// SetDescription sets a description for the error
SetDescription(string)
// Description returns the description of the error
Description() string
// SetDescriptionFormat sets the format for the description in the default text/template format
SetDescriptionFormat(string)
// DescriptionFormat returns the format for the description in the default text/template format
DescriptionFormat() string
// SetValue sets the value related to the error
SetValue(any)
// Value returns the value related to the error
Value() any
// SetDetails sets the details specific to the error
SetDetails(ErrorDetails)
// Details returns details about the error
Details() ErrorDetails
// String returns a string representation of the error
String() string
}
// ResultErrorFields holds the fields for each ResultError implementation.
// ResultErrorFields implements the ResultError interface, so custom errors
// can be defined by just embedding this type
ResultErrorFields struct {
errorType string // A string with the type of error (i.e. invalid_type)
context *JSONContext // Tree like notation of the part that failed the validation. ex (root).a.b ...
description string // A human readable error message
descriptionFormat string // A format for human readable error message
value any // Value given by the JSON file that is the source of the error
details ErrorDetails
}
// Result holds the result of a validation
Result struct {
errors []ResultError
// Scores how well the validation matched. Useful in generating
// better error messages for anyOf and oneOf.
score int
}
)
// Field returns the field name without the root context
// i.e. firstName or person.firstName instead of (root).firstName or (root).person.firstName
func (v *ResultErrorFields) Field() string {
return strings.TrimPrefix(v.context.String(), StringRootSchemaProperty+".")
}
// SetType sets the error-type
func (v *ResultErrorFields) SetType(errorType string) {
v.errorType = errorType
}
// Type returns the error-type
func (v *ResultErrorFields) Type() string {
return v.errorType
}
// SetContext sets the JSON-context for the error
func (v *ResultErrorFields) SetContext(context *JSONContext) {
v.context = context
}
// Context returns the JSON-context of the error
func (v *ResultErrorFields) Context() *JSONContext {
return v.context
}
// SetDescription sets a description for the error
func (v *ResultErrorFields) SetDescription(description string) {
v.description = description
}
// Description returns the description of the error
func (v *ResultErrorFields) Description() string {
return v.description
}
// SetDescriptionFormat sets the format for the description in the default text/template format
func (v *ResultErrorFields) SetDescriptionFormat(descriptionFormat string) {
v.descriptionFormat = descriptionFormat
}
// DescriptionFormat returns the format for the description in the default text/template format
func (v *ResultErrorFields) DescriptionFormat() string {
return v.descriptionFormat
}
// SetValue sets the value related to the error
func (v *ResultErrorFields) SetValue(value any) {
v.value = value
}
// Value returns the value related to the error
func (v *ResultErrorFields) Value() any {
return v.value
}
// SetDetails sets the details specific to the error
func (v *ResultErrorFields) SetDetails(details ErrorDetails) {
v.details = details
}
// Details returns details about the error
func (v *ResultErrorFields) Details() ErrorDetails {
return v.details
}
// String returns a string representation of the error
func (v ResultErrorFields) String() string {
// as a fallback, the value is displayed go style
valueString := fmt.Sprintf("%v", v.value)
// marshal the go value value to json
if v.value == nil {
valueString = TypeNull
} else {
if vs, err := marshalToJSONString(v.value); err == nil {
if vs == nil {
valueString = TypeNull
} else {
valueString = *vs
}
}
}
return formatErrorDescription(Locale.ErrorFormat(), ErrorDetails{
"context": v.context.String(),
"description": v.description,
"value": valueString,
"field": v.Field(),
})
}
// Valid indicates if no errors were found
func (v *Result) Valid() bool {
return len(v.errors) == 0
}
// Errors returns the errors that were found
func (v *Result) Errors() []ResultError {
return v.errors
}
// AddError appends a fully filled error to the error set
// SetDescription() will be called with the result of the parsed err.DescriptionFormat()
func (v *Result) AddError(err ResultError, details ErrorDetails) {
if _, exists := details["context"]; !exists && err.Context() != nil {
details["context"] = err.Context().String()
}
err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details))
v.errors = append(v.errors, err)
}
func (v *Result) addInternalError(err ResultError, context *JSONContext, value any, details ErrorDetails) {
newError(err, context, value, Locale, details)
v.errors = append(v.errors, err)
v.score -= 2 // results in a net -1 when added to the +1 we get at the end of the validation function
}
// Used to copy errors from a sub-schema to the main one
func (v *Result) mergeErrors(otherResult *Result) {
v.errors = append(v.errors, otherResult.Errors()...)
v.score += otherResult.score
}
func (v *Result) incrementScore() {
v.score++
}
@@ -0,0 +1,957 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Defines Schema, the main entry to every SubSchema.
// Contains the parsing logic and error checking.
//
// created 26-02-2013
package gojsonschema
import (
"encoding/json"
"errors"
"math/big"
"regexp"
"text/template"
"github.com/xeipuuv/gojsonreference"
)
var (
// Locale is the default locale to use
// Library users can overwrite with their own implementation
Locale locale = DefaultLocale{}
// ErrorTemplateFuncs allows you to define custom template funcs for use in localization.
ErrorTemplateFuncs template.FuncMap
)
// NewSchema instances a schema using the given JSONLoader
func NewSchema(l JSONLoader) (*Schema, error) {
return NewSchemaLoader().Compile(l)
}
// Schema holds a schema
type Schema struct {
DocumentReference gojsonreference.JsonReference
RootSchema *SubSchema
Pool *schemaPool
ReferencePool *schemaReferencePool
}
func (d *Schema) parse(document any, draft Draft) error {
d.RootSchema = &SubSchema{Property: StringRootSchemaProperty, Draft: &draft}
return d.parseSchema(document, d.RootSchema)
}
// SetRootSchemaName sets the root-schema name
func (d *Schema) SetRootSchemaName(name string) {
d.RootSchema.Property = name
}
// Parses a SubSchema
//
// Pretty long function ( sorry :) )... but pretty straight forward, repetitive and boring
// Not much magic involved here, most of the job is to validate the key names and their values,
// then the values are copied into SubSchema struct
func (d *Schema) parseSchema(documentNode any, currentSchema *SubSchema) error {
if currentSchema.Draft == nil {
if currentSchema.Parent == nil {
return errors.New("Draft not set")
}
currentSchema.Draft = currentSchema.Parent.Draft
}
// As of draft 6 "true" is equivalent to an empty schema "{}" and false equals "{"not":{}}"
if *currentSchema.Draft >= Draft6 {
if b, isBool := documentNode.(bool); isBool {
currentSchema.pass = &b
return nil
}
}
m, isMap := documentNode.(map[string]any)
if !isMap {
return errors.New(formatErrorDescription(
Locale.ParseError(),
ErrorDetails{
"expected": StringSchema,
},
))
}
if currentSchema.Parent == nil {
currentSchema.Ref = &d.DocumentReference
currentSchema.ID = &d.DocumentReference
}
if currentSchema.ID == nil && currentSchema.Parent != nil {
currentSchema.ID = currentSchema.Parent.ID
}
// In draft 6 the id keyword was renamed to $id
// Hybrid mode uses the old id by default
var keyID string
switch *currentSchema.Draft {
case Draft4:
keyID = KeyID
case Hybrid:
keyID = KeyIDNew
if _, found := m[KeyID]; found {
keyID = KeyID
}
default:
keyID = KeyIDNew
}
if id, err := getString(m, keyID); err != nil {
return err
} else if id != nil {
jsonReference, err := gojsonreference.NewJsonReference(*id)
if err != nil {
return err
}
if currentSchema == d.RootSchema {
currentSchema.ID = &jsonReference
} else {
ref, err := currentSchema.Parent.ID.Inherits(jsonReference)
if err != nil {
return err
}
currentSchema.ID = ref
}
}
// definitions
if v, ok := m[KeyDefinitions]; ok {
switch mt := v.(type) {
case map[string]any:
for _, dv := range mt {
switch dv.(type) {
case bool, map[string]any:
newSchema := &SubSchema{Property: KeyDefinitions, Parent: currentSchema}
err := d.parseSchema(dv, newSchema)
if err != nil {
return err
}
default:
return invalidType(StringArrayOfSchemas, KeyDefinitions)
}
}
default:
return invalidType(StringArrayOfSchemas, KeyDefinitions)
}
}
// title
var err error
currentSchema.title, err = getString(m, KeyTitle)
if err != nil {
return err
}
// description
currentSchema.description, err = getString(m, KeyDescription)
if err != nil {
return err
}
// $ref
if ref, err := getString(m, KeyRef); err != nil {
return err
} else if ref != nil {
jsonReference, err := gojsonreference.NewJsonReference(*ref)
if err != nil {
return err
}
currentSchema.Ref = &jsonReference
if sch, ok := d.ReferencePool.Get(currentSchema.Ref.String()); ok {
currentSchema.RefSchema = sch
} else {
return d.parseReference(documentNode, currentSchema)
}
}
// type
if typ, found := m[KeyType]; found {
switch t := typ.(type) {
case string:
err := currentSchema.Types.Add(t)
if err != nil {
return err
}
case []any:
for _, typeInArray := range t {
s, isString := typeInArray.(string)
if !isString {
return invalidType(KeyType, TypeString+"/"+StringArrayOfStrings)
}
if err := currentSchema.Types.Add(s); err != nil {
return err
}
}
default:
return invalidType(KeyType, TypeString+"/"+StringArrayOfStrings)
}
}
// properties
if properties, found := m[KeyProperties]; found {
err := d.parseProperties(properties, currentSchema)
if err != nil {
return err
}
}
// additionalProperties
if additionalProperties, found := m[KeyAdditionalProperties]; found {
switch v := additionalProperties.(type) {
case bool:
currentSchema.additionalProperties = v
case map[string]any:
newSchema := &SubSchema{Property: KeyAdditionalProperties, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema.additionalProperties = newSchema
err := d.parseSchema(v, newSchema)
if err != nil {
return errors.New(err.Error())
}
default:
return invalidType(TypeBoolean+"/"+StringSchema, KeyAdditionalProperties)
}
}
// patternProperties
if patternProperties, err := getMap(m, KeyPatternProperties); err != nil {
return err
} else if patternProperties != nil {
if len(patternProperties) > 0 {
currentSchema.patternProperties = make(map[string]*SubSchema)
for k, v := range patternProperties {
_, err := regexp.MatchString(k, "")
if err != nil {
return errors.New(formatErrorDescription(
Locale.RegexPattern(),
ErrorDetails{"pattern": k},
))
}
newSchema := &SubSchema{Property: k, Parent: currentSchema, Ref: currentSchema.Ref}
err = d.parseSchema(v, newSchema)
if err != nil {
return errors.New(err.Error())
}
currentSchema.patternProperties[k] = newSchema
}
}
}
// propertyNames
if propertyNames, found := m[KeyPropertyNames]; found && *currentSchema.Draft >= Draft6 {
switch propertyNames.(type) {
case bool, map[string]any:
newSchema := &SubSchema{Property: KeyPropertyNames, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema.propertyNames = newSchema
err := d.parseSchema(propertyNames, newSchema)
if err != nil {
return err
}
default:
return errors.New(formatErrorDescription(
Locale.InvalidType(),
ErrorDetails{
"expected": StringSchema,
"given": KeyPatternProperties,
},
))
}
}
// dependencies
if dependencies, found := m[KeyDependencies]; found {
err := d.parseDependencies(dependencies, currentSchema)
if err != nil {
return err
}
}
// items
if items, found := m[KeyItems]; found {
switch i := items.(type) {
case []any:
for _, itemElement := range i {
switch itemElement.(type) {
case map[string]any, bool:
newSchema := &SubSchema{Parent: currentSchema, Property: KeyItems}
newSchema.Ref = currentSchema.Ref
currentSchema.ItemsChildren = append(currentSchema.ItemsChildren, newSchema)
err := d.parseSchema(itemElement, newSchema)
if err != nil {
return err
}
default:
return invalidType(StringSchema+"/"+StringArrayOfSchemas, KeyItems)
}
currentSchema.ItemsChildrenIsSingleSchema = false
}
case map[string]any, bool:
newSchema := &SubSchema{Parent: currentSchema, Property: KeyItems}
newSchema.Ref = currentSchema.Ref
currentSchema.ItemsChildren = append(currentSchema.ItemsChildren, newSchema)
err := d.parseSchema(items, newSchema)
if err != nil {
return err
}
currentSchema.ItemsChildrenIsSingleSchema = true
default:
return invalidType(StringSchema+"/"+StringArrayOfSchemas, KeyItems)
}
}
// additionalItems
if additionalItems, found := m[KeyAdditionalItems]; found {
switch i := additionalItems.(type) {
case bool:
currentSchema.additionalItems = i
case map[string]any:
newSchema := &SubSchema{Property: KeyAdditionalItems, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema.additionalItems = newSchema
err := d.parseSchema(additionalItems, newSchema)
if err != nil {
return errors.New(err.Error())
}
default:
return invalidType(TypeBoolean+"/"+StringSchema, KeyAdditionalItems)
}
}
// validation : number / integer
if multipleOf, found := m[KeyMultipleOf]; found {
multipleOfValue := mustBeNumber(multipleOf)
if multipleOfValue == nil {
return invalidType(StringNumber, KeyMultipleOf)
}
if multipleOfValue.Cmp(big.NewRat(0, 1)) <= 0 {
return errors.New(formatErrorDescription(
Locale.GreaterThanZero(),
ErrorDetails{"number": KeyMultipleOf},
))
}
currentSchema.multipleOf = multipleOfValue
}
if minimum, found := m[KeyMinimum]; found {
minimumValue := mustBeNumber(minimum)
if minimumValue == nil {
return errors.New(formatErrorDescription(
Locale.MustBeOfA(),
ErrorDetails{"x": KeyMinimum, "y": StringNumber},
))
}
currentSchema.minimum = minimumValue
}
if exclusiveMinimum, found := m[KeyExclusiveMinimum]; found {
switch *currentSchema.Draft {
case Draft4:
boolExclusiveMinimum, isBool := exclusiveMinimum.(bool)
if !isBool {
return invalidType(TypeBoolean, KeyExclusiveMinimum)
}
if currentSchema.minimum == nil {
return errors.New(formatErrorDescription(
Locale.CannotBeUsedWithout(),
ErrorDetails{"x": KeyExclusiveMinimum, "y": KeyMinimum},
))
}
if boolExclusiveMinimum {
currentSchema.exclusiveMinimum = currentSchema.minimum
currentSchema.minimum = nil
}
case Hybrid:
switch b := exclusiveMinimum.(type) {
case bool:
if currentSchema.minimum == nil {
return errors.New(formatErrorDescription(
Locale.CannotBeUsedWithout(),
ErrorDetails{"x": KeyExclusiveMinimum, "y": KeyMinimum},
))
}
if b {
currentSchema.exclusiveMinimum = currentSchema.minimum
currentSchema.minimum = nil
}
case json.Number:
currentSchema.exclusiveMinimum = mustBeNumber(m[KeyExclusiveMinimum])
default:
return invalidType(TypeBoolean+"/"+TypeNumber, KeyExclusiveMinimum)
}
default:
if isJSONNumber(exclusiveMinimum) {
currentSchema.exclusiveMinimum = mustBeNumber(exclusiveMinimum)
} else {
return invalidType(TypeNumber, KeyExclusiveMinimum)
}
}
}
if maximum, found := m[KeyMaximum]; found {
maximumValue := mustBeNumber(maximum)
if maximumValue == nil {
return errors.New(formatErrorDescription(
Locale.MustBeOfA(),
ErrorDetails{"x": KeyMaximum, "y": StringNumber},
))
}
currentSchema.maximum = maximumValue
}
if exclusiveMaximum, found := m[KeyExclusiveMaximum]; found {
switch *currentSchema.Draft {
case Draft4:
boolExclusiveMaximum, isBool := exclusiveMaximum.(bool)
if !isBool {
return invalidType(TypeBoolean, KeyExclusiveMaximum)
}
if currentSchema.maximum == nil {
return errors.New(formatErrorDescription(
Locale.CannotBeUsedWithout(),
ErrorDetails{"x": KeyExclusiveMaximum, "y": KeyMaximum},
))
}
if boolExclusiveMaximum {
currentSchema.exclusiveMaximum = currentSchema.maximum
currentSchema.maximum = nil
}
case Hybrid:
switch b := exclusiveMaximum.(type) {
case bool:
if currentSchema.maximum == nil {
return errors.New(formatErrorDescription(
Locale.CannotBeUsedWithout(),
ErrorDetails{"x": KeyExclusiveMaximum, "y": KeyMaximum},
))
}
if b {
currentSchema.exclusiveMaximum = currentSchema.maximum
currentSchema.maximum = nil
}
case json.Number:
currentSchema.exclusiveMaximum = mustBeNumber(exclusiveMaximum)
default:
return invalidType(TypeBoolean+"/"+TypeNumber, KeyExclusiveMaximum)
}
default:
if isJSONNumber(exclusiveMaximum) {
currentSchema.exclusiveMaximum = mustBeNumber(exclusiveMaximum)
} else {
return invalidType(TypeNumber, KeyExclusiveMaximum)
}
}
}
// validation : string
if minLength, found := m[KeyMinLength]; found {
minLengthIntegerValue := mustBeInteger(minLength)
if minLengthIntegerValue == nil {
return errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": KeyMinLength, "y": TypeInteger},
))
}
if *minLengthIntegerValue < 0 {
return errors.New(formatErrorDescription(
Locale.MustBeGTEZero(),
ErrorDetails{"key": KeyMinLength},
))
}
currentSchema.minLength = minLengthIntegerValue
}
if maxLength, found := m[KeyMaxLength]; found {
maxLengthIntegerValue := mustBeInteger(maxLength)
if maxLengthIntegerValue == nil {
return errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": KeyMaxLength, "y": TypeInteger},
))
}
if *maxLengthIntegerValue < 0 {
return errors.New(formatErrorDescription(
Locale.MustBeGTEZero(),
ErrorDetails{"key": KeyMaxLength},
))
}
currentSchema.maxLength = maxLengthIntegerValue
}
if currentSchema.minLength != nil && currentSchema.maxLength != nil {
if *currentSchema.minLength > *currentSchema.maxLength {
return errors.New(formatErrorDescription(
Locale.CannotBeGT(),
ErrorDetails{"x": KeyMinLength, "y": KeyMaxLength},
))
}
}
// NOTE: Regex compilation step removed as we don't use "pattern" attribute for
// type checking, and this would cause schemas to fail if they included patterns
// that were valid ECMA regex dialect but not known to Go (i.e. the regexp.Compile
// function), such as patterns with negative lookahead
if _, err := getString(m, KeyPattern); err != nil {
return err
}
if format, err := getString(m, KeyFormat); err != nil {
return err
} else if format != nil {
currentSchema.format = *format
}
// validation : object
if minProperties, found := m[KeyMinProperties]; found {
minPropertiesIntegerValue := mustBeInteger(minProperties)
if minPropertiesIntegerValue == nil {
return errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": KeyMinProperties, "y": TypeInteger},
))
}
if *minPropertiesIntegerValue < 0 {
return errors.New(formatErrorDescription(
Locale.MustBeGTEZero(),
ErrorDetails{"key": KeyMinProperties},
))
}
currentSchema.minProperties = minPropertiesIntegerValue
}
if maxProperties, found := m[KeyMaxProperties]; found {
maxPropertiesIntegerValue := mustBeInteger(maxProperties)
if maxPropertiesIntegerValue == nil {
return errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": KeyMaxProperties, "y": TypeInteger},
))
}
if *maxPropertiesIntegerValue < 0 {
return errors.New(formatErrorDescription(
Locale.MustBeGTEZero(),
ErrorDetails{"key": KeyMaxProperties},
))
}
currentSchema.maxProperties = maxPropertiesIntegerValue
}
if currentSchema.minProperties != nil && currentSchema.maxProperties != nil {
if *currentSchema.minProperties > *currentSchema.maxProperties {
return errors.New(formatErrorDescription(
Locale.KeyCannotBeGreaterThan(),
ErrorDetails{"key": KeyMinProperties, "y": KeyMaxProperties},
))
}
}
required, err := getSlice(m, KeyRequired)
if err != nil {
return err
}
for _, requiredValue := range required {
s, isString := requiredValue.(string)
if !isString {
return invalidType(TypeString, KeyRequired)
} else if isStringInSlice(currentSchema.required, s) {
return errors.New(formatErrorDescription(
Locale.KeyItemsMustBeUnique(),
ErrorDetails{"key": KeyRequired},
))
}
currentSchema.required = append(currentSchema.required, s)
}
// validation : array
if minItems, found := m[KeyMinItems]; found {
minItemsIntegerValue := mustBeInteger(minItems)
if minItemsIntegerValue == nil {
return errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": KeyMinItems, "y": TypeInteger},
))
}
if *minItemsIntegerValue < 0 {
return errors.New(formatErrorDescription(
Locale.MustBeGTEZero(),
ErrorDetails{"key": KeyMinItems},
))
}
currentSchema.minItems = minItemsIntegerValue
}
if maxItems, found := m[KeyMaxItems]; found {
maxItemsIntegerValue := mustBeInteger(maxItems)
if maxItemsIntegerValue == nil {
return errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": KeyMaxItems, "y": TypeInteger},
))
}
if *maxItemsIntegerValue < 0 {
return errors.New(formatErrorDescription(
Locale.MustBeGTEZero(),
ErrorDetails{"key": KeyMaxItems},
))
}
currentSchema.maxItems = maxItemsIntegerValue
}
if uniqueItems, found := m[KeyUniqueItems]; found {
bUniqueItems, isBool := uniqueItems.(bool)
if !isBool {
return errors.New(formatErrorDescription(
Locale.MustBeOfA(),
ErrorDetails{"x": KeyUniqueItems, "y": TypeBoolean},
))
}
currentSchema.uniqueItems = bUniqueItems
}
if contains, found := m[KeyContains]; found && *currentSchema.Draft >= Draft6 {
newSchema := &SubSchema{Property: KeyContains, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema.contains = newSchema
err := d.parseSchema(contains, newSchema)
if err != nil {
return err
}
}
// validation : all
if vConst, found := m[KeyConst]; found && *currentSchema.Draft >= Draft6 {
is, err := marshalWithoutNumber(vConst)
if err != nil {
return err
}
currentSchema._const = is
}
enum, err := getSlice(m, KeyEnum)
if err != nil {
return err
}
for _, v := range enum {
is, err := marshalWithoutNumber(v)
if err != nil {
return err
}
if isStringInSlice(currentSchema.enum, *is) {
return errors.New(formatErrorDescription(
Locale.KeyItemsMustBeUnique(),
ErrorDetails{"key": KeyEnum},
))
}
currentSchema.enum = append(currentSchema.enum, *is)
}
// validation : SubSchema
oneOf, err := getSlice(m, KeyOneOf)
if err != nil {
return err
}
for _, v := range oneOf {
newSchema := &SubSchema{Property: KeyOneOf, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema.oneOf = append(currentSchema.oneOf, newSchema)
err := d.parseSchema(v, newSchema)
if err != nil {
return err
}
}
anyOf, err := getSlice(m, KeyAnyOf)
if err != nil {
return err
}
for _, v := range anyOf {
newSchema := &SubSchema{Property: KeyAnyOf, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema.AnyOf = append(currentSchema.AnyOf, newSchema)
err := d.parseSchema(v, newSchema)
if err != nil {
return err
}
}
allOf, err := getSlice(m, KeyAllOf)
if err != nil {
return err
}
for _, v := range allOf {
newSchema := &SubSchema{Property: KeyAllOf, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema.AllOf = append(currentSchema.AllOf, newSchema)
err := d.parseSchema(v, newSchema)
if err != nil {
return err
}
}
if vNot, found := m[KeyNot]; found {
switch vNot.(type) {
case bool, map[string]any:
newSchema := &SubSchema{Property: KeyNot, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema.not = newSchema
err := d.parseSchema(vNot, newSchema)
if err != nil {
return err
}
default:
return errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": KeyNot, "y": TypeObject},
))
}
}
if *currentSchema.Draft >= Draft7 {
if vIf, found := m[KeyIf]; found {
switch vIf.(type) {
case bool, map[string]any:
newSchema := &SubSchema{Property: KeyIf, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema._if = newSchema
err := d.parseSchema(vIf, newSchema)
if err != nil {
return err
}
default:
return errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": KeyIf, "y": TypeObject},
))
}
}
if then, found := m[KeyThen]; found {
switch then.(type) {
case bool, map[string]any:
newSchema := &SubSchema{Property: KeyThen, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema._then = newSchema
err := d.parseSchema(then, newSchema)
if err != nil {
return err
}
default:
return errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": KeyThen, "y": TypeObject},
))
}
}
if vElse, found := m[KeyElse]; found {
switch vElse.(type) {
case bool, map[string]any:
newSchema := &SubSchema{Property: KeyElse, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema._else = newSchema
err := d.parseSchema(vElse, newSchema)
if err != nil {
return err
}
default:
return errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": KeyElse, "y": TypeObject},
))
}
}
}
return nil
}
func (d *Schema) parseReference(_ any, currentSchema *SubSchema) error {
var (
refdDocumentNode any
dsp *schemaPoolDocument
err error
)
newSchema := &SubSchema{Property: KeyRef, Parent: currentSchema, Ref: currentSchema.Ref}
d.ReferencePool.Add(currentSchema.Ref.String(), newSchema)
dsp, err = d.Pool.GetDocument(*currentSchema.Ref)
if err != nil {
return err
}
newSchema.ID = currentSchema.Ref
refdDocumentNode = dsp.Document
newSchema.Draft = dsp.Draft
switch refdDocumentNode.(type) {
case bool, map[string]any:
// expected
default:
return errors.New(formatErrorDescription(
Locale.MustBeOfType(),
ErrorDetails{"key": StringSchema, "type": TypeObject},
))
}
err = d.parseSchema(refdDocumentNode, newSchema)
if err != nil {
return err
}
currentSchema.RefSchema = newSchema
return nil
}
func (d *Schema) parseProperties(documentNode any, currentSchema *SubSchema) error {
m, isMap := documentNode.(map[string]any)
if !isMap {
return errors.New(formatErrorDescription(
Locale.MustBeOfType(),
ErrorDetails{"key": StringProperties, "type": TypeObject},
))
}
for k := range m {
schemaProperty := k
newSchema := &SubSchema{Property: schemaProperty, Parent: currentSchema, Ref: currentSchema.Ref}
currentSchema.PropertiesChildren = append(currentSchema.PropertiesChildren, newSchema)
err := d.parseSchema(m[k], newSchema)
if err != nil {
return err
}
}
return nil
}
func (d *Schema) parseDependencies(documentNode any, currentSchema *SubSchema) error {
m, isMap := documentNode.(map[string]any)
if !isMap {
return errors.New(formatErrorDescription(
Locale.MustBeOfType(),
ErrorDetails{"key": KeyDependencies, "type": TypeObject},
))
}
currentSchema.dependencies = make(map[string]any)
for k := range m {
switch values := m[k].(type) {
case []any:
var valuesToRegister []string
for _, value := range values {
str, isString := value.(string)
if !isString {
return errors.New(formatErrorDescription(
Locale.MustBeOfType(),
ErrorDetails{
"key": StringDependency,
"type": StringSchemaOrArrayOfStrings,
},
))
}
valuesToRegister = append(valuesToRegister, str)
currentSchema.dependencies[k] = valuesToRegister
}
case bool, map[string]any:
depSchema := &SubSchema{Property: k, Parent: currentSchema, Ref: currentSchema.Ref}
err := d.parseSchema(m[k], depSchema)
if err != nil {
return err
}
currentSchema.dependencies[k] = depSchema
default:
return errors.New(formatErrorDescription(
Locale.MustBeOfType(),
ErrorDetails{
"key": StringDependency,
"type": StringSchemaOrArrayOfStrings,
},
))
}
}
return nil
}
func invalidType(expected, given string) error {
return errors.New(formatErrorDescription(
Locale.InvalidType(),
ErrorDetails{
"expected": expected,
"given": given,
},
))
}
func getString(m map[string]any, key string) (*string, error) {
v, found := m[key]
if !found {
// not found
return nil, nil
}
s, isString := v.(string)
if !isString {
// wrong type
return nil, invalidType(TypeString, key)
}
return &s, nil
}
func getMap(m map[string]any, key string) (map[string]any, error) {
v, found := m[key]
if !found {
// not found
return nil, nil
}
s, isMap := v.(map[string]any)
if !isMap {
// wrong type
return nil, invalidType(StringSchema, key)
}
return s, nil
}
func getSlice(m map[string]any, key string) ([]any, error) {
v, found := m[key]
if !found {
return nil, nil
}
s, isArray := v.([]any)
if !isArray {
return nil, errors.New(formatErrorDescription(
Locale.MustBeOfAn(),
ErrorDetails{"x": key, "y": TypeArray},
))
}
return s, nil
}
@@ -0,0 +1,206 @@
// Copyright 2018 johandorland ( https://github.com/johandorland )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gojsonschema
import (
"bytes"
"errors"
"github.com/xeipuuv/gojsonreference"
)
// SchemaLoader is used to load schemas
type SchemaLoader struct {
pool *schemaPool
AutoDetect bool
Validate bool
Draft Draft
}
// NewSchemaLoader creates a new NewSchemaLoader
func NewSchemaLoader() *SchemaLoader {
ps := &SchemaLoader{
pool: &schemaPool{
schemaPoolDocuments: make(map[string]*schemaPoolDocument),
},
AutoDetect: true,
Validate: false,
Draft: Hybrid,
}
ps.pool.autoDetect = &ps.AutoDetect
return ps
}
func (sl *SchemaLoader) validateMetaschema(documentNode any) error {
var (
schema string
err error
)
if sl.AutoDetect {
schema, _, err = parseSchemaURL(documentNode)
if err != nil {
return err
}
}
// If no explicit "$schema" is used, use the default metaschema associated with the draft used
if schema == "" {
if sl.Draft == Hybrid {
return nil
}
schema = drafts.GetSchemaURL(sl.Draft)
}
//Disable validation when loading the metaschema to prevent an infinite recursive loop
sl.Validate = false
metaSchema, err := sl.Compile(NewReferenceLoader(schema))
if err != nil {
return err
}
sl.Validate = true
result := metaSchema.validateDocument(documentNode)
if !result.Valid() {
var res bytes.Buffer
for _, err := range result.Errors() {
res.WriteString(err.String())
res.WriteString("\n")
}
return errors.New(res.String())
}
return nil
}
// AddSchemas adds an arbritrary amount of schemas to the schema cache. As this function does not require
// an explicit URL, every schema should contain an $id, so that it can be referenced by the main schema
func (sl *SchemaLoader) AddSchemas(loaders ...JSONLoader) error {
emptyRef, _ := gojsonreference.NewJsonReference("")
for _, loader := range loaders {
doc, err := loader.LoadJSON()
if err != nil {
return err
}
if sl.Validate {
if err := sl.validateMetaschema(doc); err != nil {
return err
}
}
// Directly use the Recursive function, so that it get only added to the schema Pool by $id
// and not by the ref of the document as it's empty
if err = sl.pool.parseReferences(doc, emptyRef, false); err != nil {
return err
}
}
return nil
}
// AddSchema adds a schema under the provided URL to the schema cache
func (sl *SchemaLoader) AddSchema(url string, loader JSONLoader) error {
ref, err := gojsonreference.NewJsonReference(url)
if err != nil {
return err
}
doc, err := loader.LoadJSON()
if err != nil {
return err
}
if sl.Validate {
if err := sl.validateMetaschema(doc); err != nil {
return err
}
}
return sl.pool.parseReferences(doc, ref, true)
}
// Compile loads and compiles a schema
func (sl *SchemaLoader) Compile(rootSchema JSONLoader) (*Schema, error) {
ref, err := rootSchema.JSONReference()
if err != nil {
return nil, err
}
d := Schema{}
d.Pool = sl.pool
d.Pool.jsonLoaderFactory = rootSchema.LoaderFactory()
d.DocumentReference = ref
d.ReferencePool = newSchemaReferencePool()
var doc any
if ref.String() != "" {
// Get document from schema pool
spd, err := d.Pool.GetDocument(d.DocumentReference)
if err != nil {
return nil, err
}
doc = spd.Document
} else {
// Load JSON directly
doc, err = rootSchema.LoadJSON()
if err != nil {
return nil, err
}
// References need only be parsed if loading JSON directly
// as pool.GetDocument already does this for us if loading by reference
err = sl.pool.parseReferences(doc, ref, true)
if err != nil {
return nil, err
}
}
if sl.Validate {
if err := sl.validateMetaschema(doc); err != nil {
return nil, err
}
}
draft := sl.Draft
if sl.AutoDetect {
_, detectedDraft, err := parseSchemaURL(doc)
if err != nil {
return nil, err
}
if detectedDraft != nil {
draft = *detectedDraft
}
}
err = d.parse(doc, draft)
if err != nil {
return nil, err
}
return &d, nil
}
@@ -0,0 +1,230 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Defines resources pooling.
// Eases referencing and avoids downloading the same resource twice.
//
// created 26-02-2013
package gojsonschema
import (
"errors"
"fmt"
"github.com/xeipuuv/gojsonreference"
)
type schemaPoolDocument struct {
Document any
Draft *Draft
}
type schemaPool struct {
schemaPoolDocuments map[string]*schemaPoolDocument
jsonLoaderFactory JSONLoaderFactory
autoDetect *bool
}
func (p *schemaPool) parseReferences(document any, ref gojsonreference.JsonReference, pooled bool) error {
var (
draft *Draft
err error
reference = ref.String()
)
// Only the root document should be added to the schema pool if pooled is true
if _, ok := p.schemaPoolDocuments[reference]; pooled && ok {
return fmt.Errorf("Reference already exists: \"%s\"", reference)
}
if *p.autoDetect {
_, draft, err = parseSchemaURL(document)
if err != nil {
return err
}
}
err = p.parseReferencesRecursive(document, ref, draft)
if pooled {
p.schemaPoolDocuments[reference] = &schemaPoolDocument{Document: document, Draft: draft}
}
return err
}
func (p *schemaPool) parseReferencesRecursive(document any, ref gojsonreference.JsonReference, draft *Draft) error {
// parseReferencesRecursive parses a JSON document and resolves all $id and $ref references.
// For $ref references it takes into account the $id scope it is in and replaces
// the reference by the absolute resolved reference
// When encountering errors it fails silently. Error handling is done when the schema
// is syntactically parsed and any error encountered here should also come up there.
switch m := document.(type) {
case []any:
for _, v := range m {
err := p.parseReferencesRecursive(v, ref, draft)
if err != nil {
return err
}
}
case map[string]any:
localRef := &ref
keyID := KeyIDNew
if _, ok := m[KeyID]; ok {
keyID = KeyID
}
if v, ok := m[keyID]; ok {
if value, isString := v.(string); isString {
jsonReference, err := gojsonreference.NewJsonReference(value)
if err == nil {
localRef, err = ref.Inherits(jsonReference)
if err == nil {
if _, ok := p.schemaPoolDocuments[localRef.String()]; ok {
return fmt.Errorf("Reference already exists: \"%s\"", localRef.String())
}
p.schemaPoolDocuments[localRef.String()] = &schemaPoolDocument{Document: document, Draft: draft}
}
}
}
}
if v, ok := m[KeyRef]; ok {
if s, isString := v.(string); isString {
jsonReference, err := gojsonreference.NewJsonReference(s)
if err == nil {
absoluteRef, err := localRef.Inherits(jsonReference)
if err == nil {
m[KeyRef] = absoluteRef.String()
}
}
}
}
for k, v := range m {
// const and enums should be interpreted literally, so ignore them
if k == KeyConst || k == KeyEnum {
continue
}
// Something like a property or a dependency is not a valid schema, as it might describe properties named "$ref", "$id" or "const", etc
// Therefore don't treat it like a schema.
if k == KeyProperties || k == KeyDependencies || k == KeyPatternProperties {
if child, ok := v.(map[string]any); ok {
for _, v := range child {
err := p.parseReferencesRecursive(v, *localRef, draft)
if err != nil {
return err
}
}
}
} else {
err := p.parseReferencesRecursive(v, *localRef, draft)
if err != nil {
return err
}
}
}
}
return nil
}
func (p *schemaPool) GetDocument(reference gojsonreference.JsonReference) (*schemaPoolDocument, error) {
var (
spd *schemaPoolDocument
draft *Draft
ok bool
err error
)
if internalLogEnabled {
internalLog("Get Document ( %s )", reference.String())
}
// Create a deep copy, so we can remove the fragment part later on without altering the original
refToURL, _ := gojsonreference.NewJsonReference(reference.String())
// First check if the given fragment is a location independent identifier
// http://json-schema.org/latest/json-schema-core.html#rfc.section.8.2.3
if spd, ok = p.schemaPoolDocuments[refToURL.String()]; ok {
if internalLogEnabled {
internalLog(" From pool")
}
return spd, nil
}
// If the given reference is not a location independent identifier,
// strip the fragment and look for a document with it's base URI
refToURL.GetUrl().Fragment = ""
if cachedSpd, ok := p.schemaPoolDocuments[refToURL.String()]; ok {
document, _, err := reference.GetPointer().Get(cachedSpd.Document)
if err != nil {
return nil, err
}
if internalLogEnabled {
internalLog(" From pool")
}
spd = &schemaPoolDocument{Document: document, Draft: cachedSpd.Draft}
p.schemaPoolDocuments[reference.String()] = spd
return spd, nil
}
// It is not possible to load anything remotely that is not canonical...
if !reference.IsCanonical() {
return nil, errors.New(formatErrorDescription(
Locale.ReferenceMustBeCanonical(),
ErrorDetails{"reference": reference.String()},
))
}
jsonReferenceLoader := p.jsonLoaderFactory.New(reference.String())
document, err := jsonReferenceLoader.LoadJSON()
if err != nil {
return nil, err
}
// add the whole document to the pool for potential re-use
err = p.parseReferences(document, refToURL, true)
if err != nil {
return nil, err
}
_, draft, _ = parseSchemaURL(document)
// resolve the potential fragment and also cache it
document, _, err = reference.GetPointer().Get(document)
if err != nil {
return nil, err
}
return &schemaPoolDocument{Document: document, Draft: draft}, nil
}
@@ -0,0 +1,64 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Pool of referenced schemas.
//
// created 25-06-2013
package gojsonschema
type schemaReferencePool struct {
documents map[string]*SubSchema
}
func newSchemaReferencePool() *schemaReferencePool {
p := &schemaReferencePool{}
p.documents = make(map[string]*SubSchema)
return p
}
func (p *schemaReferencePool) Get(ref string) (r *SubSchema, o bool) {
if internalLogEnabled {
internalLog("Schema Reference ( %s )", ref)
}
if sch, ok := p.documents[ref]; ok {
if internalLogEnabled {
internalLog(" From pool")
}
return sch, true
}
return nil, false
}
func (p *schemaReferencePool) Add(ref string, sch *SubSchema) {
if internalLogEnabled {
internalLog("Add Schema Reference %s to pool", ref)
}
if _, ok := p.documents[ref]; !ok {
p.documents[ref] = sch
}
}
@@ -0,0 +1,78 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Helper structure to handle schema types, and the combination of them.
//
// created 28-02-2013
package gojsonschema
import (
"errors"
"fmt"
"slices"
"strings"
)
type jsonSchemaType struct {
types []string
}
// Is the schema typed ? that is containing at least one type
// When not typed, the schema does not need any type validation
func (t *jsonSchemaType) IsTyped() bool {
return len(t.types) > 0
}
func (t *jsonSchemaType) Add(etype string) error {
if !isStringInSlice(JSONTypes, etype) {
return errors.New(formatErrorDescription(Locale.NotAValidType(), ErrorDetails{"given": "/" + etype + "/", "expected": JSONTypes}))
}
if t.Contains(etype) {
return errors.New(formatErrorDescription(Locale.Duplicated(), ErrorDetails{"type": etype}))
}
t.types = append(t.types, etype)
return nil
}
func (t *jsonSchemaType) Contains(etype string) bool {
return slices.Contains(t.types, etype)
}
func (t *jsonSchemaType) String() string {
if len(t.types) == 0 {
return StringUndefined // should never happen
}
// Displayed as a list [type1,type2,...]
if len(t.types) > 1 {
return fmt.Sprintf("[%s]", strings.Join(t.types, ","))
}
// Only one type: name only
return t.types[0]
}
@@ -0,0 +1,151 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Defines the structure of a sub-SubSchema.
// A sub-SubSchema can contain other sub-schemas.
//
// created 27-02-2013
package gojsonschema
import (
"math/big"
"regexp"
"github.com/xeipuuv/gojsonreference"
)
// Constants
const (
KeySchema = "$schema"
KeyID = "id"
KeyIDNew = "$id"
KeyRef = "$ref"
KeyTitle = "title"
KeyDescription = "description"
KeyType = "type"
KeyItems = "items"
KeyAdditionalItems = "additionalItems"
KeyProperties = "properties"
KeyPatternProperties = "patternProperties"
KeyAdditionalProperties = "additionalProperties"
KeyPropertyNames = "propertyNames"
KeyDefinitions = "definitions"
KeyMultipleOf = "multipleOf"
KeyMinimum = "minimum"
KeyMaximum = "maximum"
KeyExclusiveMinimum = "exclusiveMinimum"
KeyExclusiveMaximum = "exclusiveMaximum"
KeyMinLength = "minLength"
KeyMaxLength = "maxLength"
KeyPattern = "pattern"
KeyFormat = "format"
KeyMinProperties = "minProperties"
KeyMaxProperties = "maxProperties"
KeyDependencies = "dependencies"
KeyRequired = "required"
KeyMinItems = "minItems"
KeyMaxItems = "maxItems"
KeyUniqueItems = "uniqueItems"
KeyContains = "contains"
KeyConst = "const"
KeyEnum = "enum"
KeyOneOf = "oneOf"
KeyAnyOf = "anyOf"
KeyAllOf = "allOf"
KeyNot = "not"
KeyIf = "if"
KeyThen = "then"
KeyElse = "else"
)
// SubSchema holds a sub schema
type SubSchema struct {
Draft *Draft
// basic SubSchema meta properties
ID *gojsonreference.JsonReference
title *string
description *string
Property string
// Quick pass/fail for boolean schemas
pass *bool
// Types associated with the SubSchema
Types jsonSchemaType
// Reference url
Ref *gojsonreference.JsonReference
// Schema referenced
RefSchema *SubSchema
// hierarchy
Parent *SubSchema
ItemsChildren []*SubSchema
ItemsChildrenIsSingleSchema bool
PropertiesChildren []*SubSchema
// validation : number / integer
multipleOf *big.Rat
maximum *big.Rat
exclusiveMaximum *big.Rat
minimum *big.Rat
exclusiveMinimum *big.Rat
// validation : string
minLength *int
maxLength *int
pattern *regexp.Regexp
format string
// validation : object
minProperties *int
maxProperties *int
required []string
dependencies map[string]any
additionalProperties any
patternProperties map[string]*SubSchema
propertyNames *SubSchema
// validation : array
minItems *int
maxItems *int
uniqueItems bool
contains *SubSchema
additionalItems any
// validation : all
_const *string //const is a golang keyword
enum []string
// validation : SubSchema
oneOf []*SubSchema
AnyOf []*SubSchema
AllOf []*SubSchema
not *SubSchema
_if *SubSchema // if/else are golang keywords
_then *SubSchema
_else *SubSchema
}
@@ -0,0 +1,62 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Contains const types for schema and JSON.
//
// created 28-02-2013
package gojsonschema
// Type constants
const (
TypeArray = `array`
TypeBoolean = `boolean`
TypeInteger = `integer`
TypeNumber = `number`
TypeNull = `null`
TypeObject = `object`
TypeString = `string`
)
// JSONTypes hosts the list of type that are supported in JSON
var JSONTypes []string
// SchemaTypes Hosts The List Of Type That Are Supported In Schemas
var SchemaTypes []string
func init() {
JSONTypes = []string{
TypeArray,
TypeBoolean,
TypeInteger,
TypeNumber,
TypeNull,
TypeObject,
TypeString}
SchemaTypes = []string{
TypeArray,
TypeBoolean,
TypeInteger,
TypeNumber,
TypeObject,
TypeString}
}
@@ -0,0 +1,161 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Various utility functions.
//
// created 26-02-2013
// nolint:unused // Package in development (2021).
package gojsonschema
import (
"encoding/json"
"math/big"
"slices"
)
func isStringInSlice(s []string, what string) bool {
return slices.Contains(s, what)
}
func marshalToJSONString(value any) (*string, error) {
mBytes, err := json.Marshal(value)
if err != nil {
return nil, err
}
sBytes := string(mBytes)
return &sBytes, nil
}
func marshalWithoutNumber(value any) (*string, error) {
// The JSON is decoded using https://golang.org/pkg/encoding/json/#Decoder.UseNumber
// This means the numbers are internally still represented as strings and therefore 1.00 is unequal to 1
// One way to eliminate these differences is to decode and encode the JSON one more time without Decoder.UseNumber
// so that these differences in representation are removed
jsonString, err := marshalToJSONString(value)
if err != nil {
return nil, err
}
var document any
err = json.Unmarshal([]byte(*jsonString), &document)
if err != nil {
return nil, err
}
return marshalToJSONString(document)
}
func isJSONNumber(what any) bool {
switch what.(type) {
case json.Number:
return true
}
return false
}
func checkJSONInteger(what any) (isInt bool) {
jsonNumber := what.(json.Number)
bigFloat, isValidNumber := new(big.Rat).SetString(string(jsonNumber))
return isValidNumber && bigFloat.IsInt()
}
// same as ECMA Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER
const (
maxJSONFloat = float64(1<<53 - 1) // 9007199254740991.0 2^53 - 1
minJSONFloat = -float64(1<<53 - 1) //-9007199254740991.0 -2^53 - 1
)
func mustBeInteger(what any) *int {
number, ok := what.(json.Number)
if !ok {
return nil
}
isInt := checkJSONInteger(number)
if !isInt {
return nil
}
int64Value, err := number.Int64()
if err != nil {
return nil
}
// This doesn't actually convert to an int32 value; it converts to the
// system-specific default integer. Assuming this is a valid int32 could cause
// bugs.
int32Value := int(int64Value)
return &int32Value
}
func mustBeNumber(what any) *big.Rat {
number, ok := what.(json.Number)
if !ok {
return nil
}
float64Value, success := new(big.Rat).SetString(string(number))
if success {
return float64Value
}
return nil
}
func convertDocumentNode(val any) any {
if lval, ok := val.([]any); ok {
res := []any{}
for _, v := range lval {
res = append(res, convertDocumentNode(v))
}
return res
}
if mval, ok := val.(map[any]any); ok {
res := map[string]any{}
for k, v := range mval {
res[k.(string)] = convertDocumentNode(v)
}
return res
}
return val
}
@@ -0,0 +1,837 @@
// Copyright 2015 xeipuuv ( https://github.com/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.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Extends Schema and SubSchema, implements the validation phase.
//
// created 28-02-2013
package gojsonschema
import (
"encoding/json"
"math/big"
"reflect"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
// Validate loads and validates a JSON schema
func Validate(ls JSONLoader, ld JSONLoader) (*Result, error) {
// load schema
schema, err := NewSchema(ls)
if err != nil {
return nil, err
}
return schema.Validate(ld)
}
// Validate loads and validates a JSON document
func (v *Schema) Validate(l JSONLoader) (*Result, error) {
root, err := l.LoadJSON()
if err != nil {
return nil, err
}
return v.validateDocument(root), nil
}
func (v *Schema) validateDocument(root any) *Result {
result := &Result{}
context := NewJSONContext(StringContextRoot, nil)
v.RootSchema.validateRecursive(v.RootSchema, root, result, context)
return result
}
func (v *SubSchema) subValidateWithContext(document any, context *JSONContext) *Result {
result := &Result{}
v.validateRecursive(v, document, result, context)
return result
}
// Walker function to validate the json recursively against the SubSchema
func (v *SubSchema) validateRecursive(currentSubSchema *SubSchema, currentNode any, result *Result, context *JSONContext) {
if internalLogEnabled {
internalLog("validateRecursive %s", context.String())
internalLog(" %v", currentNode)
}
// Handle true/false schema as early as possible as all other fields will be nil
if currentSubSchema.pass != nil {
if !*currentSubSchema.pass {
result.addInternalError(
new(FalseError),
context,
currentNode,
ErrorDetails{},
)
}
return
}
// Handle referenced schemas, returns directly when a $ref is found
if currentSubSchema.RefSchema != nil {
v.validateRecursive(currentSubSchema.RefSchema, currentNode, result, context)
return
}
// Check for null value
if currentNode == nil {
if currentSubSchema.Types.IsTyped() && !currentSubSchema.Types.Contains(TypeNull) {
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.Types.String(),
"given": TypeNull,
},
)
return
}
currentSubSchema.validateSchema(currentSubSchema, currentNode, result, context)
v.validateCommon(currentSubSchema, currentNode, result, context)
} else { // Not a null value
if value, isNumber := currentNode.(json.Number); isNumber {
isInt := checkJSONInteger(value)
validType := currentSubSchema.Types.Contains(TypeNumber) || (isInt && currentSubSchema.Types.Contains(TypeInteger))
if currentSubSchema.Types.IsTyped() && !validType {
givenType := TypeInteger
if !isInt {
givenType = TypeNumber
}
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.Types.String(),
"given": givenType,
},
)
return
}
currentSubSchema.validateSchema(currentSubSchema, value, result, context)
v.validateNumber(currentSubSchema, value, result, context)
v.validateCommon(currentSubSchema, value, result, context)
v.validateString(currentSubSchema, value, result, context)
} else {
rValue := reflect.ValueOf(currentNode)
rKind := rValue.Kind()
switch rKind {
// Slice => JSON array
case reflect.Slice:
if currentSubSchema.Types.IsTyped() && !currentSubSchema.Types.Contains(TypeArray) {
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.Types.String(),
"given": TypeArray,
},
)
return
}
castCurrentNode := currentNode.([]any)
currentSubSchema.validateSchema(currentSubSchema, castCurrentNode, result, context)
v.validateArray(currentSubSchema, castCurrentNode, result, context)
v.validateCommon(currentSubSchema, castCurrentNode, result, context)
// Map => JSON object
case reflect.Map:
if currentSubSchema.Types.IsTyped() && !currentSubSchema.Types.Contains(TypeObject) {
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.Types.String(),
"given": TypeObject,
},
)
return
}
castCurrentNode, ok := currentNode.(map[string]any)
if !ok {
castCurrentNode = convertDocumentNode(currentNode).(map[string]any)
}
currentSubSchema.validateSchema(currentSubSchema, castCurrentNode, result, context)
v.validateObject(currentSubSchema, castCurrentNode, result, context)
v.validateCommon(currentSubSchema, castCurrentNode, result, context)
for _, pSchema := range currentSubSchema.PropertiesChildren {
nextNode, ok := castCurrentNode[pSchema.Property]
if ok {
subContext := NewJSONContext(pSchema.Property, context)
v.validateRecursive(pSchema, nextNode, result, subContext)
}
}
// Simple JSON values : string, number, boolean
case reflect.Bool:
if currentSubSchema.Types.IsTyped() && !currentSubSchema.Types.Contains(TypeBoolean) {
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.Types.String(),
"given": TypeBoolean,
},
)
return
}
value := currentNode.(bool)
currentSubSchema.validateSchema(currentSubSchema, value, result, context)
v.validateNumber(currentSubSchema, value, result, context)
v.validateCommon(currentSubSchema, value, result, context)
v.validateString(currentSubSchema, value, result, context)
case reflect.String:
if currentSubSchema.Types.IsTyped() && !currentSubSchema.Types.Contains(TypeString) {
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.Types.String(),
"given": TypeString,
},
)
return
}
value := currentNode.(string)
currentSubSchema.validateSchema(currentSubSchema, value, result, context)
v.validateNumber(currentSubSchema, value, result, context)
v.validateCommon(currentSubSchema, value, result, context)
v.validateString(currentSubSchema, value, result, context)
}
}
}
result.incrementScore()
}
// Different kinds of validation there, SubSchema / common / array / object / string...
func (v *SubSchema) validateSchema(currentSubSchema *SubSchema, currentNode any, result *Result, context *JSONContext) {
if internalLogEnabled {
internalLog("validateSchema %s", context.String())
internalLog(" %v", currentNode)
}
if len(currentSubSchema.AnyOf) > 0 {
validatedAnyOf := false
var bestValidationResult *Result
for _, anyOfSchema := range currentSubSchema.AnyOf {
if !validatedAnyOf {
validationResult := anyOfSchema.subValidateWithContext(currentNode, context)
validatedAnyOf = validationResult.Valid()
if !validatedAnyOf && (bestValidationResult == nil || validationResult.score > bestValidationResult.score) {
bestValidationResult = validationResult
}
}
}
if !validatedAnyOf {
result.addInternalError(new(NumberAnyOfError), context, currentNode, ErrorDetails{})
if bestValidationResult != nil {
// add error messages of closest matching SubSchema as
// that's probably the one the user was trying to match
result.mergeErrors(bestValidationResult)
}
}
}
if len(currentSubSchema.oneOf) > 0 {
nbValidated := 0
var bestValidationResult *Result
for _, oneOfSchema := range currentSubSchema.oneOf {
validationResult := oneOfSchema.subValidateWithContext(currentNode, context)
if validationResult.Valid() {
nbValidated++
} else if nbValidated == 0 && (bestValidationResult == nil || validationResult.score > bestValidationResult.score) {
bestValidationResult = validationResult
}
}
if nbValidated != 1 {
result.addInternalError(new(NumberOneOfError), context, currentNode, ErrorDetails{})
if nbValidated == 0 {
// add error messages of closest matching SubSchema as
// that's probably the one the user was trying to match
result.mergeErrors(bestValidationResult)
}
}
}
if len(currentSubSchema.AllOf) > 0 {
nbValidated := 0
for _, allOfSchema := range currentSubSchema.AllOf {
validationResult := allOfSchema.subValidateWithContext(currentNode, context)
if validationResult.Valid() {
nbValidated++
}
result.mergeErrors(validationResult)
}
if nbValidated != len(currentSubSchema.AllOf) {
result.addInternalError(new(NumberAllOfError), context, currentNode, ErrorDetails{})
}
}
if currentSubSchema.not != nil {
validationResult := currentSubSchema.not.subValidateWithContext(currentNode, context)
if validationResult.Valid() {
result.addInternalError(new(NumberNotError), context, currentNode, ErrorDetails{})
}
}
if len(currentSubSchema.dependencies) > 0 {
if currentNodeMap, ok := currentNode.(map[string]any); ok {
for elementKey := range currentNodeMap {
if dependency, ok := currentSubSchema.dependencies[elementKey]; ok {
switch dependency := dependency.(type) {
case []string:
for _, dependOnKey := range dependency {
if _, dependencyResolved := currentNode.(map[string]any)[dependOnKey]; !dependencyResolved {
result.addInternalError(
new(MissingDependencyError),
context,
currentNode,
ErrorDetails{"dependency": dependOnKey},
)
}
}
case *SubSchema:
dependency.validateRecursive(dependency, currentNode, result, context)
}
}
}
}
}
if currentSubSchema._if != nil {
validationResultIf := currentSubSchema._if.subValidateWithContext(currentNode, context)
if currentSubSchema._then != nil && validationResultIf.Valid() {
validationResultThen := currentSubSchema._then.subValidateWithContext(currentNode, context)
if !validationResultThen.Valid() {
result.addInternalError(new(ConditionThenError), context, currentNode, ErrorDetails{})
result.mergeErrors(validationResultThen)
}
}
if currentSubSchema._else != nil && !validationResultIf.Valid() {
validationResultElse := currentSubSchema._else.subValidateWithContext(currentNode, context)
if !validationResultElse.Valid() {
result.addInternalError(new(ConditionElseError), context, currentNode, ErrorDetails{})
result.mergeErrors(validationResultElse)
}
}
}
result.incrementScore()
}
func (v *SubSchema) validateCommon(currentSubSchema *SubSchema, value any, result *Result, context *JSONContext) {
if internalLogEnabled {
internalLog("validateCommon %s", context.String())
internalLog(" %v", value)
}
// const:
if currentSubSchema._const != nil {
vString, err := marshalWithoutNumber(value)
if err != nil {
result.addInternalError(new(InternalError), context, value, ErrorDetails{"error": err})
}
if *vString != *currentSubSchema._const {
result.addInternalError(new(ConstError),
context,
value,
ErrorDetails{
"allowed": *currentSubSchema._const,
},
)
}
}
// enum:
if len(currentSubSchema.enum) > 0 {
vString, err := marshalWithoutNumber(value)
if err != nil {
result.addInternalError(new(InternalError), context, value, ErrorDetails{"error": err})
}
if !isStringInSlice(currentSubSchema.enum, *vString) {
result.addInternalError(
new(EnumError),
context,
value,
ErrorDetails{
"allowed": strings.Join(currentSubSchema.enum, ", "),
},
)
}
}
// format:
if currentSubSchema.format != "" {
if !FormatCheckers.IsFormat(currentSubSchema.format, value) {
result.addInternalError(
new(DoesNotMatchFormatError),
context,
value,
ErrorDetails{"format": currentSubSchema.format},
)
}
}
result.incrementScore()
}
func (v *SubSchema) validateArray(currentSubSchema *SubSchema, value []any, result *Result, context *JSONContext) {
if internalLogEnabled {
internalLog("validateArray %s", context.String())
internalLog(" %v", value)
}
nbValues := len(value)
// TODO explain
if currentSubSchema.ItemsChildrenIsSingleSchema {
for i := range value {
subContext := NewJSONContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.ItemsChildren[0].subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
} else {
if len(currentSubSchema.ItemsChildren) > 0 {
nbItems := len(currentSubSchema.ItemsChildren)
// while we have both schemas and values, check them against each other
for i := 0; i != nbItems && i != nbValues; i++ {
subContext := NewJSONContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.ItemsChildren[i].subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
if nbItems < nbValues {
// we have less schemas than elements in the instance array,
// but that might be ok if "additionalItems" is specified.
switch currentSubSchema.additionalItems.(type) {
case bool:
if !currentSubSchema.additionalItems.(bool) {
result.addInternalError(new(ArrayNoAdditionalItemsError), context, value, ErrorDetails{})
}
case *SubSchema:
additionalItemSchema := currentSubSchema.additionalItems.(*SubSchema)
for i := nbItems; i != nbValues; i++ {
subContext := NewJSONContext(strconv.Itoa(i), context)
validationResult := additionalItemSchema.subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
}
}
}
}
// minItems & maxItems
if currentSubSchema.minItems != nil {
if nbValues < *currentSubSchema.minItems {
result.addInternalError(
new(ArrayMinItemsError),
context,
value,
ErrorDetails{"min": *currentSubSchema.minItems},
)
}
}
if currentSubSchema.maxItems != nil {
if nbValues > *currentSubSchema.maxItems {
result.addInternalError(
new(ArrayMaxItemsError),
context,
value,
ErrorDetails{"max": *currentSubSchema.maxItems},
)
}
}
// uniqueItems:
if currentSubSchema.uniqueItems {
var stringifiedItems = make(map[string]int)
for j, v := range value {
vString, err := marshalWithoutNumber(v)
if err != nil {
result.addInternalError(new(InternalError), context, value, ErrorDetails{"err": err})
}
if i, ok := stringifiedItems[*vString]; ok {
result.addInternalError(
new(ItemsMustBeUniqueError),
context,
value,
ErrorDetails{"type": TypeArray, "i": i, "j": j},
)
}
stringifiedItems[*vString] = j
}
}
// contains:
if currentSubSchema.contains != nil {
validatedOne := false
var bestValidationResult *Result
for i, v := range value {
subContext := NewJSONContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.contains.subValidateWithContext(v, subContext)
if validationResult.Valid() {
validatedOne = true
break
}
if bestValidationResult == nil || validationResult.score > bestValidationResult.score {
bestValidationResult = validationResult
}
}
if !validatedOne {
result.addInternalError(
new(ArrayContainsError),
context,
value,
ErrorDetails{},
)
if bestValidationResult != nil {
result.mergeErrors(bestValidationResult)
}
}
}
result.incrementScore()
}
func (v *SubSchema) validateObject(currentSubSchema *SubSchema, value map[string]any, result *Result, context *JSONContext) {
if internalLogEnabled {
internalLog("validateObject %s", context.String())
internalLog(" %v", value)
}
// minProperties & maxProperties:
if currentSubSchema.minProperties != nil {
if len(value) < *currentSubSchema.minProperties {
result.addInternalError(
new(ArrayMinPropertiesError),
context,
value,
ErrorDetails{"min": *currentSubSchema.minProperties},
)
}
}
if currentSubSchema.maxProperties != nil {
if len(value) > *currentSubSchema.maxProperties {
result.addInternalError(
new(ArrayMaxPropertiesError),
context,
value,
ErrorDetails{"max": *currentSubSchema.maxProperties},
)
}
}
// required:
for _, requiredProperty := range currentSubSchema.required {
_, ok := value[requiredProperty]
if ok {
result.incrementScore()
} else {
result.addInternalError(
new(RequiredError),
context,
value,
ErrorDetails{"property": requiredProperty},
)
}
}
// additionalProperty & patternProperty:
for pk := range value {
// Check whether this property is described by "properties"
found := false
for _, spValue := range currentSubSchema.PropertiesChildren {
if pk == spValue.Property {
found = true
}
}
// Check whether this property is described by "patternProperties"
ppMatch := v.validatePatternProperty(currentSubSchema, pk, value[pk], result, context)
// If it is not described by neither "properties" nor "patternProperties" it must pass "additionalProperties"
if !found && !ppMatch {
switch ap := currentSubSchema.additionalProperties.(type) {
case bool:
// Handle the boolean case separately as it's cleaner to return a specific error than failing to pass the false schema
if !ap {
result.addInternalError(
new(AdditionalPropertyNotAllowedError),
context,
value[pk],
ErrorDetails{"property": pk},
)
}
case *SubSchema:
validationResult := ap.subValidateWithContext(value[pk], NewJSONContext(pk, context))
result.mergeErrors(validationResult)
}
}
}
// propertyNames:
if currentSubSchema.propertyNames != nil {
for pk := range value {
validationResult := currentSubSchema.propertyNames.subValidateWithContext(pk, context)
if !validationResult.Valid() {
result.addInternalError(new(InvalidPropertyNameError),
context,
value, ErrorDetails{
"property": pk,
})
result.mergeErrors(validationResult)
}
}
}
result.incrementScore()
}
func (v *SubSchema) validatePatternProperty(currentSubSchema *SubSchema, key string, value any, result *Result, context *JSONContext) bool {
if internalLogEnabled {
internalLog("validatePatternProperty %s", context.String())
internalLog(" %s %v", key, value)
}
validated := false
for pk, pv := range currentSubSchema.patternProperties {
if matches, _ := regexp.MatchString(pk, key); matches {
validated = true
subContext := NewJSONContext(key, context)
validationResult := pv.subValidateWithContext(value, subContext)
result.mergeErrors(validationResult)
}
}
if !validated {
return false
}
result.incrementScore()
return true
}
func (v *SubSchema) validateString(currentSubSchema *SubSchema, value any, result *Result, context *JSONContext) {
// Ignore JSON numbers
stringValue, isString := value.(string)
if !isString {
return
}
if internalLogEnabled {
internalLog("validateString %s", context.String())
internalLog(" %v", value)
}
// minLength & maxLength:
if currentSubSchema.minLength != nil {
if utf8.RuneCountInString(stringValue) < *currentSubSchema.minLength {
result.addInternalError(
new(StringLengthGTEError),
context,
value,
ErrorDetails{"min": *currentSubSchema.minLength},
)
}
}
if currentSubSchema.maxLength != nil {
if utf8.RuneCountInString(stringValue) > *currentSubSchema.maxLength {
result.addInternalError(
new(StringLengthLTEError),
context,
value,
ErrorDetails{"max": *currentSubSchema.maxLength},
)
}
}
// pattern:
if currentSubSchema.pattern != nil {
if !currentSubSchema.pattern.MatchString(stringValue) {
result.addInternalError(
new(DoesNotMatchPatternError),
context,
value,
ErrorDetails{"pattern": currentSubSchema.pattern},
)
}
}
result.incrementScore()
}
func (v *SubSchema) validateNumber(currentSubSchema *SubSchema, value any, result *Result, context *JSONContext) {
// Ignore non numbers
number, isNumber := value.(json.Number)
if !isNumber {
return
}
if internalLogEnabled {
internalLog("validateNumber %s", context.String())
internalLog(" %v", value)
}
float64Value, _ := new(big.Rat).SetString(string(number))
// multipleOf:
if currentSubSchema.multipleOf != nil {
if q := new(big.Rat).Quo(float64Value, currentSubSchema.multipleOf); !q.IsInt() {
result.addInternalError(
new(MultipleOfError),
context,
number,
ErrorDetails{
"multiple": new(big.Float).SetRat(currentSubSchema.multipleOf),
},
)
}
}
//maximum & exclusiveMaximum:
if currentSubSchema.maximum != nil {
if float64Value.Cmp(currentSubSchema.maximum) == 1 {
result.addInternalError(
new(NumberLTEError),
context,
number,
ErrorDetails{
"max": new(big.Float).SetRat(currentSubSchema.maximum),
},
)
}
}
if currentSubSchema.exclusiveMaximum != nil {
if float64Value.Cmp(currentSubSchema.exclusiveMaximum) >= 0 {
result.addInternalError(
new(NumberLTError),
context,
number,
ErrorDetails{
"max": new(big.Float).SetRat(currentSubSchema.exclusiveMaximum),
},
)
}
}
//minimum & exclusiveMinimum:
if currentSubSchema.minimum != nil {
if float64Value.Cmp(currentSubSchema.minimum) == -1 {
result.addInternalError(
new(NumberGTEError),
context,
number,
ErrorDetails{
"min": new(big.Float).SetRat(currentSubSchema.minimum),
},
)
}
}
if currentSubSchema.exclusiveMinimum != nil {
if float64Value.Cmp(currentSubSchema.exclusiveMinimum) <= 0 {
result.addInternalError(
new(NumberGTError),
context,
number,
ErrorDetails{
"min": new(big.Float).SetRat(currentSubSchema.exclusiveMinimum),
},
)
}
}
result.incrementScore()
}
@@ -0,0 +1,45 @@
// Copyright 2021 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package patch
import (
"strings"
"github.com/open-policy-agent/opa/v1/storage"
)
// ParsePatchPathEscaped returns a new path for the given escaped str.
// This is based on storage.ParsePathEscaped so will do URL unescaping of
// the provided str for backwards compatibility, but also handles the
// specific escape strings defined in RFC 6901 (JSON Pointer) because
// that's what's mandated by RFC 6902 (JSON Patch).
func ParsePatchPathEscaped(str string) (path storage.Path, ok bool) {
path, ok = storage.ParsePathEscaped(str)
if !ok {
return
}
for i := range path {
// RFC 6902 section 4: "[The "path" member's] value is a string containing
// a JSON-Pointer value [RFC6901] that references a location within the
// target document (the "target location") where the operation is performed."
//
// RFC 6901 section 3: "Because the characters '~' (%x7E) and '/' (%x2F)
// have special meanings in JSON Pointer, '~' needs to be encoded as '~0'
// and '/' needs to be encoded as '~1' when these characters appear in a
// reference token."
// RFC 6901 section 4: "Evaluation of each reference token begins by
// decoding any escaped character sequence. This is performed by first
// transforming any occurrence of the sequence '~1' to '/', and then
// transforming any occurrence of the sequence '~0' to '~'. By performing
// the substitutions in this order, an implementation avoids the error of
// turning '~01' first into '~1' and then into '/', which would be
// incorrect (the string '~01' correctly becomes '~1' after transformation)."
path[i] = strings.ReplaceAll(path[i], "~1", "/")
path[i] = strings.ReplaceAll(path[i], "~0", "~")
}
return
}
@@ -0,0 +1,3 @@
# Longest Common Substring
Original source https://github.com/vmarkovtsev/go-lcss
+197
View File
@@ -0,0 +1,197 @@
package lcss
import "bytes"
// LongestCommonSubstring returns the longest substring which is present in all the given strings.
// https://en.wikipedia.org/wiki/Longest_common_substring_problem
// Not to be confused with the Longest Common Subsequence.
// Complexity:
// * time: sum of `n_i*log(n_i)` where `n_i` is the length of each string.
// * space: sum of `n_i`.
// Returns a byte slice which is never a nil.
//
// ### Algorithm.
// We build suffix arrays for each of the passed string and then follow the same procedure
// as in merge sort: pick the least suffix in the lexicographical order. It is possible
// because the suffix arrays are already sorted.
// We record the last encountered suffixes from each of the strings and measure the longest
// common prefix of those at each "merge sort" step.
// The string comparisons are optimized by maintaining the char-level prefix tree of the "heads"
// of the suffix array sequences.
func LongestCommonSubstring(strs ...[]byte) []byte {
strslen := len(strs)
if strslen == 0 {
return []byte{}
}
if strslen == 1 {
return strs[0]
}
suffixes := make([][]int, strslen)
for i, str := range strs {
suffixes[i] = qsufsort(str)
}
return lcss(strs, suffixes)
}
func lcss(strs [][]byte, suffixes [][]int) []byte {
strslen := len(strs)
if strslen == 0 {
return []byte{}
}
if strslen == 1 {
return strs[0]
}
minstrlen := len(strs[0]) // minimum length of the strings
for _, str := range strs {
if minstrlen > len(str) {
minstrlen = len(str)
}
}
heads := make([]int, strslen) // position in each suffix array
boilerplate := make([][]byte, strslen) // existing suffixes in the tree
boiling := 0 // indicates how many distinct suffix arrays are presented in `boilerplate`
var root charNode // the character tree built on the strings from `boilerplate`
lcs := []byte{} // our function's return value, `var lcss []byte` does *not* work
for {
mini := -1
var minSuffixStr []byte
for i, head := range heads {
if head >= len(suffixes[i]) {
// this suffix array has been scanned till the end
continue
}
suffix := strs[i][suffixes[i][head]:]
if minSuffixStr == nil {
// initialize
mini = i
minSuffixStr = suffix
} else if bytes.Compare(minSuffixStr, suffix) > 0 {
// the current suffix is the smallest in the lexicographical order
mini = i
minSuffixStr = suffix
}
}
if mini == -1 {
// all heads exhausted
break
}
if boilerplate[mini] != nil {
// if we already have a suffix from this string, replace it with the new one
root.Remove(boilerplate[mini])
} else {
// we track the number of distinct strings which have been touched
// when `boiling` becomes strslen we can start measuring the longest common prefix
boiling++
}
boilerplate[mini] = minSuffixStr
root.Add(minSuffixStr)
heads[mini]++
if boiling == strslen && root.LongestCommonPrefixLength() > len(lcs) {
// all heads > 0, the current common prefix of the suffixes is the longest
lcs = root.LongestCommonPrefix()
if len(lcs) == minstrlen {
// early exit - we will never find a longer substring
break
}
}
}
return lcs
}
// charNode builds a tree of individual characters.
// `used` is the counter for collecting garbage: those nodes which have `used`=0 are removed.
// The root charNode always remains intact apart from `children`.
// The tree supports 4 operations:
// 1. Add() a new string.
// 2. Remove() an existing string which was previously Add()-ed.
// 3. LongestCommonPrefixLength().
// 4. LongestCommonPrefix().
type charNode struct {
char byte
children []charNode
used int
}
// Add includes a new string into the tree. We start from the root and
// increment `used` of all the nodes we visit.
func (cn *charNode) Add(str []byte) {
head := cn
for i, char := range str {
found := false
for j, child := range head.children {
if child.char == char {
head.children[j].used++
head = &head.children[j] // -> child
found = true
break
}
}
if !found {
// add the missing nodes one by one
for _, char = range str[i:] {
head.children = append(head.children, charNode{char: char, children: nil, used: 1})
head = &head.children[len(head.children)-1]
}
break
}
}
}
// Remove excludes a node which was previously Add()-ed.
// We start from the root and decrement `used` of all the nodes we visit.
// If there is a node with `used`=0, we erase it from the parent's list of children
// and stop traversing the tree.
func (cn *charNode) Remove(str []byte) {
stop := false
head := cn
for _, char := range str {
for j, child := range head.children {
if child.char != char {
continue
}
head.children[j].used--
var parent *charNode
head, parent = &head.children[j], head // shift to the child
if head.used == 0 {
parent.children = append(parent.children[:j], parent.children[j+1:]...)
// we can skip deleting the rest of the nodes - they have been already discarded
stop = true
}
break
}
if stop {
break
}
}
}
// LongestCommonPrefixLength returns the length of the longest common prefix of the strings
// which are stored in the tree. We visit the children recursively starting from the root and
// stop if `used` value decreases or there is more than one child.
func (cn charNode) LongestCommonPrefixLength() int {
var result int
for head := cn; len(head.children) == 1 && head.children[0].used >= head.used; head = head.children[0] {
result++
}
return result
}
// LongestCommonPrefix returns the longest common prefix of the strings
// which are stored in the tree. We compute the length by calling LongestCommonPrefixLength()
// and then record the characters which we visit along the way from the root to the last node.
func (cn charNode) LongestCommonPrefix() []byte {
result := make([]byte, cn.LongestCommonPrefixLength())
if len(result) == 0 {
return result
}
var i int
for head := cn.children[0]; ; head = head.children[0] {
result[i] = head.char
i++
if i == len(result) {
break
}
}
return result
}
@@ -0,0 +1,169 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This algorithm is based on "Faster Suffix Sorting"
// by N. Jesper Larsson and Kunihiko Sadakane
// paper: http://www.larsson.dogma.net/ssrev-tr.pdf
// code: http://www.larsson.dogma.net/qsufsort.c
// This algorithm computes the suffix array sa by computing its inverse.
// Consecutive groups of suffixes in sa are labeled as sorted groups or
// unsorted groups. For a given pass of the sorter, all suffixes are ordered
// up to their first h characters, and sa is h-ordered. Suffixes in their
// final positions and unambiguously sorted in h-order are in a sorted group.
// Consecutive groups of suffixes with identical first h characters are an
// unsorted group. In each pass of the algorithm, unsorted groups are sorted
// according to the group number of their following suffix.
// In the implementation, if sa[i] is negative, it indicates that i is
// the first element of a sorted group of length -sa[i], and can be skipped.
// An unsorted group sa[i:k] is given the group number of the index of its
// last element, k-1. The group numbers are stored in the inverse slice (inv),
// and when all groups are sorted, this slice is the inverse suffix array.
package lcss
import "sort"
// qsufsort constructs the suffix array for a given string.
func qsufsort(data []byte) []int {
// initial sorting by first byte of suffix
sa := sortedByFirstByte(data)
if len(sa) < 2 {
return sa
}
// initialize the group lookup table
// this becomes the inverse of the suffix array when all groups are sorted
inv := initGroups(sa, data)
// the index starts 1-ordered
sufSortable := &suffixSortable{sa: sa, inv: inv, h: 1}
for sa[0] > -len(sa) { // until all suffixes are one big sorted group
// The suffixes are h-ordered, make them 2*h-ordered
pi := 0 // pi is first position of first group
sl := 0 // sl is negated length of sorted groups
for pi < len(sa) {
if s := sa[pi]; s < 0 { // if pi starts sorted group
pi -= s // skip over sorted group
sl += s // add negated length to sl
} else { // if pi starts unsorted group
if sl != 0 {
sa[pi+sl] = sl // combine sorted groups before pi
sl = 0
}
pk := inv[s] + 1 // pk-1 is last position of unsorted group
sufSortable.sa = sa[pi:pk]
sort.Sort(sufSortable)
sufSortable.updateGroups(pi)
pi = pk // next group
}
}
if sl != 0 { // if the array ends with a sorted group
sa[pi+sl] = sl // combine sorted groups at end of sa
}
sufSortable.h *= 2 // double sorted depth
}
for i := range sa { // reconstruct suffix array from inverse
sa[inv[i]] = i
}
return sa
}
func sortedByFirstByte(data []byte) []int {
// total byte counts
var count [256]int
for _, b := range data {
count[b]++
}
// make count[b] equal index of first occurrence of b in sorted array
sum := 0
for b := range count {
count[b], sum = sum, count[b]+sum
}
// iterate through bytes, placing index into the correct spot in sa
sa := make([]int, len(data))
for i, b := range data {
sa[count[b]] = i
count[b]++
}
return sa
}
func initGroups(sa []int, data []byte) []int {
// label contiguous same-letter groups with the same group number
inv := make([]int, len(data))
prevGroup := len(sa) - 1
groupByte := data[sa[prevGroup]]
for i := len(sa) - 1; i >= 0; i-- {
if b := data[sa[i]]; b < groupByte {
if prevGroup == i+1 {
sa[i+1] = -1
}
groupByte = b
prevGroup = i
}
inv[sa[i]] = prevGroup
if prevGroup == 0 {
sa[0] = -1
}
}
// Separate out the final suffix to the start of its group.
// This is necessary to ensure the suffix "a" is before "aba"
// when using a potentially unstable sort.
lastByte := data[len(data)-1]
s := -1
for i := range sa {
if sa[i] >= 0 {
if data[sa[i]] == lastByte && s == -1 {
s = i
}
if sa[i] == len(sa)-1 {
sa[i], sa[s] = sa[s], sa[i]
inv[sa[s]] = s
sa[s] = -1 // mark it as an isolated sorted group
break
}
}
}
return inv
}
type suffixSortable struct {
sa []int
inv []int
h int
buf []int // common scratch space
}
func (x *suffixSortable) Len() int { return len(x.sa) }
func (x *suffixSortable) Less(i, j int) bool { return x.inv[x.sa[i]+x.h] < x.inv[x.sa[j]+x.h] }
func (x *suffixSortable) Swap(i, j int) { x.sa[i], x.sa[j] = x.sa[j], x.sa[i] }
func (x *suffixSortable) updateGroups(offset int) {
bounds := x.buf[0:0]
group := x.inv[x.sa[0]+x.h]
for i := 1; i < len(x.sa); i++ {
if g := x.inv[x.sa[i]+x.h]; g > group {
bounds = append(bounds, i)
group = g
}
}
bounds = append(bounds, len(x.sa))
x.buf = bounds
// update the group numberings after all new groups are determined
prev := 0
for _, b := range bounds {
for i := prev; i < b; i++ {
x.inv[x.sa[i]] = offset + b - 1
}
if b-prev == 1 {
x.sa[prev] = -1
}
prev = b
}
}
@@ -0,0 +1,170 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package leb128 implements LEB128 integer encoding.
package leb128
import (
"io"
)
// MustReadVarInt32 returns an int32 from r or panics.
func MustReadVarInt32(r io.Reader) int32 {
i32, err := ReadVarInt32(r)
if err != nil {
panic(err)
}
return i32
}
// MustReadVarInt64 returns an int64 from r or panics.
func MustReadVarInt64(r io.Reader) int64 {
i64, err := ReadVarInt64(r)
if err != nil {
panic(err)
}
return i64
}
// MustReadVarUint32 returns an uint32 from r or panics.
func MustReadVarUint32(r io.Reader) uint32 {
u32, err := ReadVarUint32(r)
if err != nil {
panic(err)
}
return u32
}
// MustReadVarUint64 returns an uint64 from r or panics.
func MustReadVarUint64(r io.Reader) uint64 {
u64, err := ReadVarUint64(r)
if err != nil {
panic(err)
}
return u64
}
// Copied rom http://dwarfstd.org/doc/Dwarf3.pdf.
// ReadVarUint32 tries to read a uint32 from r.
func ReadVarUint32(r io.Reader) (uint32, error) {
u64, err := ReadVarUint64(r)
if err != nil {
return 0, err
}
return uint32(u64), nil
}
// ReadVarUint64 tries to read a uint64 from r.
func ReadVarUint64(r io.Reader) (uint64, error) {
var result uint64
var shift uint64
buf := make([]byte, 1)
for {
if _, err := r.Read(buf); err != nil {
return 0, err
}
v := uint64(buf[0])
result |= (v & 0x7F) << shift
if v&0x80 == 0 {
return result, nil
}
shift += 7
}
}
// ReadVarInt32 tries to read a int32 from r.
func ReadVarInt32(r io.Reader) (int32, error) {
i64, err := ReadVarInt64(r)
if err != nil {
return 0, err
}
return int32(i64), nil
}
// ReadVarInt64 tries to read a int64 from r.
func ReadVarInt64(r io.Reader) (int64, error) {
var result int64
var shift uint64
size := uint64(32)
buf := make([]byte, 1)
for {
if _, err := r.Read(buf); err != nil {
return 0, err
}
v := int64(buf[0])
result |= (v & 0x7F) << shift
shift += 7
if v&0x80 == 0 {
if (shift < size) && (v&0x40 != 0) {
result |= (^0 << shift)
}
return result, nil
}
}
}
// WriteVarUint32 writes u to w.
func WriteVarUint32(w io.Writer, u uint32) error {
var b []byte
_, err := w.Write(appendUleb128(b, uint64(u)))
return err
}
// WriteVarUint64 writes u to w.
func WriteVarUint64(w io.Writer, u uint64) error {
var b []byte
_, err := w.Write(appendUleb128(b, u))
return err
}
// WriteVarInt32 writes u to w.
func WriteVarInt32(w io.Writer, i int32) error {
var b []byte
_, err := w.Write(appendSleb128(b, int64(i)))
return err
}
// WriteVarInt64 writes u to w.
func WriteVarInt64(w io.Writer, i int64) error {
var b []byte
_, err := w.Write(appendSleb128(b, i))
return err
}
// Copied from https://github.com/golang/go/blob/master/src/cmd/internal/dwarf/dwarf.go.
// appendUleb128 appends v to b using DWARF's unsigned LEB128 encoding.
func appendUleb128(b []byte, v uint64) []byte {
for {
c := uint8(v & 0x7f)
v >>= 7
if v != 0 {
c |= 0x80
}
b = append(b, c)
if c&0x80 == 0 {
break
}
}
return b
}
// appendSleb128 appends v to b using DWARF's signed LEB128 encoding.
func appendSleb128(b []byte, v int64) []byte {
for {
c := uint8(v & 0x7f)
s := uint8(v & 0x40)
v >>= 7
if (v != -1 || s == 0) && (v != 0 || s != 0) {
c |= 0x80
}
b = append(b, c)
if c&0x80 == 0 {
break
}
}
return b
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2017 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package merge contains helpers to merge data structures
// frequently encountered in OPA.
package merge
// InterfaceMaps returns the result of merging a and b. If a and b cannot be
// merged because of conflicting key-value pairs, ok is false.
func InterfaceMaps(a map[string]any, b map[string]any) (map[string]any, bool) {
if a == nil {
return b, true
}
if hasConflicts(a, b) {
return nil, false
}
return merge(a, b), true
}
func merge(a, b map[string]any) map[string]any {
for k := range b {
add := b[k]
exist, ok := a[k]
if !ok {
a[k] = add
continue
}
existObj := exist.(map[string]any)
addObj := add.(map[string]any)
a[k] = merge(existObj, addObj)
}
return a
}
func hasConflicts(a, b map[string]any) bool {
for k := range b {
add := b[k]
exist, ok := a[k]
if !ok {
continue
}
existObj, existOk := exist.(map[string]any)
addObj, addOk := add.(map[string]any)
if !existOk || !addOk {
return true
}
if hasConflicts(existObj, addObj) {
return true
}
}
return false
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,337 @@
package planner
import (
"fmt"
"sort"
"github.com/open-policy-agent/opa/v1/ast"
)
// funcstack implements a simple map structure used to keep track of virtual
// document => planned function names. The structure supports Push and Pop
// operations so that the planner can shadow planned functions when 'with'
// statements are found.
// The "gen" numbers indicate the "generations"; whenever a 'with' statement
// is planned (a new map is `Push()`ed), it will jump to a previously unused
// number.
type funcstack struct {
stack []taggedPairs
next int
}
type taggedPairs struct {
pairs map[string]string
vars []ast.Var
vcount int
gen int
}
func newFuncstack() *funcstack {
return &funcstack{
stack: []taggedPairs{
{
pairs: map[string]string{},
gen: 0,
vars: []ast.Var{
ast.InputRootDocument.Value.(ast.Var),
ast.DefaultRootDocument.Value.(ast.Var),
},
vcount: 2,
},
},
next: 1}
}
func (p funcstack) last() taggedPairs {
return p.stack[len(p.stack)-1]
}
func (p funcstack) argVars() int {
return p.last().vcount
}
func (p funcstack) vars() []ast.Var {
ret := make([]ast.Var, 0, p.last().vcount)
for i := range p.stack {
ret = append(ret, p.stack[i].vars...)
}
return ret
}
func (p funcstack) Add(key, value string) {
p.last().pairs[key] = value
}
func (p funcstack) Get(key string) (string, bool) {
value, ok := p.last().pairs[key]
return value, ok
}
func (p *funcstack) Push(funcs map[string]string, vars []ast.Var) {
p.stack = append(p.stack, taggedPairs{
pairs: funcs,
gen: p.next,
vars: vars,
vcount: p.last().vcount + len(vars),
})
p.next++
}
func (p *funcstack) Pop() map[string]string {
last := p.last()
p.stack = p.stack[:len(p.stack)-1]
return last.pairs
}
func (p funcstack) gen() int {
return p.last().gen
}
// ruletrie implements a simple trie structure for organizing rules that may be
// planned. The trie nodes are keyed by the rule path. The ruletrie supports
// Push and Pop operations that allow the planner to shadow subtrees when 'with'
// statements are found.
type ruletrie struct {
children map[ast.Value][]*ruletrie
rules []*ast.Rule
}
func newRuletrie() *ruletrie {
return &ruletrie{
children: map[ast.Value][]*ruletrie{},
}
}
func (t *ruletrie) Arity() int {
rules := t.Rules()
if len(rules) > 0 {
return len(rules[0].Head.Args)
}
return 0
}
func (t *ruletrie) Rules() []*ast.Rule {
if t != nil {
if t.rules == nil {
return nil
}
rules := make([]*ast.Rule, len(t.rules), len(t.rules)+len(t.children)) // could be too little
copy(rules, t.rules)
// NOTE(sr): We pull in one layer of children: the compiler ensures
// that these are the only possible, relevant rule sources for a given
// ref: If the trie is what we get for
//
// a.b.c = 1 { ... }
// a.b[x] = 2 { ... }
//
// and we're retrieving a.b, we want Rules() to include the rule body
// of a.b.c.
// FIXME: We need to go deeper than just immediate children (?)
for _, rs := range t.children {
if r := rs[len(rs)-1].rules; r != nil {
rules = append(rules, r...)
}
}
return rules
}
return nil
}
func (t *ruletrie) Push(key ast.Ref) {
node := t
for i := range len(key) - 1 {
node = node.Get(key[i].Value)
if node == nil {
return
}
}
elem := key[len(key)-1]
node.children[elem.Value] = append(node.children[elem.Value], nil)
}
func (t *ruletrie) Pop(key ast.Ref) {
node := t
for i := range len(key) - 1 {
node = node.Get(key[i].Value)
if node == nil {
return
}
}
elem := key[len(key)-1]
sl := node.children[elem.Value]
node.children[elem.Value] = sl[:len(sl)-1]
}
func (t *ruletrie) Insert(key ast.Ref) *ruletrie {
node := t
for _, elem := range key {
child := node.Get(elem.Value)
if child == nil {
child = newRuletrie()
node.children[elem.Value] = append(node.children[elem.Value], child)
}
node = child
}
return node
}
func (t *ruletrie) Lookup(key ast.Ref) *ruletrie {
node := t
for _, elem := range key {
node = node.Get(elem.Value)
if node == nil {
return nil
}
}
return node
}
func (t *ruletrie) LookupShallowest(key ast.Ref) *ruletrie {
node := t
for _, elem := range key {
node = node.Get(elem.Value)
if node == nil {
return nil
}
if len(node.rules) > 0 {
return node
}
}
return node
}
// TODO: Collapse rules with overlapping extent to same node(?)
func (t *ruletrie) LookupOrInsert(key ast.Ref) *ruletrie {
if val := t.LookupShallowest(key); val != nil {
return val
}
return t.Insert(key)
}
func (t *ruletrie) DescendantRules() []*ast.Rule {
if len(t.children) == 0 {
return t.rules
}
rules := make([]*ast.Rule, len(t.rules), len(t.rules)+len(t.children)) // could be too little
copy(rules, t.rules)
for _, cs := range t.children {
for _, c := range cs {
rules = append(rules, c.DescendantRules()...)
}
}
return rules
}
func (t *ruletrie) ChildrenCount() int {
return len(t.children)
}
func (t *ruletrie) Children() []ast.Value {
if t == nil {
return nil
}
sorted := make([]ast.Value, 0, len(t.children))
for key := range t.children {
if t.Get(key) != nil {
sorted = append(sorted, key)
}
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Compare(sorted[j]) < 0
})
return sorted
}
func (t *ruletrie) Get(k ast.Value) *ruletrie {
if t == nil {
return nil
}
nodes := t.children[k]
if len(nodes) == 0 {
return nil
}
return nodes[len(nodes)-1]
}
func (t *ruletrie) DepthFirst(f func(*ruletrie) bool) {
if f(t) {
return
}
for _, rules := range t.children {
for i := range rules {
rules[i].DepthFirst(f)
}
}
}
func (t *ruletrie) Depth() int {
if len(t.Children()) == 0 {
return 0
}
c := make([]int, 0, len(t.Children()))
for _, nodes := range t.children {
c = append(c, nodes[len(nodes)-1].Depth())
}
max := 0
for i := range c {
if max < c[i] {
max = c[i]
}
}
return max + 1
}
func (t *ruletrie) String() string {
return fmt.Sprintf("<ruletrie rules:%v children:%v>", t.rules, t.children)
}
type functionMocksStack struct {
stack []*functionMocksElem
}
type functionMocksElem []frame
type frame map[string]*ast.Term
func newFunctionMocksStack() *functionMocksStack {
stack := &functionMocksStack{}
stack.Push()
return stack
}
func newFunctionMocksElem() *functionMocksElem {
return &functionMocksElem{}
}
func (s *functionMocksStack) Push() {
s.stack = append(s.stack, newFunctionMocksElem())
}
func (s *functionMocksStack) Pop() {
s.stack = s.stack[:len(s.stack)-1]
}
func (s *functionMocksStack) PushFrame(f frame) {
current := s.stack[len(s.stack)-1]
*current = append(*current, f)
}
func (s *functionMocksStack) PopFrame() {
current := s.stack[len(s.stack)-1]
*current = (*current)[:len(*current)-1]
}
func (s *functionMocksStack) Lookup(f string) *ast.Term {
current := *s.stack[len(s.stack)-1]
for i := len(current) - 1; i >= 0; i-- {
if t, ok := current[i][f]; ok {
return t
}
}
return nil
}
@@ -0,0 +1,71 @@
// Copyright 2019 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package planner
import (
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/ir"
)
type varstack []map[ast.Var]ir.Local
func newVarstack(frames ...map[ast.Var]ir.Local) *varstack {
vs := &varstack{}
for _, f := range frames {
vs.Push(f)
}
return vs
}
func (vs varstack) GetOrElse(k ast.Var, orElse func() ir.Local) ir.Local {
l, ok := vs.Get(k)
if !ok {
l = orElse()
vs.Put(k, l)
}
return l
}
func (vs varstack) GetOrEmpty(k ast.Var) ir.Local {
l, _ := vs.Get(k)
return l
}
func (vs varstack) Get(k ast.Var) (ir.Local, bool) {
for i := len(vs) - 1; i >= 0; i-- {
if l, ok := vs[i][k]; ok {
return l, true
}
}
return 0, false
}
func (vs varstack) GetOpOrEmpty(k ast.Var) ir.Operand {
l := vs.GetOrEmpty(k)
return ir.Operand{Value: l}
}
func (vs varstack) GetOp(k ast.Var) (ir.Operand, bool) {
l, ok := vs.Get(k)
if !ok {
return ir.Operand{}, false
}
return ir.Operand{Value: l}, true
}
func (vs varstack) Put(k ast.Var, v ir.Local) {
vs[len(vs)-1][k] = v
}
func (vs *varstack) Push(frame map[ast.Var]ir.Local) {
*vs = append(*vs, frame)
}
func (vs *varstack) Pop() map[ast.Var]ir.Local {
sl := *vs
last := sl[len(sl)-1]
*vs = sl[:len(sl)-1]
return last
}
@@ -0,0 +1,3 @@
AWS SDK for Go
Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Copyright 2014-2015 Stripe, Inc.
@@ -0,0 +1,30 @@
package crypto
import "errors"
// ConstantTimeByteCompare is a constant-time byte comparison of x and y. This function performs an absolute comparison
// if the two byte slices assuming they represent a big-endian number.
//
// error if len(x) != len(y)
// -1 if x < y
// 0 if x == y
// +1 if x > y
func ConstantTimeByteCompare(x, y []byte) (int, error) {
if len(x) != len(y) {
return 0, errors.New("slice lengths do not match")
}
xLarger, yLarger := 0, 0
for i := range x {
xByte, yByte := int(x[i]), int(y[i])
x := ((yByte - xByte) >> 8) & 1
y := ((xByte - yByte) >> 8) & 1
xLarger |= x &^ yLarger
yLarger |= y &^ xLarger
}
return xLarger - yLarger, nil
}
@@ -0,0 +1,165 @@
package crypto
import (
"bytes"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/hmac"
"encoding/asn1"
"encoding/binary"
"errors"
"fmt"
"hash"
"math"
"math/big"
)
type ecdsaSignature struct {
R, S *big.Int
}
// ECDSAKey takes the given elliptic curve, and private key (d) byte slice
// and returns the private ECDSA key.
func ECDSAKey(curve elliptic.Curve, d []byte) *ecdsa.PrivateKey {
return ECDSAKeyFromPoint(curve, (&big.Int{}).SetBytes(d))
}
// ECDSAKeyFromPoint takes the given elliptic curve and point and returns the
// private and public keypair
func ECDSAKeyFromPoint(curve elliptic.Curve, d *big.Int) *ecdsa.PrivateKey {
dBytes := make([]byte, (curve.Params().BitSize+7)/8)
d.FillBytes(dBytes)
privKey := &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: curve,
},
D: d,
}
var pubBytes []byte
switch curve {
case elliptic.P256():
if ecdhPriv, err := ecdh.P256().NewPrivateKey(dBytes); err == nil {
pubBytes = ecdhPriv.PublicKey().Bytes()
}
case elliptic.P384():
if ecdhPriv, err := ecdh.P384().NewPrivateKey(dBytes); err == nil {
pubBytes = ecdhPriv.PublicKey().Bytes()
}
case elliptic.P521():
if ecdhPriv, err := ecdh.P521().NewPrivateKey(dBytes); err == nil {
pubBytes = ecdhPriv.PublicKey().Bytes()
}
}
if len(pubBytes) > 0 {
byteLen := (curve.Params().BitSize + 7) / 8
privKey.X = new(big.Int).SetBytes(pubBytes[1 : 1+byteLen])
privKey.Y = new(big.Int).SetBytes(pubBytes[1+byteLen:])
} else {
panic(fmt.Sprintf("unsupported curve or invalid private key: %v", curve))
}
return privKey
}
// mathIntToBytes writes val as a big-endian, fixed-length byte slice into out,
// zero-padding on the left when val.Bytes() is shorter than out. This satisfies
// the uncompressed SEC 1 encoding (0x04 || X || Y) expected by crypto/ecdh's
// NewPublicKey: https://pkg.go.dev/crypto/ecdh#Curve.NewPublicKey
func mathIntToBytes(val *big.Int, out []byte) {
valBytes := val.Bytes()
copy(out[len(out)-len(valBytes):], valBytes)
}
// ECDSAPublicKey takes the provide curve and (x, y) coordinates and returns
// *ecdsa.PublicKey. Returns an error if the given points are not on the curve.
func ECDSAPublicKey(curve elliptic.Curve, x, y []byte) (*ecdsa.PublicKey, error) {
xPoint := (&big.Int{}).SetBytes(x)
yPoint := (&big.Int{}).SetBytes(y)
byteLen := (curve.Params().BitSize + 7) / 8
buf := make([]byte, 1+2*byteLen)
buf[0] = 4 // uncompressed point
mathIntToBytes(xPoint, buf[1:1+byteLen])
mathIntToBytes(yPoint, buf[1+byteLen:])
var err error
switch curve {
case elliptic.P256():
_, err = ecdh.P256().NewPublicKey(buf)
case elliptic.P384():
_, err = ecdh.P384().NewPublicKey(buf)
case elliptic.P521():
_, err = ecdh.P521().NewPublicKey(buf)
default:
err = fmt.Errorf("unsupported curve for ECDSA: %v", curve)
}
if err != nil {
return nil, fmt.Errorf("point(%v, %v) is not on the given curve", xPoint.String(), yPoint.String())
}
return &ecdsa.PublicKey{
Curve: curve,
X: xPoint,
Y: yPoint,
}, nil
}
// VerifySignature takes the provided public key, hash, and asn1 encoded signature and returns
// whether the given signature is valid.
func VerifySignature(key *ecdsa.PublicKey, hash []byte, signature []byte) (bool, error) {
var ecdsaSignature ecdsaSignature
_, err := asn1.Unmarshal(signature, &ecdsaSignature)
if err != nil {
return false, err
}
return ecdsa.Verify(key, hash, ecdsaSignature.R, ecdsaSignature.S), nil
}
// HMACKeyDerivation provides an implementation of a NIST-800-108 of a KDF (Key Derivation Function) in Counter Mode.
// For the purposes of this implantation HMAC is used as the PRF (Pseudorandom function), where the value of
// `r` is defined as a 4 byte counter.
func HMACKeyDerivation(hash func() hash.Hash, bitLen int, key []byte, label, context []byte) ([]byte, error) {
// verify that we won't overflow the counter
n := int64(math.Ceil((float64(bitLen) / 8) / float64(hash().Size())))
if n > 0x7FFFFFFF {
return nil, fmt.Errorf("unable to derive key of size %d using 32-bit counter", bitLen)
}
// verify the requested bit length is not larger then the length encoding size
if int64(bitLen) > 0x7FFFFFFF {
return nil, errors.New("bitLen is greater than 32-bits")
}
fixedInput := bytes.NewBuffer(nil)
fixedInput.Write(label)
fixedInput.WriteByte(0x00)
fixedInput.Write(context)
if err := binary.Write(fixedInput, binary.BigEndian, int32(bitLen)); err != nil {
return nil, fmt.Errorf("failed to write bit length to fixed input string: %v", err)
}
var output []byte
h := hmac.New(hash, key)
for i := int64(1); i <= n; i++ {
h.Reset()
if err := binary.Write(h, binary.BigEndian, int32(i)); err != nil {
return nil, err
}
_, err := h.Write(fixedInput.Bytes())
if err != nil {
return nil, err
}
output = append(output, h.Sum(nil)...)
}
return output[:bitLen/8], nil
}
@@ -0,0 +1,148 @@
package aws
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"net/http"
"strings"
"time"
"github.com/open-policy-agent/opa/internal/version"
"github.com/open-policy-agent/opa/v1/logging"
)
// Values taken from
// https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html
const (
ecrGetAuthorizationTokenTarget = "AmazonEC2ContainerRegistry_V20150921.GetAuthorizationToken"
ecrEndpointFmt = "https://ecr.%s.amazonaws.com/"
)
// ECR is used to request tokens from Elastic Container Registry.
type ECR struct {
// endpoint returns the region-specifc ECR endpoint.
// It can be overridden by tests.
endpoint func(region string) string
// client is used to send authorization tokens requests.
client *http.Client
logger logging.Logger
}
func NewECR(logger logging.Logger) *ECR {
return &ECR{
endpoint: func(region string) string {
return fmt.Sprintf(ecrEndpointFmt, region)
},
client: &http.Client{},
logger: logger,
}
}
// GetAuthorizationToken requests a token that can be used to authenticate image pull requests.
func (e *ECR) GetAuthorizationToken(ctx context.Context, creds Credentials, signatureVersion string) (ECRAuthorizationToken, error) {
endpoint := e.endpoint(creds.RegionName)
body := strings.NewReader("{}")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
if err != nil {
return ECRAuthorizationToken{}, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("X-Amz-Target", ecrGetAuthorizationTokenTarget)
req.Header.Set("Accept-Encoding", "identity")
req.Header.Set("Content-Type", "application/x-amz-json-1.1")
req.Header.Set("User-Agent", version.UserAgent)
e.logger.Debug("Signing ECR authorization token request")
if err := SignRequest(req, "ecr", creds, time.Now(), signatureVersion); err != nil {
return ECRAuthorizationToken{}, fmt.Errorf("failed to sign request: %w", err)
}
resp, err := DoRequestWithClient(req, e.client, "ecr get authorization token", e.logger)
if err != nil {
return ECRAuthorizationToken{}, err
}
var data struct {
AuthorizationData []struct {
AuthorizationToken string `json:"authorizationToken"`
ExpiresAt json.Number `json:"expiresAt"`
} `json:"authorizationData"`
}
if err := json.Unmarshal(resp, &data); err != nil {
return ECRAuthorizationToken{}, fmt.Errorf("failed to unmarshal response: %w", err)
}
if len(data.AuthorizationData) < 1 {
return ECRAuthorizationToken{}, errors.New("empty authorization data")
}
// The GetAuthorizationToken request returns a list of tokens for
// backwards compatibility reasons. We should only ever get one token back
// because we don't define any registryIDs in the request.
// See https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html#API_GetAuthorizationToken_ResponseSyntax
resultToken := data.AuthorizationData[0]
expiresAt, err := parseTimestamp(resultToken.ExpiresAt)
if err != nil {
return ECRAuthorizationToken{}, fmt.Errorf("failed to parse expiresAt: %w", err)
}
return ECRAuthorizationToken{
AuthorizationToken: resultToken.AuthorizationToken,
ExpiresAt: expiresAt,
}, nil
}
// ECRAuthorizationToken can sign requests to AWS ECR.
//
// It corresponds to data returned by the AWS GetAuthorizationToken API.
// See https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_AuthorizationData.html
type ECRAuthorizationToken struct {
AuthorizationToken string
ExpiresAt time.Time
}
// IsValid returns true if the token is set and not expired.
// It respects a margin of error for time handling and will mark it as expired early.
func (t *ECRAuthorizationToken) IsValid() bool {
const tokenExpirationMargin = 5 * time.Minute
expired := time.Now().Add(tokenExpirationMargin).After(t.ExpiresAt)
return t.AuthorizationToken != "" && !expired
}
var millisecondsFloat = new(big.Float).SetInt64(1e3)
// parseTimestamp parses the AWS format for timestamps.
// The time precision is in milliseconds.
//
// The logic is taken from
// https://github.com/aws/aws-sdk-go/blob/41717ba2c04d3fd03f94d09ea984a10899574935/private/protocol/json/jsonutil/unmarshal.go#L294-L302
func parseTimestamp(raw json.Number) (time.Time, error) {
s := raw.String()
float, ok := new(big.Float).SetString(s)
if !ok {
return time.Time{}, fmt.Errorf("not a float: %q", raw)
}
// The float is expected to be in second resolution with millisecond
// decimal places.
// Multiply by millisecondsFloat to obtain an integer in millisecond
// resolution
ms, _ := float.Mul(float, millisecondsFloat).Int64()
// Multiply again to obtain nanosecond resolution for time.Unix
ns := ms * 1e6
t := time.Unix(0, ns).UTC()
return t, nil
}
@@ -0,0 +1,106 @@
package aws
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/open-policy-agent/opa/internal/version"
"github.com/open-policy-agent/opa/v1/logging"
)
// Values taken from
// https://docs.aws.amazon.com/kms/latest/APIReference/Welcome.html
// https://docs.aws.amazon.com/general/latest/gr/kms.html
const (
kmsSignTarget = "TrentService.Sign"
kmsEndpointFmt = "https://kms.%s.amazonaws.com/"
)
// KMS is used to sign payloads using AWS Key Management Service.
type KMS struct {
// endpoint returns the region-specifc KMS endpoint.
// It can be overridden by tests.
endpoint func(region string) string
// client is used to send authorization tokens requests.
client *http.Client
logger logging.Logger
}
func NewKMS(logger logging.Logger) *KMS {
return &KMS{
endpoint: func(region string) string {
return fmt.Sprintf(kmsEndpointFmt, region)
},
client: &http.Client{},
logger: logger,
}
}
func NewKMSWithURLClient(url string, client *http.Client, logger logging.Logger) *KMS {
return &KMS{
endpoint: func(string) string { return url },
client: client,
logger: logger,
}
}
type KMSSignRequest struct {
KeyID string `json:"KeyId"`
Message string `json:"Message"`
MessageType string `json:"MessageType"`
SigningAlgorithm string `json:"SigningAlgorithm"`
}
type KMSSignResponse struct {
KeyID string `json:"KeyId"`
Signature string `json:"Signature"`
SigningAlgorithm string `json:"SigningAlgorithm"`
}
// SignDigest signs a digest using KMS.
func (k *KMS) SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string, creds Credentials, signatureVersion string) (string, error) {
endpoint := k.endpoint(creds.RegionName)
kmsRequest := KMSSignRequest{
KeyID: keyID,
Message: base64.StdEncoding.EncodeToString(digest),
MessageType: "DIGEST",
SigningAlgorithm: signingAlgorithm,
}
requestJSONBytes, err := json.Marshal(kmsRequest)
if err != nil {
return "", fmt.Errorf("failed to marshall request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(requestJSONBytes))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("X-Amz-Target", kmsSignTarget)
req.Header.Set("Accept-Encoding", "identity")
req.Header.Set("Content-Type", "application/x-amz-json-1.1")
req.Header.Set("User-Agent", version.UserAgent)
if err := SignRequest(req, "kms", creds, time.Now(), signatureVersion); err != nil {
return "", fmt.Errorf("failed to sign request: %w", err)
}
resp, err := DoRequestWithClient(req, k.client, "kms sign digest", k.logger)
if err != nil {
return "", err
}
var data KMSSignResponse
if err := json.Unmarshal(resp, &data); err != nil {
return "", fmt.Errorf("failed to unmarshal response: %w", err)
}
return data.Signature, nil
}
@@ -0,0 +1,206 @@
// Copyright 2022 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package aws
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
v4 "github.com/open-policy-agent/opa/internal/providers/aws/v4"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/util"
)
func stringFromTerm(t *ast.Term) string {
if v, ok := t.Value.(ast.String); ok {
return string(v)
}
return ""
}
// Headers that may be mutated before reaching an aws service (eg by a proxy) should be added here to omit them from
// the sigv4 canonical request
// ref. https://github.com/aws/aws-sdk-go/blob/master/aws/signer/v4/v4.go#L92
var awsSigv4IgnoredHeaders = map[string]struct{}{
"authorization": {},
"user-agent": {},
"x-amzn-trace-id": {},
}
type Credentials struct {
AccessKey string
SecretKey string
RegionName string
SessionToken string
}
func CredentialsFromObject(v ast.Object) Credentials {
var creds Credentials
awsAccessKey := v.Get(ast.StringTerm("aws_access_key"))
awsSecretKey := v.Get(ast.StringTerm("aws_secret_access_key"))
awsRegion := v.Get(ast.StringTerm("aws_region"))
awsSessionToken := v.Get(ast.StringTerm("aws_session_token"))
creds.AccessKey = stringFromTerm(awsAccessKey)
creds.SecretKey = stringFromTerm(awsSecretKey)
creds.RegionName = stringFromTerm(awsRegion)
if awsSessionToken != nil {
creds.SessionToken = stringFromTerm(awsSessionToken)
}
return creds
}
func sha256MAC(message string, key []byte) []byte {
mac := hmac.New(sha256.New, key)
mac.Write([]byte(message))
return mac.Sum(nil)
}
// SignRequest modifies an http.Request to include an AWS V4 signature based on the provided credentials.
func SignRequest(req *http.Request, service string, creds Credentials, theTime time.Time, sigVersion string) error {
// General ref. https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
// S3 ref. https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html
// APIGateway ref. https://docs.aws.amazon.com/apigateway/api-reference/signing-requests/
var body []byte
if req.Body == nil {
body = []byte("")
} else {
var err error
body, err = io.ReadAll(req.Body)
if err != nil {
return errors.New("error getting request body: " + err.Error())
}
// Since ReadAll consumed the body ReadCloser, we must create a new ReadCloser for the request so that the
// subsequent read starts from the beginning
req.Body = io.NopCloser(bytes.NewReader(body))
}
now := theTime.UTC()
if sigVersion == "4a" {
signedHeaders := SignV4a(req.Header, req.Method, req.URL, body, service, creds, now)
req.Header = signedHeaders
} else {
authHeader, awsHeaders := SignV4(req.Header, req.Method, req.URL, body, service, creds, now, false)
req.Header.Set("Authorization", authHeader)
for k, v := range awsHeaders {
req.Header.Add(k, v)
}
}
return nil
}
// SignV4 modifies a map[string][]string of headers to generate an AWS V4 signature + headers based on the config/credentials provided.
func SignV4(headers map[string][]string, method string, theURL *url.URL, body []byte, service string,
awsCreds Credentials, theTime time.Time, disablePayloadSigning bool) (string, map[string]string) {
// General ref. https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
// S3 ref. https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html
// APIGateway ref. https://docs.aws.amazon.com/apigateway/api-reference/signing-requests/
now := theTime.UTC()
contentSha256 := getContentHash(disablePayloadSigning, body)
// V4 signing has specific ideas of how it wants to see dates/times encoded
dateNow := now.Format("20060102")
iso8601Now := now.Format("20060102T150405Z")
awsHeaders := map[string]string{
"host": theURL.Host,
"x-amz-date": iso8601Now,
}
// s3 and glacier require the extra x-amz-content-sha256 header. other services do not.
if service == "s3" || service == "glacier" {
awsHeaders[amzContentSha256Key] = contentSha256
}
// the security token header is necessary for ephemeral credentials, e.g. from
// the EC2 metadata service
if awsCreds.SessionToken != "" {
awsHeaders["x-amz-security-token"] = awsCreds.SessionToken
}
headersToSign := map[string][]string{}
// sign all of the aws headers.
for k, v := range awsHeaders {
headersToSign[k] = []string{v}
}
// sign all of the request's headers, except for those in the ignore list
for k, v := range headers {
lowercaseHeader := strings.ToLower(k)
if _, ok := awsSigv4IgnoredHeaders[lowercaseHeader]; !ok {
headersToSign[lowercaseHeader] = v
}
}
// the "canonical request" is the normalized version of the AWS service access
// that we're attempting to perform
canonicalReq := method + "\n" // HTTP method
canonicalReq += theURL.EscapedPath() + "\n" // URI-escaped path
canonicalReq += theURL.RawQuery + "\n" // RAW Query String
// include the values for the signed headers
orderedKeys := util.KeysSorted(headersToSign)
for _, k := range orderedKeys {
// TODO: fix later
//nolint:perfsprint
canonicalReq += k + ":" + strings.Join(headersToSign[k], ",") + "\n"
}
canonicalReq += "\n" // linefeed to terminate headers
// include the list of the signed headers
headerList := strings.Join(orderedKeys, ";")
canonicalReq += headerList + "\n"
canonicalReq += contentSha256
// the "string to sign" is a time-bounded, scoped request token which
// is linked to the "canonical request" by inclusion of its SHA-256 hash
strToSign := "AWS4-HMAC-SHA256\n" // V4 signing with SHA-256 HMAC
strToSign += iso8601Now + "\n" // ISO 8601 time
strToSign += dateNow + "/" + awsCreds.RegionName + "/" + service + "/aws4_request\n" // scoping for signature
strToSign += fmt.Sprintf("%x", sha256.Sum256([]byte(canonicalReq))) // SHA-256 of canonical request
// the "signing key" is generated by repeated HMAC-SHA256 based on the same
// scoping that's included in the "string to sign"; but including the secret key
// to allow AWS to validate it
signingKey := sha256MAC(dateNow, []byte("AWS4"+awsCreds.SecretKey))
signingKey = sha256MAC(awsCreds.RegionName, signingKey)
signingKey = sha256MAC(service, signingKey)
signingKey = sha256MAC("aws4_request", signingKey)
// the "signature" is finally the "string to sign" signed by the "signing key"
signature := sha256MAC(strToSign, signingKey)
// required format of Authorization header; n.b. the access key corresponding to
// the secret key is included here
authHeader := "AWS4-HMAC-SHA256 Credential=" + awsCreds.AccessKey + "/" + dateNow
authHeader += "/" + awsCreds.RegionName + "/" + service + "/aws4_request,"
authHeader += "SignedHeaders=" + headerList + ","
authHeader += "Signature=" + hex.EncodeToString(signature)
return authHeader, awsHeaders
}
// getContentHash returns UNSIGNED-PAYLOAD if payload signing is disabled else will compute sha256 from body
func getContentHash(disablePayloadSigning bool, body []byte) string {
if disablePayloadSigning {
return v4.UnsignedPayload
}
return fmt.Sprintf("%x", sha256.Sum256(body))
}
@@ -0,0 +1,422 @@
// modified from github.com/aws/aws-sdk-go-v2/internal/v4a@7a32d707af
package aws
import (
"bytes"
"crypto"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"hash"
"io"
"math/big"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
signerCrypto "github.com/open-policy-agent/opa/internal/providers/aws/crypto"
v4Internal "github.com/open-policy-agent/opa/internal/providers/aws/v4"
)
const (
// AmzRegionSetKey represents the region set header used for sigv4a
AmzRegionSetKey = "X-Amz-Region-Set"
amzSecurityTokenKey = v4Internal.AmzSecurityTokenKey
amzDateKey = v4Internal.AmzDateKey
authorizationHeader = "Authorization"
amzContentSha256Key = "x-amz-content-sha256"
signingAlgorithm = "AWS4-ECDSA-P256-SHA256"
timeFormat = "20060102T150405Z"
shortTimeFormat = "20060102"
)
var (
p256 elliptic.Curve
nMinusTwoP256 *big.Int
one = new(big.Int).SetInt64(1)
cache = credsCache{}
randomSource = rand.Reader
)
func init() {
// Ensure the elliptic curve parameters are initialized on package import rather then on first usage
p256 = elliptic.P256()
nMinusTwoP256 = new(big.Int).SetBytes(p256.Params().N.Bytes())
nMinusTwoP256 = nMinusTwoP256.Sub(nMinusTwoP256, new(big.Int).SetInt64(2))
}
type credsCache struct {
asymmetric atomic.Value
m sync.Mutex
}
// SetRandomSource used for testing to override rand so tests can expect stable output
func SetRandomSource(reader io.Reader) {
randomSource = reader
}
// deriveKeyFromAccessKeyPair derives a NIST P-256 PrivateKey from the given
// IAM AccessKey and SecretKey pair.
//
// Based on FIPS.186-4 Appendix B.4.2
func deriveKeyFromAccessKeyPair(accessKey, secretKey string) (*ecdsa.PrivateKey, error) {
params := p256.Params()
bitLen := params.BitSize // Testing random candidates does not require an additional 64 bits
counter := 0x01
buffer := make([]byte, 1+len(accessKey)) // 1 byte counter + len(accessKey)
kdfContext := bytes.NewBuffer(buffer)
inputKey := append([]byte("AWS4A"), []byte(secretKey)...)
d := new(big.Int)
for {
kdfContext.Reset()
kdfContext.WriteString(accessKey)
kdfContext.WriteByte(byte(counter))
key, err := signerCrypto.HMACKeyDerivation(sha256.New, bitLen, inputKey, []byte(signingAlgorithm), kdfContext.Bytes())
if err != nil {
return nil, err
}
// Check key first before calling SetBytes if key is in fact a valid candidate.
// This ensures the byte slice is the correct length (32-bytes) to compare in constant-time
cmp, err := signerCrypto.ConstantTimeByteCompare(key, nMinusTwoP256.Bytes())
if err != nil {
return nil, err
}
if cmp == -1 {
d.SetBytes(key)
break
}
counter++
if counter > 0xFF {
return nil, errors.New("exhausted single byte external counter")
}
}
d = d.Add(d, one)
priv := new(ecdsa.PrivateKey)
priv.PublicKey.Curve = p256
priv.D = d
dBytes := make([]byte, 32)
d.FillBytes(dBytes)
ecdhPriv, err := ecdh.P256().NewPrivateKey(dBytes)
if err != nil {
return nil, err
}
pubBytes := ecdhPriv.PublicKey().Bytes()
priv.PublicKey.X = new(big.Int).SetBytes(pubBytes[1:33])
priv.PublicKey.Y = new(big.Int).SetBytes(pubBytes[33:])
return priv, nil
}
// v4aCredentials is Context, ECDSA, and Optional Session Token that can be used
// to sign requests using SigV4a
type v4aCredentials struct {
Context string
PrivateKey *ecdsa.PrivateKey
SessionToken string
}
// retrievePrivateKey returns credentials suitable for SigV4a signing
func retrievePrivateKey(symmetric Credentials) (v4aCredentials, error) {
cache.m.Lock()
defer cache.m.Unlock()
// try to get creds from cache
v := cache.asymmetric.Load()
if v != nil {
c := v.(*v4aCredentials)
// if the cached Context matches the symmetric AccessKey ID, then use cached value. Otherwise, creds have
// changed and we need to derive new asymmetric creds
if c != nil && c.Context == symmetric.AccessKey {
return *c, nil
}
}
privateKey, err := deriveKeyFromAccessKeyPair(symmetric.AccessKey, symmetric.SecretKey)
if err != nil {
return v4aCredentials{}, errors.New("failed to derive asymmetric key from credentials")
}
creds := v4aCredentials{
Context: symmetric.AccessKey,
PrivateKey: privateKey,
SessionToken: symmetric.SessionToken,
}
// cache derived asymmetric creds so we don't derive new ones until symmetric creds change
cache.asymmetric.Store(&creds)
return creds, nil
}
type httpSigner struct {
Request *http.Request
ServiceName string
RegionSet []string
Time time.Time
Credentials v4aCredentials
// PayloadHash is the hex encoded SHA-256 hash of the request payload
// If len(PayloadHash) == 0 the signer will attempt to send the request
// as an unsigned payload. Note: Unsigned payloads only work for a subset of services.
PayloadHash string
}
func (s *httpSigner) setRequiredSigningFields(headers http.Header, _ url.Values) {
amzDate := s.Time.Format(timeFormat)
headers.Set(AmzRegionSetKey, strings.Join(s.RegionSet, ","))
headers.Set(amzDateKey, amzDate)
if len(s.Credentials.SessionToken) > 0 {
headers.Set(amzSecurityTokenKey, s.Credentials.SessionToken)
}
}
// Build modifies the Request attribute of the httpSigner, adding an Authorization header
func (s *httpSigner) Build() (signedRequest, error) {
req := s.Request
query := req.URL.Query()
headers := req.Header
// seemingly required by S3/MRAP -- 403 Forbidden otherwise
headers.Set("host", req.URL.Host)
headers.Set(amzContentSha256Key, s.PayloadHash)
s.setRequiredSigningFields(headers, query)
// Sort Each Query Key's Values
for key := range query {
sort.Strings(query[key])
}
v4Internal.SanitizeHostForHeader(req)
credentialScope := s.buildCredentialScope()
credentialStr := s.Credentials.Context + "/" + credentialScope
unsignedHeaders := headers
host := req.URL.Host
if len(req.Host) > 0 {
host = req.Host
}
signedHeaders, signedHeadersStr, canonicalHeaderStr := s.buildCanonicalHeaders(host, v4Internal.IgnoredHeaders, unsignedHeaders, s.Request.ContentLength)
rawQuery := strings.ReplaceAll(query.Encode(), "+", "%20")
canonicalURI := v4Internal.GetURIPath(req.URL)
canonicalString := s.buildCanonicalString(
req.Method,
canonicalURI,
rawQuery,
signedHeadersStr,
canonicalHeaderStr,
)
strToSign := s.buildStringToSign(credentialScope, canonicalString)
signingSignature, err := s.buildSignature(strToSign)
if err != nil {
return signedRequest{}, err
}
headers[authorizationHeader] = append(headers[authorizationHeader][:0], buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature))
req.URL.RawQuery = rawQuery
return signedRequest{
Request: req,
SignedHeaders: signedHeaders,
CanonicalString: canonicalString,
StringToSign: strToSign,
}, nil
}
func (s *httpSigner) buildCredentialScope() string {
return strings.Join([]string{
s.Time.Format(shortTimeFormat),
s.ServiceName,
"aws4_request",
}, "/")
}
func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature string) string {
const credential = "Credential="
const signedHeaders = "SignedHeaders="
const signature = "Signature="
const commaSpace = ", "
var parts strings.Builder
parts.Grow(len(signingAlgorithm) + 1 +
len(credential) + len(credentialStr) + len(commaSpace) +
len(signedHeaders) + len(signedHeadersStr) + len(commaSpace) +
len(signature) + len(signingSignature),
)
parts.WriteString(signingAlgorithm)
parts.WriteRune(' ')
parts.WriteString(credential)
parts.WriteString(credentialStr)
parts.WriteString(commaSpace)
parts.WriteString(signedHeaders)
parts.WriteString(signedHeadersStr)
parts.WriteString(commaSpace)
parts.WriteString(signature)
parts.WriteString(signingSignature)
return parts.String()
}
func (*httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, header http.Header, length int64) (signed http.Header, signedHeaders, canonicalHeadersStr string) {
signed = make(http.Header)
const hostHeader = "host"
headers := make([]string, 0)
if length > 0 {
const contentLengthHeader = "content-length"
headers = append(headers, contentLengthHeader)
signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10))
}
for k, v := range header {
if !rule.IsValid(k) {
continue // ignored header
}
lowerCaseKey := strings.ToLower(k)
if _, ok := signed[lowerCaseKey]; ok {
// include additional values
signed[lowerCaseKey] = append(signed[lowerCaseKey], v...)
continue
}
headers = append(headers, lowerCaseKey)
signed[lowerCaseKey] = v
}
sort.Strings(headers)
signedHeaders = strings.Join(headers, ";")
var canonicalHeaders strings.Builder
n := len(headers)
const colon = ':'
for i := range n {
if headers[i] == hostHeader {
canonicalHeaders.WriteString(hostHeader)
canonicalHeaders.WriteRune(colon)
canonicalHeaders.WriteString(v4Internal.StripExcessSpaces(host))
} else {
canonicalHeaders.WriteString(headers[i])
canonicalHeaders.WriteRune(colon)
// Trim out leading, trailing, and dedup inner spaces from signed header values.
values := signed[headers[i]]
for j, v := range values {
cleanedValue := strings.TrimSpace(v4Internal.StripExcessSpaces(v))
canonicalHeaders.WriteString(cleanedValue)
if j < len(values)-1 {
canonicalHeaders.WriteRune(',')
}
}
}
canonicalHeaders.WriteRune('\n')
}
canonicalHeadersStr = canonicalHeaders.String()
return signed, signedHeaders, canonicalHeadersStr
}
func (s *httpSigner) buildCanonicalString(method, uri, query, signedHeaders, canonicalHeaders string) string {
return strings.Join([]string{
method,
uri,
query,
canonicalHeaders,
signedHeaders,
s.PayloadHash,
}, "\n")
}
func (s *httpSigner) buildStringToSign(credentialScope, canonicalRequestString string) string {
return strings.Join([]string{
signingAlgorithm,
s.Time.Format(timeFormat),
credentialScope,
hex.EncodeToString(makeHash(sha256.New(), []byte(canonicalRequestString))),
}, "\n")
}
func makeHash(hash hash.Hash, b []byte) []byte {
hash.Reset()
hash.Write(b)
return hash.Sum(nil)
}
func (s *httpSigner) buildSignature(strToSign string) (string, error) {
sig, err := s.Credentials.PrivateKey.Sign(randomSource, makeHash(sha256.New(), []byte(strToSign)), crypto.SHA256)
if err != nil {
return "", err
}
return hex.EncodeToString(sig), nil
}
type signedRequest struct {
Request *http.Request
SignedHeaders http.Header
CanonicalString string
StringToSign string
}
// SignV4a returns a map[string][]string of headers, including an added AWS V4a signature based on the config/credentials provided.
func SignV4a(headers map[string][]string, method string, theURL *url.URL, body []byte, service string, awsCreds Credentials, theTime time.Time) map[string][]string {
contentSha256 := getContentHash(false, body)
key, err := retrievePrivateKey(awsCreds)
if err != nil {
return map[string][]string{}
}
bodyReader := bytes.NewReader(body)
req, _ := http.NewRequest(method, theURL.String(), bodyReader)
req.Header = headers
signer := &httpSigner{
Request: req,
PayloadHash: contentSha256,
ServiceName: service,
RegionSet: []string{"*"},
Credentials: key,
Time: theTime,
}
_, err = signer.Build()
if err != nil {
return map[string][]string{}
}
return req.Header
}
@@ -0,0 +1,39 @@
package aws
import (
"errors"
"io"
"net/http"
"github.com/open-policy-agent/opa/v1/logging"
)
// DoRequestWithClient is a convenience function to get the body of an HTTP response with
// appropriate error-handling boilerplate and logging.
func DoRequestWithClient(req *http.Request, client *http.Client, desc string, logger logging.Logger) ([]byte, error) {
resp, err := client.Do(req)
if err != nil {
// some kind of catastrophe talking to the service
return nil, errors.New(desc + " HTTP request failed: " + err.Error())
}
defer resp.Body.Close()
logger.WithFields(map[string]any{
"url": req.URL.String(),
"status": resp.Status,
"headers": resp.Header,
}).Debug("Received response from " + desc + " service.")
body, err := io.ReadAll(resp.Body)
if err != nil {
// deal with problems reading the body, whatever those might be
return nil, errors.New(desc + " HTTP response body could not be read: " + err.Error())
}
if resp.StatusCode != 200 {
logger.Debug("Error response with response body: %s", body)
// could be 404 for role that's not available, but cover all the bases
return nil, errors.New(desc + " HTTP request returned unexpected status: " + resp.Status)
}
return body, nil
}
@@ -0,0 +1,36 @@
package v4
const (
// EmptyStringSHA256 is the hex encoded sha256 value of an empty string
EmptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
// UnsignedPayload indicates that the request payload body is unsigned
UnsignedPayload = "UNSIGNED-PAYLOAD"
// AmzAlgorithmKey indicates the signing algorithm
AmzAlgorithmKey = "X-Amz-Algorithm"
// AmzSecurityTokenKey indicates the security token to be used with temporary credentials
AmzSecurityTokenKey = "X-Amz-Security-Token"
// AmzDateKey is the UTC timestamp for the request in the format YYYYMMDD'T'HHMMSS'Z'
AmzDateKey = "X-Amz-Date"
// AmzCredentialKey is the access key ID and credential scope
AmzCredentialKey = "X-Amz-Credential"
// AmzSignedHeadersKey is the set of headers signed for the request
AmzSignedHeadersKey = "X-Amz-SignedHeaders"
// AmzSignatureKey is the query parameter to store the SigV4 signature
AmzSignatureKey = "X-Amz-Signature"
// TimeFormat is the time format to be used in the X-Amz-Date header or query parameter
TimeFormat = "20060102T150405Z"
// ShortTimeFormat is the shorten time format used in the credential scope
ShortTimeFormat = "20060102"
// ContentSHAKey is the SHA256 of request body
ContentSHAKey = "X-Amz-Content-Sha256"
)
@@ -0,0 +1,87 @@
package v4
import (
"strings"
)
// Rules houses a set of Rule needed for validation of a
// string value
type Rules []Rule
// Rule interface allows for more flexible rules and just simply
// checks whether or not a value adheres to that Rule
type Rule interface {
IsValid(value string) bool
}
// IsValid will iterate through all rules and see if any rules
// apply to the value and supports nested rules
func (r Rules) IsValid(value string) bool {
for _, rule := range r {
if rule.IsValid(value) {
return true
}
}
return false
}
// MapRule generic Rule for maps
type MapRule map[string]struct{}
// IsValid for the map Rule satisfies whether it exists in the map
func (m MapRule) IsValid(value string) bool {
_, ok := m[value]
return ok
}
// AllowList is a generic Rule for whitelisting
type AllowList struct {
Rule
}
// IsValid for AllowList checks if the value is within the AllowList
func (w AllowList) IsValid(value string) bool {
return w.Rule.IsValid(value)
}
// DenyList is a generic Rule for blacklisting
type DenyList struct {
Rule
}
// IsValid for AllowList checks if the value is within the AllowList
func (b DenyList) IsValid(value string) bool {
return !b.Rule.IsValid(value)
}
// Patterns is a list of strings to match against
type Patterns []string
// FORK: copied from aws-sdk-go-v2/internal/strings
func hasPrefixFold(s, prefix string) bool {
return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix)
}
// IsValid for Patterns checks each pattern and returns if a match has
// been found
func (p Patterns) IsValid(value string) bool {
for _, pattern := range p {
if hasPrefixFold(value, pattern) {
return true
}
}
return false
}
// InclusiveRules rules allow for rules to depend on one another
type InclusiveRules []Rule
// IsValid will return true if all rules are true
func (r InclusiveRules) IsValid(value string) bool {
for _, rule := range r {
if !rule.IsValid(value) {
return false
}
}
return true
}
@@ -0,0 +1,67 @@
package v4
// IgnoredHeaders is a list of headers that are ignored during signing
var IgnoredHeaders = Rules{
DenyList{
MapRule{
"Authorization": struct{}{},
"User-Agent": struct{}{},
"X-Amzn-Trace-Id": struct{}{},
},
},
}
// RequiredSignedHeaders is a whitelist for Build canonical headers.
var RequiredSignedHeaders = Rules{
AllowList{
MapRule{
"Cache-Control": struct{}{},
"Content-Disposition": struct{}{},
"Content-Encoding": struct{}{},
"Content-Language": struct{}{},
"Content-Md5": struct{}{},
"Content-Type": struct{}{},
"Expires": struct{}{},
"If-Match": struct{}{},
"If-Modified-Since": struct{}{},
"If-None-Match": struct{}{},
"If-Unmodified-Since": struct{}{},
"Range": struct{}{},
"X-Amz-Acl": struct{}{},
"X-Amz-Copy-Source": struct{}{},
"X-Amz-Copy-Source-If-Match": struct{}{},
"X-Amz-Copy-Source-If-Modified-Since": struct{}{},
"X-Amz-Copy-Source-If-None-Match": struct{}{},
"X-Amz-Copy-Source-If-Unmodified-Since": struct{}{},
"X-Amz-Copy-Source-Range": struct{}{},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
"X-Amz-Grant-Full-control": struct{}{},
"X-Amz-Grant-Read": struct{}{},
"X-Amz-Grant-Read-Acp": struct{}{},
"X-Amz-Grant-Write": struct{}{},
"X-Amz-Grant-Write-Acp": struct{}{},
"X-Amz-Metadata-Directive": struct{}{},
"X-Amz-Mfa": struct{}{},
"X-Amz-Request-Payer": struct{}{},
"X-Amz-Server-Side-Encryption": struct{}{},
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{},
"X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{},
"X-Amz-Server-Side-Encryption-Customer-Key": struct{}{},
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
"X-Amz-Storage-Class": struct{}{},
"X-Amz-Website-Redirect-Location": struct{}{},
"X-Amz-Content-Sha256": struct{}{},
"X-Amz-Tagging": struct{}{},
},
},
Patterns{"X-Amz-Meta-"},
}
// AllowedQueryHoisting is a whitelist for Build query headers. The boolean value
// represents whether or not it is a pattern.
var AllowedQueryHoisting = InclusiveRules{
DenyList{RequiredSignedHeaders},
Patterns{"X-Amz-"},
}
@@ -0,0 +1,75 @@
package v4
import (
"net/http"
"strings"
)
// SanitizeHostForHeader removes default port from host and updates request.Host
func SanitizeHostForHeader(r *http.Request) {
host := getHost(r)
port := portOnly(host)
if port != "" && isDefaultPort(r.URL.Scheme, port) {
r.Host = stripPort(host)
}
}
// Returns host from request
func getHost(r *http.Request) string {
if r.Host != "" {
return r.Host
}
return r.URL.Host
}
// Hostname returns u.Host, without any port number.
//
// If Host is an IPv6 literal with a port number, Hostname returns the
// IPv6 literal without the square brackets. IPv6 literals may include
// a zone identifier.
//
// Copied from the Go 1.8 standard library (net/url)
func stripPort(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return hostport
}
if i := strings.IndexByte(hostport, ']'); i != -1 {
return strings.TrimPrefix(hostport[:i], "[")
}
return hostport[:colon]
}
// Port returns the port part of u.Host, without the leading colon.
// If u.Host doesn't contain a port, Port returns an empty string.
//
// Copied from the Go 1.8 standard library (net/url)
func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return ""
}
if i := strings.Index(hostport, "]:"); i != -1 {
return hostport[i+len("]:"):]
}
if strings.Contains(hostport, "]") {
return ""
}
return hostport[colon+len(":"):]
}
// Returns true if the specified URI is using the standard port
// (i.e. port 80 for HTTP URIs or 443 for HTTPS URIs)
func isDefaultPort(scheme, port string) bool {
if port == "" {
return true
}
lowerCaseScheme := strings.ToLower(scheme)
if (lowerCaseScheme == "http" && port == "80") || (lowerCaseScheme == "https" && port == "443") {
return true
}
return false
}
@@ -0,0 +1,59 @@
package v4
import (
"net/url"
"strings"
)
const doubleSpace = " "
// StripExcessSpaces will rewrite the passed in slice's string values to not
// contain multiple side-by-side spaces.
func StripExcessSpaces(str string) string {
var j, k, l, m, spaces int
// Trim leading and trailing spaces
str = strings.Trim(str, " ")
// Strip multiple spaces.
j = strings.Index(str, doubleSpace)
if j < 0 {
return str
}
buf := []byte(str)
for k, m, l = j, j, len(buf); k < l; k++ {
if buf[k] == ' ' {
if spaces == 0 {
// First space.
buf[m] = buf[k]
m++
}
spaces++
} else {
// End of multiple spaces.
spaces = 0
buf[m] = buf[k]
m++
}
}
return string(buf[:m])
}
// GetURIPath returns the escaped URI component from the provided URL
func GetURIPath(u *url.URL) string {
var uri string
if len(u.Opaque) > 0 {
uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/")
} else {
uri = u.EscapedPath()
}
if len(uri) == 0 {
uri = "/"
}
return uri
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2020 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package ref implements internal helpers for references
package ref
import (
"errors"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/storage"
"github.com/open-policy-agent/opa/v1/util"
)
// ParseDataPath returns a ref from the slash separated path s rooted at data.
// All path segments are treated as identifier strings.
func ParseDataPath(s string) (ast.Ref, error) {
path, ok := storage.ParsePath(util.WithPrefix(s, "/"))
if !ok {
return nil, errors.New("invalid path")
}
return path.Ref(ast.DefaultRootDocument), nil
}
// ArrayPath will take an ast.Array and build an ast.Ref using the ast.Terms in the Array
func ArrayPath(a *ast.Array) ast.Ref {
ref := make(ast.Ref, 0, a.Len())
a.Foreach(func(term *ast.Term) {
ref = append(ref, term)
})
return ref
}
@@ -0,0 +1,65 @@
// Copyright 2021 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package opa
import (
"context"
)
// ErrEngineNotFound is returned by LookupEngine if no wasm engine was
// registered by that name.
var ErrEngineNotFound error = &errEngineNotFound{}
type errEngineNotFound struct{}
func (*errEngineNotFound) Error() string { return "engine not found" }
func (*errEngineNotFound) Lines() []string {
return []string{
`WebAssembly runtime not supported in this build.`,
`----------------------------------------------------------------------------------`,
`Please download an OPA binary with Wasm enabled from`,
`https://www.openpolicyagent.org/docs/latest/#running-opa`,
`or build it yourself (with Wasm enabled).`,
`----------------------------------------------------------------------------------`,
}
}
// Engine repesents a factory for instances of EvalEngine implementations
type Engine interface {
New() EvalEngine
}
// EvalEngine is the interface implemented by an engine used to eval a policy
type EvalEngine interface {
Init() (EvalEngine, error)
Entrypoints(context.Context) (map[string]int32, error)
WithPolicyBytes([]byte) EvalEngine
WithDataJSON(any) EvalEngine
Eval(context.Context, EvalOpts) (*Result, error)
SetData(context.Context, any) error
SetDataPath(context.Context, []string, any) error
RemoveDataPath(context.Context, []string) error
Close()
}
var engines = map[string]Engine{}
// RegisterEngine registers an evaluation engine by its target name.
// Note that the "rego" target is always available.
func RegisterEngine(name string, e Engine) {
if engines[name] != nil {
panic("duplicate engine registration")
}
engines[name] = e
}
// LookupEngine allows retrieving an engine registered by name
func LookupEngine(name string) (Engine, error) {
e, ok := engines[name]
if !ok {
return nil, ErrEngineNotFound
}
return e, nil
}
@@ -0,0 +1,31 @@
package opa
import (
"io"
"time"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/metrics"
"github.com/open-policy-agent/opa/v1/topdown/builtins"
"github.com/open-policy-agent/opa/v1/topdown/cache"
"github.com/open-policy-agent/opa/v1/topdown/print"
)
// Result holds the evaluation result.
type Result struct {
Result []byte
}
// EvalOpts define options for performing an evaluation.
type EvalOpts struct {
Input *any
Metrics metrics.Metrics
Entrypoint int32
Time time.Time
Seed io.Reader
InterQueryBuiltinCache cache.InterQueryCache
InterQueryBuiltinValueCache cache.InterQueryValueCache
NDBuiltinCache builtins.NDBCache
PrintHook print.Hook
Capabilities *ast.Capabilities
}
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
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 [yyyy] [name of copyright owner]
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.
@@ -0,0 +1,247 @@
// Copyright 2013-2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Semantic Versions http://semver.org
// This file was originally vendored from:
// https://github.com/coreos/go-semver/tree/e214231b295a8ea9479f11b70b35d5acf3556d9b/semver
// There isn't a single line left from the original source today, but being generous about
// attribution won't hurt.
package semver
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/open-policy-agent/opa/v1/util"
)
// reMetaIdentifier matches pre-release and metadata identifiers against the spec requirements
var reMetaIdentifier = regexp.MustCompile(`^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$`)
// Version represents a parsed SemVer
type Version struct {
Major int64
Minor int64
Patch int64
PreRelease string `json:"PreRelease,omitempty"`
Metadata string `json:"Metadata,omitempty"`
}
// Parse constructs new semver Version from version string.
func Parse(version string) (v Version, err error) {
version = strings.TrimPrefix(version, "v")
version, v.Metadata = cut(version, '+')
if v.Metadata != "" && !reMetaIdentifier.MatchString(v.Metadata) {
return v, fmt.Errorf("invalid metadata identifier: %s", v.Metadata)
}
version, v.PreRelease = cut(version, '-')
if v.PreRelease != "" && !reMetaIdentifier.MatchString(v.PreRelease) {
return v, fmt.Errorf("invalid pre-release identifier: %s", v.PreRelease)
}
if strings.Count(version, ".") != 2 {
return v, fmt.Errorf("%s should contain major, minor, and patch versions", version)
}
major, after := cut(version, '.')
if v.Major, err = strconv.ParseInt(major, 10, 64); err != nil {
return v, err
}
minor, after := cut(after, '.')
if v.Minor, err = strconv.ParseInt(minor, 10, 64); err != nil {
return v, err
}
if v.Patch, err = strconv.ParseInt(after, 10, 64); err != nil {
return v, err
}
return v, nil
}
// MustParse is like Parse but panics if the version string is invalid instead of returning an error.
func MustParse(version string) Version {
v, err := Parse(version)
if err != nil {
panic(err)
}
return v
}
// Compare compares two semver strings.
func Compare(a, b string) int {
aV, err := Parse(a)
if err != nil {
return -1
}
bV, err := Parse(b)
if err != nil {
return 1
}
return aV.Compare(bV)
}
// AppendText appends the textual representation of the version to b and returns the extended buffer.
// This method conforms to the encoding.TextAppender interface, and is useful for serializing the Version
// without allocating, provided the caller has pre-allocated sufficient space in b.
func (v Version) AppendText(b []byte) ([]byte, error) {
if b == nil {
b = make([]byte, 0, length(v))
}
b = append(strconv.AppendInt(b, v.Major, 10), '.')
b = append(strconv.AppendInt(b, v.Minor, 10), '.')
b = strconv.AppendInt(b, v.Patch, 10)
if v.PreRelease != "" {
b = append(append(b, '-'), v.PreRelease...)
}
if v.Metadata != "" {
b = append(append(b, '+'), v.Metadata...)
}
return b, nil
}
// String returns the string representation of the version.
func (v Version) String() string {
bs := make([]byte, 0, length(v))
bs, _ = v.AppendText(bs)
return string(bs)
}
// Compare tests if v is less than, equal to, or greater than other, returning -1, 0, or +1 respectively.
// Comparison is based on the SemVer specification (https://semver.org/#spec-item-11).
func (v Version) Compare(other Version) int {
if v.Major > other.Major {
return 1
} else if v.Major < other.Major {
return -1
}
if v.Minor > other.Minor {
return 1
} else if v.Minor < other.Minor {
return -1
}
if v.Patch > other.Patch {
return 1
} else if v.Patch < other.Patch {
return -1
}
if v.PreRelease == other.PreRelease {
return 0
}
// if two versions are otherwise equal it is the one without a pre-release that is greater
if v.PreRelease == "" && other.PreRelease != "" {
return 1
}
if other.PreRelease == "" && v.PreRelease != "" {
return -1
}
a, afterA := cut(v.PreRelease, '.')
b, afterB := cut(other.PreRelease, '.')
for {
if a == "" && b != "" {
return -1
}
if a != "" && b == "" {
return 1
}
aIsInt := isAllDecimals(a)
bIsInt := isAllDecimals(b)
// numeric identifiers have lower precedence than non-numeric
if aIsInt && !bIsInt {
return -1
} else if !aIsInt && bIsInt {
return 1
}
if aIsInt && bIsInt {
aInt, _ := strconv.Atoi(a)
bInt, _ := strconv.Atoi(b)
if aInt > bInt {
return 1
} else if aInt < bInt {
return -1
}
} else {
// string comparison
if a > b {
return 1
} else if a < b {
return -1
}
}
// a larger set of pre-release fields has a higher precedence than a
// smaller set, if all of the preceding identifiers are equal.
if afterA != "" && afterB == "" {
return 1
} else if afterA == "" && afterB != "" {
return -1
}
a, afterA = cut(afterA, '.')
b, afterB = cut(afterB, '.')
}
}
func isAllDecimals(s string) bool {
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return s != ""
}
// length allows calculating the length of the version for pre-allocation.
func length(v Version) int {
n := util.NumDigitsInt64(v.Major) + util.NumDigitsInt64(v.Minor) + util.NumDigitsInt64(v.Patch) + 2
if v.PreRelease != "" {
n += len(v.PreRelease) + 1
}
if v.Metadata != "" {
n += len(v.Metadata) + 1
}
return n
}
// cut is a *slightly* faster version of strings.Cut only accepting
// single byte separators, and skipping the boolean return value.
func cut(s string, sep byte) (before, after string) {
if i := strings.IndexByte(s, sep); i >= 0 {
return s[:i], s[i+1:]
}
return s, ""
}
@@ -0,0 +1,82 @@
// Copyright 2021 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package strings contains helpers to perform string manipulation
package strings
import (
"path/filepath"
"strings"
"github.com/open-policy-agent/opa/internal/lcss"
)
// TruncateFilePaths truncates the given file paths to conform to the given
// "ideal" width and returns the shortened paths by replacing the middle parts of paths
// with "...", ex: bundle1/.../a/b/policy.rego
func TruncateFilePaths(maxIdealWidth, maxWidth int, path ...string) (map[string]string, int) {
canShorten := make([][]byte, 0, len(path))
for _, p := range path {
canShorten = append(canShorten, []byte(getPathFromFirstSeparator(p)))
}
// Find the longest common path segment
var lcs string
if len(canShorten) > 1 {
lcs = string(lcss.LongestCommonSubstring(canShorten...))
} else {
lcs = string(canShorten[0])
}
// Don't just swap in the full LCSS, trim it down to be the least amount of
// characters to reach our "ideal" width boundary giving as much
// detail as possible without going too long.
diff := maxIdealWidth - (maxWidth - len(lcs) + 3)
if diff > 0 {
if diff > len(lcs) {
lcs = ""
} else {
// Favor data on the right hand side of the path
lcs = lcs[:len(lcs)-diff]
}
}
result := map[string]string{}
for _, p := range path {
result[p] = p
}
longestLocation := maxWidth
// Swap in "..." for the longest common path, but if it makes things better
if len(lcs) > 3 {
for path := range result {
result[path] = strings.Replace(path, lcs, "...", 1)
}
// Drop the overall length down to match our substitution
longestLocation -= (len(lcs) - 3)
}
return result, longestLocation
}
func Truncate(str string, maxWidth int) string {
if len(str) <= maxWidth {
return str
}
return str[:maxWidth-3] + "..."
}
func getPathFromFirstSeparator(path string) string {
s := filepath.Dir(path)
s = strings.TrimPrefix(s, string(filepath.Separator))
firstSlash := strings.IndexRune(s, filepath.Separator)
if firstSlash > 0 {
return s[firstSlash+1:]
}
return s
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright 2017 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package uuid
import (
"fmt"
"io"
"strings"
"github.com/google/uuid"
)
const (
BILLION = 1000000000
)
// New Create a version 4 random UUID
func New(r io.Reader) (string, error) {
bs := make([]byte, 16)
n, err := io.ReadFull(r, bs)
if n != len(bs) || err != nil {
return "", err
}
bs[8] = bs[8]&^0xc0 | 0x80
bs[6] = bs[6]&^0xf0 | 0x40
return fmt.Sprintf("%x-%x-%x-%x-%x", bs[0:4], bs[4:6], bs[6:8], bs[8:10], bs[10:]), nil
}
// Parse will use the google/uuid library to parse the string into a uuid
// if parsing fails, it will return an empty map. It will fill the map
// with some decoded values with fillMap
// ref: https://datatracker.ietf.org/doc/html/rfc4122
func Parse(s string) (map[string]any, error) {
uuid, err := uuid.Parse(s)
if err != nil {
return nil, err
}
out := make(map[string]any, getVersionLen(int(uuid.Version())))
fillMap(out, uuid)
return out, nil
}
// Fills the map with values from the uuid. Version and variant for every version.
// Version 1-2 has decodable values that could be of use, version 4 is random,
// and version 3,5 is not feasible to extract data. Generated with either MD5 or SHA1 hash
// ref: https://datatracker.ietf.org/doc/html/rfc4122 about creation of UUIDs
func fillMap(m map[string]any, u uuid.UUID) {
m["version"] = int(u.Version())
m["variant"] = u.Variant().String()
switch version := m["version"]; version {
case 1, 2:
m["time"] = nanoUnix(u.Time())
m["nodeid"] = byteDecimalToHexMAC(u.NodeID(), "-")
m["macvariables"] = macVars(u.NodeID()[0])
m["clocksequence"] = u.ClockSequence()
if version == 2 {
m["id"] = int(u.ID())
m["domain"] = u.Domain().String()
}
}
}
// macVars will take the first byte of a MAC-address and check for the
// local/global bit and check for the unicast/multicast bit of the byte,
// and return a string with this info.
// ref: https://datatracker.ietf.org/doc/html/rfc7042#section-2.1
func macVars(inpb byte) string {
switch {
case inpb&byte(0b11) == byte(0b11):
return "local:multicast"
case inpb&byte(0b01) == byte(0b01):
return "global:multicast"
case inpb&byte(0b10) == byte(0b10):
return "local:unicast"
}
return "global:unicast"
}
// loops through the byte array to convert all bytes to hexes.
// It will also put the separator between every other to make it human-readable
func byteDecimalToHexMAC(bytes []byte, sep string) string {
hexs := strings.Builder{}
l := len(bytes)
hexs.Grow((l * 3) - 1) // 1 byte -> 2 hexes + 1 separator (if one char)
for i, b := range bytes {
fmt.Fprintf(&hexs, "%02x", b)
if i < l-1 {
hexs.WriteString(sep)
}
}
return hexs.String()
}
// nanoUnix Converts the uuids encoded time into unix represented time in nanoseconds
func nanoUnix(t uuid.Time) int64 {
unixsec, unixnsec := t.UnixTime()
return unixsec*BILLION + unixnsec
}
// Helper function to make map with length based on version of uuid
// Most are 2 in length (version, variant), but version 1 and 2 have more.
func getVersionLen(version int) int {
switch version {
case 1:
return 5
case 2:
return 7
default:
return 2
}
}
@@ -0,0 +1,36 @@
// Copyright 2019 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package version implements helper functions for the stored version.
package version
import (
"context"
"fmt"
"runtime"
"github.com/open-policy-agent/opa/v1/storage"
"github.com/open-policy-agent/opa/v1/version"
)
var versionPath = storage.MustParsePath("/system/version")
// Write the build version information into storage. This makes the
// version information available to the REPL and the HTTP server.
func Write(ctx context.Context, store storage.Store, txn storage.Transaction) error {
if err := storage.MakeDir(ctx, store, txn, versionPath); err != nil {
return err
}
return store.Write(ctx, txn, storage.AddOp, versionPath, map[string]any{
"version": version.Version,
"build_commit": version.Vcs,
"build_timestamp": version.Timestamp,
"build_hostname": version.Hostname,
})
}
// UserAgent defines the current OPA instances User-Agent default header value.
var UserAgent = fmt.Sprintf("Open Policy Agent/%s (%s, %s)", version.Version, runtime.GOOS, runtime.GOARCH)
@@ -0,0 +1,77 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package constant contains WASM constant definitions.
package constant
// Magic bytes at the beginning of every WASM file ("\0asm").
const Magic = uint32(0x6D736100)
// Version defines the WASM version.
const Version = uint32(1)
// WASM module section IDs.
const (
CustomSectionID uint8 = iota
TypeSectionID
ImportSectionID
FunctionSectionID
TableSectionID
MemorySectionID
GlobalSectionID
ExportSectionID
StartSectionID
ElementSectionID
CodeSectionID
DataSectionID
)
// FunctionTypeID indicates the start of a function type definition.
const FunctionTypeID = byte(0x60)
// ValueType represents an intrinsic value type in WASM.
const (
ValueTypeF64 byte = iota + 0x7C
ValueTypeF32
ValueTypeI64
ValueTypeI32
)
// WASM import descriptor types.
const (
ImportDescType byte = iota
ImportDescTable
ImportDescMem
ImportDescGlobal
)
// WASM export descriptor types.
const (
ExportDescType byte = iota
ExportDescTable
ExportDescMem
ExportDescGlobal
)
// ElementTypeAnyFunc indicates the type of a table import.
const ElementTypeAnyFunc byte = 0x70
// BlockTypeEmpty represents a block type.
const BlockTypeEmpty byte = 0x40
// WASM global varialbe mutability flag.
const (
Const byte = iota
Mutable
)
// NameSectionCustomID is the ID of the "Name" section Custom Section
const NameSectionCustomID = "name"
// Subtypes of the 'name' custom section
const (
NameSectionModuleType byte = iota
NameSectionFunctionsType
NameSectionLocalsType
)
@@ -0,0 +1,6 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package encoding implements WASM module reading and writing.
package encoding
@@ -0,0 +1,965 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package encoding
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/open-policy-agent/opa/internal/leb128"
"github.com/open-policy-agent/opa/internal/wasm/constant"
"github.com/open-policy-agent/opa/internal/wasm/instruction"
"github.com/open-policy-agent/opa/internal/wasm/module"
"github.com/open-policy-agent/opa/internal/wasm/opcode"
"github.com/open-policy-agent/opa/internal/wasm/types"
)
// ReadModule reads a binary-encoded WASM module from r.
func ReadModule(r io.Reader) (*module.Module, error) {
wr := &reader{r: r, n: 0}
module, err := readModule(wr)
if err != nil {
return nil, fmt.Errorf("offset 0x%x: %w", wr.n, err)
}
return module, nil
}
// ReadCodeEntry reads a binary-encoded WASM code entry from r.
func ReadCodeEntry(r io.Reader) (*module.CodeEntry, error) {
wr := &reader{r: r, n: 0}
entry, err := readCodeEntry(wr)
if err != nil {
return nil, fmt.Errorf("offset 0x%x: %w", wr.n, err)
}
return entry, nil
}
// CodeEntries returns the WASM code entries contained in r.
func CodeEntries(m *module.Module) ([]*module.CodeEntry, error) {
entries := make([]*module.CodeEntry, len(m.Code.Segments))
for i, s := range m.Code.Segments {
buf := bytes.NewBuffer(s.Code)
entry, err := ReadCodeEntry(buf)
if err != nil {
return nil, err
}
entries[i] = entry
}
return entries, nil
}
type reader struct {
r io.Reader
n int
}
func (r *reader) Read(bs []byte) (int, error) {
n, err := r.r.Read(bs)
r.n += n
return n, err
}
func readModule(r io.Reader) (*module.Module, error) {
if err := readMagic(r); err != nil {
return nil, err
}
if err := readVersion(r); err != nil {
return nil, err
}
var m module.Module
if err := readSections(r, &m); err != io.EOF {
return nil, err
}
return &m, nil
}
func readCodeEntry(r io.Reader) (*module.CodeEntry, error) {
var entry module.CodeEntry
if err := readLocals(r, &entry.Func.Locals); err != nil {
return nil, fmt.Errorf("local declarations: %w", err)
}
return &entry, readExpr(r, &entry.Func.Expr)
}
func readMagic(r io.Reader) error {
var v uint32
if err := binary.Read(r, binary.LittleEndian, &v); err != nil {
return err
} else if v != constant.Magic {
return errors.New("illegal magic value")
}
return nil
}
func readVersion(r io.Reader) error {
var v uint32
if err := binary.Read(r, binary.LittleEndian, &v); err != nil {
return err
} else if v != constant.Version {
return errors.New("illegal wasm version")
}
return nil
}
func readSections(r io.Reader, m *module.Module) error {
for {
id, err := readByte(r)
if err != nil {
return err
}
size, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
buf := make([]byte, size)
if _, err := io.ReadFull(r, buf); err != nil {
return err
}
bufr := bytes.NewReader(buf)
switch id {
case constant.StartSectionID:
if err := readStartSection(bufr, &m.Start); err != nil {
return fmt.Errorf("start section: %w", err)
}
case constant.CustomSectionID:
var name string
if err := readByteVectorString(bufr, &name); err != nil {
return fmt.Errorf("read custom section type: %w", err)
}
if name == "name" {
if err := readCustomNameSections(bufr, &m.Names); err != nil {
return fmt.Errorf("custom 'name' section: %w", err)
}
} else {
if err := readCustomSection(bufr, name, &m.Customs); err != nil {
return fmt.Errorf("custom section: %w", err)
}
}
case constant.TypeSectionID:
if err := readTypeSection(bufr, &m.Type); err != nil {
return fmt.Errorf("type section: %w", err)
}
case constant.ImportSectionID:
if err := readImportSection(bufr, &m.Import); err != nil {
return fmt.Errorf("import section: %w", err)
}
case constant.TableSectionID:
if err := readTableSection(bufr, &m.Table); err != nil {
return fmt.Errorf("table section: %w", err)
}
case constant.MemorySectionID:
if err := readMemorySection(bufr, &m.Memory); err != nil {
return fmt.Errorf("memory section: %w", err)
}
case constant.GlobalSectionID:
if err := readGlobalSection(bufr, &m.Global); err != nil {
return fmt.Errorf("global section: %w", err)
}
case constant.FunctionSectionID:
if err := readFunctionSection(bufr, &m.Function); err != nil {
return fmt.Errorf("function section: %w", err)
}
case constant.ExportSectionID:
if err := readExportSection(bufr, &m.Export); err != nil {
return fmt.Errorf("export section: %w", err)
}
case constant.ElementSectionID:
if err := readElementSection(bufr, &m.Element); err != nil {
return fmt.Errorf("element section: %w", err)
}
case constant.DataSectionID:
if err := readDataSection(bufr, &m.Data); err != nil {
return fmt.Errorf("data section: %w", err)
}
case constant.CodeSectionID:
if err := readRawCodeSection(bufr, &m.Code); err != nil {
return fmt.Errorf("code section: %w", err)
}
default:
return errors.New("illegal section id")
}
}
}
func readCustomSection(r io.Reader, name string, s *[]module.CustomSection) error {
buf, err := io.ReadAll(r)
if err != nil {
return err
}
*s = append(*s, module.CustomSection{
Name: name,
Data: buf,
})
return nil
}
func readCustomNameSections(r io.Reader, s *module.NameSection) error {
for {
id, err := readByte(r)
if err != nil {
if err == io.EOF {
break
}
return err
}
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
buf := make([]byte, n)
if _, err := io.ReadFull(r, buf); err != nil {
return err
}
bufr := bytes.NewReader(buf)
switch id {
case constant.NameSectionModuleType:
err = readNameSectionModule(bufr, s)
case constant.NameSectionFunctionsType:
err = readNameSectionFunctions(bufr, s)
case constant.NameSectionLocalsType:
err = readNameSectionLocals(bufr, s)
}
if err != nil {
return err
}
}
return nil
}
func readNameSectionModule(r io.Reader, s *module.NameSection) error {
return readByteVectorString(r, &s.Module)
}
func readNameSectionFunctions(r io.Reader, s *module.NameSection) error {
nm, err := readNameMap(r)
if err != nil {
return err
}
s.Functions = nm
return nil
}
func readNameMap(r io.Reader) ([]module.NameMap, error) {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return nil, err
}
nm := make([]module.NameMap, n)
for i := range n {
var name string
id, err := leb128.ReadVarUint32(r)
if err != nil {
return nil, err
}
if err := readByteVectorString(r, &name); err != nil {
return nil, err
}
nm[i] = module.NameMap{Index: id, Name: name}
}
return nm, nil
}
func readNameSectionLocals(r io.Reader, s *module.NameSection) error {
n, err := leb128.ReadVarUint32(r) // length of vec(indirectnameassoc)
if err != nil {
return err
}
for range n {
id, err := leb128.ReadVarUint32(r) // func index
if err != nil {
return err
}
nm, err := readNameMap(r)
if err != nil {
return err
}
for _, m := range nm {
s.Locals = append(s.Locals, module.LocalNameMap{
FuncIndex: id,
NameMap: module.NameMap{
Index: m.Index,
Name: m.Name,
}})
}
}
return nil
}
func readStartSection(r io.Reader, s *module.StartSection) error {
idx, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
s.FuncIndex = &idx
return nil
}
func readTypeSection(r io.Reader, s *module.TypeSection) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
for range n {
var ftype module.FunctionType
if err := readFunctionType(r, &ftype); err != nil {
return err
}
s.Functions = append(s.Functions, ftype)
}
return nil
}
func readImportSection(r io.Reader, s *module.ImportSection) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
for range n {
var imp module.Import
if err := readImport(r, &imp); err != nil {
return err
}
s.Imports = append(s.Imports, imp)
}
return nil
}
func readTableSection(r io.Reader, s *module.TableSection) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
for range n {
var table module.Table
if elem, err := readByte(r); err != nil {
return err
} else if elem != constant.ElementTypeAnyFunc {
return errors.New("illegal element type")
}
table.Type = types.Anyfunc
if err := readLimits(r, &table.Lim); err != nil {
return err
}
s.Tables = append(s.Tables, table)
}
return nil
}
func readMemorySection(r io.Reader, s *module.MemorySection) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
for range n {
var mem module.Memory
if err := readLimits(r, &mem.Lim); err != nil {
return err
}
s.Memories = append(s.Memories, mem)
}
return nil
}
func readGlobalSection(r io.Reader, s *module.GlobalSection) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
for range n {
var global module.Global
if err := readGlobal(r, &global); err != nil {
return err
}
s.Globals = append(s.Globals, global)
}
return nil
}
func readFunctionSection(r io.Reader, s *module.FunctionSection) error {
return readVarUint32Vector(r, &s.TypeIndices)
}
func readExportSection(r io.Reader, s *module.ExportSection) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
for range n {
var exp module.Export
if err := readExport(r, &exp); err != nil {
return err
}
s.Exports = append(s.Exports, exp)
}
return nil
}
func readElementSection(r io.Reader, s *module.ElementSection) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
for range n {
var seg module.ElementSegment
if err := readElementSegment(r, &seg); err != nil {
return err
}
s.Segments = append(s.Segments, seg)
}
return nil
}
func readDataSection(r io.Reader, s *module.DataSection) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
for range n {
var seg module.DataSegment
if err := readDataSegment(r, &seg); err != nil {
return err
}
s.Segments = append(s.Segments, seg)
}
return nil
}
func readRawCodeSection(r io.Reader, s *module.RawCodeSection) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
for range n {
var seg module.RawCodeSegment
if err := readRawCodeSegment(r, &seg); err != nil {
return err
}
s.Segments = append(s.Segments, seg)
}
return nil
}
func readFunctionType(r io.Reader, ftype *module.FunctionType) error {
if b, err := readByte(r); err != nil {
return err
} else if b != constant.FunctionTypeID {
return fmt.Errorf("illegal function type id 0x%x", b)
}
if err := readValueTypeVector(r, &ftype.Params); err != nil {
return err
}
return readValueTypeVector(r, &ftype.Results)
}
func readGlobal(r io.Reader, global *module.Global) error {
if err := readValueType(r, &global.Type); err != nil {
return err
}
b, err := readByte(r)
if err != nil {
return err
}
if b == 1 {
global.Mutable = true
} else if b != 0 {
return errors.New("illegal mutability flag")
}
return readConstantExpr(r, &global.Init)
}
func readImport(r io.Reader, imp *module.Import) error {
if err := readByteVectorString(r, &imp.Module); err != nil {
return err
}
if err := readByteVectorString(r, &imp.Name); err != nil {
return err
}
b, err := readByte(r)
if err != nil {
return err
}
if b == constant.ImportDescType {
index, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
imp.Descriptor = module.FunctionImport{
Func: index,
}
return nil
}
if b == constant.ImportDescTable {
if elem, err := readByte(r); err != nil {
return err
} else if elem != constant.ElementTypeAnyFunc {
return errors.New("illegal element type")
}
desc := module.TableImport{
Type: types.Anyfunc,
}
if err := readLimits(r, &desc.Lim); err != nil {
return err
}
imp.Descriptor = desc
return nil
}
if b == constant.ImportDescMem {
desc := module.MemoryImport{}
if err := readLimits(r, &desc.Mem.Lim); err != nil {
return err
}
imp.Descriptor = desc
return nil
}
if b == constant.ImportDescGlobal {
desc := module.GlobalImport{}
if err := readValueType(r, &desc.Type); err != nil {
return err
}
b, err := readByte(r)
if err != nil {
return err
}
if b == 1 {
desc.Mutable = true
} else if b != 0 {
return errors.New("illegal mutability flag")
}
return nil
}
return errors.New("illegal import descriptor type")
}
func readExport(r io.Reader, exp *module.Export) error {
if err := readByteVectorString(r, &exp.Name); err != nil {
return err
}
b, err := readByte(r)
if err != nil {
return err
}
switch b {
case constant.ExportDescType:
exp.Descriptor.Type = module.FunctionExportType
case constant.ExportDescTable:
exp.Descriptor.Type = module.TableExportType
case constant.ExportDescMem:
exp.Descriptor.Type = module.MemoryExportType
case constant.ExportDescGlobal:
exp.Descriptor.Type = module.GlobalExportType
default:
return errors.New("illegal export descriptor type")
}
exp.Descriptor.Index, err = leb128.ReadVarUint32(r)
if err != nil {
return err
}
return nil
}
func readElementSegment(r io.Reader, seg *module.ElementSegment) error {
if err := readVarUint32(r, &seg.Index); err != nil {
return err
}
if err := readConstantExpr(r, &seg.Offset); err != nil {
return err
}
return readVarUint32Vector(r, &seg.Indices)
}
func readDataSegment(r io.Reader, seg *module.DataSegment) error {
if err := readVarUint32(r, &seg.Index); err != nil {
return err
}
if err := readConstantExpr(r, &seg.Offset); err != nil {
return err
}
return readByteVector(r, &seg.Init)
}
func readRawCodeSegment(r io.Reader, seg *module.RawCodeSegment) error {
return readByteVector(r, &seg.Code)
}
func readConstantExpr(r io.Reader, expr *module.Expr) error {
instrs := make([]instruction.Instruction, 0)
for {
b, err := readByte(r)
if err != nil {
return err
}
switch opcode.Opcode(b) {
case opcode.I32Const:
i32, err := leb128.ReadVarInt32(r)
if err != nil {
return err
}
instrs = append(instrs, instruction.I32Const{Value: i32})
case opcode.I64Const:
i64, err := leb128.ReadVarInt64(r)
if err != nil {
return err
}
instrs = append(instrs, instruction.I64Const{Value: i64})
case opcode.End:
expr.Instrs = instrs
return nil
default:
return fmt.Errorf("illegal constant expr opcode 0x%x", b)
}
}
}
func readExpr(r io.Reader, expr *module.Expr) (err error) {
defer func() {
if r := recover(); r != nil {
switch r := r.(type) {
case error:
err = r
default:
err = errors.New("unknown panic")
}
}
}()
return readInstructions(r, &expr.Instrs)
}
func readInstructions(r io.Reader, instrs *[]instruction.Instruction) error {
ret := make([]instruction.Instruction, 0)
for {
b, err := readByte(r)
if err != nil {
return err
}
switch opcode.Opcode(b) {
case opcode.I32Const:
ret = append(ret, instruction.I32Const{Value: leb128.MustReadVarInt32(r)})
case opcode.I64Const:
ret = append(ret, instruction.I64Const{Value: leb128.MustReadVarInt64(r)})
case opcode.I32Eqz:
ret = append(ret, instruction.I32Eqz{})
case opcode.GetLocal:
ret = append(ret, instruction.GetLocal{Index: leb128.MustReadVarUint32(r)})
case opcode.SetLocal:
ret = append(ret, instruction.SetLocal{Index: leb128.MustReadVarUint32(r)})
case opcode.Call:
ret = append(ret, instruction.Call{Index: leb128.MustReadVarUint32(r)})
case opcode.CallIndirect:
ret = append(ret, instruction.CallIndirect{
Index: leb128.MustReadVarUint32(r),
Reserved: mustReadByte(r),
})
case opcode.BrIf:
ret = append(ret, instruction.BrIf{Index: leb128.MustReadVarUint32(r)})
case opcode.Return:
ret = append(ret, instruction.Return{})
case opcode.Block:
block := instruction.Block{}
if err := readBlockValueType(r, block.Type); err != nil {
return err
}
if err := readInstructions(r, &block.Instrs); err != nil {
return err
}
ret = append(ret, block)
case opcode.Loop:
loop := instruction.Loop{}
if err := readBlockValueType(r, loop.Type); err != nil {
return err
}
if err := readInstructions(r, &loop.Instrs); err != nil {
return err
}
ret = append(ret, loop)
case opcode.End:
*instrs = ret
return nil
default:
return fmt.Errorf("illegal opcode 0x%x", b)
}
}
}
func mustReadByte(r io.Reader) byte {
b, err := readByte(r)
if err != nil {
panic(err)
}
return b
}
func readLimits(r io.Reader, l *module.Limit) error {
b, err := readByte(r)
if err != nil {
return err
}
minLim, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
l.Min = minLim
if b == 1 {
maxLim, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
l.Max = &maxLim
} else if b != 0 {
return errors.New("illegal limit flag")
}
return nil
}
func readLocals(r io.Reader, locals *[]module.LocalDeclaration) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
ret := make([]module.LocalDeclaration, n)
for i := range n {
if err := readVarUint32(r, &ret[i].Count); err != nil {
return err
}
if err := readValueType(r, &ret[i].Type); err != nil {
return err
}
}
*locals = ret
return nil
}
func readByteVector(r io.Reader, v *[]byte) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
buf := make([]byte, n)
if _, err := io.ReadFull(r, buf); err != nil {
return err
}
*v = buf
return nil
}
func readByteVectorString(r io.Reader, v *string) error {
var buf []byte
if err := readByteVector(r, &buf); err != nil {
return err
}
*v = string(buf)
return nil
}
func readVarUint32Vector(r io.Reader, v *[]uint32) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
ret := make([]uint32, n)
for i := range n {
if err := readVarUint32(r, &ret[i]); err != nil {
return err
}
}
*v = ret
return nil
}
func readValueTypeVector(r io.Reader, v *[]types.ValueType) error {
n, err := leb128.ReadVarUint32(r)
if err != nil {
return err
}
ret := make([]types.ValueType, n)
for i := range n {
if err := readValueType(r, &ret[i]); err != nil {
return err
}
}
*v = ret
return nil
}
func readVarUint32(r io.Reader, v *uint32) error {
var err error
*v, err = leb128.ReadVarUint32(r)
return err
}
func readValueType(r io.Reader, v *types.ValueType) error {
if b, err := readByte(r); err != nil {
return err
} else if b == constant.ValueTypeI32 {
*v = types.I32
} else if b == constant.ValueTypeI64 {
*v = types.I64
} else if b == constant.ValueTypeF32 {
*v = types.F32
} else if b == constant.ValueTypeF64 {
*v = types.F64
} else {
return fmt.Errorf("illegal value type: 0x%x", b)
}
return nil
}
func readBlockValueType(r io.Reader, v *types.ValueType) error {
if b, err := readByte(r); err != nil {
return err
} else if b == constant.ValueTypeI32 {
*v = types.I32
} else if b == constant.ValueTypeI64 {
*v = types.I64
} else if b == constant.ValueTypeF32 {
*v = types.F32
} else if b == constant.ValueTypeF64 {
*v = types.F64
} else if b != constant.BlockTypeEmpty {
return fmt.Errorf("illegal value type: 0x%x", b)
}
return nil
}
func readByte(r io.Reader) (byte, error) {
buf := make([]byte, 1)
_, err := io.ReadFull(r, buf)
return buf[0], err
}
@@ -0,0 +1,778 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package encoding
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"github.com/open-policy-agent/opa/internal/leb128"
"github.com/open-policy-agent/opa/internal/wasm/constant"
"github.com/open-policy-agent/opa/internal/wasm/instruction"
"github.com/open-policy-agent/opa/internal/wasm/module"
"github.com/open-policy-agent/opa/internal/wasm/opcode"
"github.com/open-policy-agent/opa/internal/wasm/types"
)
// WriteModule writes a binary-encoded representation of module to w.
func WriteModule(w io.Writer, module *module.Module) error {
if err := writeMagic(w); err != nil {
return err
}
if err := writeVersion(w); err != nil {
return err
}
if module == nil {
return nil
}
if err := writeTypeSection(w, module.Type); err != nil {
return err
}
if err := writeImportSection(w, module.Import); err != nil {
return err
}
if err := writeFunctionSection(w, module.Function); err != nil {
return err
}
if err := writeTableSection(w, module.Table); err != nil {
return err
}
if err := writeMemorySection(w, module.Memory); err != nil {
return err
}
if err := writeGlobalSection(w, module.Global); err != nil {
return err
}
if err := writeExportSection(w, module.Export); err != nil {
return err
}
if err := writeStartSection(w, module.Start); err != nil {
return err
}
if err := writeElementSection(w, module.Element); err != nil {
return err
}
if err := writeRawCodeSection(w, module.Code); err != nil {
return err
}
if err := writeDataSection(w, module.Data); err != nil {
return err
}
if err := writeNameSection(w, module.Names); err != nil {
return err
}
for _, custom := range module.Customs {
if err := writeCustomSection(w, custom); err != nil {
return err
}
}
return nil
}
// WriteCodeEntry writes a binary encoded representation of entry to w.
func WriteCodeEntry(w io.Writer, entry *module.CodeEntry) error {
if err := leb128.WriteVarUint32(w, uint32(len(entry.Func.Locals))); err != nil {
return err
}
for _, local := range entry.Func.Locals {
if err := leb128.WriteVarUint32(w, local.Count); err != nil {
return err
}
if err := writeValueType(w, local.Type); err != nil {
return err
}
}
return writeInstructions(w, entry.Func.Expr.Instrs)
}
func writeMagic(w io.Writer) error {
return binary.Write(w, binary.LittleEndian, constant.Magic)
}
func writeVersion(w io.Writer) error {
return binary.Write(w, binary.LittleEndian, constant.Version)
}
func writeStartSection(w io.Writer, s module.StartSection) error {
if s.FuncIndex == nil {
return nil
}
if err := writeByte(w, constant.StartSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, *s.FuncIndex); err != nil {
return err
}
return writeRawSection(w, &buf)
}
func writeTypeSection(w io.Writer, s module.TypeSection) error {
if len(s.Functions) == 0 {
return nil
}
if err := writeByte(w, constant.TypeSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, uint32(len(s.Functions))); err != nil {
return err
}
for _, fsig := range s.Functions {
if err := writeFunctionType(&buf, fsig); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeImportSection(w io.Writer, s module.ImportSection) error {
if len(s.Imports) == 0 {
return nil
}
if err := writeByte(w, constant.ImportSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, uint32(len(s.Imports))); err != nil {
return err
}
for _, imp := range s.Imports {
if err := writeImport(&buf, imp); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeGlobalSection(w io.Writer, s module.GlobalSection) error {
if len(s.Globals) == 0 {
return nil
}
if err := writeByte(w, constant.GlobalSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, uint32(len(s.Globals))); err != nil {
return err
}
for _, global := range s.Globals {
if err := writeGlobal(&buf, global); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeFunctionSection(w io.Writer, s module.FunctionSection) error {
if len(s.TypeIndices) == 0 {
return nil
}
if err := writeByte(w, constant.FunctionSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, uint32(len(s.TypeIndices))); err != nil {
return err
}
for _, idx := range s.TypeIndices {
if err := leb128.WriteVarUint32(&buf, idx); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeTableSection(w io.Writer, s module.TableSection) error {
if len(s.Tables) == 0 {
return nil
}
if err := writeByte(w, constant.TableSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, uint32(len(s.Tables))); err != nil {
return err
}
for _, table := range s.Tables {
switch table.Type {
case types.Anyfunc:
if err := writeByte(&buf, constant.ElementTypeAnyFunc); err != nil {
return err
}
default:
return errors.New("illegal table element type")
}
if err := writeLimits(&buf, table.Lim); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeMemorySection(w io.Writer, s module.MemorySection) error {
if len(s.Memories) == 0 {
return nil
}
if err := writeByte(w, constant.MemorySectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, uint32(len(s.Memories))); err != nil {
return err
}
for _, mem := range s.Memories {
if err := writeLimits(&buf, mem.Lim); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeExportSection(w io.Writer, s module.ExportSection) error {
if len(s.Exports) == 0 {
return nil
}
if err := writeByte(w, constant.ExportSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, uint32(len(s.Exports))); err != nil {
return err
}
for _, exp := range s.Exports {
if err := writeByteVector(&buf, []byte(exp.Name)); err != nil {
return err
}
var tpe byte
switch exp.Descriptor.Type {
case module.FunctionExportType:
tpe = constant.ExportDescType
case module.TableExportType:
tpe = constant.ExportDescTable
case module.MemoryExportType:
tpe = constant.ExportDescMem
case module.GlobalExportType:
tpe = constant.ExportDescGlobal
default:
return fmt.Errorf("illegal export descriptor type 0x%x", exp.Descriptor.Type)
}
if err := writeByte(&buf, tpe); err != nil {
return err
}
if err := leb128.WriteVarUint32(&buf, exp.Descriptor.Index); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeElementSection(w io.Writer, s module.ElementSection) error {
if len(s.Segments) == 0 {
return nil
}
if err := writeByte(w, constant.ElementSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, uint32(len(s.Segments))); err != nil {
return err
}
for _, seg := range s.Segments {
if err := leb128.WriteVarUint32(&buf, seg.Index); err != nil {
return err
}
if err := writeInstructions(&buf, seg.Offset.Instrs); err != nil {
return err
}
if err := writeVarUint32Vector(&buf, seg.Indices); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeRawCodeSection(w io.Writer, s module.RawCodeSection) error {
if len(s.Segments) == 0 {
return nil
}
if err := writeByte(w, constant.CodeSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, uint32(len(s.Segments))); err != nil {
return err
}
for _, seg := range s.Segments {
if err := leb128.WriteVarUint32(&buf, uint32(len(seg.Code))); err != nil {
return err
}
if _, err := buf.Write(seg.Code); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeDataSection(w io.Writer, s module.DataSection) error {
if len(s.Segments) == 0 {
return nil
}
if err := writeByte(w, constant.DataSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := leb128.WriteVarUint32(&buf, uint32(len(s.Segments))); err != nil {
return err
}
for _, seg := range s.Segments {
if err := leb128.WriteVarUint32(&buf, seg.Index); err != nil {
return err
}
if err := writeInstructions(&buf, seg.Offset.Instrs); err != nil {
return err
}
if err := writeByteVector(&buf, seg.Init); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeNameSection(w io.Writer, s module.NameSection) error {
if s.Module == "" && len(s.Functions) == 0 && len(s.Locals) == 0 {
return nil
}
if err := writeByte(w, constant.CustomSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := writeByteVector(&buf, []byte(constant.NameSectionCustomID)); err != nil {
return err
}
// "module" subsection
if s.Module != "" {
if err := writeByte(&buf, constant.NameSectionModuleType); err != nil {
return err
}
var mbuf bytes.Buffer
if err := writeByteVector(&mbuf, []byte(s.Module)); err != nil {
return err
}
if err := writeRawSection(&buf, &mbuf); err != nil {
return err
}
}
// "functions" subsection
if len(s.Functions) != 0 {
if err := writeByte(&buf, constant.NameSectionFunctionsType); err != nil {
return err
}
var fbuf bytes.Buffer
if err := writeNameMap(&fbuf, s.Functions); err != nil {
return err
}
if err := writeRawSection(&buf, &fbuf); err != nil {
return err
}
}
// "locals" subsection
if len(s.Locals) != 0 {
if err := writeByte(&buf, constant.NameSectionLocalsType); err != nil {
return err
}
funs := map[uint32][]module.NameMap{}
for _, e := range s.Locals {
funs[e.FuncIndex] = append(funs[e.FuncIndex], module.NameMap{Index: e.Index, Name: e.Name})
}
var lbuf bytes.Buffer
if err := leb128.WriteVarUint32(&lbuf, uint32(len(funs))); err != nil {
return err
}
for fidx, e := range funs {
if err := leb128.WriteVarUint32(&lbuf, fidx); err != nil {
return err
}
if err := writeNameMap(&lbuf, e); err != nil {
return err
}
}
if err := writeRawSection(&buf, &lbuf); err != nil {
return err
}
}
return writeRawSection(w, &buf)
}
func writeNameMap(buf io.Writer, nm []module.NameMap) error {
if err := leb128.WriteVarUint32(buf, uint32(len(nm))); err != nil {
return err
}
for _, m := range nm {
if err := leb128.WriteVarUint32(buf, m.Index); err != nil {
return err
}
if err := writeByteVector(buf, []byte(m.Name)); err != nil {
return err
}
}
return nil
}
func writeCustomSection(w io.Writer, s module.CustomSection) error {
if err := writeByte(w, constant.CustomSectionID); err != nil {
return err
}
var buf bytes.Buffer
if err := writeByteVector(&buf, []byte(s.Name)); err != nil {
return err
}
if _, err := io.Copy(&buf, bytes.NewReader(s.Data)); err != nil {
return err
}
return writeRawSection(w, &buf)
}
func writeFunctionType(w io.Writer, fsig module.FunctionType) error {
if err := writeByte(w, constant.FunctionTypeID); err != nil {
return err
}
if err := writeValueTypeVector(w, fsig.Params); err != nil {
return err
}
return writeValueTypeVector(w, fsig.Results)
}
func writeImport(w io.Writer, imp module.Import) error {
if err := writeByteVector(w, []byte(imp.Module)); err != nil {
return err
}
if err := writeByteVector(w, []byte(imp.Name)); err != nil {
return err
}
switch desc := imp.Descriptor.(type) {
case module.FunctionImport:
if err := writeByte(w, constant.ImportDescType); err != nil {
return err
}
return leb128.WriteVarUint32(w, desc.Func)
case module.TableImport:
if err := writeByte(w, constant.ImportDescTable); err != nil {
return err
}
if err := writeByte(w, constant.ElementTypeAnyFunc); err != nil {
return err
}
return writeLimits(w, desc.Lim)
case module.MemoryImport:
if err := writeByte(w, constant.ImportDescMem); err != nil {
return err
}
return writeLimits(w, desc.Mem.Lim)
case module.GlobalImport:
if err := writeByte(w, constant.ImportDescGlobal); err != nil {
return err
}
if err := writeValueType(w, desc.Type); err != nil {
return err
}
if desc.Mutable {
return writeByte(w, constant.Mutable)
}
return writeByte(w, constant.Const)
default:
return errors.New("illegal import descriptor type")
}
}
func writeGlobal(w io.Writer, global module.Global) error {
if err := writeValueType(w, global.Type); err != nil {
return err
}
var err error
if global.Mutable {
err = writeByte(w, constant.Mutable)
} else {
err = writeByte(w, constant.Const)
}
if err != nil {
return err
}
return writeInstructions(w, global.Init.Instrs)
}
func writeInstructions(w io.Writer, instrs []instruction.Instruction) error {
for i, instr := range instrs {
_, err := w.Write([]byte{byte(instr.Op())})
if err != nil {
return err
}
for _, arg := range instr.ImmediateArgs() {
var err error
switch arg := arg.(type) {
case int32:
err = leb128.WriteVarInt32(w, arg)
case int64:
err = leb128.WriteVarInt64(w, arg)
case uint32:
err = leb128.WriteVarUint32(w, arg)
case uint64:
err = leb128.WriteVarUint64(w, arg)
case float32:
u32 := math.Float32bits(arg)
err = binary.Write(w, binary.LittleEndian, u32)
case float64:
u64 := math.Float64bits(arg)
err = binary.Write(w, binary.LittleEndian, u64)
case byte:
_, err = w.Write([]byte{arg})
default:
return fmt.Errorf("illegal immediate argument type on instruction %d", i)
}
if err != nil {
return err
}
}
if si, ok := instr.(instruction.StructuredInstruction); ok {
if err := writeBlockValueType(w, si.BlockType()); err != nil {
return err
}
if err := writeInstructions(w, si.Instructions()); err != nil {
return err
}
}
}
_, err := w.Write([]byte{byte(opcode.End)})
return err
}
func writeLimits(w io.Writer, lim module.Limit) error {
if lim.Max == nil {
if err := writeByte(w, 0); err != nil {
return err
}
} else {
if err := writeByte(w, 1); err != nil {
return err
}
}
if err := leb128.WriteVarUint32(w, lim.Min); err != nil {
return err
}
if lim.Max != nil {
return leb128.WriteVarUint32(w, *lim.Max)
}
return nil
}
func writeVarUint32Vector(w io.Writer, v []uint32) error {
if err := leb128.WriteVarUint32(w, uint32(len(v))); err != nil {
return err
}
for i := range v {
if err := leb128.WriteVarUint32(w, v[i]); err != nil {
return err
}
}
return nil
}
func writeValueTypeVector(w io.Writer, v []types.ValueType) error {
if err := leb128.WriteVarUint32(w, uint32(len(v))); err != nil {
return err
}
for i := range v {
if err := writeValueType(w, v[i]); err != nil {
return err
}
}
return nil
}
func writeBlockValueType(w io.Writer, v *types.ValueType) error {
var b byte
if v != nil {
switch *v {
case types.I32:
b = constant.ValueTypeI32
case types.I64:
b = constant.ValueTypeI64
case types.F32:
b = constant.ValueTypeF32
case types.F64:
b = constant.ValueTypeF64
}
} else {
b = constant.BlockTypeEmpty
}
return writeByte(w, b)
}
func writeValueType(w io.Writer, v types.ValueType) error {
var b byte
switch v {
case types.I32:
b = constant.ValueTypeI32
case types.I64:
b = constant.ValueTypeI64
case types.F32:
b = constant.ValueTypeF32
case types.F64:
b = constant.ValueTypeF64
}
return writeByte(w, b)
}
func writeRawSection(w io.Writer, buf *bytes.Buffer) error {
size := buf.Len()
if err := leb128.WriteVarUint32(w, uint32(size)); err != nil {
return err
}
_, err := io.Copy(w, buf)
return err
}
func writeByteVector(w io.Writer, bs []byte) error {
if err := leb128.WriteVarUint32(w, uint32(len(bs))); err != nil {
return err
}
_, err := w.Write(bs)
return err
}
func writeByte(w io.Writer, b byte) error {
buf := make([]byte, 1)
buf[0] = b
_, err := w.Write(buf)
return err
}
@@ -0,0 +1,183 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package instruction
import (
"github.com/open-policy-agent/opa/internal/wasm/opcode"
"github.com/open-policy-agent/opa/internal/wasm/types"
)
// !!! If you find yourself adding support for more control
// instructions (br_table, if, ...), please adapt the
// `withControlInstr` functions of
// `compiler/wasm/optimizations.go`
// Unreachable represents a WASM unreachable instruction.
type Unreachable struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (Unreachable) Op() opcode.Opcode {
return opcode.Unreachable
}
// Nop represents a WASM no-op instruction.
type Nop struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (Nop) Op() opcode.Opcode {
return opcode.Nop
}
// Block represents a WASM block instruction.
type Block struct {
NoImmediateArgs
Type *types.ValueType
Instrs []Instruction
}
// Op returns the opcode of the instruction
func (Block) Op() opcode.Opcode {
return opcode.Block
}
// BlockType returns the type of the block's return value.
func (i Block) BlockType() *types.ValueType {
return i.Type
}
// Instructions returns the instructions contained in the block.
func (i Block) Instructions() []Instruction {
return i.Instrs
}
// If represents a WASM if instruction.
// NOTE(sr): we only use if with one branch so far!
type If struct {
NoImmediateArgs
Type *types.ValueType
Instrs []Instruction
}
// Op returns the opcode of the instruction.
func (If) Op() opcode.Opcode {
return opcode.If
}
// BlockType returns the type of the if's THEN branch.
func (i If) BlockType() *types.ValueType {
return i.Type
}
// Instructions represents the instructions contained in the if's THEN branch.
func (i If) Instructions() []Instruction {
return i.Instrs
}
// Loop represents a WASM loop instruction.
type Loop struct {
NoImmediateArgs
Type *types.ValueType
Instrs []Instruction
}
// Op returns the opcode of the instruction.
func (Loop) Op() opcode.Opcode {
return opcode.Loop
}
// BlockType returns the type of the loop's return value.
func (i Loop) BlockType() *types.ValueType {
return i.Type
}
// Instructions represents the instructions contained in the loop.
func (i Loop) Instructions() []Instruction {
return i.Instrs
}
// Br represents a WASM br instruction.
type Br struct {
Index uint32
}
// Op returns the opcode of the instruction.
func (Br) Op() opcode.Opcode {
return opcode.Br
}
// ImmediateArgs returns the block index to break to.
func (i Br) ImmediateArgs() []any {
return []any{i.Index}
}
// BrIf represents a WASM br_if instruction.
type BrIf struct {
Index uint32
}
// Op returns the opcode of the instruction.
func (BrIf) Op() opcode.Opcode {
return opcode.BrIf
}
// ImmediateArgs returns the block index to break to.
func (i BrIf) ImmediateArgs() []any {
return []any{i.Index}
}
// Call represents a WASM call instruction.
type Call struct {
Index uint32
}
// Op returns the opcode of the instruction.
func (Call) Op() opcode.Opcode {
return opcode.Call
}
// ImmediateArgs returns the function index.
func (i Call) ImmediateArgs() []any {
return []any{i.Index}
}
// CallIndirect represents a WASM call_indirect instruction.
type CallIndirect struct {
Index uint32 // type index
Reserved byte // zero for now
}
// Op returns the opcode of the instruction.
func (CallIndirect) Op() opcode.Opcode {
return opcode.CallIndirect
}
// ImmediateArgs returns the function index.
func (i CallIndirect) ImmediateArgs() []any {
return []any{i.Index, i.Reserved}
}
// Return represents a WASM return instruction.
type Return struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (Return) Op() opcode.Opcode {
return opcode.Return
}
// End represents the special WASM end instruction.
type End struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (End) Op() opcode.Opcode {
return opcode.End
}
@@ -0,0 +1,33 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package instruction defines WASM instruction types.
package instruction
import (
"github.com/open-policy-agent/opa/internal/wasm/opcode"
"github.com/open-policy-agent/opa/internal/wasm/types"
)
// NoImmediateArgs indicates the instruction has no immediate arguments.
type NoImmediateArgs struct {
}
// ImmediateArgs returns the immedate arguments of an instruction.
func (NoImmediateArgs) ImmediateArgs() []any {
return nil
}
// Instruction represents a single WASM instruction.
type Instruction interface {
Op() opcode.Opcode
ImmediateArgs() []any
}
// StructuredInstruction represents a structured control instruction like br_if.
type StructuredInstruction interface {
Instruction
BlockType() *types.ValueType
Instructions() []Instruction
}
@@ -0,0 +1,39 @@
// Copyright 2019 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package instruction
import "github.com/open-policy-agent/opa/internal/wasm/opcode"
// I32Load represents the WASM i32.load instruction.
type I32Load struct {
Offset int32
Align int32 // expressed as a power of two
}
// Op returns the opcode of the instruction.
func (I32Load) Op() opcode.Opcode {
return opcode.I32Load
}
// ImmediateArgs returns the static offset and alignment operands.
func (i I32Load) ImmediateArgs() []any {
return []any{i.Align, i.Offset}
}
// I32Store represents the WASM i32.store instruction.
type I32Store struct {
Offset int32
Align int32 // expressed as a power of two
}
// Op returns the opcode of the instruction.
func (I32Store) Op() opcode.Opcode {
return opcode.I32Store
}
// ImmediateArgs returns the static offset and alignment operands.
func (i I32Store) ImmediateArgs() []any {
return []any{i.Align, i.Offset}
}
@@ -0,0 +1,199 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package instruction
import (
"github.com/open-policy-agent/opa/internal/wasm/opcode"
)
// I32Const represents the WASM i32.const instruction.
type I32Const struct {
Value int32
}
// Op returns the opcode of the instruction.
func (I32Const) Op() opcode.Opcode {
return opcode.I32Const
}
// ImmediateArgs returns the i32 value to push onto the stack.
func (i I32Const) ImmediateArgs() []any {
return []any{i.Value}
}
// I64Const represents the WASM i64.const instruction.
type I64Const struct {
Value int64
}
// Op returns the opcode of the instruction.
func (I64Const) Op() opcode.Opcode {
return opcode.I64Const
}
// ImmediateArgs returns the i64 value to push onto the stack.
func (i I64Const) ImmediateArgs() []any {
return []any{i.Value}
}
// F32Const represents the WASM f32.const instruction.
type F32Const struct {
Value int32
}
// Op returns the opcode of the instruction.
func (F32Const) Op() opcode.Opcode {
return opcode.F32Const
}
// ImmediateArgs returns the f32 value to push onto the stack.
func (i F32Const) ImmediateArgs() []any {
return []any{i.Value}
}
// F64Const represents the WASM f64.const instruction.
type F64Const struct {
Value float64
}
// Op returns the opcode of the instruction.
func (F64Const) Op() opcode.Opcode {
return opcode.F64Const
}
// ImmediateArgs returns the f64 value to push onto the stack.
func (i F64Const) ImmediateArgs() []any {
return []any{i.Value}
}
// I32Eqz represents the WASM i32.eqz instruction.
type I32Eqz struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I32Eqz) Op() opcode.Opcode {
return opcode.I32Eqz
}
// I32Eq represents the WASM i32.eq instruction.
type I32Eq struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I32Eq) Op() opcode.Opcode {
return opcode.I32Eq
}
// I32Ne represents the WASM i32.ne instruction.
type I32Ne struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I32Ne) Op() opcode.Opcode {
return opcode.I32Ne
}
// I32GtS represents the WASM i32.gt_s instruction.
type I32GtS struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I32GtS) Op() opcode.Opcode {
return opcode.I32GtS
}
// I32GeS represents the WASM i32.ge_s instruction.
type I32GeS struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I32GeS) Op() opcode.Opcode {
return opcode.I32GeS
}
// I32LtS represents the WASM i32.lt_s instruction.
type I32LtS struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I32LtS) Op() opcode.Opcode {
return opcode.I32LtS
}
// I32LeS represents the WASM i32.le_s instruction.
type I32LeS struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I32LeS) Op() opcode.Opcode {
return opcode.I32LeS
}
// I32Add represents the WASM i32.add instruction.
type I32Add struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I32Add) Op() opcode.Opcode {
return opcode.I32Add
}
// I64Add represents the WASM i64.add instruction.
type I64Add struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I64Add) Op() opcode.Opcode {
return opcode.I64Add
}
// F32Add represents the WASM f32.add instruction.
type F32Add struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (F32Add) Op() opcode.Opcode {
return opcode.F32Add
}
// F64Add represents the WASM f64.add instruction.
type F64Add struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (F64Add) Op() opcode.Opcode {
return opcode.F64Add
}
// I32Mul represents the WASM i32.mul instruction.
type I32Mul struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I32Mul) Op() opcode.Opcode {
return opcode.I32Mul
}
// I32Sub represents the WASM i32.sub instruction.
type I32Sub struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (I32Sub) Op() opcode.Opcode {
return opcode.I32Sub
}
@@ -0,0 +1,29 @@
// Copyright 2021 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package instruction
import (
"github.com/open-policy-agent/opa/internal/wasm/opcode"
)
// Drop reprsents a WASM drop instruction.
type Drop struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (Drop) Op() opcode.Opcode {
return opcode.Drop
}
// Select reprsents a WASM select instruction.
type Select struct {
NoImmediateArgs
}
// Op returns the opcode of the instruction.
func (Select) Op() opcode.Opcode {
return opcode.Select
}
@@ -0,0 +1,54 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package instruction
import "github.com/open-policy-agent/opa/internal/wasm/opcode"
// GetLocal represents the WASM get_local instruction.
type GetLocal struct {
Index uint32
}
// Op returns the opcode of the instruction.
func (GetLocal) Op() opcode.Opcode {
return opcode.GetLocal
}
// ImmediateArgs returns the index of the local variable to push onto the stack.
func (i GetLocal) ImmediateArgs() []any {
return []any{i.Index}
}
// SetLocal represents the WASM set_local instruction.
type SetLocal struct {
Index uint32
}
// Op returns the opcode of the instruction.
func (SetLocal) Op() opcode.Opcode {
return opcode.SetLocal
}
// ImmediateArgs returns the index of the local variable to set with the top of
// the stack.
func (i SetLocal) ImmediateArgs() []any {
return []any{i.Index}
}
// TeeLocal represents the WASM tee_local instruction.
type TeeLocal struct {
Index uint32
}
// Op returns the opcode of the instruction.
func (TeeLocal) Op() opcode.Opcode {
return opcode.TeeLocal
}
// ImmediateArgs returns the index of the local variable to "tee" with the top of
// the stack (like set, but retaining the top of the stack).
func (i TeeLocal) ImmediateArgs() []any {
return []any{i.Index}
}
@@ -0,0 +1,385 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package module
import (
"fmt"
"strings"
"github.com/open-policy-agent/opa/internal/wasm/instruction"
"github.com/open-policy-agent/opa/internal/wasm/types"
)
type (
// Module represents a WASM module.
Module struct {
Version uint32
Start StartSection
Type TypeSection
Import ImportSection
Function FunctionSection
Table TableSection
Memory MemorySection
Element ElementSection
Global GlobalSection
Export ExportSection
Code RawCodeSection
Data DataSection
Customs []CustomSection
Names NameSection
}
// StartSection represents a WASM start section.
StartSection struct {
FuncIndex *uint32
}
// TypeSection represents a WASM type section.
TypeSection struct {
Functions []FunctionType
}
// ImportSection represents a WASM import section.
ImportSection struct {
Imports []Import
}
// FunctionSection represents a WASM function section.
FunctionSection struct {
TypeIndices []uint32
}
// TableSection represents a WASM table section.
TableSection struct {
Tables []Table
}
// MemorySection represents a Wasm memory section.
MemorySection struct {
Memories []Memory
}
// ElementSection represents a WASM element section.
ElementSection struct {
Segments []ElementSegment
}
// GlobalSection represents a WASM global section.
GlobalSection struct {
Globals []Global
}
// ExportSection represents a WASM export section.
ExportSection struct {
Exports []Export
}
// RawCodeSection represents a WASM code section. The code section is left as a
// raw byte sequence.
RawCodeSection struct {
Segments []RawCodeSegment
}
// DataSection represents a WASM data section.
DataSection struct {
Segments []DataSegment
}
// CustomSection represents a WASM custom section.
CustomSection struct {
Name string
Data []byte
}
// NameSection represents the WASM custom section "name".
NameSection struct {
Module string
Functions []NameMap
Locals []LocalNameMap
}
// NameMap maps function or local arg indices to their names.
NameMap struct {
Index uint32
Name string
}
// LocalNameMap maps function indices, and argument indices for the args
// of the indexed function to their names.
LocalNameMap struct {
FuncIndex uint32
NameMap
}
// FunctionType represents a WASM function type definition.
FunctionType struct {
Params []types.ValueType
Results []types.ValueType
}
// Import represents a WASM import statement.
Import struct {
Module string
Name string
Descriptor ImportDescriptor
}
// ImportDescriptor represents a WASM import descriptor.
ImportDescriptor interface {
fmt.Stringer
Kind() ImportDescriptorType
}
// ImportDescriptorType defines allowed kinds of import descriptors.
ImportDescriptorType int
// FunctionImport represents a WASM function import statement.
FunctionImport struct {
Func uint32
}
// MemoryImport represents a WASM memory import statement.
MemoryImport struct {
Mem MemType
}
// MemType defines the attributes of a memory import.
MemType struct {
Lim Limit
}
// TableImport represents a WASM table import statement.
TableImport struct {
Type types.ElementType
Lim Limit
}
// ElementSegment represents a WASM element segment.
ElementSegment struct {
Index uint32
Offset Expr
Indices []uint32
}
// GlobalImport represents a WASM global variable import statement.
GlobalImport struct {
Type types.ValueType
Mutable bool
}
// Limit represents a WASM limit.
Limit struct {
Min uint32
Max *uint32
}
// Table represents a WASM table statement.
Table struct {
Type types.ElementType
Lim Limit
}
// Memory represents a Wasm memory statement.
Memory struct {
Lim Limit
}
// Global represents a WASM global statement.
Global struct {
Type types.ValueType
Mutable bool
Init Expr
}
// Export represents a WASM export statement.
Export struct {
Name string
Descriptor ExportDescriptor
}
// ExportDescriptor represents a WASM export descriptor.
ExportDescriptor struct {
Type ExportDescriptorType
Index uint32
}
// ExportDescriptorType defines the allowed kinds of export descriptors.
ExportDescriptorType int
// RawCodeSegment represents a binary-encoded WASM code segment.
RawCodeSegment struct {
Code []byte
}
// DataSegment represents a WASM data segment.
DataSegment struct {
Index uint32
Offset Expr
Init []byte
}
// Expr represents a WASM expression.
Expr struct {
Instrs []instruction.Instruction
}
// CodeEntry represents a code segment entry.
CodeEntry struct {
Func Function
}
// Function represents a function in a code segment.
Function struct {
Locals []LocalDeclaration
Expr Expr
}
// LocalDeclaration represents a local variable declaration.
LocalDeclaration struct {
Count uint32
Type types.ValueType
}
)
// Defines the allowed kinds of imports.
const (
FunctionImportType ImportDescriptorType = iota
TableImportType
MemoryImportType
GlobalImportType
)
func (x ImportDescriptorType) String() string {
switch x {
case FunctionImportType:
return "func"
case TableImportType:
return "table"
case MemoryImportType:
return "memory"
case GlobalImportType:
return "global"
}
panic("illegal value")
}
// Defines the allowed kinds of exports.
const (
FunctionExportType ExportDescriptorType = iota
TableExportType
MemoryExportType
GlobalExportType
)
func (x ExportDescriptorType) String() string {
switch x {
case FunctionExportType:
return "func"
case TableExportType:
return "table"
case MemoryExportType:
return "memory"
case GlobalExportType:
return "global"
}
panic("illegal value")
}
// Kind returns the function import type kind.
func (FunctionImport) Kind() ImportDescriptorType {
return FunctionImportType
}
func (i FunctionImport) String() string {
return fmt.Sprintf("%v[type=%v]", i.Kind(), i.Func)
}
// Kind returns the memory import type kind.
func (MemoryImport) Kind() ImportDescriptorType {
return MemoryImportType
}
func (i MemoryImport) String() string {
return fmt.Sprintf("%v[%v]", i.Kind(), i.Mem.Lim)
}
// Kind returns the table import type kind.
func (TableImport) Kind() ImportDescriptorType {
return TableImportType
}
func (i TableImport) String() string {
return fmt.Sprintf("%v[%v, %v]", i.Kind(), i.Type, i.Lim)
}
// Kind returns the global import type kind.
func (GlobalImport) Kind() ImportDescriptorType {
return GlobalImportType
}
func (i GlobalImport) String() string {
return fmt.Sprintf("%v[%v, mut=%v]", i.Kind(), i.Type, i.Mutable)
}
func (tpe FunctionType) String() string {
params := make([]string, len(tpe.Params))
results := make([]string, len(tpe.Results))
for i := range tpe.Params {
params[i] = tpe.Params[i].String()
}
for i := range tpe.Results {
results[i] = tpe.Results[i].String()
}
return "(" + strings.Join(params, ", ") + ") -> (" + strings.Join(results, ", ") + ")"
}
// Equal returns true if tpe equals other.
func (tpe FunctionType) Equal(other FunctionType) bool {
if len(tpe.Params) != len(other.Params) || len(tpe.Results) != len(other.Results) {
return false
}
for i := range tpe.Params {
if tpe.Params[i] != other.Params[i] {
return false
}
}
for i := range tpe.Results {
if tpe.Results[i] != other.Results[i] {
return false
}
}
return true
}
func (imp Import) String() string {
return fmt.Sprintf("%v %v.%v", imp.Descriptor.String(), imp.Module, imp.Name)
}
func (exp Export) String() string {
return fmt.Sprintf("%v[%v] %v", exp.Descriptor.Type, exp.Descriptor.Index, exp.Name)
}
func (seg RawCodeSegment) String() string {
return fmt.Sprintf("<code %d bytes>", len(seg.Code))
}
func (seg DataSegment) String() string {
return fmt.Sprintf("<data index=%v [%v] len=%d bytes>", seg.Index, seg.Offset, len(seg.Init))
}
func (e Expr) String() string {
return fmt.Sprintf("%d instr(s)", len(e.Instrs))
}
func (lim Limit) String() string {
if lim.Max == nil {
return fmt.Sprintf("min=%v", lim.Min)
}
return fmt.Sprintf("min=%v max=%v", lim.Min, lim.Max)
}
@@ -0,0 +1,84 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package module
import (
"encoding/hex"
"fmt"
"io"
)
// PrettyOption defines options for controlling pretty printing.
type PrettyOption struct {
Contents bool // show raw byte content of data+code sections.
}
// Pretty writes a human-readable representation of m to w.
func Pretty(w io.Writer, m *Module, opts ...PrettyOption) {
fmt.Fprintln(w, "version:", m.Version)
fmt.Fprintln(w, "types:")
for _, fn := range m.Type.Functions {
fmt.Fprintln(w, " -", fn)
}
fmt.Fprintln(w, "imports:")
for i, imp := range m.Import.Imports {
if imp.Descriptor.Kind() == FunctionImportType {
fmt.Printf(" - [%d] %v\n", i, imp)
} else {
fmt.Fprintln(w, " -", imp)
}
}
fmt.Fprintln(w, "functions:")
for _, fn := range m.Function.TypeIndices {
if fn >= uint32(len(m.Type.Functions)) {
fmt.Fprintln(w, " -", "???")
} else {
fmt.Fprintln(w, " -", m.Type.Functions[fn])
}
}
fmt.Fprintln(w, "exports:")
for _, exp := range m.Export.Exports {
fmt.Fprintln(w, " -", exp)
}
fmt.Fprintln(w, "code:")
for _, seg := range m.Code.Segments {
fmt.Fprintln(w, " -", seg)
}
fmt.Fprintln(w, "data:")
for _, seg := range m.Data.Segments {
fmt.Fprintln(w, " -", seg)
}
if len(opts) == 0 {
return
}
fmt.Fprintln(w)
for _, opt := range opts {
if opt.Contents {
newline := false
if len(m.Data.Segments) > 0 {
fmt.Fprintln(w, "data section:")
for _, seg := range m.Data.Segments {
if newline {
fmt.Fprintln(w)
}
fmt.Fprintln(w, hex.Dump(seg.Init))
newline = true
}
newline = false
}
if len(m.Code.Segments) > 0 {
fmt.Fprintln(w, "code section:")
for _, seg := range m.Code.Segments {
if newline {
fmt.Fprintln(w)
}
fmt.Fprintln(w, hex.Dump(seg.Code))
newline = true
}
newline = false
}
}
}
}
@@ -0,0 +1,218 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package opcode contains constants and utilities for working with WASM opcodes.
package opcode
// Opcode represents a WASM instruction opcode.
type Opcode byte
// Control instructions.
const (
Unreachable Opcode = iota
Nop
Block
Loop
If
Else
)
const (
// End defines the special end WASM opcode.
End Opcode = 0x0B
)
// Extended control instructions.
const (
Br Opcode = iota + 0x0C
BrIf
BrTable
Return
Call
CallIndirect
)
// Parameter instructions.
const (
Drop Opcode = iota + 0x1A
Select
)
// Variable instructions.
const (
GetLocal Opcode = iota + 0x20
SetLocal
TeeLocal
GetGlobal
SetGlobal
)
// Memory instructions.
const (
I32Load Opcode = iota + 0x28
I64Load
F32Load
F64Load
I32Load8S
I32Load8U
I32Load16S
I32Load16U
I64Load8S
I64Load8U
I64Load16S
I64Load16U
I64Load32S
I64Load32U
I32Store
I64Store
F32Store
F64Store
I32Store8
I32Store16
I64Store8
I64Store16
I64Store32
MemorySize
MemoryGrow
)
// Numeric instructions.
const (
I32Const Opcode = iota + 0x41
I64Const
F32Const
F64Const
I32Eqz
I32Eq
I32Ne
I32LtS
I32LtU
I32GtS
I32GtU
I32LeS
I32LeU
I32GeS
I32GeU
I64Eqz
I64Eq
I64Ne
I64LtS
I64LtU
I64GtS
I64GtU
I64LeS
I64LeU
I64GeS
I64GeU
F32Eq
F32Ne
F32Lt
F32Gt
F32Le
F32Ge
F64Eq
F64Ne
F64Lt
F64Gt
F64Le
F64Ge
I32Clz
I32Ctz
I32Popcnt
I32Add
I32Sub
I32Mul
I32DivS
I32DivU
I32RemS
I32RemU
I32And
I32Or
I32Xor
I32Shl
I32ShrS
I32ShrU
I32Rotl
I32Rotr
I64Clz
I64Ctz
I64Popcnt
I64Add
I64Sub
I64Mul
I64DivS
I64DivU
I64RemS
I64RemU
I64And
I64Or
I64Xor
I64Shl
I64ShrS
I64ShrU
I64Rotl
I64Rotr
F32Abs
F32Neg
F32Ceil
F32Floor
F32Trunc
F32Nearest
F32Sqrt
F32Add
F32Sub
F32Mul
F32Div
F32Min
F32Max
F32Copysign
F64Abs
F64Neg
F64Ceil
F64Floor
F64Trunc
F64Nearest
F64Sqrt
F64Add
F64Sub
F64Mul
F64Div
F64Min
F64Max
F64Copysign
I32WrapI64
I32TruncSF32
I32TruncUF32
I32TruncSF64
I32TruncUF64
I64ExtendSI32
I64ExtendUI32
I64TruncSF32
I64TruncUF32
I64TruncSF64
I64TruncUF64
F32ConvertSI32
F32ConvertUI32
F32ConvertSI64
F32ConvertUI64
F32DemoteF64
F64ConvertSI32
F64ConvertUI32
F64ConvertSI64
F64ConvertUI64
F64PromoteF32
I32ReinterpretF32
I64ReinterpretF64
F32ReinterpretI32
F64ReinterpretI64
)
@@ -0,0 +1,12 @@
// Copyright 2021 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// +build opa_wasm generate
package capabilities
// ABIVersions returns the ABI versions that this SDK supports
func ABIVersions() [][2]int {
return [][2]int{{1, 1}, {1, 2}}
}
@@ -0,0 +1,14 @@
// Copyright 2021 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
//go:build !opa_wasm && !generate
// +build !opa_wasm,!generate
package capabilities
// ABIVersions returns the supported Wasm ABI versions for this
// build: none
func ABIVersions() [][2]int {
return nil
}
@@ -0,0 +1,36 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package types defines the WASM value type constants.
package types
// ValueType represents an intrinsic value in WASM.
type ValueType int
// Defines the intrinsic value types.
const (
I32 ValueType = iota
I64
F32
F64
)
func (tpe ValueType) String() string {
if tpe == I32 {
return "i32"
} else if tpe == I64 {
return "i64"
} else if tpe == F32 {
return "f32"
}
return "f64"
}
// ElementType defines the type of table elements.
type ElementType int
const (
// Anyfunc is the union of all table types.
Anyfunc ElementType = iota
)
@@ -0,0 +1,18 @@
// Copyright 2020 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package util
// PageSize represents the WASM page size in bytes.
const PageSize = 65535
// Pages converts a byte size to Pages, rounding up as necessary.
func Pages(n uint32) uint32 {
pages := n / PageSize
if pages*PageSize == n {
return pages
}
return pages + 1
}