pyloxi: move version-independent utility code to a new file

Also adds a test for unpack_list.
diff --git a/Makefile b/Makefile
index e77edfe..be487ad 100644
--- a/Makefile
+++ b/Makefile
@@ -73,6 +73,7 @@
 	PYTHONPATH=. ./utest/test_parser.py
 
 check-py: python
+	PYTHONPATH=${LOXI_OUTPUT_DIR}/pyloxi python py_gen/tests/generic_util.py
 	PYTHONPATH=${LOXI_OUTPUT_DIR}/pyloxi python py_gen/tests/of10.py
 	PYTHONPATH=${LOXI_OUTPUT_DIR}/pyloxi python py_gen/tests/of11.py
 	PYTHONPATH=${LOXI_OUTPUT_DIR}/pyloxi python py_gen/tests/of12.py
diff --git a/lang_python.py b/lang_python.py
index 0314738..9dd44b2 100644
--- a/lang_python.py
+++ b/lang_python.py
@@ -89,6 +89,7 @@
 targets = {
     prefix+'/__init__.py': static('toplevel_init.py'),
     prefix+'/pp.py': static('pp.py'),
+    prefix+'/generic_util.py': static('generic_util.py'),
 }
 
 for version, subdir in versions.items():
diff --git a/py_gen/oftype.py b/py_gen/oftype.py
index 481321c..d5f2df0 100644
--- a/py_gen/oftype.py
+++ b/py_gen/oftype.py
@@ -135,7 +135,7 @@
             if ((element_cls, self.version) in of_g.is_fixed_length):
                 klass_name = self.base[8:-2]
                 element_size, = of_g.base_length[(element_cls, self.version)],
-                return 'util.unpack_array(common.%s.unpack, %d, buffer(%s, %s))' % (klass_name, element_size, buf_expr, offset_expr)
+                return 'loxi.generic_util.unpack_array(common.%s.unpack, %d, buffer(%s, %s))' % (klass_name, element_size, buf_expr, offset_expr)
             else:
                 return "None # TODO unpack list %s" % self.base
         else:
diff --git a/py_gen/templates/action.py b/py_gen/templates/action.py
index 8462dfd..1c7a75a 100644
--- a/py_gen/templates/action.py
+++ b/py_gen/templates/action.py
@@ -34,6 +34,7 @@
 import struct
 import const
 import util
+import loxi.generic_util
 import loxi
 
 def unpack_list(buf):
@@ -44,7 +45,7 @@
         parser = parsers.get(type)
         if not parser: raise loxi.ProtocolError("unknown action type %d" % type)
         return parser(buf)
-    return util.unpack_list(deserializer, "!2xH", buf)
+    return loxi.generic_util.unpack_list(deserializer, "!2xH", buf)
 
 class Action(object):
     type = None # override in subclass
diff --git a/py_gen/templates/common.py b/py_gen/templates/common.py
index 2f9310e..516bf05 100644
--- a/py_gen/templates/common.py
+++ b/py_gen/templates/common.py
@@ -34,12 +34,13 @@
 import action
 import const
 import util
+import loxi.generic_util
 
 # HACK make this module visible as 'common' to simplify code generation
 common = sys.modules[__name__]
 
 def unpack_list_flow_stats_entry(buf):
-    return util.unpack_list(flow_stats_entry.unpack, "!H", buf)
+    return loxi.generic_util.unpack_list(flow_stats_entry.unpack, "!H", buf)
 
 def unpack_list_queue_prop(buf):
     def deserializer(buf):
@@ -48,10 +49,10 @@
             return queue_prop_min_rate.unpack(buf)
         else:
             raise loxi.ProtocolError("unknown queue prop %d" % type)
-    return util.unpack_list(deserializer, "!2xH", buf)
+    return loxi.generic_util.unpack_list(deserializer, "!2xH", buf)
 
 def unpack_list_packet_queue(buf):
-    return util.unpack_list(packet_queue.unpack, "!4xH", buf)
+    return loxi.generic_util.unpack_list(packet_queue.unpack, "!4xH", buf)
 
 def unpack_list_hello_elem(buf):
     def deserializer(buf):
@@ -60,7 +61,7 @@
             return hello_elem_versionbitmap.unpack(buf)
         else:
             return None
-    return [x for x in util.unpack_list(deserializer, "!2xH", buf) if x != None]
+    return [x for x in loxi.generic_util.unpack_list(deserializer, "!2xH", buf) if x != None]
 
 :: for ofclass in ofclasses:
 :: include('_ofclass.py', ofclass=ofclass, superclass="object")
diff --git a/py_gen/templates/generic_util.py b/py_gen/templates/generic_util.py
new file mode 100644
index 0000000..cba8c86
--- /dev/null
+++ b/py_gen/templates/generic_util.py
@@ -0,0 +1,66 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: include('_copyright.py')
+"""
+Utility functions independent of the protocol version
+"""
+
+:: include('_autogen.py')
+
+import loxi
+import struct
+
+def unpack_array(deserializer, element_size, buf):
+    """
+    Deserialize an array of fixed length elements.
+    The deserializer function should take a buffer and return the new object.
+    """
+    if len(buf) % element_size != 0: raise loxi.ProtocolError("invalid array length")
+    n = len(buf) / element_size
+    return [deserializer(buffer(buf, i*element_size, element_size)) for i in range(n)]
+
+def unpack_list(deserializer, length_fmt, buf, extra_len=0):
+    """
+    Deserialize a list of variable-length entries.
+    'length_fmt' is a struct format string with exactly one non-padding format
+    character that returns the length of the given element, minus extra_len.
+    The deserializer function should take a buffer and return the new object.
+    """
+    entries = []
+    offset = 0
+    length_struct = struct.Struct(length_fmt)
+    n = len(buf)
+    while offset < n:
+        if offset + length_struct.size > len(buf): raise loxi.ProtocolError("entry header overruns list length")
+        length, = length_struct.unpack_from(buf, offset)
+        length += extra_len
+        if length < length_struct.size: raise loxi.ProtocolError("entry length is less than the header length")
+        if offset + length > len(buf): raise loxi.ProtocolError("entry length overruns list length")
+        entries.append(deserializer(buffer(buf, offset, length)))
+        offset += length
+    return entries
diff --git a/py_gen/templates/message.py b/py_gen/templates/message.py
index 8eb051d..d501854 100644
--- a/py_gen/templates/message.py
+++ b/py_gen/templates/message.py
@@ -37,6 +37,7 @@
 import common
 import action # for unpack_list
 import util
