2022-06-24 18:27:00 -07:00

508 lines
20 KiB
Ruby

require "digest/sha2"
require "google/protobuf/wrappers_pb"
require "google/protobuf/well_known_types"
module VagrantPlugins
module CommandServe
# Provides value mapping to ease interaction
# with protobuf and clients
class Mappers
# The default maps define the default mapping of proto
# messages to a proper Ruby type. This is used when
# mapping a value and a destination type is not provided.
DEFAULT_MAPS = {
Client::Project => Vagrant::Environment,
Client::Target => Vagrant::Machine,
Client::Terminal => Vagrant::UI::Remote,
Client::SyncedFolder => Vagrant::Plugin::Remote::SyncedFolder,
Google::Protobuf::BoolValue => Type::Boolean,
Google::Protobuf::BytesValue => String,
Google::Protobuf::DoubleValue => Float,
Google::Protobuf::FloatValue => Float,
Google::Protobuf::Int32Value => Integer,
Google::Protobuf::Int64Value => Integer,
Google::Protobuf::UInt32Value => Integer,
Google::Protobuf::UInt64Value => Integer,
Google::Protobuf::StringValue => String,
SDK::Args::Array => Array,
SDK::Args::ConfigData => Vagrant::Plugin::V2::Config,
SDK::Args::Class => Class,
SDK::Args::CorePluginManager => Client::CorePluginManager,
SDK::Args::Direct => Type::Direct,
SDK::Args::Folders => Type::Folders,
SDK::Args::Guest => Client::Guest,
SDK::Args::Hash => Hash,
SDK::Args::Host => Client::Host,
SDK::Args::NamedCapability => Symbol,
SDK::Args::Null => NilClass,
SDK::Args::Options => Type::Options,
SDK::Args::Path => Pathname,
SDK::Args::ProcRef => Proc,
SDK::Args::Project => Vagrant::Environment,
SDK::Args::Provider => Client::Provider,
SDK::Args::StateBag => Client::StateBag,
SDK::Args::SyncedFolder => Vagrant::Plugin::Remote::SyncedFolder,
SDK::Args::Target => Vagrant::Machine,
SDK::Args::TargetIndex => Client::TargetIndex,
SDK::Args::Target::Machine => Vagrant::Machine,
SDK::Args::TimeDuration => Type::Duration,
SDK::Args::TerminalUI => Vagrant::UI::Remote,
SDK::Command::Arguments => Type::CommandArguments,
SDK::Command::CommandInfo => Type::CommandInfo,
SDK::Communicator::Command => Type::CommunicatorCommandArguments,
SDK::Config::RawRubyValue => Object,
}
# The reverse maps define the default mapping from Ruby types
# to proto messages. This map is built by reversing the default
# maps. The key values are checked against the source value's
# class and its ancestors for a match. This is why the UI interface
# is merged into the map.
REVERSE_MAPS = Hash[DEFAULT_MAPS.values.zip(DEFAULT_MAPS.keys)].merge(
Vagrant::UI::Interface => SDK::Args::TerminalUI,
)
# Remove any top level classes
REVERSE_MAPS.delete_if { |k, _| !k.name.include?("::") }
# REVERSE_MAPS.delete(Object)
# @return [Symbol] marker value for failed direct conversions
FAILED_CONVERT = :__FAILED_CONVERT__
# Constant used for generating value
GENERATE_CLASS = Class.new {
def self.to_s
"[Value Generation]"
end
def to_s
"[Value Generation]"
end
def inspect
to_s
end
}
GENERATE = GENERATE_CLASS.new.freeze
include Util::HasLogger
autoload :Internal, Vagrant.source_root.join("plugins/commands/serve/mappers/internal").to_s
autoload :Mapper, Vagrant.source_root.join("plugins/commands/serve/mappers/mapper").to_s
# @return [Array<Object>] arguments provided to all mapper calls
attr_reader :known_arguments
# @return [Array<Mapper>] list of mappers
attr_reader :mappers
# @return [Util::Cacher] cached mapped values
attr_accessor :cacher
class << self
# @return [Array<Mapper>] frozen list of available mappers
def mappers
@mappers ||= Mapper.registered.map(&:new).freeze
end
# @return [Util::Cacher]
def cache
@cache ||= Util::Cacher.new
end
# Register a destination type for blind mappings
#
# @param src [Class] source type
# @param dst [Class] destination type
# @return [Class] destination type
def register_blind_map(src, dst)
@blind_map_registry[src] = dst
end
# Get a destination type for blind mapping if registered
#
# @param src [Class] source type
# @return [Class, NilClass] destination type or nil
def blind_map_for(src)
@blind_map_registry[src]
end
end
# Initialize our lookup table
@blind_map_registry = {}
# Create a new mappers instance. Any arguments provided will be
# available to all mapper calls
def initialize(*args)
@known_arguments = Array(args).compact
Mapper.generate_anys
@mappers = self.class.mappers
@cacher = self.class.cache
end
def initialize_copy(orig)
@mappers = orig.mappers.dup
@cacher = orig.cacher
@known_arguments = orig.known_arguments
end
# Add an argument to be included with mapping calls
#
# @param v [Object] Argument value
# @return [Object]
def add_argument(v)
if v.nil?
raise TypeError,
"Expected valid argument but received nil value"
end
known_arguments << v
v
end
# Convert Any proto message to actual message type
#
# @param any [Google::Protobuf::Any]
# @return [Google::Protobuf::MessageExts]
def unany(any)
type = find_type(any.type_name.split("/").last.to_s)
any.unpack(type)
end
# Get const from name
#
# @param name [String]
# @return [Class]
def find_type(name)
name.to_s.split(".").inject(Object) { |memo, n|
c = memo.constants.detect { |mc| mc.to_s.downcase == n.to_s.downcase }
raise NameError,
"Failed to find constant for `#{name}'" if c.nil?
memo.const_get(c)
}
end
# Attempt to directly convert value. If a destination type
# is provided, validate the direct conversion matches the
# desired type.
#
# @param value [Object] value to convert
# @param to [Class] Resultant type (optional)
# @return [Object]
def direct_convert(value, to:)
# If we don't have a destination, attempt to do direct conversion
if to.nil?
begin
logger.trace { "running direct blind pre-map on #{value.class}" }
return value.is_a?(Google::Protobuf::MessageExts) ? value.to_ruby : value.to_proto
rescue => err
logger.trace { "direct blind conversion failed in pre-map stage, reason: #{err}" }
end
end
if !to.nil?
# If we are mapping to an any, try doing it directly first
if to == Google::Protobuf::Any
begin
return value.to_any
rescue => err
logger.trace { "direct any conversion failed in pre-map stage, reason: #{err}"}
end
end
# If the destination type is a proto, try doing that directly
if to.ancestors.include?(Google::Protobuf::MessageExts)
begin
proto = value.to_proto
return proto if proto.is_a?(to)
rescue => err
logger.trace { "direct proto conversion failed in pre-map stage, reason: #{err}" }
end
end
# If the destination type is not a proto, but the value is, try that directly
if value.is_a?(Google::Protobuf::MessageExts) && !to.ancestors.include?(Google::Protobuf::MessageExts)
begin
val = value.to_ruby
return val if val.is_a?(to)
rescue => err
logger.trace { "direct ruby conversion failed in pre-map stage, reason: #{err}" }
end
end
end
FAILED_CONVERT
end
# Map a given value
#
# @param value [Object] Value to map
# @param named [String] Named argument to prefer
# @param to [Class] Resultant type (optional)
# @return [Object]
def map(value, *extra_args, named: nil, to: nil)
extra_args = [value, *extra_args].map do |a|
if a.is_a?(Type::NamedArgument)
name = a.name
a = a.value
end
if a.is_a?(Google::Protobuf::Any)
non_any = unany(a)
logger.debug { "extracted any proto message #{a.class} -> #{non_any.class}" }
a = non_any
end
if name
a = Type::NamedArgument.new(value: a, name: name)
end
a
end
value = extra_args.shift if value
# If we don't have a destination type provided, attempt
# to set it using our default maps
to = DEFAULT_MAPS[value.class] if to.nil?
if value != GENERATE && to.nil?
to = REVERSE_MAPS.detect do |k, v|
v if value.class.ancestors.include?(k) &&
v.ancestors.include?(Google::Protobuf::MessageExts)
end&.last
end
# If the value given is the desired type, just return the value
return value if value != GENERATE && !to.nil? && to != Object && value.is_a?(to)
# Let's try some shortcuts before we actually put in the work
# of doing all the mapping stuff
if value == GENERATE
extra_args.each do |ea|
val = direct_convert(ea, to: to)
return val if val != FAILED_CONVERT
end
else
val = direct_convert(value, to: to)
return val if val != FAILED_CONVERT
end
# These only work if we know our destination
logger.debug { "starting value mapping process #{value.class} -> #{to.nil? ? 'unknown' : to.inspect}" }
if value.nil? && to
val = (extra_args + known_arguments).detect do |item|
item.is_a?(to)
end
if val && val != GENERATE
return val
end
end
# NOTE: We set the cacher instance into the extra args
# instead of adding it as a known argument so if it is
# changed the correct instance will be used
extra_args << cacher
extra_args << self
# If the provided value is a protobuf value, just return that value
if value.is_a?(Google::Protobuf::Value)
logger.debug { "direct return of protobuf value contents - #{value.to_ruby}" }
return value.to_ruby
end
args = ([value] + extra_args.compact + known_arguments.compact)
result = nil
# For funcspec values, we want to pre-filter since they use
# a custom validator. This will prevent invalid paths.
if value.is_a?(SDK::FuncSpec::Value)
map_mapper = self.clone
valid_mappers = map_mapper.mappers.map do |m|
next if !m.inputs.first.valid?(SDK::FuncSpec::Value) &&
m.output.ancestors.include?(Google::Protobuf::MessageExts)
next m if !m.inputs.first.valid?(SDK::FuncSpec::Value) ||
m.inputs.first.valid?(value)
logger.trace { "removing mapper - invalid funcspec match - #{m}" }
nil
end.compact
map_mapper.mappers.replace(valid_mappers)
elsif value.is_a?(Google::Protobuf::MessageExts)
map_mapper = self.clone
valid_mappers = map_mapper.mappers.map do |m|
next if value.class == m.output
# next if value.is_a?(m.output)
m
end.compact
map_mapper.mappers.replace(valid_mappers)
else
map_mapper = self
end
# If we don't have a desired final type, test for mappers
# that are satisfied by the arguments we have and run that
# directly
if to.nil? && value != GENERATE && self.class.blind_map_for(value.class)
blind_to = self.class.blind_map_for(value.class)
logger.debug { "found existing blind mapping for type #{value.class} -> #{blind_to}" }
to = blind_to
end
if to.nil?
valid_outputs = []
cb = lambda do |k|
matches = map_mapper.mappers.find_all do |m|
m.inputs.first.valid?(k)
end
outs = matches.map(&:output)
to_search = outs - valid_outputs
valid_outputs |= outs
to_search.each do |o|
cb.call(o)
end
end
cb.call(value)
if valid_outputs.empty?
raise TypeError,
"No valid mappers found for input type `#{value.class}'"
end
valid_outputs.reverse!
valid_outputs.delete_if do |o|
(value.class.ancestors.include?(Google::Protobuf::MessageExts) &&
o.ancestors.include?(Google::Protobuf::MessageExts)) ||
o.ancestors.include?(value.class)
end
last_error = nil
valid_outputs.each do |out|
logger.debug { "attempting blind map #{value.class} -> #{out}" }
begin
m_graph = Internal::Graph::Mappers.new(
output_type: out,
mappers: map_mapper,
named: named,
input_values: args,
source: value != GENERATE ? value.class : nil,
)
result = m_graph.execute
to = out
break
rescue => err
logger.debug { "typeless mapping failure (non-critical): #{err} (input - #{value.class} / output #{out})" }
last_error = err
end
end
raise last_error if result.nil? && last_error
self.class.register_blind_map(value.class, to)
else
m_graph = Internal::Graph::Mappers.new(
output_type: to,
mappers: map_mapper,
named: named,
input_values: args,
source: value != GENERATE ? value.class : nil
)
result = m_graph.execute
end
logger.debug { "map of #{value.class} to #{to.nil? ? 'unknown' : to.inspect} => #{result.class}" }
if !result.is_a?(to)
raise TypeError,
"Value is not expected destination type `#{to}' (actual type: #{result.class})"
end
result
rescue => err
logger.debug { "mapping failed of #{value.class} to #{to.nil? ? 'unknown' : to.inspect} - #{err}" }
logger.debug { "#{err.class}: #{err}\n" + err.backtrace.join("\n") }
raise
end
# Generate the given type based on given and/or
# added arguments
def generate(*args, named: nil, type:)
map(GENERATE, *args, named: named, to: type)
end
# Map values provided by a FuncSpec request into
# actual values
#
# @param spec [SDK::FuncSpec::Spec]
# @param expect [Array<Class>] Expected types for each argument
# @return [Array<Object>, Object]
def funcspec_map(spec, *extra_args, expect: [])
expect = Array(expect)
args = spec.args.dup
# NOTE: the spec will have the order of the arguments
# shifted one. not sure why, but we can just work around
# it here for now.
args.push(args.shift)
# Start with unpacking the funcspec values so the #map method can
# apply known default expectations to values
args = args.map { |a| unfuncspec(a) }
# Now send the arguments through the mapping process
result = Array.new.tap do |result_args|
args.each_with_index do |arg, i|
logger.debug { "mapping funcspec value #{arg.class} to expected type #{expect[i]}" }
result_args << map(arg, *(extra_args + result_args), to: expect[i])
end
end
if result.size == 1
return result.first
end
result
end
# Extracts proto message from funcspec argument proto
#
# @param v [SDK::FuncSpec::Value]
# @return [Google:Protobuf::MessageExts]
def unfuncspec(v)
m = mappers.find_all { |map|
map.inputs.size == 1 &&
map.output.ancestors.include?(Google::Protobuf::MessageExts) &&
map.inputs.first.valid?(v)
}
if m.size > 1
raise TypeError,
"FuncSpec value of type `#{v.class}' matches more than one mapper (#{v})"
end
if m.empty?
raise ArgumentError,
"FuncSpec value of type `#{v.class}' has no valid mappers (#{v})"
end
result = m.first.call(v)
logger.trace { "converted funcspec argument #{v.class} -> #{result.class}" }
result
end
end
end
end
# NOTE: Always directly load mappers so they are automatically registered and
# available. Using autoloading behavior will result in them being unavailable
# until explicitly requested by name
require Vagrant.source_root.join("plugins/commands/serve/mappers/basis.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/box.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/capabilities.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/command.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/communicator.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/config_data.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/core_plugin_manager.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/direct.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/duration.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/environment.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/folders.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/guest.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/host.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/known_types.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/machine.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/options.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/pathname.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/plugin_manager.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/proc.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/project.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/provider.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/provisioner.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/push.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/state_bag.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/synced_folder.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/target.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/target_index.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/terminal.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/ui.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/vagrantfile.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/wrappers.rb").to_s