### py_interface -- A Python-implementation of an Erlang node
###
### $Id: erl_term.py,v 1.16 2006/06/07 23:02:57 tab Exp $
###
### Copyright (C) 2002  Tomas Abrahamsson
###
### Author: Tomas Abrahamsson <tab@lysator.liu.se>
### 
### This file is part of the Py-Interface library
###
### This library is free software; you can redistribute it and/or
### modify it under the terms of the GNU Library General Public
### License as published by the Free Software Foundation; either
### version 2 of the License, or (at your option) any later version.
### 
### This library is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
### Library General Public License for more details.
### 
### You should have received a copy of the GNU Library General Public
### License along with this library; if not, write to the Free
### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

### erl_term.py -- python types/classes for all erlang types.
###                Also: packing and unpacking all types to/from
###                      the erlang external binary format

### Log of fixes:
###
### * integer unpacking by Jimmy Olgeni <olgeni@uli.it>
###
### * string packing by Jimmy Olgeni <olgeni@uli.it>
###
### * reference unpacking, independently by Nigel Head <nigel.head@esa.int>
###                                and Tomas Abrahamsson <tab@lysator.liu.se>
###
### * bignum packing by Tomas Abrahamsson <tab@lysator.liu.se>
###
### * error handling changed a bit: throws exceptions instead of silently
###   hiding some nasties by Nigel Head <nigel.head@esa.int>
###
### * deals with received list tails better for non-proper lists
###                       by Nigel Head <nigel.head@esa.int>

import os
import sys
import math
import types
import string


import erl_common

def ErlNumber(number):
    return number

_atom_cache = {}
class ErlAtom:
    """An Erlang atom. The following attributes are defined:
    atomText = string
    """
    def __init__(self, atomText, cache=-1):
        global _atom_cache
        if atomText == None and cache != -1:
            if _atom_cache.has_key(cache):
                self.atomText = _atom_cache[cache]
            else:
                raise "No such cached atom: %s" % `cache`
        elif atomText != None and cache != -1:
            self.atomText = atomText
            _atom_cache[cache] = atomText
        else:
            self.atomText = atomText
    def __repr__(self):
        return "<erl-atom: %s>" % `self.atomText`
    def equals(self, other):
        return self.atomText == other.atomText
    def __str__(self):
        return self.atomText

def IsErlAtom(term):
    """Checks whether a term is an Erlang atom or not."""
    return type(term) == types.InstanceType and isinstance(term, ErlAtom)

            
class ErlRef:
    """An Erlang reference. The following attributes are defined:
    node     = <ErlAtom>
    id       = integer | list(integer)
    creation = integer
    """
    def __init__(self, node, id, creation):
        self.node = node
        self.id = id                    # id is either an int or a list of ints
        self.creation = creation
    def __repr__(self):
        return "<erl-ref: node=%s, id=%s, creation=%d>" % \
               (`self.node`, `self.id`, self.creation)
    def equals(self, other):
        return self.node.equals(other.node) and \
               self.id == other.id and \
               self.creation == other.creation

def IsErlRef(term):
    """Checks whether a term is an Erlang reference or not."""
    return type(term) == types.InstanceType and isinstance(term, ErlRef)


class ErlPort:
    """An Erlang port. The following attributes are defined:
    node     = <ErlAtom>
    id       = integer
    creation = integer
    """
    def __init__(self, node, id, creation):
        self.node = node
        self.id = id
        self.creation = creation
    def __repr__(self):
        return "<erl-port: node=%s, id=%d, creation=%d>" % \
               (`self.node`, self.id, self.creation)
    def equals(self, other):
        return self.node.equals(other.node) and \
               self.id == other.id and \
               self.creation == other.creation

def IsErlPort(term):
    """Checks whether a term is an Erlang reference or not."""
    return type(term) == types.InstanceType and isinstance(term, ErlPort)

class ErlPid:
    """An Erlang process id. The following attributes are defined:
    node     = <ErlAtom>
    id       = integer
    serial   = integer
    creation = integer
    """
    def __init__(self, node, id, serial, creation):
        self.node = node
        self.id = id
        self.serial = serial
        self.creation = creation
    def __repr__(self):
        return "<erl-pid: node=%s, id=%d, serial=%d, creation=%d>" % \
               (`self.node`, self.id, self.serial, self.creation)
    def equals(self, other):
        return self.node.equals(other.node) and \
               self.id == other.id and \
               self.serial == other.serial and \
               self.creation == other.creation

