├── .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 | [![CI/CD](https://github.com/micromdm/plist/workflows/CI%2FCD/badge.svg)](https://github.com/micromdm/plist/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/micromdm/plist.svg)](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 | bplist070944119244897549443 -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------