From 1aeb08025038b8810118a4871c2894de54960dac Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Wed, 29 Apr 2026 14:27:57 +0200 Subject: [PATCH] LibCompress: Treat LZW decoding errors as end of stream The LZW data for both GIF and TIFF images is sometimes intentionally missing an end-of-information (EOI) code, which technically is a decoding error, but in practive is handled gracefully by Firefox, Safari and Chrome for GIFs and Safari for TIFFs. Let's mirror their behavior. The included WPT test exposes the fact that trailing garbage bytes can also result in decoding errors. We handle this in the LZW logic rather than in the image decoding since our LZW implementation is currently only used by GIF and TIFF decoding. The error is logged behind the LZW_DEBUG flag. --- Libraries/LibCompress/Lzw.h | 10 +++++- ...-child-percentage-height-in-auto-table.txt | 24 ++++++------- .../wpt-import/gif/reset-no-gce-ref.html | 13 +++++++ .../input/wpt-import/gif/reset-no-gce-1.html | 32 ++++++++++++++++++ .../Ref/input/wpt-import/gif/reset-no-gce.gif | Bin 0 -> 90 bytes ...zw-image-without-EOI-should-still-load.txt | 2 ++ ...w-image-without-EOI-should-still-load.html | 25 ++++++++++++++ 7 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 Tests/LibWeb/Ref/expected/wpt-import/gif/reset-no-gce-ref.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce-1.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce.gif create mode 100644 Tests/LibWeb/Text/expected/HTML/lzw-image-without-EOI-should-still-load.txt create mode 100644 Tests/LibWeb/Text/input/HTML/lzw-image-without-EOI-should-still-load.html diff --git a/Libraries/LibCompress/Lzw.h b/Libraries/LibCompress/Lzw.h index b0fa8793fe9..201450baa5d 100644 --- a/Libraries/LibCompress/Lzw.h +++ b/Libraries/LibCompress/Lzw.h @@ -109,7 +109,15 @@ public: u16 const end_of_data_code = lzw_decompressor.add_control_code(); while (true) { - auto const code = TRY(lzw_decompressor.next_code()); + // Some encoders omit the End-of-Information code or emit trailing padding that decodes as invalid LZW + // codes. Treat any LZW error as an implicit end of the compressed stream and use whatever was successfully + // decoded so far - this matches the behavior of Firefox, Chrome and Safari. + auto code_or_error = lzw_decompressor.next_code(); + if (code_or_error.is_error()) { + dbgln_if(LZW_DEBUG, "LZW stream ended unexpectedly: {}", code_or_error.release_error()); + break; + } + auto const code = code_or_error.release_value(); if (code == clear_code) { lzw_decompressor.reset(); diff --git a/Tests/LibWeb/Layout/expected/table-replaced-child-percentage-height-in-auto-table.txt b/Tests/LibWeb/Layout/expected/table-replaced-child-percentage-height-in-auto-table.txt index cf7969c29fd..ffa541a765e 100644 --- a/Tests/LibWeb/Layout/expected/table-replaced-child-percentage-height-in-auto-table.txt +++ b/Tests/LibWeb/Layout/expected/table-replaced-child-percentage-height-in-auto-table.txt @@ -1,21 +1,21 @@ Viewport <#document> at [0,0] [0+0+0 800 0+0+0] [0+0+0 600 0+0+0] [BFC] children: not-inline BlockContainer at [0,0] [0+0+0 800 0+0+0] [0+0+0 0 0+0+0] [BFC] children: not-inline - TableWrapper <(anonymous)> at [8,8] positioned [8+0+0 0 0+0+8] [8+0+0 0 0+0+8] [BFC] children: not-inline - Box at [8,8] table-box [0+0+0 0 0+0+0] [0+0+0 0 0+0+0] [TFC] children: not-inline - Box <(anonymous)> at [8,8] table-row [0+0+0 0 0+0+0] [0+0+0 0 0+0+0] children: not-inline - BlockContainer <(anonymous)> at [8,8] table-cell [0+0+0 0 0+0+0] [0+0+0 0 0+0+0] [BFC] children: not-inline - ImageBox at [8,8] [0+0+0 0 0+0+0] [0+0+0 0 0+0+0] children: not-inline - BlockContainer <(anonymous)> at [8,8] [0+0+0 0 0+0+0] [0+0+0 0 0+0+0] children: inline + TableWrapper <(anonymous)> at [8,8] positioned [8+0+0 1 0+0+8] [8+0+0 1 0+0+8] [BFC] children: not-inline + Box at [8,8] table-box [0+0+0 1 0+0+0] [0+0+0 1 0+0+0] [TFC] children: not-inline + Box <(anonymous)> at [8,8] table-row [0+0+0 1 0+0+0] [0+0+0 1 0+0+0] children: not-inline + BlockContainer <(anonymous)> at [8,8] table-cell [0+0+0 1 0+0+0] [0+0+0 1 0+0+0] [BFC] children: not-inline + ImageBox at [8,8] [0+0+0 1 0+0+0] [0+0+0 1 0+0+0] children: not-inline + BlockContainer <(anonymous)> at [8,9] [0+0+0 1 0+0+0] [0+0+0 0 0+0+0] children: inline TextNode <#text> (not painted) ViewportPaintable (Viewport<#document>) [0,0 800x600] PaintableWithLines (BlockContainer) [0,0 800x0] - PaintableWithLines (TableWrapper(anonymous)) [8,8 0x0] - PaintableBox (Box) [8,8 0x0] - PaintableBox (Box(anonymous)) [8,8 0x0] - PaintableWithLines (BlockContainer(anonymous)) [8,8 0x0] - ImagePaintable (ImageBox) [8,8 0x0] - PaintableWithLines (BlockContainer(anonymous)) [8,8 0x0] + PaintableWithLines (TableWrapper(anonymous)) [8,8 1x1] + PaintableBox (Box) [8,8 1x1] + PaintableBox (Box(anonymous)) [8,8 1x1] + PaintableWithLines (BlockContainer(anonymous)) [8,8 1x1] + ImagePaintable (ImageBox) [8,8 1x1] + PaintableWithLines (BlockContainer(anonymous)) [8,9 1x0] SC for Viewport<#document> [0,0 800x600] [children: 1] (z-index: auto) SC for BlockContainer [0,0 800x0] [children: 0] (z-index: auto) diff --git a/Tests/LibWeb/Ref/expected/wpt-import/gif/reset-no-gce-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/gif/reset-no-gce-ref.html new file mode 100644 index 00000000000..5bf308fd63b --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/gif/reset-no-gce-ref.html @@ -0,0 +1,13 @@ + + + + gif no graphics control extension + + + + + +
+ + + diff --git a/Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce-1.html b/Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce-1.html new file mode 100644 index 00000000000..21a6dd8a298 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce-1.html @@ -0,0 +1,32 @@ + + + + gif no graphics control extension + + + + + + + + + + + + + diff --git a/Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce.gif b/Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce.gif new file mode 100644 index 0000000000000000000000000000000000000000..98e6bc61ac4a43e5d5e3df7165b87402a72c915b GIT binary patch literal 90 zcmZ?wbhEHbWMW`q_{abP|6y41Ckv+tkfQ?<1<5loG50KHV1S4-p@>geimDF8w*~;0 CgA9%U literal 0 HcmV?d00001 diff --git a/Tests/LibWeb/Text/expected/HTML/lzw-image-without-EOI-should-still-load.txt b/Tests/LibWeb/Text/expected/HTML/lzw-image-without-EOI-should-still-load.txt new file mode 100644 index 00000000000..ae6ab609aa5 --- /dev/null +++ b/Tests/LibWeb/Text/expected/HTML/lzw-image-without-EOI-should-still-load.txt @@ -0,0 +1,2 @@ +GIF: loaded, width=1, height=1 +TIFF: loaded, width=1, height=1 diff --git a/Tests/LibWeb/Text/input/HTML/lzw-image-without-EOI-should-still-load.html b/Tests/LibWeb/Text/input/HTML/lzw-image-without-EOI-should-still-load.html new file mode 100644 index 00000000000..6d667d0b737 --- /dev/null +++ b/Tests/LibWeb/Text/input/HTML/lzw-image-without-EOI-should-still-load.html @@ -0,0 +1,25 @@ + + +