From eae2e03e9d00717b94509c376534ca60c096fb5f Mon Sep 17 00:00:00 2001 From: Daniel Bae <157205701+MrBeldum@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:27:22 -0700 Subject: [PATCH] Escape text values in OutputXML OutputXML wrote JSON string values into the XML verbatim, so a value containing <, > or & (e.g. "AT&T" or "") produced malformed XML that no XML parser could read back. Escape those characters in text nodes. --- node.go | 6 +++++- node_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/node.go b/node.go index 62d0425..ebe5be3 100644 --- a/node.go +++ b/node.go @@ -67,10 +67,14 @@ func (n *Node) InnerText() string { return b.String() } +// xmlTextEscaper escapes the characters that are not allowed to appear +// literally in XML character data. +var xmlTextEscaper = strings.NewReplacer("&", "&", "<", "<", ">", ">") + func outputXML(b *strings.Builder, n *Node, level int, skip bool) { level++ if n.Type == TextNode { - b.WriteString(fmt.Sprintf("%v", n.value)) + b.WriteString(xmlTextEscaper.Replace(fmt.Sprintf("%v", n.value))) return } if v := reflect.ValueOf(n.value); v.Kind() == reflect.Slice { diff --git a/node_test.go b/node_test.go index cc1fb1f..a171c6e 100644 --- a/node_test.go +++ b/node_test.go @@ -1,6 +1,7 @@ package jsonquery import ( + "encoding/xml" "reflect" "strings" "testing" @@ -161,3 +162,27 @@ func TestLargeFloat(t *testing.T) { t.Fatalf("expected %v but %v", "365823929453", n.InnerText()) } } + +func TestOutputXMLEscapesText(t *testing.T) { + doc, err := parseString(`{"a": "x < y && y > z", "b": ["", "AT&T"]}`) + if err != nil { + t.Fatal(err) + } + out := doc.OutputXML() + want := `x < y && y > z<i>AT&T` + if out != want { + t.Fatalf("OutputXML() = %s, want %s", out, want) + } + + // The output must be well-formed and decode back to the original text. + var v struct { + A string `xml:"a"` + B []string `xml:"b"` + } + if err := xml.Unmarshal([]byte(out), &v); err != nil { + t.Fatalf("OutputXML() is not well-formed XML: %v", err) + } + if v.A != "x < y && y > z" || len(v.B) != 2 || v.B[0] != "" || v.B[1] != "AT&T" { + t.Fatalf("decoded %+v", v) + } +}