Skip to content

Commit fc69e9f

Browse files
committed
gh-156965: Make the utf-7-imap incremental decoder and stream reader stateful
utf_7_imap_decode() raised on a shift sequence with no terminator, so the incremental decoder and the stream reader, which both called it on whole chunks, failed whenever a chunk boundary fell inside a sequence. Give it a final parameter that reports the consumed byte count instead, and take the buffering from codecs.BufferedIncrementalDecoder, as utf_7 does.
1 parent e56f86f commit fc69e9f

2 files changed

Lines changed: 26 additions & 5 deletions

File tree

Lib/encodings/utf_7_imap.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def flush(end):
5454
flush(len(input))
5555
return res.take_bytes(), len(input)
5656

57-
def utf_7_imap_decode(input, errors='strict'):
57+
def utf_7_imap_decode(input, errors='strict', final=True):
5858
if errors != 'strict':
5959
raise UnicodeError(f"Unsupported error handling: {errors}")
6060
input = bytes(input)
@@ -71,6 +71,8 @@ def flush(end):
7171
flush(i)
7272
j = input.find(b'-', i + 1)
7373
if j < 0:
74+
if not final:
75+
return ''.join(res), i
7476
raise UnicodeDecodeError('utf-7-imap', input, i, n,
7577
'unterminated shift sequence')
7678
if j == i + 1: # '&-'
@@ -106,15 +108,16 @@ class IncrementalEncoder(codecs.IncrementalEncoder):
106108
def encode(self, input, final=False):
107109
return utf_7_imap_encode(input, self.errors)[0]
108110

109-
class IncrementalDecoder(codecs.IncrementalDecoder):
110-
def decode(self, input, final=False):
111-
return utf_7_imap_decode(input, self.errors)[0]
111+
class IncrementalDecoder(codecs.BufferedIncrementalDecoder):
112+
def _buffer_decode(self, input, errors, final):
113+
return utf_7_imap_decode(input, errors, final)
112114

113115
class StreamWriter(Codec, codecs.StreamWriter):
114116
pass
115117

116118
class StreamReader(Codec, codecs.StreamReader):
117-
pass
119+
def decode(self, input, errors='strict'):
120+
return utf_7_imap_decode(input, errors, False)
118121

119122
### encodings module API
120123

Lib/test/test_codecs.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,24 @@ def test_only_strict_errors(self):
14671467
with self.assertRaises(UnicodeError):
14681468
b'x'.decode('utf-7-imap', 'ignore')
14691469

1470+
@support.subTests('uni,encoded', utf_7_imap_testcases)
1471+
def test_incremental_decode(self, uni, encoded):
1472+
# A shift sequence split across chunks is not an error: the joined
1473+
# output has to match the stateless decoder.
1474+
decoder = codecs.getincrementaldecoder('utf-7-imap')()
1475+
out = ''.join(decoder.decode(encoded[i:i + 1])
1476+
for i in range(len(encoded)))
1477+
self.assertEqual(out + decoder.decode(b'', True), uni)
1478+
1479+
def test_stream_read(self):
1480+
# Reading a fixed number of characters stops inside a shift sequence.
1481+
uni = '\u53f0' * 2000
1482+
encoded = uni.encode('utf-7-imap')
1483+
with io.TextIOWrapper(io.BytesIO(encoded), encoding='utf-7-imap') as f:
1484+
self.assertEqual(f.read(100), uni[:100])
1485+
chunks = [encoded[i:i + 16] for i in range(0, len(encoded), 16)]
1486+
self.assertEqual(''.join(codecs.iterdecode(chunks, 'utf-7-imap')), uni)
1487+
14701488
def test_stateless(self):
14711489
# The codec is registered and exposes the standard interface.
14721490
info = codecs.lookup('utf-7-imap')

0 commit comments

Comments
 (0)