1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
|
module MultiXml
# Hash key for storing text content within element hashes
#
# @api public
# @return [String] the key "__content__" used for text content
# @example Accessing text content
# result = MultiXml.parse('<name>John</name>')
# result["name"] #=> "John" (simplified, but internally uses __content__)
TEXT_CONTENT_KEY = "__content__".freeze
# Maps Ruby class names to XML type attribute values
#
# @api public
# @return [Hash{String => String}] mapping of Ruby class names to XML types
# @example Check XML type for a Ruby class
# RUBY_TYPE_TO_XML["Integer"] #=> "integer"
RUBY_TYPE_TO_XML = {
"Symbol" => "symbol",
"Integer" => "integer",
"BigDecimal" => "decimal",
"Float" => "float",
"TrueClass" => "boolean",
"FalseClass" => "boolean",
"Date" => "date",
"DateTime" => "datetime",
"Time" => "datetime",
"Array" => "array",
"Hash" => "hash"
}.freeze
# XML type attributes disallowed by default for security
#
# These types are blocked to prevent code execution vulnerabilities.
#
# @api public
# @return [Array<String>] list of disallowed type names
# @example Check default disallowed types
# DISALLOWED_TYPES #=> ["symbol", "yaml"]
DISALLOWED_TYPES = %w[symbol yaml].freeze
# Values that represent false in XML boolean attributes
#
# @api public
# @return [Set<String>] values considered false
# @example Check false values
# FALSE_BOOLEAN_VALUES.include?("0") #=> true
FALSE_BOOLEAN_VALUES = Set.new(%w[0 false]).freeze
# Default parsing options
#
# @api public
# @return [Hash] default options for parse method
# @example View defaults
# DEFAULT_OPTIONS[:symbolize_keys] #=> false
DEFAULT_OPTIONS = {
typecast_xml_value: true,
disallowed_types: DISALLOWED_TYPES,
symbolize_keys: false
}.freeze
# Parser libraries in preference order (fastest first)
#
# @api public
# @return [Array<Array>] pairs of [require_path, parser_symbol]
# @example View parser order
# PARSER_PREFERENCE.first #=> ["ox", :ox]
PARSER_PREFERENCE = [
["ox", :ox],
["libxml", :libxml],
["nokogiri", :nokogiri],
["rexml/document", :rexml],
["oga", :oga]
].freeze
# Parses datetime strings, trying Time first then DateTime
#
# @api private
# @return [Proc] lambda that parses datetime strings
PARSE_DATETIME = lambda do |string|
Time.parse(string).utc
rescue ArgumentError
DateTime.parse(string).to_time.utc
end
# Creates a file-like StringIO from base64-encoded content
#
# @api private
# @return [Proc] lambda that creates file objects
FILE_CONVERTER = lambda do |content, entity|
StringIO.new(content.unpack1("m")).tap do |io|
io.extend(FileLike)
file_io = io # : FileIO
file_io.original_filename = entity["name"]
file_io.content_type = entity["content_type"]
end
end
# Type converters for XML type attributes
#
# Maps type attribute values to lambdas that convert string content.
# Converters with arity 2 receive the content and the full entity hash.
#
# @api public
# @return [Hash{String => Proc}] mapping of type names to converter procs
# @example Using a converter
# TYPE_CONVERTERS["integer"].call("42") #=> 42
TYPE_CONVERTERS = {
# Primitive types
"symbol" => :to_sym.to_proc,
"string" => :to_s.to_proc,
"integer" => :to_i.to_proc,
"float" => :to_f.to_proc,
"double" => :to_f.to_proc,
"decimal" => ->(s) { BigDecimal(s) },
"boolean" => ->(s) { !FALSE_BOOLEAN_VALUES.include?(s.strip) },
# Date and time types
"date" => Date.method(:parse),
"datetime" => PARSE_DATETIME,
"dateTime" => PARSE_DATETIME,
# Binary types
"base64Binary" => ->(s) { s.unpack1("m") },
"binary" => ->(s, entity) { (entity["encoding"] == "base64") ? s.unpack1("m") : s },
"file" => FILE_CONVERTER,
# Structured types
"yaml" => lambda do |string|
YAML.safe_load(string, permitted_classes: [Symbol, Date, Time])
rescue ArgumentError, Psych::SyntaxError
string
end
}.freeze
end
|