Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Implement Unmarshal

Include a set of tests cribbed from encoding/json.
  • Loading branch information...
commit a97c4ad18b816fae7cf18daaf77201d11313a6e6 1 parent ccbecbd
Kevin Ballard authored
Showing with 433 additions and 30 deletions.
  1. +52 −16 convert.go
  2. +252 −3 marshal.go
  3. +19 −11 plist.go
  4. +110 −0 unmarshal_test.go
68 convert.go
View
@@ -95,7 +95,7 @@ func convertCFTypeToInterface(cfType cfTypeRef) (interface{}, error) {
dict, err := convertCFDictionaryToMap(C.CFDictionaryRef(cfType))
return dict, err
}
- return nil, &UnknownCFTypeError{int(typeId)}
+ return nil, &UnknownCFTypeError{typeId}
}
// ===== CFData =====
@@ -370,23 +370,42 @@ func convertSliceToCFArrayHelper(slice reflect.Value, helper func(reflect.Value)
}
func convertCFArrayToSlice(cfArray C.CFArrayRef) ([]interface{}, error) {
+ var result []interface{}
+ err := convertCFArrayToSliceHelper(cfArray, func(elem cfTypeRef, idx, count int) (bool, error) {
+ if result == nil {
+ result = make([]interface{}, count)
+ }
+ val, err := convertCFTypeToInterface(elem)
+ if err != nil {
+ return false, err
+ }
+ result[idx] = val
+ return true, nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
+func convertCFArrayToSliceHelper(cfArray C.CFArrayRef, helper func(elem cfTypeRef, idx, count int) (bool, error)) error {
count := C.CFArrayGetCount(cfArray)
if count == 0 {
- // short-circuit zero so we can assume cfTypes[0] is valid later
- return nil, nil
+ return nil
}
cfTypes := make([]cfTypeRef, int(count))
cfRange := C.CFRange{0, count}
C.CFArrayGetValues(cfArray, cfRange, (*unsafe.Pointer)(&cfTypes[0]))
- result := make([]interface{}, int(count))
for i, cfObj := range cfTypes {
- val, err := convertCFTypeToInterface(cfObj)
+ keepGoing, err := helper(cfObj, i, int(count))
if err != nil {
- return nil, err
+ return err
+ }
+ if !keepGoing {
+ break
}
- result[i] = val
}
- return result, nil
+ return nil
}
// ===== CFDictionary =====
@@ -447,26 +466,43 @@ func createCFDictionary(keys, values []cfTypeRef) C.CFDictionaryRef {
}
func convertCFDictionaryToMap(cfDict C.CFDictionaryRef) (map[string]interface{}, error) {
+ var m map[string]interface{}
+ convertCFDictionaryToMapHelper(cfDict, func(key string, value cfTypeRef, count int) error {
+ if m == nil {
+ m = make(map[string]interface{}, count)
+ }
+ val, err := convertCFTypeToInterface(value)
+ if err != nil {
+ return err
+ }
+ m[key] = val
+ return nil
+ })
+ if m == nil {
+ // must have been an empty dictionary
+ m = make(map[string]interface{}, 0)
+ }
+ return m, nil
+}
+
+func convertCFDictionaryToMapHelper(cfDict C.CFDictionaryRef, helper func(key string, value cfTypeRef, count int) error) error {
count := int(C.CFDictionaryGetCount(cfDict))
if count == 0 {
- return map[string]interface{}{}, nil
+ return nil
}
cfKeys := make([]cfTypeRef, count)
cfVals := make([]cfTypeRef, count)
C.CFDictionaryGetKeysAndValues(cfDict, (*unsafe.Pointer)(&cfKeys[0]), (*unsafe.Pointer)(&cfVals[0]))
- m := make(map[string]interface{}, count)
for i := 0; i < count; i++ {
cfKey := cfKeys[i]
typeId := C.CFGetTypeID(C.CFTypeRef(cfKey))
if typeId != C.CFStringGetTypeID() {
- return nil, &UnsupportedKeyTypeError{int(typeId)}
+ return &UnsupportedKeyTypeError{int(typeId)}
}
key := convertCFStringToString(C.CFStringRef(cfKey))
- val, err := convertCFTypeToInterface(cfVals[i])
- if err != nil {
- return nil, err
+ if err := helper(key, cfVals[i], count); err != nil {
+ return err
}
- m[key] = val
}
- return m, nil
+ return nil
}
255 marshal.go
View
@@ -94,6 +94,7 @@ func Marshal(obj interface{}, format int) ([]byte, error) {
var timeType = reflect.TypeOf(time.Time{})
var byteSliceType = reflect.TypeOf([]byte(nil))
+var stringType = reflect.TypeOf("")
func marshalValue(v reflect.Value) (cfTypeRef, error) {
if !v.IsValid() {
@@ -320,11 +321,259 @@ func isValidName(name string) bool {
// the unmarshalling as best it can. If no more serious errors are encountered,
// Unmarshal returns an UnmarshalTypeError describing the earliest such error.
func Unmarshal(data []byte, v interface{}) (format int, err error) {
- panic("Unimplemented")
+ cfObj, format, err := cfPropertyListCreateWithData(data)
+ if err != nil {
+ return 0, err
+ }
+ defer cfRelease(cfObj)
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
+ return format, &InvalidUnmarshalError{reflect.TypeOf(v)}
+ }
+ state := &unmarshalState{}
+ if err := state.unmarshalValue(cfObj, rv); err != nil {
+ return format, err
+ }
+ return format, state.err
+}
+
+type unmarshalState struct {
+ err error
+}
+
+var (
+ cfArrayTypeID = C.CFArrayGetTypeID()
+ cfBooleanTypeID = C.CFBooleanGetTypeID()
+ cfDataTypeID = C.CFDataGetTypeID()
+ cfDateTypeID = C.CFDateGetTypeID()
+ cfDictionaryTypeID = C.CFDictionaryGetTypeID()
+ cfNumberTypeID = C.CFNumberGetTypeID()
+ cfStringTypeID = C.CFStringGetTypeID()
+)
+
+var cfTypeMap = map[C.CFTypeID]reflect.Type{
+ cfArrayTypeID: reflect.TypeOf([]interface{}(nil)),
+ cfBooleanTypeID: reflect.TypeOf(false),
+ cfDataTypeID: reflect.TypeOf([]byte(nil)),
+ cfDateTypeID: reflect.TypeOf(time.Time{}),
+ cfDictionaryTypeID: reflect.TypeOf(map[string]interface{}(nil)),
+ cfNumberTypeID: reflect.TypeOf(int64(0)),
+ cfStringTypeID: reflect.TypeOf(""),
+}
+
+var cfTypeNames = map[C.CFTypeID]string{
+ cfArrayTypeID: "CFArray",
+ cfBooleanTypeID: "CFBoolean",
+ cfDataTypeID: "CFData",
+ cfDateTypeID: "CFDate",
+ cfDictionaryTypeID: "CFDictionary",
+ cfNumberTypeID: "CFNumber",
+ cfStringTypeID: "CFString",
+}
+
+func (state *unmarshalState) unmarshalValue(cfObj cfTypeRef, v reflect.Value) error {
+ vType := v.Type()
+ var unmarshaler Unmarshaler
+ if u, ok := v.Interface().(Unmarshaler); ok {
+ unmarshaler = u
+ } else if v.Kind() != reflect.Ptr && vType.Name() != "" && v.CanAddr() {
+ // matching the encoding/json behavior here
+ // If v is a named type and is addressable, check its address for Unmarshaler.
+ vA := v.Addr()
+ if u, ok := vA.Interface().(Unmarshaler); ok {
+ unmarshaler = u
+ }
+ }
+ if unmarshaler != nil {
+ // flip over to the dumb conversion routine so we have something to give UnmarshalPlist()
+ plist, err := convertCFTypeToInterface(cfObj)
+ if err != nil {
+ return err
+ }
+ if v.Kind() == reflect.Ptr && v.IsNil() {
+ v.Set(reflect.New(vType.Elem()))
+ unmarshaler = v.Interface().(Unmarshaler)
+ }
+ return unmarshaler.UnmarshalPlist(plist)
+ }
+ if v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ v.Set(reflect.New(vType.Elem()))
+ }
+ return state.unmarshalValue(cfObj, v.Elem())
+ }
+ typeID := C.CFGetTypeID(C.CFTypeRef(cfObj))
+ if v.Kind() == reflect.Interface {
+ if v.IsNil() {
+ // pick an appropriate type based on the cfobj
+ typ, ok := cfTypeMap[typeID]
+ if !ok {
+ return &UnknownCFTypeError{typeID}
+ }
+ if !typ.AssignableTo(vType) {
+ // v must be some interface that our object doesn't conform to
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID], vType})
+ return nil
+ }
+ v.Set(reflect.Zero(typ))
+ }
+ return state.unmarshalValue(cfObj, v.Elem())
+ }
+ switch typeID {
+ case cfArrayTypeID:
+ if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID], vType})
+ return nil
+ }
+ return convertCFArrayToSliceHelper(C.CFArrayRef(cfObj), func(elem cfTypeRef, idx, count int) (bool, error) {
+ if idx == 0 && v.Kind() == reflect.Slice {
+ v.Set(reflect.MakeSlice(vType, count, count))
+ } else if v.Kind() == reflect.Array && idx >= v.Len() {
+ return false, nil
+ }
+ if err := state.unmarshalValue(elem, v.Index(idx)); err != nil {
+ return false, err
+ }
+ return true, nil
+ })
+ case cfBooleanTypeID:
+ if v.Kind() != reflect.Bool {
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID], vType})
+ return nil
+ }
+ v.SetBool(C.CFBooleanGetValue(C.CFBooleanRef(cfObj)) != C.false)
+ return nil
+ case cfDataTypeID:
+ if !byteSliceType.AssignableTo(vType) {
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID], vType})
+ return nil
+ }
+ v.SetBytes(convertCFDataToBytes(C.CFDataRef(cfObj)))
+ return nil
+ case cfDateTypeID:
+ if !timeType.AssignableTo(vType) {
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID], vType})
+ return nil
+ }
+ v.Set(reflect.ValueOf(convertCFDateToTime(C.CFDateRef(cfObj))))
+ case cfDictionaryTypeID:
+ if v.Kind() == reflect.Map {
+ // it's a map. Check its key type first
+ if !stringType.AssignableTo(vType.Key()) {
+ state.recordError(&UnmarshalTypeError{cfTypeNames[cfStringTypeID], vType.Key()})
+ return nil
+ }
+ if v.IsNil() {
+ v.Set(reflect.MakeMap(vType))
+ }
+ return convertCFDictionaryToMapHelper(C.CFDictionaryRef(cfObj), func(key string, value cfTypeRef, count int) error {
+ keyVal := reflect.ValueOf(key)
+ val := v.MapIndex(keyVal)
+ if !val.IsValid() {
+ val = reflect.New(vType.Elem())
+ v.SetMapIndex(keyVal, val)
+ }
+ if err := state.unmarshalValue(value, val); err != nil {
+ return err
+ }
+ return nil
+ })
+ } else if v.Kind() == reflect.Struct {
+ return convertCFDictionaryToMapHelper(C.CFDictionaryRef(cfObj), func(key string, value cfTypeRef, count int) error {
+ // we need to iterate the fields because the tag might rename the key
+ var f reflect.StructField
+ var ok bool
+ for i := 0; i < vType.NumField(); i++ {
+ sf := vType.Field(i)
+ tag := sf.Tag.Get("plist")
+ if tag == "-" {
+ // Pretend this field doesn't exist
+ continue
+ }
+ if sf.Anonymous {
+ // Match encoding/json's behavior here and pretend it doesn't exist
+ continue
+ }
+ name, _ := parseTag(tag)
+ if name == key {
+ f = sf
+ ok = true
+ // This is unambiguously the right match
+ break
+ }
+ if sf.Name == key {
+ f = sf
+ ok = true
+ }
+ // encoding/json does a case-insensitive match. Lets do that too
+ if !ok && strings.EqualFold(sf.Name, key) {
+ f = sf
+ ok = true
+ }
+ }
+ if ok {
+ if f.PkgPath != "" {
+ // this is an unexported field
+ return &UnmarshalFieldError{key, vType, f}
+ }
+ vElem := v.FieldByIndex(f.Index)
+ if err := state.unmarshalValue(value, vElem); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ }
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID], vType})
+ return nil
+ case cfNumberTypeID:
+ switch v.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ i := convertCFNumberToInt64(C.CFNumberRef(cfObj))
+ if v.OverflowInt(i) {
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID] + " " + strconv.FormatInt(i, 10), vType})
+ return nil
+ }
+ v.SetInt(i)
+ return nil
+ case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ u := uint64(convertCFNumberToUInt32(C.CFNumberRef(cfObj)))
+ if v.OverflowUint(u) {
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID] + " " + strconv.FormatUint(u, 10), vType})
+ return nil
+ }
+ v.SetUint(u)
+ return nil
+ case reflect.Float32, reflect.Float64:
+ f := convertCFNumberToFloat64(C.CFNumberRef(cfObj))
+ if v.OverflowFloat(f) {
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID] + " " + strconv.FormatFloat(f, 'f', -1, 64), vType})
+ return nil
+ }
+ v.SetFloat(f)
+ return nil
+ }
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID], vType})
+ return nil
+ case cfStringTypeID:
+ if v.Kind() != reflect.String {
+ state.recordError(&UnmarshalTypeError{cfTypeNames[typeID], vType})
+ return nil
+ }
+ v.SetString(convertCFStringToString(C.CFStringRef(cfObj)))
+ return nil
+ }
+ return &UnknownCFTypeError{typeID}
+}
+
+func (state *unmarshalState) recordError(err error) {
+ if state.err == nil {
+ state.err = err
+ }
}
// Marshaler is the interface implemented by objects that can marshal themselves
-// into a property list.
+// into a property list.}
type Marshaler interface {
MarshalPlist() (interface{}, error)
}
@@ -339,7 +588,7 @@ type Unmarshaler interface {
// An UnmarshalTypeError describes a plist value that was not appropriate for a
// value of a specific Go type.
type UnmarshalTypeError struct {
- Value string // description of plist value - "CFBoolean", "CFArray", etc.
+ Value string // description of plist value - "CFBoolean, "CFArray", "CFNumber -5"
Type reflect.Type // type of Go value it could not be assigned to
}
30 plist.go
View
@@ -36,25 +36,33 @@ const (
// CFPropertyListCreateWithData decodes the given data into a property list object.
func CFPropertyListCreateWithData(data []byte) (plist interface{}, format int, err error) {
+ cfObj, format, err := cfPropertyListCreateWithData(data)
+ if err != nil {
+ return nil, 0, err
+ }
+ defer cfRelease(cfObj)
+ val, err := convertCFTypeToInterface(cfObj)
+ if err != nil {
+ return nil, 0, err
+ }
+ return val, format, nil
+}
+
+func cfPropertyListCreateWithData(data []byte) (cfObj cfTypeRef, format int, err error) {
cfData := convertBytesToCFData(data)
defer C.CFRelease(C.CFTypeRef(cfData))
var cfFormat C.CFPropertyListFormat
var cfError C.CFErrorRef
- cfObj := C.CFPropertyListCreateWithData(nil, cfData, 0, &cfFormat, &cfError)
- if cfObj == nil {
+ cfPlist := C.CFPropertyListCreateWithData(nil, cfData, 0, &cfFormat, &cfError)
+ if cfPlist == nil {
// an error occurred
if cfError != nil {
- defer C.CFRelease(C.CFTypeRef(cfError))
+ defer cfRelease(cfTypeRef(cfError))
return nil, 0, NewCFError(cfError)
}
return nil, 0, errors.New("plist: unknown error in CFPropertyListCreateWithData")
}
- defer C.CFRelease(C.CFTypeRef(cfObj))
- val, err := convertCFTypeToInterface(cfTypeRef(cfObj))
- if err != nil {
- return nil, 0, err
- }
- return val, int(cfFormat), nil
+ return cfTypeRef(cfPlist), int(cfFormat), nil
}
// CFPropertyListCreateData returns a []byte containing a serialized representation
@@ -101,11 +109,11 @@ func (e *UnsupportedValueError) Error() string {
}
type UnknownCFTypeError struct {
- CFTypeID int
+ CFTypeID C.CFTypeID
}
func (e *UnknownCFTypeError) Error() string {
- return "plist: unknown CFTypeID " + strconv.Itoa(e.CFTypeID)
+ return "plist: unknown CFTypeID " + strconv.Itoa(int(e.CFTypeID))
}
// UnsupportedKeyTypeError represents the case where a CFDictionary is being converted
110 unmarshal_test.go
View
@@ -0,0 +1,110 @@
+package plist
+
+import (
+ "encoding/json"
+ "reflect"
+ "testing"
+)
+
+// The tests here are based off of the ones in encoding/json
+
+type T struct {
+ X string
+ Y int
+ Z int `plist:"-"`
+}
+
+type tx struct {
+ x int
+}
+
+var txType = reflect.TypeOf((*tx)(nil)).Elem()
+
+// A type that can unmarshal itself.
+
+type unmarshaler struct {
+ T bool
+}
+
+func (u *unmarshaler) UnmarshalPlist(plist interface{}) error {
+ *u = unmarshaler{true} // All we need to see that UnmarshalPlist is called
+ return nil
+}
+
+type ustruct struct {
+ M unmarshaler
+}
+
+var (
+ um0, um1 unmarshaler // target2 of unmarshaling
+ ump = &um1
+ umtrue = unmarshaler{true}
+ umslice = []unmarshaler{{true}}
+ umslicep = new([]unmarshaler)
+ umstruct = ustruct{unmarshaler{true}}
+)
+
+type unmarshalTest struct {
+ in string
+ ptr interface{}
+ out interface{}
+ err error
+}
+
+var unmarshalTests = []unmarshalTest{
+ // basic types
+ {`true`, new(bool), true, nil},
+ {`1`, new(int), 1, nil},
+ {`1.2`, new(float64), 1.2, nil},
+ {`-5`, new(int16), int16(-5), nil},
+ {`"a\u1234"`, new(string), "a\u1234", nil},
+ {`"http:\/\/"`, new(string), "http://", nil},
+ {`"g-clef: \uD834\uDD1E"`, new(string), "g-clef: \U0001D11E", nil},
+ {`"invalid: \uD834x\uDD1E"`, new(string), "invalid: \uFFFDx\uFFFD", nil},
+ // skip the null one
+ {`{"X": [1,2,3], "Y": 4}`, new(T), T{Y: 4}, &UnmarshalTypeError{"CFArray", reflect.TypeOf("")}},
+ {`{"x": 1}`, new(tx), tx{}, &UnmarshalFieldError{"x", txType, txType.Field(0)}},
+
+ // Z has a "-" tag.
+ {`{"Y": 1, "Z": 2}`, new(T), T{Y: 1}, nil},
+
+ // array tests
+ {`[1, 2, 3]`, new([3]int), [3]int{1, 2, 3}, nil},
+ {`[1, 2, 3]`, new([1]int), [1]int{1}, nil},
+ {`[1, 2, 3]`, new([5]int), [5]int{1, 2, 3, 0, 0}, nil},
+
+ // unmarshal interface test
+ {`{"T":false}`, &um0, umtrue, nil}, // use "false" so test will fail if custom unmarshaler is not called
+ {`{"T":false}`, &ump, &umtrue, nil},
+ {`[{"T":false}]`, &umslice, umslice, nil},
+ {`[{"T":false}]`, &umslicep, &umslice, nil},
+ {`{"M":{"T":false}}`, &umstruct, umstruct, nil},
+}
+
+func TestUnmarshal(t *testing.T) {
+ for i, tt := range unmarshalTests {
+ var in interface{}
+ if err := json.Unmarshal([]byte(tt.in), &in); err != nil {
+ t.Errorf("#%d: %#v", i, err)
+ continue
+ }
+ indata, err := CFPropertyListCreateData(in, CFPropertyListXMLFormat_v1_0)
+ if err != nil {
+ t.Errorf("#%d: %#v", i, err)
+ continue
+ }
+ if tt.ptr == nil {
+ // why is this here? encoding/json's tests do this. But why?
+ continue
+ }
+ v := reflect.New(reflect.TypeOf(tt.ptr).Elem())
+ if _, err := Unmarshal(indata, v.Interface()); !reflect.DeepEqual(err, tt.err) {
+ t.Errorf("#%d: %v want %v", i, err, tt.err)
+ continue
+ }
+ if !reflect.DeepEqual(v.Elem().Interface(), tt.out) {
+ t.Errorf("#%d: mismatch\nhave: %#+v\nwant: %#+v", i, v.Elem().Interface(), tt.out)
+ continue
+ }
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.