Skip to content

Commit 19a68cc

Browse files
committed
Deserializer: Array keys are not always 2 token positions apart.
Now keeping track of the end of arrays and objects and using that information when iterating over an array with either array or object values. Fixes #40
1 parent 2fb12ed commit 19a68cc

File tree

6 files changed

+111
-22
lines changed

6 files changed

+111
-22
lines changed

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
# Future
1+
# 2.0.1 (2024-11-18)
2+
3+
## Bugfixes
4+
- Fixed validation error when deserializing a list of objects. The deserializer would check the wrong token for it's
5+
datatype and throw and exception like this:
6+
`Can not deserialize array at position [x] to list: It has a non-integer key 'name' at element [y]`
7+
[GH #40](https://github.com/StringEpsilon/PhpSerializerNET/issues/40)
8+
- Related to the above: Some nested arrays or arrays with object values would never implicetly deserialize into a
9+
`List<object>` because the check if the array keys are consecutive integers was faulty.
10+
11+
# 2.0.0 (2024-11-13)
212

313
## Breaking
414
- Now targets .NET 8.0 and .NET 9.0

PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,25 @@ public void MixedKeyArrayIntoObject() {
239239
Assert.Equal("A", result.Baz);
240240
Assert.Equal("B", result.Dummy);
241241
}
242+
243+
public class ArrayItem
244+
{
245+
[PhpProperty("foo")] public string Foo { get; set; }
246+
[PhpProperty("bar")] public InnerArrayItem Bar { get; set; }
247+
}
248+
249+
public class InnerArrayItem
250+
{
251+
public string A { get; set; }
252+
public string B { get; set; }
253+
}
254+
255+
[Fact]
256+
public void NestedArrays() {
257+
// Regression test for https://github.com/StringEpsilon/PhpSerializerNET/issues/40
258+
var value = """a:2:{i:0;a:2:{s:3:"foo";s:4:"ixcg";s:3:"bar";a:2:{s:1:"A";s:5:"04381";s:1:"B";s:5:"11576";}}i:1;a:2:{s:3:"foo";s:4:"atnp";s:3:"bar";a:2:{s:1:"A";s:5:"33267";s:1:"B";s:5:"68391";}}}""";
259+
// PropertyType is
260+
var item = PhpSerialization.Deserialize<List<ArrayItem>>(value);
261+
Assert.NotNull(item);
262+
}
242263
}

PhpSerializerNET.Test/Deserialize/ObjectDeserialization.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This Source Code Form is subject to the terms of the Mozilla Public
66
**/
77

88
using Xunit;
9+
using System.Collections.Generic;
910
using PhpSerializerNET.Test.DataTypes;
1011

1112
namespace PhpSerializerNET.Test.Deserialize;
@@ -23,4 +24,24 @@ public void IntegerKeysClass() {
2324
Assert.Equal("A", result.Baz);
2425
Assert.Equal("B", result.Dummy);
2526
}
27+
28+
[Fact]
29+
public void ListOfObjects() {
30+
// Regression test for https://github.com/StringEpsilon/PhpSerializerNET/issues/40
31+
var result = PhpSerialization.Deserialize<List<MixedKeysPhpClass>>(
32+
"""a:3:{i:0;a:4:{s:3:"Foo";s:3:"Foo";s:3:"Bar";s:3:"Bar";s:1:"a";s:1:"A";s:1:"b";s:1:"B";}i:1;a:4:{s:3:"Foo";s:3:"Foo";s:3:"Bar";s:3:"Bar";s:1:"a";s:1:"A";s:1:"b";s:1:"B";}i:2;a:4:{s:3:"Foo";s:3:"Foo";s:3:"Bar";s:3:"Bar";s:1:"a";s:1:"A";s:1:"b";s:1:"B";}}"""
33+
);
34+
35+
Assert.Equal(3, result.Count);
36+
}
37+
[Fact]
38+
public void ImplicitListOfObjects() {
39+
// Regression test for https://github.com/StringEpsilon/PhpSerializerNET/issues/40
40+
var result = PhpSerialization.Deserialize(
41+
"""a:3:{i:0;a:4:{s:3:"Foo";s:3:"Foo";s:3:"Bar";s:3:"Bar";s:1:"a";s:1:"A";s:1:"b";s:1:"B";}i:1;a:4:{s:3:"Foo";s:3:"Foo";s:3:"Bar";s:3:"Bar";s:1:"a";s:1:"A";s:1:"b";s:1:"B";}i:2;a:4:{s:3:"Foo";s:3:"Foo";s:3:"Bar";s:3:"Bar";s:1:"a";s:1:"A";s:1:"b";s:1:"B";}}""",
42+
new PhpDeserializationOptions { UseLists = ListOptions.Default }
43+
) as List<object>;
44+
45+
Assert.Equal(3, result.Count);
46+
}
2647
}

PhpSerializerNET/Deserialization/PhpDeserializer.cs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -395,14 +395,23 @@ private object MakeArray(Type targetType, in PhpToken token) {
395395
}
396396

