Skip to content

Commit f375d00

Browse files
committed
Apply fixed Codex review.
1 parent 058381b commit f375d00

6 files changed

Lines changed: 192 additions & 14 deletions

File tree

‎src/Storage/DebugValue.php‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,10 @@ private static function normalize(
457457
'key' => $key,
458458
'value' => self::normalize($entry, $depth + 1, $nodes, $objects),
459459
];
460+
461+
if ($nodes > self::MAX_NODES) {
462+
break;
463+
}
460464
}
461465

462466
return new self(
@@ -483,6 +487,10 @@ private static function normalize(
483487
'key' => $key,
484488
'value' => self::normalize($entry, $depth + 1, $nodes, $objects),
485489
];
490+
491+
if ($nodes > self::MAX_NODES) {
492+
break;
493+
}
486494
}
487495

488496
$objects->offsetUnset($value);
@@ -527,17 +535,17 @@ private static function objectLabel(object $value): string
527535
$class = $value::class;
528536
$message = $value->getMessage();
529537

530-
return "{$class}: {$message}";
538+
return Json::safeString($class) . ': ' . Json::safeString($message);
531539
}
532540

533541
if ($value instanceof Stringable) {
534542
try {
535-
return (string) $value;
543+
return Json::safeString((string) $value);
536544
} catch (Throwable) {
537545
// Fall through to the class name when userland string conversion fails.
538546
}
539547
}
540548

541-
return $value::class;
549+
return Json::safeString($value::class);
542550
}
543551
}

‎src/Storage/ExceptionSnapshot.php‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,12 @@ public static function fromThrowable(Throwable $throwable): self
172172
$args = is_array($entry['args'] ?? null) ? $entry['args'] : [];
173173

174174
$trace[] = [
175-
'namespace' => self::namespacePart($class),
176-
'short_class' => self::shortName($class),
177-
'class' => $class,
178-
'type' => is_string($entry['type'] ?? null) ? $entry['type'] : '',
179-
'function' => $entry['function'],
180-
'file' => is_string($entry['file'] ?? null) ? $entry['file'] : null,
175+
'namespace' => Json::safeString(self::namespacePart($class)),
176+
'short_class' => Json::safeString(self::shortName($class)),
177+
'class' => Json::safeString($class),
178+
'type' => Json::safeString(is_string($entry['type'] ?? null) ? $entry['type'] : ''),
179+
'function' => Json::safeString($entry['function']),
180+
'file' => is_string($entry['file'] ?? null) ? Json::safeString($entry['file']) : null,
181181
'line' => is_int($entry['line'] ?? null) ? $entry['line'] : null,
182182
'args' => DebugArray::capture($args),
183183
];
@@ -186,13 +186,13 @@ public static function fromThrowable(Throwable $throwable): self
186186
$code = $throwable->getCode();
187187

188188
return new self(
189-
class: $throwable::class,
190-
message: $throwable->getMessage(),
189+
class: Json::safeString($throwable::class),
190+
message: Json::safeString($throwable->getMessage()),
191191
code: $code,
192-
file: $throwable->getFile(),
192+
file: Json::safeString($throwable->getFile()),
193193
line: $throwable->getLine(),
194194
trace: $trace,
195-
toString: (string) $throwable,
195+
toString: Json::safeString((string) $throwable),
196196
previous: $throwable->getPrevious() !== null ? self::fromThrowable($throwable->getPrevious()) : null,
197197
);
198198
}

‎src/Storage/Json.php‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPForge\Debug\Storage;
6+
7+
use function base64_encode;
8+
use function mb_check_encoding;
9+
use function sprintf;
10+
11+
/**
12+
* Converts strings to JSON-safe debug storage representations.
13+
*/
14+
final class Json
15+
{
16+
/**
17+
* Prevents instantiation of this static helper.
18+
*/
19+
private function __construct() {}
20+
21+
/**
22+
* Returns valid UTF-8, representing binary text as base64.
23+
*
24+
* Usage example:
25+
*
26+
* ```php
27+
* $label = \PHPForge\Debug\Storage\Json::safeString("\xB1\x31");
28+
* ```
29+
*
30+
* @param string $value String to normalize.
31+
*
32+
* @return string JSON-safe string.
33+
*/
34+
public static function safeString(string $value): string
35+
{
36+
return mb_check_encoding($value, 'UTF-8')
37+
? $value
38+
: sprintf('(binary: base64 %s)', base64_encode($value));
39+
}
40+
}

‎tests/Storage/DebugValueTest.php‎

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,75 @@ public function testCaptureLabelsAThrowableWithItsMessage(): void
123123
);
124124
}
125125