def IsErlPid(term):
    """Checks whether a term is an Erlang process id or not."""
    return type(term) == types.InstanceType and isinstance(term, ErlPid)

def ErlTuple(elementsAsList):
    """An Erlang tuple. This maps to a python tuple."""
    return tuple(elementsAsList)

def ErlList(elements):
    """An Erlang list. This maps to a python list."""
    return elements
    
class ErlImproperList:
    """An improper erlang list (one where the tail is not []).
    Can be iterated over to get the elements, by default will include
    the tail as the last element."""
    def __init__(self,elements,tail,useTail=1):
        self.elements = elements
        self.tail = tail
        # if true, we include tail element in iterations on this list
        self.iterOnTail = useTail
    def __repr__(self):
        return "<erl-improper-list: head=%s, tail=%s>" % (`self.elements`,`self.tail`)
    def equals(self,other):
        return self.elements==other.elements and self.tail==other.tail
    def __getitem__(self,key):
        try:
            return self.elements[key]
        except IndexError:
            if self.iterOnTail and key==len(self.elements):
                return self.tail
            raise IndexError
        
def IsErlImproperList(term):
    return type(term)== types.InstanceType and isinstance(term, ErlImproperList)
    
class ErlBinary:
    """An Erlang binary. The following attributes are defined:
    contents = string
    """
    def __init__(self, contents):
        self.contents = contents        #  a string
    def __repr__(self):
        return "<erl-binary: size=%d>" % len(self.contents)
    def equals(self, other):
        return self.contents == other.contents

def IsErlBinary(term):
    """Checks whether a term is an Erlang binary or not."""
    return type(term) == types.InstanceType and isinstance(term, ErlBinary)

def ErlString(s):
    """An Erlang list. This maps to a python string."""
    return s

class ErlFun:
    """An Erlang process id. The following attributes are defined:
    pid      = <ErlPid>
    module   = <ErlAtom>
    index    = integer
    uniq     = integer
    freeVars = list(term)
    """
    def __init__(self, pid, module, index, uniq, freeVars):
        self.pid = pid
        self.module = module
        self.index = index
        self.uniq = uniq
        self.freeVars = freeVars
    def __repr__(self):
        return "<erl-fun: pid=%s, module=%s, index=%d, uniq=%d, freeVars=%s>"%\
               (`self.pid`, `self.module`, self.index, self.uniq,
                `self.freeVars`)
    def equals(self, other):
        return self.pid.equals(other.pid) and \
               self.module.equals(other.module) and \
               self.index == other.index and \
               self.uniq == other.uniq and \
               self.freeVars == other.freeVars


def IsErlFun(term):
    """Checks whether a term is an Erlang function or not."""
    return type(term) == types.InstanceType and isinstance(term, ErlFun)

###
### MAGIC tags used in packing/unpacking. See erl_ext_dist.txt
###
MAGIC_VERSION = 131
MAGIC_STRING = 107
MAGIC_NIL = 106
MAGIC_LIST = 108
MAGIC_SMALL_TUPLE = 104
MAGIC_LARGE_TUPLE = 105
MAGIC_LARGE_BIG = 111
MAGIC_SMALL_BIG = 110
MAGIC_FLOAT = 99
MAGIC_SMALL_INTEGER = 97
MAGIC_INTEGER = 98
MAGIC_ATOM = 100
MAGIC_NEW_REFERENCE = 114
MAGIC_REFERENCE = 101
MAGIC_PORT = 102
MAGIC_PID = 103
MAGIC_BINARY = 109
MAGIC_FUN = 117
MAGIC_NEW_CACHE = 78
MAGIC_CACHED_ATOM = 67

###
### UNPACKING
###
def BinaryToTerm(binary):
    """Unpack a binary to a term.

    BINARY = string

    Returns: term
    Throws:  "BinaryToTerm: Extraneous data in binary"
    """
    try:
        (term, remaining) = _UnpackOneTermTop(binary)
    except:
        raise "BinaryToTerm: Panic -- invalid binary received?"
    if len(remaining) != 0:
        raise "BinaryToTerm: Extraneous data in binary"
    return term

def BinariesToTerms(binary):
    """Unpack a binary/binaries to term(s).
    This is mainly for use by the erl_node_conn, where, in some cases,
    two or more terms are packed together.

    BINARY = string

    Returns: list(term)
    Throws:  "BinaryToTerm: Extraneous data in binary"
    """
    try:
        (terms, remaining) = BufToTerm(binary)
    except:
        raise "BinariesToTerms: Panic -- invalid binary received?"
    if len(remaining) != 0:
        raise "BinariesToTerms: Extraneous data in binary"
    return terms

