This is an automated email from the ASF dual-hosted git repository.

xiaoxiang781216 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nuttx-ntfc.git

commit 1922d76d523af8140559be652f9f6532508b00da
Author: raiden00pl <[email protected]>
AuthorDate: Tue Aug 4 17:38:14 2026 +0200

    lib/elf: add AppBinDir for kernel-mode application discovery
    
    Kernel builds install every application as a standalone ELF binary, so
    the command name is the file name; entry points are renamed to main and
    flat-mode symbols like hello_main do not exist. AppBinDir lists the
    application directory as the command set, supports the cmd_check marker
    syntax (alternatives, regex, trailing _main), and falls back to symbol
    lookups over the unstripped binaries in bin_debug for names that are
    not files (NSH builtins, cmocka entries).
    
    Signed-off-by: raiden00pl <[email protected]>
    Assisted-by: Claude Code
---
 src/ntfc/lib/elf/app_bindir.py | 137 +++++++++++++++++++++++++++++++++++++++++
 tests/lib/test_app_bindir.py   | 103 +++++++++++++++++++++++++++++++
 2 files changed, 240 insertions(+)

diff --git a/src/ntfc/lib/elf/app_bindir.py b/src/ntfc/lib/elf/app_bindir.py
new file mode 100644
index 0000000..8f6e845
--- /dev/null
+++ b/src/ntfc/lib/elf/app_bindir.py
@@ -0,0 +1,137 @@
+############################################################################
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+############################################################################
+
+"""Kernel-mode application binary directory handler.
+
+In kernel builds every application is a standalone ELF binary installed
+to the target PATH, so the command name is the file name; entry points
+are renamed to ``main`` and symbols like ``hello_main`` do not exist.
+"""
+
+import os
+import re
+from functools import cached_property
+from typing import Collection, Iterator, List, Optional, Pattern, Set, Union
+
+from ntfc.lib.elf.elf_parser import ElfParser
+
+_MAIN_SUFFIX = "_main"
+
+
+def _match_one(name: str, commands: List[str]) -> bool:
+    """Match a single name against a list of command names."""
+    if ".*" in name:
+        regex = re.compile(name)
+        return any(regex.fullmatch(cmd) for cmd in commands)
+    return name in commands
+
+
+def match_command(pattern: str, commands: List[str]) -> bool:
+    """Match a cmd_check pattern against a list of command names.
+
+    Accepts ``a|b`` alternatives and ``.*`` regex patterns. A trailing
+    ``_main`` is stripped so flat-mode symbol markers (e.g.
+    ``hello_main``) match the ``hello`` command.
+    """
+    return any(
+        _match_one(name, commands)
+        for alt in pattern.split("|")
+        for name in (alt, alt.removesuffix(_MAIN_SUFFIX))
+    )
+
+
+def symbol_patterns(pattern: str) -> Iterator[Union[str, Pattern[str]]]:
+    """Yield normalized ELF symbol patterns for a cmd_check expression."""
+    for alternative in pattern.split("|"):
+        symbol = (
+            f"{alternative}{_MAIN_SUFFIX}"
+            if "cmocka" in alternative
+            else alternative
+        )
+        yield re.compile(symbol) if ".*" in symbol else symbol
+
+
+def match_symbol(pattern: str, symbols: Collection[str]) -> bool:
+    """Match a cmd_check expression against ELF symbol names."""
+    for candidate in symbol_patterns(pattern):
+        if isinstance(candidate, str):
+            if candidate in symbols:
+                return True
+        elif any(candidate.search(symbol) for symbol in symbols):
+            return True
+
+    return False
+
+
+class AppBinDir:
+    """Application binary directory of a kernel-mode build."""
+
+    #: sibling directory with the unstripped application binaries
+    DEBUG_DIR_NAME = "bin_debug"
+
+    def __init__(self, bindir: str, debug_bindir: Optional[str] = None):
+        """Initialize application binary directory handler.
+
+        :param bindir: directory with application binaries
+        :param debug_bindir: unstripped binaries for symbol lookups,
+                             the sibling ``bin_debug`` by default
+        """
+        self._bindir = bindir
+        self._debug_bindir = debug_bindir or os.path.join(
+            os.path.dirname(bindir), self.DEBUG_DIR_NAME
+        )
+
+    @staticmethod
+    def _scan(dirpath: Optional[str]) -> List[ElfParser]:
+        """Return a parser for every ELF file in a directory."""
+        if not dirpath or not os.path.isdir(dirpath):
+            return []
+
+        parsers = []
+        for name in sorted(os.listdir(dirpath)):
+            try:
+                parsers.append(ElfParser(os.path.join(dirpath, name)))
+            except AttributeError:
+                # not an ELF file, e.g. a script or a subdirectory
+                continue
+
+        return parsers
+
+    @cached_property
+    def commands(self) -> List[str]:
+        """Return available command names (application file names)."""
+        return [os.path.basename(p.elf_path) for p in self._scan(self._bindir)]
+
+    @cached_property
+    def _symbols(self) -> Set[str]:
+        """Return the symbols defined by the debug application binaries."""
+        return {
+            symbol.name
+            for parser in self._scan(self._debug_bindir)
+            for symbol in parser.symbols
+        }
+
+    def has_command(self, pattern: str) -> bool:
+        """Check if a command is available, see :func:`match_command`."""
+        return match_command(pattern, self.commands)
+
+    def has_symbol(self, pattern: str) -> bool:
+        """Check symbols using cmd_check alternatives and regex syntax."""
+        return match_symbol(pattern, self._symbols)
diff --git a/tests/lib/test_app_bindir.py b/tests/lib/test_app_bindir.py
new file mode 100644
index 0000000..af5ae38
--- /dev/null
+++ b/tests/lib/test_app_bindir.py
@@ -0,0 +1,103 @@
+############################################################################
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+############################################################################
+
+import shutil
+
+import pytest
+
+from ntfc.lib.elf.app_bindir import AppBinDir
+
+ELF_MAGIC = b"\x7fELF" + b"\x00" * 12
+
+
[email protected]
+def bindir(tmp_path):
+    path = tmp_path / "bin"
+    path.mkdir()
+    for name in ("init", "hello", "getprime", "sh"):
+        (path / name).write_bytes(ELF_MAGIC)
+    # non-ELF entries are not commands
+    (path / "README").write_text("not an application")
+    (path / "subdir").mkdir()
+    return path
+
+
+def test_app_bindir_commands(bindir):
+    b = AppBinDir(str(bindir))
+    assert b.commands == ["getprime", "hello", "init", "sh"]
+
+
+def test_app_bindir_missing_dir(tmp_path):
+    # neither the binaries nor the sibling debug directory exist
+    b = AppBinDir(str(tmp_path / "nonexistent"))
+    assert b.commands == []
+    assert b.has_command("hello") is False
+    assert b.has_symbol("hello_main") is False
+
+
+def test_app_bindir_has_command(bindir):
+    b = AppBinDir(str(bindir))
+
+    # exact file name
+    assert b.has_command("hello") is True
+    assert b.has_command("free") is False
+
+    # flat-mode symbol markers map to the file name
+    assert b.has_command("hello_main") is True
+    assert b.has_command("free_main") is False
+
+    # alternatives
+    assert b.has_command("free|hello") is True
+    assert b.has_command("free|df") is False
+
+    # regex
+    assert b.has_command("get.*") is True
+    assert b.has_command("xyz.*") is False
+
+
+def test_app_bindir_has_symbol(bindir):
+    # unstripped sim ELF stands in for a debug application binary
+    debug = bindir.parent / "bin_debug"
+    debug.mkdir()
+    shutil.copy("./tests/resources/nuttx/sim/nuttx", debug / "sh")
+    (debug / "README").write_text("skipped: not an ELF")
+
+    b = AppBinDir(str(bindir), str(debug))
+    assert b.has_symbol("hello_main") is True
+    assert b.has_symbol("missing|hello_main") is True
+    assert b.has_symbol("missing.*|hello_main") is True
+    assert b.has_symbol("hello_.*") is True
+    assert b.has_symbol("no_such_symbol_xyz") is False
+
+    # the sibling bin_debug directory is the default
+    assert AppBinDir(str(bindir)).has_symbol("hello_main") is True
+
+
+def test_app_bindir_symbols_span_all_binaries(bindir):
+    debug = bindir.parent / "bin_debug"
+    debug.mkdir()
+    for name in ("a", "b"):
+        shutil.copy("./tests/resources/nuttx/sim/nuttx", debug / name)
+
+    b = AppBinDir(str(bindir), str(debug))
+
+    # every binary is read, not just the first one
+    assert b.has_symbol("hello_main") is True
+    assert b.has_symbol("no_such_symbol_xyz") is False

Reply via email to