diff --git a/src/Exporter.php b/src/Exporter.php index dd878b7..d7893ba 100644 --- a/src/Exporter.php +++ b/src/Exporter.php @@ -28,11 +28,16 @@ use function is_resource; use function is_string; use function mb_strlen; use function mb_substr; +use function ord; use function preg_match; +use function preg_match_all; +use function preg_replace_callback; use function spl_object_id; use function sprintf; +use function str_contains; use function str_repeat; use function str_replace; +use function strlen; use function strpbrk; use function strtr; use function var_export; @@ -437,21 +442,44 @@ final readonly class Exporter private function exportString(string $value): string { // Match for most non-printable chars somewhat taking multibyte chars into account - if (preg_match('/[^\x09-\x0d\x1b\x20-\xff]/', $value) === 1) { + $unprintableCount = preg_match_all('/[^\x09-\x0d\x1b\x20-\xff]/', $value); + + if ($unprintableCount === false || $unprintableCount === 0) { + return "'" . + strtr( + $value, + [ + "\r\n" => '\r\n' . "\n", + "\n\r" => '\n\r' . "\n", + "\r" => '\r' . "\n", + "\n" => '\n' . "\n", + ], + ) . + "'"; + } + + // A NUL byte or a high ratio of unprintable bytes signals truly + // binary data; keep the compact hex dump in those cases. + if (str_contains($value, "\x00") || ($unprintableCount / strlen($value)) > 0.3) { return 'Binary String: 0x' . bin2hex($value); } - return "'" . - strtr( + // Mostly printable: keep printable bytes visible and escape only + // the offending ones inline using PHP-style \xNN escapes. + return 'Binary String: "' . + preg_replace_callback( + '/[\x00-\x1f\x7f"\\\\]/', + static fn (array $m): string => match ($m[0]) { + "\t" => '\t', + "\n" => '\n', + "\r" => '\r', + '"' => '\"', + '\\' => '\\\\', + default => sprintf('\x%02x', ord($m[0])), + }, $value, - [ - "\r\n" => '\r\n' . "\n", - "\n\r" => '\n\r' . "\n", - "\r" => '\r' . "\n", - "\n" => '\n' . "\n", - ], ) . - "'"; + '"'; } /** diff --git a/tests/ExporterTest.php b/tests/ExporterTest.php index 15a889a..ca3682a 100644 --- a/tests/ExporterTest.php +++ b/tests/ExporterTest.php @@ -239,6 +239,26 @@ EOF, 'Binary String: 0x0009', 0, ], + 'mostly printable string with a single control byte' => [ + 'Hello' . chr(0x01) . 'World', + 'Binary String: "Hello\x01World"', + 0, + ], + 'mostly printable string with several control bytes' => [ + 'BEGIN' . chr(0x01) . 'DATA' . chr(0x1F) . 'END', + 'Binary String: "BEGIN\x01DATA\x1fEND"', + 0, + ], + 'mixed string with embedded double quote and backslash' => [ + 'a"b\\c' . chr(0x07) . 'd', + 'Binary String: "a\"b\\\\c\x07d"', + 0, + ], + 'mixed string preserves UTF-8 high bytes' => [ + 'café' . chr(0x01), + 'Binary String: "café\x01"', + 0, + ], [ '', "''",