blob: 09ab71b1f4e647bcc63a91a2169a9bdfcb16fc6d [file] [log] [blame]
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}},
}
}