1package gojsonschema
2
3import (
4	"net"
5	"net/url"
6	"reflect"
7	"regexp"
8	"strings"
9	"time"
10)
11
12type (
13	// FormatChecker is the interface all formatters added to FormatCheckerChain must implement
14	FormatChecker interface {
15		IsFormat(input string) bool
16	}
17
18	// FormatCheckerChain holds the formatters
19	FormatCheckerChain struct {
20		formatters map[string]FormatChecker
21	}
22
23	// EmailFormatter verifies email address formats
24	EmailFormatChecker struct{}
25
26	// IPV4FormatChecker verifies IP addresses in the ipv4 format
27	IPV4FormatChecker struct{}
28
29	// IPV6FormatChecker verifies IP addresses in the ipv6 format
30	IPV6FormatChecker struct{}
31
32	// DateTimeFormatChecker verifies date/time formats per RFC3339 5.6
33	//
34	// Valid formats:
35	// 		Partial Time: HH:MM:SS
36	//		Full Date: YYYY-MM-DD
37	// 		Full Time: HH:MM:SSZ-07:00
38	//		Date Time: YYYY-MM-DDTHH:MM:SSZ-0700
39	//
40	// 	Where
41	//		YYYY = 4DIGIT year
42	//		MM = 2DIGIT month ; 01-12
43	//		DD = 2DIGIT day-month ; 01-28, 01-29, 01-30, 01-31 based on month/year
44	//		HH = 2DIGIT hour ; 00-23
45	//		MM = 2DIGIT ; 00-59
46	//		SS = 2DIGIT ; 00-58, 00-60 based on leap second rules
47	//		T = Literal
48	//		Z = Literal
49	//
50	//	Note: Nanoseconds are also suported in all formats
51	//
52	// http://tools.ietf.org/html/rfc3339#section-5.6
53	DateTimeFormatChecker struct{}
54
55	// URIFormatCheckers validates a URI with a valid Scheme per RFC3986
56	URIFormatChecker struct{}
57
58	// HostnameFormatChecker validates a hostname is in the correct format
59	HostnameFormatChecker struct{}
60
61	// UUIDFormatChecker validates a UUID is in the correct format
62	UUIDFormatChecker struct{}
63
64	// RegexFormatChecker validates a regex is in the correct format
65	RegexFormatChecker struct{}
66)
67
68var (
69	// Formatters holds the valid formatters, and is a public variable
70	// so library users can add custom formatters
71	FormatCheckers = FormatCheckerChain{
72		formatters: map[string]FormatChecker{
73			"date-time": DateTimeFormatChecker{},
74			"hostname":  HostnameFormatChecker{},
75			"email":     EmailFormatChecker{},
76			"ipv4":      IPV4FormatChecker{},
77			"ipv6":      IPV6FormatChecker{},
78			"uri":       URIFormatChecker{},
79			"uuid":      UUIDFormatChecker{},
80			"regex":     RegexFormatChecker{},
81		},
82	}
83
84	// Regex credit: https://github.com/asaskevich/govalidator
85	rxEmail = regexp.MustCompile("^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
86
87	// Regex credit: https://www.socketloop.com/tutorials/golang-validate-hostname
88	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]))*$`)
89
90	rxUUID = regexp.MustCompile("^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$")
91)
92
93// Add adds a FormatChecker to the FormatCheckerChain
94// The name used will be the value used for the format key in your json schema
95func (c *FormatCheckerChain) Add(name string, f FormatChecker) *FormatCheckerChain {
96	c.formatters[name] = f
97
98	return c
99}
100
101// Remove deletes a FormatChecker from the FormatCheckerChain (if it exists)
102func (c *FormatCheckerChain) Remove(name string) *FormatCheckerChain {
103	delete(c.formatters, name)
104
105	return c
106}
107
108// Has checks to see if the FormatCheckerChain holds a FormatChecker with the given name
109func (c *FormatCheckerChain) Has(name string) bool {
110	_, ok := c.formatters[name]
111
112	return ok
113}
114
115// IsFormat will check an input against a FormatChecker with the given name
116// to see if it is the correct format
117func (c *FormatCheckerChain) IsFormat(name string, input interface{}) bool {
118	f, ok := c.formatters[name]
119
120	if !ok {
121		return false
122	}
123
124	if !isKind(input, reflect.String) {
125		return false
126	}
127
128	inputString := input.(string)
129
130	return f.IsFormat(inputString)
131}
132
133func (f EmailFormatChecker) IsFormat(input string) bool {
134	return rxEmail.MatchString(input)
135}
136
137// Credit: https://github.com/asaskevich/govalidator
138func (f IPV4FormatChecker) IsFormat(input string) bool {
139	ip := net.ParseIP(input)
140	return ip != nil && strings.Contains(input, ".")
141}
142
143// Credit: https://github.com/asaskevich/govalidator
144func (f IPV6FormatChecker) IsFormat(input string) bool {
145	ip := net.ParseIP(input)
146	return ip != nil && strings.Contains(input, ":")
147}
148
149func (f DateTimeFormatChecker) IsFormat(input string) bool {
150	formats := []string{
151		"15:04:05",
152		"15:04:05Z07:00",
153		"2006-01-02",
154		time.RFC3339,
155		time.RFC3339Nano,
156	}
157
158	for _, format := range formats {
159		if _, err := time.Parse(format, input); err == nil {
160			return true
161		}
162	}
163
164	return false
165}
166
167func (f URIFormatChecker) IsFormat(input string) bool {
168	u, err := url.Parse(input)
169	if err != nil || u.Scheme == "" {
170		return false
171	}
172
173	return true
174}
175
176func (f HostnameFormatChecker) IsFormat(input string) bool {
177	return rxHostname.MatchString(input) && len(input) < 256
178}
179
180func (f UUIDFormatChecker) IsFormat(input string) bool {
181	return rxUUID.MatchString(input)
182}
183
184// IsFormat implements FormatChecker interface.
185func (f RegexFormatChecker) IsFormat(input string) bool {
186	if input == "" {
187		return true
188	}
189	_, err := regexp.Compile(input)
190	if err != nil {
191		return false
192	}
193	return true
194}
195