Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/reader/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,18 @@ impl StreamingDecoder {
}
ExtensionDataSubBlockStart(sub_block_len) => {
self.ext.data.clear();
goto!(0, ExtensionDataSubBlock(sub_block_len))
if sub_block_len == 0 {
// A zero-length sub-block is a block terminator (GIF89a spec §23).
// Short-circuit here rather than entering ExtensionDataSubBlock(0),
// which would re-present the next record byte and misparse it as a
// new sub-block length.
if self.ext.id.into_known() == Some(Extension::Control) {
self.read_control_extension()?;
}
goto!(0, ExtensionBlockEnd, emit Decoded::SubBlock { ext: self.ext.id, is_last: true })
} else {
goto!(0, ExtensionDataSubBlock(sub_block_len))
}
}
ExtensionDataSubBlock(left) => {
if left > 0 {
Expand Down
71 changes: 71 additions & 0 deletions tests/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,74 @@ fn check_last_extension_returns() {

assert_eq!(xmp_len, EXPECTED_METADATA.len() + 257);
}

// A GIF with an empty comment extension (`21 fe 00`) immediately before the image
// descriptor should decode to exactly one frame.
#[test]
fn empty_comment_extension_decodes_one_frame() {
// Minimal hand-crafted GIF: 1x1 pixel, global color table (2 colors),
// empty comment extension (21 fe 00), then a single image frame.
// The same bytes without the comment extension decode correctly (verified).
#[rustfmt::skip]
let gif_bytes: &[u8] = &[
// Header
b'G', b'I', b'F', b'8', b'9', b'a',
// Logical Screen Descriptor: 1x1, GCT flag set, GCT size=0 (2 colors)
0x01, 0x00, // width=1
0x01, 0x00, // height=1
0x80, // packed: GCT flag=1, color resolution=0, sort=0, GCT size=0
0x00, // background color index
0x00, // pixel aspect ratio
// Global Color Table: 2 entries (black, white)
0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF,
// Empty comment extension: intro, label, immediate block terminator
0x21, 0xFE, 0x00,
// Image Descriptor
0x2C,
0x00, 0x00, // left=0
0x00, 0x00, // top=0
0x01, 0x00, // width=1
0x01, 0x00, // height=1
0x00, // packed: no LCT
// LZW minimum code size=2, one sub-block (2 bytes), block terminator
0x02, 0x02, 0x4C, 0x01, 0x00,
// Trailer
0x3B,
];

let mut decoder = DecodeOptions::new()
.read_info(gif_bytes)
.expect("decoder should initialize");

let frame = decoder
.read_next_frame()
.expect("read_next_frame should not error")
.expect("should return a frame, not None");

assert_eq!(frame.width, 1);
assert_eq!(frame.height, 1);

assert!(
decoder.read_next_frame().unwrap().is_none(),
"should have exactly one frame"
);
}

// Same as above, using a real-world GIF with an empty comment extension to ensure
// it is parsed and decoded as a valid single-frame image.
#[test]
fn empty_comment_extension_real_world_gif() {
let path = "tests/samples/bufo-thinks-about-fishsticks.gif";
let mut decoder = DecodeOptions::new()
.read_info(File::open(path).unwrap())
.expect("decoder should initialize");

let frame = decoder
.read_next_frame()
.expect("read_next_frame should not error")
.expect("bufo-thinks-about-fishsticks.gif should decode to at least one frame");

assert_eq!(frame.width, 128);
assert_eq!(frame.height, 128);
}
1 change: 1 addition & 0 deletions tests/results.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ tests/samples/2x2.gif: 3802149240
tests/samples/sample_big.gif: 4184562096
tests/samples/gifplayer-muybridge.gif: 4267078865
tests/samples/set_hsts.gif: 224161812
tests/samples/bufo-thinks-about-fishsticks.gif: 1018609591
Binary file added tests/samples/bufo-thinks-about-fishsticks.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading