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) 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 { writeItemsInline(sb, s.Items) sb.WriteString("; ") } sb.WriteString("}") } needSpace = true } } }