Class: AceMQ::AMQP::YAMLCodec
- Inherits:
-
Object
- Object
- AceMQ::AMQP::YAMLCodec
- Defined in:
- lib/acemq/amqp/codec/yaml.rb
Overview
Reads and writes YAML.
Chosen when a message is meant to be read by a person as much as by a program: a configuration change broadcast to a fleet, a deployment instruction, a command replayed by hand from a dead-letter queue. It costs more to parse than JSON and is a poor choice for high volume; it earns its place where somebody will actually look at the message.
Psych is a default gem, so this needs nothing installed.
Why this loads safely, and what that costs
A message body is input from another machine. YAML.load on it is remote
code execution: !ruby/object:Gem::Requirement and its relatives are how
a body turns into an instantiated object graph, and a queue is exactly
where a body from somewhere unexpected arrives. So this uses
Psych.safe_load, which builds only the types it has been told about.
That is not free, and the cost lands on legitimate messages:
- A date or a timestamp is a YAML scalar type, not a Ruby one, and
safe_loadrefuses it unless the class is permitted. Java and Go both write timestamps into YAML, so this permitsDate,TimeandDateTimeby default — refusing them would reject messages the other libraries write every day. - A symbol does not arrive from any other language, so
Symbolis not permitted by default. Pass it inpermitted_classesfor a Ruby-to-Ruby queue that wants them, knowing that turning arbitrary remote text into symbols is a decision worth making on purpose. - Anchors and aliases are refused, because the billion-laughs expansion
needs nothing but aliases to take a consumer down, and neither Jackson
nor gopkg.in/yaml.v3 writes them. Pass
aliases: truefor a producer that does.
What it will not write
Psych will happily serialise an arbitrary Ruby object as !ruby/object:,
which is a message no other AceMQ library can read and the exact tag this
codec refuses on the way in. Encoding one raises EncodeError rather than
publishing something only Ruby can load — and only Ruby with the safety
off. Publish a Hash, an Array or a scalar.
A symbol key is written as :name, because that is how YAML spells a Ruby
symbol and this library does not quietly rename keys. No other language
reads :name as name; spell keys as strings.
Constant Summary collapse
- CONTENT_TYPE =
What this codec writes: the type RFC 9512 registered, and what Java, Go and .NET write.
"application/yaml"- ALSO_READS =
The three spellings that predate RFC 9512, which is what most senders still write. Read wider than written, on purpose: a producer in another stack uses whichever spelling its library picked.
["application/x-yaml", "text/yaml", "text/x-yaml"].freeze
- ALIAS_REFUSED =
What
safe_loadraises for an alias it was told not to follow, which is not the same class on every Ruby this gem supports: Psych 5 introducedAliasesNotEnabled, and Psych 4 -- what Ruby 3.1 ships, and 3.1 is the floor -- raisesBadAlias.Resolved once here rather than named in the
rescue, because arescuenaming a constant that does not exist raises NameError while it is being matched. That does not merely miss the alias case: it escapes every later clause too, so a body that was not YAML at all came out as a NameError instead of a DecodeError. if ::Psych.const_defined?(:AliasesNotEnabled) ::Psych::AliasesNotEnabled else ::Psych::BadAlias end
- TIME_CLASSES =
What
safe_loadis allowed to build without being asked.Java and Go write timestamps into YAML, and a codec that rejected them would refuse messages the other libraries send routinely. Everything else — every Ruby class,
Symbolincluded — has to be named. [Date, DateTime, Time].freeze
Instance Method Summary collapse
-
#can_decode?(content_type) ⇒ Boolean
Accepts
application/yamland the three spellings in ALSO_READS, plus any +...+yaml+ media type. - #content_type ⇒ Object
- #decode(body) ⇒ Object
- #encode(payload) ⇒ String
-
#initialize(permitted_classes: [], aliases: false) ⇒ YAMLCodec
constructor
A new instance of YAMLCodec.
Constructor Details
#initialize(permitted_classes: [], aliases: false) ⇒ YAMLCodec
Returns a new instance of YAMLCodec.
106 107 108 109 110 |
# File 'lib/acemq/amqp/codec/yaml.rb', line 106 def initialize(permitted_classes: [], aliases: false) @permitted_classes = (TIME_CLASSES + Array(permitted_classes)).uniq.freeze @aliases = aliases freeze end |
Instance Method Details
#can_decode?(content_type) ⇒ Boolean
Accepts application/yaml and the three spellings in ALSO_READS, plus
any +...+yaml+ media type.
Never a message whose sender set no content type. YAML is a superset of JSON, so its parser accepts JSON bytes quite happily and would answer for messages meant for JSONCodec — giving the right value while recording that a YAML message arrived, which is the sort of wrong that is found much later.
158 159 160 161 162 163 |
# File 'lib/acemq/amqp/codec/yaml.rb', line 158 def can_decode?(content_type) lower = content_type.to_s.downcase return false if lower.empty? lower.start_with?(CONTENT_TYPE, *ALSO_READS) || lower.include?("+yaml") end |
#content_type ⇒ Object
112 |
# File 'lib/acemq/amqp/codec/yaml.rb', line 112 def content_type = CONTENT_TYPE |
#decode(body) ⇒ Object
135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/acemq/amqp/codec/yaml.rb', line 135 def decode(body) ::Psych.safe_load(body.to_s, permitted_classes: @permitted_classes, aliases: @aliases) rescue ::Psych::DisallowedClass => e raise DecodeError, "this message asks for a class the YAML codec will not build: #{e.}. " \ "Pass permitted_classes: if the sender is trusted and really needs it." rescue ALIAS_REFUSED raise DecodeError, "this message uses YAML anchors and aliases, which are refused because " \ "they are how a small body expands into one large enough to take a " \ "consumer down. Pass aliases: true if the sender is trusted." rescue ::Psych::Exception => e raise DecodeError, "this message is not YAML: #{e.}" end |
#encode(payload) ⇒ String
117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/acemq/amqp/codec/yaml.rb', line 117 def encode(payload) written = ::Psych.dump(payload) if written.include?("!ruby/") raise EncodeError, "this payload only serialises as YAML with a Ruby-specific tag " \ "(#{written[%r{!ruby/[\w:]+}]}), which no other AceMQ library can read. " \ "Publish a Hash, an Array or a scalar." end # No leading "---". It is valid and it is noise, a message body is a # single document by definition, and neither Java nor Go writes one. written.sub(/\A---(?: |\n)/, "") end |