Repository: construct/construct

Issue:
Error message says 'bytes', not bits when there aren't enough bits for BitsInteger
If you try to read `x` more bits than are available using a BitsInteger, the error message incorrectly states that you are trying to request `x` extra bytes, not bits.

Example code:

```python
from construct import BitStruct, Bit, BitsInteger

MY_MESSAGE = BitStruct(
    Bit[7],
    BitsInteger(17)
)

MY_MESSAGE.parse(b'A')
```

```
Traceback (most recent call last):
  File "test.py", line 8, in <module>
    MY_MESSAGE.parse(b'A')
  File "env\lib\site-packages\construct\core.py", line 165, in parse
    return self.parse_stream(BytesIO(data), context, **kw)
  File "env\lib\site-packages\construct\core.py", line 176, in parse_stream
    return self._parse(stream, context, "parsing")
  File "env\lib\site-packages\construct\core.py", line 1791, in _parse
    obj = self.subcon._parse(self.stream2, context, path)
  File "env\lib\site-packages\construct\core.py", line 849, in _parse
    subobj = sc._parse(stream, context, path)
  File "env\lib\site-packages\construct\core.py", line 576, in _parse
    data = _read_stream(stream, length)
  File "env\lib\site-packages\construct\core.py", line 72, in _read_stream
    data = stream.read(length)
  File "env\lib\site-packages\construct\lib\bitstream.py", line 26, in read
    raise IOError("Restreamed cannot satisfy read request of %d bytes" % count)
OSError: Restreamed cannot satisfy read request of 17 bytes
```

While trying to parse a complicated object, I was confused as to what combination of my bit fields added to the number of bytes presented in the error message.

In the above example, it should report `OSError: Restreamed cannot satisfy read request of 17 bits`

Discussion:

Patch:
diff --git a/construct/core.py b/construct/core.py
index ec37366..976384f 100644
--- a/construct/core.py
+++ b/construct/core.py
@@ -64,26 +64,26 @@ def singleton(cls):
 def singletonfunction(func):
     return func()
 
-def _read_stream(stream, length):
+def _read_stream(stream, length, unitname="bytes"):
     # if not isinstance(length, int):
     #     raise TypeError("expected length to be int")
     if length < 0:
         raise ValueError("length must be >= 0", length)
     data = stream.read(length)
     if len(data) != length:
-        raise FieldError("could not read enough bytes, expected %d, found %d" % (length, len(data)))
+        raise FieldError("could not read enough %s, expected %d, found %d" % (unitname, length, len(data)))
     return data
 
-def _write_stream(stream, length, data):
+def _write_stream(stream, length, data, unitname="bytes"):
     # if not isinstance(data, bytes):
     #     raise TypeError("expected data to be a bytes")
     if length < 0:
         raise ValueError("length must be >= 0", length)
     if len(data) != length:
-        raise FieldError("could not write bytes, expected %d, found %d" % (length, len(data)))
+        raise FieldError("could not write %s, expected %d, found %d" % (unitname, length, len(data)))
     written = stream.write(data)
     if written is not None and written != length:
-        raise FieldError("could not write bytes, written %d, should %d" % (written, length))
+        raise FieldError("could not write %s, written %d, should %d" % (unitname, written, length))
 
 
 #===============================================================================
@@ -481,7 +481,7 @@ def Bitwise(subcon):
         >>> Bitwise(Octet).sizeof()
         1
     """
-    return Restreamed(subcon, bits2bytes, 8, bytes2bits, 1, lambda n: n//8)
+    return Restreamed(subcon, bits2bytes, 8, bytes2bits, 1, "bits", lambda n: n//8)
 
 
 def Bytewise(subcon):
@@ -499,7 +499,7 @@ def Bytewise(subcon):
         >>> Bitwise(Bytewise(Byte)).sizeof()
         1
     """
-    return Restreamed(subcon, bytes2bits, 1, bits2bytes, 8, lambda n: n*8)
+    return Restreamed(subcon, bytes2bits, 1, bits2bytes, 8, "bytes", lambda n: n*8)
 
 
 class BytesInteger(Construct):
