fix(SharedLibrary): Missing DLLs not reported #5069

This commit is contained in:
Alex Fabijanic
2025-12-05 13:50:53 -06:00
parent 2bdeaff849
commit 42ffe44e5c
21 changed files with 920 additions and 42 deletions

30
.gitignore vendored
View File

@@ -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

View File

@@ -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)
{
}

15
Foundation/include/Poco/SharedLibrary.h Normal file → Executable file
View File

@@ -19,6 +19,8 @@
#include "Poco/Foundation.h"
#include <vector>
#include <string>
#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<std::string> 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&);

View File

@@ -21,6 +21,8 @@
#include "Poco/Foundation.h"
#include "Poco/Mutex.h"
#include <dl.h>
#include <vector>
#include <string>
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<std::string> findMissingDependenciesImpl(const std::string& path);
private:
std::string _path;

View File

@@ -20,6 +20,8 @@
#include "Poco/Foundation.h"
#include "Poco/Mutex.h"
#include <vector>
#include <string>
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<std::string> findMissingDependenciesImpl(const std::string& path);
private:
std::string _path;

View File

@@ -21,6 +21,8 @@
#include "Poco/Foundation.h"
#include "Poco/Mutex.h"
#include <moduleLib.h>
#include <vector>
#include <string>
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<std::string> findMissingDependenciesImpl(const std::string& path);
private:
std::string _path;

View File

@@ -20,6 +20,8 @@
#include "Poco/Foundation.h"
#include "Poco/Mutex.h"
#include <vector>
#include <string>
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<std::string> findMissingDependenciesImpl(const std::string& path);
private:
std::string _path;

View File

@@ -110,4 +110,10 @@ bool SharedLibrary::setSearchPath(const std::string& path)
}
std::vector<std::string> SharedLibrary::findMissingDependencies(const std::string& path)
{
return findMissingDependenciesImpl(path);
}
} // namespace Poco

View File

@@ -98,4 +98,11 @@ bool SharedLibraryImpl::setSearchPathImpl(const std::string&)
}
std::vector<std::string> SharedLibraryImpl::findMissingDependenciesImpl(const std::string&)
{
// not implemented
return std::vector<std::string>();
}
} // namespace Poco

View File

@@ -15,6 +15,17 @@
#include "Poco/SharedLibrary_UNIX.h"
#include "Poco/Exception.h"
#include <dlfcn.h>
#include <vector>
#include <string>
#include <fstream>
#include <cstring>
#if defined(__APPLE__)
#include <mach-o/loader.h>
#include <mach-o/fat.h>
#elif defined(__linux__)
#include <elf.h>
#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<std::string> 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<std::string> SharedLibraryImpl::findMissingDependenciesImpl(const std::string& path)
{
std::vector<std::string> missingDeps;
std::ifstream file(path, std::ios::binary);
if (!file)
{
missingDeps.push_back(path);
return missingDeps;
}
try
{
uint32_t magic;
file.read(reinterpret_cast<char*>(&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<char*>(&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<char*>(&arch), sizeof(arch));
if (!file)
return missingDeps;
uint32_t offset = fatSwap ? __builtin_bswap32(arch.offset) : arch.offset;
file.seekg(offset);
file.read(reinterpret_cast<char*>(&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<std::streamoff>(sizeof(magic)), std::ios::cur);
mach_header_64 header;
file.read(reinterpret_cast<char*>(&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<std::streamoff>(sizeof(magic)), std::ios::cur);
mach_header header;
file.read(reinterpret_cast<char*>(&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<char> 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<const load_command*>(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<const dylib_command*>(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<std::string> SharedLibraryImpl::findMissingDependenciesImpl(const std::string& path)
{
std::vector<std::string> 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<char*>(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<std::string> neededLibs;
if (is64)
{
Elf64_Ehdr ehdr;
file.read(reinterpret_cast<char*>(&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<Elf64_Shdr> sections(shnum);
file.seekg(shoff);
for (uint16_t i = 0; i < shnum; ++i)
{
file.read(reinterpret_cast<char*>(&sections[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 = &sections[dynSection->sh_link];
if (!dynSection || !dynstrSection)
return missingDeps;
// Read string table
std::vector<char> 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<char*>(&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<char*>(&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<Elf32_Shdr> sections(shnum);
file.seekg(shoff);
for (uint16_t i = 0; i < shnum; ++i)
{
file.read(reinterpret_cast<char*>(&sections[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 = &sections[dynSection->sh_link];
if (!dynSection || !dynstrSection)
return missingDeps;
std::vector<char> 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<char*>(&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<std::string> SharedLibraryImpl::findMissingDependenciesImpl(const std::string&)
{
// Unsupported platform - return empty list
return std::vector<std::string>();
}
#endif
} // namespace Poco

View File

@@ -139,4 +139,11 @@ bool SharedLibraryImpl::setSearchPathImpl(const std::string&)
}
std::vector<std::string> SharedLibraryImpl::findMissingDependenciesImpl(const std::string&)
{
// not implemented
return std::vector<std::string>();
}
} // namespace Poco

176
Foundation/src/SharedLibrary_WIN32U.cpp Normal file → Executable file
View File

@@ -19,6 +19,8 @@
#include "Poco/Error.h"
#include "Poco/Format.h"
#include "Poco/String.h"
#include <vector>
#include <string>
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<std::string> 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<std::string> SharedLibraryImpl::findMissingDependenciesImpl(const std::string& path)
{
std::vector<std::string> 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<PIMAGE_DOS_HEADER>(pBase);
if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
return;
PIMAGE_NT_HEADERS pNtHeaders = reinterpret_cast<PIMAGE_NT_HEADERS>(
static_cast<BYTE*>(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<PIMAGE_NT_HEADERS32>(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<PIMAGE_NT_HEADERS64>(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<PIMAGE_IMPORT_DESCRIPTOR>(
static_cast<BYTE*>(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<const char*>(static_cast<BYTE*>(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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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;
}

161
Foundation/testsuite/src/SharedLibraryTest.cpp Normal file → Executable file
View File

@@ -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 <algorithm>
#include <vector>
#include <string>
#include <set>
#include <iostream>
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<std::string> 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<std::string> 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;
}

View File

@@ -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);
};

View File

@@ -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);
}