Skip to content

Commit

Permalink
Better parity with macOS XML plist parsing (#37)
Browse files Browse the repository at this point in the history
* added test for parity with macOS's plutil -lint

* return error when extraneous tags/chardata are present or start/end tags
are mismatched/missing (parity with plutil)

* mark test cases as failure by filename
  • Loading branch information
korylprince authored Dec 5, 2024
1 parent 69fee72 commit 6cf8243
Show file tree
Hide file tree
Showing 21 changed files with 247 additions and 26 deletions.
42 changes: 42 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"log"
"net/http"
"net/http/httptest"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -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)
}
}
}
8 changes: 8 additions & 0 deletions testdata/xml/empty-doctype.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE>
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
4 changes: 4 additions & 0 deletions testdata/xml/empty-plist.failure.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
</plist>
8 changes: 8 additions & 0 deletions testdata/xml/empty-xml.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
9 changes: 9 additions & 0 deletions testdata/xml/invalid-before-plist.failure.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
invalid
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
9 changes: 9 additions & 0 deletions testdata/xml/invalid-data.failure.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
invalid
</dict>
</plist>
9 changes: 9 additions & 0 deletions testdata/xml/invalid-end.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
invalid
9 changes: 9 additions & 0 deletions testdata/xml/invalid-middle.failure.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
invalid
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
8 changes: 8 additions & 0 deletions testdata/xml/invalid-start.failure.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
invalid<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
8 changes: 8 additions & 0 deletions testdata/xml/malformed-xml.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
6 changes: 6 additions & 0 deletions testdata/xml/no-both.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
7 changes: 7 additions & 0 deletions testdata/xml/no-dict-end.failure.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</plist>
7 changes: 7 additions & 0 deletions testdata/xml/no-doctype.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
7 changes: 7 additions & 0 deletions testdata/xml/no-plist-end.failure.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
8 changes: 8 additions & 0 deletions testdata/xml/no-plist-version.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist>
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
7 changes: 7 additions & 0 deletions testdata/xml/no-xml-tag.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
8 changes: 8 additions & 0 deletions testdata/xml/swapped.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
8 changes: 8 additions & 0 deletions testdata/xml/unescaped-plist.failure.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
8 changes: 8 additions & 0 deletions testdata/xml/unescaped-xml.failure.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
8 changes: 8 additions & 0 deletions testdata/xml/valid.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>key</key>
<string>val</string>
</dict>
</plist>
85 changes: 59 additions & 26 deletions xml_parser.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package plist

import (
"bytes"
"encoding/base64"
"encoding/xml"
"errors"
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down

0 comments on commit 6cf8243

Please sign in to comment.