From 7196cc535a8be228121179fa77bdaa01ad6b491e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Feb 2026 19:18:08 +0300 Subject: [PATCH 1/2] Fix reference counting for null values and value references (r:) --- PhpSerializerNET.Test/DataTypes/MultiRef.cs | 19 +++++++++++++++ .../DataTypes/ReferenceWithNulls.cs | 21 ++++++++++++++++ .../ReferenceDeserializationTest.cs | 24 +++++++++++++++++++ .../Validation/TestArrayValidation.cs | 2 +- .../Deserialization/PhpTokenizer.cs | 21 +++++++++++++--- 5 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 PhpSerializerNET.Test/DataTypes/MultiRef.cs create mode 100644 PhpSerializerNET.Test/DataTypes/ReferenceWithNulls.cs diff --git a/PhpSerializerNET.Test/DataTypes/MultiRef.cs b/PhpSerializerNET.Test/DataTypes/MultiRef.cs new file mode 100644 index 0000000..37f2d97 --- /dev/null +++ b/PhpSerializerNET.Test/DataTypes/MultiRef.cs @@ -0,0 +1,19 @@ +/** + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +**/ + +namespace PhpSerializerNET.Test.DataTypes; + +public class MultiRef { + public MultiRefObj first { get; set; } + public MultiRefObj second { get; set; } + public MultiRefObj third { get; set; } + public MultiRefObj fourth { get; set; } + public MultiRefObj fifth { get; set; } +} + +public class MultiRefObj { + public string id { get; set; } +} diff --git a/PhpSerializerNET.Test/DataTypes/ReferenceWithNulls.cs b/PhpSerializerNET.Test/DataTypes/ReferenceWithNulls.cs new file mode 100644 index 0000000..d95f0de --- /dev/null +++ b/PhpSerializerNET.Test/DataTypes/ReferenceWithNulls.cs @@ -0,0 +1,21 @@ +/** + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +**/ + +namespace PhpSerializerNET.Test.DataTypes; + +public class ReferenceWithNulls { + public ReferenceWithNullsItem first { get; set; } + public ReferenceWithNullsItem second { get; set; } +} + +public class ReferenceWithNullsItem { + public object modifiers { get; set; } + public ReferenceWithNullsInner inner { get; set; } +} + +public class ReferenceWithNullsInner { + public string value { get; set; } +} diff --git a/PhpSerializerNET.Test/Deserialize/ReferenceDeserializationTest.cs b/PhpSerializerNET.Test/Deserialize/ReferenceDeserializationTest.cs index 8b7e9d2..33a6012 100644 --- a/PhpSerializerNET.Test/Deserialize/ReferenceDeserializationTest.cs +++ b/PhpSerializerNET.Test/Deserialize/ReferenceDeserializationTest.cs @@ -55,4 +55,28 @@ public void ReferencingArray() { Assert.Equal("two", value.Second.bar); } + [Fact] + public void ReferenceWithNullValuesBefore() { + var value = PhpSerialization.Deserialize( + """a:2:{s:5:"first";a:2:{s:9:"modifiers";N;s:5:"inner";O:5:"Inner":1:{s:5:"value";s:4:"test";}}s:6:"second";a:2:{s:9:"modifiers";N;s:5:"inner";r:4;}}""" + ); + + Assert.Null(value.first.modifiers); + Assert.Null(value.second.modifiers); + Assert.Equal("test", value.first.inner.value); + Assert.Equal("test", value.second.inner.value); + } + + [Fact] + public void MultipleValueReferencesBeforeTarget() { + var value = PhpSerialization.Deserialize( + """a:5:{s:5:"first";O:3:"Obj":1:{s:2:"id";s:1:"A";}s:6:"second";r:2;s:5:"third";r:2;s:6:"fourth";O:3:"Obj":1:{s:2:"id";s:1:"B";}s:5:"fifth";r:6;}""" + ); + + Assert.Equal("A", value.first.id); + Assert.Equal("A", value.second.id); + Assert.Equal("A", value.third.id); + Assert.Equal("B", value.fourth.id); + Assert.Equal("B", value.fifth.id); + } } diff --git a/PhpSerializerNET.Test/Deserialize/Validation/TestArrayValidation.cs b/PhpSerializerNET.Test/Deserialize/Validation/TestArrayValidation.cs index 2091992..c5340de 100644 --- a/PhpSerializerNET.Test/Deserialize/Validation/TestArrayValidation.cs +++ b/PhpSerializerNET.Test/Deserialize/Validation/TestArrayValidation.cs @@ -29,7 +29,7 @@ public void AllowsValidArray() { if (result is List) { Assert.Equal(24, ((List)result).Count); } else { - Assert.True(false, "Expected List but got " + result.GetType().Name); + Assert.Fail("Expected List but got " + result.GetType().Name); } } } diff --git a/PhpSerializerNET/Deserialization/PhpTokenizer.cs b/PhpSerializerNET/Deserialization/PhpTokenizer.cs index 803c64b..f870f22 100644 --- a/PhpSerializerNET/Deserialization/PhpTokenizer.cs +++ b/PhpSerializerNET/Deserialization/PhpTokenizer.cs @@ -45,8 +45,10 @@ private int GetLength() { private void GetToken(bool countReference) { switch (this._input[this._position++]) { case (byte)'r': + this.GetValueReferenceToken(countReference); + break; case (byte)'R': - this.GetReferenceToken(); + this.GetVariableReferenceToken(); break; case (byte)'b': this.GetBooleanToken(countReference); @@ -56,7 +58,7 @@ private void GetToken(bool countReference) { PhpDataType.Null, this._position - 1, ValueSpan.Empty, - 0 + countReference ? ++this._reference : 0 ); this._position++; break; @@ -114,7 +116,7 @@ private void GetIntegerToken(bool reference) { this._position++; } - private void GetReferenceToken() { + private void GetVariableReferenceToken() { this._position++; int index = this.GetNumbers().GetInt(this._input); if (index <= 0 || index > this._reference) { @@ -124,6 +126,19 @@ private void GetReferenceToken() { this._position++; } + private void GetValueReferenceToken(bool reference) { + this._position++; + int index = this.GetNumbers().GetInt(this._input); + if (index <= 0 || index > this._reference) { + throw new DeserializationException($"Invalid reference: '{index}' can not be resolved."); + } + if (reference) { + this._reference++; + } + this._tokens[this._tokenPosition++] = new PhpToken(PhpDataType.Reference, this._position, ValueSpan.Empty, index); + this._position++; + } + private void GetFloatingToken(bool reference) { this._position++; this._tokens[this._tokenPosition++] = new PhpToken( From 3103e931b9160aa2282d34e2455a6a397079c332 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Feb 2026 19:19:02 +0300 Subject: [PATCH 2/2] Version 2.1.2 --- CHANGELOG.md | 6 ++++++ PhpSerializerNET/PhpSerializerNET.csproj | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df2f9c5..7826b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2.1.2 (future) + +## Bugfixes +- Fixed reference numbering for `null` values: In PHP serialization, `null` values (`N;`) are counted in reference numbering, but the library was not incrementing the reference counter for them. This caused incorrect reference resolution when `null` values appeared before referenced objects. +- Fixed reference numbering for value references: In PHP, value references (`r:`) are counted towards reference numbering, while variable references (`R:`) are not. The library was treating both types the same way (not counting either), which caused reference resolution failures when multiple value references appeared before a later reference target. + # 2.1.1 (2025-03-12) ## Bugfixes diff --git a/PhpSerializerNET/PhpSerializerNET.csproj b/PhpSerializerNET/PhpSerializerNET.csproj index bd692f9..f550139 100644 --- a/PhpSerializerNET/PhpSerializerNET.csproj +++ b/PhpSerializerNET/PhpSerializerNET.csproj @@ -3,7 +3,7 @@ PhpSerializerNET net8.0;net9.0 12.0 - 2.1.1 + 2.1.2 StringEpsilon A library for working with the PHP serialization format. php-serialization, php-serializer, php-deserializer