Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 185 additions & 21 deletions consume.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package phpserialize

import (
"errors"
"fmt"
"reflect"
"sort"
"strconv"
)

Expand Down Expand Up @@ -182,44 +184,120 @@ func setField(structFieldValue reflect.Value, value interface{}) error {

switch structFieldValue.Type().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
structFieldValue.SetInt(val.Int())
if val.CanInt() {
structFieldValue.SetInt(val.Int())
} else {
intVal, err := strconv.ParseInt(fmt.Sprintf("%v", val.Interface()), 10, 64)
if err != nil {
return err
}
structFieldValue.SetInt(intVal)
}

case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
structFieldValue.SetUint(val.Uint())
if val.CanUint() {
structFieldValue.SetUint(val.Uint())
} else {
uintVal, err := strconv.ParseUint(fmt.Sprintf("%v", val.Interface()), 10, 64)
if err != nil {
return err
}
structFieldValue.SetUint(uintVal)
}

case reflect.Float32, reflect.Float64:
structFieldValue.SetFloat(val.Float())
if val.CanFloat() {
structFieldValue.SetFloat(val.Float())
} else {
floatVal, err := strconv.ParseFloat(fmt.Sprintf("%v", val.Interface()), 64)
if err != nil {
return err
}
structFieldValue.SetFloat(floatVal)
}

case reflect.Struct:
m := val.Interface().(map[interface{}]interface{})
fillStruct(structFieldValue, m)
return fillStruct(structFieldValue, m)

case reflect.Slice:
l := val.Len()
if l == 0 {
break
}

arrayOfObjects := reflect.MakeSlice(structFieldValue.Type(), l, l)

for i := 0; i < l; i++ {
if m, ok := val.Index(i).Interface().(map[interface{}]interface{}); ok {
obj := arrayOfObjects.Index(i)
fillStruct(obj, m)
if obj.Kind() == reflect.Ptr {
obj.Set(reflect.New(obj.Type().Elem()))
obj = obj.Elem()
}
if err := setField(obj, m); err != nil {
return err
}
} else {
switch arrayOfObjects.Index(i).Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
arrayOfObjects.Index(i).SetInt(val.Index(i).Elem().Int())
case reflect.Float32, reflect.Float64:
arrayOfObjects.Index(i).SetFloat(val.Index(i).Elem().Float())
default:
arrayOfObjects.Index(i).Set(val.Index(i).Elem())
if err := setField(arrayOfObjects.Index(i), val.Index(i).Interface()); err != nil {
return err
}

}
}

structFieldValue.Set(arrayOfObjects)

case reflect.Map:
l := val.Len()
if l == 0 {
break
}

mapType := structFieldValue.Type()

mapOfObjects := reflect.MakeMapWithSize(mapType, l)

// Go randomises maps. To be able to test this we need to make sure the
// map keys always come out in the same order. So we sort them first.
mapKeys := val.MapKeys()
sort.Slice(mapKeys, func(i, j int) bool {
return lessValue(mapKeys[i], mapKeys[j])
})

for _, k := range mapKeys {
kValue := reflect.New(mapType.Key()).Elem()
if err := setField(kValue, k.Interface()); err != nil {
return err
}

v := val.MapIndex(k)
vValue := reflect.New(mapType.Elem()).Elem()
if err := setField(vValue, v.Interface()); err != nil {
return err
}

mapOfObjects.SetMapIndex(kValue, vValue)
}

structFieldValue.Set(mapOfObjects)

case reflect.Ptr:
// Instantiate structFieldValue.
structFieldValue.Set(reflect.New(structFieldValue.Type().Elem()))
return setField(structFieldValue.Elem(), value)

case reflect.String:
var str string
if val.CanInterface() {
str = fmt.Sprintf("%v", val.Interface())
} else {
str = val.String()
}
structFieldValue.SetString(str)

case reflect.Bool:
structFieldValue.SetBool(val.Bool())

default:
structFieldValue.Set(val)
}
Expand All @@ -235,16 +313,36 @@ func fillStruct(obj reflect.Value, m map[interface{}]interface{}) error {
if !field.CanSet() {
continue
}
var key string
if tag := tt.Field(i).Tag.Get("php"); tag == "-" {

fieldType := tt.Field(i)

if fieldType.Anonymous {
// embedded struct
if err := setField(field, m); err != nil {
return err
}
continue
}
tag := fieldType.Tag.Get("php")
if v, ok := m[tag]; tag != "" && ok {
if err := setField(field, v); err != nil {
return err
}
continue
}
fieldName := fieldType.Name
lowerCaseFieldName := lowerCaseFirstLetter(fieldName)
if v, ok := m[lowerCaseFieldName]; ok {
if err := setField(field, v); err != nil {
return err
}
continue
} else if tag != "" {
key = tag
} else {
key = lowerCaseFirstLetter(tt.Field(i).Name)
}
if v, ok := m[key]; ok {
setField(field, v)
if v, ok := m[fieldName]; ok {
if err := setField(field, v); err != nil {
return err
}
continue
}
}

Expand Down Expand Up @@ -340,6 +438,15 @@ func consumeAssociativeArray(data []byte, offset int) (map[interface{}]interface
return result, offset + 1, nil
}

func consumeAssociativeArrayIntoStruct(data []byte, offset int, v reflect.Value) (int, error) {
m, offset, err := consumeAssociativeArray(data, offset)
if err != nil {
return -1, err
}

return offset, fillStruct(v, stringifyKeys(m).(map[interface{}]interface{}))
}

func consumeIndexedArray(data []byte, offset int) ([]interface{}, int, error) {
if !checkType(data, 'a', offset) {
return []interface{}{}, -1, errors.New("not an array")
Expand Down Expand Up @@ -381,3 +488,60 @@ func consumeIndexedArray(data []byte, offset int) ([]interface{}, int, error) {
// The +1 is for the final '}'
return result, offset + 1, nil
}

func consumeIndexedArrayIntoStruct(data []byte, offset int, v reflect.Value) (int, error) {
s, offset, err := consumeIndexedArray(data, offset)
if err != nil {
return -1, err
}

s = stringifyKeys(s).([]interface{})

l := len(s)
arrayOfObjects := reflect.MakeSlice(v.Type(), l, l)

for i := 0; i < l; i++ {
if m, ok := s[i].(map[interface{}]interface{}); ok {
obj := arrayOfObjects.Index(i)
if obj.Kind() == reflect.Ptr {
obj.Set(reflect.New(obj.Type().Elem()))
obj = obj.Elem()
}
if err := setField(obj, m); err != nil {
return -1, err
}
} else {
if err := setField(arrayOfObjects.Index(i), s[i]); err != nil {
return -1, err
}
}
}

v.Set(arrayOfObjects)

return offset, nil
}

// stringifyKeys recursively casts map keys into a real string but
// still stored as type interface{}, so the output still can be used
// in fillStruct() because it assumes a map[interface{}]interface{}.
func stringifyKeys(in interface{}) interface{} {
switch x := in.(type) {
case []interface{}:
newSlice := make([]interface{}, len(x))
for i, v := range x {
newSlice[i] = stringifyKeys(v)
}
return newSlice

case map[interface{}]interface{}:
newMap := map[interface{}]interface{}{}
for k, v := range x {
newMap[fmt.Sprintf("%v", k)] = stringifyKeys(v)
}
return newMap

default:
return in
}
}
67 changes: 53 additions & 14 deletions serialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type MarshalOptions struct {
// If this is true, then all struct names will be stripped from objects
// and "stdClass" will be used instead. The default value is false.
OnlyStdClass bool
// If this is true, then a struct will be marshalled as if it is a map.
MarshalStructAsMap bool
}

// DefaultMarshalOptions will create a new instance of MarshalOptions with
Expand Down Expand Up @@ -143,6 +145,11 @@ func MarshalNil() []byte {
// name are maintained. At the moment there is no way to change this behaviour,
// unlike other marshallers that use a tag on the field.
func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) {
_, m, err := marshalStruct(input, options, false)
return m, err
}

func marshalStruct(input interface{}, options *MarshalOptions, embeded bool) (int, []byte, error) {
value := reflect.ValueOf(input)
typeOfValue := value.Type()

Expand All @@ -153,52 +160,84 @@ func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) {
var buffer bytes.Buffer
for i := 0; i < value.NumField(); i++ {
f := value.Field(i)
ft := typeOfValue.Field(i)

if !f.CanInterface() {
// This is an unexported field, we cannot read it.
continue
}

visibleFieldCount++
fieldName, fieldOptions := parseTag(ft.Tag.Get("php"))
if fieldOptions.Contains("omitnilptr") && f.Kind() == reflect.Ptr && f.IsNil() {
continue
}
if fieldName == "-" {
continue
}

fieldName, fieldOptions := parseTag(typeOfValue.Field(i).Tag.Get("php"))
if ft.Anonymous && options.MarshalStructAsMap {
if f.Kind() == reflect.Struct {
// the field is embedded struct
fields, m, err := marshalStruct(f.Interface(), options, true)
if err != nil {
return -1, nil, err
}
buffer.Write(m)
visibleFieldCount += fields
continue
}

if fieldOptions.Contains("omitnilptr") {
if f.Kind() == reflect.Ptr && f.IsNil() {
visibleFieldCount--
if f.Kind() == reflect.Ptr && f.Elem().Kind() == reflect.Struct {
// the field is embedded struct ptr
fields, m, err := marshalStruct(f.Elem().Interface(), options, true)
if err != nil {
return -1, nil, err
}
buffer.Write(m)
visibleFieldCount += fields
continue
}
}

if fieldName == "-" {
visibleFieldCount--
continue
} else if fieldName == "" {
fieldName = lowerCaseFirstLetter(typeOfValue.Field(i).Name)
visibleFieldCount++

if fieldName == "" {
fieldName = ft.Name
if !options.MarshalStructAsMap {
fieldName = lowerCaseFirstLetter(fieldName)
}
}

buffer.Write(MarshalString(fieldName))

m, err := Marshal(f.Interface(), options)
if err != nil {
return nil, err
return -1, nil, err
}

buffer.Write(m)
}

if options.MarshalStructAsMap {
if embeded {
return visibleFieldCount, buffer.Bytes(), nil
}
return visibleFieldCount, []byte(fmt.Sprintf("a:%d:{%s}", visibleFieldCount, buffer.String())), nil
}

className := reflect.ValueOf(input).Type().Name()
if options.OnlyStdClass {
className = "stdClass"
}

return []byte(fmt.Sprintf("O:%d:\"%s\":%d:{%s}", len(className),
className, visibleFieldCount, buffer.String())), nil
return visibleFieldCount,
[]byte(fmt.Sprintf("O:%d:\"%s\":%d:{%s}", len(className), className, visibleFieldCount, buffer.String())),
nil
}

// Marshal is the canonical way to perform the equivalent of serialize() in PHP.
// It can handle encoding scalar types, slices and maps.
func Marshal(input interface{}, options *MarshalOptions) ([]byte, error) {

if options == nil {
options = DefaultMarshalOptions()
}
Expand Down
Loading