[prev in list] [next in list] [prev in thread] [next in thread]
List: pyamf-commits
Subject: [pyamf-commits] r80 - in trunk: . pyamf pyamf/tests
From: pyamf-commits () collab ! com (pyamf-commits () collab ! com)
Date: 2007-10-26 19:22:12
Message-ID: 20071026172203.2DC047BC06D () mail ! collab ! com
[Download RAW message or body]
Author: nick
Date: 2007-10-26 19:22:03 +0200 (Fri, 26 Oct 2007)
New Revision: 80
Added:
trunk/pyamf/tests/amf3.py
trunk/pyamf/tests/util.py
Modified:
trunk/
trunk/pyamf/__init__.py
trunk/pyamf/amf0.py
trunk/pyamf/amf3.py
trunk/pyamf/tests/__init__.py
trunk/pyamf/tests/amf0.py
trunk/pyamf/util.py
Log:
Merged source:branches/amf3-5 to source:trunk r72
Property changes on: trunk
___________________________________________________________________
Name: svn:ignore
- *.pyc
+ *.pyc
PyAMF.egg-info
Modified: trunk/pyamf/__init__.py
===================================================================
--- trunk/pyamf/__init__.py 2007-10-26 17:15:52 UTC (rev 79)
+++ trunk/pyamf/__init__.py 2007-10-26 17:22:03 UTC (rev 80)
@@ -4,6 +4,7 @@
#
# Arnar Birgisson
# Thijs Triemstra
+# Nick Joyce
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -32,6 +33,9 @@
from pyamf import util, amf0
+CLASS_CACHE = {}
+CLASS_LOADERS = []
+
class GeneralTypes:
"""
PyAMF global constants
@@ -56,11 +60,260 @@
AMF_MIMETYPE = 'application/x-amf'
class BaseError(Exception):
- pass
+ """
+ Base AMF Error. All AMF related errors should be subclassed from this.
+ """
class ParseError(BaseError):
- pass
+ """
+ Raised if there is an error in parsing an AMF data stream.
+ """
+class ReferenceError(BaseError):
+ """
+ Raised if an AMF data stream refers to a non-existant object or string
+ reference.
+ """
+
+class EncodeError(BaseError):
+ """
+ Raised if the element could not be encoded to the stream. This is mainly
+ used to pick up the empty key string array bug.
+
+ See http://www.docuverse.com/blog/donpark/2007/05/14/flash-9-amf3-bug for
+ more info
+ """
+
+class Context(object):
+ """
+ """
+ objects = []
+ strings = []
+ classes = []
+
+ def clear(self):
+ """
+ Resets the context
+ """
+ self.objects = []
+ self.strings = []
+ self.classes = []
+
+ def getObject(self, ref):
+ """
+ Gets an object based on a reference ref
+
+ Raises L{pyamf.ReferenceError} if the object could not be found
+ """
+ try:
+ return self.objects[ref]
+ except IndexError:
+ raise ReferenceError("Object reference %d not found" % ref)
+
+ def getObjectReference(self, obj):
+ try:
+ return self.objects.index(obj)
+ except ValueError:
+ raise ReferenceError("Reference for object %r not found" % obj)
+
+ def addObject(self, obj):
+ """
+ Gets a reference to obj, creating one if necessary
+ """
+ try:
+ return self.objects.index(obj)
+ except ValueError:
+ self.objects.append(obj)
+
+ return len(self.objects)
+
+ def getString(self, ref):
+ """
+ Gets a string based on a reference ref
+
+ Raises L{pyamf.ReferenceError} if the string could not be found
+ """
+ try:
+ return self.strings[ref]
+ except IndexError:
+ raise ReferenceError("String reference %d not found" % ref)
+
+ def getStringReference(self, s):
+ try:
+ return self.strings.index(s)
+ except ValueError:
+ raise ReferenceError("Reference for string %r not found" % s)
+
+ def addString(self, s):
+ """
+ Creates a reference to s
+ """
+ try:
+ return self.strings.index(s)
+ except ValueError:
+ self.strings.append(s)
+
+ return len(self.strings)
+
+ def getClassDefinition(self, ref):
+ try:
+ return self.classes[ref]
+ except IndexError:
+ raise ReferenceError("Class reference %d not found" % ref)
+
+ def getClassDefinitionReference(self, class_def):
+ try:
+ return self.classes.index(class_def)
+ except ValueError:
+ raise ReferenceError("Reference for class %r not found" %
+ class_def)
+
+ def addClassDefinition(self, class_def):
+ """
+ Creates a reference to class_def
+ """
+ try:
+ return self.classes.index(class_def)
+ except ValueError:
+ self.classes.append(class_def)
+
+ return len(self.classes)
+
+ def getClass(self, class_def):
+ if not class_def.name:
+ return Bag
+
+ return load_class(class_def.name)
+
+class Bag(dict):
+ """
+ I supply a thin layer over the __builtin__.dict type to support
+ get/setattr calls
+ """
+
+ def __init__(self, d={}):
+ for k, v in d.items():
+ self[k] = v
+
+ def __setattr__(self, name, value):
+ self[name] = value
+
+ def __getattr__(self, name):
+ return self[name]
+
+def register_class(klass, alias):
+ """
+ Registers a class to be used in the data streaming.
+ """
+ if not callable(klass):
+ raise TypeError("klass must be callable")
+
+ if klass in CLASS_CACHE:
+ raise ValueError("klass %s already registered" % k)
+
+ alias = str(alias)
+
+ if alias in CLASS_CACHE.keys():
+ raise ValueError("alias '%s' already registered" % alias)
+
+ CLASS_CACHE[alias] = klass
+
+def register_class_loader(loader):
+ """
+ Registers a loader that is called to provide the Class for a specific
+ alias. loader is provided with one argument, the class alias. If the loader
+ succeeds in finding a suitable class then it should return that class,
+ otherwise it should return None.
+ """
+ if not callable(loader):
+ raise TypeError("loader must be callable")
+
+ if loader in CLASS_LOADERS:
+ raise ValueError("loader has already been registered")
+
+ CLASS_LOADERS.append(loader)
+
+def get_module(mod_name):
+ """
+ Load a module based on mod_name
+ """
+ mod = __import__(mod_name)
+ components = mod_name.split('.')
+
+ for comp in components[1:]:
+ mod = getattr(mod, comp)
+
+ return mod
+
+def load_class(alias):
+ """
+ Finds the class registered to the alias. Raises LookupError if not found
+
+ The search is done in order:
+ 1. Checks if the class name has been registered via pyamf.register_class
+ 2. Checks all functions registered via register_class_loader
+ 3. Attempts to load the class via standard module loading techniques
+ """
+ alias = str(alias)
+
+ # Try the CLASS_CACHE first
+ try:
+ return CLASS_CACHE[alias]
+ except KeyError:
+ pass
+
+ # Check each CLASS_LOADERS in turn
+ for loader in CLASS_LOADERS:
+ klass = loader(alias)
+
+ if callable(ret):
+ # Cache the result
+ CLASS_CACHE[str(alias)] = klass
+
+ return klass
+
+ # XXX nick: Are there security concerns for loading classes this way?
+ mod_class = alias.split('.')
+ if mod_class:
+ module = '.'.join(mod_class[:-1])
+ klass = mod_class[-1]
+
+ try:
+ module = get_module(module)
+ except ImportError, AttributeError:
+ # XXX What to do here?
+ pass
+ else:
+ klass = getattr(module, klass)
+
+ if callable(klass):
+ CLASS_CACHE[alias] = klass
+
+ return klass
+
+ # All available methods for finding the class have been exhausted
+ raise LookupError("Unknown alias %s" % alias)
+
+def get_class_alias(obj):
+ """
+ Finds the alias registered to the class. Raises LookupError if not found
+
+ See L{load_class} for more info
+ """
+ klass = obj.__class__
+
+ # Try the CLASS_CACHE first
+ for a, k in CLASS_CACHE.iteritems():
+ if klass == k:
+ return a
+
+ # All available methods for finding the alias have been exhausted
+ raise LookupError("Unknown alias for class %s" % klass)
+
+# Register some basic classes
+register_class(Bag, 'flex.messaging.io.ArrayCollection')
+register_class(Bag, 'flex.messaging.io.ObjectProxy')
+
class AMFMessageDecoder:
def __init__(self, data):
Modified: trunk/pyamf/amf0.py
===================================================================
--- trunk/pyamf/amf0.py 2007-10-26 17:15:52 UTC (rev 79)
+++ trunk/pyamf/amf0.py 2007-10-26 17:22:03 UTC (rev 80)
@@ -71,7 +71,6 @@
"""
Parses an AMF0 stream
"""
- obj_refs = []
# XXX nick: Do we need to support ASTypes.MOVIECLIP here?
type_map = {
ASTypes.NUMBER: 'readNumber',
@@ -93,13 +92,18 @@
ASTypes.AMF3: 'readAMF3'
}
- def __init__(self, data=None):
+ def __init__(self, data=None, context=None):
# coersce data to BufferedByteStream
if isinstance(data, util.BufferedByteStream):
self.input = data
else:
self.input = util.BufferedByteStream(data)
+ if context == None:
+ self.context = pyamf.Context()
+ else:
+ self.context = context
+
def readType(self):
"""
Read and returns the next byte in the stream and determine its type.
@@ -147,30 +151,42 @@
def readList(self):
len = self.input.read_ulong()
- obj = []
- self.obj_refs.append(obj)
+ obj = [self.readElement() for i in xrange(len)]
- obj.extend(self.readElement() for i in xrange(len))
-
+ self.context.addObject(obj)
return obj
def readTypedObject(self):
+ """
+ Reads an object from the stream and attempts to 'cast' it. See
+ L{pyamf.load_class} for more info.
+ """
classname = self.readString()
- obj = self.readObject()
+ klass = pyamf.load_class(classname)
- # TODO do some class mapping?
- return obj
+ ret = klass()
+ obj = {}
+ self._readObject(obj)
+ for k, v in obj.iteritems():
+ setattr(ret, k, v)
+
+ self.context.addObject(ret)
+
+ return ret
+
def readAMF3(self):
from pyamf import amf3
# XXX: Does the amf3 parser have access to the same references as amf0?
- p = amf3.Parser(self.input)
+ p = amf3.Parser(self.input, self.context)
return p.readElement()
def readElement(self):
- """Reads the data type."""
+ """
+ Reads an element from the data stream
+ """
type = self.readType()
try:
@@ -182,9 +198,12 @@
return func()
def readString(self):
+ """
+ Reads a string from the data stream
+ """
len = self.input.read_ushort()
return self.input.read_utf8_string(len)
-
+
def _readObject(self, obj):
key = self.readString()
while self.input.peek() != chr(ASTypes.OBJECTTERM):
@@ -202,17 +221,20 @@
@rettype __builtin__.object
"""
obj = {}
- self.obj_refs.append(obj)
self._readObject(obj)
+ self.context.addObject(obj)
+
return obj
def readReference(self):
idx = self.input.read_ushort()
- return self.obj_refs[idx]
+ return self.context.getObject(idx)
def readDate(self):
- """Reads a date.
+ """
+ Reads a date from the data stream
+
Date: 0x0B T7 T6 .. T0 Z1 Z2 T7 to T0 form a 64 bit Big Endian number
that specifies the number of nanoseconds that have passed since
1/1/1970 0:00 to the specified time. This format is UTC 1970. Z1 an
@@ -247,12 +269,16 @@
((types.InstanceType,types.ObjectType,), "writeObject"),
]
- def __init__(self, output):
+ def __init__(self, output, context=None):
"""Constructs a new Encoder. output should be a writable
file-like object."""
self.output = output
- self.obj_refs = []
+ if context == None:
+ self.context = pyamf.Context()
+ else:
+ self.context = context
+
def writeType(self, type):
"""
Writes the type to the stream. Raises ValueError if type is not
@@ -311,7 +337,7 @@
self.output.write(s)
def writeReference(self, o):
- idx = self.obj_refs.index(o)
+ idx = self.context.getObjectReference(o)
self.writeType(ASTypes.REFERENCE)
self.output.write_ushort(idx)
@@ -349,14 +375,26 @@
try:
self.writeReference(o)
return
- except ValueError, e:
+ except pyamf.ReferenceError:
pass
- self.obj_refs.append(o)
- self.writeType(ASTypes.OBJECT)
+ self.context.addObject(o)
+ # Need to check here if this object has a registered alias
+ try:
+ alias = pyamf.get_class_alias(o)
+ self.writeType(ASTypes.TYPEDOBJECT)
+ self.writeString(alias, False)
+ except LookupError:
+ self.writeType(ASTypes.OBJECT)
+
# TODO: give objects a chance of controlling what we send
- for key, val in o.__dict__.items():
+ if 'iteritems' in dir(o):
+ it = o.iteritems()
+ else:
+ it = o.__dict__.iteritems()
+
+ for key, val in it:
self.writeString(key, False)
self.writeElement(val)
Modified: trunk/pyamf/amf3.py
===================================================================
--- trunk/pyamf/amf3.py 2007-10-26 17:15:52 UTC (rev 79)
+++ trunk/pyamf/amf3.py 2007-10-26 17:22:03 UTC (rev 80)
@@ -27,45 +27,52 @@
#
# Resources:
# http://www.vanrijkom.org/archives/2005/06/amf_format.html
-# http://osflash.org/documentation/amf/astypes
+# http://osflash.org/documentation/amf3
-import datetime
+"""AMF3 Implementation"""
-from pyamf.util import BufferedByteStream
+import types, datetime, time, copy
-"""AMF3 Implementation"""
+import pyamf
+from pyamf import util
class ASTypes:
- UNDEFINED = 0x00
- NULL = 0x01
- BOOL_FALSE = 0x02
- BOOL_TRUE = 0x03
- INTEGER = 0x04
- NUMBER = 0x05
- STRING = 0x06
+ UNDEFINED = 0x00
+ NULL = 0x01
+ BOOL_FALSE = 0x02
+ BOOL_TRUE = 0x03
+ INTEGER = 0x04
+ NUMBER = 0x05
+ STRING = 0x06
# TODO: not defined on site, says it's only XML type,
# so we'll assume it is for the time being..
- XML = 0x07
- DATE = 0x08
- ARRAY = 0x09
- OBJECT = 0x0a
- XMLSTRING = 0x0b
- BYTEARRAY = 0x0c
- # Unkown = 0x0d
+ # XXX nick: According to http://osflash.org/documentation/amf3 this
+ # represents the legacy XMLDocument
+ XML = 0x07
+ DATE = 0x08
+ ARRAY = 0x09
+ OBJECT = 0x0a
+ XMLSTRING = 0x0b
+ BYTEARRAY = 0x0c
-class AMF3ObjectTypes:
+ACTIONSCRIPT_TYPES = set(
+ ASTypes.__dict__[x] for x in ASTypes.__dict__ if not x.startswith('__'))
+
+REFERENCE_BIT = 0x01
+
+class ObjectEncoding:
# 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.
- PROPERTY = 0x00
+ 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
# ArrayCollection and ObjectProxy.
- EXTERNALIZABLE = 0x01
+ EXTERNAL = 0x01
# Name-value encoding.
# The property names and values are encoded as string-data
@@ -73,230 +80,620 @@
# 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.
- VALUE = 0x02
+ DYNAMIC = 0x02
# Proxy object
PROXY = 0x03
- # Flex class mappings.
- flex_mappings = [
- # (RemotingMessage, "flex.messaging.messages.RemotingMessage"),
- # (CommandMessage, "flex.messaging.messages.CommandMessage"),
- # (AcknowledgeMessage, "flex.messaging.messages.AcknowledgeMessage"),
- # (ErrorMessage, "flex.messaging.messages.ErrorMessage"),
- # (ArrayCollection, "flex.messaging.io.ArrayCollection"),
- # (ObjectProxy, "flex.messaging.io.ObjectProxy"),
- ]
-
+class ByteArray(str):
+ """
+ I am a file type object containing byte data from the AMF stream
+ """
+
+class ClassDefinition(object):
+ """
+ I contain meta relating to the class definition
+ """
+ attrs = []
+
+ def __init__(self, name, encoding):
+ self.name = name
+ self.encoding = encoding
+
+ def is_external(self):
+ return self.encoding == ObjectEncoding.EXTERNAL
+
+ def is_static(self):
+ return self.encoding == ObjectEncoding.STATIC
+
+ def is_dynamic(self):
+ return self.encoding == ObjectEncoding.DYNAMIC
+
+ external = property(is_external)
+ static = property(is_static)
+ dynamic = property(is_dynamic)
+
class Parser(object):
+ """
+ Parses an AMF3 data stream
+ """
- def __init__(self, data):
- self.obj_refs = list()
- self.str_refs = list()
- self.class_refs = list()
- if isinstance(data, BufferedByteStream):
+ type_map = {
+ ASTypes.UNDEFINED: 'readNull',
+ ASTypes.NULL: 'readNull',
+ ASTypes.BOOL_FALSE: 'readBoolFalse',
+ ASTypes.BOOL_TRUE: 'readBoolTrue',
+ ASTypes.INTEGER: 'readInteger',
+ ASTypes.NUMBER: 'readNumber',
+ ASTypes.STRING: 'readString',
+ ASTypes.XML: 'readXML',
+ ASTypes.DATE: 'readDate',
+ ASTypes.ARRAY: 'readArray',
+ ASTypes.OBJECT: 'readObject',
+ ASTypes.XMLSTRING: 'readString',
+ ASTypes.BYTEARRAY: 'readByteArray',
+ }
+
+ def __init__(self, data=None, context=None):
+ if isinstance(data, util.BufferedByteStream):
self.input = data
else:
- self.input = BufferedByteStream(data)
+ self.input = util.BufferedByteStream(data)
+ if context == None:
+ context = pyamf.Context()
+
+ self.context = context
+
+ def readType(self):
+ """
+ Read and returns the next byte in the stream and determine its type.
+ Raises ValueError if not recognized
+ """
+ type = self.input.read_uchar()
+
+ if type not in ACTIONSCRIPT_TYPES:
+ raise pyamf.ParseError("Unknown AMF3 type 0x%02x at %d" % (
+ type, self.input.tell() - 1))
+
+ return type
+
+ def readNull(self):
+ return None
+
+ def readBoolFalse(self):
+ return False
+
+ def readBoolTrue(self):
+ return True
+
+ def readNumber(self):
+ return self.input.read_double()
+
def readElement(self):
- type = self.input.read_uchar()
- if type == ASTypes.UNDEFINED:
- return None
-
- if type == ASTypes.NULL:
- return None
-
- if type == ASTypes.BOOL_FALSE:
- return False
-
- if type == ASTypes.BOOL_TRUE:
- return True
-
- if type == ASTypes.INTEGER:
- return self.readInteger()
-
- if type == ASTypes.NUMBER:
- return self.input.read_double()
-
- if type == ASTypes.STRING:
- return self.readString()
-
- if type == ASTypes.XML:
- return self.readXML()
-
- if type == ASTypes.DATE:
- return self.readDate()
-
- if type == ASTypes.ARRAY:
- return self.readArray()
-
- if type == ASTypes.OBJECT:
- return self.readObject()
-
- if type == ASTypes.XMLSTRING:
- return self.readString(use_references=False)
-
- if type == ASTypes.BYTEARRAY:
- raise self.readByteArray()
-
- else:
- raise ValueError("Unknown AMF3 datatype 0x%02x at %d" % (type, \
self.input.tell()-1))
-
+ """Reads the data type."""
+ type = self.readType()
+
+ try:
+ func = getattr(self, self.type_map[type])
+ except KeyError, e:
+ raise NotImplementedError(
+ "Unsupported ActionScript type 0x%02x" % type)
+
+ return func()
+
def readInteger(self):
- # see http://osflash.org/amf3/parsing_integers for AMF3 integer data format
+ """
+ Reads and returns an integer from the stream
+ See http://osflash.org/amf3/parsing_integers for AMF3 integer data
+ format
+ """
n = 0
b = self.input.read_uchar()
result = 0
-
+
while b & 0x80 and n < 3:
result <<= 7
result |= b & 0x7f
b = self.input.read_uchar()
n += 1
+
if n < 3:
result <<= 7
result |= b
else:
result <<= 8
result |= b
- if result & 0x10000000:
- result |= 0xe0000000
-
- # return a converted integer value
+
+ if result & 0x10000000:
+ result |= 0xe0000000
+
return result
-
+
def readString(self, use_references=True):
- length = self.readInteger()
- if use_references and length & 0x01 == 0:
- return self.str_refs[length >> 1]
-
- length >>= 1
+ """
+ Reads and returns a string from the stream.
+ """
+ def readLength():
+ x = self.readInteger()
+
+ return (x >> 1, x & REFERENCE_BIT == 0)
+
+ length, is_reference = readLength()
+
+ if use_references and is_reference:
+ return self.context.getString(length)
+
buf = self.input.read(length)
+
try:
# Try decoding as regular utf8 first since that will
# cover most cases and is more efficient.
- # XXX: I'm not sure if it's ok though.. will it always raise exception?
+ # XXX: I'm not sure if it's ok though..
+ # will it always raise exception?
result = unicode(buf, "utf8")
except UnicodeDecodeError:
- result = util.decode_utf8_modified(buf)
-
- if use_references and len(result) != 0:
- self.str_refs.append(result)
-
+ result = decode_utf8_modified(buf)
+
+ if len(result) != 0 and use_references:
+ self.context.addString(result)
+
return result
-
+
def readXML(self):
- data = self.readString(False)
- return ET.fromstring(data)
-
+ return util.ET.fromstring(self.readString(False))
+
def readDate(self):
ref = self.readInteger()
- if ref & 0x01 == 0:
- return self.obj_refs[ref >> 1]
+
+ if ref & REFERENCE_BIT == 0:
+ return self.context.getObject(ref >> 1)
+
ms = self.input.read_double()
- result = datetime.datetime.fromtimestamp(ms/1000.0)
- self.obj_refs.append(result)
+ result = datetime.datetime.fromtimestamp(ms / 100)
+
+ self.context.addObject(result)
+
return result
-
+
def readArray(self):
+ """
+ Reads an array from the stream.
+
+ There is a very specific problem with AMF3 where the first three bytes
+ of an encoded empty dict will mirror that of an encoded {'': 1, '2': 2}
+
+ See http://www.docuverse.com/blog/donpark/2007/05/14/flash-9-amf3-bug
+ for more information.
+ """
+ if self.input.peek(2) == '\x01\x01':
+ raise pyamf.ParseError("empty dict bug encountered")
+
size = self.readInteger()
- if size & 0x01 == 0:
- return self.obj_refs[size >> 1]
+
+ if size & REFERENCE_BIT == 0:
+ return self.context.getObject(size >> 1)
+
size >>= 1
+
key = self.readString()
+
if key == "":
# integer indexes only -> python list
- result = []
- self.obj_refs.append(result)
- for i in xrange(size):
- el = self.readElement()
- result.append(el)
+ result = [self.readElement() for i in xrange(size)]
+
else:
# key,value pairs -> python dict
result = {}
- self.obj_refs.append(result)
+
while key != "":
el = self.readElement()
- result[key] = el
+
+ try:
+ result[str(key)] = el
+ except UnicodeError:
+ result[key] = el
+
key = self.readString()
+
for i in xrange(size):
el = self.readElement()
result[i] = el
+
+ self.context.addObject(result)
+
+ return result
+
+ def _getClassDefinition(self, ref):
+ class_ref = ref & REFERENCE_BIT == 0
- return result
-
- def readObject(self):
- type = self.readInteger()
- if type & 0x01 == 0:
- return self.obj_refs[type >> 1]
- class_ref = (type >> 1) & 0x01 == 0
- type >>= 2
+ ref >>= 1
+
if class_ref:
- class_ = self.class_refs[type]
+ class_def = self.context.getClassDefinition(ref)
else:
- class_ = AMF3Class()
- class_.name = self.readString()
- class_.encoding = type & 0x03
- class_.attrs = []
-
- type >>= 2
- if class_.name:
- # TODO : do some class mapping?
- obj = AMF3Object(class_)
- else:
- obj = AMF3Object()
-
- self.obj_refs.append(obj)
-
- if class_.encoding & AMF3ObjectTypes.EXTERNALIZABLE:
- if not class_ref:
- self.class_refs.append(class_)
+ class_def = ClassDefinition(self.readString(), ref & 0x03)
+ self.context.addClassDefinition(class_def)
+
+ return class_ref, class_def
+
+ def readObject(self):
+ """
+ Reads an object from the stream.
+ """
+ ref = self.readInteger()
+
+ if ref & REFERENCE_BIT == 0:
+ return self.context.getObject(ref >> 1)
+
+ ref >>= 1
+ (class_ref, class_def) = self._getClassDefinition(ref)
+ ref >>= 3
+
+ klass = self.context.getClass(class_def)
+ obj = klass()
+
+ if class_def.external:
# TODO: implement externalizeable interface here
obj.__amf_externalized_data = self.readElement()
-
+
+ elif class_def.dynamic:
+ attr = self.readString()
+
+ while attr != "":
+ if attr not in class_def.attrs:
+ class_def.attrs.append(attr)
+
+ obj[attr] = self.readElement()
+ attr = self.readString()
+
+ elif class_def.static:
+ if not class_ref:
+ class_def.attrs = [self.readString() for i in range(ref)]
+
+ for attr in class_def.attrs:
+ setattr(obj, attr, self.readElement())
else:
- if class_.encoding & AMF3ObjectTypes.VALUE:
- if not class_ref:
- self.class_refs.append(class_)
- attr = self.readString()
- while attr != "":
- class_.attrs.append(attr)
- setattr(obj, attr, self.readElement())
- attr = self.readString()
- else:
- if not class_ref:
- for i in range(type):
- class_.attrs.append(self.readString())
- self.class_refs.append(class_)
- for attr in class_.attrs:
- setattr(obj, attr, self.readElement())
-
+ raise pyamf.ParseError("Unknown object encoding")
+
+ self.context.addObject(obj)
+
return obj
def readByteArray(self):
+ """
+ Reads a string of data from the stream.
+ """
length = self.readInteger()
- return self.input.read(length >> 1)
-class AMF3Class:
-
- def __init__(self, name=None, encoding=None, attrs=None):
- self.name = name
- self.encoding = encoding
- self.attrs = attrs
+ return ByteArray(self.input.read(length >> 1))
+
+class Encoder(object):
+
+ type_map = [
+ # Unsupported types go first
+ ((types.BuiltinFunctionType, types.BuiltinMethodType,), "writeUnsupported"),
+ ((bool,), "writeBoolean"),
+ ((int,long), "writeInteger"),
+ ((float,), "writeNumber"),
+ ((ByteArray,), "writeByteArray"),
+ ((types.StringTypes,), "writeString"),
+ ((util.ET._ElementInterface,), "writeXML"),
+ ((types.DictType,), "writeDict"),
+ ((types.ListType,types.TupleType,), "writeList"),
+ ((datetime.date, datetime.datetime), "writeDate"),
+ ((types.NoneType,), "writeNull"),
+ ((types.InstanceType,types.ObjectType,), "writeObject"),
+ ]
+
+ def __init__(self, output, context=None):
+ """Constructs a new Encoder. output should be a writable
+ file-like object."""
+ self.output = output
+
+ if context == None:
+ context = pyamf.Context()
+
+ self.context = context
+
+ def writeType(self, type):
+ """
+ Writes the type to the stream. Raises ValueError if type is not
+ recognized
+ """
+ if type not in ACTIONSCRIPT_TYPES:
+ raise ValueError("Unknown AMF0 type 0x%02x at %d" % (
+ type, self.output.tell() - 1))
+
+ self.output.write_uchar(type)
+
+ def writeElement(self, data):
+ """
+ Writes an encoded version of data to the output stream
+ """
+ for tlist, method in self.type_map:
+ for t in tlist:
+ if isinstance(data, t):
+ try:
+ return getattr(self, method)(data)
+ except AttributeError:
+ # Should NotImplementedError be raised here?
+ raise
+
+ def writeNull(self, n):
+ self.writeType(ASTypes.NULL)
+
+ def writeBoolean(self, n):
+ if n:
+ self.writeType(ASTypes.BOOL_TRUE)
+ else:
+ self.writeType(ASTypes.BOOL_FALSE)
+
+ def _writeInteger(self, n):
+ """
+ AMF Integers are encoded.
-class AMF3Object:
-
- def __init__(self, class_=None):
- self.__amf_class = class_
-
- def __repr__(self):
- return "<AMF3Object [%s] at 0x%08X>" % (
- self.__amf_class and self.__amf_class.name or "no class",
- id(self))
+ See http://osflash.org/documentation/amf3/parsing_integers for more
+ info.
+ """
+ bytes = []
-class AbstractMessage:
-
+ if n & 0xff000000 == 0:
+ for i in xrange(3, -1, -1):
+ bytes.append((n >> (7 * i)) & 0x7F)
+ else:
+ for i in xrange(2, -1, -1):
+ bytes.append(n >> (8 + 7 * i) & 0x7F)
+
+ bytes.append(n & 0xFF)
+
+ for x in bytes[:-1]:
+ if x > 0:
+ self.output.write_uchar(x | 0x80)
+
+ self.output.write_uchar(bytes[-1])
+
+ def writeInteger(self, n):
+ """
+ Writes an integer to the data stream
+ """
+ self.writeType(ASTypes.INTEGER)
+ self._writeInteger(n)
+
+ def writeNumber(self, n):
+ """
+ Writes a non integer to the data stream
+ """
+ self.writeType(ASTypes.NUMBER)
+ self.output.write_double(n)
+
+ def _writeString(self, n):
+ """
+ Writes a raw string to the stream.
+ """
+ if len(n) == 0:
+ self._writeInteger(REFERENCE_BIT)
+
+ return
+
+ try:
+ ref = self.context.getStringReference(n)
+ self._writeInteger(ref << 1)
+
+ return
+ except pyamf.ReferenceError:
+ self.context.addString(n)
+
+ s = encode_utf8_modified(n)[2:]
+ self._writeInteger((len(s) << 1) | REFERENCE_BIT)
+
+ for ch in s:
+ self.output.write_uchar(ord(ch))
+
+ def writeString(self, n):
+ """
+ Writes a unicode string to the stream.
+ """
+ self.writeType(ASTypes.STRING)
+ self._writeString(n)
+
+ def writeDate(self, n):
+ """
+ Writes a datetime instance to the stream.
+ """
+ if isinstance(n, datetime.date):
+ n = datetime.datetime.combine(n, datetime.time(0))
+
+ self.writeType(ASTypes.DATE)
+
+ try:
+ ref = self.context.getObjectReference(n)
+ self._writeInteger(ref << 1)
+
+ return
+ except pyamf.ReferenceError:
+ pass
+
+ self.context.addObject(n)
+ self._writeInteger(REFERENCE_BIT)
+
+ ms = time.mktime(n.timetuple())
+ self.output.write_double(ms * 100.0)
+
+ def writeList(self, n):
+ """
+ Writes a list to the stream.
+ """
+ self.writeType(ASTypes.ARRAY)
+
+ try:
+ ref = self.context.getObjectReference(n)
+ self._writeInteger(ref << 1)
+
+ return
+ except pyamf.ReferenceError:
+ pass
+
+ self.context.addObject(n)
+ self._writeInteger(len(n) << 1 | REFERENCE_BIT)
+
+ self.output.write_uchar(0x01)
+ for x in n:
+ self.writeElement(x)
+
+ def writeDict(self, n):
+ """
+ Writes a dict to the stream.
+ """
+ self.writeType(ASTypes.ARRAY)
+
+ try:
+ ref = self.context.getObjectReference(n)
+ self._writeInteger(ref << 1)
+
+ return
+ except pyamf.ReferenceError:
+ pass
+
+ 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, (int, long)):
+ int_keys.append(x)
+ elif isinstance(x, (str, unicode)):
+ 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:
+ # Design bug in AMF3 that cannot read/write empty key strings
+ # http://www.docuverse.com/blog/donpark/2007/05/14/flash-9-amf3-bug
+ # for more info
+ if x == '':
+ raise pyamf.EncodeError(
+ "dicts cannot contain empty string keys")
+
+ self._writeString(x)
+ self.writeElement(n[x])
+
+ self.output.write_uchar(0x01)
+
+ for k in int_keys:
+ self.writeElement(n[k])
+
+ def _getClassDefinition(self, obj):
+ try:
+ alias = pyamf.get_class_alias(obj)
+ except LookupError:
+ alias = '%s.%s' % (obj.__module__, obj.__class__.__name__)
+
+ class_def = ClassDefinition(alias, ObjectEncoding.STATIC)
+
+ for name in obj.__dict__.keys():
+ class_def.attrs.append(name)
+
+ return class_def
+
+ def writeObject(self, obj):
+ """
+ Writes an object to the stream.
+ """
+ self.writeType(ASTypes.OBJECT)
+ try:
+ ref = self.context.getObjectReference(obj)
+ self._writeInteger(ref << 1)
+
+ return
+ except pyamf.ReferenceError:
+ pass
+
+ self.context.addObject(obj)
+
+ try:
+ ref = self.context.getClassDefinitionReference(obj)
+ class_ref = True
+
+ self._writeInteger(ref << 2 | REFERENCE_BIT)
+ except pyamf.ReferenceError:
+ class_def = self._getClassDefinition(obj)
+ class_ref = False
+
+ ref = 0
+
+ if class_def.encoding != ObjectEncoding.EXTERNAL:
+ ref += len(class_def.attrs) << 4
+
+ self._writeInteger(ref | class_def.encoding << 2 |
+ REFERENCE_BIT << 1 | REFERENCE_BIT)
+ self._writeString(class_def.name)
+
+ if class_def.encoding == ObjectEncoding.EXTERNAL:
+ # TODO
+ pass
+ elif class_def.encoding == ObjectEncoding.DYNAMIC:
+ if not class_ref:
+ for attr in class_def.attrs:
+ self._writeString(attr)
+ self.writeElement(getattr(obj, attr))
+
+ self.writeString("")
+ else:
+ for attr in class_def.attrs:
+ self.writeElement(getattr(obj, attr))
+ elif class_def.encoding == ObjectEncoding.STATIC:
+ if not class_ref:
+ for attr in class_def.attrs:
+ self._writeString(attr)
+
+ for attr in class_def.attrs:
+ self.writeElement(getattr(obj, attr))
+
+ def writeByteArray(self, n):
+ """
+ Writes a L{ByteArray} to the data stream.
+ """
+ self.writeType(ASTypes.BYTEARRAY)
+
+ try:
+ ref = self.context.getObjectReference(n)
+ self._writeInteger(ref << 1)
+
+ return
+ except pyamf.ReferenceError:
+ pass
+
+ self.context.addObject(n)
+ self._writeInteger(len(n) << 1 | REFERENCE_BIT)
+
+ for ch in n:
+ self.output.write_uchar(ord(ch))
+
+class AbstractMessage(object):
+
def __init__(self):
# The body of the message.
self.data = None
@@ -312,12 +709,12 @@
self.timeToLive = None
# timestamp
self.timestamp = None
-
+
def __repr__(self):
return "<AbstractMessage clientId=%s data=%r>" % (self.clientId, self.data)
class AcknowledgeMessage(AbstractMessage):
-
+
def __init__(self):
"""
This is the receipt for any message thats being sent.
@@ -325,12 +722,12 @@
AbstractMessage.__init__(self)
# The ID of the message where this is a receipt of.
self.correlationId = None
-
+
def __repr__(self):
return "<AcknowledgeMessage correlationId=%s>" % (self.correlationId)
class CommandMessage(AbstractMessage):
-
+
def __init__(self):
"""
This class is used for service commands, like pinging the server.
@@ -344,9 +741,9 @@
def __repr__(self):
return "<CommandMessage correlationId=%s operation=%r messageRefType=%d>" % \
( self.correlationId, self.operation, self.messageRefType)
-
+
class ErrorMessage(AbstractMessage):
-
+
def __init__(self):
"""
This is the receipt for Error Messages.
@@ -363,18 +760,83 @@
self.faultString = None
# Should a root cause exist for the error, this property contains those \
details. self.rootCause = {}
-
+
def __repr__(self):
return "<ErrorMessage faultCode=%s faultString=%r>" % (
self.faultCode, self.faultString)
-
+
class RemotingMessage(AbstractMessage):
-
+
def __init__(self):
AbstractMessage.__init__(self)
self.operation = None
self.source = None
-
+
def __repr__(self):
return "<RemotingMessage operation=%s source=%r>" % (self.operation, \
self.source)
+def encode_utf8_modified(data):
+ """
+ Encodes a unicode string to Modified UTF-8 data.
+ See http://en.wikipedia.org/wiki/UTF-8#Java for details.
+ """
+ if not isinstance(data, unicode):
+ data = unicode(data, "utf8")
+
+ bytes = ''
+ charr = data.encode("utf_16_be")
+ utflen = 0
+ i = 0
+
+ for i in xrange(0, len(charr), 2):
+ ch = ord(charr[i]) << 8 | ord(charr[i+1])
+
+ if 0x00 < ch < 0x80:
+ utflen += 1
+ bytes += chr(ch)
+ elif ch < 0x800:
+ utflen += 2
+ bytes += chr(0xc0 | ((ch >> 6) & 0x1f))
+ bytes += chr(0x80 | ((ch >> 0) & 0x3f))
+ else:
+ utflen += 3
+ bytes += chr(0xe0 | ((ch >> 12) & 0x0f))
+ bytes += chr(0x80 | ((ch >> 6) & 0x3f))
+ bytes += chr(0x80 | ((ch >> 0) & 0x3f))
+
+ return chr((utflen >> 8) & 0xff) + chr((utflen >> 0) & 0xff) + bytes
+
+# Ported from http://viewvc.rubyforge.mmmultiworks.com/cgi/viewvc.cgi/trunk/lib/ruva/class.rb
+# Ruby version is Copyright (c) 2006 Ross Bamford (rosco AT roscopeco DOT co DOT \
uk). +def decode_utf8_modified(data):
+ """
+ Decodes a unicode string from Modified UTF-8 data.
+ See http://en.wikipedia.org/wiki/UTF-8#Java for details.
+ """
+ size = ((ord(data[0]) << 8) & 0xff) + ((ord(data[1]) << 0) & 0xff)
+ data = data[2:]
+ utf16 = []
+ i = 0
+
+ while i < len(data):
+ ch = ord(data[i])
+ c = ch >> 4
+
+ if 0 <= c <= 7:
+ utf16.append(ch)
+ i += 1
+ elif 12 <= c <= 13:
+ utf16.append(((ch & 0x1f) << 6) | (ord(data[i+1]) & 0x3f))
+ i += 2
+ elif c == 14:
+ utf16.append(
+ ((ch & 0x0f) << 12) |
+ ((ord(data[i+1]) & 0x3f) << 6) |
+ (ord(data[i+2]) & 0x3f))
+ i += 3
+ else:
+ raise ValueError("Data is not valid modified UTF-8")
+
+ utf16 = "".join([chr((c >> 8) & 0xff) + chr(c & 0xff) for c in utf16])
+
+ return unicode(utf16, "utf_16_be")
Modified: trunk/pyamf/tests/__init__.py
===================================================================
--- trunk/pyamf/tests/__init__.py 2007-10-26 17:15:52 UTC (rev 79)
+++ trunk/pyamf/tests/__init__.py 2007-10-26 17:22:03 UTC (rev 80)
@@ -30,10 +30,11 @@
def suite():
import pyamf
- from pyamf.tests import amf0
+ from pyamf.tests import amf0, amf3
suite = unittest.TestSuite()
suite.addTest(amf0.suite())
+ suite.addTest(amf3.suite())
return suite
Modified: trunk/pyamf/tests/amf0.py
===================================================================
--- trunk/pyamf/tests/amf0.py 2007-10-26 17:15:52 UTC (rev 79)
+++ trunk/pyamf/tests/amf0.py 2007-10-26 17:22:03 UTC (rev 80)
@@ -30,6 +30,7 @@
import pyamf
from pyamf import amf0, util
+from pyamf.tests.util import GenericObject, EncoderTester, ParserTester
class TypesTestCase(unittest.TestCase):
def test_types(self):
@@ -52,36 +53,6 @@
self.assertEquals(amf0.ASTypes.TYPEDOBJECT, 0x10)
self.assertEquals(amf0.ASTypes.AMF3, 0x11)
-class GenericObject(object):
- def __init__(self, dict):
- self.__dict__ = dict
-
- def __cmp__(self, other):
- return cmp(self.__dict__, other)
-
-class EncoderTester(object):
- """
- A helper object that takes some input, runs over the encoder and checks
- the output
- """
-
- def __init__(self, encoder, data):
- self.encoder = encoder
- self.buf = encoder.output
- self.data = data
-
- def getval(self):
- t = self.buf.getvalue()
- self.buf.truncate(0)
-
- return t
-
- def run(self, testcase):
- for n, s in self.data:
- self.encoder.writeElement(n)
-
- testcase.assertEqual(self.getval(), s)
-
class EncoderTestCase(unittest.TestCase):
"""
Tests the output from the Encoder class.
@@ -177,31 +148,23 @@
(GenericObject({'a': 'b'}),
'\x03\x00\x01a\x02\x00\x01b\x00\x00\x09')])
-class ParserTester(object):
- """
- A helper object that takes some input, runs over the parser and checks
- the output
- """
+ def test_typed_object(self):
+ class Foo(object):
+ pass
- def __init__(self, parser, data):
- self.parser = parser
- self.buf = parser.input
- self.data = data
+ pyamf.CLASS_CACHE = {}
+ pyamf.register_class(Foo, alias='com.collab.dev.pyamf.foo')
- def getval(self):
- t = self.buf.getvalue()
- self.buf.truncate(0)
+ x = Foo()
+ x.baz = 'hello'
- return t
+ self.e.writeElement(x)
+
+ self.assertEquals(self.buf.getvalue(),
+ '\x10\x00\x18\x63\x6f\x6d\x2e\x63\x6f\x6c\x6c\x61\x62\x2e\x64\x65'
+ '\x76\x2e\x70\x79\x61\x6d\x66\x2e\x66\x6f\x6f\x00\x03\x62\x61\x7a'
+ '\x02\x00\x05\x68\x65\x6c\x6c\x6f\x00\x00\x09')
- def run(self, testcase):
- for n, s in self.data:
- self.buf.truncate(0)
- self.buf.write(s)
- self.buf.seek(0)
-
- testcase.assertEqual(self.parser.readElement(), n)
-
class ParserTestCase(unittest.TestCase):
def setUp(self):
self.buf = util.BufferedByteStream()
@@ -289,6 +252,25 @@
(GenericObject({'a': 'b'}),
'\x03\x00\x01a\x02\x00\x01b\x00\x00\x09')])
+ def test_registered_class(self):
+ class Foo(object):
+ pass
+
+ pyamf.CLASS_CACHE = {}
+ pyamf.register_class(Foo, alias='com.collab.dev.pyamf.foo')
+
+ self.buf.write('\x10\x00\x18\x63\x6f\x6d\x2e\x63\x6f\x6c\x6c\x61\x62'
+ '\x2e\x64\x65\x76\x2e\x70\x79\x61\x6d\x66\x2e\x66\x6f\x6f\x00\x03'
+ '\x62\x61\x7a\x02\x00\x05\x68\x65\x6c\x6c\x6f\x00\x00\x09')
+ self.buf.seek(0)
+
+ obj = self.parser.readElement()
+
+ self.assertEquals(obj.__class__, Foo)
+
+ self.failUnless(hasattr(obj, 'baz'))
+ self.assertEquals(obj.baz, 'hello')
+
def suite():
suite = unittest.TestSuite()
Copied: trunk/pyamf/tests/amf3.py (from rev 79, branches/amf3-5/pyamf/tests/amf3.py)
===================================================================
--- trunk/pyamf/tests/amf3.py (rev 0)
+++ trunk/pyamf/tests/amf3.py 2007-10-26 17:22:03 UTC (rev 80)
@@ -0,0 +1,385 @@
+# -*- encoding: utf-8 -*-
+#
+# Copyright (c) 2007 The PyAMF Project. All rights reserved.
+#
+# Arnar Birgisson
+# Thijs Triemstra
+# Nick Joyce
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+import unittest
+
+import pyamf
+from pyamf import amf3, util
+from pyamf.tests.util import GenericObject, EncoderTester, ParserTester
+
+class TypesTestCase(unittest.TestCase):
+ def test_types(self):
+ self.assertEquals(amf3.ASTypes.UNDEFINED, 0x00)
+ self.assertEquals(amf3.ASTypes.NULL, 0x01)
+ self.assertEquals(amf3.ASTypes.BOOL_FALSE, 0x02)
+ self.assertEquals(amf3.ASTypes.BOOL_TRUE, 0x03)
+ self.assertEquals(amf3.ASTypes.INTEGER, 0x04)
+ self.assertEquals(amf3.ASTypes.NUMBER, 0x05)
+ self.assertEquals(amf3.ASTypes.STRING, 0x06)
+ self.assertEquals(amf3.ASTypes.XML, 0x07)
+ self.assertEquals(amf3.ASTypes.DATE, 0x08)
+ self.assertEquals(amf3.ASTypes.ARRAY, 0x09)
+ self.assertEquals(amf3.ASTypes.OBJECT, 0x0a)
+ self.assertEquals(amf3.ASTypes.XMLSTRING, 0x0b)
+ self.assertEquals(amf3.ASTypes.BYTEARRAY, 0x0c)
+
+class EncoderTestCase(unittest.TestCase):
+ def setUp(self):
+ self.buf = util.BufferedByteStream()
+ self.context = pyamf.Context()
+ self.e = amf3.Encoder(self.buf, context=self.context)
+
+ def _run(self, data):
+ self.context.clear()
+
+ e = EncoderTester(self.e, data)
+ e.run(self)
+
+ def test_undefined(self):
+ def x():
+ self._run([(ord, '\x00')])
+
+ self.assertRaises(AttributeError, x)
+
+ def test_null(self):
+ self._run([(None, '\x01')])
+
+ def test_boolean(self):
+ self._run([(True, '\x03'), (False, '\x02')])
+
+ def test_integer(self):
+ self._run([
+ (0, '\x04\x00'),
+ (94L, '\x04\x5e'),
+ (-3422345L, '\x04\xff\x97\xc7\x77')])
+
+ def test_number(self):
+ self._run([
+ (0.1, '\x05\x3f\xb9\x99\x99\x99\x99\x99\x9a'),
+ (0.123456789, '\x05\x3f\xbf\x9a\xdd\x37\x39\x63\x5f')])
+
+ def test_string(self):
+ self._run([
+ ('hello', '\x06\x0bhello'),
+ (u'??????????????????', \
'\x06\x13\xe1\x9a\xa0\xe1\x9b\x87\xe1\x9a\xbb')]) +
+ def test_string_references(self):
+ self._run([
+ ('hello', '\x06\x0bhello'),
+ ('hello', '\x06\x00'),
+ ('hello', '\x06\x00')])
+
+ def test_date(self):
+ import datetime, time
+
+ self._run([
+ (datetime.datetime(1999, 9, 9, 3, 4, 43),
+ '\x08\x01B5\xcf\xf3\x93\xc0\x00\x00')])
+
+ def test_date_references(self):
+ import datetime, time
+
+ self.e.obj_refs = []
+
+ x = datetime.datetime(1999, 9, 9, 3, 4, 43)
+
+ self._run([
+ (x, '\x08\x01B5\xcf\xf3\x93\xc0\x00\x00'),
+ (x, '\x08\x00'),
+ (x, '\x08\x00')])
+
+ def test_list(self):
+ self._run([
+ ([0, 1, 2, 3], '\x09\x09\x01\x04\x00\x04\x01\x04\x02\x04\x03')])
+
+ def test_list_references(self):
+ y = [0, 1, 2, 3]
+
+ self._run([
+ (y, '\x09\x09\x01\x04\x00\x04\x01\x04\x02\x04\x03'),
+ (y, '\x09\x00'),
+ (y, '\x09\x00')])
+
+ def test_dict(self):
+ self._run([
+ ({0: u'hello', 'foo': u'bar'},
+ '\x09\x03\x07\x66\x6f\x6f\x06\x07\x62\x61\x72\x01\x06\x0b\x68\x65'
+ '\x6c\x6c\x6f')])
+ self._run([({0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 'a': 'a'},
+ '\x09\x0d\x03\x61\x06\x00\x01\x04\x00\x04\x01\x04\x02\x04\x03\x04'
+ '\x04\x04\x05')])
+
+ x = amf3.Parser('\x09\x09\x03\x62\x06\x00\x03\x64\x06\x02\x03\x61\x06'
+ '\x04\x03\x63\x06\x06\x01\x04\x00\x04\x01\x04\x02\x04\x03')
+
+ self.assertEqual(
+ x.readElement(),
+ {'a': u'a', 'b': u'b', 'c': u'c', 'd': u'd',
+ 0: 0, 1: 1, 2: 2, 3: 3})
+
+ def test_empty_key_string(self):
+ """
+ Test to see if there is an empty key in the dict. There is a design
+ bug in Flash 9 which means that it cannot read this specific data.
+
+ See http://www.docuverse.com/blog/donpark/2007/05/14/flash-9-amf3-bug
+ for more info.
+ """
+ def x():
+ self._run([({'': 1, 0: 1}, '\x09\x03\x01\x04\x01\x01\x04\x01')])
+
+ self.failUnlessRaises(pyamf.EncodeError, x)
+
+ def test_object(self):
+ class Foo(object):
+ pass
+
+ pyamf.CLASS_CACHE = {}
+
+ pyamf.register_class(Foo, 'com.collab.dev.pyamf.foo')
+
+ obj = Foo()
+ obj.baz = 'hello'
+
+ self.e.writeElement(obj)
+
+ self.assertEqual(self.buf.getvalue(), \
'\x0a\x13\x31\x63\x6f\x6d\x2e\x63\x6f\x6c\x6c\x61\x62' + \
'\x2e\x64\x65\x76\x2e\x70\x79\x61\x6d\x66\x2e\x66\x6f\x6f\x07\x62' + \
'\x61\x7a\x06\x0b\x68\x65\x6c\x6c\x6f') +
+ def test_byte_array(self):
+ self._run([(amf3.ByteArray('hello'), '\x0c\x0bhello')])
+
+class ParserTestCase(unittest.TestCase):
+ def setUp(self):
+ self.buf = util.BufferedByteStream()
+ self.context = pyamf.Context()
+ self.parser = amf3.Parser(context=self.context)
+ self.parser.input = self.buf
+
+ def _run(self, data):
+ self.context.clear()
+ e = ParserTester(self.parser, data)
+ e.run(self)
+
+ def test_types(self):
+ for x in amf3.ACTIONSCRIPT_TYPES:
+ self.buf.write(chr(x))
+ self.buf.seek(0)
+ self.parser.readType()
+ self.buf.truncate(0)
+
+ self.buf.write('x')
+ self.buf.seek(0)
+ self.assertRaises(pyamf.ParseError, self.parser.readType)
+
+ def test_number(self):
+ self._run([
+ (0, '\x04\x00'),
+ (0.2, '\x05\x3f\xc9\x99\x99\x99\x99\x99\x9a'),
+ (1, '\x04\x01'),
+ (42, '\x04\x2a'),
+ (-123, '\x05\xc0\x5e\xc0\x00\x00\x00\x00\x00'),
+ (1.23456789, '\x05\x3f\xf3\xc0\xca\x42\x83\xde\x1b')])
+
+ def test_boolean(self):
+ self._run([(True, '\x03'), (False, '\x02')])
+
+ def test_null(self):
+ self._run([(None, '\x01')])
+
+ def test_undefined(self):
+ self._run([(None, '\x00')])
+
+ def test_string(self):
+ self._run([
+ ('', '\x06\x01'),
+ ('hello', '\x06\x0bhello'),
+ (u'?????????????????????????????????????????? \
????????????????????????????????????????????????, ???????????????????????? \
???????????????????????? ???????????????????????????????????????????????? \
?????????????????????????????????????????? \
??????????????????????????????????????????, ????????????????????????????????????', + \
'\x06\x82\x45\xe1\x83\xa6\xe1\x83\x9b\xe1\x83\x94\xe1\x83\xa0' + \
'\xe1\x83\x97\xe1\x83\xa1\xe1\x83\x98\x20\xe1\x83\xa8\xe1\x83' + \
'\x94\xe1\x83\x9b\xe1\x83\x95\xe1\x83\x94\xe1\x83\x93\xe1\x83' + \
'\xa0\xe1\x83\x94\x2c\x20\xe1\x83\x9c\xe1\x83\xa3\xe1\x83\x97' + \
'\xe1\x83\xa3\x20\xe1\x83\x99\xe1\x83\x95\xe1\x83\x9a\xe1\x83' + \
'\x90\x20\xe1\x83\x93\xe1\x83\x90\xe1\x83\x9b\xe1\x83\xae\xe1' + \
'\x83\xa1\xe1\x83\x9c\xe1\x83\x90\xe1\x83\xa1\x20\xe1\x83\xa1' + \
'\xe1\x83\x9d\xe1\x83\xa4\xe1\x83\x9a\xe1\x83\x98\xe1\x83\xa1' + \
'\xe1\x83\x90\x20\xe1\x83\xa8\xe1\x83\xa0\xe1\x83\x9d\xe1\x83' + \
'\x9b\xe1\x83\x90\xe1\x83\xa1\xe1\x83\x90\x2c\x20\xe1\x83\xaa' + \
'\xe1\x83\x94\xe1\x83\xaa\xe1\x83\xae\xe1\x83\x9a\xe1\x83\xa1' + )])
+
+ def test_string_references(self):
+ self.parser.str_refs = []
+
+ self._run([
+ ('hello', '\x06\x0bhello'),
+ ('hello', '\x06\x00'),
+ ('hello', '\x06\x00')])
+
+ def test_xml(self):
+ self.buf.truncate(0)
+ self.buf.write('\x07\x33<a><b>hello world</b></a>')
+ self.buf.seek(0)
+
+ self.assertEquals(
+ util.ET.tostring(util.ET.fromstring('<a><b>hello world</b></a>')),
+ util.ET.tostring(self.parser.readElement()))
+
+ def test_xmlstring(self):
+ self._run([
+ ('<a><b>hello world</b></a>', '\x06\x33<a><b>hello world</b></a>')
+ ])
+
+ def test_list(self):
+ self._run([
+ ([0, 1, 2, 3], '\x09\x09\x01\x04\x00\x04\x01\x04\x02\x04\x03'),
+ (["Hello", 2, 3, 4, 5], '\x09\x0b\x01\x06\x0b\x48\x65\x6c\x6c\x6f'
+ '\x04\x02\x04\x03\x04\x04\x04\x05')])
+
+ def test_list_references(self):
+ y = [0, 1, 2, 3]
+
+ self._run([
+ (y, '\x09\x09\x01\x04\x00\x04\x01\x04\x02\x04\x03'),
+ (y, '\x09\x00')])
+
+ def test_dict(self):
+ self._run([
+ ({0: u'hello', 'foo': u'bar'},
+ '\x09\x03\x07\x66\x6f\x6f\x06\x07\x62\x61\x72\x01\x06\x0b\x68\x65'
+ '\x6c\x6c\x6f')])
+ self._run([({0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 'a': 'a'},
+ '\x09\x0d\x03\x61\x06\x00\x01\x04\x00\x04\x01\x04\x02\x04\x03\x04'
+ '\x04\x04\x05')])
+ self._run([(
+ {'a': u'a', 'b': u'b', 'c': u'c', 'd': u'd',
+ 0: 0, 1: 1, 2: 2, 3: 3},
+ '\x09\x09\x03\x62\x06\x00\x03\x64\x06\x02\x03\x61\x06\x04\x03\x63'
+ '\x06\x06\x01\x04\x00\x04\x01\x04\x02\x04\x03')
+ ])
+ self._run([
+ ({'a': 1, 'b': 2}, '\x0a\x0b\x01\x03\x62\x04\x02\x03\x61\x04\x01'
+ '\x01')])
+ self._run([
+ ({'baz': u'hello'}, '\x0a\x0b\x01\x07\x62\x61\x7a\x06\x0b\x68\x65'
+ '\x6c\x6c\x6f\x01')])
+ self._run([
+ ({'baz': u'hello'}, '\x0a\x13\x01\x07\x62\x61\x7a\x06\x0b\x68\x65'
+ '\x6c\x6c\x6f')])
+
+ def test_empty_dict(self):
+ """
+ There is a very specific problem with AMF3 where the first three bytes
+ of an encoded empty dict will mirror that of an encoded {'': 1, '2': 2}
+
+ See http://www.docuverse.com/blog/donpark/2007/05/14/flash-9-amf3-bug
+ for more information.
+ """
+ self.buf.truncate(0)
+ self.buf.write('\x09\x01\x01')
+ self.buf.seek(0)
+
+ self.failUnlessRaises(pyamf.ParseError, self.parser.readElement)
+
+ def x():
+ self._run([({'': 1}, '\x09\x01\x01\x04\x01\x01')])
+
+ self.failUnlessRaises(pyamf.ParseError, x)
+
+ def test_object(self):
+ class Foo(object):
+ pass
+
+ pyamf.CLASS_CACHE = {}
+
+ pyamf.register_class(Foo, 'com.collab.dev.pyamf.foo')
+
+ self.buf.truncate(0)
+ self.buf.write('\x0a\x13\x31\x63\x6f\x6d\x2e\x63\x6f\x6c\x6c\x61\x62'
+ '\x2e\x64\x65\x76\x2e\x70\x79\x61\x6d\x66\x2e\x66\x6f\x6f\x07\x62'
+ '\x61\x7a\x06\x0b\x68\x65\x6c\x6c\x6f')
+ self.buf.seek(0)
+
+ obj = self.parser.readElement()
+
+ self.assertEquals(obj.__class__, Foo)
+
+ self.failUnless(hasattr(obj, 'baz'))
+ self.assertEquals(obj.baz, 'hello')
+
+ def test_byte_array(self):
+ self._run([(amf3.ByteArray('hello'), '\x0c\x0bhello')])
+
+class ModifiedUTF8TestCase(unittest.TestCase):
+ data = [
+ ('hello', '\x00\x05\x68\x65\x6c\x6c\x6f'),
+ (u'?????????????????????????????????????????????????????????????????????????? \
?????????????????????????????????????????????????????????????????????????????????????? \
?????????????????????????????????????????????????????????????????????????????????????? \
????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????'
+ u'?????????????????????????????????????????????????????????????????????? \
?????????????????????????????????????????????????????????????????????????????????????? \
????????????????????????????????????????????????????????????????????????????????????',
+ '\x01\x41\xe1\x9a\xa0\xe1\x9b\x87\xe1\x9a\xbb\xe1\x9b\xab\xe1\x9b'
+ '\x92\xe1\x9b\xa6\xe1\x9a\xa6\xe1\x9b\xab\xe1\x9a\xa0\xe1\x9a\xb1'
+ '\xe1\x9a\xa9\xe1\x9a\xa0\xe1\x9a\xa2\xe1\x9a\xb1\xe1\x9b\xab\xe1'
+ '\x9a\xa0\xe1\x9b\x81\xe1\x9a\xb1\xe1\x9a\xaa\xe1\x9b\xab\xe1\x9a'
+ '\xb7\xe1\x9b\x96\xe1\x9a\xbb\xe1\x9a\xb9\xe1\x9b\xa6\xe1\x9b\x9a'
+ '\xe1\x9a\xb3\xe1\x9a\xa2\xe1\x9b\x97\xe1\x9b\x8b\xe1\x9a\xb3\xe1'
+ '\x9b\x96\xe1\x9a\xaa\xe1\x9b\x9a\xe1\x9b\xab\xe1\x9a\xa6\xe1\x9b'
+ '\x96\xe1\x9a\xaa\xe1\x9a\xbb\xe1\x9b\xab\xe1\x9b\x97\xe1\x9a\xaa'
+ '\xe1\x9a\xbe\xe1\x9a\xbe\xe1\x9a\xaa\xe1\x9b\xab\xe1\x9a\xb7\xe1'
+ '\x9b\x96\xe1\x9a\xbb\xe1\x9a\xb9\xe1\x9b\xa6\xe1\x9b\x9a\xe1\x9a'
+ '\xb3\xe1\x9b\xab\xe1\x9b\x97\xe1\x9b\x81\xe1\x9a\xb3\xe1\x9b\x9a'
+ '\xe1\x9a\xa2\xe1\x9a\xbe\xe1\x9b\xab\xe1\x9a\xbb\xe1\x9b\xa6\xe1'
+ '\x9b\x8f\xe1\x9b\xab\xe1\x9b\x9e\xe1\x9a\xab\xe1\x9b\x9a\xe1\x9a'
+ '\xaa\xe1\x9a\xbe\xe1\x9a\xb7\xe1\x9b\x81\xe1\x9a\xa0\xe1\x9b\xab'
+ '\xe1\x9a\xbb\xe1\x9b\x96\xe1\x9b\xab\xe1\x9a\xb9\xe1\x9b\x81\xe1'
+ '\x9b\x9a\xe1\x9b\x96\xe1\x9b\xab\xe1\x9a\xa0\xe1\x9a\xa9\xe1\x9a'
+ '\xb1\xe1\x9b\xab\xe1\x9b\x9e\xe1\x9a\xb1\xe1\x9b\x81\xe1\x9a\xbb'
+ '\xe1\x9b\x8f\xe1\x9a\xbe\xe1\x9b\x96\xe1\x9b\xab\xe1\x9b\x9e\xe1'
+ '\x9a\xa9\xe1\x9b\x97\xe1\x9b\x96\xe1\x9b\x8b\xe1\x9b\xab\xe1\x9a'
+ '\xbb\xe1\x9b\x9a\xe1\x9b\x87\xe1\x9b\x8f\xe1\x9a\xaa\xe1\x9a\xbe'
+ '\xe1\x9b\xac')]
+
+ def test_encode(self):
+ for x in self.data:
+ self.assertEqual(amf3.encode_utf8_modified(x[0]), x[1])
+
+ def test_decode(self):
+ for x in self.data:
+ self.assertEqual(amf3.decode_utf8_modified(x[1]), x[0])
+
+def suite():
+ suite = unittest.TestSuite()
+
+ suite.addTest(unittest.makeSuite(TypesTestCase, 'test'))
+ suite.addTest(unittest.makeSuite(ModifiedUTF8TestCase, 'test'))
+ suite.addTest(unittest.makeSuite(EncoderTestCase, 'test'))
+ suite.addTest(unittest.makeSuite(ParserTestCase, 'test'))
+
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
Copied: trunk/pyamf/tests/util.py (from rev 79, branches/amf3-5/pyamf/tests/util.py)
===================================================================
--- trunk/pyamf/tests/util.py (rev 0)
+++ trunk/pyamf/tests/util.py 2007-10-26 17:22:03 UTC (rev 80)
@@ -0,0 +1,92 @@
+# -*- encoding: utf-8 -*-
+#
+# Copyright (c) 2007 The PyAMF Project. All rights reserved.
+#
+# Arnar Birgisson
+# Thijs Triemstra
+# Nick Joyce
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+"""
+Utility for PyAMF tests
+"""
+
+class GenericObject(object):
+ """
+ A basic object for en/decoding.
+ """
+
+ def __init__(self, dict):
+ self.__dict__ = dict
+
+ def __cmp__(self, other):
+ return cmp(self.__dict__, other)
+
+class EncoderTester(object):
+ """
+ A helper object that takes some input, runs over the encoder and checks
+ the output
+ """
+
+ def __init__(self, encoder, data):
+ self.encoder = encoder
+ self.buf = encoder.output
+ self.data = data
+
+ def getval(self):
+ t = self.buf.getvalue()
+ self.buf.truncate(0)
+
+ return t
+
+ def run(self, testcase):
+ for n, s in self.data:
+ self.encoder.writeElement(n)
+
+ testcase.assertEqual(self.getval(), s)
+
+class ParserTester(object):
+ """
+ A helper object that takes some input, runs over the parser and checks
+ the output
+ """
+
+ def __init__(self, parser, data):
+ self.parser = parser
+ self.buf = parser.input
+ self.data = data
+
+ def run(self, testcase):
+ for n, s in self.data:
+ self.buf.truncate(0)
+ self.buf.write(s)
+ self.buf.seek(0)
+
+ testcase.assertEqual(self.parser.readElement(), n)
+
+ if self.buf.remaining() != 0:
+ from pyamf.util import hexdump
+
+ print hexdump(self.buf.getvalue())
+
+ # make sure that the entire buffer was consumed
+ testcase.assertEqual(self.buf.remaining(), 0)
Modified: trunk/pyamf/util.py
===================================================================
--- trunk/pyamf/util.py 2007-10-26 17:15:52 UTC (rev 79)
+++ trunk/pyamf/util.py 2007-10-26 17:22:03 UTC (rev 80)
@@ -36,7 +36,7 @@
except ImportError:
import elementtree.ElementTree as ET
-class NetworkIOMixIn:
+class NetworkIOMixIn(object):
"""Provides mix-in methods for file like objects to read and write basic
datatypes in network (= big-endian) byte-order."""
@@ -105,18 +105,28 @@
length = self.len - self.tell()
return StringIO.read(self, length)
- def peek(self):
- if self.at_eof():
- return None
- else:
- c = self.read(1)
- self.seek(self.tell()-1)
- return c
+ def peek(self, size=1):
+ """
+ Looks size bytes ahead in the stream, returning what it finds,
+ returning the stream pointer to its initial position.
+ """
+ if size == -1:
+ return self.peek(self.len - self.tell())
+ bytes = ''
+ pos = self.tell()
+
+ while not self.at_eof() and len(bytes) != size:
+ bytes += self.read(1)
+
+ self.seek(pos)
+
+ return bytes
+
def at_eof(self):
"Returns true if next .read(1) will trigger EOFError"
return self.tell() >= self.len
-
+
def remaining(self):
"Returns number of remaining bytes"
return self.len - self.tell()
@@ -139,28 +149,3 @@
if len(ascii):
buf += "%04x: %-24s %-24s %s\n" % (index, hex[:24], hex[24:], ascii)
return buf
-
-def decode_utf8_modified(data):
- """Decodes a unicode string from Modified UTF-8 data.
- See http://en.wikipedia.org/wiki/UTF-8#Java for details."""
- # Ported from http://viewvc.rubyforge.mmmultiworks.com/cgi/viewvc.cgi/trunk/lib/ruva/class.rb
- # Ruby version is Copyright (c) 2006 Ross Bamford (rosco AT roscopeco DOT co DOT \
uk).
- # The string is first converted to UTF16 BE
- utf16 = []
- i = 0
- while i < len(data):
- c = ord(data[i])
- if 0x00 < c < 0x80:
- utf16.append(c)
- i += 1
- elif c & 0xc0 == 0xc0:
- utf16.append(((c & 0x1f) << 6) | (ord(data[i+1]) & 0x3f))
- i += 2
- elif c & 0xe0 == 0xe0:
- utf16.append(((c & 0x0f) << 12) | ((ord(data[i+1]) & 0x3f) << 6) | \
(ord(data[i+2]) & 0x3f))
- i += 3
- else:
- raise ValueError("Data is not valid modified UTF-8")
-
- utf16 = "".join([chr((c >> 8) & 0xff) + chr(c & 0xff) for c in utf16])
- return unicode(utf16, "utf_16_be")
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic