From b32fa33c777d103d2b53c341275d104f06b0a356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20S=C3=B6derlund?= Date: Mon, 24 May 2021 11:00:12 +0200 Subject: [PATCH] feat: add resourcename.Validate Includes a copied implementation of the not-yet-public (due to some edge cases) implementation of domain name validation in go/src/net. --- resourcename/isdomainname.go | 54 ++++++++++++++++++++ resourcename/validate.go | 32 ++++++++++++ resourcename/validate_test.go | 96 +++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 resourcename/isdomainname.go create mode 100644 resourcename/validate.go create mode 100644 resourcename/validate_test.go diff --git a/resourcename/isdomainname.go b/resourcename/isdomainname.go new file mode 100644 index 0000000000..823c608a5a --- /dev/null +++ b/resourcename/isdomainname.go @@ -0,0 +1,54 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package resourcename + +// isDomainName is copied from go/src/net/dnsclient.go pending resolution of the following issues: +// - https://github.com/golang/go/issues/31671 +// - https://github.com/golang/go/issues/17659 +func isDomainName(s string) bool { + // See RFC 1035, RFC 3696. + // Presentation format has dots before every label except the first, and the + // terminal empty label is optional here because we assume fully-qualified + // (absolute) input. We must therefore reserve space for the first and last + // labels' length octets in wire format, where they are necessary and the + // maximum total length is 255. + // So our _effective_ maximum is 253, but 254 is not rejected if the last + // character is a dot. + l := len(s) + if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { + return false + } + last := byte('.') + partlen := 0 + for i := 0; i < len(s); i++ { + c := s[i] + switch { + default: + return false + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || '0' <= c && c <= '9': + partlen++ + case c == '-': + // Byte before dash cannot be dot. + if last == '.' { + return false + } + partlen++ + case c == '.': + // Byte before dot cannot be dot, dash. + if last == '.' || last == '-' { + return false + } + if partlen > 63 || partlen == 0 { + return false + } + partlen = 0 + } + last = c + } + if last == '-' || partlen > 63 { + return false + } + return true +} diff --git a/resourcename/validate.go b/resourcename/validate.go new file mode 100644 index 0000000000..6cd8e646c1 --- /dev/null +++ b/resourcename/validate.go @@ -0,0 +1,32 @@ +package resourcename + +import ( + "fmt" +) + +// Validate that a resource name conforms to the restrictions outlined in AIP-122, primarily that each segment +// must be a valid DNS name. +// See: https://google.aip.dev/122 +func Validate(name string) error { + if name == "" { + return fmt.Errorf("resource name is empty") + } + var sc Scanner + sc.Init(name) + var i int + for sc.Scan() { + i++ + switch { + case sc.Segment() == "": + return fmt.Errorf("segment %d is empty", i) + case sc.Segment() == Wildcard: + continue + case !isDomainName(string(sc.Segment())): + return fmt.Errorf("segment '%s': not a valid DNS name", sc.Segment()) + } + } + if sc.Full() && !isDomainName(sc.ServiceName()) { + return fmt.Errorf("service '%s': not a valid DNS name", sc.Segment()) + } + return nil +} diff --git a/resourcename/validate_test.go b/resourcename/validate_test.go new file mode 100644 index 0000000000..8439d7722e --- /dev/null +++ b/resourcename/validate_test.go @@ -0,0 +1,96 @@ +package resourcename + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestValidate(t *testing.T) { + t.Parallel() + for _, tt := range []struct { + name string + input string + errorContains string + }{ + { + name: "empty", + input: "", + errorContains: "empty", + }, + + { + name: "invalid DNS characters", + input: "ice cream is best", + errorContains: "not a valid DNS name", + }, + + { + name: "invalid DNS characters in segment", + input: "foo/bar/ice cream is best", + errorContains: "not a valid DNS name", + }, + + { + name: "invalid DNS characters in domain", + input: "//ice cream is best.com/foo/bar", + errorContains: "not a valid DNS name", + }, + + { + name: "singleton", + input: "foo", + }, + + { + name: "singleton wildcard", + input: "-", + }, + + { + name: "multi", + input: "foo/bar", + }, + + { + name: "multi wildcard at start", + input: "-/bar", + }, + + { + name: "multi wildcard at end", + input: "foo/-", + }, + + { + name: "multi wildcard at middle", + input: "foo/-/bar", + }, + + { + name: "numeric", + input: "foo/1234/bar", + }, + + { + name: "camelCase", + input: "FOO/1234/bAr", + }, + + { + name: "full", + input: "//example.com/foo/bar", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := Validate(tt.input) + if tt.errorContains != "" { + assert.ErrorContains(t, err, tt.errorContains) + } else { + assert.NilError(t, err) + } + }) + } +}