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