This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
require "safe_yaml/load"
|
||||
|
||||
module YAML
|
||||
def self.load_with_options(yaml, *original_arguments)
|
||||
filename, options = filename_and_options_from_arguments(original_arguments)
|
||||
safe_mode = safe_mode_from_options("load", options)
|
||||
arguments = [yaml]
|
||||
|
||||
if safe_mode == :safe
|
||||
arguments << filename if SafeYAML::YAML_ENGINE == "psych"
|
||||
arguments << options_for_safe_load(options)
|
||||
safe_load(*arguments)
|
||||
else
|
||||
arguments << filename if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
||||
unsafe_load(*arguments)
|
||||
end
|
||||
end
|
||||
|
||||
def self.load_file_with_options(file, options={})
|
||||
safe_mode = safe_mode_from_options("load_file", options)
|
||||
if safe_mode == :safe
|
||||
safe_load_file(file, options_for_safe_load(options))
|
||||
else
|
||||
unsafe_load_file(file)
|
||||
end
|
||||
end
|
||||
|
||||
def self.safe_load(*args)
|
||||
SafeYAML.load(*args)
|
||||
end
|
||||
|
||||
def self.safe_load_file(*args)
|
||||
SafeYAML.load_file(*args)
|
||||
end
|
||||
|
||||
if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
||||
def self.unsafe_load_file(filename)
|
||||
# https://github.com/tenderlove/psych/blob/v1.3.2/lib/psych.rb#L296-298
|
||||
File.open(filename, 'r:bom|utf-8') { |f| self.unsafe_load(f, filename) }
|
||||
end
|
||||
|
||||
else
|
||||
def self.unsafe_load_file(filename)
|
||||
# https://github.com/tenderlove/psych/blob/v1.2.2/lib/psych.rb#L231-233
|
||||
self.unsafe_load File.open(filename)
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
alias_method :unsafe_load, :load
|
||||
alias_method :load, :load_with_options
|
||||
alias_method :load_file, :load_file_with_options
|
||||
|
||||
private
|
||||
def filename_and_options_from_arguments(arguments)
|
||||
if arguments.count == 1
|
||||
if arguments.first.is_a?(String)
|
||||
return arguments.first, {}
|
||||
else
|
||||
return nil, arguments.first || {}
|
||||
end
|
||||
|
||||
else
|
||||
return arguments.first, arguments.last || {}
|
||||
end
|
||||
end
|
||||
|
||||
def safe_mode_from_options(method, options={})
|
||||
if options[:safe].nil?
|
||||
safe_mode = SafeYAML::OPTIONS[:default_mode] || :safe
|
||||
|
||||
if SafeYAML::OPTIONS[:default_mode].nil? && !SafeYAML::OPTIONS[:suppress_warnings]
|
||||
|
||||
Kernel.warn <<-EOWARNING.gsub(/^\s+/, '')
|
||||
Called '#{method}' without the :safe option -- defaulting to #{safe_mode} mode.
|
||||
You can avoid this warning in the future by setting the SafeYAML::OPTIONS[:default_mode] option (to :safe or :unsafe).
|
||||
EOWARNING
|
||||
|
||||
SafeYAML::OPTIONS[:suppress_warnings] = true
|
||||
end
|
||||
|
||||
return safe_mode
|
||||
end
|
||||
|
||||
options[:safe] ? :safe : :unsafe
|
||||
end
|
||||
|
||||
def options_for_safe_load(base_options)
|
||||
options = base_options.dup
|
||||
options.delete(:safe)
|
||||
options
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
module SafeYAML
|
||||
class Deep
|
||||
def self.freeze(object)
|
||||
object.each do |*entry|
|
||||
value = entry.last
|
||||
case value
|
||||
when String, Regexp
|
||||
value.freeze
|
||||
when Enumerable
|
||||
Deep.freeze(value)
|
||||
end
|
||||
end
|
||||
|
||||
return object.freeze
|
||||
end
|
||||
|
||||
def self.copy(object)
|
||||
duplicate = object.dup rescue object
|
||||
|
||||
case object
|
||||
when Array
|
||||
(0...duplicate.count).each do |i|
|
||||
duplicate[i] = Deep.copy(duplicate[i])
|
||||
end
|
||||
when Hash
|
||||
duplicate.keys.each do |key|
|
||||
duplicate[key] = Deep.copy(duplicate[key])
|
||||
end
|
||||
end
|
||||
|
||||
duplicate
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
require "set"
|
||||
|
||||
module SafeYAML
|
||||
class LibyamlChecker
|
||||
LIBYAML_VERSION = Psych::LIBYAML_VERSION rescue nil
|
||||
|
||||
# Do proper version comparison (e.g. so 0.1.10 is >= 0.1.6)
|
||||
SAFE_LIBYAML_VERSION = Gem::Version.new("0.1.6")
|
||||
|
||||
KNOWN_PATCHED_LIBYAML_VERSIONS = Set.new([
|
||||
# http://people.canonical.com/~ubuntu-security/cve/2014/CVE-2014-2525.html
|
||||
"0.1.4-2ubuntu0.12.04.3",
|
||||
"0.1.4-2ubuntu0.12.10.3",
|
||||
"0.1.4-2ubuntu0.13.10.3",
|
||||
"0.1.4-3ubuntu3",
|
||||
|
||||
# https://security-tracker.debian.org/tracker/CVE-2014-2525
|
||||
"0.1.3-1+deb6u4",
|
||||
"0.1.4-2+deb7u4",
|
||||
"0.1.4-3.2"
|
||||
]).freeze
|
||||
|
||||
def self.libyaml_version_ok?
|
||||
return true if YAML_ENGINE != "psych" || defined?(JRUBY_VERSION)
|
||||
return true if Gem::Version.new(LIBYAML_VERSION || "0") >= SAFE_LIBYAML_VERSION
|
||||
return libyaml_patched?
|
||||
end
|
||||
|
||||
def self.libyaml_patched?
|
||||
return false if (`which dpkg` rescue '').empty?
|
||||
libyaml_version = `dpkg -s libyaml-0-2`.match(/^Version: (.*)$/)
|
||||
return false if libyaml_version.nil?
|
||||
KNOWN_PATCHED_LIBYAML_VERSIONS.include?(libyaml_version[1])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,181 @@
|
||||
require "set"
|
||||
require "yaml"
|
||||
|
||||
# This needs to be defined up front in case any internal classes need to base
|
||||
# their behavior off of this.
|
||||
module SafeYAML
|
||||
YAML_ENGINE = defined?(YAML::ENGINE) ? YAML::ENGINE.yamler : (defined?(Psych) && YAML == Psych ? "psych" : "syck")
|
||||
end
|
||||
|
||||
require "safe_yaml/libyaml_checker"
|
||||
require "safe_yaml/deep"
|
||||
require "safe_yaml/parse/hexadecimal"
|
||||
require "safe_yaml/parse/sexagesimal"
|
||||
require "safe_yaml/parse/date"
|
||||
require "safe_yaml/transform/transformation_map"
|
||||
require "safe_yaml/transform/to_boolean"
|
||||
require "safe_yaml/transform/to_date"
|
||||
require "safe_yaml/transform/to_float"
|
||||
require "safe_yaml/transform/to_integer"
|
||||
require "safe_yaml/transform/to_nil"
|
||||
require "safe_yaml/transform/to_symbol"
|
||||
require "safe_yaml/transform"
|
||||
require "safe_yaml/resolver"
|
||||
require "safe_yaml/syck_hack" if SafeYAML::YAML_ENGINE == "syck" && defined?(JRUBY_VERSION)
|
||||
|
||||
module SafeYAML
|
||||
MULTI_ARGUMENT_YAML_LOAD = YAML.method(:load).arity != 1
|
||||
|
||||
DEFAULT_OPTIONS = Deep.freeze({
|
||||
:default_mode => nil,
|
||||
:suppress_warnings => false,
|
||||
:deserialize_symbols => false,
|
||||
:whitelisted_tags => [],
|
||||
:custom_initializers => {},
|
||||
:raise_on_unknown_tag => false
|
||||
})
|
||||
|
||||
OPTIONS = Deep.copy(DEFAULT_OPTIONS)
|
||||
|
||||
PREDEFINED_TAGS = {}
|
||||
|
||||
if YAML_ENGINE == "syck"
|
||||
YAML.tagged_classes.each do |tag, klass|
|
||||
PREDEFINED_TAGS[klass] = tag
|
||||
end
|
||||
|
||||
else
|
||||
# Special tags appear to be hard-coded in Psych:
|
||||
# https://github.com/tenderlove/psych/blob/v1.3.4/lib/psych/visitors/to_ruby.rb
|
||||
# Fortunately, there aren't many that SafeYAML doesn't already support.
|
||||
PREDEFINED_TAGS.merge!({
|
||||
Exception => "!ruby/exception",
|
||||
Range => "!ruby/range",
|
||||
Regexp => "!ruby/regexp",
|
||||
})
|
||||
end
|
||||
|
||||
Deep.freeze(PREDEFINED_TAGS)
|
||||
|
||||
module_function
|
||||
|
||||
def restore_defaults!
|
||||
OPTIONS.clear.merge!(Deep.copy(DEFAULT_OPTIONS))
|
||||
end
|
||||
|
||||
def tag_safety_check!(tag, options)
|
||||
return if tag.nil? || tag == "!"
|
||||
if options[:raise_on_unknown_tag] && !options[:whitelisted_tags].include?(tag) && !tag_is_explicitly_trusted?(tag)
|
||||
raise "Unknown YAML tag '#{tag}'"
|
||||
end
|
||||
end
|
||||
|
||||
def whitelist!(*classes)
|
||||
classes.each do |klass|
|
||||
whitelist_class!(klass)
|
||||
end
|
||||
end
|
||||
|
||||
def whitelist_class!(klass)
|
||||
raise "#{klass} not a Class" unless klass.is_a?(::Class)
|
||||
|
||||
klass_name = klass.name
|
||||
raise "#{klass} cannot be anonymous" if klass_name.nil? || klass_name.empty?
|
||||
|
||||
# Whitelist any built-in YAML tags supplied by Syck or Psych.
|
||||
predefined_tag = PREDEFINED_TAGS[klass]
|
||||
if predefined_tag
|
||||
OPTIONS[:whitelisted_tags] << predefined_tag
|
||||
return
|
||||
end
|
||||
|
||||
# Exception is exceptional (har har).
|
||||
tag_class = klass < Exception ? "exception" : "object"
|
||||
|
||||
tag_prefix = case YAML_ENGINE
|
||||
when "psych" then "!ruby/#{tag_class}"
|
||||
when "syck" then "tag:ruby.yaml.org,2002:#{tag_class}"
|
||||
else raise "unknown YAML_ENGINE #{YAML_ENGINE}"
|
||||
end
|
||||
OPTIONS[:whitelisted_tags] << "#{tag_prefix}:#{klass_name}"
|
||||
end
|
||||
|
||||
if YAML_ENGINE == "psych"
|
||||
def tag_is_explicitly_trusted?(tag)
|
||||
false
|
||||
end
|
||||
|
||||
else
|
||||
TRUSTED_TAGS = Set.new([
|
||||
"tag:yaml.org,2002:binary",
|
||||
"tag:yaml.org,2002:bool#no",
|
||||
"tag:yaml.org,2002:bool#yes",
|
||||
"tag:yaml.org,2002:float",
|
||||
"tag:yaml.org,2002:float#fix",
|
||||
"tag:yaml.org,2002:int",
|
||||
"tag:yaml.org,2002:map",
|
||||
"tag:yaml.org,2002:null",
|
||||
"tag:yaml.org,2002:seq",
|
||||
"tag:yaml.org,2002:str",
|
||||
"tag:yaml.org,2002:timestamp",
|
||||
"tag:yaml.org,2002:timestamp#ymd"
|
||||
]).freeze
|
||||
|
||||
def tag_is_explicitly_trusted?(tag)
|
||||
TRUSTED_TAGS.include?(tag)
|
||||
end
|
||||
end
|
||||
|
||||
if SafeYAML::YAML_ENGINE == "psych"
|
||||
require "safe_yaml/psych_handler"
|
||||
require "safe_yaml/psych_resolver"
|
||||
require "safe_yaml/safe_to_ruby_visitor"
|
||||
|
||||
def self.load(yaml, filename=nil, options={})
|
||||
# If the user hasn't whitelisted any tags, we can go with this implementation which is
|
||||
# significantly faster.
|
||||
if (options && options[:whitelisted_tags] || SafeYAML::OPTIONS[:whitelisted_tags]).empty?
|
||||
safe_handler = SafeYAML::PsychHandler.new(options) do |result|
|
||||
return result
|
||||
end
|
||||
arguments_for_parse = [yaml]
|
||||
arguments_for_parse << filename if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
||||
Psych::Parser.new(safe_handler).parse(*arguments_for_parse)
|
||||
return safe_handler.result
|
||||
|
||||
else
|
||||
safe_resolver = SafeYAML::PsychResolver.new(options)
|
||||
tree = SafeYAML::MULTI_ARGUMENT_YAML_LOAD ?
|
||||
Psych.parse(yaml, filename) :
|
||||
Psych.parse(yaml)
|
||||
return safe_resolver.resolve_node(tree)
|
||||
end
|
||||
end
|
||||
|
||||
def self.load_file(filename, options={})
|
||||
if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
||||
File.open(filename, 'r:bom|utf-8') { |f| self.load(f, filename, options) }
|
||||
|
||||
else
|
||||
# Ruby pukes on 1.9.2 if we try to open an empty file w/ 'r:bom|utf-8';
|
||||
# so we'll not specify those flags here. This mirrors the behavior for
|
||||
# unsafe_load_file so it's probably preferable anyway.
|
||||
self.load File.open(filename), nil, options
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
require "safe_yaml/syck_resolver"
|
||||
require "safe_yaml/syck_node_monkeypatch"
|
||||
|
||||
def self.load(yaml, options={})
|
||||
resolver = SafeYAML::SyckResolver.new(SafeYAML::OPTIONS.merge(options || {}))
|
||||
tree = YAML.parse(yaml)
|
||||
return resolver.resolve_node(tree)
|
||||
end
|
||||
|
||||
def self.load_file(filename, options={})
|
||||
File.open(filename) { |f| self.load(f, options) }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
require 'time'
|
||||
|
||||
module SafeYAML
|
||||
class Parse
|
||||
class Date
|
||||
# This one's easy enough :)
|
||||
DATE_MATCHER = /\A(\d{4})-(\d{2})-(\d{2})\Z/.freeze
|
||||
|
||||
# This unbelievable little gem is taken basically straight from the YAML spec, but made
|
||||
# slightly more readable (to my poor eyes at least) to me:
|
||||
# http://yaml.org/type/timestamp.html
|
||||
TIME_MATCHER = /\A\d{4}-\d{1,2}-\d{1,2}(?:[Tt]|\s+)\d{1,2}:\d{2}:\d{2}(?:\.\d*)?\s*(?:Z|[-+]\d{1,2}(?::?\d{2})?)?\Z/.freeze
|
||||
|
||||
SECONDS_PER_DAY = 60 * 60 * 24
|
||||
MICROSECONDS_PER_SECOND = 1000000
|
||||
|
||||
# So this is weird. In Ruby 1.8.7, the DateTime#sec_fraction method returned fractional
|
||||
# seconds in units of DAYS for some reason. In 1.9.2, they changed the units -- much more
|
||||
# reasonably -- to seconds.
|
||||
SEC_FRACTION_MULTIPLIER = RUBY_VERSION == "1.8.7" ? (SECONDS_PER_DAY * MICROSECONDS_PER_SECOND) : MICROSECONDS_PER_SECOND
|
||||
|
||||
# The DateTime class has a #to_time method in Ruby 1.9+;
|
||||
# Before that we'll just need to convert DateTime to Time ourselves.
|
||||
TO_TIME_AVAILABLE = DateTime.instance_methods.include?(:to_time)
|
||||
|
||||
def self.value(value)
|
||||
d = DateTime.parse(value)
|
||||
|
||||
return d.to_time if TO_TIME_AVAILABLE
|
||||
|
||||
usec = d.sec_fraction * SEC_FRACTION_MULTIPLIER
|
||||
time = Time.utc(d.year, d.month, d.day, d.hour, d.min, d.sec, usec) - (d.offset * SECONDS_PER_DAY)
|
||||
time.getlocal
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
module SafeYAML
|
||||
class Parse
|
||||
class Hexadecimal
|
||||
MATCHER = /\A[-+]?0x[0-9a-fA-F_]+\Z/.freeze
|
||||
|
||||
def self.value(value)
|
||||
# This is safe to do since we already validated the value.
|
||||
return Integer(value.gsub(/_/, ""))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
module SafeYAML
|
||||
class Parse
|
||||
class Sexagesimal
|
||||
INTEGER_MATCHER = /\A[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\Z/.freeze
|
||||
FLOAT_MATCHER = /\A[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\.[0-9_]*\Z/.freeze
|
||||
|
||||
def self.value(value)
|
||||
before_decimal, after_decimal = value.split(".")
|
||||
|
||||
whole_part = 0
|
||||
multiplier = 1
|
||||
|
||||
before_decimal = before_decimal.split(":")
|
||||
until before_decimal.empty?
|
||||
whole_part += (Float(before_decimal.pop) * multiplier)
|
||||
multiplier *= 60
|
||||
end
|
||||
|
||||
result = whole_part
|
||||
result += Float("." + after_decimal) unless after_decimal.nil?
|
||||
result *= -1 if value[0] == "-"
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,99 @@
|
||||
require "psych"
|
||||
require "base64"
|
||||
|
||||
module SafeYAML
|
||||
class PsychHandler < Psych::Handler
|
||||
def initialize(options, &block)
|
||||
@options = SafeYAML::OPTIONS.merge(options || {})
|
||||
@block = block
|
||||
@initializers = @options[:custom_initializers] || {}
|
||||
@anchors = {}
|
||||
@stack = []
|
||||
@current_key = nil
|
||||
@result = nil
|
||||
@begun = false
|
||||
end
|
||||
|
||||
def result
|
||||
@begun ? @result : false
|
||||
end
|
||||
|
||||
def add_to_current_structure(value, anchor=nil, quoted=nil, tag=nil)
|
||||
value = Transform.to_proper_type(value, quoted, tag, @options)
|
||||
|
||||
@anchors[anchor] = value if anchor
|
||||
|
||||
if !@begun
|
||||
@begun = true
|
||||
@result = value
|
||||
@current_structure = @result
|
||||
return
|
||||
end
|
||||
|
||||
if @current_structure.respond_to?(:<<)
|
||||
@current_structure << value
|
||||
|
||||
elsif @current_structure.respond_to?(:[]=)
|
||||
if @current_key.nil?
|
||||
@current_key = value
|
||||
|
||||
else
|
||||
if @current_key == "<<"
|
||||
@current_structure.merge!(value)
|
||||
else
|
||||
@current_structure[@current_key] = value
|
||||
end
|
||||
|
||||
@current_key = nil
|
||||
end
|
||||
|
||||
else
|
||||
raise "Don't know how to add to a #{@current_structure.class}!"
|
||||
end
|
||||
end
|
||||
|
||||
def end_current_structure
|
||||
@stack.pop
|
||||
@current_structure = @stack.last
|
||||
end
|
||||
|
||||
def streaming?
|
||||
true
|
||||
end
|
||||
|
||||
# event handlers
|
||||
def alias(anchor)
|
||||
add_to_current_structure(@anchors[anchor])
|
||||
end
|
||||
|
||||
def scalar(value, anchor, tag, plain, quoted, style)
|
||||
add_to_current_structure(value, anchor, quoted, tag)
|
||||
end
|
||||
|
||||
def end_document(implicit)
|
||||
@block.call(@result)
|
||||
end
|
||||
|
||||
def start_mapping(anchor, tag, implicit, style)
|
||||
map = @initializers.include?(tag) ? @initializers[tag].call : {}
|
||||
self.add_to_current_structure(map, anchor)
|
||||
@current_structure = map
|
||||
@stack.push(map)
|
||||
end
|
||||
|
||||
def end_mapping
|
||||
self.end_current_structure()
|
||||
end
|
||||
|
||||
def start_sequence(anchor, tag, implicit, style)
|
||||
seq = @initializers.include?(tag) ? @initializers[tag].call : []
|
||||
self.add_to_current_structure(seq, anchor)
|
||||
@current_structure = seq
|
||||
@stack.push(seq)
|
||||
end
|
||||
|
||||
def end_sequence
|
||||
self.end_current_structure()
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
module SafeYAML
|
||||
class PsychResolver < Resolver
|
||||
NODE_TYPES = {
|
||||
Psych::Nodes::Document => :root,
|
||||
Psych::Nodes::Mapping => :map,
|
||||
Psych::Nodes::Sequence => :seq,
|
||||
Psych::Nodes::Scalar => :scalar,
|
||||
Psych::Nodes::Alias => :alias
|
||||
}.freeze
|
||||
|
||||
def initialize(options={})
|
||||
super
|
||||
@aliased_nodes = {}
|
||||
end
|
||||
|
||||
def resolve_root(root)
|
||||
resolve_seq(root).first
|
||||
end
|
||||
|
||||
def resolve_alias(node)
|
||||
resolve_node(@aliased_nodes[node.anchor])
|
||||
end
|
||||
|
||||
def native_resolve(node)
|
||||
@visitor ||= SafeYAML::SafeToRubyVisitor.new(self)
|
||||
@visitor.accept(node)
|
||||
end
|
||||
|
||||
def get_node_type(node)
|
||||
NODE_TYPES[node.class]
|
||||
end
|
||||
|
||||
def get_node_tag(node)
|
||||
node.tag
|
||||
end
|
||||
|
||||
def get_node_value(node)
|
||||
@aliased_nodes[node.anchor] = node if node.respond_to?(:anchor) && node.anchor
|
||||
|
||||
case get_node_type(node)
|
||||
when :root, :map, :seq
|
||||
node.children
|
||||
when :scalar
|
||||
node.value
|
||||
end
|
||||
end
|
||||
|
||||
def value_is_quoted?(node)
|
||||
node.quoted
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
module SafeYAML
|
||||
class Resolver
|
||||
def initialize(options)
|
||||
@options = SafeYAML::OPTIONS.merge(options || {})
|
||||
@whitelist = @options[:whitelisted_tags] || []
|
||||
@initializers = @options[:custom_initializers] || {}
|
||||
@raise_on_unknown_tag = @options[:raise_on_unknown_tag]
|
||||
end
|
||||
|
||||
def resolve_node(node)
|
||||
return node if !node
|
||||
return self.native_resolve(node) if tag_is_whitelisted?(self.get_node_tag(node))
|
||||
|
||||
case self.get_node_type(node)
|
||||
when :root
|
||||
resolve_root(node)
|
||||
when :map
|
||||
resolve_map(node)
|
||||
when :seq
|
||||
resolve_seq(node)
|
||||
when :scalar
|
||||
resolve_scalar(node)
|
||||
when :alias
|
||||
resolve_alias(node)
|
||||
else
|
||||
raise "Don't know how to resolve this node: #{node.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_map(node)
|
||||
tag = get_and_check_node_tag(node)
|
||||
hash = @initializers.include?(tag) ? @initializers[tag].call : {}
|
||||
map = normalize_map(self.get_node_value(node))
|
||||
|
||||
# Take the "<<" key nodes first, as these are meant to approximate a form of inheritance.
|
||||
inheritors = map.select { |key_node, value_node| resolve_node(key_node) == "<<" }
|
||||
inheritors.each do |key_node, value_node|
|
||||
merge_into_hash(hash, resolve_node(value_node))
|
||||
end
|
||||
|
||||
# All that's left should be normal (non-"<<") nodes.
|
||||
(map - inheritors).each do |key_node, value_node|
|
||||
hash[resolve_node(key_node)] = resolve_node(value_node)
|
||||
end
|
||||
|
||||
return hash
|
||||
end
|
||||
|
||||
def resolve_seq(node)
|
||||
seq = self.get_node_value(node)
|
||||
|
||||
tag = get_and_check_node_tag(node)
|
||||
arr = @initializers.include?(tag) ? @initializers[tag].call : []
|
||||
|
||||
seq.inject(arr) { |array, n| array << resolve_node(n) }
|
||||
end
|
||||
|
||||
def resolve_scalar(node)
|
||||
Transform.to_proper_type(self.get_node_value(node), self.value_is_quoted?(node), get_and_check_node_tag(node), @options)
|
||||
end
|
||||
|
||||
def get_and_check_node_tag(node)
|
||||
tag = self.get_node_tag(node)
|
||||
SafeYAML.tag_safety_check!(tag, @options)
|
||||
tag
|
||||
end
|
||||
|
||||
def tag_is_whitelisted?(tag)
|
||||
@whitelist.include?(tag)
|
||||
end
|
||||
|
||||
def options
|
||||
@options
|
||||
end
|
||||
|
||||
private
|
||||
def normalize_map(map)
|
||||
# Syck creates Hashes from maps.
|
||||
if map.is_a?(Hash)
|
||||
map.inject([]) { |arr, key_and_value| arr << key_and_value }
|
||||
|
||||
# Psych is really weird; it flattens out a Hash completely into: [key, value, key, value, ...]
|
||||
else
|
||||
map.each_slice(2).to_a
|
||||
end
|
||||
end
|
||||
|
||||
def merge_into_hash(hash, array)
|
||||
array.each do |key, value|
|
||||
hash[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
module SafeYAML
|
||||
class SafeToRubyVisitor < Psych::Visitors::ToRuby
|
||||
INITIALIZE_ARITY = superclass.instance_method(:initialize).arity
|
||||
|
||||
def initialize(resolver)
|
||||
case INITIALIZE_ARITY
|
||||
when 2
|
||||
# https://github.com/tenderlove/psych/blob/v2.0.0/lib/psych/visitors/to_ruby.rb#L14-L28
|
||||
loader = Psych::ClassLoader.new
|
||||
scanner = Psych::ScalarScanner.new(loader)
|
||||
super(scanner, loader)
|
||||
|
||||
else
|
||||
super()
|
||||
end
|
||||
|
||||
@resolver = resolver
|
||||
end
|
||||
|
||||
def accept(node)
|
||||
if node.tag
|
||||
SafeYAML.tag_safety_check!(node.tag, @resolver.options)
|
||||
return super
|
||||
end
|
||||
|
||||
@resolver.resolve_node(node)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
require 'safe_yaml/load'
|
||||
require 'yaml/store'
|
||||
|
||||
module SafeYAML
|
||||
|
||||
class Store < YAML::Store
|
||||
|
||||
# Override YAML::Store#initialize to accept additional option
|
||||
# +safe_yaml_opts+.
|
||||
def initialize(file_name, yaml_opts = {}, safe_yaml_opts = {})
|
||||
@safe_yaml_opts = safe_yaml_opts
|
||||
super(file_name, yaml_opts)
|
||||
end
|
||||
|
||||
# Override YAML::Store#load to use SafeYAML.load instead of
|
||||
# YAML.load (via #safe_yaml_load).
|
||||
#--
|
||||
# PStore#load is private, while YAML::Store#load is public.
|
||||
#++
|
||||
def load(content)
|
||||
table = safe_yaml_load(content)
|
||||
table == false ? {} : table
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
if SafeYAML::YAML_ENGINE == 'psych'
|
||||
def safe_yaml_load(content)
|
||||
SafeYAML.load(content, nil, @safe_yaml_opts)
|
||||
end
|
||||
else
|
||||
def safe_yaml_load(content)
|
||||
SafeYAML.load(content, @safe_yaml_opts)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# Hack to JRuby 1.8's YAML Parser Yecht
|
||||
#
|
||||
# This file is always loaded AFTER either syck or psych are already
|
||||
# loaded. It then looks at what constants are available and creates
|
||||
# a consistent view on all rubys.
|
||||
#
|
||||
# Taken from rubygems and modified.
|
||||
# See https://github.com/rubygems/rubygems/blob/master/lib/rubygems/syck_hack.rb
|
||||
|
||||
module YAML
|
||||
# In newer 1.9.2, there is a Syck toplevel constant instead of it
|
||||
# being underneith YAML. If so, reference it back under YAML as
|
||||
# well.
|
||||
if defined? ::Syck
|
||||
# for tests that change YAML::ENGINE
|
||||
# 1.8 does not support the second argument to const_defined?
|
||||
remove_const :Syck rescue nil
|
||||
|
||||
Syck = ::Syck
|
||||
|
||||
# JRuby's "Syck" is called "Yecht"
|
||||
elsif defined? YAML::Yecht
|
||||
Syck = YAML::Yecht
|
||||
end
|
||||
end
|
||||
|
||||
# Sometime in the 1.9 dev cycle, the Syck constant was moved from under YAML
|
||||
# to be a toplevel constant. So gemspecs created under these versions of Syck
|
||||
# will have references to Syck::DefaultKey.
|
||||
#
|
||||
# So we need to be sure that we reference Syck at the toplevel too so that
|
||||
# we can always load these kind of gemspecs.
|
||||
#
|
||||
if !defined?(Syck)
|
||||
Syck = YAML::Syck
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
# This is, admittedly, pretty insane. Fundamentally the challenge here is this: if we want to allow
|
||||
# whitelisting of tags (while still leveraging Syck's internal functionality), then we have to
|
||||
# change how Syck::Node#transform works. But since we (SafeYAML) do not control instantiation of
|
||||
# Syck::Node objects, we cannot, for example, subclass Syck::Node and override #tranform the "easy"
|
||||
# way. So the only choice is to monkeypatch, like this. And the only way to make this work
|
||||
# recursively with potentially call-specific options (that my feeble brain can think of) is to set
|
||||
# pseudo-global options on the first call and unset them once the recursive stack has fully unwound.
|
||||
|
||||
monkeypatch = <<-EORUBY
|
||||
class Node
|
||||
@@safe_transform_depth = 0
|
||||
@@safe_transform_whitelist = nil
|
||||
|
||||
def safe_transform(options={})
|
||||
begin
|
||||
@@safe_transform_depth += 1
|
||||
@@safe_transform_whitelist ||= options[:whitelisted_tags]
|
||||
|
||||
if self.type_id
|
||||
SafeYAML.tag_safety_check!(self.type_id, options)
|
||||
return unsafe_transform if @@safe_transform_whitelist.include?(self.type_id)
|
||||
end
|
||||
|
||||
SafeYAML::SyckResolver.new.resolve_node(self)
|
||||
|
||||
ensure
|
||||
@@safe_transform_depth -= 1
|
||||
if @@safe_transform_depth == 0
|
||||
@@safe_transform_whitelist = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :unsafe_transform, :transform
|
||||
alias_method :transform, :safe_transform
|
||||
end
|
||||
EORUBY
|
||||
|
||||
if defined?(YAML::Syck::Node)
|
||||
YAML::Syck.module_eval monkeypatch
|
||||
else
|
||||
Syck.module_eval monkeypatch
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
module SafeYAML
|
||||
class SyckResolver < Resolver
|
||||
QUOTE_STYLES = [
|
||||
:quote1,
|
||||
:quote2
|
||||
].freeze
|
||||
|
||||
NODE_TYPES = {
|
||||
Hash => :map,
|
||||
Array => :seq,
|
||||
String => :scalar
|
||||
}.freeze
|
||||
|
||||
def initialize(options={})
|
||||
super
|
||||
end
|
||||
|
||||
def native_resolve(node)
|
||||
node.transform(self.options)
|
||||
end
|
||||
|
||||
def get_node_type(node)
|
||||
NODE_TYPES[node.value.class]
|
||||
end
|
||||
|
||||
def get_node_tag(node)
|
||||
node.type_id
|
||||
end
|
||||
|
||||
def get_node_value(node)
|
||||
node.value
|
||||
end
|
||||
|
||||
def value_is_quoted?(node)
|
||||
QUOTE_STYLES.include?(node.instance_variable_get(:@style))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
require 'base64'
|
||||
|
||||
module SafeYAML
|
||||
class Transform
|
||||
TRANSFORMERS = [
|
||||
Transform::ToSymbol.new,
|
||||
Transform::ToInteger.new,
|
||||
Transform::ToFloat.new,
|
||||
Transform::ToNil.new,
|
||||
Transform::ToBoolean.new,
|
||||
Transform::ToDate.new
|
||||
]
|
||||
|
||||
def self.to_guessed_type(value, quoted=false, options=nil)
|
||||
return value if quoted
|
||||
|
||||
if value.is_a?(String)
|
||||
TRANSFORMERS.each do |transformer|
|
||||
success, transformed_value = transformer.method(:transform?).arity == 1 ?
|
||||
transformer.transform?(value) :
|
||||
transformer.transform?(value, options)
|
||||
|
||||
return transformed_value if success
|
||||
end
|
||||
end
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
def self.to_proper_type(value, quoted=false, tag=nil, options=nil)
|
||||
case tag
|
||||
when "tag:yaml.org,2002:binary", "x-private:binary", "!binary"
|
||||
decoded = Base64.decode64(value)
|
||||
decoded = decoded.force_encoding(value.encoding) if decoded.respond_to?(:force_encoding)
|
||||
decoded
|
||||
else
|
||||
self.to_guessed_type(value, quoted, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
module SafeYAML
|
||||
class Transform
|
||||
class ToBoolean
|
||||
include TransformationMap
|
||||
|
||||
set_predefined_values({
|
||||
"yes" => true,
|
||||
"on" => true,
|
||||
"true" => true,
|
||||
"no" => false,
|
||||
"off" => false,
|
||||
"false" => false
|
||||
})
|
||||
|
||||
def transform?(value)
|
||||
return false if value.length > 5
|
||||
return PREDEFINED_VALUES.include?(value), PREDEFINED_VALUES[value]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
module SafeYAML
|
||||
class Transform
|
||||
class ToDate
|
||||
def transform?(value)
|
||||
return true, Date.parse(value) if Parse::Date::DATE_MATCHER.match(value)
|
||||
return true, Parse::Date.value(value) if Parse::Date::TIME_MATCHER.match(value)
|
||||
false
|
||||
rescue ArgumentError
|
||||
return true, value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
module SafeYAML
|
||||
class Transform
|
||||
class ToFloat
|
||||
Infinity = 1.0 / 0.0
|
||||
NaN = 0.0 / 0.0
|
||||
|
||||
PREDEFINED_VALUES = {
|
||||
".inf" => Infinity,
|
||||
".Inf" => Infinity,
|
||||
".INF" => Infinity,
|
||||
"-.inf" => -Infinity,
|
||||
"-.Inf" => -Infinity,
|
||||
"-.INF" => -Infinity,
|
||||
".nan" => NaN,
|
||||
".NaN" => NaN,
|
||||
".NAN" => NaN,
|
||||
}.freeze
|
||||
|
||||
MATCHER = /\A[-+]?(?:\d[\d_]*)?\.[\d_]+(?:[eE][-+][\d]+)?\Z/.freeze
|
||||
|
||||
def transform?(value)
|
||||
return true, Float(value) if MATCHER.match(value)
|
||||
try_edge_cases?(value)
|
||||
end
|
||||
|
||||
def try_edge_cases?(value)
|
||||
return true, PREDEFINED_VALUES[value] if PREDEFINED_VALUES.include?(value)
|
||||
return true, Parse::Sexagesimal.value(value) if Parse::Sexagesimal::FLOAT_MATCHER.match(value)
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
module SafeYAML
|
||||
class Transform
|
||||
class ToInteger
|
||||
MATCHERS = Deep.freeze([
|
||||
/\A[-+]?(0|([1-9][0-9_,]*))\Z/, # decimal
|
||||
/\A0[0-7]+\Z/, # octal
|
||||
/\A0x[0-9a-f]+\Z/i, # hexadecimal
|
||||
/\A0b[01_]+\Z/ # binary
|
||||
])
|
||||
|
||||
def transform?(value)
|
||||
MATCHERS.each_with_index do |matcher, idx|
|
||||
value = value.gsub(/[_,]/, "") if idx == 0
|
||||
return true, Integer(value) if matcher.match(value)
|
||||
end
|
||||
try_edge_cases?(value)
|
||||
end
|
||||
|
||||
def try_edge_cases?(value)
|
||||
return true, Parse::Hexadecimal.value(value) if Parse::Hexadecimal::MATCHER.match(value)
|
||||
return true, Parse::Sexagesimal.value(value) if Parse::Sexagesimal::INTEGER_MATCHER.match(value)
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
module SafeYAML
|
||||
class Transform
|
||||
class ToNil
|
||||
include TransformationMap
|
||||
|
||||
set_predefined_values({
|
||||
"" => nil,
|
||||
"~" => nil,
|
||||
"null" => nil
|
||||
})
|
||||
|
||||
def transform?(value)
|
||||
return false if value.length > 4
|
||||
return PREDEFINED_VALUES.include?(value), PREDEFINED_VALUES[value]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
module SafeYAML
|
||||
class Transform
|
||||
class ToSymbol
|
||||
def transform?(value, options=SafeYAML::OPTIONS)
|
||||
if options[:deserialize_symbols] && value =~ /\A:./
|
||||
if value =~ /\A:(["'])(.*)\1\Z/
|
||||
return true, $2.sub(/^:/, "").to_sym
|
||||
else
|
||||
return true, value.sub(/^:/, "").to_sym
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
module SafeYAML
|
||||
class Transform
|
||||
module TransformationMap
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
class CaseAgnosticMap < Hash
|
||||
def initialize(*args)
|
||||
super
|
||||
end
|
||||
|
||||
def include?(key)
|
||||
super(key.downcase)
|
||||
end
|
||||
|
||||
def [](key)
|
||||
super(key.downcase)
|
||||
end
|
||||
|
||||
# OK, I actually don't think it's all that important that this map be
|
||||
# frozen.
|
||||
def freeze
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def set_predefined_values(predefined_values)
|
||||
if SafeYAML::YAML_ENGINE == "syck"
|
||||
expanded_map = predefined_values.inject({}) do |hash, (key, value)|
|
||||
hash[key] = value
|
||||
hash[key.capitalize] = value
|
||||
hash[key.upcase] = value
|
||||
hash
|
||||
end
|
||||
else
|
||||
expanded_map = CaseAgnosticMap.new
|
||||
expanded_map.merge!(predefined_values)
|
||||
end
|
||||
|
||||
self.const_set(:PREDEFINED_VALUES, expanded_map.freeze)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
module SafeYAML
|
||||
VERSION = "1.0.5"
|
||||
end
|
||||
Reference in New Issue
Block a user