126+
public function testCaptureNormalizesInvalidUtf8StringableLabel(): void
127+
{
128+
$value = DebugValue::capture(
129+
new class implements Stringable {
130+
/**
131+
* Returns a binary fixture label.
132+
*
133+
* @return string Binary fixture label.
134+
*/
135+
public function __toString(): string
136+
{
137+
return "\xB1\x31";
138+
}
139+
},
140+
);
141+
142+
self::assertSame(
143+
'(binary: base64 sTE=)',
144+
$value->value,
145+
'A binary Stringable label must be represented as base64.',
146+
);
147+
self::assertJson(
148+
json_encode($value, JSON_THROW_ON_ERROR),
149+
'A binary Stringable label must remain JSON-safe.',
150+
);
151+
}
152+
153+
public function testCaptureNormalizesInvalidUtf8ThrowableMessage(): void
154+
{
155+
$value = DebugValue::capture(new RuntimeException("\xB1\x31"));
156+
157+
self::assertSame(
158+
RuntimeException::class . ': (binary: base64 sTE=)',
159+
$value->value,
160+
'A binary throwable message must be represented as base64.',
161+
);
162+
self::assertJson(
163+
json_encode($value, JSON_THROW_ON_ERROR),
164+
'A binary throwable message must remain JSON-safe.',
165+
);
166+
}
167+
168+
public function testCaptureStopsTraversingObjectBeyondTheNodeLimit(): void
169+
{
170+
$object = new stdClass();
171+
172+
for ($index = 0; $index < 10_050; $index++) {
173+
$object->{"property-{$index}"} = $index;
174+
}
175+
176+
$value = DebugValue::capture($object);
177+
$entries = $value->jsonSerialize()['entries'] ?? null;
178+
179+
self::assertIsArray(
180+
$entries,
181+
'A captured object must contain serialized entries.',
182+
);
183+
self::assertCount(
184+
10_000,
185+
$entries,
186+
'Object traversal must stop after recording the node-limit marker.',
187+
);
188+
self::assertStringContainsString(
189+
'SKIPPED over 10000 nodes',
190+
$this->flatten($value),
191+
'Object values past the node budget must be truncated.',
192+
);
193+
}
194+
126195
public function testCaptureStringifiesAStringableObject(): void
127196
{
128197
$value = DebugValue::capture(
@@ -163,9 +232,21 @@ public function testCaptureTruncatesBeyondTheDepthLimit(): void
163232

164233
public function testCaptureTruncatesBeyondTheNodeLimit(): void
165234
{
235+
$value = DebugValue::capture(range(1, 10_050));
236+
$entries = $value->jsonSerialize()['entries'] ?? null;
237+
238+
self::assertIsArray(
239+
$entries,
240+
'A captured array must contain serialized entries.',
241+
);
242+
self::assertCount(
243+
10_000,
244+
$entries,
245+
'Traversal must stop after recording the node-limit marker.',
246+
);
166247
self::assertStringContainsString(
167248
'SKIPPED over 10000 nodes',
168-
$this->flatten(DebugValue::capture(range(1, 10_050))),
249+
$this->flatten($value),
169250
'Values past the node budget must be truncated.',
170251
);
171252
}

‎tests/Storage/ExceptionSnapshotTest.php‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ public function testThrowableRoundTripsThroughJson(): void
8989
);
9090
}
9191

92+
public function testThrowableWithInvalidUtf8MessageRemainsJsonSafe(): void
93+
{
94+
$snapshot = ExceptionSnapshot::fromThrowable(new RuntimeException("\xB1\x31"));
95+
96+
self::assertSame(
97+
'(binary: base64 sTE=)',
98+
$snapshot->getMessage(),
99+
'A binary throwable message must be represented as base64.',
100+
);
101+
self::assertJson(
102+
json_encode($snapshot, JSON_THROW_ON_ERROR),
103+
'A binary throwable message must not break snapshot serialization.',
104+
);
105+
}
106+
92107
public function testThrowHydrationExceptionForInvalidCodeType(): void
93108
{
94109
$payload = ExceptionSnapshot::fromThrowable(new RuntimeException('failure'))

‎tests/Storage/JsonTest.php‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPForge\Debug\Tests\Storage;
6+
7+
use PHPForge\Debug\Storage\Json;
8+
use PHPUnit\Framework\Attributes\Group;
9+
use PHPUnit\Framework\TestCase;
10+
11+
/**
12+
* Unit tests for {@see Json} covering UTF-8 preservation and binary representation.
13+
*/
14+
#[Group('storage')]
15+
final class JsonTest extends TestCase
16+
{
17+
public function testSafeStringPreservesUtf8(): void
18+
{
19+
self::assertSame(
20+
'Debug failure.',
21+
Json::safeString('Debug failure.'),
22+
'Valid UTF-8 must remain unchanged.',
23+
);
24+
}
25+
26+
public function testSafeStringRepresentsBinaryAsBase64(): void
27+
{
28+
self::assertSame(
29+
'(binary: base64 sTE=)',
30+
Json::safeString("\xB1\x31"),
31+
'Binary text must be represented as base64.',
32+
);
33+
}
34+
}

0 commit comments

Comments
 (0)