def BufToTerm(data):
    unpackedTerms = []
    inputData = data
    while 1:
        (unpackedTerm, remainingData) = _UnpackOneTermTop(inputData)
        if unpackedTerm == None:
            return (unpackedTerms, remainingData)
        unpackedTerms.append(unpackedTerm)
        inputData = remainingData

def _UnpackOneTermTop(data):
    if len(data) == 0: 
        return (None, data)
    if data[0] != chr(MAGIC_VERSION):
        return (None, data)
    return _UnpackOneTerm(data[1:])


def _UnpackOneTerm(data):
    dataLen = len(data)

    if len(data) == 0:
        return (None, data)

    data0 = ord(data[0])

    if data0 == MAGIC_SMALL_INTEGER:
        n = _ReadInt1(data[1])
        return (ErlNumber(n), data[2:])

    elif data0 == MAGIC_INTEGER:
        n = _ReadInt4(data[1:5])
        return (ErlNumber(n), data[5:])

    elif data0 == MAGIC_FLOAT:
        floatData = data[1:32]
        try:
            nullIndex = string.index(floatData, chr(0))
            floatStr = floatData[0:nullIndex]
        except ValueError:
            floatStr = floatData
        f = string.atof(floatStr)
        return (ErlNumber(f), data[32:])

    elif data0 == MAGIC_ATOM:
        atomLen = _ReadInt2(data[1:3])
        atomText = data[3:3 + atomLen]
        return (ErlAtom(atomText), data[3 + atomLen:])

    elif data0 == MAGIC_REFERENCE:
        (node, remainingData) = _UnpackOneTerm(data[1:])
        id = _ReadId(remainingData[0:4])
        creation = _ReadCreation(remainingData[4])
        return (ErlRef(node, id, creation), remainingData[5:])

    elif data0 == MAGIC_PORT:
        (node, remainingData) = _UnpackOneTerm(data[1:])
        id = _ReadId(remainingData[0:4], 28)
        creation = _ReadCreation(remainingData[4])
        return (ErlPort(node, id, creation), remainingData[5:])

    elif data0 == MAGIC_PID:
        (node, remainingData) = _UnpackOneTerm(data[1:])
        id = _ReadId(remainingData[0:4], 28)
        serial = _ReadInt4(remainingData[4:8])
        creation = _ReadCreation(remainingData[8])
        return (ErlPid(node, id, serial, creation), remainingData[9:])

    elif data0 == MAGIC_SMALL_TUPLE:
        arity = _ReadInt1(data[1])
        (elements, remainingData) = _UnpackTermSeq(arity, data[2:])
        return (ErlTuple(elements), remainingData)

    elif data0 == MAGIC_LARGE_TUPLE:
        arity = _ReadInt4(data[1:5])
        (elements, remainingData) = _UnpackTermSeq(arity, data[5:])
        return (ErlTuple(elements), remainingData)

    elif data0 == MAGIC_NIL:
        return (ErlList([]), data[1:])

    elif data0 == MAGIC_STRING:
        strlen = _ReadInt2(data[1:3])
        s = data[3:3 + strlen]
        return (ErlString(s), data[3 + strlen:])

    elif data0 == MAGIC_LIST:
        # get the list head
        arity = _ReadInt4(data[1:5])
        (elements, remainingData) = _UnpackTermSeq(arity, data[5:])
        # now get the list tail (usually this is [] but
        # for not well formed lists it may be any term).
        (tail, newRemainingData) = _UnpackOneTerm(remainingData)
        if tail <> []:
            return (ErlImproperList(elements,tail), newRemainingData)
        return (ErlList(elements), newRemainingData)

    elif data0 == MAGIC_BINARY:
        binlen = _ReadInt4(data[1:5])
        s = data[5:5 + binlen]
        return (ErlBinary(s), data[5 + binlen:])

    elif data0 == MAGIC_SMALL_BIG:
        n = _ReadInt1(data[1])
        sign = _ReadInt1(data[2])
        bignum = 0L
        for i in range(n):
            d = _ReadInt1(data[3 + n - i - 1])
            bignum = bignum * 256L + long(d)
        if sign:
            bignum = bignum * -1L
        return (ErlNumber(bignum), data[3 + n:])

    elif data0 == MAGIC_LARGE_BIG:
        n = _ReadInt4(data[1:5])
        sign = _ReadInt1(data[5])
        bignum = 0L
        for i in range(n):
            d = _ReadInt1(data[6 + n - i - 1])
            bignum = bignum * 256L + long(d)
        if sign:
            bignum = bignum * -1L
        return (ErlNumber(bignum), data[6 + n:])

    elif data0 == MAGIC_NEW_CACHE:
        index = _ReadInt1(data[1])
        atomLen = _ReadInt2(data[2:4])
        atomText = data[4:4 + atomLen]
        return (ErlAtom(atomText, cache=index), data[4 + atomLen:])

    elif data0 == MAGIC_CACHED_ATOM:
        index = _ReadInt1(data[1])
        return (ErlAtom(None, cache=index), data[2:])

    elif data0 == MAGIC_NEW_REFERENCE:
        idLen = _ReadInt2(data[1:3])
        (node, remainingData) = _UnpackOneTerm(data[3:])
        nprim = 4 * idLen
        creation = _ReadCreation(remainingData[0])
        remainingData = remainingData[1:]
        id0 = _ReadId(remainingData[0:4])
        ids = [id0]
        remainingData = remainingData[4:]
        for i in range(idLen-1):
            id = _ReadInt4(remainingData[0:4])
            remainingData = remainingData[4:]
            ids.append(id)
        return (ErlRef(node, ids, creation), remainingData)

    elif data0 == MAGIC_FUN:
        freevarsLen = _ReadInt4(data[1:5])
        (pid, remainingData1) = _UnpackOneTerm(data[5:])
        (module, remainingData2) = _UnpackOneTerm(remainingData1)
        (index, remainingData3)  = _UnpackOneTerm(remainingData2)
        (uniq, remainingData4) = _UnpackOneTerm(remainingData3)
        (freeVars, remainingData5) = _UnpackTermSeq(freevarsLen,remainingData4)
        print "MAGIC_FUN"
        print pid
        print module
        print index
        print uniq
        print freeVars
        return (ErlFun(pid, module, index, uniq, freeVars),
                remainingData5)

    else:
        print "Bad tag %s" % `data0`


    return (None, data)

