├── .gitignore
├── LICENSE
├── README.md
├── diff.go
├── go-plist
├── .gitignore
├── LICENSE
├── README.md
├── bplist.go
├── bplist_generator.go
├── bplist_parser.go
├── bplist_test.go
├── cmd
│ ├── experimental
│ │ └── plait
│ │ │ ├── plait.go
│ │ │ └── web
│ │ │ ├── index.html
│ │ │ ├── ply.js
│ │ │ └── ply_exec.js
│ └── ply
│ │ ├── README.md
│ │ ├── ply.go
│ │ └── prettyprint.go
├── common_data_for_test.go
├── decode.go
├── decode_test.go
├── doc.go
├── dump_test.go
├── encode.go
├── encode_test.go
├── example_custom_marshaler_test.go
├── fuzz.go
├── go16_test.go
├── go17_test.go
├── internal
│ └── cmd
│ │ └── tabler
│ │ └── tabler.go
├── invalid_bplist_test.go
├── invalid_text_test.go
├── marshal.go
├── marshal_test.go
├── must.go
├── plist.go
├── plist_types.go
├── testdata
│ └── xml_unusual_cases
│ │ ├── s01.plist
│ │ ├── s02.plist
│ │ ├── s03.plist
│ │ ├── s04.plist
│ │ ├── s05.plist
│ │ ├── s06.plist
│ │ ├── s07.plist
│ │ ├── s08.plist
│ │ ├── s09.plist
│ │ ├── s10.plist
│ │ └── s11.plist
├── text_generator.go
├── text_parser.go
├── text_tables.go
├── text_test.go
├── typeinfo.go
├── unmarshal.go
├── unmarshal_test.go
├── util.go
├── xml_generator.go
├── xml_parser.go
├── xml_test.go
├── zerocopy.go
└── zerocopy_appengine.go
├── go.mod
├── go.sum
└── main.go
/.gitignore:
--------------------------------------------------------------------------------
1 | plistwatch
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License Copyright (c) 2020 Chirag Davé
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice (including the next
11 | paragraph) shall be included in all copies or substantial portions of the
12 | Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PlistWatch
2 |
3 | ## About
4 | PlistWatch monitors real-time changes to plist files on your system.
5 | It outputs a `defaults` command to recreate that change.
6 |
7 | ## Install
8 | ```
9 | go install github.com/catilac/plistwatch@latest
10 | ```
11 |
12 | ## Usage
13 | Just run:
14 | ```
15 | plistwatch
16 | ```
17 |
18 | Now make some changes, such as moving the Dock and moving it back by clicking the *Position of Screen* options.
19 | You should see the changes being reported.
20 | You may also see other events being reported.
21 |
22 | And you should see output such as:
23 | ```
24 | defaults write "com.apple.dock" "orientation" 'left'
25 | ```
26 |
27 |
--------------------------------------------------------------------------------
/diff.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "strings"
7 | "os/exec"
8 |
9 | "github.com/catilac/plistwatch/go-plist"
10 | )
11 |
12 | func Diff(d1 map[string]interface{}, d2 map[string]interface{}) error {
13 | // check for additions and changes of domains
14 | for domain, v2 := range d2 {
15 | if v1, ok := d1[domain]; ok {
16 | // compare v1 and v2
17 | prev := v1.(map[string]interface{})
18 | curr := v2.(map[string]interface{})
19 |
20 | // check for deleted keys
21 | for key, _ := range prev {
22 | if _, ok := curr[key]; !ok {
23 | fmt.Printf("defaults delete \"%s\" \"%s\"\n", domain, key)
24 | }
25 | }
26 |
27 | for key, currVal := range curr {
28 | prevVal, ok := prev[key]
29 | if !ok || !cmp(prevVal, currVal) {
30 | // add this key
31 | s, err := marshal(currVal)
32 | if err != nil {
33 | return err
34 | }
35 |
36 | out, _ := exec.Command("defaults", "read-type", domain, key).Output()
37 | typ := strings.TrimSpace(strings.Replace(string(out), "Type is ", "", -1))
38 |
39 | value := ""
40 | switch typ {
41 | case "boolean":
42 | if *s == "1" {
43 | value = "-bool true"
44 | } else {
45 | value = "-bool false"
46 | }
47 | break
48 | case "integer":
49 | case "float":
50 | case "date":
51 | value = "-" + typ + " " + *s
52 | break
53 | // strings, arrays and dicts
54 | default:
55 | value = "'" + *s + "'"
56 | break
57 | }
58 | fmt.Printf("defaults write \"%s\" \"%s\" %s\n", key, domain, value)
59 | }
60 | }
61 | } else {
62 | s, err := marshal(v2)
63 | if err != nil {
64 | return err
65 | }
66 | fmt.Printf("defaults write \"%s\" '%v'\n", domain, *s)
67 | }
68 | }
69 |
70 | // check for deletions
71 | for domain, _ := range d1 {
72 | if _, ok := d2[domain]; !ok {
73 | fmt.Printf("defaults delete \"%s\"\n", domain)
74 | }
75 | }
76 |
77 | return nil
78 | }
79 |
80 | func cmp(a interface{}, b interface{}) bool {
81 | if reflect.TypeOf(a) != reflect.TypeOf(b) {
82 | return false
83 | }
84 |
85 | switch valA := a.(type) {
86 | case string:
87 | return a.(string) == b.(string)
88 | case int:
89 | return a.(int) == b.(int)
90 | case []interface{}:
91 | valB := b.([]interface{})
92 |
93 | if len(valA) != len(valB) {
94 | return false
95 | }
96 | for i := range valA {
97 | if !cmp(valA[i], valB[i]) {
98 | return false
99 | }
100 | }
101 | case map[string]interface{}:
102 | valB := b.(map[string]interface{})
103 | if len(valA) != len(valB) {
104 | return false
105 | }
106 |
107 | for k := range valA {
108 | if !cmp(valA[k], valB[k]) {
109 | return false
110 | }
111 | }
112 | }
113 |
114 | return true
115 | }
116 |
117 | func marshal(v interface{}) (*string, error) {
118 | bytes, err := plist.Marshal(v, plist.OpenStepFormat)
119 | if err != nil {
120 | return nil, err
121 | }
122 |
123 | s := string(bytes)
124 |
125 | return &s, nil
126 | }
127 |
--------------------------------------------------------------------------------
/go-plist/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 | *.wasm
8 |
9 | # Test binary, built with `go test -c`
10 | *.test
11 |
12 | # Output of the go coverage tool, specifically when used with LiteIDE
13 | *.out
14 |
15 | # Dependency directories (remove the comment below to include it)
16 | # vendor/
17 |
--------------------------------------------------------------------------------
/go-plist/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, Dustin L. Howett. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this
7 | list of conditions and the following disclaimer.
8 | 2. Redistributions in binary form must reproduce the above copyright notice,
9 | this list of conditions and the following disclaimer in the documentation
10 | and/or other materials provided with the distribution.
11 |
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
19 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22 |
23 | The views and conclusions contained in the software and documentation are those
24 | of the authors and should not be interpreted as representing official policies,
25 | either expressed or implied, of the FreeBSD Project.
26 |
27 | --------------------------------------------------------------------------------
28 | Parts of this package were made available under the license covering
29 | the Go language and all attended core libraries. That license follows.
30 | --------------------------------------------------------------------------------
31 |
32 | Copyright (c) 2012 The Go Authors. All rights reserved.
33 |
34 | Redistribution and use in source and binary forms, with or without
35 | modification, are permitted provided that the following conditions are
36 | met:
37 |
38 | * Redistributions of source code must retain the above copyright
39 | notice, this list of conditions and the following disclaimer.
40 | * Redistributions in binary form must reproduce the above
41 | copyright notice, this list of conditions and the following disclaimer
42 | in the documentation and/or other materials provided with the
43 | distribution.
44 | * Neither the name of Google Inc. nor the names of its
45 | contributors may be used to endorse or promote products derived from
46 | this software without specific prior written permission.
47 |
48 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
49 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
50 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
51 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
52 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
53 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
54 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
55 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
56 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
57 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
58 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
59 |
--------------------------------------------------------------------------------
/go-plist/README.md:
--------------------------------------------------------------------------------
1 | # plist - A pure Go property list transcoder [](https://gitlab.howett.net/go/plist/commits/master)
2 | ## INSTALL
3 | ```
4 | $ go get howett.net/plist
5 | ```
6 |
7 | ## FEATURES
8 | * Supports encoding/decoding property lists (Apple XML, Apple Binary, OpenStep and GNUStep) from/to arbitrary Go types
9 |
10 | ## USE
11 | ```go
12 | package main
13 | import (
14 | "howett.net/plist"
15 | "os"
16 | )
17 | func main() {
18 | encoder := plist.NewEncoder(os.Stdout)
19 | encoder.Encode(map[string]string{"hello": "world"})
20 | }
21 | ```
22 |
--------------------------------------------------------------------------------
/go-plist/bplist.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | type bplistTrailer struct {
4 | Unused [5]uint8
5 | SortVersion uint8
6 | OffsetIntSize uint8
7 | ObjectRefSize uint8
8 | NumObjects uint64
9 | TopObject uint64
10 | OffsetTableOffset uint64
11 | }
12 |
13 | const (
14 | bpTagNull uint8 = 0x00
15 | bpTagBoolFalse = 0x08
16 | bpTagBoolTrue = 0x09
17 | bpTagInteger = 0x10
18 | bpTagReal = 0x20
19 | bpTagDate = 0x30
20 | bpTagData = 0x40
21 | bpTagASCIIString = 0x50
22 | bpTagUTF16String = 0x60
23 | bpTagUID = 0x80
24 | bpTagArray = 0xA0
25 | bpTagDictionary = 0xD0
26 | )
27 |
--------------------------------------------------------------------------------
/go-plist/bplist_generator.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "encoding/binary"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "time"
9 | "unicode/utf16"
10 | )
11 |
12 | func bplistMinimumIntSize(n uint64) int {
13 | switch {
14 | case n <= uint64(0xff):
15 | return 1
16 | case n <= uint64(0xffff):
17 | return 2
18 | case n <= uint64(0xffffffff):
19 | return 4
20 | default:
21 | return 8
22 | }
23 | }
24 |
25 | func bplistValueShouldUnique(pval cfValue) bool {
26 | switch pval.(type) {
27 | case cfString, *cfNumber, *cfReal, cfDate, cfData:
28 | return true
29 | }
30 | return false
31 | }
32 |
33 | type bplistGenerator struct {
34 | writer *countedWriter
35 | objmap map[interface{}]uint64 // maps pValue.hash()es to object locations
36 | objtable []cfValue
37 | trailer bplistTrailer
38 | }
39 |
40 | func (p *bplistGenerator) flattenPlistValue(pval cfValue) {
41 | key := pval.hash()
42 | if bplistValueShouldUnique(pval) {
43 | if _, ok := p.objmap[key]; ok {
44 | return
45 | }
46 | }
47 |
48 | p.objmap[key] = uint64(len(p.objtable))
49 | p.objtable = append(p.objtable, pval)
50 |
51 | switch pval := pval.(type) {
52 | case *cfDictionary:
53 | pval.sort()
54 | for _, k := range pval.keys {
55 | p.flattenPlistValue(cfString(k))
56 | }
57 | for _, v := range pval.values {
58 | p.flattenPlistValue(v)
59 | }
60 | case *cfArray:
61 | for _, v := range pval.values {
62 | p.flattenPlistValue(v)
63 | }
64 | }
65 | }
66 |
67 | func (p *bplistGenerator) indexForPlistValue(pval cfValue) (uint64, bool) {
68 | v, ok := p.objmap[pval.hash()]
69 | return v, ok
70 | }
71 |
72 | func (p *bplistGenerator) generateDocument(root cfValue) {
73 | p.objtable = make([]cfValue, 0, 16)
74 | p.objmap = make(map[interface{}]uint64)
75 | p.flattenPlistValue(root)
76 |
77 | p.trailer.NumObjects = uint64(len(p.objtable))
78 | p.trailer.ObjectRefSize = uint8(bplistMinimumIntSize(p.trailer.NumObjects))
79 |
80 | p.writer.Write([]byte("bplist00"))
81 |
82 | offtable := make([]uint64, p.trailer.NumObjects)
83 | for i, pval := range p.objtable {
84 | offtable[i] = uint64(p.writer.BytesWritten())
85 | p.writePlistValue(pval)
86 | }
87 |
88 | p.trailer.OffsetIntSize = uint8(bplistMinimumIntSize(uint64(p.writer.BytesWritten())))
89 | p.trailer.TopObject = p.objmap[root.hash()]
90 | p.trailer.OffsetTableOffset = uint64(p.writer.BytesWritten())
91 |
92 | for _, offset := range offtable {
93 | p.writeSizedInt(offset, int(p.trailer.OffsetIntSize))
94 | }
95 |
96 | binary.Write(p.writer, binary.BigEndian, p.trailer)
97 | }
98 |
99 | func (p *bplistGenerator) writePlistValue(pval cfValue) {
100 | if pval == nil {
101 | return
102 | }
103 |
104 | switch pval := pval.(type) {
105 | case *cfDictionary:
106 | p.writeDictionaryTag(pval)
107 | case *cfArray:
108 | p.writeArrayTag(pval.values)
109 | case cfString:
110 | p.writeStringTag(string(pval))
111 | case *cfNumber:
112 | p.writeIntTag(pval.signed, pval.value)
113 | case *cfReal:
114 | if pval.wide {
115 | p.writeRealTag(pval.value, 64)
116 | } else {
117 | p.writeRealTag(pval.value, 32)
118 | }
119 | case cfBoolean:
120 | p.writeBoolTag(bool(pval))
121 | case cfData:
122 | p.writeDataTag([]byte(pval))
123 | case cfDate:
124 | p.writeDateTag(time.Time(pval))
125 | case cfUID:
126 | p.writeUIDTag(UID(pval))
127 | default:
128 | panic(fmt.Errorf("unknown plist type %t", pval))
129 | }
130 | }
131 |
132 | func (p *bplistGenerator) writeSizedInt(n uint64, nbytes int) {
133 | var val interface{}
134 | switch nbytes {
135 | case 1:
136 | val = uint8(n)
137 | case 2:
138 | val = uint16(n)
139 | case 4:
140 | val = uint32(n)
141 | case 8:
142 | val = n
143 | default:
144 | panic(errors.New("illegal integer size"))
145 | }
146 | binary.Write(p.writer, binary.BigEndian, val)
147 | }
148 |
149 | func (p *bplistGenerator) writeBoolTag(v bool) {
150 | tag := uint8(bpTagBoolFalse)
151 | if v {
152 | tag = bpTagBoolTrue
153 | }
154 | binary.Write(p.writer, binary.BigEndian, tag)
155 | }
156 |
157 | func (p *bplistGenerator) writeIntTag(signed bool, n uint64) {
158 | var tag uint8
159 | var val interface{}
160 | switch {
161 | case n <= uint64(0xff):
162 | val = uint8(n)
163 | tag = bpTagInteger | 0x0
164 | case n <= uint64(0xffff):
165 | val = uint16(n)
166 | tag = bpTagInteger | 0x1
167 | case n <= uint64(0xffffffff):
168 | val = uint32(n)
169 | tag = bpTagInteger | 0x2
170 | case n > uint64(0x7fffffffffffffff) && !signed:
171 | // 64-bit values are always *signed* in format 00.
172 | // Any unsigned value that doesn't intersect with the signed
173 | // range must be sign-extended and stored as a SInt128
174 | val = n
175 | tag = bpTagInteger | 0x4
176 | default:
177 | val = n
178 | tag = bpTagInteger | 0x3
179 | }
180 |
181 | binary.Write(p.writer, binary.BigEndian, tag)
182 | if tag&0xF == 0x4 {
183 | // SInt128; in the absence of true 128-bit integers in Go,
184 | // we'll just fake the top half. We only got here because
185 | // we had an unsigned 64-bit int that didn't fit,
186 | // so sign extend it with zeroes.
187 | binary.Write(p.writer, binary.BigEndian, uint64(0))
188 | }
189 | binary.Write(p.writer, binary.BigEndian, val)
190 | }
191 |
192 | func (p *bplistGenerator) writeUIDTag(u UID) {
193 | nbytes := bplistMinimumIntSize(uint64(u))
194 | tag := uint8(bpTagUID | (nbytes - 1))
195 |
196 | binary.Write(p.writer, binary.BigEndian, tag)
197 | p.writeSizedInt(uint64(u), nbytes)
198 | }
199 |
200 | func (p *bplistGenerator) writeRealTag(n float64, bits int) {
201 | var tag uint8 = bpTagReal | 0x3
202 | var val interface{} = n
203 | if bits == 32 {
204 | val = float32(n)
205 | tag = bpTagReal | 0x2
206 | }
207 |
208 | binary.Write(p.writer, binary.BigEndian, tag)
209 | binary.Write(p.writer, binary.BigEndian, val)
210 | }
211 |
212 | func (p *bplistGenerator) writeDateTag(t time.Time) {
213 | tag := uint8(bpTagDate) | 0x3
214 | val := float64(t.In(time.UTC).UnixNano()) / float64(time.Second)
215 | val -= 978307200 // Adjust to Apple Epoch
216 |
217 | binary.Write(p.writer, binary.BigEndian, tag)
218 | binary.Write(p.writer, binary.BigEndian, val)
219 | }
220 |
221 | func (p *bplistGenerator) writeCountedTag(tag uint8, count uint64) {
222 | marker := tag
223 | if count >= 0xF {
224 | marker |= 0xF
225 | } else {
226 | marker |= uint8(count)
227 | }
228 |
229 | binary.Write(p.writer, binary.BigEndian, marker)
230 |
231 | if count >= 0xF {
232 | p.writeIntTag(false, count)
233 | }
234 | }
235 |
236 | func (p *bplistGenerator) writeDataTag(data []byte) {
237 | p.writeCountedTag(bpTagData, uint64(len(data)))
238 | binary.Write(p.writer, binary.BigEndian, data)
239 | }
240 |
241 | func (p *bplistGenerator) writeStringTag(str string) {
242 | for _, r := range str {
243 | if r > 0x7F {
244 | utf16Runes := utf16.Encode([]rune(str))
245 | p.writeCountedTag(bpTagUTF16String, uint64(len(utf16Runes)))
246 | binary.Write(p.writer, binary.BigEndian, utf16Runes)
247 | return
248 | }
249 | }
250 |
251 | p.writeCountedTag(bpTagASCIIString, uint64(len(str)))
252 | binary.Write(p.writer, binary.BigEndian, []byte(str))
253 | }
254 |
255 | func (p *bplistGenerator) writeDictionaryTag(dict *cfDictionary) {
256 | // assumption: sorted already; flattenPlistValue did this.
257 | cnt := len(dict.keys)
258 | p.writeCountedTag(bpTagDictionary, uint64(cnt))
259 | vals := make([]uint64, cnt*2)
260 | for i, k := range dict.keys {
261 | // invariant: keys have already been "uniqued" (as PStrings)
262 | keyIdx, ok := p.objmap[cfString(k).hash()]
263 | if !ok {
264 | panic(errors.New("failed to find key " + k + " in object map during serialization"))
265 | }
266 | vals[i] = keyIdx
267 | }
268 |
269 | for i, v := range dict.values {
270 | // invariant: values have already been "uniqued"
271 | objIdx, ok := p.indexForPlistValue(v)
272 | if !ok {
273 | panic(errors.New("failed to find value in object map during serialization"))
274 | }
275 | vals[i+cnt] = objIdx
276 | }
277 |
278 | for _, v := range vals {
279 | p.writeSizedInt(v, int(p.trailer.ObjectRefSize))
280 | }
281 | }
282 |
283 | func (p *bplistGenerator) writeArrayTag(arr []cfValue) {
284 | p.writeCountedTag(bpTagArray, uint64(len(arr)))
285 | for _, v := range arr {
286 | objIdx, ok := p.indexForPlistValue(v)
287 | if !ok {
288 | panic(errors.New("failed to find value in object map during serialization"))
289 | }
290 |
291 | p.writeSizedInt(objIdx, int(p.trailer.ObjectRefSize))
292 | }
293 | }
294 |
295 | func (p *bplistGenerator) Indent(i string) {
296 | // There's nothing to indent.
297 | }
298 |
299 | func newBplistGenerator(w io.Writer) *bplistGenerator {
300 | return &bplistGenerator{
301 | writer: &countedWriter{Writer: mustWriter{w}},
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/go-plist/bplist_parser.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "io/ioutil"
10 | "math"
11 | "runtime"
12 | "time"
13 | "unicode/utf16"
14 | )
15 |
16 | const (
17 | signedHighBits = 0xFFFFFFFFFFFFFFFF
18 | )
19 |
20 | type offset uint64
21 |
22 | type bplistParser struct {
23 | buffer []byte
24 |
25 | reader io.ReadSeeker
26 | version int
27 | objects []cfValue // object ID to object
28 | trailer bplistTrailer
29 | trailerOffset uint64
30 |
31 | containerStack []offset // slice of object offsets; manipulated during container deserialization
32 | }
33 |
34 | func (p *bplistParser) validateDocumentTrailer() {
35 | if p.trailer.OffsetTableOffset >= p.trailerOffset {
36 | panic(fmt.Errorf("offset table beyond beginning of trailer (0x%x, trailer@0x%x)", p.trailer.OffsetTableOffset, p.trailerOffset))
37 | }
38 |
39 | if p.trailer.OffsetTableOffset < 9 {
40 | panic(fmt.Errorf("offset table begins inside header (0x%x)", p.trailer.OffsetTableOffset))
41 | }
42 |
43 | if p.trailerOffset > (p.trailer.NumObjects*uint64(p.trailer.OffsetIntSize))+p.trailer.OffsetTableOffset {
44 | panic(errors.New("garbage between offset table and trailer"))
45 | }
46 |
47 | if p.trailer.OffsetTableOffset+(uint64(p.trailer.OffsetIntSize)*p.trailer.NumObjects) > p.trailerOffset {
48 | panic(errors.New("offset table isn't long enough to address every object"))
49 | }
50 |
51 | maxObjectRef := uint64(1) << (8 * p.trailer.ObjectRefSize)
52 | if p.trailer.NumObjects > maxObjectRef {
53 | panic(fmt.Errorf("more objects (%v) than object ref size (%v bytes) can support", p.trailer.NumObjects, p.trailer.ObjectRefSize))
54 | }
55 |
56 | if p.trailer.OffsetIntSize < uint8(8) && (uint64(1)<<(8*p.trailer.OffsetIntSize)) <= p.trailer.OffsetTableOffset {
57 | panic(errors.New("offset size isn't big enough to address entire file"))
58 | }
59 |
60 | if p.trailer.TopObject >= p.trailer.NumObjects {
61 | panic(fmt.Errorf("top object #%d is out of range (only %d exist)", p.trailer.TopObject, p.trailer.NumObjects))
62 | }
63 | }
64 |
65 | func (p *bplistParser) parseDocument() (pval cfValue, parseError error) {
66 | defer func() {
67 | if r := recover(); r != nil {
68 | if _, ok := r.(runtime.Error); ok {
69 | panic(r)
70 | }
71 |
72 | parseError = plistParseError{"binary", r.(error)}
73 | }
74 | }()
75 |
76 | p.buffer, _ = ioutil.ReadAll(p.reader)
77 |
78 | l := len(p.buffer)
79 | if l < 40 {
80 | panic(errors.New("not enough data"))
81 | }
82 |
83 | if !bytes.Equal(p.buffer[0:6], []byte{'b', 'p', 'l', 'i', 's', 't'}) {
84 | panic(errors.New("incomprehensible magic"))
85 | }
86 |
87 | p.version = int(((p.buffer[6] - '0') * 10) + (p.buffer[7] - '0'))
88 |
89 | if p.version > 1 {
90 | panic(fmt.Errorf("unexpected version %d", p.version))
91 | }
92 |
93 | p.trailerOffset = uint64(l - 32)
94 | p.trailer = bplistTrailer{
95 | SortVersion: p.buffer[p.trailerOffset+5],
96 | OffsetIntSize: p.buffer[p.trailerOffset+6],
97 | ObjectRefSize: p.buffer[p.trailerOffset+7],
98 | NumObjects: binary.BigEndian.Uint64(p.buffer[p.trailerOffset+8:]),
99 | TopObject: binary.BigEndian.Uint64(p.buffer[p.trailerOffset+16:]),
100 | OffsetTableOffset: binary.BigEndian.Uint64(p.buffer[p.trailerOffset+24:]),
101 | }
102 |
103 | p.validateDocumentTrailer()
104 |
105 | // INVARIANTS:
106 | // - Entire offset table is before trailer
107 | // - Offset table begins after header
108 | // - Offset table can address entire document
109 | // - Object IDs are big enough to support the number of objects in this plist
110 | // - Top object is in range
111 |
112 | p.objects = make([]cfValue, p.trailer.NumObjects)
113 |
114 | pval = p.objectAtIndex(p.trailer.TopObject)
115 | return
116 | }
117 |
118 | // parseSizedInteger returns a 128-bit integer as low64, high64
119 | func (p *bplistParser) parseSizedInteger(off offset, nbytes int) (lo uint64, hi uint64, newOffset offset) {
120 | // Per comments in CoreFoundation, format version 00 requires that all
121 | // 1, 2 or 4-byte integers be interpreted as unsigned. 8-byte integers are
122 | // signed (always?) and therefore must be sign extended here.
123 | // negative 1, 2, or 4-byte integers are always emitted as 64-bit.
124 | switch nbytes {
125 | case 1:
126 | lo, hi = uint64(p.buffer[off]), 0
127 | case 2:
128 | lo, hi = uint64(binary.BigEndian.Uint16(p.buffer[off:])), 0
129 | case 4:
130 | lo, hi = uint64(binary.BigEndian.Uint32(p.buffer[off:])), 0
131 | case 8:
132 | lo = binary.BigEndian.Uint64(p.buffer[off:])
133 | if p.buffer[off]&0x80 != 0 {
134 | // sign extend if lo is signed
135 | hi = signedHighBits
136 | }
137 | case 16:
138 | lo, hi = binary.BigEndian.Uint64(p.buffer[off+8:]), binary.BigEndian.Uint64(p.buffer[off:])
139 | default:
140 | panic(errors.New("illegal integer size"))
141 | }
142 | newOffset = off + offset(nbytes)
143 | return
144 | }
145 |
146 | func (p *bplistParser) parseObjectRefAtOffset(off offset) (uint64, offset) {
147 | oid, _, next := p.parseSizedInteger(off, int(p.trailer.ObjectRefSize))
148 | return oid, next
149 | }
150 |
151 | func (p *bplistParser) parseOffsetAtOffset(off offset) (offset, offset) {
152 | parsedOffset, _, next := p.parseSizedInteger(off, int(p.trailer.OffsetIntSize))
153 | return offset(parsedOffset), next
154 | }
155 |
156 | func (p *bplistParser) objectAtIndex(index uint64) cfValue {
157 | if index >= p.trailer.NumObjects {
158 | panic(fmt.Errorf("invalid object#%d (max %d)", index, p.trailer.NumObjects))
159 | }
160 |
161 | if pval := p.objects[index]; pval != nil {
162 | return pval
163 | }
164 |
165 | off, _ := p.parseOffsetAtOffset(offset(p.trailer.OffsetTableOffset + (index * uint64(p.trailer.OffsetIntSize))))
166 | if off > offset(p.trailer.OffsetTableOffset-1) {
167 | panic(fmt.Errorf("object#%d starts beyond beginning of object table (0x%x, table@0x%x)", index, off, p.trailer.OffsetTableOffset))
168 | }
169 |
170 | pval := p.parseTagAtOffset(off)
171 | p.objects[index] = pval
172 | return pval
173 |
174 | }
175 |
176 | func (p *bplistParser) pushNestedObject(off offset) {
177 | for _, v := range p.containerStack {
178 | if v == off {
179 | p.panicNestedObject(off)
180 | }
181 | }
182 | p.containerStack = append(p.containerStack, off)
183 | }
184 |
185 | func (p *bplistParser) panicNestedObject(off offset) {
186 | ids := ""
187 | for _, v := range p.containerStack {
188 | ids += fmt.Sprintf("0x%x > ", v)
189 | }
190 |
191 | // %s0x%d: ids above ends with " > "
192 | panic(fmt.Errorf("self-referential collection@0x%x (%s0x%x) cannot be deserialized", off, ids, off))
193 | }
194 |
195 | func (p *bplistParser) popNestedObject() {
196 | p.containerStack = p.containerStack[:len(p.containerStack)-1]
197 | }
198 |
199 | func (p *bplistParser) parseTagAtOffset(off offset) cfValue {
200 | tag := p.buffer[off]
201 |
202 | switch tag & 0xF0 {
203 | case bpTagNull:
204 | switch tag & 0x0F {
205 | case bpTagBoolTrue, bpTagBoolFalse:
206 | return cfBoolean(tag == bpTagBoolTrue)
207 | }
208 | case bpTagInteger:
209 | lo, hi, _ := p.parseIntegerAtOffset(off)
210 | return &cfNumber{
211 | signed: hi == signedHighBits, // a signed integer is stored as a 128-bit integer with the top 64 bits set
212 | value: lo,
213 | }
214 | case bpTagReal:
215 | nbytes := 1 << (tag & 0x0F)
216 | switch nbytes {
217 | case 4:
218 | bits := binary.BigEndian.Uint32(p.buffer[off+1:])
219 | return &cfReal{wide: false, value: float64(math.Float32frombits(bits))}
220 | case 8:
221 | bits := binary.BigEndian.Uint64(p.buffer[off+1:])
222 | return &cfReal{wide: true, value: math.Float64frombits(bits)}
223 | }
224 | panic(errors.New("illegal float size"))
225 | case bpTagDate:
226 | bits := binary.BigEndian.Uint64(p.buffer[off+1:])
227 | val := math.Float64frombits(bits)
228 |
229 | // Apple Epoch is 20110101000000Z
230 | // Adjust for UNIX Time
231 | val += 978307200
232 |
233 | sec, fsec := math.Modf(val)
234 | time := time.Unix(int64(sec), int64(fsec*float64(time.Second))).In(time.UTC)
235 | return cfDate(time)
236 | case bpTagData:
237 | data := p.parseDataAtOffset(off)
238 | return cfData(data)
239 | case bpTagASCIIString:
240 | str := p.parseASCIIStringAtOffset(off)
241 | return cfString(str)
242 | case bpTagUTF16String:
243 | str := p.parseUTF16StringAtOffset(off)
244 | return cfString(str)
245 | case bpTagUID: // Somehow different than int: low half is nbytes - 1 instead of log2(nbytes)
246 | lo, _, _ := p.parseSizedInteger(off+1, int(tag&0xF)+1)
247 | return cfUID(lo)
248 | case bpTagDictionary:
249 | return p.parseDictionaryAtOffset(off)
250 | case bpTagArray:
251 | return p.parseArrayAtOffset(off)
252 | }
253 | panic(fmt.Errorf("unexpected atom 0x%2.02x at offset 0x%x", tag, off))
254 | }
255 |
256 | func (p *bplistParser) parseIntegerAtOffset(off offset) (uint64, uint64, offset) {
257 | tag := p.buffer[off]
258 | return p.parseSizedInteger(off+1, 1<<(tag&0xF))
259 | }
260 |
261 | func (p *bplistParser) countForTagAtOffset(off offset) (uint64, offset) {
262 | tag := p.buffer[off]
263 | cnt := uint64(tag & 0x0F)
264 | if cnt == 0xF {
265 | cnt, _, off = p.parseIntegerAtOffset(off + 1)
266 | return cnt, off
267 | }
268 | return cnt, off + 1
269 | }
270 |
271 | func (p *bplistParser) parseDataAtOffset(off offset) []byte {
272 | len, start := p.countForTagAtOffset(off)
273 | if start+offset(len) > offset(p.trailer.OffsetTableOffset) {
274 | panic(fmt.Errorf("data@0x%x too long (%v bytes, max is %v)", off, len, p.trailer.OffsetTableOffset-uint64(start)))
275 | }
276 | return p.buffer[start : start+offset(len)]
277 | }
278 |
279 | func (p *bplistParser) parseASCIIStringAtOffset(off offset) string {
280 | len, start := p.countForTagAtOffset(off)
281 | if start+offset(len) > offset(p.trailer.OffsetTableOffset) {
282 | panic(fmt.Errorf("ascii string@0x%x too long (%v bytes, max is %v)", off, len, p.trailer.OffsetTableOffset-uint64(start)))
283 | }
284 |
285 | return zeroCopy8BitString(p.buffer, int(start), int(len))
286 | }
287 |
288 | func (p *bplistParser) parseUTF16StringAtOffset(off offset) string {
289 | len, start := p.countForTagAtOffset(off)
290 | bytes := len * 2
291 | if start+offset(bytes) > offset(p.trailer.OffsetTableOffset) {
292 | panic(fmt.Errorf("utf16 string@0x%x too long (%v bytes, max is %v)", off, bytes, p.trailer.OffsetTableOffset-uint64(start)))
293 | }
294 |
295 | u16s := make([]uint16, len)
296 | for i := offset(0); i < offset(len); i++ {
297 | u16s[i] = binary.BigEndian.Uint16(p.buffer[start+(i*2):])
298 | }
299 | runes := utf16.Decode(u16s)
300 | return string(runes)
301 | }
302 |
303 | func (p *bplistParser) parseObjectListAtOffset(off offset, count uint64) []cfValue {
304 | if off+offset(count*uint64(p.trailer.ObjectRefSize)) > offset(p.trailer.OffsetTableOffset) {
305 | panic(fmt.Errorf("list@0x%x length (%v) puts its end beyond the offset table at 0x%x", off, count, p.trailer.OffsetTableOffset))
306 | }
307 | objects := make([]cfValue, count)
308 |
309 | next := off
310 | var oid uint64
311 | for i := uint64(0); i < count; i++ {
312 | oid, next = p.parseObjectRefAtOffset(next)
313 | objects[i] = p.objectAtIndex(oid)
314 | }
315 |
316 | return objects
317 | }
318 |
319 | func (p *bplistParser) parseDictionaryAtOffset(off offset) *cfDictionary {
320 | p.pushNestedObject(off)
321 | defer p.popNestedObject()
322 |
323 | // a dictionary is an object list of [key key key val val val]
324 | cnt, start := p.countForTagAtOffset(off)
325 | objects := p.parseObjectListAtOffset(start, cnt*2)
326 |
327 | keys := make([]string, cnt)
328 | for i := uint64(0); i < cnt; i++ {
329 | if str, ok := objects[i].(cfString); ok {
330 | keys[i] = string(str)
331 | } else {
332 | panic(fmt.Errorf("dictionary@0x%x contains non-string key at index %d", off, i))
333 | }
334 | }
335 |
336 | return &cfDictionary{
337 | keys: keys,
338 | values: objects[cnt:],
339 | }
340 | }
341 |
342 | func (p *bplistParser) parseArrayAtOffset(off offset) *cfArray {
343 | p.pushNestedObject(off)
344 | defer p.popNestedObject()
345 |
346 | // an array is just an object list
347 | cnt, start := p.countForTagAtOffset(off)
348 | return &cfArray{p.parseObjectListAtOffset(start, cnt)}
349 | }
350 |
351 | func newBplistParser(r io.ReadSeeker) *bplistParser {
352 | return &bplistParser{reader: r}
353 | }
354 |
--------------------------------------------------------------------------------
/go-plist/bplist_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "io/ioutil"
7 | "math"
8 | "testing"
9 | )
10 |
11 | func BenchmarkBplistGenerate(b *testing.B) {
12 | for i := 0; i < b.N; i++ {
13 | d := newBplistGenerator(ioutil.Discard)
14 | d.generateDocument(plistValueTree)
15 | }
16 | }
17 |
18 | func BenchmarkBplistParse(b *testing.B) {
19 | buf := bytes.NewReader(plistValueTreeAsBplist)
20 | b.ResetTimer()
21 | for i := 0; i < b.N; i++ {
22 | b.StartTimer()
23 | d := newBplistParser(buf)
24 | d.parseDocument()
25 | b.StopTimer()
26 | buf.Seek(0, 0)
27 | }
28 | }
29 |
30 | func TestBplistInt128(t *testing.T) {
31 | bplist := []byte{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0x14, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19}
32 | expected := uint64(0x090a0b0c0d0e0f10)
33 | buf := bytes.NewReader(bplist)
34 | d := newBplistParser(buf)
35 | pval, _ := d.parseDocument()
36 | if pinteger, ok := pval.(*cfNumber); !ok || pinteger.value != expected {
37 | t.Error("Expected", expected, "received", pval)
38 | }
39 | }
40 |
41 | func TestBplistSignedIntValues(t *testing.T) {
42 | bplist := []byte{
43 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
44 |
45 | // Array (8 entries)
46 | 0xA8,
47 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
48 |
49 | // 0xFFFFFFFFFFFFFF80 (MinInt8, sign extended)
50 | 0x13, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80,
51 |
52 | // 0x7F (MaxInt8)
53 | 0x10, 0x7f,
54 |
55 | // 0xFFFFFFFFFFFF8000 (MinInt16, sign extended)
56 | 0x13, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00,
57 |
58 | // 0x7FFF (MaxInt16)
59 | 0x11, 0x7f, 0xff,
60 |
61 | // 0xFFFFFFFF80000000 (MinInt32, sign extended)
62 | 0x13, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00,
63 |
64 | // 0x7FFFFFFF (MaxInt32)
65 | 0x12, 0x7f, 0xff, 0xff, 0xff,
66 |
67 | // 0x8000000000000000 (MinInt64)
68 | 0x13, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
69 |
70 | // 0x7FFFFFFFFFFFFFFF (MaxInt64)
71 | 0x13, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
72 |
73 | // Offset table
74 | 0x08, 0x11, 0x1a, 0x1c, 0x25, 0x28, 0x31, 0x36, 0x3f,
75 |
76 | // Trailer
77 | 0x00, 0x00, 0x00, 0x00, 0x00,
78 | 0x00,
79 | 0x01,
80 | 0x01,
81 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
82 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
83 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48,
84 | }
85 |
86 | expectedValues := []int64{
87 | math.MinInt8,
88 | math.MaxInt8,
89 | math.MinInt16,
90 | math.MaxInt16,
91 | math.MinInt32,
92 | math.MaxInt32,
93 | math.MinInt64,
94 | math.MaxInt64,
95 | }
96 |
97 | buf := bytes.NewReader(bplist)
98 | d := newBplistParser(buf)
99 | pval, _ := d.parseDocument()
100 | parsedValues := pval.(*cfArray).values
101 | for i, cfv := range parsedValues {
102 | value := int64(cfv.(*cfNumber).value)
103 | if value != expectedValues[i] {
104 | t.Error("Expected", expectedValues[i], "received", value)
105 | }
106 | }
107 | }
108 |
109 | func TestBplistLatin1ToUTF16(t *testing.T) {
110 | expectedPrefix := []byte{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xd1, 0x01, 0x02, 0x51, 0x5f, 0x6f, 0x10, 0x80}
111 | expectedPostfix := []byte{0x00, 0x08, 0x00, 0x0b, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x10}
112 | expectedBuf := bytes.NewBuffer(expectedPrefix)
113 |
114 | sBuf := &bytes.Buffer{}
115 | for i := uint16(0xc280); i <= 0xc2bf; i++ {
116 | binary.Write(sBuf, binary.BigEndian, i)
117 | binary.Write(expectedBuf, binary.BigEndian, i-0xc200)
118 | }
119 |
120 | for i := uint16(0xc380); i <= 0xc3bf; i++ {
121 | binary.Write(sBuf, binary.BigEndian, i)
122 | binary.Write(expectedBuf, binary.BigEndian, i-0xc300+0x0040)
123 | }
124 |
125 | expectedBuf.Write(expectedPostfix)
126 |
127 | var buf bytes.Buffer
128 | encoder := NewBinaryEncoder(&buf)
129 |
130 | data := map[string]string{
131 | "_": string(sBuf.Bytes()),
132 | }
133 | if err := encoder.Encode(data); err != nil {
134 | t.Error(err.Error())
135 | }
136 |
137 | if !bytes.Equal(buf.Bytes(), expectedBuf.Bytes()) {
138 | t.Error("Expected", expectedBuf.Bytes(), "received", buf.Bytes())
139 | return
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/go-plist/cmd/experimental/plait/plait.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "os"
8 | "syscall/js"
9 |
10 | "howett.net/plist"
11 | )
12 |
13 | const JSONFormat int = 100
14 |
15 | var nameFormatMap = map[string]int{
16 | "xml": plist.XMLFormat,
17 | "binary": plist.BinaryFormat,
18 | "openstep": plist.OpenStepFormat,
19 | "gnustep": plist.GNUStepFormat,
20 | "json": JSONFormat,
21 | }
22 |
23 | func main() {
24 | convert := os.Args[1]
25 | format, ok := nameFormatMap[convert]
26 | if !ok {
27 | fmt.Fprintf(os.Stderr, "unknown output format %s\n", convert)
28 | return
29 | }
30 |
31 | jsConverter := js.Global().Get("ply")
32 | jsDocumentLength := jsConverter.Call("readDocument").Int()
33 | document := make([]byte, jsDocumentLength)
34 | jsDocumentTemp := js.TypedArrayOf(document)
35 | jsConverter.Call("readDocument", jsDocumentTemp, jsDocumentLength)
36 | jsDocumentTemp.Release()
37 |
38 | file := bytes.NewReader(document)
39 | outfile := &bytes.Buffer{}
40 |
41 | var val interface{}
42 | dec := plist.NewDecoder(file)
43 | err := dec.Decode(&val)
44 |
45 | if err != nil {
46 | bail(err)
47 | }
48 |
49 | if format == JSONFormat {
50 | enc := json.NewEncoder(outfile)
51 | enc.SetIndent("", "\t")
52 | err = enc.Encode(val)
53 | } else {
54 | enc := plist.NewEncoderForFormat(outfile, format)
55 | enc.Indent("\t")
56 | err = enc.Encode(val)
57 | }
58 |
59 | if err != nil {
60 | bail(err)
61 | }
62 |
63 | a := js.TypedArrayOf(outfile.Bytes())
64 | jsConverter.Call("writeDocument", a)
65 | a.Release()
66 | }
67 |
68 | func bail(err error) {
69 | fmt.Fprintln(os.Stderr, err.Error())
70 | os.Exit(1)
71 | }
72 |
--------------------------------------------------------------------------------
/go-plist/cmd/experimental/plait/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
17 |
18 |
19 |
20 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/go-plist/cmd/experimental/plait/web/ply.js:
--------------------------------------------------------------------------------
1 | import "./ply_exec.js";
2 |
3 | let wasmModule;
4 | async function ply(doc, format) {
5 | const go = new Ply();
6 | if (typeof(wasmModule) === "undefined") {
7 | let plyWasm = fetch("ply.wasm");
8 | await WebAssembly.compileStreaming(plyWasm).then(m => {
9 | wasmModule = m;
10 | })
11 | }
12 | return WebAssembly.instantiate(wasmModule, go.importObject).then(inst => {
13 | return go.run(inst, Uint8Array.from(doc), format);
14 | });
15 | }
16 |
17 | var encoder;
18 | var decoder;
19 |
20 | async function toU8(string) {
21 | if (typeof(encoder) === "undefined") {
22 | encoder = new TextEncoder("utf-8");
23 | }
24 | return encoder.encode(string);
25 | }
26 |
27 | async function fromU8(buf) {
28 | if (typeof(decoder) === "undefined") {
29 | decoder = new TextDecoder("utf-8");
30 | }
31 | return decoder.decode(buf);
32 | }
33 |
34 | export function convertDocument() {
35 | let outTextField = document.getElementById("plistOut");
36 | outTextField.value = "(loading, hold on. first time's slow.)";
37 | toU8(document.getElementById("plistIn").value).then(plistDocument => {
38 | return ply(plistDocument, document.getElementById("plistConvertTo").value);
39 | }).then(out => {
40 | return fromU8(out)
41 | }).then(out => {
42 | outTextField.value = out;
43 | }).catch(err => {
44 | outTextField.value = "FAILED!\n" + err;
45 | });
46 | }
--------------------------------------------------------------------------------
/go-plist/cmd/experimental/plait/web/ply_exec.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | (() => {
6 | // Map web browser API and Node.js API to a single common API (preferring web standards over Node.js API).
7 | const isNodeJS = typeof process !== "undefined";
8 | if (isNodeJS) {
9 | global.require = require;
10 | global.fs = require("fs");
11 |
12 | const nodeCrypto = require("crypto");
13 | global.crypto = {
14 | getRandomValues(b) {
15 | nodeCrypto.randomFillSync(b);
16 | },
17 | };
18 |
19 | global.performance = {
20 | now() {
21 | const [sec, nsec] = process.hrtime();
22 | return sec * 1000 + nsec / 1000000;
23 | },
24 | };
25 |
26 | const util = require("util");
27 | global.TextEncoder = util.TextEncoder;
28 | global.TextDecoder = util.TextDecoder;
29 | } else {
30 | if (typeof window !== "undefined") {
31 | window.global = window;
32 | } else if (typeof self !== "undefined") {
33 | self.global = self;
34 | } else {
35 | throw new Error("cannot export Go (neither window nor self is defined)");
36 | }
37 | }
38 |
39 | const encoder = new TextEncoder("utf-8");
40 | const decoder = new TextDecoder("utf-8");
41 |
42 | global.Go = class {
43 | constructor() {
44 | this.argv = ["js"];
45 | this.env = {};
46 | this.exit = (code) => {
47 | if (code !== 0) {
48 | console.warn("exit code:", code);
49 | }
50 | };
51 | this._callbackTimeouts = new Map();
52 | this._nextCallbackTimeoutID = 1;
53 |
54 | const mem = () => {
55 | // The buffer may change when requesting more memory.
56 | return new DataView(this._inst.exports.mem.buffer);
57 | }
58 |
59 | const setInt64 = (addr, v) => {
60 | mem().setUint32(addr + 0, v, true);
61 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
62 | }
63 |
64 | const getInt64 = (addr) => {
65 | const low = mem().getUint32(addr + 0, true);
66 | const high = mem().getInt32(addr + 4, true);
67 | return low + high * 4294967296;
68 | }
69 |
70 | const loadValue = (addr) => {
71 | const f = mem().getFloat64(addr, true);
72 | if (!isNaN(f)) {
73 | return f;
74 | }
75 |
76 | const id = mem().getUint32(addr, true);
77 | return this._values[id];
78 | }
79 |
80 | const storeValue = (addr, v) => {
81 | const nanHead = 0x7FF80000;
82 |
83 | if (typeof v === "number") {
84 | if (isNaN(v)) {
85 | mem().setUint32(addr + 4, nanHead, true);
86 | mem().setUint32(addr, 0, true);
87 | return;
88 | }
89 | mem().setFloat64(addr, v, true);
90 | return;
91 | }
92 |
93 | switch (v) {
94 | case undefined:
95 | mem().setUint32(addr + 4, nanHead, true);
96 | mem().setUint32(addr, 1, true);
97 | return;
98 | case null:
99 | mem().setUint32(addr + 4, nanHead, true);
100 | mem().setUint32(addr, 2, true);
101 | return;
102 | case true:
103 | mem().setUint32(addr + 4, nanHead, true);
104 | mem().setUint32(addr, 3, true);
105 | return;
106 | case false:
107 | mem().setUint32(addr + 4, nanHead, true);
108 | mem().setUint32(addr, 4, true);
109 | return;
110 | }
111 |
112 | let ref = this._refs.get(v);
113 | if (ref === undefined) {
114 | ref = this._values.length;
115 | this._values.push(v);
116 | this._refs.set(v, ref);
117 | }
118 | let typeFlag = 0;
119 | switch (typeof v) {
120 | case "string":
121 | typeFlag = 1;
122 | break;
123 | case "symbol":
124 | typeFlag = 2;
125 | break;
126 | case "function":
127 | typeFlag = 3;
128 | break;
129 | }
130 | mem().setUint32(addr + 4, nanHead | typeFlag, true);
131 | mem().setUint32(addr, ref, true);
132 | }
133 |
134 | const loadSlice = (addr) => {
135 | const array = getInt64(addr + 0);
136 | const len = getInt64(addr + 8);
137 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
138 | }
139 |
140 | const loadSliceOfValues = (addr) => {
141 | const array = getInt64(addr + 0);
142 | const len = getInt64(addr + 8);
143 | const a = new Array(len);
144 | for (let i = 0; i < len; i++) {
145 | a[i] = loadValue(array + i * 8);
146 | }
147 | return a;
148 | }
149 |
150 | const loadString = (addr) => {
151 | const saddr = getInt64(addr + 0);
152 | const len = getInt64(addr + 8);
153 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
154 | }
155 |
156 | const timeOrigin = Date.now() - performance.now();
157 | this.importObject = {
158 | go: {
159 | // func wasmExit(code int32)
160 | "runtime.wasmExit": (sp) => {
161 | const code = mem().getInt32(sp + 8, true);
162 | this.exited = true;
163 | delete this._inst;
164 | delete this._values;
165 | delete this._refs;
166 | this.exit(code);
167 | },
168 |
169 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
170 | "runtime.wasmWrite": (sp) => {
171 | const fd = getInt64(sp + 8);
172 | const p = getInt64(sp + 16);
173 | const n = mem().getInt32(sp + 24, true);
174 | this.getGlobals().fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
175 | },
176 |
177 | // func nanotime() int64
178 | "runtime.nanotime": (sp) => {
179 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
180 | },
181 |
182 | // func walltime() (sec int64, nsec int32)
183 | "runtime.walltime": (sp) => {
184 | const msec = (new Date).getTime();
185 | setInt64(sp + 8, msec / 1000);
186 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
187 | },
188 |
189 | // func scheduleCallback(delay int64) int32
190 | "runtime.scheduleCallback": (sp) => {
191 | const id = this._nextCallbackTimeoutID;
192 | this._nextCallbackTimeoutID++;
193 | this._callbackTimeouts.set(id, setTimeout(
194 | () => { this._resolveCallbackPromise(); },
195 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
196 | ));
197 | mem().setInt32(sp + 16, id, true);
198 | },
199 |
200 | // func clearScheduledCallback(id int32)
201 | "runtime.clearScheduledCallback": (sp) => {
202 | const id = mem().getInt32(sp + 8, true);
203 | clearTimeout(this._callbackTimeouts.get(id));
204 | this._callbackTimeouts.delete(id);
205 | },
206 |
207 | // func getRandomData(r []byte)
208 | "runtime.getRandomData": (sp) => {
209 | crypto.getRandomValues(loadSlice(sp + 8));
210 | },
211 |
212 | // func stringVal(value string) ref
213 | "syscall/js.stringVal": (sp) => {
214 | storeValue(sp + 24, loadString(sp + 8));
215 | },
216 |
217 | // func valueGet(v ref, p string) ref
218 | "syscall/js.valueGet": (sp) => {
219 | storeValue(sp + 32, Reflect.get(loadValue(sp + 8), loadString(sp + 16)));
220 | },
221 |
222 | // func valueSet(v ref, p string, x ref)
223 | "syscall/js.valueSet": (sp) => {
224 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
225 | },
226 |
227 | // func valueIndex(v ref, i int) ref
228 | "syscall/js.valueIndex": (sp) => {
229 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
230 | },
231 |
232 | // valueSetIndex(v ref, i int, x ref)
233 | "syscall/js.valueSetIndex": (sp) => {
234 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
235 | },
236 |
237 | // func valueCall(v ref, m string, args []ref) (ref, bool)
238 | "syscall/js.valueCall": (sp) => {
239 | try {
240 | const v = loadValue(sp + 8);
241 | const m = Reflect.get(v, loadString(sp + 16));
242 | const args = loadSliceOfValues(sp + 32);
243 | storeValue(sp + 56, Reflect.apply(m, v, args));
244 | mem().setUint8(sp + 64, 1);
245 | } catch (err) {
246 | storeValue(sp + 56, err);
247 | mem().setUint8(sp + 64, 0);
248 | }
249 | },
250 |
251 | // func valueInvoke(v ref, args []ref) (ref, bool)
252 | "syscall/js.valueInvoke": (sp) => {
253 | try {
254 | const v = loadValue(sp + 8);
255 | const args = loadSliceOfValues(sp + 16);
256 | storeValue(sp + 40, Reflect.apply(v, undefined, args));
257 | mem().setUint8(sp + 48, 1);
258 | } catch (err) {
259 | storeValue(sp + 40, err);
260 | mem().setUint8(sp + 48, 0);
261 | }
262 | },
263 |
264 | // func valueNew(v ref, args []ref) (ref, bool)
265 | "syscall/js.valueNew": (sp) => {
266 | try {
267 | const v = loadValue(sp + 8);
268 | const args = loadSliceOfValues(sp + 16);
269 | storeValue(sp + 40, Reflect.construct(v, args));
270 | mem().setUint8(sp + 48, 1);
271 | } catch (err) {
272 | storeValue(sp + 40, err);
273 | mem().setUint8(sp + 48, 0);
274 | }
275 | },
276 |
277 | // func valueLength(v ref) int
278 | "syscall/js.valueLength": (sp) => {
279 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
280 | },
281 |
282 | // valuePrepareString(v ref) (ref, int)
283 | "syscall/js.valuePrepareString": (sp) => {
284 | const str = encoder.encode(String(loadValue(sp + 8)));
285 | storeValue(sp + 16, str);
286 | setInt64(sp + 24, str.length);
287 | },
288 |
289 | // valueLoadString(v ref, b []byte)
290 | "syscall/js.valueLoadString": (sp) => {
291 | const str = loadValue(sp + 8);
292 | loadSlice(sp + 16).set(str);
293 | },
294 |
295 | // func valueInstanceOf(v ref, t ref) bool
296 | "syscall/js.valueInstanceOf": (sp) => {
297 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
298 | },
299 |
300 | "debug": (value) => {
301 | console.log(value);
302 | },
303 | }
304 | };
305 | }
306 |
307 | getGlobals() {
308 | return global;
309 | }
310 |
311 | async run(instance) {
312 | this._inst = instance;
313 | this._values = [ // TODO: garbage collection
314 | NaN,
315 | undefined,
316 | null,
317 | true,
318 | false,
319 | this.getGlobals(),
320 | this._inst.exports.mem,
321 | this,
322 | ];
323 | this._refs = new Map();
324 | this._callbackShutdown = false;
325 | this.exited = false;
326 |
327 | const mem = new DataView(this._inst.exports.mem.buffer)
328 |
329 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
330 | let offset = 4096;
331 |
332 | const strPtr = (str) => {
333 | let ptr = offset;
334 | new Uint8Array(mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0"));
335 | offset += str.length + (8 - (str.length % 8));
336 | return ptr;
337 | };
338 |
339 | const argc = this.argv.length;
340 |
341 | const argvPtrs = [];
342 | this.argv.forEach((arg) => {
343 | argvPtrs.push(strPtr(arg));
344 | });
345 |
346 | const keys = Object.keys(this.env).sort();
347 | argvPtrs.push(keys.length);
348 | keys.forEach((key) => {
349 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
350 | });
351 |
352 | const argv = offset;
353 | argvPtrs.forEach((ptr) => {
354 | mem.setUint32(offset, ptr, true);
355 | mem.setUint32(offset + 4, 0, true);
356 | offset += 8;
357 | });
358 |
359 | while (true) {
360 | const callbackPromise = new Promise((resolve) => {
361 | this._resolveCallbackPromise = () => {
362 | if (this.exited) {
363 | throw new Error("bad callback: Go program has already exited");
364 | }
365 | setTimeout(resolve, 0); // make sure it is asynchronous
366 | };
367 | });
368 | this._inst.exports.run(argc, argv);
369 | if (this.exited) {
370 | break;
371 | }
372 | await callbackPromise;
373 | }
374 | }
375 | }
376 |
377 | global.Ply = class extends Go {
378 | constructor() {
379 | super();
380 | const requiredFsConstants = { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_NONBLOCK: -1, O_SYNC: -1 };
381 | this._fd = 3;
382 | this._files = {};
383 | let that = this;
384 |
385 | const enosys = function() {
386 | const err = new Error("not implemented");
387 | err.code = "ENOSYS";
388 | throw err;
389 | }
390 |
391 | this._stdout = "";
392 | this._stderr = "";
393 | this._globals = {
394 | /* go required */
395 | Array: global.Array,
396 | eval: global.eval,
397 | console: global.console,
398 | Int8Array: global.Int8Array,
399 | Int16Array: global.Int16Array,
400 | Int32Array: global.Int32Array,
401 | Uint8Array: global.Uint8Array,
402 | Uint16Array: global.Uint16Array,
403 | Uint32Array: global.Uint32Array,
404 | Float32Array: global.Float32Array,
405 | Float64Array: global.Float64Array,
406 | /* -- */
407 | ply: this,
408 | fs: {
409 | constants: requiredFsConstants, // unused
410 | // catch output to stdout/stderr
411 | writeSync(fd, buf) {
412 | let s = decoder.decode(buf);
413 | switch (fd) {
414 | case 1:
415 | that._stdout += s;
416 | break;
417 | case 2:
418 | that._stderr += s;
419 | break;
420 | }
421 | return buf.length;
422 | },
423 | readSync: enosys,
424 | fstatSync: enosys,
425 | openSync: enosys,
426 | closeSync: enosys,
427 | },
428 | };
429 | }
430 |
431 | getGlobals() {
432 | return this._globals;
433 | }
434 |
435 | readDocument(buf, len) {
436 | if (typeof(buf) === "undefined") {
437 | return this._plistDocument.length;
438 | }
439 | let r = Math.min(len, this._plistDocument.length);
440 | buf.set(this._plistDocument.subarray(0, r));
441 | return r;
442 | }
443 |
444 | writeDocument(buf) {
445 | this._outputDocument = buf;
446 | }
447 |
448 | _reset() {
449 | this._stdout = "";
450 | this._stderr = "";
451 | }
452 |
453 | async run(inst, document, format) {
454 | this.argv = ["ply", format || "xml"];
455 | this._plistDocument = document;
456 | this._reset();
457 | await super.run(inst);
458 | if (this._stderr != "") {
459 | throw new Error(this._stderr);
460 | }
461 | return this._outputDocument;
462 | }
463 | }
464 |
465 | if (isNodeJS) {
466 | const go = new Ply();
467 | WebAssembly.instantiate(global.fs.readFileSync("ply.wasm"), go.importObject).then((result) => {
468 | process.on("exit", (code) => { // Node.js exits if no callback is pending
469 | if (code === 0 && !go.exited) {
470 | // deadlock, make Go print error and stack traces
471 | go._callbackShutdown = true;
472 | go._inst.exports.run();
473 | }
474 | });
475 | return go.run(result.instance, global.fs.readFileSync(process.argv[3]), process.argv[2]);
476 | }).then((out) => {
477 | process.stdout.write(out);
478 | }).catch((err) => {
479 | throw err;
480 | });
481 | }
482 | })();
483 |
--------------------------------------------------------------------------------
/go-plist/cmd/ply/README.md:
--------------------------------------------------------------------------------
1 | # Ply
2 | Property list pretty-printer powered by `howett.net/plist`.
3 |
4 | _verb. work with (a tool, especially one requiring steady, rhythmic movements)._
5 |
6 | ## Installation
7 |
8 | `go get howett.net/plist/cmd/ply`
9 |
10 | ## Usage
11 |
12 | ```
13 | ply [OPTIONS]
14 |
15 | Application Options:
16 | -c, --convert= convert the property list to a new format (c=list for list) (pretty)
17 | -k, --key= A keypath! (/)
18 | -o, --out= output filename
19 | -I, --indent indent indentable output formats (xml, openstep, gnustep, json)
20 |
21 | Help Options:
22 | -h, --help Show this help message
23 | ```
24 |
25 | ## Features
26 |
27 | ### Keypath evaluation
28 |
29 | ```
30 | $ ply file.plist
31 | {
32 | x: {
33 | y: {
34 | z: 1024
35 | }
36 | }
37 | }
38 | $ ply -k x/y/z file.plist
39 | 1024
40 | ```
41 |
42 | Keypaths are composed of a number of path expressions:
43 |
44 | * `/name` - dictionary key access
45 | * `[i]` - index array, string, or data
46 | * `[i:j]` - silce array, string, or data in the range `[i, j)`
47 | * `!` - parse the data value as a property list and use it as the base of evaluation for further path components
48 | * `$(subexpression)` - evaluate `subexpression` and paste its value
49 |
50 | #### Examples
51 |
52 | Given the following property list:
53 |
54 | ```
55 | {
56 | a = {
57 | b = {
58 | c = (1, 2, 3);
59 | d = hello;
60 | };
61 | data = <414243>;
62 | };
63 | sub = <7b0a0974 6869733d 22612064 69637469 6f6e6172 7920696e 73696465 20616e6f 74686572 20706c69 73742122 3b7d>;
64 | hello = subexpression;
65 | }
66 | ```
67 |
68 | ##### pretty print
69 | ```
70 | $ ply file.plist
71 | {
72 | a: {
73 | b: {
74 | c: (
75 | [0]: 1
76 | [1]: 2
77 | [2]: 3
78 | )
79 | d: hello
80 | }
81 | data: 00000000 41 42 43 |ABC.............|
82 | }
83 | hello: subexpression
84 | sub: 00000000 7b 0a 09 74 68 69 73 3d 22 61 20 64 69 63 74 69 |{..this="a dicti|
85 | 00000010 6f 6e 61 72 79 20 69 6e 73 69 64 65 20 61 6e 6f |onary inside ano|
86 | 00000020 74 68 65 72 20 70 6c 69 73 74 21 22 3b 7d |ther plist!";}..|
87 | }
88 | ```
89 |
90 | ##### consecutive dictionary keys
91 | ```
92 | $ ply file.plist -k 'a/b/d'
93 | hello
94 | ```
95 |
96 | ##### array indexing
97 | ```
98 | $ ply file.plist -k 'a/b/c[1]'
99 | 2
100 | ```
101 |
102 | ##### data hexdump
103 | ```
104 | $ ply file.plist -k 'a/data'
105 | 00000000 41 42 43 |ABC.............|
106 | ```
107 |
108 | ##### data and array slicing
109 | ```
110 | $ ply file.plist -k 'a/data[2:3]'
111 | 00000000 43 |C...............|
112 | ```
113 |
114 | ```
115 | $ ply -k 'sub[0:10]' file.plist
116 | 00000000 7b 0a 09 74 68 69 73 3d 22 61 |{..this="a......|
117 | ```
118 |
119 | ##### subplist parsing
120 | ```
121 | $ ply -k 'sub!' file.plist
122 | {
123 | this: a dictionary inside another plist!
124 | }
125 | ```
126 |
127 | ##### subplist keypath evaluation
128 | ```
129 | $ ply -k 'sub!/this' file.plist
130 | a dictionary inside another plist!
131 | ```
132 |
133 | ##### subexpression evaluation
134 | ```
135 | $ ply -k '/$(/a/b/d)' file.plist
136 | subexpression
137 | ```
138 |
139 | ### Property list conversion
140 |
141 | `-c `, or `-c list` to list them all.
142 |
143 | * Binary property list [`bplist`]
144 | * XML [`xml`]
145 | * GNUstep [`gnustep`, `gs`]
146 | * OpenStep [`openstep`, `os`]
147 | * JSON (for a subset of data types) [`json`]
148 | * YAML [`yaml`]
149 |
150 | #### Notes
151 | By default, ply will emit the most compact representation it can for a given format. The `-I` flag influences the inclusion of whitespace.
152 |
153 | Ply will overwrite the input file unless an output filename is specified with `-o `.
154 |
155 | ### Property list subsetting
156 |
157 | (and subset conversion)
158 |
159 | ```
160 | $ ply -k '/a/b' -o file-a-b.plist -c openstep -I file.plist
161 | $ cat file-a-b.plist
162 | {
163 | c = (
164 | 1,
165 | 2,
166 | 3,
167 | );
168 | d = hello;
169 | }
170 | ```
171 |
172 | #### Subplist extraction
173 |
174 | ```
175 | $ ply -k '/sub!' -o file-sub.plist -c openstep -I file.plist
176 | $ cat file-sub.plist
177 | {
178 | this = "a dictionary inside another plist!";
179 | }
180 | ```
181 |
--------------------------------------------------------------------------------
/go-plist/cmd/ply/ply.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "encoding/binary"
7 | "encoding/json"
8 | "errors"
9 | "fmt"
10 | "io"
11 | "os"
12 | "path/filepath"
13 | "reflect"
14 | "strconv"
15 | "strings"
16 |
17 | "github.com/jessevdk/go-flags"
18 | "gopkg.in/yaml.v1"
19 | "howett.net/plist"
20 | )
21 |
22 | //import "github.com/mgutz/ansi"
23 |
24 | const (
25 | PrettyFormat = 100 + iota
26 | JSONFormat
27 | YAMLFormat
28 | RawFormat
29 | )
30 |
31 | var nameFormatMap = map[string]int{
32 | "x": plist.XMLFormat,
33 | "xml": plist.XMLFormat,
34 | "xml1": plist.XMLFormat,
35 | "b": plist.BinaryFormat,
36 | "bin": plist.BinaryFormat,
37 | "binary": plist.BinaryFormat,
38 | "binary1": plist.BinaryFormat,
39 | "o": plist.OpenStepFormat,
40 | "os": plist.OpenStepFormat,
41 | "openstep": plist.OpenStepFormat,
42 | "step": plist.OpenStepFormat,
43 | "g": plist.GNUStepFormat,
44 | "gs": plist.GNUStepFormat,
45 | "gnustep": plist.GNUStepFormat,
46 | "pretty": PrettyFormat,
47 | "json": JSONFormat,
48 | "yaml": YAMLFormat,
49 | "r": RawFormat,
50 | "raw": RawFormat,
51 | }
52 |
53 | var opts struct {
54 | Convert string `short:"c" long:"convert" description:"convert the property list to a new format (c=list for list)" default:"pretty" value-name:""`
55 | Keypath string `short:"k" long:"key" description:"A keypath!" default:"/" value-name:""`
56 | Output string `short:"o" long:"out" description:"output filename" default:"" value-name:""`
57 | Indent bool `short:"I" long:"indent" description:"indent indentable output formats (xml, openstep, gnustep, json)"`
58 | }
59 |
60 | func main() {
61 | parser := flags.NewParser(&opts, flags.Default)
62 | args, err := parser.Parse()
63 | if err != nil {
64 | // flags.Default implies flags.PrintError; there's no reason to print it here
65 | return
66 | }
67 |
68 | if opts.Convert == "list" {
69 | formats := make([]string, len(nameFormatMap))
70 | i := 0
71 | for k, _ := range nameFormatMap {
72 | formats[i] = k
73 | i++
74 | }
75 |
76 | fmt.Fprintln(os.Stderr, "Supported output formats:")
77 | fmt.Fprintln(os.Stderr, strings.Join(formats, ", "))
78 | return
79 | }
80 |
81 | if len(args) < 1 {
82 | parser.WriteHelp(os.Stderr)
83 | return
84 | }
85 |
86 | filename := args[0]
87 |
88 | keypath := opts.Keypath
89 | if len(keypath) == 0 {
90 | c := strings.Index(filename, ":")
91 | if c > -1 {
92 | keypath = filename[c+1:]
93 | filename = filename[:c]
94 | }
95 | }
96 |
97 | file, err := os.Open(filename)
98 | if err != nil {
99 | fmt.Fprintln(os.Stderr, err.Error())
100 | return
101 | }
102 |
103 | var val interface{}
104 | switch strings.ToLower(filepath.Ext(filename)) {
105 | case ".json", ".yaml", ".yml":
106 | buf := &bytes.Buffer{}
107 | io.Copy(buf, file)
108 | err = yaml.Unmarshal(buf.Bytes(), &val)
109 | default:
110 | dec := plist.NewDecoder(file)
111 | err = dec.Decode(&val)
112 | }
113 |
114 | if err != nil {
115 | fmt.Fprintln(os.Stderr, err.Error())
116 | return
117 | }
118 | file.Close()
119 |
120 | convert := strings.ToLower(opts.Convert)
121 | format, ok := nameFormatMap[convert]
122 | if !ok {
123 | fmt.Fprintf(os.Stderr, "unknown output format %s\n", convert)
124 | return
125 | }
126 |
127 | output := opts.Output
128 | newline := false
129 | var outputStream io.WriteCloser
130 | if format < PrettyFormat && output == "" {
131 | // Writing a plist, but no output filename. Save to original.
132 | output = filename
133 | } else if format >= PrettyFormat && output == "" {
134 | // Writing a non-plist, but no output filename: Stdout
135 | outputStream = os.Stdout
136 | newline = true
137 | } else if output == "-" {
138 | // - means stdout.
139 | outputStream = os.Stdout
140 | newline = true
141 | }
142 |
143 | if outputStream == nil {
144 | outfile, err := os.Create(output)
145 | if err != nil {
146 | fmt.Fprintln(os.Stderr, err.Error())
147 | return
148 | }
149 | outputStream = outfile
150 | }
151 |
152 | keypathContext := &KeypathWalker{}
153 | rval, err := keypathContext.WalkKeypath(reflect.ValueOf(val), keypath)
154 | if err != nil {
155 | fmt.Fprintln(os.Stderr, err.Error())
156 | return
157 | }
158 | val = rval.Interface()
159 |
160 | switch {
161 | case format >= 0 && format < PrettyFormat:
162 | enc := plist.NewEncoderForFormat(outputStream, format)
163 | if opts.Indent {
164 | enc.Indent("\t")
165 | }
166 | err := enc.Encode(val)
167 | if err != nil {
168 | fmt.Fprintln(os.Stderr, err.Error())
169 | return
170 | }
171 | case format == PrettyFormat:
172 | PrettyPrint(outputStream, rval.Interface())
173 | case format == JSONFormat:
174 | var out []byte
175 | var err error
176 | if opts.Indent {
177 | out, err = json.MarshalIndent(val, "", "\t")
178 | } else {
179 | out, err = json.Marshal(val)
180 | }
181 | if err != nil {
182 | fmt.Fprintln(os.Stderr, err.Error())
183 | return
184 | }
185 | outputStream.Write(out)
186 | case format == YAMLFormat:
187 | out, err := yaml.Marshal(val)
188 | if err != nil {
189 | fmt.Fprintln(os.Stderr, err.Error())
190 | return
191 | }
192 | outputStream.Write(out)
193 | case format == RawFormat:
194 | newline = false
195 | switch rval.Kind() {
196 | case reflect.String:
197 | outputStream.Write([]byte(val.(string)))
198 | case reflect.Slice:
199 | if rval.Elem().Kind() == reflect.Uint8 {
200 | outputStream.Write(val.([]byte))
201 | }
202 | default:
203 | binary.Write(outputStream, binary.LittleEndian, val)
204 | }
205 | }
206 | if newline {
207 | fmt.Fprintf(outputStream, "\n")
208 | }
209 | outputStream.Close()
210 | }
211 |
212 | type KeypathWalker struct {
213 | rootVal *reflect.Value
214 | curVal reflect.Value
215 | }
216 |
217 | func (ctx *KeypathWalker) Split(data []byte, atEOF bool) (advance int, token []byte, err error) {
218 | mode, oldmode := 0, 0
219 | depth := 0
220 | tok, subexpr := "", ""
221 | // modes:
222 | // 0: normal string, separated by /
223 | // 1: array index (reading between [])
224 | // 2: found $, looking for ( or nothing
225 | // 3: found $(, reading subkey, looking for )
226 | // 4: "escape"? unused as yet.
227 | if len(data) == 0 && atEOF {
228 | return 0, nil, io.EOF
229 | }
230 | each:
231 | for _, v := range data {
232 | advance++
233 | switch {
234 | case mode == 4:
235 | // Completing an escape sequence.
236 | tok += string(v)
237 | mode = 0
238 | continue each
239 | case mode == 0 && v == '/':
240 | if tok != "" {
241 | break each
242 | } else {
243 | continue each
244 | }
245 | case mode == 0 && v == '[':
246 | if tok != "" {
247 | // We have encountered a [ after text, we want only the text
248 | advance-- // We don't want to consume this character.
249 | break each
250 | } else {
251 | tok += string(v)
252 | mode = 1
253 | }
254 | case mode == 1 && v == ']':
255 | mode = 0
256 | tok += string(v)
257 | break each
258 | case mode == 0 && v == '!':
259 | if tok == "" {
260 | tok = "!"
261 | break each
262 | } else {
263 | // We have encountered a ! after text, we want the text
264 | advance-- // We don't want to consume this character.
265 | break each
266 | }
267 | case (mode == 0 || mode == 1) && v == '$':
268 | oldmode = mode
269 | mode = 2
270 | case mode == 2:
271 | if v == '(' {
272 | mode = 3
273 | depth++
274 | subexpr = ""
275 | } else {
276 | // We didn't emit the $ to begin with, so we have to do it here.
277 | tok += "$" + string(v)
278 | mode = 0
279 | }
280 | case mode == 3 && v == '(':
281 | subexpr += string(v)
282 | depth++
283 | case mode == 3 && v == ')':
284 | depth--
285 | if depth == 0 {
286 | newCtx := &KeypathWalker{rootVal: ctx.rootVal}
287 | subexprVal, e := newCtx.WalkKeypath(*ctx.rootVal, subexpr)
288 | if e != nil {
289 | return 0, nil, errors.New("Dynamic subexpression " + subexpr + " failed: " + e.Error())
290 | }
291 | if subexprVal.Kind() == reflect.Interface {
292 | subexprVal = subexprVal.Elem()
293 | }
294 | s := ""
295 | if subexprVal.Kind() == reflect.String {
296 | s = subexprVal.String()
297 | } else if subexprVal.Kind() == reflect.Uint64 {
298 | s = strconv.Itoa(int(subexprVal.Uint()))
299 | } else {
300 | return 0, nil, errors.New("Dynamic subexpression " + subexpr + " evaluated to non-string/non-int.")
301 | }
302 | tok += s
303 | mode = oldmode
304 | } else {
305 | subexpr += string(v)
306 | }
307 | case mode == 3:
308 | subexpr += string(v)
309 | default:
310 | tok += string(v)
311 | }
312 |
313 | }
314 | return advance, []byte(tok), nil
315 | }
316 |
317 | func (ctx *KeypathWalker) WalkKeypath(val reflect.Value, keypath string) (reflect.Value, error) {
318 | if keypath == "" {
319 | return val, nil
320 | }
321 |
322 | if ctx.rootVal == nil {
323 | ctx.rootVal = &val
324 | }
325 |
326 | ctx.curVal = val
327 |
328 | scanner := bufio.NewScanner(strings.NewReader(keypath))
329 | scanner.Split(ctx.Split)
330 | for scanner.Scan() {
331 | token := scanner.Text()
332 | if ctx.curVal.Kind() == reflect.Interface {
333 | ctx.curVal = ctx.curVal.Elem()
334 | }
335 |
336 | switch {
337 | case len(token) == 0:
338 | continue
339 | case token[0] == '[': // array
340 | s := token[1 : len(token)-1]
341 | if ctx.curVal.Kind() != reflect.Slice && ctx.curVal.Kind() != reflect.String {
342 | return reflect.ValueOf(nil), errors.New("keypath attempted to index non-indexable with " + s)
343 | }
344 |
345 | colon := strings.Index(s, ":")
346 | if colon > -1 {
347 | var err error
348 | var si, sj int
349 | is := s[:colon]
350 | js := s[colon+1:]
351 | if is != "" {
352 | si, err = strconv.Atoi(is)
353 | if err != nil {
354 | return reflect.ValueOf(nil), err
355 | }
356 | }
357 | if js != "" {
358 | sj, err = strconv.Atoi(js)
359 | if err != nil {
360 | return reflect.ValueOf(nil), err
361 | }
362 | }
363 | if si < 0 || sj > ctx.curVal.Len() {
364 | return reflect.ValueOf(nil), errors.New("keypath attempted to index outside of indexable with " + s)
365 | }
366 | ctx.curVal = ctx.curVal.Slice(si, sj)
367 | } else {
368 | idx, _ := strconv.Atoi(s)
369 | ctx.curVal = ctx.curVal.Index(idx)
370 | }
371 | case token[0] == '!': // subplist!
372 | if ctx.curVal.Kind() != reflect.Slice || ctx.curVal.Type().Elem().Kind() != reflect.Uint8 {
373 | return reflect.Value{}, errors.New("Attempted to subplist non-data.")
374 | }
375 | byt := ctx.curVal.Interface().([]uint8)
376 | buf := bytes.NewReader(byt)
377 | dec := plist.NewDecoder(buf)
378 | var subval interface{}
379 | dec.Decode(&subval)
380 | ctx.curVal = reflect.ValueOf(subval)
381 | default: // just a string
382 | if ctx.curVal.Kind() != reflect.Map {
383 | return reflect.ValueOf(nil), errors.New("keypath attempted to descend into non-map using key " + token)
384 | }
385 | if token != "" {
386 | ctx.curVal = ctx.curVal.MapIndex(reflect.ValueOf(token))
387 | }
388 | }
389 | }
390 | err := scanner.Err()
391 | if err != nil {
392 | return reflect.ValueOf(nil), err
393 | }
394 | return ctx.curVal, nil
395 | }
396 |
--------------------------------------------------------------------------------
/go-plist/cmd/ply/prettyprint.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/hex"
5 | "fmt"
6 | "io"
7 | "reflect"
8 | "sort"
9 | "time"
10 |
11 | "howett.net/plist"
12 | )
13 |
14 | func PrettyPrint(w io.Writer, val interface{}) {
15 | printValue(w, val, "")
16 | }
17 |
18 | func printMap(w io.Writer, tv reflect.Value, depth string) {
19 | fmt.Fprintf(w, "{\n")
20 | ss := make(sort.StringSlice, tv.Len())
21 | i := 0
22 | for _, kval := range tv.MapKeys() {
23 | if kval.Kind() == reflect.Interface {
24 | kval = kval.Elem()
25 | }
26 |
27 | if kval.Kind() != reflect.String {
28 | continue
29 | }
30 |
31 | ss[i] = kval.String()
32 | i++
33 | }
34 | sort.Sort(ss)
35 | for _, k := range ss {
36 | val := tv.MapIndex(reflect.ValueOf(k))
37 | v := val.Interface()
38 | nd := depth + " "
39 | for i := 0; i < len(k)+2; i++ {
40 | nd += " "
41 | }
42 | fmt.Fprintf(w, " %s%s: ", depth, k)
43 | printValue(w, v, nd)
44 | }
45 | fmt.Fprintf(w, "%s}\n", depth)
46 | }
47 |
48 | func printValue(w io.Writer, val interface{}, depth string) {
49 | switch tv := val.(type) {
50 | case map[interface{}]interface{}:
51 | printMap(w, reflect.ValueOf(tv), depth)
52 | case map[string]interface{}:
53 | printMap(w, reflect.ValueOf(tv), depth)
54 | case []interface{}:
55 | fmt.Fprintf(w, "(\n")
56 | for i, v := range tv {
57 | id := fmt.Sprintf("[%d]", i)
58 | nd := depth + " "
59 | for i := 0; i < len(id)+2; i++ {
60 | nd += " "
61 | }
62 | fmt.Fprintf(w, " %s%s: ", depth, id)
63 | printValue(w, v, nd)
64 | }
65 | fmt.Fprintf(w, "%s)\n", depth)
66 | case plist.UID:
67 | fmt.Fprintf(w, "#%d\n", uint64(tv))
68 | case int64, uint64, string, float32, float64, bool, time.Time:
69 | fmt.Fprintf(w, "%+v\n", tv)
70 | case uint8:
71 | fmt.Fprintf(w, "0x%2.02x\n", tv)
72 | case []byte:
73 | l := len(tv)
74 | sxl := l / 16
75 | if l%16 > 0 {
76 | sxl++
77 | }
78 | sxl *= 16
79 | var buf [4]byte
80 | var off [8]byte
81 | var asc [16]byte
82 | var ol int
83 | for i := 0; i < sxl; i++ {
84 | if i%16 == 0 {
85 | if i > 0 {
86 | io.WriteString(w, depth)
87 | }
88 | buf[0] = byte(i >> 24)
89 | buf[1] = byte(i >> 16)
90 | buf[2] = byte(i >> 8)
91 | buf[3] = byte(i)
92 | hex.Encode(off[:], buf[:])
93 | io.WriteString(w, string(off[:])+" ")
94 | }
95 | if i < l {
96 | hex.Encode(off[:], tv[i:i+1])
97 | if tv[i] < 32 || tv[i] > 126 {
98 | asc[i%16] = '.'
99 | } else {
100 | asc[i%16] = tv[i]
101 | }
102 | } else {
103 | off[0] = ' '
104 | off[1] = ' '
105 | asc[i%16] = '.'
106 | }
107 | off[2] = ' '
108 | ol = 3
109 | if i%16 == 7 || i%16 == 15 {
110 | off[3] = ' '
111 | ol = 4
112 | }
113 | io.WriteString(w, string(off[:ol]))
114 | if i%16 == 15 {
115 | io.WriteString(w, "|"+string(asc[:])+"|\n")
116 | }
117 | }
118 | default:
119 | fmt.Fprintf(w, "%#v\n", val)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/go-plist/decode.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "reflect"
7 | "runtime"
8 | )
9 |
10 | type parser interface {
11 | parseDocument() (cfValue, error)
12 | }
13 |
14 | // A Decoder reads a property list from an input stream.
15 | type Decoder struct {
16 | // the format of the most-recently-decoded property list
17 | Format int
18 |
19 | reader io.ReadSeeker
20 | lax bool
21 | }
22 |
23 | // Decode works like Unmarshal, except it reads the decoder stream to find property list elements.
24 | //
25 | // After Decoding, the Decoder's Format field will be set to one of the plist format constants.
26 | func (p *Decoder) Decode(v interface{}) (err error) {
27 | defer func() {
28 | if r := recover(); r != nil {
29 | if _, ok := r.(runtime.Error); ok {
30 | panic(r)
31 | }
32 | err = r.(error)
33 | }
34 | }()
35 |
36 | header := make([]byte, 6)
37 | p.reader.Read(header)
38 | p.reader.Seek(0, 0)
39 |
40 | var parser parser
41 | var pval cfValue
42 | if bytes.Equal(header, []byte("bplist")) {
43 | parser = newBplistParser(p.reader)
44 | pval, err = parser.parseDocument()
45 | if err != nil {
46 | // Had a bplist header, but still got an error: we have to die here.
47 | return err
48 | }
49 | p.Format = BinaryFormat
50 | } else {
51 | parser = newXMLPlistParser(p.reader)
52 | pval, err = parser.parseDocument()
53 | if _, ok := err.(invalidPlistError); ok {
54 | // Rewind: the XML parser might have exhausted the file.
55 | p.reader.Seek(0, 0)
56 | // We don't use parser here because we want the textPlistParser type
57 | tp := newTextPlistParser(p.reader)
58 | pval, err = tp.parseDocument()
59 | if err != nil {
60 | return err
61 | }
62 | p.Format = tp.format
63 | if p.Format == OpenStepFormat {
64 | // OpenStep property lists can only store strings,
65 | // so we have to turn on lax mode here for the unmarshal step later.
66 | p.lax = true
67 | }
68 | } else {
69 | if err != nil {
70 | return err
71 | }
72 | p.Format = XMLFormat
73 | }
74 | }
75 |
76 | p.unmarshal(pval, reflect.ValueOf(v))
77 | return
78 | }
79 |
80 | // NewDecoder returns a Decoder that reads property list elements from a stream reader, r.
81 | // NewDecoder requires a Seekable stream for the purposes of file type detection.
82 | func NewDecoder(r io.ReadSeeker) *Decoder {
83 | return &Decoder{Format: InvalidFormat, reader: r, lax: false}
84 | }
85 |
86 | // Unmarshal parses a property list document and stores the result in the value pointed to by v.
87 | //
88 | // Unmarshal uses the inverse of the type encodings that Marshal uses, allocating heap-borne types as necessary.
89 | //
90 | // When given a nil pointer, Unmarshal allocates a new value for it to point to.
91 | //
92 | // To decode property list values into an interface value, Unmarshal decodes the property list into the concrete value contained
93 | // in the interface value. If the interface value is nil, Unmarshal stores one of the following in the interface value:
94 | //
95 | // string, bool, uint64, float64
96 | // plist.UID for "CoreFoundation Keyed Archiver UIDs" (convertible to uint64)
97 | // []byte, for plist data
98 | // []interface{}, for plist arrays
99 | // map[string]interface{}, for plist dictionaries
100 | //
101 | // If a property list value is not appropriate for a given value type, Unmarshal aborts immediately and returns an error.
102 | //
103 | // As Go does not support 128-bit types, and we don't want to pretend we're giving the user integer types (as opposed to
104 | // secretly passing them structs), Unmarshal will drop the high 64 bits of any 128-bit integers encoded in binary property lists.
105 | // (This is important because CoreFoundation serializes some large 64-bit values as 128-bit values with an empty high half.)
106 | //
107 | // When Unmarshal encounters an OpenStep property list, it will enter a relaxed parsing mode: OpenStep property lists can only store
108 | // plain old data as strings, so we will attempt to recover integer, floating-point, boolean and date values wherever they are necessary.
109 | // (for example, if Unmarshal attempts to unmarshal an OpenStep property list into a time.Time, it will try to parse the string it
110 | // receives as a time.)
111 | //
112 | // Unmarshal returns the detected property list format and an error, if any.
113 | func Unmarshal(data []byte, v interface{}) (format int, err error) {
114 | r := bytes.NewReader(data)
115 | dec := NewDecoder(r)
116 | err = dec.Decode(v)
117 | format = dec.Format
118 | return
119 | }
120 |
--------------------------------------------------------------------------------
/go-plist/decode_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "reflect"
7 | "testing"
8 | )
9 |
10 | func BenchmarkXMLDecode(b *testing.B) {
11 | for i := 0; i < b.N; i++ {
12 | b.StopTimer()
13 | var bval interface{}
14 | buf := bytes.NewReader([]byte(plistValueTreeAsXML))
15 | b.StartTimer()
16 | decoder := NewDecoder(buf)
17 | decoder.Decode(bval)
18 | b.StopTimer()
19 | }
20 | }
21 |
22 | func BenchmarkBplistDecode(b *testing.B) {
23 | for i := 0; i < b.N; i++ {
24 | b.StopTimer()
25 | var bval interface{}
26 | buf := bytes.NewReader(plistValueTreeAsBplist)
27 | b.StartTimer()
28 | decoder := NewDecoder(buf)
29 | decoder.Decode(bval)
30 | b.StopTimer()
31 | }
32 | }
33 |
34 | func TestLaxDecode(t *testing.T) {
35 | var laxTestDataStringsOnlyAsXML = `{B=1;D="2013-11-27 00:34:00 +0000";I64=1;F64="3.0";U64=2;}`
36 | d := LaxTestData{}
37 | buf := bytes.NewReader([]byte(laxTestDataStringsOnlyAsXML))
38 | decoder := NewDecoder(buf)
39 | decoder.lax = true
40 | err := decoder.Decode(&d)
41 | if err != nil {
42 | t.Error(err.Error())
43 | }
44 |
45 | if d != laxTestData {
46 | t.Logf("Expected: %#v", laxTestData)
47 | t.Logf("Received: %#v", d)
48 | t.Fail()
49 | }
50 | }
51 |
52 | func TestIllegalLaxDecode(t *testing.T) {
53 | i := int64(0)
54 | u := uint64(0)
55 | f := float64(0)
56 | b := false
57 | plists := []struct {
58 | pl string
59 | d interface{}
60 | }{
61 | {"abc", &i},
62 | {"abc", &u},
63 | {"def", &f},
64 | {"ghi", &b},
65 | {"jkl", []byte{0x00}},
66 | }
67 |
68 | for _, plist := range plists {
69 | buf := bytes.NewReader([]byte(plist.pl))
70 | decoder := NewDecoder(buf)
71 | decoder.lax = true
72 | err := decoder.Decode(plist.d)
73 | t.Logf("Error: %v", err)
74 | if err == nil {
75 | t.Error("Expected error, received nothing.")
76 | }
77 | }
78 | }
79 |
80 | func TestIllegalDecode(t *testing.T) {
81 | i := int64(0)
82 | b := false
83 | plists := []struct {
84 | pl string
85 | d interface{}
86 | }{
87 | {"abc", &i},
88 | {"ABC=", &i},
89 | {"34.1", &i},
90 | {"def", &i},
91 | {"2010-01-01T00:00:00Z", &i},
92 | {"0", &b},
93 | {"0", &b},
94 | {"a0", &b},
95 | {"", &[1]int{1}},
96 | }
97 |
98 | for _, plist := range plists {
99 | buf := bytes.NewReader([]byte(plist.pl))
100 | decoder := NewDecoder(buf)
101 | err := decoder.Decode(plist.d)
102 | t.Logf("Error: %v", err)
103 | if err == nil {
104 | t.Error("Expected error, received nothing.")
105 | }
106 | }
107 | }
108 |
109 | func TestDecode(t *testing.T) {
110 | for _, test := range tests {
111 | subtest(t, test.Name, func(t *testing.T) {
112 | expVal := test.DecodeValue
113 | if expVal == nil {
114 | expVal = test.Value
115 | }
116 |
117 | expReflect := reflect.ValueOf(expVal)
118 | if !expReflect.IsValid() || isEmptyInterface(expReflect) {
119 | return
120 | }
121 | if expReflect.Kind() == reflect.Ptr || expReflect.Kind() == reflect.Interface {
122 | // Unbox pointer for comparison's sake
123 | expReflect = expReflect.Elem()
124 | }
125 | expVal = expReflect.Interface()
126 |
127 | results := make(map[int]interface{})
128 | for fmt, doc := range test.Documents {
129 | if test.SkipDecode[fmt] {
130 | return
131 | }
132 | subtest(t, FormatNames[fmt], func(t *testing.T) {
133 | val := reflect.New(expReflect.Type()).Interface()
134 | _, err := Unmarshal(doc, val)
135 | if err != nil {
136 | t.Error(err)
137 | }
138 |
139 | valReflect := reflect.ValueOf(val)
140 | if valReflect.Kind() == reflect.Ptr || valReflect.Kind() == reflect.Interface {
141 | // Unbox pointer for comparison's sake
142 | valReflect = valReflect.Elem()
143 | val = valReflect.Interface()
144 | }
145 |
146 | results[fmt] = val
147 | if !reflect.DeepEqual(expVal, val) {
148 | t.Logf("Expected: %#v\n", expVal)
149 | t.Logf("Received: %#v\n", val)
150 | t.Fail()
151 | }
152 | })
153 | }
154 |
155 | if results[BinaryFormat] != nil && results[XMLFormat] != nil {
156 | if !reflect.DeepEqual(results[BinaryFormat], results[XMLFormat]) {
157 | t.Log("Binary and XML decoding yielded different values.")
158 | t.Log("Binary:", results[BinaryFormat])
159 | t.Log("XML :", results[XMLFormat])
160 | t.Fail()
161 | }
162 | }
163 | })
164 | }
165 | }
166 |
167 | func TestInterfaceDecode(t *testing.T) {
168 | var xval interface{}
169 | buf := bytes.NewReader([]byte{98, 112, 108, 105, 115, 116, 48, 48, 214, 1, 13, 17, 21, 25, 27, 2, 14, 18, 22, 26, 28, 88, 105, 110, 116, 97, 114, 114, 97, 121, 170, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 1, 16, 8, 16, 16, 16, 32, 16, 64, 16, 2, 16, 9, 16, 17, 16, 33, 16, 65, 86, 102, 108, 111, 97, 116, 115, 162, 15, 16, 34, 66, 0, 0, 0, 35, 64, 80, 0, 0, 0, 0, 0, 0, 88, 98, 111, 111, 108, 101, 97, 110, 115, 162, 19, 20, 9, 8, 87, 115, 116, 114, 105, 110, 103, 115, 162, 23, 24, 92, 72, 101, 108, 108, 111, 44, 32, 65, 83, 67, 73, 73, 105, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 44, 0, 32, 78, 22, 117, 76, 84, 100, 97, 116, 97, 68, 1, 2, 3, 4, 84, 100, 97, 116, 101, 51, 65, 184, 69, 117, 120, 0, 0, 0, 8, 21, 30, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 68, 71, 76, 85, 94, 97, 98, 99, 107, 110, 123, 142, 147, 152, 157, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166})
170 | decoder := NewDecoder(buf)
171 | err := decoder.Decode(&xval)
172 | if err != nil {
173 | t.Log("Error:", err)
174 | t.Fail()
175 | }
176 | }
177 |
178 | func TestFormatDetection(t *testing.T) {
179 | type formatTest struct {
180 | expectedFormat int
181 | data []byte
182 | }
183 | plists := []formatTest{
184 | {BinaryFormat, []byte{98, 112, 108, 105, 115, 116, 48, 48, 85, 72, 101, 108, 108, 111, 8, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14}},
185 | {XMLFormat, []byte(`<*I3>`)},
186 | {InvalidFormat, []byte(`bplist00`)}, // Looks like a binary property list, and bplist does not have fallbacks(!)
187 | {OpenStepFormat, []byte(`(1,2,3,4,5)`)},
188 | {OpenStepFormat, []byte(``)},
189 | {GNUStepFormat, []byte(`(1,2,<*I3>)`)},
190 | {InvalidFormat, []byte{0x00}}, // This isn't a valid property list of any sort.
191 | }
192 |
193 | for i, fmttest := range plists {
194 | fmt, err := Unmarshal(fmttest.data, nil)
195 | if fmt != fmttest.expectedFormat {
196 | t.Errorf("plist %d: Wanted %s, received %s.", i, FormatNames[fmttest.expectedFormat], FormatNames[fmt])
197 | }
198 | if err != nil {
199 | t.Logf("plist %d: Error: %v", i, err)
200 | }
201 | }
202 | }
203 |
204 | func ExampleDecoder_Decode() {
205 | type sparseBundleHeader struct {
206 | InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
207 | BandSize uint64 `plist:"band-size"`
208 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
209 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
210 | Size uint64 `plist:"size"`
211 | }
212 |
213 | buf := bytes.NewReader([]byte(`
214 |
215 |
216 |
217 | CFBundleInfoDictionaryVersion
218 | 6.0
219 | band-size
220 | 8388608
221 | bundle-backingstore-version
222 | 1
223 | diskimage-bundle-type
224 | com.apple.diskimage.sparsebundle
225 | size
226 | 4398046511104
227 |
228 | `))
229 |
230 | var data sparseBundleHeader
231 | decoder := NewDecoder(buf)
232 | err := decoder.Decode(&data)
233 | if err != nil {
234 | fmt.Println(err)
235 | }
236 | fmt.Println(data)
237 |
238 | // Output: {6.0 8388608 1 com.apple.diskimage.sparsebundle 4398046511104}
239 | }
240 |
--------------------------------------------------------------------------------
/go-plist/doc.go:
--------------------------------------------------------------------------------
1 | // Package plist implements encoding and decoding of Apple's "property list" format.
2 | // Property lists come in three sorts: plain text (GNUStep and OpenStep), XML and binary.
3 | // plist supports all of them.
4 | // The mapping between property list and Go objects is described in the documentation for the Marshal and Unmarshal functions.
5 | package plist
6 |
--------------------------------------------------------------------------------
/go-plist/dump_test.go:
--------------------------------------------------------------------------------
1 | // +build dump
2 |
3 | // To dump a directory containing all the plist package test data, run
4 | // $ go test -tags dump
5 | //
6 | // To customize where the dumps are stored, set the env variable PLIST_DUMP_DIR.
7 |
8 | package plist
9 |
10 | import (
11 | "encoding/gob"
12 | "fmt"
13 | "io/ioutil"
14 | "os"
15 | "path/filepath"
16 | "strings"
17 | "testing"
18 | )
19 |
20 | var filenameReplacer = strings.NewReplacer(`<`, `_`, `>`, `_`, `:`, `_`, `"`, `_`, `/`, `_`, `\`, `_`, `|`, `_`, `?`, `_`, `*`, `_`)
21 |
22 | var extensions = map[int]string{
23 | BinaryFormat: ".binary.plist",
24 | XMLFormat: ".xml.plist",
25 | GNUStepFormat: ".gnustep.plist",
26 | OpenStepFormat: ".openstep.plist",
27 | }
28 |
29 | func sanitizeFilename(f string) string {
30 | return filenameReplacer.Replace(f)
31 | }
32 |
33 | func oneshotGob(v interface{}, path string) {
34 | f, _ := os.Create(path)
35 | defer f.Close()
36 | enc := gob.NewEncoder(f)
37 | enc.Encode(v)
38 | }
39 |
40 | func makeDirs(dirs ...string) error {
41 | for _, v := range dirs {
42 | err := os.MkdirAll(v, 0777)
43 | if err != nil {
44 | return err
45 | }
46 | }
47 | return nil
48 | }
49 |
50 | func touch(path string) {
51 | f, _ := os.Create(path)
52 | f.Close()
53 | }
54 |
55 | func TestDump(t *testing.T) {
56 | dir := os.Getenv("PLIST_DUMP_DIR")
57 | if dir == "" {
58 | dir = "dump"
59 | }
60 |
61 | dir, err := filepath.Abs(dir)
62 | if err != nil {
63 | t.Fatal(err)
64 | }
65 |
66 | documentDir := filepath.Join(dir, "golden")
67 | encodeDir := filepath.Join(dir, "encode_from")
68 | decodeDir := filepath.Join(dir, "decode_as")
69 | invalidDir := filepath.Join(dir, "invalid")
70 | err = makeDirs(dir, documentDir, encodeDir, decodeDir, invalidDir)
71 | if err != nil {
72 | t.Fatal(err)
73 | }
74 |
75 | // Dump golden plists for known-valid tests and gobs for their encode/decode values
76 | for _, td := range tests {
77 | t.Log("Dumping", td.Name)
78 |
79 | saneName := sanitizeFilename(td.Name)
80 |
81 | encv := td.Value
82 | if encv != nil && len(td.SkipEncode) < len(extensions) {
83 | // If we have an "encode from" and we are intending to encode
84 | oneshotGob(encv, filepath.Join(encodeDir, saneName+".gob"))
85 | }
86 |
87 | decv := td.DecodeValue
88 | if decv != nil && len(td.SkipDecode) < len(extensions) {
89 | // If we have an "expected to decode as" and we are intending to decode
90 | oneshotGob(decv, filepath.Join(decodeDir, saneName+".gob"))
91 | }
92 |
93 | for k, v := range td.Documents {
94 | extName := saneName + extensions[k]
95 | path := filepath.Join(documentDir, extName)
96 | _ = ioutil.WriteFile(path, v, 0666)
97 | if td.SkipEncode[k] {
98 | touch(path + ".decode_only")
99 | }
100 | if td.SkipDecode[k] {
101 | touch(path + ".encode_only")
102 | }
103 | }
104 | }
105 |
106 | // Dump invalid text plists
107 | for _, td := range InvalidTextPlists {
108 | saneName := sanitizeFilename(td.Name)
109 | ext := extensions[OpenStepFormat]
110 | if strings.Contains(td.Name, "GNUStep") {
111 | ext = extensions[GNUStepFormat]
112 | }
113 |
114 | ioutil.WriteFile(filepath.Join(invalidDir, saneName+ext), []byte(td.Data), 0666)
115 | }
116 |
117 | // Dump invalid XML plists (We don't have any right now.)
118 | for i, v := range InvalidXMLPlists {
119 | ioutil.WriteFile(filepath.Join(invalidDir, fmt.Sprintf("invalid-x-%2.02d", i)+extensions[XMLFormat]), []byte(v), 0666)
120 | }
121 |
122 | // Dump invalid binary plists
123 | for i, v := range InvalidBplists {
124 | ioutil.WriteFile(filepath.Join(invalidDir, fmt.Sprintf("invalid-b-%2.02d", i)+extensions[BinaryFormat]), v, 0666)
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/go-plist/encode.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "io"
7 | "reflect"
8 | "runtime"
9 | )
10 |
11 | type generator interface {
12 | generateDocument(cfValue)
13 | Indent(string)
14 | }
15 |
16 | // An Encoder writes a property list to an output stream.
17 | type Encoder struct {
18 | writer io.Writer
19 | format int
20 |
21 | indent string
22 | }
23 |
24 | // Encode writes the property list encoding of v to the stream.
25 | func (p *Encoder) Encode(v interface{}) (err error) {
26 | defer func() {
27 | if r := recover(); r != nil {
28 | if _, ok := r.(runtime.Error); ok {
29 | panic(r)
30 | }
31 | err = r.(error)
32 | }
33 | }()
34 |
35 | pval := p.marshal(reflect.ValueOf(v))
36 | if pval == nil {
37 | panic(errors.New("plist: no root element to encode"))
38 | }
39 |
40 | var g generator
41 | switch p.format {
42 | case XMLFormat:
43 | g = newXMLPlistGenerator(p.writer)
44 | case BinaryFormat, AutomaticFormat:
45 | g = newBplistGenerator(p.writer)
46 | case OpenStepFormat, GNUStepFormat:
47 | g = newTextPlistGenerator(p.writer, p.format)
48 | }
49 | g.Indent(p.indent)
50 | g.generateDocument(pval)
51 | return
52 | }
53 |
54 | // Indent turns on pretty-printing for the XML and Text property list formats.
55 | // Each element begins on a new line and is preceded by one or more copies of indent according to its nesting depth.
56 | func (p *Encoder) Indent(indent string) {
57 | p.indent = indent
58 | }
59 |
60 | // NewEncoder returns an Encoder that writes an XML property list to w.
61 | func NewEncoder(w io.Writer) *Encoder {
62 | return NewEncoderForFormat(w, XMLFormat)
63 | }
64 |
65 | // NewEncoderForFormat returns an Encoder that writes a property list to w in the specified format.
66 | // Pass AutomaticFormat to allow the library to choose the best encoding (currently BinaryFormat).
67 | func NewEncoderForFormat(w io.Writer, format int) *Encoder {
68 | return &Encoder{
69 | writer: w,
70 | format: format,
71 | }
72 | }
73 |
74 | // NewBinaryEncoder returns an Encoder that writes a binary property list to w.
75 | func NewBinaryEncoder(w io.Writer) *Encoder {
76 | return NewEncoderForFormat(w, BinaryFormat)
77 | }
78 |
79 | // Marshal returns the property list encoding of v in the specified format.
80 | //
81 | // Pass AutomaticFormat to allow the library to choose the best encoding (currently BinaryFormat).
82 | //
83 | // Marshal traverses the value v recursively.
84 | // Any nil values encountered, other than the root, will be silently discarded as
85 | // the property list format bears no representation for nil values.
86 | //
87 | // Strings, integers of varying size, floats and booleans are encoded unchanged.
88 | // Strings bearing non-ASCII runes will be encoded differently depending upon the property list format:
89 | // UTF-8 for XML property lists and UTF-16 for binary property lists.
90 | //
91 | // Slice and Array values are encoded as property list arrays, except for
92 | // []byte values, which are encoded as data.
93 | //
94 | // Map values encode as dictionaries. The map's key type must be string; there is no provision for encoding non-string dictionary keys.
95 | //
96 | // Struct values are encoded as dictionaries, with only exported fields being serialized. Struct field encoding may be influenced with the use of tags.
97 | // The tag format is:
98 | //
99 | // `plist:"[,flags...]"`
100 | //
101 | // The following flags are supported:
102 | //
103 | // omitempty Only include the field if it is not set to the zero value for its type.
104 | //
105 | // If the key is "-", the field is ignored.
106 | //
107 | // Anonymous struct fields are encoded as if their exported fields were exposed via the outer struct.
108 | //
109 | // Pointer values encode as the value pointed to.
110 | //
111 | // Channel, complex and function values cannot be encoded. Any attempt to do so causes Marshal to return an error.
112 | func Marshal(v interface{}, format int) ([]byte, error) {
113 | return MarshalIndent(v, format, "")
114 | }
115 |
116 | // MarshalIndent works like Marshal, but each property list element
117 | // begins on a new line and is preceded by one or more copies of indent according to its nesting depth.
118 | func MarshalIndent(v interface{}, format int, indent string) ([]byte, error) {
119 | buf := &bytes.Buffer{}
120 | enc := NewEncoderForFormat(buf, format)
121 | enc.Indent(indent)
122 | if err := enc.Encode(v); err != nil {
123 | return nil, err
124 | }
125 | return buf.Bytes(), nil
126 | }
127 |
--------------------------------------------------------------------------------
/go-plist/encode_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "testing"
7 | )
8 |
9 | func BenchmarkXMLEncode(b *testing.B) {
10 | for i := 0; i < b.N; i++ {
11 | NewEncoder(&bytes.Buffer{}).Encode(plistValueTreeRawData)
12 | }
13 | }
14 |
15 | func BenchmarkBplistEncode(b *testing.B) {
16 | for i := 0; i < b.N; i++ {
17 | NewBinaryEncoder(&bytes.Buffer{}).Encode(plistValueTreeRawData)
18 | }
19 | }
20 |
21 | func BenchmarkOpenStepEncode(b *testing.B) {
22 | for i := 0; i < b.N; i++ {
23 | NewEncoderForFormat(&bytes.Buffer{}, OpenStepFormat).Encode(plistValueTreeRawData)
24 | }
25 | }
26 |
27 | func TestEncode(t *testing.T) {
28 | for _, test := range tests {
29 | subtest(t, test.Name, func(t *testing.T) {
30 | for fmt, doc := range test.Documents {
31 | if test.SkipEncode[fmt] {
32 | continue
33 | }
34 | subtest(t, FormatNames[fmt], func(t *testing.T) {
35 | encoded, err := Marshal(test.Value, fmt)
36 |
37 | if err != nil {
38 | t.Error(err)
39 | }
40 |
41 | if !bytes.Equal(doc, encoded) {
42 | printype := "%s"
43 | if fmt == BinaryFormat {
44 | printype = "%2x"
45 | }
46 | t.Logf("Value: %#v", test.Value)
47 | t.Logf("Expected: "+printype+"\n", doc)
48 | t.Logf("Received: "+printype+"\n", encoded)
49 | t.Fail()
50 | }
51 | })
52 | }
53 | })
54 | }
55 | }
56 |
57 | func ExampleEncoder_Encode() {
58 | type sparseBundleHeader struct {
59 | InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
60 | BandSize uint64 `plist:"band-size"`
61 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
62 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
63 | Size uint64 `plist:"size"`
64 | }
65 | data := &sparseBundleHeader{
66 | InfoDictionaryVersion: "6.0",
67 | BandSize: 8388608,
68 | Size: 4 * 1048576 * 1024 * 1024,
69 | DiskImageBundleType: "com.apple.diskimage.sparsebundle",
70 | BackingStoreVersion: 1,
71 | }
72 |
73 | buf := &bytes.Buffer{}
74 | encoder := NewEncoder(buf)
75 | err := encoder.Encode(data)
76 | if err != nil {
77 | fmt.Println(err)
78 | }
79 | fmt.Println(buf.String())
80 |
81 | // Output:
82 | //
83 | // CFBundleInfoDictionaryVersion6.0band-size8388608bundle-backingstore-version1diskimage-bundle-typecom.apple.diskimage.sparsebundlesize4398046511104
84 | }
85 |
86 | func ExampleMarshal_xml() {
87 | type sparseBundleHeader struct {
88 | InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
89 | BandSize uint64 `plist:"band-size"`
90 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
91 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
92 | Size uint64 `plist:"size"`
93 | }
94 | data := &sparseBundleHeader{
95 | InfoDictionaryVersion: "6.0",
96 | BandSize: 8388608,
97 | Size: 4 * 1048576 * 1024 * 1024,
98 | DiskImageBundleType: "com.apple.diskimage.sparsebundle",
99 | BackingStoreVersion: 1,
100 | }
101 |
102 | plist, err := MarshalIndent(data, XMLFormat, "\t")
103 | if err != nil {
104 | fmt.Println(err)
105 | }
106 | fmt.Println(string(plist))
107 |
108 | // Output:
109 | //
110 | //
111 | //
112 | // CFBundleInfoDictionaryVersion
113 | // 6.0
114 | // band-size
115 | // 8388608
116 | // bundle-backingstore-version
117 | // 1
118 | // diskimage-bundle-type
119 | // com.apple.diskimage.sparsebundle
120 | // size
121 | // 4398046511104
122 | //
123 | //
124 | }
125 |
126 | func ExampleMarshal_gnustep() {
127 | type sparseBundleHeader struct {
128 | InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
129 | BandSize uint64 `plist:"band-size"`
130 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
131 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
132 | Size uint64 `plist:"size"`
133 | }
134 | data := &sparseBundleHeader{
135 | InfoDictionaryVersion: "6.0",
136 | BandSize: 8388608,
137 | Size: 4 * 1048576 * 1024 * 1024,
138 | DiskImageBundleType: "com.apple.diskimage.sparsebundle",
139 | BackingStoreVersion: 1,
140 | }
141 |
142 | plist, err := MarshalIndent(data, GNUStepFormat, "\t")
143 | if err != nil {
144 | fmt.Println(err)
145 | }
146 | fmt.Println(string(plist))
147 |
148 | // Output: {
149 | // CFBundleInfoDictionaryVersion = 6.0;
150 | // band-size = <*I8388608>;
151 | // bundle-backingstore-version = <*I1>;
152 | // diskimage-bundle-type = com.apple.diskimage.sparsebundle;
153 | // size = <*I4398046511104>;
154 | // }
155 | }
156 |
--------------------------------------------------------------------------------
/go-plist/example_custom_marshaler_test.go:
--------------------------------------------------------------------------------
1 | package plist_test
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 |
7 | "howett.net/plist"
8 | )
9 |
10 | type Base64String string
11 |
12 | func (e Base64String) MarshalPlist() (interface{}, error) {
13 | return base64.StdEncoding.EncodeToString([]byte(e)), nil
14 | }
15 |
16 | func (e *Base64String) UnmarshalPlist(unmarshal func(interface{}) error) error {
17 | var b64 string
18 | if err := unmarshal(&b64); err != nil {
19 | return err
20 | }
21 |
22 | bytes, err := base64.StdEncoding.DecodeString(b64)
23 | if err != nil {
24 | return err
25 | }
26 |
27 | *e = Base64String(bytes)
28 | return nil
29 | }
30 |
31 | func Example() {
32 | s := Base64String("Dustin")
33 |
34 | data, err := plist.Marshal(&s, plist.OpenStepFormat)
35 | if err != nil {
36 | panic(err)
37 | }
38 |
39 | fmt.Println("Property List:", string(data))
40 |
41 | var decoded Base64String
42 | _, err = plist.Unmarshal(data, &decoded)
43 | if err != nil {
44 | panic(err)
45 | }
46 |
47 | fmt.Println("Raw Data:", string(decoded))
48 |
49 | // Output:
50 | // Property List: RHVzdGlu
51 | // Raw Data: Dustin
52 | }
53 |
--------------------------------------------------------------------------------
/go-plist/fuzz.go:
--------------------------------------------------------------------------------
1 | // +build gofuzz
2 |
3 | package plist
4 |
5 | import (
6 | "bytes"
7 | )
8 |
9 | func Fuzz(data []byte) int {
10 | buf := bytes.NewReader(data)
11 |
12 | var obj interface{}
13 | if err := NewDecoder(buf).Decode(&obj); err != nil {
14 | return 0
15 | }
16 | return 1
17 | }
18 |
--------------------------------------------------------------------------------
/go-plist/go16_test.go:
--------------------------------------------------------------------------------
1 | // +build !go1.7
2 |
3 | package plist
4 |
5 | import "testing"
6 |
7 | func subtest(t *testing.T, name string, f func(t *testing.T)) {
8 | // Subtests don't exist for Go <1.7, and we can't create our own testing.T to substitute in
9 | // for f's argument.
10 | f(t)
11 | }
12 |
--------------------------------------------------------------------------------
/go-plist/go17_test.go:
--------------------------------------------------------------------------------
1 | // +build go1.7
2 |
3 | package plist
4 |
5 | import "testing"
6 |
7 | func subtest(t *testing.T, name string, f func(t *testing.T)) {
8 | t.Run(name, f)
9 | }
10 |
--------------------------------------------------------------------------------
/go-plist/internal/cmd/tabler/tabler.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | )
7 |
8 | var usage = `Usage: tabler
9 |
10 | Produces a text_tables.go-compatible character table with the given
11 | variable name.`
12 |
13 | func main() {
14 | if len(os.Args) != 3 {
15 | fmt.Fprintln(os.Stderr, usage)
16 | os.Exit(1)
17 | }
18 |
19 | nam := os.Args[1]
20 | arg := os.Args[2]
21 | var vals [4]uint64
22 | for _, v := range arg {
23 | bucket := uint(v) / 64
24 | pos := uint(v) % 64
25 | vals[bucket] = vals[bucket] | (1 << pos)
26 | }
27 | fmt.Printf("var %s = characterSet{\n", nam)
28 | for _, v := range vals {
29 | fmt.Printf("\t0x%16.016x,\n", v)
30 | }
31 | fmt.Printf("}\n")
32 | }
33 |
--------------------------------------------------------------------------------
/go-plist/invalid_bplist_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | /*
9 | []byte{
10 | 'b', 'p', 'l', 'i', 's', 't', '0', '0', // Magic
11 |
12 | // Object Table
13 | // Offset Table
14 |
15 | // Trailer
16 | 0x00, 0x00, 0x00, 0x00, 0x00, // - U8[5] Unused
17 | 0x01, // - U8 Sort Version
18 | 0x01, // - U8 Offset Table Entry Size (#bytes)
19 | 0x01, // - U8 Object Reference Size (#bytes)
20 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // - U64 # Objects
21 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // - U64 Top Object
22 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // - U64 Offset Table Offset
23 | },
24 | */
25 |
26 | var InvalidBplists = [][]byte{
27 | // Too short
28 | []byte{
29 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
30 | 0x00,
31 | },
32 | // Bad magic
33 | []byte{
34 | 'x', 'p', 'l', 'i', 's', 't', '0', '0',
35 |
36 | 0x00,
37 | 0x08,
38 |
39 | 0x00, 0x00, 0x00, 0x00, 0x00,
40 | 0x00,
41 | 0x01,
42 | 0x01,
43 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
44 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
45 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
46 | },
47 | // Bad version
48 | []byte{
49 | 'b', 'p', 'l', 'i', 's', 't', '3', '0',
50 |
51 | 0x00,
52 | 0x08,
53 |
54 | 0x00, 0x00, 0x00, 0x00, 0x00,
55 | 0x00,
56 | 0x01,
57 | 0x01,
58 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
59 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
60 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
61 | },
62 | // Bad version II
63 | []byte{
64 | 'b', 'p', 'l', 'i', 's', 't', '@', 'A',
65 |
66 | 0x00,
67 | 0x08,
68 |
69 | 0x00, 0x00, 0x00, 0x00, 0x00,
70 | 0x00,
71 | 0x01,
72 | 0x01,
73 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
74 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
75 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
76 | },
77 | // Offset table inside trailer
78 | []byte{
79 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
80 |
81 | 0x00, 0x00, 0x00, 0x00, 0x00,
82 | 0x00,
83 | 0x01,
84 | 0x01,
85 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
86 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
87 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A,
88 | },
89 | // Offset table inside header
90 | []byte{
91 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
92 |
93 | 0x00, 0x00, 0x00, 0x00, 0x00,
94 | 0x00,
95 | 0x01,
96 | 0x01,
97 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
98 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
99 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
100 | },
101 | // Offset table off end of file
102 | []byte{
103 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
104 |
105 | 0x00, 0x00, 0x00, 0x00, 0x00,
106 | 0x00,
107 | 0x01,
108 | 0x01,
109 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
110 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
111 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00,
112 | },
113 | // Garbage between offset table and trailer
114 | []byte{
115 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
116 |
117 | 0x00,
118 | 0x09,
119 |
120 | 0xAB, 0xCD,
121 |
122 | 0x00, 0x00, 0x00, 0x00, 0x00,
123 | 0x00,
124 | 0x01,
125 | 0x01,
126 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
127 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
128 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A,
129 | },
130 | // Top Object out of range
131 | []byte{
132 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
133 |
134 | 0x00,
135 | 0x08,
136 |
137 | 0x00, 0x00, 0x00, 0x00, 0x00,
138 | 0x00,
139 | 0x01,
140 | 0x01,
141 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
142 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
143 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
144 | },
145 | // Object out of range
146 | []byte{
147 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
148 |
149 | 0x00,
150 | 0xFF,
151 |
152 | 0x00, 0x00, 0x00, 0x00, 0x00,
153 | 0x00,
154 | 0x01,
155 | 0x01,
156 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
157 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
158 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
159 | },
160 | // Object references too small (1 byte, but 257 objects)
161 | []byte{
162 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
163 |
164 | 0x00,
165 |
166 | // 257 bytes worth of object table
167 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
168 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
169 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
170 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
171 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
172 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
173 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
174 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
175 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
176 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
177 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
178 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
179 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
180 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
181 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
182 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
183 | 0x00,
184 |
185 | 0x00, 0x00, 0x00, 0x00, 0x00,
186 | 0x00,
187 | 0x01,
188 | 0x01,
189 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01,
190 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
191 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
192 | },
193 | // Offset references too small (1 byte, but 257 bytes worth of objects)
194 | []byte{
195 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
196 |
197 | // 257 bytes worth of "objects"
198 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
199 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
200 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
201 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
202 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
203 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
204 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
205 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
206 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
207 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
208 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
209 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
210 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
211 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
212 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
213 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
214 | 0x00,
215 |
216 | 0x00,
217 |
218 | 0x00, 0x00, 0x00, 0x00, 0x00,
219 | 0x00,
220 | 0x01,
221 | 0x01,
222 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
223 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
224 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x09,
225 | },
226 | // Too many objects
227 | []byte{
228 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
229 |
230 | 0x00,
231 | 0x08,
232 |
233 | 0x00, 0x00, 0x00, 0x00, 0x00,
234 | 0x00,
235 | 0x01,
236 | 0x01,
237 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
238 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
239 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
240 | },
241 | // String way too long
242 | []byte{
243 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
244 |
245 | 0x5F, 0x10, 0xFF,
246 | 0x08,
247 |
248 | 0x00, 0x00, 0x00, 0x00, 0x00,
249 | 0x00,
250 | 0x01,
251 | 0x01,
252 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
253 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
254 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B,
255 | },
256 | // UTF-16 String way too long
257 | []byte{
258 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
259 |
260 | 0x6F, 0x10, 0xFF,
261 | 0x08,
262 |
263 | 0x00, 0x00, 0x00, 0x00, 0x00,
264 | 0x00,
265 | 0x01,
266 | 0x01,
267 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
268 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
269 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B,
270 | },
271 | // Data way too long
272 | []byte{
273 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
274 |
275 | 0x4F, 0x10, 0xFF,
276 | 0x08,
277 |
278 | 0x00, 0x00, 0x00, 0x00, 0x00,
279 | 0x00,
280 | 0x01,
281 | 0x01,
282 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
283 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
284 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B,
285 | },
286 | // Array way too long
287 | []byte{
288 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
289 |
290 | 0xAF, 0x10, 0xFF,
291 | 0x08,
292 |
293 | 0x00, 0x00, 0x00, 0x00, 0x00,
294 | 0x00,
295 | 0x01,
296 | 0x01,
297 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
298 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
299 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B,
300 | },
301 | // Dictionary way too long
302 | []byte{
303 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
304 |
305 | 0xDF, 0x10, 0xFF,
306 | 0x08,
307 |
308 | 0x00, 0x00, 0x00, 0x00, 0x00,
309 | 0x00,
310 | 0x01,
311 | 0x01,
312 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
313 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
314 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B,
315 | },
316 | // Array self-referential
317 | []byte{
318 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
319 |
320 | 0xA1, 0x00,
321 |
322 | 0x08,
323 |
324 | 0x00, 0x00, 0x00, 0x00, 0x00,
325 | 0x00,
326 | 0x01,
327 | 0x01,
328 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
329 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
330 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A,
331 | },
332 | // Dictionary self-referential key
333 | []byte{
334 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
335 |
336 | 0xD1, 0x00, 0x01,
337 | 0x50, // 0-byte string
338 |
339 | 0x08, 0x0B,
340 |
341 | 0x00, 0x00, 0x00, 0x00, 0x00,
342 | 0x00,
343 | 0x01,
344 | 0x01,
345 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
346 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
347 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C,
348 | },
349 | // Dictionary self-referential value
350 | []byte{
351 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
352 |
353 | 0xD1, 0x01, 0x00,
354 | 0x50, // 0-byte string
355 |
356 | 0x08, 0x0B,
357 |
358 | 0x00, 0x00, 0x00, 0x00, 0x00,
359 | 0x00,
360 | 0x01,
361 | 0x01,
362 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
363 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
364 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C,
365 | },
366 | // Dictionary non-string key
367 | []byte{
368 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
369 |
370 | 0xD1, 0x01, 0x02,
371 | 0x08,
372 | 0x09,
373 |
374 | 0x08, 0x0B, 0x0C,
375 |
376 | 0x00, 0x00, 0x00, 0x00, 0x00,
377 | 0x00,
378 | 0x01,
379 | 0x01,
380 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
381 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
382 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D,
383 | },
384 | // Array contains invalid reference
385 | []byte{
386 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
387 |
388 | 0xA1, 0x0F,
389 |
390 | 0x08,
391 |
392 | 0x00, 0x00, 0x00, 0x00, 0x00,
393 | 0x00,
394 | 0x01,
395 | 0x01,
396 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
397 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
398 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A,
399 | },
400 | // Dictionary contains invalid reference
401 | []byte{
402 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
403 |
404 | 0xD1, 0x01, 0x0F,
405 | 0x50, // 0-byte string
406 |
407 | 0x08, 0x0B,
408 |
409 | 0x00, 0x00, 0x00, 0x00, 0x00,
410 | 0x00,
411 | 0x01,
412 | 0x01,
413 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
414 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
415 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C,
416 | },
417 | // Invalid float ("7-byte")
418 | []byte{
419 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
420 |
421 | 0x27,
422 |
423 | 0x08,
424 |
425 | 0x00, 0x00, 0x00, 0x00, 0x00,
426 | 0x00,
427 | 0x01,
428 | 0x01,
429 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
430 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
431 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
432 | },
433 | // Invalid integer (8^5)
434 | []byte{
435 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
436 |
437 | 0x15,
438 |
439 | 0x08,
440 |
441 | 0x00, 0x00, 0x00, 0x00, 0x00,
442 | 0x00,
443 | 0x01,
444 | 0x01,
445 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
446 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
447 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
448 | },
449 | // Invalid atom
450 | []byte{
451 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
452 |
453 | 0xFF,
454 |
455 | 0x08,
456 |
457 | 0x00, 0x00, 0x00, 0x00, 0x00,
458 | 0x00,
459 | 0x01,
460 | 0x01,
461 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
462 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
463 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
464 | },
465 |
466 | // array refers to self through a second level
467 | []byte{
468 | 'b', 'p', 'l', 'i', 's', 't', '0', '0',
469 |
470 | 0xA1, 0x01,
471 | 0xA1, 0x00,
472 |
473 | 0x08, 0x0A,
474 |
475 | 0x00, 0x00, 0x00, 0x00, 0x00,
476 | 0x00,
477 | 0x01,
478 | 0x01,
479 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
480 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
481 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C,
482 | },
483 | }
484 |
485 | func TestInvalidBinaryPlists(t *testing.T) {
486 | for _, data := range InvalidBplists {
487 | buf := bytes.NewReader(data)
488 | d := newBplistParser(buf)
489 | _, err := d.parseDocument()
490 | if err == nil {
491 | t.Fatal("invalid plist failed to throw error")
492 | } else {
493 | t.Log(err)
494 | }
495 | }
496 | }
497 |
--------------------------------------------------------------------------------
/go-plist/invalid_text_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "strings"
5 | "testing"
6 | )
7 |
8 | var InvalidTextPlists = []struct {
9 | Name string
10 | Data string
11 | }{
12 | {"Truncated array", "("},
13 | {"Truncated dictionary", "{a=b;"},
14 | {"Truncated dictionary 2", "{"},
15 | {"Unclosed nested array", "{0=(/"},
16 | {"Unclosed dictionary", "{0=/"},
17 | {"Broken GNUStep data", "(<*I5>,<*I5>,<*I5>,<*I5>,*I16777215>,<*I268435455>,<*I4294967295>,<*I18446744073709551615>,)"},
18 | {"Truncated nested array", "{0=(((/"},
19 | {"Truncated dictionary with comment-like", "{/"},
20 | {"Truncated array with comment-like", "(/"},
21 | {"Truncated array with empty data", "(<>"},
22 | {"Bad Extended Character", "{¬=A;}"},
23 | {"Missing Equals in Dictionary", `{"A"A;}`},
24 | {"Missing Semicolon in Dictionary", `{"A"=A}`},
25 | {"Invalid GNUStep type", "<*F33>"},
26 | {"Invalid GNUStep int", "(<*I>"},
27 | {"Invalid GNUStep date", "<*D5>"},
28 | {"Truncated GNUStep value", "<*I3"},
29 | {"Invalid data", ""},
30 | {"Truncated unicode escape", `"\u231`},
31 | {"Truncated hex escape", `"\x2`},
32 | {"Truncated octal escape", `"\02`},
33 | {"Truncated data", `<33`},
34 | {"Uneven data", `<3>`},
35 | {"Truncated block comment", `/* hello`},
36 | {"Truncated quoted string", `"hi`},
37 | {"Garbage after end of non-string", " cde"},
38 | {"Broken UTF-16", "\xFE\xFF\x01"},
39 | {"Truncated GNUStep data", "<"},
40 | {"Truncated GNUStep base64 data (missing ])", `<[33==`},
41 | {"Truncated GNUStep base64 data (missing >)", `<[33==]`},
42 | {"Invalid GNUStep base64 data", `<[3]>`}, // TODO: this is actually valid
43 | {"GNUStep extended value with EOF before type", "<*"},
44 | {"GNUStep extended value terminated before type", "<*>"},
45 | {"Empty GNUStep extended value", "<*I>"},
46 | {"Unterminated GNUStep quoted value", "<*D\"5>"},
47 | {"Unterminated GNUStep quoted value (EOF)", "<*D\""},
48 | {"Poorly-terminated GNUStep quoted value", "<*D\">"},
49 | {"Empty GNUStep quoted extended value", "<*D\"\">"},
50 | }
51 |
52 | func TestInvalidTextPlists(t *testing.T) {
53 | for _, test := range InvalidTextPlists {
54 | subtest(t, test.Name, func(t *testing.T) {
55 | var obj interface{}
56 | buf := strings.NewReader(test.Data)
57 | err := NewDecoder(buf).Decode(&obj)
58 | if err == nil {
59 | t.Fatal("invalid plist failed to throw error")
60 | } else {
61 | t.Log(err)
62 | }
63 | })
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/go-plist/marshal.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "encoding"
5 | "reflect"
6 | "time"
7 | )
8 |
9 | func isEmptyValue(v reflect.Value) bool {
10 | switch v.Kind() {
11 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
12 | return v.Len() == 0
13 | case reflect.Bool:
14 | return !v.Bool()
15 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
16 | return v.Int() == 0
17 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
18 | return v.Uint() == 0
19 | case reflect.Float32, reflect.Float64:
20 | return v.Float() == 0
21 | case reflect.Interface, reflect.Ptr:
22 | return v.IsNil()
23 | }
24 | return false
25 | }
26 |
27 | var (
28 | plistMarshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem()
29 | textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
30 | timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
31 | )
32 |
33 | func implementsInterface(val reflect.Value, interfaceType reflect.Type) (interface{}, bool) {
34 | if val.CanInterface() && val.Type().Implements(interfaceType) {
35 | return val.Interface(), true
36 | }
37 |
38 | if val.CanAddr() {
39 | pv := val.Addr()
40 | if pv.CanInterface() && pv.Type().Implements(interfaceType) {
41 | return pv.Interface(), true
42 | }
43 | }
44 | return nil, false
45 | }
46 |
47 | func (p *Encoder) marshalPlistInterface(marshalable Marshaler) cfValue {
48 | value, err := marshalable.MarshalPlist()
49 | if err != nil {
50 | panic(err)
51 | }
52 | return p.marshal(reflect.ValueOf(value))
53 | }
54 |
55 | // marshalTextInterface marshals a TextMarshaler to a plist string.
56 | func (p *Encoder) marshalTextInterface(marshalable encoding.TextMarshaler) cfValue {
57 | s, err := marshalable.MarshalText()
58 | if err != nil {
59 | panic(err)
60 | }
61 | return cfString(s)
62 | }
63 |
64 | // marshalStruct marshals a reflected struct value to a plist dictionary
65 | func (p *Encoder) marshalStruct(typ reflect.Type, val reflect.Value) cfValue {
66 | tinfo, _ := getTypeInfo(typ)
67 |
68 | dict := &cfDictionary{
69 | keys: make([]string, 0, len(tinfo.fields)),
70 | values: make([]cfValue, 0, len(tinfo.fields)),
71 | }
72 | for _, finfo := range tinfo.fields {
73 | value := finfo.value(val)
74 | if !value.IsValid() || finfo.omitEmpty && isEmptyValue(value) {
75 | continue
76 | }
77 | dict.keys = append(dict.keys, finfo.name)
78 | dict.values = append(dict.values, p.marshal(value))
79 | }
80 |
81 | return dict
82 | }
83 |
84 | func (p *Encoder) marshalTime(val reflect.Value) cfValue {
85 | time := val.Interface().(time.Time)
86 | return cfDate(time)
87 | }
88 |
89 | func (p *Encoder) marshal(val reflect.Value) cfValue {
90 | if !val.IsValid() {
91 | return nil
92 | }
93 |
94 | if receiver, can := implementsInterface(val, plistMarshalerType); can {
95 | return p.marshalPlistInterface(receiver.(Marshaler))
96 | }
97 |
98 | // time.Time implements TextMarshaler, but we need to store it in RFC3339
99 | if val.Type() == timeType {
100 | return p.marshalTime(val)
101 | }
102 | if val.Kind() == reflect.Ptr || (val.Kind() == reflect.Interface && val.NumMethod() == 0) {
103 | ival := val.Elem()
104 | if ival.IsValid() && ival.Type() == timeType {
105 | return p.marshalTime(ival)
106 | }
107 | }
108 |
109 | // Check for text marshaler.
110 | if receiver, can := implementsInterface(val, textMarshalerType); can {
111 | return p.marshalTextInterface(receiver.(encoding.TextMarshaler))
112 | }
113 |
114 | // Descend into pointers or interfaces
115 | if val.Kind() == reflect.Ptr || (val.Kind() == reflect.Interface && val.NumMethod() == 0) {
116 | val = val.Elem()
117 | }
118 |
119 | // We got this far and still may have an invalid anything or nil ptr/interface
120 | if !val.IsValid() || ((val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface) && val.IsNil()) {
121 | return nil
122 | }
123 |
124 | typ := val.Type()
125 |
126 | if typ == uidType {
127 | return cfUID(val.Uint())
128 | }
129 |
130 | if val.Kind() == reflect.Struct {
131 | return p.marshalStruct(typ, val)
132 | }
133 |
134 | switch val.Kind() {
135 | case reflect.String:
136 | return cfString(val.String())
137 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
138 | return &cfNumber{signed: true, value: uint64(val.Int())}
139 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
140 | return &cfNumber{signed: false, value: val.Uint()}
141 | case reflect.Float32:
142 | return &cfReal{wide: false, value: val.Float()}
143 | case reflect.Float64:
144 | return &cfReal{wide: true, value: val.Float()}
145 | case reflect.Bool:
146 | return cfBoolean(val.Bool())
147 | case reflect.Slice, reflect.Array:
148 | if typ.Elem().Kind() == reflect.Uint8 {
149 | bytes := []byte(nil)
150 | if val.CanAddr() {
151 | bytes = val.Bytes()
152 | } else {
153 | bytes = make([]byte, val.Len())
154 | reflect.Copy(reflect.ValueOf(bytes), val)
155 | }
156 | return cfData(bytes)
157 | } else {
158 | values := make([]cfValue, val.Len())
159 | for i, length := 0, val.Len(); i < length; i++ {
160 | if subpval := p.marshal(val.Index(i)); subpval != nil {
161 | values[i] = subpval
162 | }
163 | }
164 | return &cfArray{values}
165 | }
166 | case reflect.Map:
167 | if typ.Key().Kind() != reflect.String {
168 | panic(&unknownTypeError{typ})
169 | }
170 |
171 | l := val.Len()
172 | dict := &cfDictionary{
173 | keys: make([]string, 0, l),
174 | values: make([]cfValue, 0, l),
175 | }
176 | for _, keyv := range val.MapKeys() {
177 | if subpval := p.marshal(val.MapIndex(keyv)); subpval != nil {
178 | dict.keys = append(dict.keys, keyv.String())
179 | dict.values = append(dict.values, subpval)
180 | }
181 | }
182 | return dict
183 | default:
184 | panic(&unknownTypeError{typ})
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/go-plist/marshal_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func BenchmarkStructMarshal(b *testing.B) {
10 | for i := 0; i < b.N; i++ {
11 | e := &Encoder{}
12 | e.marshal(reflect.ValueOf(plistValueTreeRawData))
13 | }
14 | }
15 |
16 | func BenchmarkMapMarshal(b *testing.B) {
17 | data := map[string]interface{}{
18 | "intarray": []interface{}{
19 | int(1),
20 | int8(8),
21 | int16(16),
22 | int32(32),
23 | int64(64),
24 | uint(2),
25 | uint8(9),
26 | uint16(17),
27 | uint32(33),
28 | uint64(65),
29 | },
30 | "floats": []interface{}{
31 | float32(32.0),
32 | float64(64.0),
33 | },
34 | "booleans": []bool{
35 | true,
36 | false,
37 | },
38 | "strings": []string{
39 | "Hello, ASCII",
40 | "Hello, 世界",
41 | },
42 | "data": []byte{1, 2, 3, 4},
43 | "date": time.Date(2013, 11, 27, 0, 34, 0, 0, time.UTC),
44 | }
45 | b.ResetTimer()
46 | for i := 0; i < b.N; i++ {
47 | e := &Encoder{}
48 | e.marshal(reflect.ValueOf(data))
49 | }
50 | }
51 |
52 | func TestInvalidMarshal(t *testing.T) {
53 | tests := []struct {
54 | Name string
55 | Thing interface{}
56 | }{
57 | {"Function", func() {}},
58 | {"Nil", nil},
59 | {"Map with integer keys", map[int]string{1: "hi"}},
60 | {"Channel", make(chan int)},
61 | }
62 |
63 | for _, v := range tests {
64 | subtest(t, v.Name, func(t *testing.T) {
65 | data, err := Marshal(v.Thing, OpenStepFormat)
66 | if err == nil {
67 | t.Fatalf("expected error; got plist data: %x", data)
68 | } else {
69 | t.Log(err)
70 | }
71 | })
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/go-plist/must.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "io"
5 | "strconv"
6 | )
7 |
8 | type mustWriter struct {
9 | io.Writer
10 | }
11 |
12 | func (w mustWriter) Write(p []byte) (int, error) {
13 | n, err := w.Writer.Write(p)
14 | if err != nil {
15 | panic(err)
16 | }
17 | return n, nil
18 | }
19 |
20 | func mustParseInt(str string, base, bits int) int64 {
21 | i, err := strconv.ParseInt(str, base, bits)
22 | if err != nil {
23 | panic(err)
24 | }
25 | return i
26 | }
27 |
28 | func mustParseUint(str string, base, bits int) uint64 {
29 | i, err := strconv.ParseUint(str, base, bits)
30 | if err != nil {
31 | panic(err)
32 | }
33 | return i
34 | }
35 |
36 | func mustParseFloat(str string, bits int) float64 {
37 | i, err := strconv.ParseFloat(str, bits)
38 | if err != nil {
39 | panic(err)
40 | }
41 | return i
42 | }
43 |
44 | func mustParseBool(str string) bool {
45 | i, err := strconv.ParseBool(str)
46 | if err != nil {
47 | panic(err)
48 | }
49 | return i
50 | }
51 |
--------------------------------------------------------------------------------
/go-plist/plist.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "reflect"
5 | )
6 |
7 | // Property list format constants
8 | const (
9 | // Used by Decoder to represent an invalid property list.
10 | InvalidFormat int = 0
11 |
12 | // Used to indicate total abandon with regards to Encoder's output format.
13 | AutomaticFormat = 0
14 |
15 | XMLFormat = 1
16 | BinaryFormat = 2
17 | OpenStepFormat = 3
18 | GNUStepFormat = 4
19 |
20 | // On MacOS there is a `defaults` command which outputs data
21 | // in the spirit of OpenStepFormat. But has an additional Byte Summary
22 | // TODO - is this necessary?
23 | DefaultsFormat = 5
24 | )
25 |
26 | var FormatNames = map[int]string{
27 | InvalidFormat: "unknown/invalid",
28 | XMLFormat: "XML",
29 | BinaryFormat: "Binary",
30 | OpenStepFormat: "OpenStep",
31 | GNUStepFormat: "GNUStep",
32 | DefaultsFormat: "Defaults(OpenStep)",
33 | }
34 |
35 | type unknownTypeError struct {
36 | typ reflect.Type
37 | }
38 |
39 | func (u *unknownTypeError) Error() string {
40 | return "plist: can't marshal value of type " + u.typ.String()
41 | }
42 |
43 | type invalidPlistError struct {
44 | format string
45 | err error
46 | }
47 |
48 | func (e invalidPlistError) Error() string {
49 | s := "plist: invalid " + e.format + " property list"
50 | if e.err != nil {
51 | s += ": " + e.err.Error()
52 | }
53 | return s
54 | }
55 |
56 | type plistParseError struct {
57 | format string
58 | err error
59 | }
60 |
61 | func (e plistParseError) Error() string {
62 | s := "plist: error parsing " + e.format + " property list"
63 | if e.err != nil {
64 | s += ": " + e.err.Error()
65 | }
66 | return s
67 | }
68 |
69 | // A UID represents a unique object identifier. UIDs are serialized in a manner distinct from
70 | // that of integers.
71 | type UID uint64
72 |
73 | // Marshaler is the interface implemented by types that can marshal themselves into valid
74 | // property list objects. The returned value is marshaled in place of the original value
75 | // implementing Marshaler
76 | //
77 | // If an error is returned by MarshalPlist, marshaling stops and the error is returned.
78 | type Marshaler interface {
79 | MarshalPlist() (interface{}, error)
80 | }
81 |
82 | // Unmarshaler is the interface implemented by types that can unmarshal themselves from
83 | // property list objects. The UnmarshalPlist method receives a function that may
84 | // be called to unmarshal the original property list value into a field or variable.
85 | //
86 | // It is safe to call the unmarshal function more than once.
87 | type Unmarshaler interface {
88 | UnmarshalPlist(unmarshal func(interface{}) error) error
89 | }
90 |
--------------------------------------------------------------------------------
/go-plist/plist_types.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "hash/crc32"
5 | "sort"
6 | "time"
7 | "strconv"
8 | )
9 |
10 | // magic value used in the non-binary encoding of UIDs
11 | // (stored as a dictionary mapping CF$UID->integer)
12 | const cfUIDMagic = "CF$UID"
13 |
14 | type cfValue interface {
15 | typeName() string
16 | hash() interface{}
17 | }
18 |
19 | type cfDictionary struct {
20 | keys sort.StringSlice
21 | values []cfValue
22 | }
23 |
24 | func (*cfDictionary) typeName() string {
25 | return "dictionary"
26 | }
27 |
28 | func (p *cfDictionary) hash() interface{} {
29 | return p
30 | }
31 |
32 | func (p *cfDictionary) Len() int {
33 | return len(p.keys)
34 | }
35 |
36 | func (p *cfDictionary) Less(i, j int) bool {
37 | return p.keys.Less(i, j)
38 | }
39 |
40 | func (p *cfDictionary) Swap(i, j int) {
41 | p.keys.Swap(i, j)
42 | p.values[i], p.values[j] = p.values[j], p.values[i]
43 | }
44 |
45 | func (p *cfDictionary) sort() {
46 | sort.Sort(p)
47 | }
48 |
49 | func (p *cfDictionary) maybeUID(lax bool) cfValue {
50 | if len(p.keys) == 1 && p.keys[0] == "CF$UID" && len(p.values) == 1 {
51 | pval := p.values[0]
52 | if integer, ok := pval.(*cfNumber); ok {
53 | return cfUID(integer.value)
54 | }
55 | // Openstep only has cfString. Act like the unmarshaller a bit.
56 | if lax {
57 | if str, ok := pval.(cfString); ok {
58 | if i, err := strconv.ParseUint(string(str), 10, 64); err == nil {
59 | return cfUID(i)
60 | }
61 | }
62 | }
63 | }
64 | return p
65 | }
66 |
67 | type cfArray struct {
68 | values []cfValue
69 | }
70 |
71 | func (*cfArray) typeName() string {
72 | return "array"
73 | }
74 |
75 | func (p *cfArray) hash() interface{} {
76 | return p
77 | }
78 |
79 | type cfString string
80 |
81 | func (cfString) typeName() string {
82 | return "string"
83 | }
84 |
85 | func (p cfString) hash() interface{} {
86 | return string(p)
87 | }
88 |
89 | type cfNumber struct {
90 | signed bool
91 | value uint64
92 | }
93 |
94 | func (*cfNumber) typeName() string {
95 | return "integer"
96 | }
97 |
98 | func (p *cfNumber) hash() interface{} {
99 | if p.signed {
100 | return int64(p.value)
101 | }
102 | return p.value
103 | }
104 |
105 | type cfReal struct {
106 | wide bool
107 | value float64
108 | }
109 |
110 | func (cfReal) typeName() string {
111 | return "real"
112 | }
113 |
114 | func (p *cfReal) hash() interface{} {
115 | if p.wide {
116 | return p.value
117 | }
118 | return float32(p.value)
119 | }
120 |
121 | type cfBoolean bool
122 |
123 | func (cfBoolean) typeName() string {
124 | return "boolean"
125 | }
126 |
127 | func (p cfBoolean) hash() interface{} {
128 | return bool(p)
129 | }
130 |
131 | type cfUID UID
132 |
133 | func (cfUID) typeName() string {
134 | return "UID"
135 | }
136 |
137 | func (p cfUID) hash() interface{} {
138 | return p
139 | }
140 |
141 | func (p cfUID) toDict() *cfDictionary {
142 | return &cfDictionary{
143 | keys: []string{cfUIDMagic},
144 | values: []cfValue{&cfNumber{
145 | signed: false,
146 | value: uint64(p),
147 | }},
148 | }
149 | }
150 |
151 | type cfData []byte
152 |
153 | func (cfData) typeName() string {
154 | return "data"
155 | }
156 |
157 | func (p cfData) hash() interface{} {
158 | // Data are uniqued by their checksums.
159 | // Todo: Look at calculating this only once and storing it somewhere;
160 | // crc32 is fairly quick, however.
161 | return crc32.ChecksumIEEE([]byte(p))
162 | }
163 |
164 | type cfDate time.Time
165 |
166 | func (cfDate) typeName() string {
167 | return "date"
168 | }
169 |
170 | func (p cfDate) hash() interface{} {
171 | return time.Time(p)
172 | }
173 |
--------------------------------------------------------------------------------
/go-plist/testdata/xml_unusual_cases/s01.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 | copyright
4 | ©
5 |
6 |
7 |
--------------------------------------------------------------------------------
/go-plist/testdata/xml_unusual_cases/s02.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 | name
4 | value
5 |
6 |
7 |
--------------------------------------------------------------------------------
/go-plist/testdata/xml_unusual_cases/s03.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | value
5 |
6 |
7 |
--------------------------------------------------------------------------------
/go-plist/testdata/xml_unusual_cases/s04.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | value
5 |
6 |
7 |
--------------------------------------------------------------------------------
/go-plist/testdata/xml_unusual_cases/s05.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 | test
4 | value
5 |
6 |
7 |
--------------------------------------------------------------------------------
/go-plist/testdata/xml_unusual_cases/s06.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 | test&
4 | value
5 |
6 |
7 |
--------------------------------------------------------------------------------
/go-plist/testdata/xml_unusual_cases/s07.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | test
5 | value
6 |
7 |
8 |
--------------------------------------------------------------------------------
/go-plist/testdata/xml_unusual_cases/s10.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 | test
4 | apple
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/go-plist/testdata/xml_unusual_cases/s11.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 | test
4 | libxml2
5 | test
6 | apple
7 | test
8 | libplist
9 |
10 |
11 |
--------------------------------------------------------------------------------
/go-plist/text_generator.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "encoding/hex"
5 | "io"
6 | "strconv"
7 | "time"
8 | )
9 |
10 | type textPlistGenerator struct {
11 | writer io.Writer
12 | format int
13 |
14 | quotableTable *characterSet
15 |
16 | indent string
17 | depth int
18 |
19 | dictKvDelimiter, dictEntryDelimiter, arrayDelimiter []byte
20 | }
21 |
22 | var (
23 | textPlistTimeLayout = "2006-01-02 15:04:05 -0700"
24 | padding = "0000"
25 | )
26 |
27 | func (p *textPlistGenerator) generateDocument(pval cfValue) {
28 | p.writePlistValue(pval)
29 | }
30 |
31 | func (p *textPlistGenerator) plistQuotedString(str string) string {
32 | if str == "" {
33 | return `""`
34 | }
35 | s := ""
36 | quot := false
37 | for _, r := range str {
38 | if r > 0xFF {
39 | quot = true
40 | s += `\U`
41 | us := strconv.FormatInt(int64(r), 16)
42 | s += padding[len(us):]
43 | s += us
44 | } else if r > 0x7F {
45 | quot = true
46 | s += `\`
47 | us := strconv.FormatInt(int64(r), 8)
48 | s += padding[1+len(us):]
49 | s += us
50 | } else {
51 | c := uint8(r)
52 | if p.quotableTable.ContainsByte(c) {
53 | quot = true
54 | }
55 |
56 | switch c {
57 | case '\a':
58 | s += `\a`
59 | case '\b':
60 | s += `\b`
61 | case '\v':
62 | s += `\v`
63 | case '\f':
64 | s += `\f`
65 | case '\\':
66 | s += `\\`
67 | case '"':
68 | s += `\"`
69 | case '\t':
70 | s += `\t`
71 | case '\r':
72 | s += `\r`
73 | case '\n':
74 | s += `\n`
75 | default:
76 | s += string(c)
77 | }
78 | }
79 | }
80 | if quot {
81 | s = `"` + s + `"`
82 | }
83 | return s
84 | }
85 |
86 | func (p *textPlistGenerator) deltaIndent(depthDelta int) {
87 | if depthDelta < 0 {
88 | p.depth--
89 | } else if depthDelta > 0 {
90 | p.depth++
91 | }
92 | }
93 |
94 | func (p *textPlistGenerator) writeIndent() {
95 | if len(p.indent) == 0 {
96 | return
97 | }
98 | if len(p.indent) > 0 {
99 | p.writer.Write([]byte("\n"))
100 | for i := 0; i < p.depth; i++ {
101 | io.WriteString(p.writer, p.indent)
102 | }
103 | }
104 | }
105 |
106 | func (p *textPlistGenerator) writePlistValue(pval cfValue) {
107 | if pval == nil {
108 | return
109 | }
110 |
111 | switch pval := pval.(type) {
112 | case *cfDictionary:
113 | pval.sort()
114 | p.writer.Write([]byte(`{`))
115 | p.deltaIndent(1)
116 | for i, k := range pval.keys {
117 | p.writeIndent()
118 | io.WriteString(p.writer, p.plistQuotedString(k))
119 | p.writer.Write(p.dictKvDelimiter)
120 | p.writePlistValue(pval.values[i])
121 | p.writer.Write(p.dictEntryDelimiter)
122 | }
123 | p.deltaIndent(-1)
124 | p.writeIndent()
125 | p.writer.Write([]byte(`}`))
126 | case *cfArray:
127 | p.writer.Write([]byte(`(`))
128 | p.deltaIndent(1)
129 | for _, v := range pval.values {
130 | p.writeIndent()
131 | p.writePlistValue(v)
132 | p.writer.Write(p.arrayDelimiter)
133 | }
134 | p.deltaIndent(-1)
135 | p.writeIndent()
136 | p.writer.Write([]byte(`)`))
137 | case cfString:
138 | io.WriteString(p.writer, p.plistQuotedString(string(pval)))
139 | case *cfNumber:
140 | if p.format == GNUStepFormat {
141 | p.writer.Write([]byte(`<*I`))
142 | }
143 | if pval.signed {
144 | io.WriteString(p.writer, strconv.FormatInt(int64(pval.value), 10))
145 | } else {
146 | io.WriteString(p.writer, strconv.FormatUint(pval.value, 10))
147 | }
148 | if p.format == GNUStepFormat {
149 | p.writer.Write([]byte(`>`))
150 | }
151 | case *cfReal:
152 | if p.format == GNUStepFormat {
153 | p.writer.Write([]byte(`<*R`))
154 | }
155 | // GNUstep does not differentiate between 32/64-bit floats.
156 | io.WriteString(p.writer, strconv.FormatFloat(pval.value, 'g', -1, 64))
157 | if p.format == GNUStepFormat {
158 | p.writer.Write([]byte(`>`))
159 | }
160 | case cfBoolean:
161 | if p.format == GNUStepFormat {
162 | if pval {
163 | p.writer.Write([]byte(`<*BY>`))
164 | } else {
165 | p.writer.Write([]byte(`<*BN>`))
166 | }
167 | } else {
168 | if pval {
169 | p.writer.Write([]byte(`1`))
170 | } else {
171 | p.writer.Write([]byte(`0`))
172 | }
173 | }
174 | case cfData:
175 | var hexencoded [9]byte
176 | var l int
177 | var asc = 9
178 | hexencoded[8] = ' '
179 |
180 | p.writer.Write([]byte(`<`))
181 | b := []byte(pval)
182 | for i := 0; i < len(b); i += 4 {
183 | l = i + 4
184 | if l >= len(b) {
185 | l = len(b)
186 | // We no longer need the space - or the rest of the buffer.
187 | // (we used >= above to get this part without another conditional :P)
188 | asc = (l - i) * 2
189 | }
190 | // Fill the buffer (only up to 8 characters, to preserve the space we implicitly include
191 | // at the end of every encode)
192 | hex.Encode(hexencoded[:8], b[i:l])
193 | io.WriteString(p.writer, string(hexencoded[:asc]))
194 | }
195 | p.writer.Write([]byte(`>`))
196 | case cfDate:
197 | if p.format == GNUStepFormat {
198 | p.writer.Write([]byte(`<*D`))
199 | io.WriteString(p.writer, time.Time(pval).In(time.UTC).Format(textPlistTimeLayout))
200 | p.writer.Write([]byte(`>`))
201 | } else {
202 | io.WriteString(p.writer, p.plistQuotedString(time.Time(pval).In(time.UTC).Format(textPlistTimeLayout)))
203 | }
204 | case cfUID:
205 | p.writePlistValue(pval.toDict())
206 | }
207 | }
208 |
209 | func (p *textPlistGenerator) Indent(i string) {
210 | p.indent = i
211 | if i == "" {
212 | p.dictKvDelimiter = []byte(`=`)
213 | } else {
214 | // For pretty-printing
215 | p.dictKvDelimiter = []byte(` = `)
216 | }
217 | }
218 |
219 | func newTextPlistGenerator(w io.Writer, format int) *textPlistGenerator {
220 | table := &osQuotable
221 | if format == GNUStepFormat {
222 | table = &gsQuotable
223 | }
224 | return &textPlistGenerator{
225 | writer: mustWriter{w},
226 | format: format,
227 | quotableTable: table,
228 | dictKvDelimiter: []byte(`=`),
229 | arrayDelimiter: []byte(`,`),
230 | dictEntryDelimiter: []byte(`;`),
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/go-plist/text_parser.go:
--------------------------------------------------------------------------------
1 | // Parser for text plist formats.
2 | // @see https://github.com/apple/swift-corelibs-foundation/blob/master/CoreFoundation/Parsing.subproj/CFOldStylePList.c
3 | // @see https://github.com/gnustep/libs-base/blob/master/Source/NSPropertyList.m
4 | // This parser also handles strings files.
5 |
6 | package plist
7 |
8 | import (
9 | "encoding/base64"
10 | "encoding/binary"
11 | "errors"
12 | "fmt"
13 | "io"
14 | "io/ioutil"
15 | "regexp"
16 | "runtime"
17 | "strings"
18 | "time"
19 | "unicode/utf16"
20 | "unicode/utf8"
21 | )
22 |
23 | type textPlistParser struct {
24 | reader io.Reader
25 | format int
26 |
27 | input string
28 | start int
29 | pos int
30 | width int
31 | }
32 |
33 | func convertU16(buffer []byte, bo binary.ByteOrder) (string, error) {
34 | if len(buffer)%2 != 0 {
35 | return "", errors.New("truncated utf16")
36 | }
37 |
38 | tmp := make([]uint16, len(buffer)/2)
39 | for i := 0; i < len(buffer); i += 2 {
40 | tmp[i/2] = bo.Uint16(buffer[i : i+2])
41 | }
42 | return string(utf16.Decode(tmp)), nil
43 | }
44 |
45 | func guessEncodingAndConvert(buffer []byte) (string, error) {
46 | if len(buffer) >= 3 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF {
47 | // UTF-8 BOM
48 | return zeroCopy8BitString(buffer, 3, len(buffer)-3), nil
49 | } else if len(buffer) >= 2 {
50 | // UTF-16 guesses
51 |
52 | switch {
53 | // stream is big-endian (BOM is FE FF or head is 00 XX)
54 | case (buffer[0] == 0xFE && buffer[1] == 0xFF):
55 | return convertU16(buffer[2:], binary.BigEndian)
56 | case (buffer[0] == 0 && buffer[1] != 0):
57 | return convertU16(buffer, binary.BigEndian)
58 |
59 | // stream is little-endian (BOM is FE FF or head is XX 00)
60 | case (buffer[0] == 0xFF && buffer[1] == 0xFE):
61 | return convertU16(buffer[2:], binary.LittleEndian)
62 | case (buffer[0] != 0 && buffer[1] == 0):
63 | return convertU16(buffer, binary.LittleEndian)
64 | }
65 | }
66 |
67 | // fallback: assume ASCII (not great!)
68 | return zeroCopy8BitString(buffer, 0, len(buffer)), nil
69 | }
70 |
71 | func (p *textPlistParser) parseDocument() (pval cfValue, parseError error) {
72 | defer func() {
73 | if r := recover(); r != nil {
74 | if _, ok := r.(runtime.Error); ok {
75 | panic(r)
76 | }
77 | // Wrap all non-invalid-plist errors.
78 | parseError = plistParseError{"text", r.(error)}
79 | }
80 | }()
81 |
82 | buffer, err := ioutil.ReadAll(p.reader)
83 | if err != nil {
84 | panic(err)
85 | }
86 |
87 | p.input, err = guessEncodingAndConvert(buffer)
88 | if err != nil {
89 | panic(err)
90 | }
91 |
92 | val := p.parsePlistValue()
93 |
94 | p.skipWhitespaceAndComments()
95 | if p.peek() != eof {
96 | if _, ok := val.(cfString); !ok {
97 | p.error("garbage after end of document")
98 | }
99 |
100 | // Try parsing as .strings.
101 | // See -[NSDictionary propertyListFromStringsFileFormat:].
102 | p.start = 0
103 | p.pos = 0
104 | val = p.parseDictionary(true)
105 | }
106 |
107 | pval = val
108 |
109 | return
110 | }
111 |
112 | const eof rune = -1
113 |
114 | func (p *textPlistParser) error(e string, args ...interface{}) {
115 | line := strings.Count(p.input[:p.pos], "\n")
116 | char := p.pos - strings.LastIndex(p.input[:p.pos], "\n") - 1
117 | panic(fmt.Errorf("%s at line %d character %d", fmt.Sprintf(e, args...), line, char))
118 | }
119 |
120 | func (p *textPlistParser) next() rune {
121 | if int(p.pos) >= len(p.input) {
122 | p.width = 0
123 | return eof
124 | }
125 | r, w := utf8.DecodeRuneInString(p.input[p.pos:])
126 | p.width = w
127 | p.pos += p.width
128 | return r
129 | }
130 |
131 | func (p *textPlistParser) backup() {
132 | p.pos -= p.width
133 | }
134 |
135 | func (p *textPlistParser) peek() rune {
136 | r := p.next()
137 | p.backup()
138 | return r
139 | }
140 |
141 | func (p *textPlistParser) emit() string {
142 | s := p.input[p.start:p.pos]
143 | p.start = p.pos
144 | return s
145 | }
146 |
147 | func (p *textPlistParser) ignore() {
148 | p.start = p.pos
149 | }
150 |
151 | func (p *textPlistParser) empty() bool {
152 | return p.start == p.pos
153 | }
154 |
155 | func (p *textPlistParser) scanUntil(ch rune) {
156 | if x := strings.IndexRune(p.input[p.pos:], ch); x >= 0 {
157 | p.pos += x
158 | return
159 | }
160 | p.pos = len(p.input)
161 | }
162 |
163 | func (p *textPlistParser) scanUntilAny(chs string) {
164 | if x := strings.IndexAny(p.input[p.pos:], chs); x >= 0 {
165 | p.pos += x
166 | return
167 | }
168 | p.pos = len(p.input)
169 | }
170 |
171 | func (p *textPlistParser) scanCharactersInSet(ch *characterSet) {
172 | for ch.Contains(p.next()) {
173 | }
174 | p.backup()
175 | }
176 |
177 | func (p *textPlistParser) scanCharactersNotInSet(ch *characterSet) {
178 | var r rune
179 | for {
180 | r = p.next()
181 | if r == eof || ch.Contains(r) {
182 | break
183 | }
184 | }
185 | p.backup()
186 | }
187 |
188 | func (p *textPlistParser) skipWhitespaceAndComments() {
189 | for {
190 | p.scanCharactersInSet(&whitespace)
191 | if strings.HasPrefix(p.input[p.pos:], "//") {
192 | p.scanCharactersNotInSet(&newlineCharacterSet)
193 | } else if strings.HasPrefix(p.input[p.pos:], "/*") {
194 | if x := strings.Index(p.input[p.pos:], "*/"); x >= 0 {
195 | p.pos += x + 2 // skip the */ as well
196 | continue // consume more whitespace
197 | } else {
198 | p.error("unexpected eof in block comment")
199 | }
200 | } else {
201 | break
202 | }
203 | }
204 | p.ignore()
205 | }
206 |
207 | func (p *textPlistParser) parseOctalDigits(max int) uint64 {
208 | var val uint64
209 |
210 | for i := 0; i < max; i++ {
211 | r := p.next()
212 |
213 | if r >= '0' && r <= '7' {
214 | val <<= 3
215 | val |= uint64((r - '0'))
216 | } else {
217 | p.backup()
218 | break
219 | }
220 | }
221 | return val
222 | }
223 |
224 | func (p *textPlistParser) parseHexDigits(max int) uint64 {
225 | var val uint64
226 |
227 | for i := 0; i < max; i++ {
228 | r := p.next()
229 |
230 | if r >= 'a' && r <= 'f' {
231 | val <<= 4
232 | val |= 10 + uint64((r - 'a'))
233 | } else if r >= 'A' && r <= 'F' {
234 | val <<= 4
235 | val |= 10 + uint64((r - 'A'))
236 | } else if r >= '0' && r <= '9' {
237 | val <<= 4
238 | val |= uint64((r - '0'))
239 | } else {
240 | p.backup()
241 | break
242 | }
243 | }
244 | return val
245 | }
246 |
247 | // the \ has already been consumed
248 | func (p *textPlistParser) parseEscape() string {
249 | var s string
250 | switch p.next() {
251 | case 'a':
252 | s = "\a"
253 | case 'b':
254 | s = "\b"
255 | case 'v':
256 | s = "\v"
257 | case 'f':
258 | s = "\f"
259 | case 't':
260 | s = "\t"
261 | case 'r':
262 | s = "\r"
263 | case 'n':
264 | s = "\n"
265 | case '\\':
266 | s = `\`
267 | case '"':
268 | s = `"`
269 | case 'x': // This is our extension.
270 | s = string(rune(p.parseHexDigits(2)))
271 | case 'u', 'U': // 'u' is a GNUstep extension.
272 | s = string(rune(p.parseHexDigits(4)))
273 | case '0', '1', '2', '3', '4', '5', '6', '7':
274 | p.backup() // we've already consumed one of the digits
275 | s = string(rune(p.parseOctalDigits(3)))
276 | default:
277 | p.backup() // everything else should be accepted
278 | }
279 | p.ignore() // skip the entire escape sequence
280 | return s
281 | }
282 |
283 | // the " has already been consumed
284 | func (p *textPlistParser) parseQuotedString() cfString {
285 | p.ignore() // ignore the "
286 |
287 | slowPath := false
288 | s := ""
289 |
290 | for {
291 | p.scanUntilAny(`"\`)
292 | switch p.peek() {
293 | case eof:
294 | p.error("unexpected eof in quoted string")
295 | case '"':
296 | section := p.emit()
297 | p.pos++ // skip "
298 | if !slowPath {
299 | return cfString(section)
300 | } else {
301 | s += section
302 | return cfString(s)
303 | }
304 | case '\\':
305 |
306 | slowPath = true
307 | s += p.emit()
308 |
309 | // Escaping a backslash is the only thing that is correctly stored and represented in `defaults`
310 | if p.input[p.pos:p.pos+4] == "\\\\\\\\" {
311 | p.pos += 4
312 | p.ignore()
313 | s += "\\"
314 | } else {
315 | // everything else is incorrectly encoded with one additional backslash \\"
316 | p.next() // consume \
317 | p.next() // consume \
318 | s += p.parseEscape()
319 | }
320 | }
321 | }
322 | }
323 |
324 | func (p *textPlistParser) parseUnquotedString() cfString {
325 | p.scanCharactersNotInSet(&gsQuotable)
326 | s := p.emit()
327 | if s == "" {
328 | p.error("invalid unquoted string (found an unquoted character that should be quoted?)")
329 | }
330 |
331 | return cfString(s)
332 | }
333 |
334 | func (p *textPlistParser) parseByteSummary() cfString {
335 | p.scanUntil('}')
336 | p.next()
337 | s := p.emit()
338 | return cfString(s)
339 | }
340 |
341 | // the { has already been consumed
342 | func (p *textPlistParser) parseDictionary(ignoreEof bool) cfValue {
343 | //p.ignore() // ignore the {
344 | var keypv cfValue
345 | keys := make([]string, 0, 32)
346 | values := make([]cfValue, 0, 32)
347 | outer:
348 | for {
349 | p.skipWhitespaceAndComments()
350 |
351 | switch p.next() {
352 | case eof:
353 | if !ignoreEof {
354 | p.error("unexpected eof in dictionary")
355 | }
356 | fallthrough
357 | case '}':
358 | break outer
359 | case '"':
360 | keypv = p.parseQuotedString()
361 | default:
362 | p.backup()
363 | keypv = p.parseUnquotedString()
364 | }
365 |
366 | // INVARIANT: key can't be nil; parseQuoted and parseUnquoted
367 | // will panic out before they return nil.
368 |
369 | p.skipWhitespaceAndComments()
370 |
371 | var val cfValue
372 | n := p.next()
373 | if n == ';' {
374 | // This is supposed to be .strings-specific.
375 | // GNUstep parses this as an empty string.
376 | // Apple copies the key like we do.
377 | val = keypv
378 | } else if n == '=' {
379 | // whitespace is consumed within
380 | val = p.parsePlistValue()
381 |
382 | p.skipWhitespaceAndComments()
383 |
384 | if p.next() != ';' {
385 | p.error("missing ; in dictionary")
386 | }
387 | } else {
388 | p.error("missing = in dictionary")
389 | }
390 |
391 | keys = append(keys, string(keypv.(cfString)))
392 | values = append(values, val)
393 | }
394 |
395 | dict := &cfDictionary{keys: keys, values: values}
396 | return dict.maybeUID(p.format == OpenStepFormat)
397 | }
398 |
399 | // the ( has already been consumed
400 | func (p *textPlistParser) parseArray() *cfArray {
401 | //p.ignore() // ignore the (
402 | values := make([]cfValue, 0, 32)
403 | outer:
404 | for {
405 | p.skipWhitespaceAndComments()
406 |
407 | switch p.next() {
408 | case eof:
409 | p.error("unexpected eof in array")
410 | case ')':
411 | break outer // done here
412 | case ',':
413 | continue // restart; ,) is valid and we don't want to blow it
414 | default:
415 | p.backup()
416 | }
417 |
418 | pval := p.parsePlistValue() // whitespace is consumed within
419 | if str, ok := pval.(cfString); ok && string(str) == "" {
420 | // Empty strings in arrays are apparently skipped?
421 | // TODO: Figure out why this was implemented.
422 | continue
423 | }
424 | values = append(values, pval)
425 | }
426 | return &cfArray{values}
427 | }
428 |
429 | // the <* have already been consumed
430 | func (p *textPlistParser) parseGNUStepValue() cfValue {
431 | typ := p.next()
432 |
433 | if typ == '>' || typ == eof { // <*>, <*EOF
434 | p.error("invalid GNUStep extended value")
435 | }
436 |
437 | if typ != 'I' && typ != 'R' && typ != 'B' && typ != 'D' {
438 | // early out: no need to collect the value if we'll fail to understand it
439 | p.error("unknown GNUStep extended value type `" + string(typ) + "'")
440 | }
441 |
442 | if p.peek() == '"' { // <*x"
443 | p.next()
444 | }
445 |
446 | p.ignore()
447 | p.scanUntil('>')
448 |
449 | if p.peek() == eof { // <*xEOF or <*x"EOF
450 | p.error("unterminated GNUStep extended value")
451 | }
452 |
453 | if p.empty() { // <*x>, <*x"">
454 | p.error("empty GNUStep extended value")
455 | }
456 |
457 | v := p.emit()
458 | p.next() // consume the >
459 |
460 | if v[len(v)-1] == '"' {
461 | // GNUStep tolerates malformed quoted values, as in <*I5"> and <*I"5>
462 | // It purportedly does so by stripping the trailing quote
463 | v = v[:len(v)-1]
464 | }
465 |
466 | switch typ {
467 | case 'I':
468 | if v[0] == '-' {
469 | n := mustParseInt(v, 10, 64)
470 | return &cfNumber{signed: true, value: uint64(n)}
471 | } else {
472 | n := mustParseUint(v, 10, 64)
473 | return &cfNumber{signed: false, value: n}
474 | }
475 | case 'R':
476 | n := mustParseFloat(v, 64)
477 | return &cfReal{wide: true, value: n} // TODO(DH) 32/64
478 | case 'B':
479 | b := v[0] == 'Y'
480 | return cfBoolean(b)
481 | case 'D':
482 | t, err := time.Parse(textPlistTimeLayout, v)
483 | if err != nil {
484 | p.error(err.Error())
485 | }
486 |
487 | return cfDate(t.In(time.UTC))
488 | }
489 | // We should never get here; we checked the type above
490 | return nil
491 | }
492 |
493 | // the <[ have already been consumed
494 | func (p *textPlistParser) parseGNUStepBase64() cfData {
495 | p.ignore()
496 | p.scanUntil(']')
497 | v := p.emit()
498 |
499 | if p.next() != ']' {
500 | p.error("invalid GNUStep base64 data (expected ']')")
501 | }
502 |
503 | if p.next() != '>' {
504 | p.error("invalid GNUStep base64 data (expected '>')")
505 | }
506 |
507 | // Emulate NSDataBase64DecodingIgnoreUnknownCharacters
508 | filtered := strings.Map(base64ValidChars.Map, v)
509 | data, err := base64.StdEncoding.DecodeString(filtered)
510 | if err != nil {
511 | p.error("invalid GNUStep base64 data: " + err.Error())
512 | }
513 | return cfData(data)
514 | }
515 |
516 | // The < has already been consumed
517 | func (p *textPlistParser) parseHexData() cfData {
518 | buf := make([]byte, 256)
519 | i := 0
520 | c := 0
521 |
522 | for {
523 | r := p.next()
524 | switch r {
525 | case eof:
526 | p.error("unexpected eof in data")
527 | case '>':
528 | if c&1 == 1 {
529 | p.error("uneven number of hex digits in data")
530 | }
531 | p.ignore()
532 | return cfData(buf[:i])
533 | // Apple and GNUstep both want these in pairs. We are a bit more lax.
534 | // GS accepts comments too, but that seems like a lot of work.
535 | case ' ', '\t', '\n', '\r', '\u2028', '\u2029':
536 | continue
537 | }
538 |
539 | buf[i] <<= 4
540 | if r >= 'a' && r <= 'f' {
541 | buf[i] |= 10 + byte((r - 'a'))
542 | } else if r >= 'A' && r <= 'F' {
543 | buf[i] |= 10 + byte((r - 'A'))
544 | } else if r >= '0' && r <= '9' {
545 | buf[i] |= byte((r - '0'))
546 | } else {
547 | p.error("unexpected hex digit `%c'", r)
548 | }
549 |
550 | c++
551 | if c&1 == 0 {
552 | i++
553 | if i >= len(buf) {
554 | realloc := make([]byte, len(buf)*2)
555 | copy(realloc, buf)
556 | buf = realloc
557 | }
558 | }
559 | }
560 | }
561 |
562 | func (p *textPlistParser) parsePlistValue() cfValue {
563 |
564 | re := regexp.MustCompile(`length = [0-9]+, bytes =`)
565 |
566 | for {
567 | p.skipWhitespaceAndComments()
568 |
569 | switch p.next() {
570 | case eof:
571 | return &cfDictionary{}
572 | case '<':
573 | switch p.next() {
574 | case '*':
575 | p.format = GNUStepFormat
576 | return p.parseGNUStepValue()
577 | case '[':
578 | p.format = GNUStepFormat
579 | return p.parseGNUStepBase64()
580 | default:
581 | p.backup()
582 | return p.parseHexData()
583 | }
584 | case '"':
585 | return p.parseQuotedString()
586 | case '{':
587 | if loc := re.FindStringIndex(p.input[p.pos:]); loc != nil && loc[0] == 0 {
588 | p.format = DefaultsFormat
589 | p.backup()
590 | return p.parseByteSummary()
591 | }
592 |
593 | return p.parseDictionary(false)
594 | case '(':
595 | return p.parseArray()
596 | default:
597 | p.backup()
598 | return p.parseUnquotedString()
599 | }
600 | }
601 | }
602 |
603 | func newTextPlistParser(r io.Reader) *textPlistParser {
604 | return &textPlistParser{
605 | reader: r,
606 | format: OpenStepFormat,
607 | }
608 | }
609 |
--------------------------------------------------------------------------------
/go-plist/text_tables.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | type characterSet [4]uint64
4 |
5 | func (s *characterSet) Map(ch rune) rune {
6 | if s.Contains(ch) {
7 | return ch
8 | } else {
9 | return -1
10 | }
11 | }
12 |
13 | func (s *characterSet) Contains(ch rune) bool {
14 | return ch >= 0 && ch <= 255 && s.ContainsByte(byte(ch))
15 | }
16 |
17 | func (s *characterSet) ContainsByte(ch byte) bool {
18 | return (s[ch/64]&(1<<(ch%64)) > 0)
19 | }
20 |
21 | // Bitmap of characters that must be inside a quoted string
22 | // when written to an old-style property list
23 | // Low bits represent lower characters, and each uint64 represents 64 characters.
24 | var gsQuotable = characterSet{
25 | 0x78001385ffffffff,
26 | 0xa800000138000000,
27 | 0xffffffffffffffff,
28 | 0xffffffffffffffff,
29 | }
30 |
31 | // 7f instead of 3f in the top line: CFOldStylePlist.c says . is valid, but they quote it.
32 | var osQuotable = characterSet{
33 | 0xf4007f6fffffffff,
34 | 0xf8000001f8000001,
35 | 0xffffffffffffffff,
36 | 0xffffffffffffffff,
37 | }
38 |
39 | var whitespace = characterSet{
40 | 0x0000000100003f00,
41 | 0x0000000000000000,
42 | 0x0000000000000000,
43 | 0x0000000000000000,
44 | }
45 |
46 | var newlineCharacterSet = characterSet{
47 | 0x0000000000002400,
48 | 0x0000000000000000,
49 | 0x0000000000000000,
50 | 0x0000000000000000,
51 | }
52 |
53 | // Bitmap of characters that are valid in base64-encoded strings.
54 | // Used to filter out non-b64 characters to emulate NSDataBase64DecodingIgnoreUnknownCharacters
55 | var base64ValidChars = characterSet{
56 | 0x23ff880000000000,
57 | 0x07fffffe07fffffe,
58 | 0x0000000000000000,
59 | 0x0000000000000000,
60 | }
61 |
--------------------------------------------------------------------------------
/go-plist/text_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "testing"
7 | )
8 |
9 | func BenchmarkOpenStepGenerate(b *testing.B) {
10 | for i := 0; i < b.N; i++ {
11 | d := newTextPlistGenerator(ioutil.Discard, OpenStepFormat)
12 | d.generateDocument(plistValueTree)
13 | }
14 | }
15 |
16 | func BenchmarkOpenStepParse(b *testing.B) {
17 | buf := bytes.NewReader([]byte(plistValueTreeAsOpenStep))
18 | b.ResetTimer()
19 | for i := 0; i < b.N; i++ {
20 | b.StartTimer()
21 | d := newTextPlistParser(buf)
22 | d.parseDocument()
23 | b.StopTimer()
24 | buf.Seek(0, 0)
25 | }
26 | }
27 |
28 | func BenchmarkGNUStepParse(b *testing.B) {
29 | buf := bytes.NewReader([]byte(plistValueTreeAsGNUStep))
30 | b.ResetTimer()
31 | for i := 0; i < b.N; i++ {
32 | b.StartTimer()
33 | d := newTextPlistParser(buf)
34 | d.parseDocument()
35 | b.StopTimer()
36 | buf.Seek(0, 0)
37 | }
38 | }
39 |
40 | // The valid text test cases have been merged into the common/global test cases.
41 |
--------------------------------------------------------------------------------
/go-plist/typeinfo.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "reflect"
5 | "strings"
6 | "sync"
7 | )
8 |
9 | // typeInfo holds details for the plist representation of a type.
10 | type typeInfo struct {
11 | fields []fieldInfo
12 | }
13 |
14 | // fieldInfo holds details for the plist representation of a single field.
15 | type fieldInfo struct {
16 | idx []int
17 | name string
18 | omitEmpty bool
19 | }
20 |
21 | var tinfoMap = make(map[reflect.Type]*typeInfo)
22 | var tinfoLock sync.RWMutex
23 |
24 | // getTypeInfo returns the typeInfo structure with details necessary
25 | // for marshalling and unmarshalling typ.
26 | func getTypeInfo(typ reflect.Type) (*typeInfo, error) {
27 | tinfoLock.RLock()
28 | tinfo, ok := tinfoMap[typ]
29 | tinfoLock.RUnlock()
30 | if ok {
31 | return tinfo, nil
32 | }
33 | tinfo = &typeInfo{}
34 | if typ.Kind() == reflect.Struct {
35 | n := typ.NumField()
36 | for i := 0; i < n; i++ {
37 | f := typ.Field(i)
38 | if f.PkgPath != "" || f.Tag.Get("plist") == "-" {
39 | continue // Private field
40 | }
41 |
42 | // For embedded structs, embed its fields.
43 | if f.Anonymous {
44 | t := f.Type
45 | if t.Kind() == reflect.Ptr {
46 | t = t.Elem()
47 | }
48 | if t.Kind() == reflect.Struct {
49 | inner, err := getTypeInfo(t)
50 | if err != nil {
51 | return nil, err
52 | }
53 | for _, finfo := range inner.fields {
54 | finfo.idx = append([]int{i}, finfo.idx...)
55 | if err := addFieldInfo(typ, tinfo, &finfo); err != nil {
56 | return nil, err
57 | }
58 | }
59 | continue
60 | }
61 | }
62 |
63 | finfo, err := structFieldInfo(typ, &f)
64 | if err != nil {
65 | return nil, err
66 | }
67 |
68 | // Add the field if it doesn't conflict with other fields.
69 | if err := addFieldInfo(typ, tinfo, finfo); err != nil {
70 | return nil, err
71 | }
72 | }
73 | }
74 | tinfoLock.Lock()
75 | tinfoMap[typ] = tinfo
76 | tinfoLock.Unlock()
77 | return tinfo, nil
78 | }
79 |
80 | // structFieldInfo builds and returns a fieldInfo for f.
81 | func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, error) {
82 | finfo := &fieldInfo{idx: f.Index}
83 |
84 | // Split the tag from the xml namespace if necessary.
85 | tag := f.Tag.Get("plist")
86 |
87 | // Parse flags.
88 | tokens := strings.Split(tag, ",")
89 | tag = tokens[0]
90 | if len(tokens) > 1 {
91 | tag = tokens[0]
92 | for _, flag := range tokens[1:] {
93 | switch flag {
94 | case "omitempty":
95 | finfo.omitEmpty = true
96 | }
97 | }
98 | }
99 |
100 | if tag == "" {
101 | // If the name part of the tag is completely empty,
102 | // use the field name
103 | finfo.name = f.Name
104 | return finfo, nil
105 | }
106 |
107 | finfo.name = tag
108 | return finfo, nil
109 | }
110 |
111 | // addFieldInfo adds finfo to tinfo.fields if there are no
112 | // conflicts, or if conflicts arise from previous fields that were
113 | // obtained from deeper embedded structures than finfo. In the latter
114 | // case, the conflicting entries are dropped.
115 | // A conflict occurs when the path (parent + name) to a field is
116 | // itself a prefix of another path, or when two paths match exactly.
117 | // It is okay for field paths to share a common, shorter prefix.
118 | func addFieldInfo(typ reflect.Type, tinfo *typeInfo, newf *fieldInfo) error {
119 | var conflicts []int
120 | // First, figure all conflicts. Most working code will have none.
121 | for i := range tinfo.fields {
122 | oldf := &tinfo.fields[i]
123 | if newf.name == oldf.name {
124 | conflicts = append(conflicts, i)
125 | }
126 | }
127 |
128 | // Without conflicts, add the new field and return.
129 | if conflicts == nil {
130 | tinfo.fields = append(tinfo.fields, *newf)
131 | return nil
132 | }
133 |
134 | // If any conflict is shallower, ignore the new field.
135 | // This matches the Go field resolution on embedding.
136 | for _, i := range conflicts {
137 | if len(tinfo.fields[i].idx) < len(newf.idx) {
138 | return nil
139 | }
140 | }
141 |
142 | // Otherwise, the new field is shallower, and thus takes precedence,
143 | // so drop the conflicting fields from tinfo and append the new one.
144 | for c := len(conflicts) - 1; c >= 0; c-- {
145 | i := conflicts[c]
146 | copy(tinfo.fields[i:], tinfo.fields[i+1:])
147 | tinfo.fields = tinfo.fields[:len(tinfo.fields)-1]
148 | }
149 | tinfo.fields = append(tinfo.fields, *newf)
150 | return nil
151 | }
152 |
153 | // value returns v's field value corresponding to finfo.
154 | // It's equivalent to v.FieldByIndex(finfo.idx), but initializes
155 | // and dereferences pointers as necessary.
156 | func (finfo *fieldInfo) value(v reflect.Value) reflect.Value {
157 | for i, x := range finfo.idx {
158 | if i > 0 {
159 | t := v.Type()
160 | if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
161 | if v.IsNil() {
162 | v.Set(reflect.New(v.Type().Elem()))
163 | }
164 | v = v.Elem()
165 | }
166 | }
167 | v = v.Field(x)
168 | }
169 | return v
170 | }
171 |
--------------------------------------------------------------------------------
/go-plist/unmarshal.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "encoding"
5 | "fmt"
6 | "reflect"
7 | "runtime"
8 | "time"
9 | )
10 |
11 | type incompatibleDecodeTypeError struct {
12 | dest reflect.Type
13 | src string // type name (from cfValue)
14 | }
15 |
16 | func (u *incompatibleDecodeTypeError) Error() string {
17 | return fmt.Sprintf("plist: type mismatch: tried to decode plist type `%v' into value of type `%v'", u.src, u.dest)
18 | }
19 |
20 | var (
21 | plistUnmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
22 | textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
23 | uidType = reflect.TypeOf(UID(0))
24 | )
25 |
26 | func isEmptyInterface(v reflect.Value) bool {
27 | return v.Kind() == reflect.Interface && v.NumMethod() == 0
28 | }
29 |
30 | func (p *Decoder) unmarshalPlistInterface(pval cfValue, unmarshalable Unmarshaler) {
31 | err := unmarshalable.UnmarshalPlist(func(i interface{}) (err error) {
32 | defer func() {
33 | if r := recover(); r != nil {
34 | if _, ok := r.(runtime.Error); ok {
35 | panic(r)
36 | }
37 | err = r.(error)
38 | }
39 | }()
40 | p.unmarshal(pval, reflect.ValueOf(i))
41 | return
42 | })
43 |
44 | if err != nil {
45 | panic(err)
46 | }
47 | }
48 |
49 | func (p *Decoder) unmarshalTextInterface(pval cfString, unmarshalable encoding.TextUnmarshaler) {
50 | err := unmarshalable.UnmarshalText([]byte(pval))
51 | if err != nil {
52 | panic(err)
53 | }
54 | }
55 |
56 | func (p *Decoder) unmarshalTime(pval cfDate, val reflect.Value) {
57 | val.Set(reflect.ValueOf(time.Time(pval)))
58 | }
59 |
60 | func (p *Decoder) unmarshalLaxString(s string, val reflect.Value) {
61 | switch val.Kind() {
62 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
63 | i := mustParseInt(s, 10, 64)
64 | val.SetInt(i)
65 | return
66 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
67 | i := mustParseUint(s, 10, 64)
68 | val.SetUint(i)
69 | return
70 | case reflect.Float32, reflect.Float64:
71 | f := mustParseFloat(s, 64)
72 | val.SetFloat(f)
73 | return
74 | case reflect.Bool:
75 | b := mustParseBool(s)
76 | val.SetBool(b)
77 | return
78 | case reflect.Struct:
79 | if val.Type() == timeType {
80 | t, err := time.Parse(textPlistTimeLayout, s)
81 | if err != nil {
82 | panic(err)
83 | }
84 | val.Set(reflect.ValueOf(t.In(time.UTC)))
85 | return
86 | }
87 | fallthrough
88 | default:
89 | panic(&incompatibleDecodeTypeError{val.Type(), "string"})
90 | }
91 | }
92 |
93 | func (p *Decoder) unmarshal(pval cfValue, val reflect.Value) {
94 | if pval == nil {
95 | return
96 | }
97 |
98 | if val.Kind() == reflect.Ptr {
99 | if val.IsNil() {
100 | val.Set(reflect.New(val.Type().Elem()))
101 | }
102 | val = val.Elem()
103 | }
104 |
105 | if isEmptyInterface(val) {
106 | v := p.valueInterface(pval)
107 | val.Set(reflect.ValueOf(v))
108 | return
109 | }
110 |
111 | incompatibleTypeError := &incompatibleDecodeTypeError{val.Type(), pval.typeName()}
112 |
113 | // time.Time implements TextMarshaler, but we need to parse it as RFC3339
114 | if date, ok := pval.(cfDate); ok {
115 | if val.Type() == timeType {
116 | p.unmarshalTime(date, val)
117 | return
118 | }
119 | panic(incompatibleTypeError)
120 | }
121 |
122 | if receiver, can := implementsInterface(val, plistUnmarshalerType); can {
123 | p.unmarshalPlistInterface(pval, receiver.(Unmarshaler))
124 | return
125 | }
126 |
127 | if val.Type() != timeType {
128 | if receiver, can := implementsInterface(val, textUnmarshalerType); can {
129 | if str, ok := pval.(cfString); ok {
130 | p.unmarshalTextInterface(str, receiver.(encoding.TextUnmarshaler))
131 | } else {
132 | panic(incompatibleTypeError)
133 | }
134 | return
135 | }
136 | }
137 |
138 | typ := val.Type()
139 |
140 | switch pval := pval.(type) {
141 | case cfString:
142 | if val.Kind() == reflect.String {
143 | val.SetString(string(pval))
144 | return
145 | }
146 | if p.lax {
147 | p.unmarshalLaxString(string(pval), val)
148 | return
149 | }
150 |
151 | panic(incompatibleTypeError)
152 | case *cfNumber:
153 | switch val.Kind() {
154 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
155 | val.SetInt(int64(pval.value))
156 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
157 | val.SetUint(pval.value)
158 | default:
159 | panic(incompatibleTypeError)
160 | }
161 | case *cfReal:
162 | if val.Kind() == reflect.Float32 || val.Kind() == reflect.Float64 {
163 | // TODO: Consider warning on a downcast (storing a 64-bit value in a 32-bit reflect)
164 | val.SetFloat(pval.value)
165 | } else {
166 | panic(incompatibleTypeError)
167 | }
168 | case cfBoolean:
169 | if val.Kind() == reflect.Bool {
170 | val.SetBool(bool(pval))
171 | } else {
172 | panic(incompatibleTypeError)
173 | }
174 | case cfData:
175 | if val.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Uint8 {
176 | val.SetBytes([]byte(pval))
177 | } else {
178 | panic(incompatibleTypeError)
179 | }
180 | case cfUID:
181 | if val.Type() == uidType {
182 | val.SetUint(uint64(pval))
183 | } else {
184 | switch val.Kind() {
185 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
186 | val.SetInt(int64(pval))
187 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
188 | val.SetUint(uint64(pval))
189 | default:
190 | panic(incompatibleTypeError)
191 | }
192 | }
193 | case *cfArray:
194 | p.unmarshalArray(pval, val)
195 | case *cfDictionary:
196 | p.unmarshalDictionary(pval, val)
197 | }
198 | }
199 |
200 | func (p *Decoder) unmarshalArray(a *cfArray, val reflect.Value) {
201 | var n int
202 | if val.Kind() == reflect.Slice {
203 | // Slice of element values.
204 | // Grow slice.
205 | cnt := len(a.values) + val.Len()
206 | if cnt >= val.Cap() {
207 | ncap := 2 * cnt
208 | if ncap < 4 {
209 | ncap = 4
210 | }
211 | new := reflect.MakeSlice(val.Type(), val.Len(), ncap)
212 | reflect.Copy(new, val)
213 | val.Set(new)
214 | }
215 | n = val.Len()
216 | val.SetLen(cnt)
217 | } else if val.Kind() == reflect.Array {
218 | if len(a.values) > val.Cap() {
219 | panic(fmt.Errorf("plist: attempted to unmarshal %d values into an array of size %d", len(a.values), val.Cap()))
220 | }
221 | } else {
222 | panic(&incompatibleDecodeTypeError{val.Type(), a.typeName()})
223 | }
224 |
225 | // Recur to read element into slice.
226 | for _, sval := range a.values {
227 | p.unmarshal(sval, val.Index(n))
228 | n++
229 | }
230 | return
231 | }
232 |
233 | func (p *Decoder) unmarshalDictionary(dict *cfDictionary, val reflect.Value) {
234 | typ := val.Type()
235 | switch val.Kind() {
236 | case reflect.Struct:
237 | tinfo, err := getTypeInfo(typ)
238 | if err != nil {
239 | panic(err)
240 | }
241 |
242 | entries := make(map[string]cfValue, len(dict.keys))
243 | for i, k := range dict.keys {
244 | sval := dict.values[i]
245 | entries[k] = sval
246 | }
247 |
248 | for _, finfo := range tinfo.fields {
249 | p.unmarshal(entries[finfo.name], finfo.value(val))
250 | }
251 | case reflect.Map:
252 | if val.IsNil() {
253 | val.Set(reflect.MakeMap(typ))
254 | }
255 |
256 | for i, k := range dict.keys {
257 | sval := dict.values[i]
258 |
259 | keyv := reflect.ValueOf(k).Convert(typ.Key())
260 | mapElem := reflect.New(typ.Elem()).Elem()
261 |
262 | p.unmarshal(sval, mapElem)
263 | val.SetMapIndex(keyv, mapElem)
264 | }
265 | default:
266 | panic(&incompatibleDecodeTypeError{typ, dict.typeName()})
267 | }
268 | }
269 |
270 | /* *Interface is modelled after encoding/json */
271 | func (p *Decoder) valueInterface(pval cfValue) interface{} {
272 | switch pval := pval.(type) {
273 | case cfString:
274 | return string(pval)
275 | case *cfNumber:
276 | if pval.signed {
277 | return int64(pval.value)
278 | }
279 | return pval.value
280 | case *cfReal:
281 | if pval.wide {
282 | return pval.value
283 | } else {
284 | return float32(pval.value)
285 | }
286 | case cfBoolean:
287 | return bool(pval)
288 | case *cfArray:
289 | return p.arrayInterface(pval)
290 | case *cfDictionary:
291 | return p.dictionaryInterface(pval)
292 | case cfData:
293 | return []byte(pval)
294 | case cfDate:
295 | return time.Time(pval)
296 | case cfUID:
297 | return UID(pval)
298 | }
299 | return nil
300 | }
301 |
302 | func (p *Decoder) arrayInterface(a *cfArray) []interface{} {
303 | out := make([]interface{}, len(a.values))
304 | for i, subv := range a.values {
305 | out[i] = p.valueInterface(subv)
306 | }
307 | return out
308 | }
309 |
310 | func (p *Decoder) dictionaryInterface(dict *cfDictionary) map[string]interface{} {
311 | out := make(map[string]interface{})
312 | for i, k := range dict.keys {
313 | subv := dict.values[i]
314 | out[k] = p.valueInterface(subv)
315 | }
316 | return out
317 | }
318 |
--------------------------------------------------------------------------------
/go-plist/unmarshal_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func BenchmarkStructUnmarshal(b *testing.B) {
10 | type Data struct {
11 | Intarray []uint64 `plist:"intarray"`
12 | Floats []float64 `plist:"floats"`
13 | Booleans []bool `plist:"booleans"`
14 | Strings []string `plist:"strings"`
15 | Dat []byte `plist:"data"`
16 | Date time.Time `plist:"date"`
17 | }
18 | b.ResetTimer()
19 | for i := 0; i < b.N; i++ {
20 | var xval Data
21 | d := &Decoder{}
22 | d.unmarshal(plistValueTree, reflect.ValueOf(&xval))
23 | }
24 | }
25 |
26 | func BenchmarkInterfaceUnmarshal(b *testing.B) {
27 | for i := 0; i < b.N; i++ {
28 | var xval interface{}
29 | d := &Decoder{}
30 | d.unmarshal(plistValueTree, reflect.ValueOf(&xval))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/go-plist/util.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import "io"
4 |
5 | type countedWriter struct {
6 | io.Writer
7 | nbytes int
8 | }
9 |
10 | func (w *countedWriter) Write(p []byte) (int, error) {
11 | n, err := w.Writer.Write(p)
12 | w.nbytes += n
13 | return n, err
14 | }
15 |
16 | func (w *countedWriter) BytesWritten() int {
17 | return w.nbytes
18 | }
19 |
20 | func unsignedGetBase(s string) (string, int) {
21 | if len(s) > 1 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') {
22 | return s[2:], 16
23 | }
24 | return s, 10
25 | }
26 |
--------------------------------------------------------------------------------
/go-plist/xml_generator.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bufio"
5 | "encoding/base64"
6 | "encoding/xml"
7 | "io"
8 | "math"
9 | "strconv"
10 | "time"
11 | )
12 |
13 | const (
14 | xmlHEADER string = `` + "\n"
15 | xmlDOCTYPE = `` + "\n"
16 | xmlArrayTag = "array"
17 | xmlDataTag = "data"
18 | xmlDateTag = "date"
19 | xmlDictTag = "dict"
20 | xmlFalseTag = "false"
21 | xmlIntegerTag = "integer"
22 | xmlKeyTag = "key"
23 | xmlPlistTag = "plist"
24 | xmlRealTag = "real"
25 | xmlStringTag = "string"
26 | xmlTrueTag = "true"
27 | )
28 |
29 | func formatXMLFloat(f float64) string {
30 | switch {
31 | case math.IsInf(f, 1):
32 | return "inf"
33 | case math.IsInf(f, -1):
34 | return "-inf"
35 | case math.IsNaN(f):
36 | return "nan"
37 | }
38 | return strconv.FormatFloat(f, 'g', -1, 64)
39 | }
40 |
41 | type xmlPlistGenerator struct {
42 | *bufio.Writer
43 |
44 | indent string
45 | depth int
46 | putNewline bool
47 | }
48 |
49 | func (p *xmlPlistGenerator) generateDocument(root cfValue) {
50 | p.WriteString(xmlHEADER)
51 | p.WriteString(xmlDOCTYPE)
52 |
53 | p.openTag(`plist version="1.0"`)
54 | p.writePlistValue(root)
55 | p.closeTag(xmlPlistTag)
56 | p.Flush()
57 | }
58 |
59 | func (p *xmlPlistGenerator) openTag(n string) {
60 | p.writeIndent(1)
61 | p.WriteByte('<')
62 | p.WriteString(n)
63 | p.WriteByte('>')
64 | }
65 |
66 | func (p *xmlPlistGenerator) closeTag(n string) {
67 | p.writeIndent(-1)
68 | p.WriteString("")
69 | p.WriteString(n)
70 | p.WriteByte('>')
71 | }
72 |
73 | func (p *xmlPlistGenerator) element(n string, v string) {
74 | p.writeIndent(0)
75 | if len(v) == 0 {
76 | p.WriteByte('<')
77 | p.WriteString(n)
78 | p.WriteString("/>")
79 | } else {
80 | p.WriteByte('<')
81 | p.WriteString(n)
82 | p.WriteByte('>')
83 |
84 | err := xml.EscapeText(p.Writer, []byte(v))
85 | if err != nil {
86 | panic(err)
87 | }
88 |
89 | p.WriteString("")
90 | p.WriteString(n)
91 | p.WriteByte('>')
92 | }
93 | }
94 |
95 | func (p *xmlPlistGenerator) writeDictionary(dict *cfDictionary) {
96 | dict.sort()
97 | p.openTag(xmlDictTag)
98 | for i, k := range dict.keys {
99 | p.element(xmlKeyTag, k)
100 | p.writePlistValue(dict.values[i])
101 | }
102 | p.closeTag(xmlDictTag)
103 | }
104 |
105 | func (p *xmlPlistGenerator) writeArray(a *cfArray) {
106 | p.openTag(xmlArrayTag)
107 | for _, v := range a.values {
108 | p.writePlistValue(v)
109 | }
110 | p.closeTag(xmlArrayTag)
111 | }
112 |
113 | func (p *xmlPlistGenerator) writePlistValue(pval cfValue) {
114 | if pval == nil {
115 | return
116 | }
117 |
118 | switch pval := pval.(type) {
119 | case cfString:
120 | p.element(xmlStringTag, string(pval))
121 | case *cfNumber:
122 | if pval.signed {
123 | p.element(xmlIntegerTag, strconv.FormatInt(int64(pval.value), 10))
124 | } else {
125 | p.element(xmlIntegerTag, strconv.FormatUint(pval.value, 10))
126 | }
127 | case *cfReal:
128 | p.element(xmlRealTag, formatXMLFloat(pval.value))
129 | case cfBoolean:
130 | if bool(pval) {
131 | p.element(xmlTrueTag, "")
132 | } else {
133 | p.element(xmlFalseTag, "")
134 | }
135 | case cfData:
136 | p.element(xmlDataTag, base64.StdEncoding.EncodeToString([]byte(pval)))
137 | case cfDate:
138 | p.element(xmlDateTag, time.Time(pval).In(time.UTC).Format(time.RFC3339))
139 | case *cfDictionary:
140 | p.writeDictionary(pval)
141 | case *cfArray:
142 | p.writeArray(pval)
143 | case cfUID:
144 | p.writePlistValue(pval.toDict())
145 | }
146 | }
147 |
148 | func (p *xmlPlistGenerator) writeIndent(delta int) {
149 | if len(p.indent) == 0 {
150 | return
151 | }
152 |
153 | if delta < 0 {
154 | p.depth--
155 | }
156 |
157 | if p.putNewline {
158 | // from encoding/xml/marshal.go; it seems to be intended
159 | // to suppress the first newline.
160 | p.WriteByte('\n')
161 | } else {
162 | p.putNewline = true
163 | }
164 | for i := 0; i < p.depth; i++ {
165 | p.WriteString(p.indent)
166 | }
167 | if delta > 0 {
168 | p.depth++
169 | }
170 | }
171 |
172 | func (p *xmlPlistGenerator) Indent(i string) {
173 | p.indent = i
174 | }
175 |
176 | func newXMLPlistGenerator(w io.Writer) *xmlPlistGenerator {
177 | return &xmlPlistGenerator{Writer: bufio.NewWriter(w)}
178 | }
179 |
--------------------------------------------------------------------------------
/go-plist/xml_parser.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/xml"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "runtime"
10 | "strings"
11 | "time"
12 | )
13 |
14 | type xmlPlistParser struct {
15 | reader io.Reader
16 | xmlDecoder *xml.Decoder
17 | whitespaceReplacer *strings.Replacer
18 | ntags int
19 | }
20 |
21 | func (p *xmlPlistParser) parseDocument() (pval cfValue, parseError error) {
22 | defer func() {
23 | if r := recover(); r != nil {
24 | if _, ok := r.(runtime.Error); ok {
25 | panic(r)
26 | }
27 | if _, ok := r.(invalidPlistError); ok {
28 | parseError = r.(error)
29 | } else {
30 | // Wrap all non-invalid-plist errors.
31 | parseError = plistParseError{"XML", r.(error)}
32 | }
33 | }
34 | }()
35 | for {
36 | if token, err := p.xmlDecoder.Token(); err == nil {
37 | if element, ok := token.(xml.StartElement); ok {
38 | pval = p.parseXMLElement(element)
39 | if p.ntags == 0 {
40 | panic(invalidPlistError{"XML", errors.New("no elements encountered")})
41 | }
42 | return
43 | }
44 | } else {
45 | // The first XML parse turned out to be invalid:
46 | // we do not have an XML property list.
47 | panic(invalidPlistError{"XML", err})
48 | }
49 | }
50 | }
51 |
52 | func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) cfValue {
53 | var charData xml.CharData
54 | switch element.Name.Local {
55 | case "plist":
56 | p.ntags++
57 | for {
58 | token, err := p.xmlDecoder.Token()
59 | if err != nil {
60 | panic(err)
61 | }
62 |
63 | if el, ok := token.(xml.EndElement); ok && el.Name.Local == "plist" {
64 | break
65 | }
66 |
67 | if el, ok := token.(xml.StartElement); ok {
68 | return p.parseXMLElement(el)
69 | }
70 | }
71 | return nil
72 | case "string":
73 | p.ntags++
74 | err := p.xmlDecoder.DecodeElement(&charData, &element)
75 | if err != nil {
76 | panic(err)
77 | }
78 |
79 | return cfString(charData)
80 | case "integer":
81 | p.ntags++
82 | err := p.xmlDecoder.DecodeElement(&charData, &element)
83 | if err != nil {
84 | panic(err)
85 | }
86 |
87 | s := string(charData)
88 | if len(s) == 0 {
89 | panic(errors.New("invalid empty "))
90 | }
91 |
92 | if s[0] == '-' {
93 | s, base := unsignedGetBase(s[1:])
94 | n := mustParseInt("-"+s, base, 64)
95 | return &cfNumber{signed: true, value: uint64(n)}
96 | } else {
97 | s, base := unsignedGetBase(s)
98 | n := mustParseUint(s, base, 64)
99 | return &cfNumber{signed: false, value: n}
100 | }
101 | case "real":
102 | p.ntags++
103 | err := p.xmlDecoder.DecodeElement(&charData, &element)
104 | if err != nil {
105 | panic(err)
106 | }
107 |
108 | n := mustParseFloat(string(charData), 64)
109 | return &cfReal{wide: true, value: n}
110 | case "true", "false":
111 | p.ntags++
112 | p.xmlDecoder.Skip()
113 |
114 | b := element.Name.Local == "true"
115 | return cfBoolean(b)
116 | case "date":
117 | p.ntags++
118 | err := p.xmlDecoder.DecodeElement(&charData, &element)
119 | if err != nil {
120 | panic(err)
121 | }
122 |
123 | t, err := time.ParseInLocation(time.RFC3339, string(charData), time.UTC)
124 | if err != nil {
125 | panic(err)
126 | }
127 |
128 | return cfDate(t)
129 | case "data":
130 | p.ntags++
131 | err := p.xmlDecoder.DecodeElement(&charData, &element)
132 | if err != nil {
133 | panic(err)
134 | }
135 |
136 | str := p.whitespaceReplacer.Replace(string(charData))
137 |
138 | l := base64.StdEncoding.DecodedLen(len(str))
139 | bytes := make([]uint8, l)
140 | l, err = base64.StdEncoding.Decode(bytes, []byte(str))
141 | if err != nil {
142 | panic(err)
143 | }
144 |
145 | return cfData(bytes[:l])
146 | case "dict":
147 | p.ntags++
148 | var key *string
149 | keys := make([]string, 0, 32)
150 | values := make([]cfValue, 0, 32)
151 | for {
152 | token, err := p.xmlDecoder.Token()
153 | if err != nil {
154 | panic(err)
155 | }
156 |
157 | if el, ok := token.(xml.EndElement); ok && el.Name.Local == "dict" {
158 | if key != nil {
159 | panic(errors.New("missing value in dictionary"))
160 | }
161 | break
162 | }
163 |
164 | if el, ok := token.(xml.StartElement); ok {
165 | if el.Name.Local == "key" {
166 | var k string
167 | p.xmlDecoder.DecodeElement(&k, &el)
168 | key = &k
169 | } else {
170 | if key == nil {
171 | panic(errors.New("missing key in dictionary"))
172 | }
173 | keys = append(keys, *key)
174 | values = append(values, p.parseXMLElement(el))
175 | key = nil
176 | }
177 | }
178 | }
179 |
180 | dict := &cfDictionary{keys: keys, values: values}
181 | return dict.maybeUID(false)
182 | case "array":
183 | p.ntags++
184 | values := make([]cfValue, 0, 10)
185 | for {
186 | token, err := p.xmlDecoder.Token()
187 | if err != nil {
188 | panic(err)
189 | }
190 |
191 | if el, ok := token.(xml.EndElement); ok && el.Name.Local == "array" {
192 | break
193 | }
194 |
195 | if el, ok := token.(xml.StartElement); ok {
196 | values = append(values, p.parseXMLElement(el))
197 | }
198 | }
199 | return &cfArray{values}
200 | }
201 | err := fmt.Errorf("encountered unknown element %s", element.Name.Local)
202 | if p.ntags == 0 {
203 | // If out first XML tag is invalid, it might be an openstep data element, ala or <0101>
204 | panic(invalidPlistError{"XML", err})
205 | }
206 | panic(err)
207 | }
208 |
209 | func newXMLPlistParser(r io.Reader) *xmlPlistParser {
210 | return &xmlPlistParser{r, xml.NewDecoder(r), strings.NewReplacer("\t", "", "\n", "", " ", "", "\r", ""), 0}
211 | }
212 |
--------------------------------------------------------------------------------
/go-plist/xml_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "testing"
7 | )
8 |
9 | func BenchmarkXMLGenerate(b *testing.B) {
10 | for i := 0; i < b.N; i++ {
11 | d := newXMLPlistGenerator(ioutil.Discard)
12 | d.generateDocument(plistValueTree)
13 | }
14 | }
15 |
16 | func BenchmarkXMLParse(b *testing.B) {
17 | buf := bytes.NewReader([]byte(plistValueTreeAsXML))
18 | b.ResetTimer()
19 | for i := 0; i < b.N; i++ {
20 | b.StartTimer()
21 | d := newXMLPlistParser(buf)
22 | d.parseDocument()
23 | b.StopTimer()
24 | buf.Seek(0, 0)
25 | }
26 | }
27 |
28 | var InvalidXMLPlists = []string{
29 | `0x`,
30 | "helo",
31 | "helo",
32 | "helo",
33 | "helo",
34 | "",
35 | "helo",
36 | "*@&%#helo",
37 | "*@&%#helo",
38 | "*@&%#helo",
39 | "10",
40 | "10",
41 | "10",
42 | "10",
43 | "10",
44 | "",
45 | "",
46 | "",
47 | "",
48 | "",
49 | "