├── .github
├── dependabot.yml
└── workflows
│ └── on-push-pr.yml
├── LICENSE
├── README.md
├── binary_parser.go
├── decode.go
├── decode_test.go
├── encode.go
├── encode_test.go
├── example_unmarshaler_test.go
├── go.mod
├── plist.go
├── tags.go
├── testdata
├── crashers
│ ├── 0d16bb1b5a9de90807d6e23316222a70267f48f0
│ ├── 137f12b963e1abb2aedf1e78dab4217d365e61cf
│ ├── 1ac1e0b8585d245f0e7bbc48ef33500984a7fb6b
│ ├── 30864f343b3b987e140eff6769e091b3b60c3ce7
│ ├── 5feced23aa2767c77c8d2bb5c35321f926b4537a
│ ├── 76632470f8fdd06ea7f5eaa53376aa3ffdbe00b0
│ ├── a453429d65a952b8f54dc233d0ac304178a75b41
│ ├── a7f6152b23463dbeb12cf9621b9c5962b8b71d01
│ ├── aac34b3c8cbcc6d607e807fa920b2aaa4294f1fa
│ ├── b6d3ae7d57c52b1139cd4cb097382371be5508f4
│ ├── d2e984d7ef5d4fbcda46e217c28a7ad0077fb820
│ └── e322917c1e9ed2ac460865f9455ef8981f765522
├── sample2.binary.plist
└── xml
│ ├── comment.plist
│ ├── empty-doctype.plist
│ ├── empty-plist.failure.plist
│ ├── empty-xml.plist
│ ├── invalid-before-plist.failure.plist
│ ├── invalid-data.failure.plist
│ ├── invalid-end.plist
│ ├── invalid-middle.failure.plist
│ ├── invalid-start.failure.plist
│ ├── malformed-xml.plist
│ ├── no-both.plist
│ ├── no-dict-end.failure.plist
│ ├── no-doctype.plist
│ ├── no-plist-end.failure.plist
│ ├── no-plist-version.plist
│ ├── no-xml-tag.plist
│ ├── swapped.plist
│ ├── unescaped-plist.failure.plist
│ ├── unescaped-xml.failure.plist
│ └── valid.plist
├── xml_parser.go
└── xml_writer.go
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/" # Don't change this despite the path being .github/workflows
5 | schedule:
6 | # Check for updates to GitHub Actions on the first day of the month
7 | interval: "monthly"
8 |
9 | - package-ecosystem: "gomod"
10 | directory: "/"
11 | schedule:
12 | # Check for updates to Go modules on the first day of the month
13 | interval: "monthly"
14 |
--------------------------------------------------------------------------------
/.github/workflows/on-push-pr.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD
2 | on:
3 | push:
4 | branches: [main]
5 | tags: ["v*.*.*"]
6 | pull_request:
7 | types: [opened, reopened, synchronize]
8 | jobs:
9 | format-build-test:
10 | strategy:
11 | matrix:
12 | go-version: ['1.21.x', '1.22.x']
13 | platform: [ubuntu-latest, macos-latest, windows-latest]
14 | runs-on: ${{ matrix.platform }}
15 | steps:
16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
17 |
18 | - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
19 | with:
20 | go-version: ${{ matrix.go-version }}
21 |
22 | - if: matrix.platform == 'ubuntu-latest'
23 | run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi
24 |
25 | - run: go build -v ./...
26 |
27 | - run: go test -cover -race -v ./...
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Victor Vrantchan. 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
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following disclaimer
11 | in the documentation and/or other materials provided with the
12 | distribution.
13 | * Neither the name of Google Inc. nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 | --------------------------------------------------------------------------------
30 | Parts of this package were made available under the license covering
31 | the go-plist library by Dustin L. Howett. That license follows.
32 | --------------------------------------------------------------------------------
33 |
34 | Copyright (c) 2013, Dustin L. Howett. All rights reserved.
35 |
36 | Redistribution and use in source and binary forms, with or without
37 | modification, are permitted provided that the following conditions are met:
38 |
39 | 1. Redistributions of source code must retain the above copyright notice, this
40 | list of conditions and the following disclaimer.
41 | 2. Redistributions in binary form must reproduce the above copyright notice,
42 | this list of conditions and the following disclaimer in the documentation
43 | and/or other materials provided with the distribution.
44 |
45 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
46 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
47 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
48 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
49 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
50 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
51 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
52 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
53 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
54 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
55 |
56 | The views and conclusions contained in the software and documentation are those
57 | of the authors and should not be interpreted as representing official policies,
58 | either expressed or implied, of the FreeBSD Project.
59 |
60 | --------------------------------------------------------------------------------
61 | Parts of this package were made available under the license covering
62 | the Go language and all attended core libraries. That license follows.
63 | --------------------------------------------------------------------------------
64 |
65 | Copyright (c) 2012 The Go Authors. All rights reserved.
66 |
67 | Redistribution and use in source and binary forms, with or without
68 | modification, are permitted provided that the following conditions are
69 | met:
70 |
71 | * Redistributions of source code must retain the above copyright
72 | notice, this list of conditions and the following disclaimer.
73 | * Redistributions in binary form must reproduce the above
74 | copyright notice, this list of conditions and the following disclaimer
75 | in the documentation and/or other materials provided with the
76 | distribution.
77 | * Neither the name of Google Inc. nor the names of its
78 | contributors may be used to endorse or promote products derived from
79 | this software without specific prior written permission.
80 |
81 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
82 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
83 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
84 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
85 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
86 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
87 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
88 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
89 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
90 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
91 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Go Plist library
2 |
3 | [](https://github.com/micromdm/plist/actions) [](https://pkg.go.dev/github.com/micromdm/plist)
4 |
5 | This Plist library is used for decoding and encoding Apple Property Lists in both XML and binary forms.
6 |
7 | Example using HTTP streams:
8 |
9 | ```go
10 | func someHTTPHandler(w http.ResponseWriter, r *http.Request) {
11 | var sparseBundleHeader struct {
12 | InfoDictionaryVersion *string `plist:"CFBundleInfoDictionaryVersion"`
13 | BandSize *uint64 `plist:"band-size"`
14 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
15 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
16 | Size uint64 `plist:"unknownKey"`
17 | }
18 |
19 | // decode an HTTP request body into the sparseBundleHeader struct
20 | if err := plist.NewXMLDecoder(r.Body).Decode(&sparseBundleHeader); err != nil {
21 | log.Println(err)
22 | return
23 | }
24 | }
25 | ```
26 |
27 | ## Credit
28 |
29 | This library is based on the [DHowett go-plist](https://github.com/DHowett/go-plist) library but has an API that is more like the XML and JSON package in the Go standard library. I.e. the `plist.Decoder()` accepts an `io.Reader` instead of an `io.ReadSeeker`
30 |
--------------------------------------------------------------------------------
/binary_parser.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "fmt"
7 | "io"
8 | "time"
9 | "unicode/utf16"
10 | )
11 |
12 | // plistTrailer is the last 32 bytes of a binary plist
13 | // See definition of CFBinaryPlistTrailer here
14 | // https://opensource.apple.com/source/CF/CF-550.29/ForFoundationOnly.h
15 | type plistTrailer struct {
16 | _ [5]byte // unused padding
17 | SortVersion uint8 // seems to be unused (always zero)
18 | OffsetIntSize uint8 // byte size of offset ints in offset table
19 | ObjectRefSize uint8 // byte size of object refs in arrays and dicts
20 | NumObjects uint64 // number of objects (also number of offsets in offset table)
21 | RootObject uint64 // object ref of top level object
22 | OffsetTableOffset uint64 // offset of the offset table
23 | }
24 |
25 | type binaryParser struct {
26 | OffsetTable []uint64 // array of offsets for each object in plist
27 | plistTrailer // last 32 bytes of plist
28 | io.ReadSeeker // reader for plist data
29 | }
30 |
31 | const numObjectsMax = 4 << 20
32 |
33 | // newBinaryParser takes in a ReadSeeker for the bytes of a binary plist and
34 | // returns a parser after reading the offset table and trailer.
35 | func newBinaryParser(r io.ReadSeeker) (*binaryParser, error) {
36 | var bp binaryParser
37 | bp.ReadSeeker = r
38 |
39 | // Read the trailer.
40 | if _, err := bp.Seek(-32, io.SeekEnd); err != nil {
41 | return nil, fmt.Errorf("plist: couldn't seek to start of trailer: %v", err)
42 | }
43 | if err := binary.Read(bp, binary.BigEndian, &bp.plistTrailer); err != nil {
44 | return nil, fmt.Errorf("plist: couldn't read trailer: %v", err)
45 | }
46 |
47 | // Read the offset table.
48 | if _, err := bp.Seek(int64(bp.OffsetTableOffset), io.SeekStart); err != nil {
49 | return nil, fmt.Errorf("plist: couldn't seek to start of offset table: %v", err)
50 | }
51 |
52 | // numObjectsMax is arbitrary. Please fix.
53 | // TODO(github.com/micromdm/plist/issues/28)
54 | if bp.NumObjects > numObjectsMax {
55 | return nil, fmt.Errorf("plist: offset size larger than expected %d", numObjectsMax)
56 | }
57 |
58 | bp.OffsetTable = make([]uint64, bp.NumObjects)
59 | if bp.OffsetIntSize > 8 {
60 | return nil, fmt.Errorf("plist: can't decode when offset int size (%d) is greater than 8", bp.OffsetIntSize)
61 | }
62 | for i := uint64(0); i < bp.NumObjects; i++ {
63 | buf := make([]byte, 8)
64 | if _, err := bp.Read(buf[8-bp.OffsetIntSize:]); err != nil {
65 | return nil, fmt.Errorf("plist: couldn't read offset table: %v", err)
66 | }
67 | bp.OffsetTable[i] = uint64(binary.BigEndian.Uint64(buf))
68 | }
69 |
70 | return &bp, nil
71 | }
72 |
73 | // parseDocument parses the entire binary plist starting from the root object
74 | // and returns a plistValue representing the root object.
75 | func (bp *binaryParser) parseDocument() (*plistValue, error) {
76 | // Decode and return the root object.
77 | return bp.parseObjectRef(bp.RootObject)
78 | }
79 |
80 | // parseObjectRef decodes and returns the plist object with the given index.
81 | // Index 0 is the first object in the object table, 1 is the second, etc.
82 | // This function restores the current plist offset when it's done so that you
83 | // may call it while decoding a collection object without losing your place.
84 | func (bp *binaryParser) parseObjectRef(index uint64) (val *plistValue, err error) {
85 | // Save the current offset.
86 | offset, err := bp.Seek(0, io.SeekCurrent)
87 | if err != nil {
88 | return nil, err
89 | }
90 | // Then restore the original offset in a defer.
91 | defer func() {
92 | _, err2 := bp.Seek(offset, io.SeekStart)
93 | if err2 != nil {
94 | err = err2
95 | }
96 | }()
97 |
98 | if index > uint64(len(bp.OffsetTable)) {
99 | return nil, fmt.Errorf("plist: offset too large: %d", index)
100 | }
101 | // Move to the start of the object we want to decode.
102 | if _, err := bp.Seek(int64(bp.OffsetTable[index]), io.SeekStart); err != nil {
103 | return nil, err
104 | }
105 | // The first byte of the object is its marker byte.
106 | // High 4 bits of marker byte indicates the object type.
107 | // Low 4 bits contain additional info, typically a count.
108 | // Defined here: https://opensource.apple.com/source/CF/CF-550.29/CFBinaryPList.c
109 | // (using Read instead of ReadByte so that we can accept a ReadSeeker)
110 | b := make([]byte, 1)
111 | if _, err := bp.Read(b); err != nil {
112 | return nil, err
113 | }
114 | marker := b[0]
115 | switch marker >> 4 {
116 | case 0x0: // null, bool, or fill
117 | return bp.parseSingleton(marker)
118 | case 0x1: // integer
119 | return bp.parseInteger(marker)
120 | case 0x2: // real
121 | return bp.parseReal(marker)
122 | case 0x3: // date
123 | return bp.parseDate(marker)
124 | case 0x4: // data
125 | return bp.parseData(marker)
126 | case 0x5: // ascii string
127 | return bp.parseASCII(marker)
128 | case 0x6: // unicode (utf-16) string
129 | return bp.parseUTF16(marker)
130 | case 0x8: // uid (not supported)
131 | return &plistValue{Invalid, nil}, nil
132 | case 0xa: // array
133 | return bp.parseArray(marker)
134 | case 0xc: // set (not supported)
135 | return &plistValue{Invalid, nil}, nil
136 | case 0xd: // dictionary
137 | return bp.parseDict(marker)
138 | }
139 | return nil, fmt.Errorf("plist: unknown object type %x", marker>>4)
140 | }
141 |
142 | func (bp *binaryParser) parseSingleton(marker byte) (*plistValue, error) {
143 | switch marker & 0xf {
144 | case 0x0: // null (not supported)
145 | return &plistValue{Invalid, nil}, nil
146 | case 0x8: // bool false
147 | return &plistValue{Boolean, false}, nil
148 | case 0x9: // bool true
149 | return &plistValue{Boolean, true}, nil
150 | case 0xf: // fill (not supported)
151 | return &plistValue{Invalid, nil}, nil
152 | }
153 | return nil, fmt.Errorf("plist: unrecognized singleton type %x", marker&0xf)
154 | }
155 |
156 | func (bp *binaryParser) parseInteger(marker byte) (*plistValue, error) {
157 | // Integers are always stored as signed 64-bit integer, with leading zeros
158 | // removed, so that the serialized form is either 1, 2, 4, or 8 bytes in
159 | // length.
160 | // See: https://opensource.apple.com/source/CF/CF-550.29/CFBinaryPList.c
161 | //
162 | // There is some discussion regarding 128-bit number support in the Python
163 | // bug report below. The conclusion was that public APIs only allow plists
164 | // to contain up to 64-bit values.
165 | //
166 | // There is also an example of a bplist containing an unsigned value
167 | // 0xffffffffffffffff [1], but when you look at its encoding, it is crafted
168 | // using a 128bit field, with the upper 8 bytes all zeros. Loading it into
169 | // the Xcode plist editor, it appears as -1! That must be a cosmetic bug
170 | // in Xcode because if you then export the same plist as XML, you get the full
171 | // value, 18446744073709551615 [2].
172 | //
173 | // So that we can decode such bplists, we will allow 16-byte integer values,
174 | // but they will always be truncated to 64 bits.
175 | //
176 | // If you try and create a new plist in the Xcode editor, and paste in the
177 | // 64-bit number 18446744073709551615, Xcode automatically rewrites it as
178 | // the truncated 63-bit number 9223372036854775807.
179 | //
180 | // Separately, if you now create another new plist in the Xcode editor with
181 | // the value -1, you get [3] and [4]: both negative values.
182 | //
183 | // [1] "hand crafted" bplist with 128-bit value.
184 | // 00000000 62 70 6c 69 73 74 30 30 d1 01 02 53 6b 65 79 14 |bplist00...Skey.|
185 | // 00000010 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff |................|
186 | // 00000020 08 0b 0f 00 00 00 00 00 00 01 01 00 00 00 00 00 |................|
187 | // 00000030 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
188 | // 00000040 00 00 20 |.. |
189 | //
190 | // [2] export of [1] to XML form, using Xcode.
191 | //
192 | //
193 | //
194 | //
195 | // key
196 | // 18446744073709551615
197 | //
198 | //
199 | //
200 | // [3] bplist with a value of -1, created with Xcode.
201 | // 00000000 62 70 6c 69 73 74 30 30 d1 01 02 53 6b 65 79 13 |bplist00...Skey.|
202 | // 00000010 ff ff ff ff ff ff ff ff 08 0b 0f 00 00 00 00 00 |................|
203 | // 00000020 00 01 01 00 00 00 00 00 00 00 03 00 00 00 00 00 |................|
204 | // 00000030 00 00 00 00 00 00 00 00 00 00 18 |...........|
205 | //
206 | // [4] export of [3] to XML form, using Xcode.
207 | //
208 | //
209 | //
210 | //
211 | // key
212 | // -1
213 | //
214 | //
215 | //
216 | // We have two choices here: restrict unsigned values to 63-bits, or just
217 | // let the package user interpret the serialized value. This implementation
218 | // lets the user decide: if they want to unmarshal into a uint64 value,
219 | // we give them the full 8-bytes as such. If they unmarshal into a signed
220 | // int64 value, we again give them the full 8-bytes as such, which will be
221 | // interpreted as negative if the top bit is set. This is achieved by
222 | // setting the signed field of the signedInt value below to false
223 | // unconditionally. That way, the Decoder.unmarshalInteger method will do
224 | // the right thing.
225 | //
226 | // For XML property list unmarshaling, the presence of the "negative sign"
227 | // on an integer value makes the above unambigious, and the current practice
228 | // of using signedInt.signed = true in that case remains valid; see the
229 | // xmlParser.parseInteger implementation.
230 | //
231 | // See: https://bugs.python.org/issue14455
232 | nbytes := 1 << (marker & 0xf)
233 | if nbytes > 16 {
234 | return nil, fmt.Errorf("plist: cannot decode integers longer than 16 bytes (%d)", nbytes)
235 | }
236 | // Read into the right-most bytes of a 16-byte zero-valued buffer.
237 | buf := make([]byte, 16)
238 | _, err := bp.Read(buf[16-nbytes:])
239 | if err != nil {
240 | return nil, err
241 | }
242 | // Truncate values to 64 bits (8 bytes), and treat them all as "unsigned",
243 | // so they can be unmarshaled to unsigned and signed integers alike as
244 | // discussed above.
245 | result := signedInt{binary.BigEndian.Uint64(buf[8:]), false}
246 |
247 | return &plistValue{Integer, result}, nil
248 | }
249 |
250 | func (bp *binaryParser) parseReal(marker byte) (*plistValue, error) {
251 | nbytes := 1 << (marker & 0xf)
252 | buf := make([]byte, nbytes)
253 | if _, err := bp.Read(buf); err != nil {
254 | return nil, err
255 | }
256 | var r float64
257 | if err := binary.Read(bytes.NewReader(buf), binary.BigEndian, &r); err != nil {
258 | return nil, err
259 | }
260 | return &plistValue{Real, sizedFloat{r, nbytes * 8}}, nil
261 | }
262 |
263 | func (bp *binaryParser) parseDate(marker byte) (*plistValue, error) {
264 | if marker&0xf != 0x3 {
265 | return nil, fmt.Errorf("plist: invalid marker byte for date: %x", marker)
266 | }
267 | buf := make([]byte, 8)
268 | if _, err := bp.Read(buf); err != nil {
269 | return nil, err
270 | }
271 | var t float64
272 | if err := binary.Read(bytes.NewReader(buf), binary.BigEndian, &t); err != nil {
273 | return nil, err
274 | }
275 | // The float time is Apple Epoch time (secs since Jan 1, 2001 GMT) but we
276 | // need to convert it to Unix Epoch time (secs since Jan 1, 1970 GMT)
277 | t += 978307200
278 | secs := int64(t)
279 | nsecs := int64((t - float64(secs)) * 1e9)
280 | return &plistValue{Date, time.Unix(secs, nsecs)}, nil
281 | }
282 |
283 | func (bp *binaryParser) parseData(marker byte) (*plistValue, error) {
284 | count, err := bp.readCount(marker)
285 | if err != nil {
286 | return nil, err
287 | }
288 | buf := make([]byte, count)
289 | if _, err := bp.Read(buf); err != nil {
290 | return nil, err
291 | }
292 | return &plistValue{Data, buf}, nil
293 | }
294 |
295 | func (bp *binaryParser) parseASCII(marker byte) (*plistValue, error) {
296 | count, err := bp.readCount(marker)
297 | if err != nil {
298 | return nil, err
299 | }
300 | buf := make([]byte, count)
301 | if _, err := bp.Read(buf); err != nil {
302 | return nil, err
303 | }
304 | return &plistValue{String, string(buf)}, nil
305 | }
306 |
307 | func (bp *binaryParser) parseUTF16(marker byte) (*plistValue, error) {
308 | count, err := bp.readCount(marker)
309 | if err != nil {
310 | return nil, err
311 | }
312 | // Each character in the UTF16 string is 2 bytes. First we read everything
313 | // into a byte slice, then convert this into a slice of uint16, then this
314 | // gets converted into a slice of rune, which gets converted to a string.
315 | buf := make([]byte, 2*count)
316 | if _, err := bp.Read(buf); err != nil {
317 | return nil, err
318 | }
319 | uni := make([]uint16, count)
320 | if err := binary.Read(bytes.NewReader(buf), binary.BigEndian, uni); err != nil {
321 | return nil, err
322 | }
323 | return &plistValue{String, string(utf16.Decode(uni))}, nil
324 | }
325 |
326 | func (bp *binaryParser) parseArray(marker byte) (*plistValue, error) {
327 | count, err := bp.readCount(marker)
328 | if err != nil {
329 | return nil, err
330 | }
331 | // A list of count object refs representing the items in the array follow.
332 | list, err := bp.readObjectList(count)
333 | if err != nil {
334 | return nil, err
335 | }
336 | return &plistValue{Array, list}, nil
337 | }
338 |
339 | func (bp *binaryParser) parseDict(marker byte) (*plistValue, error) {
340 | count, err := bp.readCount(marker)
341 | if err != nil {
342 | return nil, err
343 | }
344 | // A list of 2*count object refs follow. All of the keys are listed first,
345 | // followed by all of the values.
346 | keys, err := bp.readObjectList(count)
347 | if err != nil {
348 | return nil, err
349 | }
350 | vals, err := bp.readObjectList(count)
351 | if err != nil {
352 | return nil, err
353 | }
354 | m := make(map[string]*plistValue)
355 | for i := uint64(0); i < count; i++ {
356 | if keys[i].kind != String {
357 | return nil, fmt.Errorf("plist: dictionary key is not a string: %v", keys[i])
358 | }
359 | m[keys[i].value.(string)] = vals[i]
360 | }
361 | return &plistValue{Dictionary, &dictionary{m: m}}, nil
362 | }
363 |
364 | // readCount reads the variable-length encoded integer count
365 | // used by data, strings, arrays, and dicts
366 | func (bp *binaryParser) readCount(marker byte) (uint64, error) {
367 | // Check marker for count < 15 in lower 4 bits.
368 | if marker&0xf != 0xf {
369 | return uint64(marker & 0xf), nil
370 | }
371 | // Otherwise must read additional bytes to get count. Read first byte:
372 | // (using Read instead of ReadByte so that we can accept a ReadSeeker)
373 | b := make([]byte, 1)
374 | if _, err := bp.Read(b); err != nil {
375 | return 0, err
376 | }
377 | first := b[0]
378 | // The lower 4 bits of indicate how many additional bytes to read:
379 | // 0 means 1 additional byte
380 | // 1 means 2 additional bytes
381 | // 2 means 4 additional bytes
382 | // 3 means 8 additional bytes
383 | nbytes := 1 << (first & 0x0f)
384 | // Number of bytes in count should be at most 8.
385 | if nbytes > 8 {
386 | return 0, fmt.Errorf("plist: invalid nbytes (%d) in readCount", nbytes)
387 | }
388 | buf := make([]byte, 8)
389 | // Shove these bytes into the low end of an 8-byte buffer.
390 | if _, err := bp.Read(buf[8-nbytes:]); err != nil {
391 | return 0, err
392 | }
393 | return binary.BigEndian.Uint64(buf), nil
394 | }
395 |
396 | // readObjectRef reads the next ObjectRefSize bytes from the binary plist
397 | // and returns the bytes decoded into an integer value.
398 | func (bp *binaryParser) readObjectRef() (uint64, error) {
399 | buf := make([]byte, 8)
400 | if _, err := bp.Read(buf[8-bp.ObjectRefSize:]); err != nil {
401 | return 0, err
402 | }
403 | return binary.BigEndian.Uint64(buf), nil
404 | }
405 |
406 | // readObjectList is a helper function for parseArray and parseDict.
407 | // It decodes a sequence of object refs from the current offset in the plist
408 | // and returns the decoded objects in a slice.
409 | func (bp *binaryParser) readObjectList(count uint64) ([]*plistValue, error) {
410 | list := make([]*plistValue, count)
411 | for i := uint64(0); i < count; i++ {
412 | // Read index of object in offset table.
413 | ref, err := bp.readObjectRef()
414 | if err != nil {
415 | return nil, err
416 | }
417 | // Find and decode the object in object table, then add it to list.
418 | v, err := bp.parseObjectRef(ref)
419 | if err != nil {
420 | return nil, err
421 | }
422 | list[i] = v
423 | }
424 | return list, nil
425 | }
426 |
--------------------------------------------------------------------------------
/decode.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "reflect"
9 | "time"
10 | )
11 |
12 | // MarshalFunc is a function used to Unmarshal custom plist types.
13 | type MarshalFunc func(interface{}) error
14 |
15 | // Unmarshaler is the interface implemented by types that can unmarshal
16 | // themselves from property list objects. The UnmarshalPlist method
17 | // receives a function that may be called to unmarshal the original
18 | // property list value into a field or variable.
19 | //
20 | // It is safe to call the unmarshal function more than once.
21 | type Unmarshaler interface {
22 | UnmarshalPlist(f func(interface{}) error) error
23 | }
24 |
25 | // Unmarshal parses the plist-encoded data and stores the result in the value pointed to by v.
26 | func Unmarshal(data []byte, v interface{}) error {
27 | // Check for binary plist here before setting up the decoder.
28 | if bytes.HasPrefix(data, []byte("bplist0")) {
29 | return NewBinaryDecoder(bytes.NewReader(data)).Decode(v)
30 | }
31 | return NewXMLDecoder(bytes.NewReader(data)).Decode(v)
32 | }
33 |
34 | // A Decoder reads and decodes Apple plist objects from an input stream.
35 | // The plists can be in XML or binary format.
36 | type Decoder struct {
37 | reader io.Reader // binary decoders assert this to io.ReadSeeker
38 | isBinary bool // true if this is a binary plist
39 | }
40 |
41 | // NewDecoder returns a new XML plist decoder.
42 | // DEPRECATED: Please use NewXMLDecoder instead.
43 | func NewDecoder(r io.Reader) *Decoder {
44 | return NewXMLDecoder(r)
45 | }
46 |
47 | // NewXMLDecoder returns a new decoder that reads an XML plist from r.
48 | func NewXMLDecoder(r io.Reader) *Decoder {
49 | return &Decoder{reader: r, isBinary: false}
50 | }
51 |
52 | // NewBinaryDecoder returns a new decoder that reads a binary plist from r.
53 | // No error checking is done to make sure that r is actually a binary plist.
54 | func NewBinaryDecoder(r io.ReadSeeker) *Decoder {
55 | return &Decoder{reader: r, isBinary: true}
56 | }
57 |
58 | // Decode reads the next plist-encoded value from its input and stores it in
59 | // the value pointed to by v. Decode uses xml.Decoder to do the heavy lifting
60 | // for XML plists, and uses binaryParser for binary plists.
61 | func (d *Decoder) Decode(v interface{}) error {
62 | val := reflect.ValueOf(v)
63 | if val.Kind() != reflect.Ptr {
64 | return errors.New("plist: non-pointer passed to Unmarshal")
65 | }
66 | var pval *plistValue
67 | if d.isBinary {
68 | // For binary decoder, type assert the reader to an io.ReadSeeker
69 | var err error
70 | r, ok := d.reader.(io.ReadSeeker)
71 | if !ok {
72 | return fmt.Errorf("binary plist decoder requires an io.ReadSeeker")
73 | }
74 | parser, err := newBinaryParser(r)
75 | if err != nil {
76 | return err
77 | }
78 | pval, err = parser.parseDocument()
79 | if err != nil {
80 | return err
81 | }
82 | } else {
83 | var err error
84 | parser := newXMLParser(d.reader)
85 | pval, err = parser.parseDocument(nil)
86 | if err != nil {
87 | return err
88 | }
89 | }
90 | return d.unmarshal(pval, val.Elem())
91 | }
92 |
93 | func (d *Decoder) unmarshal(pval *plistValue, v reflect.Value) error {
94 | if v.Kind() == reflect.Ptr {
95 | if v.IsNil() {
96 | v.Set(reflect.New(v.Type().Elem()))
97 | }
98 | v = v.Elem()
99 | }
100 |
101 | // check for empty interface v type
102 | if v.Kind() == reflect.Interface && v.NumMethod() == 0 {
103 | val := reflect.ValueOf(d.valueInterface(pval))
104 | if !val.IsValid() {
105 | return fmt.Errorf("plist: invalid reflect.Value %v", v)
106 | }
107 | v.Set(val)
108 | return nil
109 | }
110 |
111 | unmarshalerType := reflect.TypeOf((*Unmarshaler)(nil)).Elem()
112 |
113 | if v.CanInterface() && v.Type().Implements(unmarshalerType) {
114 | u := v.Interface().(Unmarshaler)
115 | return u.UnmarshalPlist(func(i interface{}) error {
116 | return d.unmarshal(pval, reflect.ValueOf(i))
117 | })
118 | }
119 |
120 | if v.CanAddr() {
121 | pv := v.Addr()
122 | if pv.CanInterface() && pv.Type().Implements(unmarshalerType) {
123 | u := pv.Interface().(Unmarshaler)
124 | return u.UnmarshalPlist(func(i interface{}) error {
125 | return d.unmarshal(pval, reflect.ValueOf(i))
126 | })
127 | }
128 |
129 | }
130 |
131 | // change pointer values to the correct type
132 | // ex type foo struct {
133 | // Foo *string `plist:"foo"
134 | // }
135 | if v.Kind() == reflect.Ptr {
136 | if v.IsNil() {
137 | v.Set(reflect.New(v.Type().Elem()))
138 | }
139 | v = v.Elem()
140 |
141 | }
142 |
143 | switch pval.kind {
144 | case String:
145 | return d.unmarshalString(pval, v)
146 | case Dictionary:
147 | return d.unmarshalDictionary(pval, v)
148 | case Array:
149 | return d.unmarshalArray(pval, v)
150 | case Boolean:
151 | return d.unmarshalBoolean(pval, v)
152 | case Real:
153 | return d.unmarshalReal(pval, v)
154 | case Integer:
155 | return d.unmarshalInteger(pval, v)
156 | case Data:
157 | return d.unmarshalData(pval, v)
158 | case Date:
159 | return d.unmarshalDate(pval, v)
160 | default:
161 | return fmt.Errorf("plist: %v is an unsuported plist element kind", pval.kind)
162 | }
163 | }
164 |
165 | func (d *Decoder) unmarshalDate(pval *plistValue, v reflect.Value) error {
166 | if v.Type() != reflect.TypeOf((*time.Time)(nil)).Elem() {
167 | return UnmarshalTypeError{fmt.Sprintf("%v", pval.value), v.Type()}
168 | }
169 | v.Set(reflect.ValueOf(pval.value.(time.Time)))
170 | return nil
171 | }
172 |
173 | func (d *Decoder) unmarshalData(pval *plistValue, v reflect.Value) error {
174 | if v.Kind() != reflect.Slice || v.Type().Elem().Kind() != reflect.Uint8 {
175 | return UnmarshalTypeError{fmt.Sprintf("%s", pval.value.([]byte)), v.Type()}
176 | }
177 | v.SetBytes(pval.value.([]byte))
178 | return nil
179 | }
180 |
181 | func (d *Decoder) unmarshalReal(pval *plistValue, v reflect.Value) error {
182 | if v.Kind() != reflect.Float32 && v.Kind() != reflect.Float64 {
183 | return UnmarshalTypeError{fmt.Sprintf("%v", pval.value.(sizedFloat).value), v.Type()}
184 | }
185 | v.SetFloat(pval.value.(sizedFloat).value)
186 | return nil
187 | }
188 |
189 | func (d *Decoder) unmarshalBoolean(pval *plistValue, v reflect.Value) error {
190 | if v.Kind() != reflect.Bool {
191 | return UnmarshalTypeError{fmt.Sprintf("%v", pval.value), v.Type()}
192 | }
193 | v.SetBool(pval.value.(bool))
194 | return nil
195 | }
196 |
197 | func (d *Decoder) unmarshalDictionary(pval *plistValue, v reflect.Value) error {
198 | subvalues := pval.value.(*dictionary).m
199 | switch v.Kind() {
200 | case reflect.Struct:
201 | fields := cachedTypeFields(v.Type())
202 | for _, field := range fields {
203 | if _, ok := subvalues[field.name]; !ok {
204 | continue
205 | }
206 | if err := d.unmarshal(subvalues[field.name], field.value(v)); err != nil {
207 | return err
208 | }
209 | }
210 | case reflect.Map:
211 | if v.IsNil() {
212 | v.Set(reflect.MakeMap(v.Type()))
213 | }
214 | for k, sval := range subvalues {
215 | keyv := reflect.ValueOf(k).Convert(v.Type().Key())
216 | mapElem := v.MapIndex(keyv)
217 | if !mapElem.IsValid() {
218 | mapElem = reflect.New(v.Type().Elem()).Elem()
219 | }
220 | if err := d.unmarshal(sval, mapElem); err != nil {
221 | return err
222 | }
223 | v.SetMapIndex(keyv, mapElem)
224 | }
225 | default:
226 | return UnmarshalTypeError{"dict", v.Type()}
227 | }
228 | return nil
229 | }
230 |
231 | func (d *Decoder) unmarshalString(pval *plistValue, v reflect.Value) error {
232 | if v.Kind() != reflect.String {
233 | return UnmarshalTypeError{fmt.Sprintf("%s", pval.value.(string)), v.Type()}
234 | }
235 | v.SetString(pval.value.(string))
236 | return nil
237 | }
238 |
239 | func (d *Decoder) unmarshalArray(pval *plistValue, v reflect.Value) error {
240 | subvalues := pval.value.([]*plistValue)
241 | switch v.Kind() {
242 | case reflect.Slice:
243 | // Slice of element values.
244 | // Grow slice.
245 | // Borrowed from https://golang.org/src/encoding/xml/read.go
246 | cnt := len(subvalues)
247 | if cnt >= v.Cap() {
248 | ncap := 2 * cnt
249 | if ncap < 4 {
250 | ncap = 4
251 | }
252 | new := reflect.MakeSlice(v.Type(), v.Len(), ncap)
253 | reflect.Copy(new, v)
254 | v.Set(new)
255 | }
256 | n := v.Len()
257 | v.SetLen(cnt)
258 | for _, sval := range subvalues {
259 | if err := d.unmarshal(sval, v.Index(n)); err != nil {
260 | v.SetLen(cnt)
261 | return err
262 | }
263 | n++
264 | }
265 | default:
266 | return UnmarshalTypeError{"array", v.Type()}
267 | }
268 | return nil
269 | }
270 |
271 | func (d *Decoder) unmarshalInteger(pval *plistValue, v reflect.Value) error {
272 | switch v.Kind() {
273 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
274 | v.SetInt(int64(pval.value.(signedInt).value))
275 | case reflect.Uint, reflect.Uint8, reflect.Uint16,
276 | reflect.Uint32, reflect.Uint64, reflect.Uintptr:
277 | // Make sure plistValue isn't negative when decoding into uint.
278 | if pval.value.(signedInt).signed {
279 | return UnmarshalTypeError{
280 | fmt.Sprintf("%v", int64(pval.value.(signedInt).value)), v.Type()}
281 | }
282 | v.SetUint(pval.value.(signedInt).value)
283 | default:
284 | return UnmarshalTypeError{
285 | fmt.Sprintf("%v", pval.value.(signedInt).value), v.Type()}
286 | }
287 | return nil
288 | }
289 |
290 | // empty interface values
291 | // borrowed from go-plist
292 | func (d *Decoder) valueInterface(pval *plistValue) interface{} {
293 | switch pval.kind {
294 | case String:
295 | return pval.value.(string)
296 | case Integer:
297 | if pval.value.(signedInt).signed {
298 | return int64(pval.value.(signedInt).value)
299 | }
300 | return pval.value.(signedInt).value
301 | case Real:
302 | bits := pval.value.(sizedFloat).bits
303 | switch bits {
304 | case 32:
305 | return float32(pval.value.(sizedFloat).value)
306 | case 64:
307 | return pval.value.(sizedFloat).value
308 | default:
309 | return nil
310 | }
311 | case Boolean:
312 | return pval.value.(bool)
313 | case Array:
314 | return d.arrayInterface(pval.value.([]*plistValue))
315 | case Dictionary:
316 | return d.dictionaryInterface(pval.value.(*dictionary))
317 | case Data:
318 | return pval.value.([]byte)
319 | case Date:
320 | return pval.value.(time.Time)
321 | default:
322 | return nil
323 | }
324 | }
325 |
326 | func (d *Decoder) arrayInterface(subvalues []*plistValue) []interface{} {
327 | out := make([]interface{}, len(subvalues))
328 | for i, subv := range subvalues {
329 | out[i] = d.valueInterface(subv)
330 | }
331 | return out
332 | }
333 |
334 | func (d *Decoder) dictionaryInterface(dict *dictionary) map[string]interface{} {
335 | out := make(map[string]interface{})
336 | for k, subv := range dict.m {
337 | out[k] = d.valueInterface(subv)
338 | }
339 | return out
340 | }
341 |
342 | // An UnmarshalTypeError describes a plist value that was
343 | // not appropriate for a value of a specific Go type.
344 | type UnmarshalTypeError struct {
345 | Value string // description of plist value - "true", "string", "date"
346 | Type reflect.Type
347 | }
348 |
349 | func (e UnmarshalTypeError) Error() string {
350 | return "plist: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
351 | }
352 |
--------------------------------------------------------------------------------
/decode_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "io"
7 | "io/ioutil"
8 | "log"
9 | "net/http"
10 | "net/http/httptest"
11 | "os/exec"
12 | "path/filepath"
13 | "reflect"
14 | "strings"
15 | "testing"
16 | "time"
17 | )
18 |
19 | var decodeTests = []struct {
20 | out interface{}
21 | in string
22 | }{
23 | {"foo", fooRef},
24 | {"UTF-8 ☼", utf8Ref},
25 | {uint64(0), zeroRef},
26 | {uint64(1), oneRef},
27 | {1.2, realRef},
28 | {false, falseRef},
29 | {true, trueRef},
30 | {[]interface{}{"a", "b", "c", uint64(4), true}, arrRef},
31 | {time.Date(1900, 01, 01, 12, 00, 00, 0, time.UTC), time1900Ref},
32 | {map[string]interface{}{
33 | "foo": "bar",
34 | "bool": true},
35 | dictRef},
36 | }
37 |
38 | func TestDecodeEmptyInterface(t *testing.T) {
39 | for _, tt := range decodeTests {
40 | var out interface{}
41 | if err := Unmarshal([]byte(tt.in), &out); err != nil {
42 | t.Error(err)
43 | continue
44 | }
45 | eq := reflect.DeepEqual(out, tt.out)
46 | if !eq {
47 | t.Errorf("Unmarshal(%v) = \n%v, want %v", tt.in, out, tt.out)
48 | }
49 | }
50 | }
51 |
52 | func TestDecodeDict(t *testing.T) {
53 | // Test struct
54 | expected := struct {
55 | InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
56 | BandSize uint64 `plist:"band-size"`
57 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
58 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
59 | Size uint64 `plist:"size"`
60 | }{
61 | InfoDictionaryVersion: "6.0",
62 | BandSize: 8388608,
63 | Size: 4 * 1048576 * 1024 * 1024,
64 | DiskImageBundleType: "com.apple.diskimage.sparsebundle",
65 | BackingStoreVersion: 1,
66 | }
67 |
68 | var sparseBundleHeader struct {
69 | InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
70 | BandSize uint64 `plist:"band-size"`
71 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
72 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
73 | Size uint64 `plist:"size"`
74 | }
75 |
76 | if err := Unmarshal([]byte(indentRef), &sparseBundleHeader); err != nil {
77 | t.Fatal(err)
78 | }
79 | if sparseBundleHeader != expected {
80 | t.Error("Expected", expected, "got", sparseBundleHeader)
81 | }
82 |
83 | // Test Map
84 | var mapHeader = map[string]interface{}{}
85 | // Output map[CFBundleInfoDictionaryVersion:6.0 band-size:8388608 bundle-backingstore-version:1 diskimage-bundle-type:com.apple.diskimage.sparsebundle size:4398046511104]
86 | if err := Unmarshal([]byte(indentRef), &mapHeader); err != nil {
87 | t.Fatal(err)
88 | }
89 | if mapHeader["CFBundleInfoDictionaryVersion"] != "6.0" {
90 | t.Fatal("Expected", "6.0", "got", mapHeader["CFBundleInfoDictionaryVersion"])
91 | }
92 | }
93 |
94 | func TestDecodeArray(t *testing.T) {
95 | const input = `
96 |
97 | foobar`
98 | var data []string
99 | expected := []string{"foo", "bar"}
100 | if err := Unmarshal([]byte(input), &data); err != nil {
101 | t.Fatal(err)
102 | }
103 | if eq := reflect.DeepEqual(data, expected); !eq {
104 | t.Error("Expected", expected, "got", data)
105 | }
106 | }
107 |
108 | func TestDecodeBoolean(t *testing.T) {
109 | const input = `
110 |
111 | `
112 | var data bool
113 | expected := true
114 | if err := Unmarshal([]byte(input), &data); err != nil {
115 | t.Fatal(err)
116 | }
117 | if data != expected {
118 | t.Error("Expected", expected, "got", data)
119 | }
120 | }
121 |
122 | func TestDecodeLargeInteger(t *testing.T) {
123 | const input = `
124 |
125 | 18446744073709551615`
126 | var data uint64
127 | expected := uint64(18446744073709551615)
128 | if err := Unmarshal([]byte(input), &data); err != nil {
129 | t.Fatal(err)
130 | }
131 | if data != expected {
132 | t.Error("Expected", expected, "got", data)
133 | }
134 | }
135 |
136 | func TestDecodeNegativeInteger(t *testing.T) {
137 | // There is an intentional space before -42.
138 | const input = `
139 |
140 | -42`
141 | var data int
142 | expected := -42
143 | if err := Unmarshal([]byte(input), &data); err != nil {
144 | t.Fatal(err)
145 | }
146 | if data != expected {
147 | t.Error("Expected", expected, "got", data)
148 | }
149 | }
150 |
151 | func TestDecodeNegativeIntegerIntoUint(t *testing.T) {
152 | const input = `
153 |
154 | -42`
155 | var data uint
156 | if err := Unmarshal([]byte(input), &data); err == nil {
157 | t.Error("Expected error, but unmarshal gave", data)
158 | }
159 | }
160 |
161 | func TestDecodeLargeNegativeInteger(t *testing.T) {
162 | const input = `
163 |
164 | -9223372036854775808`
165 | var data int64
166 | expected := int64(-9223372036854775808)
167 | if err := Unmarshal([]byte(input), &data); err != nil {
168 | t.Fatal(err)
169 | }
170 | if data != expected {
171 | t.Error("Expected", expected, "got", data)
172 | }
173 | }
174 |
175 | func TestDecodeReal(t *testing.T) {
176 | const input = `
177 |
178 | 1.2`
179 | var data float64
180 | expected := 1.2
181 | if err := Unmarshal([]byte(input), &data); err != nil {
182 | t.Fatal(err)
183 | }
184 | if data != expected {
185 | t.Error("Expected", expected, "got", data)
186 | }
187 | }
188 |
189 | func TestDecodeNegativeReal(t *testing.T) {
190 | const input = `
191 |
192 | -3.14159`
193 | var data float64
194 | expected := -3.14159
195 | if err := Unmarshal([]byte(input), &data); err != nil {
196 | t.Fatal(err)
197 | }
198 | if data != expected {
199 | t.Error("Expected", expected, "got", data)
200 | }
201 | }
202 |
203 | func TestDecodeDate(t *testing.T) {
204 | const input = `
205 |
206 |
207 | 2011-05-12T01:00:00Z
208 | `
209 | var data time.Time
210 | expected, _ := time.Parse(time.RFC3339, "2011-05-12T01:00:00Z")
211 | if err := Unmarshal([]byte(input), &data); err != nil {
212 | t.Fatal(err)
213 | }
214 | if data != expected {
215 | t.Error("Expected", expected, "got", data)
216 | }
217 | }
218 |
219 | func TestDecodeData(t *testing.T) {
220 | expected := `
221 |
222 | foo
223 | `
224 | type data []byte
225 | out := data{}
226 | if err := Unmarshal([]byte(dataRef), &out); err != nil {
227 | t.Fatal(err)
228 | }
229 | if string(out) != expected {
230 | t.Error("Want:\n", expected, "\ngot:\n", string(out))
231 | }
232 | }
233 |
234 | func TestDecodeData_emptyData(t *testing.T) {
235 | var before, after []byte
236 | if err := Unmarshal([]byte(emptyDataRef), &after); err != nil {
237 | t.Fatal(err)
238 | }
239 | if !reflect.DeepEqual(before, after) {
240 | t.Log("empty should result in []byte(nil)")
241 | t.Errorf("before %#v, after %#v", before, after)
242 | }
243 | }
244 |
245 | func TestDecodeUnicodeString(t *testing.T) {
246 | const input = `
247 |
248 | こんにちは世界`
249 | var data string
250 | expected := "こんにちは世界"
251 | if err := Unmarshal([]byte(input), &data); err != nil {
252 | t.Fatal(err)
253 | }
254 | if data != expected {
255 | t.Error("Expected", expected, "got", data)
256 | }
257 | }
258 |
259 | // Unknown struct fields should return an error
260 | func TestDecodeUnknownStructField(t *testing.T) {
261 | var sparseBundleHeader struct {
262 | InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
263 | BandSize uint64 `plist:"band-size"`
264 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
265 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
266 | Size uint64 `plist:"unknownKey"`
267 | }
268 | if err := Unmarshal([]byte(indentRef), &sparseBundleHeader); err != nil {
269 | t.Error("Expected error `plist: unknown struct field unknownKey`, got nil")
270 | }
271 | }
272 |
273 | func TestHTTPDecoding(t *testing.T) {
274 | const raw = `
275 |
276 | bar`
277 |
278 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
279 | w.Write([]byte(raw))
280 | }))
281 | defer ts.Close()
282 | res, err := http.Get(ts.URL)
283 | if err != nil {
284 | log.Fatalf("GET failed: %v", err)
285 | }
286 | defer res.Body.Close()
287 | var foo string
288 | d := NewDecoder(res.Body)
289 | err = d.Decode(&foo)
290 | if err != nil {
291 | t.Fatalf("Decode: %v", err)
292 | }
293 | if foo != "bar" {
294 | t.Errorf("decoded %q; want \"bar\"", foo)
295 | }
296 | err = d.Decode(&foo)
297 | if err != io.EOF {
298 | t.Errorf("err = %v; want io.EOF", err)
299 | }
300 | }
301 |
302 | func TestDecodePointer(t *testing.T) {
303 | var sparseBundleHeader struct {
304 | InfoDictionaryVersion *string `plist:"CFBundleInfoDictionaryVersion"`
305 | BandSize *uint64 `plist:"band-size"`
306 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
307 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
308 | Size uint64 `plist:"unknownKey"`
309 | }
310 | if err := Unmarshal([]byte(indentRef), &sparseBundleHeader); err != nil {
311 | t.Fatal(err)
312 | }
313 | if *sparseBundleHeader.InfoDictionaryVersion != "6.0" {
314 | t.Error("Expected", "6.0", "got", *sparseBundleHeader.InfoDictionaryVersion)
315 | }
316 | }
317 |
318 | func TestDecodeBinaryPlist(t *testing.T) {
319 | tests := []struct {
320 | filename string
321 | expectedInts []int64
322 | }{
323 | {
324 | filename: "sample2.binary.plist",
325 | expectedInts: []int64{0, 42, -42, 255, -255, -123456, -9223372036854775807, 9223372036854775807},
326 | },
327 | }
328 |
329 | for _, tt := range tests {
330 | t.Run(tt.filename, func(t *testing.T) {
331 | var sample struct {
332 | Ints []int64 `plist:"ints"`
333 | Signed int64 `plist:"signed"`
334 | Unsigned uint64 `plist:"unsigned"`
335 | Uint64 uint64 `plist:"uint64"`
336 | Reals []float64 `plist:"reals"`
337 | Date time.Time `plist:"date"`
338 | Strings []string `plist:"strings"`
339 | Data [][]byte `plist:"data"`
340 | }
341 |
342 | content, err := ioutil.ReadFile(filepath.Join("testdata", tt.filename))
343 | if err != nil {
344 | t.Fatal(err)
345 | }
346 |
347 | if err := Unmarshal(content, &sample); err != nil {
348 | t.Fatal(err)
349 | }
350 |
351 | if got, want := len(sample.Ints), len(tt.expectedInts); got != want {
352 | t.Errorf("decoded %d ints, want %d", got, want)
353 | }
354 |
355 | for i, x := range tt.expectedInts {
356 | if sample.Ints[i] != x {
357 | t.Error("expected", x, "got", sample.Ints[i])
358 | }
359 | }
360 |
361 | expectedUnsigned := uint64(1<<63 - 1)
362 | if sample.Unsigned != expectedUnsigned {
363 | t.Error("expected", expectedUnsigned, "got", sample.Unsigned)
364 | }
365 |
366 | expectedSigned := int64(-1)
367 | if sample.Signed != expectedSigned {
368 | t.Error("expected", expectedSigned, "got", sample.Signed)
369 | }
370 |
371 | expectedUint64 := ^uint64(0) // all bits set
372 | if sample.Uint64 != expectedUint64 {
373 | t.Error("expected", expectedUint64, "got", sample.Uint64)
374 | }
375 |
376 | expectedReals := []float64{0.0, 3.14159, -1234.5678}
377 | if len(expectedReals) != len(sample.Reals) {
378 | t.Errorf("expected %d reals, but only decoded %d reals", len(expectedReals), len(sample.Reals))
379 | }
380 |
381 | for i, x := range expectedReals {
382 | if sample.Reals[i] != x {
383 | t.Error("expected", x, "got", sample.Reals[i])
384 | }
385 | }
386 |
387 | expectedDate, _ := time.Parse(time.RFC3339, "2038-01-19T03:14:08Z")
388 | if !sample.Date.Equal(expectedDate) {
389 | t.Error("expected", expectedDate, "got", sample.Date)
390 | }
391 |
392 | expectedStrings := []string{
393 | "short",
394 | "こんにちは世界",
395 | "this is a much longer string having more than 14 characters",
396 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
397 | }
398 | if len(expectedStrings) != len(sample.Strings) {
399 | t.Errorf("expected %d strings, but only decoded %d strings", len(expectedStrings), len(sample.Strings))
400 | }
401 | for i, x := range expectedStrings {
402 | if sample.Strings[i] != x {
403 | t.Error("expected", x, "got", sample.Strings[i])
404 | }
405 | }
406 |
407 | expectedData := [][]byte{
408 | MustDecodeBase64("PEKBpYGlmYFCPA=="),
409 | MustDecodeBase64("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwgc2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRldXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLg=="),
410 | }
411 | if len(expectedData) != len(sample.Data) {
412 | t.Errorf("expected %d data items, but only decoded %d", len(expectedData), len(sample.Data))
413 | }
414 | for i, x := range expectedData {
415 | if !bytes.Equal(sample.Data[i], x) {
416 | t.Error("expected", x, "got", sample.Data[i])
417 | }
418 | }
419 | })
420 | }
421 | }
422 |
423 | func MustDecodeBase64(b64 string) []byte {
424 | data, err := base64.StdEncoding.DecodeString(b64)
425 | if err != nil {
426 | panic(err)
427 | }
428 | return data
429 | }
430 |
431 | type unmarshalerTest struct {
432 | unmarshalInvoked bool
433 | MustDecode string
434 | }
435 |
436 | func (u *unmarshalerTest) UnmarshalPlist(f func(i interface{}) error) error {
437 | u.unmarshalInvoked = true
438 | return f(&u.MustDecode)
439 | }
440 |
441 | func TestUnmarshaler(t *testing.T) {
442 | const raw = `
443 |
444 | bar`
445 |
446 | var u unmarshalerTest
447 | if err := Unmarshal([]byte(raw), &u); err != nil {
448 | t.Fatal(err)
449 | }
450 |
451 | if !u.unmarshalInvoked {
452 | t.Errorf("expected the UnmarshalPlist method to be invoked for unmarshaler")
453 | }
454 |
455 | if have, want := u.MustDecode, "bar"; have != want {
456 | t.Errorf("have %s, want %s", have, want)
457 | }
458 | }
459 |
460 | func TestFuzzCrashers(t *testing.T) {
461 | dir := filepath.Join("testdata", "crashers")
462 | testDir, err := ioutil.ReadDir(dir)
463 | if err != nil {
464 | t.Fatalf("reading dir %q: %s", dir, err)
465 | }
466 |
467 | for _, tc := range testDir {
468 | tc := tc
469 | t.Run(tc.Name(), func(t *testing.T) {
470 | t.Parallel()
471 |
472 | crasher, err := ioutil.ReadFile(filepath.Join("testdata", "crashers", tc.Name()))
473 | if err != nil {
474 | t.Fatal(err)
475 | }
476 |
477 | var i interface{}
478 | Unmarshal(crasher, &i)
479 | })
480 | }
481 | }
482 |
483 | func TestSmallInput(t *testing.T) {
484 | type nop struct{}
485 | nopStruct := &nop{}
486 | for _, test := range []string{
487 | "",
488 | "!",
489 | "
502 | NoTagNoTagOtherTagTagSkipTagSkipTag`
503 | // Test struct
504 | testStruct := struct {
505 | NoTag string
506 | Tag string `plist:"OtherTag"`
507 | SkipTag string `plist:"-"`
508 | }{}
509 |
510 | if err := Unmarshal([]byte(input), &testStruct); err != nil {
511 | t.Fatal(err)
512 | }
513 |
514 | if testStruct.SkipTag != "" {
515 | t.Error("field decoded when it was tagged as -")
516 | }
517 | }
518 |
519 | // TestXMLPlutilParity tests parity with plutil -lint on macOS
520 | func TestXMLPlutilParity(t *testing.T) {
521 | type data struct {
522 | Key string `plist:"key"`
523 | }
524 | tests, err := ioutil.ReadDir("testdata/xml/")
525 | if err != nil {
526 | t.Fatalf("could not open testdata/xml: %v", err)
527 | }
528 |
529 | plutil, _ := exec.LookPath("plutil")
530 |
531 | for _, test := range tests {
532 | testPath := filepath.Join("testdata/xml/", test.Name())
533 | buf, err := ioutil.ReadFile(testPath)
534 | if err != nil {
535 | t.Errorf("could not read test %s: %v", test.Name(), err)
536 | continue
537 | }
538 | v := new(data)
539 | err = Unmarshal(buf, v)
540 |
541 | shouldFail := strings.HasSuffix(test.Name(), ".failure.plist")
542 | if plutil != "" {
543 | plutilFail := exec.Command(plutil, "-lint", testPath).Run() != nil
544 | if shouldFail != plutilFail {
545 | t.Errorf("expected plutil test failure: %v for %s, but got test failure: %v", shouldFail, test.Name(), plutilFail)
546 | }
547 | }
548 |
549 | if shouldFail && err == nil {
550 | t.Errorf("expected error for test %s but got: nil", test.Name())
551 | } else if !shouldFail && err != nil {
552 | t.Errorf("expected no error for test %s but got: %v", test.Name(), err)
553 | } else if !shouldFail && v.Key != "val" {
554 | t.Errorf("expected key=val for test %s but got: key=%s", test.Name(), v.Key)
555 | }
556 | }
557 | }
558 |
559 | type testVal struct {
560 | s string
561 | b bool
562 | }
563 |
564 | func (v *testVal) UnmarshalPlist(f func(interface{}) error) (err error) {
565 | var val interface{}
566 | err = f(&val)
567 | if err != nil {
568 | return err
569 | }
570 | switch value := val.(type) {
571 | case string:
572 | v.s = value
573 | case bool:
574 | v.b = value
575 | }
576 | return nil
577 | }
578 |
579 | type nestedType struct {
580 | Val *testVal `plist:"val"`
581 | Val2 *testVal `plist:"val2"`
582 | }
583 |
584 | // TestDecodeCustomType tests decoding a type that decodes into multiple types
585 | // based on the underlying plist type
586 | func TestDecodeCustomType(t *testing.T) {
587 | p := `
588 |
589 |
590 |
591 | val
592 | val
593 | val2
594 |
595 |
596 | `
597 | r := bytes.NewBuffer([]byte(p))
598 | decoder := NewXMLDecoder(r)
599 | typ := new(nestedType)
600 | err := decoder.Decode(typ)
601 | if err != nil {
602 | t.Fatalf("could not read profile: %v", err)
603 | }
604 |
605 | if typ.Val == nil {
606 | t.Fatal("unexpected nil for typ.Val")
607 | }
608 | if have, want := typ.Val.s, "val"; have != want {
609 | t.Errorf("typ.Val: have %v, want %v", have, want)
610 | }
611 |
612 | if typ.Val2 == nil {
613 | t.Fatal("unexpected nil for typ.Val2")
614 | }
615 | if have, want := typ.Val2.b, true; have != want {
616 | t.Errorf("typ.Val2: have %v, want %v", have, want)
617 | }
618 | }
619 |
--------------------------------------------------------------------------------
/encode.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "reflect"
7 | "time"
8 | )
9 |
10 | type Marshaler interface {
11 | MarshalPlist() (interface{}, error)
12 | }
13 |
14 | // Encoder ...
15 | type Encoder struct {
16 | w io.Writer
17 |
18 | indent string
19 | }
20 |
21 | // Marshal ...
22 | func Marshal(v interface{}) ([]byte, error) {
23 | var buf bytes.Buffer
24 | if err := NewEncoder(&buf).Encode(v); err != nil {
25 | return nil, err
26 | }
27 | return buf.Bytes(), nil
28 | }
29 |
30 | // MarshalIndent ...
31 | func MarshalIndent(v interface{}, indent string) ([]byte, error) {
32 | var buf bytes.Buffer
33 | enc := NewEncoder(&buf)
34 | enc.Indent(indent)
35 | if err := enc.Encode(v); err != nil {
36 | return nil, err
37 | }
38 | return buf.Bytes(), nil
39 | }
40 |
41 | // NewEncoder returns a new encoder that writes to w.
42 | func NewEncoder(w io.Writer) *Encoder {
43 | return &Encoder{w: w}
44 | }
45 |
46 | // Encode ...
47 | func (e *Encoder) Encode(v interface{}) error {
48 | pval, err := e.marshal(reflect.ValueOf(v))
49 | if err != nil {
50 | return err
51 | }
52 |
53 | enc := newXMLEncoder(e.w)
54 | enc.indent = e.indent
55 | enc.Indent("", e.indent)
56 | return enc.generateDocument(pval)
57 | }
58 |
59 | // Indent ...
60 | func (e *Encoder) Indent(indent string) {
61 | e.indent = indent
62 | }
63 |
64 | func (e *Encoder) marshal(v reflect.Value) (*plistValue, error) {
65 | marshalerType := reflect.TypeOf((*Marshaler)(nil)).Elem()
66 |
67 | if v.CanInterface() && v.Type().Implements(marshalerType) {
68 | m := v.Interface().(Marshaler)
69 | val, err := m.MarshalPlist()
70 | if err != nil {
71 | return nil, err
72 | }
73 | return e.marshal(reflect.ValueOf(val))
74 | }
75 |
76 | if v.CanAddr() {
77 | pv := v.Addr()
78 | if pv.CanInterface() && pv.Type().Implements(marshalerType) {
79 | m := pv.Interface().(Marshaler)
80 | val, err := m.MarshalPlist()
81 | if err != nil {
82 | return nil, err
83 | }
84 | return e.marshal(reflect.ValueOf(val))
85 | }
86 | }
87 |
88 | // check for empty interface v type
89 | if v.Kind() == reflect.Interface && v.NumMethod() == 0 || v.Kind() == reflect.Ptr {
90 | v = v.Elem()
91 | }
92 |
93 | // check for time type
94 | if v.Type() == reflect.TypeOf((*time.Time)(nil)).Elem() {
95 | if date, ok := v.Interface().(time.Time); ok {
96 | return &plistValue{Date, date}, nil
97 | }
98 | return nil, &UnsupportedValueError{v, v.String()}
99 | }
100 |
101 | switch v.Kind() {
102 | case reflect.String:
103 | return &plistValue{String, v.String()}, nil
104 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
105 | return &plistValue{Integer, signedInt{uint64(v.Int()), true}}, nil
106 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
107 | return &plistValue{Integer, signedInt{uint64(v.Uint()), false}}, nil
108 | case reflect.Float32, reflect.Float64:
109 | return &plistValue{Real, sizedFloat{v.Float(), v.Type().Bits()}}, nil
110 | case reflect.Bool:
111 | return &plistValue{Boolean, v.Bool()}, nil
112 | case reflect.Slice, reflect.Array:
113 | return e.marshalArray(v)
114 | case reflect.Map:
115 | return e.marshalMap(v)
116 | case reflect.Struct:
117 | return e.marshalStruct(v)
118 | default:
119 | return nil, &UnsupportedTypeError{v.Type()}
120 | }
121 | }
122 |
123 | func (e *Encoder) marshalStruct(v reflect.Value) (*plistValue, error) {
124 | fields := cachedTypeFields(v.Type())
125 | dict := &dictionary{
126 | m: make(map[string]*plistValue, len(fields)),
127 | }
128 | for _, field := range fields {
129 | val := field.value(v)
130 | if field.omitEmpty && isEmptyValue(val) {
131 | continue
132 | }
133 | value, err := e.marshal(field.value(v))
134 | if err != nil {
135 | return nil, err
136 | }
137 | dict.m[field.name] = value
138 | }
139 | return &plistValue{Dictionary, dict}, nil
140 | }
141 |
142 | func (e *Encoder) marshalArray(v reflect.Value) (*plistValue, error) {
143 | if v.Type().Elem().Kind() == reflect.Uint8 {
144 | bytes := []byte(nil)
145 | if v.CanAddr() {
146 | bytes = v.Slice(0, v.Len()).Bytes()
147 | } else {
148 | bytes = make([]byte, v.Len())
149 | reflect.Copy(reflect.ValueOf(bytes), v)
150 | }
151 | return &plistValue{Data, bytes}, nil
152 | }
153 | subvalues := make([]*plistValue, v.Len())
154 | for idx, length := 0, v.Len(); idx < length; idx++ {
155 | subpval, err := e.marshal(v.Index(idx))
156 | if err != nil {
157 | return nil, err
158 | }
159 | if subpval != nil {
160 | subvalues[idx] = subpval
161 | }
162 | }
163 | return &plistValue{Array, subvalues}, nil
164 | }
165 |
166 | func (e *Encoder) marshalMap(v reflect.Value) (*plistValue, error) {
167 | if v.Type().Key().Kind() != reflect.String {
168 | return nil, &UnsupportedTypeError{v.Type()}
169 | }
170 |
171 | l := v.Len()
172 | dict := &dictionary{
173 | m: make(map[string]*plistValue, l),
174 | }
175 | for _, keyv := range v.MapKeys() {
176 | subpval, err := e.marshal(v.MapIndex(keyv))
177 | if err != nil {
178 | return nil, err
179 | }
180 | if subpval != nil {
181 | dict.m[keyv.String()] = subpval
182 | }
183 | }
184 | return &plistValue{Dictionary, dict}, nil
185 | }
186 |
187 | // An UnsupportedTypeError is returned by Marshal when attempting
188 | // to encode an unsupported value type.
189 | type UnsupportedTypeError struct {
190 | Type reflect.Type
191 | }
192 |
193 | func (e *UnsupportedTypeError) Error() string {
194 | return "plist: unsupported type: " + e.Type.String()
195 | }
196 |
197 | // UnsupportedValueError ...
198 | type UnsupportedValueError struct {
199 | Value reflect.Value
200 | Str string
201 | }
202 |
203 | func (e *UnsupportedValueError) Error() string {
204 | return "plist: unsupported value: " + e.Str
205 | }
206 |
207 | func isEmptyValue(v reflect.Value) bool {
208 | switch v.Kind() {
209 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
210 | return v.Len() == 0
211 | case reflect.Bool:
212 | return !v.Bool()
213 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
214 | return v.Int() == 0
215 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
216 | return v.Uint() == 0
217 | case reflect.Float32, reflect.Float64:
218 | return v.Float() == 0
219 | case reflect.Interface, reflect.Ptr:
220 | return v.IsNil()
221 | case reflect.Struct:
222 | return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
223 | }
224 | return false
225 | }
226 |
--------------------------------------------------------------------------------
/encode_test.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | "time"
7 | )
8 |
9 | var fooRef = `
10 |
11 | foo
12 | `
13 |
14 | var utf8Ref = `
15 |
16 | UTF-8 ☼
17 | `
18 |
19 | var zeroRef = `
20 |
21 | 0
22 | `
23 |
24 | var oneRef = `
25 |
26 | 1
27 | `
28 |
29 | var minOneRef = `
30 |
31 | -1
32 | `
33 |
34 | var realRef = `
35 |
36 | 1.2
37 | `
38 |
39 | var falseRef = `
40 |
41 |
42 | `
43 |
44 | var trueRef = `
45 |
46 |
47 | `
48 |
49 | var arrRef = `
50 |
51 | abc4
52 | `
53 |
54 | var byteArrRef = `
55 |
56 | /////////////////////w==
57 | `
58 |
59 | var time1900Ref = `
60 |
61 | 1900-01-01T12:00:00Z
62 | `
63 |
64 | var dataRef = `
65 |
66 | PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPjxzdHJpbmc+Zm9vPC9zdHJpbmc+PC9wbGlzdD4K
67 | `
68 |
69 | var emptyDataRef = `
70 |
71 |
72 | `
73 |
74 | var dictRef = `
75 |
76 | boolfoobar
77 | `
78 |
79 | var indentRef = `
80 |
81 |
82 |
83 | Boolean
84 |
85 | BooleanList
86 |
87 |
88 |
89 |
90 | CFBundleInfoDictionaryVersion
91 | 6.0
92 | Strings
93 |
94 | a
95 | b
96 |
97 | band-size
98 | 8388608
99 | bundle-backingstore-version
100 | 1
101 | diskimage-bundle-type
102 | com.apple.diskimage.sparsebundle
103 | size
104 | 4398046511104
105 | useless
106 |
107 | unused-string
108 | unused
109 |
110 |
111 |
112 | `
113 |
114 | var indentRefOmit = `
115 |
116 |
117 |
118 | Boolean
119 |
120 | BooleanList
121 |
122 |
123 |
124 |
125 | CFBundleInfoDictionaryVersion
126 | 6.0
127 | Strings
128 |
129 | a
130 | b
131 |
132 | bundle-backingstore-version
133 | 1
134 | diskimage-bundle-type
135 | com.apple.diskimage.sparsebundle
136 | size
137 | 4398046511104
138 |
139 |
140 | `
141 |
142 | type testStruct struct {
143 | UnusedString string `plist:"unused-string"`
144 | UnusedByte []byte `plist:"unused-byte,omitempty"`
145 | }
146 |
147 | var encodeTests = []struct {
148 | in interface{}
149 | out string
150 | }{
151 | {"foo", fooRef},
152 | {"UTF-8 ☼", utf8Ref},
153 | {0, zeroRef},
154 | {1, oneRef},
155 | {uint64(1), oneRef},
156 | {-1, minOneRef},
157 | {1.2, realRef},
158 | {false, falseRef},
159 | {true, trueRef},
160 | {[]interface{}{"a", "b", "c", 4, true}, arrRef},
161 | {time.Date(1900, 01, 01, 12, 00, 00, 0, time.UTC), time1900Ref},
162 | {[]byte(fooRef), dataRef},
163 | {map[string]interface{}{
164 | "foo": "bar",
165 | "bool": true},
166 | dictRef},
167 | {struct {
168 | Foo string `plist:"foo"`
169 | Bool bool `plist:"bool"`
170 | }{"bar", true},
171 | dictRef},
172 | {[][16]byte{
173 | {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
174 | }, byteArrRef},
175 | }
176 |
177 | func TestEncodeValues(t *testing.T) {
178 | t.Parallel()
179 | for _, tt := range encodeTests {
180 | b, err := Marshal(tt.in)
181 | if err != nil {
182 | t.Error(err)
183 | continue
184 | }
185 | out := string(b)
186 | if out != tt.out {
187 | t.Errorf("Marshal(%v) = \n%v, \nwant\n %v", tt.in, out, tt.out)
188 | }
189 | }
190 | }
191 |
192 | func TestNewLineString(t *testing.T) {
193 | t.Parallel()
194 | multiline := struct {
195 | Content string
196 | }{
197 | Content: "foo\nbar",
198 | }
199 |
200 | b, err := MarshalIndent(multiline, " ")
201 | if err != nil {
202 | t.Fatal(err)
203 | }
204 | var ok = `
205 |
206 |
207 |
208 | Content
209 | foo
210 | bar
211 |
212 |
213 | `
214 | out := string(b)
215 | if out != ok {
216 | t.Errorf("Marshal(%v) = \n%v, \nwant\n %v", multiline, out, ok)
217 | }
218 |
219 | }
220 |
221 | func TestIndent(t *testing.T) {
222 | t.Parallel()
223 | sparseBundleHeader := struct {
224 | InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
225 | BandSize uint64 `plist:"band-size"`
226 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
227 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
228 | Size uint64 `plist:"size"`
229 | Unused testStruct `plist:"useless"`
230 | Boolean bool
231 | BooleanList []bool
232 | Strings []string
233 | }{
234 | InfoDictionaryVersion: "6.0",
235 | BandSize: 8388608,
236 | Size: 4 * 1048576 * 1024 * 1024,
237 | DiskImageBundleType: "com.apple.diskimage.sparsebundle",
238 | BackingStoreVersion: 1,
239 | Unused: testStruct{UnusedString: "unused"},
240 | Boolean: true,
241 | BooleanList: []bool{true, false},
242 | Strings: []string{"a", "b"},
243 | }
244 | b, err := MarshalIndent(sparseBundleHeader, " ")
245 | if err != nil {
246 | t.Fatal(err)
247 | }
248 | out := string(b)
249 | if out != indentRef {
250 | t.Errorf("MarshalIndent(%v) = \n%v, \nwant\n%v", sparseBundleHeader, out, indentRef)
251 | }
252 | }
253 |
254 | func TestOmitNotEmpty(t *testing.T) {
255 | t.Parallel()
256 | sparseBundleHeader := struct {
257 | InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
258 | BandSize uint64 `plist:"band-size,omitempty"`
259 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
260 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
261 | Size uint64 `plist:"size"`
262 | Unused testStruct `plist:"useless"`
263 | Boolean bool
264 | BooleanList []bool
265 | Strings []string
266 | }{
267 | InfoDictionaryVersion: "6.0",
268 | BandSize: 8388608,
269 | Size: 4 * 1048576 * 1024 * 1024,
270 | DiskImageBundleType: "com.apple.diskimage.sparsebundle",
271 | BackingStoreVersion: 1,
272 | Unused: testStruct{UnusedString: "unused"},
273 | Boolean: true,
274 | BooleanList: []bool{true, false},
275 | Strings: []string{"a", "b"},
276 | }
277 | b, err := MarshalIndent(sparseBundleHeader, " ")
278 | if err != nil {
279 | t.Fatal(err)
280 | }
281 | out := string(b)
282 | if out != indentRef {
283 | t.Errorf("MarshalIndent(%v) = \n%v, \nwant\n %v", sparseBundleHeader, out, indentRefOmit)
284 | }
285 | }
286 |
287 | func TestOmitIsEmpty(t *testing.T) {
288 | t.Parallel()
289 | sparseBundleHeader := struct {
290 | InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
291 | BandSize uint64 `plist:"band-size,omitempty"`
292 | BackingStoreVersion int `plist:"bundle-backingstore-version"`
293 | DiskImageBundleType string `plist:"diskimage-bundle-type"`
294 | Size uint64 `plist:"size"`
295 | Unused testStruct `plist:"useless,omitempty"`
296 | Boolean bool
297 | BooleanList []bool
298 | Strings []string
299 | }{
300 | InfoDictionaryVersion: "6.0",
301 | Size: 4 * 1048576 * 1024 * 1024,
302 | DiskImageBundleType: "com.apple.diskimage.sparsebundle",
303 | BackingStoreVersion: 1,
304 | Boolean: true,
305 | BooleanList: []bool{true, false},
306 | Strings: []string{"a", "b"},
307 | }
308 | b, err := MarshalIndent(sparseBundleHeader, " ")
309 | if err != nil {
310 | t.Fatal(err)
311 | }
312 | out := string(b)
313 | if out != indentRefOmit {
314 | t.Errorf("MarshalIndent(%v) = \n%v, \nwant\n %v", sparseBundleHeader, out, indentRefOmit)
315 | }
316 | }
317 |
318 | type marshalerTest struct {
319 | marshalFuncInvoked bool
320 | MustMarshal string
321 | }
322 |
323 | func (m *marshalerTest) MarshalPlist() (interface{}, error) {
324 | m.marshalFuncInvoked = true
325 | return &m.MustMarshal, nil
326 | }
327 |
328 | func TestMarshaler(t *testing.T) {
329 | t.Parallel()
330 | want := []byte(`
331 |
332 | pants
333 | `)
334 | m := marshalerTest{MustMarshal: "pants"}
335 | have, err := Marshal(&m)
336 | if err != nil {
337 | t.Fatal(err)
338 | }
339 |
340 | if !bytes.Equal(have, want) {
341 | t.Errorf("expected \n%s got \n%s\n", have, want)
342 | }
343 | }
344 |
345 | func TestSelfClosing(t *testing.T) {
346 | t.Parallel()
347 | selfClosing := struct {
348 | True bool
349 | False bool
350 | Absent bool
351 | }{
352 | True: true,
353 | False: false,
354 | }
355 |
356 | want := []byte(`
357 |
358 | AbsentFalseTrue
359 | `)
360 |
361 | have, err := Marshal(selfClosing)
362 | if err != nil {
363 | t.Fatal(err)
364 | }
365 |
366 | if !bytes.Equal(have, want) {
367 | t.Errorf("expected \n%s got \n%s\n", have, want)
368 | }
369 |
370 | }
371 |
372 | func TestEncodeTagSkip(t *testing.T) {
373 | // Test struct
374 | testStruct := struct {
375 | NoTag string
376 | Tag string `plist:"OtherTag"`
377 | SkipTag string `plist:"-"`
378 | }{
379 | NoTag: "NoTag",
380 | Tag: "Tag",
381 | SkipTag: "SkipTag",
382 | }
383 |
384 | have, err := Marshal(&testStruct)
385 | if err != nil {
386 | t.Fatal(err)
387 | }
388 |
389 | if bytes.Contains([]byte(have), []byte(testStruct.SkipTag)) {
390 | t.Error("field encoded when it was tagged as -")
391 | }
392 | }
393 |
--------------------------------------------------------------------------------
/example_unmarshaler_test.go:
--------------------------------------------------------------------------------
1 | package plist_test
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/micromdm/plist"
7 | )
8 |
9 | const data = `
10 |
11 |
12 |
13 | typekey
14 | A
15 | typeAkey
16 | VALUE-A
17 |
18 | `
19 |
20 | type TypeDecider struct {
21 | ActualType interface{} `plist:"-"`
22 | }
23 |
24 | type TypeA struct {
25 | TypeAKey string `plist:"typeAkey"`
26 | }
27 |
28 | type TypeB struct {
29 | TypeBKey string `plist:"typeBkey"`
30 | }
31 |
32 | func (t *TypeDecider) UnmarshalPlist(f func(interface{}) error) error {
33 | // stub struct for decoding a single key to tell which
34 | // specific type we should umarshal into
35 | typeKey := &struct {
36 | TypeKey string `plist:"typekey"`
37 | }{}
38 | if err := f(typeKey); err != nil {
39 | return err
40 | }
41 |
42 | // switch using the decoded value to determine the correct type
43 | switch typeKey.TypeKey {
44 | case "A":
45 | t.ActualType = new(TypeA)
46 | case "B":
47 | t.ActualType = new(TypeB)
48 | case "":
49 | return fmt.Errorf("empty typekey (or wrong input data)")
50 | default:
51 | return fmt.Errorf("unknown typekey: %s", typeKey.TypeKey)
52 | }
53 |
54 | // decode into the actual type
55 | return f(t.ActualType)
56 | }
57 |
58 | // ExampleUnmarshaler demonstrates using structs that use the Unmarshaler interface.
59 | func ExampleUnmarshaler() {
60 | decider := new(TypeDecider)
61 | if err := plist.Unmarshal([]byte(data), decider); err != nil {
62 | fmt.Println(err)
63 | return
64 | }
65 |
66 | typeA, ok := decider.ActualType.(*TypeA)
67 | if !ok {
68 | fmt.Println("actual type is not TypeA")
69 | return
70 | }
71 |
72 | fmt.Println(typeA.TypeAKey)
73 | // Output: VALUE-A
74 | }
75 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/micromdm/plist
2 |
3 | go 1.14
4 |
--------------------------------------------------------------------------------
/plist.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import "sort"
4 |
5 | type plistKind uint
6 |
7 | const (
8 | Invalid plistKind = iota
9 | Dictionary
10 | Array
11 | String
12 | Integer
13 | Real
14 | Boolean
15 | Data
16 | Date
17 | )
18 |
19 | var plistKindNames = map[plistKind]string{
20 | Invalid: "invalid",
21 | Dictionary: "dictionary",
22 | Array: "array",
23 | String: "string",
24 | Integer: "integer",
25 | Real: "real",
26 | Boolean: "boolean",
27 | Data: "data",
28 | Date: "date",
29 | }
30 |
31 | type plistValue struct {
32 | kind plistKind
33 | value interface{}
34 | }
35 |
36 | type signedInt struct {
37 | value uint64
38 | signed bool
39 | }
40 |
41 | type sizedFloat struct {
42 | value float64
43 | bits int
44 | }
45 |
46 | type dictionary struct {
47 | count int
48 | m map[string]*plistValue
49 | keys sort.StringSlice
50 | values []*plistValue
51 | }
52 |
53 | func (d *dictionary) Len() int {
54 | return len(d.m)
55 | }
56 |
57 | func (d *dictionary) Less(i, j int) bool {
58 | return d.keys.Less(i, j)
59 | }
60 |
61 | func (d *dictionary) Swap(i, j int) {
62 | d.keys.Swap(i, j)
63 | d.values[i], d.values[j] = d.values[j], d.values[i]
64 | }
65 |
66 | func (d *dictionary) populateArrays() {
67 | d.keys = make([]string, len(d.m))
68 | d.values = make([]*plistValue, len(d.m))
69 | i := 0
70 | for k, v := range d.m {
71 | d.keys[i] = k
72 | d.values[i] = v
73 | i++
74 | }
75 | sort.Sort(d)
76 | }
77 |
--------------------------------------------------------------------------------
/tags.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "reflect"
5 | "sort"
6 | "strings"
7 | "sync"
8 | "unicode"
9 | )
10 |
11 | // tagOptions is the string following a comma in a struct field's "json"
12 | // tag, or the empty string. It does not include the leading comma.
13 | type tagOptions string
14 |
15 | // parseTag splits a struct field's json tag into its name and
16 | // comma-separated options.
17 | func parseTag(tag string) (string, tagOptions) {
18 | if idx := strings.Index(tag, ","); idx != -1 {
19 | return tag[:idx], tagOptions(tag[idx+1:])
20 | }
21 | return tag, tagOptions("")
22 | }
23 |
24 | // Contains reports whether a comma-separated list of options
25 | // contains a particular substr flag. substr must be surrounded by a
26 | // string boundary or commas.
27 | func (o tagOptions) Contains(optionName string) bool {
28 | if len(o) == 0 {
29 | return false
30 | }
31 | s := string(o)
32 | for s != "" {
33 | var next string
34 | i := strings.Index(s, ",")
35 | if i >= 0 {
36 | s, next = s[:i], s[i+1:]
37 | }
38 | if s == optionName {
39 | return true
40 | }
41 | s = next
42 | }
43 | return false
44 | }
45 |
46 | type field struct {
47 | name string
48 | tag bool
49 | index []int
50 | typ reflect.Type
51 | omitEmpty bool
52 | }
53 |
54 | func (f field) value(v reflect.Value) reflect.Value {
55 | for _, i := range f.index {
56 | if v.Kind() == reflect.Ptr {
57 | if v.IsNil() {
58 | v.Set(reflect.New(v.Type().Elem()))
59 | }
60 | v = v.Elem()
61 | }
62 | v = v.Field(i)
63 | }
64 | return v
65 | }
66 |
67 | type byName []field
68 |
69 | func (x byName) Len() int { return len(x) }
70 |
71 | func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
72 |
73 | func (x byName) Less(i, j int) bool {
74 | if x[i].name != x[j].name {
75 | return x[i].name < x[j].name
76 | }
77 | if len(x[i].index) != len(x[j].index) {
78 | return len(x[i].index) < len(x[j].index)
79 | }
80 | if x[i].tag != x[j].tag {
81 | return x[i].tag
82 | }
83 | return byIndex(x).Less(i, j)
84 | }
85 |
86 | type byIndex []field
87 |
88 | func (x byIndex) Len() int { return len(x) }
89 |
90 | func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
91 |
92 | func (x byIndex) Less(i, j int) bool {
93 | for k, xik := range x[i].index {
94 | if k >= len(x[j].index) {
95 | return false
96 | }
97 | if xik != x[j].index[k] {
98 | return xik < x[j].index[k]
99 | }
100 | }
101 | return len(x[i].index) < len(x[j].index)
102 | }
103 |
104 | // typeFields returns a list of fields that plist should recognize for the given
105 | // type. The algorithm is breadth-first search over the set of structs to
106 | // include - the top struct and then any reachable anonymous structs.
107 | func typeFields(t reflect.Type) []field {
108 | // Anonymous fields to explore at the current level and the next.
109 | current := []field{}
110 | next := []field{{typ: t}}
111 |
112 | // Count of queued names for current level and the next.
113 | count := map[reflect.Type]int{}
114 | nextCount := map[reflect.Type]int{}
115 |
116 | // Types already visited at an earlier level.
117 | visited := map[reflect.Type]bool{}
118 |
119 | // Fields found.
120 | var fields []field
121 |
122 | for len(next) > 0 {
123 | current, next = next, current[:0]
124 | count, nextCount = nextCount, map[reflect.Type]int{}
125 |
126 | for _, f := range current {
127 | if visited[f.typ] {
128 | continue
129 | }
130 | visited[f.typ] = true
131 |
132 | // Scan f.typ for fields to include.
133 | for i := 0; i < f.typ.NumField(); i++ {
134 | sf := f.typ.Field(i)
135 | if sf.PkgPath != "" && !sf.Anonymous { // unexported
136 | continue
137 | }
138 | tag := sf.Tag.Get("plist")
139 | if tag == "-" {
140 | continue
141 | }
142 | name, opts := parseTag(tag)
143 | if !isValidTag(name) {
144 | name = ""
145 | }
146 | index := make([]int, len(f.index)+1)
147 | copy(index, f.index)
148 | index[len(f.index)] = i
149 |
150 | ft := sf.Type
151 | if ft.Name() == "" && ft.Kind() == reflect.Ptr {
152 | // Follow pointer.
153 | ft = ft.Elem()
154 | }
155 |
156 | // Record found field and index sequence.
157 | if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
158 | tagged := name != ""
159 | if name == "" {
160 | name = sf.Name
161 | }
162 | fields = append(fields, field{
163 | name: name,
164 | tag: tagged,
165 | index: index,
166 | typ: ft,
167 | omitEmpty: opts.Contains("omitempty"),
168 | })
169 | if count[f.typ] > 1 {
170 | // If there were multiple instances, add a second,
171 | // so that the annihilation code will see a duplicate.
172 | // It only cares about the distinction between 1 or 2,
173 | // so don't bother generating any more copies.
174 | fields = append(fields, fields[len(fields)-1])
175 | }
176 | continue
177 | }
178 |
179 | // Record new anonymous struct to explore in next round.
180 | nextCount[ft]++
181 | if nextCount[ft] == 1 {
182 | f := field{name: ft.Name(), index: index, typ: ft}
183 | next = append(next, f)
184 | }
185 | }
186 | }
187 | }
188 |
189 | sort.Sort(byName(fields))
190 |
191 | // Delete all fields that are hidden by the Go rules for embedded fields,
192 | // except that fields with plist tags are promoted.
193 |
194 | // The fields are sorted in primary order of name, secondary order
195 | // of field index length. Loop over names; for each name, delete
196 | // hidden fields by choosing the one dominant field that survives.
197 | out := fields[:0]
198 | for advance, i := 0, 0; i < len(fields); i += advance {
199 | // One iteration per name.
200 | // Find the sequence of fields with the name of this first field.
201 | fi := fields[i]
202 | name := fi.name
203 | for advance = 1; i+advance < len(fields); advance++ {
204 | fj := fields[i+advance]
205 | if fj.name != name {
206 | break
207 | }
208 | }
209 | if advance == 1 { // Only one field with this name
210 | out = append(out, fi)
211 | continue
212 | }
213 | dominant, ok := dominantField(fields[i : i+advance])
214 | if ok {
215 | out = append(out, dominant)
216 | }
217 | }
218 |
219 | fields = out
220 | sort.Sort(byIndex(fields))
221 |
222 | return fields
223 | }
224 |
225 | func dominantField(fields []field) (field, bool) {
226 | // The fields are sorted in increasing index-length order. The winner
227 | // must therefore be one with the shortest index length. Drop all
228 | // longer entries, which is easy: just truncate the slice.
229 | length := len(fields[0].index)
230 | tagged := -1 // Index of first tagged field.
231 | for i, f := range fields {
232 | if len(f.index) > length {
233 | fields = fields[:i]
234 | break
235 | }
236 | if f.tag {
237 | if tagged >= 0 {
238 | // Multiple tagged fields at the same level: conflict.
239 | // Return no field.
240 | return field{}, false
241 | }
242 | tagged = i
243 | }
244 | }
245 | if tagged >= 0 {
246 | return fields[tagged], true
247 | }
248 | // All remaining fields have the same length. If there's more than one,
249 | // we have a conflict (two fields named "X" at the same level) and we
250 | // return no field.
251 | if len(fields) > 1 {
252 | return field{}, false
253 | }
254 | return fields[0], true
255 | }
256 |
257 | var fieldCache struct {
258 | sync.RWMutex
259 | m map[reflect.Type][]field
260 | }
261 |
262 | // cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
263 | func cachedTypeFields(t reflect.Type) []field {
264 | fieldCache.RLock()
265 | f := fieldCache.m[t]
266 | fieldCache.RUnlock()
267 | if f != nil {
268 | return f
269 | }
270 |
271 | // Compute fields without lock.
272 | // Might duplicate effort but won't hold other computations back.
273 | f = typeFields(t)
274 | if f == nil {
275 | f = []field{}
276 | }
277 |
278 | fieldCache.Lock()
279 | if fieldCache.m == nil {
280 | fieldCache.m = map[reflect.Type][]field{}
281 | }
282 | fieldCache.m[t] = f
283 | fieldCache.Unlock()
284 | return f
285 | }
286 |
287 | func isValidTag(s string) bool {
288 | if s == "" {
289 | return false
290 | }
291 | for _, c := range s {
292 | switch {
293 | case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
294 | // Backslash and quote chars are reserved, but
295 | // otherwise any punctuation chars are allowed
296 | // in a tag name.
297 | default:
298 | if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
299 | return false
300 | }
301 | }
302 | }
303 | return true
304 | }
305 |
--------------------------------------------------------------------------------
/testdata/crashers/0d16bb1b5a9de90807d6e23316222a70267f48f0:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/crashers/0d16bb1b5a9de90807d6e23316222a70267f48f0
--------------------------------------------------------------------------------
/testdata/crashers/137f12b963e1abb2aedf1e78dab4217d365e61cf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/crashers/137f12b963e1abb2aedf1e78dab4217d365e61cf
--------------------------------------------------------------------------------
/testdata/crashers/1ac1e0b8585d245f0e7bbc48ef33500984a7fb6b:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/crashers/1ac1e0b8585d245f0e7bbc48ef33500984a7fb6b
--------------------------------------------------------------------------------
/testdata/crashers/30864f343b3b987e140eff6769e091b3b60c3ce7:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/crashers/30864f343b3b987e140eff6769e091b3b60c3ce7
--------------------------------------------------------------------------------
/testdata/crashers/5feced23aa2767c77c8d2bb5c35321f926b4537a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/crashers/5feced23aa2767c77c8d2bb5c35321f926b4537a
--------------------------------------------------------------------------------
/testdata/crashers/76632470f8fdd06ea7f5eaa53376aa3ffdbe00b0:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/crashers/76632470f8fdd06ea7f5eaa53376aa3ffdbe00b0
--------------------------------------------------------------------------------
/testdata/crashers/a453429d65a952b8f54dc233d0ac304178a75b41:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/crashers/a453429d65a952b8f54dc233d0ac304178a75b41
--------------------------------------------------------------------------------
/testdata/crashers/a7f6152b23463dbeb12cf9621b9c5962b8b71d01:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/crashers/a7f6152b23463dbeb12cf9621b9c5962b8b71d01
--------------------------------------------------------------------------------
/testdata/crashers/aac34b3c8cbcc6d607e807fa920b2aaa4294f1fa:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/crashers/aac34b3c8cbcc6d607e807fa920b2aaa4294f1fa
--------------------------------------------------------------------------------
/testdata/crashers/b6d3ae7d57c52b1139cd4cb097382371be5508f4:
--------------------------------------------------------------------------------
1 | bplist00000000000000000000000000
--------------------------------------------------------------------------------
/testdata/crashers/d2e984d7ef5d4fbcda46e217c28a7ad0077fb820:
--------------------------------------------------------------------------------
1 | bplist07 0944119244897549443
--------------------------------------------------------------------------------
/testdata/crashers/e322917c1e9ed2ac460865f9455ef8981f765522:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/crashers/e322917c1e9ed2ac460865f9455ef8981f765522
--------------------------------------------------------------------------------
/testdata/sample2.binary.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micromdm/plist/c0c2f164ded96615816ecc44cf789bbe5b6017e8/testdata/sample2.binary.plist
--------------------------------------------------------------------------------
/testdata/xml/comment.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | key
10 |
11 | val
12 |
13 | key2
14 |
15 |
16 | val1
17 | val2
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/testdata/xml/empty-doctype.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | key
6 | val
7 |
8 |
9 |
--------------------------------------------------------------------------------
/testdata/xml/empty-plist.failure.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/testdata/xml/empty-xml.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | key
6 | val
7 |
8 |
9 |
--------------------------------------------------------------------------------
/testdata/xml/invalid-before-plist.failure.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 | invalid
4 |
5 |
6 | key
7 | val
8 |
9 |
10 |
--------------------------------------------------------------------------------
/testdata/xml/invalid-data.failure.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | key
6 | val
7 | invalid
8 |
9 |
10 |
--------------------------------------------------------------------------------
/testdata/xml/invalid-end.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | key
6 | val
7 |
8 |
9 | invalid
10 |
--------------------------------------------------------------------------------
/testdata/xml/invalid-middle.failure.plist:
--------------------------------------------------------------------------------
1 |
2 | invalid
3 |
4 |
5 |
6 | key
7 | val
8 |
9 |
10 |
--------------------------------------------------------------------------------
/testdata/xml/invalid-start.failure.plist:
--------------------------------------------------------------------------------
1 | invalid
2 |
3 |
4 |
5 | key
6 | val
7 |
8 |
9 |
--------------------------------------------------------------------------------
/testdata/xml/malformed-xml.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | key
6 | val
7 |
8 |
9 |
--------------------------------------------------------------------------------
/testdata/xml/no-both.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 | key
4 | val
5 |
6 |
7 |
--------------------------------------------------------------------------------
/testdata/xml/no-dict-end.failure.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | key
6 | val
7 |
8 |
--------------------------------------------------------------------------------
/testdata/xml/no-doctype.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | key
5 | val
6 |
7 |
8 |
--------------------------------------------------------------------------------
/testdata/xml/no-plist-end.failure.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | key
6 | val
7 |
8 |
--------------------------------------------------------------------------------
/testdata/xml/no-plist-version.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | key
6 | val
7 |
8 |
9 |
--------------------------------------------------------------------------------
/testdata/xml/no-xml-tag.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | key
5 | val
6 |
7 |
8 |
--------------------------------------------------------------------------------
/testdata/xml/swapped.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | key
6 | val
7 |
8 |
9 |
--------------------------------------------------------------------------------
/testdata/xml/unescaped-plist.failure.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 | key
6 | val
7 |
8 |
9 |
--------------------------------------------------------------------------------
/testdata/xml/unescaped-xml.failure.plist:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | key
6 | val
7 |
8 |
9 |
--------------------------------------------------------------------------------
/testdata/xml/valid.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | key
6 | val
7 |
8 |
9 |
--------------------------------------------------------------------------------
/xml_parser.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "encoding/xml"
7 | "errors"
8 | "fmt"
9 | "io"
10 | "strconv"
11 | "strings"
12 | "time"
13 | )
14 |
15 | // xmlParser uses xml.Decoder to parse an xml plist into the corresponding plistValues
16 | type xmlParser struct {
17 | *xml.Decoder
18 | }
19 |
20 | func (p *xmlParser) Token() (xml.Token, error) {
21 | for {
22 | token, err := p.Decoder.Token()
23 | if err != nil {
24 | return nil, err
25 | }
26 | if _, ok := token.(xml.Comment); ok {
27 | continue
28 | }
29 | return token, nil
30 | }
31 | }
32 |
33 | // newXMLParser returns a new xmlParser
34 | func newXMLParser(r io.Reader) *xmlParser {
35 | return &xmlParser{xml.NewDecoder(r)}
36 | }
37 |
38 | func (p *xmlParser) parseDocument(start *xml.StartElement) (*plistValue, error) {
39 | if start != nil {
40 | return p.parseXMLElement(start)
41 | }
42 |
43 | for {
44 | tok, err := p.Token()
45 | if err != nil {
46 | return nil, err
47 | }
48 | switch el := tok.(type) {
49 | case xml.StartElement:
50 | return p.parseXMLElement(&el)
51 | case xml.ProcInst, xml.Directive:
52 | continue
53 | case xml.CharData:
54 | if len(bytes.TrimSpace(el)) != 0 {
55 | return nil, errors.New("plist: unexpected non-empty xml.CharData")
56 | }
57 | default:
58 | return nil, fmt.Errorf("unexpected element: %T", el)
59 | }
60 | }
61 | }
62 |
63 | func (p *xmlParser) parseXMLElement(element *xml.StartElement) (*plistValue, error) {
64 | switch element.Name.Local {
65 | case "plist":
66 | return p.parsePlist(element)
67 | case "dict":
68 | return p.parseDict(element)
69 | case "string":
70 | return p.parseString(element)
71 | case "true", "false":
72 | return p.parseBoolean(element)
73 | case "array":
74 | return p.parseArray(element)
75 | case "real":
76 | return p.parseReal(element)
77 | case "integer":
78 | return p.parseInteger(element)
79 | case "data":
80 | return p.parseData(element)
81 | case "date":
82 | return p.parseDate(element)
83 | default:
84 | return nil, fmt.Errorf("plist: Unknown plist element %s", element.Name.Local)
85 | }
86 | }
87 |
88 | func (p *xmlParser) parsePlist(element *xml.StartElement) (*plistValue, error) {
89 | var val *plistValue
90 | for {
91 | token, err := p.Token()
92 | if err != nil {
93 | return nil, err
94 | }
95 | switch el := token.(type) {
96 | case xml.EndElement:
97 | if val == nil {
98 | return nil, errors.New("plist: empty plist tag")
99 | }
100 | return val, nil
101 | case xml.StartElement:
102 | v, err := p.parseXMLElement(&el)
103 | if err != nil {
104 | return v, err
105 | }
106 | val = v
107 | case xml.CharData:
108 | if len(bytes.TrimSpace(el)) != 0 {
109 | return nil, errors.New("plist: unexpected non-empty xml.CharData")
110 | }
111 | default:
112 | return nil, fmt.Errorf("unexpected element: %T", el)
113 | }
114 | }
115 | }
116 |
117 | func (p *xmlParser) parseDict(element *xml.StartElement) (*plistValue, error) {
118 | var key *string
119 | var subvalues = make(map[string]*plistValue)
120 | for {
121 | token, err := p.Token()
122 | if err != nil {
123 | return nil, err
124 | }
125 | switch el := token.(type) {
126 | case xml.EndElement:
127 | return &plistValue{Dictionary, &dictionary{m: subvalues}}, nil
128 | case xml.StartElement:
129 | if el.Name.Local == "key" {
130 | var k string
131 | if err := p.DecodeElement(&k, &el); err != nil {
132 | return nil, err
133 | }
134 | key = &k
135 | continue
136 | }
137 | if key == nil {
138 | return nil, errors.New("plist: missing key in dict")
139 | }
140 | subvalues[*key], err = p.parseXMLElement(&el)
141 | if err != nil {
142 | return nil, err
143 | }
144 | key = nil
145 | case xml.CharData:
146 | if len(bytes.TrimSpace(el)) != 0 {
147 | return nil, errors.New("plist: unexpected non-empty xml.CharData")
148 | }
149 | default:
150 | return nil, fmt.Errorf("unexpected element: %T", el)
151 | }
152 | }
153 | }
154 |
155 | func (p *xmlParser) parseString(element *xml.StartElement) (*plistValue, error) {
156 | var value string
157 | if err := p.DecodeElement(&value, element); err != nil {
158 | return nil, err
159 | }
160 | return &plistValue{String, value}, nil
161 | }
162 |
163 | func (p *xmlParser) parseBoolean(element *xml.StartElement) (*plistValue, error) {
164 | if err := p.Skip(); err != nil {
165 | return nil, err
166 | }
167 | plistBoolean := element.Name.Local == "true"
168 | return &plistValue{Boolean, plistBoolean}, nil
169 | }
170 |
171 | func (p *xmlParser) parseArray(element *xml.StartElement) (*plistValue, error) {
172 | var subvalues []*plistValue
173 | for {
174 | token, err := p.Token()
175 | if err != nil {
176 | return nil, err
177 | }
178 | switch el := token.(type) {
179 | case xml.EndElement:
180 | return &plistValue{Array, subvalues}, nil
181 | case xml.StartElement:
182 | subv, err := p.parseXMLElement(&el)
183 | if err != nil {
184 | return nil, err
185 | }
186 | subvalues = append(subvalues, subv)
187 | case xml.CharData:
188 | if len(bytes.TrimSpace(el)) != 0 {
189 | return nil, errors.New("plist: unexpected non-empty xml.CharData")
190 | }
191 | default:
192 | return nil, fmt.Errorf("unexpected element: %T", el)
193 | }
194 | }
195 | }
196 |
197 | func (p *xmlParser) parseReal(element *xml.StartElement) (*plistValue, error) {
198 | var n float64
199 | if err := p.DecodeElement(&n, element); err != nil {
200 | return nil, err
201 | }
202 | return &plistValue{Real, sizedFloat{n, 64}}, nil
203 | }
204 |
205 | func (p *xmlParser) parseInteger(element *xml.StartElement) (*plistValue, error) {
206 | // Based on testing with plutil -lint, the largest positive integer
207 | // that you can store in an XML plist is 2^64 - 1 (in a uint64)
208 | // and the largest negative integer you can store is -2^63 (in an int64)
209 | // Since we need to know the sign before we can know what integer type
210 | // to decode into, first decode into a string to check for "-".
211 | var s string
212 | if err := p.DecodeElement(&s, element); err != nil {
213 | return nil, err
214 | }
215 | // Determine if this is a negative number by checking for minus sign.
216 | s = strings.TrimSpace(s)
217 | if strings.HasPrefix(s, "-") {
218 | i, err := strconv.ParseInt(s, 10, 64)
219 | if err != nil {
220 | return nil, err
221 | }
222 | return &plistValue{Integer, signedInt{uint64(i), true}}, nil
223 | }
224 | // Otherwise assume positive number and put into uint64.
225 | u, err := strconv.ParseUint(s, 10, 64)
226 | if err != nil {
227 | return nil, err
228 | }
229 | return &plistValue{Integer, signedInt{u, false}}, nil
230 | }
231 |
232 | func (p *xmlParser) parseData(element *xml.StartElement) (*plistValue, error) {
233 | replacer := strings.NewReplacer("\t", "", "\n", "", " ", "", "\r", "")
234 | var data []byte
235 | if err := p.DecodeElement(&data, element); err != nil {
236 | return nil, err
237 | }
238 | if len(data) == 0 {
239 | return &plistValue{Data, []byte(nil)}, nil
240 | }
241 | str := replacer.Replace(string(data))
242 | decoded, err := base64.StdEncoding.DecodeString(str)
243 | if err != nil {
244 | return nil, err
245 | }
246 | data = []byte(decoded)
247 | return &plistValue{Data, data}, nil
248 | }
249 |
250 | func (p *xmlParser) parseDate(element *xml.StartElement) (*plistValue, error) {
251 | var date time.Time
252 | if err := p.DecodeElement(&date, element); err != nil {
253 | return nil, err
254 | }
255 | return &plistValue{Date, date}, nil
256 | }
257 |
--------------------------------------------------------------------------------
/xml_writer.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/xml"
6 | "fmt"
7 | "io"
8 | "math"
9 | "reflect"
10 | "time"
11 | )
12 |
13 | const xmlDOCTYPE = ``
14 | const plistStart = ``
15 | const plistEnd = ``
16 |
17 | type xmlEncoder struct {
18 | indent string
19 | indentCount int
20 | err error
21 | writer io.Writer
22 | *xml.Encoder
23 | }
24 |
25 | func (e *xmlEncoder) write(buf []byte) {
26 | if e.err != nil {
27 | return
28 | }
29 | _, e.err = e.writer.Write(buf)
30 | }
31 |
32 | func newXMLEncoder(w io.Writer) *xmlEncoder {
33 | return &xmlEncoder{writer: w, Encoder: xml.NewEncoder(w)}
34 | }
35 |
36 | func (e *xmlEncoder) generateDocument(pval *plistValue) error {
37 | e.write([]byte(xml.Header))
38 | e.write([]byte(xmlDOCTYPE))
39 | e.write([]byte("\n"))
40 | e.write([]byte(plistStart))
41 | if e.indent != "" {
42 | e.write([]byte("\n"))
43 | }
44 |
45 | if err := e.writePlistValue(pval); err != nil {
46 | return err
47 | }
48 |
49 | if e.indent != "" {
50 | e.write([]byte("\n"))
51 | }
52 | e.write([]byte(plistEnd))
53 | e.write([]byte("\n"))
54 | return e.err
55 | }
56 |
57 | func (e *xmlEncoder) writePlistValue(pval *plistValue) error {
58 | switch pval.kind {
59 | case String:
60 | return e.writeStringValue(pval)
61 | case Boolean:
62 | return e.writeBoolValue(pval)
63 | case Integer:
64 | return e.writeIntegerValue(pval)
65 | case Dictionary:
66 | return e.writeDictionaryValue(pval)
67 | case Date:
68 | return e.writeDateValue(pval)
69 | case Array:
70 | return e.writeArrayValue(pval)
71 | case Real:
72 | return e.writeRealValue(pval)
73 | case Data:
74 | return e.writeDataValue(pval)
75 | default:
76 | return &UnsupportedTypeError{reflect.ValueOf(pval.value).Type()}
77 | }
78 | }
79 |
80 | func (e *xmlEncoder) writeDataValue(pval *plistValue) error {
81 | encodedValue := base64.StdEncoding.EncodeToString(pval.value.([]byte))
82 | return e.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: "data"}})
83 | }
84 |
85 | func (e *xmlEncoder) writeRealValue(pval *plistValue) error {
86 | encodedValue := pval.value
87 | switch {
88 | case math.IsInf(pval.value.(sizedFloat).value, 1):
89 | encodedValue = "inf"
90 | case math.IsInf(pval.value.(sizedFloat).value, -1):
91 | encodedValue = "-inf"
92 | case math.IsNaN(pval.value.(sizedFloat).value):
93 | encodedValue = "nan"
94 | default:
95 | encodedValue = pval.value.(sizedFloat).value
96 | }
97 | return e.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: "real"}})
98 | }
99 |
100 | // writeElement writes an xml element like , or
101 | func (e *xmlEncoder) writeElement(name string, pval *plistValue, valFunc func(*plistValue) error) error {
102 | startElement := xml.StartElement{
103 | Name: xml.Name{
104 | Space: "",
105 | Local: name,
106 | },
107 | }
108 |
109 | if name == "dict" || name == "array" {
110 | e.indentCount++
111 | }
112 |
113 | // Encode xml.StartElement token
114 | if err := e.EncodeToken(startElement); err != nil {
115 | return err
116 | }
117 |
118 | // flush
119 | if err := e.Flush(); err != nil {
120 | return err
121 | }
122 |
123 | // execute valFunc()
124 | if err := valFunc(pval); err != nil {
125 | return err
126 | }
127 |
128 | // Encode xml.EndElement token
129 | if err := e.EncodeToken(startElement.End()); err != nil {
130 | return err
131 | }
132 |
133 | if name == "dict" || name == "array" {
134 | e.indentCount--
135 | }
136 |
137 | // flush
138 | return e.Flush()
139 | }
140 |
141 | func (e *xmlEncoder) writeArrayValue(pval *plistValue) error {
142 | tokenFunc := func(pval *plistValue) error {
143 | encodedValue := pval.value
144 | values := encodedValue.([]*plistValue)
145 | wroteBool := false
146 | for _, v := range values {
147 | if !wroteBool {
148 | wroteBool = v.kind == Boolean
149 | }
150 | if err := e.writePlistValue(v); err != nil {
151 | return err
152 | }
153 | }
154 |
155 | if e.indent != "" && wroteBool {
156 | e.writer.Write([]byte("\n"))
157 | e.writer.Write([]byte(e.indent))
158 | }
159 | return nil
160 | }
161 | return e.writeElement("array", pval, tokenFunc)
162 |
163 | }
164 |
165 | func (e *xmlEncoder) writeDictionaryValue(pval *plistValue) error {
166 | tokenFunc := func(pval *plistValue) error {
167 | encodedValue := pval.value
168 | dict := encodedValue.(*dictionary)
169 | dict.populateArrays()
170 | for i, k := range dict.keys {
171 | if err := e.EncodeElement(k, xml.StartElement{Name: xml.Name{Local: "key"}}); err != nil {
172 | return err
173 | }
174 | if err := e.writePlistValue(dict.values[i]); err != nil {
175 | return err
176 | }
177 | }
178 | return nil
179 | }
180 | return e.writeElement("dict", pval, tokenFunc)
181 | }
182 |
183 | // encode strings as CharData, which doesn't escape newline
184 | // see https://github.com/golang/go/issues/9204
185 | func (e *xmlEncoder) writeStringValue(pval *plistValue) error {
186 | startElement := xml.StartElement{Name: xml.Name{Local: "string"}}
187 | // Encode xml.StartElement token
188 | if err := e.EncodeToken(startElement); err != nil {
189 | return err
190 | }
191 |
192 | // flush
193 | if err := e.Flush(); err != nil {
194 | return err
195 | }
196 |
197 | stringValue := pval.value.(string)
198 | if err := e.EncodeToken(xml.CharData(stringValue)); err != nil {
199 | return err
200 | }
201 |
202 | // flush
203 | if err := e.Flush(); err != nil {
204 | return err
205 | }
206 |
207 | // Encode xml.EndElement token
208 | if err := e.EncodeToken(startElement.End()); err != nil {
209 | return err
210 | }
211 |
212 | // flush
213 | return e.Flush()
214 |
215 | }
216 |
217 | func (e *xmlEncoder) writeBoolValue(pval *plistValue) error {
218 | // EncodeElement results in instead of
219 | // use writer to write self closing tags
220 | b := pval.value.(bool)
221 | if e.indent != "" {
222 | e.write([]byte("\n"))
223 | for i := 0; i < e.indentCount; i++ {
224 | e.write([]byte(e.indent))
225 | }
226 | }
227 | e.write([]byte(fmt.Sprintf("<%t/>", b)))
228 | return e.err
229 | }
230 |
231 | func (e *xmlEncoder) writeIntegerValue(pval *plistValue) error {
232 | encodedValue := pval.value
233 | if pval.value.(signedInt).signed {
234 | encodedValue = int64(pval.value.(signedInt).value)
235 | } else {
236 | encodedValue = pval.value.(signedInt).value
237 | }
238 | return e.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: "integer"}})
239 | }
240 |
241 | func (e *xmlEncoder) writeDateValue(pval *plistValue) error {
242 | encodedValue := pval.value.(time.Time).In(time.UTC).Format(time.RFC3339)
243 | return e.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: "date"}})
244 | }
245 |
--------------------------------------------------------------------------------