From c6d50b3b2acea59a254fd6d38e5f15f63d602adc Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Wed, 29 Jul 2026 13:07:59 +1000 Subject: [PATCH] G-128: fleet renamed to PUNKSHELL via punkres; punkres 0.3.0 -drop-opaque-overlay for mingw stub symbol tables; punkshell 0.30.3 Forced fleet rebake (user-directed): all ten win32 kits now carry the embedded PUNKSHELL group icon via punkres, verified per kit (punkres list, sidecar icon_group=PUNKSHELL + embedding=punkres, extract-ico byte-identical spot check) and booting (incl. metakit punksys 8.6.13 / punk86 8.6.17 and punk902z 9.0.2). punkmagic excluded (runtime absent - keeps its old icon and _build cache); punkshell902 linux = not applicable. The first fleet pass surfaced a real shape: mingw-built runtimes (tclsh902z, tclkit-win64-dyn family) keep COFF symbol/debug tables AFTER their sections, the extraction head carries them, and punkres correctly refused those stubs as data-classified overlay. Measured against twapi-era artifacts: Windows' EndUpdateResource has always silently STRIPPED exactly those bytes at stub-stamp time (leaving PointerToSymbolTable dangling) and the kits work - so punkres 0.3.0 (upstream dca8634, re-vendored, 33/33) encodes the distinction: -drop-opaque-overlay DISCARDS expendable trailing data and zeroes the orphaned symbol-table header fields, vs -allow-opaque-overlay which RELOCATES a real self-locating payload; conflicting flags rejected; drop is inert on zip overlays. The seam's punkres arm passes -drop (twapi-arm-equivalent stub hygiene; bin/AGENTS.md documents it). Recovery note: a 10-minute foreground timeout killed the first 3-kit rerun during deploy - stale src/_build/.punkcheck.lock removed after a dead-pid check per the recorded procedure; the completion bake deployed the already-wrapped kits. Full punkexe suite 83 tests 0 failed; goals_lint + architecture_lint clean. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 15 ++++ bin/AGENTS.md | 12 ++- goals/G-128-portable-pe-resource-stamping.md | 36 +++++++++ punkproject.toml | 2 +- src/make.tcl | 9 +++ src/tools/punkres/PROVENANCE.md | 4 +- src/tools/punkres/README.md | 12 ++- src/tools/punkres/build.zig.zon | 2 +- src/tools/punkres/src/punkres.zig | 59 +++++++++++--- src/tools/punkres/src/punkres_test.zig | 82 +++++++++++++++++++- 10 files changed, 210 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ce59e9..0fceb253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.30.3] - 2026-07-29 + +- Fleet renamed: every win32 kit now carries the embedded `PUNKSHELL` group + icon via punkres (punksys, punk86, punk91, punkbi, punkluck86, punk902z, + punk905, punk91ix86, punk9_beta, punk9bi_beta; punkmagic excluded - its + runtime is absent; punkshell902 is a linux target, not applicable). +- punkres 0.3.0: new `set-icon -drop-opaque-overlay` consent DISCARDS non-zip + trailing stub bytes (the COFF symbol/debug tables mingw-built runtimes + carry, which ride into extraction heads and previously made punkres refuse + those stubs), zeroing the orphaned symbol-table header pointer; distinct + from `-allow-opaque-overlay` which RELOCATES a real self-locating payload. + The bake's punkres arm passes the drop flag - matching the twapi arm's + measured long-standing EndUpdateResource stripping - so mingw-lineage stubs + (tclsh902z, tclkit-win64-dyn family) stamp identically to MSVC/zig ones. + ## [0.30.2] - 2026-07-29 - Baked kits' embedded RT_GROUP_ICON is now NAMED from the icon file's diff --git a/bin/AGENTS.md b/bin/AGENTS.md index 8d708c65..dc95ec85 100644 --- a/bin/AGENTS.md +++ b/bin/AGENTS.md @@ -171,9 +171,15 @@ is selected when present; otherwise the twapi arm serves windows hosts with nothing built. The embedded RT_GROUP_ICON is NAMED from the icon file's uppercased rootname (`punkshell.ico` -> `PUNKSHELL`, RC convention; applies to per-kit overrides identically since they are also named `punkshell.ico`), -with the resource language adopted from the group being replaced. Named -"resources", not "icon", so the parked RT_VERSION stamping extends the same -file. +with the resource language adopted from the group being replaced. Stub +hygiene: non-zip trailing bytes on a stub (the COFF symbol/debug tables +mingw-built runtimes carry after their sections, which ride into the +extraction head) are DROPPED at stamp time - the punkres arm via its +`-drop-opaque-overlay` consent (which also zeroes the orphaned +symbol-table header pointer), the twapi arm implicitly via +`EndUpdateResource`'s long-standing stripping - so both arms emit the same +payload-free stamped stub. Named "resources", not "icon", so the parked +RT_VERSION stamping extends the same file. - Icon choice: a root-level `punkshell.ico` in the kit's own custom `.vfs` folder (consulted pre-merge, so the `_vfscommon.vfs` copy never reads as an diff --git a/goals/G-128-portable-pe-resource-stamping.md b/goals/G-128-portable-pe-resource-stamping.md index 35e01ac0..30a44e75 100644 --- a/goals/G-128-portable-pe-resource-stamping.md +++ b/goals/G-128-portable-pe-resource-stamping.md @@ -166,6 +166,26 @@ the parked RT_VERSION stamping wants, since the project version is known late. start and moves by exactly the shift delta. punkres handles this (verify invariant base2 == base1 + delta) and reports the leading-byte distance via an overlay_note line. +- COFF symbol-table finding (2026-07-29, fleet rebake): mingw-built runtimes + (tclsh902z, the tclkit-win64-dyn* family) keep their COFF symbol/debug + tables AFTER the sections (located by the COFF header's + PointerToSymbolTable, outside every section - .file/crtbegin.c strings), + and the extraction head carries them, so the "payload-free" stub handed to + the icon seam has a data-classified overlay (306KB on tclsh902z, ~272KB on + the tclkits). punkres correctly REFUSED those stubs where MSVC/zig-built + stubs (symbols stripped to PDB / none) stamped clean. Measured against the + twapi-era artifacts: EndUpdateResource has ALWAYS silently stripped exactly + those bytes at stub-stamp time - the working twapi-era punk902z has zip + base == overlay start with the 306KB gone, the twapi-era punksys has the + metakit starting immediately at PE end, and both left PointerToSymbolTable + DANGLING (2294272 with the table gone) - so the bytes are proven + expendable for assembled kits. punkres 0.3.0 encodes the distinction as + two consent verbs: -allow-opaque-overlay RELOCATES (self-locating payloads + like metakit), -drop-opaque-overlay DISCARDS (expendable trailing data), + zeroing the symbol-table header fields it orphans rather than leaving them + dangling; the seam's punkres arm passes -drop (stub context: trailing + non-zip bytes are detritus by contract, matching the twapi arm's proven + de-facto behaviour), and the flags conflict if combined. - Recorded 2026-07-29 (user decision, increment-1 review): the acceptance's 'kit' (sdx/metakit) shape clause is SATISFIED FOR NOW by the measured consent path (-allow-opaque-overlay; moved payload boots - see Progress and @@ -301,3 +321,19 @@ the parked RT_VERSION stamping wants, since the project version is known late. (codepage divergence as per the Notes finding); sidecar icon_group + embedding recorded; final deployed state punkres-stamped and booting. punkshell 0.30.2. +- 2026-07-29 increment 1c (user-directed fleet rebake): ALL ten win32 kits now + wear RT_GROUP_ICON "PUNKSHELL" lang 1033 embedded via punkres, verified by + punkres list on every deployed kit, sidecars icon_group=PUNKSHELL + + embedding=punkres, extract-ico byte-identical to punkshell.ico (spot + punk902z), and boots: punk91-family/punkluck86/punk91ix86 (earlier + increments) + punksys 8.6.13, punk86 8.6.17, punk902z 9.0.2 (this one). + punkshell902 (linux) NOT APPLICABLE as designed; punkmagic EXCLUDED (its + tclsh90magic runtime is absent - it keeps its old icon and its _build cache + until the runtime returns). The first fleet pass surfaced the COFF + symbol-table refusals on the three mingw-lineage stubs (see Notes) - fixed + properly via punkres 0.3.0 -drop-opaque-overlay (upstream dca8634, + re-vendored, suite 33/33) with the seam passing the flag; second pass + stamped all three (a foreground timeout killed the first 3-kit rerun during + deploy - stale .punkcheck.lock removed per the recorded recovery procedure, + completion bake deployed the already-wrapped kits). Full punkexe suite 83 + tests 0 failed. punkshell 0.30.3. diff --git a/punkproject.toml b/punkproject.toml index d1df9c35..3c47c2fb 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.30.2" +version = "0.30.3" license = "BSD-2-Clause" diff --git a/src/make.tcl b/src/make.tcl index 383113e1..a7c3c8db 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -6562,6 +6562,15 @@ proc ::punkboot::kit_icon_embed_punkres {punkres_exe exepath icopath {group_over if {$group_override ne ""} { lappend cmd -group $group_override } + #-drop-opaque-overlay: the stub contract is a payload-free prefix, so any + #non-zip trailing bytes are build detritus - concretely the COFF + #symbol/debug tables mingw-built runtimes carry after their sections, + #which ride into the extraction head. The twapi arm's EndUpdateResource + #has always silently stripped exactly those bytes (measured 2026-07-29, + #G-128 detail); punkres drops them deliberately and zeroes the dangling + #symbol-table header pointer. A REAL zip payload on a stub is unaffected + #(the flag is inert for zip-classified overlays, which punkres preserves). + lappend cmd -drop-opaque-overlay lappend cmd [file nativename $exepath] [file nativename $icopath] 2>@1 if {[catch $cmd output]} { error "punkres set-icon failed: $output" diff --git a/src/tools/punkres/PROVENANCE.md b/src/tools/punkres/PROVENANCE.md index 024b2c79..a87d5aeb 100644 --- a/src/tools/punkres/PROVENANCE.md +++ b/src/tools/punkres/PROVENANCE.md @@ -8,8 +8,8 @@ the punkshell repo root. That is a layout convention, not a requirement - if no checkout is present there, clone the remote to that location; the Vendored state commit below identifies the exact upstream state in any clone. -- Vendored state: commit `c0eb919adf4bd542ff7b9827e49b4b7d791c51b6` (punkres - v0.2.0, 2026-07-29; upstream working tree clean at copy time) +- Vendored state: commit `dca8634f190d8e9e800637211d666951737909da` (punkres + v0.3.0, 2026-07-29; upstream working tree clean at copy time) - Vendored: 2026-07-29 for goal G-128 (see `goals/G-128-portable-pe-resource-stamping.md`) - Lineage: first-party punkshell tool, written 2026-07-29 for G-128 - no diff --git a/src/tools/punkres/README.md b/src/tools/punkres/README.md index 82be65b2..2e6f53a2 100644 --- a/src/tools/punkres/README.md +++ b/src/tools/punkres/README.md @@ -26,7 +26,15 @@ Windows. file-relative convention. - zip64 / multi-disk: always refused. - non-zip overlay bytes: refused by default (their offset semantics are - unknowable); `-allow-opaque-overlay` moves them verbatim. + unknowable). Two distinct consents: `-allow-opaque-overlay` RELOCATES + them verbatim (a self-locating payload such as a metakit vfs survives); + `-drop-opaque-overlay` DISCARDS them - correct only for expendable + trailing data such as the COFF symbol/debug tables mingw-built runtimes + carry after their sections (a real payload would be destroyed). A drop + that removes the region the COFF `PointerToSymbolTable` points into also + zeroes that pointer and `NumberOfSymbols` instead of leaving them + dangling (which is what Windows' own `EndUpdateResource` leaves behind + when it strips trailing data). - Every `set-icon` run re-parses the file it just built and verifies the resources and the overlay before reporting success (read-back is built in). - Deterministic: stamping the same input twice yields byte-identical output. @@ -38,7 +46,7 @@ Windows. punkres extract-ico [-group ] punkres set-icon [-o ] [-group ] [-allow-file-relative-shift] [-allow-opaque-overlay] - [-strip-signature] + [-drop-opaque-overlay] [-strip-signature] punkres version `set-icon` follows the semantics of punkshell's twapi arm exactly: ALL existing diff --git a/src/tools/punkres/build.zig.zon b/src/tools/punkres/build.zig.zon index bd0f094e..dbcb1342 100644 --- a/src/tools/punkres/build.zig.zon +++ b/src/tools/punkres/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .punkres, - .version = "0.2.0", + .version = "0.3.0", .fingerprint = 0x663d2953305aee5d, .minimum_zig_version = "0.16.0", .paths = .{""}, diff --git a/src/tools/punkres/src/punkres.zig b/src/tools/punkres/src/punkres.zig index 98a48e81..180d2892 100644 --- a/src/tools/punkres/src/punkres.zig +++ b/src/tools/punkres/src/punkres.zig @@ -10,7 +10,13 @@ //are moved verbatim; file-relative ones are REFUSED by default and stamped //only with -allow-file-relative-shift (offsets rewritten by the shift delta, //convention preserved); zip64/multi-disk are refused always; non-zip overlay -//bytes need -allow-opaque-overlay. +//bytes need explicit consent - -allow-opaque-overlay RELOCATES them verbatim +//(a real payload, e.g. metakit, survives if it self-locates), while +//-drop-opaque-overlay DISCARDS them (correct only for expendable trailing +//data such as the COFF symbol/debug tables mingw-built runtimes carry after +//their sections; a real payload would be destroyed). When a drop removes the +//region the COFF PointerToSymbolTable points into, that pointer and +//NumberOfSymbols are zeroed rather than left dangling. // //Exit codes: 0 ok, 1 error, 2 hard refusal (zip64/multi-disk/odd signature), //3 refusal that a consent flag can lift (stated in the message). @@ -34,10 +40,10 @@ var g_io: Io = undefined; //stdout with honestly-probed flags - set at entry (see stdout_file) var g_stdout: Io.File = undefined; -pub const VERSION = "0.2.0"; +pub const VERSION = "0.3.0"; pub const HEADER = - \\# PUNKRES 0.2.0 -- portable PE resource stamper (punkshell G-128) + \\# PUNKRES 0.3.0 -- portable PE resource stamper (punkshell G-128) \\ ; @@ -117,7 +123,7 @@ fn print_usage(exe: []const u8) !void { \\ {s} extract-ico [-group ] \\ {s} set-icon [-o ] [-group ] \\ [-allow-file-relative-shift] [-allow-opaque-overlay] - \\ [-strip-signature] + \\ [-drop-opaque-overlay] [-strip-signature] \\ {s} version \\ \\ set-icon replaces ALL RT_ICON/RT_GROUP_ICON resources with the images @@ -125,8 +131,12 @@ fn print_usage(exe: []const u8) !void { \\ language; name 1 / lang 1033 when none). -group overrides the group \\ NAME (stored as given; lookups are case-insensitive) - language and \\ codepage adoption are unchanged. Every other resource type and any - \\ appended payload are preserved. Without -o the file is updated in - \\ place. Exit 3 = refused, a consent flag applies. + \\ appended payload are preserved. Non-zip trailing bytes: + \\ -allow-opaque-overlay RELOCATES them (a self-locating payload such + \\ as metakit survives); -drop-opaque-overlay DISCARDS them (only for + \\ expendable data such as COFF symbol tables - a real payload would + \\ be destroyed). Without -o the file is updated in place. + \\ Exit 3 = refused, a consent flag applies. \\ , .{ tail, tail, tail, tail, tail }); } @@ -540,10 +550,15 @@ const SetIconFlags = struct { group_arg: ?[]const u8 = null, allow_file_relative_shift: bool = false, allow_opaque_overlay: bool = false, + drop_opaque_overlay: bool = false, strip_signature: bool = false, }; fn cmd_set_icon(pe_path: []const u8, ico_path: []const u8, flags: SetIconFlags) !u8 { + if (flags.allow_opaque_overlay and flags.drop_opaque_overlay) { + errprintf("choose ONE of -allow-opaque-overlay (relocate) or -drop-opaque-overlay (discard) - they are opposite treatments of the same bytes.\n", .{}); + return 1; + } var data = try read_file(pe_path); const pe = pemod.Pe.parse(allocator, data) catch |err| { errprintf("{s}: not a usable PE image: {}\n", .{ pe_path, err }); @@ -575,16 +590,21 @@ fn cmd_set_icon(pe_path: []const u8, ico_path: []const u8, flags: SetIconFlags) } //overlay classification and consent gates - const rep = try classifyOverlay(data, pe.overlay_offset, file_end); + var rep = try classifyOverlay(data, pe.overlay_offset, file_end); + var dropped_bytes: usize = 0; switch (rep.kind) { .zip_refused => { errprintf("REFUSED: appended zip payload is {s} - punkres does not process zip64/multi-disk archives (no consent flag lifts this).\n", .{rep.refused_reason}); return 2; }, .data => { - if (!flags.allow_opaque_overlay) { + if (flags.drop_opaque_overlay) { + dropped_bytes = rep.size; + file_end = pe.overlay_offset; + rep = .{ .kind = .none, .size = 0 }; + } else if (!flags.allow_opaque_overlay) { errprintf("REFUSED: {d} bytes of non-zip overlay follow the PE image. punkres cannot know whether their internal offsets survive being moved.\n", .{rep.size}); - errprintf("Remedy: if the payload is safe to relocate verbatim (or offset-free), re-run with -allow-opaque-overlay.\n", .{}); + errprintf("Remedy: -allow-opaque-overlay RELOCATES them verbatim (a self-locating payload such as metakit survives); -drop-opaque-overlay DISCARDS them (only for expendable trailing data such as COFF symbol/debug tables - a real payload would be destroyed).\n", .{}); return 3; } }, @@ -684,7 +704,23 @@ fn cmd_set_icon(pe_path: []const u8, ico_path: []const u8, flags: SetIconFlags) try printf("overlay: {d} opaque bytes moved verbatim by {d} (consented)\n", .{ overlay_len, delta }); } } else { - try print("overlay: none\n"); + if (dropped_bytes > 0) { + try printf("overlay: {d} bytes of non-zip trailing data dropped (consented) - nothing appended\n", .{dropped_bytes}); + } else { + try print("overlay: none\n"); + } + } + //a drop that removed the region the COFF symbol table lives in must not + //leave the header pointing past EOF (twapi's EndUpdateResource leaves it + //dangling; punkres zeroes it honestly) + if (dropped_bytes > 0) { + const psym = mem.readInt(u32, final[pe.coff_offset + 8 ..][0..4], .little); + const nsym = mem.readInt(u32, final[pe.coff_offset + 12 ..][0..4], .little); + if (psym != 0 and psym >= pe.overlay_offset) { + mem.writeInt(u32, final[pe.coff_offset + 8 ..][0..4], 0, .little); + mem.writeInt(u32, final[pe.coff_offset + 12 ..][0..4], 0, .little); + try printf("coff_symbols: symbol table ({d} entries) was in the dropped region - header pointer zeroed\n", .{nsym}); + } } if (stripped_signature) { //zero the security directory in the OUTPUT (table bytes were dropped) @@ -853,6 +889,9 @@ pub fn main(init: std.process.Init) !u8 { } else if (mem.eql(u8, argv[i], "-allow-opaque-overlay")) { flags.allow_opaque_overlay = true; i += 1; + } else if (mem.eql(u8, argv[i], "-drop-opaque-overlay")) { + flags.drop_opaque_overlay = true; + i += 1; } else if (mem.eql(u8, argv[i], "-strip-signature")) { flags.strip_signature = true; i += 1; diff --git a/src/tools/punkres/src/punkres_test.zig b/src/tools/punkres/src/punkres_test.zig index 5dad0bfd..56ae3ef1 100644 --- a/src/tools/punkres/src/punkres_test.zig +++ b/src/tools/punkres/src/punkres_test.zig @@ -187,7 +187,7 @@ test "print usage" { \\ punkres.exe extract-ico [-group ] \\ punkres.exe set-icon [-o ] [-group ] \\ [-allow-file-relative-shift] [-allow-opaque-overlay] - \\ [-strip-signature] + \\ [-drop-opaque-overlay] [-strip-signature] \\ punkres.exe version \\ \\ set-icon replaces ALL RT_ICON/RT_GROUP_ICON resources with the images @@ -195,8 +195,12 @@ test "print usage" { \\ language; name 1 / lang 1033 when none). -group overrides the group \\ NAME (stored as given; lookups are case-insensitive) - language and \\ codepage adoption are unchanged. Every other resource type and any - \\ appended payload are preserved. Without -o the file is updated in - \\ place. Exit 3 = refused, a consent flag applies. + \\ appended payload are preserved. Non-zip trailing bytes: + \\ -allow-opaque-overlay RELOCATES them (a self-locating payload such + \\ as metakit survives); -drop-opaque-overlay DISCARDS them (only for + \\ expendable data such as COFF symbol tables - a real payload would + \\ be destroyed). Without -o the file is updated in place. + \\ Exit 3 = refused, a consent flag applies. \\ ; try expectExecute(&args, 1, output, tmp); @@ -206,7 +210,7 @@ test "version" { var tmp = initTestDir(); defer tmp.cleanup(); const args = [_][]const u8{"version"}; - try expectExecute(&args, 0, "punkres 0.2.0\n", tmp); + try expectExecute(&args, 0, "punkres 0.3.0\n", tmp); } test "set-icon -group renames the group; later stamps adopt it (convergent)" { @@ -472,6 +476,76 @@ test "opaque non-zip overlay: refused by default, moved verbatim with consent" { try expect(mem.eql(u8, out[out.len - blob.len ..], blob)); } +test "drop-opaque-overlay discards expendable trailing data and zeroes the dangling symbol pointer" { + //the shape mingw-built runtimes hand the stub-stamping seam: PE image + //followed by its COFF symbol/debug tables (located by the COFF header + //pointer, outside every section). -drop discards them; the output must be + //byte-identical to stamping the same image with no trailing data at all + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var tmp = initTestDir(); + defer tmp.cleanup(); + + const img = try synthpe.buildPe(.{ .alloc = alloc }); + try writeTmp(tmp, "clean.exe", img); + const junk = "FAKE-COFF-SYMBOL-TABLE-DATA .file g-crtbegin.c ..............."; + const dirty = try alloc.alloc(u8, img.len + junk.len); + @memcpy(dirty[0..img.len], img); + @memcpy(dirty[img.len..], junk); + //point the COFF symbol-table header fields at the trailing region + //(e_lfanew 0x80 -> COFF header at 0x84 -> PointerToSymbolTable at +8) + std.mem.writeInt(u32, dirty[0x8c..][0..4], @intCast(img.len), .little); + std.mem.writeInt(u32, dirty[0x90..][0..4], 12332, .little); + //keep the stored checksum honest after the header patch (parse tolerates + //any checksum; equality with the clean stamp is what matters below) + try writeTmp(tmp, "dirty.exe", dirty); + const test_ico = try synthpe.buildTestIco(alloc, 2); + try writeTmp(tmp, "test.ico", test_ico); + + //refusal without a flag names BOTH remedies + const refuse_args = [_][]const u8{ "set-icon", "-o", "dout.exe", "dirty.exe", "test.ico" }; + try expectExecuteContains(&refuse_args, 3, &[_][]const u8{}, &[_][]const u8{ + "-allow-opaque-overlay RELOCATES", + "-drop-opaque-overlay DISCARDS", + }, tmp); + + //conflicting consent flags are rejected + const conflict_args = [_][]const u8{ "set-icon", "-o", "dout.exe", "-allow-opaque-overlay", "-drop-opaque-overlay", "dirty.exe", "test.ico" }; + try expectExecuteContains(&conflict_args, 1, &[_][]const u8{}, &[_][]const u8{ + "choose ONE", + }, tmp); + + //drop: stamps, reports the drop and the symbol-pointer zeroing + const drop_args = [_][]const u8{ "set-icon", "-o", "dout.exe", "-drop-opaque-overlay", "dirty.exe", "test.ico" }; + try expectExecuteContains(&drop_args, 0, &[_][]const u8{ + "bytes of non-zip trailing data dropped (consented)", + "coff_symbols: symbol table (12332 entries) was in the dropped region - header pointer zeroed", + "verify: resources re-read OK; no overlay", + }, &[_][]const u8{}, tmp); + + //byte-identical to stamping the trailing-free image + const clean_args = [_][]const u8{ "set-icon", "-o", "cout.exe", "clean.exe", "test.ico" }; + try expectExecuteContains(&clean_args, 0, &[_][]const u8{}, &[_][]const u8{}, tmp); + const dout = try readTmp(alloc, tmp, "dout.exe"); + const cout = try readTmp(alloc, tmp, "cout.exe"); + try expect(mem.eql(u8, dout, cout)); + + //-drop alongside a ZIP overlay is inert: the payload is preserved + const entries = [_]synthpe.ZipEntry{.{ .name = "p.txt", .data = "payload" }}; + const zip_bytes = try synthpe.buildStoredZip(alloc, &entries, .archive, 0, ""); + const zkit = try alloc.alloc(u8, img.len + zip_bytes.len); + @memcpy(zkit[0..img.len], img); + @memcpy(zkit[img.len..], zip_bytes); + try writeTmp(tmp, "zkit.exe", zkit); + const zdrop_args = [_][]const u8{ "set-icon", "-o", "zout.exe", "-drop-opaque-overlay", "zkit.exe", "test.ico" }; + try expectExecuteContains(&zdrop_args, 0, &[_][]const u8{ + "bytes preserved verbatim", + }, &[_][]const u8{}, tmp); + const zout = try readTmp(alloc, tmp, "zout.exe"); + try expect(mem.eql(u8, zout[zout.len - zip_bytes.len ..], zip_bytes)); +} + test "archive-relative zip preceded by other overlay bytes (nested-payload shape) stamps and verifies" { //the punkshell tclsfe stub carries its own zip payload; concatenating a //further zip (the zipcat shape built from a raw stub) yields an overlay