diff --git a/pj_plugins/include/pj_plugins/host/plugin_catalog.hpp b/pj_plugins/include/pj_plugins/host/plugin_catalog.hpp index 098a873..cd9fbbc 100644 --- a/pj_plugins/include/pj_plugins/host/plugin_catalog.hpp +++ b/pj_plugins/include/pj_plugins/host/plugin_catalog.hpp @@ -98,8 +98,9 @@ struct PluginScanResult { namespace detail { /// Return the plugin families whose getter symbols are defined by @p dso_path -/// itself. Missing getters and getters supplied only by dependencies are omitted. -[[nodiscard]] std::vector exportedPluginFamilies( +/// itself. Missing getters are omitted; provenance and path-identity failures +/// are returned to the caller instead of being mistaken for absent families. +[[nodiscard]] Expected> exportedPluginFamilies( const std::shared_ptr& handle, const std::filesystem::path& dso_path); } // namespace detail diff --git a/pj_plugins/src/plugin_catalog.cpp b/pj_plugins/src/plugin_catalog.cpp index 6beb978..dbde92e 100644 --- a/pj_plugins/src/plugin_catalog.cpp +++ b/pj_plugins/src/plugin_catalog.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include "detail/library_loader.hpp" #include "detail/vtable_validation.hpp" @@ -43,6 +45,16 @@ bool hasDsoSuffix(const std::filesystem::path& path) { return path.extension().string() == kDsoSuffix; } +bool nativeSymbolPresent(void* handle, const char* symbol) noexcept { +#if defined(_WIN32) + return GetProcAddress(reinterpret_cast(handle), symbol) != nullptr; +#else + dlerror(); + void* resolved = dlsym(handle, symbol); + return dlerror() == nullptr && resolved != nullptr; +#endif +} + // Direct-vtable families share the exact same probe sequence: resolve symbol, // call entry, check protocol_version, check struct_size, read manifest_json. // Only the family-specific types and constants vary. @@ -348,26 +360,38 @@ Expected inspectPluginDso( namespace detail { -std::vector exportedPluginFamilies( +Expected> exportedPluginFamilies( const std::shared_ptr& handle, const std::filesystem::path& dso_path) { std::vector families; if (handle == nullptr) { - return families; + return unexpected("library not loaded"); } auto recorded_path = recordLibraryPathIdentity(dso_path); if (!recorded_path) { - return families; + return unexpected(recorded_path.error()); } - auto append_if_owned = [&](const char* symbol, PluginFamily family) { - if (resolveSymbol(handle.get(), symbol, *recorded_path)) { - families.push_back(family); + auto append_if_owned = [&](const char* symbol, PluginFamily family) -> Expected { + if (!nativeSymbolPresent(handle.get(), symbol)) { + return {}; + } + auto resolved = resolveSymbol(handle.get(), symbol, *recorded_path); + if (!resolved) { + return unexpected(resolved.error()); } + families.push_back(family); + return {}; }; - append_if_owned("PJ_get_data_source_vtable", PluginFamily::kDataSource); - append_if_owned("PJ_get_message_parser_vtable", PluginFamily::kMessageParser); - append_if_owned("PJ_get_toolbox_vtable", PluginFamily::kToolbox); - append_if_owned("PJ_get_dialog_vtable", PluginFamily::kDialog); + for (const auto& [symbol, family] : std::array, 4>{ + std::pair{"PJ_get_data_source_vtable", PluginFamily::kDataSource}, + {"PJ_get_message_parser_vtable", PluginFamily::kMessageParser}, + {"PJ_get_toolbox_vtable", PluginFamily::kToolbox}, + {"PJ_get_dialog_vtable", PluginFamily::kDialog}, + }) { + if (auto status = append_if_owned(symbol, family); !status) { + return unexpected(status.error()); + } + } return families; } diff --git a/pj_plugins/tests/plugin_catalog_test.cpp b/pj_plugins/tests/plugin_catalog_test.cpp index a9e8091..89d4fb2 100644 --- a/pj_plugins/tests/plugin_catalog_test.cpp +++ b/pj_plugins/tests/plugin_catalog_test.cpp @@ -148,6 +148,20 @@ TEST_F(PluginCatalogTest, EntryPointSymbolResolvedFromDependencyIsRejected) { auto dependency_descriptor = inspectPluginDso(PJ_ENTRY_POINT_VIA_DEPENDENCY_PLUGIN_PATH); ASSERT_FALSE(dependency_descriptor.has_value()); EXPECT_TRUE(is_provenance_error(dependency_descriptor.error())) << dependency_descriptor.error(); + + auto raw_handle = detail::loadLibraryHandle(PJ_ENTRY_POINT_VIA_DEPENDENCY_PLUGIN_PATH); + ASSERT_TRUE(raw_handle.has_value()) << raw_handle.error(); + auto family_query = detail::exportedPluginFamilies( + detail::adoptLibraryHandle(*raw_handle), PJ_ENTRY_POINT_VIA_DEPENDENCY_PLUGIN_PATH); +#if defined(__APPLE__) + // RTLD_FIRST hides dependency symbols from dlsym outright, so the getter is + // cleanly absent on macOS rather than a provenance violation. + ASSERT_TRUE(family_query.has_value()) << family_query.error(); + EXPECT_TRUE(family_query->empty()); +#else + ASSERT_FALSE(family_query.has_value()); + EXPECT_TRUE(is_provenance_error(family_query.error())) << family_query.error(); +#endif #endif auto library = DataSourceLibrary::load(PJ_ENTRY_POINT_WITH_OWN_EXPORTS_PLUGIN_PATH); @@ -193,7 +207,8 @@ TEST_F(PluginCatalogTest, AlreadyOpenHandleSupportsInspectionLoadingAndFamilyQue auto shared_handle = detail::adoptLibraryHandleNonOwning(owner.get()); const auto families = detail::exportedPluginFamilies(shared_handle, plugin_path); - EXPECT_EQ(families, std::vector{PluginFamily::kDataSource}); + ASSERT_TRUE(families.has_value()) << families.error(); + EXPECT_EQ(*families, std::vector{PluginFamily::kDataSource}); auto descriptor = inspectPluginDso(shared_handle, plugin_path); ASSERT_TRUE(descriptor.has_value()) << descriptor.error();