feat(xmlgen): XML generation with automatic escaping and validation (1/4) - #199
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
f247b28 to
1639005
Compare
There was a problem hiding this comment.
Please add tests for both define and block with a markup-carrying value, next to TestEscaping_Metacharacters.
Also check this: the rewriter only walks the main parse tree. {{ define }} and {{ block }} install their bodies as associated templates that text/template removes from that tree, and walkNode treats TemplateNode as a no-op. I rendered this template with v set to <Consignee><Name>Evil</Name></Consignee>:
<R>{{block "item" .}}<Item>{{ .v }}</Item>{{end}}</R>
Generate returned:
<R><Item><Consignee><Name>Evil</Name></Consignee></Item></R>
The same injection happens with {{ define }} plus {{ template }}. That contradicts the claim that every printing action is piped through xml and that a value carrying markup cannot forge elements. checkDocument cannot catch this: the output is well-formed. PR 201 does not close the hole. It still rewrites only t.Tree, and a line-item fragment is the natural place someone would reach for block.
1639005 to
c74da2b
Compare
|
Good catch, and you're right — this was a real hole, not a documentation gap. Fixed in c74da2b. Confirmed and reproduced, including a case worse than the one you found: Root cause was a false premise I wrote into Fix: This is complete rather than merely broader, and it's worth stating why: execution reaches a tree only through Tests added — Two further defects turned up while I was verifying the fix, both now addressed:
Also reserved The chain has been rebased and force-pushed; #201, #202 and #200 are unchanged apart from that. Subtest counts across the chain: 115 / 153 / 170 / 195. |
Core had no way to produce an XML document: encoding/xml was imported
nowhere, and the only templating in the repo performs no escaping. This adds
the xmlgen module -- the rendering engine and its two entry points. The
built-in helper vocabulary, caller-supplied resolvers and the whole-document
golden fixtures follow in stacked PRs.
Escaping is not the template author's job. After parsing, the template is
rewritten so every printing action is piped through an escape function --
the approach html/template takes to the same problem -- so forgetting is not
something a template can do, and a value carrying markup cannot forge
elements. Rewriting the template rather than the data also covers what a
data-side approach cannot reach: range variables, map keys, pipeline tails
and function return values. Templates opt out per action with raw or cdata.
Text outside an action is never touched, so a literal marker element written
in an else branch reaches the document verbatim.
The rewrite walks every tree a parse produced, not only the main one. One
Parse call returns a map of trees: {{ define }} and {{ block }} install their
bodies as associated templates, leaving only a TemplateNode behind, so
walking the main tree alone would let everything a block prints reach the
document unescaped. Templates() is exactly the set execution can reach --
walkTemplate resolves a name through Lookup, which reads the same map -- so
walking all of it is complete rather than merely thorough.
JSON input is decoded with UseNumber, so numbers keep the text they were
written with: 10000000 stays 10000000 rather than becoming 1e+07, and 1.50
keeps its trailing zero. Following a pointer or interface is depth-bounded:
a caller-supplied cycle would otherwise overflow the stack, which Go treats
as fatal and recover cannot catch.
Output is checked as a document before it is returned, and never rewritten,
so the bytes stay stable and remain valid to sign. Beyond well-formedness
that means exactly one root element, no DOCTYPE, no text outside the root,
and no undeclared namespace prefix -- encoding/xml documents that it does
not reject the last, recording the prefix as the namespace instead, so a
prefix typed one letter wrong would otherwise reach the far end unnoticed.
That check is not a backstop for raw, which is documented as the deliberate
opt-out: a raw value placed in an attribute can close the quote and forge
further attributes, and the result is well-formed.
Refs #189
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
c74da2b to
e9f36c6
Compare
Problem
Core has no way to generate an XML document.
encoding/xmlis imported nowhere, and the only templating in the repo (uiprojector/projector.go) performs no escaping — acceptable for a markdown blurb, not for a document that leaves the system.Changes
New standalone module
xmlgen(stdlib + testify; root module untouched). A template is the target XML withtext/templateactions in it.{{ .name }}becomes{{ .name | xml }}, the techniquehtml/templateuses. Rewriting the template rather than the data also coversrangevariables, map keys and pipeline tails. The rewrite walks every tree a parse produced, not just the main one:{{ define }}and{{ block }}install their bodies as associated templates, andTemplates()is provably the complete set (execution reaches a tree only viaLookup, which reads the same map). Opt out per action withraworcdata; text outside an action is never touched.UseNumber, so10000000stays10000000rather than becoming1e+07.DOCTYPE, no text outside the root, no undeclared namespace prefix — the last becauseencoding/xmldocuments that it does not reject one. The check is not a backstop forraw, which is the deliberate opt-out: arawvalue in an attribute can close the quote and forge further attributes, and that output is well-formed. The README says so explicitly.recovercannot catch.ci.yml,dependabot.ymland the root README table.Worth a reviewer's eye:
rewrite.gobuilds aparse.CommandNodeliteral, so its unexportedtrfield is nil. OnlyCommandNode.Copy()dereferences it, reached solely viahtml/template, which xmlgen never uses.TestInjectEscaping_SurvivesClonepins that.Testing
115 subtests, race-clean, 0 lint issues. Covers escaping (metacharacters, attribute contexts, a value that is itself valid XML, already-escaped input, invalid UTF-8, control characters, map keys,
rangevariables), every structural rejection above, a UTF-8 BOM, a non-UTF-8 encoding declaration, the size cap, context cancellation, andGenerateTowriting zero bytes when validation fails.Related
Implements #189. Stacked chain — 1 of 4, review in order:
#199 (this) → #201 helpers and resolvers → #202 golden fixtures → #200 failure paths
🤖 Generated with Claude Code