Skip to content

Instantly share code, notes, and snippets.

@zunda
Last active March 17, 2023 01:50
Show Gist options
  • Save zunda/e663a7401af0e85b84799aa5bfb46c9e to your computer and use it in GitHub Desktop.
Save zunda/e663a7401af0e85b84799aa5bfb46c9e to your computer and use it in GitHub Desktop.
Trying to parse marshaled object
# 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