Skip to content
This repository was archived by the owner on Dec 7, 2025. It is now read-only.

Commit 4e80f2d

Browse files
authored
Merge pull request #52 from nao1215/nchika/hostnames
Add hostname, hostname_rfc1123, hostname_port tag
2 parents faef923 + dd87a87 commit 4e80f2d

File tree

9 files changed

+188
-3
lines changed

9 files changed

+188
-3
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ line:3 column password: target is not greater than or equal to the threshold val
154154
| url_encoded| URL-encoded string (percent escapes, no malformed `%` sequences) |
155155
| datauri | Valid Data URI (data:*;base64,…) |
156156
| fqdn | Valid fully qualified domain name |
157+
| hostname | Hostname (RFC 952) |
158+
| hostname_rfc1123 | Hostname (RFC 1123) |
159+
| hostname_port | Host and port combination |
157160
| uuid | UUID string (with hyphens) |
158161
159162
#### Network rules

errors.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ var (
160160
ErrURLEncodedID = "ErrURLEncoded"
161161
// ErrDataURIID is the error ID used when the target is not a valid data URI.
162162
ErrDataURIID = "ErrDataURI"
163+
// ErrHostnameID is the error ID used when the target is not a valid hostname (RFC 952).
164+
ErrHostnameID = "ErrHostname"
165+
// ErrHostnameRFC1123ID is the error ID used when the target is not a valid hostname (RFC 1123).
166+
ErrHostnameRFC1123ID = "ErrHostnameRFC1123"
167+
// ErrHostnamePortID is the error ID used when the target is not a valid hostname:port.
168+
ErrHostnamePortID = "ErrHostnamePort"
163169
// ErrFQDNID is the error ID used when the target is not a valid fully qualified domain name.
164170
ErrFQDNID = "ErrFQDN"
165171
// ErrIPAddrID is the error ID used when the target is not an IP address (ip_addr).

i18n/en.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@
159159
- id: "ErrDataURI"
160160
translation: "target is not a valid data URI"
161161

162+
- id: "ErrHostname"
163+
translation: "target is not a valid hostname (RFC 952)"
164+
165+
- id: "ErrHostnameRFC1123"
166+
translation: "target is not a valid hostname (RFC 1123)"
167+
168+
- id: "ErrHostnamePort"
169+
translation: "target is not a valid host:port"
170+
162171
- id: "ErrFQDN"
163172
translation: "target is not a valid fully qualified domain name"
164173

i18n/ja.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,15 @@
165165
- id: "ErrDataURI"
166166
translation: "値が有効な Data URI ではありません"
167167

168+
- id: "ErrHostname"
169+
translation: "値が有効なホスト名(RFC 952)ではありません"
170+
171+
- id: "ErrHostnameRFC1123"
172+
translation: "値が有効なホスト名(RFC 1123)ではありません"
173+
174+
- id: "ErrHostnamePort"
175+
translation: "値が有効なホスト名:ポートではありません"
176+
168177
- id: "ErrFQDN"
169178
translation: "値が有効な FQDN ではありません"
170179

i18n/ru.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@
160160
- id: "ErrDataURI"
161161
translation: "целевое значение не является допустимым Data URI"
162162

163+
- id: "ErrHostname"
164+
translation: "целевое значение не является допустимым именем хоста (RFC 952)"
165+
166+
- id: "ErrHostnameRFC1123"
167+
translation: "целевое значение не является допустимым именем хоста (RFC 1123)"
168+
169+
- id: "ErrHostnamePort"
170+
translation: "целевое значение не является допустимым host:port"
171+
163172
- id: "ErrFQDN"
164173
translation: "целевое значение не является допустимым FQDN"
165174

parser.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,12 @@ func (c *CSV) parseValidateTag(tags string, fieldIndex int) (validators, error)
274274
validatorList = append(validatorList, newDataURIValidator())
275275
case strings.HasPrefix(t, fqdnTagValue.String()):
276276
validatorList = append(validatorList, newFQDNValidator())
277+
case strings.HasPrefix(t, hostnameTagValue.String()):
278+
validatorList = append(validatorList, newHostnameValidator(hostnameRFC952LabelRegexp, ErrHostnameID))
279+
case strings.HasPrefix(t, hostnameRFC1123TagValue.String()):
280+
validatorList = append(validatorList, newHostnameValidator(hostnameRFC1123LabelRegexp, ErrHostnameRFC1123ID))
281+
case strings.HasPrefix(t, hostnamePortTagValue.String()):
282+
validatorList = append(validatorList, newHostnamePortValidator())
277283
case strings.HasPrefix(t, uriTagValue.String()):
278284
validatorList = append(validatorList, newURIValidator())
279285
case strings.HasPrefix(t, urlTagValue.String()):

tag.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ const (
8484
dataURITagValue tagValue = "datauri"
8585
// fqdnTagValue is the struct tag name for fully qualified domain name fields.
8686
fqdnTagValue tagValue = "fqdn"
87+
// hostnameTagValue is the struct tag name for hostname (RFC 952) fields.
88+
hostnameTagValue tagValue = "hostname"
89+
// hostnameRFC1123TagValue is the struct tag name for hostname (RFC 1123) fields.
90+
hostnameRFC1123TagValue tagValue = "hostname_rfc1123"
91+
// hostnamePortTagValue is the struct tag name for hostname with port fields.
92+
hostnamePortTagValue tagValue = "hostname_port"
8793
// ipAddrTagValue is the struct tag name for ip_addr fields (IPv4 or IPv6).
8894
ipAddrTagValue tagValue = "ip_addr"
8995
// ip4AddrTagValue is the struct tag name for ip4_addr fields (IPv4 only).

validation.go

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -813,9 +813,11 @@ func (u *httpsURLValidator) Do(localizer *i18n.Localizer, target any) error {
813813
}
814814

815815
var (
816-
urlEncodedRegexp = regexp.MustCompile(`^(?:[^%]|%[0-9A-Fa-f]{2})*$`)
817-
dataURIRegex = regexp.MustCompile(dataURIRegexPattern)
818-
fqdnLabelRegexp = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
816+
urlEncodedRegexp = regexp.MustCompile(`^(?:[^%]|%[0-9A-Fa-f]{2})*$`)
817+
dataURIRegex = regexp.MustCompile(dataURIRegexPattern)
818+
fqdnLabelRegexp = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
819+
hostnameRFC952LabelRegexp = regexp.MustCompile(`^[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$`)
820+
hostnameRFC1123LabelRegexp = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$`)
819821
)
820822

821823
// urlEncodedValidator validates URL-encoded strings (no invalid % escapes).
@@ -840,6 +842,26 @@ func newFQDNValidator() *fqdnValidator {
840842
return &fqdnValidator{}
841843
}
842844

845+
type hostnameValidator struct {
846+
labelRegexp *regexp.Regexp
847+
errID string
848+
}
849+
850+
// newHostnameValidator returns a new hostnameValidator with the given label regex and error id.
851+
func newHostnameValidator(labelRegexp *regexp.Regexp, errID string) *hostnameValidator {
852+
return &hostnameValidator{
853+
labelRegexp: labelRegexp,
854+
errID: errID,
855+
}
856+
}
857+
858+
type hostnamePortValidator struct{}
859+
860+
// newHostnamePortValidator returns a new hostnamePortValidator.
861+
func newHostnamePortValidator() *hostnamePortValidator {
862+
return &hostnamePortValidator{}
863+
}
864+
843865
// Do validates the target is URL encoded.
844866
func (u *urlEncodedValidator) Do(localizer *i18n.Localizer, target any) error {
845867
v, ok := target.(string)
@@ -907,6 +929,71 @@ func (f *fqdnValidator) Do(localizer *i18n.Localizer, target any) error {
907929
return nil
908930
}
909931

932+
// Do validates the target is a hostname according to the provided label regexp.
933+
func (h *hostnameValidator) Do(localizer *i18n.Localizer, target any) error {
934+
v, ok := target.(string)
935+
if !ok {
936+
return NewError(localizer, h.errID, fmt.Sprintf("value=%v", target))
937+
}
938+
939+
if strings.HasPrefix(v, ".") || strings.HasSuffix(v, ".") {
940+
return NewError(localizer, h.errID, fmt.Sprintf("value=%v", target))
941+
}
942+
943+
labels := strings.Split(v, ".")
944+
if len(labels) < 1 {
945+
return NewError(localizer, h.errID, fmt.Sprintf("value=%v", target))
946+
}
947+
948+
totalLen := 0
949+
for _, label := range labels {
950+
totalLen += len(label) + 1 // include dot later
951+
if !h.labelRegexp.MatchString(label) {
952+
return NewError(localizer, h.errID, fmt.Sprintf("value=%v", target))
953+
}
954+
}
955+
if totalLen-1 > 253 {
956+
return NewError(localizer, h.errID, fmt.Sprintf("value=%v", target))
957+
}
958+
959+
return nil
960+
}
961+
962+
// Do validates the target is a hostname:port where host is IP or RFC1123 hostname.
963+
func (h *hostnamePortValidator) Do(localizer *i18n.Localizer, target any) error {
964+
v, ok := target.(string)
965+
if !ok {
966+
return NewError(localizer, ErrHostnamePortID, fmt.Sprintf("value=%v", target))
967+
}
968+
969+
host, portStr, err := net.SplitHostPort(v)
970+
if err != nil {
971+
return NewError(localizer, ErrHostnamePortID, fmt.Sprintf("value=%v", target))
972+
}
973+
974+
p, err := strconv.Atoi(portStr)
975+
if err != nil || p < 1 || p > 65535 {
976+
return NewError(localizer, ErrHostnamePortID, fmt.Sprintf("value=%v", target))
977+
}
978+
979+
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
980+
if ip := net.ParseIP(strings.Trim(host, "[]")); ip != nil {
981+
return nil
982+
}
983+
return NewError(localizer, ErrHostnamePortID, fmt.Sprintf("value=%v", target))
984+
}
985+
986+
if ip := net.ParseIP(host); ip != nil {
987+
return nil
988+
}
989+
990+
if err := newHostnameValidator(hostnameRFC1123LabelRegexp, ErrHostnamePortID).Do(localizer, host); err != nil {
991+
return NewError(localizer, ErrHostnamePortID, fmt.Sprintf("value=%v", target))
992+
}
993+
994+
return nil
995+
}
996+
910997
// emailValidator is a struct that contains the validation rules for an email column.
911998
type emailValidator struct {
912999
regexp *regexp.Regexp

validation_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,56 @@ func Test_dataURIValidator_Do(t *testing.T) {
11171117
}
11181118
}
11191119

1120+
func Test_hostnameValidators_Do(t *testing.T) {
1121+
t.Parallel()
1122+
1123+
hostnameTests := []struct {
1124+
name string
1125+
v *hostnameValidator
1126+
arg any
1127+
wantErr bool
1128+
}{
1129+
{"hostname ok", newHostnameValidator(hostnameRFC952LabelRegexp, ErrHostnameID), "example.com", false},
1130+
{"hostname starts with digit invalid", newHostnameValidator(hostnameRFC952LabelRegexp, ErrHostnameID), "1example.com", true},
1131+
{"hostname underscore invalid", newHostnameValidator(hostnameRFC952LabelRegexp, ErrHostnameID), "exa_mple.com", true},
1132+
{"hostname RFC1123 digit ok", newHostnameValidator(hostnameRFC1123LabelRegexp, ErrHostnameRFC1123ID), "1example.com", false},
1133+
{"hostname RFC1123 trailing dot", newHostnameValidator(hostnameRFC1123LabelRegexp, ErrHostnameRFC1123ID), "example.com.", true},
1134+
{"hostname non-string", newHostnameValidator(hostnameRFC952LabelRegexp, ErrHostnameID), 123, true},
1135+
}
1136+
1137+
for _, tt := range hostnameTests {
1138+
t.Run(tt.name, func(t *testing.T) {
1139+
t.Parallel()
1140+
if err := tt.v.Do(helperLocalizer(t), tt.arg); (err != nil) != tt.wantErr {
1141+
t.Errorf("hostnameValidator.Do() error = %v, wantErr %v, test %s", err, tt.wantErr, tt.name)
1142+
}
1143+
})
1144+
}
1145+
1146+
hostPortTests := []struct {
1147+
name string
1148+
arg any
1149+
wantErr bool
1150+
}{
1151+
{"host with port ok", "example.com:80", false},
1152+
{"ipv4 with port ok", "127.0.0.1:8080", false},
1153+
{"ipv6 with port ok", "[2001:db8::1]:443", false},
1154+
{"missing port", "example.com", true},
1155+
{"bad port", "example.com:99999", true},
1156+
{"bad host", "exa_mple.com:80", true},
1157+
{"non-string", 123, true},
1158+
}
1159+
1160+
for _, tt := range hostPortTests {
1161+
t.Run(tt.name, func(t *testing.T) {
1162+
t.Parallel()
1163+
if err := newHostnamePortValidator().Do(helperLocalizer(t), tt.arg); (err != nil) != tt.wantErr {
1164+
t.Errorf("hostnamePortValidator.Do() error = %v, wantErr %v, test %s", err, tt.wantErr, tt.name)
1165+
}
1166+
})
1167+
}
1168+
}
1169+
11201170
func Test_fqdnValidator_Do(t *testing.T) {
11211171
t.Parallel()
11221172

0 commit comments

Comments
 (0)