|
|
@@ -456,20 +456,20 @@ func TestMultistatusWriter(t *testing.T) {
|
|
|
respdesc: "There has been an access violation error.",
|
|
|
wantXML: `` +
|
|
|
`<?xml version="1.0" encoding="UTF-8"?>` +
|
|
|
- `<multistatus xmlns="DAV:">` +
|
|
|
+ `<multistatus xmlns="DAV:" xmlns:B="http://ns.example.com/boxschema/">` +
|
|
|
` <response>` +
|
|
|
` <href>http://example.com/foo</href>` +
|
|
|
` <propstat>` +
|
|
|
` <prop>` +
|
|
|
- ` <bigbox xmlns="http://ns.example.com/boxschema/"><BoxType xmlns="http://ns.example.com/boxschema/">Box type A</BoxType></bigbox>` +
|
|
|
- ` <author xmlns="http://ns.example.com/boxschema/"><Name xmlns="http://ns.example.com/boxschema/">J.J. Johnson</Name></author>` +
|
|
|
+ ` <B:bigbox><B:BoxType>Box type A</B:BoxType></B:bigbox>` +
|
|
|
+ ` <B:author><B:Name>J.J. Johnson</B:Name></B:author>` +
|
|
|
` </prop>` +
|
|
|
` <status>HTTP/1.1 200 OK</status>` +
|
|
|
` </propstat>` +
|
|
|
` <propstat>` +
|
|
|
` <prop>` +
|
|
|
- ` <DingALing xmlns="http://ns.example.com/boxschema/"></DingALing>` +
|
|
|
- ` <Random xmlns="http://ns.example.com/boxschema/"></Random>` +
|
|
|
+ ` <B:DingALing/>` +
|
|
|
+ ` <B:Random/>` +
|
|
|
` </prop>` +
|
|
|
` <status>HTTP/1.1 403 Forbidden</status>` +
|
|
|
` <responsedescription>The user does not have access to the DingALing property.</responsedescription>` +
|
|
|
@@ -564,6 +564,7 @@ func TestMultistatusWriter(t *testing.T) {
|
|
|
wantCode: http.StatusOK,
|
|
|
}}
|
|
|
|
|
|
+ n := xmlNormalizer{omitWhitespace: true}
|
|
|
loop:
|
|
|
for _, tc := range testCases {
|
|
|
rec := httptest.NewRecorder()
|
|
|
@@ -593,44 +594,14 @@ loop:
|
|
|
tc.desc, rec.Code, tc.wantCode)
|
|
|
continue
|
|
|
}
|
|
|
-
|
|
|
- // normalize returns the normalized XML content of s. In contrast to
|
|
|
- // the WebDAV specification, it ignores whitespace within property
|
|
|
- // values of mixed XML content.
|
|
|
- normalize := func(s string) string {
|
|
|
- d := xml.NewDecoder(strings.NewReader(s))
|
|
|
- var b bytes.Buffer
|
|
|
- e := xml.NewEncoder(&b)
|
|
|
- for {
|
|
|
- tok, err := d.Token()
|
|
|
- if err != nil {
|
|
|
- if err == io.EOF {
|
|
|
- break
|
|
|
- }
|
|
|
- t.Fatalf("%s: Token %v", tc.desc, err)
|
|
|
- }
|
|
|
- switch val := tok.(type) {
|
|
|
- case xml.Comment, xml.Directive, xml.ProcInst:
|
|
|
- continue
|
|
|
- case xml.CharData:
|
|
|
- if len(bytes.TrimSpace(val)) == 0 {
|
|
|
- continue
|
|
|
- }
|
|
|
- }
|
|
|
- if err := e.EncodeToken(tok); err != nil {
|
|
|
- t.Fatalf("%s: EncodeToken: %v", tc.desc, err)
|
|
|
- }
|
|
|
- }
|
|
|
- if err := e.Flush(); err != nil {
|
|
|
- t.Fatalf("%s: Flush: %v", tc.desc, err)
|
|
|
- }
|
|
|
- return b.String()
|
|
|
+ gotXML := rec.Body.String()
|
|
|
+ eq, err := n.equalXML(strings.NewReader(gotXML), strings.NewReader(tc.wantXML))
|
|
|
+ if err != nil {
|
|
|
+ t.Errorf("%s: equalXML: %v", tc.desc, err)
|
|
|
+ continue
|
|
|
}
|
|
|
-
|
|
|
- gotXML := normalize(rec.Body.String())
|
|
|
- wantXML := normalize(tc.wantXML)
|
|
|
- if gotXML != wantXML {
|
|
|
- t.Errorf("%s: XML body\ngot %q\nwant %q", tc.desc, gotXML, wantXML)
|
|
|
+ if !eq {
|
|
|
+ t.Errorf("%s: XML body\ngot %s\nwant %s", tc.desc, gotXML, tc.wantXML)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -828,66 +799,101 @@ func TestUnmarshalXMLValue(t *testing.T) {
|
|
|
` </notes>`,
|
|
|
}}
|
|
|
|
|
|
- // Normalize namespace declarations, prefixes and attribute order.
|
|
|
- normalize := func(s string) (string, error) {
|
|
|
- d := xml.NewDecoder(strings.NewReader(s))
|
|
|
- var b bytes.Buffer
|
|
|
- e := xml.NewEncoder(&b)
|
|
|
- for {
|
|
|
- t, err := d.Token()
|
|
|
- if err != nil {
|
|
|
- if t == nil && err == io.EOF {
|
|
|
- break
|
|
|
- }
|
|
|
- return "", err
|
|
|
- }
|
|
|
- t = xml.CopyToken(t)
|
|
|
- if start, ok := t.(xml.StartElement); ok {
|
|
|
- attr := start.Attr[:0]
|
|
|
- for _, a := range start.Attr {
|
|
|
- if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" {
|
|
|
- continue
|
|
|
- }
|
|
|
- attr = append(attr, a)
|
|
|
- }
|
|
|
- sort.Sort(byName(attr))
|
|
|
- start.Attr = attr
|
|
|
- t = start
|
|
|
- }
|
|
|
- err = e.EncodeToken(t)
|
|
|
- if err != nil {
|
|
|
- return "", err
|
|
|
- }
|
|
|
- }
|
|
|
- err := e.Flush()
|
|
|
- if err != nil {
|
|
|
- return "", err
|
|
|
- }
|
|
|
- return b.String(), nil
|
|
|
- }
|
|
|
-
|
|
|
+ var n xmlNormalizer
|
|
|
for _, tc := range testCases {
|
|
|
d := xml.NewDecoder(strings.NewReader(tc.input))
|
|
|
var v xmlValue
|
|
|
- err := d.Decode(&v)
|
|
|
- if err != nil {
|
|
|
+ if err := d.Decode(&v); err != nil {
|
|
|
t.Errorf("%s: got error %v, want nil", tc.desc, err)
|
|
|
continue
|
|
|
}
|
|
|
- got, err := normalize(string(v))
|
|
|
+ eq, err := n.equalXML(bytes.NewReader(v), strings.NewReader(tc.wantVal))
|
|
|
if err != nil {
|
|
|
- t.Errorf("%s: normalize: %v", tc.desc, err)
|
|
|
+ t.Errorf("%s: equalXML: %v", tc.desc, err)
|
|
|
continue
|
|
|
}
|
|
|
- want, err := normalize(tc.wantVal)
|
|
|
+ if !eq {
|
|
|
+ t.Errorf("%s:\ngot %s\nwant %s", tc.desc, string(v), tc.wantVal)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// xmlNormalizer normalizes XML.
|
|
|
+type xmlNormalizer struct {
|
|
|
+ // omitWhitespace instructs to ignore whitespace between element tags.
|
|
|
+ omitWhitespace bool
|
|
|
+ // omitComments instructs to ignore XML comments.
|
|
|
+ omitComments bool
|
|
|
+}
|
|
|
+
|
|
|
+// normalize writes the normalized XML content of r to w. It applies the
|
|
|
+// following rules
|
|
|
+//
|
|
|
+// * Rename namespace prefixes according to an internal heuristic.
|
|
|
+// * Remove unnecessary namespace declarations.
|
|
|
+// * Sort attributes in XML start elements in lexical order of their
|
|
|
+// fully qualified name.
|
|
|
+// * Remove XML directives and processing instructions.
|
|
|
+// * Remove CDATA between XML tags that only contains whitespace, if
|
|
|
+// instructed to do so.
|
|
|
+// * Remove comments, if instructed to do so.
|
|
|
+//
|
|
|
+func (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error {
|
|
|
+ d := xml.NewDecoder(r)
|
|
|
+ e := xml.NewEncoder(w)
|
|
|
+ for {
|
|
|
+ t, err := d.Token()
|
|
|
if err != nil {
|
|
|
- t.Errorf("%s: normalize: %v", tc.desc, err)
|
|
|
+ if t == nil && err == io.EOF {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ switch val := t.(type) {
|
|
|
+ case xml.Directive, xml.ProcInst:
|
|
|
continue
|
|
|
+ case xml.Comment:
|
|
|
+ if n.omitComments {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ case xml.CharData:
|
|
|
+ if n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ case xml.StartElement:
|
|
|
+ start, _ := xml.CopyToken(val).(xml.StartElement)
|
|
|
+ attr := start.Attr[:0]
|
|
|
+ for _, a := range start.Attr {
|
|
|
+ if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ attr = append(attr, a)
|
|
|
+ }
|
|
|
+ sort.Sort(byName(attr))
|
|
|
+ start.Attr = attr
|
|
|
+ t = start
|
|
|
}
|
|
|
- if got != want {
|
|
|
- t.Errorf("%s:\ngot %s\nwant %s", tc.desc, got, want)
|
|
|
+ err = e.EncodeToken(t)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
}
|
|
|
}
|
|
|
+ return e.Flush()
|
|
|
+}
|
|
|
+
|
|
|
+// equalXML tests for equality of the normalized XML contents of a and b.
|
|
|
+func (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) {
|
|
|
+ var buf bytes.Buffer
|
|
|
+ if err := n.normalize(&buf, a); err != nil {
|
|
|
+ return false, err
|
|
|
+ }
|
|
|
+ normA := buf.String()
|
|
|
+ buf.Reset()
|
|
|
+ if err := n.normalize(&buf, b); err != nil {
|
|
|
+ return false, err
|
|
|
+ }
|
|
|
+ normB := buf.String()
|
|
|
+ return normA == normB, nil
|
|
|
}
|
|
|
|
|
|
type byName []xml.Attr
|