This commit is contained in:
vasyansk committed 2026-11-02 17:37:51 +07:00
1 parent c5f1e64873
commit 550427e2d4
4 files changed
+141 -1

No files matched your search

+47 -1
View File
@@ -55,7 +55,9 @@ func normalizeName(name string) string {
// normalizeValue canonicalizes a single RR value for comparison.
func normalizeValue(t RecordType, content string) string {
if t == TXT {
return content // byte-exact — case and whitespace are significant (DKIM/SPF/DMARC)
// byte-exact — case and whitespace are significant (DKIM/SPF/DMARC);
// only the presentation quoting/chunking is stripped.
return TXTPayload(content)
}
c := strings.Join(strings.Fields(content), " ") // collapse whitespace
switch t {
@@ -95,3 +97,47 @@ func (r Record) Equal(o Record) bool {
}
return slices.Equal(r.NormalizedValues(), o.NormalizedValues())
}
// TXTPayload returns the logical content of a TXT value. A value written as
// one or more quoted strings ("a" "b") is the concatenation of their
// contents; an unquoted value is returned as is. This makes a template value
// without quotes comparable with the quoted form providers store.
func TXTPayload(v string) string {
t := strings.TrimSpace(v)
if !strings.HasPrefix(t, `"`) {
return v
}
var b strings.Builder
inQ := false
for i := 0; i < len(t); i++ {
c := t[i]
switch {
case inQ && c == '\\' && i+1 < len(t):
i++
b.WriteByte(t[i])
case c == '"':
inQ = !inQ
case inQ:
b.WriteByte(c)
}
}
return b.String()
}
// QuoteTXT renders a TXT value in the quoted form providers require: payload
// split into <=255-byte strings, each quoted. Already-quoted values pass
// through unchanged.
func QuoteTXT(v string) string {
if strings.HasPrefix(strings.TrimSpace(v), `"`) {
return v
}
const max = 255
esc := strings.NewReplacer(`\`, `\\`, `"`, `\"`)
var parts []string
for len(v) > max {
parts = append(parts, `"`+esc.Replace(v[:max])+`"`)
v = v[max:]
}
parts = append(parts, `"`+esc.Replace(v)+`"`)
return strings.Join(parts, " ")
}
+30
View File
@@ -0,0 +1,30 @@
package model
import (
"strings"
"testing"
)
func TestQuoteTXTAndPayloadRoundTrip(t *testing.T) {
long := "v=DKIM1; p=" + strings.Repeat("A", 400)
for _, v := range []string{"v=spf1 ~all", long, `a"b\c`} {
q := QuoteTXT(v)
if !strings.HasPrefix(q, `"`) {
t.Fatalf("not quoted: %q", q)
}
if got := TXTPayload(q); got != v {
t.Fatalf("roundtrip: got %q want %q", got, v)
}
if TXTPayload(v) != v && !strings.HasPrefix(v, `"`) {
t.Fatalf("unquoted payload changed")
}
}
if QuoteTXT(`"x"`) != `"x"` {
t.Fatal("quoted must pass through")
}
a := Record{Type: TXT, Values: []string{"v=DKIM1; k=rsa"}}
b := Record{Type: TXT, Values: []string{`"v=DKIM1; k=rsa"`}}
if !a.Equal(b) {
t.Fatal("quoted and unquoted must compare equal")
}
}