Skip to content

Commit be5ca6e

Browse files
committed
Experimental reference support.
1 parent 390b630 commit be5ca6e

File tree

10 files changed

+218
-63
lines changed

10 files changed

+218
-63
lines changed

PhpSerializerNET.Test/DataTypes/AStruct.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ This Source Code Form is subject to the terms of the Mozilla Public
44
file, You can obtain one at http://mozilla.org/MPL/2.0/.
55
**/
66

7-
namespace PhpSerializerNET.Test.DataTypes {
8-
public struct AStruct {
9-
public string foo;
10-
public string bar;
11-
}
12-
}
7+
namespace PhpSerializerNET.Test.DataTypes;
8+
9+
public struct AStruct {
10+
public string foo;
11+
public string bar;
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
This Source Code Form is subject to the terms of the Mozilla Public
3+
License, v. 2.0. If a copy of the MPL was not distributed with this
4+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
**/
6+
7+
namespace PhpSerializerNET.Test.DataTypes;
8+
9+
public struct BStruct {
10+
public AStruct First;
11+
public AStruct Second;
12+
}

PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ public void ExplicitToList() {
173173
Assert.Equal(new List<string>() { "Hello", "World", "12345" }, result);
174174
}
175175

176+
[Fact]
177+
public void ExplicitToListReference() {
178+
var result = PhpSerialization.Deserialize<List<string>>("a:2:{i:0;s:5:\"Hello\";i:1;R:2;}");
179+
180+
Assert.Equal(2, result.Count);
181+
Assert.Equal(new List<string>() { "Hello", "Hello" }, result);
182+
}
183+
176184
[Fact]
177185
public void ExplicitToObjectList() {
178186
var result = PhpSerialization.Deserialize<List<object>>("a:3:{i:0;s:5:\"Hello\";i:1;s:5:\"World\";i:2;i:12345;}");

PhpSerializerNET.Test/Deserialize/DeserializeStructs.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ public void DeserializeArrayToStruct() {
1818
Assert.Equal("Foo", value.foo);
1919
Assert.Equal("Bar", value.bar);
2020
}
21+
[Fact]
22+
public void DeserializeArrayToStructWithReference() {
23+
var value = PhpSerialization.Deserialize<AStruct>(
24+
"a:2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";R:2;}"
25+
);
26+
Assert.Equal("Foo", value.foo);
27+
Assert.Equal("Foo", value.bar);
28+
}
2129

2230
[Fact]
2331
public void DeserializeObjectToStruct() {
@@ -81,6 +89,27 @@ public void DeserializeStringToStruct() {
8189
ex.Message
8290
);
8391
}
92+
[Fact]
93+
public void DeserializeNestedStruct() {
94+
var value = PhpSerialization.Deserialize<BStruct>(
95+
"""a:2:{s:5:"First";a:1:{s:3:"foo";s:3:"one";}s:6:"Second";a:1:{s:3:"foo";s:3:"two";}}"""
96+
);
97+
98+
Assert.Equal("one", value.First.foo);
99+
Assert.Equal("two", value.Second.foo);
100+
}
101+
102+
[Fact]
103+
public void DeserializeStructReference() {
104+
var value = PhpSerialization.Deserialize<BStruct>(
105+
"""a:2:{s:5:"First";a:2:{s:3:"foo";s:3:"one";s:3:"bar";s:3:"two";}s:6:"Second";R:2;}"""
106+
);
107+
108+
Assert.Equal("one", value.First.foo);
109+
Assert.Equal("two", value.First.bar);
110+
Assert.Equal("one", value.Second.foo);
111+
Assert.Equal("two", value.Second.bar);
112+
}
84113

85114
[Fact]
86115
public void DeserializeNullToStruct() {

PhpSerializerNET.Test/Deserialize/ObjectDeserialization.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,29 @@ This Source Code Form is subject to the terms of the Mozilla Public
1212
namespace PhpSerializerNET.Test.Deserialize;
1313

1414
public class ObjectDeserializationTest {
15+
[Fact]
16+
public void References() {
17+
var result = PhpSerialization.Deserialize<MixedKeysPhpClass>(
18+
"""O:8:"stdClass":4:{i:0;s:3:"Foo";i:1;R:2;s:1:"a";s:1:"A";s:1:"b";R:3;}"""
19+
);
20+
Assert.NotNull(result);
21+
Assert.NotNull(result);
22+
Assert.Equal("Foo", result.Foo);
23+
Assert.Equal("Foo", result.Bar);
24+
Assert.Equal("A", result.Baz);
25+
Assert.Equal("A", result.Dummy);
26+
27+
result = PhpSerialization.Deserialize<MixedKeysPhpClass>(
28+
"""O:8:"stdClass":4:{i:0;s:3:"Foo";i:1;R:2;s:1:"a";s:1:"A";s:1:"b";R:2;}"""
29+
);
30+
Assert.NotNull(result);
31+
Assert.NotNull(result);
32+
Assert.Equal("Foo", result.Foo);
33+
Assert.Equal("Foo", result.Bar);
34+
Assert.Equal("A", result.Baz);
35+
Assert.Equal("Foo", result.Dummy);
36+
}
37+
1538
[Fact]
1639
public void IntegerKeysClass() {
1740
var result = PhpSerialization.Deserialize<MixedKeysPhpClass>(

PhpSerializerNET/Deserialization/PhpDataType.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ internal enum PhpDataType : byte {
3737
/// <summary>
3838
/// Object (O:[identLength]:"[ident]":[length]:{[children]})
3939
/// </summary>
40-
Object
40+
Object,
41+
/// <summary>
42+
/// Reference (r:[index])
43+
/// </summary>
44+
Reference
4145
}
4246

PhpSerializerNET/Deserialization/PhpDeserializer.cs

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,31 @@ internal PhpDeserializer(Span<PhpToken> tokens, ReadOnlySpan<byte> input, PhpDes
2626
}
2727

2828
internal object Deserialize() {
29-
return this.DeserializeToken();
29+
return this.Next();
3030
}
3131

3232
internal object Deserialize(Type targetType) {
33-
return this.DeserializeToken(targetType);
33+
return this.Next(targetType);
3434
}
3535

36-
private object DeserializeToken() {
37-
var token = this._tokens[this._currentToken];
38-
this._currentToken++;
36+
private object DeserializeToken(in PhpToken token) {
3937
switch (token.Type) {
38+
case PhpDataType.Reference:
39+
for(int i = 0; i < this._tokens.Length; i++) {
40+
if (this._tokens[i].Reference == token.Reference) {
41+
// This is a hack because this whole class was never designed with references in mind.
42+
// Because of that we're always assuming that this._currentToken points to the token
43+
// that needs to be deserialized next when filling arrays and objects.
44+
// Going back in the token-list therefore does not work if we don't also reset the
45+
// currentToken position and restore it after.
46+
var tokenPosition = this._currentToken;
47+
this._currentToken = i+1;
48+
var reference = this.DeserializeToken(this._tokens[i]);
49+
this._currentToken = tokenPosition;
50+
return reference;
51+
}
52+
}
53+
throw new DeserializationException("Can not resolve reference");
4054
case PhpDataType.Boolean:
4155
return token.Value.GetBool(this._input);
4256
case PhpDataType.Integer:
@@ -49,7 +63,6 @@ private object DeserializeToken() {
4963
return token.Value.GetBool(this._input);
5064
}
5165
}
52-
5366
return this.GetString(token);
5467
case PhpDataType.Array:
5568
return this.MakeCollection(token);
@@ -60,11 +73,30 @@ private object DeserializeToken() {
6073
return null;
6174
}
6275
}
63-
64-
private object DeserializeToken(Type targetType) {
76+
private object Next() {
6577
var token = this._tokens[this._currentToken];
6678
this._currentToken++;
79+
return this.DeserializeToken(token);
80+
}
81+
82+
private object DeserializeToken(Type targetType, in PhpToken token) {
6783
switch (token.Type) {
84+
case PhpDataType.Reference:
85+
for(int i = 0; i < this._tokens.Length; i++) {
86+
if (this._tokens[i].Reference == token.Reference) {
87+
// This is a hack because this whole class was never designed with references in mind.
88+
// Because of that we're always assuming that this._currentToken points to the token
89+
// that needs to be deserialized next when filling arrays and objects.
90+
// Going back in the token-list therefore does not work if we don't also reset the
91+
// currentToken position and restore it after.
92+
var tokenPosition = this._currentToken;
93+
this._currentToken = i+1;
94+
var reference = this.DeserializeToken(targetType, this._tokens[i]);
95+
this._currentToken = tokenPosition;
96+
return reference;
97+
}
98+
}
99+
throw new DeserializationException("Can not resolve reference");
68100
case PhpDataType.Boolean:
69101
return this.DeserializeBoolean(targetType, token);
70102
case PhpDataType.Integer:
@@ -110,15 +142,21 @@ private object DeserializeToken(Type targetType) {
110142
}
111143
}
112144

145+
private object Next(Type targetType) {
146+
var token = this._tokens[this._currentToken];
147+
this._currentToken++;
148+
return DeserializeToken(targetType, token);
149+
}
150+
113151
private object DeserializeInteger(Type targetType, in PhpToken token) {
114152
return Type.GetTypeCode(targetType) switch {
115-
TypeCode.Int16 => short.Parse(token.Value.GetSlice(this._input), CultureInfo.InvariantCulture),
116-
TypeCode.Int32 => int.Parse(token.Value.GetSlice(this._input), CultureInfo.InvariantCulture),
117-
TypeCode.Int64 => long.Parse(token.Value.GetSlice(this._input), CultureInfo.InvariantCulture),
118-
TypeCode.UInt16 => ushort.Parse(token.Value.GetSlice(this._input), CultureInfo.InvariantCulture),
119-
TypeCode.UInt32 => uint.Parse(token.Value.GetSlice(this._input), CultureInfo.InvariantCulture),
120-
TypeCode.UInt64 => ulong.Parse(token.Value.GetSlice(this._input), CultureInfo.InvariantCulture),
121-
TypeCode.SByte => sbyte.Parse(token.Value.GetSlice(this._input), CultureInfo.InvariantCulture),
153+
TypeCode.Int16 => short.Parse(token.Value.GetSlice(in this._input), CultureInfo.InvariantCulture),
154+
TypeCode.Int32 => int.Parse(token.Value.GetSlice(in this._input), CultureInfo.InvariantCulture),
155+
TypeCode.Int64 => long.Parse(token.Value.GetSlice(in this._input), CultureInfo.InvariantCulture),
156+
TypeCode.UInt16 => ushort.Parse(token.Value.GetSlice(in this._input), CultureInfo.InvariantCulture),
157+
TypeCode.UInt32 => uint.Parse(token.Value.GetSlice(in this._input), CultureInfo.InvariantCulture),
158+
TypeCode.UInt64 => ulong.Parse(token.Value.GetSlice(in this._input), CultureInfo.InvariantCulture),
159+
TypeCode.SByte => sbyte.Parse(token.Value.GetSlice(in this._input), CultureInfo.InvariantCulture),
122160
_ => this.DeserializeTokenFromSimpleType(targetType, token.Type, this.GetString(token), token.Position),
123161
};
124162
}
@@ -251,15 +289,15 @@ private object MakeClass(in PhpToken token) {
251289
var result = new PhpDynamicObject(token.Length, typeName);
252290
result.SetClassName(typeName);
253291
for (int i = 0; i < token.Length; i++) {
254-
result.TryAdd((string)this.DeserializeToken(), this.DeserializeToken());
292+
result.TryAdd((string)this.Next(), this.Next());
255293
}
256294

257295
return result;
258296
} else if (this._options.StdClass == StdClassOption.Dictionary) {
259297
var result = new PhpObjectDictionary(token.Length, typeName);
260298
result.SetClassName(typeName);
261299
for (int i = 0; i < token.Length; i++) {
262-
result.TryAdd((string)this.DeserializeToken(), this.DeserializeToken());
300+
result.TryAdd((string)this.Next(), this.Next());
263301
}
264302

265303
return result;
@@ -272,7 +310,7 @@ private object MakeClass(in PhpToken token) {
272310
// go back one because we're basically re-entering the object-token from the top.
273311
// If we don't decrement the pointer, we'd start with the first child token instead of the object token.
274312
this._currentToken--;
275-
var constructedObject = this.DeserializeToken(targetType);
313+
var constructedObject = this.Next(targetType);
276314
if (constructedObject is IPhpObject phpObject and not PhpDateTime) {
277315
phpObject.SetClassName(typeName);
278316
}
@@ -303,7 +341,7 @@ private object MakeStruct(Type targetType, in PhpToken token) {
303341
if (fields[fieldName] != null) {
304342
var field = fields[fieldName];
305343
try {
306-
field.SetValue(result, this.DeserializeToken(field.FieldType));
344+
field.SetValue(result, this.Next(field.FieldType));
307345
} catch (Exception exception) {
308346
var valueToken = this._tokens[this._currentToken];
309347
throw new DeserializationException(
@@ -353,7 +391,7 @@ private object MakeObject(Type targetType, in PhpToken token) {
353391
// null if PhpIgnore'd
354392
try {
355393
property.SetValue(
356-
result, this.DeserializeToken(property.PropertyType)
394+
result, this.Next(property.PropertyType)
357395
);
358396
} catch (Exception exception) {
359397
var valueToken = this._tokens[this._currentToken-1];
@@ -378,12 +416,12 @@ private object MakeArray(Type targetType, in PhpToken token) {
378416
if (elementType == typeof(object)) {
379417
for (int i = 0; i < token.Length; i++) {
380418
this._currentToken++;
381-
result.SetValue(this.DeserializeToken(), i);
419+
result.SetValue(this.Next(), i);
382420
}
383421
} else {
384422
for (int i = 0; i < token.Length; i++) {
385423
this._currentToken++;
386-
result.SetValue(this.DeserializeToken(elementType), i);
424+
result.SetValue(this.Next(elementType), i);
387425
}
388426
}
389427

@@ -425,12 +463,12 @@ private object MakeList(Type targetType, in PhpToken token) {
425463
if (itemType == typeof(object)) {
426464
for (int i = 0; i < token.Length; i++) {
427465
this._currentToken++;
428-
result.Add(this.DeserializeToken());
466+
result.Add(this.Next());
429467
}
430468
} else {
431469
for (int i = 0; i < token.Length; i++) {
432470
this._currentToken++;
433-
result.Add(this.DeserializeToken(itemType));
471+
result.Add(this.Next(itemType));
434472
}
435473
}
436474
return result;
@@ -444,7 +482,7 @@ private object MakeDictionary(Type targetType, in PhpToken token) {
444482

445483
if (!targetType.GenericTypeArguments.Any()) {
446484
for (int i = 0; i < token.Length; i++) {
447-
result.Add(this.DeserializeToken(), this.DeserializeToken());
485+
result.Add(this.Next(), this.Next());
448486
}
449487
return result;
450488
}
@@ -455,11 +493,11 @@ private object MakeDictionary(Type targetType, in PhpToken token) {
455493
for (int i = 0; i < token.Length; i++) {
456494
result.Add(
457495
keyType == typeof(object)
458-
? this.DeserializeToken()
459-
: this.DeserializeToken(keyType),
496+
? this.Next()
497+
: this.Next(keyType),
460498
valueType == typeof(object)
461-
? this.DeserializeToken()
462-
: this.DeserializeToken(valueType)
499+
? this.Next()
500+
: this.Next(valueType)
463501
);
464502
}
465503
return result;
@@ -498,14 +536,14 @@ private object MakeCollection(in PhpToken token) {
498536
if (!isList || (this._options.UseLists == ListOptions.Default && !consecutive)) {
499537
var result = new Dictionary<object, object>(token.Length);
500538
for (int i = 0; i < token.Length; i++) {
501-
result.Add(this.DeserializeToken(), this.DeserializeToken());
539+
result.Add(this.Next(), this.Next());
502540
}
503541
return result;
504542
} else {
505543
var result = new List<object>(token.Length);
506544
for (int i = 0; i < token.Length; i++) {
507545
this._currentToken++;
508-
result.Add(this.DeserializeToken());
546+
result.Add(this.Next());
509547
}
510548
return result;
511549
}

PhpSerializerNET/Deserialization/PhpToken.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ internal readonly struct PhpToken {
1717
internal readonly PhpDataType Type;
1818
internal readonly int Position;
1919
internal readonly int Length;
20-
internal readonly ValueSpan Value;
2120
/// <summary>
2221
/// For <see cref="PhpDataType.Array"/> and <see cref="PhpDataType.Object"/> only. Holds the index of the last value
2322
/// token inside the respective array/object.
@@ -27,18 +26,22 @@ internal readonly struct PhpToken {
2726
/// object inside an array, when the "last value" of the array would be the object itself.
2827
/// </remarks>
2928
internal readonly int LastValuePosition;
29+
internal readonly int Reference;
30+
internal readonly ValueSpan Value;
3031

3132
internal PhpToken(
3233
in PhpDataType type,
3334
in int position,
3435
in ValueSpan value,
36+
int reference,
3537
int length = 0,
3638
int lastValuePosition = 0
3739
) {
3840
this.Type = type;
3941
this.Position = position;
4042
this.Value = value;
4143
this.Length = length;
44+
this.Reference = reference;
4245
this.LastValuePosition = lastValuePosition;
4346
}
4447
}

PhpSerializerNET/Deserialization/PhpTokenValidator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ internal PhpTokenValidator(in ReadOnlySpan<byte> input) {
2626

2727
internal void GetToken() {
2828
switch (this._input[this._position++]) {
29+
case (byte)'r':
30+
case (byte)'R':
31+
this.GetCharacter(':');
32+
this.GetInteger();
33+
this.GetCharacter(';');
34+
break;
2935
case (byte)'b':
3036
this.GetCharacter(':');
3137
this.GetBoolean();

0 commit comments

Comments
 (0)