diff options
-rwxr-xr-x | bitbake/bin/bitbake-selftest | 1 | ||||
-rw-r--r-- | bitbake/lib/bb/filter.py | 142 | ||||
-rw-r--r-- | bitbake/lib/bb/tests/filter.py | 88 |
3 files changed, 231 insertions, 0 deletions
diff --git a/bitbake/bin/bitbake-selftest b/bitbake/bin/bitbake-selftest index 1b7a783fdc..d45c2d406d 100755 --- a/bitbake/bin/bitbake-selftest +++ b/bitbake/bin/bitbake-selftest | |||
@@ -32,6 +32,7 @@ tests = ["bb.tests.codeparser", | |||
32 | "bb.tests.siggen", | 32 | "bb.tests.siggen", |
33 | "bb.tests.utils", | 33 | "bb.tests.utils", |
34 | "bb.tests.compression", | 34 | "bb.tests.compression", |
35 | "bb.tests.filter", | ||
35 | "hashserv.tests", | 36 | "hashserv.tests", |
36 | "prserv.tests", | 37 | "prserv.tests", |
37 | "layerindexlib.tests.layerindexobj", | 38 | "layerindexlib.tests.layerindexobj", |
diff --git a/bitbake/lib/bb/filter.py b/bitbake/lib/bb/filter.py new file mode 100644 index 0000000000..0b5b5d92ca --- /dev/null +++ b/bitbake/lib/bb/filter.py | |||
@@ -0,0 +1,142 @@ | |||
1 | # | ||
2 | # Copyright (C) 2025 Garmin Ltd. or its subsidiaries | ||
3 | # | ||
4 | # SPDX-License-Identifier: GPL-2.0-only | ||
5 | # | ||
6 | |||
7 | import builtins | ||
8 | |||
9 | # Purposely blank out __builtins__ which prevents users from | ||
10 | # calling any normal builtin python functions | ||
11 | FILTERS = { | ||
12 | "__builtins__": {}, | ||
13 | } | ||
14 | |||
15 | CACHE = {} | ||
16 | |||
17 | |||
18 | def apply_filters(val, expressions): | ||
19 | g = FILTERS.copy() | ||
20 | |||
21 | for e in expressions: | ||
22 | e = e.strip() | ||
23 | if not e: | ||
24 | continue | ||
25 | |||
26 | k = (val, e) | ||
27 | if k not in CACHE: | ||
28 | # Set val as a local so it can be cleared out while keeping the | ||
29 | # globals | ||
30 | l = {"val": val} | ||
31 | |||
32 | CACHE[k] = eval(e, g, l) | ||
33 | |||
34 | val = CACHE[k] | ||
35 | |||
36 | return val | ||
37 | |||
38 | |||
39 | class Namespace(object): | ||
40 | """ | ||
41 | Helper class to simulate a python namespace. The object properties can be | ||
42 | set as if it were a dictionary. Properties cannot be changed or deleted | ||
43 | through the object interface | ||
44 | """ | ||
45 | |||
46 | def __getitem__(self, name): | ||
47 | return self.__dict__[name] | ||
48 | |||
49 | def __setitem__(self, name, value): | ||
50 | self.__dict__[name] = value | ||
51 | |||
52 | def __contains__(self, name): | ||
53 | return name in self.__dict__ | ||
54 | |||
55 | def __setattr__(self, name, value): | ||
56 | raise AttributeError(f"Attribute {name!r} cannot be changed") | ||
57 | |||
58 | def __delattr__(self, name): | ||
59 | raise AttributeError(f"Attribute {name!r} cannot be deleted") | ||
60 | |||
61 | |||
62 | def filter_proc(*, name=None): | ||
63 | """ | ||
64 | Decorator to mark a function that can be called in `apply_filters`, either | ||
65 | directly in a filter expression, or indirectly. The `name` argument can be | ||
66 | used to specify an alternate name for the function if the actual name is | ||
67 | not desired. The `name` can be a fully qualified namespace if desired. | ||
68 | |||
69 | All functions must be "pure" in that they do not depend on global state and | ||
70 | have no global side effects (e.g. the output only depends on the input | ||
71 | arguments); the results of filter expressions are cached to optimize | ||
72 | repeated calls. | ||
73 | """ | ||
74 | |||
75 | def inner(func): | ||
76 | global FILTERS | ||
77 | nonlocal name | ||
78 | |||
79 | if name is None: | ||
80 | name = func.__name__ | ||
81 | |||
82 | ns = name.split(".") | ||
83 | o = FILTERS | ||
84 | for n in ns[:-1]: | ||
85 | if not n in o: | ||
86 | o[n] = Namespace() | ||
87 | o = o[n] | ||
88 | |||
89 | o[ns[-1]] = func | ||
90 | |||
91 | return func | ||
92 | |||
93 | return inner | ||
94 | |||
95 | |||
96 | # A select set of builtins that are supported in filter expressions | ||
97 | filter_proc()(all) | ||
98 | filter_proc()(all) | ||
99 | filter_proc()(any) | ||
100 | filter_proc()(bin) | ||
101 | filter_proc()(bool) | ||
102 | filter_proc()(chr) | ||
103 | filter_proc()(enumerate) | ||
104 | filter_proc()(float) | ||
105 | filter_proc()(format) | ||
106 | filter_proc()(hex) | ||
107 | filter_proc()(int) | ||
108 | filter_proc()(len) | ||
109 | filter_proc()(map) | ||
110 | filter_proc()(max) | ||
111 | filter_proc()(min) | ||
112 | filter_proc()(oct) | ||
113 | filter_proc()(ord) | ||
114 | filter_proc()(pow) | ||
115 | filter_proc()(str) | ||
116 | filter_proc()(sum) | ||
117 | |||
118 | |||
119 | @filter_proc() | ||
120 | def suffix(val, suffix): | ||
121 | return " ".join(v + suffix for v in val.split()) | ||
122 | |||
123 | |||
124 | @filter_proc() | ||
125 | def prefix(val, prefix): | ||
126 | return " ".join(prefix + v for v in val.split()) | ||
127 | |||
128 | |||
129 | @filter_proc() | ||
130 | def sort(val): | ||
131 | return " ".join(sorted(val.split())) | ||
132 | |||
133 | |||
134 | @filter_proc() | ||
135 | def remove(val, remove, sep=None): | ||
136 | if isinstance(remove, str): | ||
137 | remove = remove.split(sep) | ||
138 | new = [i for i in val.split(sep) if not i in remove] | ||
139 | |||
140 | if not sep: | ||
141 | return " ".join(new) | ||
142 | return sep.join(new) | ||
diff --git a/bitbake/lib/bb/tests/filter.py b/bitbake/lib/bb/tests/filter.py new file mode 100644 index 0000000000..245df7b22b --- /dev/null +++ b/bitbake/lib/bb/tests/filter.py | |||
@@ -0,0 +1,88 @@ | |||
1 | # | ||
2 | # Copyright (C) 2025 Garmin Ltd. or its subsidiaries | ||
3 | # | ||
4 | # SPDX-License-Identifier: GPL-2.0-only | ||
5 | # | ||
6 | |||
7 | import unittest | ||
8 | import bb.filter | ||
9 | |||
10 | |||
11 | class BuiltinFilterTest(unittest.TestCase): | ||
12 | def test_disallowed_builtins(self): | ||
13 | with self.assertRaises(NameError): | ||
14 | val = bb.filter.apply_filters("1", ["open('foo.txt', 'rb')"]) | ||
15 | |||
16 | def test_prefix(self): | ||
17 | val = bb.filter.apply_filters("1 2 3", ["prefix(val, 'a')"]) | ||
18 | self.assertEqual(val, "a1 a2 a3") | ||
19 | |||
20 | val = bb.filter.apply_filters("", ["prefix(val, 'a')"]) | ||
21 | self.assertEqual(val, "") | ||
22 | |||
23 | def test_suffix(self): | ||
24 | val = bb.filter.apply_filters("1 2 3", ["suffix(val, 'b')"]) | ||
25 | self.assertEqual(val, "1b 2b 3b") | ||
26 | |||
27 | val = bb.filter.apply_filters("", ["suffix(val, 'b')"]) | ||
28 | self.assertEqual(val, "") | ||
29 | |||
30 | def test_sort(self): | ||
31 | val = bb.filter.apply_filters("z y x", ["sort(val)"]) | ||
32 | self.assertEqual(val, "x y z") | ||
33 | |||
34 | val = bb.filter.apply_filters("", ["sort(val)"]) | ||
35 | self.assertEqual(val, "") | ||
36 | |||
37 | def test_identity(self): | ||
38 | val = bb.filter.apply_filters("1 2 3", ["val"]) | ||
39 | self.assertEqual(val, "1 2 3") | ||
40 | |||
41 | val = bb.filter.apply_filters("123", ["val"]) | ||
42 | self.assertEqual(val, "123") | ||
43 | |||
44 | def test_empty(self): | ||
45 | val = bb.filter.apply_filters("1 2 3", ["", "prefix(val, 'a')", ""]) | ||
46 | self.assertEqual(val, "a1 a2 a3") | ||
47 | |||
48 | def test_nested(self): | ||
49 | val = bb.filter.apply_filters("1 2 3", ["prefix(prefix(val, 'a'), 'b')"]) | ||
50 | self.assertEqual(val, "ba1 ba2 ba3") | ||
51 | |||
52 | val = bb.filter.apply_filters("1 2 3", ["prefix(prefix(val, 'b'), 'a')"]) | ||
53 | self.assertEqual(val, "ab1 ab2 ab3") | ||
54 | |||
55 | def test_filter_order(self): | ||
56 | val = bb.filter.apply_filters("1 2 3", ["prefix(val, 'a')", "prefix(val, 'b')"]) | ||
57 | self.assertEqual(val, "ba1 ba2 ba3") | ||
58 | |||
59 | val = bb.filter.apply_filters("1 2 3", ["prefix(val, 'b')", "prefix(val, 'a')"]) | ||
60 | self.assertEqual(val, "ab1 ab2 ab3") | ||
61 | |||
62 | val = bb.filter.apply_filters("1 2 3", ["prefix(val, 'a')", "suffix(val, 'b')"]) | ||
63 | self.assertEqual(val, "a1b a2b a3b") | ||
64 | |||
65 | val = bb.filter.apply_filters("1 2 3", ["suffix(val, 'b')", "prefix(val, 'a')"]) | ||
66 | self.assertEqual(val, "a1b a2b a3b") | ||
67 | |||
68 | def test_remove(self): | ||
69 | val = bb.filter.apply_filters("1 2 3", ["remove(val, ['2'])"]) | ||
70 | self.assertEqual(val, "1 3") | ||
71 | |||
72 | val = bb.filter.apply_filters("1,2,3", ["remove(val, ['2'], ',')"]) | ||
73 | self.assertEqual(val, "1,3") | ||
74 | |||
75 | val = bb.filter.apply_filters("1 2 3", ["remove(val, ['4'])"]) | ||
76 | self.assertEqual(val, "1 2 3") | ||
77 | |||
78 | val = bb.filter.apply_filters("1 2 3", ["remove(val, ['1', '2'])"]) | ||
79 | self.assertEqual(val, "3") | ||
80 | |||
81 | val = bb.filter.apply_filters("1 2 3", ["remove(val, '2')"]) | ||
82 | self.assertEqual(val, "1 3") | ||
83 | |||
84 | val = bb.filter.apply_filters("1 2 3", ["remove(val, '4')"]) | ||
85 | self.assertEqual(val, "1 2 3") | ||
86 | |||
87 | val = bb.filter.apply_filters("1 2 3", ["remove(val, '1 2')"]) | ||
88 | self.assertEqual(val, "3") | ||