Skip to content

Commit 9ba7966

Browse files
committed
Support "R" and "r" (Reference) type
1 parent f754dc3 commit 9ba7966

File tree

2 files changed

+113
-14
lines changed

2 files changed

+113
-14
lines changed

lib/php_serialize.rb

+72-14
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,42 @@ def read_until(char)
1717

1818
module SerializationState
1919
class Base
20+
def initialize
21+
@last_processed_value_index = -1
22+
end
23+
2024
# @return [Boolean]
2125
attr_accessor :assoc
26+
27+
# @return [Integer]
28+
attr_accessor :last_processed_value_index
2229
end
2330

2431
class ToSerialize < Base
32+
def initialize
33+
super
34+
@serialized_object_id_to_index_map = {}
35+
end
36+
37+
# @return [Hash{Integer => Integer}]
38+
attr_accessor :serialized_object_id_to_index_map
2539
end
2640

2741
class ToUnserialize < Base
2842
def initialize
43+
super
2944
@classmap = {}
45+
@unserialized_values = []
3046
end
3147

3248
# @return [Hash{String => Class}]
3349
attr_accessor :classmap
3450

3551
# @return [String]
3652
attr_accessor :original_encoding
53+
54+
# @return [Array<Object>]
55+
attr_accessor :unserialized_values
3756
end
3857
end
3958

@@ -58,6 +77,7 @@ def PHP.serialize(var, assoc = false) # {{{
5877
end
5978

6079
def PHP.do_serialize(var, state)
80+
this_value_index = (state.last_processed_value_index += 1)
6181
s = String.new
6282
case var
6383
when Array
@@ -68,7 +88,7 @@ def PHP.do_serialize(var, state)
6888
}
6989
else
7090
var.each_with_index { |v,i|
71-
s << "i:#{i};#{PHP.do_serialize(v, state)}"
91+
s << PHP.do_serialize(i, state) << PHP.do_serialize(v, state)
7292
}
7393
end
7494

@@ -82,12 +102,15 @@ def PHP.do_serialize(var, state)
82102
s << '}'
83103

