blob: 91cfeaa68d519aa85dd0364e009f1a654281ed93 [file] [log] [blame]
package plist
import (
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"io"
"math"
"runtime"
"strings"
"time"
)
const xmlDOCTYPE = `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
`
type xmlPlistGenerator struct {
writer io.Writer
xmlEncoder *xml.Encoder
}
func (p *xmlPlistGenerator) generateDocument(root cfValue) {
io.WriteString(p.writer, xml.Header)
io.WriteString(p.writer, xmlDOCTYPE)
plistStartElement := xml.StartElement{
Name: xml.Name{
Space: "",
Local: "plist",
},
Attr: []xml.Attr{{
Name: xml.Name{
Space: "",
Local: "version"},
Value: "1.0"},
},
}
p.xmlEncoder.EncodeToken(plistStartElement)
p.writePlistValue(root)
p.xmlEncoder.EncodeToken(plistStartElement.End())
p.xmlEncoder.Flush()
}
func (p *xmlPlistGenerator) writeDictionary(dict *cfDictionary) {
dict.sort()
startElement := xml.StartElement{Name: xml.Name{Local: "dict"}}
p.xmlEncoder.EncodeToken(startElement)
for i, k := range dict.keys {
p.xmlEncoder.EncodeElement(k, xml.StartElement{Name: xml.Name{Local: "key"}})
p.writePlistValue(dict.values[i])
}
p.xmlEncoder.EncodeToken(startElement.End())
}
func (p *xmlPlistGenerator) writeArray(a *cfArray) {
startElement := xml.StartElement{Name: xml.Name{Local: "array"}}
p.xmlEncoder.EncodeToken(startElement)
for _, v := range a.values {
p.writePlistValue(v)
}
p.xmlEncoder.EncodeToken(startElement.End())
}
func (p *xmlPlistGenerator) writePlistValue(pval cfValue) {
if pval == nil {
return
}
defer p.xmlEncoder.Flush()
if dict, ok := pval.(*cfDictionary); ok {
p.writeDictionary(dict)
return
} else if a, ok := pval.(*cfArray); ok {
p.writeArray(a)
return
} else if uid, ok := pval.(cfUID); ok {
p.writeDictionary(&cfDictionary{
keys: []string{"CF$UID"},
values: []cfValue{
&cfNumber{
signed: false,
value: uint64(uid),
},
},
})
return
}
// Everything here and beyond is encoded the same way: <key>value</key>
key := ""
var encodedValue interface{} = pval
switch pval := pval.(type) {
case cfString:
key = "string"
case *cfNumber:
key = "integer"
if pval.signed {
encodedValue = int64(pval.value)
} else {
encodedValue = pval.value
}
case *cfReal:
key = "real"
encodedValue = pval.value
switch {
case math.IsInf(pval.value, 1):
encodedValue = "inf"
case math.IsInf(pval.value, -1):
encodedValue = "-inf"
case math.IsNaN(pval.value):
encodedValue = "nan"
}
case cfBoolean:
key = "false"
b := bool(pval)
if b {
key = "true"
}
encodedValue = ""
case cfData:
key = "data"
encodedValue = xml.CharData(base64.StdEncoding.EncodeToString([]byte(pval)))
case cfDate:
key = "date"
encodedValue = time.Time(pval).In(time.UTC).Format(time.RFC3339)
}
if key != "" {
err := p.xmlEncoder.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: key}})
if err != nil {
panic(err)
}
}
}
func (p *xmlPlistGenerator) Indent(i string) {
p.xmlEncoder.Indent("", i)
}
func newXMLPlistGenerator(w io.Writer) *xmlPlistGenerator {
mw := mustWriter{w}
return &xmlPlistGenerator{mw, xml.NewEncoder(mw)}
}
type xmlPlistParser struct {
reader io.Reader
xmlDecoder *xml.Decoder
whitespaceReplacer *strings.Replacer
ntags int
}
func (p *xmlPlistParser) parseDocument() (pval cfValue, parseError error) {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(runtime.Error); ok {
panic(r)
}
if _, ok := r.(invalidPlistError); ok {
parseError = r.(error)
} else {
// Wrap all non-invalid-plist errors.
parseError = plistParseError{"XML", r.(error)}
}
}
}()
for {
if token, err := p.xmlDecoder.Token(); err == nil {
if element, ok := token.(xml.StartElement); ok {
pval = p.parseXMLElement(element)
if p.ntags == 0 {
panic(invalidPlistError{"XML", errors.New("no elements encountered")})
}
return
}
} else {
// The first XML parse turned out to be invalid:
// we do not have an XML property list.
panic(invalidPlistError{"XML", err})
}
}
}
func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) cfValue {
var charData xml.CharData
switch element.Name.Local {
case "plist":
p.ntags++
for {
token, err := p.xmlDecoder.Token()
if err != nil {
panic(err)
}
if el, ok := token.(xml.EndElement); ok && el.Name.Local == "plist" {
break
}
if el, ok := token.(xml.StartElement); ok {
return p.parseXMLElement(el)
}
}
return nil
case "string":
p.ntags++
err := p.xmlDecoder.DecodeElement(&charData, &element)
if err != nil {
panic(err)
}
return cfString(charData)
case "integer":
p.ntags++
err := p.xmlDecoder.DecodeElement(&charData, &element)
if err != nil {
panic(err)
}
s := string(charData)
if len(s) == 0 {
panic(errors.New("invalid empty <integer/>"))
}
if s[0] == '-' {
s, base := unsignedGetBase(s[1:])
n := mustParseInt("-"+s, base, 64)
return &cfNumber{signed: true, value: uint64(n)}
} else {
s, base := unsignedGetBase(s)
n := mustParseUint(s, base, 64)
return &cfNumber{signed: false, value: n}
}
case "real":
p.ntags++
err := p.xmlDecoder.DecodeElement(&charData, &element)
if err != nil {
panic(err)
}
n := mustParseFloat(string(charData), 64)
return &cfReal{wide: true, value: n}
case "true", "false":
p.ntags++
p.xmlDecoder.Skip()
b := element.Name.Local == "true"
return cfBoolean(b)
case "date":
p.ntags++
err := p.xmlDecoder.DecodeElement(&charData, &element)
if err != nil {
panic(err)
}
t, err := time.ParseInLocation(time.RFC3339, string(charData), time.UTC)
if err != nil {
panic(err)
}
return cfDate(t)
case "data":
p.ntags++
err := p.xmlDecoder.DecodeElement(&charData, &element)
if err != nil {
panic(err)
}
str := p.whitespaceReplacer.Replace(string(charData))
l := base64.StdEncoding.DecodedLen(len(str))
bytes := make([]uint8, l)
l, err = base64.StdEncoding.Decode(bytes, []byte(str))
if err != nil {
panic(err)
}
return cfData(bytes[:l])
case "dict":
p.ntags++
var key *string
keys := make([]string, 0, 32)
values := make([]cfValue, 0, 32)
for {
token, err := p.xmlDecoder.Token()
if err != nil {
panic(err)
}
if el, ok := token.(xml.EndElement); ok && el.Name.Local == "dict" {
if key != nil {
panic(errors.New("missing value in dictionary"))
}
break
}
if el, ok := token.(xml.StartElement); ok {
if el.Name.Local == "key" {
var k string
p.xmlDecoder.DecodeElement(&k, &el)
key = &k
} else {
if key == nil {
panic(errors.New("missing key in dictionary"))
}
keys = append(keys, *key)
values = append(values, p.parseXMLElement(el))
key = nil
}
}
}
if len(keys) == 1 && keys[0] == "CF$UID" && len(values) == 1 {
if integer, ok := values[0].(*cfNumber); ok {
return cfUID(integer.value)
}
}
return &cfDictionary{keys: keys, values: values}
case "array":
p.ntags++
values := make([]cfValue, 0, 10)
for {
token, err := p.xmlDecoder.Token()
if err != nil {
panic(err)
}
if el, ok := token.(xml.EndElement); ok && el.Name.Local == "array" {
break
}
if el, ok := token.(xml.StartElement); ok {
values = append(values, p.parseXMLElement(el))
}
}
return &cfArray{values}
}
err := fmt.Errorf("encountered unknown element %s", element.Name.Local)
if p.ntags == 0 {
// If out first XML tag is invalid, it might be an openstep data element, ala <abab> or <0101>
panic(invalidPlistError{"XML", err})
}
panic(err)
}
func newXMLPlistParser(r io.Reader) *xmlPlistParser {
return &xmlPlistParser{r, xml.NewDecoder(r), strings.NewReplacer("\t", "", "\n", "", " ", "", "\r", ""), 0}
}