Skip to content

Commit c8761e6

Browse files
committed
Dumper: improved encoding of strings, added colors
1 parent b54326b commit c8761e6

5 files changed

Lines changed: 85 additions & 38 deletions

File tree

src/Framework/Dumper.php

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,6 @@ class Dumper
3333
*/
3434
public static function toLine($var): string
3535
{
36-
static $table;
37-
if ($table === null) {
38-
foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
39-
$table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
40-
}
41-
$table['\\'] = '\\\\';
42-
$table["\r"] = '\r';
43-
$table["\n"] = '\n';
44-
$table["\t"] = '\t';
45-
}
46-
4736
if (is_bool($var)) {
4837
return $var ? 'TRUE' : 'FALSE';
4938

@@ -62,9 +51,7 @@ public static function toLine($var): string
6251
} elseif (strlen($var) > self::$maxLength) {
6352
$var = substr($var, 0, self::$maxLength) . '...';
6453
}
65-
return preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $var) || preg_last_error()
66-
? '"' . strtr($var, $table) . '"'
67-
: "'$var'";
54+
return self::encodeStringLine($var);
6855

6956
} elseif (is_array($var)) {
7057
$out = '';
@@ -146,20 +133,10 @@ private static function _toPhp(&$var, array &$list = [], int $level = 0, int &$l
146133
} elseif ($var === null) {
147134
return 'null';
148135

149-
} elseif (is_string($var) && (preg_match('#[^\x09\x20-\x7E\xA0-\x{10FFFF}]#u', $var) || preg_last_error())) {
150-
static $table;
151-
if ($table === null) {
152-
foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
153-
$table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
154-
}
155-
$table['\\'] = '\\\\';
156-
$table["\r"] = '\r';
157-
$table["\n"] = '\n';
158-
$table["\t"] = '\t';
159-
$table['$'] = '\$';
160-
$table['"'] = '\"';
161-
}
162-
return '"' . strtr($var, $table) . '"';
136+
} elseif (is_string($var)) {
137+
$res = self::encodeStringPhp($var);
138+
$line += substr_count($res, "\n");
139+
return $res;
163140

164141
} elseif (is_array($var)) {
165142
$space = str_repeat("\t", $level);
@@ -242,9 +219,72 @@ private static function _toPhp(&$var, array &$list = [], int $level = 0, int &$l
242219
return '/* resource ' . get_resource_type($var) . ' */';
243220

244221
} else {
245-
$res = var_export($var, true);
246-
$line += substr_count($res, "\n");
247-
return $res;
222+
return var_export($var, true);
223+
}
224+
}
225+
226+
227+
private static function encodeStringPhp(string $s): string
228+
{
229+
static $special = [
230+
"\r" => '\r',
231+
"\n" => '\n',
232+
"\t" => "\t",
233+
"\e" => '\e',
234+
'\\' => '\\\\',
235+
];
236+
$utf8 = preg_match('##u', $s);
237+
$escaped = preg_replace_callback(
238+
$utf8 ? '#[\p{C}\\\\]#u' : '#[\x00-\x1F\x7F-\xFF\\\\]#',
239+
function ($m) use ($special) {
240+
return $special[$m[0]] ?? (strlen($m[0]) === 1
241+
? '\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT) . ''
242+
: '\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '}');
243+
},
244+
$s
245+
);
246+
return $s === str_replace('\\\\', '\\', $escaped)
247+
? "'" . preg_replace('#\'|\\\\(?=[\'\\\\]|$)#D', '\\\\$0', $s) . "'"
248+
: '"' . addcslashes($escaped, '"$') . '"';
249+
}
250+
251+
252+
private static function encodeStringLine(string $s): string
253+
{
254+
static $special = [
255+
"\r" => "\\r\r",
256+
"\n" => "\\n\n",
257+
"\t" => "\\t\t",
258+
"\e" => '\\e',
259+
"'" => "'",
260+
];
261+
$utf8 = preg_match('##u', $s);
262+
$escaped = preg_replace_callback(
263+
$utf8 ? '#[\p{C}\']#u' : '#[\x00-\x1F\x7F-\xFF\']#',
264+
function ($m) use ($special) {
265+
return "\e[22m"
266+
. ($special[$m[0]] ?? (strlen($m[0]) === 1
267+
? '\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT)
268+
: '\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '}'))
269+
. "\e[1m";
270+
},
271+
$s
272+
);
273+
return "'" . $escaped . "'";
274+
}
275+
276+
277+
private static function utf8Ord(string $c): int
278+
{
279+
$ord0 = ord($c[0]);
280+
if ($ord0 < 0x80) {
281+
return $ord0;
282+
} elseif ($ord0 < 0xE0) {
283+
return ($ord0 << 6) + ord($c[1]) - 0x3080;
284+
} elseif ($ord0 < 0xF0) {
285+
return ($ord0 << 12) + (ord($c[1]) << 6) + ord($c[2]) - 0xE2080;
286+
} else {
287+
return ($ord0 << 18) + (ord($c[1]) << 12) + (ord($c[2]) << 6) + ord($c[3]) - 0x3C82080;
248288
}
249289
}
250290

tests/Framework/Assert.match.phpt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
declare(strict_types=1);
44

55
use Tester\Assert;
6+
use Tester\Dumper;
67

78
require __DIR__ . '/../bootstrap.php';
89

@@ -92,7 +93,7 @@ foreach ($notMatches as [$expected, $actual, $expected2, $actual2]) {
9293

9394
$ex = Assert::exception(function () use ($expected, $actual) {
9495
Assert::match($expected, $actual);
95-
}, Tester\AssertException::class, "'$actual3' should match '$expected3'");
96+
}, Tester\AssertException::class, Dumper::toLine($actual3) . " should match " . Dumper::toLine($expected3));
9697

