[lldb] [llvm] [WIP][lldb] Add mechanism for auto-loading Python scripts from pre-configured paths (PR #187031)

Michael Buch via llvm-commits llvm-commits at lists.llvm.org
Wed Mar 18 06:13:51 PDT 2026


https://github.com/Michael137 updated https://github.com/llvm/llvm-project/pull/187031

>From b76f9ce3cb7562563787c1b5c8c5c57c5b5a5b7a Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Wed, 18 Mar 2026 09:31:45 +0000
Subject: [PATCH 1/3] [lldb][Platform][NFC] Move SanitizedScriptingModuleName
 into ScriptInterpreter

In preparation for https://github.com/llvm/llvm-project/pull/187031

The `SanitizedScriptingModuleName` will be re-used from `Platform` (in
addition to `PlatformDarwin` where it currently lives). To do that we'll
need to move it to a common place. `ScriptInterpreter` seems like the
most natural place for this to live.

I introduced a new virtual `GetSanitizedScriptingModuleName` that
`ScriptInterpreter`s can override in the future if they have their own
sanitization logic. I made the default implementation the one we've been
using for Python because that way the unit-tests that currently mock the
`ScriptInterpreter` don't need to copy the implementation.

Also made `WarnIfInvalidUnsanitizedScriptExists` a protected static
function on `Platform` because it didn't seem right for that to also
live in `ScriptInterpreter`.
---
 .../lldb/Interpreter/ScriptInterpreter.h      |  45 ++++++++
 lldb/include/lldb/Target/Platform.h           |   9 ++
 lldb/source/Interpreter/ScriptInterpreter.cpp |  25 +++++
 .../Platform/MacOSX/PlatformDarwin.cpp        | 103 ++----------------
 lldb/source/Target/Platform.cpp               |  31 ++++++
 5 files changed, 117 insertions(+), 96 deletions(-)

diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 557d73a415452..dac74cc8ab1da 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -572,6 +572,51 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  /// Holds an lldb_private::Module name and a "sanitized" version
+  /// of it for the purposes of loading a script of that name by
+  /// the relevant ScriptInterpreter.
+  ///
+  /// E.g., for Python the sanitized name can't include:
+  /// * Special characters: '-', ' ', '.'
+  /// * Python keywords
+  class SanitizedScriptingModuleName {
+  public:
+    SanitizedScriptingModuleName(std::string name, std::string sanitized_name,
+                                 std::string conflicting_keyword)
+        : m_original_name(std::move(name)),
+          m_sanitized_name(std::move(sanitized_name)),
+          m_conflicting_keyword(std::move(conflicting_keyword)) {}
+
+    /// Returns \c true if this name is a keyword in the associated scripting
+    /// language.
+    bool IsKeyword() const { return !m_conflicting_keyword.empty(); }
+
+    /// Returns \c true if the original name has been sanitized (i.e., required
+    /// changes).
+    bool RequiredSanitization() const {
+      return m_sanitized_name != m_original_name;
+    }
+
+    llvm::StringRef GetSanitizedName() const { return m_sanitized_name; }
+    llvm::StringRef GetOriginalName() const { return m_original_name; }
+    llvm::StringRef GetConflictingKeyword() const {
+      return m_conflicting_keyword;
+    }
+
+  private:
+    std::string m_original_name;
+    std::string m_sanitized_name;
+
+    /// If the m_sanitized_name conflicts with a keyword for the
+    /// ScriptInterpreter language associated with this
+    /// SanitizedScriptingModuleName, is set to the conflicting keyword. Empty
+    /// otherwise.
+    std::string m_conflicting_keyword;
+  };
+
+  virtual SanitizedScriptingModuleName
+  GetSanitizedScriptingModuleName(llvm::StringRef name);
+
   lldb::DataExtractorSP
   GetDataExtractorFromSBData(const lldb::SBData &data) const;
 
diff --git a/lldb/include/lldb/Target/Platform.h b/lldb/include/lldb/Target/Platform.h
index 1ba7516f0102c..1c30bc1003ec9 100644
--- a/lldb/include/lldb/Target/Platform.h
+++ b/lldb/include/lldb/Target/Platform.h
@@ -21,6 +21,7 @@
 #include "lldb/Core/UserSettingsController.h"
 #include "lldb/Host/File.h"
 #include "lldb/Interpreter/Options.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Target/StopInfo.h"
 #include "lldb/Utility/ArchSpec.h"
 #include "lldb/Utility/ConstString.h"
@@ -1060,6 +1061,14 @@ class Platform : public PluginInterface {
 
   virtual const char *GetCacheHostname();
 
+  /// If we did some replacements of reserved characters, and a
+  /// file with the untampered name exists, then warn the user
+  /// that the file as-is shall not be loaded.
+  static void WarnIfInvalidUnsanitizedScriptExists(
+      Stream &os,
+      const ScriptInterpreter::SanitizedScriptingModuleName &sanitized_name,
+      const FileSpec &original_fspec, const FileSpec &fspec);
+
 private:
   typedef std::function<Status(const ModuleSpec &)> ModuleResolver;
 
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 5e8478c2670bb..b00f4db528ce3 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -214,6 +214,31 @@ ScriptInterpreter::AcquireInterpreterLock() {
   return std::make_unique<ScriptInterpreterLocker>();
 }
 
+ScriptInterpreter::SanitizedScriptingModuleName
+ScriptInterpreter::GetSanitizedScriptingModuleName(llvm::StringRef name) {
+  std::string sanitized_name(name);
+  std::string conflicting_keyword;
+
+  // FIXME: for Python, don't allow certain characters in imported module
+  // filenames. Theoretically, different scripting languages may have
+  // different sets of forbidden tokens in filenames, and that should
+  // be dealt with by each ScriptInterpreter. For now, just replace dots
+  // with underscores. In order to support anything other than Python
+  // this will need to be reworked.
+  llvm::replace(sanitized_name, '.', '_');
+  llvm::replace(sanitized_name, ' ', '_');
+  llvm::replace(sanitized_name, '-', '_');
+  llvm::replace(sanitized_name, '+', 'x');
+
+  if (IsReservedWord(sanitized_name.c_str())) {
+    conflicting_keyword = sanitized_name;
+    sanitized_name.insert(sanitized_name.begin(), '_');
+  }
+
+  return ScriptInterpreter::SanitizedScriptingModuleName(
+      name.str(), std::move(sanitized_name), std::move(conflicting_keyword));
+}
+
 static void ReadThreadBytesReceived(void *baton, const void *src,
                                     size_t src_len) {
   if (src && src_len) {
diff --git a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
index 2f2eed3359a93..39abd9a8a1176 100644
--- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
+++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
@@ -80,97 +80,6 @@ static Status ExceptionMaskValidator(const char *string, void *unused) {
   return {};
 }
 
-namespace {
-/// Holds an lldb_private::Module name and a "sanitized" version
-/// of it for the purposes of loading a script of that name by
-/// the relevant ScriptInterpreter.
-///
-/// E.g., for Python the sanitized name can't include:
-/// * Special characters: '-', ' ', '.'
-/// * Python keywords
-class SanitizedScriptingModuleName {
-public:
-  SanitizedScriptingModuleName(llvm::StringRef name,
-                               ScriptInterpreter &script_interpreter)
-      : m_original_name(name), m_sanitized_name(name.str()) {
-    // FIXME: for Python, don't allow certain characters in imported module
-    // filenames. Theoretically, different scripting languages may have
-    // different sets of forbidden tokens in filenames, and that should
-    // be dealt with by each ScriptInterpreter. For now, just replace dots
-    // with underscores. In order to support anything other than Python
-    // this will need to be reworked.
-    llvm::replace(m_sanitized_name, '.', '_');
-    llvm::replace(m_sanitized_name, ' ', '_');
-    llvm::replace(m_sanitized_name, '-', '_');
-    llvm::replace(m_sanitized_name, '+', 'x');
-
-    if (script_interpreter.IsReservedWord(m_sanitized_name.c_str())) {
-      m_conflicting_keyword = m_sanitized_name;
-      m_sanitized_name.insert(m_sanitized_name.begin(), '_');
-    }
-  }
-
-  /// Returns \c true if this name is a keyword in the associated scripting
-  /// language.
-  bool IsKeyword() const { return !m_conflicting_keyword.empty(); }
-
-  /// Returns \c true if the original name has been sanitized (i.e., required
-  /// changes).
-  bool RequiredSanitization() const {
-    return m_sanitized_name != m_original_name;
-  }
-
-  llvm::StringRef GetSanitizedName() const { return m_sanitized_name; }
-  llvm::StringRef GetOriginalName() const { return m_original_name; }
-  llvm::StringRef GetConflictingKeyword() const {
-    return m_conflicting_keyword;
-  }
-
-  /// If we did some replacements of reserved characters, and a
-  /// file with the untampered name exists, then warn the user
-  /// that the file as-is shall not be loaded.
-  void WarnIfInvalidUnsanitizedScriptExists(Stream &os,
-                                            const FileSpec &original_fspec,
-                                            const FileSpec &fspec) const {
-    if (!RequiredSanitization())
-      return;
-
-    // Path to unsanitized script name doesn't exist. Nothing to warn about.
-    if (!FileSystem::Instance().Exists(original_fspec))
-      return;
-
-    std::string reason_for_complaint =
-        IsKeyword() ? llvm::formatv("conflicts with the keyword '{0}'",
-                                    GetConflictingKeyword())
-                          .str()
-                    : "contains reserved characters";
-
-    if (FileSystem::Instance().Exists(fspec))
-       os.Format(
-            "debug script '{0}' cannot be loaded because '{1}' {2}. "
-            "Ignoring '{1}' and loading '{3}' instead.\n",
-            original_fspec.GetPath(), original_fspec.GetFilename(),
-            std::move(reason_for_complaint), fspec.GetFilename());
-    else
-      os.Format(
-            "debug script '{0}' cannot be loaded because '{1}' {2}. "
-            "If you intend to have this script loaded, please rename it to "
-            "'{3}' and retry.\n",
-            original_fspec.GetPath(), original_fspec.GetFilename(),
-            std::move(reason_for_complaint), fspec.GetFilename());
-  }
-
-private:
-  llvm::StringRef m_original_name;
-  std::string m_sanitized_name;
-
-  /// If the m_sanitized_name conflicts with a keyword for the ScriptInterpreter
-  /// language associated with this SanitizedScriptingModuleName, is set to the
-  /// conflicting keyword. Empty otherwise.
-  std::string m_conflicting_keyword;
-};
-} // namespace
-
 /// Destructor.
 ///
 /// The destructor is virtual since this class is designed to be
@@ -297,9 +206,11 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesFromDSYM(
 
   FileSpecList file_list;
   while (module_spec.GetFilename()) {
-    SanitizedScriptingModuleName sanitized_name(
-        module_spec.GetFilename().GetStringRef(),
-        *target.GetDebugger().GetScriptInterpreter());
+    ScriptInterpreter::SanitizedScriptingModuleName sanitized_name =
+        target.GetDebugger()
+            .GetScriptInterpreter()
+            ->GetSanitizedScriptingModuleName(
+                module_spec.GetFilename().GetStringRef());
 
     StreamString path_string;
     StreamString original_path_string;
@@ -319,8 +230,8 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesFromDSYM(
     FileSpec orig_script_fspec(original_path_string.GetString());
     FileSystem::Instance().Resolve(orig_script_fspec);
 
-    sanitized_name.WarnIfInvalidUnsanitizedScriptExists(
-        feedback_stream, orig_script_fspec, script_fspec);
+    WarnIfInvalidUnsanitizedScriptExists(feedback_stream, sanitized_name,
+                                         orig_script_fspec, script_fspec);
 
     if (FileSystem::Instance().Exists(script_fspec)) {
       file_list.Append(script_fspec);
diff --git a/lldb/source/Target/Platform.cpp b/lldb/source/Target/Platform.cpp
index 647f8389dc41b..e159632a446fd 100644
--- a/lldb/source/Target/Platform.cpp
+++ b/lldb/source/Target/Platform.cpp
@@ -2111,6 +2111,37 @@ Platform::LocateModuleCallback Platform::GetLocateModuleCallback() const {
   return m_locate_module_callback;
 }
 
+void Platform::WarnIfInvalidUnsanitizedScriptExists(
+    Stream &os,
+    const ScriptInterpreter::SanitizedScriptingModuleName &sanitized_name,
+    const FileSpec &original_fspec, const FileSpec &fspec) {
+  if (!sanitized_name.RequiredSanitization())
+    return;
+
+  // Path to unsanitized script name doesn't exist. Nothing to warn about.
+  if (!FileSystem::Instance().Exists(original_fspec))
+    return;
+
+  std::string reason_for_complaint =
+      sanitized_name.IsKeyword()
+          ? llvm::formatv("conflicts with the keyword '{0}'",
+                          sanitized_name.GetConflictingKeyword())
+                .str()
+          : "contains reserved characters";
+
+  if (FileSystem::Instance().Exists(fspec))
+    os.Format("debug script '{0}' cannot be loaded because '{1}' {2}. "
+              "Ignoring '{1}' and loading '{3}' instead.\n",
+              original_fspec.GetPath(), original_fspec.GetFilename(),
+              std::move(reason_for_complaint), fspec.GetFilename());
+  else
+    os.Format("debug script '{0}' cannot be loaded because '{1}' {2}. "
+              "If you intend to have this script loaded, please rename it to "
+              "'{3}' and retry.\n",
+              original_fspec.GetPath(), original_fspec.GetFilename(),
+              std::move(reason_for_complaint), fspec.GetFilename());
+}
+
 PlatformSP PlatformList::GetOrCreate(llvm::StringRef name) {
   std::lock_guard<std::recursive_mutex> guard(m_mutex);
   for (const PlatformSP &platform_sp : m_platforms) {

>From 1120b45e6cfadbf8124ec0cef3003e4f9df61a1d Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Fri, 13 Mar 2026 15:40:12 +0000
Subject: [PATCH 2/3] TEMPORARY: initial libc++ test

---
 libc++.1/libcxx_1.py | 471 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 471 insertions(+)
 create mode 100644 libc++.1/libcxx_1.py

diff --git a/libc++.1/libcxx_1.py b/libc++.1/libcxx_1.py
new file mode 100644
index 0000000000000..b2cf2c47ebbe1
--- /dev/null
+++ b/libc++.1/libcxx_1.py
@@ -0,0 +1,471 @@
+"""
+Python LLDB synthetic child provider for libc++ std::map
+
+Converted from LibCxxMap.cpp
+
+Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+See https://llvm.org/LICENSE.txt for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""
+
+import lldb
+
+
+# The flattened layout of the std::__tree_iterator::__ptr_ looks
+# as follows:
+#
+# The following shows the contiguous block of memory:
+#
+#        +-----------------------------+ class __tree_end_node
+# __ptr_ | pointer __left_;            |
+#        +-----------------------------+ class __tree_node_base
+#        | pointer __right_;           |
+#        | __parent_pointer __parent_; |
+#        | bool __is_black_;           |
+#        +-----------------------------+ class __tree_node
+#        | __node_value_type __value_; | <<< our key/value pair
+#        +-----------------------------+
+#
+# where __ptr_ has type __iter_pointer.
+
+
+class MapEntry:
+    """Wrapper around an LLDB ValueObject representing a tree node entry."""
+
+    def __init__(self, entry_sp=None):
+        self.m_entry_sp = entry_sp
+
+    def left(self):
+        """Get the left child pointer (offset 0)."""
+        if not self.m_entry_sp:
+            return None
+        return self.m_entry_sp.CreateChildAtOffset("", 0, self.m_entry_sp.GetType())
+
+    def right(self):
+        """Get the right child pointer (offset = address size)."""
+        if not self.m_entry_sp:
+            return None
+        addr_size = self.m_entry_sp.GetProcess().GetAddressByteSize()
+        return self.m_entry_sp.CreateChildAtOffset(
+            "", addr_size, self.m_entry_sp.GetType()
+        )
+
+    def parent(self):
+        """Get the parent pointer (offset = 2 * address size)."""
+        if not self.m_entry_sp:
+            return None
+        addr_size = self.m_entry_sp.GetProcess().GetAddressByteSize()
+        return self.m_entry_sp.CreateChildAtOffset(
+            "", 2 * addr_size, self.m_entry_sp.GetType()
+        )
+
+    def value(self):
+        """Get the unsigned integer value of the entry (pointer address)."""
+        if not self.m_entry_sp:
+            return 0
+        return self.m_entry_sp.GetValueAsUnsigned(0)
+
+    def error(self):
+        """Check if the entry has an error."""
+        if not self.m_entry_sp:
+            return True
+        return self.m_entry_sp.GetError().Fail()
+
+    def null(self):
+        """Check if the entry is null (value == 0)."""
+        return self.value() == 0
+
+    def get_entry(self):
+        """Get the underlying ValueObject."""
+        return self.m_entry_sp
+
+    def set_entry(self, entry):
+        """Set the underlying ValueObject."""
+        self.m_entry_sp = entry
+
+    def __eq__(self, other):
+        if not isinstance(other, MapEntry):
+            return False
+        if self.m_entry_sp is None and other.m_entry_sp is None:
+            return True
+        if self.m_entry_sp is None or other.m_entry_sp is None:
+            return False
+        return self.m_entry_sp.GetLoadAddress() == other.m_entry_sp.GetLoadAddress()
+
+
+class MapIterator:
+    """Iterator for traversing the red-black tree backing std::map."""
+
+    def __init__(self, entry=None, depth=0):
+        self.m_entry = MapEntry(entry) if entry else MapEntry()
+        self.m_max_depth = depth
+        self.m_error = False
+
+    def value(self):
+        """Get the current entry."""
+        return self.m_entry.get_entry()
+
+    def advance(self, count):
+        """Advance the iterator by count steps and return the entry."""
+        if self.m_error:
+            return None
+
+        steps = 0
+        while count > 0:
+            self._next()
+            count -= 1
+            steps += 1
+            if self.m_error or self.m_entry.null() or (steps > self.m_max_depth):
+                return None
+
+        return self.m_entry.get_entry()
+
+    def _next(self):
+        """
+        Mimics libc++'s __tree_next algorithm, which libc++ uses
+        in its __tree_iterator::operator++.
+        """
+        if self.m_entry.null():
+            return
+
+        right = MapEntry(self.m_entry.right())
+        if not right.null():
+            self.m_entry = self._tree_min(right)
+            return
+
+        steps = 0
+        while not self._is_left_child(self.m_entry):
+            if self.m_entry.error():
+                self.m_error = True
+                return
+            self.m_entry.set_entry(self.m_entry.parent())
+            steps += 1
+            if steps > self.m_max_depth:
+                self.m_entry = MapEntry()
+                return
+
+        self.m_entry = MapEntry(self.m_entry.parent())
+
+    def _tree_min(self, x):
+        """Mimics libc++'s __tree_min algorithm."""
+        if x.null():
+            return MapEntry()
+
+        left = MapEntry(x.left())
+        steps = 0
+        while not left.null():
+            if left.error():
+                self.m_error = True
+                return MapEntry()
+            x = MapEntry(left.get_entry())  # Create new MapEntry to avoid aliasing
+            left = MapEntry(x.left())
+            steps += 1
+            if steps > self.m_max_depth:
+                return MapEntry()
+
+        return x
+
+    def _is_left_child(self, x):
+        """Check if x is a left child of its parent."""
+        if x.null():
+            return False
+        rhs = MapEntry(x.parent())
+        rhs.set_entry(rhs.left())
+        return x.value() == rhs.value()
+
+
+def _get_first_value_of_libcxx_compressed_pair(pair):
+    """
+    Get the first value from a libc++ compressed pair.
+    Handles both old and new layouts.
+    """
+    # Try __value_ first (newer layout)
+    value_sp = pair.GetChildMemberWithName("__value_")
+    if value_sp and value_sp.IsValid():
+        return value_sp
+
+    # Try __first_ (older layout)
+    first_sp = pair.GetChildMemberWithName("__first_")
+    if first_sp and first_sp.IsValid():
+        return first_sp
+
+    return None
+
+
+def _get_value_or_old_compressed_pair(tree, value_name, pair_name):
+    """
+    Try to get a value member directly, or fall back to compressed pair layout.
+    Returns (value_sp, is_compressed_pair).
+    """
+    # Try new layout first (direct member)
+    value_sp = tree.GetChildMemberWithName(value_name)
+    if value_sp and value_sp.IsValid():
+        return (value_sp, False)
+
+    # Fall back to old compressed pair layout
+    pair_sp = tree.GetChildMemberWithName(pair_name)
+    if pair_sp and pair_sp.IsValid():
+        return (pair_sp, True)
+
+    return (None, False)
+
+
+class LibcxxStdMapSyntheticProvider:
+    """Synthetic children provider for libc++ std::map."""
+
+    def __init__(self, valobj, internal_dict):
+        self.valobj = valobj
+        self.m_tree = None
+        self.m_root_node = None
+        self.m_node_ptr_type = None
+        self.m_count = None
+        self.m_iterators = {}
+
+    def num_children(self):
+        """Calculate the number of children (map size)."""
+        if self.m_count is not None:
+            return self.m_count
+
+        if self.m_tree is None:
+            return 0
+
+        # Try new layout (__size_) or old compressed pair layout (__pair3_)
+        size_sp, is_compressed_pair = _get_value_or_old_compressed_pair(
+            self.m_tree, "__size_", "__pair3_"
+        )
+
+        if not size_sp:
+            return 0
+
+        if is_compressed_pair:
+            return self._calculate_num_children_for_old_compressed_pair_layout(size_sp)
+
+        self.m_count = size_sp.GetValueAsUnsigned(0)
+        return self.m_count
+
+    def _calculate_num_children_for_old_compressed_pair_layout(self, pair):
+        """Handle old libc++ compressed pair layout."""
+        node_sp = _get_first_value_of_libcxx_compressed_pair(pair)
+
+        if not node_sp:
+            return 0
+
+        self.m_count = node_sp.GetValueAsUnsigned(0)
+        return self.m_count
+
+    def get_child_index(self, name):
+        """Get the index of a child with the given name (e.g., "[0]" -> 0)."""
+        try:
+            if name.startswith("[") and name.endswith("]"):
+                return int(name[1:-1])
+        except ValueError:
+            pass
+        return None
+
+    def get_child_at_index(self, index):
+        """Get the child at the given index."""
+        num_children = self.num_children()
+        if index >= num_children:
+            return None
+
+        if self.m_tree is None or self.m_root_node is None:
+            return None
+
+        key_val_sp = self._get_key_value_pair(index, num_children)
+        if not key_val_sp:
+            # This will stop all future searches until an update() happens
+            self.m_tree = None
+            return None
+
+        # Create a synthetic child with the appropriate name
+        name = "[%d]" % index
+        potential_child_sp = key_val_sp.Clone(name)
+
+        if potential_child_sp and potential_child_sp.IsValid():
+            num_child_children = potential_child_sp.GetNumChildren()
+
+            # Handle __cc_ or __cc wrapper (1 child case)
+            if num_child_children == 1:
+                child0_sp = potential_child_sp.GetChildAtIndex(0)
+                if child0_sp:
+                    child_name = child0_sp.GetName()
+                    if child_name in ("__cc_", "__cc"):
+                        potential_child_sp = child0_sp.Clone(name)
+
+            # Handle __cc_ and __nc wrapper (2 children case)
+            elif num_child_children == 2:
+                child0_sp = potential_child_sp.GetChildAtIndex(0)
+                child1_sp = potential_child_sp.GetChildAtIndex(1)
+                if child0_sp and child1_sp:
+                    child0_name = child0_sp.GetName()
+                    child1_name = child1_sp.GetName()
+                    if child0_name in ("__cc_", "__cc") and child1_name == "__nc":
+                        potential_child_sp = child0_sp.Clone(name)
+
+        return potential_child_sp
+
+    def update(self):
+        """Update the cached state. Called when the underlying value changes."""
+        self.m_count = None
+        self.m_tree = None
+        self.m_root_node = None
+        self.m_iterators.clear()
+
+        self.m_tree = self.valobj.GetChildMemberWithName("__tree_")
+        if not self.m_tree or not self.m_tree.IsValid():
+            return False
+
+        self.m_root_node = self.m_tree.GetChildMemberWithName("__begin_node_")
+
+        # Get the __node_pointer type from the tree's type
+        tree_type = self.m_tree.GetType()
+        self.m_node_ptr_type = tree_type.FindDirectNestedType("__node_pointer")
+
+        return False
+
+    def has_children(self):
+        """Check if this object has children."""
+        return True
+
+    def _get_key_value_pair(self, idx, max_depth):
+        """
+        Returns the ValueObject for the __tree_node type that
+        holds the key/value pair of the node at index idx.
+        """
+        iterator = MapIterator(self.m_root_node, max_depth)
+
+        advance_by = idx
+        if idx > 0:
+            # If we have already created the iterator for the previous
+            # index, we can start from there and advance by 1.
+            if idx - 1 in self.m_iterators:
+                iterator = self.m_iterators[idx - 1]
+                advance_by = 1
+
+        iterated_sp = iterator.advance(advance_by)
+        if not iterated_sp:
+            # This tree is garbage - stop
+            return None
+
+        if not self.m_node_ptr_type or not self.m_node_ptr_type.IsValid():
+            return None
+
+        # iterated_sp is a __iter_pointer at this point.
+        # We can cast it to a __node_pointer (which is what libc++ does).
+        value_type_sp = iterated_sp.Cast(self.m_node_ptr_type)
+        if not value_type_sp or not value_type_sp.IsValid():
+            return None
+
+        # Finally, get the key/value pair.
+        value_type_sp = value_type_sp.GetChildMemberWithName("__value_")
+        if not value_type_sp or not value_type_sp.IsValid():
+            return None
+
+        self.m_iterators[idx] = iterator
+
+        return value_type_sp
+
+
+class LibCxxMapIteratorSyntheticProvider:
+    """Synthetic children provider for libc++ std::map::iterator."""
+
+    def __init__(self, valobj, internal_dict):
+        self.valobj = valobj
+        self.m_pair_sp = None
+
+    def num_children(self):
+        """Map iterators always have 2 children (first and second)."""
+        return 2
+
+    def get_child_index(self, name):
+        """Get the index of a child with the given name."""
+        if not self.m_pair_sp:
+            return None
+        return self.m_pair_sp.GetIndexOfChildWithName(name)
+
+    def get_child_at_index(self, index):
+        """Get the child at the given index."""
+        if not self.m_pair_sp:
+            return None
+        return self.m_pair_sp.GetChildAtIndex(index)
+
+    def update(self):
+        """Update the cached state."""
+        self.m_pair_sp = None
+
+        if not self.valobj.IsValid():
+            return False
+
+        target = self.valobj.GetTarget()
+        if not target or not target.IsValid():
+            return False
+
+        # valobj is a std::map::iterator
+        # ...which is a __map_iterator<__tree_iterator<..., __node_pointer, ...>>
+        #
+        # Then, __map_iterator::__i_ is a __tree_iterator
+        tree_iter_sp = self.valobj.GetChildMemberWithName("__i_")
+        if not tree_iter_sp or not tree_iter_sp.IsValid():
+            return False
+
+        # Type is __tree_iterator::__node_pointer
+        # (We could alternatively also get this from the template argument)
+        tree_iter_type = tree_iter_sp.GetType()
+        node_pointer_type = tree_iter_type.FindDirectNestedType("__node_pointer")
+        if not node_pointer_type or not node_pointer_type.IsValid():
+            return False
+
+        # __ptr_ is a __tree_iterator::__iter_pointer
+        iter_pointer_sp = tree_iter_sp.GetChildMemberWithName("__ptr_")
+        if not iter_pointer_sp or not iter_pointer_sp.IsValid():
+            return False
+
+        # Cast the __iter_pointer to a __node_pointer (which stores our key/value pair)
+        node_pointer_sp = iter_pointer_sp.Cast(node_pointer_type)
+        if not node_pointer_sp or not node_pointer_sp.IsValid():
+            return False
+
+        key_value_sp = node_pointer_sp.GetChildMemberWithName("__value_")
+        if not key_value_sp or not key_value_sp.IsValid():
+            return False
+
+        # Create the synthetic child, which is a pair where the key and value can be
+        # retrieved by querying the synthetic provider for
+        # get_child_index("first") and get_child_index("second")
+        # respectively.
+        #
+        # std::map stores the actual key/value pair in value_type::__cc_ (or
+        # previously __cc).
+        key_value_sp = key_value_sp.Clone("pair")
+        if key_value_sp.GetNumChildren() == 1:
+            child0_sp = key_value_sp.GetChildAtIndex(0)
+            if child0_sp:
+                child_name = child0_sp.GetName()
+                if child_name in ("__cc_", "__cc"):
+                    key_value_sp = child0_sp.Clone("pair")
+
+        self.m_pair_sp = key_value_sp
+        return False
+
+    def has_children(self):
+        """Check if this object has children."""
+        return True
+
+
+def __lldb_init_module(debugger, internal_dict):
+    debugger.HandleCommand(
+        f'type synthetic add -x "^std::__[[:alnum:]]+::map<.+> >$" -l libcxx_1.LibcxxStdMapSyntheticProvider -w "cplusplus-py"'
+    )
+    debugger.HandleCommand(
+        f'type synthetic add -x "^std::__[[:alnum:]]+::set<.+> >$" -l libcxx_1.LibcxxStdMapSyntheticProvider -w "cplusplus-py"'
+    )
+    debugger.HandleCommand(
+        f'type synthetic add -x "^std::__[[:alnum:]]+::multiset<.+> >$" -l libcxx_1.LibcxxStdMapSyntheticProvider -w "cplusplus-py"'
+    )
+    debugger.HandleCommand(
+        f'type synthetic add -x "^std::__[[:alnum:]]+::multimap<.+> >$" -l libcxx_1.LibcxxStdMapSyntheticProvider -w "cplusplus-py"'
+    )
+    debugger.HandleCommand(
+        f'type synthetic add -x "^std::__[[:alnum:]]+::__map_(const_)?iterator<.+>$" -l libcxx_1.LibCxxMapIteratorSyntheticProvider -w "cplusplus-py"'
+    )
+    debugger.HandleCommand(f"type category enable cplusplus-py")

>From bd0b0b56e33bae931f30add4f55797107c7a2634 Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Tue, 10 Mar 2026 14:17:59 +0000
Subject: [PATCH 3/3] [lldb] Add mechanism for auto-loading Python scripts from
 pre-configured paths

Depends on:
* https://github.com/llvm/llvm-project/pull/187229

(only last commit is relevant)

This patch implements the base infrastructure described in this [RFC re. Moving libc++ data-formatters out of LLDB](https://discourse.llvm.org/t/rfc-lldb-moving-libc-data-formatters-out-of-lldb/89591)

The intention is to provide vendors with a way to pre-configure a set of paths that LLDB can automatically ingest formatter scripts from.

Three main changes
---
 lldb/include/lldb/Core/Debugger.h             |   5 +
 lldb/include/lldb/Target/Platform.h           |  17 +-
 lldb/source/Core/CMakeLists.txt               |  18 +
 lldb/source/Core/CoreProperties.td            |   4 +
 lldb/source/Core/Debugger.cpp                 |  51 ++
 lldb/source/Core/Module.cpp                   |   3 +
 lldb/source/Core/SafeAutoloadPaths.inc.in     |   1 +
 .../Platform/MacOSX/PlatformDarwin.cpp        |   3 +-
 .../Plugins/Platform/MacOSX/PlatformDarwin.h  |   4 +-
 lldb/source/Target/Platform.cpp               |  73 ++-
 lldb/source/Target/TargetProperties.td        |   4 +
 .../Darwin/dsym-takes-priority.test           |  31 ++
 .../safe-path-fallback-no-dsym-script.test    |  24 +
 .../SafeAutoLoad/UNIX/basic-load.test         |  24 +
 .../SafeAutoLoad/UNIX/last-path-wins.test     |  31 ++
 .../UNIX/no-match-nested-dir.test             |  24 +
 .../UNIX/no-match-wrong-dirname.test          |  24 +
 .../UNIX/no-match-wrong-scriptname.test       |  24 +
 .../UNIX/no-paths-configured.test             |  22 +
 .../UNIX/special-chars-sanitized.test         |  24 +
 .../SafeAutoLoad/UNIX/submodule-import.test   |  30 ++
 .../SafeAutoLoad/UNIX/subpackage-import.test  |  30 ++
 lldb/unittests/Platform/PlatformTest.cpp      | 471 ++++++++++++++++++
 23 files changed, 935 insertions(+), 7 deletions(-)
 create mode 100644 lldb/source/Core/SafeAutoloadPaths.inc.in
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test

diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index 96f586a6f1989..e8f830337d26a 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -76,6 +76,9 @@ struct TestingProperties : public Properties {
   TestingProperties();
   bool GetInjectVarLocListError() const;
   static TestingProperties &GetGlobalTestingProperties();
+  void SetSafeAutoLoadPaths(FileSpecList paths);
+  void AppendSafeAutoLoadPaths(FileSpec path);
+  FileSpecList GetSafeAutoLoadPaths() const;
 };
 #endif
 
@@ -131,6 +134,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
   static void AssertCallback(llvm::StringRef message, llvm::StringRef backtrace,
                              llvm::StringRef prompt);
 
+  static FileSpecList GetSafeAutoLoadPaths();
+
   void Clear();
 
   void DispatchClientTelemetry(const lldb_private::StructuredDataImpl &entry);
diff --git a/lldb/include/lldb/Target/Platform.h b/lldb/include/lldb/Target/Platform.h
index 1c30bc1003ec9..d150cc35a1bd1 100644
--- a/lldb/include/lldb/Target/Platform.h
+++ b/lldb/include/lldb/Target/Platform.h
@@ -275,9 +275,22 @@ class Platform : public PluginInterface {
   //
   // Locating the file should happen only on the local computer or using the
   // current computers global settings.
+  FileSpecList LocateExecutableScriptingResources(Target *target,
+                                                  Module &module,
+                                                  Stream &feedback_stream);
+
   virtual FileSpecList
-  LocateExecutableScriptingResources(Target *target, Module &module,
-                                     Stream &feedback_stream);
+  LocateExecutableScriptingResourcesImpl(Target *target, Module &module,
+                                         Stream &feedback_stream);
+
+  /// Helper function for \c LocateExecutableScriptingResources
+  /// which gathers FileSpecs for executable scripts from
+  /// pre-configured "safe" auto-load paths.
+  ///
+  /// Looks for a script at:
+  ///   \c <safe-path>/<module-name>/<module-name>.py
+  static FileSpecList LocateExecutableScriptingResourcesFromSafePaths(
+      Stream &feedback_stream, FileSpec module_spec, const Target &target);
 
   /// \param[in] module_spec
   ///     The ModuleSpec of a binary to find.
diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt
index df35bd5c025f3..8770039ae412e 100644
--- a/lldb/source/Core/CMakeLists.txt
+++ b/lldb/source/Core/CMakeLists.txt
@@ -85,6 +85,24 @@ add_lldb_library(lldbCore NO_PLUGIN_DEPENDENCIES
     clangDriver
   )
 
+set(LLDB_SAFE_AUTO_LOAD_PATHS "" CACHE STRING
+    "Semicolon-separated list of paths that LLDB will automatically load scripting resources from.")
+
+# Turn list of paths into a comma-separated list to be #include'd
+# into a C-array.
+set(_entries "")
+foreach(path IN LISTS LLDB_SAFE_AUTO_LOAD_PATHS)
+  string(REPLACE "\\" "\\\\" escaped_path "${path}")
+  list(APPEND _entries "\"${escaped_path}\",")
+endforeach()
+string(JOIN "\n" SAFE_PATH_ENTRIES "${_entries}")
+
+configure_file(
+  ${CMAKE_CURRENT_SOURCE_DIR}/SafeAutoloadPaths.inc.in
+  ${CMAKE_CURRENT_BINARY_DIR}/SafeAutoloadPaths.inc
+  @ONLY
+)
+
 add_dependencies(lldbCore
   LLDBCorePropertiesGen
   LLDBCorePropertiesEnumGen)
diff --git a/lldb/source/Core/CoreProperties.td b/lldb/source/Core/CoreProperties.td
index 03326b130e067..8cea0931868aa 100644
--- a/lldb/source/Core/CoreProperties.td
+++ b/lldb/source/Core/CoreProperties.td
@@ -60,6 +60,10 @@ let Definition = "testing", Path = "testing" in {
         Global,
         DefaultFalse,
         Desc<"Used for testing LLDB only. Hide locations of local variables.">;
+  def SafeAutoloadPaths : Property<"safe-auto-load-paths", "FileSpecList">,
+                          DefaultStringValue<"">,
+                          Desc<"List of paths that LLDB will automatically "
+                               "load scripting resources from.">;
 }
 #endif
 
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index fc099891443df..3d4bd5b55b434 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -32,6 +32,7 @@
 #include "lldb/Interpreter/CommandInterpreter.h"
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Interpreter/OptionValue.h"
+#include "lldb/Interpreter/OptionValueFileSpecList.h"
 #include "lldb/Interpreter/OptionValueLanguage.h"
 #include "lldb/Interpreter/OptionValueProperties.h"
 #include "lldb/Interpreter/OptionValueSInt64.h"
@@ -212,6 +213,27 @@ enum {
 };
 #endif
 
+static const FileSpecList &GetDefaultSafeAutoLoadPaths() {
+  static const FileSpecList sSafePaths = [] {
+    // FIXME: in c++20 this could be a std::array (with CTAD deduced size)
+    // and we could statically assert that all members are non-empty.
+    const llvm::SmallVector<llvm::StringRef> kVendorSafePaths = {
+#include "SafeAutoloadPaths.inc"
+    };
+    FileSpecList fspecs;
+    for (auto path : kVendorSafePaths) {
+      assert(!path.empty());
+      LLDB_LOG(GetLog(SystemLog::System), "Safe auto-load path configured: {0}",
+               path);
+      fspecs.EmplaceBack(path);
+    }
+
+    return fspecs;
+  }();
+
+  return sSafePaths;
+}
+
 #ifndef NDEBUG
 TestingProperties::TestingProperties() {
   m_collection_sp = std::make_shared<OptionValueProperties>("testing");
@@ -228,6 +250,27 @@ TestingProperties &TestingProperties::GetGlobalTestingProperties() {
   static TestingProperties g_testing_properties;
   return g_testing_properties;
 }
+
+void TestingProperties::SetSafeAutoLoadPaths(FileSpecList paths) {
+  const uint32_t idx = ePropertySafeAutoloadPaths;
+  OptionValueFileSpecList *option_value =
+      m_collection_sp->GetPropertyAtIndexAsOptionValueFileSpecList(idx);
+  assert(option_value);
+  option_value->SetCurrentValue(std::move(paths));
+}
+
+void TestingProperties::AppendSafeAutoLoadPaths(FileSpec path) {
+  const uint32_t idx = ePropertySafeAutoloadPaths;
+  OptionValueFileSpecList *option_value =
+      m_collection_sp->GetPropertyAtIndexAsOptionValueFileSpecList(idx);
+  assert(option_value);
+  option_value->AppendCurrentValue(path);
+}
+
+FileSpecList TestingProperties::GetSafeAutoLoadPaths() const {
+  const uint32_t idx = ePropertySafeAutoloadPaths;
+  return GetPropertyAtIndexAs<FileSpecList>(idx, GetDefaultSafeAutoLoadPaths());
+}
 #endif
 
 LoadPluginCallbackType Debugger::g_load_plugin_callback = nullptr;
@@ -2547,3 +2590,11 @@ StructuredData::DictionarySP Debugger::GetBuildConfiguration() {
   AddLLVMTargets(*config_up);
   return config_up;
 }
+
+FileSpecList Debugger::GetSafeAutoLoadPaths() {
+#ifndef NDEBUG
+  return TestingProperties::GetGlobalTestingProperties().GetSafeAutoLoadPaths();
+#else
+  return GetDefaultSafeAutoLoadPaths();
+#endif
+}
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index bbab89ff8db4f..eb0345158dd2a 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -1489,6 +1489,9 @@ To run all discovered debug scripts in this session:
       return false;
     }
 
+    LLDB_LOG(GetLog(LLDBLog::Modules), "Auto-loading {0}",
+             scripting_fspec.GetPath());
+
     StreamString scripting_stream;
     scripting_fspec.Dump(scripting_stream.AsRawOstream());
     LoadScriptOptions options;
diff --git a/lldb/source/Core/SafeAutoloadPaths.inc.in b/lldb/source/Core/SafeAutoloadPaths.inc.in
new file mode 100644
index 0000000000000..4312239c10a63
--- /dev/null
+++ b/lldb/source/Core/SafeAutoloadPaths.inc.in
@@ -0,0 +1 @@
+ at SAFE_PATH_ENTRIES@
diff --git a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
index 39abd9a8a1176..960616b2fef1c 100644
--- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
+++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
@@ -251,7 +251,7 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesFromDSYM(
   return file_list;
 }
 
-FileSpecList PlatformDarwin::LocateExecutableScriptingResources(
+FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesImpl(
     Target *target, Module &module, Stream &feedback_stream) {
   if (!target)
     return {};
@@ -268,7 +268,6 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResources(
   // stripped while leaving "this.binary.file" as-is.
 
   const FileSpec &module_spec = module.GetFileSpec();
-
   if (!module_spec)
     return {};
 
diff --git a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
index e884bcba5c2cc..f17636e8b2e42 100644
--- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
+++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
@@ -68,8 +68,8 @@ class PlatformDarwin : public PlatformPOSIX {
                            FileSpec &sym_file) override;
 
   FileSpecList
-  LocateExecutableScriptingResources(Target *target, Module &module,
-                                     Stream &feedback_stream) override;
+  LocateExecutableScriptingResourcesImpl(Target *target, Module &module_spec,
+                                         Stream &feedback_stream) override;
 
   Status GetSharedModule(const ModuleSpec &module_spec, Process *process,
                          lldb::ModuleSP &module_sp,
diff --git a/lldb/source/Target/Platform.cpp b/lldb/source/Target/Platform.cpp
index e159632a446fd..c0933b7e78f40 100644
--- a/lldb/source/Target/Platform.cpp
+++ b/lldb/source/Target/Platform.cpp
@@ -27,6 +27,7 @@
 #include "lldb/Interpreter/OptionValueFileSpec.h"
 #include "lldb/Interpreter/OptionValueProperties.h"
 #include "lldb/Interpreter/Property.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Symbol/ObjectFile.h"
 #include "lldb/Target/ModuleCache.h"
 #include "lldb/Target/Platform.h"
@@ -41,6 +42,7 @@
 #include "lldb/Utility/StructuredData.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/Path.h"
 
 // Define these constants from POSIX mman.h rather than include the file so
@@ -155,10 +157,79 @@ Status Platform::GetFileWithUUID(const FileSpec &platform_file,
   return Status();
 }
 
+FileSpecList Platform::LocateExecutableScriptingResourcesFromSafePaths(
+    Stream &feedback_stream, FileSpec module_spec, const Target &target) {
+  assert(module_spec);
+  assert(target.GetDebugger().GetScriptInterpreter());
+
+  // For now only Python scripts supported for auto-loading.
+  if (target.GetDebugger().GetScriptLanguage() != eScriptLanguagePython)
+    return {};
+
+  ScriptInterpreter::SanitizedScriptingModuleName sanitized_name =
+      target.GetDebugger()
+          .GetScriptInterpreter()
+          ->GetSanitizedScriptingModuleName(
+              module_spec.GetFileNameStrippingExtension().GetStringRef());
+
+  FileSpecList file_list;
+  FileSpecList paths = Debugger::GetSafeAutoLoadPaths();
+
+  // Iterate in reverse so we consider the latest appended path first.
+  for (FileSpec path : llvm::reverse(paths)) {
+    path.AppendPathComponent(sanitized_name.GetOriginalName());
+
+    if (!FileSystem::Instance().Exists(path))
+      continue;
+
+    FileSpec script_fspec = path;
+    script_fspec.AppendPathComponent(
+        llvm::formatv("{0}.py", sanitized_name.GetSanitizedName()).str());
+    FileSystem::Instance().Resolve(script_fspec);
+
+    FileSpec orig_script_fspec = path;
+    orig_script_fspec.AppendPathComponent(
+        llvm::formatv("{0}.py", sanitized_name.GetOriginalName()).str());
+    FileSystem::Instance().Resolve(orig_script_fspec);
+
+    WarnIfInvalidUnsanitizedScriptExists(feedback_stream, sanitized_name,
+                                         orig_script_fspec, script_fspec);
+
+    if (FileSystem::Instance().Exists(script_fspec))
+      file_list.Append(script_fspec);
+
+    // If we successfully found a directory in a safe auto-load path
+    // stop looking at any other paths.
+    break;
+  }
+
+  return file_list;
+}
+
+FileSpecList
+Platform::LocateExecutableScriptingResourcesImpl(Target *target, Module &module,
+                                                 Stream &feedback_stream) {
+  return {};
+}
+
 FileSpecList
 Platform::LocateExecutableScriptingResources(Target *target, Module &module,
                                              Stream &feedback_stream) {
-  return FileSpecList();
+  if (!target)
+    return {};
+
+  // Give derived platforms a chance to locate scripting resources.
+  if (FileSpecList fspecs = LocateExecutableScriptingResourcesImpl(
+          target, module, feedback_stream);
+      !fspecs.IsEmpty())
+    return fspecs;
+
+  const FileSpec &module_spec = module.GetFileSpec();
+  if (!module_spec)
+    return {};
+
+  return LocateExecutableScriptingResourcesFromSafePaths(feedback_stream,
+                                                         module_spec, *target);
 }
 
 Status Platform::GetSharedModule(
diff --git a/lldb/source/Target/TargetProperties.td b/lldb/source/Target/TargetProperties.td
index 2361314d506ac..a4e18f5b59eea 100644
--- a/lldb/source/Target/TargetProperties.td
+++ b/lldb/source/Target/TargetProperties.td
@@ -180,6 +180,10 @@ let Definition = "target", Path = "target" in {
     DefaultEnumValue<"eLoadScriptFromSymFileWarn">,
     EnumValues<"OptionEnumValues(g_load_script_from_sym_file_values)">,
     Desc<"Allow LLDB to load scripting resources embedded in symbol files when available.">;
+  def SafeLoadPaths
+      : Property<"safe-load-paths", "String">,
+        DefaultStringValue<"">,
+        Desc<"Paths that LLDB can auto-load scripting resources from.">;
   def LoadCWDlldbinitFile: Property<"load-cwd-lldbinit", "Enum">,
     DefaultEnumValue<"eLoadCWDlldbinitWarn">,
     EnumValues<"OptionEnumValues(g_load_cwd_lldbinit_values)">,
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test
new file mode 100644
index 0000000000000..60b875a643c88
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test
@@ -0,0 +1,31 @@
+# REQUIRES: python, asserts, system-darwin
+#
+# Test that when both a dSYM script and a safe-path script exist,
+# the dSYM script takes priority.
+
+# RUN: split-file %s %t
+# RUN: %clang_host -g %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/TestModule.out.dSYM/Contents/Resources/Python
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/dsym_script.py %t/TestModule.out.dSYM/Contents/Resources/Python/TestModule.py
+# RUN: cp %t/safe_script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s --implicit-check-not=SAFE_PATH_SCRIPT
+
+# CHECK: DSYM_SCRIPT
+
+#--- main.c
+int main() { return 0; }
+
+#--- dsym_script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("DSYM_SCRIPT", file=sys.stderr)
+
+#--- safe_script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SAFE_PATH_SCRIPT", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test
new file mode 100644
index 0000000000000..b00484ecdbe6d
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, system-darwin
+#
+# Test that when a dSYM exists but has no Python script inside,
+# the safe-path script is loaded as a fallback.
+
+# RUN: split-file %s %t
+# RUN: %clang_host -g %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: SAFE_PATH_FALLBACK
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SAFE_PATH_FALLBACK", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test
new file mode 100644
index 0000000000000..0ee57c14130ae
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+
+# Test that LLDB auto-loads <safe-path>/<module-name>/<module-name>.py on
+# module load.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: AUTOLOAD_SUCCESS
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("AUTOLOAD_SUCCESS", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test
new file mode 100644
index 0000000000000..160b771fda024
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test
@@ -0,0 +1,31 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that the last appended safe-auto-load path takes priority.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/first-path/TestModule
+# RUN: mkdir -p %t/second-path/TestModule
+
+# RUN: cp %t/first.py %t/first-path/TestModule/TestModule.py
+# RUN: cp %t/second.py %t/second-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/first-path' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/second-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s --implicit-check-not=FIRST_PATH
+
+# CHECK: SECOND_PATH
+
+#--- main.c
+int main() { return 0; }
+
+#--- first.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("FIRST_PATH", file=sys.stderr)
+
+#--- second.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SECOND_PATH", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test
new file mode 100644
index 0000000000000..aeee2e8cd473d
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that LLDB does not load scripts from nested subdirectories inside the
+# module directory.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule/nested
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/nested/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test
new file mode 100644
index 0000000000000..345613a378a19
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that LLDB does not load scripts when the directory name doesn't match
+# the module name.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/WrongName
+
+# RUN: cp %t/script.py %t/safe-path/WrongName/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test
new file mode 100644
index 0000000000000..f7fb13878d9a8
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that LLDB does not load scripts when the script filename inside the
+# module directory doesn't match <module-name>.py.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/other.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test
new file mode 100644
index 0000000000000..14e713aee5caa
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test
@@ -0,0 +1,22 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that nothing loads when no safe-auto-load-paths are configured.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test
new file mode 100644
index 0000000000000..c2ad4a96c1bd4
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that a module name with special characters (dashes, dots, spaces) is
+# sanitized and the sanitized <module-name>.py is loaded.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o "%t/Test- Module.1.out"
+# RUN: mkdir -p "%t/safe-path/Test- Module.1"
+
+# RUN: cp %t/script.py "%t/safe-path/Test- Module.1/Test__Module_1.py"
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create "%t/Test- Module.1.out"' 2>&1 | FileCheck %s
+
+# CHECK: SANITIZED_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SANITIZED_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test
new file mode 100644
index 0000000000000..a6fa1856aa588
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test
@@ -0,0 +1,30 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that an auto-loaded script can import a sibling Python submodule
+# from the same directory.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: cp %t/helper.py %t/safe-path/TestModule/helper.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: HELPER_LOADED
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+import helper
+def __lldb_init_module(debugger, internal_dict):
+    print(helper.get_marker(), file=sys.stderr)
+
+#--- helper.py
+def get_marker():
+    return "HELPER_LOADED"
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test
new file mode 100644
index 0000000000000..754d6482b4d87
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test
@@ -0,0 +1,30 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that an auto-loaded script can import a sibling Python package
+# (a subdirectory with an __init__.py).
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule/helpers
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: cp %t/init.py %t/safe-path/TestModule/helpers/__init__.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: PACKAGE_LOADED
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+import helpers
+def __lldb_init_module(debugger, internal_dict):
+    print(helpers.get_marker(), file=sys.stderr)
+
+#--- init.py
+def get_marker():
+    return "PACKAGE_LOADED"
diff --git a/lldb/unittests/Platform/PlatformTest.cpp b/lldb/unittests/Platform/PlatformTest.cpp
index 6299197791fc4..be20baf5d1f33 100644
--- a/lldb/unittests/Platform/PlatformTest.cpp
+++ b/lldb/unittests/Platform/PlatformTest.cpp
@@ -8,8 +8,12 @@
 
 #include "gtest/gtest.h"
 
+#include "TestUtils.h"
+
 #include "Plugins/Platform/POSIX/PlatformPOSIX.h"
 #include "TestingSupport/SubsystemRAII.h"
+#include "TestingSupport/TestUtilities.h"
+#include "lldb/Core/Debugger.h"
 #include "lldb/Core/PluginManager.h"
 #include "lldb/Host/FileSystem.h"
 #include "lldb/Host/HostInfo.h"
@@ -163,3 +167,470 @@ TEST_F(PlatformTest, CreateUnknown) {
   ASSERT_EQ(list.Create("unknown-platform-name"), nullptr);
   ASSERT_EQ(list.GetOrCreate("dummy"), nullptr);
 }
+
+struct PlatformLocateSafePathTest : public PlatformTest {
+protected:
+  void SetUp() override {
+    std::call_once(TestUtilities::g_debugger_initialize_flag,
+                   []() { Debugger::Initialize(nullptr); });
+
+    ArchSpec arch("x86_64-apple-macosx-");
+    m_platform_sp = std::make_shared<PlatformArm>();
+    Platform::SetHostPlatform(m_platform_sp);
+
+    m_debugger_sp = Debugger::CreateInstance();
+
+    m_debugger_sp->GetTargetList().CreateTarget(*m_debugger_sp, "", arch,
+                                                lldb_private::eLoadDependentsNo,
+                                                m_platform_sp, m_target_sp);
+
+    ASSERT_TRUE(m_target_sp);
+    ASSERT_TRUE(m_platform_sp);
+
+    ASSERT_FALSE(llvm::sys::fs::createUniqueDirectory(
+        "locate-scripts-from-safe-paths-test", m_tmp_root_dir))
+        << "Failed to create test directory.";
+  };
+
+  void TearDown() override {
+    llvm::sys::fs::remove_directories(m_tmp_root_dir);
+    TestingProperties::GetGlobalTestingProperties().SetSafeAutoLoadPaths({});
+  }
+
+  DebuggerSP m_debugger_sp;
+  PlatformSP m_platform_sp;
+  TargetSP m_target_sp;
+
+  /// Root directory for m_tmp_dsym_dwarf_dir and m_tmp_dsym_python_dir
+  llvm::SmallString<128> m_tmp_root_dir;
+
+  SubsystemRAII<MockScriptInterpreterPython> subsystems;
+};
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_NoSetting) {
+  // Tests LocateScriptingResourcesFromSafePaths finds no script if we don't set
+  // the safe path setting.
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  ASSERT_EQ(file_specs.GetSize(), 0u);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_NoMatch) {
+  // Tests LocateScriptingResourcesFromSafePaths finds no directory to load
+  // from.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  // Directory name doesn't match the module name.
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule1.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  ASSERT_EQ(file_specs.GetSize(), 0u);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Match) {
+  // Tests LocateScriptingResourcesFromSafePaths locates the
+  // <module-name>/<module-name>.py script correctly.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule.py", module_dir);
+  // Other files should be ignored.
+  CreateFile("helper.py", module_dir);
+  CreateFile("not_a_script.txt", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "TestModule.py");
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_NestedDir) {
+  // Tests that a matching Python file nested inside a subdirectory is not
+  // picked up. Only <module-name>/<module-name>.py at the top level matters.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  // Create a nested directory that contains the matching Python file.
+  llvm::SmallString<128> nested_dir(module_dir);
+  llvm::sys::path::append(nested_dir, "nested");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(nested_dir));
+
+  CreateFile("TestModule.py", nested_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_MultiplePaths) {
+  // Tests LocateScriptingResourcesFromSafePaths locates the script from the
+  // last appended auto-load path.
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> path1(m_tmp_root_dir);
+  llvm::sys::path::append(path1, "AnotherSafePath");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path1));
+
+  llvm::SmallString<128> path2(m_tmp_root_dir);
+  llvm::sys::path::append(path2, "AnotherAnotherSafePath");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path2));
+
+  llvm::SmallString<128> path3(m_tmp_root_dir);
+  llvm::sys::path::append(path3, "EmptySafePath");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path3));
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  llvm::SmallString<128> path1_module_dir(path1);
+  llvm::sys::path::append(path1_module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path1_module_dir));
+
+  llvm::SmallString<128> path2_module_dir(path2);
+  llvm::sys::path::append(path2_module_dir, "NotTheTestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path2_module_dir));
+
+  llvm::SmallString<128> path3_module_dir(path3);
+  llvm::sys::path::append(path3_module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path3_module_dir));
+
+  // Place the correctly named script in each module directory.
+  CreateFile("TestModule.py", module_dir);
+  CreateFile("TestModule.py", path1_module_dir);
+  CreateFile("TestModule.py", path2_module_dir);
+  // Keep path3 (EmptySafePath) empty.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(path1));
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(path2));
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  // path1 was the last appended path with a matching directory.
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_TRUE(llvm::StringRef(file_specs.GetFileSpecAtIndex(0).GetPath())
+                  .contains("AnotherSafePath"));
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "TestModule.py");
+
+  // Now add another safe path with a valid module directory but no
+  // TestModule.py inside. LLDB shouldn't fall back to other matching safe
+  // paths.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(path3));
+
+  file_specs = Platform::LocateExecutableScriptingResourcesFromSafePaths(
+      ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+
+  // Now place the correctly named script in path3.
+  CreateFile("TestModule.py", path3_module_dir);
+
+  file_specs = Platform::LocateExecutableScriptingResourcesFromSafePaths(
+      ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_TRUE(llvm::StringRef(file_specs.GetFileSpecAtIndex(0).GetPath())
+                  .contains("EmptySafePath"));
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "TestModule.py");
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_SpecialChars_NoMatch) {
+  // Module name has special characters. The directory exists but only contains
+  // a script with the original (unsanitized) name. No match.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule-1.1 1.o
+  FileSpec module_fspec(CreateFile("TestModule-1.1 1.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule-1.1 1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  // Only the unsanitized name exists.
+  FileSpec orig_fspec(CreateFile("TestModule-1.1 1.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+
+  std::string expected = llvm::formatv(
+      "debug script '{0}' cannot be loaded because"
+      " 'TestModule-1.1 1.py' contains reserved characters. If you intend to"
+      " have this script loaded, please rename it to 'TestModule_1_1_1.py' and "
+      "retry.\n",
+      orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_SpecialChars_Match_Warning) {
+  // Module name has special characters. Both the original and sanitized scripts
+  // exist. LLDB loads the sanitized one and warns.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule-1.1 1.o
+  FileSpec module_fspec(CreateFile("TestModule-1.1 1.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule-1.1 1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  FileSpec orig_fspec(CreateFile("TestModule-1.1 1.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  CreateFile("TestModule_1_1_1.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(),
+            "TestModule_1_1_1.py");
+
+  std::string expected = llvm::formatv(
+      "debug script '{0}' cannot be loaded because"
+      " 'TestModule-1.1 1.py' contains reserved characters. Ignoring"
+      " 'TestModule-1.1 1.py' and loading 'TestModule_1_1_1.py' instead.\n",
+      orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_SpecialChars_Match_NoWarning) {
+  // Module name has special characters. Only the sanitized script exists.
+  // No warning.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule-1.1 1.o
+  FileSpec module_fspec(CreateFile("TestModule-1.1 1.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule-1.1 1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule_1_1_1.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(),
+            "TestModule_1_1_1.py");
+  EXPECT_TRUE(ss.GetString().empty());
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Keyword_NoMatch) {
+  // Module name is a reserved keyword. Only the original script exists.
+  // Warns and returns nothing.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/import.o
+  FileSpec module_fspec(CreateFile("import.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "import");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  FileSpec orig_fspec(CreateFile("import.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+
+  std::string expected = llvm::formatv(
+      "debug script '{0}' cannot be loaded because 'import.py' "
+      "conflicts with the keyword 'import'. If you intend to have this script "
+      "loaded, please rename it to '_import.py' and retry.\n",
+      orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Keyword_Match) {
+  // Module name is a reserved keyword. Both original and sanitized scripts
+  // exist. Loads the sanitized one and warns.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/import.o
+  FileSpec module_fspec(CreateFile("import.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "import");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  FileSpec orig_fspec(CreateFile("import.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  CreateFile("_import.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "_import.py");
+
+  std::string expected =
+      llvm::formatv("debug script '{0}' cannot be loaded because 'import.py' "
+                    "conflicts with the keyword 'import'. Ignoring 'import.py' "
+                    "and loading '_import.py' instead.\n",
+                    orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Keyword_Match_NoWarning) {
+  // Module name is a reserved keyword. Only the sanitized script exists.
+  // No warning.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/import.o
+  FileSpec module_fspec(CreateFile("import.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "import");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("_import.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "_import.py");
+  EXPECT_TRUE(ss.GetString().empty());
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_InnerDirectoryHasModuleName) {
+  // Test a directory structure like
+  // <safe-path>/TestModule/TestModule/TestModule.py. LLDB should not load that
+  // inner script.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> inner_dir(m_tmp_root_dir);
+  llvm::sys::path::append(inner_dir, "TestModule", "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directories(inner_dir));
+
+  CreateFile("TestModule.py", inner_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+  EXPECT_TRUE(ss.GetString().empty());
+}



More information about the llvm-commits mailing list