Skip to content

Commit 58d7231

Browse files
jeddenleanigeltao
authored andcommitted
image/gif: make blockReader a ByteReader, harden tests
golang.org/cl/37258 was committed to fix issue #16146. This patch seemed intent to allow at most one dangling byte. But, as implemented, many more bytes may actually slip through. This is because the LZW layer creates a bufio.Reader which will itself consume data beyond the end of the LZW stream, and this isn't accounted for anywhere. This change means to avoid the allocation of the bufio.Reader by making blockReader implement io.ByteReader. Further, it adds a close() method which detects extra data in the block sequence. To avoid any regressions with poorly encoded GIFs which may have worked accidentally, there are no restrictions on how many extra bytes may exist in the final full sub-block that contained LZW data. If the end of the LZW stream happened to align with the end of a sub-block, at most one more sub-block with a length of 1 byte may exist before the block terminator. This change aims to be at least as performant as the prior implementation. But the primary gain is avoiding the allocation of a bufio.Reader per frame: name old time/op new time/op delta Decode-8 276µs ± 0% 275µs ± 2% ~ (p=0.690 n=5+5) name old speed new speed delta Decode-8 55.9MB/s ± 0% 56.3MB/s ± 2% ~ (p=0.690 n=5+5) name old alloc/op new alloc/op delta Decode-8 49.2kB ± 0% 44.8kB ± 0% -9.10% (p=0.008 n=5+5) name old allocs/op new allocs/op delta Decode-8 269 ± 0% 267 ± 0% -0.74% (p=0.008 n=5+5) Change-Id: Iec4f9b895561ad52266313fbc73ec82c070c3349 Reviewed-on: https://go-review.googlesource.com/68350 Run-TryBot: Emmanuel Odeke <emm.odeke@gmail.com> Reviewed-by: Nigel Tao <nigeltao@golang.org>
1 parent 717d375 commit 58d7231

File tree

2 files changed

+104
-39
lines changed

2 files changed

+104
-39
lines changed

src/image/gif/reader.go

Lines changed: 97 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -109,46 +109,112 @@ type decoder struct {
109109
tmp [1024]byte // must be at least 768 so we can read color table
110110
}
111111

112-
// blockReader parses the block structure of GIF image data, which
113-
// comprises (n, (n bytes)) blocks, with 1 <= n <= 255. It is the
114-
// reader given to the LZW decoder, which is thus immune to the
115-
// blocking. After the LZW decoder completes, there will be a 0-byte
116-
// block remaining (0, ()), which is consumed when checking that the
117-
// blockReader is exhausted.
112+
// blockReader parses the block structure of GIF image data, which comprises
113+
// (n, (n bytes)) blocks, with 1 <= n <= 255. It is the reader given to the
114+
// LZW decoder, which is thus immune to the blocking. After the LZW decoder
115+
// completes, there will be a 0-byte block remaining (0, ()), which is
116+
// consumed when checking that the blockReader is exhausted.
117+
//
118+
// To avoid the allocation of a bufio.Reader for the lzw Reader, blockReader
119+
// implements io.ReadByte and buffers blocks into the decoder's "tmp" buffer.
118120
type blockReader struct {
119-
r reader
120-
slice []byte
121-
err error
122-
tmp [256]byte
121+
d *decoder
122+
i, j uint8 // d.tmp[i:j] contains the buffered bytes
123+
err error
123124
}
124125

