# frozen_string_literal: true require("bootsnap/bootsnap") module Bootsnap module CompileCache module YAML Uncompilable = Class.new(StandardError) UnsupportedTags = Class.new(Uncompilable) SUPPORTED_INTERNAL_ENCODINGS = [ nil, # UTF-8 Encoding::UTF_8, Encoding::ASCII, Encoding::BINARY, ].freeze class << self attr_accessor(:msgpack_factory, :supported_options) attr_reader(:implementation, :cache_dir) def cache_dir=(cache_dir) @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}yaml" : "#{cache_dir}-yaml" end def precompile(path) return false unless CompileCache::YAML.supported_internal_encoding? CompileCache::Native.precompile( cache_dir, path.to_s, @implementation, ) end def install!(cache_dir) self.cache_dir = cache_dir init! ::YAML.singleton_class.prepend(@implementation::Patch) end # Psych coerce strings to `Encoding.default_internal` but Message Pack only support # UTF-8, US-ASCII and BINARY. So if Encoding.default_internal is set to anything else # we can't safely use the cache def supported_internal_encoding? SUPPORTED_INTERNAL_ENCODINGS.include?(Encoding.default_internal) end module EncodingAwareSymbols extend self def unpack(payload) (+payload).force_encoding(Encoding::UTF_8).to_sym end end def init! require("yaml") require("msgpack") require("date") @implementation = ::YAML::VERSION >= "4" ? Psych4 : Psych3 if @implementation::Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file) @implementation::Patch.send(:remove_method, :unsafe_load_file) end # MessagePack serializes symbols as strings by default. # We want them to roundtrip cleanly, so we use a custom factory. # see: https://github.com/msgpack/msgpack-ruby/pull/122 factory = MessagePack::Factory.new factory.register_type( 0x00, Symbol, packer: :to_msgpack_ext, unpacker: EncodingAwareSymbols.method(:unpack).to_proc, ) if defined? MessagePack::Timestamp factory.register_type( MessagePack::Timestamp::TYPE, # or just -1 Time, packer: MessagePack::Time::Packer, unpacker: MessagePack::Time::Unpacker, ) marshal_fallback = { packer: ->(value) { Marshal.dump(value) }, unpacker: ->(payload) { Marshal.load(payload) }, } { Date => 0x01, Regexp => 0x02, }.each do |type, code| factory.register_type(code, type, marshal_fallback) end end self.msgpack_factory = factory self.supported_options = [] params = ::YAML.method(:load).parameters if params.include?([:key, :symbolize_names]) supported_options << :symbolize_names end if params.include?([:key, :freeze]) if factory.load(factory.dump("yaml"), freeze: true).frozen? supported_options << :freeze end end supported_options.freeze end def patch @implementation::Patch end def strict_load(payload) ast = ::YAML.parse(payload) return ast unless ast strict_visitor.create.visit(ast) end def strict_visitor self::NoTagsVisitor ||= Class.new(Psych::Visitors::ToRuby) do def visit(target) if target.tag raise UnsupportedTags, "YAML tags are not supported: #{target.tag}" end super end end end end module Psych4 extend self def input_to_storage(contents, _) obj = SafeLoad.input_to_storage(contents, nil) if UNCOMPILABLE.equal?(obj) obj = UnsafeLoad.input_to_storage(contents, nil) end obj end module UnsafeLoad extend self def input_to_storage(contents, _) obj = ::YAML.unsafe_load(contents) packer = CompileCache::YAML.msgpack_factory.packer packer.pack(false) # not safe loaded begin packer.pack(obj) rescue NoMethodError, RangeError return UNCOMPILABLE # The object included things that we can't serialize end packer.to_s end def storage_to_output(data, kwargs) if kwargs&.key?(:symbolize_names) kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names) end unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs) unpacker.feed(data) _safe_loaded = unpacker.unpack unpacker.unpack end def input_to_output(data, kwargs) ::YAML.unsafe_load(data, **(kwargs || {})) end end module SafeLoad extend self def input_to_storage(contents, _) obj = begin CompileCache::YAML.strict_load(contents) rescue Psych::DisallowedClass, Psych::BadAlias, Uncompilable return UNCOMPILABLE end packer = CompileCache::YAML.msgpack_factory.packer packer.pack(true) # safe loaded begin packer.pack(obj) rescue NoMethodError, RangeError return UNCOMPILABLE end packer.to_s end def storage_to_output(data, kwargs) if kwargs&.key?(:symbolize_names) kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names) end unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs) unpacker.feed(data) safe_loaded = unpacker.unpack if safe_loaded unpacker.unpack else UNCOMPILABLE end end def input_to_output(data, kwargs) ::YAML.load(data, **(kwargs || {})) end end module Patch def load_file(path, *args) return super unless CompileCache::YAML.supported_internal_encoding? return super if args.size > 1 if (kwargs = args.first) return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty? end begin CompileCache::Native.fetch( CompileCache::YAML.cache_dir, File.realpath(path), CompileCache::YAML::Psych4::SafeLoad, kwargs, ) rescue Errno::EACCES CompileCache.permission_error(path) end end ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true) def unsafe_load_file(path, *args) return super unless CompileCache::YAML.supported_internal_encoding? return super if args.size > 1 if (kwargs = args.first) return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty? end begin CompileCache::Native.fetch( CompileCache::YAML.cache_dir, File.realpath(path), CompileCache::YAML::Psych4::UnsafeLoad, kwargs, ) rescue Errno::EACCES CompileCache.permission_error(path) end end ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true) end end module Psych3 extend self def input_to_storage(contents, _) obj = ::YAML.load(contents) packer = CompileCache::YAML.msgpack_factory.packer packer.pack(false) # not safe loaded begin packer.pack(obj) rescue NoMethodError, RangeError return UNCOMPILABLE # The object included things that we can't serialize end packer.to_s end def storage_to_output(data, kwargs) if kwargs&.key?(:symbolize_names) kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names) end unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs) unpacker.feed(data) _safe_loaded = unpacker.unpack unpacker.unpack end def input_to_output(data, kwargs) ::YAML.load(data, **(kwargs || {})) end module Patch def load_file(path, *args) return super unless CompileCache::YAML.supported_internal_encoding? return super if args.size > 1 if (kwargs = args.first) return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty? end begin CompileCache::Native.fetch( CompileCache::YAML.cache_dir, File.realpath(path), CompileCache::YAML::Psych3, kwargs, ) rescue Errno::EACCES CompileCache.permission_error(path) end end ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true) def unsafe_load_file(path, *args) return super unless CompileCache::YAML.supported_internal_encoding? return super if args.size > 1 if (kwargs = args.first) return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty? end begin CompileCache::Native.fetch( CompileCache::YAML.cache_dir, File.realpath(path), CompileCache::YAML::Psych3, kwargs, ) rescue Errno::EACCES CompileCache.permission_error(path) end end ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true) end end end end end