Files

176 lines
4.7 KiB
Go

package birdfmt
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
)
var standardPairRE = regexp.MustCompile(`^\s*(\d{1,5}):(\d{1,5})\s*$`)
// badCommunityAddRE matches invalid BIRD filter calls like bgp_community.add(65000,1)
// (first argument is an int). Valid form is bgp_community.add((65000,1)); — note the pair tuple.
var badCommunityAddRE = regexp.MustCompile(`\b(?:bgp_community|bgp_large_community)\.add\(\s*\d`)
// RouteCommunityAttrs returns BIRD 2 lines (indented with 4 spaces) for use inside a static `route … { … }` block.
// Empty string means no community attributes.
func RouteCommunityAttrs(kind, name, valueJSON string) (string, error) {
name = strings.TrimSpace(name)
raw := strings.TrimSpace(valueJSON)
if raw == "" {
raw = "{}"
}
if s, ok := parseLargeFromJSONLoose(raw); ok {
if err := validateCommunityRouteBody(s); err != nil {
return "", err
}
return s, nil
}
_ = kind // reserved for future kinds (e.g. extended communities)
if m := standardPairRE.FindStringSubmatch(name); len(m) == 3 {
s, err := standardAddLine(m[1], m[2])
if err != nil {
return "", err
}
return s, validateCommunityRouteBody(s)
}
var obj map[string]any
if json.Unmarshal([]byte(raw), &obj) == nil {
if v, ok := obj["standard"].(string); ok {
if m := standardPairRE.FindStringSubmatch(v); len(m) == 3 {
s, err := standardAddLine(m[1], m[2])
if err != nil {
return "", err
}
return s, validateCommunityRouteBody(s)
}
}
if arr, ok := obj["standard"].([]any); ok {
if len(arr) == 1 {
return "", fmt.Errorf(`birdfmt: value_json "standard" must be a pair [a,b] or "a:b" string, got single element`)
}
if len(arr) == 2 {
a, aok := numberToUint32String(arr[0])
b, bok := numberToUint32String(arr[1])
if aok && bok {
s, err := standardAddLine(a, b)
if err != nil {
return "", err
}
return s, validateCommunityRouteBody(s)
}
}
}
}
var strVal string
if json.Unmarshal([]byte(raw), &strVal) == nil {
if m := standardPairRE.FindStringSubmatch(strVal); len(m) == 3 {
s, err := standardAddLine(m[1], m[2])
if err != nil {
return "", err
}
return s, validateCommunityRouteBody(s)
}
}
return "", nil
}
// validateCommunityRouteBody rejects lines that would make BIRD 2 fail parse-check with
// "Argument 1 of 'add' expected clist / quad / ip / pair, got int" (wrong add() shape).
func validateCommunityRouteBody(body string) error {
body = strings.TrimSpace(body)
if body == "" {
return nil
}
for _, line := range strings.Split(body, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if badCommunityAddRE.MatchString(line) {
return fmt.Errorf("birdfmt: invalid community line (expected pair/triple tuple inside add(), e.g. bgp_community.add((65000,1));): %q", line)
}
}
return nil
}
func standardAddLine(a, b string) (string, error) {
ai, err1 := strconv.ParseUint(strings.TrimSpace(a), 10, 16)
bi, err2 := strconv.ParseUint(strings.TrimSpace(b), 10, 16)
if err1 != nil || err2 != nil {
return "", fmt.Errorf("birdfmt: standard community parts must be 0..65535 (%s:%s)", a, b)
}
return fmt.Sprintf(" bgp_community.add((%d,%d));", ai, bi), nil
}
func parseLargeFromJSONLoose(raw string) (string, bool) {
var obj map[string]any
if json.Unmarshal([]byte(raw), &obj) != nil {
return "", false
}
arr, ok := obj["large"].([]any)
if !ok || len(arr) == 0 {
return "", false
}
var b strings.Builder
for _, row := range arr {
triple, ok := row.([]any)
if !ok || len(triple) != 3 {
continue
}
a, ok1 := numberToUint32String(triple[0])
bb, ok2 := numberToUint32String(triple[1])
c, ok3 := numberToUint32String(triple[2])
if !ok1 || !ok2 || !ok3 {
continue
}
ai, _ := strconv.ParseUint(a, 10, 32)
bi, _ := strconv.ParseUint(bb, 10, 32)
ci, _ := strconv.ParseUint(c, 10, 32)
fmt.Fprintf(&b, " bgp_large_community.add((%d,%d,%d));\n", ai, bi, ci)
}
s := strings.TrimRight(b.String(), "\r\n")
return s, s != ""
}
func numberToUint32String(v any) (string, bool) {
switch x := v.(type) {
case float64:
if x < 0 || x > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(x), 10), true
case json.Number:
n, err := x.Int64()
if err != nil || n < 0 || n > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(n), 10), true
case int:
if x < 0 || x > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(x), 10), true
case int64:
if x < 0 || x > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(x), 10), true
case string:
n, err := strconv.ParseUint(strings.TrimSpace(x), 10, 32)
if err != nil {
return "", false
}
return strconv.FormatUint(n, 10), true
default:
return "", false
}
}