Skip to content

Commit

Permalink
feat: parse host IDs
Browse files Browse the repository at this point in the history
  • Loading branch information
favonia committed Jan 25, 2025
1 parent 3a6454f commit 7212ad4
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 23 deletions.
4 changes: 2 additions & 2 deletions internal/domainexp/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func splitter(data []byte, atEOF bool) (int, []byte, error) {
switch {
case unicode.IsSpace(ch):
startIndex += size
case strings.ContainsRune("(),!", ch):
case strings.ContainsRune("()[],!", ch):
return returnToken()
case ch == '&':
state = StateAnd0
Expand All @@ -79,7 +79,7 @@ func splitter(data []byte, atEOF bool) (int, []byte, error) {
}
return returnToken()
case StateOther:
if unicode.IsSpace(ch) || strings.ContainsRune("(),!&|", ch) {
if unicode.IsSpace(ch) || strings.ContainsRune("()[],!&|", ch) {
if err = reader.UnreadRune(); err != nil {
return startIndex, nil, fmt.Errorf("reader.UnreadRune: %w", err)
}
Expand Down
111 changes: 96 additions & 15 deletions internal/domainexp/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,99 @@ package domainexp

import (
"errors"
"net/netip"
"strings"

"github.com/favonia/cloudflare-ddns/internal/domain"
"github.com/favonia/cloudflare-ddns/internal/ipnet"
"github.com/favonia/cloudflare-ddns/internal/pp"
)

func scanList(ppfmt pp.PP, key string, input string, tokens []string) ([]string, []string) {
var list []string
readyForNext := true
expectingElement := true
for len(tokens) > 0 {
switch tokens[0] {
case ",":
readyForNext = true
expectingElement = true
tokens = tokens[1:]
case ")":
return list, tokens
case "(", "&&", "||", "!":
case "[", "]", "(", "&&", "||", "!":
ppfmt.Noticef(pp.EmojiUserError, `%s (%q) has unexpected token %q`, key, input, tokens[0])
return nil, nil
default:
if !readyForNext {
if !expectingElement {
ppfmt.Noticef(pp.EmojiUserError, `%s (%q) is missing a comma "," before %q`, key, input, tokens[0])
}
list = append(list, tokens[0])
readyForNext = false
expectingElement = false
tokens = tokens[1:]
}
}
return list, tokens
}

tokens = tokens[1:]
type taggedItem struct {
Element string
Tag string
}

func scanTaggedList(ppfmt pp.PP, key string, input string, tokens []string) ([]taggedItem, []string) {
var list []taggedItem
expectingElement := true
for len(tokens) > 0 {
switch tokens[0] {
case ",":
expectingElement = true
tokens = tokens[1:]
case ")":
return list, tokens
case "[":
ppfmt.Noticef(pp.EmojiUserError, `%s (%q) is missing a domain before the opening bracket %q`, key, input, tokens[0])
return nil, nil
case "]", "(", "&&", "||", "!":
ppfmt.Noticef(pp.EmojiUserError, `%s (%q) has unexpected token %q`, key, input, tokens[0])
return nil, nil
default:
if !expectingElement {
ppfmt.Noticef(pp.EmojiUserError, `%s (%q) is missing a comma "," before %q`, key, input, tokens[0])
}
domain := tokens[0]

host := ""
switch {
case len(tokens) == 1, tokens[1] != "[":
tokens = tokens[1:]
case len(tokens) == 2: // 'domain', '['
ppfmt.Noticef(pp.EmojiUserError, `%s (%q) has unclosed "[" at the end`, key, input)
return nil, nil
default: // 'domain', '[', ?
switch tokens[2] {
case "]", ",", "(", ")", "&&", "||", "!":
ppfmt.Noticef(pp.EmojiUserError, `%s (%q) has unexpected token %q when a host ID is expected`,
key, input, tokens[0])
return nil, nil
default:
switch {
case len(tokens) == 3: // 'domain', '[', 'host'
ppfmt.Noticef(pp.EmojiUserError, `%s (%q) has unclosed "[" at the end`, key, input)
return nil, nil
case tokens[3] != "]":
ppfmt.Noticef(pp.EmojiUserError,
`%s (%q) has unexpected token %q when %q is expected`,
key, input, tokens[2], "]")
return nil, nil
default: // 'domain', '[', 'host', ']'
host = tokens[2]
tokens = tokens[4:]
}
}
}
list = append(list, taggedItem{Element: domain, Tag: host})

expectingElement = false
}
}
return list, tokens
}
Expand All @@ -43,25 +109,40 @@ func scanASCIIDomainList(ppfmt pp.PP, key string, input string, tokens []string)
return domains, tokens
}

func scanDomainList(ppfmt pp.PP, key string, input string, tokens []string) ([]domain.Domain, []string) {
list, tokens := scanList(ppfmt, key, input, tokens)
domains := make([]domain.Domain, 0, len(list))
// DomainWithHostID is a domain with an (optional) host ID.
type DomainWithHostID struct {
domain.Domain
HostID netip.Addr
}

func scanDomainList(ppfmt pp.PP, key string, input string, tokens []string) ([]DomainWithHostID, []string) {
list, tokens := scanTaggedList(ppfmt, key, input, tokens)
domains := make([]DomainWithHostID, 0, len(list))
for _, raw := range list {
d, err := domain.New(raw)
d, err := domain.New(raw.Element)
if err != nil {
if errors.Is(err, domain.ErrNotFQDN) {
ppfmt.Noticef(pp.EmojiUserError,
`%s (%q) contains a domain %q that is probably not fully qualified; a fully qualified domain name (FQDN) would look like "*.example.org" or "sub.example.org"`, //nolint:lll
key, input, d.Describe())
return nil, nil
} else {
}
ppfmt.Noticef(pp.EmojiUserError,
"%s (%q) contains an ill-formed domain %q: %v",
key, input, d.Describe(), err)
return nil, nil
}
h := netip.Addr{}
if raw.Tag != "" {
h, err = ipnet.ParseHost(raw.Tag)

Check failure on line 137 in internal/domainexp/parser.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: ipnet.ParseHost (typecheck)

Check failure on line 137 in internal/domainexp/parser.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: ipnet.ParseHost) (typecheck)

Check failure on line 137 in internal/domainexp/parser.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: ipnet.ParseHost) (typecheck)

Check failure on line 137 in internal/domainexp/parser.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: ipnet.ParseHost) (typecheck)

Check failure on line 137 in internal/domainexp/parser.go

View workflow job for this annotation

GitHub Actions / Test

undefined: ipnet.ParseHost

Check failure on line 137 in internal/domainexp/parser.go

View workflow job for this annotation

GitHub Actions / Fuzz

undefined: ipnet.ParseHost
if err != nil {
ppfmt.Noticef(pp.EmojiUserError,
"%s (%q) contains an ill-formed domain %q: %v",
key, input, d.Describe(), err)
"%s (%q) contains an ill-formed host ID %q: %v",
key, input, raw.Tag, err)
return nil, nil
}
}
domains = append(domains, d)
domains = append(domains, DomainWithHostID{Domain: d, HostID: h})
}
return domains, tokens
}
Expand Down Expand Up @@ -232,7 +313,7 @@ func scanExpression(ppfmt pp.PP, key string, input string, tokens []string) (pre
}

// ParseList parses a list of comma-separated domains. Internationalized domain names are fully supported.
func ParseList(ppfmt pp.PP, key string, input string) ([]domain.Domain, bool) {
func ParseList(ppfmt pp.PP, key string, input string) ([]DomainWithHostID, bool) {
tokens, ok := tokenize(ppfmt, key, input)
if !ok {
return nil, false
Expand Down
22 changes: 16 additions & 6 deletions internal/domainexp/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package domainexp_test

import (
"errors"
"net/netip"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -17,22 +18,31 @@ import (
func TestParseList(t *testing.T) {
t.Parallel()
key := "key"
noHost := netip.Addr{}
type f = domain.FQDN
type w = domain.Wildcard
type ds = []domain.Domain
type ds = []domainexp.DomainWithHostID
for name, tc := range map[string]struct {
input string
ok bool
expected ds
prepareMockPP func(m *mocks.MockPP)
}{
"a.a": {"a.a", true, ds{f("a.a")}, nil},
"a.a,a.b": {" a.a , a.b ", true, ds{f("a.a"), f("a.b")}, nil},
"a.a,a.b,a.c": {" a.a , a.b ,,,,,, a.c ", true, ds{f("a.a"), f("a.b"), f("a.c")}, nil},
"wildcard": {" a.a , a.b ,,,,,, *.c ", true, ds{f("a.a"), f("a.b"), w("c")}, nil},
"a.a": {"a.a", true, ds{{f("a.a"), noHost}}, nil},
"a.a,a.b": {" a.a , a.b ", true, ds{{f("a.a"), noHost}, {f("a.b"), noHost}}, nil},
"a.a,a.b,a.c": {" a.a , a.b ,,,,,, a.c ", true, ds{{f("a.a"), noHost}, {f("a.b"), noHost}, {f("a.c"), noHost}}, nil},
"wildcard": {" a.a , a.b ,,,,,, *.c ", true, ds{{f("a.a"), noHost}, {f("a.b"), noHost}, {w("c"), noHost}}, nil},
"hosts": {
" a.a [ :: ],,,,,, *.c [aa:bb:cc:dd:ee:ff] ", true,
ds{
{f("a.a"), netip.MustParseAddr("::")},
{w("c"), netip.MustParseAddr("::a8bb:ccff:fedd:eeff")},
},
nil,
},
"missing-comma": {
" a.a a.b a.c a.d ", true,
ds{f("a.a"), f("a.b"), f("a.c"), f("a.d")},
ds{{f("a.a"), noHost}, {f("a.b"), noHost}, {f("a.c"), noHost}, {f("a.d"), noHost}},
func(m *mocks.MockPP) {
gomock.InOrder(
m.EXPECT().Noticef(pp.EmojiUserError, `%s (%q) is missing a comma "," before %q`, key, " a.a a.b a.c a.d ", "a.b"),
Expand Down

0 comments on commit 7212ad4

Please sign in to comment.