Skip to content

Commit 68dd4bb

Browse files
committed
Small stream improvements
1 parent d9a1ccf commit 68dd4bb

1 file changed

Lines changed: 87 additions & 62 deletions

File tree

dissect/util/stream.py

Lines changed: 87 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,27 @@
1212

1313

1414
class AlignedStream(io.RawIOBase):
15-
"""Basic buffered stream that provides easy aligned reads.
15+
"""Basic buffered stream that provides aligned reads.
1616
1717
Must be subclassed for various stream implementations. Subclasses can implement:
18-
- _read(offset, length)
19-
- _seek(pos, whence=io.SEEK_SET)
18+
- :meth:`~AlignedStream._read`
19+
- :meth:`~AlignedStream._seek`
2020
21-
The offset and length for _read are guaranteed to be aligned. The only time
22-
that overriding _seek would make sense is if there's no known size of your stream,
23-
but still want to provide SEEK_END functionality.
21+
The offset and length for ``_read`` are guaranteed to be aligned for streams of a known size.
22+
If your stream has an unknown size (i.e. ``size == None``), reads of length ``-1`` (i.e. read until EOF) will be
23+
passed through to your implementation of ``_read``.
24+
The only time that overriding ``_seek`` would make sense is if there's no known size of your stream,
25+
but still want to provide ``SEEK_END`` functionality.
2426
25-
Most subclasses of AlignedStream take one or more file-like objects as source.
27+
Most subclasses of ``AlignedStream`` take one or more file-like objects as source.
2628
Operations on these subclasses, like reading, will modify the source file-like object as a side effect.
2729
2830
Args:
29-
size: The size of the stream. This is used in read and seek operations. None if unknown.
31+
size: The size of the stream. This is used in read and seek operations. ``None`` if unknown.
3032
align: The alignment size. Read operations are aligned on this boundary. Also determines buffer size.
33+
34+
.. automethod:: _read
35+
.. automethod:: _seek
3136
"""
3237

3338
def __init__(self, size: int | None = None, align: int = STREAM_BUFFER_SIZE):
@@ -39,26 +44,30 @@ def __init__(self, size: int | None = None, align: int = STREAM_BUFFER_SIZE):
3944
self._pos_align = 0
4045

4146
self._buf = None
42-
self._seek_lock = Lock()
47+
self._lock = Lock()
4348

44-
def _set_pos(self, pos: int) -> None:
45-
"""Update the position and aligned position within the stream."""
46-
new_pos_align = pos - (pos % self.align)
49+
def readable(self) -> bool:
50+
"""Indicate that the stream is readable."""
51+
return True
4752

48-
if self._pos_align != new_pos_align:
49-
self._pos_align = new_pos_align
50-
self._buf = None
53+
def seekable(self) -> bool:
54+
"""Indicate that the stream is seekable."""
55+
return True
5156

52-
self._pos = pos
57+
def seek(self, pos: int, whence: int = io.SEEK_SET) -> int:
58+
"""Seek the stream to the specified position.
5359
54-
def _fill_buf(self) -> None:
55-
"""Fill the alignment buffer if we can."""
56-
if self._buf or (self.size is not None and (self.size <= self._pos or self.size <= self._pos_align)):
57-
return
60+
Returns:
61+
The new stream position after seeking.
62+
"""
63+
with self._lock:
64+
pos = self._seek(pos, whence)
65+
self._set_pos(pos)
5866

59-
self._buf = self._read(self._pos_align, self.align)
67+
return pos
6068

6169
def _seek(self, pos: int, whence: int = io.SEEK_SET) -> int:
70+
"""Calculate and return the new stream position after a seek."""
6271
if whence == io.SEEK_SET:
6372
if pos < 0:
6473
raise ValueError(f"negative seek position {pos}")
@@ -73,16 +82,32 @@ def _seek(self, pos: int, whence: int = io.SEEK_SET) -> int:
7382

7483
return pos
7584

76-
def seek(self, pos: int, whence: int = io.SEEK_SET) -> int:
77-
"""Seek the stream to the specified position."""
78-
with self._seek_lock:
79-
pos = self._seek(pos, whence)
80-
self._set_pos(pos)
85+
def _set_pos(self, pos: int) -> None:
86+
"""Update the position and aligned position within the stream."""
87+
new_pos_align = pos - (pos % self.align)
8188

82-
return pos
89+
if self._pos_align != new_pos_align:
90+
self._pos_align = new_pos_align
91+
self._buf = None
92+
93+
self._pos = pos
94+
95+
def tell(self) -> int:
96+
"""Return current stream position."""
97+
return self._pos
98+
99+
def _fill_buf(self) -> None:
100+
"""Fill the alignment buffer if we can."""
101+
if self._buf or (self.size is not None and (self.size <= self._pos or self.size <= self._pos_align)):
102+
# Don't fill the buffer if:
103+
# - We already have a buffer
104+
# - The stream position is at the end (or beyond) the stream size
105+
return
106+
107+
self._buf = self._read(self._pos_align, self.align)
83108

84109
def read(self, n: int = -1) -> bytes:
85-
"""Read and return up to n bytes, or read to the end of the stream if n is -1.
110+
"""Read and return up to ``n`` bytes, or read to the end of the stream if ``n`` is ``-1``.
86111
87112
Returns an empty bytes object on EOF.
88113
"""
@@ -93,7 +118,7 @@ def read(self, n: int = -1) -> bytes:
93118
size = self.size
94119
align = self.align
95120

