package amf import ( "errors" "encoding/binary" "math" ) type AMFObj map[interface{}]interface{} // interface mostly so root obj can be included as an index of ints // always a msg type of 20 for AMF0 func DecodeAMF(data *[]byte) (AMFObj, error) { var byte_idx uint32 var root_obj_idx int // top level object can be referred to as int amf_root_obj := make(AMFObj) for { // read the next thing and store it under the given root index top_level_obj, err := read_next(data, &byte_idx) if err != nil { return amf_root_obj, err } amf_root_obj[root_obj_idx] = top_level_obj root_obj_idx += 1 if byte_idx == uint32(len(*data)) { // check if full message read break } } return amf_root_obj, nil } // generic wrapper to read n bytes, what I tried to avoid with ReadChunk in rtmp func read_bytes(data *[]byte, byte_idx *uint32, n uint32) ([]byte, error) { if int(*byte_idx + n) > len(*data) { return make([]byte, 0), errors.New("Read goes past end") } read_slice := (*data)[*byte_idx:*byte_idx + n] *byte_idx += n return read_slice, nil } // assuming the next thing is a number, read it // format is 8 bytes encoded big-endian as float64 func read_number(data *[]byte, byte_idx *uint32) (float64, error) { float_bytes, err := read_bytes(data, byte_idx, 8) if err != nil { return 0.0, err } return math.Float64frombits(binary.BigEndian.Uint64(float_bytes)), nil } // assuming next is boolean, read // format is 1 byte, 0 for false 1 for true func read_bool(data *[]byte, byte_idx *uint32) (bool, error) { bool_byte, err := read_bytes(data, byte_idx, 1) if err != nil { return false, err } if bool_byte[0] > 1 { return false, errors.New("bool byte must be 0 or 1") } return bool_byte[0] != 0, nil } // assuming next is string, read // format is 2 bytes for string len n, next n bytes are ascii chars func read_string(data *[]byte, byte_idx *uint32) (string, error) { string_len, err := read_bytes(data, byte_idx, 2) if err != nil { return "", err } string_bytes, err := read_bytes(data, byte_idx, uint32(binary.BigEndian.Uint16(string_len))) if err != nil { return "", err } return string(string_bytes), err } // assuming next is an object, read // format is always a string for a key, then a marker to determine next data type then data // ends with an empty string for a key followed by a 9 func read_object(data *[]byte, byte_idx *uint32) (AMFObj, error) { root_obj := make(AMFObj) for { key, err := read_string(data, byte_idx) if err != nil { return root_obj, err } if key == "" { if end_byte, err := read_bytes(data, byte_idx, 1); err != nil { return root_obj, err } else if end_byte[0] != 9 { return root_obj, errors.New("Object should end but didnt") } return root_obj, nil } val, err := read_next(data, byte_idx) if err != nil { return root_obj, err } root_obj[key] = val } } // read the next thing // read the next byte to determine the data type, then call one of the above to read // defined as AMF0 spec func read_next(data *[]byte, byte_idx *uint32) (interface{}, error) { data_type, err := read_bytes(data, byte_idx, 1) var next_obj interface{} if err != nil { return nil, err } switch data_type[0] { case 0: next_obj, err = read_number(data, byte_idx) case 1: next_obj, err = read_bool(data, byte_idx) case 2: next_obj, err = read_string(data, byte_idx) case 3: next_obj, err = read_object(data, byte_idx) case 5: // null marker, no extra empty bytes, just skip to next object return nil, nil default: return nil, errors.New("Unhandled data type") } if err != nil { return nil, err } return next_obj, nil }