Last active
March 17, 2023 01:50
-
-
Save zunda/e663a7401af0e85b84799aa5bfb46c9e to your computer and use it in GitHub Desktop.
Trying to parse marshaled object
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# https://docs.ruby-lang.org/ja/latest/doc/marshal_format.html | |
class MarshalDump | |
class DumpedClass | |
attr_reader :class_name, :parent, :ivars, :dump | |
def initialize(class_name) | |
@class_name = class_name | |
end | |
def set_parent(parent) | |
@parent = parent | |
end | |
def set_ivar(name, value) | |
@ivars ||= Hash.new | |
@ivars[name] = value | |
end | |
def set_dump(dump) | |
@dump = dump | |
end | |
end | |
def initialize(dump) | |
@major_version, @minor_version, @dump = dump.unpack("CCa*") | |
@symbols = [] | |
@objects = [] | |
end | |
def remainder | |
@dump | |
end | |
def consume! | |
if @dump.empty? | |
raise RuntimeError, "No more dump to consume" | |
end | |
type, @dump = @dump.unpack("aa*") | |
case type | |
when "0" | |
return nil | |
when "T" | |
return true | |
when "F" | |
return false | |
when "i" | |
return consume_fixnum! | |
when "l" | |
sign, @dump = @dump.unpack("aa*") | |
len = consume_fixnum! | |
*short, @dump = @dump.unpack("S#{len}a*") | |
r = short.reverse.inject(0){|sum, i| (sum << 16) + i} | |
r *= -1 if sign == "-" | |
return r | |
when ":" | |
len = consume_fixnum! | |
name, @dump = @dump.unpack("a#{len}a*") | |
sym = name.to_sym | |
@symbols << sym | |
return sym | |
when ";" | |
n = consume_fixnum! | |
if n < 0 or @symbols.length <= n | |
raise RuntimeError, "Index for linked Symbols: #{@symbols.inspect} is out of range: #{n}" | |
end | |
return @symbols[n] | |
when "[" | |
len = consume_fixnum! | |
array = Array.new(len) | |
@objects << array | |
array.map!{consume!} | |
return array | |
when "C" | |
class_name = consume! | |
if class_name.class != Symbol | |
raise RuntimeError, "Class name isn't a Symbol: #{class_name.inspect}" | |
end | |
subclass = DumpedClass.new(class_name) | |
@objects << subclass | |
subclass.set_parent(consume!) | |
return subclass | |
when "I" | |
subclass = consume! | |
if subclass.class != DumpedClass and subclass.class != String | |
raise RuntimeError, "Subclass with ivars isn't an expected class: #{subclass.inspect}" | |
end | |
nvars = consume_fixnum! | |
nvars.times do | |
name = consume! | |
if name.class != Symbol | |
raise RuntimeError, "Name of an ivar isn't a Symbol: #{name.inspect}" | |
end | |
value = consume! | |
if subclass.class == DumpedClass | |
subclass.set_ivar(name, value) | |
else | |
# subclass is a String | |
if name == :encoding | |
subclass.encode!(value) | |
elsif name == :E | |
if value | |
subclass.encode!("UTF-8") | |
else | |
subclass.encode!("US-ASCII") | |
end | |
end | |
end | |
end | |
return subclass | |
when "o" | |
class_name = consume! | |
if class_name.class != Symbol | |
raise RuntimeError, "Class name isn't a Symbol: #{class_name.inspect}" | |
end | |
object = DumpedClass.new(class_name) | |
@objects << object | |
nvars = consume_fixnum! | |
nvars.times do | |
name = consume! | |
if name.class != Symbol | |
raise RuntimeError, "Name of an ivar isn't a Symbol: #{name.inspect}" | |
end | |
value = consume! | |
object.set_ivar(name, value) | |
end | |
return object | |
when "@" | |
n = consume_fixnum! | |
if n < 0 or @objects.length <= n | |
raise RuntimeError, "Index (#{n}) for linked Objects (#{@objects.size} total): #{@objects.inspect} is out of range" | |
end | |
return @objects[n] | |
when '"' | |
len = consume_fixnum! | |
str, @dump = @dump.unpack("a#{len}A*") | |
@objects << str | |
return str | |
when "{" | |
hash = Hash.new | |
@objects << hash | |
n = consume_fixnum! | |
n.times do | |
key = consume! | |
value = consume! | |
hash[key] = value | |
end | |
return hash | |
when "}" | |
hash = Hash.new | |
@objects << hash | |
n = consume_fixnum! | |
n.times do | |
key = consume! | |
value = consume! | |
hash[key] = value | |
end | |
hash.default = consume! | |
return hash | |
when "U" | |
class_name = consume! | |
if class_name.class != Symbol | |
raise RuntimeError, "Class name isn't a Symbol: #{class_name.inspect}" | |
end | |
object = DumpedClass.new(class_name) | |
object.set_dump(consume!) | |
return object | |
when "u" | |
class_name = consume! | |
if class_name.class != Symbol | |
raise RuntimeError, "Class name isn't a Symbol: #{class_name.inspect}" | |
end | |
len = consume_fixnum! | |
dump, @dump = @dump.unpack("a#{len}a*") | |
object = DumpedClass.new(class_name) | |
object.set_dump(dump) | |
return object | |
else | |
raise RuntimeError, "Unknown object type: #{type.inspect}, remainder: #{@dump.inspect}" | |
end | |
end | |
def consume_fixnum! | |
n, @dump = @dump.unpack("ca*") | |
if n == 0 | |
return 0 | |
elsif n > 5 | |
return n - 5 | |
elsif n < -5 | |
return n + 5 | |
else | |
nn = Array.new(4){n >= 0 ? 0 : 255} | |
0.upto(n.abs - 1) do |i| | |
nn[i], @dump = @dump.unpack("Ca*") | |
end | |
x = (0xffffff00 | nn[0]) & | |
(0xffff00ff | nn[1] * 0x100) & | |
(0xff00ffff | nn[2] * 0x10000) & | |
(0x00ffffff | nn[3] * 0x1000000) | |
x = -((x ^ 0xffff_ffff) + 1) if n < 0 | |
return x | |
end | |
end | |
end | |
unless ARGV.empty? | |
md = MarshalDump.new(ARGF.read) | |
md.consume! | |
p md | |
else | |
require "minitest/autorun" | |
class MarshalDumpTest < MiniTest::Test | |
def test_nil_true_false | |
assert_nil MarshalDump.new(Marshal.dump(nil)).consume! | |
[true, false].each do |obj| | |
assert_equal obj, MarshalDump.new(Marshal.dump(obj)).consume! | |
end | |
end | |
def test_fixnum | |
[0, 1, -1, -125, -255, -256, -257, 124, 256].each do |i| | |
assert_equal i, MarshalDump.new(Marshal.dump(i)).consume! | |
end | |
end | |
def test_bignum | |
[2**32, -(2**33)].each do |i| | |
assert_equal i, MarshalDump.new(Marshal.dump(i)).consume! | |
end | |
end | |
def test_symbol | |
assert_equal :symbol, MarshalDump.new(Marshal.dump(:symbol)).consume! | |
end | |
def test_array | |
md = MarshalDump.new(Marshal.dump([1, 2, 3])) | |
assert_equal [1, 2, 3], md.consume! | |
assert_empty md.remainder | |
end | |
class ::SimpleSubclass < Array; end | |
def test_simple_subclass | |
x = MarshalDump.new(Marshal.dump(SimpleSubclass.new)).consume! | |
assert_equal :SimpleSubclass, x.class_name | |
assert_equal [], x.parent | |
end | |
class ::SubclassWithIvars < Array | |
def initialize(x) | |
@ivar = 42 | |
super(x) | |
end | |
end | |
def test_subclass_with_ivars | |
md = MarshalDump.new(Marshal.dump(SubclassWithIvars.new([true]))) | |
x = md.consume! | |
assert_equal :SubclassWithIvars, x.class_name | |
assert_equal [true], x.parent | |
h = {:@ivar => 42} | |
assert_equal h, x.ivars | |
assert_empty md.remainder | |
end | |
def test_custom_object_without_ivars | |
md = MarshalDump.new(Marshal.dump(Object.new)) | |
x = md.consume! | |
assert_equal :Object, x.class_name | |
assert_nil x.ivars | |
assert_empty md.remainder | |
end | |
class ::CustomObjectWithIvars | |
def initialize | |
@foo = :bar | |
@one = 1 | |
end | |
end | |
def test_custom_object_with_ivars | |
md = MarshalDump.new(Marshal.dump(CustomObjectWithIvars.new)) | |
x = md.consume! | |
assert_equal :CustomObjectWithIvars, x.class_name | |
h = {:@foo => :bar, :@one => 1} | |
assert_equal h, x.ivars | |
assert_empty md.remainder | |
end | |
def test_string | |
str = "Hello, World!".encode("EUC-JP") | |
md = MarshalDump.new(Marshal.dump(str)) | |
x = md.consume! | |
assert_equal str, x | |
assert_equal ::Encoding::EUC_JP, x.encoding | |
str = "Hello, World!".encode("US-ASCII") | |
md = MarshalDump.new(Marshal.dump(str)) | |
x = md.consume! | |
assert_equal str, x | |
assert_equal ::Encoding::US_ASCII, x.encoding | |
str = "Hello, World!".encode("UTF-8") | |
md = MarshalDump.new(Marshal.dump(str)) | |
x = md.consume! | |
assert_equal str, x | |
assert_equal ::Encoding::UTF_8, x.encoding | |
assert_empty md.remainder | |
end | |
def test_hash | |
h = {true => false, false => true, nil => nil, :hello => "world"} | |
md = MarshalDump.new(Marshal.dump(h)) | |
assert_equal h, md.consume! | |
assert_empty md.remainder | |
end | |
def test_hash_with_default | |
h = Hash.new(0) | |
h[10] = 20 | |
md = MarshalDump.new(Marshal.dump(h)) | |
obj = md.consume! | |
assert_equal h, obj | |
assert_equal h.default, obj.default | |
assert_empty md.remainder | |
end | |
class ::CustomMarshalDumpLoad | |
def marshal_dump; "dumped"; end | |
def marshal_load(obj); end | |
end | |
def test_custom_marshal_dump_load | |
md = MarshalDump.new(Marshal.dump(CustomMarshalDumpLoad.new)) | |
x = md.consume! | |
assert_equal :CustomMarshalDumpLoad, x.class_name | |
assert_equal "dumped", x.dump | |
assert_empty md.remainder | |
end | |
class ::CustomDumpLoad | |
def self._load; end | |
def _dump(obj); "dumped"; end | |
end | |
def test_custom_dump_load | |
md = MarshalDump.new(Marshal.dump(CustomDumpLoad.new)) | |
x = md.consume! | |
assert_equal :CustomDumpLoad, x.class_name | |
assert_equal "dumped", x.dump | |
assert_empty md.remainder | |
end | |
def test_linked_symbol | |
obj = [:foo, :foo, :bar, :foo] | |
md = MarshalDump.new(Marshal.dump(obj)) | |
x = md.consume! | |
assert_equal obj, x | |
assert_empty md.remainder | |
end | |
def test_linked_object | |
obj = Object.new | |
md = MarshalDump.new(Marshal.dump([obj, obj])) | |
x = md.consume! | |
assert_equal :Object, x[0].class_name | |
assert_equal :Object, x[1].class_name | |
assert_empty md.remainder | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment