From c9c1b6e290188d019f9035ece7ce080b4120cbd9 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Sun, 2 Aug 2026 09:17:38 +0000 Subject: [PATCH 69/71] feat(macos): inflate kitty o=z payloads with system zlib Wires the engine's new injected Terminal::inflate() callback (vendor) to a zlib inflater on the macOS side, the sibling of the ImageIO PNG decoder already injected here. Without an inflater the engine answers "ENOTSUP: compressed payloads are not supported" for any o=, yet `kitten icat` -- the kitty graphics protocol's own reference client -- deflates its raw RGB/RGBA payloads by default. So compressed sends went dark where kitty renders; this closes that gap for the native frontend. Uses system libz (guaranteed on every macOS incl. 10.6/PPC), linked via the plain -lz flag inside the existing if(APPLE) block rather than find_package(ZLIB), sidestepping the old SDK's CMake-module quirks. The inflater streams through a growing buffer (the wire states only the final pixel dimensions, not the inflated byte count) and caps output at 512 MiB as a decompression-bomb guard; the engine re-checks the inflated size against the declared dimensions and bounds the compressed input, so a corrupt or mismatched stream falls into the engine's existing EINVAL path. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_011h7K9xLzUrQysDc7pNd7Br --- src/contour_macos/CMakeLists.txt | 8 ++-- src/contour_macos/TerminalSession.cpp | 53 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/contour_macos/CMakeLists.txt b/src/contour_macos/CMakeLists.txt index e2a62704..1259d932 100644 --- a/src/contour_macos/CMakeLists.txt +++ b/src/contour_macos/CMakeLists.txt @@ -52,9 +52,11 @@ target_compile_options(contour_macos PRIVATE ${_contour_macos_warn_opts}) if(APPLE) option(CONTOUR_GL_BASELINE_105 "Build the GL backend against the conservative 10.5 GL 2.0 baseline" ON) # GLRenderTarget uses OpenGL; TerminalSession uses ImageIO/CoreGraphics (via ApplicationServices, - # as on 10.6) to decode inline PNG images. Link both into the library so the probe/smoke targets - # that depend only on contour_macos link too. - target_link_libraries(contour_macos PUBLIC "-framework OpenGL" "-framework ApplicationServices") + # as on 10.6) to decode inline PNG images, and system libz to inflate kitty `o=z` payloads. Link + # all into the library so the probe/smoke targets that depend only on contour_macos link too. Use + # the plain `-lz` system linker flag rather than find_package(ZLIB): libz is a guaranteed system + # dylib on every macOS incl. 10.6/PPC, and this sidesteps the old SDK's CMake module quirks. + target_link_libraries(contour_macos PUBLIC "-framework OpenGL" "-framework ApplicationServices" "-lz") if(CONTOUR_GL_BASELINE_105) target_compile_definitions(contour_macos PUBLIC CONTOUR_GL_BASELINE_105=1) else() diff --git a/src/contour_macos/TerminalSession.cpp b/src/contour_macos/TerminalSession.cpp index 3042ba2d..7772791b 100644 --- a/src/contour_macos/TerminalSession.cpp +++ b/src/contour_macos/TerminalSession.cpp @@ -13,6 +13,9 @@ // a C API, so it is usable from this ordinary-C++ translation unit (no ObjC needed). #include +// zlib (system libz on macOS, incl. 10.6/PPC) for inflating kitty graphics `o=z` payloads. +#include + #include #include #include @@ -164,6 +167,52 @@ namespace CFRelease(cfData); return result; } + + /// Inflates a raw zlib-deflate stream, for kitty graphics `o=z` payloads. `kitten icat` -- the + /// protocol's reference client -- deflates its raw pixel payloads by default, so without this the + /// engine answers ENOTSUP where kitty renders. The engine re-checks the inflated size against the + /// declared image dimensions once this returns and caps the compressed input, so the only ceiling + /// enforced here is a decompression-bomb guard. + [[nodiscard]] std::optional inflateZlib(std::span data) + { + z_stream stream {}; + if (inflateInit(&stream) != Z_OK) + return std::nullopt; + + stream.next_in = const_cast(reinterpret_cast(data.data())); + stream.avail_in = static_cast(data.size()); + + // The output size is not known up front (the wire states only the final pixel dimensions), so + // stream through a growing buffer 64 KiB at a time until Z_STREAM_END. Cap the total against a + // deflate bomb: the engine's dimension check runs only after inflation completes. + constexpr size_t chunkSize = 64 * 1024; + constexpr size_t maxInflatedSize = static_cast(512) * 1024 * 1024; + vtbackend::Image::Data out; + int status = Z_OK; + do + { + auto const oldSize = out.size(); + if (oldSize + chunkSize > maxInflatedSize) + { + inflateEnd(&stream); + return std::nullopt; + } + out.resize(oldSize + chunkSize); + stream.next_out = reinterpret_cast(out.data() + oldSize); + stream.avail_out = static_cast(chunkSize); + + status = inflate(&stream, Z_NO_FLUSH); + if (status != Z_OK && status != Z_STREAM_END) + { + inflateEnd(&stream); + return std::nullopt; + } + out.resize(oldSize + (chunkSize - stream.avail_out)); + } while (status != Z_STREAM_END); + + inflateEnd(&stream); + return out; + } } // namespace TerminalSession::TerminalSession(vtbackend::PageSize pageSize, @@ -230,6 +279,10 @@ TerminalSession::TerminalSession(vtbackend::PageSize pageSize, // decodePng() no-ops and PNG inline images are silently dropped. _terminal->setImageDecoder(&decodePngViaImageIO); + // Inject the zlib inflater for kitty graphics `o=z` payloads; without it the engine answers + // ENOTSUP where kitty renders (kitten icat compresses its payloads by default). + _terminal->setInflate(&inflateZlib); + // setWordDelimiters() is what actually assigns the selection helper's `wordDelimited` // std::function; without calling it, mouse selection invokes an empty std::function and // throws std::bad_function_call. Seed with the usual terminal word-boundary set.