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