[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