Skip to content

Commit e5b1ab0

Browse files
[canvaskit] Fix GIF decode failure (#161536)
Fixes an error when decoding GIFs to check if they are animated. The decoder needs to be more resilient in the face of Special Purpose blocks that are in the stream in places not specified in the GIF89a spec. Fixes flutter/flutter#161376 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 80942d5 commit e5b1ab0

2 files changed

Lines changed: 123 additions & 25 deletions

File tree

engine/src/flutter/lib/web_ui/lib/src/engine/image_format_detector.dart

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ class _GifHeaderReader {
282282
int framesFound = 0;
283283
// Read the GIF until we either find 2 frames or reach the end of the GIF.
284284
while (true) {
285+
_maybeSkipSpecialPurposeBlocks();
285286
final bool isTrailer = _checkForTrailer();
286287
if (isTrailer) {
287288
return framesFound > 1;
@@ -290,11 +291,7 @@ class _GifHeaderReader {
290291
// If we haven't reached the end, then the next block must either be a
291292
// graphic block or a special-purpose block (comment extension or
292293
// application extension).
293-
final bool isSpecialPurposeBlock = _checkForSpecialPurposeBlock();
294-
if (isSpecialPurposeBlock) {
295-
_skipSpecialPurposeBlock();
296-
continue;
297-
}
294+
_maybeSkipSpecialPurposeBlocks();
298295

299296
// If the next block isn't a special-purpose block, it must be a graphic
300297
// block. Increase the frame count, skip the graphic block, and keep
@@ -322,8 +319,15 @@ class _GifHeaderReader {
322319
return nextByte == 0x3b;
323320
}
324321

325-
/// Returns [true] if the next block is a Special-Purpose Block (either a
326-
/// Comment Extension or an Application Extension).
322+
/// Skip Special Purpose Blocks (they do not effect decoding).
323+
void _maybeSkipSpecialPurposeBlocks() {
324+
while (_checkForSpecialPurposeBlock()) {
325+
_skipSpecialPurposeBlock();
326+
}
327+
}
328+
329+
/// Returns [true] if the next block is a Special-Purpose Block (extension
330+
/// label between 0xFA and 0xFF).
327331
bool _checkForSpecialPurposeBlock() {
328332
final int extensionIntroducer = bytes.getUint8(_position);
329333
if (extensionIntroducer != 0x21) {
@@ -332,9 +336,8 @@ class _GifHeaderReader {
332336

333337
final int extensionLabel = bytes.getUint8(_position + 1);
334338

335-
// The Comment Extension label is 0xFE, the Application Extension Label is
336-
// 0xFF.
337-
return extensionLabel == 0xfe || extensionLabel == 0xff;
339+
// A Special Purpose Block has a label between 0xFA and 0xFF.
340+
return extensionLabel >= 0xfa && extensionLabel <= 0xff;
338341
}
339342

340343
/// Skips past the current control block.
@@ -364,17 +367,21 @@ class _GifHeaderReader {
364367

365368
/// Skip past the graphic block.
366369
void _skipGraphicBlock() {
370+
_maybeSkipSpecialPurposeBlocks();
367371
// Check for the optional Graphic Control Extension.
368372
if (_checkForGraphicControlExtension()) {
369373
_skipGraphicControlExtension();
370374
}
371375

376+
_maybeSkipSpecialPurposeBlocks();
372377
// Check if the Graphic Block is a Plain Text Extension.
373378
if (_checkForPlainTextExtension()) {
374379
_skipPlainTextExtension();
375380
return;
376381
}
377382

383+
_maybeSkipSpecialPurposeBlocks();
384+
378385
// This is a Table-Based Image block.
379386
assert(bytes.getUint8(_position) == 0x2c);
380387

engine/src/flutter/lib/web_ui/test/engine/image_format_detector_test.dart

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ Future<void> testMain() async {
2121

2222
Future<List<String>> createTestFiles() async {
2323
final HttpFetchResponse listingResponse = await httpFetch('/test_images/');
24-
final List<String> testFiles = (await listingResponse.json() as List<dynamic>).cast<String>();
24+
List<String> testFiles = (await listingResponse.json() as List<dynamic>).cast<String>();
25+
testFiles = testFiles.map((String baseName) => '/test_images/$baseName').toList();
2526

2627
// Sanity-check the test file list. If suddenly test files are moved or
2728
// deleted, and the test server returns an empty list, or is missing some
@@ -40,7 +41,7 @@ Future<void> testMain() async {
4041

4142
for (final String testFile in testFiles!) {
4243
test('can detect image type of $testFile', () async {
43-
final HttpFetchResponse response = await httpFetch('/test_images/$testFile');
44+
final HttpFetchResponse response = await httpFetch(testFile);
4445

4546
if (!response.hasPayload) {
4647
throw Exception('Unable to fetch() image test file "$testFile"');
@@ -50,23 +51,23 @@ Future<void> testMain() async {
5051

5152
// WebP files which are known to be animated.
5253
const List<String> animatedWebpFiles = <String>[
53-
'blendBG.webp',
54-
'required.webp',
55-
'stoplight_h.webp',
56-
'stoplight.webp',
54+
'/test_images/blendBG.webp',
55+
'/test_images/required.webp',
56+
'/test_images/stoplight_h.webp',
57+
'/test_images/stoplight.webp',
5758
];
5859

5960
// GIF files which are known to be animated.
6061
const List<String> animatedGifFiles = <String>[
61-
'alphabetAnim.gif',
62-
'colorTables.gif',
63-
'flightAnim.gif',
64-
'gif-transparent-index.gif',
65-
'randPixelsAnim.gif',
66-
'randPixelsAnim2.gif',
67-
'required.gif',
68-
'test640x479.gif',
69-
'xOffsetTooBig.gif',
62+
'/test_images/alphabetAnim.gif',
63+
'/test_images/colorTables.gif',
64+
'/test_images/flightAnim.gif',
65+
'/test_images/gif-transparent-index.gif',
66+
'/test_images/randPixelsAnim.gif',
67+
'/test_images/randPixelsAnim2.gif',
68+
'/test_images/required.gif',
69+
'/test_images/test640x479.gif',
70+
'/test_images/xOffsetTooBig.gif',
7071
];
7172

7273
final String testFileExtension = testFile.substring(testFile.lastIndexOf('.') + 1);
@@ -84,4 +85,94 @@ Future<void> testMain() async {
8485
expect(detectImageType(responseBytes), expectedImageType);
8586
});
8687
}
88+
89+
test('can decode GIF with many nonstandard Special Purpose Blocks', () async {
90+
expect(detectImageType(_createTestGif()), ImageType.animatedGif);
91+
});
92+
}
93+
94+
/// Generates a blank GIF to be used in tests.
95+
Uint8List _createTestGif({
96+
int width = 1,
97+
int height = 1,
98+
int numFrames = 2,
99+
bool includeManyCommentBlocks = true,
100+
}) {
101+
final List<int> bytes = <int>[];
102+
// Generate header.
103+
bytes.addAll('GIF'.codeUnits);
104+
bytes.addAll('89a'.codeUnits);
105+
106+
// Generate logical screen.
107+
List<int> padInt(int x) {
108+
assert(x >= 0 && x.bitLength <= 16);
109+
if (x.bitLength > 8) {
110+
return <int>[x >> 8, x & 0xff];
111+
}
112+
return <int>[0, x];
113+
}
114+
115+
bytes.addAll(padInt(width));
116+
bytes.addAll(padInt(height));
117+
// Indicate there is no Global Color Table.
118+
bytes.add(0x70);
119+
bytes.add(0);
120+
bytes.add(0);
121+
122+
// Generate data.
123+
List<int> generateCommentBlock() {
124+
final List<int> comment = <int>[];
125+
comment.add(0x21);
126+
comment.add(0xfe);
127+
const String commentString = 'This is a comment';
128+
comment.add(commentString.codeUnits.length);
129+
comment.addAll(commentString.codeUnits);
130+
comment.add(0);
131+
return comment;
132+
}
133+
134+
for (int i = 0; i < numFrames; i++) {
135+
if (includeManyCommentBlocks) {
136+
bytes.addAll(generateCommentBlock());
137+
}
138+
// Add a Graphic Control Extension block.
139+
bytes.add(0x21);
140+
bytes.add(0xf9);
141+
bytes.add(4);
142+
bytes.add(0);
143+
// Indicate a delay of 1/10 of a second between frames.
144+
bytes.add(0);
145+
bytes.add(10);
146+
bytes.add(0);
147+
bytes.add(0);
148+
149+
if (includeManyCommentBlocks) {
150+
bytes.addAll(generateCommentBlock());
151+
}
152+
153+
// Add a Table-Based Image.
154+
bytes.add(0x2c);
155+
bytes.add(0);
156+
bytes.add(0);
157+
bytes.add(0);
158+
bytes.add(0);
159+
bytes.addAll(padInt(width));
160+
bytes.addAll(padInt(height));
161+
bytes.add(0);
162+
163+
bytes.add(0);
164+
const String fakeImageData = 'This is an image';
165+
bytes.add(fakeImageData.codeUnits.length);
166+
bytes.addAll(fakeImageData.codeUnits);
167+
bytes.add(0);
168+
}
169+
170+
if (includeManyCommentBlocks) {
171+
bytes.addAll(generateCommentBlock());
172+
}
173+
174+
// Generate trailer.
175+
bytes.add(0x3b);
176+
177+
return Uint8List.fromList(bytes);
87178
}

0 commit comments

Comments
 (0)