diff options
| author | user <user@clank> | 2026-08-15 19:14:59 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-15 19:14:59 +0200 |
| commit | 2f0b3426d0f40d2cf53740cb597495af2c45bc2e (patch) | |
| tree | 1969c511f2ec69e234e4eb8b2ba1aa2bf78c7904 /src/png.c | |
| parent | sl0ppty: measure chrome in columns, not characters (diff) | |
| download | slosh-2f0b3426d0f40d2cf53740cb597495af2c45bc2e.tar.gz slosh-2f0b3426d0f40d2cf53740cb597495af2c45bc2e.tar.xz slosh-2f0b3426d0f40d2cf53740cb597495af2c45bc2e.zip | |
sl0ppty: decode the PNGs programs actually send
Kitty graphics were green in the suite and a real program still drew
nothing. libghostty-vt ships no image codec: built as a library it sets
its `sys.decode_png` hook to null, and a null hook rejects every `f=100`
transmission outright -- no image stored, so no placement exists, so the
re-emission layer this project is mostly *about* is never reached. The
program gets no error and no picture.
Raw RGB worked throughout, which is why it looked done: every test here
sent raw pixels, because that is what is easy to write in a test. It is
the same lesson the last kitty graphics bug taught -- write the test the
way a program actually behaves -- and a program with a picture on disk
has a PNG. Found because ida-tui's startup logo never appeared, and its
detection probe answers `OK` first, so it fails silently rather than
falling back to its block art.
PNG is the whole of the gap and cannot grow: the protocol has exactly
three formats (`f=24`, `f=32`, `f=100`), the first two are raw pixels
lib-vt already handles, and `decode_png` is the only codec hook it has.
Decoding only, never encoding.
The decoder is the stb_image lib-vt already vendors and we already
commit -- upstream compiles that same header with the same
`STBI_ONLY_PNG` for this same purpose, it is just not in the
`-Demit-lib-vt` build. So nothing new is pulled in; a file we already
ship is finally compiled. src/png.c is the hook, a header pre-check and
a copy: dimensions are read before decoding because the buffer we return
is allocated through lib-vt's limited allocator but stb's own working
allocation is plain malloc and answers to nothing.
Tests cover a PNG placed at the size it asked for (RGB and RGBA sources,
both normalised to RGBA), the client being handed `f=32` rather than the
png it was given, and a malformed png placing nothing without taking the
pane with it. They fail on the previous binary.
Diffstat (limited to 'src/png.c')
| -rw-r--r-- | src/png.c | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/src/png.c b/src/png.c new file mode 100644 index 0000000..daf0f2e --- /dev/null +++ b/src/png.c @@ -0,0 +1,87 @@ +#define _GNU_SOURCE +#include "png.h" + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include <string.h> + +/* stb_image is already in the tree: libghostty-vt vendors it, and upstream + * compiles that same header with exactly these defines (see + * vendor/libghostty-vt/src/stb/stb.c) to answer exactly this question — + * "the only use case we have right now is the Kitty Graphics protocol which + * only supports PNG as a format besides raw RGB/RGBA buffers". So this is not + * a new dependency; it is a file we already ship and pin, finally compiled. + * + * ONLY_PNG keeps the other decoders out of the binary, NO_STDIO the file + * paths: the bytes always arrive in memory, straight off a pane's pty, so a + * decoder that can open files is surface we have no use for. */ +#define STBI_ONLY_PNG +#define STBI_NO_STDIO +#define STB_IMAGE_IMPLEMENTATION +#include "stb_image.h" + +#include <ghostty/vt.h> + +/* The library refuses anything past this (`max_size` in + * kitty/graphics_image.zig), so decoding past it can only ever be work done + * to have the result thrown away. Matched rather than tightened: being + * stricter here would silently reject images the library would have taken. */ +#define MAX_DECODED (400u * 1024u * 1024u) + +static bool decode(void *ud, const GhosttyAllocator *alloc, const uint8_t *data, + size_t data_len, GhosttySysImage *out) { + if (!alloc || !data || !data_len || !out) return false; + if (data_len > INT32_MAX) return false; /* stb counts in int */ + + /* Read the header before decoding it. The buffer we return is allocated + * through the library's allocator, which is wrapped in a limit — but + * stb_image's own working allocation is plain malloc and answers to nothing, + * so a header claiming 60000x60000 would have us try for 14GB before anyone + * got to refuse it. This costs an IHDR parse instead. */ + int w = 0, h = 0, channels = 0; + if (!stbi_info_from_memory(data, (int)data_len, &w, &h, &channels)) + return false; + if (w <= 0 || h <= 0) return false; + if ((uint64_t)w * (uint64_t)h * 4u > MAX_DECODED) return false; + + /* 4 = "give me RGBA whatever the file is", so palette, greyscale and + * 16-bit all arrive in the one layout the protocol wants. */ + int got_w = 0, got_h = 0; + stbi_uc *px = + stbi_load_from_memory(data, (int)data_len, &got_w, &got_h, &channels, 4); + if (!px) return false; + if (got_w <= 0 || got_h <= 0) { + stbi_image_free(px); + return false; + } + + /* Sized from what came back, not from what the header promised. */ + uint64_t len = (uint64_t)got_w * (uint64_t)got_h * 4u; + if (len > MAX_DECODED) { + stbi_image_free(px); + return false; + } + + /* Copied into the library's allocator because that is what it frees the + * buffer with. Routing stb's own allocation there instead would mean + * smuggling a per-call allocator into a global, for one memcpy that is + * already far cheaper than the decode that produced it. */ + uint8_t *buf = ghostty_alloc(alloc, (size_t)len); + if (!buf) { + stbi_image_free(px); + return false; + } + memcpy(buf, px, (size_t)len); + stbi_image_free(px); + + *out = (GhosttySysImage){.width = (uint32_t)got_w, + .height = (uint32_t)got_h, + .data = buf, + .data_len = (size_t)len}; + return true; +} + +void png_init(void) { + ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, (const void *)(uintptr_t)decode); +} |
