310 lines
9.6 KiB
Ruby
310 lines
9.6 KiB
Ruby
# ©AngelaMos | 2026
|
|
# parser.rb
|
|
# frozen_string_literal: true
|
|
|
|
module Marshalsea
|
|
module Marshal
|
|
class Parser
|
|
include Constants
|
|
|
|
def initialize(source, max_depth: nil, limits: Limits.new)
|
|
raise InputTypeError, "expected String, got #{source.class}" unless source.is_a?(String)
|
|
|
|
@limits = limits
|
|
@max_depth = max_depth || limits.max_depth
|
|
enforce_size(source)
|
|
@source = source.dup.force_encoding(Encoding::BINARY)
|
|
@budget = Budget.new(limits)
|
|
@position = 0
|
|
@symbols = []
|
|
@objects = []
|
|
@role_anomalies = []
|
|
end
|
|
|
|
def parse
|
|
read_header
|
|
root = read_value(1)
|
|
raise TrailingBytesError, "#{remaining} unread bytes" unless remaining.zero?
|
|
|
|
root.each(&:seal)
|
|
Result.new(root, major: @major, minor: @minor,
|
|
role_anomalies: @role_anomalies.freeze).freeze
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :source, :max_depth, :symbols, :objects, :budget, :limits
|
|
|
|
def enforce_size(candidate)
|
|
return if candidate.bytesize <= limits.max_bytes
|
|
|
|
raise LimitExceededError, "#{Limits::ROLE_BYTES} #{candidate.bytesize} exceeds #{limits.max_bytes}"
|
|
end
|
|
|
|
def remaining
|
|
source.bytesize - @position
|
|
end
|
|
|
|
def take(count)
|
|
raise MalformedCountError, "negative byte count #{count}" if count.negative?
|
|
raise TruncatedStreamError, "wanted #{count} bytes, had #{remaining}" if count > remaining
|
|
|
|
slice = source.byteslice(@position, count)
|
|
@position += count
|
|
slice
|
|
end
|
|
|
|
def read_count(role)
|
|
value = read_fixnum
|
|
raise MalformedCountError, "negative #{role} count #{value}" if value.negative?
|
|
|
|
value
|
|
end
|
|
|
|
def take_byte
|
|
take(1).unpack1("C")
|
|
end
|
|
|
|
def take_signed_byte
|
|
byte = take_byte
|
|
byte >= BYTE_SIGN_THRESHOLD ? byte - BYTE_MODULUS : byte
|
|
end
|
|
|
|
def read_header
|
|
@major, @minor = take(HEADER_LENGTH).unpack("CC")
|
|
return if @major == MAJOR_VERSION && @minor <= MINOR_VERSION
|
|
|
|
raise UnsupportedVersionError, "stream declares #{@major}.#{@minor}"
|
|
end
|
|
|
|
def read_fixnum
|
|
marker = take_signed_byte
|
|
return 0 if marker.zero?
|
|
return marker - FIXNUM_INLINE_OFFSET if marker > FIXNUM_MAX_INLINE
|
|
return marker + FIXNUM_INLINE_OFFSET if marker < FIXNUM_MIN_INLINE
|
|
|
|
width = marker.abs
|
|
value = little_endian(take(width))
|
|
marker.negative? ? value - (1 << (BITS_PER_BYTE * width)) : value
|
|
end
|
|
|
|
def little_endian(bytes)
|
|
bytes.each_byte.with_index.sum { |byte, index| byte << (BITS_PER_BYTE * index) }
|
|
end
|
|
|
|
def read_counted_bytes
|
|
size = read_count(ROLE_LENGTH)
|
|
budget.scalar!(size)
|
|
take(size)
|
|
end
|
|
|
|
def read_entry_count(role)
|
|
count = read_count(role)
|
|
budget.entries!(count)
|
|
count
|
|
end
|
|
|
|
def register(node)
|
|
budget.registered!
|
|
objects << node
|
|
node
|
|
end
|
|
|
|
def read_value(depth)
|
|
raise DepthLimitError, "exceeded depth #{max_depth}" if depth > max_depth
|
|
|
|
budget.node!
|
|
tag = take(1)
|
|
|
|
case tag
|
|
when TAG_NIL then Node.new(type: :nil, tag: tag)
|
|
when TAG_TRUE then Node.new(type: :boolean, tag: tag, value: true)
|
|
when TAG_FALSE then Node.new(type: :boolean, tag: tag, value: false)
|
|
when TAG_FIXNUM then Node.new(type: :fixnum, tag: tag, value: read_fixnum)
|
|
when TAG_SYMBOL then read_symbol(tag)
|
|
when TAG_SYMLINK then read_symlink(tag)
|
|
when TAG_OBJECT_LINK then read_object_link(tag)
|
|
when TAG_BIGNUM then register(read_bignum(tag))
|
|
when TAG_FLOAT then register(read_float(tag))
|
|
when TAG_STRING then register(read_string(tag))
|
|
when TAG_REGEXP then register(read_regexp(tag))
|
|
when TAG_ARRAY then read_array(tag, depth)
|
|
when TAG_HASH, TAG_HASH_DEFAULT then read_hash(tag, depth)
|
|
when TAG_IVAR then read_ivar(tag, depth)
|
|
when TAG_OBJECT then read_object(tag, depth)
|
|
when TAG_STRUCT then read_struct(tag, depth)
|
|
when TAG_USERDEF then register(read_userdef(tag, depth))
|
|
when TAG_USERMARSHAL then read_usermarshal(tag, depth)
|
|
when TAG_DATA then read_wrapped(tag, :data, depth)
|
|
when TAG_USERCLASS then read_wrapped(tag, :user_class, depth)
|
|
when TAG_EXTENDED then read_wrapped(tag, :extended, depth)
|
|
when TAG_CLASS then register(read_named(tag, :class))
|
|
when TAG_MODULE, TAG_MODULE_OLD then register(read_named(tag, :module))
|
|
else raise UnknownTagError, "byte #{tag.unpack1('C')} at offset #{@position - 1}"
|
|
end
|
|
end
|
|
|
|
def read_symbol(tag)
|
|
budget.symbol!
|
|
size = read_count(ROLE_LENGTH)
|
|
budget.symbol_name!(size)
|
|
budget.scalar!(size)
|
|
node = Node.new(type: :symbol, tag: tag, value: take(size).to_sym)
|
|
symbols << node.value
|
|
node
|
|
end
|
|
|
|
def read_symlink(tag)
|
|
budget.symbol_reference!
|
|
index = read_fixnum
|
|
raise InvalidLinkError, "symlink #{index} of #{symbols.length}" if
|
|
index.negative? || index >= symbols.length
|
|
|
|
Node.new(type: :symlink, tag: tag, value: symbols[index])
|
|
end
|
|
|
|
def read_object_link(tag)
|
|
budget.link!
|
|
index = read_fixnum
|
|
raise InvalidLinkError, "object link #{index} of #{objects.length}" if
|
|
index.negative? || index >= objects.length
|
|
|
|
node = Node.new(type: :object_link, tag: tag, value: index)
|
|
node.link_target = objects[index]
|
|
node
|
|
end
|
|
|
|
def read_bignum(tag)
|
|
sign = take(1)
|
|
raise MalformedValueError, "bignum sign #{sign.inspect}" unless BIGNUM_SIGNS.include?(sign)
|
|
|
|
size = read_count(ROLE_BIGNUM) * BIGNUM_WORD_BYTES
|
|
budget.scalar!(size)
|
|
magnitude = little_endian(take(size))
|
|
Node.new(type: :bignum, tag: tag,
|
|
value: sign == BIGNUM_SIGN_NEGATIVE ? -magnitude : magnitude)
|
|
end
|
|
|
|
def read_float(tag)
|
|
value, tail = FloatBody.decode(read_counted_bytes)
|
|
Node.new(type: :float, tag: tag, value: value, undecoded_tail: tail)
|
|
end
|
|
|
|
def read_string(tag)
|
|
Node.new(type: :string, tag: tag, value: read_counted_bytes)
|
|
end
|
|
|
|
def read_regexp(tag)
|
|
node = Node.new(type: :regexp, tag: tag, value: read_counted_bytes)
|
|
node.regexp_options = take_byte
|
|
node
|
|
end
|
|
|
|
def read_array(tag, depth)
|
|
node = register(Node.new(type: :array, tag: tag))
|
|
read_entry_count(ROLE_ARRAY).times { node.children << read_value(depth + 1) }
|
|
node
|
|
end
|
|
|
|
def read_hash(tag, depth)
|
|
node = register(Node.new(type: :hash, tag: tag))
|
|
read_entry_count(ROLE_HASH).times { node.children << read_pair(depth) }
|
|
node.children << read_value(depth + 1) if tag == TAG_HASH_DEFAULT
|
|
node
|
|
end
|
|
|
|
def read_pair(depth)
|
|
pair = Node.new(type: :pair)
|
|
pair.children << read_value(depth + 1)
|
|
pair.children << read_value(depth + 1)
|
|
pair
|
|
end
|
|
|
|
def note_role_anomaly(role, node)
|
|
return if CLASS_NAME_TYPES.include?(node.type)
|
|
|
|
@role_anomalies << format(ROLE_ANOMALY, role, node.type).freeze
|
|
end
|
|
|
|
def read_class_name(node, depth)
|
|
class_node = read_value(depth)
|
|
unless CLASS_NAME_TYPES.include?(class_node.type)
|
|
raise MalformedValueError,
|
|
"class name slot holds #{class_node.type}, not a symbol"
|
|
end
|
|
|
|
node.class_name = class_node.value.to_s
|
|
node.auxiliary << class_node
|
|
node
|
|
end
|
|
|
|
def read_instance_variables(node, depth)
|
|
count = read_entry_count(ROLE_IVAR)
|
|
budget.instance_variables!(count)
|
|
count.times do
|
|
name = read_value(depth + 1)
|
|
value = read_value(depth + 1)
|
|
note_role_anomaly(ROLE_IVAR, name)
|
|
node.auxiliary << name
|
|
node.auxiliary << value
|
|
node.instance_variables_map[name.value] = value
|
|
node.instance_variable_pairs << [name, value].freeze
|
|
end
|
|
node
|
|
end
|
|
|
|
def read_ivar(_tag, depth)
|
|
read_instance_variables(read_value(depth + 1), depth)
|
|
end
|
|
|
|
def read_object(tag, depth)
|
|
node = register(Node.new(type: :object, tag: tag))
|
|
read_class_name(node, depth + 1)
|
|
read_instance_variables(node, depth)
|
|
end
|
|
|
|
def read_struct(tag, depth)
|
|
node = register(Node.new(type: :struct, tag: tag))
|
|
read_class_name(node, depth + 1)
|
|
count = read_entry_count(ROLE_STRUCT)
|
|
budget.struct_members!(count)
|
|
count.times do
|
|
pair = read_pair(depth)
|
|
note_role_anomaly(ROLE_STRUCT, pair.children.first)
|
|
node.children << pair
|
|
end
|
|
node
|
|
end
|
|
|
|
def read_userdef(tag, depth)
|
|
node = Node.new(type: :userdef, tag: tag)
|
|
read_class_name(node, depth + 1)
|
|
node.value = read_counted_bytes
|
|
node
|
|
end
|
|
|
|
def read_usermarshal(tag, depth)
|
|
node = register(Node.new(type: :usermarshal, tag: tag))
|
|
read_class_name(node, depth + 1)
|
|
node.children << read_value(depth + 1)
|
|
node
|
|
end
|
|
|
|
def read_wrapped(tag, type, depth)
|
|
node = Node.new(type: type, tag: tag)
|
|
register(node) if REGISTERED_WRAPPER_TYPES.include?(type)
|
|
read_class_name(node, depth + 1)
|
|
node.children << read_value(depth + 1)
|
|
node
|
|
end
|
|
|
|
def read_named(tag, type)
|
|
size = read_count(ROLE_LENGTH)
|
|
budget.class_name!(size)
|
|
budget.scalar!(size)
|
|
Node.new(type: type, tag: tag, class_name: take(size))
|
|
end
|
|
end
|
|
end
|
|
end
|