From f3b313f19c2fc97144881db5a1b4c5dad4bc4350 Mon Sep 17 00:00:00 2001 From: xiaonan chen Date: Mon, 18 Nov 2024 16:00:07 +0800 Subject: [PATCH] init --- .github/dependabot.yml | 13 + .github/workflows/on-push-pr.yml | 27 + .gitignore | 33 + LICENSE | 21 + README.md | 30 + binary_parser.go | 436 +++++++++++++ decode.go | 345 +++++++++++ decode_test.go | 580 ++++++++++++++++++ encode.go | 238 +++++++ encode_test.go | 387 ++++++++++++ go.mod | 3 + plist.go | 76 +++ tags.go | 304 +++++++++ .../0d16bb1b5a9de90807d6e23316222a70267f48f0 | Bin 0 -> 310 bytes .../137f12b963e1abb2aedf1e78dab4217d365e61cf | Bin 0 -> 3283 bytes .../1ac1e0b8585d245f0e7bbc48ef33500984a7fb6b | Bin 0 -> 310 bytes .../30864f343b3b987e140eff6769e091b3b60c3ce7 | Bin 0 -> 310 bytes .../5feced23aa2767c77c8d2bb5c35321f926b4537a | Bin 0 -> 3283 bytes .../76632470f8fdd06ea7f5eaa53376aa3ffdbe00b0 | Bin 0 -> 3283 bytes .../a453429d65a952b8f54dc233d0ac304178a75b41 | Bin 0 -> 3283 bytes .../a7f6152b23463dbeb12cf9621b9c5962b8b71d01 | Bin 0 -> 310 bytes .../aac34b3c8cbcc6d607e807fa920b2aaa4294f1fa | Bin 0 -> 3283 bytes .../b6d3ae7d57c52b1139cd4cb097382371be5508f4 | 1 + .../d2e984d7ef5d4fbcda46e217c28a7ad0077fb820 | Bin 0 -> 32 bytes .../e322917c1e9ed2ac460865f9455ef8981f765522 | Bin 0 -> 3283 bytes testdata/sample2.binary.plist | Bin 0 -> 1293 bytes testdata/xml/empty-doctype.plist | 8 + testdata/xml/empty-plist.plist | 4 + testdata/xml/empty-xml.plist | 8 + testdata/xml/invalid-before-plist.plist | 9 + testdata/xml/invalid-data.plist | 9 + testdata/xml/invalid-end.plist | 9 + testdata/xml/invalid-middle.plist | 9 + testdata/xml/invalid-start.plist | 8 + testdata/xml/malformed-xml.plist | 8 + testdata/xml/no-both.plist | 6 + testdata/xml/no-dict-end.plist | 7 + testdata/xml/no-doctype.plist | 7 + testdata/xml/no-plist-end.plist | 7 + testdata/xml/no-plist-version.plist | 8 + testdata/xml/no-xml-tag.plist | 7 + testdata/xml/swapped.plist | 8 + testdata/xml/unescaped-plist.plist | 8 + testdata/xml/unescaped-xml.plist | 8 + testdata/xml/valid.plist | 8 + xml_parser.go | 243 ++++++++ xml_writer.go | 237 +++++++ 47 files changed, 3120 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/on-push-pr.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 binary_parser.go create mode 100644 decode.go create mode 100644 decode_test.go create mode 100644 encode.go create mode 100644 encode_test.go create mode 100644 go.mod create mode 100644 plist.go create mode 100644 tags.go create mode 100644 testdata/crashers/0d16bb1b5a9de90807d6e23316222a70267f48f0 create mode 100644 testdata/crashers/137f12b963e1abb2aedf1e78dab4217d365e61cf create mode 100644 testdata/crashers/1ac1e0b8585d245f0e7bbc48ef33500984a7fb6b create mode 100644 testdata/crashers/30864f343b3b987e140eff6769e091b3b60c3ce7 create mode 100644 testdata/crashers/5feced23aa2767c77c8d2bb5c35321f926b4537a create mode 100644 testdata/crashers/76632470f8fdd06ea7f5eaa53376aa3ffdbe00b0 create mode 100644 testdata/crashers/a453429d65a952b8f54dc233d0ac304178a75b41 create mode 100644 testdata/crashers/a7f6152b23463dbeb12cf9621b9c5962b8b71d01 create mode 100644 testdata/crashers/aac34b3c8cbcc6d607e807fa920b2aaa4294f1fa create mode 100644 testdata/crashers/b6d3ae7d57c52b1139cd4cb097382371be5508f4 create mode 100644 testdata/crashers/d2e984d7ef5d4fbcda46e217c28a7ad0077fb820 create mode 100644 testdata/crashers/e322917c1e9ed2ac460865f9455ef8981f765522 create mode 100644 testdata/sample2.binary.plist create mode 100644 testdata/xml/empty-doctype.plist create mode 100644 testdata/xml/empty-plist.plist create mode 100644 testdata/xml/empty-xml.plist create mode 100644 testdata/xml/invalid-before-plist.plist create mode 100644 testdata/xml/invalid-data.plist create mode 100644 testdata/xml/invalid-end.plist create mode 100644 testdata/xml/invalid-middle.plist create mode 100644 testdata/xml/invalid-start.plist create mode 100644 testdata/xml/malformed-xml.plist create mode 100644 testdata/xml/no-both.plist create mode 100644 testdata/xml/no-dict-end.plist create mode 100644 testdata/xml/no-doctype.plist create mode 100644 testdata/xml/no-plist-end.plist create mode 100644 testdata/xml/no-plist-version.plist create mode 100644 testdata/xml/no-xml-tag.plist create mode 100644 testdata/xml/swapped.plist create mode 100644 testdata/xml/unescaped-plist.plist create mode 100644 testdata/xml/unescaped-xml.plist create mode 100644 testdata/xml/valid.plist create mode 100644 xml_parser.go create mode 100644 xml_writer.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cc3185a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # Don't change this despite the path being .github/workflows + schedule: + # Check for updates to GitHub Actions on the first day of the month + interval: "monthly" + + - package-ecosystem: "gomod" + directory: "/" + schedule: + # Check for updates to Go modules on the first day of the month + interval: "monthly" diff --git a/.github/workflows/on-push-pr.yml b/.github/workflows/on-push-pr.yml new file mode 100644 index 0000000..b41424b --- /dev/null +++ b/.github/workflows/on-push-pr.yml @@ -0,0 +1,27 @@ +name: CI/CD +on: + push: + branches: [main] + tags: ["v*.*.*"] + pull_request: + types: [opened, reopened, synchronize] +jobs: + format-build-test: + strategy: + matrix: + go-version: ['1.21.x', '1.22.x'] + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version: ${{ matrix.go-version }} + + - if: matrix.platform == 'ubuntu-latest' + run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi + + - run: go build -v ./... + + - run: go test -cover -race -v ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80dd8ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Reference https://github.com/github/gitignore/blob/master/Go.gitignore +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# OS General +Thumbs.db +.DS_Store + +# project +*.log +bin/ + +# Develop tools +.vscode/ +.idea/ +.run +*.swp +logs diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..35875f3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 xiaonan chen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7caa0f9 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Go Plist library + +[![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) + +This Plist library is used for decoding and encoding Apple Property Lists in both XML and binary forms. + +Example using HTTP streams: + +```go +func someHTTPHandler(w http.ResponseWriter, r *http.Request) { + var sparseBundleHeader struct { + InfoDictionaryVersion *string `plist:"CFBundleInfoDictionaryVersion"` + BandSize *uint64 `plist:"band-size"` + BackingStoreVersion int `plist:"bundle-backingstore-version"` + DiskImageBundleType string `plist:"diskimage-bundle-type"` + Size uint64 `plist:"unknownKey"` + Payload []any `plist:"payload"` + } + + // decode an HTTP request body into the sparseBundleHeader struct + if err := plist.NewXMLDecoder(r.Body).Decode(&sparseBundleHeader); err != nil { + log.Println(err) + return + } +} +``` + +## Credit + +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` diff --git a/binary_parser.go b/binary_parser.go new file mode 100644 index 0000000..7d2c051 --- /dev/null +++ b/binary_parser.go @@ -0,0 +1,436 @@ +package plist + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "time" + "unicode/utf16" +) + +// plistTrailer is the last 32 bytes of a binary plist +// See definition of CFBinaryPlistTrailer here +// https://opensource.apple.com/source/CF/CF-550.29/ForFoundationOnly.h +type plistTrailer struct { + _ [5]byte // unused padding + SortVersion uint8 // seems to be unused (always zero) + OffsetIntSize uint8 // byte size of offset ints in offset table + ObjectRefSize uint8 // byte size of object refs in arrays and dicts + NumObjects uint64 // number of objects (also number of offsets in offset table) + RootObject uint64 // object ref of top level object + OffsetTableOffset uint64 // offset of the offset table +} + +type binaryParser struct { + OffsetTable []uint64 // array of offsets for each object in plist + plistTrailer // last 32 bytes of plist + io.ReadSeeker // reader for plist data +} + +const numObjectsMax = 4 << 20 + +// newBinaryParser takes in a ReadSeeker for the bytes of a binary plist and +// returns a parser after reading the offset table and trailer. +func newBinaryParser(r io.ReadSeeker) (*binaryParser, error) { + var bp binaryParser + bp.ReadSeeker = r + + // Read the trailer. + if _, err := bp.Seek(-32, io.SeekEnd); err != nil { + return nil, fmt.Errorf("plist: couldn't seek to start of trailer: %v", err) + } + if err := binary.Read(bp, binary.BigEndian, &bp.plistTrailer); err != nil { + return nil, fmt.Errorf("plist: couldn't read trailer: %v", err) + } + + // Read the offset table. + if _, err := bp.Seek(int64(bp.OffsetTableOffset), io.SeekStart); err != nil { + return nil, fmt.Errorf("plist: couldn't seek to start of offset table: %v", err) + } + + // numObjectsMax is arbitrary. Please fix. + // TODO(github.com/groob/plist/issues/28) + if bp.NumObjects > numObjectsMax { + return nil, fmt.Errorf("plist: offset size larger than expected %d", numObjectsMax) + } + + bp.OffsetTable = make([]uint64, bp.NumObjects) + if bp.OffsetIntSize > 8 { + return nil, fmt.Errorf("plist: can't decode when offset int size (%d) is greater than 8", bp.OffsetIntSize) + } + for i := uint64(0); i < bp.NumObjects; i++ { + buf := make([]byte, 8) + if _, err := bp.Read(buf[8-bp.OffsetIntSize:]); err != nil { + return nil, fmt.Errorf("plist: couldn't read offset table: %v", err) + } + bp.OffsetTable[i] = uint64(binary.BigEndian.Uint64(buf)) + } + + return &bp, nil +} + +// parseDocument parses the entire binary plist starting from the root object +// and returns a plistValue representing the root object. +func (bp *binaryParser) parseDocument() (*plistValue, error) { + // Decode and return the root object. + return bp.parseObjectRef(bp.RootObject) +} + +// parseObjectRef decodes and returns the plist object with the given index. +// Index 0 is the first object in the object table, 1 is the second, etc. +// This function restores the current plist offset when it's done so that you +// may call it while decoding a collection object without losing your place. +func (bp *binaryParser) parseObjectRef(index uint64) (val *plistValue, err error) { + // Save the current offset. + offset, err := bp.Seek(0, io.SeekCurrent) + if err != nil { + return nil, err + } + // Then restore the original offset in a defer. + defer func() { + _, err2 := bp.Seek(offset, io.SeekStart) + if err2 != nil { + err = err2 + } + }() + + if index > uint64(len(bp.OffsetTable)) { + return nil, fmt.Errorf("plist: offset too large: %d", index) + } + // Move to the start of the object we want to decode. + if _, err := bp.Seek(int64(bp.OffsetTable[index]), io.SeekStart); err != nil { + return nil, err + } + // The first byte of the object is its marker byte. + // High 4 bits of marker byte indicates the object type. + // Low 4 bits contain additional info, typically a count. + // Defined here: https://opensource.apple.com/source/CF/CF-550.29/CFBinaryPList.c + // (using Read instead of ReadByte so that we can accept a ReadSeeker) + b := make([]byte, 1) + if _, err := bp.Read(b); err != nil { + return nil, err + } + marker := b[0] + switch marker >> 4 { + case 0x0: // null, bool, or fill + return bp.parseSingleton(marker) + case 0x1: // integer + return bp.parseInteger(marker) + case 0x2: // real + return bp.parseReal(marker) + case 0x3: // date + return bp.parseDate(marker) + case 0x4: // data + return bp.parseData(marker) + case 0x5: // ascii string + return bp.parseASCII(marker) + case 0x6: // unicode (utf-16) string + return bp.parseUTF16(marker) + case 0x8: // uid (not supported) + return &plistValue{Invalid, nil}, nil + case 0xa: // array + return bp.parseArray(marker) + case 0xc: // set (not supported) + return &plistValue{Invalid, nil}, nil + case 0xd: // dictionary + return bp.parseDict(marker) + } + return nil, fmt.Errorf("plist: unknown object type %x", marker>>4) +} + +func (bp *binaryParser) parseSingleton(marker byte) (*plistValue, error) { + switch marker & 0xf { + case 0x0: // null (not supported) + return &plistValue{Invalid, nil}, nil + case 0x8: // bool false + return &plistValue{Boolean, false}, nil + case 0x9: // bool true + return &plistValue{Boolean, true}, nil + case 0xf: // fill (not supported) + return &plistValue{Invalid, nil}, nil + } + return nil, fmt.Errorf("plist: unrecognized singleton type %x", marker&0xf) +} + +func (bp *binaryParser) parseInteger(marker byte) (*plistValue, error) { + // Integers are always stored as signed 64-bit integer, with leading zeros + // removed, so that the serialized form is either 1, 2, 4, or 8 bytes in + // length. + // See: https://opensource.apple.com/source/CF/CF-550.29/CFBinaryPList.c + // + // There is some discussion regarding 128-bit number support in the Python + // bug report below. The conclusion was that public APIs only allow plists + // to contain up to 64-bit values. + // + // There is also an example of a bplist containing an unsigned value + // 0xffffffffffffffff [1], but when you look at its encoding, it is crafted + // using a 128bit field, with the upper 8 bytes all zeros. Loading it into + // the Xcode plist editor, it appears as -1! That must be a cosmetic bug + // in Xcode because if you then export the same plist as XML, you get the full + // value, 18446744073709551615 [2]. + // + // So that we can decode such bplists, we will allow 16-byte integer values, + // but they will always be truncated to 64 bits. + // + // If you try and create a new plist in the Xcode editor, and paste in the + // 64-bit number 18446744073709551615, Xcode automatically rewrites it as + // the truncated 63-bit number 9223372036854775807. + // + // Separately, if you now create another new plist in the Xcode editor with + // the value -1, you get [3] and [4]: both negative values. + // + // [1] "hand crafted" bplist with 128-bit value. + // 00000000 62 70 6c 69 73 74 30 30 d1 01 02 53 6b 65 79 14 |bplist00...Skey.| + // 00000010 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff |................| + // 00000020 08 0b 0f 00 00 00 00 00 00 01 01 00 00 00 00 00 |................| + // 00000030 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + // 00000040 00 00 20 |.. | + // + // [2] export of [1] to XML form, using Xcode. + // + // + // + // + // key + // 18446744073709551615 + // + // + // + // [3] bplist with a value of -1, created with Xcode. + // 00000000 62 70 6c 69 73 74 30 30 d1 01 02 53 6b 65 79 13 |bplist00...Skey.| + // 00000010 ff ff ff ff ff ff ff ff 08 0b 0f 00 00 00 00 00 |................| + // 00000020 00 01 01 00 00 00 00 00 00 00 03 00 00 00 00 00 |................| + // 00000030 00 00 00 00 00 00 00 00 00 00 18 |...........| + // + // [4] export of [3] to XML form, using Xcode. + // + // + // + // + // key + // -1 + // + // + // + // We have two choices here: restrict unsigned values to 63-bits, or just + // let the package user interpret the serialized value. This implementation + // lets the user decide: if they want to unmarshal into a uint64 value, + // we give them the full 8-bytes as such. If they unmarshal into a signed + // int64 value, we again give them the full 8-bytes as such, which will be + // interpreted as negative if the top bit is set. This is achieved by + // setting the signed field of the signedInt value below to false + // unconditionally. That way, the Decoder.unmarshalInteger method will do + // the right thing. + // + // For XML property list unmarshaling, the presence of the "negative sign" + // on an integer value makes the above unambigious, and the current practice + // of using signedInt.signed = true in that case remains valid; see the + // xmlParser.parseInteger implementation. + // + // See: https://bugs.python.org/issue14455 + nbytes := 1 << (marker & 0xf) + if nbytes > 16 { + return nil, fmt.Errorf("plist: cannot decode integers longer than 16 bytes (%d)", nbytes) + } + // Read into the right-most bytes of a 16-byte zero-valued buffer. + buf := make([]byte, 16) + _, err := bp.Read(buf[16-nbytes:]) + if err != nil { + return nil, err + } + // Truncate values to 64 bits (8 bytes), and treat them all as "unsigned", + // so they can be unmarshaled to unsigned and signed integers alike as + // discussed above. + result := signedInt{binary.BigEndian.Uint64(buf[8:]), false} + + return &plistValue{Integer, result}, nil +} + +func (bp *binaryParser) parseReal(marker byte) (*plistValue, error) { + nbytes := 1 << (marker & 0xf) + buf := make([]byte, nbytes) + if _, err := bp.Read(buf); err != nil { + return nil, err + } + switch len(buf) { + case 4: + var r float32 + if err := binary.Read(bytes.NewReader(buf), binary.BigEndian, &r); err != nil { + return nil, err + } + return &plistValue{Real, sizedFloat{float64(r), nbytes * 8}}, nil + case 8: + var r float64 + if err := binary.Read(bytes.NewReader(buf), binary.BigEndian, &r); err != nil { + return nil, err + } + return &plistValue{Real, sizedFloat{r, nbytes * 8}}, nil + default: + return nil, fmt.Errorf("plist: invalid length of real number: expected: 4 or 8, got: %d", len(buf)) + } +} + +func (bp *binaryParser) parseDate(marker byte) (*plistValue, error) { + if marker&0xf != 0x3 { + return nil, fmt.Errorf("plist: invalid marker byte for date: %x", marker) + } + buf := make([]byte, 8) + if _, err := bp.Read(buf); err != nil { + return nil, err + } + var t float64 + if err := binary.Read(bytes.NewReader(buf), binary.BigEndian, &t); err != nil { + return nil, err + } + // The float time is Apple Epoch time (secs since Jan 1, 2001 GMT) but we + // need to convert it to Unix Epoch time (secs since Jan 1, 1970 GMT) + t += 978307200 + secs := int64(t) + nsecs := int64((t - float64(secs)) * 1e9) + return &plistValue{Date, time.Unix(secs, nsecs)}, nil +} + +func (bp *binaryParser) parseData(marker byte) (*plistValue, error) { + count, err := bp.readCount(marker) + if err != nil { + return nil, err + } + buf := make([]byte, count) + if _, err := bp.Read(buf); err != nil { + return nil, err + } + return &plistValue{Data, buf}, nil +} + +func (bp *binaryParser) parseASCII(marker byte) (*plistValue, error) { + count, err := bp.readCount(marker) + if err != nil { + return nil, err + } + buf := make([]byte, count) + if _, err := bp.Read(buf); err != nil { + return nil, err + } + return &plistValue{String, string(buf)}, nil +} + +func (bp *binaryParser) parseUTF16(marker byte) (*plistValue, error) { + count, err := bp.readCount(marker) + if err != nil { + return nil, err + } + // Each character in the UTF16 string is 2 bytes. First we read everything + // into a byte slice, then convert this into a slice of uint16, then this + // gets converted into a slice of rune, which gets converted to a string. + buf := make([]byte, 2*count) + if _, err := bp.Read(buf); err != nil { + return nil, err + } + uni := make([]uint16, count) + if err := binary.Read(bytes.NewReader(buf), binary.BigEndian, uni); err != nil { + return nil, err + } + return &plistValue{String, string(utf16.Decode(uni))}, nil +} + +func (bp *binaryParser) parseArray(marker byte) (*plistValue, error) { + count, err := bp.readCount(marker) + if err != nil { + return nil, err + } + // A list of count object refs representing the items in the array follow. + list, err := bp.readObjectList(count) + if err != nil { + return nil, err + } + return &plistValue{Array, list}, nil +} + +func (bp *binaryParser) parseDict(marker byte) (*plistValue, error) { + count, err := bp.readCount(marker) + if err != nil { + return nil, err + } + // A list of 2*count object refs follow. All of the keys are listed first, + // followed by all of the values. + keys, err := bp.readObjectList(count) + if err != nil { + return nil, err + } + vals, err := bp.readObjectList(count) + if err != nil { + return nil, err + } + m := make(map[string]*plistValue) + for i := uint64(0); i < count; i++ { + if keys[i].kind != String { + return nil, fmt.Errorf("plist: dictionary key is not a string: %v", keys[i]) + } + m[keys[i].value.(string)] = vals[i] + } + return &plistValue{Dictionary, &dictionary{m: m}}, nil +} + +// readCount reads the variable-length encoded integer count +// used by data, strings, arrays, and dicts +func (bp *binaryParser) readCount(marker byte) (uint64, error) { + // Check marker for count < 15 in lower 4 bits. + if marker&0xf != 0xf { + return uint64(marker & 0xf), nil + } + // Otherwise must read additional bytes to get count. Read first byte: + // (using Read instead of ReadByte so that we can accept a ReadSeeker) + b := make([]byte, 1) + if _, err := bp.Read(b); err != nil { + return 0, err + } + first := b[0] + // The lower 4 bits of indicate how many additional bytes to read: + // 0 means 1 additional byte + // 1 means 2 additional bytes + // 2 means 4 additional bytes + // 3 means 8 additional bytes + nbytes := 1 << (first & 0x0f) + // Number of bytes in count should be at most 8. + if nbytes > 8 { + return 0, fmt.Errorf("plist: invalid nbytes (%d) in readCount", nbytes) + } + buf := make([]byte, 8) + // Shove these bytes into the low end of an 8-byte buffer. + if _, err := bp.Read(buf[8-nbytes:]); err != nil { + return 0, err + } + return binary.BigEndian.Uint64(buf), nil +} + +// readObjectRef reads the next ObjectRefSize bytes from the binary plist +// and returns the bytes decoded into an integer value. +func (bp *binaryParser) readObjectRef() (uint64, error) { + buf := make([]byte, 8) + if _, err := bp.Read(buf[8-bp.ObjectRefSize:]); err != nil { + return 0, err + } + return binary.BigEndian.Uint64(buf), nil +} + +// readObjectList is a helper function for parseArray and parseDict. +// It decodes a sequence of object refs from the current offset in the plist +// and returns the decoded objects in a slice. +func (bp *binaryParser) readObjectList(count uint64) ([]*plistValue, error) { + list := make([]*plistValue, count) + for i := uint64(0); i < count; i++ { + // Read index of object in offset table. + ref, err := bp.readObjectRef() + if err != nil { + return nil, err + } + // Find and decode the object in object table, then add it to list. + v, err := bp.parseObjectRef(ref) + if err != nil { + return nil, err + } + list[i] = v + } + return list, nil +} diff --git a/decode.go b/decode.go new file mode 100644 index 0000000..7fb9d84 --- /dev/null +++ b/decode.go @@ -0,0 +1,345 @@ +package plist + +import ( + "bytes" + "errors" + "fmt" + "io" + "reflect" + "time" +) + +// MarshalFunc is a function used to Unmarshal custom plist types. +type MarshalFunc func(interface{}) error + +type Unmarshaler interface { + UnmarshalPlist(f func(interface{}) error) error +} + +// Unmarshal parses the plist-encoded data and stores the result in the value pointed to by v. +func Unmarshal(data []byte, v interface{}) error { + // Check for binary plist here before setting up the decoder. + if bytes.HasPrefix(data, []byte("bplist0")) { + return NewBinaryDecoder(bytes.NewReader(data)).Decode(v) + } + return NewXMLDecoder(bytes.NewReader(data)).Decode(v) +} + +// A Decoder reads and decodes Apple plist objects from an input stream. +// The plists can be in XML or binary format. +type Decoder struct { + reader io.Reader // binary decoders assert this to io.ReadSeeker + isBinary bool // true if this is a binary plist +} + +// NewDecoder returns a new XML plist decoder. +// DEPRECATED: Please use NewXMLDecoder instead. +func NewDecoder(r io.Reader) *Decoder { + return NewXMLDecoder(r) +} + +// NewXMLDecoder returns a new decoder that reads an XML plist from r. +func NewXMLDecoder(r io.Reader) *Decoder { + return &Decoder{reader: r, isBinary: false} +} + +// NewBinaryDecoder returns a new decoder that reads a binary plist from r. +// No error checking is done to make sure that r is actually a binary plist. +func NewBinaryDecoder(r io.ReadSeeker) *Decoder { + return &Decoder{reader: r, isBinary: true} +} + +// Decode reads the next plist-encoded value from its input and stores it in +// the value pointed to by v. Decode uses xml.Decoder to do the heavy lifting +// for XML plists, and uses binaryParser for binary plists. +func (d *Decoder) Decode(v interface{}) error { + val := reflect.ValueOf(v) + if val.Kind() != reflect.Ptr { + return errors.New("plist: non-pointer passed to Unmarshal") + } + var pval *plistValue + if d.isBinary { + // For binary decoder, type assert the reader to an io.ReadSeeker + var err error + r, ok := d.reader.(io.ReadSeeker) + if !ok { + return fmt.Errorf("binary plist decoder requires an io.ReadSeeker") + } + parser, err := newBinaryParser(r) + if err != nil { + return err + } + pval, err = parser.parseDocument() + if err != nil { + return err + } + } else { + var err error + parser := newXMLParser(d.reader) + pval, err = parser.parseDocument(nil) + if err != nil { + return err + } + } + return d.unmarshal(pval, val.Elem()) +} + +func (d *Decoder) unmarshal(pval *plistValue, v reflect.Value) error { + // check for empty interface v type + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + val := reflect.ValueOf(d.valueInterface(pval)) + if !val.IsValid() { + return fmt.Errorf("plist: invalid reflect.Value %v", v) + } + v.Set(val) + return nil + } + + if v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + + unmarshalerType := reflect.TypeOf((*Unmarshaler)(nil)).Elem() + + if v.CanInterface() && v.Type().Implements(unmarshalerType) { + u := v.Interface().(Unmarshaler) + return u.UnmarshalPlist(func(i interface{}) error { + return d.unmarshal(pval, reflect.ValueOf(i)) + }) + } + + if v.CanAddr() { + pv := v.Addr() + if pv.CanInterface() && pv.Type().Implements(unmarshalerType) { + u := pv.Interface().(Unmarshaler) + return u.UnmarshalPlist(func(i interface{}) error { + return d.unmarshal(pval, reflect.ValueOf(i)) + }) + } + + } + + // change pointer values to the correct type + // ex type foo struct { + // Foo *string `plist:"foo" + // } + if v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + + } + + switch pval.kind { + case String: + return d.unmarshalString(pval, v) + case Dictionary: + return d.unmarshalDictionary(pval, v) + case Array: + return d.unmarshalArray(pval, v) + case Boolean: + return d.unmarshalBoolean(pval, v) + case Real: + return d.unmarshalReal(pval, v) + case Integer: + return d.unmarshalInteger(pval, v) + case Data: + return d.unmarshalData(pval, v) + case Date: + return d.unmarshalDate(pval, v) + default: + return fmt.Errorf("plist: %v is an unsuported plist element kind", pval.kind) + } +} + +func (d *Decoder) unmarshalDate(pval *plistValue, v reflect.Value) error { + if v.Type() != reflect.TypeOf((*time.Time)(nil)).Elem() { + return UnmarshalTypeError{fmt.Sprintf("%v", pval.value), v.Type()} + } + v.Set(reflect.ValueOf(pval.value.(time.Time))) + return nil +} + +func (d *Decoder) unmarshalData(pval *plistValue, v reflect.Value) error { + if v.Kind() != reflect.Slice || v.Type().Elem().Kind() != reflect.Uint8 { + return UnmarshalTypeError{fmt.Sprintf("%s", pval.value.([]byte)), v.Type()} + } + v.SetBytes(pval.value.([]byte)) + return nil +} + +func (d *Decoder) unmarshalReal(pval *plistValue, v reflect.Value) error { + if v.Kind() != reflect.Float32 && v.Kind() != reflect.Float64 { + return UnmarshalTypeError{fmt.Sprintf("%v", pval.value.(sizedFloat).value), v.Type()} + } + v.SetFloat(pval.value.(sizedFloat).value) + return nil +} + +func (d *Decoder) unmarshalBoolean(pval *plistValue, v reflect.Value) error { + if v.Kind() != reflect.Bool { + return UnmarshalTypeError{fmt.Sprintf("%v", pval.value), v.Type()} + } + v.SetBool(pval.value.(bool)) + return nil +} + +func (d *Decoder) unmarshalDictionary(pval *plistValue, v reflect.Value) error { + subvalues := pval.value.(*dictionary).m + switch v.Kind() { + case reflect.Struct: + fields := cachedTypeFields(v.Type()) + for _, field := range fields { + if _, ok := subvalues[field.name]; !ok { + continue + } + if err := d.unmarshal(subvalues[field.name], field.value(v)); err != nil { + return err + } + } + case reflect.Map: + if v.IsNil() { + v.Set(reflect.MakeMap(v.Type())) + } + for k, sval := range subvalues { + keyv := reflect.ValueOf(k).Convert(v.Type().Key()) + mapElem := v.MapIndex(keyv) + if !mapElem.IsValid() { + mapElem = reflect.New(v.Type().Elem()).Elem() + } + if err := d.unmarshal(sval, mapElem); err != nil { + return err + } + v.SetMapIndex(keyv, mapElem) + } + default: + return UnmarshalTypeError{"dict", v.Type()} + } + return nil +} + +func (d *Decoder) unmarshalString(pval *plistValue, v reflect.Value) error { + if v.Kind() != reflect.String { + return UnmarshalTypeError{fmt.Sprintf("%s", pval.value.(string)), v.Type()} + } + v.SetString(pval.value.(string)) + return nil +} + +func (d *Decoder) unmarshalArray(pval *plistValue, v reflect.Value) error { + subvalues := pval.value.([]*plistValue) + switch v.Kind() { + case reflect.Slice: + // Slice of element values. + // Grow slice. + // Borrowed from https://golang.org/src/encoding/xml/read.go + cnt := len(subvalues) + if cnt >= v.Cap() { + ncap := 2 * cnt + if ncap < 4 { + ncap = 4 + } + new := reflect.MakeSlice(v.Type(), v.Len(), ncap) + reflect.Copy(new, v) + v.Set(new) + } + n := v.Len() + v.SetLen(cnt) + for _, sval := range subvalues { + if err := d.unmarshal(sval, v.Index(n)); err != nil { + v.SetLen(cnt) + return err + } + n++ + } + default: + return UnmarshalTypeError{"array", v.Type()} + } + return nil +} + +func (d *Decoder) unmarshalInteger(pval *plistValue, v reflect.Value) error { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + v.SetInt(int64(pval.value.(signedInt).value)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + // Make sure plistValue isn't negative when decoding into uint. + if pval.value.(signedInt).signed { + return UnmarshalTypeError{ + fmt.Sprintf("%v", int64(pval.value.(signedInt).value)), v.Type()} + } + v.SetUint(pval.value.(signedInt).value) + default: + return UnmarshalTypeError{ + fmt.Sprintf("%v", pval.value.(signedInt).value), v.Type()} + } + return nil +} + +// empty interface values +// borrowed from go-plist +func (d *Decoder) valueInterface(pval *plistValue) interface{} { + switch pval.kind { + case String: + return pval.value.(string) + case Integer: + if pval.value.(signedInt).signed { + return int64(pval.value.(signedInt).value) + } + return pval.value.(signedInt).value + case Real: + bits := pval.value.(sizedFloat).bits + switch bits { + case 32: + return float32(pval.value.(sizedFloat).value) + case 64: + return pval.value.(sizedFloat).value + default: + return nil + } + case Boolean: + return pval.value.(bool) + case Array: + return d.arrayInterface(pval.value.([]*plistValue)) + case Dictionary: + return d.dictionaryInterface(pval.value.(*dictionary)) + case Data: + return pval.value.([]byte) + case Date: + return pval.value.(time.Time) + default: + return nil + } +} + +func (d *Decoder) arrayInterface(subvalues []*plistValue) []interface{} { + out := make([]interface{}, len(subvalues)) + for i, subv := range subvalues { + out[i] = d.valueInterface(subv) + } + return out +} + +func (d *Decoder) dictionaryInterface(dict *dictionary) map[string]interface{} { + out := make(map[string]interface{}) + for k, subv := range dict.m { + out[k] = d.valueInterface(subv) + } + return out +} + +// An UnmarshalTypeError describes a plist value that was +// not appropriate for a value of a specific Go type. +type UnmarshalTypeError struct { + Value string // description of plist value - "true", "string", "date" + Type reflect.Type +} + +func (e UnmarshalTypeError) Error() string { + return "plist: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() +} diff --git a/decode_test.go b/decode_test.go new file mode 100644 index 0000000..a91cfb3 --- /dev/null +++ b/decode_test.go @@ -0,0 +1,580 @@ +package plist + +import ( + "bytes" + "encoding/base64" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "os/exec" + "path/filepath" + "reflect" + "testing" + "time" +) + +var decodeTests = []struct { + out interface{} + in string +}{ + {"foo", fooRef}, + {"UTF-8 ☼", utf8Ref}, + {uint64(0), zeroRef}, + {uint64(1), oneRef}, + {1.2, realRef}, + {false, falseRef}, + {true, trueRef}, + {[]interface{}{"a", "b", "c", uint64(4), true}, arrRef}, + {time.Date(1900, 01, 01, 12, 00, 00, 0, time.UTC), time1900Ref}, + { + map[string]interface{}{ + "foo": "bar", + "bool": true, + }, + dictRef, + }, +} + +func TestDecodeEmptyInterface(t *testing.T) { + for _, tt := range decodeTests { + var out interface{} + if err := Unmarshal([]byte(tt.in), &out); err != nil { + t.Error(err) + continue + } + eq := reflect.DeepEqual(out, tt.out) + if !eq { + t.Errorf("Unmarshal(%v) = \n%v, want %v", tt.in, out, tt.out) + } + } +} + +func TestDecodeDict(t *testing.T) { + // Test struct + expected := struct { + InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"` + BandSize uint64 `plist:"band-size"` + BackingStoreVersion int `plist:"bundle-backingstore-version"` + DiskImageBundleType string `plist:"diskimage-bundle-type"` + Size uint64 `plist:"size"` + }{ + InfoDictionaryVersion: "6.0", + BandSize: 8388608, + Size: 4 * 1048576 * 1024 * 1024, + DiskImageBundleType: "com.apple.diskimage.sparsebundle", + BackingStoreVersion: 1, + } + + var sparseBundleHeader struct { + InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"` + BandSize uint64 `plist:"band-size"` + BackingStoreVersion int `plist:"bundle-backingstore-version"` + DiskImageBundleType string `plist:"diskimage-bundle-type"` + Size uint64 `plist:"size"` + } + + if err := Unmarshal([]byte(indentRef), &sparseBundleHeader); err != nil { + t.Fatal(err) + } + if sparseBundleHeader != expected { + t.Error("Expected", expected, "got", sparseBundleHeader) + } + + // Test Map + var mapHeader = map[string]interface{}{} + // Output map[CFBundleInfoDictionaryVersion:6.0 band-size:8388608 bundle-backingstore-version:1 diskimage-bundle-type:com.apple.diskimage.sparsebundle size:4398046511104] + if err := Unmarshal([]byte(indentRef), &mapHeader); err != nil { + t.Fatal(err) + } + if mapHeader["CFBundleInfoDictionaryVersion"] != "6.0" { + t.Fatal("Expected", "6.0", "got", mapHeader["CFBundleInfoDictionaryVersion"]) + } +} + +func TestDecodeArray(t *testing.T) { + const input = ` + +foobar` + var data []string + expected := []string{"foo", "bar"} + if err := Unmarshal([]byte(input), &data); err != nil { + t.Fatal(err) + } + if eq := reflect.DeepEqual(data, expected); !eq { + t.Error("Expected", expected, "got", data) + } +} + +func TestDecodeBoolean(t *testing.T) { + const input = ` + +` + var data bool + expected := true + if err := Unmarshal([]byte(input), &data); err != nil { + t.Fatal(err) + } + if data != expected { + t.Error("Expected", expected, "got", data) + } +} + +func TestDecodeLargeInteger(t *testing.T) { + const input = ` + +18446744073709551615` + var data uint64 + expected := uint64(18446744073709551615) + if err := Unmarshal([]byte(input), &data); err != nil { + t.Fatal(err) + } + if data != expected { + t.Error("Expected", expected, "got", data) + } +} + +func TestDecodeNegativeInteger(t *testing.T) { + // There is an intentional space before -42. + const input = ` + + -42` + var data int + expected := -42 + if err := Unmarshal([]byte(input), &data); err != nil { + t.Fatal(err) + } + if data != expected { + t.Error("Expected", expected, "got", data) + } +} + +func TestDecodeNegativeIntegerIntoUint(t *testing.T) { + const input = ` + +-42` + var data uint + if err := Unmarshal([]byte(input), &data); err == nil { + t.Error("Expected error, but unmarshal gave", data) + } +} + +func TestDecodeLargeNegativeInteger(t *testing.T) { + const input = ` + +-9223372036854775808` + var data int64 + expected := int64(-9223372036854775808) + if err := Unmarshal([]byte(input), &data); err != nil { + t.Fatal(err) + } + if data != expected { + t.Error("Expected", expected, "got", data) + } +} + +func TestDecodeReal(t *testing.T) { + const input = ` + +1.2` + var data float64 + expected := 1.2 + if err := Unmarshal([]byte(input), &data); err != nil { + t.Fatal(err) + } + if data != expected { + t.Error("Expected", expected, "got", data) + } +} + +func TestDecodeNegativeReal(t *testing.T) { + const input = ` + +-3.14159` + var data float64 + expected := -3.14159 + if err := Unmarshal([]byte(input), &data); err != nil { + t.Fatal(err) + } + if data != expected { + t.Error("Expected", expected, "got", data) + } +} + +func TestDecodeDate(t *testing.T) { + const input = ` + + + 2011-05-12T01:00:00Z +` + var data time.Time + expected, _ := time.Parse(time.RFC3339, "2011-05-12T01:00:00Z") + if err := Unmarshal([]byte(input), &data); err != nil { + t.Fatal(err) + } + if data != expected { + t.Error("Expected", expected, "got", data) + } +} + +func TestDecodeData(t *testing.T) { + expected := ` + +foo +` + type data []byte + out := data{} + if err := Unmarshal([]byte(dataRef), &out); err != nil { + t.Fatal(err) + } + if string(out) != expected { + t.Error("Want:\n", expected, "\ngot:\n", string(out)) + } +} + +func TestDecodeData_emptyData(t *testing.T) { + var before, after []byte + if err := Unmarshal([]byte(emptyDataRef), &after); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(before, after) { + t.Log("empty should result in []byte(nil)") + t.Errorf("before %#v, after %#v", before, after) + } +} + +func TestDecodeUnicodeString(t *testing.T) { + const input = ` + +こんにちは世界` + var data string + expected := "こんにちは世界" + if err := Unmarshal([]byte(input), &data); err != nil { + t.Fatal(err) + } + if data != expected { + t.Error("Expected", expected, "got", data) + } +} + +// Unknown struct fields should return an error +func TestDecodeUnknownStructField(t *testing.T) { + var sparseBundleHeader struct { + InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"` + BandSize uint64 `plist:"band-size"` + BackingStoreVersion int `plist:"bundle-backingstore-version"` + DiskImageBundleType string `plist:"diskimage-bundle-type"` + Size uint64 `plist:"unknownKey"` + } + if err := Unmarshal([]byte(indentRef), &sparseBundleHeader); err != nil { + t.Error("Expected error `plist: unknown struct field unknownKey`, got nil") + } +} + +func TestHTTPDecoding(t *testing.T) { + const raw = ` + +bar` + + ts := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(raw)) + }, + ), + ) + defer ts.Close() + res, err := http.Get(ts.URL) + if err != nil { + log.Fatalf("GET failed: %v", err) + } + defer res.Body.Close() + var foo string + d := NewDecoder(res.Body) + err = d.Decode(&foo) + if err != nil { + t.Fatalf("Decode: %v", err) + } + if foo != "bar" { + t.Errorf("decoded %q; want \"bar\"", foo) + } + err = d.Decode(&foo) + if err != io.EOF { + t.Errorf("err = %v; want io.EOF", err) + } +} + +func TestDecodePointer(t *testing.T) { + var sparseBundleHeader struct { + InfoDictionaryVersion *string `plist:"CFBundleInfoDictionaryVersion"` + BandSize *uint64 `plist:"band-size"` + BackingStoreVersion int `plist:"bundle-backingstore-version"` + DiskImageBundleType string `plist:"diskimage-bundle-type"` + Size uint64 `plist:"unknownKey"` + } + if err := Unmarshal([]byte(indentRef), &sparseBundleHeader); err != nil { + t.Fatal(err) + } + if *sparseBundleHeader.InfoDictionaryVersion != "6.0" { + t.Error("Expected", "6.0", "got", *sparseBundleHeader.InfoDictionaryVersion) + } +} + +func TestDecodeBinaryPlist(t *testing.T) { + tests := []struct { + filename string + expectedInts []int64 + }{ + { + filename: "sample2.binary.plist", + expectedInts: []int64{0, 42, -42, 255, -255, -123456, -9223372036854775807, 9223372036854775807}, + }, + } + + for _, tt := range tests { + t.Run( + tt.filename, func(t *testing.T) { + var sample struct { + Ints []int64 `plist:"ints"` + Signed int64 `plist:"signed"` + Unsigned uint64 `plist:"unsigned"` + Uint64 uint64 `plist:"uint64"` + Reals []float64 `plist:"reals"` + Date time.Time `plist:"date"` + Strings []string `plist:"strings"` + Data [][]byte `plist:"data"` + } + + content, err := ioutil.ReadFile(filepath.Join("testdata", tt.filename)) + if err != nil { + t.Fatal(err) + } + + if err := Unmarshal(content, &sample); err != nil { + t.Fatal(err) + } + + if got, want := len(sample.Ints), len(tt.expectedInts); got != want { + t.Errorf("decoded %d ints, want %d", got, want) + } + + for i, x := range tt.expectedInts { + if sample.Ints[i] != x { + t.Error("expected", x, "got", sample.Ints[i]) + } + } + + expectedUnsigned := uint64(1<<63 - 1) + if sample.Unsigned != expectedUnsigned { + t.Error("expected", expectedUnsigned, "got", sample.Unsigned) + } + + expectedSigned := int64(-1) + if sample.Signed != expectedSigned { + t.Error("expected", expectedSigned, "got", sample.Signed) + } + + expectedUint64 := ^uint64(0) // all bits set + if sample.Uint64 != expectedUint64 { + t.Error("expected", expectedUint64, "got", sample.Uint64) + } + + expectedReals := []float64{0.0, 3.14159, -1234.5678} + if len(expectedReals) != len(sample.Reals) { + t.Errorf("expected %d reals, but only decoded %d reals", len(expectedReals), len(sample.Reals)) + } + + for i, x := range expectedReals { + if sample.Reals[i] != x { + t.Error("expected", x, "got", sample.Reals[i]) + } + } + + expectedDate, _ := time.Parse(time.RFC3339, "2038-01-19T03:14:08Z") + if !sample.Date.Equal(expectedDate) { + t.Error("expected", expectedDate, "got", sample.Date) + } + + expectedStrings := []string{ + "short", + "こんにちは世界", + "this is a much longer string having more than 14 characters", + "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.", + } + if len(expectedStrings) != len(sample.Strings) { + t.Errorf( + "expected %d strings, but only decoded %d strings", + len(expectedStrings), + len(sample.Strings), + ) + } + for i, x := range expectedStrings { + if sample.Strings[i] != x { + t.Error("expected", x, "got", sample.Strings[i]) + } + } + + expectedData := [][]byte{ + MustDecodeBase64("PEKBpYGlmYFCPA=="), + MustDecodeBase64("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwgc2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRldXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLg=="), + } + if len(expectedData) != len(sample.Data) { + t.Errorf("expected %d data items, but only decoded %d", len(expectedData), len(sample.Data)) + } + for i, x := range expectedData { + if !bytes.Equal(sample.Data[i], x) { + t.Error("expected", x, "got", sample.Data[i]) + } + } + }, + ) + } +} + +func MustDecodeBase64(b64 string) []byte { + data, err := base64.StdEncoding.DecodeString(b64) + if err != nil { + panic(err) + } + return data +} + +type unmarshalerTest struct { + unmarshalInvoked bool + MustDecode string +} + +func (u *unmarshalerTest) UnmarshalPlist(f func(i interface{}) error) error { + u.unmarshalInvoked = true + return f(&u.MustDecode) +} + +func TestUnmarshaler(t *testing.T) { + const raw = ` + +bar` + + var u unmarshalerTest + if err := Unmarshal([]byte(raw), &u); err != nil { + t.Fatal(err) + } + + if !u.unmarshalInvoked { + t.Errorf("expected the UnmarshalPlist method to be invoked for unmarshaler") + } + + if have, want := u.MustDecode, "bar"; have != want { + t.Errorf("have %s, want %s", have, want) + } +} + +func TestFuzzCrashers(t *testing.T) { + dir := filepath.Join("testdata", "crashers") + testDir, err := ioutil.ReadDir(dir) + if err != nil { + t.Fatalf("reading dir %q: %s", dir, err) + } + + for _, tc := range testDir { + tc := tc + t.Run( + tc.Name(), func(t *testing.T) { + t.Parallel() + + crasher, err := ioutil.ReadFile(filepath.Join("testdata", "crashers", tc.Name())) + if err != nil { + t.Fatal(err) + } + + var i interface{} + Unmarshal(crasher, &i) + }, + ) + } +} + +func TestSmallInput(t *testing.T) { + type nop struct{} + nopStruct := &nop{} + for _, test := range []string{ + "", + "!", + " +NoTagNoTagOtherTagTagSkipTagSkipTag` + // Test struct + testStruct := struct { + NoTag string + Tag string `plist:"OtherTag"` + SkipTag string `plist:"-"` + }{} + + if err := Unmarshal([]byte(input), &testStruct); err != nil { + t.Fatal(err) + } + + if testStruct.SkipTag != "" { + t.Error("field decoded when it was tagged as -") + } +} + +// test parity with plutil -lint on macOS +var xmlParityTestFailures = map[string]struct{}{ + "empty-plist.plist": {}, + "invalid-before-plist.plist": {}, + "invalid-data.plist": {}, + "invalid-middle.plist": {}, + "invalid-start.plist": {}, + "no-dict-end.plist": {}, + "no-plist-end.plist": {}, + "unescaped-plist.plist": {}, + "unescaped-xml.plist": {}, +} + +func TestXMLPlutilParity(t *testing.T) { + type data struct { + Key string `plist:"key"` + } + tests, err := ioutil.ReadDir("testdata/xml/") + if err != nil { + t.Fatalf("could not open testdata/xml: %v", err) + } + + plutil, _ := exec.LookPath("plutil") + + for _, test := range tests { + testPath := filepath.Join("testdata/xml/", test.Name()) + buf, err := ioutil.ReadFile(testPath) + if err != nil { + t.Errorf("could not read test %s: %v", test.Name(), err) + continue + } + v := new(data) + err = Unmarshal(buf, v) + + _, check := xmlParityTestFailures[test.Name()] + if plutil != "" { + check = exec.Command(plutil, "-lint", testPath).Run() != nil + } + + if check && err == nil { + t.Errorf("expected error for test %s but got: nil", test.Name()) + } else if !check && err != nil { + t.Errorf("expected no error for test %s but got: %v", test.Name(), err) + } else if !check && v.Key != "val" { + t.Errorf("expected key=val for test %s but got: key=%s", test.Name(), v.Key) + } + } +} diff --git a/encode.go b/encode.go new file mode 100644 index 0000000..b3b29e9 --- /dev/null +++ b/encode.go @@ -0,0 +1,238 @@ +package plist + +import ( + "bytes" + "io" + "reflect" + "time" +) + +type Marshaler interface { + MarshalPlist() (interface{}, error) +} + +// Encoder ... +type Encoder struct { + w io.Writer + + indent string +} + +// Marshal ... +func Marshal(v interface{}) ([]byte, error) { + var buf bytes.Buffer + if err := NewEncoder(&buf).Encode(v); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// MarshalIndent ... +func MarshalIndent(v interface{}, indent string) ([]byte, error) { + var buf bytes.Buffer + enc := NewEncoder(&buf) + enc.Indent(indent) + if err := enc.Encode(v); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// NewEncoder returns a new encoder that writes to w. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{w: w} +} + +// Encode ... +func (e *Encoder) Encode(v interface{}) error { + pval, err := e.marshal(reflect.ValueOf(v)) + if err != nil { + return err + } + + enc := newXMLEncoder(e.w) + enc.Indent("", e.indent) + return enc.generateDocument(pval) +} + +// Indent ... +func (e *Encoder) Indent(indent string) { + e.indent = indent +} + +func (e *Encoder) marshal(v reflect.Value) (*plistValue, error) { + marshalerType := reflect.TypeOf((*Marshaler)(nil)).Elem() + + if v.CanInterface() && v.Type().Implements(marshalerType) { + m := v.Interface().(Marshaler) + val, err := m.MarshalPlist() + if err != nil { + return nil, err + } + return e.marshal(reflect.ValueOf(val)) + } + + if v.CanAddr() { + pv := v.Addr() + if pv.CanInterface() && pv.Type().Implements(marshalerType) { + m := pv.Interface().(Marshaler) + val, err := m.MarshalPlist() + if err != nil { + return nil, err + } + return e.marshal(reflect.ValueOf(val)) + } + } + + // check for empty interface v type + if v.Kind() == reflect.Interface && v.NumMethod() == 0 || v.Kind() == reflect.Ptr { + v = v.Elem() + } + + // check for time type + if v.Type() == reflect.TypeOf((*time.Time)(nil)).Elem() { + if date, ok := v.Interface().(time.Time); ok { + return &plistValue{Date, date}, nil + } + return nil, &UnsupportedValueError{v, v.String()} + } + + switch v.Kind() { + case reflect.String: + return &plistValue{String, v.String()}, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return &plistValue{Integer, signedInt{uint64(v.Int()), true}}, nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return &plistValue{Integer, signedInt{uint64(v.Uint()), false}}, nil + case reflect.Float32, reflect.Float64: + return &plistValue{Real, sizedFloat{v.Float(), v.Type().Bits()}}, nil + case reflect.Bool: + return &plistValue{Boolean, v.Bool()}, nil + case reflect.Slice, reflect.Array: + return e.marshalArray(v) + case reflect.Map: + return e.marshalMap(v) + case reflect.Struct: + return e.marshalStruct(v) + case reflect.Interface, reflect.Ptr: + // Attempt to marshal the underlying type of the interface + if v.CanInterface() { + if inter := v.Interface(); inter != nil { + // Pointer to interface + interptr := reflect.New(reflect.TypeOf(inter)) + // Elem requires an Interface or Pointer + if elem := interptr.Elem(); elem.IsValid() && elem.CanSet() { + elem.Set(reflect.ValueOf(inter)) + return e.marshal(reflect.Indirect(interptr)) + } + } + } + return nil, &UnsupportedTypeError{v.Type()} + default: + return nil, &UnsupportedTypeError{v.Type()} + } +} + +func (e *Encoder) marshalStruct(v reflect.Value) (*plistValue, error) { + fields := cachedTypeFields(v.Type()) + dict := &dictionary{ + m: make(map[string]*plistValue, len(fields)), + } + for _, field := range fields { + val := field.value(v) + if field.omitEmpty && isEmptyValue(val) { + continue + } + value, err := e.marshal(field.value(v)) + if err != nil { + return nil, err + } + dict.m[field.name] = value + } + return &plistValue{Dictionary, dict}, nil +} + +func (e *Encoder) marshalArray(v reflect.Value) (*plistValue, error) { + if v.Type().Elem().Kind() == reflect.Uint8 { + bytes := []byte(nil) + if v.CanAddr() { + bytes = v.Slice(0, v.Len()).Bytes() + } else { + bytes = make([]byte, v.Len()) + reflect.Copy(reflect.ValueOf(bytes), v) + } + return &plistValue{Data, bytes}, nil + } + subvalues := make([]*plistValue, v.Len()) + for idx, length := 0, v.Len(); idx < length; idx++ { + subpval, err := e.marshal(v.Index(idx)) + if err != nil { + return nil, err + } + if subpval != nil { + subvalues[idx] = subpval + } + } + return &plistValue{Array, subvalues}, nil +} + +func (e *Encoder) marshalMap(v reflect.Value) (*plistValue, error) { + if v.Type().Key().Kind() != reflect.String { + return nil, &UnsupportedTypeError{v.Type()} + } + + l := v.Len() + dict := &dictionary{ + m: make(map[string]*plistValue, l), + } + for _, keyv := range v.MapKeys() { + subpval, err := e.marshal(v.MapIndex(keyv)) + if err != nil { + return nil, err + } + if subpval != nil { + dict.m[keyv.String()] = subpval + } + } + return &plistValue{Dictionary, dict}, nil +} + +// An UnsupportedTypeError is returned by Marshal when attempting +// to encode an unsupported value type. +type UnsupportedTypeError struct { + Type reflect.Type +} + +func (e *UnsupportedTypeError) Error() string { + return "plist: unsupported type: " + e.Type.String() +} + +// UnsupportedValueError ... +type UnsupportedValueError struct { + Value reflect.Value + Str string +} + +func (e *UnsupportedValueError) Error() string { + return "plist: unsupported value: " + e.Str +} + +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflect.Struct: + return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) + } + return false +} diff --git a/encode_test.go b/encode_test.go new file mode 100644 index 0000000..79f3d04 --- /dev/null +++ b/encode_test.go @@ -0,0 +1,387 @@ +package plist + +import ( + "bytes" + "testing" + "time" +) + +var fooRef = ` + +foo +` + +var utf8Ref = ` + +UTF-8 ☼ +` + +var zeroRef = ` + +0 +` + +var oneRef = ` + +1 +` + +var minOneRef = ` + +-1 +` + +var realRef = ` + +1.2 +` + +var falseRef = ` + + +` + +var trueRef = ` + + +` + +var arrRef = ` + +abc4 +` + +var byteArrRef = ` + +/////////////////////w== +` + +var time1900Ref = ` + +1900-01-01T12:00:00Z +` + +var dataRef = ` + +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPjxzdHJpbmc+Zm9vPC9zdHJpbmc+PC9wbGlzdD4K +` + +var emptyDataRef = ` + + +` + +var dictRef = ` + +boolfoobar +` + +var indentRef = ` + + + + CFBundleInfoDictionaryVersion + 6.0 + band-size + 8388608 + bundle-backingstore-version + 1 + diskimage-bundle-type + com.apple.diskimage.sparsebundle + size + 4398046511104 + useless + + unused-string + unused + + + +` + +var indentRefOmit = ` + + + + CFBundleInfoDictionaryVersion + 6.0 + bundle-backingstore-version + 1 + diskimage-bundle-type + com.apple.diskimage.sparsebundle + size + 4398046511104 + + +` + +type testStruct struct { + UnusedString string `plist:"unused-string"` + UnusedByte []byte `plist:"unused-byte,omitempty"` +} + +var encodeTests = []struct { + in interface{} + out string +}{ + {"foo", fooRef}, + {"UTF-8 ☼", utf8Ref}, + {0, zeroRef}, + {1, oneRef}, + {uint64(1), oneRef}, + {-1, minOneRef}, + {1.2, realRef}, + {false, falseRef}, + {true, trueRef}, + {[]interface{}{"a", "b", "c", 4, true}, arrRef}, + {time.Date(1900, 01, 01, 12, 00, 00, 0, time.UTC), time1900Ref}, + {[]byte(fooRef), dataRef}, + { + map[string]interface{}{ + "foo": "bar", + "bool": true, + }, + dictRef, + }, + { + struct { + Foo string `plist:"foo"` + Bool bool `plist:"bool"` + }{"bar", true}, + dictRef, + }, + { + [][16]byte{ + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + }, byteArrRef, + }, +} + +func TestEncodeValues(t *testing.T) { + t.Parallel() + for _, tt := range encodeTests { + b, err := Marshal(tt.in) + if err != nil { + t.Error(err) + continue + } + out := string(b) + if out != tt.out { + t.Errorf("Marshal(%v) = \n%v, \nwant\n %v", tt.in, out, tt.out) + } + } +} + +func TestNewLineString(t *testing.T) { + t.Parallel() + multiline := struct { + Content string + }{ + Content: "foo\nbar", + } + + b, err := MarshalIndent(multiline, " ") + if err != nil { + t.Fatal(err) + } + var ok = ` + + + + Content + foo +bar + + +` + out := string(b) + if out != ok { + t.Errorf("Marshal(%v) = \n%v, \nwant\n %v", multiline, out, ok) + } + +} + +func TestIndent(t *testing.T) { + t.Parallel() + sparseBundleHeader := struct { + InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"` + BandSize uint64 `plist:"band-size"` + BackingStoreVersion int `plist:"bundle-backingstore-version"` + DiskImageBundleType string `plist:"diskimage-bundle-type"` + Size uint64 `plist:"size"` + Unused testStruct `plist:"useless"` + }{ + InfoDictionaryVersion: "6.0", + BandSize: 8388608, + Size: 4 * 1048576 * 1024 * 1024, + DiskImageBundleType: "com.apple.diskimage.sparsebundle", + BackingStoreVersion: 1, + Unused: testStruct{UnusedString: "unused"}, + } + b, err := MarshalIndent(sparseBundleHeader, " ") + if err != nil { + t.Fatal(err) + } + out := string(b) + if out != indentRef { + t.Errorf("MarshalIndent(%v) = \n%v, \nwant\n %v", sparseBundleHeader, out, indentRef) + } +} + +func TestOmitNotEmpty(t *testing.T) { + t.Parallel() + sparseBundleHeader := struct { + InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"` + BandSize uint64 `plist:"band-size,omitempty"` + BackingStoreVersion int `plist:"bundle-backingstore-version"` + DiskImageBundleType string `plist:"diskimage-bundle-type"` + Size uint64 `plist:"size"` + Unused testStruct `plist:"useless"` + }{ + InfoDictionaryVersion: "6.0", + BandSize: 8388608, + Size: 4 * 1048576 * 1024 * 1024, + DiskImageBundleType: "com.apple.diskimage.sparsebundle", + BackingStoreVersion: 1, + Unused: testStruct{UnusedString: "unused"}, + } + b, err := MarshalIndent(sparseBundleHeader, " ") + if err != nil { + t.Fatal(err) + } + out := string(b) + if out != indentRef { + t.Errorf("MarshalIndent(%v) = \n%v, \nwant\n %v", sparseBundleHeader, out, indentRef) + } +} + +func TestOmitIsEmpty(t *testing.T) { + t.Parallel() + sparseBundleHeader := struct { + InfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"` + BandSize uint64 `plist:"band-size,omitempty"` + BackingStoreVersion int `plist:"bundle-backingstore-version"` + DiskImageBundleType string `plist:"diskimage-bundle-type"` + Size uint64 `plist:"size"` + Unused testStruct `plist:"useless,omitempty"` + }{ + InfoDictionaryVersion: "6.0", + Size: 4 * 1048576 * 1024 * 1024, + DiskImageBundleType: "com.apple.diskimage.sparsebundle", + BackingStoreVersion: 1, + } + b, err := MarshalIndent(sparseBundleHeader, " ") + if err != nil { + t.Fatal(err) + } + out := string(b) + if out != indentRefOmit { + t.Errorf("MarshalIndent(%v) = \n%v, \nwant\n %v", sparseBundleHeader, out, indentRefOmit) + } +} + +type marshalerTest struct { + marshalFuncInvoked bool + MustMarshal string +} + +func (m *marshalerTest) MarshalPlist() (interface{}, error) { + m.marshalFuncInvoked = true + return &m.MustMarshal, nil +} + +func TestMarshaler(t *testing.T) { + t.Parallel() + want := []byte(` + +pants +`) + m := marshalerTest{MustMarshal: "pants"} + have, err := Marshal(&m) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(have, want) { + t.Errorf("expected \n%s got \n%s\n", have, want) + } +} + +func TestSelfClosing(t *testing.T) { + t.Parallel() + selfClosing := struct { + True bool + False bool + Absent bool + }{ + True: true, + False: false, + } + + want := []byte(` + +AbsentFalseTrue +`) + + have, err := Marshal(selfClosing) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(have, want) { + t.Errorf("expected \n%s got \n%s\n", have, want) + } + +} + +func TestEncodeTagSkip(t *testing.T) { + // Test struct + testStruct := struct { + NoTag string + Tag string `plist:"OtherTag"` + SkipTag string `plist:"-"` + }{ + NoTag: "NoTag", + Tag: "Tag", + SkipTag: "SkipTag", + } + + have, err := Marshal(&testStruct) + if err != nil { + t.Fatal(err) + } + + if bytes.Contains([]byte(have), []byte(testStruct.SkipTag)) { + t.Error("field encoded when it was tagged as -") + } +} + +type Dog struct { + Name string +} + +type Animal interface{} + +func TestInterfaceSliceMarshal(t *testing.T) { + x := make([]Animal, 0) + x = append(x, &Dog{Name: "dog"}) + + b, err := Marshal(x) + if err != nil { + t.Error(err) + } else if len(b) == 0 { + t.Error("expect non-zero data") + } +} + +func TestInterfaceGeneralSliceMarshal(t *testing.T) { + x := make([]interface{}, 0) // accept any type + x = append(x, &Dog{Name: "dog"}, "a string", 1, true) + + b, err := Marshal(x) + if err != nil { + t.Error(err) + } else if len(b) == 0 { + t.Error("expect non-zero data") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bbbc84f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/sawyer523/plist + +go 1.14 diff --git a/plist.go b/plist.go new file mode 100644 index 0000000..a89089a --- /dev/null +++ b/plist.go @@ -0,0 +1,76 @@ +package plist + +import "sort" + +type plistKind uint + +const ( + Invalid plistKind = iota + Dictionary + Array + String + Integer + Real + Boolean + Data + Date +) + +var plistKindNames = map[plistKind]string{ + Invalid: "invalid", + Dictionary: "dictionary", + Array: "array", + String: "string", + Integer: "integer", + Real: "real", + Boolean: "boolean", + Data: "data", + Date: "date", +} + +type plistValue struct { + kind plistKind + value interface{} +} + +type signedInt struct { + value uint64 + signed bool +} + +type sizedFloat struct { + value float64 + bits int +} + +type dictionary struct { + count int + m map[string]*plistValue + keys sort.StringSlice + values []*plistValue +} + +func (d *dictionary) Len() int { + return len(d.m) +} + +func (d *dictionary) Less(i, j int) bool { + return d.keys.Less(i, j) +} + +func (d *dictionary) Swap(i, j int) { + d.keys.Swap(i, j) + d.values[i], d.values[j] = d.values[j], d.values[i] +} + +func (d *dictionary) populateArrays() { + d.keys = make([]string, len(d.m)) + d.values = make([]*plistValue, len(d.m)) + i := 0 + for k, v := range d.m { + d.keys[i] = k + d.values[i] = v + i++ + } + sort.Sort(d) +} diff --git a/tags.go b/tags.go new file mode 100644 index 0000000..ceeb2d4 --- /dev/null +++ b/tags.go @@ -0,0 +1,304 @@ +package plist + +import ( + "reflect" + "sort" + "strings" + "sync" + "unicode" +) + +// tagOptions is the string following a comma in a struct field's "json" +// tag, or the empty string. It does not include the leading comma. +type tagOptions string + +// parseTag splits a struct field's json tag into its name and +// comma-separated options. +func parseTag(tag string) (string, tagOptions) { + if idx := strings.Index(tag, ","); idx != -1 { + return tag[:idx], tagOptions(tag[idx+1:]) + } + return tag, tagOptions("") +} + +// Contains reports whether a comma-separated list of options +// contains a particular substr flag. substr must be surrounded by a +// string boundary or commas. +func (o tagOptions) Contains(optionName string) bool { + if len(o) == 0 { + return false + } + s := string(o) + for s != "" { + var next string + i := strings.Index(s, ",") + if i >= 0 { + s, next = s[:i], s[i+1:] + } + if s == optionName { + return true + } + s = next + } + return false +} + +type field struct { + name string + tag bool + index []int + typ reflect.Type + omitEmpty bool +} + +func (f field) value(v reflect.Value) reflect.Value { + for _, i := range f.index { + if v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + v = v.Field(i) + } + return v +} + +type byName []field + +func (x byName) Len() int { return len(x) } + +func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x byName) Less(i, j int) bool { + if x[i].name != x[j].name { + return x[i].name < x[j].name + } + if len(x[i].index) != len(x[j].index) { + return len(x[i].index) < len(x[j].index) + } + if x[i].tag != x[j].tag { + return x[i].tag + } + return byIndex(x).Less(i, j) +} + +type byIndex []field + +func (x byIndex) Len() int { return len(x) } + +func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x byIndex) Less(i, j int) bool { + for k, xik := range x[i].index { + if k >= len(x[j].index) { + return false + } + if xik != x[j].index[k] { + return xik < x[j].index[k] + } + } + return len(x[i].index) < len(x[j].index) +} + +// typeFields returns a list of fields that plist should recognize for the given +// type. The algorithm is breadth-first search over the set of structs to +// include - the top struct and then any reachable anonymous structs. +func typeFields(t reflect.Type) []field { + // Anonymous fields to explore at the current level and the next. + current := []field{} + next := []field{{typ: t}} + + // Count of queued names for current level and the next. + count := map[reflect.Type]int{} + nextCount := map[reflect.Type]int{} + + // Types already visited at an earlier level. + visited := map[reflect.Type]bool{} + + // Fields found. + var fields []field + + for len(next) > 0 { + current, next = next, current[:0] + count, nextCount = nextCount, map[reflect.Type]int{} + + for _, f := range current { + if visited[f.typ] { + continue + } + visited[f.typ] = true + + // Scan f.typ for fields to include. + for i := 0; i < f.typ.NumField(); i++ { + sf := f.typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { // unexported + continue + } + tag := sf.Tag.Get("plist") + if tag == "-" { + continue + } + name, opts := parseTag(tag) + if !isValidTag(name) { + name = "" + } + index := make([]int, len(f.index)+1) + copy(index, f.index) + index[len(f.index)] = i + + ft := sf.Type + if ft.Name() == "" && ft.Kind() == reflect.Ptr { + // Follow pointer. + ft = ft.Elem() + } + + // Record found field and index sequence. + if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { + tagged := name != "" + if name == "" { + name = sf.Name + } + fields = append(fields, field{ + name: name, + tag: tagged, + index: index, + typ: ft, + omitEmpty: opts.Contains("omitempty"), + }) + if count[f.typ] > 1 { + // If there were multiple instances, add a second, + // so that the annihilation code will see a duplicate. + // It only cares about the distinction between 1 or 2, + // so don't bother generating any more copies. + fields = append(fields, fields[len(fields)-1]) + } + continue + } + + // Record new anonymous struct to explore in next round. + nextCount[ft]++ + if nextCount[ft] == 1 { + f := field{name: ft.Name(), index: index, typ: ft} + next = append(next, f) + } + } + } + } + + sort.Sort(byName(fields)) + + // Delete all fields that are hidden by the Go rules for embedded fields, + // except that fields with plist tags are promoted. + + // The fields are sorted in primary order of name, secondary order + // of field index length. Loop over names; for each name, delete + // hidden fields by choosing the one dominant field that survives. + out := fields[:0] + for advance, i := 0, 0; i < len(fields); i += advance { + // One iteration per name. + // Find the sequence of fields with the name of this first field. + fi := fields[i] + name := fi.name + for advance = 1; i+advance < len(fields); advance++ { + fj := fields[i+advance] + if fj.name != name { + break + } + } + if advance == 1 { // Only one field with this name + out = append(out, fi) + continue + } + dominant, ok := dominantField(fields[i : i+advance]) + if ok { + out = append(out, dominant) + } + } + + fields = out + sort.Sort(byIndex(fields)) + + return fields +} + +func dominantField(fields []field) (field, bool) { + // The fields are sorted in increasing index-length order. The winner + // must therefore be one with the shortest index length. Drop all + // longer entries, which is easy: just truncate the slice. + length := len(fields[0].index) + tagged := -1 // Index of first tagged field. + for i, f := range fields { + if len(f.index) > length { + fields = fields[:i] + break + } + if f.tag { + if tagged >= 0 { + // Multiple tagged fields at the same level: conflict. + // Return no field. + return field{}, false + } + tagged = i + } + } + if tagged >= 0 { + return fields[tagged], true + } + // All remaining fields have the same length. If there's more than one, + // we have a conflict (two fields named "X" at the same level) and we + // return no field. + if len(fields) > 1 { + return field{}, false + } + return fields[0], true +} + +var fieldCache struct { + sync.RWMutex + m map[reflect.Type][]field +} + +// cachedTypeFields is like typeFields but uses a cache to avoid repeated work. +func cachedTypeFields(t reflect.Type) []field { + fieldCache.RLock() + f := fieldCache.m[t] + fieldCache.RUnlock() + if f != nil { + return f + } + + // Compute fields without lock. + // Might duplicate effort but won't hold other computations back. + f = typeFields(t) + if f == nil { + f = []field{} + } + + fieldCache.Lock() + if fieldCache.m == nil { + fieldCache.m = map[reflect.Type][]field{} + } + fieldCache.m[t] = f + fieldCache.Unlock() + return f +} + +func isValidTag(s string) bool { + if s == "" { + return false + } + for _, c := range s { + switch { + case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c): + // Backslash and quote chars are reserved, but + // otherwise any punctuation chars are allowed + // in a tag name. + default: + if !unicode.IsLetter(c) && !unicode.IsDigit(c) { + return false + } + } + } + return true +} diff --git a/testdata/crashers/0d16bb1b5a9de90807d6e23316222a70267f48f0 b/testdata/crashers/0d16bb1b5a9de90807d6e23316222a70267f48f0 new file mode 100644 index 0000000000000000000000000000000000000000..6260b94215baaf0d5552bf71593330a1b4a2aa51 GIT binary patch literal 310 zcmYc)$jK}&F(^rmD9tO*OwUVA2`x%Z%qb2pE-A{)OD|r=!O6wV!z;ibpe6ht0x`_PJUi`YLNoOtqK{5Wk4q@WWHFR7)G_okOlFwFu#{mP!zP9u49A%mp^lVfg}D|? HGqM5z0bpfC literal 0 HcmV?d00001 diff --git a/testdata/crashers/137f12b963e1abb2aedf1e78dab4217d365e61cf b/testdata/crashers/137f12b963e1abb2aedf1e78dab4217d365e61cf new file mode 100644 index 0000000000000000000000000000000000000000..8326a742d989cd3dd7f544b7f864f015c4f9a9d7 GIT binary patch literal 3283 zcmd5;OOxAJ6?V&ZlW{}ho-kw}8OCIiNivPwvL)M6NC3V3mTg(`Gsze|u4LJ=BwO-B zT}2f;_yJSJ0tzaDs(}qv3{X_DV-_rD!3M%+*s){7V?_bi-R(Sz9!OCvP?a9{+)Vb;UaNjU~ z@pNHhZpgNIB%esHDft|h&S**!D0()D6Iw1wsv1b5N=8xD3?O8nL5-X}cPwiPZ6um= zM>j3d5d+T$(+AKK-82^SV}iHyzDez0h(}W7ttgNAu$Z*Zm0}tbuRA+i9yRHQeJ)kb;rXL)7 zDsX@Edg60e-?(=D%^Npwy>;;UUKIq1O$z(WNVY6s8=wnCo_PfNo2j-Z8$jA5Uu?H( zU2&m|f$EDbk?KuNb?!{H>HfJ;JUWcO$%3V+f&?$yaLaKLPSWiM2?oqJX;)<*4C7Zu zzVCXsQz;EB54TDVO((T+?@==S@bFOz6&gK$qn~H_hnYth-eT&*_|>f_@kH~@>x?sX z7e1J8e7t^Z7{9qWxMBLEh4NJU6Eol#BFB*!#xHDP0ldT}cu!1z7>|>;zi{X77r*pn zie^}@P%M?JHNM(7-xOM+)b4b95N;xvFTtFVU0E?L(>H;4Pc#ji2i_)VXQ6(;jDR{R z+c1<6>&Jt}!taiN-2zi*c?{xH2Cz?Qv8B@~yS6CZWNrI>g4j#jusjp*%}oEq(_^b= z(E4oJ-KSI)o}^_OBj0OFj%%vV@&923Cgdr|AP^2+Fi$Q#_)s0&hA@GY_FfvsZ@frG z;QnF!jgNu`oK2~l*qJs|c=?3rUSb8)obX2wbS$5MhG9A$Jm=CdeoX_ijH6^GiDh&m ziDOtcNfHDcW{F%zhP5M&VQ|@tFzi1$cyj8eFxr3pAH(>aXB;x7{vo%9V6|oIP{wco zt&>SjCKSl&d?u-&86rt&1oW6hF#xbrI{$WX@BV|ghhLEu^}GgjWAt3!AD)rZe}kFr z)c>qHt3cC<3_&C_D1g;j#X*w9aWqNfH3G;vGOx-o!mqU)$4{tFD?vhc=7}w%&n{SV zxTUclc_+EKXs%eUCm60Jl?!EtlDK1Oj4zj2{)l0y(ZZlsWtuYJEm3van9N2Kvq-K` zni9MM#Zq}*@K%h_XFGyWWfNl&BtU)H^ig zNgS812Ltq7d(8g9HCGb=q6{Y@!tjHO^ zmLHq$BpWluCSOcrrpjgq)rN8~c3e&Jj*4lmtyOK2pUu&}=Ua5=$VsiNqD$(Iv6?mx z2%%1xYQc@IYpR`FPgrz05c6D+T}kU2Ff5)VXu~GmR3IK56{?#Vz$>qlP$}FC#ddnrlw*d_TZ6qQ+$=coBF#lNG%p zsI#+H)g1eHEGW2>oJkaG?OHzvG@^_5ivh3s1h*6z=qy>o43e_Td3(|Cn9zMA)6C$D za$Tz0RR&+hY+765J=*G3P&`*C)bndyP?vhOc>J?PvILdY3z!lQ!9akh-7G%YHom3YQcp%!O;@LqEJflGB){2CFtttU$KGve^vKWThs=T=YOGjz(1t z%}?`Kxr?GkH{Tr@q>9c47)8nR0&V0QhESshO{Z7T?Vvs`bUN7vrKqK1J7zWg8c!Ug zsY10@Yj!7rr4lQ0fZ1}*Gc|85blY@W&eP^Efb++b4W`OJM3PvpIb-$6qv%IIXVwY9W)x0c@Oz1@^j=D z$cN!r_)7SCxDHez@K3_;hd&7aF8t^4-y;_zw<5X7ok%%Sjku9nPe& z7YhlCD8mGm9XJ(#evod}C{ZD|G&w^dCqFMewMYTtE`^N5GNAJma`TH)6-qJ^^ArqC6p}L%ixQJd zQj3ZiI2c43q!{EGR2Xy_j2WyMoETgf+!*{BA{Y`GvKUGk>KOVNCNs=oSjxb}2z8w# K%#{$Dkre(Vqn<8aGZ$|>Jmwqiy$;3 FD*({iWi9{! literal 0 HcmV?d00001 diff --git a/testdata/crashers/5feced23aa2767c77c8d2bb5c35321f926b4537a b/testdata/crashers/5feced23aa2767c77c8d2bb5c35321f926b4537a new file mode 100644 index 0000000000000000000000000000000000000000..ab0521ac72cff6d912876755da904061f33ec51c GIT binary patch literal 3283 zcmd5;&9mE96}M#@5(g5ep|sGHnlx#WKKv|OvMrSa(3ihuTb5)$J4uZ`u4LJ=BwO-_ z-V8JB=pWD>Uz5NLuwl`{FvE^6Sa!h%(oI;g>;~8|!1a62`50ay!>~Xz`nc!bd(OG% z{C?+1^=!)zQ1oFGiNzD2`t+Hz$#b9i?D-4UWO}Eggtn{bvRq?FsTWR0tmS*Q61J2X zxO*ZJHP-{X)l%qzw@)T|VBuS?vl}w5696X|r7oV#*K|0v3=1rt(hq!XVR-@EH%eVP zS=e5fienur$I`26sfgwBx|#v1QOMwgUd)i14l<~kS2ZmU2nFa+Bj+q!+nzxi>CVD6 zEF1L1&=0`u9`wYp%;n-pVJiBg6ft;s@r6sDd-3xxz5InQzH<51^Is|yOS>ce{{;XC zM!sO^XeM7o(F{%+r3{Iw@JqvUga)*NqN9??aGjF_k5Vsb?rdN2JR9u$KwB=XAl&yg z;Qju!^p~%^e)ZZL*Kgc>bMGq$4G^ZcDI73k#kPTCf<6@a)*%>dr(|C-fwWD&D0iEE zaj8y#7KmMuI+$77!ky`}i)SLK#3=O!3s#l}(!AopEk{W>PInNd8L-%Gy?`wz0H&_3X|`gxYWpMQYiU8XfkUD=7!k2T-8#<(+Y z8Gyys$7?r7sTcS2dz)x?3_t@m4REm7-tG8}{?d#v5 zXols=m1?cgyeT`+T3M<7070Q-a%J35`PYlqTp)^^{=h<#}nmZ##qof#Z^dSvw! zTAxn4yOgHElXNX}9Qd;2dY1MK|L@mfLY{yO0%6|+i_Fr856yKP2op$Y|LiDr{dqD1 z_m5Jqe-bp{Y*yRG&T^o_SB{D9r`Ise>0k^&$MR`t7^dUiGcJu%S9PE$I7;R-Sl%Eq zIEEE6BtgJofhgt`SUYkU2A4e#!~TToW0bn}ltbn$*ylD7td3&uD;N%- zbuyzXgbF!b%4bwGPh<$4fF6@51^{+K=RXYZ+`aeq=$neFo!5b3j-Sc<{Zn%KZ!oi) z`kz&24d@1uCx}cQ1+Y46ILMGVj%J9GP5`AymNW%M_|>lK25Id{B}g0YBE4hu=>=;6 zx3n+D-_2|*mUpE&93c=M;flVp0)P~ zp+#6)*-LJknp50NS#&iNOI%o3OPeMzZJs1((;>ZVC>|b`8@`Gu^~r)6NOi$n8Cgf4 zRtsp|s~#lBZE?O>N{W=zm;HkxFsj2v=mdLMZ#`6-o;AfwgAmh+CYPV$72qFOtms!j zi=B5H)+E4_VcDA&Euzwtn}Z_Ii9SB4guEUQ+)7}evt$!9Ny@2}oaLZrLHEskCyy^{ zEvex&7<`>{Xnlnr(Dp$c#f#-~tF$o$ZAGXnRoPU5=+a)V)Kh754+DE4hxe^+Jy)qO zR+P!p6jhldg$YgZ6ut$M&aOE+yB0WzNQP#q&K5}3E?j6394$dC3e_x6qsMDap-Qt zWE{<9D-%KzCncdak-EBMOBHnKI$n8+3xk0n)0_ExTyAvDrBO12UO(CHgK@!F7#5?9 z{aFEs%mB=*h2emh+It1LR4m$!1Ao>H$1Q;vu4SHBgF>5S3wfX`YEwvh=$={`j~hB# znw79xA4Sc6sXsPJ4V@1$ic%J3+AOtAp-Bxp?m^jb!q%kR>lNCRs#Pm;((VLJp4iJ# zHbFO%e1VNXluPz#%4Jnhs%}J4>p*%3|rGGnaqO5G&{(3 z3gs1PP3&H~67FG~AXN>NS)!&k-*96q#0|NQ5zAI9XlL;P?;EVxXH~R2G?JvwdnReq zA$9w9@~FmeE%smQ&M39_Jbi6j@b)!jl*-f`KY+hj{!zo)R<<-lePTWAz+Uql>R}6( zVEa36ed(DJrWG5umvrbZ7f>1mmSg&le`_LVBCkZQM{Y%G5jmnq{K$7A-;2B#c^LUP z^4DlAnuwl_UXI>~-j3Fzy(owV(RZTXkG>!MDEcV+CnSPgKwd_!Aa5c#LL#>j4yhqs zL`M3^w~-$r50D=tKS6$myodY(`4#eO6iS-;Mt!{z3eY@sATPCT=CvgqE-qZsO;OUnYK=_}x#> UeR%HANHlVM*(dLl*SWv_6M0EC*#H0l literal 0 HcmV?d00001 diff --git a/testdata/crashers/76632470f8fdd06ea7f5eaa53376aa3ffdbe00b0 b/testdata/crashers/76632470f8fdd06ea7f5eaa53376aa3ffdbe00b0 new file mode 100644 index 0000000000000000000000000000000000000000..3c51c803b68dbe48bdfa99e57b1fbc862d5d8ac1 GIT binary patch literal 3283 zcmb_e%dgvL6~A$kOPdRAI%ViUJ51=b({?VoabhR2OAGjwx05(_o^2=i@!4_W*iP&` zd?h57^9LZ!0s(448rUGgfRNZR3zoBB12dblf*l(WD+GMG_ofeV2g(RbethRU-+BJd zIaXZD^n4WkFoc97(cMp-IUC#i^k>eUH^-BEO*ya}S!=h;>@fb)$%-{S*OG&}JO%ep zNTTYvV9PB9E_mx?qXXui>Db#Px)wO_fV!BQesJKa z!2R88i7#D!{o3_6Zrr@}=Khy^RS+aLN$WEs*|LCbfG#9?<{{{Bw%VR-0BMtMvE8b5 z#f35ksxP)gsy8*&xii(K7tZX&qr>N6GZVgGVV;X!Q7ve4gbWW*%X9i>VLeSGT0Z6UjHPGtSgq z_+Y-V@%pV{{N~1S!}LcB<*D>1YQPahjv_IPpWng)c!^E$o~V2rkCV5*a_8<>zxH*C zW>~IJES0M@zS=m~6k4Lx?sR()ZX%d3!JLs@SurisH-UFgG!2^v-X>_nQ<6SlMnIjE zZ5T?3^`k{&;de*CZh@(@JOc3vQrM?uu`QsJdfK9NleO*lF=8)m!}3hLH#_}fOOLdk zQpl&5-F-?`p(icV82MgXa$Hk=uHqk7U_zdP3gAdiQZ3q)s(%#u&{Kku9 z1fCzpU;j90z}=L(iJfUfftQa9-Ak-sniKvAf{x`AP%%u$gXc^d#;<8WmT{EKB(aQ6 zBykMOCP{*T%Pf)0$gp>$F$}W3NQM131CPCa3Zo0J{c{+<^Nc~p)IZ?X5UjRr9mp6C zpms8;$%F!HI-f}@Xog4<8UZaPQ49d=q?~_0xOe}-Tf=Y2ih521x-oh_@2BMSKVW9N zwe82J;Pij%&MMG!B0~_#3<_X(R&kIdaU4w&d5r*aj?AkvjPR>1$MF;D(@v1koq1wg z(Wf`8IXu$15czg;bJ1L}Tu(4uODY%23?*?#)EHkbv-}~$P@{!Gt;#fIz+0l~v@w~D zCT5Xbp)@6U1&XEeyx^@Eq0e>%p~$T&o%SKuit!Xtq|$AU8S!PQYmL<@S#NRZx+qaS z+NgJE%9A)QT@liirqpiL%Y{1I>QIJ&a;wrvt;cvtUGprLf%lp}Y@=OBzZok))N+84#YecWLMI<1`LZQ3EHqpHx-D7hlQ%AU~*+VXZlh_ zFqV4C)+VJaT5(Ig*r*}S<_k%d(%Pci%K^PKm*W z$TTzfqFk4%c9p?bF`L$wc#pPv6%@}E3ibS27t|%8EERcO0-{5^oqR{3jeQL42OQot zTa|RNGG9^#Pg7KJ923Sg#Z&kOOd7l5=+sK!AR-x>rJ5Tcm0FNdCpcPySQJVro<@&( z4WUcZQaxr+Qe9up*0zN;mNUJd$O;&0`%tiN= z;%HRW(EK!ymAfcvbo1SjL8|C%fKik@FVIH5VF)#9&~$nQ-45#GLZ_2$P>NbAwqsV) zukplwnkrOlwPtq`SSqn12be9_JX7=5Lbpw~1MXDB+aqaX%vHfj1#1ijxr0>P-kmyWCpn2u3*HXUiTX*Jj;7JD|T50Z4LAo zsqwBsT693&y&F5~Fd{LtH>?}pwBeH3~e`XjP~oJU?kt|D(DI6@+K5e_LMEu@Wfk#8a2 zM;;+RMBYVyjQkAwIr2;7SIFaVEPN$=JzNSu2%F(=hTjf#iXA0mH_z8t+1RibLtiaODsM1K+eP4u_XKSlqxi|k(5 py}o;Ux4FC8eP{Qld*!{=-n)B0+WTPdFGy(T__9vkC$GJ~{|oKZGXekr literal 0 HcmV?d00001 diff --git a/testdata/crashers/a453429d65a952b8f54dc233d0ac304178a75b41 b/testdata/crashers/a453429d65a952b8f54dc233d0ac304178a75b41 new file mode 100644 index 0000000000000000000000000000000000000000..8c73a97287b70cf65a22612ca527630067aaec68 GIT binary patch literal 3283 zcmd5;OOxAJ6?V&ZlW{}ho-kw}GmOb3lVlpVWlOfDkN|r5E!(o>XOb~`T*)Vb;UaNjU~ z@pNHhZpgNIB%esHD)}6i&S**!D0()D6Iw1wsv1b5N=8xD3?O8nL5-X}cPwiPZ6um= zM>j3d5d+T$)BDg9-82^SV}*qh6&E>a8`u__6 z4)jb`*U)4phoVWG)bmLaQ{b11rwJ9PSy@9Rk>Q%B2Oh>Rsm}CJc3le`dO%&wO+PsF zRN(&ljl^fJym|H7Ti0*geEZBGsFk>fD)X)BSUycyt(liv>$l1qoiZ;g;hhoTS?i5)7Dc(yq!r7{*^4 z`M&GjN~JWgJlrZdG@aDOy@$#4gTsd@RA}`0jeefxA7mb4c#EkI<5#w##1qXot})Kk zUHD+W@$uTtVf@DC;D+gs7RpoYPt1U0h#W^^7{9QE1@ID^;5{+Uj<5#^|}cKR6?&{{}PL zssCAZR)MAy8G=Y=Pynm5ii0GH<7kq|YXp#UWL}kFgkNttj-ODUR)U1?%oAHipIxx# za7$x9^7Z8AqPb$Zo?y6^R4$YmO5%>CF}_@8`6Gs*Mhk;lm1)X=w?x%xV=@~}%p$o$ zX-e=46ielK!CNsxpX~@jky}+d?IW%g<0+y@rP~}c;>%Lk8mm*X-r~@8QKEXZQSZ={ zCvjZ5BBU!#sokoV3w5^Dp$q}#R;7_zkMWYa=2K{96W2AqVO`!uBa7UP^BCg4}lf%1K+JPNb|tNAz_56dpbeXJQ-OGNRH%9iCRfIDrY}_lW2vWX zZBojj6}QxjjT+)?zK~=otu4B}9MDUHd0_hoSZ6g*YOXoK^ZfwRh#Hrf;6>o|Ojh)Y zpw7-(Rdej)v7q2iawbu%wQKzx(18{i$W>I)9A@sL+H}9 zRF4^yRM(fYwQXUIvJpX(+g zkGyFXh)f^MO4&i5m{#o!%Vsk`la-nfbI}8(I2u(o zG(XK_c{Ydmp~ zrV7z{rLMom#6?h_l*m{LR41u^ zx|uC3Npoy<8pYrM;{>Usqs#&|)Y+OFnE|f1D;Tk;*ZoEc&+?wmid|MgTLV2tYP@TZ z79CJ`?!=C33|D9WweAe#2QSjsHU;lcm51?U+4g++i{%|RtW9N0Fw}?E!zS!CFQ6W_ zU&6M zUqs%I{4NrGC3-umMAfJjb)r9v{xtf_=&z!Gi2ijK+1=m0wtH*0xx3o^#_o^y%6qH5 YclW-(_rczuke$%UWu3lHUweQ1Ct=DrO#lD@ literal 0 HcmV?d00001 diff --git a/testdata/crashers/a7f6152b23463dbeb12cf9621b9c5962b8b71d01 b/testdata/crashers/a7f6152b23463dbeb12cf9621b9c5962b8b71d01 new file mode 100644 index 0000000000000000000000000000000000000000..64ae84cf916f10d87c00d86b251b5d2d0e7428d2 GIT binary patch literal 310 zcmYc)$jK}&F)+Bs$i&RT%Er#eFCZuyl9^Xh9Fme)k_aMFBTDm%Gt=`@Q$mYU6PXyo zi%W_!^U{l#ad2{R^Y98V2xtlahk$E9hL?>~VTi?7*q`^MiC-p7MdoUaq+@zt%1m6PE}r&d4t+NjC^Km~4>Exp)72Ke1Au zcmeB@jLc#MAWBroElti)$jQ%3Pc2e_xJe-+u?*-mh1~q2RE3g^#5@H<6NTiA#G=IH zlGLJN1`Y;M1}O%41{DTf24eTZY3)D3_e43Za5=6M4SZPNzx=kx<#pW9vl_YIPl zP8K%jmg?Gv>ap~iRxDuIoS~(GX6Dm4VHDD&Zh$nZspCwbjZ|ws zHEjoUq|gt*_48lJ7mC{>{r?33 z2WBpB8fZFKK+!Z#n#DAUY4A(OvxE-xylSAb#Bi;X0}qmy_330^^*jgc`#@jJ?I7Iu zb>RK(jnr4KzIpBXTQ_drdVB9{2Q?6;HYw~eL)CGBYk@8l`Su~`ZKf1owSc@yzNED4 zU1_0>fF4L~i8`3r`g}SuCKt~}l4l0Vw^*>Wb&wKN7j8L9!g0ETFvWoRChfW!fI;%L zVGwx!?M%i1%l)mAeS4bOxc4ZXeYpQ9gNn@qVWVGQg@?IE7~Wq zEsADXu2in@wYpGio^Oe5Nme@D0|++>%$H!!sGh3Xjvd&*zbDz2D*%5Jw9`=EXNEu@ z^DYb}#QM>oxd^&L;I_eJx;z5$2?N+?wAj+=gk4*dZnC!hK1S^2ZCIX*_hzPd?CFu! zQ)qoU?e0^$4o}jytzqCR^3=2S7x@3M3KQ}SWDp4Z9+;;WK78m?*M%^Fl=jaJk~dx^ zBXIv9`R1oV1I}jjP3&wJD!g(`bU(F%X-)-02s&0sLBlW|4_0&Oep*bQ=7zFf~L@@xc6FUFCfA9W-cLv{9HT}E+Ol$Z;-XETl(|?1R z?bQFQI_tnNi5x+sb0~n-S;s+|#BnrD6b%BX1+u8CFv735r_&&%KdS^Ob2?9L8GU-e zn!_#4i}Cl;n~Ubkd?UqhZJ96e3?*|%(uBbCtZ>LM)No-@E1sq-c*|6Sw#KvJ*e;VR zl%_<#M6py+5d9S+_SlXnmbq26qa1SWgg_BxDywkJP~hdRGtwtyqs^h~vP>P&R-;2x zzRYpis+g^|WToBUOAWT&p)3*QR+XXNNC>jN7FaF^@3nBKpj{{wM*NGi(dpJV#uNXo zb<5&xfkMj(Mw*o+Uc*?CgJ)GJ3g>BdMXlh18a0LS5Lr>^E7)qNg}xr3lH41URW%pX zizC|`=M%Qn63SW3*4cc&*3|Y!Q_qn7!*bS8jG8M6vpL%H1BdP$PBSZ~?2+cwT1}dJ zgxDZ#z2qgQxRB!=^NwFJx8D8jJ2h0hpEkJamISth4HCbyI`0%=cdi5 zLw}M764L{-O1|GC#?D?|DHaM&?ZBV3!(l@t`YS~sRv_PG*?bNds#X^h9=fNMhr^nI z7AHlF@1m&HEp~?%siU(#Mp5d#L|etCCDy5aYkE*J-LNq#bvpSbrRkNjl5koea5T!a1Bx-&@by-Z)*6uf<19VFAd>j&@`%Rg#Zo644As86kjE!b;bLOpE3 zGHidxtuHlE!<6d4_L2&xi#e1Af$duU)8CrN*~pd1jmVt{A5kJk#E*P0^25jnk;jog zMgAI%MbAXfMQ=v$Myt_I6hwpQyU`y-Ka747eG>g65BY VzWed+pOI+f_;OC(C$HVV{S#G?H1hxe literal 0 HcmV?d00001 diff --git a/testdata/crashers/b6d3ae7d57c52b1139cd4cb097382371be5508f4 b/testdata/crashers/b6d3ae7d57c52b1139cd4cb097382371be5508f4 new file mode 100644 index 0000000..82a843a --- /dev/null +++ b/testdata/crashers/b6d3ae7d57c52b1139cd4cb097382371be5508f4 @@ -0,0 +1 @@ +bplist00000000000000000000000000 \ No newline at end of file diff --git a/testdata/crashers/d2e984d7ef5d4fbcda46e217c28a7ad0077fb820 b/testdata/crashers/d2e984d7ef5d4fbcda46e217c28a7ad0077fb820 new file mode 100644 index 0000000000000000000000000000000000000000..cbabf6530718a9facbeab2070a866b3f0866483f GIT binary patch literal 32 lcmYc)$jK}&F)(LfU|=+`G%+zWv@|j?v9OdhH#GrrjRAks2KxX2 literal 0 HcmV?d00001 diff --git a/testdata/crashers/e322917c1e9ed2ac460865f9455ef8981f765522 b/testdata/crashers/e322917c1e9ed2ac460865f9455ef8981f765522 new file mode 100644 index 0000000000000000000000000000000000000000..07fcef8af6a0914ee4bbbbecbe26f50ffd991c14 GIT binary patch literal 3283 zcmd5;OOxAJ6?V&Zlei&qPZ%D#YwdQK9mZce9kHh8T5?dA zr{MmnNK_pcY*$Nx3*J7R=zzIrI`($RIJOULe;D6CoiA%}XzC`IKcgRb>fCgFxNjK0 zbh@xHH)Pv9l24@9lza|LXEY@V6g`{72`!f-RShIjC8MZn1`smPphnJ~JC-$tHWJOb zqnj4!h=J#W=>zDAZW@dEvBLPy_u@qV@%~GfKKb&eF2C~W&s=%+wF{rk=JMMk{r?33 z2YM!}YiKf)L(wEo>iHyzDez0h(}W7ttgNAu$Z*Zm0}tbuRcCrAyRHQeJ)kb;rXL)7 zDsX@Edg60e-?(=D%^Npwy>;;UUKIq1O$z(WNVY6s8=wnCo_PfNo2j-Z8$jA5Uu?H( zU2&m|f$EDbk?KuNb?!{H>HfJ;JUWcO$%3V+f&?$yaLaKLPSWiM2?oqJX;)<*4CAkk zeBbqMr&1bN9&VK!noert-lJst;o+kcDl~fhMnBK;4>ON2yv5Xq@vB==;)&*)*BNK( zE_^WG_;~%+Fn)7$aKrRR3+1WyCuYDgM2;gdj9=Ws0(gl{@Sd3bFdipwf8oyEFMjFE z6wR<)p;#(cYkakFp((UPsom-JAlyVSUxGO!yRu?hrf&l8o@g3254=s#&O-fw83A=t zwqYnC){h5`h2I?ky9K7s@)*RY3}BzqVoRq}c5P9*$=de&1hJR5VRaXB;x7{vo%9V6|oIP{wco zt&>SjCKSl&d?u-&86rt&1oW6hF#xbrI{$WX@BV|ghhLEu^@0X;WAt3!AD)rZe}kFr z)c>qHt3cC<3_&C_D1g;j#X*w9aWqNfH3G;vGOx-o!mqU)$4{tFD?vhc=7}w%&n{SV zxTUclc_+EKXs%eUCm60Jl?!EtlDK1Oj4zj2{)l0y(ZZlsWtuYJEm3van9N2Kvq-K` zni9MM#Zq}*@K%h_XFGyWWfNl&BtU)H^ig zNgS812Ltq7d(8g9HCGb=q6{Y@!tjHO^ zmLHq$BpWluCSOcrrpjgq)rN8~c3e&Jj*4lmtyOK2pUu&}=Ua5=$VsiNqD$(Iv6?mx z2%%1xYQc@IYpR`FPgrz05c6D+T}kU2Ff5)VXu~GmR3IK56{?#Vz$>qlP$}FC#ddnrlw*d_TZ6qQ+$=coBF#lNG%p zsI#+H)g1eHEGW2>oJkaG?OHzvG@^_5ivh3s1h*6z=qy>o43e_Td3(|Cn9zMA)6C$D za$Tz0RR&+hY+765J=*G3P&`*C)bndyP?vhOc>J?PvILdY3z!lQ!9akh-7G%YHom3YQcp%!O;@LqEJflGB){2CFtttU$KGve^vKWThs=T=YOGjz(1t z%}?`Kxr?GkH{Tr@q>9c47)8nR0&V0QhESshO{Z7T?Vvs`bUN7vrKqK1J7zWg8c!Ug zsY10@Yj!7rr4lQ0fZ1}*Gc|85blY@W&eP^Efb++b4W`OJM3PvpIb-$6qv%IIXVwY9W)cW>`DcUQaL-2L%hd2hA% Y?%ogfKG^#cvJ*PFtkd`DYwvIW1Sw!Om;e9( literal 0 HcmV?d00001 diff --git a/testdata/sample2.binary.plist b/testdata/sample2.binary.plist new file mode 100644 index 0000000000000000000000000000000000000000..2dbd6b4fcd3dc4d716414c424e5b7e4b5fa0ba7e GIT binary patch literal 1293 zcmeHFJ5N+W6rR1Jf{HG#qKJ<}C`=65ERoPiq7rEcY9XM72^r=dc1P!NnFr7bjR`Hp z7z-1ll9;GJ!GynHV=S$$Y_$;2aCiL=CQdQ$bI$iTi-nRtG#Xzk*@3QuhYol5^c<=6 zpE-Me-pi)Jd_GDO#>Zx9_Om$;JWco@n(}$hAy@vs4^EnRxACDL20_?AF%V{;1TRv}0eNIz2rxxv{gc^I~Ii;%;B%(~NbfK^8u0$gQ#t zybQoKh7l0fcoYFcbii|2NG}Kpppx|HF{c1P8NIeS1k?ouOCw}1^JoIZ0E#bCdcaVo zM$o)zIB+GGBd^0;05nolZqPDqR_Ms}2rNhG!B}D*bHFupLIxhBH4v5JTG%pk`Xs$f zmG)E0f<6Os0225)7UMk|c+3>l$_a>dnup_ZxZq*Wfn;=LUyM*BKkW z(KGb($KX>lyt_R;r62zK`1ZoZOEBkK)`e!{USqrQxWOCtPJf(Ps9p;#k|?PJI*Jx3 zYnn(r_S+9yd?k6Wlb@l*4UCR~XmQ7>Kb&9q-%0;=C$Vlez<^z5BW#q7v+Hb%-DdM_ zfpI37Vur1 + + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/empty-plist.plist b/testdata/xml/empty-plist.plist new file mode 100644 index 0000000..4eecfe1 --- /dev/null +++ b/testdata/xml/empty-plist.plist @@ -0,0 +1,4 @@ +?xml version="1.0" encoding="UTF-8"?> + + + \ No newline at end of file diff --git a/testdata/xml/empty-xml.plist b/testdata/xml/empty-xml.plist new file mode 100644 index 0000000..825a8e0 --- /dev/null +++ b/testdata/xml/empty-xml.plist @@ -0,0 +1,8 @@ + + + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/invalid-before-plist.plist b/testdata/xml/invalid-before-plist.plist new file mode 100644 index 0000000..e1ce521 --- /dev/null +++ b/testdata/xml/invalid-before-plist.plist @@ -0,0 +1,9 @@ + + +invalid + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/invalid-data.plist b/testdata/xml/invalid-data.plist new file mode 100644 index 0000000..fb48152 --- /dev/null +++ b/testdata/xml/invalid-data.plist @@ -0,0 +1,9 @@ + + + + + key + val +invalid + + \ No newline at end of file diff --git a/testdata/xml/invalid-end.plist b/testdata/xml/invalid-end.plist new file mode 100644 index 0000000..8d6dfc5 --- /dev/null +++ b/testdata/xml/invalid-end.plist @@ -0,0 +1,9 @@ + + + + + key + val + + +invalid \ No newline at end of file diff --git a/testdata/xml/invalid-middle.plist b/testdata/xml/invalid-middle.plist new file mode 100644 index 0000000..dd90c9f --- /dev/null +++ b/testdata/xml/invalid-middle.plist @@ -0,0 +1,9 @@ + +invalid + + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/invalid-start.plist b/testdata/xml/invalid-start.plist new file mode 100644 index 0000000..888b2b6 --- /dev/null +++ b/testdata/xml/invalid-start.plist @@ -0,0 +1,8 @@ +invalid + + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/malformed-xml.plist b/testdata/xml/malformed-xml.plist new file mode 100644 index 0000000..6ce2cff --- /dev/null +++ b/testdata/xml/malformed-xml.plist @@ -0,0 +1,8 @@ + + + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/no-both.plist b/testdata/xml/no-both.plist new file mode 100644 index 0000000..4ef91f1 --- /dev/null +++ b/testdata/xml/no-both.plist @@ -0,0 +1,6 @@ + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/no-dict-end.plist b/testdata/xml/no-dict-end.plist new file mode 100644 index 0000000..c57cbe5 --- /dev/null +++ b/testdata/xml/no-dict-end.plist @@ -0,0 +1,7 @@ + + + + + key + val + \ No newline at end of file diff --git a/testdata/xml/no-doctype.plist b/testdata/xml/no-doctype.plist new file mode 100644 index 0000000..5fb6615 --- /dev/null +++ b/testdata/xml/no-doctype.plist @@ -0,0 +1,7 @@ + + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/no-plist-end.plist b/testdata/xml/no-plist-end.plist new file mode 100644 index 0000000..a1347f6 --- /dev/null +++ b/testdata/xml/no-plist-end.plist @@ -0,0 +1,7 @@ + + + + + key + val + \ No newline at end of file diff --git a/testdata/xml/no-plist-version.plist b/testdata/xml/no-plist-version.plist new file mode 100644 index 0000000..f222c98 --- /dev/null +++ b/testdata/xml/no-plist-version.plist @@ -0,0 +1,8 @@ + + + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/no-xml-tag.plist b/testdata/xml/no-xml-tag.plist new file mode 100644 index 0000000..0890160 --- /dev/null +++ b/testdata/xml/no-xml-tag.plist @@ -0,0 +1,7 @@ + + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/swapped.plist b/testdata/xml/swapped.plist new file mode 100644 index 0000000..651902b --- /dev/null +++ b/testdata/xml/swapped.plist @@ -0,0 +1,8 @@ + + + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/unescaped-plist.plist b/testdata/xml/unescaped-plist.plist new file mode 100644 index 0000000..f462dbb --- /dev/null +++ b/testdata/xml/unescaped-plist.plist @@ -0,0 +1,8 @@ + + + + key + val + + \ No newline at end of file diff --git a/testdata/xml/unescaped-xml.plist b/testdata/xml/unescaped-xml.plist new file mode 100644 index 0000000..8f35a90 --- /dev/null +++ b/testdata/xml/unescaped-xml.plist @@ -0,0 +1,8 @@ + + + + key + val + + + + + + key + val + + \ No newline at end of file diff --git a/xml_parser.go b/xml_parser.go new file mode 100644 index 0000000..375d680 --- /dev/null +++ b/xml_parser.go @@ -0,0 +1,243 @@ +package plist + +import ( + "bytes" + "encoding/base64" + "encoding/xml" + "errors" + "fmt" + "io" + "strconv" + "strings" + "time" +) + +// xmlParser uses xml.Decoder to parse an xml plist into the corresponding plistValues +type xmlParser struct { + *xml.Decoder +} + +// newXMLParser returns a new xmlParser +func newXMLParser(r io.Reader) *xmlParser { + return &xmlParser{xml.NewDecoder(r)} +} + +func (p *xmlParser) parseDocument(start *xml.StartElement) (*plistValue, error) { + if start != nil { + return p.parseXMLElement(start) + } + + for { + tok, err := p.Token() + if err != nil { + return nil, err + } + switch el := tok.(type) { + case xml.StartElement: + return p.parseXMLElement(&el) + case xml.ProcInst, xml.Directive: + continue + case xml.CharData: + if len(bytes.TrimSpace(el)) != 0 { + return nil, errors.New("plist: unexpected non-empty xml.CharData") + } + default: + return nil, fmt.Errorf("unexpected element: %T", el) + } + } +} + +func (p *xmlParser) parseXMLElement(element *xml.StartElement) (*plistValue, error) { + switch element.Name.Local { + case "plist": + return p.parsePlist(element) + case "dict": + return p.parseDict(element) + case "string": + return p.parseString(element) + case "true", "false": + return p.parseBoolean(element) + case "array": + return p.parseArray(element) + case "real": + return p.parseReal(element) + case "integer": + return p.parseInteger(element) + case "data": + return p.parseData(element) + case "date": + return p.parseDate(element) + default: + return nil, fmt.Errorf("plist: Unknown plist element %s", element.Name.Local) + } +} + +func (p *xmlParser) parsePlist(element *xml.StartElement) (*plistValue, error) { + var val *plistValue + for { + token, err := p.Token() + if err != nil { + return nil, err + } + switch el := token.(type) { + case xml.EndElement: + if val == nil { + return nil, errors.New("plist: empty plist tag") + } + return val, nil + case xml.StartElement: + v, err := p.parseXMLElement(&el) + if err != nil { + return v, err + } + val = v + case xml.CharData: + if len(bytes.TrimSpace(el)) != 0 { + return nil, errors.New("plist: unexpected non-empty xml.CharData") + } + default: + return nil, fmt.Errorf("unexpected element: %T", el) + } + } +} + +func (p *xmlParser) parseDict(element *xml.StartElement) (*plistValue, error) { + var key *string + var subvalues = make(map[string]*plistValue) + for { + token, err := p.Token() + if err != nil { + return nil, err + } + switch el := token.(type) { + case xml.EndElement: + return &plistValue{Dictionary, &dictionary{m: subvalues}}, nil + case xml.StartElement: + if el.Name.Local == "key" { + var k string + if err := p.DecodeElement(&k, &el); err != nil { + return nil, err + } + key = &k + continue + } + if key == nil { + return nil, errors.New("plist: missing key in dict") + } + subvalues[*key], err = p.parseXMLElement(&el) + if err != nil { + return nil, err + } + key = nil + case xml.CharData: + if len(bytes.TrimSpace(el)) != 0 { + return nil, errors.New("plist: unexpected non-empty xml.CharData") + } + default: + return nil, fmt.Errorf("unexpected element: %T", el) + } + } +} + +func (p *xmlParser) parseString(element *xml.StartElement) (*plistValue, error) { + var value string + if err := p.DecodeElement(&value, element); err != nil { + return nil, err + } + return &plistValue{String, value}, nil +} + +func (p *xmlParser) parseBoolean(element *xml.StartElement) (*plistValue, error) { + if err := p.Skip(); err != nil { + return nil, err + } + plistBoolean := element.Name.Local == "true" + return &plistValue{Boolean, plistBoolean}, nil +} + +func (p *xmlParser) parseArray(element *xml.StartElement) (*plistValue, error) { + var subvalues []*plistValue + for { + token, err := p.Token() + if err != nil { + return nil, err + } + switch el := token.(type) { + case xml.EndElement: + return &plistValue{Array, subvalues}, nil + case xml.StartElement: + subv, err := p.parseXMLElement(&el) + if err != nil { + return nil, err + } + subvalues = append(subvalues, subv) + case xml.CharData: + if len(bytes.TrimSpace(el)) != 0 { + return nil, errors.New("plist: unexpected non-empty xml.CharData") + } + default: + return nil, fmt.Errorf("unexpected element: %T", el) + } + } +} + +func (p *xmlParser) parseReal(element *xml.StartElement) (*plistValue, error) { + var n float64 + if err := p.DecodeElement(&n, element); err != nil { + return nil, err + } + return &plistValue{Real, sizedFloat{n, 64}}, nil +} + +func (p *xmlParser) parseInteger(element *xml.StartElement) (*plistValue, error) { + // Based on testing with plutil -lint, the largest positive integer + // that you can store in an XML plist is 2^64 - 1 (in a uint64) + // and the largest negative integer you can store is -2^63 (in an int64) + // Since we need to know the sign before we can know what integer type + // to decode into, first decode into a string to check for "-". + var s string + if err := p.DecodeElement(&s, element); err != nil { + return nil, err + } + // Determine if this is a negative number by checking for minus sign. + s = strings.TrimSpace(s) + if strings.HasPrefix(s, "-") { + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return nil, err + } + return &plistValue{Integer, signedInt{uint64(i), true}}, nil + } + // Otherwise assume positive number and put into uint64. + u, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return nil, err + } + return &plistValue{Integer, signedInt{u, false}}, nil +} + +func (p *xmlParser) parseData(element *xml.StartElement) (*plistValue, error) { + replacer := strings.NewReplacer("\t", "", "\n", "", " ", "", "\r", "") + var data []byte + if err := p.DecodeElement(&data, element); err != nil { + return nil, err + } + if len(data) == 0 { + return &plistValue{Data, []byte(nil)}, nil + } + str := replacer.Replace(string(data)) + decoded, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return nil, err + } + data = []byte(decoded) + return &plistValue{Data, data}, nil +} + +func (p *xmlParser) parseDate(element *xml.StartElement) (*plistValue, error) { + var date time.Time + if err := p.DecodeElement(&date, element); err != nil { + return nil, err + } + return &plistValue{Date, date}, nil +} diff --git a/xml_writer.go b/xml_writer.go new file mode 100644 index 0000000..1058acd --- /dev/null +++ b/xml_writer.go @@ -0,0 +1,237 @@ +package plist + +import ( + "encoding/base64" + "encoding/xml" + "fmt" + "io" + "math" + "reflect" + "time" +) + +const xmlDOCTYPE = `` + +type xmlEncoder struct { + writer io.Writer + *xml.Encoder +} + +func newXMLEncoder(w io.Writer) *xmlEncoder { + return &xmlEncoder{w, xml.NewEncoder(w)} +} + +func (e *xmlEncoder) generateDocument(pval *plistValue) error { + // xml version=1.0 + _, err := e.writer.Write([]byte(xml.Header)) + if err != nil { + return err + } + + //!DOCTYPE plist + _, err = e.writer.Write([]byte(xmlDOCTYPE)) + if err != nil { + return err + } + + // newline after doctype + // tag starts on new line + _, err = e.writer.Write([]byte("\n")) + if err != nil { + return err + } + + tokenFunc := func(pval *plistValue) error { + if err := e.writePlistValue(pval); err != nil { + return err + } + return nil + } + err = e.writeElement("plist", pval, tokenFunc) + if err != nil { + return err + } + // newline at the end of a plist document + _, err = e.writer.Write([]byte("\n")) + if err != nil { + return err + } + return nil +} + +func (e *xmlEncoder) writePlistValue(pval *plistValue) error { + switch pval.kind { + case String: + return e.writeStringValue(pval) + case Boolean: + return e.writeBoolValue(pval) + case Integer: + return e.writeIntegerValue(pval) + case Dictionary: + return e.writeDictionaryValue(pval) + case Date: + return e.writeDateValue(pval) + case Array: + return e.writeArrayValue(pval) + case Real: + return e.writeRealValue(pval) + case Data: + return e.writeDataValue(pval) + default: + return &UnsupportedTypeError{reflect.ValueOf(pval.value).Type()} + } +} + +func (e *xmlEncoder) writeDataValue(pval *plistValue) error { + encodedValue := base64.StdEncoding.EncodeToString(pval.value.([]byte)) + return e.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: "data"}}) +} + +func (e *xmlEncoder) writeRealValue(pval *plistValue) error { + encodedValue := pval.value + switch { + case math.IsInf(pval.value.(sizedFloat).value, 1): + encodedValue = "inf" + case math.IsInf(pval.value.(sizedFloat).value, -1): + encodedValue = "-inf" + case math.IsNaN(pval.value.(sizedFloat).value): + encodedValue = "nan" + default: + encodedValue = pval.value.(sizedFloat).value + } + return e.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: "real"}}) +} + +// writeElement writes an xml element like , or +func (e *xmlEncoder) writeElement(name string, pval *plistValue, valFunc func(*plistValue) error) error { + startElement := xml.StartElement{ + Name: xml.Name{ + Space: "", + Local: name, + }} + + if name == "plist" { + startElement.Attr = []xml.Attr{{ + Name: xml.Name{ + Space: "", + Local: "version"}, + Value: "1.0"}, + } + } + + // Encode xml.StartElement token + if err := e.EncodeToken(startElement); err != nil { + return err + } + + // flush + if err := e.Flush(); err != nil { + return err + } + + // execute valFunc() + if err := valFunc(pval); err != nil { + return err + } + + // Encode xml.EndElement token + if err := e.EncodeToken(startElement.End()); err != nil { + return err + } + + // flush + return e.Flush() +} + +func (e *xmlEncoder) writeArrayValue(pval *plistValue) error { + tokenFunc := func(pval *plistValue) error { + encodedValue := pval.value + values := encodedValue.([]*plistValue) + for _, v := range values { + if err := e.writePlistValue(v); err != nil { + return err + } + } + return nil + } + return e.writeElement("array", pval, tokenFunc) + +} + +func (e *xmlEncoder) writeDictionaryValue(pval *plistValue) error { + tokenFunc := func(pval *plistValue) error { + encodedValue := pval.value + dict := encodedValue.(*dictionary) + dict.populateArrays() + for i, k := range dict.keys { + if err := e.EncodeElement(k, xml.StartElement{Name: xml.Name{Local: "key"}}); err != nil { + return err + } + if err := e.writePlistValue(dict.values[i]); err != nil { + return err + } + } + return nil + } + return e.writeElement("dict", pval, tokenFunc) +} + +// encode strings as CharData, which doesn't escape newline +// see https://github.com/golang/go/issues/9204 +func (e *xmlEncoder) writeStringValue(pval *plistValue) error { + startElement := xml.StartElement{Name: xml.Name{Local: "string"}} + // Encode xml.StartElement token + if err := e.EncodeToken(startElement); err != nil { + return err + } + + // flush + if err := e.Flush(); err != nil { + return err + } + + stringValue := pval.value.(string) + if err := e.EncodeToken(xml.CharData(stringValue)); err != nil { + return err + } + + // flush + if err := e.Flush(); err != nil { + return err + } + + // Encode xml.EndElement token + if err := e.EncodeToken(startElement.End()); err != nil { + return err + } + + // flush + return e.Flush() + +} + +func (e *xmlEncoder) writeBoolValue(pval *plistValue) error { + // EncodeElement results in instead of + // use writer to write self closing tags + b := pval.value.(bool) + _, err := e.writer.Write([]byte(fmt.Sprintf("<%t/>", b))) + if err != nil { + return err + } + return nil +} + +func (e *xmlEncoder) writeIntegerValue(pval *plistValue) error { + encodedValue := pval.value + if pval.value.(signedInt).signed { + encodedValue = int64(pval.value.(signedInt).value) + } else { + encodedValue = pval.value.(signedInt).value + } + return e.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: "integer"}}) +} + +func (e *xmlEncoder) writeDateValue(pval *plistValue) error { + encodedValue := pval.value.(time.Time).In(time.UTC).Format(time.RFC3339) + return e.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: "date"}}) +}