diff OrthancFramework/Sources/Toolbox.cpp @ 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents Core/Toolbox.cpp@058b5ade8acd
children 9214e3a7b0a2
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Toolbox.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2257 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 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 "Toolbox.h"
+
+#include "Compatibility.h"
+#include "OrthancException.h"
+#include "Logging.h"
+
+#include <boost/algorithm/string/case_conv.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/regex.hpp>
+
+#if BOOST_VERSION >= 106600
+#  include <boost/uuid/detail/sha1.hpp>
+#else
+#  include <boost/uuid/sha1.hpp>
+#endif
+ 
+#include <string>
+#include <stdint.h>
+#include <string.h>
+#include <algorithm>
+#include <ctype.h>
+
+
+#if ORTHANC_ENABLE_MD5 == 1
+// TODO - Could be replaced by <boost/uuid/detail/md5.hpp> starting
+// with Boost >= 1.66.0
+#  include "../Resources/ThirdParty/md5/md5.h"
+#endif
+
+#if ORTHANC_ENABLE_BASE64 == 1
+#  include "../Resources/ThirdParty/base64/base64.h"
+#endif
+
+#if ORTHANC_ENABLE_LOCALE == 1
+#  include <boost/locale.hpp>
+#endif
+
+#if ORTHANC_ENABLE_SSL == 1
+// For OpenSSL initialization and finalization
+#  include <openssl/conf.h>
+#  include <openssl/engine.h>
+#  include <openssl/err.h>
+#  include <openssl/evp.h>
+#  include <openssl/ssl.h>
+#endif
+
+
+#if defined(_MSC_VER) && (_MSC_VER < 1800)
+// Patch for the missing "_strtoll" symbol when compiling with Visual Studio < 2013
+extern "C"
+{
+  int64_t _strtoi64(const char *nptr, char **endptr, int base);
+  int64_t strtoll(const char *nptr, char **endptr, int base)
+  {
+    return _strtoi64(nptr, endptr, base);
+  } 
+}
+#endif
+
+
+#if defined(_WIN32)
+#  include <windows.h>   // For ::Sleep
+#endif
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include "ChunkedBuffer.h"
+#endif
+
+
+// Inclusions for UUID
+// http://stackoverflow.com/a/1626302
+
+extern "C"
+{
+#if defined(_WIN32)
+#  include <rpc.h>
+#else
+#  include <uuid/uuid.h>
+#endif
+}
+
+
+#if defined(ORTHANC_STATIC_ICU)
+#  if (ORTHANC_STATIC_ICU == 1 && ORTHANC_ENABLE_LOCALE == 1)
+#    include <OrthancFrameworkResources.h>
+#    include <unicode/udata.h>
+#    include <unicode/uloc.h>
+#    include "Compression/GzipCompressor.h"
+
+static std::string  globalIcuData_;
+
+extern "C"
+{
+  // This is dummy content for the "icudt58_dat" (resp. "icudt63_dat")
+  // global variable from the autogenerated "icudt58l_dat.c"
+  // (resp. "icudt63l_dat.c") file that contains a huge C array. In
+  // Orthanc, this array is compressed using gzip and attached as a
+  // resource, then uncompressed during the launch of Orthanc by
+  // static function "InitializeIcu()".
+  struct
+  {
+    double bogus;
+    uint8_t *bytes;
+  } U_ICUDATA_ENTRY_POINT = { 0.0, NULL };
+}
+
+#    if defined(__LSB_VERSION__)
+extern "C"
+{
+  /**
+   * The "tzname" global variable is declared as "extern" but is not
+   * defined in any compilation module, if using Linux Standard Base,
+   * as soon as OpenSSL or cURL is in use on Ubuntu >= 18.04 (glibc >=
+   * 2.27). The variable "__tzname" is always properly declared *and*
+   * defined. The reason is unclear, and is maybe a bug in the gcc 4.8
+   * linker that is used by LSB if facing a weak symbol (as "tzname").
+   * This makes Orthanc crash if the timezone is set to UTC.
+   * https://groups.google.com/d/msg/orthanc-users/0m8sxxwSm1E/2p8du_89CAAJ
+   **/
+  char *tzname[2] = { (char *) "GMT", (char *) "GMT" };
+}
+#    endif
+
+#  endif
+#endif
+ 
+
+
+#if defined(__unix__) && ORTHANC_SANDBOXED != 1
+#  include "SystemToolbox.h"  // Check out "InitializeGlobalLocale()"
+#endif
+
+
+
+namespace Orthanc
+{
+  void Toolbox::LinesIterator::FindEndOfLine()
+  {
+    lineEnd_ = lineStart_;
+
+    while (lineEnd_ < content_.size() &&
+           content_[lineEnd_] != '\n' &&
+           content_[lineEnd_] != '\r')
+    {
+      lineEnd_ += 1;
+    }
+  }
+  
+
+  Toolbox::LinesIterator::LinesIterator(const std::string& content) :
+    content_(content),
+    lineStart_(0)
+  {
+    FindEndOfLine();
+  }
+
+    
+  bool Toolbox::LinesIterator::GetLine(std::string& target) const
+  {
+    assert(lineStart_ <= content_.size() &&
+           lineEnd_ <= content_.size() &&
+           lineStart_ <= lineEnd_);
+
+    if (lineStart_ == content_.size())
+    {
+      return false;
+    }
+    else
+    {
+      target = content_.substr(lineStart_, lineEnd_ - lineStart_);
+      return true;
+    }
+  }
+
+    
+  void Toolbox::LinesIterator::Next()
+  {
+    lineStart_ = lineEnd_;
+
+    if (lineStart_ != content_.size())
+    {
+      assert(content_[lineStart_] == '\r' ||
+             content_[lineStart_] == '\n');
+
+      char second;
+      
+      if (content_[lineStart_] == '\r')
+      {
+        second = '\n';
+      }
+      else
+      {
+        second = '\r';
+      }
+        
+      lineStart_ += 1;
+
+      if (lineStart_ < content_.size() &&
+          content_[lineStart_] == second)
+      {
+        lineStart_ += 1;
+      }
+
+      FindEndOfLine();
+    }
+  }
+
+  
+  void Toolbox::ToUpperCase(std::string& s)
+  {
+    std::transform(s.begin(), s.end(), s.begin(), toupper);
+  }
+
+
+  void Toolbox::ToLowerCase(std::string& s)
+  {
+    std::transform(s.begin(), s.end(), s.begin(), tolower);
+  }
+
+
+  void Toolbox::ToUpperCase(std::string& result,
+                            const std::string& source)
+  {
+    result = source;
+    ToUpperCase(result);
+  }
+
+  void Toolbox::ToLowerCase(std::string& result,
+                            const std::string& source)
+  {
+    result = source;
+    ToLowerCase(result);
+  }
+
+
+  void Toolbox::SplitUriComponents(UriComponents& components,
+                                   const std::string& uri)
+  {
+    static const char URI_SEPARATOR = '/';
+
+    components.clear();
+
+    if (uri.size() == 0 ||
+        uri[0] != URI_SEPARATOR)
+    {
+      throw OrthancException(ErrorCode_UriSyntax);
+    }
+
+    // Count the number of slashes in the URI to make an assumption
+    // about the number of components in the URI
+    unsigned int estimatedSize = 0;
+    for (unsigned int i = 0; i < uri.size(); i++)
+    {
+      if (uri[i] == URI_SEPARATOR)
+        estimatedSize++;
+    }
+
+    components.reserve(estimatedSize - 1);
+
+    unsigned int start = 1;
+    unsigned int end = 1;
+    while (end < uri.size())
+    {
+      // This is the loop invariant
+      assert(uri[start - 1] == '/' && (end >= start));
+
+      if (uri[end] == '/')
+      {
+        components.push_back(std::string(&uri[start], end - start));
+        end++;
+        start = end;
+      }
+      else
+      {
+        end++;
+      }
+    }
+
+    if (start < uri.size())
+    {
+      components.push_back(std::string(&uri[start], end - start));
+    }
+
+    for (size_t i = 0; i < components.size(); i++)
+    {
+      if (components[i].size() == 0)
+      {
+        // Empty component, as in: "/coucou//e"
+        throw OrthancException(ErrorCode_UriSyntax);
+      }
+    }
+  }
+
+
+  void Toolbox::TruncateUri(UriComponents& target,
+                            const UriComponents& source,
+                            size_t fromLevel)
+  {
+    target.clear();
+
+    if (source.size() > fromLevel)
+    {
+      target.resize(source.size() - fromLevel);
+
+      size_t j = 0;
+      for (size_t i = fromLevel; i < source.size(); i++, j++)
+      {
+        target[j] = source[i];
+      }
+
+      assert(j == target.size());
+    }
+  }
+  
+
+
+  bool Toolbox::IsChildUri(const UriComponents& baseUri,
+                           const UriComponents& testedUri)
+  {
+    if (testedUri.size() < baseUri.size())
+    {
+      return false;
+    }
+
+    for (size_t i = 0; i < baseUri.size(); i++)
+    {
+      if (baseUri[i] != testedUri[i])
+        return false;
+    }
+
+    return true;
+  }
+
+
+  std::string Toolbox::FlattenUri(const UriComponents& components,
+                                  size_t fromLevel)
+  {
+    if (components.size() <= fromLevel)
+    {
+      return "/";
+    }
+    else
+    {
+      std::string r;
+
+      for (size_t i = fromLevel; i < components.size(); i++)
+      {
+        r += "/" + components[i];
+      }
+
+      return r;
+    }
+  }
+
+
+#if ORTHANC_ENABLE_MD5 == 1
+  static char GetHexadecimalCharacter(uint8_t value)
+  {
+    assert(value < 16);
+
+    if (value < 10)
+    {
+      return value + '0';
+    }
+    else
+    {
+      return (value - 10) + 'a';
+    }
+  }
+
+
+  void Toolbox::ComputeMD5(std::string& result,
+                           const std::string& data)
+  {
+    if (data.size() > 0)
+    {
+      ComputeMD5(result, &data[0], data.size());
+    }
+    else
+    {
+      ComputeMD5(result, NULL, 0);
+    }
+  }
+
+
+  void Toolbox::ComputeMD5(std::string& result,
+                           const void* data,
+                           size_t size)
+  {
+    md5_state_s state;
+    md5_init(&state);
+
+    if (size > 0)
+    {
+      md5_append(&state, 
+                 reinterpret_cast<const md5_byte_t*>(data), 
+                 static_cast<int>(size));
+    }
+
+    md5_byte_t actualHash[16];
+    md5_finish(&state, actualHash);
+
+    result.resize(32);
+    for (unsigned int i = 0; i < 16; i++)
+    {
+      result[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16));
+      result[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16));
+    }
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_BASE64 == 1
+  void Toolbox::EncodeBase64(std::string& result, 
+                             const std::string& data)
+  {
+    result.clear();
+    base64_encode(result, data);
+  }
+
+  void Toolbox::DecodeBase64(std::string& result, 
+                             const std::string& data)
+  {
+    for (size_t i = 0; i < data.length(); i++)
+    {
+      if (!isalnum(data[i]) &&
+          data[i] != '+' &&
+          data[i] != '/' &&
+          data[i] != '=')
+      {
+        // This is not a valid character for a Base64 string
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
+    result.clear();
+    base64_decode(result, data);
+  }
+
+
+  bool Toolbox::DecodeDataUriScheme(std::string& mime,
+                                    std::string& content,
+                                    const std::string& source)
+  {
+    boost::regex pattern("data:([^;]+);base64,([a-zA-Z0-9=+/]*)",
+                         boost::regex::icase /* case insensitive search */);
+
+    boost::cmatch what;
+    if (regex_match(source.c_str(), what, pattern))
+    {
+      mime = what[1];
+      DecodeBase64(content, what[2]);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void Toolbox::EncodeDataUriScheme(std::string& result,
+                                    const std::string& mime,
+                                    const std::string& content)
+  {
+    result = "data:" + mime + ";base64,";
+    base64_encode(result, content);
+  }
+
+#endif
+
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  static const char* GetBoostLocaleEncoding(const Encoding sourceEncoding)
+  {
+    switch (sourceEncoding)
+    {
+      case Encoding_Utf8:
+        return "UTF-8";
+
+      case Encoding_Ascii:
+        return "ASCII";
+
+      case Encoding_Latin1:
+        return "ISO-8859-1";
+
+      case Encoding_Latin2:
+        return "ISO-8859-2";
+
+      case Encoding_Latin3:
+        return "ISO-8859-3";
+
+      case Encoding_Latin4:
+        return "ISO-8859-4";
+
+      case Encoding_Latin5:
+        return "ISO-8859-9";
+
+      case Encoding_Cyrillic:
+        return "ISO-8859-5";
+
+      case Encoding_Windows1251:
+        return "WINDOWS-1251";
+
+      case Encoding_Arabic:
+        return "ISO-8859-6";
+
+      case Encoding_Greek:
+        return "ISO-8859-7";
+
+      case Encoding_Hebrew:
+        return "ISO-8859-8";
+        
+      case Encoding_Japanese:
+        return "SHIFT-JIS";
+
+      case Encoding_Chinese:
+        return "GB18030";
+
+      case Encoding_Thai:
+#if BOOST_LOCALE_WITH_ICU == 1
+        return "tis620.2533";
+#else
+        return "TIS620.2533-0";
+#endif
+
+      case Encoding_Korean:
+        return "ISO-IR-149";
+
+      case Encoding_JapaneseKanji:
+        return "JIS";
+
+      case Encoding_SimplifiedChinese:
+        return "GB2312";
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
+  std::string Toolbox::ConvertToUtf8(const std::string& source,
+                                     Encoding sourceEncoding,
+                                     bool hasCodeExtensions)
+  {
+#if ORTHANC_STATIC_ICU == 1
+    if (globalIcuData_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "Call Toolbox::InitializeGlobalLocale()");
+    }
+#endif
+
+    // The "::skip" flag makes boost skip invalid UTF-8
+    // characters. This can occur in badly-encoded DICOM files.
+    
+    try
+    {
+      if (sourceEncoding == Encoding_Ascii)
+      {
+        return ConvertToAscii(source);
+      }
+      else 
+      {
+        std::string s;
+        
+        if (sourceEncoding == Encoding_Utf8)
+        {
+          // Already in UTF-8: No conversion is required, but we ensure
+          // the output is correctly encoded
+          s = boost::locale::conv::utf_to_utf<char>(source, boost::locale::conv::skip);
+        }
+        else
+        {
+          const char* encoding = GetBoostLocaleEncoding(sourceEncoding);
+          s = boost::locale::conv::to_utf<char>(source, encoding, boost::locale::conv::skip);
+        }
+
+        if (hasCodeExtensions)
+        {
+          std::string t;
+          RemoveIso2022EscapeSequences(t, s);
+          return t;
+        }
+        else
+        {
+          return s;
+        }        
+      }
+    }
+    catch (std::runtime_error& e)
+    {
+      // Bad input string or bad encoding
+      LOG(INFO) << e.what();
+      return ConvertToAscii(source);
+    }
+  }
+#endif
+  
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  std::string Toolbox::ConvertFromUtf8(const std::string& source,
+                                       Encoding targetEncoding)
+  {
+#if ORTHANC_STATIC_ICU == 1
+    if (globalIcuData_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "Call Toolbox::InitializeGlobalLocale()");
+    }
+#endif
+
+    // The "::skip" flag makes boost skip invalid UTF-8
+    // characters. This can occur in badly-encoded DICOM files.
+    
+    try
+    {
+      if (targetEncoding == Encoding_Utf8)
+      {
+        // Already in UTF-8: No conversion is required.
+        return boost::locale::conv::utf_to_utf<char>(source, boost::locale::conv::skip);
+      }
+      else if (targetEncoding == Encoding_Ascii)
+      {
+        return ConvertToAscii(source);
+      }
+      else
+      {
+        const char* encoding = GetBoostLocaleEncoding(targetEncoding);
+        return boost::locale::conv::from_utf<char>(source, encoding, boost::locale::conv::skip);
+      }
+    }
+    catch (std::runtime_error&)
+    {
+      // Bad input string or bad encoding
+      return ConvertToAscii(source);
+    }
+  }
+#endif
+
+
+  static bool IsAsciiCharacter(uint8_t c)
+  {
+    return (c != 0 &&
+            c <= 127 &&
+            (c == '\n' || !iscntrl(c)));
+  }
+
+
+  bool Toolbox::IsAsciiString(const void* data,
+                              size_t size)
+  {
+    const uint8_t* p = reinterpret_cast<const uint8_t*>(data);
+
+    for (size_t i = 0; i < size; i++, p++)
+    {
+      if (!IsAsciiCharacter(*p))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  bool Toolbox::IsAsciiString(const std::string& s)
+  {
+    return IsAsciiString(s.c_str(), s.size());
+  }
+  
+
+  std::string Toolbox::ConvertToAscii(const std::string& source)
+  {
+    std::string result;
+
+    result.reserve(source.size() + 1);
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      if (IsAsciiCharacter(source[i]))
+      {
+        result.push_back(source[i]);
+      }
+    }
+
+    return result;
+  }
+
+
+  void Toolbox::ComputeSHA1(std::string& result,
+                            const void* data,
+                            size_t size)
+  {
+    boost::uuids::detail::sha1 sha1;
+
+    if (size > 0)
+    {
+      sha1.process_bytes(data, size);
+    }
+
+    unsigned int digest[5];
+
+    // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide
+    assert(sizeof(unsigned int) == 4 && sizeof(digest) == (160 / 8)); 
+    
+    sha1.get_digest(digest);
+
+    result.resize(8 * 5 + 4);
+    sprintf(&result[0], "%08x-%08x-%08x-%08x-%08x",
+            digest[0],
+            digest[1],
+            digest[2],
+            digest[3],
+            digest[4]);
+  }
+
+  void Toolbox::ComputeSHA1(std::string& result,
+                            const std::string& data)
+  {
+    if (data.size() > 0)
+    {
+      ComputeSHA1(result, data.c_str(), data.size());
+    }
+    else
+    {
+      ComputeSHA1(result, NULL, 0);
+    }
+  }
+
+
+  bool Toolbox::IsSHA1(const void* str,
+                       size_t size)
+  {
+    if (size == 0)
+    {
+      return false;
+    }
+
+    const char* start = reinterpret_cast<const char*>(str);
+    const char* end = start + size;
+
+    // Trim the beginning of the string
+    while (start < end)
+    {
+      if (*start == '\0' ||
+          isspace(*start))
+      {
+        start++;
+      }
+      else
+      {
+        break;
+      }
+    }
+
+    // Trim the trailing of the string
+    while (start < end)
+    {
+      if (*(end - 1) == '\0' ||
+          isspace(*(end - 1)))
+      {
+        end--;
+      }
+      else
+      {
+        break;
+      }
+    }
+
+    if (end - start != 44)
+    {
+      return false;
+    }
+
+    for (unsigned int i = 0; i < 44; i++)
+    {
+      if (i == 8 ||
+          i == 17 ||
+          i == 26 ||
+          i == 35)
+      {
+        if (start[i] != '-')
+          return false;
+      }
+      else
+      {
+        if (!isalnum(start[i]))
+          return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  bool Toolbox::IsSHA1(const std::string& s)
+  {
+    if (s.size() == 0)
+    {
+      return false;
+    }
+    else
+    {
+      return IsSHA1(s.c_str(), s.size());
+    }
+  }
+
+
+  std::string Toolbox::StripSpaces(const std::string& source)
+  {
+    size_t first = 0;
+
+    while (first < source.length() &&
+           isspace(source[first]))
+    {
+      first++;
+    }
+
+    if (first == source.length())
+    {
+      // String containing only spaces
+      return "";
+    }
+
+    size_t last = source.length();
+    while (last > first &&
+           isspace(source[last - 1]))
+    {
+      last--;
+    }          
+    
+    assert(first <= last);
+    return source.substr(first, last - first);
+  }
+
+
+  static char Hex2Dec(char c)
+  {
+    return ((c >= '0' && c <= '9') ? c - '0' :
+            ((c >= 'a' && c <= 'f') ? c - 'a' + 10 : c - 'A' + 10));
+  }
+
+  void Toolbox::UrlDecode(std::string& s)
+  {
+    // http://en.wikipedia.org/wiki/Percent-encoding
+    // http://www.w3schools.com/tags/ref_urlencode.asp
+    // http://stackoverflow.com/questions/154536/encode-decode-urls-in-c
+
+    if (s.size() == 0)
+    {
+      return;
+    }
+
+    size_t source = 0;
+    size_t target = 0;
+
+    while (source < s.size())
+    {
+      if (s[source] == '%' &&
+          source + 2 < s.size() &&
+          isalnum(s[source + 1]) &&
+          isalnum(s[source + 2]))
+      {
+        s[target] = (Hex2Dec(s[source + 1]) << 4) | Hex2Dec(s[source + 2]);
+        source += 3;
+        target += 1;
+      }
+      else
+      {
+        if (s[source] == '+')
+          s[target] = ' ';
+        else
+          s[target] = s[source];
+
+        source++;
+        target++;
+      }
+    }
+
+    s.resize(target);
+  }
+
+
+  Endianness Toolbox::DetectEndianness()
+  {
+    // http://sourceforge.net/p/predef/wiki/Endianness/
+
+    uint32_t bufferView;
+
+    uint8_t* buffer = reinterpret_cast<uint8_t*>(&bufferView);
+
+    buffer[0] = 0x00;
+    buffer[1] = 0x01;
+    buffer[2] = 0x02;
+    buffer[3] = 0x03;
+
+    switch (bufferView) 
+    {
+      case 0x00010203: 
+        return Endianness_Big;
+
+      case 0x03020100: 
+        return Endianness_Little;
+        
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+  std::string Toolbox::WildcardToRegularExpression(const std::string& source)
+  {
+    // TODO - Speed up this with a regular expression
+
+    std::string result = source;
+
+    // Escape all special characters
+    boost::replace_all(result, "\\", "\\\\");
+    boost::replace_all(result, "^", "\\^");
+    boost::replace_all(result, ".", "\\.");
+    boost::replace_all(result, "$", "\\$");
+    boost::replace_all(result, "|", "\\|");
+    boost::replace_all(result, "(", "\\(");
+    boost::replace_all(result, ")", "\\)");
+    boost::replace_all(result, "[", "\\[");
+    boost::replace_all(result, "]", "\\]");
+    boost::replace_all(result, "+", "\\+");
+    boost::replace_all(result, "/", "\\/");
+    boost::replace_all(result, "{", "\\{");
+    boost::replace_all(result, "}", "\\}");
+
+    // Convert wildcards '*' and '?' to their regex equivalents
+    boost::replace_all(result, "?", ".");
+    boost::replace_all(result, "*", ".*");
+
+    return result;
+  }
+
+
+  void Toolbox::TokenizeString(std::vector<std::string>& result,
+                               const std::string& value,
+                               char separator)
+  {
+    size_t countSeparators = 0;
+    
+    for (size_t i = 0; i < value.size(); i++)
+    {
+      if (value[i] == separator)
+      {
+        countSeparators++;
+      }
+    }
+    
+    result.clear();
+    result.reserve(countSeparators + 1);
+
+    std::string currentItem;
+
+    for (size_t i = 0; i < value.size(); i++)
+    {
+      if (value[i] == separator)
+      {
+        result.push_back(currentItem);
+        currentItem.clear();
+      }
+      else
+      {
+        currentItem.push_back(value[i]);
+      }
+    }
+
+    result.push_back(currentItem);
+  }
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  class ChunkedBufferWriter : public pugi::xml_writer
+  {
+  private:
+    ChunkedBuffer buffer_;
+
+  public:
+    virtual void write(const void *data, size_t size)
+    {
+      if (size > 0)
+      {
+        buffer_.AddChunk(reinterpret_cast<const char*>(data), size);
+      }
+    }
+
+    void Flatten(std::string& s)
+    {
+      buffer_.Flatten(s);
+    }
+  };
+
+
+  static void JsonToXmlInternal(pugi::xml_node& target,
+                                const Json::Value& source,
+                                const std::string& arrayElement)
+  {
+    // http://jsoncpp.sourceforge.net/value_8h_source.html#l00030
+
+    switch (source.type())
+    {
+      case Json::nullValue:
+      {
+        target.append_child(pugi::node_pcdata).set_value("null");
+        break;
+      }
+
+      case Json::intValue:
+      {
+        std::string s = boost::lexical_cast<std::string>(source.asInt());
+        target.append_child(pugi::node_pcdata).set_value(s.c_str());
+        break;
+      }
+
+      case Json::uintValue:
+      {
+        std::string s = boost::lexical_cast<std::string>(source.asUInt());
+        target.append_child(pugi::node_pcdata).set_value(s.c_str());
+        break;
+      }
+
+      case Json::realValue:
+      {
+        std::string s = boost::lexical_cast<std::string>(source.asFloat());
+        target.append_child(pugi::node_pcdata).set_value(s.c_str());
+        break;
+      }
+
+      case Json::stringValue:
+      {
+        target.append_child(pugi::node_pcdata).set_value(source.asString().c_str());
+        break;
+      }
+
+      case Json::booleanValue:
+      {
+        target.append_child(pugi::node_pcdata).set_value(source.asBool() ? "true" : "false");
+        break;
+      }
+
+      case Json::arrayValue:
+      {
+        for (Json::Value::ArrayIndex i = 0; i < source.size(); i++)
+        {
+          pugi::xml_node node = target.append_child();
+          node.set_name(arrayElement.c_str());
+          JsonToXmlInternal(node, source[i], arrayElement);
+        }
+        break;
+      }
+        
+      case Json::objectValue:
+      {
+        Json::Value::Members members = source.getMemberNames();
+
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          pugi::xml_node node = target.append_child();
+          node.set_name(members[i].c_str());
+          JsonToXmlInternal(node, source[members[i]], arrayElement);          
+        }
+
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void Toolbox::JsonToXml(std::string& target,
+                          const Json::Value& source,
+                          const std::string& rootElement,
+                          const std::string& arrayElement)
+  {
+    pugi::xml_document doc;
+
+    pugi::xml_node n = doc.append_child(rootElement.c_str());
+    JsonToXmlInternal(n, source, arrayElement);
+
+    pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
+    decl.append_attribute("version").set_value("1.0");
+    decl.append_attribute("encoding").set_value("utf-8");
+
+    XmlToString(target, doc);
+  }
+
+  void Toolbox::XmlToString(std::string& target,
+                            const pugi::xml_document& source)
+  {
+    ChunkedBufferWriter writer;
+    source.save(writer, "  ", pugi::format_default, pugi::encoding_utf8);
+    writer.Flatten(target);
+  }
+#endif
+
+
+  
+  bool Toolbox::IsInteger(const std::string& str)
+  {
+    std::string s = StripSpaces(str);
+
+    if (s.size() == 0)
+    {
+      return false;
+    }
+
+    size_t pos = 0;
+    if (s[0] == '-')
+    {
+      if (s.size() == 1)
+      {
+        return false;
+      }
+
+      pos = 1;
+    }
+
+    while (pos < s.size())
+    {
+      if (!isdigit(s[pos]))
+      {
+        return false;
+      }
+
+      pos++;
+    }
+
+    return true;
+  }
+
+
+  void Toolbox::CopyJsonWithoutComments(Json::Value& target,
+                                        const Json::Value& source)
+  {
+    switch (source.type())
+    {
+      case Json::nullValue:
+        target = Json::nullValue;
+        break;
+
+      case Json::intValue:
+        target = source.asInt64();
+        break;
+
+      case Json::uintValue:
+        target = source.asUInt64();
+        break;
+
+      case Json::realValue:
+        target = source.asDouble();
+        break;
+
+      case Json::stringValue:
+        target = source.asString();
+        break;
+
+      case Json::booleanValue:
+        target = source.asBool();
+        break;
+
+      case Json::arrayValue:
+      {
+        target = Json::arrayValue;
+        for (Json::Value::ArrayIndex i = 0; i < source.size(); i++)
+        {
+          Json::Value& item = target.append(Json::nullValue);
+          CopyJsonWithoutComments(item, source[i]);
+        }
+
+        break;
+      }
+
+      case Json::objectValue:
+      {
+        target = Json::objectValue;
+        Json::Value::Members members = source.getMemberNames();
+        for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
+        {
+          const std::string item = members[i];
+          CopyJsonWithoutComments(target[item], source[item]);
+        }
+
+        break;
+      }
+
+      default:
+        break;
+    }
+  }
+
+
+  bool Toolbox::StartsWith(const std::string& str,
+                           const std::string& prefix)
+  {
+    if (str.size() < prefix.size())
+    {
+      return false;
+    }
+    else
+    {
+      return str.compare(0, prefix.size(), prefix) == 0;
+    }
+  }
+  
+
+  static bool IsUnreservedCharacter(char c)
+  {
+    // This function checks whether "c" is an unserved character
+    // wrt. an URI percent-encoding
+    // https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding%5Fin%5Fa%5FURI
+
+    return ((c >= 'A' && c <= 'Z') ||
+            (c >= 'a' && c <= 'z') ||
+            (c >= '0' && c <= '9') ||
+            c == '-' ||
+            c == '_' ||
+            c == '.' ||
+            c == '~');
+  }
+
+  void Toolbox::UriEncode(std::string& target,
+                          const std::string& source)
+  {
+    // Estimate the length of the percent-encoded URI
+    size_t length = 0;
+
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      if (IsUnreservedCharacter(source[i]))
+      {
+        length += 1;
+      }
+      else
+      {
+        // This character must be percent-encoded
+        length += 3;
+      }
+    }
+
+    target.clear();
+    target.reserve(length);
+
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      if (IsUnreservedCharacter(source[i]))
+      {
+        target.push_back(source[i]);
+      }
+      else
+      {
+        // This character must be percent-encoded
+        uint8_t byte = static_cast<uint8_t>(source[i]);
+        uint8_t a = byte >> 4;
+        uint8_t b = byte & 0x0f;
+
+        target.push_back('%');
+        target.push_back(a < 10 ? a + '0' : a - 10 + 'A');
+        target.push_back(b < 10 ? b + '0' : b - 10 + 'A');
+      }
+    }
+  }
+
+
+  static bool HasField(const Json::Value& json,
+                       const std::string& key,
+                       Json::ValueType expectedType)
+  {
+    if (json.type() != Json::objectValue ||
+        !json.isMember(key))
+    {
+      return false;
+    }
+    else if (json[key].type() == expectedType)
+    {
+      return true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+  }
+
+
+  std::string Toolbox::GetJsonStringField(const Json::Value& json,
+                                          const std::string& key,
+                                          const std::string& defaultValue)
+  {
+    if (HasField(json, key, Json::stringValue))
+    {
+      return json[key].asString();
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  bool Toolbox::GetJsonBooleanField(const ::Json::Value& json,
+                                    const std::string& key,
+                                    bool defaultValue)
+  {
+    if (HasField(json, key, Json::booleanValue))
+    {
+      return json[key].asBool();
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  int Toolbox::GetJsonIntegerField(const ::Json::Value& json,
+                                   const std::string& key,
+                                   int defaultValue)
+  {
+    if (HasField(json, key, Json::intValue))
+    {
+      return json[key].asInt();
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  unsigned int Toolbox::GetJsonUnsignedIntegerField(const ::Json::Value& json,
+                                                    const std::string& key,
+                                                    unsigned int defaultValue)
+  {
+    int v = GetJsonIntegerField(json, key, defaultValue);
+
+    if (v < 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return static_cast<unsigned int>(v);
+    }
+  }
+
+
+  bool Toolbox::IsUuid(const std::string& str)
+  {
+    if (str.size() != 36)
+    {
+      return false;
+    }
+
+    for (size_t i = 0; i < str.length(); i++)
+    {
+      if (i == 8 || i == 13 || i == 18 || i == 23)
+      {
+        if (str[i] != '-')
+          return false;
+      }
+      else
+      {
+        if (!isalnum(str[i]))
+          return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  bool Toolbox::StartsWithUuid(const std::string& str)
+  {
+    if (str.size() < 36)
+    {
+      return false;
+    }
+
+    if (str.size() == 36)
+    {
+      return IsUuid(str);
+    }
+
+    assert(str.size() > 36);
+    if (!isspace(str[36]))
+    {
+      return false;
+    }
+
+    return IsUuid(str.substr(0, 36));
+  }
+
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  static std::unique_ptr<std::locale>  globalLocale_;
+
+  static bool SetGlobalLocale(const char* locale)
+  {
+    try
+    {
+      if (locale == NULL)
+      {
+        LOG(WARNING) << "Falling back to system-wide default locale";
+        globalLocale_.reset(new std::locale());
+      }
+      else
+      {
+        LOG(INFO) << "Using locale: \"" << locale << "\" for case-insensitive comparison of strings";
+        globalLocale_.reset(new std::locale(locale));
+      }
+    }
+    catch (std::runtime_error& e)
+    {
+      LOG(ERROR) << "Cannot set globale locale to "
+                 << (locale ? std::string(locale) : "(null)")
+                 << ": " << e.what();
+      globalLocale_.reset(NULL);
+    }
+
+    return (globalLocale_.get() != NULL);
+  }
+
+  
+  static void InitializeIcu()
+  {
+#if ORTHANC_STATIC_ICU == 1
+    if (globalIcuData_.empty())
+    {
+      LOG(INFO) << "Setting up the ICU common data";
+
+      GzipCompressor compressor;
+      compressor.Uncompress(globalIcuData_,
+                            FrameworkResources::GetFileResourceBuffer(FrameworkResources::LIBICU_DATA),
+                            FrameworkResources::GetFileResourceSize(FrameworkResources::LIBICU_DATA));
+
+      std::string md5;
+      Toolbox::ComputeMD5(md5, globalIcuData_);
+
+      if (md5 != ORTHANC_ICU_DATA_MD5 ||
+          globalIcuData_.empty())
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot decode the ICU common data");
+      }
+
+      // "ICU data is designed to be 16-aligned"
+      // http://userguide.icu-project.org/icudata#TOC-Alignment
+
+      {
+        static const size_t ALIGN = 16;
+
+        UErrorCode status = U_ZERO_ERROR;
+
+        if (reinterpret_cast<intptr_t>(globalIcuData_.c_str()) % ALIGN == 0)
+        {
+          // Data is already properly aligned
+          udata_setCommonData(globalIcuData_.c_str(), &status);  
+        }
+        else
+        {
+          std::string aligned;
+          aligned.resize(globalIcuData_.size() + ALIGN - 1);
+
+          intptr_t offset = reinterpret_cast<intptr_t>(aligned.c_str()) % ALIGN;
+          if (offset != 0)
+          {
+            offset = ALIGN - offset;
+          }
+
+          if (offset + globalIcuData_.size() > aligned.size())
+          {
+            throw OrthancException(ErrorCode_InternalError, "Cannot align on 16-bytes boundary");
+          }
+
+          // We don't use "memcpy()", as it expects its data to be aligned
+          const uint8_t* p = reinterpret_cast<uint8_t*>(&globalIcuData_[0]);
+          uint8_t* q = reinterpret_cast<uint8_t*>(&aligned[0]) + offset;
+          for (size_t i = 0; i < globalIcuData_.size(); i++, p++, q++)
+          {
+            *q = *p;
+          }
+        
+          globalIcuData_.swap(aligned);
+
+          const uint8_t* data = reinterpret_cast<const uint8_t*>(globalIcuData_.c_str()) + offset;
+        
+          if (reinterpret_cast<intptr_t>(data) % ALIGN != 0)
+          {
+            throw OrthancException(ErrorCode_InternalError, "Cannot align on 16-bytes boundary");
+          }
+          else
+          {
+            udata_setCommonData(data, &status);  
+          }
+        }
+
+        if (status != U_ZERO_ERROR)
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot initialize ICU");
+        }
+      }
+
+      if (Toolbox::DetectEndianness() != Endianness_Little)
+      {
+        // TODO - The data table must be swapped (uint16_t)
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+
+      // "First-use of ICU from a single thread before the
+      // multi-threaded use of ICU begins", to make sure everything is
+      // properly initialized (should not be mandatory in our
+      // case). We let boost handle calls to "u_init()" and "u_cleanup()".
+      // http://userguide.icu-project.org/design#TOC-ICU-Initialization-and-Termination
+      uloc_getDefault();
+    }
+#endif
+  }
+  
+  void Toolbox::InitializeGlobalLocale(const char* locale)
+  {
+    InitializeIcu();
+
+#if defined(__unix__) && ORTHANC_SANDBOXED != 1
+    static const char* LOCALTIME = "/etc/localtime";
+    
+    if (!SystemToolbox::IsExistingFile(LOCALTIME))
+    {
+      // Check out file
+      // "boost_1_69_0/libs/locale/src/icu/time_zone.cpp": Direct
+      // access is made to this file if ICU is not used. Crash arises
+      // in Boost if the file is a symbolic link to a non-existing
+      // file (such as in Ubuntu 16.04 base Docker image).
+      throw OrthancException(
+        ErrorCode_InternalError,
+        "On UNIX-like systems, the file " + std::string(LOCALTIME) +
+        " must be present on the filesystem (install \"tzdata\" package on Debian)");
+    }
+#endif
+
+    // Make Orthanc use English, United States locale
+    // Linux: use "en_US.UTF-8"
+    // Windows: use ""
+    // Wine: use NULL
+    
+#if defined(__MINGW32__)
+    // Visibly, there is no support of locales in MinGW yet
+    // http://mingw.5.n7.nabble.com/How-to-use-std-locale-global-with-MinGW-correct-td33048.html
+    static const char* DEFAULT_LOCALE = NULL;
+#elif defined(_WIN32)
+    // For Windows: use default locale (using "en_US" does not work)
+    static const char* DEFAULT_LOCALE = "";
+#else
+    // For Linux & cie
+    static const char* DEFAULT_LOCALE = "en_US.UTF-8";
+#endif
+
+    bool ok;
+    
+    if (locale == NULL)
+    {
+      ok = SetGlobalLocale(DEFAULT_LOCALE);
+
+#if defined(__MINGW32__)
+      LOG(WARNING) << "This is a MinGW build, case-insensitive comparison of "
+                   << "strings with accents will not work outside of Wine";
+#endif
+    }
+    else
+    {
+      ok = SetGlobalLocale(locale);
+    }
+
+    if (!ok &&
+        !SetGlobalLocale(NULL))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot initialize global locale");
+    }
+
+  }
+
+
+  void Toolbox::FinalizeGlobalLocale()
+  {
+    globalLocale_.reset();
+  }
+
+
+  std::string Toolbox::ToUpperCaseWithAccents(const std::string& source)
+  {
+    bool error = (globalLocale_.get() == NULL);
+
+#if ORTHANC_STATIC_ICU == 1
+    if (globalIcuData_.empty())
+    {
+      error = true;
+    }
+#endif
+    
+    if (error)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No global locale was set, call Toolbox::InitializeGlobalLocale()");
+    }
+
+    /**
+     * A few notes about locales:
+     *
+     * (1) We don't use "case folding":
+     * http://www.boost.org/doc/libs/1_64_0/libs/locale/doc/html/conversions.html
+     *
+     * Characters are made uppercase one by one. This is because, in
+     * static builds, we are using iconv, which is visibly not
+     * supported correctly (TODO: Understand why). Case folding seems
+     * to be working correctly if using the default backend under
+     * Linux (ICU or POSIX?). If one wishes to use case folding, one
+     * would use:
+     *
+     *   boost::locale::generator gen;
+     *   std::locale::global(gen(DEFAULT_LOCALE));
+     *   return boost::locale::to_upper(source);
+     *
+     * (2) The function "boost::algorithm::to_upper_copy" does not
+     * make use of the "std::locale::global()". We therefore create a
+     * global variable "globalLocale_".
+     * 
+     * (3) The variant of "boost::algorithm::to_upper_copy()" that
+     * uses std::string does not work properly. We need to apply it
+     * one wide strings (std::wstring). This explains the two calls to
+     * "utf_to_utf" in order to convert to/from std::wstring.
+     **/
+
+    std::wstring w = boost::locale::conv::utf_to_utf<wchar_t>(source, boost::locale::conv::skip);
+    w = boost::algorithm::to_upper_copy<std::wstring>(w, *globalLocale_);
+    return boost::locale::conv::utf_to_utf<char>(w, boost::locale::conv::skip);
+  }
+#endif
+
+
+
+#if ORTHANC_ENABLE_SSL == 0
+  /**
+   * OpenSSL is disabled
+   **/
+  void Toolbox::InitializeOpenSsl()
+  {
+  }
+  
+  void Toolbox::FinalizeOpenSsl()
+  {
+  }  
+
+
+#elif (ORTHANC_ENABLE_SSL == 1 &&               \
+       OPENSSL_VERSION_NUMBER < 0x10100000L) 
+  /**
+   * OpenSSL < 1.1.0
+   **/
+  void Toolbox::InitializeOpenSsl()
+  {
+    // https://wiki.openssl.org/index.php/Library_Initialization
+    SSL_library_init();
+    SSL_load_error_strings();
+    OpenSSL_add_all_algorithms();
+    ERR_load_crypto_strings();
+  }
+
+  void Toolbox::FinalizeOpenSsl()
+  {
+    // Finalize OpenSSL
+    // https://wiki.openssl.org/index.php/Library_Initialization#Cleanup
+#ifdef FIPS_mode_set
+    FIPS_mode_set(0);
+#endif
+
+#if !defined(OPENSSL_NO_ENGINE)
+    ENGINE_cleanup();
+#endif
+    
+    CONF_modules_unload(1);
+    EVP_cleanup();
+    CRYPTO_cleanup_all_ex_data();
+    ERR_remove_state(0);
+    ERR_free_strings();
+  }
+
+  
+#elif (ORTHANC_ENABLE_SSL == 1 &&               \
+       OPENSSL_VERSION_NUMBER >= 0x10100000L) 
+  /**
+   * OpenSSL >= 1.1.0. In this case, the initialization is
+   * automatically done by the functions of OpenSSL.
+   * https://wiki.openssl.org/index.php/Library_Initialization
+   **/
+  void Toolbox::InitializeOpenSsl()
+  {
+  }
+
+  void Toolbox::FinalizeOpenSsl()
+  {
+  }
+
+#else
+#  error "Support your platform here"
+#endif
+  
+
+
+  std::string Toolbox::GenerateUuid()
+  {
+#ifdef WIN32
+    UUID uuid;
+    UuidCreate ( &uuid );
+
+    unsigned char * str;
+    UuidToStringA ( &uuid, &str );
+
+    std::string s( ( char* ) str );
+
+    RpcStringFreeA ( &str );
+#else
+    uuid_t uuid;
+    uuid_generate_random ( uuid );
+    char s[37];
+    uuid_unparse ( uuid, s );
+#endif
+    return s;
+  }
+
+
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+
+    class VariableFormatter
+    {
+    public:
+      typedef std::map<std::string, std::string>   Dictionary;
+
+    private:
+      const Dictionary& dictionary_;
+
+    public:
+      VariableFormatter(const Dictionary& dictionary) :
+        dictionary_(dictionary)
+      {
+      }
+  
+      template<typename Out>
+      Out operator()(const boost::smatch& what,
+                     Out out) const
+      {
+        if (!what[1].str().empty())
+        {
+          // Variable without a default value
+          Dictionary::const_iterator found = dictionary_.find(what[1]);
+    
+          if (found != dictionary_.end())
+          {
+            const std::string& value = found->second;
+            out = std::copy(value.begin(), value.end(), out);
+          }
+        }
+        else
+        {
+          // Variable with a default value
+          std::string key;
+          std::string defaultValue;
+          
+          if (!what[2].str().empty())
+          {
+            key = what[2].str();
+            defaultValue = what[3].str();
+          }
+          else if (!what[4].str().empty())
+          {
+            key = what[4].str();
+            defaultValue = what[5].str();
+          }
+          else if (!what[6].str().empty())
+          {
+            key = what[6].str();
+            defaultValue = what[7].str();
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+
+          Dictionary::const_iterator found = dictionary_.find(key);
+    
+          if (found == dictionary_.end())
+          {
+            out = std::copy(defaultValue.begin(), defaultValue.end(), out);
+          }
+          else
+          {
+            const std::string& value = found->second;
+            out = std::copy(value.begin(), value.end(), out);
+          }
+        }
+    
+        return out;
+      }
+    };
+  }
+
+  
+  std::string Toolbox::SubstituteVariables(const std::string& source,
+                                           const std::map<std::string, std::string>& dictionary)
+  {
+    const boost::regex pattern("\\$\\{([^:]*?)\\}|"                 // ${what[1]}
+                               "\\$\\{([^:]*?):-([^'\"]*?)\\}|"     // ${what[2]:-what[3]}
+                               "\\$\\{([^:]*?):-\"([^\"]*?)\"\\}|"  // ${what[4]:-"what[5]"}
+                               "\\$\\{([^:]*?):-'([^']*?)'\\}");    // ${what[6]:-'what[7]'}
+
+    VariableFormatter formatter(dictionary);
+
+    return boost::regex_replace(source, pattern, formatter);
+  }
+
+
+  namespace Iso2022
+  {
+    /**
+       Returns whether the string s contains a single-byte control message
+       at index i
+    **/
+    static inline bool IsControlMessage1(const std::string& s, size_t i)
+    {
+      if (i < s.size())
+      {
+        char c = s[i];
+        return
+          (c == '\x0f') || // Locking shift zero
+          (c == '\x0e');   // Locking shift one
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    /**
+       Returns whether the string s contains a double-byte control message
+       at index i
+    **/
+    static inline size_t IsControlMessage2(const std::string& s, size_t i)
+    {
+      if (i + 1 < s.size())
+      {
+        char c1 = s[i];
+        char c2 = s[i + 1];
+        return (c1 == 0x1b) && (
+          (c2 == '\x6e') || // Locking shift two
+          (c2 == '\x6f') || // Locking shift three
+          (c2 == '\x4e') || // Single shift two (alt)
+          (c2 == '\x4f') || // Single shift three (alt)
+          (c2 == '\x7c') || // Locking shift three right
+          (c2 == '\x7d') || // Locking shift two right
+          (c2 == '\x7e')    // Locking shift one right
+          );
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    /**
+       Returns whether the string s contains a triple-byte control message
+       at index i
+    **/
+    static inline size_t IsControlMessage3(const std::string& s, size_t i)
+    {
+      if (i + 2 < s.size())
+      {
+        char c1 = s[i];
+        char c2 = s[i + 1];
+        char c3 = s[i + 2];
+        return ((c1 == '\x8e' && c2 == 0x1b && c3 == '\x4e') ||
+                (c1 == '\x8f' && c2 == 0x1b && c3 == '\x4f'));
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    /**
+       This function returns true if the index i in the supplied string s:
+       - is valid
+       - contains the c character
+       This function returns false otherwise.
+    **/
+    static inline bool TestCharValue(
+      const std::string& s, size_t i, char c)
+    {
+      if (i < s.size())
+        return s[i] == c;
+      else
+        return false;
+    }
+
+    /**
+       This function returns true if the index i in the supplied string s:
+       - is valid
+       - has a c character that is >= cMin and <= cMax (included)
+       This function returns false otherwise.
+    **/
+    static inline bool TestCharRange(
+      const std::string& s, size_t i, char cMin, char cMax)
+    {
+      if (i < s.size())
+        return (s[i] >= cMin) && (s[i] <= cMax);
+      else
+        return false;
+    }
+
+    /**
+       This function returns the total length in bytes of the escape sequence
+       located in string s at index i, if there is one, or 0 otherwise.
+    **/
+    static inline size_t GetEscapeSequenceLength(const std::string& s, size_t i)
+    {
+      if (TestCharValue(s, i, 0x1b))
+      {
+        size_t j = i+1;
+
+        // advance reading cursor while we are in a sequence 
+        while (TestCharRange(s, j, '\x20', '\x2f'))
+          ++j;
+
+        // check there is a valid termination byte AND we're long enough (there
+        // must be at least one byte between 0x20 and 0x2f
+        if (TestCharRange(s, j, '\x30', '\x7f') && (j - i) >= 2)
+          return j - i + 1;
+        else
+          return 0;
+      }
+      else
+        return 0;
+    }
+  }
+
+  
+
+  /**
+     This function will strip all ISO/IEC 2022 control codes and escape
+     sequences.
+     Please see https://en.wikipedia.org/wiki/ISO/IEC_2022 (as of 2019-02)
+     for a list of those.
+
+     Please note that this operation is potentially destructive, because
+     it removes the character set information from the byte stream.
+
+     However, in the case where the encoding is unique, then suppressing
+     the escape sequences allows one to provide us with a clean string after
+     conversion to utf-8 with boost.
+  **/
+  void Toolbox::RemoveIso2022EscapeSequences(std::string& dest, const std::string& src)
+  {
+    // we need AT MOST the same size as the source string in the output
+    dest.clear();
+    if (dest.capacity() < src.size())
+      dest.reserve(src.size());
+
+    size_t i = 0;
+
+    // uint8_t view to the string
+    while (i < src.size())
+    {
+      size_t j = i;
+
+      // The i index will only be incremented if a message is detected
+      // in that case, the message is skipped and the index is set to the
+      // next position to read
+      if (Iso2022::IsControlMessage1(src, i))
+        i += 1;
+      else if (Iso2022::IsControlMessage2(src, i))
+        i += 2;
+      else if (Iso2022::IsControlMessage3(src, i))
+        i += 3;
+      else
+        i += Iso2022::GetEscapeSequenceLength(src, i);
+
+      // if the index was NOT incremented, this means there was no message at
+      // this location: we then may copy the character at this index and 
+      // increment the index to point to the next read position
+      if (j == i)
+      {
+        dest.push_back(src[i]);
+        i++;
+      }
+    }
+  }
+
+
+  void Toolbox::Utf8ToUnicodeCharacter(uint32_t& unicode,
+                                       size_t& length,
+                                       const std::string& utf8,
+                                       size_t position)
+  {
+    // https://en.wikipedia.org/wiki/UTF-8
+
+    static const uint8_t MASK_IS_1_BYTE = 0x80;     // printf '0x%x\n' "$((2#10000000))"
+    static const uint8_t TEST_IS_1_BYTE = 0x00;
+ 
+    static const uint8_t MASK_IS_2_BYTES = 0xe0;    // printf '0x%x\n' "$((2#11100000))"
+    static const uint8_t TEST_IS_2_BYTES = 0xc0;    // printf '0x%x\n' "$((2#11000000))"
+
+    static const uint8_t MASK_IS_3_BYTES = 0xf0;    // printf '0x%x\n' "$((2#11110000))"
+    static const uint8_t TEST_IS_3_BYTES = 0xe0;    // printf '0x%x\n' "$((2#11100000))"
+
+    static const uint8_t MASK_IS_4_BYTES = 0xf8;    // printf '0x%x\n' "$((2#11111000))"
+    static const uint8_t TEST_IS_4_BYTES = 0xf0;    // printf '0x%x\n' "$((2#11110000))"
+
+    static const uint8_t MASK_CONTINUATION = 0xc0;  // printf '0x%x\n' "$((2#11000000))"
+    static const uint8_t TEST_CONTINUATION = 0x80;  // printf '0x%x\n' "$((2#10000000))"
+
+    if (position >= utf8.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(sizeof(uint8_t) == sizeof(char));
+    const uint8_t* buffer = reinterpret_cast<const uint8_t*>(utf8.c_str()) + position;
+
+    if ((buffer[0] & MASK_IS_1_BYTE) == TEST_IS_1_BYTE)
+    {
+      length = 1;
+      unicode = buffer[0] & ~MASK_IS_1_BYTE;
+    }
+    else if ((buffer[0] & MASK_IS_2_BYTES) == TEST_IS_2_BYTES &&
+             position + 1 < utf8.size() &&
+             (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION)
+    {
+      length = 2;
+      uint32_t a = buffer[0] & ~MASK_IS_2_BYTES;
+      uint32_t b = buffer[1] & ~MASK_CONTINUATION;
+      unicode = (a << 6) | b;
+    }
+    else if ((buffer[0] & MASK_IS_3_BYTES) == TEST_IS_3_BYTES &&
+             position + 2 < utf8.size() &&
+             (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION &&
+             (buffer[2] & MASK_CONTINUATION) == TEST_CONTINUATION)
+    {
+      length = 3;
+      uint32_t a = buffer[0] & ~MASK_IS_3_BYTES;
+      uint32_t b = buffer[1] & ~MASK_CONTINUATION;
+      uint32_t c = buffer[2] & ~MASK_CONTINUATION;
+      unicode = (a << 12) | (b << 6) | c;
+    }
+    else if ((buffer[0] & MASK_IS_4_BYTES) == TEST_IS_4_BYTES &&
+             position + 3 < utf8.size() &&
+             (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION &&
+             (buffer[2] & MASK_CONTINUATION) == TEST_CONTINUATION &&
+             (buffer[3] & MASK_CONTINUATION) == TEST_CONTINUATION)
+    {
+      length = 4;
+      uint32_t a = buffer[0] & ~MASK_IS_4_BYTES;
+      uint32_t b = buffer[1] & ~MASK_CONTINUATION;
+      uint32_t c = buffer[2] & ~MASK_CONTINUATION;
+      uint32_t d = buffer[3] & ~MASK_CONTINUATION;
+      unicode = (a << 18) | (b << 12) | (c << 6) | d;
+    }
+    else
+    {
+      // This is not a valid UTF-8 encoding
+      throw OrthancException(ErrorCode_BadFileFormat, "Invalid UTF-8 string");
+    }
+  }
+
+
+  std::string Toolbox::LargeHexadecimalToDecimal(const std::string& hex)
+  {
+    /**
+     * NB: Focus of the code below is *not* efficiency, but
+     * readability!
+     **/
+    
+    for (size_t i = 0; i < hex.size(); i++)
+    {
+      const char c = hex[i];
+      if (!((c >= 'A' && c <= 'F') ||
+            (c >= 'a' && c <= 'f') ||
+            (c >= '0' && c <= '9')))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "Not an hexadecimal number");
+      }
+    }
+    
+    std::vector<uint8_t> decimal;
+    decimal.push_back(0);
+
+    for (size_t i = 0; i < hex.size(); i++)
+    {
+      uint8_t hexDigit = static_cast<uint8_t>(Hex2Dec(hex[i]));
+      assert(hexDigit <= 15);
+
+      for (size_t j = 0; j < decimal.size(); j++)
+      {
+        uint8_t val = static_cast<uint8_t>(decimal[j]) * 16 + hexDigit;  // Maximum: 9 * 16 + 15
+        assert(val <= 159 /* == 9 * 16 + 15 */);
+      
+        decimal[j] = val % 10;
+        hexDigit = val / 10;
+        assert(hexDigit <= 15 /* == 159 / 10 */);
+      }
+
+      while (hexDigit > 0)
+      {
+        decimal.push_back(hexDigit % 10);
+        hexDigit /= 10;
+      }
+    }
+
+    size_t start = 0;
+    while (start < decimal.size() &&
+           decimal[start] == '0')
+    {
+      start++;
+    }
+
+    std::string s;
+    s.reserve(decimal.size() - start);
+
+    for (size_t i = decimal.size(); i > start; i--)
+    {
+      s.push_back(decimal[i - 1] + '0');
+    }
+
+    return s;
+  }
+
+
+  std::string Toolbox::GenerateDicomPrivateUniqueIdentifier()
+  {
+    /**
+     * REFERENCE: "Creating a Privately Defined Unique Identifier
+     * (Informative)" / "UUID Derived UID"
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html
+     * https://stackoverflow.com/a/46316162/881731
+     **/
+
+    std::string uuid = GenerateUuid();
+    assert(IsUuid(uuid) && uuid.size() == 36);
+
+    /**
+     * After removing the four dashes ("-") out of the 36-character
+     * UUID, we get a large hexadecimal number with 32 characters,
+     * each of those characters lying in the range [0,16[. The large
+     * number is thus in the [0,16^32[ = [0,256^16[ range. This number
+     * has a maximum of 39 decimal digits, as can be seen in Python:
+     * 
+     * # python -c 'import math; print(math.log(16**32))/math.log(10))'
+     * 38.531839445
+     *
+     * We now to convert the large hexadecimal number to a decimal
+     * number with up to 39 digits, remove the leading zeros, then
+     * prefix it with "2.25."
+     **/
+
+    // Remove the dashes
+    std::string hex = (uuid.substr(0, 8) +
+                       uuid.substr(9, 4) +
+                       uuid.substr(14, 4) +
+                       uuid.substr(19, 4) +
+                       uuid.substr(24, 12));
+    assert(hex.size() == 32);
+
+    return "2.25." + LargeHexadecimalToDecimal(hex);
+  }
+}
+
+
+
+OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content)
+{
+  return reinterpret_cast<OrthancLinesIterator*>(new Orthanc::Toolbox::LinesIterator(content));
+}
+
+
+bool OrthancLinesIterator_GetLine(std::string& target,
+                                  const OrthancLinesIterator* iterator)
+{
+  if (iterator != NULL)
+  {
+    return reinterpret_cast<const Orthanc::Toolbox::LinesIterator*>(iterator)->GetLine(target);
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+void OrthancLinesIterator_Next(OrthancLinesIterator* iterator)
+{
+  if (iterator != NULL)
+  {
+    reinterpret_cast<Orthanc::Toolbox::LinesIterator*>(iterator)->Next();
+  }
+}
+
+
+void OrthancLinesIterator_Free(OrthancLinesIterator* iterator)
+{
+  if (iterator != NULL)
+  {
+    delete reinterpret_cast<const Orthanc::Toolbox::LinesIterator*>(iterator);
+  }
+}