Repository navigation
fix: harden object store metadata and reads - #67
Conversation
There was a problem hiding this comment.
Clean fix for four related integrity issues. The base64url encoding is reversible, legacy fallback reads preserve backward compat, and the migration-on-write strategy is pragmatic. Read validation (bytes, chunks, digest) is thorough. Left a few inline notes, none blocking.
There was a problem hiding this comment.
Review — fix: harden object store metadata and reads
Solid PR covering several serious integrity and collision issues. Here are the findings:
1. Base64url padding — potential incompatibility with Go client
encodeName() uses strtr(base64_encode($name), '+/', '-_') which retains = padding.
The Go NATS client uses base64.RawURLEncoding (no padding). For the name "ab":
- PHP: subject
$O.bucket.M.YWI= - Go: subject
$O.bucket.M.YWI
This means objects created by the Go client won't be visible to the PHP client (and vice versa) for names whose base64 output contains padding.
Fix:
private function encodeName(string $name): string
{
return rtrim(strtr(base64_encode($name), '+/', '-_'), '=');
}2. updateMeta() rename doesn't leave a delete marker for the old name
When an object is renamed from A to B, updateMeta() purges the old subject without leaving a tombstone. Watchers monitoring the store won't receive a delete event for the old name — the object simply vanishes. If ObjectWatcher instances are in use, this can cause stale data on the client side.
The Go client handles this by writing a delete marker for the old name before purging.
3. putStream() drift with putRaw() in legacy cleanup logic
putRaw() uses purgeLegacyMetaSubjectIfNeeded(), but putStream() has an inline if-check that does the same thing. The TODO comment exists (L89), but it's worth fixing in this PR while the logic is fresh — otherwise it's easy to forget and introduce a subtle bug when one path changes but the other doesn't.
// putStream, L158-166 — replace with:
$this->purgeLegacyMetaSubjectIfNeeded($name, $existingMetaSubject);4. assertCompleteRead() — hash_final cannot be called twice
hash_final($this->hashContext, true) consumes the context. If assertCompleteRead() is ever called twice, we'll get a fatal error. Currently protected by the sourceExhausted guard in finishSource(), but hashContext should be nullable after finalization for defensiveness:
private ?\HashContext $hashContext;
// ...
$actualDigest = 'SHA-256=' . base64_encode(hash_final($this->hashContext, true));
$this->hashContext = null; // prevent double-finalizeThis requires removing readonly from the property, which is a minimal change.
5. Minor: import ordering in Stream.php
The JetStreamException import was added between OrderedConsumerConfig and PushConsumerInterface, breaking the alphabetical order that is consistently used throughout the rest of the file.
Positives
tryGetLastMessageForSubject()with errCode 10037 check — clean and correct approachfindObjectInfoRecordwith name verification after legacy lookup — properly handles lossy legacy encodingObjectResultintegrity validation (bytes, chunks, digest) — well-designed and in the right place (finishSource)- Test coverage is thorough — collision test, rename semantics, mid-read chunk disappearance, digest mismatch
Points 1 and 3 are the most important to address before merge. Point 2 depends on how important watchers are in practice — if nobody uses them, it can be deferred.
|
Follow-up pushed in Addressed in code:
Left unchanged intentionally:
Re-verified after the follow-up:
|
Follow-up review —
|
Summary
This hardens Object Store metadata addressing and read integrity.
updateMeta()renames remove the old logical name and reject collisions with active targetsObjectResultvalidationVerification
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --dry-run --diff src/ObjectStore/ObjectStore.php src/ObjectStore/ObjectResult.php tests/Unit/ObjectStore/ObjectResultTest.php tests/Integration/ObjectStore/ObjectStoreTest.phpvendor/bin/phpstan analyse --no-progressvendor/bin/phpunit --testsuite=unitenv -u NATS_URL vendor/bin/phpunit --testsuite=integration --stop-on-errorCloses #61
Closes #62
Closes #63
Closes #65