From e6a006959d5f449568ba637f33d1e0372c22254b Mon Sep 17 00:00:00 2001 From: Kaoru Shirai <475350+kaorukobo@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:09:34 +0900 Subject: [PATCH 1/2] Add PHP::PhpObject and use it to represent repeating stdClass # Conflicts: # lib/php_serialize.rb # test/php_serialize_test.rb --- lib/php_serialize.rb | 25 +++++++++++++++++++------ test/php_serialize_test.rb | 27 +++++++++++++++++++++------ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/lib/php_serialize.rb b/lib/php_serialize.rb index 68b1cd1..a6d820e 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 + 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&._php_classname || var.class.to_s + s << "O:#{class_name.bytesize}:\"#{class_name.downcase}\":#{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. # @@ -233,9 +245,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.to_s + } end end diff --git a/test/php_serialize_test.rb b/test/php_serialize_test.rb index 5915b4f..eb0f9a0 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";}' + serialized = PHP.unserialize(phps) + + assert_kind_of PHP::PhpObject, serialized + assert_equal "/legacy/index.php", serialized.url + + reserialized = PHP.serialize(phps) + 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 From 6de2c485b43560d0923d624914698cdfd3ce155d Mon Sep 17 00:00:00 2001 From: Kaoru Shirai <475350+kaorukobo@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:51:47 +0900 Subject: [PATCH 2/2] fixup The previous commit failed the test. --- lib/php_serialize.rb | 11 ++++++----- test/php_serialize_test.rb | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/php_serialize.rb b/lib/php_serialize.rb index a6d820e..1dfe327 100644 --- a/lib/php_serialize.rb +++ b/lib/php_serialize.rb @@ -22,7 +22,7 @@ class PhpObject < OpenStruct attr_accessor :_php_classname def to_assoc - each_pair + each_pair.to_a end end @@ -90,8 +90,8 @@ def PHP.serialize(var, assoc = false) # {{{ if var.respond_to?(:to_assoc) v = var.to_assoc # encode as Object with same name - class_name = var&._php_classname || var.class.to_s - s << "O:#{class_name.bytesize}:\"#{class_name.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 @@ -221,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 = [] @@ -247,7 +248,7 @@ def PHP.do_unserialize(string, classmap, assoc) val = val.new rescue NameError # Nope; make a new PhpObject val = PhpObject.new.tap { |php_obj| - php_obj._php_classname = klass.to_s + 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 eb0f9a0..d6b1f86 100644 --- a/test/php_serialize_test.rb +++ b/test/php_serialize_test.rb @@ -111,12 +111,12 @@ def test_sessions 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";}' - serialized = PHP.unserialize(phps) + unserialized = PHP.unserialize(phps) - assert_kind_of PHP::PhpObject, serialized - assert_equal "/legacy/index.php", serialized.url + assert_kind_of PHP::PhpObject, unserialized + assert_equal "/legacy/index.php", unserialized.url - reserialized = PHP.serialize(phps) + reserialized = PHP.serialize(unserialized) assert_equal phps, reserialized end end