def _UnpackTermSeq(numTerms, data):
    seq = []
    remainingData = data
    for i in range(numTerms):
        (term, newRemainingData) = _UnpackOneTerm(remainingData)
        seq.append(term)
        remainingData = newRemainingData
    return (seq, remainingData)

def _ReadId(s, maxSignificantBits = 18):
    return _ReadInt4(s) & ((1 << maxSignificantBits) - 1)

def _ReadCreation(s):
    return _ReadInt1(s) & ((1 << 2) - 1)

def _ReadInt1(s):
    return erl_common.ReadInt1(s)

def _ReadInt2(s):
    return erl_common.ReadInt2(s)

def _ReadInt4(s):
    return erl_common.ReadInt4(s)



###
### PACKING
###

def TermToBinary(term):
    """Pack a term to a binary.

    TERM = term

    Returns: string
    Throws:  \"Can't pack value of type ...\"
    """
    return chr(MAGIC_VERSION) + _PackOneTerm(term)

def _PackOneTerm(term):
    if type(term) == types.StringType:
        return _PackString(term)
    elif type(term) == types.ListType:
        return _PackList(term)
    elif type(term) == types.TupleType:
        return _PackTuple(term)
    elif type(term) == types.LongType:
        return _PackLong(term)
    elif type(term) == types.FloatType:
        return _PackFloat(term)
    elif type(term) == types.IntType:
        return _PackInt(term)
    elif IsErlAtom(term):
        return _PackAtom(term)
    elif IsErlRef(term):
        return _PackRef(term)
    elif IsErlPort(term):
        return _PackPort(term)
    elif IsErlPid(term):
        return _PackPid(term)
    elif IsErlBinary(term):
        return _PackBinary(term)
    elif IsErlFun(term):
        return _PackFun(term)
    else:
        print "Term=%s" % `term`
        raise "Can't pack value of type %s" % `type(term)`

    
def _PackString(term):
    if len(term) == 0:
        return _PackList([])
    elif len(term) <= 65535:
        return _PackInt1(MAGIC_STRING) + _PackInt2(len(term)) + term
    else:
        return _PackList(map(lambda c: ord(c), term))

def _PackList(term):
    if len(term) == 0:
        return _PackInt1(MAGIC_NIL)
    else:
        packedData = ""
        for elem in term:
            packedData = packedData + _PackOneTerm(elem)
        return _PackInt1(MAGIC_LIST) + _PackInt4(len(term)) + packedData + \
               _PackList([])

