@@ -1272,16 +1272,190 @@ namespace nfx::string
12721272 // JSON escape/unescape
12731273 // -----------------------------
12741274
1275- inline std::string jsonEscape ( std::string_view str )
1275+ // -----------------------------
1276+ // UTF-8 utilities
1277+ // -----------------------------
1278+
1279+ /* *
1280+ * @brief Decode a UTF-8 sequence starting at position i
1281+ * @param str Input string
1282+ * @param i Current position (will be updated to point after the sequence)
1283+ * @param codepoint Output Unicode code point
1284+ * @return true if valid UTF-8 sequence was decoded, false otherwise
1285+ */
1286+ inline bool decodeUtf8Codepoint ( std::string_view str, std::size_t & i, uint32_t & codepoint ) noexcept
1287+ {
1288+ if ( i >= str.size () )
1289+ return false ;
1290+
1291+ unsigned char c0 = static_cast <unsigned char >( str[i] );
1292+
1293+ // 1-byte sequence (ASCII): 0xxxxxxx
1294+ if ( c0 <= 0x7F )
1295+ {
1296+ codepoint = c0;
1297+ ++i;
1298+ return true ;
1299+ }
1300+
1301+ // 2-byte sequence: 110xxxxx 10xxxxxx
1302+ if ( ( c0 & 0xE0 ) == 0xC0 )
1303+ {
1304+ if ( i + 1 >= str.size () )
1305+ return false ;
1306+ unsigned char c1 = static_cast <unsigned char >( str[i + 1 ] );
1307+ if ( ( c1 & 0xC0 ) != 0x80 )
1308+ return false ;
1309+
1310+ codepoint = ( ( c0 & 0x1F ) << 6 ) | ( c1 & 0x3F );
1311+ i += 2 ;
1312+ return codepoint >= 0x80 ; // Check for overlong encoding
1313+ }
1314+
1315+ // 3-byte sequence: 1110xxxx 10xxxxxx 10xxxxxx
1316+ if ( ( c0 & 0xF0 ) == 0xE0 )
1317+ {
1318+ if ( i + 2 >= str.size () )
1319+ return false ;
1320+ unsigned char c1 = static_cast <unsigned char >( str[i + 1 ] );
1321+ unsigned char c2 = static_cast <unsigned char >( str[i + 2 ] );
1322+ if ( ( c1 & 0xC0 ) != 0x80 || ( c2 & 0xC0 ) != 0x80 )
1323+ return false ;
1324+
1325+ codepoint = ( ( c0 & 0x0F ) << 12 ) | ( ( c1 & 0x3F ) << 6 ) | ( c2 & 0x3F );
1326+ i += 3 ;
1327+ return codepoint >= 0x800 && ( codepoint < 0xD800 || codepoint > 0xDFFF ); // Check overlong and surrogates
1328+ }
1329+
1330+ // 4-byte sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
1331+ if ( ( c0 & 0xF8 ) == 0xF0 )
1332+ {
1333+ if ( i + 3 >= str.size () )
1334+ return false ;
1335+ unsigned char c1 = static_cast <unsigned char >( str[i + 1 ] );
1336+ unsigned char c2 = static_cast <unsigned char >( str[i + 2 ] );
1337+ unsigned char c3 = static_cast <unsigned char >( str[i + 3 ] );
1338+ if ( ( c1 & 0xC0 ) != 0x80 || ( c2 & 0xC0 ) != 0x80 || ( c3 & 0xC0 ) != 0x80 )
1339+ return false ;
1340+
1341+ codepoint = ( ( c0 & 0x07 ) << 18 ) | ( ( c1 & 0x3F ) << 12 ) | ( ( c2 & 0x3F ) << 6 ) | ( c3 & 0x3F );
1342+ i += 4 ;
1343+ return codepoint >= 0x10000 && codepoint <= 0x10FFFF ; // Check overlong and valid range
1344+ }
1345+
1346+ // Invalid UTF-8 sequence
1347+ return false ;
1348+ }
1349+
1350+ /* *
1351+ * @brief Encode a Unicode code point to UTF-8
1352+ * @param result Output string to append UTF-8 bytes
1353+ * @param codepoint Unicode code point to encode
1354+ */
1355+ inline void encodeUtf8Codepoint ( std::string& result, uint32_t codepoint ) noexcept
1356+ {
1357+ if ( codepoint <= 0x7F )
1358+ {
1359+ // 1-byte UTF-8
1360+ result += static_cast <char >( codepoint );
1361+ }
1362+ else if ( codepoint <= 0x7FF )
1363+ {
1364+ // 2-byte UTF-8
1365+ result += static_cast <char >( 0xC0 | ( codepoint >> 6 ) );
1366+ result += static_cast <char >( 0x80 | ( codepoint & 0x3F ) );
1367+ }
1368+ else if ( codepoint <= 0xFFFF )
1369+ {
1370+ // 3-byte UTF-8
1371+ result += static_cast <char >( 0xE0 | ( codepoint >> 12 ) );
1372+ result += static_cast <char >( 0x80 | ( ( codepoint >> 6 ) & 0x3F ) );
1373+ result += static_cast <char >( 0x80 | ( codepoint & 0x3F ) );
1374+ }
1375+ else if ( codepoint <= 0x10FFFF )
1376+ {
1377+ // 4-byte UTF-8
1378+ result += static_cast <char >( 0xF0 | ( codepoint >> 18 ) );
1379+ result += static_cast <char >( 0x80 | ( ( codepoint >> 12 ) & 0x3F ) );
1380+ result += static_cast <char >( 0x80 | ( ( codepoint >> 6 ) & 0x3F ) );
1381+ result += static_cast <char >( 0x80 | ( codepoint & 0x3F ) );
1382+ }
1383+ }
1384+
1385+ // -----------------------------
1386+ // JSON-specific utilities
1387+ // -----------------------------
1388+
1389+ namespace detail
1390+ {
1391+ /* *
1392+ * @brief Encode a Unicode code point as JSON escape sequence
1393+ * @param result Output string
1394+ * @param codepoint Unicode code point to encode
1395+ */
1396+ inline void encodeJsonEscapeSequence ( std::string& result, uint32_t codepoint ) noexcept
1397+ {
1398+ constexpr char hex[] = " 0123456789abcdef" ;
1399+
1400+ if ( codepoint <= 0xFFFF )
1401+ {
1402+ // Basic Multilingual Plane: encode as \uXXXX
1403+ result += " \\ u" ;
1404+ result += hex[( codepoint >> 12 ) & 0xF ];
1405+ result += hex[( codepoint >> 8 ) & 0xF ];
1406+ result += hex[( codepoint >> 4 ) & 0xF ];
1407+ result += hex[codepoint & 0xF ];
1408+ }
1409+ else
1410+ {
1411+ // Supplementary plane: encode as UTF-16 surrogate pair \uXXXX\uXXXX
1412+ codepoint -= 0x10000 ;
1413+ uint32_t high = 0xD800 + ( ( codepoint >> 10 ) & 0x3FF );
1414+ uint32_t low = 0xDC00 + ( codepoint & 0x3FF );
1415+
1416+ // High surrogate
1417+ result += " \\ u" ;
1418+ result += hex[( high >> 12 ) & 0xF ];
1419+ result += hex[( high >> 8 ) & 0xF ];
1420+ result += hex[( high >> 4 ) & 0xF ];
1421+ result += hex[high & 0xF ];
1422+
1423+ // Low surrogate
1424+ result += " \\ u" ;
1425+ result += hex[( low >> 12 ) & 0xF ];
1426+ result += hex[( low >> 8 ) & 0xF ];
1427+ result += hex[( low >> 4 ) & 0xF ];
1428+ result += hex[low & 0xF ];
1429+ }
1430+ }
1431+ } // namespace detail
1432+
1433+ inline std::string jsonEscape ( std::string_view str, bool escapeNonAscii )
12761434 {
12771435 std::string result;
12781436 result.reserve ( str.size () * 2 ); // Reserve for common case
12791437
12801438 // Hex digits for Unicode escape sequences
12811439 constexpr char hex_digits[] = " 0123456789ABCDEF" ;
12821440
1283- for ( unsigned char c : str )
1441+ for ( std:: size_t i = 0 ; i < str. size (); )
12841442 {
1443+ unsigned char c = static_cast <unsigned char >( str[i] );
1444+
1445+ // Check if this is a non-ASCII UTF-8 sequence that needs escaping
1446+ if ( escapeNonAscii && c > 0x7F )
1447+ {
1448+ uint32_t codepoint;
1449+ std::size_t oldPos = i;
1450+ if ( decodeUtf8Codepoint ( str, i, codepoint ) )
1451+ {
1452+ detail::encodeJsonEscapeSequence ( result, codepoint );
1453+ continue ;
1454+ }
1455+ // If UTF-8 decoding failed, restore position and fall through to byte escape
1456+ i = oldPos;
1457+ }
1458+
12851459 switch ( c )
12861460 {
12871461 case ' \" ' :
@@ -1322,6 +1496,8 @@ namespace nfx::string
13221496 }
13231497 break ;
13241498 }
1499+
1500+ ++i;
13251501 }
13261502
13271503 return result;
@@ -1332,40 +1508,69 @@ namespace nfx::string
13321508 std::string result;
13331509 result.reserve ( str.size () );
13341510
1335- auto parseUnicodeEscape = [&]( std::size_t pos ) -> bool {
1336- if ( pos + 5 >= str.size () )
1511+ auto parseUnicodeEscape = [&]( std::size_t & i ) -> bool {
1512+ if ( i + 5 >= str.size () )
13371513 {
13381514 return false ; // Not enough characters for \uXXXX
13391515 }
13401516
13411517 // Parse 4 hex digits
1342- int value = 0 ;
1518+ uint32_t value = 0 ;
13431519 for ( int j = 0 ; j < 4 ; ++j )
13441520 {
1345- int digit = hexToInt ( str[pos + 2 + j] );
1521+ int digit = hexToInt ( str[i + 2 + j] );
13461522 if ( digit == -1 )
13471523 {
13481524 return false ; // Invalid hex digit
13491525 }
1350- value = ( value << 4 ) | digit;
1526+ value = ( value << 4 ) | static_cast < uint32_t >( digit ) ;
13511527 }
13521528
1353- // Convert Unicode codepoint to UTF-8
1354- if ( value <= 0x7F )
1529+ i += 6 ; // Skip \uXXXX
1530+
1531+ // Check for UTF-16 surrogate pair
1532+ if ( value >= 0xD800 && value <= 0xDBFF )
13551533 {
1356- result += static_cast <char >( value );
1534+ // High surrogate - expect low surrogate to follow
1535+ if ( i + 5 >= str.size () || str[i] != ' \\ ' || str[i + 1 ] != ' u' )
1536+ {
1537+ return false ; // Invalid surrogate pair
1538+ }
1539+
1540+ // Parse low surrogate
1541+ uint32_t lowSurrogate = 0 ;
1542+ for ( int j = 0 ; j < 4 ; ++j )
1543+ {
1544+ int digit = hexToInt ( str[i + 2 + j] );
1545+ if ( digit == -1 )
1546+ {
1547+ return false ;
1548+ }
1549+ lowSurrogate = ( lowSurrogate << 4 ) | static_cast <uint32_t >( digit );
1550+ }
1551+
1552+ if ( lowSurrogate < 0xDC00 || lowSurrogate > 0xDFFF )
1553+ {
1554+ return false ; // Invalid low surrogate
1555+ }
1556+
1557+ i += 6 ; // Skip second \uXXXX
1558+
1559+ // Combine surrogates to get codepoint
1560+ uint32_t codepoint = 0x10000 + ( ( ( value - 0xD800 ) << 10 ) | ( lowSurrogate - 0xDC00 ) );
1561+ encodeUtf8Codepoint ( result, codepoint );
13571562 }
1358- else if ( value <= 0x7FF )
1563+ else if ( value >= 0xDC00 && value <= 0xDFFF )
13591564 {
1360- result += static_cast < char >( 0xC0 | ( value >> 6 ) );
1361- result += static_cast < char >( 0x80 | ( value & 0x3F ) ) ;
1565+ // Unexpected low surrogate
1566+ return false ;
13621567 }
13631568 else
13641569 {
1365- result += static_cast <char >( 0xE0 | ( value >> 12 ) );
1366- result += static_cast <char >( 0x80 | ( ( value >> 6 ) & 0x3F ) );
1367- result += static_cast <char >( 0x80 | ( value & 0x3F ) );
1570+ // Regular BMP codepoint
1571+ encodeUtf8Codepoint ( result, value );
13681572 }
1573+
13691574 return true ;
13701575 };
13711576
@@ -1419,7 +1624,7 @@ namespace nfx::string
14191624 {
14201625 return " " ; // Invalid Unicode escape
14211626 }
1422- i += 6 ; // Skip \uXXXX
1627+ // i is already advanced by parseUnicodeEscape
14231628 break ;
14241629 default :
14251630 return " " ; // Invalid escape sequence
0 commit comments