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 } }