aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/build/wasm_patch_growable_table.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/build/wasm_patch_growable_table.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/build/wasm_patch_growable_table.zig')
-rw-r--r--vendor/libghostty-vt/src/build/wasm_patch_growable_table.zig269
1 files changed, 269 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/build/wasm_patch_growable_table.zig b/vendor/libghostty-vt/src/build/wasm_patch_growable_table.zig
new file mode 100644
index 00000000..c40c0a9c
--- /dev/null
+++ b/vendor/libghostty-vt/src/build/wasm_patch_growable_table.zig
@@ -0,0 +1,269 @@
+//! Build tool that patches a WASM binary to make the function table
+//! growable by removing its maximum size limit.
+//!
+//! Zig's WASM linker doesn't support `--growable-table`, so the table
+//! is emitted with max == min. This tool finds the table section (id 4)
+//! and changes the limits flag from 0x01 (has max) to 0x00 (no max),
+//! removing the max field.
+//!
+//! Usage: wasm_growable_table <input.wasm> <output.wasm>
+
+const std = @import("std");
+const testing = std.testing;
+const Allocator = std.mem.Allocator;
+
+pub fn main() !void {
+ // This is a one-off patcher, so we leak all our memory on purpose
+ // and let the OS clean it up when we exit.
+ var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
+ const alloc = gpa.allocator();
+
+ // Parse args: program input output
+ const args = try std.process.argsAlloc(alloc);
+ defer std.process.argsFree(alloc, args);
+ if (args.len != 3) {
+ std.log.err("usage: wasm_growable_table <input.wasm> <output.wasm>", .{});
+ std.process.exit(1);
+ unreachable;
+ }
+
+ // Patch the file.
+ const output: []const u8 = try patchTableGrowable(
+ alloc,
+ try std.fs.cwd().readFileAlloc(
+ alloc,
+ args[1],
+ std.math.maxInt(usize),
+ ),
+ );
+
+ // Write our output
+ const out_file = try std.fs.cwd().createFile(args[2], .{});
+ defer out_file.close();
+ try out_file.writeAll(output);
+}
+
+/// Patch the WASM binary's table section to remove the maximum size
+/// limit, making the table growable. If the table already has no max
+/// or no table section is found, the input is returned unchanged.
+///
+/// The WASM table section (id=4) encodes table limits as:
+/// - flags=0x00, min (LEB128) — no max, growable
+/// - flags=0x01, min (LEB128), max (LEB128) — bounded, not growable
+///
+/// This function rewrites the section to use flags=0x00, dropping the
+/// max field entirely.
+fn patchTableGrowable(
+ alloc: Allocator,
+ input: []const u8,
+) (error{InvalidWasm} || std.Io.Writer.Error)![]const u8 {
+ if (input.len < 8) return error.InvalidWasm;
+
+ // Start after the 8-byte WASM header (magic + version).
+ var pos: usize = 8;
+
+ while (pos < input.len) {
+ const section_id = input[pos];
+ pos += 1;
+ const section_size = readLeb128(input, &pos);
+ const section_start = pos;
+
+ // We're looking for section 4 (the table section).
+ if (section_id != 4) {
+ pos = section_start + section_size;
+ continue;
+ }
+
+ _ = readLeb128(input, &pos); // table count
+ pos += 1; // elem_type (0x70 = funcref)
+ const flags = input[pos];
+
+ // flags bit 0 indicates whether a max is present.
+ if (flags & 1 == 0) {
+ // Already no max, nothing to patch.
+ return input;
+ }
+
+ // Record positions of each field so we can reconstruct
+ // the section without the max value.
+ const flags_pos = pos;
+ pos += 1; // skip flags byte
+ const min_start = pos;
+ _ = readLeb128(input, &pos); // min
+ const max_start = pos;
+ _ = readLeb128(input, &pos); // max
+ const max_end = pos;
+ const section_end = section_start + section_size;
+
+ // Build the new section payload with the max removed:
+ // [table count + elem_type] [flags=0x00] [min] [trailing data]
+ var payload: std.Io.Writer.Allocating = .init(alloc);
+ try payload.writer.writeAll(input[section_start..flags_pos]);
+ try payload.writer.writeByte(0x00); // flags: no max
+ try payload.writer.writeAll(input[min_start..max_start]);
+ try payload.writer.writeAll(input[max_end..section_end]);
+
+ // Reassemble the full binary:
+ // [everything before this section] [section id] [new size] [new payload] [everything after]
+ const before_section = input[0 .. section_start - 1 - uleb128Size(section_size)];
+ var result: std.Io.Writer.Allocating = .init(alloc);
+ try result.writer.writeAll(before_section);
+ try result.writer.writeByte(4); // table section id
+ try result.writer.writeUleb128(@as(u32, @intCast(payload.written().len)));
+ try result.writer.writeAll(payload.written());
+ try result.writer.writeAll(input[section_end..]);
+ return result.written();
+ }
+
+ // No table section found; return input unchanged.
+ return input;
+}
+
+/// Decode an unsigned LEB128 value from `bytes` starting at `pos.*`,
+/// advancing `pos` past the encoded bytes.
+fn readLeb128(bytes: []const u8, pos: *usize) u32 {
+ var result: u32 = 0;
+ var shift: u5 = 0;
+ while (true) {
+ const byte = bytes[pos.*];
+ pos.* += 1;
+ result |= @as(u32, byte & 0x7f) << shift;
+ if (byte & 0x80 == 0) return result;
+ shift +%= 7;
+ }
+}
+
+/// Return the number of bytes needed to encode `value` as unsigned LEB128.
+fn uleb128Size(value: u32) usize {
+ var v = value;
+ var size: usize = 0;
+ while (true) {
+ v >>= 7;
+ size += 1;
+ if (v == 0) return size;
+ }
+}
+
+/// Minimal valid WASM module with a bounded table (min=1, max=1).
+/// Sections: type(1), table(4), export(7).
+const test_wasm_bounded_table = [_]u8{
+ 0x00, 0x61, 0x73, 0x6d, // magic
+ 0x01, 0x00, 0x00, 0x00, // version
+ // Type section (id=1): 1 type, () -> ()
+ 0x01, 0x04, 0x01, 0x60,
+ 0x00, 0x00,
+ // Table section (id=4): 1 table, funcref, flags=1, min=1, max=1
+ 0x04, 0x05,
+ 0x01, 0x70, 0x01, 0x01,
+ 0x01,
+ // Export section (id=7): 0 exports
+ 0x07, 0x01, 0x00,
+};
+
+/// Same module but the table already has no max (flags=0).
+const test_wasm_growable_table = [_]u8{
+ 0x00, 0x61, 0x73, 0x6d, // magic
+ 0x01, 0x00, 0x00, 0x00, // version
+ // Type section (id=1)
+ 0x01, 0x04, 0x01, 0x60,
+ 0x00, 0x00,
+ // Table section (id=4): 1 table, funcref, flags=0, min=1
+ 0x04, 0x04,
+ 0x01, 0x70, 0x00, 0x01,
+ // Export section (id=7): 0 exports
+ 0x07, 0x01, 0x00,
+};
+
+/// Module with no table section at all.
+const test_wasm_no_table = [_]u8{
+ 0x00, 0x61, 0x73, 0x6d, // magic
+ 0x01, 0x00, 0x00, 0x00, // version
+ // Type section (id=1)
+ 0x01, 0x04, 0x01, 0x60,
+ 0x00, 0x00,
+ // Export section (id=7): 0 exports
+ 0x07, 0x01,
+ 0x00,
+};
+
+test "patches bounded table to remove max" {
+ // We use a non-checking allocator because the patched result is
+ // intentionally leaked (matches the real main() usage).
+ const result = try patchTableGrowable(
+ std.heap.page_allocator,
+ &test_wasm_bounded_table,
+ );
+
+ // Result should differ from input (max was removed).
+ try testing.expect(!std.mem.eql(
+ u8,
+ result,
+ &test_wasm_bounded_table,
+ ));
+
+ // Find the table section in the output and verify flags=0x00.
+ var pos: usize = 8;
+ while (pos < result.len) {
+ const id = result[pos];
+ pos += 1;
+ const size = readLeb128(result, &pos);
+ if (id == 4) {
+ _ = readLeb128(result, &pos); // table count
+ pos += 1; // elem_type
+ // flags should now be 0x00 (no max).
+ try testing.expectEqual(@as(u8, 0x00), result[pos]);
+ return;
+ }
+ pos += size;
+ }
+ return error.TableSectionNotFound;
+}
+
+test "already growable table is returned unchanged" {
+ const result = try patchTableGrowable(
+ testing.allocator,
+ &test_wasm_growable_table,
+ );
+ try testing.expectEqual(
+ @as([*]const u8, &test_wasm_growable_table),
+ result.ptr,
+ );
+}
+
+test "no table section returns input unchanged" {
+ const result = try patchTableGrowable(
+ testing.allocator,
+ &test_wasm_no_table,
+ );
+ try testing.expectEqual(@as([*]const u8, &test_wasm_no_table), result.ptr);
+}
+
+test "too short input returns InvalidWasm" {
+ try testing.expectError(
+ error.InvalidWasm,
+ patchTableGrowable(testing.allocator, "short"),
+ );
+}
+
+test "readLeb128 single byte" {
+ const bytes = [_]u8{0x05};
+ var pos: usize = 0;
+ try testing.expectEqual(@as(u32, 5), readLeb128(&bytes, &pos));
+ try testing.expectEqual(@as(usize, 1), pos);
+}
+
+test "readLeb128 multi byte" {
+ // 300 = 0b100101100 → LEB128: 0xAC 0x02
+ const bytes = [_]u8{ 0xAC, 0x02 };
+ var pos: usize = 0;
+ try testing.expectEqual(@as(u32, 300), readLeb128(&bytes, &pos));
+ try testing.expectEqual(@as(usize, 2), pos);
+}
+
+test "uleb128Size" {
+ try testing.expectEqual(@as(usize, 1), uleb128Size(0));
+ try testing.expectEqual(@as(usize, 1), uleb128Size(0x7f));
+ try testing.expectEqual(@as(usize, 2), uleb128Size(0x80));
+ try testing.expectEqual(@as(usize, 2), uleb128Size(300));
+ try testing.expectEqual(@as(usize, 5), uleb128Size(std.math.maxInt(u32)));
+}