9798
Assert::same($expected2, $ex->expected);
9899
Assert::same($actual2, $ex->actual);

tests/Framework/Dumper.dumpException.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ $cases = [
2121
'Failed: NULL should not be NULL' => function () { Assert::notSame(null, null); },
2222
'Failed: boolean should be instance of x' => function () { Assert::type('x', true); },
2323
'Failed: resource should be int' => function () { Assert::type('int', fopen(__FILE__, 'r')); },
24-
"Failed: 'Hello\nWorld' should match\n ... 'Hello'" => function () { Assert::match('%a%', "Hello\nWorld"); },
24+
"Failed: 'Hello\\n\nWorld' should match\n ... 'Hello'" => function () { Assert::match('%a%', "Hello\nWorld"); },
2525
"Failed: '...xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' should be \n ... '...xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'" => function () { Assert::same(str_repeat('x', 100), str_repeat('x', 120)); },
2626
"Failed: '...xxxxxxxxxxxxxxxxxxxxxxxxxxx****************************************' should be \n ... '...xxxxxxxxxxxxxxxxxxxxxxxxxxx'" => function () { Assert::same(str_repeat('x', 30), str_repeat('x', 30) . str_repeat('*', 40)); },
2727
"Failed: 'xxxxx*****************************************************************...' should be \n ... 'xxxxx'" => function () { Assert::same(str_repeat('x', 5), str_repeat('x', 5) . str_repeat('*', 90)); },

tests/Framework/Dumper.toLine.phpt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ Assert::match('NAN', Dumper::toLine(NAN));
2121
Assert::match("''", Dumper::toLine(''));
2222
Assert::match("' '", Dumper::toLine(' '));
2323
Assert::match("'0'", Dumper::toLine('0'));
24-
Assert::match('"\\x00"', Dumper::toLine("\x00"));
25-
Assert::match("' '", Dumper::toLine("\t"));
26-
Assert::match('"\\xff"', Dumper::toLine("\xFF"));
27-
Assert::match("'multi\nline'", Dumper::toLine("multi\nline"));
24+
Assert::match("'\e[22m\\x00\e[1m'", Dumper::toLine("\x00"));
25+
Assert::match("'\e[22m\\u{FEFF}\e[1m'", Dumper::toLine("\xEF\xBB\xBF")); // BOM
26+
Assert::match("'\e[22m\\t\t\e[1m'", Dumper::toLine("\t"));
27+
Assert::match("'\e[22m\\xFF\e[1m'", Dumper::toLine("\xFF"));
28+
Assert::match("'multi\e[22m\\n\n\e[1mline'", Dumper::toLine("multi\nline"));
2829
Assert::match("'Iñtërnâtiônàlizætiøn'", Dumper::toLine("I\xc3\xb1t\xc3\xabrn\xc3\xa2ti\xc3\xb4n\xc3\xa0liz\xc3\xa6ti\xc3\xb8n"));
2930
Assert::match('resource(stream)', Dumper::toLine(fopen(__FILE__, 'r')));
3031
Assert::match('stdClass(#%a%)', Dumper::toLine((object) [1, 2]));

tests/Framework/Dumper.toPhp.phpt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ Assert::match("''", Dumper::toPhp(''));
2929
Assert::match("' '", Dumper::toPhp(' '));
3030
Assert::match("'0'", Dumper::toPhp('0'));
3131
Assert::match('"\\x00"', Dumper::toPhp("\x00"));
32+
Assert::match('"\u{FEFF}"', Dumper::toPhp("\xEF\xBB\xBF")); // BOM
3233
Assert::match("' '", Dumper::toPhp("\t"));
33-
Assert::match('"\\xff"', Dumper::toPhp("\xFF"));
34+
Assert::match('"\\xFF"', Dumper::toPhp("\xFF"));
3435
Assert::match('"multi\nline"', Dumper::toPhp("multi\nline"));
3536
Assert::match("'Iñtërnâtiônàlizætiøn'", Dumper::toPhp("I\xc3\xb1t\xc3\xabrn\xc3\xa2ti\xc3\xb4n\xc3\xa0liz\xc3\xa6ti\xc3\xb8n"));
3637
Assert::match('[
@@ -41,6 +42,10 @@ Assert::match('[
4142
[1 => 1, 2, 3, 4, 5, 6, 7, \'abcdefgh\'],
4243
]', Dumper::toPhp([1, 'hello', "\r" => [], [1, 2], [1 => 1, 2, 3, 4, 5, 6, 7, 'abcdefgh']]));
4344

45+
Assert::match('\'$"\\\\\'', Dumper::toPhp('$"\\'));
46+
Assert::match('\'$"\\ \x00\'', Dumper::toPhp('$"\\ \x00'));
47+
Assert::match('"\\$\\"\\\\ \x00"', Dumper::toPhp("$\"\\ \x00"));
48+
4449
Assert::match('/* resource stream */', Dumper::toPhp(fopen(__FILE__, 'r')));
4550
Assert::match('(object) /* #%a% */ []', Dumper::toPhp((object) null));
4651
Assert::match("(object) /* #%a% */ [

0 commit comments

Comments
 (0)