github.com/kaptinlin/jsonschema@v0.4.6/formats.go (about)

     1  // Credit to https://github.com/santhosh-tekuri/jsonschema
     2  package jsonschema
     3  
     4  import (
     5  	"net"
     6  	"net/mail"
     7  	"net/url"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  )
    13  
    14  // Formats is a registry of functions, which know how to validate
    15  // a specific format.
    16  //
    17  // New Formats can be registered by adding to this map. Key is format name,
    18  // value is function that knows how to validate that format.
    19  var Formats = map[string]func(interface{}) bool{
    20  	"date-time":             IsDateTime,
    21  	"date":                  IsDate,
    22  	"time":                  IsTime,
    23  	"duration":              IsDuration,
    24  	"period":                IsPeriod,
    25  	"hostname":              IsHostname,
    26  	"email":                 IsEmail,
    27  	"ip-address":            IsIPV4,
    28  	"ipv4":                  IsIPV4,
    29  	"ipv6":                  IsIPV6,
    30  	"uri":                   IsURI,
    31  	"iri":                   IsURI,
    32  	"uri-reference":         IsURIReference,
    33  	"uriref":                IsURIReference,
    34  	"iri-reference":         IsURIReference,
    35  	"uri-template":          IsURITemplate,
    36  	"json-pointer":          IsJSONPointer,
    37  	"relative-json-pointer": IsRelativeJSONPointer,
    38  	"uuid":                  IsUUID,
    39  	"regex":                 IsRegex,
    40  	"unknown":               func(interface{}) bool { return true },
    41  }
    42  
    43  // IsDateTime tells whether given string is a valid date representation
    44  // as defined by RFC 3339, section 5.6.
    45  //
    46  // see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6, for details
    47  func IsDateTime(v interface{}) bool {
    48  	s, ok := v.(string)
    49  	if !ok {
    50  		return true
    51  	}
    52  	if len(s) < 20 { // yyyy-mm-ddThh:mm:ssZ
    53  		return false
    54  	}
    55  	if s[10] != 'T' && s[10] != 't' {
    56  		return false
    57  	}
    58  	return IsDate(s[:10]) && IsTime(s[11:])
    59  }
    60  
    61  // IsDate tells whether given string is a valid full-date production
    62  // as defined by RFC 3339, section 5.6.
    63  //
    64  // see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6, for details
    65  func IsDate(v interface{}) bool {
    66  	s, ok := v.(string)
    67  	if !ok {
    68  		return true
    69  	}
    70  	_, err := time.Parse("2006-01-02", s)
    71  	return err == nil
    72  }
    73  
    74  // IsTime tells whether given string is a valid full-time production
    75  // as defined by RFC 3339, section 5.6.
    76  //
    77  // see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6, for details
    78  func IsTime(v interface{}) bool {
    79  	str, ok := v.(string)
    80  	if !ok {
    81  		return true
    82  	}
    83  
    84  	// golang time package does not support leap seconds.
    85  	// so we are parsing it manually here.
    86  
    87  	// hh:mm:ss
    88  	// 01234567
    89  	if len(str) < 9 || str[2] != ':' || str[5] != ':' {
    90  		return false
    91  	}
    92  	isInRange := func(str string, min, max int) (int, bool) {
    93  		n, err := strconv.Atoi(str)
    94  		if err != nil {
    95  			return 0, false
    96  		}
    97  		if n < min || n > max {
    98  			return 0, false
    99  		}
   100  		return n, true
   101  	}
   102  	var h, m, s int
   103  	if h, ok = isInRange(str[0:2], 0, 23); !ok {
   104  		return false
   105  	}
   106  	if m, ok = isInRange(str[3:5], 0, 59); !ok {
   107  		return false
   108  	}
   109  	if s, ok = isInRange(str[6:8], 0, 60); !ok {
   110  		return false
   111  	}
   112  	str = str[8:]
   113  
   114  	// parse secfrac if present
   115  	if str[0] == '.' {
   116  		// dot following more than one digit
   117  		str = str[1:]
   118  		var numDigits int
   119  		for str != "" {
   120  			if str[0] < '0' || str[0] > '9' {
   121  				break
   122  			}
   123  			numDigits++
   124  			str = str[1:]
   125  		}
   126  		if numDigits == 0 {
   127  			return false
   128  		}
   129  	}
   130  
   131  	if len(str) == 0 {
   132  		return false
   133  	}
   134  
   135  	if str[0] == 'z' || str[0] == 'Z' {
   136  		if len(str) != 1 {
   137  			return false
   138  		}
   139  	} else {
   140  		// time-numoffset
   141  		// +hh:mm
   142  		// 012345
   143  		if len(str) != 6 || str[3] != ':' {
   144  			return false
   145  		}
   146  
   147  		var sign int
   148  		switch str[0] {
   149  		case '+':
   150  			sign = -1
   151  		case '-':
   152  			sign = +1
   153  		default:
   154  			return false
   155  		}
   156  
   157  		var zh, zm int
   158  		ok := false
   159  		if zh, ok = isInRange(str[1:3], 0, 23); !ok {
   160  			return false
   161  		}
   162  		if zm, ok = isInRange(str[4:6], 0, 59); !ok {
   163  			return false
   164  		}
   165  
   166  		// apply timezone offset
   167  		hm := (h*60 + m) + sign*(zh*60+zm)
   168  		if hm < 0 {
   169  			hm += 24 * 60
   170  		}
   171  		h, m = hm/60, hm%60
   172  	}
   173  
   174  	// check leapsecond
   175  	if s == 60 { // leap second
   176  		if h != 23 || m != 59 {
   177  			return false
   178  		}
   179  	}
   180  
   181  	return true
   182  }
   183  
   184  // IsDuration tells whether given string is a valid duration format
   185  // from the ISO 8601 ABNF as given in Appendix A of RFC 3339.
   186  //
   187  // see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A, for details
   188  func IsDuration(v interface{}) bool {
   189  	s, ok := v.(string)
   190  	if !ok {
   191  		return true
   192  	}
   193  	if len(s) == 0 || s[0] != 'P' {
   194  		return false
   195  	}
   196  	s = s[1:]
   197  	parseUnits := func() (units string, ok bool) {
   198  		for len(s) > 0 && s[0] != 'T' {
   199  			digits := false
   200  			for len(s) != 0 {
   201  				if s[0] < '0' || s[0] > '9' {
   202  					break
   203  				}
   204  				digits = true
   205  				s = s[1:]
   206  			}
   207  			if !digits || len(s) == 0 {
   208  				return units, false
   209  			}
   210  			units += s[:1]
   211  			s = s[1:]
   212  		}
   213  		return units, true
   214  	}
   215  	units, ok := parseUnits()
   216  	if !ok {
   217  		return false
   218  	}
   219  	if units == "W" {
   220  		return len(s) == 0 // P_W
   221  	}
   222  	if len(units) > 0 {
   223  		if !strings.Contains("YMD", units) { //nolint:gocritic
   224  			return false
   225  		}
   226  		if len(s) == 0 {
   227  			return true // "P" dur-date
   228  		}
   229  	}
   230  	if len(s) == 0 || s[0] != 'T' {
   231  		return false
   232  	}
   233  	s = s[1:]
   234  	units, ok = parseUnits()
   235  	return ok && len(s) == 0 && len(units) > 0 && strings.Contains("HMS", units) //nolint:gocritic
   236  }
   237  
   238  // IsPeriod tells whether given string is a valid period format
   239  // from the ISO 8601 ABNF as given in Appendix A of RFC 3339.
   240  //
   241  // see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A, for details
   242  func IsPeriod(v interface{}) bool {
   243  	s, ok := v.(string)
   244  	if !ok {
   245  		return true
   246  	}
   247  	slash := strings.IndexByte(s, '/')
   248  	if slash == -1 {
   249  		return false
   250  	}
   251  	start, end := s[:slash], s[slash+1:]
   252  	if IsDateTime(start) {
   253  		return IsDateTime(end) || IsDuration(end)
   254  	}
   255  	return IsDuration(start) && IsDateTime(end)
   256  }
   257  
   258  // IsHostname tells whether given string is a valid representation
   259  // for an Internet host name, as defined by RFC 1034 section 3.1 and
   260  // RFC 1123 section 2.1.
   261  //
   262  // See https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names, for details.
   263  func IsHostname(v interface{}) bool {
   264  	s, ok := v.(string)
   265  	if !ok {
   266  		return true
   267  	}
   268  	// entire hostname (including the delimiting dots but not a trailing dot) has a maximum of 253 ASCII characters
   269  	s = strings.TrimSuffix(s, ".")
   270  	if len(s) > 253 {
   271  		return false
   272  	}
   273  
   274  	// Hostnames are composed of series of labels concatenated with dots, as are all domain names
   275  	for _, label := range strings.Split(s, ".") {
   276  		// Each label must be from 1 to 63 characters long
   277  		if labelLen := len(label); labelLen < 1 || labelLen > 63 {
   278  			return false
   279  		}
   280  
   281  		// labels must not start with a hyphen
   282  		// RFC 1123 section 2.1: restriction on the first character
   283  		// is relaxed to allow either a letter or a digit
   284  		if first := s[0]; first == '-' {
   285  			return false
   286  		}
   287  
   288  		// must not end with a hyphen
   289  		if label[len(label)-1] == '-' {
   290  			return false
   291  		}
   292  
   293  		// labels may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner),
   294  		// the digits '0' through '9', and the hyphen ('-')
   295  		for _, c := range label {
   296  			if valid := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '-'); !valid {
   297  				return false
   298  			}
   299  		}
   300  	}
   301  
   302  	return true
   303  }
   304  
   305  // IsEmail tells whether given string is a valid Internet email address
   306  // as defined by RFC 5322, section 3.4.1.
   307  //
   308  // See https://en.wikipedia.org/wiki/Email_address, for details.
   309  func IsEmail(v interface{}) bool {
   310  	s, ok := v.(string)
   311  	if !ok {
   312  		return true
   313  	}
   314  	// entire email address to be no more than 254 characters long
   315  	if len(s) > 254 {
   316  		return false
   317  	}
   318  
   319  	// email address is generally recognized as having two parts joined with an at-sign
   320  	at := strings.LastIndexByte(s, '@')
   321  	if at == -1 {
   322  		return false
   323  	}
   324  	local := s[0:at]
   325  	domain := s[at+1:]
   326  
   327  	// local part may be up to 64 characters long
   328  	if len(local) > 64 {
   329  		return false
   330  	}
   331  
   332  	// domain if enclosed in brackets, must match an IP address
   333  	if len(domain) >= 2 && domain[0] == '[' && domain[len(domain)-1] == ']' {
   334  		ip := domain[1 : len(domain)-1]
   335  		if strings.HasPrefix(ip, "IPv6:") {
   336  			return IsIPV6(strings.TrimPrefix(ip, "IPv6:"))
   337  		}
   338  		return IsIPV4(ip)
   339  	}
   340  
   341  	// domain must match the requirements for a hostname
   342  	if !IsHostname(domain) {
   343  		return false
   344  	}
   345  
   346  	_, err := mail.ParseAddress(s)
   347  	return err == nil
   348  }
   349  
   350  // IsIPV4 tells whether given string is a valid representation of an IPv4 address
   351  // according to the "dotted-quad" ABNF syntax as defined in RFC 2673, section 3.2.
   352  func IsIPV4(v interface{}) bool {
   353  	s, ok := v.(string)
   354  	if !ok {
   355  		return true
   356  	}
   357  	groups := strings.Split(s, ".")
   358  	if len(groups) != 4 {
   359  		return false
   360  	}
   361  	for _, group := range groups {
   362  		n, err := strconv.Atoi(group)
   363  		if err != nil {
   364  			return false
   365  		}
   366  		if n < 0 || n > 255 {
   367  			return false
   368  		}
   369  		if n != 0 && group[0] == '0' {
   370  			return false // leading zeroes should be rejected, as they are treated as octals
   371  		}
   372  	}
   373  	return true
   374  }
   375  
   376  // IsIPV6 tells whether given string is a valid representation of an IPv6 address
   377  // as defined in RFC 2373, section 2.2.
   378  func IsIPV6(v interface{}) bool {
   379  	s, ok := v.(string)
   380  	if !ok {
   381  		return true
   382  	}
   383  	if !strings.Contains(s, ":") {
   384  		return false
   385  	}
   386  	return net.ParseIP(s) != nil
   387  }
   388  
   389  // IsURI tells whether given string is valid URI, according to RFC 3986.
   390  func IsURI(v interface{}) bool {
   391  	s, ok := v.(string)
   392  	if !ok {
   393  		return true
   394  	}
   395  	u, err := urlParse(s)
   396  	return err == nil && u.IsAbs()
   397  }
   398  
   399  func urlParse(s string) (*url.URL, error) {
   400  	u, err := url.Parse(s)
   401  	if err != nil {
   402  		return nil, err
   403  	}
   404  
   405  	// if hostname is ipv6, validate it
   406  	hostname := u.Hostname()
   407  	if strings.IndexByte(hostname, ':') != -1 {
   408  		if strings.IndexByte(u.Host, '[') == -1 || strings.IndexByte(u.Host, ']') == -1 {
   409  			return nil, ErrIPv6AddressNotEnclosed
   410  		}
   411  		if !IsIPV6(hostname) {
   412  			return nil, ErrInvalidIPv6Address
   413  		}
   414  	}
   415  	return u, nil
   416  }
   417  
   418  // IsURIReference tells whether given string is a valid URI Reference
   419  // (either a URI or a relative-reference), according to RFC 3986.
   420  func IsURIReference(v interface{}) bool {
   421  	s, ok := v.(string)
   422  	if !ok {
   423  		return true
   424  	}
   425  	_, err := urlParse(s)
   426  	return err == nil && !strings.Contains(s, `\`)
   427  }
   428  
   429  // IsURITemplate tells whether given string is a valid URI Template
   430  // according to RFC6570.
   431  //
   432  // Current implementation does minimal validation.
   433  func IsURITemplate(v interface{}) bool {
   434  	s, ok := v.(string)
   435  	if !ok {
   436  		return true
   437  	}
   438  	u, err := urlParse(s)
   439  	if err != nil {
   440  		return false
   441  	}
   442  	for _, item := range strings.Split(u.RawPath, "/") {
   443  		depth := 0
   444  		for _, ch := range item {
   445  			switch ch {
   446  			case '{':
   447  				depth++
   448  				if depth != 1 {
   449  					return false
   450  				}
   451  			case '}':
   452  				depth--
   453  				if depth != 0 {
   454  					return false
   455  				}
   456  			}
   457  		}
   458  		if depth != 0 {
   459  			return false
   460  		}
   461  	}
   462  	return true
   463  }
   464  
   465  // IsJSONPointer tells whether given string is a valid JSON Pointer.
   466  //
   467  // Note: It returns false for JSON Pointer URI fragments.
   468  func IsJSONPointer(v interface{}) bool {
   469  	s, ok := v.(string)
   470  	if !ok {
   471  		return true
   472  	}
   473  	if s != "" && !strings.HasPrefix(s, "/") {
   474  		return false
   475  	}
   476  	for _, item := range strings.Split(s, "/") {
   477  		for i := 0; i < len(item); i++ {
   478  			if item[i] == '~' {
   479  				if i == len(item)-1 {
   480  					return false
   481  				}
   482  				switch item[i+1] {
   483  				case '0', '1':
   484  					// valid
   485  				default:
   486  					return false
   487  				}
   488  			}
   489  		}
   490  	}
   491  	return true
   492  }
   493  
   494  // IsRelativeJSONPointer tells whether given string is a valid Relative JSON Pointer.
   495  //
   496  // see https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
   497  func IsRelativeJSONPointer(v interface{}) bool {
   498  	s, ok := v.(string)
   499  	if !ok {
   500  		return true
   501  	}
   502  	if s == "" {
   503  		return false
   504  	}
   505  	switch {
   506  	case s[0] == '0':
   507  		s = s[1:]
   508  	case s[0] >= '0' && s[0] <= '9':
   509  		for s != "" && s[0] >= '0' && s[0] <= '9' {
   510  			s = s[1:]
   511  		}
   512  	default:
   513  		return false
   514  	}
   515  	return s == "#" || IsJSONPointer(s)
   516  }
   517  
   518  // IsUUID tells whether given string is a valid uuid format
   519  // as specified in RFC4122.
   520  //
   521  // see https://datatracker.ietf.org/doc/html/rfc4122#page-4, for details
   522  func IsUUID(v interface{}) bool {
   523  	s, ok := v.(string)
   524  	if !ok {
   525  		return true
   526  	}
   527  	parseHex := func(n int) bool {
   528  		for n > 0 {
   529  			if len(s) == 0 {
   530  				return false
   531  			}
   532  			hex := (s[0] >= '0' && s[0] <= '9') || (s[0] >= 'a' && s[0] <= 'f') || (s[0] >= 'A' && s[0] <= 'F')
   533  			if !hex {
   534  				return false
   535  			}
   536  			s = s[1:]
   537  			n--
   538  		}
   539  		return true
   540  	}
   541  	groups := []int{8, 4, 4, 4, 12}
   542  	for i, numDigits := range groups {
   543  		if !parseHex(numDigits) {
   544  			return false
   545  		}
   546  		if i == len(groups)-1 {
   547  			break
   548  		}
   549  		if len(s) == 0 || s[0] != '-' {
   550  			return false
   551  		}
   552  		s = s[1:]
   553  	}
   554  	return len(s) == 0
   555  }
   556  
   557  // IsRegex tells whether given string is a valid regex pattern
   558  func IsRegex(v interface{}) bool {
   559  	pattern, ok := v.(string)
   560  	if !ok {
   561  		return true
   562  	}
   563  
   564  	// Attempt to compile the string as a regex pattern.
   565  	_, err := regexp.Compile(pattern)
   566  
   567  	// If there is no error, the pattern is a valid regex.
   568  	return err == nil
   569  }