diff --git a/lib/php_serialize.rb b/lib/php_serialize.rb index 68b1cd1..1dfe327 100644 --- a/lib/php_serialize.rb +++ b/lib/php_serialize.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'stringio' +require 'ostruct' module PHP class StringIOReader < StringIO @@ -15,6 +16,16 @@ def read_until(char) end end + # Represents a serialized PHP object + class PhpObject < OpenStruct + # @return [String] The name of the original PHP class + attr_accessor :_php_classname + + def to_assoc + each_pair.to_a + end + end + # Returns a string representing the argument in a form PHP.unserialize # and PHP's unserialize() should both be able to load. # @@ -79,7 +90,8 @@ def PHP.serialize(var, assoc = false) # {{{ if var.respond_to?(:to_assoc) v = var.to_assoc # encode as Object with same name - s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{v.length}:{" + class_name = var.respond_to?(:_php_classname) ? var._php_classname : var.class.to_s.downcase + s << "O:#{class_name.bytesize}:\"#{class_name}\":#{v.length}:{" v.each do |k,v| s << "#{PHP.serialize(k.to_s, assoc)}#{PHP.serialize(v, assoc)}" end @@ -139,8 +151,8 @@ def PHP.serialize_session(var, assoc = false) # {{{ # to be the class itself; i.e. something you could call .new on. # # If it's not found in 'classmap', the current constant namespace is searched, - # and failing that, a new Struct(classname) is generated, with the arguments - # for .new specified in the same order PHP provided; since PHP uses hashes + # and failing that, a new PHP::PhpObject (subclass of OpenStruct) is generated, + # with the properties in the same order PHP provided; since PHP uses hashes # to represent attributes, this should be the same order they're specified # in PHP, but this is untested. # @@ -209,7 +221,8 @@ def PHP.do_unserialize(string, classmap, assoc) when 'O' # object, O:length:"class":length:{[attribute][value]...} # class name (lowercase in PHP, grr) len = string.read_until(':').to_i + 3 # quotes, seperator - klass = string.read(len)[1...-2].capitalize.intern # read it, kill useless quotes + klass_in_php = string.read(len)[1...-2] + klass = klass_in_php.capitalize.intern # read it, kill useless quotes # read the attributes attrs = [] @@ -233,9 +246,10 @@ def PHP.do_unserialize(string, classmap, assoc) classmap[klass] = val = Module.const_get(klass) val = val.new - rescue NameError # Nope; make a new Struct - classmap[klass] = val = Struct.new(klass.to_s, *attrs.collect { |v| v[0].to_s }) - val = val.new + rescue NameError # Nope; make a new PhpObject + val = PhpObject.new.tap { |php_obj| + php_obj._php_classname = klass_in_php.to_s + } end end diff --git a/test/php_serialize_test.rb b/test/php_serialize_test.rb index 5915b4f..d6b1f86 100644 --- a/test/php_serialize_test.rb +++ b/test/php_serialize_test.rb @@ -108,10 +108,25 @@ def test_sessions end end - def test_new_struct_creation - assert_nothing_raised do - phps = 'O:8:"stdClass":2:{s:3:"url";s:17:"/legacy/index.php";s:8:"dateTime";s:19:"2012-10-24 22:29:13";}' - PHP.unserialize(phps) - end - end + def test_creates_php_object_instance_if_class_undefined + assert_nothing_raised do + phps = 'O:8:"stdClass":2:{s:3:"url";s:17:"/legacy/index.php";s:8:"dateTime";s:19:"2012-10-24 22:29:13";}' + unserialized = PHP.unserialize(phps) + + assert_kind_of PHP::PhpObject, unserialized + assert_equal "/legacy/index.php", unserialized.url + + reserialized = PHP.serialize(unserialized) + assert_equal phps, reserialized + end + end + + def test_same_classname_appears_twice + assert_nothing_raised do + # can be generated with: + # serialize([(object)["foo" => 1], (object)["bar" => 2]]) + phps = 'a:2:{i:0;O:8:"stdClass":1:{s:3:"foo";i:1;}i:1;O:8:"stdClass":1:{s:3:"bar";i:2;}}' + PHP.unserialize(phps) + end + end end