@@ -577,7 +577,7 @@ class BitsInteger(Construct):
         self.bytesize = bytesize
     def _parse(self, stream, context, path):
         length = self.length(context) if callable(self.length) else self.length
-        data = _read_stream(stream, length)
+        data = _read_stream(stream, length, "bits")
         if self.swapped:
             data = swapbytes(data, self.bytesize)
         return bits2integer(data, self.signed)
@@ -1767,6 +1767,7 @@ class Restreamed(Subconstruct):
     :param encoderunit: ratio as int, encoder takes that many bytes at once
     :param decoder: a function that takes a b-string and returns a b-string (used when parsing)
     :param decoderunit: ratio as int, decoder takes that many bytes at once
+    :param decoderunitname: English string that describes the units (plural) returned by the decoder. Used for error messages.
     :param sizecomputer: a function that computes amount of bytes outputed by some bytes
 
     Example::
@@ -1774,21 +1775,23 @@ class Restreamed(Subconstruct):
         Bitwise  <--> Restreamed(subcon, bits2bytes, 8, bytes2bits, 1, lambda n: n//8)
         Bytewise <--> Restreamed(subcon, bytes2bits, 1, bits2bytes, 8, lambda n: n*8)
     """
-    __slots__ = ["sizecomputer", "encoder", "encoderunit", "decoder", "decoderunit"]
-    def __init__(self, subcon, encoder, encoderunit, decoder, decoderunit, sizecomputer):
+
+    __slots__ = ["sizecomputer", "encoder", "encoderunit", "decoder", "decoderunit", "decoderunitname"]
+    def __init__(self, subcon, encoder, encoderunit, decoder, decoderunit, decoderunitname, sizecomputer):
         super(Restreamed, self).__init__(subcon)
         self.encoder = encoder
         self.encoderunit = encoderunit
         self.decoder = decoder
         self.decoderunit = decoderunit
+        self.decoderunitname = decoderunitname
         self.sizecomputer = sizecomputer
     def _parse(self, stream, context, path):
-        stream2 = RestreamedBytesIO(stream, self.encoder, self.encoderunit, self.decoder, self.decoderunit)
+        stream2 = RestreamedBytesIO(stream, self.encoder, self.encoderunit, self.decoder, self.decoderunit, self.decoderunitname)
         obj = self.subcon._parse(stream2, context, path)
         stream2.close()
         return obj
     def _build(self, obj, stream, context, path):
-        stream2 = RestreamedBytesIO(stream, self.encoder, self.encoderunit, self.decoder, self.decoderunit)
+        stream2 = RestreamedBytesIO(stream, self.encoder, self.encoderunit, self.decoder, self.decoderunit, self.decoderunitname)
         buildret = self.subcon._build(obj, stream2, context, path)
         stream2.close()
         return buildret
@@ -2163,6 +2166,7 @@ def ByteSwapped(subcon):
     return Restreamed(subcon,
         lambda s: s[::-1], subcon.sizeof(),
         lambda s: s[::-1], subcon.sizeof(),
+        "bytes",
         lambda n: n)
 
 
@@ -2182,6 +2186,7 @@ def BitsSwapped(subcon):
     return Restreamed(subcon,
         lambda s: bits2bytes(bytes2bits(s)[::-1]), 1,
         lambda s: bits2bytes(bytes2bits(s)[::-1]), 1,
+        "bits",
         lambda n: n)
 
 
diff --git a/construct/lib/bitstream.py b/construct/lib/bitstream.py
index 76c34ca..5425c05 100644
--- a/construct/lib/bitstream.py
+++ b/construct/lib/bitstream.py
@@ -5,14 +5,15 @@ from sys import maxsize
 
 
 class RestreamedBytesIO(object):
-    __slots__ = ["substream", "encoder", "encoderunit", "decoder", "decoderunit", "rbuffer", "wbuffer","sincereadwritten"]
+    __slots__ = ["substream", "encoder", "encoderunit", "decoder", "decoderunit", "decoderunitname", "rbuffer", "wbuffer","sincereadwritten"]
 
-    def __init__(self, substream, encoder, encoderunit, decoder, decoderunit):
+    def __init__(self, substream, encoder, encoderunit, decoder, decoderunit, decoderunitname):
         self.substream = substream
         self.encoder = encoder
         self.encoderunit = encoderunit
         self.decoder = decoder
         self.decoderunit = decoderunit
+        self.decoderunitname = decoderunitname
         self.rbuffer = b""
         self.wbuffer = b""
         self.sincereadwritten = 0
@@ -23,7 +24,7 @@ class RestreamedBytesIO(object):
         while len(self.rbuffer) < count:
             data = self.substream.read(self.decoderunit)
             if data is None or len(data) == 0:
-                raise IOError("Restreamed cannot satisfy read request of %d bytes" % count)
+                raise IOError("Restreamed cannot satisfy read request of %d %s" % (count, self.decoderunitname))
             self.rbuffer += self.decoder(data)
         data, self.rbuffer = self.rbuffer[:count], self.rbuffer[count:]
         self.sincereadwritten += count
@@ -40,9 +41,9 @@ class RestreamedBytesIO(object):
 
     def close(self):
         if len(self.rbuffer):
-            raise ValueError("closing stream but %d unread bytes remain, %d is decoded unit" % (len(self.rbuffer), self.decoderunit))
+            raise ValueError("closing stream but %d unread %s remain, %d is decoded unit" % (len(self.rbuffer), self.decoderunitname, self.decoderunit))
         if len(self.wbuffer):
-            raise ValueError("closing stream but %d unwritten bytes remain, %d is encoded unit" % (len(self.wbuffer), self.encoderunit))
+            raise ValueError("closing stream but %d unwritten %s remain, %d is encoded unit" % (len(self.wbuffer), self.decoderunitname, self.encoderunit))
 
     def seekable(self):
         return False


Test patch:
diff --git a/tests/test_all.py b/tests/test_all.py
index 04eed61..ad0997d 100644
--- a/tests/test_all.py
+++ b/tests/test_all.py
@@ -1084,13 +1084,13 @@ class TestCore(unittest.TestCase):
         Struct(ProbeInto(this.inner)).build({})
 
     def test_restreamed(self):
-        assert Restreamed(Int16ub, ident, 1, ident, 1, ident).parse(b"\x00\x01") == 1
-        assert Restreamed(Int16ub, ident, 1, ident, 1, ident).build(1) == b"\x00\x01"
-        assert Restreamed(Int16ub, ident, 1, ident, 1, ident).sizeof() == 2
-        assert raises(Restreamed(VarInt, ident, 1, ident, 1, ident).sizeof) == SizeofError
-        assert Restreamed(Bytes(2), None, None, lambda b: b*2, 1, None).parse(b"a") == b"aa"
-        assert Restreamed(Bytes(1), lambda b: b*2, 1, None, None, None).build(b"a") == b"aa"
-        assert Restreamed(Bytes(5), None, None, None, None, lambda n: n*2).sizeof() == 10
+        assert Restreamed(Int16ub, ident, 1, ident, 1, "bytes", ident).parse(b"\x00\x01") == 1
+        assert Restreamed(Int16ub, ident, 1, ident, 1, "bytes", ident).build(1) == b"\x00\x01"
+        assert Restreamed(Int16ub, ident, 1, ident, 1, "bytes", ident).sizeof() == 2
+        assert raises(Restreamed(VarInt, ident, 1, ident, 1, "bytes", ident).sizeof) == SizeofError
+        assert Restreamed(Bytes(2), None, None, lambda b: b*2, 1, "bytes", None).parse(b"a") == b"aa"
+        assert Restreamed(Bytes(1), lambda b: b*2, 1, None, None, "bytes", None).build(b"a") == b"aa"
+        assert Restreamed(Bytes(5), None, None, None, None, "bytes", lambda n: n*2).sizeof() == 10
 
     def test_rebuffered(self):
         data = b"0" * 1000