Skip to content

Commit 27a0f63

Browse files
committed
fixup: simplify wildcard domain parsing
1 parent c166497 commit 27a0f63

7 files changed

Lines changed: 76 additions & 65 deletions

File tree

internal/domain/domain.go

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,6 @@ type Normalization struct {
5858
RemovedExtraTrailingDots bool
5959
}
6060

61-
func (normalization Normalization) combine(other Normalization) Normalization {
62-
return Normalization{
63-
RemovedLeadingDots: normalization.RemovedLeadingDots || other.RemovedLeadingDots,
64-
RemovedExtraTrailingDots: normalization.RemovedExtraTrailingDots || other.RemovedExtraTrailingDots,
65-
}
66-
}
67-
6861
// normalizeBoundary removes compatibility dots at a name's boundaries. A
6962
// single final root dot is silent; two or more final dots are recorded. An
7063
// all-dot spelling is root cleanup, not leading-dot cleanup.
@@ -95,6 +88,15 @@ func hasEmptyInteriorLabel(ascii string) bool {
9588
return strings.HasPrefix(ascii, ".") || strings.Contains(ascii, "..")
9689
}
9790

91+
// wildcardSuffix recognizes both a bare wildcard and a wildcard with a suffix
92+
// after whole-input normalization has exposed its canonical dot separators.
93+
func wildcardSuffix(ascii string) (string, bool) {
94+
if ascii == "*" {
95+
return "", true
96+
}
97+
return strings.CutPrefix(ascii, "*.")
98+
}
99+
98100
// ErrTooFewLabels means a domain name has fewer than two labels after
99101
// normalization — a single label (com, localhost), the empty/root name (.),
100102
// or a bare "*". Such a name cannot be a reasonable target domain name.
@@ -131,11 +133,18 @@ func New(input string) (Domain, Normalization, error) {
131133
ascii, err := profileKeepingLeadingDots.ToASCII(input)
132134
normalized, normalization := normalizeBoundary(ascii)
133135

134-
if suffix, ok := strings.CutPrefix(normalized, "*."); ok {
135-
return newWildcard(suffix, normalization, strings.HasPrefix(suffix, "."))
136-
}
137-
if normalized == "*" {
138-
return Wildcard(""), normalization, ErrTooFewLabels
136+
if suffix, ok := wildcardSuffix(normalized); ok {
137+
wildcard, wildcardErr := validateNormalizedWildcardSuffix(suffix)
138+
if wildcardErr != nil {
139+
if errors.Is(wildcardErr, ErrEmptyInteriorLabel) {
140+
return nil, Normalization{}, wildcardErr
141+
}
142+
return wildcard, Normalization{}, wildcardErr
143+
}
144+
if wildcard == "" {
145+
return wildcard, normalization, ErrTooFewLabels
146+
}
147+
return wildcard, normalization, nil
139148
}
140149
if strings.IndexByte(normalized, '.') == -1 {
141150
return FQDN(normalized), normalization, ErrTooFewLabels
@@ -156,28 +165,20 @@ func New(input string) (Domain, Normalization, error) {
156165
return FQDN(normalized), normalization, nil
157166
}
158167

159-
func newWildcard(
160-
suffix string, outerNormalization Normalization, includesWildcardMarker bool,
161-
) (Domain, Normalization, error) {
168+
// validateNormalizedWildcardSuffix expects a suffix cut from a whole input
169+
// after boundary normalization. It re-runs IDNA without the wildcard marker so
170+
// the marker's own error does not mask errors in the suffix. Target-specific
171+
// wildcard policy belongs to the caller, so an empty suffix is valid here.
172+
func validateNormalizedWildcardSuffix(suffix string) (Wildcard, error) {
162173
ascii, err := profileKeepingLeadingDots.ToASCII(suffix)
163-
normalized, normalization := normalizeBoundary(ascii)
164-
normalization = outerNormalization.combine(normalization)
165174
if err != nil {
166-
return Wildcard(normalized), Normalization{
167-
RemovedLeadingDots: false,
168-
RemovedExtraTrailingDots: false,
169-
}, err
175+
normalized, _ := normalizeBoundary(ascii)
176+
return Wildcard(normalized), err
170177
}
171178
if hasEmptyInteriorLabel(suffix) {
172-
return nil, Normalization{
173-
RemovedLeadingDots: false,
174-
RemovedExtraTrailingDots: false,
175-
}, newEmptyInteriorLabelError(includesWildcardMarker)
176-
}
177-
if normalized == "" {
178-
return Wildcard(""), normalization, ErrTooFewLabels
179+
return "", newEmptyInteriorLabelError(strings.HasPrefix(suffix, "."))
179180
}
180-
return Wildcard(normalized), normalization, nil
181+
return Wildcard(ascii), nil
181182
}
182183

183184
// CompareDomain compares two domains by their ASCII representations.

internal/domain/domain_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func TestNew(t *testing.T) {
3838
{"*.example.org", w("example.org"), normalization(false, false), nil},
3939
{"*.example.org.", w("example.org"), normalization(false, false), nil},
4040
{"*.example.org..", w("example.org"), normalization(false, true), nil},
41+
{"..*.example.org...", w("example.org"), normalization(true, true), nil},
4142
{"......", f(""), normalization(false, true), domain.ErrTooFewLabels},
4243
{"*......", w(""), normalization(false, true), domain.ErrTooFewLabels},
4344
{"a..example.org", nil, normalization(false, false), domain.ErrEmptyInteriorLabel},
@@ -206,6 +207,15 @@ func TestNewTooFewLabels(t *testing.T) {
206207
}
207208
}
208209

210+
func TestNewTooFewLabelsTakesPrecedenceOverIDNAErrors(t *testing.T) {
211+
t.Parallel()
212+
213+
got, normalization, err := domain.New("\u0080")
214+
require.Equal(t, domain.FQDN("xn--a"), got)
215+
require.Empty(t, normalization)
216+
require.ErrorIs(t, err, domain.ErrTooFewLabels)
217+
}
218+
209219
func TestConstructedDomainInvariant(t *testing.T) {
210220
t.Parallel()
211221

internal/domain/fqdn.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package domain
22

3-
import "strings"
4-
53
// FQDN is a fully qualified domain in its ASCII form.
64
type FQDN string
75

@@ -33,15 +31,5 @@ func (f FQDN) HasStrictSuffix(s Suffix) bool {
3331

3432
// Zones starts from a.b.c for the domain a.b.c.
3533
func (f FQDN) Zones(yield func(Suffix) bool) {
36-
domain := string(f)
37-
for {
38-
if !yield(Suffix(domain)) {
39-
return
40-
}
41-
if i := strings.IndexRune(domain, '.'); i == -1 {
42-
return
43-
} else {
44-
domain = domain[i+1:]
45-
}
46-
}
34+
walkZonesASCII(string(f), yield)
4735
}

internal/domain/suffix.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,11 @@ func NewSuffix(input string) (Suffix, Normalization, error) {
2222
ascii, err := profileKeepingLeadingDots.ToASCII(input)
2323
normalized, normalization := normalizeBoundary(ascii)
2424

25-
if suffix, ok := strings.CutPrefix(normalized, "*."); ok {
26-
_, suffixNormalization, suffixErr := newWildcard(suffix, normalization, strings.HasPrefix(suffix, "."))
27-
if suffixErr != nil && !errors.Is(suffixErr, ErrTooFewLabels) {
28-
return "", Normalization{
29-
RemovedLeadingDots: false,
30-
RemovedExtraTrailingDots: false,
31-
}, suffixErr
25+
if suffix, ok := wildcardSuffix(normalized); ok {
26+
_, wildcardErr := validateNormalizedWildcardSuffix(suffix)
27+
if wildcardErr != nil {
28+
return "", Normalization{}, wildcardErr
3229
}
33-
return "", normalization.combine(suffixNormalization), ErrWildcardSuffix
34-
}
35-
if normalized == "*" {
3630
return "", normalization, ErrWildcardSuffix
3731
}
3832

@@ -80,3 +74,16 @@ func hasStrictSuffixASCII(s, suffix string) bool {
8074
}
8175
return strings.HasSuffix(s, suffix) && len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.'
8276
}
77+
78+
func walkZonesASCII(name string, yield func(Suffix) bool) {
79+
for {
80+
if !yield(Suffix(name)) {
81+
return
82+
}
83+
if i := strings.IndexRune(name, '.'); i == -1 {
84+
return
85+
} else {
86+
name = name[i+1:]
87+
}
88+
}
89+
}

internal/domain/suffix_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ func TestNewSuffix(t *testing.T) {
110110
{"*.a..org", "", expectedNormalization(false, false), domain.ErrEmptyInteriorLabel},
111111
{"*.example.org", "", expectedNormalization(false, false), domain.ErrWildcardSuffix},
112112
{"*.example.org..", "", expectedNormalization(false, true), domain.ErrWildcardSuffix},
113+
{"..*.example.org...", "", expectedNormalization(true, true), domain.ErrWildcardSuffix},
113114
{"*", "", expectedNormalization(false, false), domain.ErrWildcardSuffix},
115+
{"*......", "", expectedNormalization(false, true), domain.ErrWildcardSuffix},
114116
} {
115117
t.Run(tc.input, func(t *testing.T) {
116118
t.Parallel()

internal/domain/wildcard.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package domain
22

3-
import "strings"
4-
53
// Wildcard is a fully qualified zone name in its ASCII form, represnting the wildcard domain name
64
// under the zone. For example, Wildcard("example.org") represents *.example.org.
75
type Wildcard string
@@ -39,15 +37,5 @@ func (w Wildcard) HasStrictSuffix(s Suffix) bool {
3937

4038
// Zones starts from a.b.c for the wildcard domain *.a.b.c.
4139
func (w Wildcard) Zones(yield func(Suffix) bool) {
42-
domain := string(w)
43-
for {
44-
if !yield(Suffix(domain)) {
45-
return
46-
}
47-
if i := strings.IndexRune(domain, '.'); i == -1 {
48-
return
49-
} else {
50-
domain = domain[i+1:]
51-
}
52-
}
40+
walkZonesASCII(string(w), yield)
5341
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package domain
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestValidateNormalizedWildcardSuffixLeavesTargetPolicyToCaller(t *testing.T) {
10+
t.Parallel()
11+
12+
wildcard, err := validateNormalizedWildcardSuffix("")
13+
require.NoError(t, err)
14+
require.Equal(t, Wildcard(""), wildcard)
15+
}

0 commit comments

Comments
 (0)