125-
func (b *blockReader) Read(p []byte) (int, error) {
126+
func (b *blockReader) fill() {
126127
if b.err != nil {
127-
return 0, b.err
128+
return
129+
}
130+
b.j, b.err = readByte(b.d.r)
131+
if b.j == 0 && b.err == nil {
132+
b.err = io.EOF
133+
}
134+
if b.err != nil {
135+
return
128136
}
129-
if len(p) == 0 {
130-
return 0, nil
137+
138+
b.i = 0
139+
b.err = readFull(b.d.r, b.d.tmp[:b.j])
140+
if b.err != nil {
141+
b.j = 0
131142
}
132-
if len(b.slice) == 0 {
133-
var blockLen uint8
134-
blockLen, b.err = b.r.ReadByte()
143+
}
144+
145+
func (b *blockReader) ReadByte() (byte, error) {
146+
if b.i == b.j {
147+
b.fill()
135148
if b.err != nil {
136149
return 0, b.err
137150
}
138-
if blockLen == 0 {
139-
b.err = io.EOF
140-
return 0, b.err
141-
}
142-
b.slice = b.tmp[:blockLen]
143-
if b.err = readFull(b.r, b.slice); b.err != nil {
151+
}
152+
153+
c := b.d.tmp[b.i]
154+
b.i++
155+
return c, nil
156+
}
157+
158+
// blockReader must implement io.Reader, but its Read shouldn't ever actually
159+
// be called in practice. The compress/lzw package will only call ReadByte.
160+
func (b *blockReader) Read(p []byte) (int, error) {
161+
if len(p) == 0 || b.err != nil {
162+
return 0, b.err
163+
}
164+
if b.i == b.j {
165+
b.fill()
166+
if b.err != nil {
144167
return 0, b.err
145168
}
146169
}
147-
n := copy(p, b.slice)
148-
b.slice = b.slice[n:]
170+
171+
n := copy(p, b.d.tmp[b.i:b.j])
172+
b.i += uint8(n)
149173
return n, nil
150174
}
151175

176+
// close primarily detects whether or not a block terminator was encountered
177+
// after reading a sequence of data sub-blocks. It allows at most one trailing
178+
// sub-block worth of data. I.e., if some number of bytes exist in one sub-block
179+
// following the end of LZW data, the very next sub-block must be the block
180+
// terminator. If the very end of LZW data happened to fill one sub-block, at
181+
// most one more sub-block of length 1 may exist before the block-terminator.
182+
// These accomodations allow us to support GIFs created by less strict encoders.
183+
// See https://golang.org/issue/16146.
184+
func (b *blockReader) close() error {
185+
if b.err == io.EOF {
186+
// A clean block-sequence terminator was encountered while reading.
187+
return nil
188+
} else if b.err != nil {
189+
// Some other error was encountered while reading.
190+
return b.err
191+
}
192+
193+
if b.i == b.j {
194+
// We reached the end of a sub block reading LZW data. We'll allow at
195+
// most one more sub block of data with a length of 1 byte.
196+
b.fill()
197+
if b.err == io.EOF {
198+
return nil
199+
} else if b.err != nil {
200+
return b.err
201+
} else if b.j > 1 {
202+
return errTooMuch
203+
}
204+
}
205+
206+
// Part of a sub-block remains buffered. We expect that the next attempt to
207+
// buffer a sub-block will reach the block terminator.
208+
b.fill()
209+
if b.err == io.EOF {
210+
return nil
211+
} else if b.err != nil {
212+
return b.err
213+
}
214+
215+
return errTooMuch
216+
}
217+
152218
// decode reads a GIF image from r and stores the result in d.
153219
func (d *decoder) decode(r io.Reader, configOnly, keepAllFrames bool) error {
154220
// Add buffering if r does not provide ReadByte.
@@ -222,7 +288,7 @@ func (d *decoder) decode(r io.Reader, configOnly, keepAllFrames bool) error {
222288
return fmt.Errorf("gif: pixel size in decode out of range: %d", litWidth)
223289
}
224290
// A wonderfully Go-like piece of magic.
225-
br := &blockReader{r: d.r}
291+
br := &blockReader{d: d}
226292
lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
227293
defer lzwr.Close()
228294
if err = readFull(lzwr, m.Pix); err != nil {
@@ -242,7 +308,7 @@ func (d *decoder) decode(r io.Reader, configOnly, keepAllFrames bool) error {
242308
// before the LZW decoder saw an explicit end code), provided that
243309
// the io.ReadFull call above successfully read len(m.Pix) bytes.
244310
// See https://golang.org/issue/9856 for an example GIF.
245-
if n, err := lzwr.Read(d.tmp[:1]); n != 0 || (err != io.EOF && err != io.ErrUnexpectedEOF) {
311+
if n, err := lzwr.Read(d.tmp[256:257]); n != 0 || (err != io.EOF && err != io.ErrUnexpectedEOF) {
246312
if err != nil {
247313
return fmt.Errorf("gif: reading image data: %v", err)
248314
}
@@ -251,18 +317,10 @@ func (d *decoder) decode(r io.Reader, configOnly, keepAllFrames bool) error {
251317

252318
// In practice, some GIFs have an extra byte in the data sub-block
253319
// stream, which we ignore. See https://golang.org/issue/16146.
254-
for nExtraBytes := 0; ; {
255-
n, err := br.Read(d.tmp[:2])
256-
nExtraBytes += n
257-
if nExtraBytes > 1 {
258-
return errTooMuch
259-
}
260-
if err == io.EOF {
261-
break
262-
}
263-
if err != nil {
264-
return fmt.Errorf("gif: reading image data: %v", err)
265-
}
320+
if err := br.close(); err == errTooMuch {
321+
return errTooMuch
322+
} else if err != nil {
323+
return fmt.Errorf("gif: reading image data: %v", err)
266324
}
267325

268326
// Check that the color indexes are inside the palette.

src/image/gif/reader_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"compress/lzw"
1010
"image"
1111
"image/color"
12+
"io"
1213
"io/ioutil"
1314
"reflect"
1415
"strings"
@@ -24,6 +25,9 @@ const (
2425
trailerStr = "\x3b"
2526
)
2627

28+
// lzw.NewReader wants a io.ByteReader, this ensures we're compatible.
29+
var _ io.ByteReader = (*blockReader)(nil)
30+
2731
// lzwEncode returns an LZW encoding (with 2-bit literals) of in.
2832
func lzwEncode(in []byte) []byte {
2933
b := &bytes.Buffer{}
@@ -67,6 +71,9 @@ func TestDecode(t *testing.T) {
6771
{2, 1, 0, nil},
6872
// Two extra bytes after LZW data, but inside the same data sub-block.
6973
{2, 2, 0, nil},
74+
// Extra data exists in the final sub-block with LZW data, AND there is
75+
// a bogus sub-block following.
76+
{2, 1, 1, errTooMuch},
7077
}
7178
for _, tc := range testCases {
7279
b := &bytes.Buffer{}

0 commit comments

Comments
 (0)