diff --git a/decode_test.go b/decode_test.go
index bd171a1..e0b9d75 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -8,8 +8,10 @@ import (
"log"
"net/http"
"net/http/httptest"
+ "os/exec"
"path/filepath"
"reflect"
+ "strings"
"testing"
"time"
)
@@ -513,3 +515,43 @@ func TestDecodeTagSkip(t *testing.T) {
t.Error("field decoded when it was tagged as -")
}
}
+
+// TestXMLPlutilParity tests parity with plutil -lint on macOS
+func TestXMLPlutilParity(t *testing.T) {
+ type data struct {
+ Key string `plist:"key"`
+ }
+ tests, err := ioutil.ReadDir("testdata/xml/")
+ if err != nil {
+ t.Fatalf("could not open testdata/xml: %v", err)
+ }
+
+ plutil, _ := exec.LookPath("plutil")
+
+ for _, test := range tests {
+ testPath := filepath.Join("testdata/xml/", test.Name())
+ buf, err := ioutil.ReadFile(testPath)
+ if err != nil {
+ t.Errorf("could not read test %s: %v", test.Name(), err)
+ continue
+ }
+ v := new(data)
+ err = Unmarshal(buf, v)
+
+ shouldFail := strings.HasSuffix(test.Name(), ".failure.plist")
+ if plutil != "" {
+ plutilFail := exec.Command(plutil, "-lint", testPath).Run() != nil
+ if shouldFail != plutilFail {
+ t.Errorf("expected plutil test failure: %v for %s, but got test failure: %v", shouldFail, test.Name(), plutilFail)
+ }
+ }
+
+ if shouldFail && err == nil {
+ t.Errorf("expected error for test %s but got: nil", test.Name())
+ } else if !shouldFail && err != nil {
+ t.Errorf("expected no error for test %s but got: %v", test.Name(), err)
+ } else if !shouldFail && v.Key != "val" {
+ t.Errorf("expected key=val for test %s but got: key=%s", test.Name(), v.Key)
+ }
+ }
+}
diff --git a/testdata/xml/empty-doctype.plist b/testdata/xml/empty-doctype.plist
new file mode 100644
index 0000000..aa4a935
--- /dev/null
+++ b/testdata/xml/empty-doctype.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/empty-plist.failure.plist b/testdata/xml/empty-plist.failure.plist
new file mode 100644
index 0000000..8b232f6
--- /dev/null
+++ b/testdata/xml/empty-plist.failure.plist
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/testdata/xml/empty-xml.plist b/testdata/xml/empty-xml.plist
new file mode 100644
index 0000000..05a26d1
--- /dev/null
+++ b/testdata/xml/empty-xml.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/invalid-before-plist.failure.plist b/testdata/xml/invalid-before-plist.failure.plist
new file mode 100644
index 0000000..ad9bd75
--- /dev/null
+++ b/testdata/xml/invalid-before-plist.failure.plist
@@ -0,0 +1,9 @@
+
+
+invalid
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/invalid-data.failure.plist b/testdata/xml/invalid-data.failure.plist
new file mode 100644
index 0000000..7bb255e
--- /dev/null
+++ b/testdata/xml/invalid-data.failure.plist
@@ -0,0 +1,9 @@
+
+
+
+
+ key
+ val
+invalid
+
+
diff --git a/testdata/xml/invalid-end.plist b/testdata/xml/invalid-end.plist
new file mode 100644
index 0000000..bafbc0c
--- /dev/null
+++ b/testdata/xml/invalid-end.plist
@@ -0,0 +1,9 @@
+
+
+
+
+ key
+ val
+
+
+invalid
diff --git a/testdata/xml/invalid-middle.failure.plist b/testdata/xml/invalid-middle.failure.plist
new file mode 100644
index 0000000..ed4abab
--- /dev/null
+++ b/testdata/xml/invalid-middle.failure.plist
@@ -0,0 +1,9 @@
+
+invalid
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/invalid-start.failure.plist b/testdata/xml/invalid-start.failure.plist
new file mode 100644
index 0000000..8ac26f5
--- /dev/null
+++ b/testdata/xml/invalid-start.failure.plist
@@ -0,0 +1,8 @@
+invalid
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/malformed-xml.plist b/testdata/xml/malformed-xml.plist
new file mode 100644
index 0000000..539ffb2
--- /dev/null
+++ b/testdata/xml/malformed-xml.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/no-both.plist b/testdata/xml/no-both.plist
new file mode 100644
index 0000000..4fa7ef5
--- /dev/null
+++ b/testdata/xml/no-both.plist
@@ -0,0 +1,6 @@
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/no-dict-end.failure.plist b/testdata/xml/no-dict-end.failure.plist
new file mode 100644
index 0000000..508e45c
--- /dev/null
+++ b/testdata/xml/no-dict-end.failure.plist
@@ -0,0 +1,7 @@
+
+
+
+
+ key
+ val
+
diff --git a/testdata/xml/no-doctype.plist b/testdata/xml/no-doctype.plist
new file mode 100644
index 0000000..f8134b4
--- /dev/null
+++ b/testdata/xml/no-doctype.plist
@@ -0,0 +1,7 @@
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/no-plist-end.failure.plist b/testdata/xml/no-plist-end.failure.plist
new file mode 100644
index 0000000..d34f609
--- /dev/null
+++ b/testdata/xml/no-plist-end.failure.plist
@@ -0,0 +1,7 @@
+
+
+
+
+ key
+ val
+
diff --git a/testdata/xml/no-plist-version.plist b/testdata/xml/no-plist-version.plist
new file mode 100644
index 0000000..6ef5b11
--- /dev/null
+++ b/testdata/xml/no-plist-version.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/no-xml-tag.plist b/testdata/xml/no-xml-tag.plist
new file mode 100644
index 0000000..80047b0
--- /dev/null
+++ b/testdata/xml/no-xml-tag.plist
@@ -0,0 +1,7 @@
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/swapped.plist b/testdata/xml/swapped.plist
new file mode 100644
index 0000000..0304264
--- /dev/null
+++ b/testdata/xml/swapped.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/unescaped-plist.failure.plist b/testdata/xml/unescaped-plist.failure.plist
new file mode 100644
index 0000000..d45ca28
--- /dev/null
+++ b/testdata/xml/unescaped-plist.failure.plist
@@ -0,0 +1,8 @@
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/unescaped-xml.failure.plist b/testdata/xml/unescaped-xml.failure.plist
new file mode 100644
index 0000000..116dfe4
--- /dev/null
+++ b/testdata/xml/unescaped-xml.failure.plist
@@ -0,0 +1,8 @@
+
+
+
+ key
+ val
+
+
diff --git a/testdata/xml/valid.plist b/testdata/xml/valid.plist
new file mode 100644
index 0000000..96f10ba
--- /dev/null
+++ b/testdata/xml/valid.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ key
+ val
+
+
diff --git a/xml_parser.go b/xml_parser.go
index 146e72b..375d680 100644
--- a/xml_parser.go
+++ b/xml_parser.go
@@ -1,6 +1,7 @@
package plist
import (
+ "bytes"
"encoding/base64"
"encoding/xml"
"errors"
@@ -22,19 +23,28 @@ func newXMLParser(r io.Reader) *xmlParser {
}
func (p *xmlParser) parseDocument(start *xml.StartElement) (*plistValue, error) {
- if start == nil {
- for {
- tok, err := p.Token()
- if err != nil {
- return nil, err
- }
- if t, ok := tok.(xml.StartElement); ok {
- start = &t
- break
+ if start != nil {
+ return p.parseXMLElement(start)
+ }
+
+ for {
+ tok, err := p.Token()
+ if err != nil {
+ return nil, err
+ }
+ switch el := tok.(type) {
+ case xml.StartElement:
+ return p.parseXMLElement(&el)
+ case xml.ProcInst, xml.Directive:
+ continue
+ case xml.CharData:
+ if len(bytes.TrimSpace(el)) != 0 {
+ return nil, errors.New("plist: unexpected non-empty xml.CharData")
}
+ default:
+ return nil, fmt.Errorf("unexpected element: %T", el)
}
}
- return p.parseXMLElement(start)
}
func (p *xmlParser) parseXMLElement(element *xml.StartElement) (*plistValue, error) {
@@ -63,19 +73,32 @@ func (p *xmlParser) parseXMLElement(element *xml.StartElement) (*plistValue, err
}
func (p *xmlParser) parsePlist(element *xml.StartElement) (*plistValue, error) {
+ var val *plistValue
for {
token, err := p.Token()
if err != nil {
return nil, err
}
- if el, ok := token.(xml.EndElement); ok && el.Name.Local == "plist" {
- break
- }
- if el, ok := token.(xml.StartElement); ok {
- return p.parseXMLElement(&el)
+ switch el := token.(type) {
+ case xml.EndElement:
+ if val == nil {
+ return nil, errors.New("plist: empty plist tag")
+ }
+ return val, nil
+ case xml.StartElement:
+ v, err := p.parseXMLElement(&el)
+ if err != nil {
+ return v, err
+ }
+ val = v
+ case xml.CharData:
+ if len(bytes.TrimSpace(el)) != 0 {
+ return nil, errors.New("plist: unexpected non-empty xml.CharData")
+ }
+ default:
+ return nil, fmt.Errorf("unexpected element: %T", el)
}
}
- return nil, errors.New("plist: Invalid plist")
}
func (p *xmlParser) parseDict(element *xml.StartElement) (*plistValue, error) {
@@ -86,10 +109,10 @@ func (p *xmlParser) parseDict(element *xml.StartElement) (*plistValue, error) {
if err != nil {
return nil, err
}
- if el, ok := token.(xml.EndElement); ok && el.Name.Local == "dict" {
- break
- }
- if el, ok := token.(xml.StartElement); ok {
+ switch el := token.(type) {
+ case xml.EndElement:
+ return &plistValue{Dictionary, &dictionary{m: subvalues}}, nil
+ case xml.StartElement:
if el.Name.Local == "key" {
var k string
if err := p.DecodeElement(&k, &el); err != nil {
@@ -106,9 +129,14 @@ func (p *xmlParser) parseDict(element *xml.StartElement) (*plistValue, error) {
return nil, err
}
key = nil
+ case xml.CharData:
+ if len(bytes.TrimSpace(el)) != 0 {
+ return nil, errors.New("plist: unexpected non-empty xml.CharData")
+ }
+ default:
+ return nil, fmt.Errorf("unexpected element: %T", el)
}
}
- return &plistValue{Dictionary, &dictionary{m: subvalues}}, nil
}
func (p *xmlParser) parseString(element *xml.StartElement) (*plistValue, error) {
@@ -134,18 +162,23 @@ func (p *xmlParser) parseArray(element *xml.StartElement) (*plistValue, error) {
if err != nil {
return nil, err
}
- if el, ok := token.(xml.EndElement); ok && el.Name.Local == "array" {
- break
- }
- if el, ok := token.(xml.StartElement); ok {
+ switch el := token.(type) {
+ case xml.EndElement:
+ return &plistValue{Array, subvalues}, nil
+ case xml.StartElement:
subv, err := p.parseXMLElement(&el)
if err != nil {
return nil, err
}
subvalues = append(subvalues, subv)
+ case xml.CharData:
+ if len(bytes.TrimSpace(el)) != 0 {
+ return nil, errors.New("plist: unexpected non-empty xml.CharData")
+ }
+ default:
+ return nil, fmt.Errorf("unexpected element: %T", el)
}
}
- return &plistValue{Array, subvalues}, nil
}
func (p *xmlParser) parseReal(element *xml.StartElement) (*plistValue, error) {