Mercurial > hg > orthanc
changeset 3398:4acd1431e603
new classes: StringMatcher and MultipartStreamReader
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 07 Jun 2019 13:36:43 +0200 |
parents | 9019279dbfd7 |
children | 4e8205871967 |
files | Core/HttpServer/MultipartStreamReader.cpp Core/HttpServer/MultipartStreamReader.h Core/HttpServer/StringMatcher.cpp Core/HttpServer/StringMatcher.h Resources/CMake/OrthancFrameworkConfiguration.cmake UnitTestsSources/RestApiTests.cpp |
diffstat | 6 files changed, 921 insertions(+), 7 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/MultipartStreamReader.cpp Fri Jun 07 13:36:43 2019 +0200 @@ -0,0 +1,357 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeaders.h" +#include "MultipartStreamReader.h" + +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include <boost/algorithm/string/predicate.hpp> + +namespace Orthanc +{ + static void ParseHeaders(MultipartStreamReader::HttpHeaders& headers, + StringMatcher::Iterator start, + StringMatcher::Iterator end) + { + std::string tmp(start, end); + + std::vector<std::string> lines; + Toolbox::TokenizeString(lines, tmp, '\n'); + + headers.clear(); + + for (size_t i = 0; i < lines.size(); i++) + { + size_t separator = lines[i].find(':'); + if (separator != std::string::npos) + { + std::string key = Toolbox::StripSpaces(lines[i].substr(0, separator)); + std::string value = Toolbox::StripSpaces(lines[i].substr(separator + 1)); + + Toolbox::ToLowerCase(key); + headers[key] = value; + } + } + } + + + static bool LookupHeaderSizeValue(size_t& target, + const MultipartStreamReader::HttpHeaders& headers, + const std::string& key) + { + MultipartStreamReader::HttpHeaders::const_iterator it = headers.find(key); + if (it == headers.end()) + { + return false; + } + else + { + int64_t value; + + try + { + value = boost::lexical_cast<int64_t>(it->second); + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (value < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + target = static_cast<size_t>(value); + return true; + } + } + } + + + void MultipartStreamReader::ParseStream() + { + if (handler_ == NULL || + state_ == State_Done) + { + return; + } + + std::string corpus; + buffer_.Flatten(corpus); + + StringMatcher::Iterator current = corpus.begin(); + StringMatcher::Iterator corpusEnd = corpus.end(); + + if (state_ == State_UnusedArea) + { + /** + * "Before the first boundary is an area that is ignored by + * MIME-compliant clients. This area is generally used to put + * a message to users of old non-MIME clients." + * https://en.wikipedia.org/wiki/MIME#Multipart_messages + **/ + + if (boundaryMatcher_.Apply(current, corpusEnd)) + { + current = boundaryMatcher_.GetMatchBegin(); + state_ = State_Content; + } + else + { + // We have not seen the end of the unused area yet + std::string reminder(current, corpusEnd); + buffer_.AddChunkDestructive(reminder); + return; + } + } + + for (;;) + { + size_t patternSize = boundaryMatcher_.GetPattern().size(); + size_t remainingSize = std::distance(current, corpusEnd); + if (remainingSize < patternSize + 2) + { + break; // Not enough data available + } + + std::string boundary(current, current + patternSize + 2); + if (boundary == boundaryMatcher_.GetPattern() + "--") + { + state_ = State_Done; + return; + } + + if (boundary != boundaryMatcher_.GetPattern() + "\r\n") + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Garbage between two items in a multipart stream"); + } + + StringMatcher::Iterator start = current + patternSize + 2; + + if (!headersMatcher_.Apply(start, corpusEnd)) + { + break; // Not enough data available + } + + HttpHeaders headers; + ParseHeaders(headers, start, headersMatcher_.GetMatchBegin()); + + size_t contentLength; + if (!LookupHeaderSizeValue(contentLength, headers, "content-length")) + { + if (boundaryMatcher_.Apply(headersMatcher_.GetMatchEnd(), corpusEnd)) + { + size_t d = std::distance(headersMatcher_.GetMatchEnd(), boundaryMatcher_.GetMatchBegin()); + if (d <= 1) + { + throw OrthancException(ErrorCode_NetworkProtocol); + } + else + { + contentLength = d - 2; + } + } + else + { + break; // Not enough data available to have a full part + } + } + + if (headersMatcher_.GetMatchEnd() + contentLength + 2 > corpusEnd) + { + break; // Not enough data available to have a full part + } + + const char* p = headersMatcher_.GetPointerEnd() + contentLength; + if (p[0] != '\r' || + p[1] != '\n') + { + throw OrthancException(ErrorCode_NetworkProtocol, + "No endline at the end of a part"); + } + + handler_->Apply(headers, headersMatcher_.GetPointerEnd(), contentLength); + current = headersMatcher_.GetMatchEnd() + contentLength + 2; + } + + if (current != corpusEnd) + { + std::string reminder(current, corpusEnd); + buffer_.AddChunkDestructive(reminder); + } + } + + + MultipartStreamReader::MultipartStreamReader(const std::string& boundary) : + state_(State_UnusedArea), + handler_(NULL), + headersMatcher_("\r\n\r\n"), + boundaryMatcher_("--" + boundary), + blockSize_(10 * 1024 * 1024) + { + } + + + void MultipartStreamReader::SetBlockSize(size_t size) + { + if (size == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + blockSize_ = size; + } + } + + + void MultipartStreamReader::AddChunk(const void* chunk, + size_t size) + { + if (state_ != State_Done && + size != 0) + { + size_t oldSize = buffer_.GetNumBytes(); + + buffer_.AddChunk(chunk, size); + + if (oldSize / blockSize_ != buffer_.GetNumBytes() / blockSize_) + { + ParseStream(); + } + } + } + + + void MultipartStreamReader::AddChunk(const std::string& chunk) + { + if (!chunk.empty()) + { + AddChunk(chunk.c_str(), chunk.size()); + } + } + + + void MultipartStreamReader::CloseStream() + { + if (buffer_.GetNumBytes() != 0) + { + ParseStream(); + } + } + + + bool MultipartStreamReader::GetMainContentType(std::string& contentType, + const HttpHeaders& headers) + { + HttpHeaders::const_iterator it = headers.find("content-type"); + + if (it == headers.end()) + { + return false; + } + else + { + contentType = it->second; + return true; + } + } + + + bool MultipartStreamReader::ParseMultipartHeaders(std::string& contentType, + std::string& subType, + std::string& boundary, + const HttpHeaders& headers) + { + std::string tmp; + if (!GetMainContentType(tmp, headers)) + { + return false; + } + + std::vector<std::string> tokens; + Orthanc::Toolbox::TokenizeString(tokens, tmp, ';'); + + if (tokens.empty()) + { + return false; + } + + contentType = Orthanc::Toolbox::StripSpaces(tokens[0]); + Orthanc::Toolbox::ToLowerCase(contentType); + + if (contentType.empty()) + { + return false; + } + + bool valid = false; + subType.clear(); + + for (size_t i = 0; i < tokens.size(); i++) + { + std::vector<std::string> items; + Orthanc::Toolbox::TokenizeString(items, tokens[i], '='); + + if (items.size() == 2) + { + if (boost::iequals("boundary", Orthanc::Toolbox::StripSpaces(items[0]))) + { + boundary = Orthanc::Toolbox::StripSpaces(items[1]); + valid = !boundary.empty(); + } + else if (boost::iequals("type", Orthanc::Toolbox::StripSpaces(items[0]))) + { + subType = Orthanc::Toolbox::StripSpaces(items[1]); + Orthanc::Toolbox::ToLowerCase(subType); + + // https://bitbucket.org/sjodogne/orthanc/issues/54/decide-what-to-do-wrt-quoting-of-multipart + // https://tools.ietf.org/html/rfc7231#section-3.1.1.1 + if (subType.size() >= 2 && + subType[0] == '"' && + subType[subType.size() - 1] == '"') + { + subType = subType.substr(1, subType.size() - 2); + } + } + } + } + + return valid; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/MultipartStreamReader.h Fri Jun 07 13:36:43 2019 +0200 @@ -0,0 +1,107 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "StringMatcher.h" +#include "../ChunkedBuffer.h" + +#include <map> + +namespace Orthanc +{ + class MultipartStreamReader : public boost::noncopyable + { + public: + typedef std::map<std::string, std::string> HttpHeaders; + + class IHandler : public boost::noncopyable + { + public: + virtual ~IHandler() + { + } + + virtual void Apply(const HttpHeaders& headers, + const void* part, + size_t size) = 0; + }; + + private: + enum State + { + State_UnusedArea, + State_Content, + State_Done + }; + + State state_; + IHandler* handler_; + StringMatcher headersMatcher_; + StringMatcher boundaryMatcher_; + ChunkedBuffer buffer_; + size_t blockSize_; + + void ParseStream(); + + public: + MultipartStreamReader(const std::string& boundary); + + void SetBlockSize(size_t size); + + size_t GetBlockSize() const + { + return blockSize_; + } + + void SetHandler(IHandler& handler) + { + handler_ = &handler; + } + + void AddChunk(const void* chunk, + size_t size); + + void AddChunk(const std::string& chunk); + + void CloseStream(); + + static bool GetMainContentType(std::string& contentType, + const HttpHeaders& headers); + + static bool ParseMultipartHeaders(std::string& contentType, + std::string& subType, // Possibly empty + std::string& boundary, + const HttpHeaders& headers); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/StringMatcher.cpp Fri Jun 07 13:36:43 2019 +0200 @@ -0,0 +1,136 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeaders.h" +#include "StringMatcher.h" + +#include "../OrthancException.h" + +#include <boost/algorithm/searching/boyer_moore.hpp> +//#include <boost/algorithm/searching/boyer_moore_horspool.hpp> +//#include <boost/algorithm/searching/knuth_morris_pratt.hpp> + +namespace Orthanc +{ + class StringMatcher::Search + { + private: + typedef boost::algorithm::boyer_moore<Iterator> Algorithm; + //typedef boost::algorithm::boyer_moore_horspool<std::string::const_iterator> Algorithm; + + Algorithm algorithm_; + + public: + Search(const std::string& pattern) : + algorithm_(pattern.begin(), pattern.end()) + { + } + + Iterator Apply(Iterator start, + Iterator end) const + { +#if BOOST_VERSION >= 106200 + return algorithm_(start, end).first; +#else + return algorithm_(start, end); +#endif + } + }; + + + StringMatcher::StringMatcher(const std::string& pattern) : + search_(new Search(pattern)), + pattern_(pattern), + valid_(false) + { + } + + + bool StringMatcher::Apply(Iterator start, + Iterator end) + { + assert(search_.get() != NULL); + matchBegin_ = search_->Apply(start, end); + + if (matchBegin_ == end) + { + valid_ = false; + } + else + { + matchEnd_ = matchBegin_ + pattern_.size(); + assert(matchEnd_ <= end); + valid_ = true; + } + + return valid_; + } + + + StringMatcher::Iterator StringMatcher::GetMatchBegin() const + { + if (valid_) + { + return matchBegin_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + StringMatcher::Iterator StringMatcher::GetMatchEnd() const + { + if (valid_) + { + return matchEnd_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const char* StringMatcher::GetPointerBegin() const + { + return &GetMatchBegin()[0]; + } + + + const char* StringMatcher::GetPointerEnd() const + { + return &GetMatchEnd()[0]; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/StringMatcher.h Fri Jun 07 13:36:43 2019 +0200 @@ -0,0 +1,88 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <boost/noncopyable.hpp> +#include <boost/shared_ptr.hpp> +#include <string> + +namespace Orthanc +{ + // Convenience class that wraps a Boost algorithm for string matching + class StringMatcher : public boost::noncopyable + { + public: + typedef std::string::const_iterator Iterator; + + private: + class Search; + + // WARNING - The lifetime of "pattern_" must be larger than + // "search_", as the latter references "pattern_" + boost::shared_ptr<Search> search_; // PImpl pattern + std::string pattern_; + bool valid_; + Iterator matchBegin_; + Iterator matchEnd_; + + public: + StringMatcher(const std::string& pattern); + + const std::string& GetPattern() const + { + return pattern_; + } + + bool IsValid() const + { + return valid_; + } + + bool Apply(Iterator start, + Iterator end); + + bool Apply(const std::string& corpus) + { + return Apply(corpus.begin(), corpus.end()); + } + + Iterator GetMatchBegin() const; + + Iterator GetMatchEnd() const; + + const char* GetPointerBegin() const; + + const char* GetPointerEnd() const; + }; +}
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri Jun 07 11:26:34 2019 +0200 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri Jun 07 13:36:43 2019 +0200 @@ -128,6 +128,8 @@ ${ORTHANC_ROOT}/Core/EnumerationDictionary.h ${ORTHANC_ROOT}/Core/Enumerations.cpp ${ORTHANC_ROOT}/Core/FileStorage/MemoryStorageArea.cpp + ${ORTHANC_ROOT}/Core/HttpServer/MultipartStreamReader.cpp + ${ORTHANC_ROOT}/Core/HttpServer/StringMatcher.cpp ${ORTHANC_ROOT}/Core/Logging.cpp ${ORTHANC_ROOT}/Core/SerializationToolbox.cpp ${ORTHANC_ROOT}/Core/Toolbox.cpp
--- a/UnitTestsSources/RestApiTests.cpp Fri Jun 07 11:26:34 2019 +0200 +++ b/UnitTestsSources/RestApiTests.cpp Fri Jun 07 13:36:43 2019 +0200 @@ -47,6 +47,8 @@ #include "../Core/Compression/ZlibCompressor.h" #include "../Core/RestApi/RestApiHierarchy.h" #include "../Core/HttpServer/HttpContentNegociation.h" +#include "../Core/HttpServer/MultipartStreamReader.h" + using namespace Orthanc; @@ -353,7 +355,7 @@ namespace { - class AcceptHandler : public Orthanc::HttpContentNegociation::IHandler + class AcceptHandler : public HttpContentNegociation::IHandler { private: std::string type_; @@ -397,7 +399,7 @@ AcceptHandler h; { - Orthanc::HttpContentNegociation d; + HttpContentNegociation d; d.Register("audio/mp3", h); d.Register("audio/basic", h); @@ -422,7 +424,7 @@ const std::string T1 = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"; { - Orthanc::HttpContentNegociation d; + HttpContentNegociation d; d.Register("text/plain", h); d.Register("text/html", h); d.Register("text/x-dvi", h); @@ -432,7 +434,7 @@ } { - Orthanc::HttpContentNegociation d; + HttpContentNegociation d; d.Register("text/plain", h); d.Register("text/x-dvi", h); d.Register("text/x-c", h); @@ -442,7 +444,7 @@ } { - Orthanc::HttpContentNegociation d; + HttpContentNegociation d; d.Register("text/plain", h); d.Register("text/x-dvi", h); d.Register("text/x-c", h); @@ -453,7 +455,7 @@ } { - Orthanc::HttpContentNegociation d; + HttpContentNegociation d; d.Register("text/plain", h); d.Register("text/x-dvi", h); ASSERT_TRUE(d.Apply(T1)); @@ -462,7 +464,7 @@ } { - Orthanc::HttpContentNegociation d; + HttpContentNegociation d; d.Register("text/plain", h); ASSERT_TRUE(d.Apply(T1)); ASSERT_EQ("text", h.GetType()); @@ -643,3 +645,225 @@ ASSERT_FALSE(p.LookupUserProperty(s, "hello")); } } + + +TEST(StringMatcher, Basic) +{ + StringMatcher matcher("---"); + + ASSERT_THROW(matcher.GetMatchBegin(), OrthancException); + + { + const std::string s = ""; + ASSERT_FALSE(matcher.Apply(s)); + } + + { + const std::string s = "abc----def"; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(3, std::distance(s.begin(), matcher.GetMatchBegin())); + ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd())); + } + + { + const std::string s = "abc---"; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(3, std::distance(s.begin(), matcher.GetMatchBegin())); + ASSERT_EQ(s.end(), matcher.GetMatchEnd()); + ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd())); + ASSERT_EQ("", std::string(matcher.GetMatchEnd(), s.end())); + } + + { + const std::string s = "abc--def"; + ASSERT_FALSE(matcher.Apply(s)); + ASSERT_THROW(matcher.GetMatchBegin(), OrthancException); + ASSERT_THROW(matcher.GetMatchEnd(), OrthancException); + } + + { + std::string s(10u, '\0'); // String with null values + ASSERT_EQ(10u, s.size()); + ASSERT_EQ(10u, s.size()); + ASSERT_FALSE(matcher.Apply(s)); + + s[9] = '-'; + ASSERT_FALSE(matcher.Apply(s)); + + s[8] = '-'; + ASSERT_FALSE(matcher.Apply(s)); + + s[7] = '-'; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(s.c_str() + 7, matcher.GetPointerBegin()); + ASSERT_EQ(s.c_str() + 10, matcher.GetPointerEnd()); + ASSERT_EQ(s.end() - 3, matcher.GetMatchBegin()); + ASSERT_EQ(s.end(), matcher.GetMatchEnd()); + } +} + + + +class MultipartTester : public MultipartStreamReader::IHandler +{ +private: + struct Part + { + MultipartStreamReader::HttpHeaders headers_; + std::string data_; + + Part(const MultipartStreamReader::HttpHeaders& headers, + const void* part, + size_t size) : + headers_(headers), + data_(reinterpret_cast<const char*>(part), size) + { + } + }; + + std::vector<Part> parts_; + +public: + virtual void Apply(const MultipartStreamReader::HttpHeaders& headers, + const void* part, + size_t size) + { + parts_.push_back(Part(headers, part, size)); + } + + unsigned int GetCount() const + { + return parts_.size(); + } + + MultipartStreamReader::HttpHeaders& GetHeaders(size_t i) + { + return parts_[i].headers_; + } + + const std::string& GetData(size_t i) const + { + return parts_[i].data_; + } +}; + + +TEST(MultipartStreamReader, ParseHeaders) +{ + std::string ct, b, st; + + { + MultipartStreamReader::HttpHeaders h; + h["hello"] = "world"; + h["Content-Type"] = "world"; // Should be in lower-case + h["CONTENT-type"] = "world"; // Should be in lower-case + ASSERT_FALSE(MultipartStreamReader::GetMainContentType(ct, h)); + ASSERT_FALSE(MultipartStreamReader::ParseMultipartHeaders(ct, st, b, h)); + } + + { + MultipartStreamReader::HttpHeaders h; + h["content-type"] = "world"; + ASSERT_TRUE(MultipartStreamReader::GetMainContentType(ct, h)); + ASSERT_EQ(ct, "world"); + ASSERT_FALSE(MultipartStreamReader::ParseMultipartHeaders(ct, st, b, h)); + } + + { + MultipartStreamReader::HttpHeaders h; + h["content-type"] = "multipart/related; dummy=value; boundary=1234; hello=world"; + ASSERT_TRUE(MultipartStreamReader::GetMainContentType(ct, h)); + ASSERT_EQ(ct, h["content-type"]); + ASSERT_TRUE(MultipartStreamReader::ParseMultipartHeaders(ct, st, b, h)); + ASSERT_EQ(ct, "multipart/related"); + ASSERT_EQ(b, "1234"); + ASSERT_TRUE(st.empty()); + } + + { + MultipartStreamReader::HttpHeaders h; + h["content-type"] = "multipart/related; boundary="; + ASSERT_TRUE(MultipartStreamReader::GetMainContentType(ct, h)); + ASSERT_EQ(ct, h["content-type"]); + ASSERT_FALSE(MultipartStreamReader::ParseMultipartHeaders(ct, st, b, h)); // Empty boundary + } + + { + MultipartStreamReader::HttpHeaders h; + h["content-type"] = "Multipart/Related; TYPE=Application/Dicom; Boundary=heLLO"; + ASSERT_TRUE(MultipartStreamReader::ParseMultipartHeaders(ct, st, b, h)); + ASSERT_EQ(ct, "multipart/related"); + ASSERT_EQ(b, "heLLO"); + ASSERT_EQ(st, "application/dicom"); + } + + { + MultipartStreamReader::HttpHeaders h; + h["content-type"] = "Multipart/Related; type=\"application/DICOM\"; Boundary=a"; + ASSERT_TRUE(MultipartStreamReader::ParseMultipartHeaders(ct, st, b, h)); + ASSERT_EQ(ct, "multipart/related"); + ASSERT_EQ(b, "a"); + ASSERT_EQ(st, "application/dicom"); + } +} + + +TEST(MultipartStreamReader, BytePerByte) +{ + std::string stream = "GARBAGE"; + + std::string boundary = "123456789123456789"; + + { + for (size_t i = 0; i < 10; i++) + { + std::string f = "hello " + boost::lexical_cast<std::string>(i); + + stream += "\r\n--" + boundary + "\r\n"; + if (i % 2 == 0) + stream += "Content-Length: " + boost::lexical_cast<std::string>(f.size()) + "\r\n"; + stream += "Content-Type: toto " + boost::lexical_cast<std::string>(i) + "\r\n\r\n"; + stream += f; + } + + stream += "\r\n--" + boundary + "--"; + stream += "GARBAGE"; + } + + for (unsigned int k = 0; k < 2; k++) + { + MultipartTester decoded; + + MultipartStreamReader reader(boundary); + reader.SetBlockSize(1); + reader.SetHandler(decoded); + + if (k == 0) + { + for (size_t i = 0; i < stream.size(); i++) + { + reader.AddChunk(&stream[i], 1); + } + } + else + { + reader.AddChunk(stream); + } + + reader.CloseStream(); + + ASSERT_EQ(10u, decoded.GetCount()); + + for (size_t i = 0; i < 10; i++) + { + ASSERT_EQ("hello " + boost::lexical_cast<std::string>(i), decoded.GetData(i)); + ASSERT_EQ("toto " + boost::lexical_cast<std::string>(i), decoded.GetHeaders(i)["content-type"]); + + if (i % 2 == 0) + { + ASSERT_EQ(2u, decoded.GetHeaders(i).size()); + ASSERT_TRUE(decoded.GetHeaders(i).find("content-length") != decoded.GetHeaders(i).end()); + } + } + } +}