Mercurial > hg > orthanc
changeset 6606:29388610f3e2
adding support for UTF-8 in ZipWriter and HierarchicalZipWriter
| author | Sebastien Jodogne <s.jodogne@gmail.com> |
|---|---|
| date | Fri, 27 Feb 2026 18:37:52 +0100 |
| parents | 6e17d039332d |
| children | f30191e9ffbf |
| files | OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp OrthancFramework/Sources/Compression/HierarchicalZipWriter.h OrthancFramework/Sources/Compression/ZipWriter.cpp OrthancFramework/Sources/Compression/ZipWriter.h OrthancFramework/Sources/Toolbox.cpp OrthancFramework/Sources/Toolbox.h OrthancFramework/UnitTestsSources/ZipTests.cpp |
| diffstat | 7 files changed, 159 insertions(+), 108 deletions(-) [+] |
line wrap: on
line diff
--- a/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp Fri Feb 27 16:57:00 2026 +0100 +++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp Fri Feb 27 18:37:52 2026 +0100 @@ -32,43 +32,6 @@ namespace Orthanc { - std::string HierarchicalZipWriter::Index::KeepAlphanumeric(const std::string& source) - { - std::string result; - - bool lastSpace = false; - - result.reserve(source.size()); - for (size_t i = 0; i < source.size(); i++) - { - char c = source[i]; - if (c == '^') - c = ' '; - - if (c <= 127 && - c >= 0) - { - if (isspace(c)) - { - if (!lastSpace) - { - lastSpace = true; - result.push_back(' '); - } - } - else if (isalnum(c) || - c == '.' || - c == '_') - { - result.push_back(c); - lastSpace = false; - } - } - } - - return Toolbox::StripSpaces(result); - } - std::string HierarchicalZipWriter::Index::GetCurrentDirectoryPath() const { std::string result; @@ -85,9 +48,15 @@ return result; } - std::string HierarchicalZipWriter::Index::EnsureUniqueFilename(const std::string& filename) + std::string HierarchicalZipWriter::Index::EnsureUniqueFilename(const std::string& filename, + bool allowUtf8) { - std::string standardized = KeepAlphanumeric(filename); + const std::string standardized = Toolbox::NormalizePath(filename, allowUtf8); + + if (standardized.find('/') != std::string::npos) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Your filename must not contain slashes or backslashes"); + } Directory& d = *stack_.back(); Directory::Content::iterator it = d.content_.find(standardized); @@ -122,14 +91,16 @@ return stack_.size() == 1; } - std::string HierarchicalZipWriter::Index::OpenFile(const std::string& name) + std::string HierarchicalZipWriter::Index::OpenFile(const std::string& name, + bool allowUtf8) { - return GetCurrentDirectoryPath() + EnsureUniqueFilename(name); + return GetCurrentDirectoryPath() + EnsureUniqueFilename(name, allowUtf8); } - void HierarchicalZipWriter::Index::OpenDirectory(const std::string& name) + void HierarchicalZipWriter::Index::OpenDirectory(const std::string& name, + bool allowUtf8) { - std::string d = EnsureUniqueFilename(name); + std::string d = EnsureUniqueFilename(name, allowUtf8); // Push the new directory onto the stack stack_.push_back(new Directory); @@ -201,13 +172,13 @@ void HierarchicalZipWriter::OpenFile(const std::string& name) { - std::string p = indexer_.OpenFile(name); + std::string p = indexer_.OpenFile(name, IsAllowUtf8()); writer_.OpenFile(p.c_str()); } void HierarchicalZipWriter::OpenDirectory(const std::string& name) { - indexer_.OpenDirectory(name); + indexer_.OpenDirectory(name, IsAllowUtf8()); } void HierarchicalZipWriter::CloseDirectory()
--- a/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h Fri Feb 27 16:57:00 2026 +0100 +++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h Fri Feb 27 18:37:52 2026 +0100 @@ -41,7 +41,6 @@ { #if ORTHANC_BUILD_UNIT_TESTS == 1 FRIEND_TEST(HierarchicalZipWriter, Index); - FRIEND_TEST(HierarchicalZipWriter, Filenames); #endif private: @@ -60,7 +59,8 @@ Stack stack_; - std::string EnsureUniqueFilename(const std::string& name); + std::string EnsureUniqueFilename(const std::string& name, + bool allowUtf8); public: Index(); @@ -69,15 +69,15 @@ bool IsRoot() const; - std::string OpenFile(const std::string& name); + std::string OpenFile(const std::string& name, + bool allowUtf8); - void OpenDirectory(const std::string& name); + void OpenDirectory(const std::string& name, + bool allowUtf8); void CloseDirectory(); std::string GetCurrentDirectoryPath() const; - - static std::string KeepAlphanumeric(const std::string& source); }; Index indexer_; @@ -95,6 +95,16 @@ bool IsZip64() const; + void SetAllowUtf8(bool allowUtf8) + { + writer_.SetAllowUtf8(allowUtf8); + } + + bool IsAllowUtf8() const + { + return writer_.IsAllowUtf8(); + } + void SetCompressionLevel(uint8_t level); uint8_t GetCompressionLevel() const;
--- a/OrthancFramework/Sources/Compression/ZipWriter.cpp Fri Feb 27 16:57:00 2026 +0100 +++ b/OrthancFramework/Sources/Compression/ZipWriter.cpp Fri Feb 27 18:37:52 2026 +0100 @@ -441,6 +441,7 @@ ZipWriter::ZipWriter() : pimpl_(new PImpl), isZip64_(false), + allowUtf8_(false), hasFileInZip_(false), append_(false), compressionLevel_(6) @@ -608,29 +609,7 @@ { Open(); - std::string utf8; - -#if 0 - if (Toolbox::IsValidUtf8(filename)) - { - /** - * Starting with minizip 1.3.2 (released on 17 Feb 2026), bit 11 - * of the ZIP header flags is appropriately set if the filename - * is encoded using UTF-8. This enables compatibility with UTF-8. - * https://zlib.net/ChangeLog.txt - * https://discourse.orthanc-server.org/t/seriesdescription-characters-and-removed-during-oe2-zip-export/6397 - **/ - utf8 = filename; - } - else - { - // Fallback if the filename is not encoded using valid UTF-8 - utf8 = Toolbox::ConvertToAscii(filename); - } -#else - // This was the code used in Orthanc <= 1.12.10 - utf8 = Toolbox::ConvertToAscii(filename); -#endif + const std::string normalized = Toolbox::NormalizePath(filename, allowUtf8_); zip_fileinfo zfi; PrepareFileInfo(zfi); @@ -639,7 +618,7 @@ if (isZip64_) { - result = zipOpenNewFileInZip64(pimpl_->file_, utf8.c_str(), + result = zipOpenNewFileInZip64(pimpl_->file_, normalized.c_str(), &zfi, NULL, 0, NULL, 0, @@ -649,7 +628,7 @@ } else { - result = zipOpenNewFileInZip(pimpl_->file_, utf8.c_str(), + result = zipOpenNewFileInZip(pimpl_->file_, normalized.c_str(), &zfi, NULL, 0, NULL, 0,
--- a/OrthancFramework/Sources/Compression/ZipWriter.h Fri Feb 27 16:57:00 2026 +0100 +++ b/OrthancFramework/Sources/Compression/ZipWriter.h Fri Feb 27 18:37:52 2026 +0100 @@ -130,6 +130,7 @@ boost::shared_ptr<PImpl> pimpl_; bool isZip64_; + bool allowUtf8_; bool hasFileInZip_; bool append_; uint8_t compressionLevel_; @@ -146,6 +147,26 @@ bool IsZip64() const; + /** + * Starting with minizip 1.3.2 (released on 17 Feb 2026), bit 11 + * of the ZIP header flags is appropriately set if the path is + * encoded using UTF-8. This enables compatibility with UTF-8. + * The behavior of Orthanc <= 1.12.10 corresponds to + * "SetAllowUtf8(false)". + * + * https://zlib.net/ChangeLog.txt + * https://discourse.orthanc-server.org/t/seriesdescription-characters-and-removed-during-oe2-zip-export/6397 + **/ + void SetAllowUtf8(bool allowUtf8) + { + allowUtf8_ = allowUtf8; + } + + bool IsAllowUtf8() const + { + return allowUtf8_; + } + void SetCompressionLevel(uint8_t level); uint8_t GetCompressionLevel() const;
--- a/OrthancFramework/Sources/Toolbox.cpp Fri Feb 27 16:57:00 2026 +0100 +++ b/OrthancFramework/Sources/Toolbox.cpp Fri Feb 27 18:37:52 2026 +0100 @@ -2958,6 +2958,67 @@ } + std::string Toolbox::NormalizePath(const std::string& utf8, + bool allowUtf8) + { + std::string converted; + + if (allowUtf8 && + IsValidUtf8(utf8)) + { + converted = utf8; + } + else + { + converted = Toolbox::ConvertToAscii(utf8); + } + + bool previousSpace = true; + + std::string result; + result.reserve(converted.size()); + + for (size_t i = 0; i < converted.size(); i++) + { + unsigned char c = converted[i]; + + if (isspace(c) || + c <= 31 || // Control characters + + // Remove characters that could break some filesystems + c == ':' || + c == '*' || + c == '?' || + c == '"' || + c == '<' || + c == '>' || + c == '|') + { + if (!previousSpace) + { + previousSpace = true; + result.push_back(' '); + } + } + else + { + previousSpace = false; + + if (c == '\\') + { + result.push_back('/'); // Replace backslashes by forward slashes + } + else + { + result.push_back(c); + } + } + } + + return Toolbox::StripSpaces(result); + } + + Toolbox::ElapsedTimer::ElapsedTimer() { Restart();
--- a/OrthancFramework/Sources/Toolbox.h Fri Feb 27 16:57:00 2026 +0100 +++ b/OrthancFramework/Sources/Toolbox.h Fri Feb 27 18:37:52 2026 +0100 @@ -407,6 +407,9 @@ static bool IsValidUtf8(const std::string& s); + static std::string NormalizePath(const std::string& utf8, + bool allowUtf8); + class ORTHANC_PUBLIC ElapsedTimer : public boost::noncopyable { private:
--- a/OrthancFramework/UnitTestsSources/ZipTests.cpp Fri Feb 27 16:57:00 2026 +0100 +++ b/OrthancFramework/UnitTestsSources/ZipTests.cpp Fri Feb 27 18:37:52 2026 +0100 @@ -43,7 +43,7 @@ TEST(ZipWriter, Basic) { - Orthanc::ZipWriter w; + ZipWriter w; w.SetOutputPath("UnitTestsResults/hello.zip"); w.Open(); w.OpenFile("world/hello"); @@ -53,7 +53,7 @@ TEST(ZipWriter, Basic64) { - Orthanc::ZipWriter w; + ZipWriter w; w.SetOutputPath("UnitTestsResults/hello64.zip"); w.SetZip64(true); w.Open(); @@ -64,18 +64,18 @@ TEST(ZipWriter, Exceptions) { - Orthanc::ZipWriter w; - ASSERT_THROW(w.Open(), Orthanc::OrthancException); + ZipWriter w; + ASSERT_THROW(w.Open(), OrthancException); w.SetOutputPath("UnitTestsResults/hello3.zip"); w.Open(); - ASSERT_THROW(w.Write("hello world"), Orthanc::OrthancException); + ASSERT_THROW(w.Write("hello world"), OrthancException); } TEST(ZipWriter, Append) { { - Orthanc::ZipWriter w; + ZipWriter w; w.SetAppendToExisting(false); w.SetOutputPath("UnitTestsResults/append.zip"); w.Open(); @@ -84,7 +84,7 @@ } { - Orthanc::ZipWriter w; + ZipWriter w; w.SetAppendToExisting(true); w.SetOutputPath("UnitTestsResults/append.zip"); w.Open(); @@ -105,40 +105,46 @@ TEST(HierarchicalZipWriter, Index) { HierarchicalZipWriter::Index i; - ASSERT_EQ("hello", i.OpenFile("hello")); - ASSERT_EQ("hello-2", i.OpenFile("hello")); - ASSERT_EQ("coucou", i.OpenFile("coucou")); - ASSERT_EQ("hello-3", i.OpenFile("hello")); + ASSERT_EQ("hello", i.OpenFile("hello", true)); + ASSERT_EQ("hello-2", i.OpenFile("hello", true)); + ASSERT_EQ("coucou", i.OpenFile("coucou", true)); + ASSERT_EQ("hello-3", i.OpenFile("hello", true)); + ASSERT_EQ("trE hell", i.OpenFile(" ÊtrE hellô ", false)); + ASSERT_EQ("ÊtrE hellô", i.OpenFile(" ÊtrE hellô ", true)); + ASSERT_THROW(i.OpenFile("hel/lo", true), OrthancException); + ASSERT_THROW(i.OpenFile("hel\\lo", true), OrthancException); - i.OpenDirectory("coucou"); + i.OpenDirectory("coucou", true); - ASSERT_EQ("coucou-2/world", i.OpenFile("world")); - ASSERT_EQ("coucou-2/world-2", i.OpenFile("world")); + ASSERT_EQ("coucou-2/world", i.OpenFile("world", true)); + ASSERT_EQ("coucou-2/world-2", i.OpenFile("world", true)); - i.OpenDirectory("world"); + ASSERT_THROW(i.OpenDirectory("hel/lo", true), OrthancException); + ASSERT_THROW(i.OpenDirectory("hel\\lo", true), OrthancException); + + i.OpenDirectory("world", true); - ASSERT_EQ("coucou-2/world-3/hello", i.OpenFile("hello")); - ASSERT_EQ("coucou-2/world-3/hello-2", i.OpenFile("hello")); + ASSERT_EQ("coucou-2/world-3/hello", i.OpenFile("hello", true)); + ASSERT_EQ("coucou-2/world-3/hello-2", i.OpenFile("hello", true)); i.CloseDirectory(); - ASSERT_EQ("coucou-2/world-4", i.OpenFile("world")); + ASSERT_EQ("coucou-2/world-4", i.OpenFile("world", true)); i.CloseDirectory(); - ASSERT_EQ("coucou-3", i.OpenFile("coucou")); + ASSERT_EQ("coucou-3", i.OpenFile("coucou", true)); ASSERT_THROW(i.CloseDirectory(), OrthancException); } +} - TEST(HierarchicalZipWriter, Filenames) - { - ASSERT_EQ("trE hell", HierarchicalZipWriter::Index::KeepAlphanumeric(" ÊtrE hellô ")); - - // The "^" character is considered as a space in DICOM - ASSERT_EQ("Hel lo world", HierarchicalZipWriter::Index::KeepAlphanumeric(" Hel^^ ^\r\n\t^^lo \t <world> ")); - } +TEST(HierarchicalZipWriter, Filenames) +{ + ASSERT_EQ("trE hell", Toolbox::NormalizePath(" ÊtrE hellô ", false)); + ASSERT_EQ("ÊtrE hellô", Toolbox::NormalizePath(" ÊtrE hellô ", true)); + ASSERT_EQ("Hel^^ ^ ^^lo world", Toolbox::NormalizePath(" Hel^^ ^\r\n\t^^lo \t <world> ", true)); } @@ -193,7 +199,7 @@ TemporaryFile f; { - Orthanc::ZipWriter w; + ZipWriter w; ASSERT_EQ(0u, w.GetArchiveSize()); w.SetOutputPath(f.GetPath()); @@ -233,7 +239,7 @@ for (int i = 0; i < 2; i++) { { - Orthanc::ZipWriter w; + ZipWriter w; w.SetMemoryOutput(memory, (i == 0) /* ZIP64? */); w.Open(); @@ -242,13 +248,13 @@ w.CancelStream(); } - ASSERT_THROW(ZipReader::CreateFromMemory(memory), Orthanc::OrthancException); + ASSERT_THROW(ZipReader::CreateFromMemory(memory), OrthancException); memory.clear(); uint64_t archiveSize; { - Orthanc::ZipWriter w; + ZipWriter w; ASSERT_EQ(0u, w.GetArchiveSize()); w.SetMemoryOutput(memory, (i == 0) /* ZIP64? */); @@ -426,14 +432,14 @@ static const std::string path = "UnitTestsResults/hello-utf8.zip"; { - Orthanc::ZipWriter w; + ZipWriter w; w.SetOutputPath(path); for (unsigned int i = 0; i < testEncodingsCount; i++) { std::string source(testEncodingsEncoded[i]); std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false, false); - ASSERT_TRUE(Orthanc::Toolbox::IsValidUtf8(s)); + ASSERT_TRUE(Toolbox::IsValidUtf8(s)); printf("[%s]\n", s.c_str()); w.OpenFile(s); w.Write("Hello");