+import loxi.generic_util
 
 class Message(object):
     version = const.OFP_VERSION
diff --git a/py_gen/templates/oxm.py b/py_gen/templates/oxm.py
index 14ea259..66886e0 100644
--- a/py_gen/templates/oxm.py
+++ b/py_gen/templates/oxm.py
@@ -34,6 +34,7 @@
 import struct
 import const
 import util
+import loxi.generic_util
 import loxi
 
 class OXM(object):
diff --git a/py_gen/templates/util.py b/py_gen/templates/util.py
index fead78f..f2f7a96 100644
--- a/py_gen/templates/util.py
+++ b/py_gen/templates/util.py
@@ -33,35 +33,6 @@
 import const
 import struct
 
-def unpack_array(deserializer, element_size, buf):
-    """
-    Deserialize an array of fixed length elements.
-    The deserializer function should take a buffer and return the new object.
-    """
-    if len(buf) % element_size != 0: raise loxi.ProtocolError("invalid array length")
-    n = len(buf) / element_size
-    return [deserializer(buffer(buf, i*element_size, element_size)) for i in range(n)]
-
-def unpack_list(deserializer, length_fmt, buf):
-    """
-    Deserialize a list of variable-length entries.
-    'length_fmt' is a struct format string with exactly one non-padding format
-    character that returns the length of the given element.
-    The deserializer function should take a buffer and return the new object.
-    """
-    entries = []
-    offset = 0
-    length_struct = struct.Struct(length_fmt)
-    n = len(buf)
-    while offset < n:
-        if offset + length_struct.size > len(buf): raise loxi.ProtocolError("entry header overruns list length")
-        length, = length_struct.unpack_from(buf, offset)
-        if length < length_struct.size: raise loxi.ProtocolError("entry length is less than the header length")
-        if offset + length > len(buf): raise loxi.ProtocolError("entry length overruns list length")
-        entries.append(deserializer(buffer(buf, offset, length)))
-        offset += length
-    return entries
-
 def pretty_mac(mac):
     return ':'.join(["%02x" % x for x in mac])
 
diff --git a/py_gen/tests/generic_util.py b/py_gen/tests/generic_util.py
new file mode 100644
index 0000000..81ecee2
--- /dev/null
+++ b/py_gen/tests/generic_util.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+# Copyright 2013, Big Switch Networks, Inc.
+#
+# LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+# the following special exception:
+#
+# LOXI Exception
+#
+# As a special exception to the terms of the EPL, you may distribute libraries
+# generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+# that copyright and licensing notices generated by LoxiGen are not altered or removed
+# from the LoxiGen Libraries and the notice provided below is (i) included in
+# the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+# documentation for the LoxiGen Libraries, if distributed in binary form.
+#
+# Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+#
+# You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+# a copy of the EPL at:
+#
+# http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# EPL for the specific language governing permissions and limitations
+# under the EPL.
+import unittest
+
+try:
+    import loxi
+    import loxi.generic_util
+except ImportError:
+    exit("loxi package not found. Try setting PYTHONPATH.")
+
+class TestUnpackArray(unittest.TestCase):
+    def test_simple(self):
+        a = loxi.generic_util.unpack_array(str, 3, "abcdefghi")
+        self.assertEquals(['abc', 'def', 'ghi'], a)
+
+        with self.assertRaisesRegexp(loxi.ProtocolError, "invalid array length"):
+            loxi.generic_util.unpack_array(str, 3, "abcdefgh")
+
+class TestUnpackList(unittest.TestCase):
+    def test_simple(self):
+        a = loxi.generic_util.unpack_list(str, '!B', "\x04abc\x03de\x02f\x01")
+        self.assertEquals(['\x04abc', '\x03de', '\x02f', '\x01'], a)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/py_gen/tests/of10.py b/py_gen/tests/of10.py
index e636973..c41c37e 100644
--- a/py_gen/tests/of10.py
+++ b/py_gen/tests/of10.py
@@ -876,16 +876,6 @@
             self.assertIsInstance(ofp.message.parse_message(klass(xid=1).pack()), klass)
 
 class TestUtils(unittest.TestCase):
-    def test_unpack_array(self):
-        import loxi
-        import loxi.of10.util as util
-
-        a = util.unpack_array(str, 3, "abcdefghi")
-        self.assertEquals(['abc', 'def', 'ghi'], a)
-
-        with self.assertRaisesRegexp(loxi.ProtocolError, "invalid array length"):
-            util.unpack_array(str, 3, "abcdefgh")
-
     def test_pretty_wildcards(self):
         self.assertEquals("OFPFW_ALL", ofp.util.pretty_wildcards(ofp.OFPFW_ALL))
         self.assertEquals("0", ofp.util.pretty_wildcards(0))