mirror of
https://github.com/containers/skopeo.git
synced 2025-04-28 03:10:18 +00:00
568 lines
12 KiB
Go
568 lines
12 KiB
Go
package jsonschema
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"net/mail"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Formats is a registry of functions, which know how to validate
|
|
// a specific format.
|
|
//
|
|
// New Formats can be registered by adding to this map. Key is format name,
|
|
// value is function that knows how to validate that format.
|
|
var Formats = map[string]func(interface{}) bool{
|
|
"date-time": isDateTime,
|
|
"date": isDate,
|
|
"time": isTime,
|
|
"duration": isDuration,
|
|
"period": isPeriod,
|
|
"hostname": isHostname,
|
|
"email": isEmail,
|
|
"ip-address": isIPV4,
|
|
"ipv4": isIPV4,
|
|
"ipv6": isIPV6,
|
|
"uri": isURI,
|
|
"iri": isURI,
|
|
"uri-reference": isURIReference,
|
|
"uriref": isURIReference,
|
|
"iri-reference": isURIReference,
|
|
"uri-template": isURITemplate,
|
|
"regex": isRegex,
|
|
"json-pointer": isJSONPointer,
|
|
"relative-json-pointer": isRelativeJSONPointer,
|
|
"uuid": isUUID,
|
|
}
|
|
|
|
// isDateTime tells whether given string is a valid date representation
|
|
// as defined by RFC 3339, section 5.6.
|
|
//
|
|
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6, for details
|
|
func isDateTime(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if len(s) < 20 { // yyyy-mm-ddThh:mm:ssZ
|
|
return false
|
|
}
|
|
if s[10] != 'T' && s[10] != 't' {
|
|
return false
|
|
}
|
|
return isDate(s[:10]) && isTime(s[11:])
|
|
}
|
|
|
|
// isDate tells whether given string is a valid full-date production
|
|
// as defined by RFC 3339, section 5.6.
|
|
//
|
|
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6, for details
|
|
func isDate(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
_, err := time.Parse("2006-01-02", s)
|
|
return err == nil
|
|
}
|
|
|
|
// isTime tells whether given string is a valid full-time production
|
|
// as defined by RFC 3339, section 5.6.
|
|
//
|
|
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6, for details
|
|
func isTime(v interface{}) bool {
|
|
str, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// golang time package does not support leap seconds.
|
|
// so we are parsing it manually here.
|
|
|
|
// hh:mm:ss
|
|
// 01234567
|
|
if len(str) < 9 || str[2] != ':' || str[5] != ':' {
|
|
return false
|
|
}
|
|
isInRange := func(str string, min, max int) (int, bool) {
|
|
n, err := strconv.Atoi(str)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
if n < min || n > max {
|
|
return 0, false
|
|
}
|
|
return n, true
|
|
}
|
|
var h, m, s int
|
|
if h, ok = isInRange(str[0:2], 0, 23); !ok {
|
|
return false
|
|
}
|
|
if m, ok = isInRange(str[3:5], 0, 59); !ok {
|
|
return false
|
|
}
|
|
if s, ok = isInRange(str[6:8], 0, 60); !ok {
|
|
return false
|
|
}
|
|
str = str[8:]
|
|
|
|
// parse secfrac if present
|
|
if str[0] == '.' {
|
|
// dot following more than one digit
|
|
str = str[1:]
|
|
var numDigits int
|
|
for str != "" {
|
|
if str[0] < '0' || str[0] > '9' {
|
|
break
|
|
}
|
|
numDigits++
|
|
str = str[1:]
|
|
}
|
|
if numDigits == 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if len(str) == 0 {
|
|
return false
|
|
}
|
|
|
|
if str[0] == 'z' || str[0] == 'Z' {
|
|
if len(str) != 1 {
|
|
return false
|
|
}
|
|
} else {
|
|
// time-numoffset
|
|
// +hh:mm
|
|
// 012345
|
|
if len(str) != 6 || str[3] != ':' {
|
|
return false
|
|
}
|
|
|
|
var sign int
|
|
if str[0] == '+' {
|
|
sign = -1
|
|
} else if str[0] == '-' {
|
|
sign = +1
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
var zh, zm int
|
|
if zh, ok = isInRange(str[1:3], 0, 23); !ok {
|
|
return false
|
|
}
|
|
if zm, ok = isInRange(str[4:6], 0, 59); !ok {
|
|
return false
|
|
}
|
|
|
|
// apply timezone offset
|
|
hm := (h*60 + m) + sign*(zh*60+zm)
|
|
if hm < 0 {
|
|
hm += 24 * 60
|
|
}
|
|
h, m = hm/60, hm%60
|
|
}
|
|
|
|
// check leapsecond
|
|
if s == 60 { // leap second
|
|
if h != 23 || m != 59 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// isDuration tells whether given string is a valid duration format
|
|
// from the ISO 8601 ABNF as given in Appendix A of RFC 3339.
|
|
//
|
|
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A, for details
|
|
func isDuration(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if len(s) == 0 || s[0] != 'P' {
|
|
return false
|
|
}
|
|
s = s[1:]
|
|
parseUnits := func() (units string, ok bool) {
|
|
for len(s) > 0 && s[0] != 'T' {
|
|
digits := false
|
|
for {
|
|
if len(s) == 0 {
|
|
break
|
|
}
|
|
if s[0] < '0' || s[0] > '9' {
|
|
break
|
|
}
|
|
digits = true
|
|
s = s[1:]
|
|
}
|
|
if !digits || len(s) == 0 {
|
|
return units, false
|
|
}
|
|
units += s[:1]
|
|
s = s[1:]
|
|
}
|
|
return units, true
|
|
}
|
|
units, ok := parseUnits()
|
|
if !ok {
|
|
return false
|
|
}
|
|
if units == "W" {
|
|
return len(s) == 0 // P_W
|
|
}
|
|
if len(units) > 0 {
|
|
if strings.Index("YMD", units) == -1 {
|
|
return false
|
|
}
|
|
if len(s) == 0 {
|
|
return true // "P" dur-date
|
|
}
|
|
}
|
|
if len(s) == 0 || s[0] != 'T' {
|
|
return false
|
|
}
|
|
s = s[1:]
|
|
units, ok = parseUnits()
|
|
return ok && len(s) == 0 && len(units) > 0 && strings.Index("HMS", units) != -1
|
|
}
|
|
|
|
// isPeriod tells whether given string is a valid period format
|
|
// from the ISO 8601 ABNF as given in Appendix A of RFC 3339.
|
|
//
|
|
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A, for details
|
|
func isPeriod(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
slash := strings.IndexByte(s, '/')
|
|
if slash == -1 {
|
|
return false
|
|
}
|
|
start, end := s[:slash], s[slash+1:]
|
|
if isDateTime(start) {
|
|
return isDateTime(end) || isDuration(end)
|
|
}
|
|
return isDuration(start) && isDateTime(end)
|
|
}
|
|
|
|
// isHostname tells whether given string is a valid representation
|
|
// for an Internet host name, as defined by RFC 1034 section 3.1 and
|
|
// RFC 1123 section 2.1.
|
|
//
|
|
// See https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names, for details.
|
|
func isHostname(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
// entire hostname (including the delimiting dots but not a trailing dot) has a maximum of 253 ASCII characters
|
|
s = strings.TrimSuffix(s, ".")
|
|
if len(s) > 253 {
|
|
return false
|
|
}
|
|
|
|
// Hostnames are composed of series of labels concatenated with dots, as are all domain names
|
|
for _, label := range strings.Split(s, ".") {
|
|
// Each label must be from 1 to 63 characters long
|
|
if labelLen := len(label); labelLen < 1 || labelLen > 63 {
|
|
return false
|
|
}
|
|
|
|
// labels must not start with a hyphen
|
|
// RFC 1123 section 2.1: restriction on the first character
|
|
// is relaxed to allow either a letter or a digit
|
|
if first := s[0]; first == '-' {
|
|
return false
|
|
}
|
|
|
|
// must not end with a hyphen
|
|
if label[len(label)-1] == '-' {
|
|
return false
|
|
}
|
|
|
|
// labels may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner),
|
|
// the digits '0' through '9', and the hyphen ('-')
|
|
for _, c := range label {
|
|
if valid := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '-'); !valid {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// isEmail tells whether given string is a valid Internet email address
|
|
// as defined by RFC 5322, section 3.4.1.
|
|
//
|
|
// See https://en.wikipedia.org/wiki/Email_address, for details.
|
|
func isEmail(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
// entire email address to be no more than 254 characters long
|
|
if len(s) > 254 {
|
|
return false
|
|
}
|
|
|
|
// email address is generally recognized as having two parts joined with an at-sign
|
|
at := strings.LastIndexByte(s, '@')
|
|
if at == -1 {
|
|
return false
|
|
}
|
|
local := s[0:at]
|
|
domain := s[at+1:]
|
|
|
|
// local part may be up to 64 characters long
|
|
if len(local) > 64 {
|
|
return false
|
|
}
|
|
|
|
// domain if enclosed in brackets, must match an IP address
|
|
if len(domain) >= 2 && domain[0] == '[' && domain[len(domain)-1] == ']' {
|
|
ip := domain[1 : len(domain)-1]
|
|
if strings.HasPrefix(ip, "IPv6:") {
|
|
return isIPV6(strings.TrimPrefix(ip, "IPv6:"))
|
|
}
|
|
return isIPV4(ip)
|
|
}
|
|
|
|
// domain must match the requirements for a hostname
|
|
if !isHostname(domain) {
|
|
return false
|
|
}
|
|
|
|
_, err := mail.ParseAddress(s)
|
|
return err == nil
|
|
}
|
|
|
|
// isIPV4 tells whether given string is a valid representation of an IPv4 address
|
|
// according to the "dotted-quad" ABNF syntax as defined in RFC 2673, section 3.2.
|
|
func isIPV4(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
groups := strings.Split(s, ".")
|
|
if len(groups) != 4 {
|
|
return false
|
|
}
|
|
for _, group := range groups {
|
|
n, err := strconv.Atoi(group)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if n < 0 || n > 255 {
|
|
return false
|
|
}
|
|
if n != 0 && group[0] == '0' {
|
|
return false // leading zeroes should be rejected, as they are treated as octals
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isIPV6 tells whether given string is a valid representation of an IPv6 address
|
|
// as defined in RFC 2373, section 2.2.
|
|
func isIPV6(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if !strings.Contains(s, ":") {
|
|
return false
|
|
}
|
|
return net.ParseIP(s) != nil
|
|
}
|
|
|
|
// isURI tells whether given string is valid URI, according to RFC 3986.
|
|
func isURI(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
u, err := urlParse(s)
|
|
return err == nil && u.IsAbs()
|
|
}
|
|
|
|
func urlParse(s string) (*url.URL, error) {
|
|
u, err := url.Parse(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// if hostname is ipv6, validate it
|
|
hostname := u.Hostname()
|
|
if strings.IndexByte(hostname, ':') != -1 {
|
|
if strings.IndexByte(u.Host, '[') == -1 || strings.IndexByte(u.Host, ']') == -1 {
|
|
return nil, errors.New("ipv6 address is not enclosed in brackets")
|
|
}
|
|
if !isIPV6(hostname) {
|
|
return nil, errors.New("invalid ipv6 address")
|
|
}
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// isURIReference tells whether given string is a valid URI Reference
|
|
// (either a URI or a relative-reference), according to RFC 3986.
|
|
func isURIReference(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
_, err := urlParse(s)
|
|
return err == nil && !strings.Contains(s, `\`)
|
|
}
|
|
|
|
// isURITemplate tells whether given string is a valid URI Template
|
|
// according to RFC6570.
|
|
//
|
|
// Current implementation does minimal validation.
|
|
func isURITemplate(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
u, err := urlParse(s)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, item := range strings.Split(u.RawPath, "/") {
|
|
depth := 0
|
|
for _, ch := range item {
|
|
switch ch {
|
|
case '{':
|
|
depth++
|
|
if depth != 1 {
|
|
return false
|
|
}
|
|
case '}':
|
|
depth--
|
|
if depth != 0 {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
if depth != 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isRegex tells whether given string is a valid regular expression,
|
|
// according to the ECMA 262 regular expression dialect.
|
|
//
|
|
// The implementation uses go-lang regexp package.
|
|
func isRegex(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
_, err := regexp.Compile(s)
|
|
return err == nil
|
|
}
|
|
|
|
// isJSONPointer tells whether given string is a valid JSON Pointer.
|
|
//
|
|
// Note: It returns false for JSON Pointer URI fragments.
|
|
func isJSONPointer(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if s != "" && !strings.HasPrefix(s, "/") {
|
|
return false
|
|
}
|
|
for _, item := range strings.Split(s, "/") {
|
|
for i := 0; i < len(item); i++ {
|
|
if item[i] == '~' {
|
|
if i == len(item)-1 {
|
|
return false
|
|
}
|
|
switch item[i+1] {
|
|
case '0', '1':
|
|
// valid
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isRelativeJSONPointer tells whether given string is a valid Relative JSON Pointer.
|
|
//
|
|
// see https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
|
|
func isRelativeJSONPointer(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if s == "" {
|
|
return false
|
|
}
|
|
if s[0] == '0' {
|
|
s = s[1:]
|
|
} else if s[0] >= '0' && s[0] <= '9' {
|
|
for s != "" && s[0] >= '0' && s[0] <= '9' {
|
|
s = s[1:]
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
return s == "#" || isJSONPointer(s)
|
|
}
|
|
|
|
// isUUID tells whether given string is a valid uuid format
|
|
// as specified in RFC4122.
|
|
//
|
|
// see https://datatracker.ietf.org/doc/html/rfc4122#page-4, for details
|
|
func isUUID(v interface{}) bool {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
parseHex := func(n int) bool {
|
|
for n > 0 {
|
|
if len(s) == 0 {
|
|
return false
|
|
}
|
|
hex := (s[0] >= '0' && s[0] <= '9') || (s[0] >= 'a' && s[0] <= 'f') || (s[0] >= 'A' && s[0] <= 'F')
|
|
if !hex {
|
|
return false
|
|
}
|
|
s = s[1:]
|
|
n--
|
|
}
|
|
return true
|
|
}
|
|
groups := []int{8, 4, 4, 4, 12}
|
|
for i, numDigits := range groups {
|
|
if !parseHex(numDigits) {
|
|
return false
|
|
}
|
|
if i == len(groups)-1 {
|
|
break
|
|
}
|
|
if len(s) == 0 || s[0] != '-' {
|
|
return false
|
|
}
|
|
s = s[1:]
|
|
}
|
|
return len(s) == 0
|
|
}
|