397397
private object MakeList(Type targetType, in PhpToken token) {
398-
for (int i = 0; i < token.Length * 2; i += 2) {
399-
if (this._tokens[this._currentToken + i].Type != PhpDataType.Integer) {
400-
var badToken = this._tokens[this._currentToken + i];
398+
int index = 0;
399+
int itemPosition = this._currentToken;
400+
while (index < token.Length) {
401+
var valueToken = this._tokens[itemPosition +1];
402+
if (this._tokens[itemPosition].Type != PhpDataType.Integer) {
403+
var keyToken = this._tokens[itemPosition];
401404
throw new DeserializationException(
402405
$"Can not deserialize array at position {token.Position} to list: " +
403-
$"It has a non-integer key '{this.GetString(badToken)}' at element {i} (position {badToken.Position})."
406+
$"It has a non-integer key '{this.GetString(keyToken)}' at element {index+1} (position {keyToken.Position})."
404407
);
405408
}
409+
index++;
410+
if (valueToken.Type == PhpDataType.Array || valueToken.Type == PhpDataType.Object) {
411+
itemPosition = valueToken.ValueEnd +1;
412+
} else {
413+
itemPosition += 2;
414+
}
406415
}
407416

408417
if (targetType.IsArray) {
@@ -468,18 +477,26 @@ private object MakeCollection(in PhpToken token) {
468477
long previousKey = -1;
469478
bool isList = true;
470479
bool consecutive = true;
471-
for (int i = 0; i < token.Length * 2; i += 2) {
472-
if (this._tokens[this._currentToken + i].Type != PhpDataType.Integer) {
480+
int index = 0;
481+
int itemPosition = this._currentToken;
482+
while (index < token.Length) {
483+
if (this._tokens[itemPosition].Type != PhpDataType.Integer) {
473484
isList = false;
474485
break;
475486
} else {
476-
int key = this._tokens[this._currentToken + i].Value.GetInt(this._input);
477-
if (i == 0 || key == previousKey + 1) {
487+
int key = this._tokens[itemPosition].Value.GetInt(this._input);
488+
if (index == 0 || key == previousKey + 1) {
478489
previousKey = key;
479490
} else {
480491
consecutive = false;
481492
}
482493
}
494+
index++;
495+
if (this._tokens[itemPosition+1].Type == PhpDataType.Array || this._tokens[itemPosition+1].Type == PhpDataType.Object) {
496+
itemPosition = this._tokens[itemPosition+1].ValueEnd +1;
497+
} else {
498+
itemPosition += 2;
499+
}
483500
}
484501

485502
if (!isList || (this._options.UseLists == ListOptions.Default && !consecutive)) {

PhpSerializerNET/Deserialization/PhpToken.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,27 @@ internal readonly struct PhpToken {
1818
internal readonly int Position;
1919
internal readonly int Length;
2020
internal readonly ValueSpan Value;
21+
/// <summary>
22+
/// For <see cref="PhpDataType.Array"/> and <see cref="PhpDataType.Object"/> only. Holds the index of the last value
23+
/// token inside the respective array/object.
24+
/// </summary>
25+
/// <remarks>
26+
/// This does NOT reference the last value token. It could for example point to the last value token of an
27+
/// object inside an array, when the "last value" of the array would be the object itself.
28+
/// </remarks>
29+
internal readonly int LastValuePosition;
2130

22-
internal PhpToken(in PhpDataType type, in int position, in ValueSpan value, int length = 0) {
31+
internal PhpToken(
32+
in PhpDataType type,
33+
in int position,
34+
in ValueSpan value,
35+
int length = 0,
36+
int lastValuePosition = 0
37+
) {
2338
this.Type = type;
2439
this.Position = position;
2540
this.Value = value;
2641
this.Length = length;
42+
this.LastValuePosition = lastValuePosition;
2743
}
2844
}

PhpSerializerNET/Deserialization/PhpTokenizer.cs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -121,41 +121,45 @@ private void GetFloatingToken() {
121121

122122
[MethodImpl(MethodImplOptions.AggressiveInlining)]
123123
private void GetArrayToken() {
124+
var tokenPosition = this._tokenPosition++;
124125
int position = this._position - 1;
125126
this._position++;
126127
int length = this.GetLength();
127-
this._tokens[this._tokenPosition++] = new PhpToken(
128-
PhpDataType.Array,
129-
position,
130-
ValueSpan.Empty,
131-
length
132-
);
133128
this.Advance(2);
134129
for (int i = 0; i < length * 2; i++) {
135130
this.GetToken();
136131
}
132+
this._tokens[tokenPosition] = new PhpToken(
133+
PhpDataType.Array,
134+
position,
135+
ValueSpan.Empty,
136+
length,
137+
lastValuePosition: this._tokenPosition -1
138+
);
137139
this._position++;
138140
}
139141

140142
[MethodImpl(MethodImplOptions.AggressiveInlining)]
141143
private void GetObjectToken() {
144+
var tokenPosition = this._tokenPosition++;
142145
int position = this._position - 1;
143146
this._position++;
144147
int classNameLength = this.GetLength();
145148
this.Advance(2);
146149
ValueSpan classNameSpan = new ValueSpan(this._position, classNameLength);
147150
this.Advance(2 + classNameLength);
148151
int propertyCount = this.GetLength();
149-
this._tokens[this._tokenPosition++] = new PhpToken(
150-
PhpDataType.Object,
151-
position,
152-
classNameSpan,
153-
propertyCount
154-
);
155152
this.Advance(2);
156153
for (int i = 0; i < propertyCount * 2; i++) {
157154
this.GetToken();
158155
}
156+
this._tokens[tokenPosition] = new PhpToken(
157+
PhpDataType.Object,
158+
position,
159+
classNameSpan,
160+
propertyCount,
161+
lastValuePosition: this._tokenPosition -1
162+
);
159163
this._position++;
160164
}
161165

0 commit comments

Comments
 (0)