Repository: construct/construct

Issue:
build_file() crashes when using RawCopy with a value only
Thank you for construct!

Here's a failing test case:

    def test_rawcopy_build_file_from_value_only(tmpdir):
        d = RawCopy(Byte)
        d.build_file(dict(value=0), filename=tmpdir.join('test'))

Version: master branch, commit 2626b39.

Expected result: success

Actual result:

    construct.core.StreamError: Error in path (building)
    stream.read() failed, requested 1 bytes

The cause is that `Construct.build_file()` opens the stream for writing only, but `RawCopy._build()` assumes that the stream is readable when it rewinds the stream to read out what was written to populate the `data` attribute.

Pull request to follow.

Discussion:

Patch:
diff --git a/construct/core.py b/construct/core.py
index d2ec15e..785f4a0 100644
--- a/construct/core.py
+++ b/construct/core.py
@@ -351,7 +351,10 @@ class Construct(object):
         r"""
         Build an object into a closed binary file. See build().
         """
-        with open(filename, 'wb') as f:
+        # Open the file for reading as well as writing. This allows builders to
+        # read back the stream just written. For example. RawCopy does this.
+        # See issue #888.
+        with open(filename, 'w+b') as f:
             self.build_stream(obj, f, **contextkw)
 
     def _build(self, obj, stream, context, path):


Test patch:
diff --git a/tests/declarativeunittest.py b/tests/declarativeunittest.py
index d02a292..a39fb67 100644
--- a/tests/declarativeunittest.py
+++ b/tests/declarativeunittest.py
@@ -3,7 +3,7 @@ xfail = pytest.mark.xfail
 skip = pytest.mark.skip
 skipif = pytest.mark.skipif
 
-import os, math, random, collections, itertools, io, hashlib, binascii
+import os, math, random, collections, itertools, io, hashlib, binascii, tempfile
 
 from construct import *
 from construct.lib import *
diff --git a/tests/test_core.py b/tests/test_core.py
index 02e2e64..9da306c 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -930,6 +930,15 @@ def test_rawcopy_issue_358():
     d = Struct("a"/RawCopy(Byte), "check"/Check(this.a.value == 255))
     assert d.build(dict(a=dict(value=255))) == b"\xff"
 
+def test_rawcopy_issue_888():
+    # If you use build_file() on a RawCopy that has only a value defined, then
+    # RawCopy._build may also attempt to read from the file, which won't work
+    # if build_file opened the file for writing only.
+    with tempfile.TemporaryDirectory() as tmpdir:
+        fname = os.path.join(tmpdir, 'test')
+        d = RawCopy(Byte)
+        d.build_file(dict(value=0), filename=fname)
+
 def test_byteswapped():
     d = ByteSwapped(Bytes(5))
     common(d, b"12345", b"54321", 5)