# HG changeset patch # User Sebastien Jodogne # Date 1771602013 -3600 # Node ID 81746b9484c451f0208faec408606df4fd3c0351 # Parent de1b2b661ec37eb1667a11f8818b8355d4ce3d96 added Toolbox::IsValidUtf8() diff -r de1b2b661ec3 -r 81746b9484c4 OrthancFramework/Sources/Toolbox.cpp --- a/OrthancFramework/Sources/Toolbox.cpp Fri Feb 20 16:03:16 2026 +0100 +++ b/OrthancFramework/Sources/Toolbox.cpp Fri Feb 20 16:40:13 2026 +0100 @@ -2825,6 +2825,139 @@ } } + + bool Toolbox::IsValidUtf8(const std::string& s) + { + if (s.empty()) + { + return true; + } + + const unsigned char* bytes = reinterpret_cast(s.data()); + size_t len = s.size(); + + size_t i = 0; + while (i < len) + { + unsigned char b1 = bytes[i]; + + if (b1 <= 0x7fu) + { + // 1-byte (ASCII): 0xxxxxxx + i++; + } + else if ((b1 >> 5) == 0x06u) + { + // 2-byte sequence: 110xxxxx 10xxxxxx + if (i + 1 >= len) + { + return false; + } + + unsigned char b2 = bytes[i + 1]; + if ((b2 >> 6) != 0x02u) + { + return false; + } + + uint32_t codepoint = + ((b1 & 0x1fu) << 6) | + (b2 & 0x3fu); + + // Overlong encoding check + if (codepoint < 0x80u) + { + return false; + } + + i += 2; + } + else if ((b1 >> 4) == 0x0eu) + { + // 3-byte sequence: 1110xxxx 10xxxxxx 10xxxxxx + if (i + 2 >= len) + { + return false; + } + + unsigned char b2 = bytes[i + 1]; + unsigned char b3 = bytes[i + 2]; + + if ((b2 >> 6) != 0x02u || + (b3 >> 6) != 0x02u) + { + return false; + } + + uint32_t codepoint = + ((b1 & 0x0fu) << 12) | + ((b2 & 0x3fu) << 6) | + (b3 & 0x3fu); + + // Overlong encoding check + if (codepoint < 0x0800u) + { + return false; + } + + // Surrogate range check (U+D800 to U+DFFF) + if (codepoint >= 0xd800u && + codepoint <= 0xdfffu) + { + return false; + } + + i += 3; + } + else if ((b1 >> 3) == 0x1eu) + { + // 4-byte sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if (i + 3 >= len) + { + return false; + } + + unsigned char b2 = bytes[i + 1]; + unsigned char b3 = bytes[i + 2]; + unsigned char b4 = bytes[i + 3]; + + if ((b2 >> 6) != 0x02u || + (b3 >> 6) != 0x02u || + (b4 >> 6) != 0x02u) + { + return false; + } + + uint32_t codepoint = + ((b1 & 0x07u) << 18) | + ((b2 & 0x3fu) << 12) | + ((b3 & 0x3fu) << 6) | + (b4 & 0x3fu); + + // Overlong encoding check + if (codepoint < 0x00010000u) + { + return false; + } + + // Maximum valid Unicode code point + if (codepoint > 0x0010ffffu) + { + return false; + } + + i += 4; + } + else + { + return false; // Invalid leading byte + } + } + + return true; + } + + Toolbox::ElapsedTimer::ElapsedTimer() { Restart(); diff -r de1b2b661ec3 -r 81746b9484c4 OrthancFramework/Sources/Toolbox.h --- a/OrthancFramework/Sources/Toolbox.h Fri Feb 20 16:03:16 2026 +0100 +++ b/OrthancFramework/Sources/Toolbox.h Fri Feb 20 16:40:13 2026 +0100 @@ -405,6 +405,8 @@ static void RemoveSurroundingQuotes(std::string& value); + static bool IsValidUtf8(const std::string& s); + class ORTHANC_PUBLIC ElapsedTimer : public boost::noncopyable { private: diff -r de1b2b661ec3 -r 81746b9484c4 OrthancFramework/UnitTestsSources/FrameworkTests.cpp --- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Fri Feb 20 16:03:16 2026 +0100 +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Fri Feb 20 16:40:13 2026 +0100 @@ -504,6 +504,7 @@ std::string s(reinterpret_cast(&data[0]), 10); ASSERT_EQ("&abc", Toolbox::ConvertToAscii(s)); + ASSERT_FALSE(Toolbox::IsValidUtf8(s)); // Open in Emacs, then save with UTF-8 encoding, then "hexdump -C" std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1, false, false); @@ -523,6 +524,7 @@ ASSERT_EQ(0x62, static_cast(utf8[12])); ASSERT_EQ(0x63, static_cast(utf8[13])); ASSERT_EQ(0x00, static_cast(utf8[14])); // Null-terminated string + ASSERT_TRUE(Toolbox::IsValidUtf8(utf8)); } @@ -532,9 +534,12 @@ const unsigned char latin1[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 }; std::string s(reinterpret_cast(&latin1[0]), sizeof(latin1) / sizeof(char)); + ASSERT_FALSE(Toolbox::IsValidUtf8(s)); - ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1, false, false), Encoding_Latin1)); + std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1, false, false); + ASSERT_EQ(s, Toolbox::ConvertFromUtf8(utf8, Encoding_Latin1)); ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8, false, false)); + ASSERT_TRUE(Toolbox::IsValidUtf8(utf8)); } diff -r de1b2b661ec3 -r 81746b9484c4 OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp --- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Fri Feb 20 16:03:16 2026 +0100 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Fri Feb 20 16:40:13 2026 +0100 @@ -265,6 +265,7 @@ std::string source(testEncodingsEncoded[i]); std::string expected(testEncodingsExpected[i]); std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false, false); + ASSERT_TRUE(Toolbox::IsValidUtf8(s)); //std::cout << EnumerationToString(testEncodings[i]) << std::endl; EXPECT_EQ(expected, s); } @@ -335,6 +336,7 @@ f.SetEncoding(testEncodings[i]); std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false, false); + ASSERT_TRUE(Toolbox::IsValidUtf8(s)); f.Insert(DICOM_TAG_PATIENT_NAME, s, false, ""); f.SaveToMemoryBuffer(dicom); } @@ -1173,6 +1175,7 @@ std::string encoded = Toolbox::ConvertFromUtf8(testEncodingsExpected[i], testEncodings[i]); ASSERT_STREQ(testEncodingsEncoded[i], encoded.c_str()); std::string decoded = Toolbox::ConvertToUtf8(encoded, testEncodings[i], false, false); + ASSERT_TRUE(Toolbox::IsValidUtf8(decoded)); ASSERT_STREQ(testEncodingsExpected[i], decoded.c_str()); if (testEncodings[i] != Encoding_Chinese) @@ -1182,6 +1185,7 @@ const std::string tmp = Toolbox::ConvertToUtf8( Toolbox::ConvertFromUtf8(utf8, testEncodings[i]), testEncodings[i], false, false); + ASSERT_TRUE(Toolbox::IsValidUtf8(tmp)); ASSERT_STREQ(testEncodingsExpected[i], tmp.c_str()); } } @@ -1612,7 +1616,8 @@ 0xea, 0xb8, 0xb8, 0xeb, 0x8f, 0x99 }; - std::string utf8(reinterpret_cast(utf8raw), sizeof(utf8raw)); + const std::string utf8(reinterpret_cast(utf8raw), sizeof(utf8raw)); + ASSERT_TRUE(Toolbox::IsValidUtf8(utf8)); ParsedDicomFile dicom(false); dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 149"); @@ -1709,7 +1714,8 @@ 0x8d, 0xe3, 0x81, 0x86 }; - std::string utf8(reinterpret_cast(utf8raw), sizeof(utf8raw)); + const std::string utf8(reinterpret_cast(utf8raw), sizeof(utf8raw)); + ASSERT_TRUE(Toolbox::IsValidUtf8(utf8)); ParsedDicomFile dicom(false); dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 87"); @@ -1914,7 +1920,10 @@ std::string value; ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME)); - ASSERT_EQ(value, std::string(reinterpret_cast(utf8), sizeof(utf8))); + + const std::string tmp(reinterpret_cast(utf8), sizeof(utf8)); + ASSERT_EQ(value, tmp); + ASSERT_TRUE(Toolbox::IsValidUtf8(tmp)); } diff -r de1b2b661ec3 -r 81746b9484c4 OrthancFramework/UnitTestsSources/ToolboxTests.cpp --- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Fri Feb 20 16:03:16 2026 +0100 +++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Fri Feb 20 16:40:13 2026 +0100 @@ -436,3 +436,26 @@ s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Latin1, false, ValueRepresentation_ShortText); ASSERT_EQ("ORIGINAL\\PRIMARY", s); } + +TEST(Toolbox, IsValidUtf8) +{ + ASSERT_TRUE(Orthanc::Toolbox::IsValidUtf8("Hello, world!")); // ASCII + ASSERT_TRUE(Orthanc::Toolbox::IsValidUtf8("")); // Empty string is valid + + ASSERT_TRUE(Orthanc::Toolbox::IsValidUtf8("\xC2\xA2")); // U+00A2 2-byte + ASSERT_TRUE(Orthanc::Toolbox::IsValidUtf8("\xE2\x82\xAC")); // U+20AC 3-byte + ASSERT_TRUE(Orthanc::Toolbox::IsValidUtf8("\xF0\x9F\x8C\x8D")); // U+1F30D 4-byte + ASSERT_TRUE(Orthanc::Toolbox::IsValidUtf8("Hello \xE2\x82\xAC \xF0\x9F\x8C\x8D")); // Mixed + ASSERT_TRUE(Orthanc::Toolbox::IsValidUtf8("\xF4\x8F\xBF\xBF")); // U+10FFFF (max valid) + + ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xC2\x20")); // Invalid continuation + ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xE2\x82")); // Missing continuation + ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\x80")); // Lone continuation byte + ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xC0\xAF")); // Overlong 2-byte + ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xE0\x80\xAF")); // Overlong 3-byte + ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xED\xA0\x80")); // Surrogate (U+D800) + ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xF4\x90\x80\x80")); // > U+10FFFF + ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xF0\x9F\x8C")); // Truncated 4-byte + ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xFF")); // Invalid leading byte + ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xF0\x28\x8C\x28")); +}