Repository: eerimoq/bincopy

Issue:
First chunk is misaligned if segment has non-aligned start address
```python
records = """
    :02000004000AF0
    :10B8440000000000000000009630000007770000B0
"""
hexfile = bincopy.BinFile()
hexfile.add_ihex(records)
align = 8
chunks = hexfile.segments.chunks(size=16, alignment=align)
assert not any(c.address % align for c in chunks)
```

IMO, the first chunk should be padded such that it is also aligned.

Comments:

Patch:
diff --git a/bincopy.py b/bincopy.py
index c26b486..fbe3ba5 100755
--- a/bincopy.py
+++ b/bincopy.py
@@ -361,22 +361,39 @@ class Segment:
     def address(self):
         return self.minimum_address // self.word_size_bytes
 
-    def chunks(self, size=32, alignment=1):
-        """Return chunks of the data aligned as given by `alignment`. `size`
-        must be a multiple of `alignment`. Each chunk is itself a Segment.
-        Both `size` and `alignment` are in words.
+    def chunks(self, size=32, alignment=1, padding=b''):
+        """Yield data chunks of `size` words, aligned as given by `alignment`.
+
+        Each chunk is itself a Segment.
+
+        `size` and `alignment` are in words. `size` must be a multiple of
+        `alignment`. If set, `padding` must be a word value.
+
+        If `padding` is set, the first and final chunks are padded so that:
+            1. The first chunk is aligned even if the segment itself is not.
+            2. The final chunk's size is a multiple of `alignment`.
 
         """
 
         if (size % alignment) != 0:
             raise Error(f'size {size} is not a multiple of alignment {alignment}')
 
+        if padding and len(padding) != self.word_size_bytes:
+            raise Error(f'padding must be a word value (size {self.word_size_bytes}),'
+                        f' got {padding}')
+
         size *= self.word_size_bytes
         alignment *= self.word_size_bytes
         address = self.minimum_address
         data = self.data
 
-        # First chunk may be shorter than `size` due to alignment.
+        # Apply padding to first and final chunk, if padding is non-empty.
+        align_offset = address % alignment
+        address -= align_offset * bool(padding)
+        data = align_offset // self.word_size_bytes * padding + data
+        data += (alignment - len(data)) % alignment // self.word_size_bytes * padding
+
+        # First chunk may be non-aligned and shorter than `size` if padding is empty.
         chunk_offset = (address % alignment)
 
         if chunk_offset != 0:
@@ -632,21 +649,51 @@ class Segments:
 
         self._list = new_list
 
-    def chunks(self, size=32, alignment=1):
-        """Iterate over all segments and return chunks of the data aligned as
-        given by `alignment`. `size` must be a multiple of
-        `alignment`. Each chunk is in turn a smaller Segment. Both `size` and
-        `alignment` are in words.
+    def chunks(self, size=32, alignment=1, padding=b''):
+        """Iterate over all segments and yield chunks of the data.
+        
+        The chunks are `size` words long, aligned as given by `alignment`.
+
+        Each chunk is itself a Segment.
+
+        `size` and `alignment` are in words. `size` must be a multiple of
+        `alignment`. If set, `padding` must be a word value.
+
+        If `padding` is set, the first and final chunks of each segment are
+        padded so that:
+            1. The first chunk is aligned even if the segment itself is not.
+            2. The final chunk's size is a multiple of `alignment`.
 
         """
 
         if (size % alignment) != 0:
             raise Error(f'size {size} is not a multiple of alignment {alignment}')
 
+        if padding and len(padding) != self.word_size_bytes:
+            raise Error(f'padding must be a word value (size {self.word_size_bytes}),'
+                        f' got {padding}')
+
+        previous = Segment(-1, -1, b'', 1)
+
         for segment in self:
-            for chunk in segment.chunks(size, alignment):
+            for chunk in segment.chunks(size, alignment, padding):
+                # When chunks are padded to alignment, the final chunk of the previous
+                # segment and the first chunk of the current segment may overlap by
+                # one alignment block. To avoid overwriting data from the lower
+                # segment, the chunks must be merged.
+                if chunk.address < previous.address + len(previous):
+                    low = previous.data[-alignment * self.word_size_bytes:]
+                    high = chunk.data[:alignment * self.word_size_bytes]
+                    merged = int.to_bytes(int.from_bytes(low, 'big') ^
+                                          int.from_bytes(high, 'big') ^
+                                          int.from_bytes(alignment * padding, 'big'),
+                                          alignment * self.word_size_bytes, 'big')
+                    chunk.data = merged + chunk.data[alignment * self.word_size_bytes:]
+
                 yield chunk
 
+            previous = chunk
+
     def __len__(self):
         """Get the number of segments.
 


Test patch:
diff --git a/tests/test_bincopy.py b/tests/test_bincopy.py
index 3a81584..945086a 100644
--- a/tests/test_bincopy.py
+++ b/tests/test_bincopy.py
@@ -910,6 +910,12 @@ class BinCopyTest(unittest.TestCase):
         self.assertEqual(str(cm.exception),
                          'size 4 is not a multiple of alignment 8')
 
+        with self.assertRaises(bincopy.Error) as cm:
+            list(binfile.segments.chunks(padding=b'\xff\xff'))
+
+        self.assertEqual(str(cm.exception),
+                         r"padding must be a word value (size 1), got b'\xff\xff'")
+
     def test_segment(self):
         binfile = bincopy.BinFile()
         binfile.add_binary(b'\x00\x01\x02\x03\x04', 2)
@@ -1863,6 +1869,44 @@ Data ranges:
         first_word = int.from_bytes(binfile[:binfile.minimum_address + 1], 'little')
         self.assertEqual(0xC9E4, first_word)
 
+    def test_chunk_padding(self):
+        records = (':02000004000AF0\n'
+                   ':10B8440000000000000000009630000007770000B0\n')
+        hexfile = bincopy.BinFile()
+        hexfile.add_ihex(records)
+        align = 8
+        size = 16
+        chunks = hexfile.segments.chunks(size=size, alignment=align, padding=b'\xff')
+        chunks = list(chunks)
+        assert not any(c.address % align for c in chunks)
+        assert not any(len(c) % align for c in chunks)
+
+    def test_merge_chunks(self):
+        records = (':0A0000001010101010101010101056\n'
+                   ':0A000E001010101010101010101048\n')
+        hexfile = bincopy.BinFile()
+        hexfile.add_ihex(records)
+        align = 8
+        size = 16
+        chunks = hexfile.segments.chunks(size=size, alignment=align, padding=b'\xff')
+        chunks = list(chunks)
+        assert list(chunks[-1]) == [8, b'\x10\x10\xff\xff\xff\xff\x10\x10\x10\x10\x10'
+                                       b'\x10\x10\x10\x10\x10']
+
+    def test_merge_chunks_16(self):
+        records = (':1000000010101010101010101010101010101010F0\n'
+                   ':10000A0010101010101010101010101010101010E6\n')
+        hexfile = bincopy.BinFile(word_size_bits=16)
+        hexfile.add_ihex(records)
+        align = 6
+        size = 12
+        chunks = hexfile.segments.chunks(size=size, alignment=align,
+                                         padding=b'\xff\xff')
+        chunks = list(chunks)
+        assert list(chunks[-1]) == [6, b'\x10\x10\x10\x10\xff\xff\xff\xff\x10\x10\x10'
+                                       b'\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'
+                                       b'\x10\x10']
+
 
 if __name__ == '__main__':
     unittest.main()