96-
with self._seek_lock:
121+
with self._lock:
97122
if size is None and n == -1:
98123
r = []
99124
if self._buf:
@@ -107,10 +132,12 @@ def read(self, n: int = -1) -> bytes:
107132
self._set_pos(self._pos + len(buf))
108133
return buf
109134

135+
# If we know the stream size, adjust n
110136
if size is not None:
111137
remaining = size - self._pos
112138
n = remaining if n == -1 else min(n, remaining)
113139

140+
# Short path for when it turns out we don't need to read anything
114141
if n == 0 or (size is not None and size <= self._pos):
115142
return b""
116143

@@ -136,7 +163,7 @@ def read(self, n: int = -1) -> bytes:
136163

137164
self._set_pos(self._pos + read_len)
138165

139-
# Misaligned end
166+
# Misaligned remaining bytes
140167
if n > 0:
141168
self._fill_buf()
142169
r.append(self._buf[:n])
@@ -163,22 +190,28 @@ def readall(self) -> bytes:
163190
return self.read()
164191

165192
def readoffset(self, offset: int, length: int) -> bytes:
166-
"""Convenience method to read from a certain offset with 1 call."""
193+
"""Convenience method to read from a given offset.
194+
195+
Args:
196+
offset: The offset in the stream to read from.
197+
length: The number of bytes to read.
198+
"""
167199
self.seek(offset)
168200
return self.read(length)
169201

170-
def tell(self) -> int:
171-
"""Return current stream position."""
172-
return self._pos
202+
def peek(self, n: int) -> bytes:
203+
"""Convenience method to peek from the current offset without advancing the stream position.
173204
174-
def close(self) -> None:
175-
pass
176-
177-
def readable(self) -> bool:
178-
return True
205+
Args:
206+
n: The number of bytes to peek.
207+
"""
208+
pos = self._pos
209+
data = self.read(n)
210+
self._set_pos(pos)
211+
return data
179212

180-
def seekable(self) -> bool:
181-
return True
213+
def close(self) -> None:
214+
"""Close the stream. Does nothing by default."""
182215

183216

184217
class RangeStream(AlignedStream):
@@ -198,18 +231,25 @@ class RangeStream(AlignedStream):
198231
align: The alignment size.
199232
"""
200233

201-
def __init__(self, fh: BinaryIO, offset: int, size: int, align: int = STREAM_BUFFER_SIZE):
234+
def __init__(self, fh: BinaryIO, offset: int, size: int | None, align: int = STREAM_BUFFER_SIZE):
202235
super().__init__(size, align)
203236
self._fh = fh
204237
self.offset = offset
205238

239+
def _seek(self, pos: int, whence: int = io.SEEK_SET) -> int:
240+
if self.size is None and whence == io.SEEK_END:
241+
if (pos := self._fh.seek(pos, whence)) is None:
242+
pos = self._fh.tell()
243+
return max(0, pos - self.offset)
244+
return super()._seek(pos, whence)
245+
206246
def _read(self, offset: int, length: int) -> bytes:
207-
read_length = min(length, self.size - offset)
247+
read_length = min(length, self.size - offset) if self.size else length
208248
self._fh.seek(self.offset + offset)
209249
return self._fh.read(read_length)
210250

211251

212-
class RelativeStream(AlignedStream):
252+
class RelativeStream(RangeStream):
213253
"""Create a relative stream from another file-like object.
214254
215255
ASCII representation::
@@ -222,27 +262,12 @@ class RelativeStream(AlignedStream):
222262
Args:
223263
fh: The source file-like object.
224264
offset: The offset the stream should start from on the source file-like object.
225-
size: The size the stream should be.
265+
size: Optional size the stream should be.
226266
align: The alignment size.
227267
"""
228268

229269
def __init__(self, fh: BinaryIO, offset: int, size: int | None = None, align: int = STREAM_BUFFER_SIZE):
230-
super().__init__(size, align)
231-
self._fh = fh
232-
self.offset = offset
233-
234-
def _seek(self, pos: int, whence: int = io.SEEK_SET) -> int:
235-
if whence == io.SEEK_END:
236-
pos = self._fh.seek(pos, whence)
237-
if pos is None:
238-
pos = self._fh.tell()
239-
return max(0, pos - self.offset)
240-
return super()._seek(pos, whence)
241-
242-
def _read(self, offset: int, length: int) -> bytes:
243-
read_length = min(length, self.size - offset) if self.size else length
244-
self._fh.seek(self.offset + offset)
245-
return self._fh.read(read_length)
270+
super().__init__(fh, offset, size, align)
246271

247272

248273
class BufferedStream(RelativeStream):
@@ -459,7 +484,7 @@ def add(self, offset: int, data: bytes | BinaryIO, size: int | None = None) -> N
459484
460485
Args:
461486
offset: The offset in bytes to add an overlay at.
462-
data: The bytes or file-like object to overlay
487+
data: The bytes or file-like object to overlay.
463488
size: Optional size specification of the overlay, if it can't be inferred.
464489
"""
465490
if not hasattr(data, "read"):

0 commit comments

Comments
 (0)