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 }