@@ -57,6 +57,47 @@ static std::string SanitizeCell(const std::string &input) {
5757 return out;
5858}
5959
60+ // Returns a valid UTF-8 copy of the input, replacing any malformed byte with
61+ // '?'. Cell values can hold raw bytes from malformed/truncated binaries, and
62+ // the bundled nlohmann::json (3.1.2) throws on invalid UTF-8 during dump().
63+ static std::string ToUtf8Safe (const std::string &in) {
64+ std::string out;
65+ out.reserve (in.size ());
66+ const std::size_t n = in.size ();
67+ std::size_t i = 0 ;
68+ while (i < n) {
69+ const unsigned char c = static_cast <unsigned char >(in[i]);
70+ if (c < 0x80 ) {
71+ out += static_cast <char >(c);
72+ ++i;
73+ continue ;
74+ }
75+ std::size_t len;
76+ uint32_t min_cp;
77+ if ((c & 0xE0 ) == 0xC0 ) { len = 2 ; min_cp = 0x80 ; }
78+ else if ((c & 0xF0 ) == 0xE0 ) { len = 3 ; min_cp = 0x800 ; }
79+ else if ((c & 0xF8 ) == 0xF0 ) { len = 4 ; min_cp = 0x10000 ; }
80+ else { out += ' ?' ; ++i; continue ; }
81+
82+ if (i + len > n) { out += ' ?' ; ++i; continue ; }
83+ uint32_t cp = c & (0x7Fu >> len);
84+ bool ok = true ;
85+ for (std::size_t k = 1 ; k < len; ++k) {
86+ const unsigned char cc = static_cast <unsigned char >(in[i + k]);
87+ if ((cc & 0xC0 ) != 0x80 ) { ok = false ; break ; }
88+ cp = (cp << 6 ) | (cc & 0x3Fu );
89+ }
90+ if (!ok || cp < min_cp || cp > 0x10FFFFu || (cp >= 0xD800u && cp <= 0xDFFFu )) {
91+ out += ' ?' ;
92+ ++i;
93+ continue ;
94+ }
95+ out.append (in, i, len);
96+ i += len;
97+ }
98+ return out;
99+ }
100+
60101static bool NodeHasImmediateContent (ViewNode *node) {
61102 const auto &table = node->table ();
62103 const auto &binary = node->binary ();
@@ -172,7 +213,7 @@ static Json TableToJson(const TableViewDataPtr &table, const ViewNodeDumpOptions
172213 if (IncludeTableHeaders (options)) {
173214 j[" headers" ] = Json::array ();
174215 for (const auto &header : table->headers ) {
175- j[" headers" ].push_back (header->data );
216+ j[" headers" ].push_back (ToUtf8Safe ( header->data ) );
176217 }
177218 }
178219
@@ -194,13 +235,13 @@ static Json TableToJson(const TableViewDataPtr &table, const ViewNodeDumpOptions
194235 r[" values" ] = Json::array ();
195236 r[" cells" ] = Json::object ();
196237 for (const auto &item : row->items ) {
197- r[" values" ].push_back (item->data );
238+ r[" values" ].push_back (ToUtf8Safe ( item->data ) );
198239 }
199240 for (size_t col = 0 ; col < row->items .size (); ++col) {
200241 const std::string key = col < table->headers .size ()
201- ? table->headers [col]->data
242+ ? ToUtf8Safe ( table->headers [col]->data )
202243 : (" column" + std::to_string (col));
203- r[" cells" ][key] = row->items [col]->data ;
244+ r[" cells" ][key] = ToUtf8Safe ( row->items [col]->data ) ;
204245 }
205246 if (row->size > 0 ) {
206247 r[" byteLength" ] = row->size ;
@@ -251,9 +292,9 @@ static Json NodeToJson(ViewNode *node,
251292 size_t child_index = 0 ) {
252293 node->Init ();
253294 Json j;
254- j[" name" ] = node->GetDisplayName ();
295+ j[" name" ] = ToUtf8Safe ( node->GetDisplayName () );
255296 j[" depth" ] = depth;
256- j[" path" ] = JoinPath (path_segments);
297+ j[" path" ] = ToUtf8Safe ( JoinPath (path_segments) );
257298 j[" kind" ] = ClassifyNodeKind (node);
258299 j[" childIndex" ] = child_index;
259300
0 commit comments