Releases: iliaal/phpser
Releases · iliaal/phpser
0.2.0
Added
- PHP 8.2 support (lowered the minimum from 8.3).
Changed
- Decode resolves dictionary strings against the engine's interned-string table; interned hits skip the per-slot allocation and all refcount traffic. Rowset decode ~9%, DTO decode ~11-14% faster (arm64, median of 12).
- Object decode installs declared properties straight into property slots via
ce->properties_infoinstead of materializing each object's properties HashTable; DTO decode a further ~22-25% faster, and decoded objects no longer carry the materialized table. - Typed-string run encode fuses the eligibility scan with emission (one intern-cache probe per element instead of two); rowset encode ~6% faster, wire bytes unchanged.
- Object encode emits properties in a single pass with a back-patched count instead of a count walk plus an emit walk; DTO encode ~4% faster, wire bytes unchanged.
- Packed-array encode (numeric, double, and typed-string runs) now reserves the whole run's worst-case output capacity once, then writes elements raw, instead of running a
smart_strcapacity check per element. On small numeric arrays — where that per-element check was a large fraction of the total work — this cuts encode time ~26% (packed_1k); rowsets, whose tag-arrays travel the typed-string run, encode ~2% faster. The mixed-run path is unchanged (its recursion can reallocate the buffer mid-loop). Wire format and decode output are identical. - The typed-string packed run now reads each element's dictionary index straight from the intern-cache slot rather than re-walking the intern path, which
detect_packed_runhas already shown is unnecessary (every element is proven dict-bound before the run is chosen). Rowset encode is a further ~5-6% faster on top of the reserve-once change. Decode and wire bytes unchanged.
Fixed
allowed_classesoptions carrying PHP references now behave like nativeunserialize(). Previously a reference-wrapped option value (['allowed_classes' => &$flag]) or an allowlist entry left referenced by aforeach (... as &$c)loop threw a spurious ValueError/TypeError instead of applying the filter. Both cases failed closed (the decode never ran), so this is a compatibility fix, not a security one.- A crafted payload with a canonical numeric string array key (
"5") now decodes to the integer key5, matching nativeunserialize()and every PHP array write. The untrusted decode path previously preserved it as a string key, a HashTable state no PHP code can produce — letting an attacker smuggle a value pastisset()/array_key_exists()checks that assume the key was already coerced. The HMAC-signed path was never affected (the encoder only ever emits integer keys for numeric strings). - A
__serialize()that returns a non-array without throwing now raises aTypeError, matching native PHP, instead of silently encoding the object asnull. The previous behavior shipped a valid payload that decoded tonullin the object's place with no error at write time. - Decoding a payload whose deferred
__wakeup()/__unserialize()throws no longer leaks the decoded object graph. The session decode handler now receives a cleared result on this error path, restoring the decoder's documented "result is null on failure" contract.
0.1.2
Security
phpser_serialize_signed()andphpser_unserialize_signed()now reject an empty signing key instead of accepting it. An empty key reduces HMAC-SHA256 to a fixed, keyless tag that anyone can recompute, so a misconfigured caller (e.g.getenv('SECRET') ?: ''with the variable unset) would silently emit and accept forgeable payloads — defeating the signed path's only purpose. Both entry points now throw before any HMAC work.
Changed
- Encode is now faster than igbinary across the whole benchmark suite (−14% to −70% depending on shape), where it previously lagged on small rowsets (+30%) and object-heavy payloads (+42%). The encoder's intern fast path is now an O(1) open-addressed pointer hash instead of a fixed linear ring, so unique value strings (names, emails, SKUs) no longer pay a linear-scan miss on every occurrence; varint emission reserves its worst-case byte count once rather than a capacity check per byte. The wire format and decode output are unchanged.
dto_mixed-style payloads (objects interleaved with arrays) are now ~17% smaller than igbinary, where they were ~33% larger. The encode intern window was widened so repeated value strings (timestamps, status enums, currency codes) graduate into the front-loaded dictionary instead of re-emitting inline on every row.- Plain objects with no dynamic-property table now serialize directly from their declared property slots instead of materializing a properties HashTable, the way native
serialize()does — faster one-shot (fresh object) encode. PHP 8.4 lazy objects fall back toget_properties()so their initializer runs before serialization. phpser_unserialize_signed()decodes associative arrays withadd_newinstead ofupdate: the HMAC proves the payload came from this extension's encoder (unique keys), so the per-key duplicate check is skipped. The unsigned path keeps last-write-wins collapse for untrusted input.
0.1.1
Changed
phpser_serialize()andphpser_serialize_signed()now throw an exception when the value nests deeper than the recursion cap (512) instead of silently emitting a truncated payload thatphpser_unserialize()could not decode (it returnednull, losing all data). The session serialize handler degrades to anE_WARNINGand skips the write rather than throwing during request shutdown.
Fixed
- Session serialize handler: a brand-new (empty) session is no longer rejected. The engine reads back an empty string for a fresh session and phpser's decoder treated it as malformed, so every first request emitted "Failed to decode session object" and destroyed the session. Empty input now decodes as an empty session, matching PHP's native serializers.
- The
allowed_classesTypeErrorraised byphpser_unserialize_signed()now names that function rather thanphpser_unserialize(). __unserialize()and__wakeup()are now selected from the class definition rather than the on-wire object form, matching nativeunserialize(). A class that defines__unserialize()but not__serialize()is now rebuilt through__unserialize()instead of by raw property writes, and a class with__serialize()plus__wakeup()but no__unserialize()now has__wakeup()called.
Security
- Decoding a crafted payload that named a missing enum case, or a class constant that is not a case, no longer crashes. The case was validated through
zend_enum_get_case(), whose internal assertion compiles out in release builds, so a missing case dereferenced a null pointer and a non-case constant was misread as an object pointer. Both are now rejected tonullbefore any object is built. - Decoding a crafted object payload that names a non-serializable class (
Closure,Generator,Fiber) no longer yields a corrupt instance that crashes on first access, and naming an interface, trait, or abstract class no longer leaves a thrown exception pending past the decoder's null-return contract. Both are rejected tonull, matching the classes PHP's nativeunserialize()refuses. Selecting the rebuild path from the class (above) also stops a crafted plain-object tag from skipping a class's__unserialize()/__wakeup()invariant rebuild.
0.1.0
Added
- Initial release of the phpser binary serializer.
phpser_serialize($value)/phpser_unserialize($payload, $options)for framed binary serialization with a front-loaded string dictionary.phpser_serialize_signed($value, $key)/phpser_unserialize_signed($payload, $key, $options)for HMAC-SHA256 tamper detection over untrusted storage (memcached, redis, files).allowed_classesoption on both unserialize entry points, matching PHP's nativeunserialize()second-arg behavior (trueallows all,falseblocks all to__PHP_Incomplete_Class, array allowlist).session.serialize_handler = phpserregistration (gated on the session extension being available at build time).- Round-trip coverage: primitives, arrays (packed / assoc / sparse / deleted), objects (stdClass + typed properties), references (
IS_REFERENCEsharing preserved), object identity (back-refs collapse toTAG_REF), cycles, enums,__serialize/__unserialize,__sleep/__wakeup, and the legacySerializableinterface.