Skip to content

Commit 8b1a5ac

Browse files
committed
add scripts for generating communities json
1 parent bce40e2 commit 8b1a5ac

File tree

3 files changed

+224
-2
lines changed

3 files changed

+224
-2
lines changed

contrib/print_communities.py

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import glob
2+
import json
3+
import math
4+
import os
5+
import re
6+
import sys
7+
8+
# range regex code vendored from
9+
# https://github.com/zhfreal/range-regex/blob/ccd5c5c89410ec5062bfb74e627788e7b31dd56f/range_regex/range_regex.py
10+
# Copyright (c) 2013, Dmitry Voronin
11+
# BSD 2-Clause "Simplified" License
12+
13+
def regex_for_range(min_, max_):
14+
"""
15+
> regex_for_range(12, 345)
16+
'1[2-9]|[2-9]\d|[1-2]\d{2}|3[0-3]\d|34[0-5]'
17+
"""
18+
positive_subpatterns = []
19+
negative_subpatterns = []
20+
21+
if min_ < 0:
22+
min__ = 1
23+
if max_ < 0:
24+
min__ = abs(max_)
25+
max__ = abs(min_)
26+
27+
negative_subpatterns = split_to_patterns(min__, max__)
28+
min_ = 0
29+
30+
if max_ >= 0:
31+
positive_subpatterns = split_to_patterns(min_, max_)
32+
33+
negative_only_subpatterns = ['-' + val for val in negative_subpatterns if val not in positive_subpatterns]
34+
positive_only_subpatterns = [val for val in positive_subpatterns if val not in negative_subpatterns]
35+
intersected_subpatterns = ['-?' + val for val in negative_subpatterns if val in positive_subpatterns]
36+
37+
subpatterns = negative_only_subpatterns + intersected_subpatterns + positive_only_subpatterns
38+
return '|'.join(subpatterns)
39+
40+
41+
def split_to_patterns(min_, max_):
42+
subpatterns = []
43+
44+
start = min_
45+
for stop in split_to_ranges(min_, max_):
46+
subpatterns.append(range_to_pattern(start, stop))
47+
start = stop + 1
48+
49+
return subpatterns
50+
51+
52+
def split_to_ranges(min_, max_):
53+
stops = {max_}
54+
55+
nines_count = 1
56+
stop = fill_by_nines(min_, nines_count)
57+
while min_ <= stop < max_:
58+
stops.add(stop)
59+
60+
nines_count += 1
61+
stop = fill_by_nines(min_, nines_count)
62+
63+
zeros_count = 1
64+
stop = fill_by_zeros(max_ + 1, zeros_count) - 1
65+
while min_ < stop <= max_:
66+
stops.add(stop)
67+
68+
zeros_count += 1
69+
stop = fill_by_zeros(max_ + 1, zeros_count) - 1
70+
71+
stops = list(stops)
72+
stops.sort()
73+
74+
return stops
75+
76+
77+
def fill_by_nines(integer, nines_count):
78+
return int(str(integer)[:-nines_count] + '9' * nines_count)
79+
80+
81+
def fill_by_zeros(integer, zeros_count):
82+
return integer - integer % 10 ** zeros_count
83+
84+
85+
def range_to_pattern(start, stop):
86+
pattern = ''
87+
any_digit_count = 0
88+
89+
for start_digit, stop_digit in zip(str(start), str(stop)):
90+
if start_digit == stop_digit:
91+
pattern += start_digit
92+
elif start_digit != '0' or stop_digit != '9':
93+
pattern += '[{}-{}]'.format(start_digit, stop_digit)
94+
else:
95+
any_digit_count += 1
96+
97+
if any_digit_count:
98+
pattern += r'\d'
99+
100+
if any_digit_count > 1:
101+
pattern += '{{{}}}'.format(any_digit_count)
102+
103+
return pattern
104+
105+
106+
# Code derived from NLNOG RING looking glass code
107+
# Copyright (c) 2022 Stichting NLNOG <[email protected]>
108+
# ISC License
109+
110+
def is_regular_community(community: str) -> bool:
111+
""" check if a community string matches a regular community, with optional ranges
112+
"""
113+
re_community = re.compile(r"^[\w\-]+:[\w\-]+$")
114+
return re_community.match(community)
115+
116+
117+
def is_large_community(community: str) -> bool:
118+
""" check if a community string matches a large community, with optional ranges
119+
"""
120+
re_large = re.compile(r"^[\w\-]+:[\w\-]+:[\w\-]+$")
121+
return re_large.match(community)
122+
123+
124+
def is_extended_community(community: str) -> bool:
125+
""" check if a community string is an extended community, with optional ranges
126+
"""
127+
re_extended = re.compile(r"^\w+ [\w\-]+(:[\w\-]+)?$")
128+
return re_extended.match(community)
129+
130+
131+
def get_community_type(community: str) -> str:
132+
""" determine the community type of a community.
133+
"""
134+
if is_regular_community(community):
135+
return "regular"
136+
if is_large_community(community):
137+
return "large"
138+
if is_extended_community(community):
139+
return "extended"
140+
141+
print(f"unknown community type for '{community}'", file=sys.stderr)
142+
return "unknown"
143+
144+
def read_communities() -> dict:
145+
""" Read the list of community definitions from communities/*.txt and translate them
146+
into a dictionary containing community lists for exact matches, ranges and regexps.
147+
"""
148+
communitylist = {
149+
"regular": {},
150+
"large": {},
151+
"extended": {},
152+
}
153+
re_range = re.compile(r"(\d+)\-(\d+)")
154+
re_regular_exact = re.compile(r"^\d+:\d+$")
155+
re_large_exact = re.compile(r"^\d+:\d+:\d+$")
156+
re_extended_exact = re.compile(r"^\w+ \w+(:\w+)$")
157+
158+
files = glob.glob(f"{sys.argv[1]}/*.txt")
159+
for filename in files:
160+
with open(filename, "r", encoding="utf8") as filehandle:
161+
as_name = os.path.basename(filename).replace("as", "AS").removesuffix(".txt")
162+
for entry in [line.strip() for line in filehandle.readlines()]:
163+
if entry.startswith("#") or "," not in entry:
164+
continue
165+
(comm, desc) = entry.split(",", 1)
166+
desc = f"{as_name}: {desc}"
167+
ctype = get_community_type(comm)
168+
if ctype == "unknown":
169+
print(f"unknown communtity format: '{comm}'", file=sys.stderr)
170+
continue
171+
172+
# funky notations:
173+
# nnn -> any number
174+
# x -> any digit
175+
# a-b -> numeric range a upto b
176+
comm = comm.lower()
177+
while "nnn" in comm:
178+
comm = comm.replace("nnn", "(\d+)")
179+
while "x" in comm:
180+
comm = comm.replace("x", "(\d)")
181+
while re_range.match(comm):
182+
match = re_range.search(comm)
183+
all, first, last = match.group(0), int(match.group(1)), int(match.group(2))
184+
if first > last:
185+
print(f"Bad range for as {comm}, {first} should be less than {last}", file=sys.stderr)
186+
continue
187+
comm = comm.replace(all, regex_for_range(first, last))
188+
communitylist[ctype][comm] = desc
189+
190+
return communitylist
191+
192+
print(json.dumps(read_communities()))

flake.lock

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

+15-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,22 @@
44

55
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
66
inputs.flake-utils.url = "github:numtide/flake-utils";
7+
inputs.communities.url = "github:NLNOG/lg.ring.nlnog.net";
8+
inputs.communities.flake = false;
79

8-
outputs = { self, nixpkgs, flake-utils }: {
10+
outputs = { self, nixpkgs, flake-utils, communities }: {
911
overlays.default = final: prev: {
12+
13+
communities-json = final.callPackage (
14+
{ runCommand, python3, jq }:
15+
16+
runCommand "communities.json" {
17+
nativeBuildInputs = [ python3 jq ];
18+
} ''
19+
python3 ${./contrib/print_communities.py} ${communities}/communities | jq . > $out
20+
''
21+
) { };
22+
1023
fernglas = final.callPackage (
1124
{ lib, stdenv, rustPlatform }:
1225

@@ -240,7 +253,7 @@
240253
};
241254
in rec {
242255
packages = {
243-
inherit (pkgs) fernglas fernglas-frontend;
256+
inherit (pkgs) fernglas fernglas-frontend communities-json;
244257
default = packages.fernglas;
245258
};
246259
legacyPackages = pkgs;

0 commit comments

Comments
 (0)