From 42ffe44e5c21b22ca1afc813b5f64166fb3b70a1 Mon Sep 17 00:00:00 2001 From: Alex Fabijanic Date: Fri, 5 Dec 2025 13:50:53 -0600 Subject: [PATCH] fix(SharedLibrary): Missing DLLs not reported #5069 --- .gitignore | 30 ++ Foundation/include/Poco/NObserver.h | 4 +- Foundation/include/Poco/SharedLibrary.h | 15 + Foundation/include/Poco/SharedLibrary_HPUX.h | 3 + Foundation/include/Poco/SharedLibrary_UNIX.h | 3 + Foundation/include/Poco/SharedLibrary_VX.h | 3 + .../include/Poco/SharedLibrary_WIN32U.h | 3 + Foundation/src/SharedLibrary.cpp | 6 + Foundation/src/SharedLibrary_HPUX.cpp | 7 + Foundation/src/SharedLibrary_UNIX.cpp | 381 +++++++++++++++++- Foundation/src/SharedLibrary_VX.cpp | 7 + Foundation/src/SharedLibrary_WIN32U.cpp | 176 ++++++++ Foundation/testsuite/CMakeLists.txt | 48 ++- Foundation/testsuite/Makefile | 2 + .../testsuite/Makefile-NonExistentLibrary | 15 + .../testsuite/Makefile-TestLibraryMissingDeps | 15 + Foundation/testsuite/src/ClassLoaderTest.cpp | 40 +- Foundation/testsuite/src/NonExistentStub.c | 15 + .../testsuite/src/SharedLibraryTest.cpp | 161 +++++++- Foundation/testsuite/src/SharedLibraryTest.h | 2 + .../testsuite/src/TestLibraryMissingDeps.cpp | 26 ++ 21 files changed, 920 insertions(+), 42 deletions(-) mode change 100644 => 100755 Foundation/include/Poco/SharedLibrary.h mode change 100644 => 100755 Foundation/src/SharedLibrary_WIN32U.cpp create mode 100644 Foundation/testsuite/Makefile-NonExistentLibrary create mode 100644 Foundation/testsuite/Makefile-TestLibraryMissingDeps create mode 100644 Foundation/testsuite/src/NonExistentStub.c mode change 100644 => 100755 Foundation/testsuite/src/SharedLibraryTest.cpp create mode 100644 Foundation/testsuite/src/TestLibraryMissingDeps.cpp diff --git a/.gitignore b/.gitignore index 241afe6d9..be3ed9062 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,33 @@ node_modules # Debug files # ############## *.core + +# Build-time symlinks to bundled libraries # +############################################ +Foundation/src/adler32.c +Foundation/src/compress.c +Foundation/src/crc32.c +Foundation/src/crc32.h +Foundation/src/deflate.c +Foundation/src/deflate.h +Foundation/src/gzguts.h +Foundation/src/infback.c +Foundation/src/inffast.c +Foundation/src/inffast.h +Foundation/src/inffixed.h +Foundation/src/inflate.c +Foundation/src/inflate.h +Foundation/src/inftrees.c +Foundation/src/inftrees.h +Foundation/src/pcre2.h +Foundation/src/pcre2_*.c +Foundation/src/pcre2_*.h +Foundation/src/trees.c +Foundation/src/trees.h +Foundation/src/utf8proc.c +Foundation/src/utf8proc.h +Foundation/src/utf8proc_data.c +Foundation/src/zconf.h +Foundation/src/zlib.h +Foundation/src/zutil.c +Foundation/src/zutil.h diff --git a/Foundation/include/Poco/NObserver.h b/Foundation/include/Poco/NObserver.h index 6f3c84b95..2016817e0 100644 --- a/Foundation/include/Poco/NObserver.h +++ b/Foundation/include/Poco/NObserver.h @@ -64,8 +64,8 @@ public: NObserver(C& object, Handler method, Matcher matcher = nullptr, SyncHandler syncMethod = nullptr): _pObject(&object), _handler(method), - _matcher(matcher), - _syncHandler(syncMethod) + _syncHandler(syncMethod), + _matcher(matcher) { } diff --git a/Foundation/include/Poco/SharedLibrary.h b/Foundation/include/Poco/SharedLibrary.h old mode 100644 new mode 100755 index 008da32fd..07c58e151 --- a/Foundation/include/Poco/SharedLibrary.h +++ b/Foundation/include/Poco/SharedLibrary.h @@ -19,6 +19,8 @@ #include "Poco/Foundation.h" +#include +#include #if defined(hpux) || defined(_hpux) @@ -130,6 +132,19 @@ public: /// SetDllDirectory(). On all other platforms, does not /// do anything and returns false. + static std::vector findMissingDependencies(const std::string& path); + /// Parses the shared library at the given path and returns + /// a list of dependent libraries that cannot be found. + /// If the library itself whose dependencies are checked is not found, + /// it returns the library itself. + /// + /// Useful for diagnosing why a library fails to load. + /// On Windows, parses the PE import table. On Linux, parses the + /// ELF dynamic section. On macOS, parses the Mach-O load commands. + /// + /// Returns an empty vector if the file cannot be parsed or + /// on unsupported platforms. + private: SharedLibrary(const SharedLibrary&); SharedLibrary& operator = (const SharedLibrary&); diff --git a/Foundation/include/Poco/SharedLibrary_HPUX.h b/Foundation/include/Poco/SharedLibrary_HPUX.h index 06a6f51bc..934caa502 100644 --- a/Foundation/include/Poco/SharedLibrary_HPUX.h +++ b/Foundation/include/Poco/SharedLibrary_HPUX.h @@ -21,6 +21,8 @@ #include "Poco/Foundation.h" #include "Poco/Mutex.h" #include +#include +#include namespace Poco { @@ -38,6 +40,7 @@ protected: const std::string& getPathImpl() const; static std::string suffixImpl(); static bool setSearchPathImpl(const std::string& path); + static std::vector findMissingDependenciesImpl(const std::string& path); private: std::string _path; diff --git a/Foundation/include/Poco/SharedLibrary_UNIX.h b/Foundation/include/Poco/SharedLibrary_UNIX.h index 1f0c288ad..64117b071 100644 --- a/Foundation/include/Poco/SharedLibrary_UNIX.h +++ b/Foundation/include/Poco/SharedLibrary_UNIX.h @@ -20,6 +20,8 @@ #include "Poco/Foundation.h" #include "Poco/Mutex.h" +#include +#include namespace Poco { @@ -43,6 +45,7 @@ protected: const std::string& getPathImpl() const; static std::string suffixImpl(); static bool setSearchPathImpl(const std::string& path); + static std::vector findMissingDependenciesImpl(const std::string& path); private: std::string _path; diff --git a/Foundation/include/Poco/SharedLibrary_VX.h b/Foundation/include/Poco/SharedLibrary_VX.h index 916edad47..68c719c3b 100644 --- a/Foundation/include/Poco/SharedLibrary_VX.h +++ b/Foundation/include/Poco/SharedLibrary_VX.h @@ -21,6 +21,8 @@ #include "Poco/Foundation.h" #include "Poco/Mutex.h" #include +#include +#include namespace Poco { @@ -38,6 +40,7 @@ protected: const std::string& getPathImpl() const; static std::string suffixImpl(); static bool setSearchPathImpl(const std::string& path); + static std::vector findMissingDependenciesImpl(const std::string& path); private: std::string _path; diff --git a/Foundation/include/Poco/SharedLibrary_WIN32U.h b/Foundation/include/Poco/SharedLibrary_WIN32U.h index 3e51c9c95..fabb061a6 100644 --- a/Foundation/include/Poco/SharedLibrary_WIN32U.h +++ b/Foundation/include/Poco/SharedLibrary_WIN32U.h @@ -20,6 +20,8 @@ #include "Poco/Foundation.h" #include "Poco/Mutex.h" +#include +#include namespace Poco { @@ -37,6 +39,7 @@ protected: const std::string& getPathImpl() const; static std::string suffixImpl(); static bool setSearchPathImpl(const std::string& path); + static std::vector findMissingDependenciesImpl(const std::string& path); private: std::string _path; diff --git a/Foundation/src/SharedLibrary.cpp b/Foundation/src/SharedLibrary.cpp index 64b9e28e5..ad075e2de 100644 --- a/Foundation/src/SharedLibrary.cpp +++ b/Foundation/src/SharedLibrary.cpp @@ -110,4 +110,10 @@ bool SharedLibrary::setSearchPath(const std::string& path) } +std::vector SharedLibrary::findMissingDependencies(const std::string& path) +{ + return findMissingDependenciesImpl(path); +} + + } // namespace Poco diff --git a/Foundation/src/SharedLibrary_HPUX.cpp b/Foundation/src/SharedLibrary_HPUX.cpp index 610ae718b..11df31274 100644 --- a/Foundation/src/SharedLibrary_HPUX.cpp +++ b/Foundation/src/SharedLibrary_HPUX.cpp @@ -98,4 +98,11 @@ bool SharedLibraryImpl::setSearchPathImpl(const std::string&) } +std::vector SharedLibraryImpl::findMissingDependenciesImpl(const std::string&) +{ + // not implemented + return std::vector(); +} + + } // namespace Poco diff --git a/Foundation/src/SharedLibrary_UNIX.cpp b/Foundation/src/SharedLibrary_UNIX.cpp index 869f3c47b..248c40b0e 100644 --- a/Foundation/src/SharedLibrary_UNIX.cpp +++ b/Foundation/src/SharedLibrary_UNIX.cpp @@ -15,6 +15,17 @@ #include "Poco/SharedLibrary_UNIX.h" #include "Poco/Exception.h" #include +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#include +#elif defined(__linux__) +#include +#endif // Note: cygwin is missing RTLD_LOCAL, set it to 0 @@ -54,7 +65,21 @@ void SharedLibraryImpl::loadImpl(const std::string& path, int flags) if (!_handle) { const char* err = dlerror(); - throw LibraryLoadException(err ? std::string(err) : path); + std::string errMsg = err ? std::string(err) : path; + + // Try to identify missing dependencies + std::vector missingDeps = findMissingDependenciesImpl(path); + if (!missingDeps.empty()) + { + errMsg += "; missing dependencies: "; + for (size_t i = 0; i < missingDeps.size(); ++i) + { + if (i > 0) errMsg += ", "; + errMsg += missingDeps[i]; + } + } + + throw LibraryLoadException(errMsg); } _path = path; } @@ -134,4 +159,358 @@ bool SharedLibraryImpl::setSearchPathImpl(const std::string&) } +#if defined(__APPLE__) + + +std::vector SharedLibraryImpl::findMissingDependenciesImpl(const std::string& path) +{ + std::vector missingDeps; + + std::ifstream file(path, std::ios::binary); + if (!file) + { + missingDeps.push_back(path); + return missingDeps; + } + + try + { + uint32_t magic; + file.read(reinterpret_cast(&magic), sizeof(magic)); + if (!file) + return missingDeps; + + bool is64 = false; + bool needsSwap = false; + uint32_t ncmds = 0; + uint32_t sizeofcmds = 0; + + if (magic == FAT_MAGIC || magic == FAT_CIGAM) + { + // Fat binary - skip to first architecture + bool fatSwap = (magic == FAT_CIGAM); + fat_header fatHeader; + file.seekg(0); + file.read(reinterpret_cast(&fatHeader), sizeof(fatHeader)); + if (!file) + return missingDeps; + + uint32_t narch = fatSwap ? __builtin_bswap32(fatHeader.nfat_arch) : fatHeader.nfat_arch; + if (narch == 0) + return missingDeps; + + fat_arch arch; + file.read(reinterpret_cast(&arch), sizeof(arch)); + if (!file) + return missingDeps; + + uint32_t offset = fatSwap ? __builtin_bswap32(arch.offset) : arch.offset; + file.seekg(offset); + file.read(reinterpret_cast(&magic), sizeof(magic)); + if (!file) + return missingDeps; + } + + if (magic == MH_MAGIC_64 || magic == MH_CIGAM_64) + { + is64 = true; + needsSwap = (magic == MH_CIGAM_64); + file.seekg(-static_cast(sizeof(magic)), std::ios::cur); + mach_header_64 header; + file.read(reinterpret_cast(&header), sizeof(header)); + if (!file) + return missingDeps; + ncmds = needsSwap ? __builtin_bswap32(header.ncmds) : header.ncmds; + sizeofcmds = needsSwap ? __builtin_bswap32(header.sizeofcmds) : header.sizeofcmds; + } + else if (magic == MH_MAGIC || magic == MH_CIGAM) + { + is64 = false; + needsSwap = (magic == MH_CIGAM); + file.seekg(-static_cast(sizeof(magic)), std::ios::cur); + mach_header header; + file.read(reinterpret_cast(&header), sizeof(header)); + if (!file) + return missingDeps; + ncmds = needsSwap ? __builtin_bswap32(header.ncmds) : header.ncmds; + sizeofcmds = needsSwap ? __builtin_bswap32(header.sizeofcmds) : header.sizeofcmds; + } + else + { + return missingDeps; + } + + // Read all load commands + std::vector cmdsBuffer(sizeofcmds); + file.read(cmdsBuffer.data(), sizeofcmds); + if (!file) + return missingDeps; + + const char* ptr = cmdsBuffer.data(); + const char* end = ptr + sizeofcmds; + + for (uint32_t i = 0; i < ncmds && ptr < end; ++i) + { + const load_command* cmd = reinterpret_cast(ptr); + uint32_t cmdType = needsSwap ? __builtin_bswap32(cmd->cmd) : cmd->cmd; + uint32_t cmdSize = needsSwap ? __builtin_bswap32(cmd->cmdsize) : cmd->cmdsize; + + if (cmdSize < sizeof(load_command) || ptr + cmdSize > end) + break; + + if (cmdType == LC_LOAD_DYLIB || cmdType == LC_LOAD_WEAK_DYLIB || cmdType == LC_REEXPORT_DYLIB) + { + const dylib_command* dylibCmd = reinterpret_cast(ptr); + uint32_t nameOffset = needsSwap ? __builtin_bswap32(dylibCmd->dylib.name.offset) : dylibCmd->dylib.name.offset; + + if (nameOffset < cmdSize) + { + const char* libPath = ptr + nameOffset; + std::string libName(libPath); + + // Check if library can be loaded + void* handle = dlopen(libName.c_str(), RTLD_LAZY | RTLD_NOLOAD); + if (!handle) + { + handle = dlopen(libName.c_str(), RTLD_LAZY); + if (!handle) + { + // Extract just the library name for cleaner output + size_t lastSlash = libName.rfind('/'); + if (lastSlash != std::string::npos) + missingDeps.push_back(libName.substr(lastSlash + 1)); + else + missingDeps.push_back(libName); + } + else + { + dlclose(handle); + } + } + } + } + + ptr += cmdSize; + } + } + catch (...) + { + // If we get an exception while parsing, just return what we have + } + + return missingDeps; +} + + +#elif defined(__linux__) + + +std::vector SharedLibraryImpl::findMissingDependenciesImpl(const std::string& path) +{ + std::vector missingDeps; + + std::ifstream file(path, std::ios::binary); + if (!file) + { + missingDeps.push_back(path); + return missingDeps; + } + + try + { + unsigned char ident[EI_NIDENT]; + file.read(reinterpret_cast(ident), EI_NIDENT); + if (!file) + return missingDeps; + + if (ident[EI_MAG0] != ELFMAG0 || ident[EI_MAG1] != ELFMAG1 || + ident[EI_MAG2] != ELFMAG2 || ident[EI_MAG3] != ELFMAG3) + return missingDeps; + + bool is64 = (ident[EI_CLASS] == ELFCLASS64); + file.seekg(0); + + std::vector neededLibs; + + if (is64) + { + Elf64_Ehdr ehdr; + file.read(reinterpret_cast(&ehdr), sizeof(ehdr)); + if (!file) + return missingDeps; + + // Find the dynamic section and string table + Elf64_Off shoff = ehdr.e_shoff; + uint16_t shnum = ehdr.e_shnum; + uint16_t shentsize = ehdr.e_shentsize; + + if (shoff == 0 || shnum == 0) + return missingDeps; + + // Read section headers + std::vector sections(shnum); + file.seekg(shoff); + for (uint16_t i = 0; i < shnum; ++i) + { + file.read(reinterpret_cast(§ions[i]), sizeof(Elf64_Shdr)); + if (!file) + return missingDeps; + if (shentsize > sizeof(Elf64_Shdr)) + file.seekg(shentsize - sizeof(Elf64_Shdr), std::ios::cur); + } + + // Find .dynamic and .dynstr sections + Elf64_Shdr* dynSection = nullptr; + Elf64_Shdr* dynstrSection = nullptr; + + for (auto& sec : sections) + { + if (sec.sh_type == SHT_DYNAMIC) + dynSection = &sec; + else if (sec.sh_type == SHT_STRTAB && dynstrSection == nullptr) + dynstrSection = &sec; + } + + // Use the string table linked from .dynamic section + if (dynSection && dynSection->sh_link < shnum) + dynstrSection = §ions[dynSection->sh_link]; + + if (!dynSection || !dynstrSection) + return missingDeps; + + // Read string table + std::vector strtab(dynstrSection->sh_size); + file.seekg(dynstrSection->sh_offset); + file.read(strtab.data(), dynstrSection->sh_size); + if (!file) + return missingDeps; + + // Read dynamic entries + file.seekg(dynSection->sh_offset); + size_t numEntries = dynSection->sh_size / sizeof(Elf64_Dyn); + for (size_t i = 0; i < numEntries; ++i) + { + Elf64_Dyn dyn; + file.read(reinterpret_cast(&dyn), sizeof(dyn)); + if (!file) + break; + + if (dyn.d_tag == DT_NULL) + break; + + if (dyn.d_tag == DT_NEEDED) + { + if (dyn.d_un.d_val < strtab.size()) + neededLibs.push_back(&strtab[dyn.d_un.d_val]); + } + } + } + else + { + Elf32_Ehdr ehdr; + file.read(reinterpret_cast(&ehdr), sizeof(ehdr)); + if (!file) + return missingDeps; + + Elf32_Off shoff = ehdr.e_shoff; + uint16_t shnum = ehdr.e_shnum; + uint16_t shentsize = ehdr.e_shentsize; + + if (shoff == 0 || shnum == 0) + return missingDeps; + + std::vector sections(shnum); + file.seekg(shoff); + for (uint16_t i = 0; i < shnum; ++i) + { + file.read(reinterpret_cast(§ions[i]), sizeof(Elf32_Shdr)); + if (!file) + return missingDeps; + if (shentsize > sizeof(Elf32_Shdr)) + file.seekg(shentsize - sizeof(Elf32_Shdr), std::ios::cur); + } + + Elf32_Shdr* dynSection = nullptr; + Elf32_Shdr* dynstrSection = nullptr; + + for (auto& sec : sections) + { + if (sec.sh_type == SHT_DYNAMIC) + dynSection = &sec; + else if (sec.sh_type == SHT_STRTAB && dynstrSection == nullptr) + dynstrSection = &sec; + } + + if (dynSection && dynSection->sh_link < shnum) + dynstrSection = §ions[dynSection->sh_link]; + + if (!dynSection || !dynstrSection) + return missingDeps; + + std::vector strtab(dynstrSection->sh_size); + file.seekg(dynstrSection->sh_offset); + file.read(strtab.data(), dynstrSection->sh_size); + if (!file) + return missingDeps; + + file.seekg(dynSection->sh_offset); + size_t numEntries = dynSection->sh_size / sizeof(Elf32_Dyn); + for (size_t i = 0; i < numEntries; ++i) + { + Elf32_Dyn dyn; + file.read(reinterpret_cast(&dyn), sizeof(dyn)); + if (!file) + break; + + if (dyn.d_tag == DT_NULL) + break; + + if (dyn.d_tag == DT_NEEDED) + { + if (dyn.d_un.d_val < strtab.size()) + neededLibs.push_back(&strtab[dyn.d_un.d_val]); + } + } + } + + // Check which libraries are missing + for (const auto& lib : neededLibs) + { + void* handle = dlopen(lib.c_str(), RTLD_LAZY | RTLD_NOLOAD); + if (!handle) + { + handle = dlopen(lib.c_str(), RTLD_LAZY); + if (!handle) + { + missingDeps.push_back(lib); + } + else + { + dlclose(handle); + } + } + } + } + catch (...) + { + // If we get an exception while parsing, just return what we have + } + + return missingDeps; +} + + +#else + + +std::vector SharedLibraryImpl::findMissingDependenciesImpl(const std::string&) +{ + // Unsupported platform - return empty list + return std::vector(); +} + + +#endif + + } // namespace Poco diff --git a/Foundation/src/SharedLibrary_VX.cpp b/Foundation/src/SharedLibrary_VX.cpp index d30c35e9d..945c471ed 100644 --- a/Foundation/src/SharedLibrary_VX.cpp +++ b/Foundation/src/SharedLibrary_VX.cpp @@ -139,4 +139,11 @@ bool SharedLibraryImpl::setSearchPathImpl(const std::string&) } +std::vector SharedLibraryImpl::findMissingDependenciesImpl(const std::string&) +{ + // not implemented + return std::vector(); +} + + } // namespace Poco diff --git a/Foundation/src/SharedLibrary_WIN32U.cpp b/Foundation/src/SharedLibrary_WIN32U.cpp old mode 100644 new mode 100755 index 4432fbc8f..3155407d9 --- a/Foundation/src/SharedLibrary_WIN32U.cpp +++ b/Foundation/src/SharedLibrary_WIN32U.cpp @@ -19,6 +19,8 @@ #include "Poco/Error.h" #include "Poco/Format.h" #include "Poco/String.h" +#include +#include namespace Poco { @@ -56,6 +58,19 @@ void SharedLibraryImpl::loadImpl(const std::string& path, int /*flags*/) DWORD errn = Error::last(); std::string err; Poco::format(err, "Error %lu while loading [%s]: [%s]", errn, path, Poco::trim(Error::getMessage(errn))); + + // Try to identify missing dependencies + std::vector missingDeps = findMissingDependenciesImpl(path); + if (!missingDeps.empty()) + { + err += "; missing dependencies: "; + for (size_t i = 0; i < missingDeps.size(); ++i) + { + if (i > 0) err += ", "; + err += missingDeps[i]; + } + } + throw LibraryLoadException(err); } _path = path; @@ -122,4 +137,165 @@ bool SharedLibraryImpl::setSearchPathImpl(const std::string& path) } +std::vector SharedLibraryImpl::findMissingDependenciesImpl(const std::string& path) +{ + std::vector missingDeps; + + std::wstring wpath; + UnicodeConverter::toUTF16(path, wpath); + + HANDLE hFile = CreateFileW(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) + { + missingDeps.push_back(path); + return missingDeps; + } + + HANDLE hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY, 0, 0, NULL); + if (!hMapping) + { + CloseHandle(hFile); + return missingDeps; + } + + LPVOID pBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); + if (!pBase) + { + CloseHandle(hMapping); + CloseHandle(hFile); + return missingDeps; + } + + auto parseImports = [&]() + { + PIMAGE_DOS_HEADER pDosHeader = static_cast(pBase); + if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) + return; + + PIMAGE_NT_HEADERS pNtHeaders = reinterpret_cast( + static_cast(pBase) + pDosHeader->e_lfanew); + if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) + return; + + DWORD importDirRVA = 0; + DWORD importDirSize = 0; + + if (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) + { + PIMAGE_NT_HEADERS32 pNtHeaders32 = reinterpret_cast(pNtHeaders); + if (pNtHeaders32->OptionalHeader.NumberOfRvaAndSizes > IMAGE_DIRECTORY_ENTRY_IMPORT) + { + importDirRVA = pNtHeaders32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; + importDirSize = pNtHeaders32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size; + } + } + else if (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) + { + PIMAGE_NT_HEADERS64 pNtHeaders64 = reinterpret_cast(pNtHeaders); + if (pNtHeaders64->OptionalHeader.NumberOfRvaAndSizes > IMAGE_DIRECTORY_ENTRY_IMPORT) + { + importDirRVA = pNtHeaders64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; + importDirSize = pNtHeaders64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size; + } + } + + if (importDirRVA == 0 || importDirSize == 0) + return; + + // Convert RVA to file offset by finding the section containing the import directory + PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeaders); + DWORD importDirOffset = 0; + for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; ++i) + { + if (importDirRVA >= pSectionHeader[i].VirtualAddress && + importDirRVA < pSectionHeader[i].VirtualAddress + pSectionHeader[i].Misc.VirtualSize) + { + importDirOffset = importDirRVA - pSectionHeader[i].VirtualAddress + pSectionHeader[i].PointerToRawData; + break; + } + } + + if (importDirOffset == 0) + return; + + PIMAGE_IMPORT_DESCRIPTOR pImportDesc = reinterpret_cast( + static_cast(pBase) + importDirOffset); + + // Get the directory containing the DLL for search path + Path dllPath(path); + std::string dllDir = dllPath.parent().toString(); + std::wstring wdllDir; + UnicodeConverter::toUTF16(dllDir, wdllDir); + + while (pImportDesc->Name != 0) + { + // Convert the Name RVA to file offset + DWORD nameRVA = pImportDesc->Name; + DWORD nameOffset = 0; + for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; ++i) + { + if (nameRVA >= pSectionHeader[i].VirtualAddress && + nameRVA < pSectionHeader[i].VirtualAddress + pSectionHeader[i].Misc.VirtualSize) + { + nameOffset = nameRVA - pSectionHeader[i].VirtualAddress + pSectionHeader[i].PointerToRawData; + break; + } + } + + if (nameOffset != 0) + { + const char* dllName = reinterpret_cast(static_cast(pBase) + nameOffset); + + // Skip API set DLLs - these are virtual DLLs resolved by Windows at runtime + // They start with "api-" or "ext-" (per Microsoft documentation) + if (_strnicmp(dllName, "api-", 4) == 0 || _strnicmp(dllName, "ext-", 4) == 0) + { + ++pImportDesc; + continue; + } + + // Check if the DLL can be found + std::wstring wDllName; + UnicodeConverter::toUTF16(std::string(dllName), wDllName); + + // Try to find the DLL using the standard search order + HMODULE hMod = LoadLibraryExW(wDllName.c_str(), NULL, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); + if (!hMod) + { + // Try in the same directory as the original DLL + std::wstring fullPath = wdllDir + wDllName; + hMod = LoadLibraryExW(fullPath.c_str(), NULL, LOAD_LIBRARY_AS_DATAFILE); + } + + if (!hMod) + { + missingDeps.push_back(dllName); + } + else + { + FreeLibrary(hMod); + } + } + + ++pImportDesc; + } + }; + + try + { + parseImports(); + } + catch (...) + { + // If we get an exception while parsing, just return what we have + } + + UnmapViewOfFile(pBase); + CloseHandle(hMapping); + CloseHandle(hFile); + + return missingDeps; +} + + } // namespace Poco diff --git a/Foundation/testsuite/CMakeLists.txt b/Foundation/testsuite/CMakeLists.txt index a78d817b2..9f40be23b 100644 --- a/Foundation/testsuite/CMakeLists.txt +++ b/Foundation/testsuite/CMakeLists.txt @@ -5,6 +5,7 @@ file(GLOB SRCS_G "src/*.cpp") file(GLOB SRCS_G_REMOVE src/TestApp.cpp src/TestLibrary.cpp + src/TestLibraryMissingDeps.cpp src/TestPlugin.cpp ) list(REMOVE_ITEM SRCS_G ${SRCS_G_REMOVE}) @@ -20,6 +21,7 @@ POCO_SOURCES_AUTO_PLAT(TEST_SRCS OFF ) add_executable(Foundation-testrunner ${TEST_SRCS}) +set_target_properties(Foundation-testrunner PROPERTIES DEBUG_POSTFIX "d") if(ANDROID) add_test( NAME Foundation @@ -54,20 +56,52 @@ endif() # TestApp add_executable(TestApp src/TestApp.cpp) # The test is run in the runtime directory. So the TestApp is built there too because it is used by the tests -set_target_properties(TestApp PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) +set_target_properties(TestApp PROPERTIES + DEBUG_POSTFIX "d" + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} +) target_link_libraries(TestApp PUBLIC Poco::Foundation) # TestLibrary add_library(TestLibrary SHARED src/TestLibrary.cpp src/TestPlugin.cpp src/TestPlugin.h) -set_target_properties(TestLibrary PROPERTIES DEBUG_POSTFIX "d") -set_target_properties(TestLibrary PROPERTIES RELEASE_POSTFIX "") -set_target_properties(TestLibrary PROPERTIES CMAKE_MINSIZEREL_POSTFIX "") -set_target_properties(TestLibrary PROPERTIES CMAKE_RELWITHDEBINFO_POSTFIX "") -# The test requires the library named TestLibrary. By default it is prefixed with lib. -set_target_properties(TestLibrary PROPERTIES PREFIX "") +set_target_properties(TestLibrary PROPERTIES + DEBUG_POSTFIX "d" + # The test requires the library named TestLibrary. By default it is prefixed with lib. + PREFIX "" +) # The test is run in the runtime directory. So the TestLibrary is built there too because it is used by the tests set_target_properties(TestLibrary PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) target_link_libraries(TestLibrary PUBLIC Poco::Foundation) add_dependencies(Foundation-testrunner TestApp TestLibrary) + +# TestLibraryMissingDeps - a library with missing dependencies for testing findMissingDependencies +# Build a stub library that TestLibraryMissingDeps links against +# The test will delete it at runtime to simulate a missing dependency +add_library(NonExistentLibrary SHARED src/NonExistentStub.c) +set_target_properties(NonExistentLibrary PROPERTIES + DEBUG_POSTFIX "d" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY} +) +# For multi-config generators (Visual Studio), set per-configuration archive output +foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG_UPPER) + set_target_properties(NonExistentLibrary PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG_UPPER} ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG_UPPER}} + ) +endforeach() + +add_library(TestLibraryMissingDeps SHARED src/TestLibraryMissingDeps.cpp) +set_target_properties(TestLibraryMissingDeps PROPERTIES + DEBUG_POSTFIX "d" + PREFIX "" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} +) +target_link_libraries(TestLibraryMissingDeps PUBLIC Poco::Foundation NonExistentLibrary) +add_dependencies(TestLibraryMissingDeps NonExistentLibrary) + +add_dependencies(Foundation-testrunner TestLibraryMissingDeps) diff --git a/Foundation/testsuite/Makefile b/Foundation/testsuite/Makefile index 27fbafb80..3bd85e003 100644 --- a/Foundation/testsuite/Makefile +++ b/Foundation/testsuite/Makefile @@ -10,5 +10,7 @@ projects: $(MAKE) -f Makefile-Driver $(MAKECMDGOALS) ifneq ($(LINKMODE),STATIC) $(MAKE) -f Makefile-TestLibrary $(MAKECMDGOALS) + $(MAKE) -f Makefile-NonExistentLibrary $(MAKECMDGOALS) + $(MAKE) -f Makefile-TestLibraryMissingDeps $(MAKECMDGOALS) endif $(MAKE) -f Makefile-TestApp $(MAKECMDGOALS) diff --git a/Foundation/testsuite/Makefile-NonExistentLibrary b/Foundation/testsuite/Makefile-NonExistentLibrary new file mode 100644 index 000000000..82fb7945a --- /dev/null +++ b/Foundation/testsuite/Makefile-NonExistentLibrary @@ -0,0 +1,15 @@ +# +# Makefile +# +# Makefile for NonExistentLibrary stub (used by TestLibraryMissingDeps) +# + +include $(POCO_BASE)/build/rules/global + +objects = NonExistentStub + +target = NonExistentLibrary +target_version = 1 +target_libs = + +include $(POCO_BASE)/build/rules/lib diff --git a/Foundation/testsuite/Makefile-TestLibraryMissingDeps b/Foundation/testsuite/Makefile-TestLibraryMissingDeps new file mode 100644 index 000000000..1c03e5336 --- /dev/null +++ b/Foundation/testsuite/Makefile-TestLibraryMissingDeps @@ -0,0 +1,15 @@ +# +# Makefile +# +# Makefile for TestLibraryMissingDeps +# + +include $(POCO_BASE)/build/rules/global + +objects = TestLibraryMissingDeps + +target = TestLibraryMissingDeps +target_version = 1 +target_libs = PocoFoundation NonExistentLibrary + +include $(POCO_BASE)/build/rules/dylib diff --git a/Foundation/testsuite/src/ClassLoaderTest.cpp b/Foundation/testsuite/src/ClassLoaderTest.cpp index 094cbeb48..55fbb7f7d 100644 --- a/Foundation/testsuite/src/ClassLoaderTest.cpp +++ b/Foundation/testsuite/src/ClassLoaderTest.cpp @@ -17,6 +17,7 @@ #include "Poco/Path.h" #include "Poco/File.h" #include "Poco/Format.h" +#include "Poco/Environment.h" #include "TestPlugin.h" @@ -28,6 +29,7 @@ using Poco::NotFoundException; using Poco::InvalidAccessException; using Poco::Path; using Poco::File; +using Poco::Environment; ClassLoaderTest::ClassLoaderTest(const std::string& name): CppUnit::TestCase(name) @@ -196,27 +198,33 @@ void ClassLoaderTest::testClassLoader3() std::string ClassLoaderTest::getFullName(const std::string& libName) { - std::string name = Path::expand("$POCO_BASE"); - char c = Path::separator(); - std::string OSNAME = Path::expand("$OSNAME"); - std::string OSARCH = Path::expand("$OSARCH"); - name.append(1, c) - .append(Poco::format("Foundation%ctestsuite%cbin%c", c, c, c)) - .append(Poco::format("%s%c%s%c", OSNAME, c, OSARCH, c)) - .append(libName).append(SharedLibrary::suffix()); + // Get the directory where the test executable is located + Path selfPath(Path::self()); + selfPath.setFileName(""); + std::string name = selfPath.toString() + libName + SharedLibrary::suffix(); - // CMake - if (!File(name).exists()) + if (File(name).exists()) + return name; + + // Fallback: make build layout + std::string pocoBase = Environment::get("POCO_BASE", ""); + if (!pocoBase.empty()) { - name = Path::expand("$POCO_BASE"); - name.append(Poco::format("%ccmake-build%cbin%c", c, c, c)) + char c = Path::separator(); + std::string OSNAME = Environment::get("OSNAME", ""); + std::string OSARCH = Environment::get("OSARCH", ""); + name = pocoBase; + name.append(1, c) + .append(Poco::format("Foundation%ctestsuite%cbin%c", c, c, c)) + .append(Poco::format("%s%c%s%c", OSNAME, c, OSARCH, c)) .append(libName).append(SharedLibrary::suffix()); + + if (File(name).exists()) + return name; } - if (!File(name).exists()) - name = libName + SharedLibrary::suffix(); - - return name; + // Last resort: just the library name + return libName + SharedLibrary::suffix(); } diff --git a/Foundation/testsuite/src/NonExistentStub.c b/Foundation/testsuite/src/NonExistentStub.c new file mode 100644 index 000000000..635730569 --- /dev/null +++ b/Foundation/testsuite/src/NonExistentStub.c @@ -0,0 +1,15 @@ +/* + * NonExistentStub.c + * + * Stub library that provides nonExistentFunction. + * This library is built and then deleted to create a missing dependency + * for TestLibraryMissingDeps. + */ + +#ifdef _WIN32 +__declspec(dllexport) +#endif +int nonExistentFunction() +{ + return 0; +} diff --git a/Foundation/testsuite/src/SharedLibraryTest.cpp b/Foundation/testsuite/src/SharedLibraryTest.cpp old mode 100644 new mode 100755 index ee26fbd00..1bcb7b41a --- a/Foundation/testsuite/src/SharedLibraryTest.cpp +++ b/Foundation/testsuite/src/SharedLibraryTest.cpp @@ -15,7 +15,14 @@ #include "Poco/Exception.h" #include "Poco/Path.h" #include "Poco/File.h" +#include "Poco/Glob.h" #include "Poco/Format.h" +#include "Poco/Environment.h" +#include +#include +#include +#include +#include using Poco::SharedLibrary; @@ -24,6 +31,8 @@ using Poco::LibraryLoadException; using Poco::LibraryAlreadyLoadedException; using Poco::Path; using Poco::File; +using Poco::Glob; +using Poco::Environment; typedef int (*GimmeFiveFunc)(); @@ -129,30 +138,149 @@ void SharedLibraryTest::testSharedLibrary3() } +void SharedLibraryTest::testMissingDependencies() +{ + // Test with a valid library - should return empty list (all dependencies found) + std::string libraryPath = getFullName("TestLibrary"); + assertTrue (File(libraryPath).exists()); + std::vector missing = SharedLibrary::findMissingDependencies(libraryPath); + // TestLibrary should have all its dependencies available + for (const auto& dep : missing) + std::cout << "TestLibrary missing: " << dep << std::endl; + assertTrue (missing.empty()); + + // Test with non-existent file - should return the file itself as missing + missing = SharedLibrary::findMissingDependencies("NonexistentFile.dll"); + assertTrue (missing.size() == 1); + assertTrue (missing[0] == "NonexistentFile.dll"); + + // Test with empty path - should return empty path as missing + missing = SharedLibrary::findMissingDependencies(""); + assertTrue (missing.size() == 1); + assertTrue (missing[0].empty()); + + // Test with a library that has missing dependencies + std::string missingDepsLib = getFullName("TestLibraryMissingDeps"); + if (File(missingDepsLib).exists()) + { + // Delete all NonExistentLibrary files (including versioned ones and symlinks) to ensure the test is valid + // Try both with and without "lib" prefix since CMake and make builds differ + // Also check the lib/ directory for make builds (libraries have absolute paths embedded) + std::set libFiles; + Path selfPath(Path::self()); + selfPath.setFileName(""); + std::string baseDir = selfPath.toString(); + std::replace(baseDir.begin(), baseDir.end(), '\\', '/'); + Glob::glob(baseDir + "NonExistentLibrary*", libFiles); + Glob::glob(baseDir + "libNonExistentLibrary*", libFiles); + + // For make builds, also check POCO_BASE/lib/OSNAME/OSARCH/ + std::string pocoBase = Environment::get("POCO_BASE", ""); + if (!pocoBase.empty()) + { + std::string OSNAME = Environment::get("OSNAME", ""); + std::string OSARCH = Environment::get("OSARCH", ""); + std::string libDir = pocoBase + "/lib/" + OSNAME + "/" + OSARCH + "/"; + Glob::glob(libDir + "NonExistentLibrary*", libFiles); + Glob::glob(libDir + "libNonExistentLibrary*", libFiles); + } + + for (const auto& libFile : libFiles) + { + File f(libFile); + if (f.exists() || f.isLink()) + f.remove(); + } + + missing = SharedLibrary::findMissingDependencies(missingDepsLib); + // Should report NonExistentLibrary as missing + assertTrue (!missing.empty()); + bool foundNonExistent = false; + for (const auto& dep : missing) + { + if (dep.find("NonExistentLibrary") != std::string::npos) + { + foundNonExistent = true; + break; + } + } + assertTrue (foundNonExistent); + for (const auto& dep : missing) + std::cout << "Missing library: " << dep; + } +} + + std::string SharedLibraryTest::getFullName(const std::string& libName) { - // make - std::string name = Path::expand("$POCO_BASE"); - char c = Path::separator(); - std::string OSNAME = Path::expand("$OSNAME"); - std::string OSARCH = Path::expand("$OSARCH"); - name.append(1, c) - .append(Poco::format("Foundation%ctestsuite%cbin%c", c, c, c)) - .append(Poco::format("%s%c%s%c", OSNAME, c, OSARCH, c)) - .append(libName).append(SharedLibrary::suffix()); + // Get the directory where the test executable is located + Path selfPath(Path::self()); + selfPath.setFileName(""); + std::string name = selfPath.toString() + libName + SharedLibrary::suffix(); - // CMake - if (!File(name).exists()) + if (File(name).exists()) + return name; + + // Fallback: make build layout + std::string pocoBase = Environment::get("POCO_BASE", ""); + if (!pocoBase.empty()) { - name = Path::expand("$POCO_BASE"); - name.append(Poco::format("%ccmake-build%cbin%c", c, c, c)) + char c = Path::separator(); + std::string OSNAME = Environment::get("OSNAME", ""); + std::string OSARCH = Environment::get("OSARCH", ""); + name = pocoBase; + name.append(1, c) + .append(Poco::format("Foundation%ctestsuite%cbin%c", c, c, c)) + .append(Poco::format("%s%c%s%c", OSNAME, c, OSARCH, c)) .append(libName).append(SharedLibrary::suffix()); + + if (File(name).exists()) + return name; } - if (!File(name).exists()) - name = libName + SharedLibrary::suffix(); + // Last resort: just the library name + return libName + SharedLibrary::suffix(); +} - return name; + +std::string SharedLibraryTest::getLibFullName(const std::string& libName) +{ + std::string suffix = SharedLibrary::suffix(); + + // Get the directory where the test executable is located + Path selfPath(Path::self()); + selfPath.setFileName(""); + +#if defined(_WIN32) + std::string name = selfPath.toString() + libName + suffix; +#else + // On Unix, shared libraries have a "lib" prefix + std::string name = selfPath.toString() + "lib" + libName + suffix; +#endif + + if (File(name).exists()) + return name; + +#if !defined(_WIN32) + // Fallback: make build layout - libraries in lib/ directory with lib prefix + std::string pocoBase = Environment::get("POCO_BASE", ""); + if (!pocoBase.empty()) + { + char c = Path::separator(); + std::string OSNAME = Environment::get("OSNAME", ""); + std::string OSARCH = Environment::get("OSARCH", ""); + name = pocoBase; + name.append(1, c) + .append(Poco::format("lib%c%s%c%s%c", c, OSNAME, c, OSARCH, c)) + .append("lib").append(libName).append(suffix); + + if (File(name).exists()) + return name; + } +#endif + + // Last resort: just the library name + return libName + suffix; } @@ -173,6 +301,7 @@ CppUnit::Test* SharedLibraryTest::suite() CppUnit_addTest(pSuite, SharedLibraryTest, testSharedLibrary1); CppUnit_addTest(pSuite, SharedLibraryTest, testSharedLibrary2); CppUnit_addTest(pSuite, SharedLibraryTest, testSharedLibrary3); + CppUnit_addTest(pSuite, SharedLibraryTest, testMissingDependencies); return pSuite; } diff --git a/Foundation/testsuite/src/SharedLibraryTest.h b/Foundation/testsuite/src/SharedLibraryTest.h index 845203994..062696ef5 100644 --- a/Foundation/testsuite/src/SharedLibraryTest.h +++ b/Foundation/testsuite/src/SharedLibraryTest.h @@ -27,6 +27,7 @@ public: void testSharedLibrary1(); void testSharedLibrary2(); void testSharedLibrary3(); + void testMissingDependencies(); void setUp(); void tearDown(); @@ -35,6 +36,7 @@ public: private: static std::string getFullName(const std::string& libName); + static std::string getLibFullName(const std::string& libName); }; diff --git a/Foundation/testsuite/src/TestLibraryMissingDeps.cpp b/Foundation/testsuite/src/TestLibraryMissingDeps.cpp new file mode 100644 index 000000000..017c8c7a4 --- /dev/null +++ b/Foundation/testsuite/src/TestLibraryMissingDeps.cpp @@ -0,0 +1,26 @@ +// +// TestLibraryMissingDeps.cpp +// +// A test library that depends on a non-existent library. +// Used to test findMissingDependencies functionality. +// +// Copyright (c) 2004-2006, Applied Informatics Software Engineering GmbH. +// and Contributors. +// +// SPDX-License-Identifier: BSL-1.0 +// + + +#include "Poco/ClassLibrary.h" + + +// Declare an external function from a non-existent library +// This will cause the linker to add a dependency on the fake library +extern "C" int nonExistentFunction(); + + +extern "C" POCO_LIBRARY_API int gimmeSix() +{ + // Reference the non-existent function to ensure the dependency is kept + return 6 + (nonExistentFunction() * 0); +}