def _PackTuple(term):
    if len(term) < 256:
        head = _PackInt1(MAGIC_SMALL_TUPLE) + _PackInt1(len(term))
    else:
        head = _PackInt1(MAGIC_LARGE_TUPLE) + _PackInt4(len(term))
    packedData = head
    for elem in term:
        packedData = packedData + _PackOneTerm(elem)
    return packedData


def _PackLong(term):
    if -long(0x7fffffff) - 1 <= term <= long(0x7fffffff):
        return _PackInt(term)
    else:
        numBytesNeeded = int(math.log(term) / math.log(256)) + 1
        if numBytesNeeded > 255:
            return _PackInt1(MAGIC_LARGE_BIG) + \
                   _PackInt4(numBytesNeeded) + \
                   _PackLongBytes(term, numBytesNeeded)
        else:
            return _PackInt1(MAGIC_SMALL_BIG) + \
                   _PackInt1(numBytesNeeded) + \
                   _PackLongBytes(term, numBytesNeeded)

def _PackLongBytes(term, numBytesNeeded):
    if term < 0:
        sign = _PackInt1(1)
    else:
        sign = _PackInt1(0)
    bignum = term
    bignumBytes = sign
    for i in range(numBytesNeeded):
        bignumBytes = bignumBytes + _PackInt1(bignum & 255)
        bignum = bignum >> 8
    return bignumBytes

def _PackFloat(term):
    floatStr = "%.20e" % term
    nullPadStr = _PackInt1(0) * (31 - len(floatStr))
    return _PackInt1(MAGIC_FLOAT) + floatStr + nullPadStr

def _PackInt(term):
    if 0 <= term < 256:
        return _PackInt1(MAGIC_SMALL_INTEGER) + _PackInt1(term)
    else:
        return _PackInt1(MAGIC_INTEGER) + _PackInt4(term)

def _PackAtom(term):
    atomText = term.atomText
    return _PackInt1(MAGIC_ATOM) + _PackInt2(len(atomText)) + atomText

def _PackRef(term):
    if type(term.id) == types.ListType:
        return _PackNewReferenceExt(term)
    else:
        return _PackReferenceExt(term)

def _PackNewReferenceExt(term):
    node = _PackOneTerm(term.node)
    creation = _PackCreation(term.creation)
    id0 = _PackId(term.id[0])
    ids = id0
    for id in term.id[1:]:
        ids = ids + _PackInt4(id)
    return _PackInt1(MAGIC_NEW_REFERENCE) + \
           _PackInt2(len(term.id)) + \
           node + creation + ids

def _PackReferenceExt(term):
    node = _PackOneTerm(term.node)
    id = _PackId(term.id)
    creation = _PackCreation(term.creation)
    return _PackInt1(MAGIC_REFERENCE) + node + id + creation

def _PackPort(term):
    node = _PackOneTerm(term.node)
    id = _PackId(term.id, 28)
    creation = _PackCreation(term.creation)
    return _PackInt1(MAGIC_PORT) + node + id + creation

def _PackPid(term):
    node = _PackOneTerm(term.node)
    id = _PackId(term.id, 28)
    serial = _PackInt4(term.serial)
    creation = _PackCreation(term.creation)
    return _PackInt1(MAGIC_PID) + node + id + serial + creation

def _PackBinary(term):
    return _PackInt1(MAGIC_BINARY) + \
           _PackInt4(len(term.contents)) + \
           term.contents

def _PackFun(term):
    numFreeVars = _PackInt4(len(term.freeVars))
    pid = _PackPid(term.pid)
    module = _PackAtom(term.module)
    index = _PackInt(term.index)
    uniq = _PackInt(term.uniq)
    freeVars = ""
    for freeVar in term.freeVars:
        freeVars = freeVars + _PackOneTerm(freeVar)
    return _PackInt4(MAGIC_FUN) + numFreeVars + \
           pid + module + index + uniq + freeVars


def _PackId(i, maxSignificantBits=18):
    return _PackInt4(i & ((1 << maxSignificantBits) - 1))

def _PackCreation(i):
    return _PackInt1(i & ((1 << 2) - 1))

def _PackInt1(i):
    return erl_common.PackInt1(i)

def _PackInt2(i):
    return erl_common.PackInt2(i)

def _PackInt4(i):
    return erl_common.PackInt4(i)



syntax highlighted by Code2HTML, v. 0.9.1