blob: abbb283c6ad1deb2cf5a3294aba3361f947efe71 [file] [log] [blame]
# Copyright (c) The PyAMF Project.
# See LICENSE.txt for details.
"""
AMF3 implementation.
C{AMF3} is the default serialization for
U{ActionScript<http://en.wikipedia.org/wiki/ActionScript>} 3.0 and provides
various advantages over L{AMF0<pyamf.amf0>}, which is used for ActionScript 1.0
and 2.0. It adds support for sending C{int} and C{uint} objects as integers and
supports data types that are available only in ActionScript 3.0, such as
L{ByteArray} and L{ArrayCollection}.
@see: U{Official AMF3 Specification in English
<http://opensource.adobe.com/wiki/download/attachments/1114283/amf3_spec_05_05_08.pdf>}
@see: U{Official AMF3 Specification in Japanese
<http://opensource.adobe.com/wiki/download/attachments/1114283/JP_amf3_spec_121207.pdf>}
@see: U{AMF3 documentation on OSFlash
<http://osflash.org/documentation/amf3>}
@since: 0.1
"""
import datetime
import zlib
import pyamf
from pyamf import codec, util, xml, python
__all__ = [
'ByteArray',
'Context',
'Encoder',
'Decoder',
'use_proxies_default',
]
#: If True encode/decode lists/tuples to L{ArrayCollection
#: <pyamf.flex.ArrayCollection>} and dicts to L{ObjectProxy
#: <pyamf.flex.ObjectProxy>}
use_proxies_default = False
#: The undefined type is represented by the undefined type marker. No further
#: information is encoded for this value.
TYPE_UNDEFINED = '\x00'
#: The null type is represented by the null type marker. No further
#: information is encoded for this value.
TYPE_NULL = '\x01'
#: The false type is represented by the false type marker and is used to
#: encode a Boolean value of C{false}. No further information is encoded for
#: this value.
TYPE_BOOL_FALSE = '\x02'
#: The true type is represented by the true type marker and is used to encode
#: a Boolean value of C{true}. No further information is encoded for this
#: value.
TYPE_BOOL_TRUE = '\x03'
#: In AMF 3 integers are serialized using a variable length signed 29-bit
#: integer.
#: @see: U{Parsing Integers on OSFlash (external)
#: <http://osflash.org/documentation/amf3/parsing_integers>}
TYPE_INTEGER = '\x04'
#: This type is used to encode an ActionScript Number or an ActionScript
#: C{int} of value greater than or equal to 2^28 or an ActionScript uint of
#: value greater than or equal to 2^29. The encoded value is is always an 8
#: byte IEEE-754 double precision floating point value in network byte order
#: (sign bit in low memory). The AMF 3 number type is encoded in the same
#: manner as the AMF 0 L{Number<pyamf.amf0.TYPE_NUMBER>} type.
TYPE_NUMBER = '\x05'
#: ActionScript String values are represented using a single string type in
#: AMF 3 - the concept of string and long string types from AMF 0 is not used.
#: Strings can be sent as a reference to a previously occurring String by
#: using an index to the implicit string reference table. Strings are encoding
#: using UTF-8 - however the header may either describe a string literal or a
#: string reference.
TYPE_STRING = '\x06'
#: ActionScript 3.0 introduced a new XML type however the legacy C{XMLDocument}
#: type from ActionScript 1.0 and 2.0.is retained in the language as
#: C{flash.xml.XMLDocument}. Similar to AMF 0, the structure of an
#: C{XMLDocument} needs to be flattened into a string representation for
#: serialization. As with other strings in AMF, the content is encoded in
#: UTF-8. XMLDocuments can be sent as a reference to a previously occurring
#: C{XMLDocument} instance by using an index to the implicit object reference
#: table.
#: @see: U{OSFlash documentation (external)
#: <http://osflash.org/documentation/amf3#x07_-_xml_legacy_flash.xml.xmldocument_class>}
TYPE_XML = '\x07'
#: In AMF 3 an ActionScript Date is serialized simply as the number of
#: milliseconds elapsed since the epoch of midnight, 1st Jan 1970 in the
#: UTC time zone. Local time zone information is not sent.
TYPE_DATE = '\x08'
#: ActionScript Arrays are described based on the nature of their indices,
#: i.e. their type and how they are positioned in the Array.
TYPE_ARRAY = '\x09'
#: A single AMF 3 type handles ActionScript Objects and custom user classes.
TYPE_OBJECT = '\x0A'
#: ActionScript 3.0 introduces a new top-level XML class that supports
#: U{E4X<http://en.wikipedia.org/wiki/E4X>} syntax.
#: For serialization purposes the XML type needs to be flattened into a
#: string representation. As with other strings in AMF, the content is
#: encoded using UTF-8.
TYPE_XMLSTRING = '\x0B'
#: ActionScript 3.0 introduces the L{ByteArray} type to hold an Array
#: of bytes. AMF 3 serializes this type using a variable length encoding
#: 29-bit integer for the byte-length prefix followed by the raw bytes
#: of the L{ByteArray}.
#: @see: U{Parsing ByteArrays on OSFlash (external)
#: <http://osflash.org/documentation/amf3/parsing_byte_arrays>}
TYPE_BYTEARRAY = '\x0C'
#: Reference bit.
REFERENCE_BIT = 0x01
#: The maximum that can be represented by a signed 29 bit integer.
MAX_29B_INT = 0x0FFFFFFF
#: The minimum that can be represented by a signed 29 bit integer.
MIN_29B_INT = -0x10000000
ENCODED_INT_CACHE = {}
class ObjectEncoding:
"""
AMF object encodings.
"""
#: Property list encoding.
#: The remaining integer-data represents the number of class members that
#: exist. The property names are read as string-data. The values are then
#: read as AMF3-data.
STATIC = 0x00
#: Externalizable object.
#: What follows is the value of the "inner" object, including type code.
#: This value appears for objects that implement IExternalizable, such as
#: L{ArrayCollection} and L{ObjectProxy}.
EXTERNAL = 0x01
#: Name-value encoding.
#: The property names and values are encoded as string-data followed by
#: AMF3-data until there is an empty string property name. If there is a
#: class-def reference there are no property names and the number of values
#: is equal to the number of properties in the class-def.
DYNAMIC = 0x02
#: Proxy object.
PROXY = 0x03
class DataOutput(object):
"""
I am a C{StringIO} type object containing byte data from the AMF stream.
ActionScript 3.0 introduced the C{flash.utils.ByteArray} class to support
the manipulation of raw data in the form of an Array of bytes.
I provide a set of methods for writing binary data with ActionScript 3.0.
This class is the I/O counterpart to the L{DataInput} class, which reads
binary data.
@see: U{IDataOutput on Livedocs (external)
<http://livedocs.adobe.com/flex/201/langref/flash/utils/IDataOutput.html>}
"""
def __init__(self, encoder):
"""
@param encoder: Encoder containing the stream.
@type encoder: L{amf3.Encoder<pyamf.amf3.Encoder>}
"""
self.encoder = encoder
self.stream = encoder.stream
def writeBoolean(self, value):
"""
Writes a Boolean value.
@type value: C{bool}
@param value: A C{Boolean} value determining which byte is written.
If the parameter is C{True}, C{1} is written; if C{False}, C{0} is
written.
@raise ValueError: Non-boolean value found.
"""
if not isinstance(value, bool):
raise ValueError("Non-boolean value found")
if value is True:
self.stream.write_uchar(1)
else:
self.stream.write_uchar(0)
def writeByte(self, value):
"""
Writes a byte.
@type value: C{int}
"""
self.stream.write_char(value)
def writeUnsignedByte(self, value):
"""
Writes an unsigned byte.
@type value: C{int}
@since: 0.5
"""
return self.stream.write_uchar(value)
def writeDouble(self, value):
"""
Writes an IEEE 754 double-precision (64-bit) floating
point number.
@type value: C{number}
"""
self.stream.write_double(value)
def writeFloat(self, value):
"""
Writes an IEEE 754 single-precision (32-bit) floating
point number.
@type value: C{float}
"""
self.stream.write_float(value)
def writeInt(self, value):
"""
Writes a 32-bit signed integer.
@type value: C{int}
"""
self.stream.write_long(value)
def writeMultiByte(self, value, charset):
"""
Writes a multibyte string to the datastream using the
specified character set.
@type value: C{str}
@param value: The string value to be written.
@type charset: C{str}
@param charset: The string denoting the character set to use. Possible
character set strings include C{shift-jis}, C{cn-gb},
C{iso-8859-1} and others.
@see: U{Supported character sets on Livedocs (external)
<http://livedocs.adobe.com/flex/201/langref/charset-codes.html>}
"""
if type(value) is unicode:
value = value.encode(charset)
self.stream.write(value)
def writeObject(self, value):
"""
Writes an object to data stream in AMF serialized format.
@param value: The object to be serialized.
"""
self.encoder.writeElement(value)
def writeShort(self, value):
"""
Writes a 16-bit integer.
@type value: C{int}
@param value: A byte value as an integer.
"""
self.stream.write_short(value)
def writeUnsignedShort(self, value):
"""
Writes a 16-bit unsigned integer.
@type value: C{int}
@param value: A byte value as an integer.
@since: 0.5
"""
self.stream.write_ushort(value)
def writeUnsignedInt(self, value):
"""
Writes a 32-bit unsigned integer.
@type value: C{int}
@param value: A byte value as an unsigned integer.
"""
self.stream.write_ulong(value)
def writeUTF(self, value):
"""
Writes a UTF-8 string to the data stream.
The length of the UTF-8 string in bytes is written first,
as a 16-bit integer, followed by the bytes representing the
characters of the string.
@type value: C{str}
@param value: The string value to be written.
"""
buf = util.BufferedByteStream()
buf.write_utf8_string(value)
bytes = buf.getvalue()
self.stream.write_ushort(len(bytes))
self.stream.write(bytes)
def writeUTFBytes(self, value):
"""
Writes a UTF-8 string. Similar to L{writeUTF}, but does
not prefix the string with a 16-bit length word.
@type value: C{str}
@param value: The string value to be written.
"""
val = None
if isinstance(value, unicode):
val = value
else:
val = unicode(value, 'utf8')
self.stream.write_utf8_string(val)
class DataInput(object):
"""
I provide a set of methods for reading binary data with ActionScript 3.0.
This class is the I/O counterpart to the L{DataOutput} class,
which writes binary data.
@see: U{IDataInput on Livedocs (external)
<http://livedocs.adobe.com/flex/201/langref/flash/utils/IDataInput.html>}
"""
def __init__(self, decoder=None):
"""
@param decoder: AMF3 decoder containing the stream.
@type decoder: L{amf3.Decoder<pyamf.amf3.Decoder>}
"""
self.decoder = decoder
self.stream = decoder.stream
def readBoolean(self):
"""
Read C{Boolean}.
@raise ValueError: Error reading Boolean.
@rtype: C{bool}
@return: A Boolean value, C{True} if the byte
is nonzero, C{False} otherwise.
"""
byte = self.stream.read(1)
if byte == '\x00':
return False
elif byte == '\x01':
return True
else:
raise ValueError("Error reading boolean")
def readByte(self):
"""
Reads a signed byte.
@rtype: C{int}
@return: The returned value is in the range -128 to 127.
"""
return self.stream.read_char()
def readDouble(self):
"""
Reads an IEEE 754 double-precision floating point number from the
data stream.
@rtype: C{number}
@return: An IEEE 754 double-precision floating point number.
"""
return self.stream.read_double()
def readFloat(self):
"""
Reads an IEEE 754 single-precision floating point number from the
data stream.
@rtype: C{number}
@return: An IEEE 754 single-precision floating point number.
"""
return self.stream.read_float()
def readInt(self):
"""
Reads a signed 32-bit integer from the data stream.
@rtype: C{int}
@return: The returned value is in the range -2147483648 to 2147483647.
"""
return self.stream.read_long()
def readMultiByte(self, length, charset):
"""
Reads a multibyte string of specified length from the data stream
using the specified character set.
@type length: C{int}
@param length: The number of bytes from the data stream to read.
@type charset: C{str}
@param charset: The string denoting the character set to use.
@rtype: C{str}
@return: UTF-8 encoded string.
"""
#FIXME nick: how to work out the code point byte size (on the fly)?
bytes = self.stream.read(length)
return unicode(bytes, charset)
def readObject(self):
"""
Reads an object from the data stream.
@return: The deserialized object.
"""
return self.decoder.readElement()
def readShort(self):
"""
Reads a signed 16-bit integer from the data stream.
@rtype: C{uint}
@return: The returned value is in the range -32768 to 32767.
"""
return self.stream.read_short()
def readUnsignedByte(self):
"""
Reads an unsigned byte from the data stream.
@rtype: C{uint}
@return: The returned value is in the range 0 to 255.
"""
return self.stream.read_uchar()
def readUnsignedInt(self):
"""
Reads an unsigned 32-bit integer from the data stream.
@rtype: C{uint}
@return: The returned value is in the range 0 to 4294967295.
"""
return self.stream.read_ulong()
def readUnsignedShort(self):
"""
Reads an unsigned 16-bit integer from the data stream.
@rtype: C{uint}
@return: The returned value is in the range 0 to 65535.
"""
return self.stream.read_ushort()
def readUTF(self):
"""
Reads a UTF-8 string from the data stream.
The string is assumed to be prefixed with an unsigned
short indicating the length in bytes.
@rtype: C{str}
@return: A UTF-8 string produced by the byte
representation of characters.
"""
length = self.stream.read_ushort()
return self.stream.read_utf8_string(length)
def readUTFBytes(self, length):
"""
Reads a sequence of C{length} UTF-8 bytes from the data
stream and returns a string.
@type length: C{int}
@param length: The number of bytes from the data stream to read.
@rtype: C{str}
@return: A UTF-8 string produced by the byte representation of
characters of specified C{length}.
"""
return self.readMultiByte(length, 'utf-8')
class ByteArray(util.BufferedByteStream, DataInput, DataOutput):
"""
I am a C{StringIO} type object containing byte data from the AMF stream.
ActionScript 3.0 introduced the C{flash.utils.ByteArray} class to support
the manipulation of raw data in the form of an Array of bytes.
Supports C{zlib} compression.
Possible uses of the C{ByteArray} class:
- Creating a custom protocol to connect to a client.
- Writing your own AMF/Remoting packet.
- Optimizing the size of your data by using custom data types.
@see: U{ByteArray on Livedocs (external)
<http://livedocs.adobe.com/flex/201/langref/flash/utils/ByteArray.html>}
"""
class __amf__:
amf3 = True
def __init__(self, *args, **kwargs):
self.context = Context()
util.BufferedByteStream.__init__(self, *args, **kwargs)
DataInput.__init__(self, Decoder(self, self.context))
DataOutput.__init__(self, Encoder(self, self.context))
self.compressed = False
def readObject(self, *args, **kwargs):
self.context.clear()
return super(ByteArray, self).readObject(*args, **kwargs)
def writeObject(self, *args, **kwargs):
self.context.clear()
return super(ByteArray, self).writeObject(*args, **kwargs)
def __cmp__(self, other):
if isinstance(other, ByteArray):
return cmp(self.getvalue(), other.getvalue())
return cmp(self.getvalue(), other)
def __str__(self):
buf = self.getvalue()
if not self.compressed:
return buf
buf = zlib.compress(buf)
#FIXME nick: hacked
return buf[0] + '\xda' + buf[2:]
def compress(self):
"""
Forces compression of the underlying stream.
"""
self.compressed = True
class ClassDefinition(object):
"""
This is an internal class used by L{Encoder}/L{Decoder} to hold details
about transient class trait definitions.
"""
def __init__(self, alias):
self.alias = alias
self.reference = None
alias.compile()
self.attr_len = 0
if alias.static_attrs:
self.attr_len = len(alias.static_attrs)
self.encoding = ObjectEncoding.DYNAMIC
if alias.external:
self.encoding = ObjectEncoding.EXTERNAL
elif not alias.dynamic:
if alias.encodable_properties is not None:
if len(alias.static_attrs) == len(alias.encodable_properties):
self.encoding = ObjectEncoding.STATIC
else:
self.encoding = ObjectEncoding.STATIC
def __repr__(self):
return '<%s.ClassDefinition reference=%r encoding=%r alias=%r at 0x%x>' % (
self.__class__.__module__, self.reference, self.encoding, self.alias, id(self))
class Context(codec.Context):
"""
I hold the AMF3 context for en/decoding streams.
@ivar strings: A list of string references.
@type strings: C{list}
@ivar classes: A list of L{ClassDefinition}.
@type classes: C{list}
"""
def __init__(self):
self.strings = codec.IndexedCollection(use_hash=True)
self.classes = {}
self.class_ref = {}
self.class_idx = 0
codec.Context.__init__(self)
def clear(self):
"""
Clears the context.
"""
codec.Context.clear(self)
self.strings.clear()
self.proxied_objects = {}
self.classes = {}
self.class_ref = {}
self.class_idx = 0
def getString(self, ref):
"""
Gets a string based on a reference C{ref}.
@param ref: The reference index.
@type ref: C{str}
@rtype: C{str} or C{None}
@return: The referenced string.
"""
return self.strings.getByReference(ref)
def getStringReference(self, s):
"""
Return string reference.
@type s: C{str}
@param s: The referenced string.
@return: The reference index to the string.
@rtype: C{int} or C{None}
"""
return self.strings.getReferenceTo(s)
def addString(self, s):
"""
Creates a reference to C{s}. If the reference already exists, that
reference is returned.
@type s: C{str}
@param s: The string to be referenced.
@rtype: C{int}
@return: The reference index.
@raise TypeError: The parameter C{s} is not of C{basestring} type.
"""
if not isinstance(s, basestring):
raise TypeError
if len(s) == 0:
return -1
return self.strings.append(s)
def getClassByReference(self, ref):
"""
Return class reference.
@return: Class reference.
"""
return self.class_ref.get(ref)
def getClass(self, klass):
"""
Return class reference.
@return: Class reference.
"""
return self.classes.get(klass)
def addClass(self, alias, klass):
"""
Creates a reference to C{class_def}.
@param alias: C{ClassDefinition} instance.
"""
ref = self.class_idx
self.class_ref[ref] = alias
cd = self.classes[klass] = alias
cd.reference = ref
self.class_idx += 1
return ref
def getObjectForProxy(self, proxy):
"""
Returns the unproxied version of C{proxy} as stored in the context, or
unproxies the proxy and returns that 'raw' object.
@see: L{pyamf.flex.unproxy_object}
@since: 0.6
"""
obj = self.proxied_objects.get(id(proxy))
if obj is None:
from pyamf import flex
obj = flex.unproxy_object(proxy)
self.addProxyObject(obj, proxy)
return obj
def addProxyObject(self, obj, proxied):
"""
Stores a reference to the unproxied and proxied versions of C{obj} for
later retrieval.
@since: 0.6
"""
self.proxied_objects[id(obj)] = proxied
self.proxied_objects[id(proxied)] = obj
def getProxyForObject(self, obj):
"""
Returns the proxied version of C{obj} as stored in the context, or
creates a new proxied object and returns that.
@see: L{pyamf.flex.proxy_object}
@since: 0.6
"""
proxied = self.proxied_objects.get(id(obj))
if proxied is None:
from pyamf import flex
proxied = flex.proxy_object(obj)
self.addProxyObject(obj, proxied)
return proxied
class Decoder(codec.Decoder):
"""
Decodes an AMF3 data stream.
"""
def __init__(self, *args, **kwargs):
self.use_proxies = kwargs.pop('use_proxies', use_proxies_default)
codec.Decoder.__init__(self, *args, **kwargs)
def buildContext(self):
return Context()
def getTypeFunc(self, data):
if data == TYPE_UNDEFINED:
return self.readUndefined
elif data == TYPE_NULL:
return self.readNull
elif data == TYPE_BOOL_FALSE:
return self.readBoolFalse
elif data == TYPE_BOOL_TRUE:
return self.readBoolTrue
elif data == TYPE_INTEGER:
return self.readInteger
elif data == TYPE_NUMBER:
return self.readNumber
elif data == TYPE_STRING:
return self.readString
elif data == TYPE_XML:
return self.readXML
elif data == TYPE_DATE:
return self.readDate
elif data == TYPE_ARRAY:
return self.readArray
elif data == TYPE_OBJECT:
return self.readObject
elif data == TYPE_XMLSTRING:
return self.readXMLString
elif data == TYPE_BYTEARRAY:
return self.readByteArray
def readProxy(self, obj):
"""
Decodes a proxied object from the stream.
@since: 0.6
"""
return self.context.getObjectForProxy(obj)
def readUndefined(self):
"""
Read undefined.
"""
return pyamf.Undefined
def readNull(self):
"""
Read null.
@return: C{None}
@rtype: C{None}
"""
return None
def readBoolFalse(self):
"""
Returns C{False}.
@return: C{False}
@rtype: C{bool}
"""
return False
def readBoolTrue(self):
"""
Returns C{True}.
@return: C{True}
@rtype: C{bool}
"""
return True
def readNumber(self):
"""
Read number.
"""
return self.stream.read_double()
def readInteger(self, signed=True):
"""
Reads and returns an integer from the stream.
@type signed: C{bool}
@see: U{Parsing integers on OSFlash
<http://osflash.org/amf3/parsing_integers>} for the AMF3 integer data
format.
"""
return decode_int(self.stream, signed)
def _readLength(self):
x = decode_int(self.stream, False)
return (x >> 1, x & REFERENCE_BIT == 0)
def readBytes(self):
"""
Reads and returns a utf-8 encoded byte array.
"""
length, is_reference = self._readLength()
if is_reference:
return self.context.getString(length)
if length == 0:
return ''
result = self.stream.read(length)
self.context.addString(result)
return result
def readString(self):
"""
Reads and returns a string from the stream.
"""
length, is_reference = self._readLength()
if is_reference:
result = self.context.getString(length)
return self.context.getStringForBytes(result)
if length == 0:
return ''
result = self.stream.read(length)
self.context.addString(result)
return self.context.getStringForBytes(result)
def readDate(self):
"""
Read date from the stream.
The timezone is ignored as the date is always in UTC.
"""
ref = self.readInteger(False)
if ref & REFERENCE_BIT == 0:
return self.context.getObject(ref >> 1)
ms = self.stream.read_double()
result = util.get_datetime(ms / 1000.0)
if self.timezone_offset is not None:
result += self.timezone_offset
self.context.addObject(result)
return result
def readArray(self):
"""
Reads an array from the stream.
@warning: There is a very specific problem with AMF3 where the first
three bytes of an encoded empty C{dict} will mirror that of an encoded
C{{'': 1, '2': 2}}
"""
size = self.readInteger(False)
if size & REFERENCE_BIT == 0:
return self.context.getObject(size >> 1)
size >>= 1
key = self.readBytes()
if key == '':
# integer indexes only -> python list
result = []
self.context.addObject(result)
for i in xrange(size):
result.append(self.readElement())
return result
result = pyamf.MixedArray()
self.context.addObject(result)
while key:
result[key] = self.readElement()
key = self.readBytes()
for i in xrange(size):
el = self.readElement()
result[i] = el
return result
def _getClassDefinition(self, ref):
"""
Reads class definition from the stream.
"""
is_ref = ref & REFERENCE_BIT == 0
ref >>= 1
if is_ref:
class_def = self.context.getClassByReference(ref)
return class_def
name = self.readBytes()
alias = None
if name == '':
name = pyamf.ASObject
try:
alias = pyamf.get_class_alias(name)
except pyamf.UnknownClassAlias:
if self.strict:
raise
alias = pyamf.TypedObjectClassAlias(name)
class_def = ClassDefinition(alias)
class_def.encoding = ref & 0x03
class_def.attr_len = ref >> 2
class_def.static_properties = []
if class_def.attr_len > 0:
for i in xrange(class_def.attr_len):
key = self.readBytes()
class_def.static_properties.append(key)
self.context.addClass(class_def, alias.klass)
return class_def
def _readStatic(self, class_def, obj):
for attr in class_def.static_properties:
obj[attr] = self.readElement()
def _readDynamic(self, class_def, obj):
attr = self.readBytes()
while attr:
obj[attr] = self.readElement()
attr = self.readBytes()
def readObject(self):
"""
Reads an object from the stream.
"""
ref = self.readInteger(False)
if ref & REFERENCE_BIT == 0:
obj = self.context.getObject(ref >> 1)
if obj is None:
raise pyamf.ReferenceError('Unknown reference %d' % (ref >> 1,))
if self.use_proxies is True:
obj = self.readProxy(obj)
return obj
ref >>= 1
class_def = self._getClassDefinition(ref)
alias = class_def.alias
obj = alias.createInstance(codec=self)
obj_attrs = dict()
self.context.addObject(obj)
if class_def.encoding in (ObjectEncoding.EXTERNAL, ObjectEncoding.PROXY):
obj.__readamf__(DataInput(self))
if self.use_proxies is True:
obj = self.readProxy(obj)
return obj
elif class_def.encoding == ObjectEncoding.DYNAMIC:
self._readStatic(class_def, obj_attrs)
self._readDynamic(class_def, obj_attrs)
elif class_def.encoding == ObjectEncoding.STATIC:
self._readStatic(class_def, obj_attrs)
else:
raise pyamf.DecodeError("Unknown object encoding")
alias.applyAttributes(obj, obj_attrs, codec=self)
if self.use_proxies is True:
obj = self.readProxy(obj)
return obj
def readXML(self):
"""
Reads an xml object from the stream.
@return: An etree interface compatible object
@see: L{xml.set_default_interface}
"""
ref = self.readInteger(False)
if ref & REFERENCE_BIT == 0:
return self.context.getObject(ref >> 1)
xmlstring = self.stream.read(ref >> 1)
x = xml.fromstring(xmlstring)
self.context.addObject(x)
return x
def readXMLString(self):
"""
Reads a string from the data stream and converts it into
an XML Tree.
@see: L{readXML}
"""
return self.readXML()
def readByteArray(self):
"""
Reads a string of data from the stream.
Detects if the L{ByteArray} was compressed using C{zlib}.
@see: L{ByteArray}
@note: This is not supported in ActionScript 1.0 and 2.0.
"""
ref = self.readInteger(False)
if ref & REFERENCE_BIT == 0:
return self.context.getObject(ref >> 1)
buffer = self.stream.read(ref >> 1)
try:
buffer = zlib.decompress(buffer)
compressed = True
except zlib.error:
compressed = False
obj = ByteArray(buffer)
obj.compressed = compressed
self.context.addObject(obj)
return obj
class Encoder(codec.Encoder):
"""
Encodes an AMF3 data stream.
"""
def __init__(self, *args, **kwargs):
self.use_proxies = kwargs.pop('use_proxies', use_proxies_default)
self.string_references = kwargs.pop('string_references', True)
codec.Encoder.__init__(self, *args, **kwargs)
def buildContext(self):
return Context()
def getTypeFunc(self, data):
"""
@see: L{codec.Encoder.getTypeFunc}
"""
t = type(data)
if t in python.int_types:
return self.writeInteger
elif t is ByteArray:
return self.writeByteArray
elif t is pyamf.MixedArray:
return self.writeDict
return codec.Encoder.getTypeFunc(self, data)
def writeUndefined(self, n):
"""
Writes an C{pyamf.Undefined} value to the stream.
"""
self.stream.write(TYPE_UNDEFINED)
def writeNull(self, n):
"""
Writes a C{null} value to the stream.
"""
self.stream.write(TYPE_NULL)
def writeBoolean(self, n):
"""
Writes a Boolean to the stream.
"""
t = TYPE_BOOL_TRUE
if n is False:
t = TYPE_BOOL_FALSE
self.stream.write(t)
def _writeInteger(self, n):
"""
AMF3 integers are encoded.
@param n: The integer data to be encoded to the AMF3 data stream.
@type n: integer data
@see: U{Parsing Integers on OSFlash
<http://osflash.org/documentation/amf3/parsing_integers>}
for more info.
"""
self.stream.write(encode_int(n))
def writeInteger(self, n):
"""
Writes an integer to the stream.
@type n: integer data
@param n: The integer data to be encoded to the AMF3 data stream.
"""
if n < MIN_29B_INT or n > MAX_29B_INT:
self.writeNumber(float(n))
return
self.stream.write(TYPE_INTEGER)
self.stream.write(encode_int(n))
def writeNumber(self, n):
"""
Writes a float to the stream.
@type n: C{float}
"""
self.stream.write(TYPE_NUMBER)
self.stream.write_double(n)
def serialiseBytes(self, b):
if len(b) == 0:
self.stream.write_uchar(REFERENCE_BIT)
return
if self.string_references:
ref = self.context.getStringReference(b)
if ref != -1:
self._writeInteger(ref << 1)
return
self.context.addString(b)
self._writeInteger((len(b) << 1) | REFERENCE_BIT)
self.stream.write(b)
def serialiseString(self, s):
"""
Writes a raw string to the stream.
@type s: C{str}
@param s: The string data to be encoded to the AMF3 data stream.
"""
if type(s) is unicode:
s = self.context.getBytesForString(s)
self.serialiseBytes(s)
def writeBytes(self, b):
"""
Writes a raw string to the stream.
"""
self.stream.write(TYPE_STRING)
self.serialiseBytes(b)
def writeString(self, s):
"""
Writes a string to the stream. It will be B{UTF-8} encoded.
"""
s = self.context.getBytesForString(s)
self.writeBytes(s)
def writeDate(self, n):
"""
Writes a C{datetime} instance to the stream.
@type n: L{datetime}
@param n: The C{Date} data to be encoded to the AMF3 data stream.
"""
if isinstance(n, datetime.time):
raise pyamf.EncodeError('A datetime.time instance was found but '
'AMF3 has no way to encode time objects. Please use '
'datetime.datetime instead (got:%r)' % (n,))
self.stream.write(TYPE_DATE)
ref = self.context.getObjectReference(n)
if ref != -1:
self._writeInteger(ref << 1)
return
self.context.addObject(n)
self.stream.write_uchar(REFERENCE_BIT)
if self.timezone_offset is not None:
n -= self.timezone_offset
ms = util.get_timestamp(n)
self.stream.write_double(ms * 1000.0)
def writeList(self, n, is_proxy=False):
"""
Writes a C{tuple}, C{set} or C{list} to the stream.
@type n: One of C{__builtin__.tuple}, C{__builtin__.set}
or C{__builtin__.list}
@param n: The C{list} data to be encoded to the AMF3 data stream.
"""
if self.use_proxies and not is_proxy:
self.writeProxy(n)
return
self.stream.write(TYPE_ARRAY)
ref = self.context.getObjectReference(n)
if ref != -1:
self._writeInteger(ref << 1)
return
self.context.addObject(n)
self._writeInteger((len(n) << 1) | REFERENCE_BIT)
self.stream.write('\x01')
[self.writeElement(x) for x in n]
def writeDict(self, n):
"""
Writes a C{dict} to the stream.
@type n: C{__builtin__.dict}
@param n: The C{dict} data to be encoded to the AMF3 data stream.
@raise ValueError: Non C{int}/C{str} key value found in the C{dict}
@raise EncodeError: C{dict} contains empty string keys.
"""
# Design bug in AMF3 that cannot read/write empty key strings
# for more info
if '' in n:
raise pyamf.EncodeError("dicts cannot contain empty string keys")
if self.use_proxies:
self.writeProxy(n)
return
self.stream.write(TYPE_ARRAY)
ref = self.context.getObjectReference(n)
if ref != -1:
self._writeInteger(ref << 1)
return
self.context.addObject(n)
# The AMF3 spec demands that all str based indicies be listed first
keys = n.keys()
int_keys = []
str_keys = []
for x in keys:
if isinstance(x, python.int_types):
int_keys.append(x)
elif isinstance(x, python.str_types):
str_keys.append(x)
else:
raise ValueError("Non int/str key value found in dict")
# Make sure the integer keys are within range
l = len(int_keys)
for x in int_keys:
if l < x <= 0:
# treat as a string key
str_keys.append(x)
del int_keys[int_keys.index(x)]
int_keys.sort()
# If integer keys don't start at 0, they will be treated as strings
if len(int_keys) > 0 and int_keys[0] != 0:
for x in int_keys:
str_keys.append(str(x))
del int_keys[int_keys.index(x)]
self._writeInteger(len(int_keys) << 1 | REFERENCE_BIT)
for x in str_keys:
self.serialiseString(x)
self.writeElement(n[x])
self.stream.write_uchar(0x01)
for k in int_keys:
self.writeElement(n[k])
def writeProxy(self, obj):
"""
Encodes a proxied object to the stream.
@since: 0.6
"""
proxy = self.context.getProxyForObject(obj)
self.writeObject(proxy, is_proxy=True)
def writeObject(self, obj, is_proxy=False):
"""
Writes an object to the stream.
"""
if self.use_proxies and not is_proxy:
self.writeProxy(obj)
return
self.stream.write(TYPE_OBJECT)
ref = self.context.getObjectReference(obj)
if ref != -1:
self._writeInteger(ref << 1)
return
self.context.addObject(obj)
# object is not referenced, serialise it
kls = obj.__class__
definition = self.context.getClass(kls)
alias = None
class_ref = False # if the class definition is a reference
if definition:
class_ref = True
alias = definition.alias
else:
alias = self.context.getClassAlias(kls)
definition = ClassDefinition(alias)
self.context.addClass(definition, alias.klass)
if class_ref:
self.stream.write(definition.reference)
else:
ref = 0
if definition.encoding != ObjectEncoding.EXTERNAL:
ref += definition.attr_len << 4
final_reference = encode_int(ref | definition.encoding << 2 |
REFERENCE_BIT << 1 | REFERENCE_BIT)
self.stream.write(final_reference)
definition.reference = encode_int(
definition.reference << 2 | REFERENCE_BIT)
if alias.anonymous:
self.stream.write('\x01')
else:
self.serialiseString(alias.alias)
# work out what the final reference for the class will be.
# this is okay because the next time an object of the same
# class is encoded, class_ref will be True and never get here
# again.
if alias.external:
obj.__writeamf__(DataOutput(self))
return
attrs = alias.getEncodableAttributes(obj, codec=self)
if alias.static_attrs:
if not class_ref:
[self.serialiseString(attr) for attr in alias.static_attrs]
for attr in alias.static_attrs:
value = attrs.pop(attr)
self.writeElement(value)
if definition.encoding == ObjectEncoding.STATIC:
return
if definition.encoding == ObjectEncoding.DYNAMIC:
if attrs:
for attr, value in attrs.iteritems():
if type(attr) in python.int_types:
attr = str(attr)
self.serialiseString(attr)
self.writeElement(value)
self.stream.write('\x01')
def writeByteArray(self, n):
"""
Writes a L{ByteArray} to the data stream.
@param n: The L{ByteArray} data to be encoded to the AMF3 data stream.
@type n: L{ByteArray}
"""
self.stream.write(TYPE_BYTEARRAY)
ref = self.context.getObjectReference(n)
if ref != -1:
self._writeInteger(ref << 1)
return
self.context.addObject(n)
buf = str(n)
l = len(buf)
self._writeInteger(l << 1 | REFERENCE_BIT)
self.stream.write(buf)
def writeXML(self, n):
"""
Writes a XML string to the data stream.
@type n: L{ET<xml.ET>}
@param n: The XML Document to be encoded to the AMF3 data stream.
"""
self.stream.write(TYPE_XMLSTRING)
ref = self.context.getObjectReference(n)
if ref != -1:
self._writeInteger(ref << 1)
return
self.context.addObject(n)
self.serialiseString(xml.tostring(n).encode('utf-8'))
def encode_int(n):
"""
Encodes an int as a variable length signed 29-bit integer as defined by
the spec.
@param n: The integer to be encoded
@return: The encoded string
@rtype: C{str}
@raise OverflowError: Out of range.
"""
global ENCODED_INT_CACHE
try:
return ENCODED_INT_CACHE[n]
except KeyError:
pass
if n < MIN_29B_INT or n > MAX_29B_INT:
raise OverflowError("Out of range")
if n < 0:
n += 0x20000000
bytes = ''
real_value = None
if n > 0x1fffff:
real_value = n
n >>= 1
bytes += chr(0x80 | ((n >> 21) & 0xff))
if n > 0x3fff:
bytes += chr(0x80 | ((n >> 14) & 0xff))
if n > 0x7f:
bytes += chr(0x80 | ((n >> 7) & 0xff))
if real_value is not None:
n = real_value
if n > 0x1fffff:
bytes += chr(n & 0xff)
else:
bytes += chr(n & 0x7f)
ENCODED_INT_CACHE[n] = bytes
return bytes
def decode_int(stream, signed=False):
"""
Decode C{int}.
"""
n = result = 0
b = stream.read_uchar()
while b & 0x80 != 0 and n < 3:
result <<= 7
result |= b & 0x7f
b = stream.read_uchar()
n += 1
if n < 3:
result <<= 7
result |= b
else:
result <<= 8
result |= b
if result & 0x10000000 != 0:
if signed:
result -= 0x20000000
else:
result <<= 1
result += 1
return result
pyamf.register_class(ByteArray)