summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbitbake/bin/bitbake-selftest1
-rw-r--r--bitbake/lib/bb/filter.py142
-rw-r--r--bitbake/lib/bb/tests/filter.py88
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
7import builtins
8
9# Purposely blank out __builtins__ which prevents users from
10# calling any normal builtin python functions
11FILTERS = {
12 "__builtins__": {},
13}
14
15CACHE = {}
16
17
18def 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
39class 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
62def 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
97filter_proc()(all)
98filter_proc()(all)
99filter_proc()(any)
100filter_proc()(bin)
101filter_proc()(bool)
102filter_proc()(chr)
103filter_proc()(enumerate)
104filter_proc()(float)
105filter_proc()(format)
106filter_proc()(hex)
107filter_proc()(int)
108filter_proc()(len)
109filter_proc()(map)
110filter_proc()(max)
111filter_proc()(min)
112filter_proc()(oct)
113filter_proc()(ord)
114filter_proc()(pow)
115filter_proc()(str)
116filter_proc()(sum)
117
118
119@filter_proc()
120def suffix(val, suffix):
121 return " ".join(v + suffix for v in val.split())
122
123
124@filter_proc()
125def prefix(val, prefix):
126 return " ".join(prefix + v for v in val.split())
127
128
129@filter_proc()
130def sort(val):
131 return " ".join(sorted(val.split()))
132
133
134@filter_proc()
135def 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
7import unittest
8import bb.filter
9
10
11class 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")