| package plist |
| |
| import ( |
| "encoding/binary" |
| "errors" |
| "fmt" |
| "io" |
| "time" |
| "unicode/utf16" |
| ) |
| |
| func bplistMinimumIntSize(n uint64) int { |
| switch { |
| case n <= uint64(0xff): |
| return 1 |
| case n <= uint64(0xffff): |
| return 2 |
| case n <= uint64(0xffffffff): |
| return 4 |
| default: |
| return 8 |
| } |
| } |
| |
| func bplistValueShouldUnique(pval cfValue) bool { |
| switch pval.(type) { |
| case cfString, *cfNumber, *cfReal, cfDate, cfData: |
| return true |
| } |
| return false |
| } |
| |
| type bplistGenerator struct { |
| writer *countedWriter |
| objmap map[interface{}]uint64 // maps pValue.hash()es to object locations |
| objtable []cfValue |
| trailer bplistTrailer |
| } |
| |
| func (p *bplistGenerator) flattenPlistValue(pval cfValue) { |
| key := pval.hash() |
| if bplistValueShouldUnique(pval) { |
| if _, ok := p.objmap[key]; ok { |
| return |
| } |
| } |
| |
| p.objmap[key] = uint64(len(p.objtable)) |
| p.objtable = append(p.objtable, pval) |
| |
| switch pval := pval.(type) { |
| case *cfDictionary: |
| pval.sort() |
| for _, k := range pval.keys { |
| p.flattenPlistValue(cfString(k)) |
| } |
| for _, v := range pval.values { |
| p.flattenPlistValue(v) |
| } |
| case *cfArray: |
| for _, v := range pval.values { |
| p.flattenPlistValue(v) |
| } |
| } |
| } |
| |
| func (p *bplistGenerator) indexForPlistValue(pval cfValue) (uint64, bool) { |
| v, ok := p.objmap[pval.hash()] |
| return v, ok |
| } |
| |
| func (p *bplistGenerator) generateDocument(root cfValue) { |
| p.objtable = make([]cfValue, 0, 16) |
| p.objmap = make(map[interface{}]uint64) |
| p.flattenPlistValue(root) |
| |
| p.trailer.NumObjects = uint64(len(p.objtable)) |
| p.trailer.ObjectRefSize = uint8(bplistMinimumIntSize(p.trailer.NumObjects)) |
| |
| p.writer.Write([]byte("bplist00")) |
| |
| offtable := make([]uint64, p.trailer.NumObjects) |
| for i, pval := range p.objtable { |
| offtable[i] = uint64(p.writer.BytesWritten()) |
| p.writePlistValue(pval) |
| } |
| |
| p.trailer.OffsetIntSize = uint8(bplistMinimumIntSize(uint64(p.writer.BytesWritten()))) |
| p.trailer.TopObject = p.objmap[root.hash()] |
| p.trailer.OffsetTableOffset = uint64(p.writer.BytesWritten()) |
| |
| for _, offset := range offtable { |
| p.writeSizedInt(offset, int(p.trailer.OffsetIntSize)) |
| } |
| |
| binary.Write(p.writer, binary.BigEndian, p.trailer) |
| } |
| |
| func (p *bplistGenerator) writePlistValue(pval cfValue) { |
| if pval == nil { |
| return |
| } |
| |
| switch pval := pval.(type) { |
| case *cfDictionary: |
| p.writeDictionaryTag(pval) |
| case *cfArray: |
| p.writeArrayTag(pval.values) |
| case cfString: |
| p.writeStringTag(string(pval)) |
| case *cfNumber: |
| p.writeIntTag(pval.signed, pval.value) |
| case *cfReal: |
| if pval.wide { |
| p.writeRealTag(pval.value, 64) |
| } else { |
| p.writeRealTag(pval.value, 32) |
| } |
| case cfBoolean: |
| p.writeBoolTag(bool(pval)) |
| case cfData: |
| p.writeDataTag([]byte(pval)) |
| case cfDate: |
| p.writeDateTag(time.Time(pval)) |
| case cfUID: |
| p.writeUIDTag(UID(pval)) |
| default: |
| panic(fmt.Errorf("unknown plist type %t", pval)) |
| } |
| } |
| |
| func (p *bplistGenerator) writeSizedInt(n uint64, nbytes int) { |
| var val interface{} |
| switch nbytes { |
| case 1: |
| val = uint8(n) |
| case 2: |
| val = uint16(n) |
| case 4: |
| val = uint32(n) |
| case 8: |
| val = n |
| default: |
| panic(errors.New("illegal integer size")) |
| } |
| binary.Write(p.writer, binary.BigEndian, val) |
| } |
| |
| func (p *bplistGenerator) writeBoolTag(v bool) { |
| tag := uint8(bpTagBoolFalse) |
| if v { |
| tag = bpTagBoolTrue |
| } |
| binary.Write(p.writer, binary.BigEndian, tag) |
| } |
| |
| func (p *bplistGenerator) writeIntTag(signed bool, n uint64) { |
| var tag uint8 |
| var val interface{} |
| switch { |
| case n <= uint64(0xff): |
| val = uint8(n) |
| tag = bpTagInteger | 0x0 |
| case n <= uint64(0xffff): |
| val = uint16(n) |
| tag = bpTagInteger | 0x1 |
| case n <= uint64(0xffffffff): |
| val = uint32(n) |
| tag = bpTagInteger | 0x2 |
| case n > uint64(0x7fffffffffffffff) && !signed: |
| // 64-bit values are always *signed* in format 00. |
| // Any unsigned value that doesn't intersect with the signed |
| // range must be sign-extended and stored as a SInt128 |
| val = n |
| tag = bpTagInteger | 0x4 |
| default: |
| val = n |
| tag = bpTagInteger | 0x3 |
| } |
| |
| binary.Write(p.writer, binary.BigEndian, tag) |
| if tag&0xF == 0x4 { |
| // SInt128; in the absence of true 128-bit integers in Go, |
| // we'll just fake the top half. We only got here because |
| // we had an unsigned 64-bit int that didn't fit, |
| // so sign extend it with zeroes. |
| binary.Write(p.writer, binary.BigEndian, uint64(0)) |
| } |
| binary.Write(p.writer, binary.BigEndian, val) |
| } |
| |
| func (p *bplistGenerator) writeUIDTag(u UID) { |
| nbytes := bplistMinimumIntSize(uint64(u)) |
| tag := uint8(bpTagUID | (nbytes - 1)) |
| |
| binary.Write(p.writer, binary.BigEndian, tag) |
| p.writeSizedInt(uint64(u), nbytes) |
| } |
| |
| func (p *bplistGenerator) writeRealTag(n float64, bits int) { |
| var tag uint8 = bpTagReal | 0x3 |
| var val interface{} = n |
| if bits == 32 { |
| val = float32(n) |
| tag = bpTagReal | 0x2 |
| } |
| |
| binary.Write(p.writer, binary.BigEndian, tag) |
| binary.Write(p.writer, binary.BigEndian, val) |
| } |
| |
| func (p *bplistGenerator) writeDateTag(t time.Time) { |
| tag := uint8(bpTagDate) | 0x3 |
| val := float64(t.In(time.UTC).UnixNano()) / float64(time.Second) |
| val -= 978307200 // Adjust to Apple Epoch |
| |
| binary.Write(p.writer, binary.BigEndian, tag) |
| binary.Write(p.writer, binary.BigEndian, val) |
| } |
| |
| func (p *bplistGenerator) writeCountedTag(tag uint8, count uint64) { |
| marker := tag |
| if count >= 0xF { |
| marker |= 0xF |
| } else { |
| marker |= uint8(count) |
| } |
| |
| binary.Write(p.writer, binary.BigEndian, marker) |
| |
| if count >= 0xF { |
| p.writeIntTag(false, count) |
| } |
| } |
| |
| func (p *bplistGenerator) writeDataTag(data []byte) { |
| p.writeCountedTag(bpTagData, uint64(len(data))) |
| binary.Write(p.writer, binary.BigEndian, data) |
| } |
| |
| func (p *bplistGenerator) writeStringTag(str string) { |
| for _, r := range str { |
| if r > 0x7F { |
| utf16Runes := utf16.Encode([]rune(str)) |
| p.writeCountedTag(bpTagUTF16String, uint64(len(utf16Runes))) |
| binary.Write(p.writer, binary.BigEndian, utf16Runes) |
| return |
| } |
| } |
| |
| p.writeCountedTag(bpTagASCIIString, uint64(len(str))) |
| binary.Write(p.writer, binary.BigEndian, []byte(str)) |
| } |
| |
| func (p *bplistGenerator) writeDictionaryTag(dict *cfDictionary) { |
| // assumption: sorted already; flattenPlistValue did this. |
| cnt := len(dict.keys) |
| p.writeCountedTag(bpTagDictionary, uint64(cnt)) |
| vals := make([]uint64, cnt*2) |
| for i, k := range dict.keys { |
| // invariant: keys have already been "uniqued" (as PStrings) |
| keyIdx, ok := p.objmap[cfString(k).hash()] |
| if !ok { |
| panic(errors.New("failed to find key " + k + " in object map during serialization")) |
| } |
| vals[i] = keyIdx |
| } |
| |
| for i, v := range dict.values { |
| // invariant: values have already been "uniqued" |
| objIdx, ok := p.indexForPlistValue(v) |
| if !ok { |
| panic(errors.New("failed to find value in object map during serialization")) |
| } |
| vals[i+cnt] = objIdx |
| } |
| |
| for _, v := range vals { |
| p.writeSizedInt(v, int(p.trailer.ObjectRefSize)) |
| } |
| } |
| |
| func (p *bplistGenerator) writeArrayTag(arr []cfValue) { |
| p.writeCountedTag(bpTagArray, uint64(len(arr))) |
| for _, v := range arr { |
| objIdx, ok := p.indexForPlistValue(v) |
| if !ok { |
| panic(errors.New("failed to find value in object map during serialization")) |
| } |
| |
| p.writeSizedInt(objIdx, int(p.trailer.ObjectRefSize)) |
| } |
| } |
| |
| func (p *bplistGenerator) Indent(i string) { |
| // There's nothing to indent. |
| } |
| |
| func newBplistGenerator(w io.Writer) *bplistGenerator { |
| return &bplistGenerator{ |
| writer: &countedWriter{Writer: mustWriter{w}}, |
| } |
| } |