176 lines
4.7 KiB
Go
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
|
|
}
|
|
}
|