@@ -37,7 +37,7 @@ def readable(self):
3737 return True
3838
3939 def __init__ (self , fp , decomp_factory , trailing_error = (), * ,
40- stream_magic = (), ** decomp_args ):
40+ stream_magic = (), stream_padding = False , ** decomp_args ):
4141 self ._fp = fp
4242 self ._eof = False
4343 self ._pos = 0 # Current offset in decompressed stream
@@ -62,6 +62,12 @@ def __init__(self, fp, decomp_factory, trailing_error=(), *,
6262 # stream rather than trailing data, so its error is not ignored.
6363 self ._stream_magic = stream_magic
6464
65+ # Whether a stream that starts with stream_magic (bytes) may be
66+ # followed by Stream Padding, as in the .xz format. _head holds the
67+ # first bytes of the current stream.
68+ self ._stream_padding = stream_padding and bool (stream_magic )
69+ self ._head = b""
70+
6571 def close (self ):
6672 self ._decompressor = None
6773 return super ().close ()
@@ -88,11 +94,15 @@ def read(self, size=-1):
8894 if self ._decompressor .eof :
8995 rawblock = (self ._decompressor .unused_data or
9096 self ._fp .read (BUFFER_SIZE ))
97+ if self ._stream_padding and self ._head == self ._stream_magic :
98+ rawblock = self ._skip_padding (rawblock )
9199 if not rawblock :
92100 break
93101 # Continue to next stream.
94102 self ._decompressor = self ._decomp_factory (
95103 ** self ._decomp_args )
104+ if self ._stream_padding :
105+ self ._head = rawblock [:len (self ._stream_magic )]
96106 try :
97107 data = self ._decompressor .decompress (rawblock , size )
98108 except self ._trailing_error :
@@ -109,6 +119,10 @@ def read(self, size=-1):
109119 "end-of-stream marker was reached" )
110120 else :
111121 rawblock = b""
122+ if (self ._stream_padding
123+ and len (self ._head ) < len (self ._stream_magic )):
124+ self ._head += rawblock [:len (self ._stream_magic )
125+ - len (self ._head )]
112126 data = self ._decompressor .decompress (rawblock , size )
113127 if data :
114128 break
@@ -129,12 +143,30 @@ def readall(self):
129143
130144 return b"" .join (chunks )
131145
146+ def _skip_padding (self , rawblock ):
147+ # Skip the null bytes at the start of the data following a stream
148+ # if they are Stream Padding, whose size is a multiple of four.
149+ size = 0
150+ while True :
151+ rest = rawblock .lstrip (b"\0 " )
152+ size += len (rawblock ) - len (rest )
153+ if rest :
154+ break
155+ rawblock = self ._fp .read (BUFFER_SIZE )
156+ if not rawblock :
157+ break
158+ if size % 4 :
159+ # Not Stream Padding: leave the null bytes to the decompressor.
160+ return bytes (size ) + rest
161+ return rest
162+
132163 # Rewind the file to the beginning of the data stream.
133164 def _rewind (self ):
134165 self ._fp .seek (0 )
135166 self ._eof = False
136167 self ._pos = 0
137168 self ._decompressor = self ._decomp_factory (** self ._decomp_args )
169+ self ._head = b""
138170
139171 def seek (self , offset , whence = io .SEEK_SET ):
140172 # Recalculate offset as an absolute file position.
@@ -171,15 +203,37 @@ def tell(self):
171203 return self ._pos
172204
173205
206+ def _padding_size (data , pos ):
207+ """Return the size of the Stream Padding at *data[pos:]*, or 0.
208+
209+ Stream Padding is a run of null bytes whose size is a multiple of four.
210+ """
211+ end = pos
212+ chunk_size = 4
213+ while end < len (data ):
214+ with data [end :end + chunk_size ] as chunk :
215+ block = chunk .tobytes ()
216+ rest = len (block .lstrip (b"\0 " ))
217+ end += len (block ) - rest
218+ if rest :
219+ break
220+ chunk_size *= 2
221+ size = end - pos
222+ return 0 if size % 4 else size
223+
224+
174225def _decompress_all (data , decomp_factory , trailing_error = (), * ,
175- stream_magic = (), truncated_error , ** decomp_args ):
226+ stream_magic = (), stream_padding = False , truncated_error ,
227+ ** decomp_args ):
176228 """Decompress all of the concatenated streams in *data*.
177229
178230 Each stream is decompressed by a new decompressor, made by calling
179231 *decomp_factory* with *decomp_args*. As in DecompressReader, an error
180232 of class *trailing_error* in a stream after the first one ends the
181233 data, unless that stream starts with *stream_magic*. A stream without
182- an end-of-stream marker raises *truncated_error*.
234+ an end-of-stream marker raises *truncated_error*. If *stream_padding*
235+ is true, a stream that starts with *stream_magic* (bytes) may be
236+ followed by Stream Padding, as in the .xz format.
183237 """
184238 results = []
185239 # The views are released on exit, so that the caller can resize a
@@ -220,6 +274,10 @@ def _decompress_all(data, decomp_factory, trailing_error=(), *,
220274 raise truncated_error ("Compressed data ended before the "
221275 "end-of-stream marker was reached" )
222276 pos -= len (decomp .unused_data )
277+ if stream_padding and stream_magic :
278+ with data [start :start + len (stream_magic )] as head :
279+ if head == stream_magic :
280+ pos += _padding_size (data , pos )
223281 if pos == len (data ):
224282 break
225283 # Later streams start small: a larger first chunk reads further
0 commit comments