84104
when Struct
85-
# encode as Object with same name
86-
s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{var.members.length}:{"
87-
var.members.each do |member|
88-
s << "#{PHP.do_serialize(member, state)}#{PHP.do_serialize(var[member], state)}"
89-
end
90-
s << '}'
105+
s =
106+
handling_reference_for_recurring_object(var, index: this_value_index, state: state) {
107+
# encode as Object with same name
108+
s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{var.members.length}:{"
109+
var.members.each do |member|
110+
s << "#{PHP.do_serialize(member, state)}#{PHP.do_serialize(var[member], state)}"
111+
end
112+
s << '}'
113+
}
91114

92115
when String, Symbol
93116
s << "s:#{var.to_s.bytesize}:\"#{var.to_s}\";"
@@ -106,13 +129,16 @@ def PHP.do_serialize(var, state)
106129

107130
else
108131
if var.respond_to?(:to_assoc)
109-
v = var.to_assoc
110-
# encode as Object with same name
132+
s =
133+
handling_reference_for_recurring_object(var, index: this_value_index, state: state) {
134+
v = var.to_assoc
135+
# encode as Object with same name
111136
s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{v.length}:{"
112-
v.each do |k,v|
113-
s << "#{PHP.do_serialize(k.to_s, state)}#{PHP.do_serialize(v, state)}"
114-
end
115-
s << '}'
137+
v.each do |k,v|
138+
s << "#{PHP.do_serialize(k.to_s, state)}#{PHP.do_serialize(v, state)}"
139+
end
140+
s << '}'
141+
}
116142
else
117143
raise TypeError, "Unable to serialize type #{var.class}"
118144
end
@@ -121,6 +147,27 @@ def PHP.do_serialize(var, state)
121147
s
122148
end # }}}
123149

150+
module InternalMethodsForSerialize
151+
private
152+
# Generate an object reference ('r') for a recurring object instead of serializing it again.
153+
#
154+
# @param [Object] object object to be serialized
155+
# @param [Integer] index index of serialized value
156+
# @param [SerializationState::ToSerialize] state
157+
# @param [Proc] block original procedure to serialize value
158+
# @return [String] serialized value or reference
159+
def handling_reference_for_recurring_object(object, index:, state:, &block)
160+
index_of_object_serialized_before = state.serialized_object_id_to_index_map[object.__id__]
161+
if index_of_object_serialized_before
162+
"r:#{index_of_object_serialized_before};"
163+
else
164+
state.serialized_object_id_to_index_map[object.__id__] = index
165+
yield
166+
end
167+
end
168+
end
169+
extend InternalMethodsForSerialize
170+
124171
# Like PHP.serialize, but only accepts a Hash or associative Array as the root
125172
# type. The results are returned in PHP session format.
126173
#
@@ -213,6 +260,7 @@ def PHP.unserialize(string, classmap = nil, assoc = false) # {{{
213260

214261
def PHP.do_unserialize(string, state)
215262
val = nil
263+
this_value_index = (state.last_processed_value_index += 1)
216264
# determine a type
217265
type = string.read(2)[0,1]
218266
case type
@@ -276,7 +324,7 @@ def PHP.do_unserialize(string, state)
276324

277325
val = val.new
278326
rescue NameError # Nope; make a new Struct
279-
classmap[klass] = val = Struct.new(klass.to_s, *attrs.collect { |v| v[0].to_s })
327+
state.classmap[klass] = val = Struct.new(klass.to_s, *attrs.collect { |v| v[0].to_s })
280328
val = val.new
281329
end
282330
end
@@ -301,10 +349,20 @@ def PHP.do_unserialize(string, state)
301349
when 'b' # bool, b:0 or 1
302350
val = string.read(2)[0] == '1'
303351

352+
when 'R', 'r' # reference to value/object, R:123 or r:123
353+
ref_index = string.read_until(';').to_i
354+
355+
unless (0...(state.unserialized_values.size)).cover?(ref_index)
356+
raise TypeError, "Data part of R/r(Reference) refers to invalid index: #{ref_index.inspect}"
357+
end
358+
359+
val = state.unserialized_values[ref_index]
304360
else
305361
raise TypeError, "Unable to unserialize type '#{type}'"
306362
end
307363

364+
state.unserialized_values[this_value_index] = val
365+
308366
val
309367
end # }}}
310368
end

test/php_serialize_test.rb

+41
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,45 @@ def test_new_struct_creation
134134
PHP.unserialize(phps)
135135
end
136136
end
137+
138+
def test_reference_of_value
139+
assert_nothing_raised do
140+
# example taken from https://www.phpinternalsbook.com/php5/classes_objects/serialization.html
141+
phps = 'a:2:{i:0;s:3:"foo";i:1;R:2;}'
142+
unserialized = PHP.unserialize(phps)
143+
144+
assert_equal 2, unserialized.length
145+
assert_equal "foo", unserialized[0]
146+
assert_equal "foo", unserialized[1]
147+
assert_same unserialized[0], unserialized[1]
148+
149+
reserialized = PHP.serialize(unserialized)
150+
# The reference is not retained on re-serialization.
151+
# It is simply dereferenced to a plain value.
152+
assert_equal 'a:2:{i:0;s:3:"foo";i:1;s:3:"foo";}', reserialized
153+
end
154+
end
155+
156+
def test_reference_of_object
157+
assert_nothing_raised do
158+
# generated with:
159+
# serialize([$o = (object)[], $o]);
160+
phps = 'a:2:{i:0;O:8:"stdClass":0:{}i:1;r:2;}'
161+
unserialized = PHP.unserialize(phps)
162+
163+
assert_equal 2, unserialized.length
164+
assert_same unserialized[0], unserialized[1]
165+
166+
reserialized = PHP.serialize(unserialized)
167+
# The reference is retained on re-serialization.
168+
assert_equal 'a:2:{i:0;O:16:"struct::stdclass":2:{s:3:"url";N;s:8:"dateTime";N;}i:1;r:2;}', reserialized
169+
end
170+
171+
# The serialization works with Struct as well
172+
assert_nothing_raised do
173+
struct = TestStruct.new("foo", "bar")
174+
serialized = PHP.serialize([struct, struct])
175+
assert_equal 'a:2:{i:0;O:10:"teststruct":2:{s:4:"name";s:3:"foo";s:5:"value";s:3:"bar";}i:1;r:2;}', serialized
176+
end
177+
end
137178
end

0 commit comments

Comments
 (0)