From 9092aea8853852a3dcb85ce7c160d9b2a8ffbe9d Mon Sep 17 00:00:00 2001 From: ptahmose Date: Mon, 4 Dec 2023 13:52:52 +0100 Subject: [PATCH] add option "kCurlHttp_CaInfo" & "kCurlHttp_CaInfoBlob", allow to retrieve properties from a stream-class (#82) * add CURLOPT_CAINFO and CURLOPT_CAINFO_BLOB * fix * test * fix * cosmetic * fix * cosmetic * ...add test-code * Revert "...add test-code" This reverts commit ca8c2ec5153be30b4060d2eec655a88255ca4843. * cosmetic * bump version * add a test * fix incompatibility with older GCC (8.3) * comaptibility-issue with older GCC * test * cosmetic * fix issue with older GCC * fix issue with older GCC --- CMakeLists.txt | 2 +- Src/CMakeLists.txt | 2 +- Src/CZICmd/cmdlineoptions.cpp | 12 +++-- Src/libCZI/CziMetadataDocumentInfo.cpp | 13 ++--- Src/libCZI/StreamsLib/curlhttpinputstream.cpp | 49 ++++++++++++++++++- Src/libCZI/StreamsLib/curlhttpinputstream.h | 1 + Src/libCZI/StreamsLib/streamsFactory.cpp | 11 +++-- Src/libCZI/libCZI_StreamsLib.h | 28 ++++++++++- Src/libCZI_UnitTests/test_metadatareading.cpp | 40 +++++++-------- Src/libCZI_UnitTests/test_reader.cpp | 23 ++++++++- Src/libCZI_UnitTests/test_streamslib.cpp | 26 ++++++++++ 11 files changed, 166 insertions(+), 41 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4971f15f..f33cb0f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15) cmake_policy(SET CMP0091 NEW) # enable new "MSVC runtime library selection" (https://cmake.org/cmake/help/latest/variable/CMAKE_MSVC_RUNTIME_LIBRARY.html) project(libCZI - VERSION 0.55.1 + VERSION 0.56.0 HOMEPAGE_URL "https://github.com/ZEISS/libczi" DESCRIPTION "libCZI is an Open Source Cross-Platform C++ library to read and write CZI") diff --git a/Src/CMakeLists.txt b/Src/CMakeLists.txt index 54ba5e5a..5875cb4c 100644 --- a/Src/CMakeLists.txt +++ b/Src/CMakeLists.txt @@ -26,7 +26,7 @@ if (LIBCZI_BUILD_CURL_BASED_STREAM) message(STATUS "Using CURL lib(s): ${CURL_LIBRARIES}") else(LIBCZI_BUILD_PREFER_EXTERNALPACKAGE_LIBCURL) - message(STATUS "Could not find libcURL. This dependency will be downloaded.") + message(STATUS "Attempting to download and build libcURL.") include(FetchContent) # It seems for MacOS, the secure transport API is deprecated (-> https://curl.se/mail/lib-2023-09/0027.html), # using OpenSSL seems to be the way to go here - so, we better do not default to secure transport here. diff --git a/Src/CZICmd/cmdlineoptions.cpp b/Src/CZICmd/cmdlineoptions.cpp index 0e980e8b..e0a350ff 100644 --- a/Src/CZICmd/cmdlineoptions.cpp +++ b/Src/CZICmd/cmdlineoptions.cpp @@ -1901,16 +1901,20 @@ void CCmdLineOptions::PrintHelpBuildInfo() stringstream ss; ss << "version : " << majorVer << "." << minorVer << "." << patchVer; this->GetLog()->WriteLineStdOut(ss.str()); - ss = stringstream(); + ss.clear(); + ss.str(""); ss << "compiler : " << buildInfo.compilerIdentification; this->GetLog()->WriteLineStdOut(ss.str()); - ss = stringstream(); + ss.clear(); + ss.str(""); ss << "repository-URL : " << buildInfo.repositoryUrl; this->GetLog()->WriteLineStdOut(ss.str()); - ss = stringstream(); + ss.clear(); + ss.str(""); ss << "repository-branch: " << buildInfo.repositoryBranch; this->GetLog()->WriteLineStdOut(ss.str()); - ss = stringstream(); + ss.clear(); + ss.str(""); ss << "repository-tag : " << buildInfo.repositoryTag; this->GetLog()->WriteLineStdOut(ss.str()); } diff --git a/Src/libCZI/CziMetadataDocumentInfo.cpp b/Src/libCZI/CziMetadataDocumentInfo.cpp index a75e6fd1..3c2641d0 100644 --- a/Src/libCZI/CziMetadataDocumentInfo.cpp +++ b/Src/libCZI/CziMetadataDocumentInfo.cpp @@ -94,14 +94,14 @@ CCziMetadataDocumentInfo::CCziMetadataDocumentInfo(std::shared_ptr static const struct { - char dimChar; + wchar_t dimChar; double(ScalingInfoEx::* scaleVarPtr); std::wstring(ScalingInfoEx::* defaultUnit); } dimScalingData[] = { - {'X', &ScalingInfoEx::scaleX, &ScalingInfoEx::defaultUnitFormatX}, - {'Y', &ScalingInfoEx::scaleY, &ScalingInfoEx::defaultUnitFormatY}, - {'Z', &ScalingInfoEx::scaleZ, &ScalingInfoEx::defaultUnitFormatZ}, + {L'X', &ScalingInfoEx::scaleX, &ScalingInfoEx::defaultUnitFormatX}, + {L'Y', &ScalingInfoEx::scaleY, &ScalingInfoEx::defaultUnitFormatY}, + {L'Z', &ScalingInfoEx::scaleZ, &ScalingInfoEx::defaultUnitFormatZ}, }; for (const auto d : dimScalingData) @@ -114,12 +114,13 @@ CCziMetadataDocumentInfo::CCziMetadataDocumentInfo(std::shared_ptr scalingInfo.*d.scaleVarPtr = nodeScalingValue.node().text().as_double(); } - ss = wstringstream(); + ss.clear(); + ss.str(L""); ss << L"Items/Distance[@Id='" << d.dimChar << L"']/DefaultUnitFormat"; auto nodeScalingDefaultUnit = np.select_node(ss.str().c_str()); if (!nodeScalingDefaultUnit.node().empty()) { - scalingInfo.*d.defaultUnit = nodeScalingDefaultUnit.node().text().as_string(); + scalingInfo.*d.defaultUnit = nodeScalingDefaultUnit.node().text().get(); } } diff --git a/Src/libCZI/StreamsLib/curlhttpinputstream.cpp b/Src/libCZI/StreamsLib/curlhttpinputstream.cpp index 84be504f..61925a65 100644 --- a/Src/libCZI/StreamsLib/curlhttpinputstream.cpp +++ b/Src/libCZI/StreamsLib/curlhttpinputstream.cpp @@ -52,6 +52,31 @@ using namespace libCZI; return string_stream.str(); } +/*static*/libCZI::StreamsFactory::Property CurlHttpInputStream::GetClassProperty(const char* property_name) +{ + if (property_name != nullptr) + { + if (strcmp(property_name, StreamsFactory::kStreamClassInfoProperty_CurlHttp_CaInfo) == 0) + { + const auto version_info = curl_version_info(CURLVERSION_NOW); + if (version_info->cainfo != nullptr) + { + return StreamsFactory::Property(version_info->cainfo); + } + } + else if (strcmp(property_name, StreamsFactory::kStreamClassInfoProperty_CurlHttp_CaPath) == 0) + { + const auto version_info = curl_version_info(CURLVERSION_NOW); + if (version_info->capath != nullptr) + { + return StreamsFactory::Property(version_info->capath); + } + } + } + + return {}; +} + CurlHttpInputStream::CurlHttpInputStream(const std::string& url, const std::map& property_bag) { /* init the curl session */ @@ -180,6 +205,28 @@ CurlHttpInputStream::CurlHttpInputStream(const std::string& url, const std::map< ThrowIfCurlSetOptError(return_code, "CURLOPT_MAXREDIRS"); } + property = property_bag.find(StreamsFactory::StreamProperties::kCurlHttp_CaInfo); + if (property != property_bag.end()) + { + return_code = curl_easy_setopt(up_curl_handle.get(), CURLOPT_CAINFO, property->second.GetAsStringOrThrow().c_str()); + ThrowIfCurlSetOptError(return_code, "CURLOPT_CAINFO"); + } + + property = property_bag.find(StreamsFactory::StreamProperties::kCurlHttp_CaInfoBlob); + if (property != property_bag.end()) + { + string ca_info_blob = property->second.GetAsStringOrThrow(); + if (!ca_info_blob.empty()) + { + struct curl_blob blob; + blob.data = &ca_info_blob[0]; + blob.len = ca_info_blob.size(); + blob.flags = CURL_BLOB_COPY; + return_code = curl_easy_setopt(up_curl_handle.get(), CURLOPT_CAINFO_BLOB, &blob); + ThrowIfCurlSetOptError(return_code, "CURLOPT_CAINFO_BLOB"); + } + } + this->curl_handle_ = up_curl_handle.release(); this->curl_url_handle_ = up_curl_url_handle.release(); } @@ -193,7 +240,7 @@ CurlHttpInputStream::CurlHttpInputStream(const std::string& url, const std::map< std::lock_guard lck(this->request_mutex_); // TODO(JBL): We may be able to use a "header-function" (https://curl.se/libcurl/c/CURLOPT_HEADERFUNCTION.html) in order to find out - // whether the server accepted out "Range-Request". According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests, + // whether the server accepted our "Range-Request". According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests, // we can expect to have a line "something like 'Accept-Ranges: bytes'" in the response header with a server that supports range // requests (and a line 'Accept-Ranges: none') would tell us explicitly that range requests are *not* supported. diff --git a/Src/libCZI/StreamsLib/curlhttpinputstream.h b/Src/libCZI/StreamsLib/curlhttpinputstream.h index 8b4eea90..a63c98e6 100644 --- a/Src/libCZI/StreamsLib/curlhttpinputstream.h +++ b/Src/libCZI/StreamsLib/curlhttpinputstream.h @@ -31,6 +31,7 @@ class CurlHttpInputStream : public libCZI::IStream static void OneTimeGlobalCurlInitialization(); static std::string GetBuildInformation(); + static libCZI::StreamsFactory::Property GetClassProperty(const char* property_name); private: /// This struct is passed to the WriteData function as user-data. struct WriteDataContext diff --git a/Src/libCZI/StreamsLib/streamsFactory.cpp b/Src/libCZI/StreamsLib/streamsFactory.cpp index 88b08fc6..728bdd1d 100644 --- a/Src/libCZI/StreamsLib/streamsFactory.cpp +++ b/Src/libCZI/StreamsLib/streamsFactory.cpp @@ -14,6 +14,9 @@ using namespace libCZI; +/*static*/const char* StreamsFactory::kStreamClassInfoProperty_CurlHttp_CaInfo = "CurlHttp_CaInfo"; +/*static*/const char* StreamsFactory::kStreamClassInfoProperty_CurlHttp_CaPath = "CurlHttp_CaPath"; + static const struct { StreamsFactory::StreamClassInfo stream_class_info; @@ -31,7 +34,7 @@ static const struct { #if LIBCZI_CURL_BASED_STREAM_AVAILABLE { - { "curl_http_inputstream", "curl-based http/https stream", CurlHttpInputStream::GetBuildInformation }, + { "curl_http_inputstream", "curl-based http/https stream", CurlHttpInputStream::GetBuildInformation, CurlHttpInputStream::GetClassProperty }, [](const StreamsFactory::CreateStreamInfo& stream_info, const std::string& file_name) -> std::shared_ptr { return std::make_shared(file_name, stream_info.property_bag); @@ -41,7 +44,7 @@ static const struct #endif // LIBCZI_CURL_BASED_STREAM_AVAILABLE #if _WIN32 { - { "windows_file_inputstream", "stream implementation based on Windows-API" }, + { "windows_file_inputstream", "stream implementation based on Windows-API", nullptr, nullptr }, [](const StreamsFactory::CreateStreamInfo& stream_info, const std::string& file_name) -> std::shared_ptr { return std::make_shared(file_name); @@ -54,7 +57,7 @@ static const struct #endif // _WIN32 #if LIBCZI_USE_PREADPWRITEBASED_STREAMIMPL { - { "pread_file_inputstream", "stream implementation based on pread-API" }, + { "pread_file_inputstream", "stream implementation based on pread-API", nullptr, nullptr }, [](const StreamsFactory::CreateStreamInfo& stream_info, const std::string& file_name) -> std::shared_ptr { return std::make_shared(file_name); @@ -63,7 +66,7 @@ static const struct }, #endif // LIBCZI_USE_PREADPWRITEBASED_STREAMIMPL { - { "c_runtime_file_inputstream", "stream implementation based on C-runtime library" }, + { "c_runtime_file_inputstream", "stream implementation based on C-runtime library", nullptr, nullptr }, [](const StreamsFactory::CreateStreamInfo& stream_info, const std::string& file_name) -> std::shared_ptr { return std::make_shared(file_name); diff --git a/Src/libCZI/libCZI_StreamsLib.h b/Src/libCZI/libCZI_StreamsLib.h index 0e03bc22..1037b7d7 100644 --- a/Src/libCZI/libCZI_StreamsLib.h +++ b/Src/libCZI/libCZI_StreamsLib.h @@ -228,6 +228,10 @@ namespace libCZI kCurlHttp_FollowLocation = 108, ///< For CurlHttpInputStream, type bool: a boolean indicating whether redirects are to be followed, c.f. https://curl.se/libcurl/c/CURLOPT_FOLLOWLOCATION.html for more information. kCurlHttp_MaxRedirs = 109, ///< For CurlHttpInputStream, type int32: gives the maximum number of redirects to follow, c.f. https://curl.se/libcurl/c/CURLOPT_MAXREDIRS.html for more information. + + kCurlHttp_CaInfo = 110, ///< For CurlHttpInputStream, type string: gives the directory to check for CA certificate bundle , c.f. https://curl.se/libcurl/c/CURLOPT_CAINFO.html for more information. + + kCurlHttp_CaInfoBlob = 111, ///< For CurlHttpInputStream, type string: give PEM encoded content holding one or more certificates to verify the HTTPS server with, c.f. https://curl.se/libcurl/c/CURLOPT_CAINFO_BLOB.html for more information. }; }; @@ -269,8 +273,18 @@ namespace libCZI struct LIBCZI_API StreamClassInfo { std::string class_name; ///< Name of the class (this uniquely identifies the class). - std::string short_description; ///< A short and informal description of the class. - std::function get_build_info; ///< A function which returns a string with build information for the class (e.g. version information). + std::string short_description; ///< A short and informal description of the class. + + /// A function which returns a string with build information for the class (e.g. version information). Note + /// that this field may be null, in which case no information is available. + std::function get_build_info; + + /// A function which returns a class-specific property about the class. This is e.g. intended for + /// providing information about build-time options for a specific class. Currently, it is used for + /// the libcurl-based stream-class to provide information about the build-time configured paths for + /// the CA certificates. + /// Note that this field may be null, in which case no information is available. + std::function get_property; }; /// Gets information about a stream class available in the factory. The function returns false if the index is out of range. @@ -300,5 +314,15 @@ namespace libCZI /// /// \returns A new instance of a streams-objects for reading the specified file from the file-system. static std::shared_ptr CreateDefaultStreamForFile(const wchar_t* filename); + + /// A static string for the property_name for the get_property-function of the StreamClassInfo identifying the + /// build-time configured file holding one or more certificates to verify the peer with. C.f. https://curl.se/libcurl/c/curl_version_info.html, this + /// property gives the value of the "cainfo"-field. If it is null, then an invalid property is returned. + static const char* kStreamClassInfoProperty_CurlHttp_CaInfo; + + /// A static string for the property_name for the get_property-function of the StreamClassInfo identifying the + /// build-time configured directory holding CA certificates. C.f. https://curl.se/libcurl/c/curl_version_info.html, this + /// property gives the value of the "capath"-field. If it is null, then an invalid property is returned. + static const char* kStreamClassInfoProperty_CurlHttp_CaPath; }; } diff --git a/Src/libCZI_UnitTests/test_metadatareading.cpp b/Src/libCZI_UnitTests/test_metadatareading.cpp index 2e50a024..9bc520c1 100644 --- a/Src/libCZI_UnitTests/test_metadatareading.cpp +++ b/Src/libCZI_UnitTests/test_metadatareading.cpp @@ -54,8 +54,8 @@ TEST(MetadataReading, ScalingInfoExTest) EXPECT_DOUBLE_EQ(scalingInfo.scaleY, 1.6432520108980473e-07); EXPECT_FALSE(scalingInfo.IsScaleZValid()); - EXPECT_TRUE(scalingInfo.defaultUnitFormatX == L"um"); - EXPECT_TRUE(scalingInfo.defaultUnitFormatY == L"um"); + EXPECT_STREQ(scalingInfo.defaultUnitFormatX.c_str(), L"um"); + EXPECT_STREQ(scalingInfo.defaultUnitFormatY.c_str(), L"um"); EXPECT_TRUE(scalingInfo.defaultUnitFormatZ.empty()); } @@ -274,8 +274,8 @@ static void EnumAllRecursively(IXmlNodeRead* node, std::function n)->bool { names.push_back(utf8_conv.to_bytes(n->Name())); - return true; + return true; }); auto cnt = md.use_count(); @@ -317,15 +317,15 @@ TEST(MetadataReading, WalkChildrenTest2) { string s(utf8_conv.to_bytes(n->Name())); - n->EnumAttributes( - [&](const std::wstring& attribName, const std::wstring& attribValue)->bool - { - s += ":" + utf8_conv.to_bytes(attribName) + "=" + utf8_conv.to_bytes(attribValue); - return true; - }); + n->EnumAttributes( + [&](const std::wstring& attribName, const std::wstring& attribValue)->bool + { + s += ":" + utf8_conv.to_bytes(attribName) + "=" + utf8_conv.to_bytes(attribValue); + return true; + }); - namesAndAttributes.push_back(s); - return true; + namesAndAttributes.push_back(s); + return true; }); auto cnt = md.use_count(); @@ -350,14 +350,14 @@ TEST(MetadataReading, WalkChildrenTest3) [&](std::shared_ptr n)->bool { string s(utf8_conv.to_bytes(n->Name())); - wstring value; - if (n->TryGetValue(&value)) - { - s += " -> " + utf8_conv.to_bytes(value); - } + wstring value; + if (n->TryGetValue(&value)) + { + s += " -> " + utf8_conv.to_bytes(value); + } - namesAndValue.push_back(s); - return true; + namesAndValue.push_back(s); + return true; }); auto cnt = md.use_count(); diff --git a/Src/libCZI_UnitTests/test_reader.cpp b/Src/libCZI_UnitTests/test_reader.cpp index 074214d0..cde5a746 100644 --- a/Src/libCZI_UnitTests/test_reader.cpp +++ b/Src/libCZI_UnitTests/test_reader.cpp @@ -10,6 +10,25 @@ using namespace std; TEST(DimCoordinate, ReaderException) { + class MyException : public std::exception + { + private: + std::string exception_text_; + std::error_code code_; + public: + MyException(const std::string& exceptionText, std::error_code code) :exception_text_(exceptionText), code_(code) {} + + const char* what() const noexcept override + { + return this->exception_text_.c_str(); + } + + std::error_code code() const noexcept + { + return this->code_; + } + }; + class CTestStreamImp :public libCZI::IStream { private: @@ -20,7 +39,7 @@ TEST(DimCoordinate, ReaderException) virtual void Read(std::uint64_t offset, void* pv, std::uint64_t size, std::uint64_t* ptrBytesRead) override { - throw std::ios_base::failure(this->exceptionText, this->code); + throw MyException(this->exceptionText, this->code); } }; @@ -39,7 +58,7 @@ TEST(DimCoordinate, ReaderException) { excp.rethrow_nested(); } - catch (std::ios_base::failure& innerExcp) + catch (MyException& innerExcp) { // according to standard, the content of the what()-test is implementation-specific, // so it is not suited for checking - but it seems that the code goes unaltered diff --git a/Src/libCZI_UnitTests/test_streamslib.cpp b/Src/libCZI_UnitTests/test_streamslib.cpp index 22e4d475..b9b68787 100644 --- a/Src/libCZI_UnitTests/test_streamslib.cpp +++ b/Src/libCZI_UnitTests/test_streamslib.cpp @@ -65,3 +65,29 @@ TEST(StreamsLib, TestGetBuildInfoAndCheckThatStringIsNonEmptyIfAvailable) } } } + +TEST(StreamsLib, TestGetProperty) +{ + // for the classes that have a get_property function, we call into this function + const int number_of_classes = StreamsFactory::GetStreamClassesCount(); + + StreamsFactory::StreamClassInfo info; + for (int i = 0; i < number_of_classes; ++i) + { + const bool b = StreamsFactory::GetStreamInfoForClass(i, info); + ASSERT_TRUE(b); + + if (info.get_property) + { + auto property = info.get_property(StreamsFactory::kStreamClassInfoProperty_CurlHttp_CaInfo); + + // the result should be either invalid or a string + EXPECT_TRUE(property.GetType() == StreamsFactory::Property::Type::Invalid || property.GetType() == StreamsFactory::Property::Type::String); + + property = info.get_property(StreamsFactory::kStreamClassInfoProperty_CurlHttp_CaPath); + + // the result should be either invalid or a string + EXPECT_TRUE(property.GetType() == StreamsFactory::Property::Type::Invalid || property.GetType() == StreamsFactory::Property::Type::String); + } + } +}