Paul King created GROOVY-12482:
----------------------------------
Summary: YamlSlurper: resolve YAML anchors and aliases rather than
returning the anchor name
Key: GROOVY-12482
URL: https://issues.apache.org/jira/browse/GROOVY-12482
Project: Groovy
Issue Type: Improvement
Components: groovy-yaml
Reporter: Paul King
*NOTE*: For Groovy 6, we closed this to avoid having to handle the security
implications mentioned below (under Points to settle). Read those before
starting work on this issue.
{{YamlSlurper}} does not resolve YAML anchors and aliases: an alias is returned
as a string holding the anchor's *name*, so the document parses without error
but with wrong values. GROOVY-12358 documented this as a limitation of the
Jackson YAML backend (FasterXML/jackson-dataformats-text#98, closed as not
planned). This issue proposes resolving aliases in Groovy instead.
h3. Current behaviour
{code:groovy}
def y = new groovy.yaml.YamlSlurper()
assert y.parseText('defaults: &d {timeout: 30}\nprod: *d') == [defaults:
[timeout: 30], prod: 'd']
assert y.parseText('host: &h db.example.com\nreplica: *h') == [host:
'db.example.com', replica: 'h']
assert y.parseText('base: &l [1, 2]\nmore: *l') == [base: [1, 2], more: 'l']
assert y.parseText('- &x {a: 1}\n- *x') == [[a: 1], 'x']
{code}
The typed API is affected the same way: {{parseTextAs(Cfg, 'host: &h
db.example.com\nreplica: *h')}} gives {{replica == 'h'}}. The documented
{{@JsonIdentityInfo}} workaround only covers mappings bound to annotated
classes.
Seen on 4.0.32, 5.0.6 and 6.0.0, and on master both with Jackson 2 and with the
Jackson 3 port in PR #2949. Anchors are common in real-world YAML (CI
pipelines, compose files, Kubernetes/Helm values), and the silently wrong value
is worse than an error.
h3. Proposed approach
Jackson's {{YAMLParser}} reports enough to do this ourselves, in both Jackson
2.x and 3.x:
* {{getObjectId()}} returns the anchor name on the anchored token
* {{isCurrentAlias()}} is true on an alias token, whose text is the anchor name
{{YamlConverter.convertYamlToJson}} currently binds each document to {{Object}}
in one step. It could instead build the tree from the token stream: record the
value completed at each anchor, and substitute it when an alias is met. An
alias to an undefined anchor should be an error.
Points to settle:
* *Expansion limit.* Resolving aliases makes the "billion laughs" document
possible: a few lines whose aliases expand exponentially. Jackson never
expanded aliases, so it never needed a guard. We need one, either a cap on
alias uses per collection (SnakeYAML's {{maxAliasesForCollections}}, default
50) or on total expanded nodes, with an exception when it is exceeded.
* *Copies or shared instances.* The slurper output goes via JSON text to
{{JsonSlurper}}, so a resolved alias naturally becomes an independent copy.
That is probably the least surprising choice for mutable maps and lists.
* *Typed {{parseAs}}.* This currently uses {{readValue(reader, type)}}
directly, so it would need to read the resolved tree and then {{convertValue}}.
It's worth deciding whether that should take precedence over
{{@JsonIdentityInfo}} handling or be opt-in.
* *Merge keys ({{<<: *d}}).* These are a YAML 1.1 type rather than part of YAML
1.2, but widely used. They could follow as a separate step once aliases
resolve, or be left documented as unsupported.
When done, the "Anchors and aliases" section of the YAML user guide and the
{{alias_behaviour}} tests in {{YamlParserTest}} need updating.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)