mirror of https://github.com/perkeep/perkeep.git
golang.org/x/net/webdav: update
commit 34ff4cd5e6de00702100a0ab3bb73de8de5ab35d webdav: make it work for Mini-Redirector, emit namespace-prefixed XML. by Mathieu Lonjaret. Change-Id: Id599fc4fe5064631fe01e59117eb98abdb85ad5f
This commit is contained in:
parent
6c2e973b61
commit
39f816d3f2
|
@ -278,7 +278,7 @@ loop:
|
|||
if conflict {
|
||||
pstatForbidden := Propstat{
|
||||
Status: http.StatusForbidden,
|
||||
XMLError: `<error xmlns="DAV:"><cannot-modify-protected-property/></error>`,
|
||||
XMLError: `<D:cannot-modify-protected-property xmlns:D="DAV:"/>`,
|
||||
}
|
||||
pstatFailedDep := Propstat{
|
||||
Status: StatusFailedDependency,
|
||||
|
@ -328,7 +328,7 @@ loop:
|
|||
|
||||
func findResourceType(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
|
||||
if fi.IsDir() {
|
||||
return `<collection xmlns="DAV:"/>`, nil
|
||||
return `<D:collection xmlns:D="DAV:"/>`, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
@ -377,8 +377,8 @@ func findETag(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string
|
|||
|
||||
func findSupportedLock(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
|
||||
return `` +
|
||||
`<lockentry xmlns="DAV:">` +
|
||||
`<lockscope><exclusive/></lockscope>` +
|
||||
`<locktype><write/></locktype>` +
|
||||
`</lockentry>`, nil
|
||||
`<D:lockentry xmlns:D="DAV:">` +
|
||||
`<D:lockscope><D:exclusive/></D:lockscope>` +
|
||||
`<D:locktype><D:write/></D:locktype>` +
|
||||
`</D:lockentry>`, nil
|
||||
}
|
||||
|
|
|
@ -44,6 +44,15 @@ func TestMemPS(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
lockEntry = `` +
|
||||
`<D:lockentry xmlns:D="DAV:">` +
|
||||
`<D:lockscope><D:exclusive/></D:lockscope>` +
|
||||
`<D:locktype><D:write/></D:locktype>` +
|
||||
`</D:lockentry>`
|
||||
statForbiddenError = `<D:cannot-modify-protected-property xmlns:D="DAV:"/>`
|
||||
)
|
||||
|
||||
type propOp struct {
|
||||
op string
|
||||
name string
|
||||
|
@ -95,7 +104,7 @@ func TestMemPS(t *testing.T) {
|
|||
Status: http.StatusOK,
|
||||
Props: []Property{{
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "resourcetype"},
|
||||
InnerXML: []byte(`<collection xmlns="DAV:"/>`),
|
||||
InnerXML: []byte(`<D:collection xmlns:D="DAV:"/>`),
|
||||
}, {
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "displayname"},
|
||||
InnerXML: []byte("dir"),
|
||||
|
@ -109,13 +118,8 @@ func TestMemPS(t *testing.T) {
|
|||
XMLName: xml.Name{Space: "DAV:", Local: "getcontenttype"},
|
||||
InnerXML: []byte("text/plain; charset=utf-8"),
|
||||
}, {
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "supportedlock"},
|
||||
InnerXML: []byte(`` +
|
||||
`<lockentry xmlns="DAV:">` +
|
||||
`<lockscope><exclusive/></lockscope>` +
|
||||
`<locktype><write/></locktype>` +
|
||||
`</lockentry>`,
|
||||
),
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "supportedlock"},
|
||||
InnerXML: []byte(lockEntry),
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
|
@ -142,13 +146,8 @@ func TestMemPS(t *testing.T) {
|
|||
XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
|
||||
InnerXML: nil, // Calculated during test.
|
||||
}, {
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "supportedlock"},
|
||||
InnerXML: []byte(`` +
|
||||
`<lockentry xmlns="DAV:">` +
|
||||
`<lockscope><exclusive/></lockscope>` +
|
||||
`<locktype><write/></locktype>` +
|
||||
`</lockentry>`,
|
||||
),
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "supportedlock"},
|
||||
InnerXML: []byte(lockEntry),
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
|
@ -179,13 +178,8 @@ func TestMemPS(t *testing.T) {
|
|||
XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
|
||||
InnerXML: nil, // Calculated during test.
|
||||
}, {
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "supportedlock"},
|
||||
InnerXML: []byte(`` +
|
||||
`<lockentry xmlns="DAV:">` +
|
||||
`<lockscope><exclusive/></lockscope>` +
|
||||
`<locktype><write/></locktype>` +
|
||||
`</lockentry>`,
|
||||
),
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "supportedlock"},
|
||||
InnerXML: []byte(lockEntry),
|
||||
}}}, {
|
||||
Status: http.StatusNotFound,
|
||||
Props: []Property{{
|
||||
|
@ -204,7 +198,7 @@ func TestMemPS(t *testing.T) {
|
|||
Status: http.StatusOK,
|
||||
Props: []Property{{
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "resourcetype"},
|
||||
InnerXML: []byte(`<collection xmlns="DAV:"/>`),
|
||||
InnerXML: []byte(`<D:collection xmlns:D="DAV:"/>`),
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
|
@ -296,7 +290,7 @@ func TestMemPS(t *testing.T) {
|
|||
}},
|
||||
wantPropstats: []Propstat{{
|
||||
Status: http.StatusForbidden,
|
||||
XMLError: `<error xmlns="DAV:"><cannot-modify-protected-property/></error>`,
|
||||
XMLError: statForbiddenError,
|
||||
Props: []Property{{
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
|
||||
}},
|
||||
|
@ -351,7 +345,7 @@ func TestMemPS(t *testing.T) {
|
|||
}},
|
||||
wantPropstats: []Propstat{{
|
||||
Status: http.StatusForbidden,
|
||||
XMLError: `<error xmlns="DAV:"><cannot-modify-protected-property/></error>`,
|
||||
XMLError: statForbiddenError,
|
||||
Props: []Property{{
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "displayname"},
|
||||
}},
|
||||
|
|
|
@ -206,32 +206,55 @@ type Property struct {
|
|||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
|
||||
// See multistatusWriter for the "D:" namespace prefix.
|
||||
type xmlError struct {
|
||||
XMLName xml.Name `xml:"DAV: error"`
|
||||
XMLName xml.Name `xml:"D:error"`
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
|
||||
// See multistatusWriter for the "D:" namespace prefix.
|
||||
type propstat struct {
|
||||
Prop []Property `xml:"DAV: prop>_ignored_"`
|
||||
Status string `xml:"DAV: status"`
|
||||
Error *xmlError `xml:"DAV: error"`
|
||||
ResponseDescription string `xml:"DAV: responsedescription,omitempty"`
|
||||
Prop []Property `xml:"D:prop>_ignored_"`
|
||||
Status string `xml:"D:status"`
|
||||
Error *xmlError `xml:"D:error"`
|
||||
ResponseDescription string `xml:"D:responsedescription,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalXML prepends the "D:" namespace prefix on properties in the DAV: namespace
|
||||
// before encoding. See multistatusWriter.
|
||||
func (ps propstat) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
for k, prop := range ps.Prop {
|
||||
if prop.XMLName.Space == "DAV:" {
|
||||
prop.XMLName = xml.Name{Space: "", Local: "D:" + prop.XMLName.Local}
|
||||
ps.Prop[k] = prop
|
||||
}
|
||||
}
|
||||
// Distinct type to avoid infinite recursion of MarshalXML.
|
||||
type newpropstat propstat
|
||||
return e.EncodeElement(newpropstat(ps), start)
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_response
|
||||
// See multistatusWriter for the "D:" namespace prefix.
|
||||
type response struct {
|
||||
XMLName xml.Name `xml:"DAV: response"`
|
||||
Href []string `xml:"DAV: href"`
|
||||
Propstat []propstat `xml:"DAV: propstat"`
|
||||
Status string `xml:"DAV: status,omitempty"`
|
||||
Error *xmlError `xml:"DAV: error"`
|
||||
ResponseDescription string `xml:"DAV: responsedescription,omitempty"`
|
||||
XMLName xml.Name `xml:"D:response"`
|
||||
Href []string `xml:"D:href"`
|
||||
Propstat []propstat `xml:"D:propstat"`
|
||||
Status string `xml:"D:status,omitempty"`
|
||||
Error *xmlError `xml:"D:error"`
|
||||
ResponseDescription string `xml:"D:responsedescription,omitempty"`
|
||||
}
|
||||
|
||||
// MultistatusWriter marshals one or more Responses into a XML
|
||||
// multistatus response.
|
||||
// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_multistatus
|
||||
// TODO(rsto, mpl): As a workaround, the "D:" namespace prefix, defined as
|
||||
// "DAV:" on this element, is prepended on the nested response, as well as on all
|
||||
// its nested elements. All property names in the DAV: namespace are prefixed as
|
||||
// well. This is because some versions of Mini-Redirector (on windows 7) ignore
|
||||
// elements with a default namespace (no prefixed namespace). A less intrusive fix
|
||||
// should be possible after golang.org/cl/11074. See https://golang.org/issue/11177
|
||||
type multistatusWriter struct {
|
||||
// ResponseDescription contains the optional responsedescription
|
||||
// of the multistatus XML element. Only the latest content before
|
||||
|
@ -291,7 +314,7 @@ func (w *multistatusWriter) writeHeader() error {
|
|||
Local: "multistatus",
|
||||
},
|
||||
Attr: []xml.Attr{{
|
||||
Name: xml.Name{Local: "xmlns"},
|
||||
Name: xml.Name{Space: "xmlns", Local: "D"},
|
||||
Value: "DAV:",
|
||||
}},
|
||||
})
|
||||
|
@ -340,6 +363,35 @@ func xmlLang(s xml.StartElement, d string) string {
|
|||
return d
|
||||
}
|
||||
|
||||
type xmlValue []byte
|
||||
|
||||
func (v *xmlValue) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// The XML value of a property can be arbitrary, mixed-content XML.
|
||||
// To make sure that the unmarshalled value contains all required
|
||||
// namespaces, we encode all the property value XML tokens into a
|
||||
// buffer. This forces the encoder to redeclare any used namespaces.
|
||||
var b bytes.Buffer
|
||||
e := xml.NewEncoder(&b)
|
||||
for {
|
||||
t, err := next(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e, ok := t.(xml.EndElement); ok && e.Name == start.Name {
|
||||
break
|
||||
}
|
||||
if err = e.EncodeToken(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := e.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = b.Bytes()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalXML appends the property names and values enclosed within start
|
||||
// to ps.
|
||||
//
|
||||
|
@ -355,7 +407,7 @@ func (ps *proppatchProps) UnmarshalXML(d *xml.Decoder, start xml.StartElement) e
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t.(type) {
|
||||
switch elem := t.(type) {
|
||||
case xml.EndElement:
|
||||
if len(*ps) == 0 {
|
||||
return fmt.Errorf("%s must not be empty", start.Name.Local)
|
||||
|
@ -366,29 +418,10 @@ func (ps *proppatchProps) UnmarshalXML(d *xml.Decoder, start xml.StartElement) e
|
|||
XMLName: t.(xml.StartElement).Name,
|
||||
Lang: xmlLang(t.(xml.StartElement), lang),
|
||||
}
|
||||
// The XML value of a property can be arbitrary, mixed-content XML.
|
||||
// To make sure that the unmarshalled value contains all required
|
||||
// namespaces, we encode all the property value XML tokens into a
|
||||
// buffer. This forces the encoder to redeclare any used namespaces.
|
||||
var b bytes.Buffer
|
||||
e := xml.NewEncoder(&b)
|
||||
for {
|
||||
t, err = next(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e, ok := t.(xml.EndElement); ok && e.Name == p.XMLName {
|
||||
break
|
||||
}
|
||||
if err = e.EncodeToken(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = e.Flush()
|
||||
err = d.DecodeElement(((*xmlValue)(&p.InnerXML)), &elem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.InnerXML = b.Bytes()
|
||||
*ps = append(*ps, p)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,12 @@ package webdav
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
@ -454,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>` +
|
||||
|
@ -562,6 +564,7 @@ func TestMultistatusWriter(t *testing.T) {
|
|||
wantCode: http.StatusOK,
|
||||
}}
|
||||
|
||||
n := xmlNormalizer{omitWhitespace: true}
|
||||
loop:
|
||||
for _, tc := range testCases {
|
||||
rec := httptest.NewRecorder()
|
||||
|
@ -591,70 +594,46 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadProppatch(t *testing.T) {
|
||||
// TODO(rost): These "golden XML" tests easily break with changes in the
|
||||
// xml package. A whitespace-preserving normalizer of XML content is
|
||||
// required to make these tests more robust.
|
||||
ppStr := func(pps []Proppatch) string {
|
||||
var outer []string
|
||||
for _, pp := range pps {
|
||||
var inner []string
|
||||
for _, p := range pp.Props {
|
||||
inner = append(inner, fmt.Sprintf("{XMLName: %q, Lang: %q, InnerXML: %q}",
|
||||
p.XMLName, p.Lang, p.InnerXML))
|
||||
}
|
||||
outer = append(outer, fmt.Sprintf("{Remove: %t, Props: [%s]}",
|
||||
pp.Remove, strings.Join(inner, ", ")))
|
||||
}
|
||||
return "[" + strings.Join(outer, ", ") + "]"
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
input string
|
||||
wantPP []Proppatch
|
||||
wantStatus int
|
||||
}{{
|
||||
desc: "proppatch: section 9.2",
|
||||
desc: "proppatch: section 9.2 (with simple property value)",
|
||||
input: `` +
|
||||
`<?xml version="1.0" encoding="utf-8" ?>` +
|
||||
`<D:propertyupdate xmlns:D="DAV:"` +
|
||||
` xmlns:Z="http://ns.example.com/z/">` +
|
||||
` <D:set>` +
|
||||
` <D:prop>` +
|
||||
` <Z:Authors>` +
|
||||
` <Z:Author>Jim Whitehead</Z:Author>` +
|
||||
` <Z:Author>Roy Fielding</Z:Author>` +
|
||||
` </Z:Authors>` +
|
||||
` </D:prop>` +
|
||||
` <D:prop><Z:Authors>somevalue</Z:Authors></D:prop>` +
|
||||
` </D:set>` +
|
||||
` <D:remove>` +
|
||||
` <D:prop><Z:Copyright-Owner/></D:prop>` +
|
||||
|
@ -664,17 +643,7 @@ func TestReadProppatch(t *testing.T) {
|
|||
Props: []Property{{
|
||||
xml.Name{Space: "http://ns.example.com/z/", Local: "Authors"},
|
||||
"",
|
||||
[]byte(`` +
|
||||
` ` +
|
||||
`<z:Author xmlns:z="http://ns.example.com/z/">` +
|
||||
`Jim Whitehead` +
|
||||
`</z:Author>` +
|
||||
` ` +
|
||||
`<z:Author xmlns:z="http://ns.example.com/z/">` +
|
||||
`Roy Fielding` +
|
||||
`</z:Author>` +
|
||||
` `,
|
||||
),
|
||||
[]byte(`somevalue`),
|
||||
}},
|
||||
}, {
|
||||
Remove: true,
|
||||
|
@ -684,59 +653,6 @@ func TestReadProppatch(t *testing.T) {
|
|||
nil,
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
desc: "proppatch: section 4.3.1 (mixed content)",
|
||||
input: `` +
|
||||
`<?xml version="1.0" encoding="utf-8" ?>` +
|
||||
`<D:propertyupdate xmlns:D="DAV:"` +
|
||||
` xmlns:Z="http://ns.example.com/z/">` +
|
||||
` <D:set>` +
|
||||
` <D:prop xml:lang="en" xmlns:D="DAV:">` +
|
||||
` <x:author xmlns:x='http://example.com/ns'>` +
|
||||
` <x:name>Jane Doe</x:name>` +
|
||||
` <!-- Jane's contact info -->` +
|
||||
` <x:uri type='email'` +
|
||||
` added='2005-11-26'>mailto:jane.doe@example.com</x:uri>` +
|
||||
` <x:uri type='web'` +
|
||||
` added='2005-11-27'>http://www.example.com</x:uri>` +
|
||||
` <x:notes xmlns:h='http://www.w3.org/1999/xhtml'>` +
|
||||
` Jane has been working way <h:em>too</h:em> long on the` +
|
||||
` long-awaited revision of <![CDATA[<RFC2518>]]>.` +
|
||||
` </x:notes>` +
|
||||
` </x:author>` +
|
||||
` </D:prop>` +
|
||||
` </D:set>` +
|
||||
`</D:propertyupdate>`,
|
||||
wantPP: []Proppatch{{
|
||||
Props: []Property{{
|
||||
xml.Name{Space: "http://example.com/ns", Local: "author"},
|
||||
"en",
|
||||
[]byte(`` +
|
||||
` ` +
|
||||
`<ns:name xmlns:ns="http://example.com/ns">Jane Doe</ns:name>` +
|
||||
` ` +
|
||||
`<ns:uri xmlns:ns="http://example.com/ns" type="email" added="2005-11-26">` +
|
||||
`mailto:jane.doe@example.com` +
|
||||
`</ns:uri>` +
|
||||
` ` +
|
||||
`<ns:uri xmlns:ns="http://example.com/ns" type="web" added="2005-11-27">` +
|
||||
`http://www.example.com` +
|
||||
`</ns:uri>` +
|
||||
` ` +
|
||||
|
||||
`<ns:notes xmlns:ns="http://example.com/ns"` +
|
||||
` xmlns:h="http://www.w3.org/1999/xhtml">` +
|
||||
` ` +
|
||||
` Jane has been working way` +
|
||||
` <h:em>too</h:em>` +
|
||||
` long on the` + ` ` +
|
||||
` long-awaited revision of <RFC2518>.` +
|
||||
` ` +
|
||||
`</ns:notes>` +
|
||||
` `,
|
||||
),
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
desc: "proppatch: lang attribute on prop",
|
||||
input: `` +
|
||||
|
@ -806,7 +722,187 @@ func TestReadProppatch(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(pp, tc.wantPP) || status != tc.wantStatus {
|
||||
t.Errorf("%s: proppatch\ngot %v\nwant %v", tc.desc, pp, tc.wantPP)
|
||||
t.Errorf("%s: proppatch\ngot %v\nwant %v", tc.desc, ppStr(pp), ppStr(tc.wantPP))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalXMLValue(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
input string
|
||||
wantVal string
|
||||
}{{
|
||||
desc: "simple char data",
|
||||
input: "<root>foo</root>",
|
||||
wantVal: "foo",
|
||||
}, {
|
||||
desc: "empty element",
|
||||
input: "<root><foo/></root>",
|
||||
wantVal: "<foo/>",
|
||||
}, {
|
||||
desc: "preserve namespace",
|
||||
input: `<root><foo xmlns="bar"/></root>`,
|
||||
wantVal: `<foo xmlns="bar"/>`,
|
||||
}, {
|
||||
desc: "preserve root element namespace",
|
||||
input: `<root xmlns:bar="bar"><bar:foo/></root>`,
|
||||
wantVal: `<foo xmlns="bar"/>`,
|
||||
}, {
|
||||
desc: "preserve whitespace",
|
||||
input: "<root> \t </root>",
|
||||
wantVal: " \t ",
|
||||
}, {
|
||||
desc: "preserve mixed content",
|
||||
input: `<root xmlns="bar"> <foo>a<bam xmlns="baz"/> </foo> </root>`,
|
||||
wantVal: ` <foo xmlns="bar">a<bam xmlns="baz"/> </foo> `,
|
||||
}, {
|
||||
desc: "section 9.2",
|
||||
input: `` +
|
||||
`<Z:Authors xmlns:Z="http://ns.example.com/z/">` +
|
||||
` <Z:Author>Jim Whitehead</Z:Author>` +
|
||||
` <Z:Author>Roy Fielding</Z:Author>` +
|
||||
`</Z:Authors>`,
|
||||
wantVal: `` +
|
||||
` <Author xmlns="http://ns.example.com/z/">Jim Whitehead</Author>` +
|
||||
` <Author xmlns="http://ns.example.com/z/">Roy Fielding</Author>`,
|
||||
}, {
|
||||
desc: "section 4.3.1 (mixed content)",
|
||||
input: `` +
|
||||
`<x:author ` +
|
||||
` xmlns:x='http://example.com/ns' ` +
|
||||
` xmlns:D="DAV:">` +
|
||||
` <x:name>Jane Doe</x:name>` +
|
||||
` <!-- Jane's contact info -->` +
|
||||
` <x:uri type='email'` +
|
||||
` added='2005-11-26'>mailto:jane.doe@example.com</x:uri>` +
|
||||
` <x:uri type='web'` +
|
||||
` added='2005-11-27'>http://www.example.com</x:uri>` +
|
||||
` <x:notes xmlns:h='http://www.w3.org/1999/xhtml'>` +
|
||||
` Jane has been working way <h:em>too</h:em> long on the` +
|
||||
` long-awaited revision of <![CDATA[<RFC2518>]]>.` +
|
||||
` </x:notes>` +
|
||||
`</x:author>`,
|
||||
wantVal: `` +
|
||||
` <name xmlns="http://example.com/ns">Jane Doe</name>` +
|
||||
` ` +
|
||||
` <uri type='email'` +
|
||||
` xmlns="http://example.com/ns" ` +
|
||||
` added='2005-11-26'>mailto:jane.doe@example.com</uri>` +
|
||||
` <uri added='2005-11-27'` +
|
||||
` type='web'` +
|
||||
` xmlns="http://example.com/ns">http://www.example.com</uri>` +
|
||||
` <notes xmlns="http://example.com/ns" ` +
|
||||
` xmlns:h="http://www.w3.org/1999/xhtml">` +
|
||||
` Jane has been working way <h:em>too</h:em> long on the` +
|
||||
` long-awaited revision of <RFC2518>.` +
|
||||
` </notes>`,
|
||||
}}
|
||||
|
||||
var n xmlNormalizer
|
||||
for _, tc := range testCases {
|
||||
d := xml.NewDecoder(strings.NewReader(tc.input))
|
||||
var v xmlValue
|
||||
if err := d.Decode(&v); err != nil {
|
||||
t.Errorf("%s: got error %v, want nil", tc.desc, err)
|
||||
continue
|
||||
}
|
||||
eq, err := n.equalXML(bytes.NewReader(v), strings.NewReader(tc.wantVal))
|
||||
if err != nil {
|
||||
t.Errorf("%s: equalXML: %v", tc.desc, err)
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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
|
||||
|
||||
func (a byName) Len() int { return len(a) }
|
||||
func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byName) Less(i, j int) bool {
|
||||
if a[i].Name.Space != a[j].Name.Space {
|
||||
return a[i].Name.Space < a[j].Name.Space
|
||||
}
|
||||
return a[i].Name.Local < a[j].Name.Local
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue