diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6122247b..0cbefbc2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,6 +52,28 @@ jobs: make install make test + build-windows: + name: build-windows (mingw, openssl, icu) + runs-on: windows-latest + steps: + - name: git-config + run: git config --global core.autocrlf input + - name: install-deps + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + pacboy: cmake:p cppunit:p gcc:p gsasl:p icu:p ninja:p openssl:p pkgconf:p + - name: clone-vmime + uses: actions/checkout@v4 + - name: buildall + shell: msys2 {0} + run: | + set -ex + cmake . -G Ninja -DVMIME_HAVE_MESSAGING_PROTO_SENDMAIL:BOOL=OFF -DVMIME_HAVE_TLS_SUPPORT:BOOL=ON -DVMIME_TLS_SUPPORT_LIB:STRING=openssl -DVMIME_CHARSETCONV_LIB:STRING=icu -DVMIME_BUILD_TESTS=ON -DVMIME_BUILD_SAMPLES=ON + cmake --build . + ctest --output-on-failure + book: name: book (pdf) runs-on: ubuntu-26.04 diff --git a/CMakeLists.txt b/CMakeLists.txt index b1c56965..4d42f15c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -268,6 +268,18 @@ IF(VMIME_BUILD_TESTS) ${CPPUNIT_LIBRARY} ) + # On Windows, embed a manifest in test executables to use UTF-8 as the + # ANSI code page, as some tests expect the local charset to be UTF-8 + SET(VMIME_TESTS_RESOURCE_FILES "") + + IF(MSVC) + # Merged by the linker with the manifest it generates + SET(VMIME_TESTS_RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/tests/utf8.manifest) + ELSEIF(MINGW) + # Replaces the default manifest added by the toolchain + SET(VMIME_TESTS_RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/tests/utf8.rc) + ENDIF() + # Build one file for each test FOREACH(VMIME_TEST_SRC_FILE ${VMIME_TESTS_SRC_FILES}) @@ -282,6 +294,7 @@ IF(VMIME_BUILD_TESTS) ADD_EXECUTABLE( ${VMIME_TEST_NAME} ${VMIME_TEST_SRC_FILE} + ${VMIME_TESTS_RESOURCE_FILES} ) TARGET_LINK_LIBRARIES( @@ -306,6 +319,7 @@ IF(VMIME_BUILD_TESTS) ADD_EXECUTABLE( "run-tests" ${VMIME_TESTS_SRC_FILES} + ${VMIME_TESTS_RESOURCE_FILES} ) TARGET_LINK_LIBRARIES( diff --git a/src/vmime/platforms/posix/posixFile.cpp b/src/vmime/platforms/posix/posixFile.cpp index 9773b3a0..3a064ea9 100644 --- a/src/vmime/platforms/posix/posixFile.cpp +++ b/src/vmime/platforms/posix/posixFile.cpp @@ -39,6 +39,7 @@ #include #include +#include #include #include "vmime/exception.hpp" @@ -673,6 +674,19 @@ bool posixFileSystemFactory::isValidPath(const vmime::utility::file::path& path) } +const vmime::utility::file::path posixFileSystemFactory::getTemporaryDirectoryPath() const { + + // Directory specified by the user (POSIX), or default one + const char* dir = ::getenv("TMPDIR"); + + if (!dir || !*dir) { + dir = "/tmp"; + } + + return stringToPathImpl(dir); +} + + void posixFileSystemFactory::reportError(const vmime::utility::path& path, const int err) { vmime::string desc; diff --git a/src/vmime/platforms/posix/posixFile.hpp b/src/vmime/platforms/posix/posixFile.hpp index d1fda9e8..2c89245f 100644 --- a/src/vmime/platforms/posix/posixFile.hpp +++ b/src/vmime/platforms/posix/posixFile.hpp @@ -210,6 +210,8 @@ class posixFileSystemFactory : public vmime::utility::fileSystemFactory { bool isValidPathComponent(const vmime::utility::file::path::component& comp) const; bool isValidPath(const vmime::utility::file::path& path) const; + const vmime::utility::file::path getTemporaryDirectoryPath() const; + static void reportError(const vmime::utility::path& path, const int err); }; diff --git a/src/vmime/platforms/windows/windowsFile.cpp b/src/vmime/platforms/windows/windowsFile.cpp index 774731c7..2d21b17d 100644 --- a/src/vmime/platforms/windows/windowsFile.cpp +++ b/src/vmime/platforms/windows/windowsFile.cpp @@ -208,6 +208,20 @@ bool windowsFileSystemFactory::isValidPath(const vmime::utility::file::path& pat } +const vmime::utility::file::path windowsFileSystemFactory::getTemporaryDirectoryPath() const { + + char buffer[MAX_PATH + 1]; + const DWORD length = GetTempPathA(static_cast (sizeof(buffer)), buffer); + + if (length == 0 || length > sizeof(buffer)) { + reportError(vmime::utility::file::path(), GetLastError()); + } + + // Trailing backslash is ignored + return stringToPathImpl(vmime::string(buffer, length)); +} + + void windowsFileSystemFactory::reportError(const vmime::utility::path& path, const int err) { vmime::string desc; @@ -408,7 +422,12 @@ void windowsFile::rename(const path& newName) { void windowsFile::remove() { - if (!DeleteFile(m_nativePath.c_str())) { + // DeleteFile() does not remove directories + const BOOL removed = isDirectory() + ? RemoveDirectory(m_nativePath.c_str()) + : DeleteFile(m_nativePath.c_str()); + + if (!removed) { windowsFileSystemFactory::reportError(m_path, GetLastError()); } } @@ -428,7 +447,8 @@ shared_ptr windowsFile::getFileReader() { shared_ptr windowsFile::getFiles() const { - return make_shared (m_path, m_nativePath); + // FindFirstFile() expects a pattern, else it finds the directory itself + return make_shared (m_path, m_nativePath + "\\*"); } diff --git a/src/vmime/platforms/windows/windowsFile.hpp b/src/vmime/platforms/windows/windowsFile.hpp index dc4b5ed9..a99e3098 100644 --- a/src/vmime/platforms/windows/windowsFile.hpp +++ b/src/vmime/platforms/windows/windowsFile.hpp @@ -59,6 +59,8 @@ class windowsFileSystemFactory : public vmime::utility::fileSystemFactory { bool firstComponent) const; bool isValidPath(const vmime::utility::file::path& path) const; + const vmime::utility::file::path getTemporaryDirectoryPath() const; + static void reportError(const vmime::utility::path& path, const int err); }; diff --git a/src/vmime/platforms/windows/windowsHandler.cpp b/src/vmime/platforms/windows/windowsHandler.cpp index f2409402..d6d63b7e 100644 --- a/src/vmime/platforms/windows/windowsHandler.cpp +++ b/src/vmime/platforms/windows/windowsHandler.cpp @@ -43,6 +43,12 @@ #if VMIME_HAVE_MLANG # include + +// Defined here, as IID_IMultiLanguage is missing from MinGW libraries +static const CLSID VMIME_CLSID_CMultiLanguage = + { 0x275c23e2, 0x3747, 0x11d0, { 0x9f, 0xea, 0x00, 0xaa, 0x00, 0x3f, 0x86, 0x46 } }; +static const IID VMIME_IID_IMultiLanguage = + { 0x275c23e1, 0x3747, 0x11d0, { 0x9f, 0xea, 0x00, 0xaa, 0x00, 0x3f, 0x86, 0x46 } }; #endif @@ -132,10 +138,10 @@ const vmime::charset windowsHandler::getLocalCharset() const { IMultiLanguage* pMultiLanguage; CoCreateInstance( - CLSID_CMultiLanguage, + VMIME_CLSID_CMultiLanguage, NULL, CLSCTX_INPROC_SERVER, - IID_IMultiLanguage, + VMIME_IID_IMultiLanguage, (void**) &pMultiLanguage ); diff --git a/src/vmime/utility/file.hpp b/src/vmime/utility/file.hpp index 6e0605e1..197dd27e 100644 --- a/src/vmime/utility/file.hpp +++ b/src/vmime/utility/file.hpp @@ -26,6 +26,7 @@ #include "vmime/config.hpp" +#include "vmime/exception.hpp" #include "vmime/utility/path.hpp" #include "vmime/utility/stream.hpp" @@ -251,6 +252,19 @@ class VMIME_EXPORT fileSystemFactory : public object { * @return true if the path is valid, false otherwise */ virtual bool isValidPath(const file::path& path) const = 0; + + /** Return the path of the directory to use for temporary files. + * + * @return path of the system temporary directory + * @throw exceptions::filesystem_exception if the temporary directory + * cannot be determined, or if not supported on this platform + */ + virtual const file::path getTemporaryDirectoryPath() const { + + throw exceptions::filesystem_exception( + "Temporary directory is not supported on this platform", file::path() + ); + } }; diff --git a/tests/net/maildir/maildirStoreTest.cpp b/tests/net/maildir/maildirStoreTest.cpp index 1f418e81..b337492d 100644 --- a/tests/net/maildir/maildirStoreTest.cpp +++ b/tests/net/maildir/maildirStoreTest.cpp @@ -43,6 +43,15 @@ const fpath operator/(const fpath& path, const std::string& c) { } +// Delimiter between message id and flags in file names, +// same as in maildirUtils::buildFilename() +#if VMIME_PLATFORM_IS_WINDOWS +# define TEST_FLAGS_DELIMITER "-" // ':' is not allowed in file names +#else +# define TEST_FLAGS_DELIMITER ":" +#endif + + /** Test messages */ static const vmime::string TEST_MESSAGE_1 = "From: \r\n" @@ -92,7 +101,7 @@ static const vmime::string TEST_MAILDIR_KMAIL[] = { // directories to create }; static const vmime::string TEST_MAILDIRFILES_KMAIL[] = { // files to create and their contents - "/.Folder.directory/.SubFolder.directory/SubSubFolder2/cur/1043236113.351.EmqD:S", TEST_MESSAGE_1, + "/.Folder.directory/.SubFolder.directory/SubSubFolder2/cur/1043236113.351.EmqD" TEST_FLAGS_DELIMITER "S", TEST_MESSAGE_1, "*" // end }; @@ -127,7 +136,7 @@ static const vmime::string TEST_MAILDIRFILES_COURIER[] = { // files to create a "/.Folder.SubFolder/maildirfolder", "", "/.Folder.SubFolder.SubSubFolder1/maildirfolder", "", "/.Folder.SubFolder.SubSubFolder2/maildirfolder", "", - "/.Folder.SubFolder.SubSubFolder2/cur/1043236113.351.EmqD:S", TEST_MESSAGE_1, + "/.Folder.SubFolder.SubSubFolder2/cur/1043236113.351.EmqD" TEST_FLAGS_DELIMITER "S", TEST_MESSAGE_1, "/.Folder2/maildirfolder", "", "*" // end }; @@ -167,8 +176,11 @@ VMIME_TEST_SUITE_BEGIN(maildirStoreTest) maildirStoreTest() { + vmime::shared_ptr fsf = + vmime::platform::getHandler()->getFileSystemFactory(); + // Temporary directory - m_tempPath = fspath() / fspathc("tmp") // Use /tmp + m_tempPath = fsf->getTemporaryDirectoryPath() / fspathc("vmime" + vmime::utility::stringUtils::toString(std::time(NULL)) + vmime::utility::stringUtils::toString(std::rand())); } @@ -500,8 +512,9 @@ VMIME_TEST_SUITE_BEGIN(maildirStoreTest) vmime::shared_ptr fsf = vmime::platform::getHandler()->getFileSystemFactory(); - vmime::utility::url url(std::string("maildir://localhost") - + fsf->pathToString(m_tempPath)); + // Path is set as is, as it may not start with a separator (eg. on Windows) + vmime::utility::url url("maildir", "localhost"); // maildir://localhost + url.setPath(fsf->pathToString(m_tempPath)); return url; } diff --git a/tests/parser/fileContentHandlerTest.cpp b/tests/parser/fileContentHandlerTest.cpp index e979b52f..2355fc6e 100644 --- a/tests/parser/fileContentHandlerTest.cpp +++ b/tests/parser/fileContentHandlerTest.cpp @@ -47,13 +47,16 @@ VMIME_TEST_SUITE_BEGIN(fileContentHandlerTest) testDataDecoded = "ABCDEFGHIJKLMNOPQRSTUVWXYZ \x12\x34\x56\x78\x90 abcdefghijklmnopqrstuvwxyz0123456789"; testDataEncoded = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogEjRWeJAgYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5"; - std::ostringstream testFilePath; - testFilePath << "/tmp/vmime_test_" << (rand() % 999999999); + std::ostringstream testFileName; + testFileName << "vmime_test_" << (rand() % 999999999); vmime::shared_ptr fsf = vmime::platform::getHandler()->getFileSystemFactory(); - testFile = fsf->create(fsf->stringToPath(testFilePath.str())); + testFile = fsf->create( + fsf->getTemporaryDirectoryPath() + / vmime::utility::file::path::component(testFileName.str()) + ); testFile->createFile(); testFile->getFileWriter()->getOutputStream()->write(testDataEncoded.data(), testDataEncoded.length()); } diff --git a/tests/parser/textTest.cpp b/tests/parser/textTest.cpp index ce767863..13e498e1 100644 --- a/tests/parser/textTest.cpp +++ b/tests/parser/textTest.cpp @@ -453,7 +453,12 @@ VMIME_TEST_SUITE_BEGIN(textTest) vmime::word("Non-quotable text \xc3\xa9") .generate(ctx, os, 0, NULL, vmime::text::QUOTE_IF_POSSIBLE, NULL); - VASSERT_EQ("3", "=?UTF-8?Q?Non-quotable_text_=C3=A9?=", cleanGeneratedWords(str)); + // Local charset name may be "UTF-8" or "utf-8", depending on the platform + VASSERT_EQ( + "3", + "=?" + vmime::charset::getLocalCharset().getName() + "?Q?Non-quotable_text_=C3=A9?=", + cleanGeneratedWords(str) + ); } void testWordGenerateSpecialCharsets() { diff --git a/tests/testUtils.cpp b/tests/testUtils.cpp index 5ee66b09..054299a8 100644 --- a/tests/testUtils.cpp +++ b/tests/testUtils.cpp @@ -465,7 +465,8 @@ const vmime::string toHex(const vmime::string str) { // Set the global C and C++ locale to a locale which uses UTF-8 encoding, -// as some tests expect the local charset to be UTF-8 +// as some tests expect the local charset to be UTF-8 (on Windows, the local +// charset is the ANSI code page instead, which is set by tests/utf8.manifest) static void setUTF8Locale() { // Try well-known UTF-8 locales, then fall back to the user-configured locale diff --git a/tests/utf8.manifest b/tests/utf8.manifest new file mode 100644 index 00000000..dd5297e2 --- /dev/null +++ b/tests/utf8.manifest @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + UTF-8 + + + diff --git a/tests/utf8.rc b/tests/utf8.rc new file mode 100644 index 00000000..b8da505b --- /dev/null +++ b/tests/utf8.rc @@ -0,0 +1,6 @@ +// Application manifest for test executables on Windows (MinGW): use UTF-8 as +// the ANSI code page, which is used as the local charset by windowsHandler, +// as some tests expect the local charset to be UTF-8. +// This replaces the default manifest added by the MinGW toolchain. + +1 24 "utf8.manifest" // CREATEPROCESS_MANIFEST_RESOURCE_ID, RT_MANIFEST