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") | ||
