pyloxi: add OFReader class

This will be used for unpacking messages instead of the current strategy which
depends on fixed offsets.
diff --git a/py_gen/templates/generic_util.py b/py_gen/templates/generic_util.py
index cba8c86..a69cd62 100644
--- a/py_gen/templates/generic_util.py
+++ b/py_gen/templates/generic_util.py
@@ -64,3 +64,54 @@
         entries.append(deserializer(buffer(buf, offset, length)))
         offset += length
     return entries
+
+class OFReader(object):
+    """
+    Cursor over a read-only buffer
+
+    OpenFlow messages are best thought of as a sequence of elements of
+    variable size, rather than a C-style struct with fixed offsets and
+    known field lengths. This class supports efficiently reading
+    fields sequentially and is intended to be used recursively by the
+    parsers of child objects which will implicitly update the offset.
+    """
+    def __init__(self, buf):
+        self.buf = buf
+        self.offset = 0
+
+    def read(self, fmt):
+        st = struct.Struct(fmt)
+        if self.offset + st.size > len(self.buf):
+            raise loxi.ProtocolError("Buffer too short")
+        result = st.unpack_from(self.buf, self.offset)
+        self.offset += st.size
+        return result
+
+    def read_all(self):
+        buf = buffer(self.buf, self.offset)
+        self.offset += len(buf)
+        return str(buf)
+
+    def peek(self, fmt):
+        st = struct.Struct(fmt)
+        if self.offset + st.size > len(self.buf):
+            raise loxi.ProtocolError("Buffer too short")
+        result = st.unpack_from(self.buf, self.offset)
+        return result
+
+    def skip(self, length):
+        if self.offset + length > len(self.buf):
+            raise loxi.ProtocolError("Buffer too short")
+        self.offset += length
+
+    def is_empty(self):
+        return self.offset == len(self.buf)
+
+    # Used when parsing variable length objects which have external length
+    # fields (e.g. the actions list in an OF 1.0 packet-out message).
+    def slice(self, length):
+        if self.offset + length > len(self.buf):
+            raise loxi.ProtocolError("Buffer too short")
+        buf = OFReader(buffer(self.buf, self.offset, length))
+        self.offset += length
+        return buf
diff --git a/py_gen/tests/generic_util.py b/py_gen/tests/generic_util.py
index 81ecee2..ba06d73 100644
--- a/py_gen/tests/generic_util.py
+++ b/py_gen/tests/generic_util.py
@@ -30,6 +30,7 @@
 try:
     import loxi
     import loxi.generic_util
+    from loxi.generic_util import OFReader
 except ImportError:
     exit("loxi package not found. Try setting PYTHONPATH.")
 
@@ -46,5 +47,69 @@
         a = loxi.generic_util.unpack_list(str, '!B', "\x04abc\x03de\x02f\x01")
         self.assertEquals(['\x04abc', '\x03de', '\x02f', '\x01'], a)
 
+class TestOFReader(unittest.TestCase):
+    def test_empty(self):
+        reader = OFReader("")
+        self.assertEquals(str(reader.read('')), "")
+        with self.assertRaisesRegexp(loxi.ProtocolError, "Buffer too short"):
+            reader.read_buf(1)
+
+    def test_simple(self):
+        reader = OFReader("abcdefg")
+        self.assertEquals(reader.read('2s')[0], "ab")
+        self.assertEquals(reader.read('2s')[0], "cd")
+        self.assertEquals(reader.read('3s')[0], "efg")
+        with self.assertRaisesRegexp(loxi.ProtocolError, "Buffer too short"):
+            reader.read('s')
+
+    def test_skip(self):
+        reader = OFReader("abcdefg")
+        reader.skip(4)
+        self.assertEquals(reader.read('s')[0], "e")
+        with self.assertRaisesRegexp(loxi.ProtocolError, "Buffer too short"):
+            reader.skip(3)
+
+    def test_empty(self):
+        reader = OFReader("abcdefg")
+        self.assertEquals(reader.is_empty(), False)
+        reader.skip(6)
+        self.assertEquals(reader.is_empty(), False)
+        reader.skip(1)
+        self.assertEquals(reader.is_empty(), True)
+        with self.assertRaisesRegexp(loxi.ProtocolError, "Buffer too short"):
+            reader.skip(1)
+
+    def test_exception_effect(self):
+        reader = OFReader("abcdefg")
+        with self.assertRaisesRegexp(loxi.ProtocolError, "Buffer too short"):
+            reader.skip(8)
+        self.assertEquals(reader.is_empty(), False)
+        reader.skip(7)
+        self.assertEquals(reader.is_empty(), True)
+
+    def test_peek(self):
+        reader = OFReader("abcdefg")
+        self.assertEquals(reader.peek('2s')[0], "ab")
+        self.assertEquals(reader.peek('2s')[0], "ab")
+        self.assertEquals(reader.read('2s')[0], "ab")
+        self.assertEquals(reader.peek('2s')[0], "cd")
+        reader.skip(2)
+        self.assertEquals(reader.read('3s')[0], "efg")
+        with self.assertRaisesRegexp(loxi.ProtocolError, "Buffer too short"):
+            reader.peek('s')
+
+    def test_read_all(self):
+        reader = OFReader("abcdefg")
+        reader.skip(2)
+        self.assertEquals(reader.read_all(), "cdefg")
+        self.assertEquals(reader.read_all(), "")
+
+    def test_slice(self):
+        reader = OFReader("abcdefg")
+        reader.skip(2)
+        self.assertEquals(reader.slice(3).read_all(), "cde")
+        self.assertEquals(reader.slice(2).read_all(), "fg")
+        self.assertEquals(reader.is_empty(), True)
+
 if __name__ == '__main__':
     unittest.main()