Files
jarl/internal/namedconf/printer.go
T
adamandClaude Opus 5.5 c27a6c10bd Mail reverse DNS check, what Jarl adds to named.conf, lists one per line
jarld checks the reverse DNS of the public zones' MX and SPF addresses as the internet sees it, every 6 hours (Exposure page, jarl exposure mail, jarl_mail_reverse_dns_ok). The Server options page, jarl option generated and GET /options/generated list what Jarl writes into named.conf by itself; stats.rate_limit_log turns off the query-errors logging. Option lists of three or more elements are shown one per line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:02:11 +01:00

220 lines
5.0 KiB
Go

package namedconf
import (
"strings"
)
// maxInline is the widest a block may be to be printed on one line.
const maxInline = 72
// Format returns stmts in canonical form, one statement per line, with
// nested blocks indented by tabs.
func Format(stmts []*Stmt) string {
var sb strings.Builder
for _, s := range stmts {
writeStmt(&sb, s, 0)
}
return sb.String()
}
// FormatStmt returns a single statement in canonical form, indented by
// indent tabs, including the trailing newline.
func FormatStmt(s *Stmt, indent int) string {
var sb strings.Builder
writeStmt(&sb, s, indent)
return sb.String()
}
// Compact returns the statement's items on a single line, without the
// trailing semicolon: `allow-query { 10.0.0.0/8; localhost; }`.
func Compact(items []Item) string {
var sb strings.Builder
writeItemsInline(&sb, items)
return sb.String()
}
// CompactStmt returns the statement on a single line with its semicolon.
func CompactStmt(s *Stmt) string { return Compact(s.Items) + ";" }
// FormatValue returns an option's value laid out as named.conf writes it:
// blocks over several lines, indented with tabs. Values without a block,
// and values that don't parse, are returned as they are.
func FormatValue(name, value string) string {
if !strings.Contains(value, "{") {
return value
}
st, err := ParseOptionValue(name, value)
if err != nil {
return value
}
out := strings.TrimSuffix(strings.TrimRight(FormatStmt(st, 0), "\n"), ";")
if rest, ok := strings.CutPrefix(out, st.Keyword()); ok {
return strings.TrimLeft(rest, " ")
}
return value
}
// QuoteString returns s as a named.conf quoted string.
func QuoteString(s string) string {
var sb strings.Builder
sb.WriteByte('"')
for i := 0; i < len(s); i++ {
if s[i] == '"' || s[i] == '\\' {
sb.WriteByte('\\')
}
sb.WriteByte(s[i])
}
sb.WriteByte('"')
return sb.String()
}
func valueText(v *Value) string {
if v.Quoted {
return QuoteString(v.Text)
}
return v.Text
}
func writeStmt(sb *strings.Builder, s *Stmt, indent int) {
tabs := strings.Repeat("\t", indent)
if s.Comment != "" {
sb.WriteString(tabs + "// " + s.Comment + "\n")
}
sb.WriteString(tabs)
inline := Compact(s.Items)
if !hasNestedBlocks(s.Items) && len(inline)+indent*8 <= maxInline+8 {
sb.WriteString(inline)
sb.WriteString(";\n")
return
}
needSpace := false
for _, it := range s.Items {
switch {
case it.Bang:
if needSpace {
sb.WriteByte(' ')
}
sb.WriteByte('!')
needSpace = false
case it.Value != nil:
if needSpace {
sb.WriteByte(' ')
}
sb.WriteString(valueText(it.Value))
needSpace = true
case it.Block != nil:
if needSpace {
sb.WriteByte(' ')
}
blockInline := Compact([]Item{it})
if !blockHasBlocks(it.Block) && len(blockInline) <= 40 {
sb.WriteString(blockInline)
} else if len(it.Block.Stmts) == 0 {
sb.WriteString("{ }")
} else {
sb.WriteString("{\n")
for _, inner := range it.Block.Stmts {
writeStmt(sb, inner, indent+1)
}
sb.WriteString(tabs)
sb.WriteString("}")
}
needSpace = true
}
}
sb.WriteString(";\n")
}
func hasNestedBlocks(items []Item) bool {
for _, it := range items {
if it.Block != nil && blockHasBlocks(it.Block) {
return true
}
}
return false
}
func blockHasBlocks(b *Block) bool {
for _, s := range b.Stmts {
for _, it := range s.Items {
if it.Block != nil {
return true
}
}
}
return false
}
func writeItemsInline(sb *strings.Builder, items []Item) {
needSpace := false
for _, it := range items {
switch {
case it.Bang:
if needSpace {
sb.WriteByte(' ')
}
sb.WriteByte('!')
needSpace = false
case it.Value != nil:
if needSpace {
sb.WriteByte(' ')
}
sb.WriteString(valueText(it.Value))
needSpace = true
case it.Block != nil:
if needSpace {
sb.WriteByte(' ')
}
if len(it.Block.Stmts) == 0 {
sb.WriteString("{ }")
} else {
sb.WriteString("{ ")
for _, s := range it.Block.Stmts {
if s.Comment != "" {
sb.WriteString("/* " + s.Comment + " */ ")
}
writeItemsInline(sb, s.Items)
sb.WriteString("; ")
}
sb.WriteString("}")
}
needSpace = true
}
}
}
// FormatValueList is FormatValue, except that a value whose block is a
// plain list (no nested blocks) of at least min elements is laid out one
// element per line, for reading: allow-query, exempt-clients, forwarders.
func FormatValueList(name, value string, min int) string {
st, err := ParseOptionValue(name, value)
if err != nil {
return FormatValue(name, value)
}
items := st.Items[1:]
blocks := 0
var list *Block
for _, it := range items {
if it.Block != nil {
blocks++
list = it.Block
}
}
if blocks != 1 || len(list.Stmts) < min || blockHasBlocks(list) {
return FormatValue(name, value)
}
var sb strings.Builder
for _, it := range items {
if it.Block == nil {
sb.WriteString(Compact([]Item{it}) + " ")
continue
}
sb.WriteString("{\n")
for _, s := range list.Stmts {
sb.WriteString("\t" + Compact(s.Items) + ";\n")
}
sb.WriteString("} ")
}
return strings.TrimSpace(sb.String())
}