Files
jarl/internal/namedconf/printer.go
T
adamandClaude Opus 5.5 e2e5007f94 Address ranges in address match lists (IPv4 and IPv6)
ACLs, views' match lists, options with address match lists and blocking
client groups take ranges such as 10.0.10.101-10.0.10.199 or
2001:db8::100-2001:db8::1ff (negated, or with spaces around the dash, too).
Jarl stores the range as written and writes the fewest prefixes that cover
exactly that range to named.conf, with the range as a comment. New package
internal/iprange; namedconf gets an AMRange element (a range used to parse
as an ACL name), ExpandRanges and statement comments. Mistakes are refused
when entered, validation sees what ranges cover, and IPv6 ranges in the
IPv4-mapped space get a warning. The web UI shows what a range becomes
while typing (GET /catalog/ranges).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:17:14 +01:00

185 lines
4.1 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
}
}
}