mirror of
https://github.com/oven-sh/bun
synced 2026-02-08 01:49:33 +00:00
Compare commits
4 Commits
dylan/pyth
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a6c95c484 | ||
|
|
51738a9646 | ||
|
|
3afc1f9594 | ||
|
|
1c54474dfc |
@@ -645,10 +645,6 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "build.zig")
|
||||
|
||||
set(BUN_USOCKETS_SOURCE ${CWD}/packages/bun-usockets)
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND BUN_CXX_SOURCES ${CWD}/src/bun.js/bindings/windows/rescle.cpp)
|
||||
list(APPEND BUN_CXX_SOURCES ${CWD}/src/bun.js/bindings/windows/rescle-binding.cpp)
|
||||
endif()
|
||||
|
||||
register_repository(
|
||||
NAME
|
||||
|
||||
@@ -491,7 +491,7 @@ pub const StandaloneModuleGraph = struct {
|
||||
windows_hide_console: bool = false,
|
||||
};
|
||||
|
||||
pub fn inject(bytes: []const u8, self_exe: [:0]const u8, inject_options: InjectOptions, target: *const CompileTarget) bun.FileDescriptor {
|
||||
pub fn inject(bytes: []const u8, self_exe: [:0]const u8, inject_options: InjectOptions, target: *const CompileTarget) !bun.FileDescriptor {
|
||||
var buf: bun.PathBuffer = undefined;
|
||||
var zname: [:0]const u8 = bun.span(bun.fs.FileSystem.instance.tmpname("bun-build", &buf, @as(u64, @bitCast(std.time.milliTimestamp()))) catch |err| {
|
||||
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get temporary file name: {s}", .{@errorName(err)});
|
||||
@@ -503,7 +503,7 @@ pub const StandaloneModuleGraph = struct {
|
||||
// Ensure we own the file
|
||||
if (Environment.isPosix) {
|
||||
// Make the file writable so we can delete it
|
||||
_ = Syscall.fchmod(fd, 0o777);
|
||||
_ = Syscall.fchmod(fd, 0o755);
|
||||
}
|
||||
fd.close();
|
||||
_ = Syscall.unlink(name);
|
||||
@@ -559,7 +559,7 @@ pub const StandaloneModuleGraph = struct {
|
||||
const fd = brk2: {
|
||||
var tried_changing_abs_dir = false;
|
||||
for (0..3) |retry| {
|
||||
switch (Syscall.open(zname, bun.O.CLOEXEC | bun.O.RDWR | bun.O.CREAT, 0)) {
|
||||
switch (Syscall.open(zname, bun.O.CLOEXEC | bun.O.RDWR | bun.O.CREAT, 0o755)) {
|
||||
.result => |res| break :brk2 res,
|
||||
.err => |err| {
|
||||
if (retry < 2) {
|
||||
@@ -627,6 +627,12 @@ pub const StandaloneModuleGraph = struct {
|
||||
cleanup(zname, fd);
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Ensure the file has proper permissions after copying
|
||||
if (comptime !Environment.isWindows) {
|
||||
_ = bun.c.fchmod(fd.native(), 0o644);
|
||||
}
|
||||
|
||||
break :brk fd;
|
||||
};
|
||||
|
||||
@@ -678,51 +684,91 @@ pub const StandaloneModuleGraph = struct {
|
||||
Global.exit(1);
|
||||
};
|
||||
if (comptime !Environment.isWindows) {
|
||||
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o777);
|
||||
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o755);
|
||||
}
|
||||
return cloned_executable_fd;
|
||||
},
|
||||
.windows => {
|
||||
const input_result = bun.sys.File.readToEnd(.{ .handle = cloned_executable_fd }, bun.default_allocator);
|
||||
if (input_result.err) |err| {
|
||||
Output.prettyErrorln("Error reading standalone module graph: {}", .{err});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
Global.exit(1);
|
||||
}
|
||||
var pe_file = bun.pe.PEFile.init(bun.default_allocator, input_result.bytes.items) catch |err| {
|
||||
Output.prettyErrorln("Error initializing PE file: {}", .{err});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
Global.exit(1);
|
||||
};
|
||||
defer pe_file.deinit();
|
||||
pe_file.addBunSection(bytes) catch |err| {
|
||||
Output.prettyErrorln("Error adding Bun section to PE file: {}", .{err});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
Global.exit(1);
|
||||
};
|
||||
input_result.bytes.deinit();
|
||||
// Implement .bun section support for Windows executables
|
||||
const pe_module = @import("./pe.zig");
|
||||
|
||||
switch (Syscall.setFileOffset(cloned_executable_fd, 0)) {
|
||||
// Get the path from fd
|
||||
var path_buf: bun.PathBuffer = undefined;
|
||||
const path = cloned_executable_fd.getFdPath(&path_buf) catch |err| {
|
||||
Output.prettyErrorln("Error getting executable path: {}", .{err});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// We need to close the fd before modifying the file
|
||||
cloned_executable_fd.close();
|
||||
|
||||
// Create temporary path for PE with .bun section
|
||||
const tmp_path = std.fmt.allocPrint(bun.default_allocator, "{s}.bun.tmp", .{path}) catch |err| {
|
||||
Output.prettyErrorln("Error allocating temporary path: {}", .{err});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
Global.exit(1);
|
||||
};
|
||||
defer bun.default_allocator.free(tmp_path);
|
||||
|
||||
// Load the PE file
|
||||
const pe_data = std.fs.cwd().readFileAlloc(bun.default_allocator, path, std.math.maxInt(usize)) catch |err| {
|
||||
Output.prettyErrorln("Error reading PE file: {}", .{err});
|
||||
Global.exit(1);
|
||||
};
|
||||
defer bun.default_allocator.free(pe_data);
|
||||
|
||||
var pe_obj = pe_module.PEFile.init(bun.default_allocator, pe_data) catch |err| {
|
||||
Output.prettyErrorln("Error parsing PE file: {}", .{err});
|
||||
Global.exit(1);
|
||||
};
|
||||
defer pe_obj.deinit();
|
||||
|
||||
// Add .bun section
|
||||
pe_obj.addBunSection(bytes) catch |err| {
|
||||
Output.prettyErrorln("Error adding .bun section: {}", .{err});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Write to temporary file
|
||||
const tmp_file = std.fs.cwd().createFile(tmp_path, .{}) catch |err| {
|
||||
Output.prettyErrorln("Error creating temporary file: {}", .{err});
|
||||
Global.exit(1);
|
||||
};
|
||||
defer tmp_file.close();
|
||||
|
||||
pe_obj.write(tmp_file.writer()) catch |err| {
|
||||
Output.prettyErrorln("Error writing PE file: {}", .{err});
|
||||
std.fs.cwd().deleteFile(tmp_path) catch {};
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Move temporary file to final location
|
||||
std.fs.cwd().rename(tmp_path, path) catch |err| {
|
||||
Output.prettyErrorln("Error renaming temporary file: {}", .{err});
|
||||
std.fs.cwd().deleteFile(tmp_path) catch {};
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Reopen the modified file
|
||||
var path_z_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
|
||||
const path_z = std.fmt.bufPrintZ(&path_z_buf, "{s}", .{path}) catch {
|
||||
Output.prettyErrorln("Error creating null-terminated path", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
const modified_fd = switch (Syscall.open(path_z, bun.O.CLOEXEC | bun.O.RDWR, 0)) {
|
||||
.result => |res| res,
|
||||
.err => |err| {
|
||||
Output.prettyErrorln("Error seeking to start of temporary file: {}", .{err});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
Output.prettyErrorln("Error reopening modified executable: {}", .{err});
|
||||
Global.exit(1);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
var file = bun.sys.File{ .handle = cloned_executable_fd };
|
||||
const writer = file.writer();
|
||||
pe_file.write(writer) catch |err| {
|
||||
Output.prettyErrorln("Error writing PE file: {}", .{err});
|
||||
cleanup(zname, cloned_executable_fd);
|
||||
Global.exit(1);
|
||||
};
|
||||
// Set executable permissions when running on POSIX hosts, even for Windows targets
|
||||
if (comptime !Environment.isWindows) {
|
||||
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o777);
|
||||
|
||||
// Set executable permissions (not needed on Windows)
|
||||
if (comptime !bun.Environment.isWindows) {
|
||||
_ = bun.c.fchmod(modified_fd.native(), 0o755);
|
||||
}
|
||||
return cloned_executable_fd;
|
||||
return modified_fd;
|
||||
},
|
||||
else => {
|
||||
var total_byte_count: usize = undefined;
|
||||
@@ -788,7 +834,7 @@ pub const StandaloneModuleGraph = struct {
|
||||
// the final 8 bytes in the file are the length of the module graph with padding, excluding the trailer and offsets
|
||||
_ = Syscall.write(cloned_executable_fd, std.mem.asBytes(&total_byte_count));
|
||||
if (comptime !Environment.isWindows) {
|
||||
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o777);
|
||||
_ = bun.c.fchmod(cloned_executable_fd.native(), 0o755);
|
||||
}
|
||||
|
||||
return cloned_executable_fd;
|
||||
@@ -831,13 +877,12 @@ pub const StandaloneModuleGraph = struct {
|
||||
outfile: []const u8,
|
||||
env: *bun.DotEnv.Loader,
|
||||
output_format: bun.options.Format,
|
||||
windows_hide_console: bool,
|
||||
windows_icon: ?[]const u8,
|
||||
windows: bun.options.WindowsSettings,
|
||||
) !void {
|
||||
const bytes = try toBytes(allocator, module_prefix, output_files, output_format);
|
||||
if (bytes.len == 0) return;
|
||||
|
||||
const fd = inject(
|
||||
const fd = try inject(
|
||||
bytes,
|
||||
if (target.isDefault())
|
||||
bun.selfExePath() catch |err| {
|
||||
@@ -849,7 +894,7 @@ pub const StandaloneModuleGraph = struct {
|
||||
Output.err(err, "failed to download cross-compiled bun executable", .{});
|
||||
Global.exit(1);
|
||||
},
|
||||
.{ .windows_hide_console = windows_hide_console },
|
||||
.{ .windows_hide_console = windows.hide_console },
|
||||
target,
|
||||
);
|
||||
bun.debugAssert(fd.kind == .system);
|
||||
@@ -875,15 +920,75 @@ pub const StandaloneModuleGraph = struct {
|
||||
|
||||
Global.exit(1);
|
||||
};
|
||||
fd.close();
|
||||
// Apply Windows resource edits if needed
|
||||
if (windows.icon != null or windows.title != null or windows.publisher != null or windows.version != null or windows.description != null or windows.hide_console) {
|
||||
const pe_module = @import("./pe.zig");
|
||||
|
||||
if (windows_icon) |icon_utf8| {
|
||||
var icon_buf: bun.OSPathBuffer = undefined;
|
||||
const icon = bun.strings.toWPathNormalized(&icon_buf, icon_utf8);
|
||||
bun.windows.rescle.setIcon(outfile_slice, icon) catch {
|
||||
Output.warn("Failed to set executable icon", .{});
|
||||
// Get file size
|
||||
const size = if (Environment.isWindows)
|
||||
Syscall.setFileOffsetToEndWindows(fd).unwrap() catch |err| {
|
||||
Output.err(err, "failed to get file size", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
else blk: {
|
||||
const fstat = Syscall.fstat(fd).unwrap() catch |err| {
|
||||
Output.err(err, "failed to stat file", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
break :blk @as(usize, @intCast(fstat.size));
|
||||
};
|
||||
|
||||
_ = Syscall.setFileOffset(fd, 0).unwrap() catch |err| {
|
||||
Output.err(err, "failed to seek to start", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Read entire file
|
||||
const pe_data = try allocator.alloc(u8, size);
|
||||
defer allocator.free(pe_data);
|
||||
|
||||
const read_result = Syscall.readAll(fd, pe_data);
|
||||
_ = read_result.unwrap() catch |err| {
|
||||
Output.err(err, "failed to read PE file", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
var pe_file = try pe_module.PEFile.init(allocator, pe_data);
|
||||
defer pe_file.deinit();
|
||||
|
||||
pe_file.applyWindowsSettings(&windows, allocator) catch |err| {
|
||||
if (windows.icon != null and (err == error.InvalidIconData or err == error.InvalidIconFormat)) {
|
||||
Output.errGeneric("Invalid icon file: {s}", .{windows.icon.?});
|
||||
Output.flush();
|
||||
Global.exit(1);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
|
||||
// Write back modified PE
|
||||
var write_buffer = std.ArrayList(u8).init(allocator);
|
||||
defer write_buffer.deinit();
|
||||
try pe_file.write(write_buffer.writer());
|
||||
|
||||
// Seek to start and write
|
||||
_ = Syscall.setFileOffset(fd, 0).unwrap() catch |err| {
|
||||
Output.err(err, "failed to seek to start for write", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
_ = Syscall.write(fd, write_buffer.items).unwrap() catch |err| {
|
||||
Output.err(err, "failed to write modified PE", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Truncate if new size is smaller
|
||||
_ = Syscall.ftruncate(fd, @intCast(write_buffer.items.len)).unwrap() catch |err| {
|
||||
Output.err(err, "failed to truncate file", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
}
|
||||
|
||||
fd.close();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -911,6 +1016,57 @@ pub const StandaloneModuleGraph = struct {
|
||||
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Apply Windows resource edits if needed
|
||||
if (target.os == .windows and (windows.icon != null or windows.title != null or windows.publisher != null or windows.version != null or windows.description != null or windows.hide_console)) {
|
||||
const pe_module = @import("./pe.zig");
|
||||
|
||||
// Read the PE file
|
||||
const pe_data = std.fs.cwd().readFileAlloc(allocator, outfile, std.math.maxInt(usize)) catch |err| {
|
||||
Output.err(err, "failed to read PE file for resource editing", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
defer allocator.free(pe_data);
|
||||
|
||||
// Parse and modify PE
|
||||
var pe_obj = pe_module.PEFile.init(allocator, pe_data) catch |err| {
|
||||
Output.err(err, "failed to parse PE file", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
defer pe_obj.deinit();
|
||||
|
||||
pe_obj.applyWindowsSettings(&windows, allocator) catch |err| {
|
||||
if (windows.icon != null and (err == error.InvalidIconData or err == error.InvalidIconFormat)) {
|
||||
Output.errGeneric("Invalid icon file: {s}", .{windows.icon.?});
|
||||
} else {
|
||||
Output.err(err, "failed to apply Windows settings", .{});
|
||||
}
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Write to temporary file then rename
|
||||
const tmp_name = try std.fmt.allocPrint(allocator, "{s}.tmp", .{outfile});
|
||||
defer allocator.free(tmp_name);
|
||||
|
||||
{
|
||||
const tmp_file = std.fs.cwd().createFile(tmp_name, .{}) catch |err| {
|
||||
Output.err(err, "failed to create temporary file", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
defer tmp_file.close();
|
||||
|
||||
pe_obj.write(tmp_file.writer()) catch |err| {
|
||||
Output.err(err, "failed to write modified PE", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
}
|
||||
|
||||
std.fs.cwd().rename(tmp_name, outfile) catch |err| {
|
||||
Output.err(err, "failed to replace PE file", .{});
|
||||
std.fs.cwd().deleteFile(tmp_name) catch {};
|
||||
Global.exit(1);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fromExecutable(allocator: std.mem.Allocator) !?StandaloneModuleGraph {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#include "root.h"
|
||||
#include "rescle.h"
|
||||
|
||||
extern "C" int rescle__setIcon(const WCHAR* exeFilename, const WCHAR* iconFilename)
|
||||
{
|
||||
rescle::ResourceUpdater updater;
|
||||
if (!updater.Load(exeFilename))
|
||||
return -1;
|
||||
if (!updater.SetIcon(iconFilename))
|
||||
return -2;
|
||||
if (!updater.Commit())
|
||||
return -3;
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,211 +0,0 @@
|
||||
// This file is from Electron's fork of rescle
|
||||
// https://github.com/electron/rcedit/blob/e36b688b42df0e236922019ce14e0ea165dc176d/src/rescle.h
|
||||
// 'bun build --compile' uses this on Windows to allow
|
||||
// patching the icon of the generated executable.
|
||||
//
|
||||
// Copyright (c) 2013 GitHub Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject to
|
||||
// the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
// Copyright (c) 2013 GitHub, Inc. All rights reserved.
|
||||
// Use of this source code is governed by MIT license that can be found in the
|
||||
// LICENSE file.
|
||||
//
|
||||
// This file is modified from Rescle written by yoshio.okumura@gmail.com:
|
||||
// http://code.google.com/p/rescle/
|
||||
|
||||
#ifndef VERSION_INFO_UPDATER
|
||||
#define VERSION_INFO_UPDATER
|
||||
|
||||
#ifndef _UNICODE
|
||||
#define _UNICODE
|
||||
#endif
|
||||
|
||||
#ifndef UNICODE
|
||||
#define UNICODE
|
||||
#endif
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
#include <windows.h>
|
||||
#include <memory> // unique_ptr
|
||||
|
||||
#define RU_VS_COMMENTS L"Comments"
|
||||
#define RU_VS_COMPANY_NAME L"CompanyName"
|
||||
#define RU_VS_FILE_DESCRIPTION L"FileDescription"
|
||||
#define RU_VS_FILE_VERSION L"FileVersion"
|
||||
#define RU_VS_INTERNAL_NAME L"InternalName"
|
||||
#define RU_VS_LEGAL_COPYRIGHT L"LegalCopyright"
|
||||
#define RU_VS_LEGAL_TRADEMARKS L"LegalTrademarks"
|
||||
#define RU_VS_ORIGINAL_FILENAME L"OriginalFilename"
|
||||
#define RU_VS_PRIVATE_BUILD L"PrivateBuild"
|
||||
#define RU_VS_PRODUCT_NAME L"ProductName"
|
||||
#define RU_VS_PRODUCT_VERSION L"ProductVersion"
|
||||
#define RU_VS_SPECIAL_BUILD L"SpecialBuild"
|
||||
|
||||
namespace rescle {
|
||||
|
||||
struct IconsValue {
|
||||
typedef struct _ICONENTRY {
|
||||
BYTE width;
|
||||
BYTE height;
|
||||
BYTE colorCount;
|
||||
BYTE reserved;
|
||||
WORD planes;
|
||||
WORD bitCount;
|
||||
DWORD bytesInRes;
|
||||
DWORD imageOffset;
|
||||
} ICONENTRY;
|
||||
|
||||
typedef struct _ICONHEADER {
|
||||
WORD reserved;
|
||||
WORD type;
|
||||
WORD count;
|
||||
std::vector<ICONENTRY> entries;
|
||||
} ICONHEADER;
|
||||
|
||||
ICONHEADER header;
|
||||
std::vector<std::vector<BYTE>> images;
|
||||
std::vector<BYTE> grpHeader;
|
||||
};
|
||||
|
||||
struct Translate {
|
||||
LANGID wLanguage;
|
||||
WORD wCodePage;
|
||||
};
|
||||
|
||||
typedef std::pair<std::wstring, std::wstring> VersionString;
|
||||
typedef std::pair<const BYTE* const, const size_t> OffsetLengthPair;
|
||||
|
||||
struct VersionStringTable {
|
||||
Translate encoding;
|
||||
std::vector<VersionString> strings;
|
||||
};
|
||||
|
||||
class VersionInfo {
|
||||
public:
|
||||
VersionInfo();
|
||||
VersionInfo(HMODULE hModule, WORD languageId);
|
||||
|
||||
std::vector<BYTE> Serialize() const;
|
||||
|
||||
bool HasFixedFileInfo() const;
|
||||
VS_FIXEDFILEINFO& GetFixedFileInfo();
|
||||
const VS_FIXEDFILEINFO& GetFixedFileInfo() const;
|
||||
void SetFixedFileInfo(const VS_FIXEDFILEINFO& value);
|
||||
|
||||
std::vector<VersionStringTable> stringTables;
|
||||
std::vector<Translate> supportedTranslations;
|
||||
|
||||
private:
|
||||
VS_FIXEDFILEINFO fixedFileInfo_;
|
||||
|
||||
void FillDefaultData();
|
||||
void DeserializeVersionInfo(const BYTE* pData, size_t size);
|
||||
|
||||
VersionStringTable DeserializeVersionStringTable(const BYTE* tableData);
|
||||
void DeserializeVersionStringFileInfo(const BYTE* offset, size_t length, std::vector<VersionStringTable>& stringTables);
|
||||
void DeserializeVarFileInfo(const unsigned char* offset, std::vector<Translate>& translations);
|
||||
OffsetLengthPair GetChildrenData(const BYTE* entryData);
|
||||
};
|
||||
|
||||
class ResourceUpdater {
|
||||
public:
|
||||
typedef std::vector<std::wstring> StringValues;
|
||||
typedef std::map<UINT, StringValues> StringTable;
|
||||
typedef std::map<WORD, StringTable> StringTableMap;
|
||||
typedef std::map<LANGID, VersionInfo> VersionStampMap;
|
||||
typedef std::map<UINT, std::unique_ptr<IconsValue>> IconTable;
|
||||
typedef std::vector<BYTE> RcDataValue;
|
||||
typedef std::map<ptrdiff_t, RcDataValue> RcDataMap;
|
||||
typedef std::map<LANGID, RcDataMap> RcDataLangMap;
|
||||
|
||||
struct IconResInfo {
|
||||
UINT maxIconId = 0;
|
||||
IconTable iconBundles;
|
||||
};
|
||||
|
||||
typedef std::map<LANGID, IconResInfo> IconTableMap;
|
||||
|
||||
ResourceUpdater();
|
||||
~ResourceUpdater();
|
||||
|
||||
bool Load(const WCHAR* filename);
|
||||
bool SetVersionString(WORD languageId, const WCHAR* name, const WCHAR* value);
|
||||
bool SetVersionString(const WCHAR* name, const WCHAR* value);
|
||||
const WCHAR* GetVersionString(WORD languageId, const WCHAR* name);
|
||||
const WCHAR* GetVersionString(const WCHAR* name);
|
||||
bool SetProductVersion(WORD languageId, UINT id, unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4);
|
||||
bool SetProductVersion(unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4);
|
||||
bool SetFileVersion(WORD languageId, UINT id, unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4);
|
||||
bool SetFileVersion(unsigned short v1, unsigned short v2, unsigned short v3, unsigned short v4);
|
||||
bool ChangeString(WORD languageId, UINT id, const WCHAR* value);
|
||||
bool ChangeString(UINT id, const WCHAR* value);
|
||||
bool ChangeRcData(UINT id, const WCHAR* pathToResource);
|
||||
const WCHAR* GetString(WORD languageId, UINT id);
|
||||
const WCHAR* GetString(UINT id);
|
||||
bool SetIcon(const WCHAR* path, const LANGID& langId, UINT iconBundle);
|
||||
bool SetIcon(const WCHAR* path, const LANGID& langId);
|
||||
bool SetIcon(const WCHAR* path);
|
||||
bool SetExecutionLevel(const WCHAR* value);
|
||||
bool IsExecutionLevelSet();
|
||||
bool SetApplicationManifest(const WCHAR* value);
|
||||
bool IsApplicationManifestSet();
|
||||
bool Commit();
|
||||
|
||||
private:
|
||||
bool SerializeStringTable(const StringValues& values, UINT blockId, std::vector<char>* out);
|
||||
|
||||
static BOOL CALLBACK OnEnumResourceName(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam);
|
||||
static BOOL CALLBACK OnEnumResourceManifest(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam);
|
||||
static BOOL CALLBACK OnEnumResourceLanguage(HANDLE hModule, LPCWSTR lpszType, LPCWSTR lpszName, WORD wIDLanguage, LONG_PTR lParam);
|
||||
|
||||
HMODULE module_;
|
||||
std::wstring filename_;
|
||||
std::wstring executionLevel_;
|
||||
std::wstring originalExecutionLevel_;
|
||||
std::wstring applicationManifestPath_;
|
||||
std::wstring manifestString_;
|
||||
VersionStampMap versionStampMap_;
|
||||
StringTableMap stringTableMap_;
|
||||
IconTableMap iconBundleMap_;
|
||||
RcDataLangMap rcDataLngMap_;
|
||||
};
|
||||
|
||||
class ScopedResourceUpdater {
|
||||
public:
|
||||
ScopedResourceUpdater(const WCHAR* filename, bool deleteOld);
|
||||
~ScopedResourceUpdater();
|
||||
|
||||
HANDLE Get() const;
|
||||
bool Commit();
|
||||
|
||||
private:
|
||||
bool EndUpdate(bool doesCommit);
|
||||
|
||||
HANDLE handle_;
|
||||
bool commited_ = false;
|
||||
};
|
||||
|
||||
} // namespace rescle
|
||||
|
||||
#endif // VERSION_INFO_UPDATER
|
||||
@@ -420,8 +420,7 @@ pub const Command = struct {
|
||||
// Compile options
|
||||
compile: bool = false,
|
||||
compile_target: Cli.CompileTarget = .{},
|
||||
windows_hide_console: bool = false,
|
||||
windows_icon: ?[]const u8 = null,
|
||||
windows: options.WindowsSettings = .{},
|
||||
};
|
||||
|
||||
pub fn create(allocator: std.mem.Allocator, log: *logger.Log, comptime command: Command.Tag) anyerror!Context {
|
||||
|
||||
@@ -171,6 +171,10 @@ pub const build_only_params = [_]ParamType{
|
||||
clap.parseParam("--env <inline|prefix*|disable> Inline environment variables into the bundle as process.env.${name}. Defaults to 'disable'. To inline environment variables matching a prefix, use my prefix like 'FOO_PUBLIC_*'.") catch unreachable,
|
||||
clap.parseParam("--windows-hide-console When using --compile targeting Windows, prevent a Command prompt from opening alongside the executable") catch unreachable,
|
||||
clap.parseParam("--windows-icon <STR> When using --compile targeting Windows, assign an executable icon") catch unreachable,
|
||||
clap.parseParam("--windows-title <STR> When using --compile targeting Windows, set the executable title") catch unreachable,
|
||||
clap.parseParam("--windows-publisher <STR> When using --compile targeting Windows, set the executable publisher") catch unreachable,
|
||||
clap.parseParam("--windows-version <STR> When using --compile targeting Windows, set the executable version (e.g. 1.2.3.4)") catch unreachable,
|
||||
clap.parseParam("--windows-description <STR> When using --compile targeting Windows, set the executable description") catch unreachable,
|
||||
} ++ if (FeatureFlags.bake_debugging_features) [_]ParamType{
|
||||
clap.parseParam("--debug-dump-server-files When --app is set, dump all server files to disk even when building statically") catch unreachable,
|
||||
clap.parseParam("--debug-no-minify When --app is set, do not minify anything") catch unreachable,
|
||||
@@ -884,26 +888,47 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
if (args.flag("--windows-hide-console")) {
|
||||
// --windows-hide-console technically doesnt depend on WinAPI, but since since --windows-icon
|
||||
// does, all of these customization options have been gated to windows-only
|
||||
if (!Environment.isWindows) {
|
||||
Output.errGeneric("Using --windows-hide-console is only available when compiling on Windows", .{});
|
||||
Global.crash();
|
||||
}
|
||||
// --windows-hide-console is now supported cross-platform
|
||||
if (!ctx.bundler_options.compile) {
|
||||
Output.errGeneric("--windows-hide-console requires --compile", .{});
|
||||
Global.crash();
|
||||
}
|
||||
ctx.bundler_options.windows_hide_console = true;
|
||||
ctx.bundler_options.windows.hide_console = true;
|
||||
}
|
||||
if (args.option("--windows-icon")) |path| {
|
||||
if (!Environment.isWindows) {
|
||||
Output.errGeneric("Using --windows-icon is only available when compiling on Windows", .{});
|
||||
Global.crash();
|
||||
}
|
||||
if (!ctx.bundler_options.compile) {
|
||||
Output.errGeneric("--windows-icon requires --compile", .{});
|
||||
Global.crash();
|
||||
}
|
||||
ctx.bundler_options.windows_icon = path;
|
||||
ctx.bundler_options.windows.icon = path;
|
||||
}
|
||||
if (args.option("--windows-title")) |title| {
|
||||
if (!ctx.bundler_options.compile) {
|
||||
Output.errGeneric("--windows-title requires --compile", .{});
|
||||
Global.crash();
|
||||
}
|
||||
ctx.bundler_options.windows.title = title;
|
||||
}
|
||||
if (args.option("--windows-publisher")) |publisher| {
|
||||
if (!ctx.bundler_options.compile) {
|
||||
Output.errGeneric("--windows-publisher requires --compile", .{});
|
||||
Global.crash();
|
||||
}
|
||||
ctx.bundler_options.windows.publisher = publisher;
|
||||
}
|
||||
if (args.option("--windows-version")) |version| {
|
||||
if (!ctx.bundler_options.compile) {
|
||||
Output.errGeneric("--windows-version requires --compile", .{});
|
||||
Global.crash();
|
||||
}
|
||||
ctx.bundler_options.windows.version = version;
|
||||
}
|
||||
if (args.option("--windows-description")) |description| {
|
||||
if (!ctx.bundler_options.compile) {
|
||||
Output.errGeneric("--windows-description requires --compile", .{});
|
||||
Global.crash();
|
||||
}
|
||||
ctx.bundler_options.windows.description = description;
|
||||
}
|
||||
|
||||
if (args.option("--outdir")) |outdir| {
|
||||
|
||||
@@ -434,8 +434,7 @@ pub const BuildCommand = struct {
|
||||
outfile,
|
||||
this_transpiler.env,
|
||||
this_transpiler.options.output_format,
|
||||
ctx.bundler_options.windows_hide_console,
|
||||
ctx.bundler_options.windows_icon,
|
||||
ctx.bundler_options.windows,
|
||||
);
|
||||
const compiled_elapsed = @divTrunc(@as(i64, @truncate(std.time.nanoTimestamp() - bundled_end)), @as(i64, std.time.ns_per_ms));
|
||||
const compiled_elapsed_digit_count: isize = switch (compiled_elapsed) {
|
||||
|
||||
@@ -11,6 +11,15 @@ pub const WriteDestination = enum {
|
||||
// eventually: wasm
|
||||
};
|
||||
|
||||
pub const WindowsSettings = struct {
|
||||
hide_console: bool = false,
|
||||
icon: ?[]const u8 = null,
|
||||
title: ?[]const u8 = null,
|
||||
publisher: ?[]const u8 = null,
|
||||
version: ?[]const u8 = null,
|
||||
description: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn validatePath(
|
||||
log: *logger.Log,
|
||||
_: *Fs.FileSystem.Implementation,
|
||||
|
||||
841
src/pe.zig
841
src/pe.zig
@@ -107,6 +107,101 @@ pub const PEFile = struct {
|
||||
const IMAGE_SCN_MEM_WRITE = 0x80000000;
|
||||
const IMAGE_SCN_MEM_EXECUTE = 0x20000000;
|
||||
|
||||
// Resource types
|
||||
const RT_ICON = 3;
|
||||
const RT_GROUP_ICON = 14;
|
||||
const RT_VERSION = 16;
|
||||
|
||||
// Language and code page IDs
|
||||
const LANGUAGE_ID_EN_US: u16 = 1033; // 0x0409, en-US
|
||||
const CODE_PAGE_ID_EN_US: u16 = 1200; // 0x04B0, UTF-16LE
|
||||
|
||||
// Version info constants
|
||||
const VS_FFI_SIGNATURE: u32 = 0xFEEF04BD;
|
||||
const VS_FFI_STRUCVERSION: u32 = 0x00010000;
|
||||
const VS_FFI_FILEFLAGSMASK: u32 = 0x0000003F;
|
||||
const VOS_NT_WINDOWS32: u32 = 0x00040004;
|
||||
const VFT_APP: u32 = 0x00000001;
|
||||
|
||||
// Resource directory structures
|
||||
const ResourceDirectoryTable = extern struct {
|
||||
characteristics: u32,
|
||||
time_date_stamp: u32,
|
||||
major_version: u16,
|
||||
minor_version: u16,
|
||||
number_of_name_entries: u16,
|
||||
number_of_id_entries: u16,
|
||||
};
|
||||
|
||||
const ResourceDirectoryEntry = extern struct {
|
||||
name_or_id: u32,
|
||||
offset_to_data: u32,
|
||||
};
|
||||
|
||||
const ResourceDataEntry = extern struct {
|
||||
offset_to_data: u32,
|
||||
size: u32,
|
||||
code_page: u32,
|
||||
reserved: u32,
|
||||
};
|
||||
|
||||
// Icon structures
|
||||
const IconDirectory = extern struct {
|
||||
reserved: u16,
|
||||
type: u16,
|
||||
count: u16,
|
||||
};
|
||||
|
||||
const IconDirectoryEntry = extern struct {
|
||||
width: u8,
|
||||
height: u8,
|
||||
color_count: u8,
|
||||
reserved: u8,
|
||||
planes: u16,
|
||||
bit_count: u16,
|
||||
bytes_in_res: u32,
|
||||
image_offset: u32,
|
||||
};
|
||||
|
||||
const GroupIconDirectoryEntry = extern struct {
|
||||
width: u8,
|
||||
height: u8,
|
||||
color_count: u8,
|
||||
reserved: u8,
|
||||
planes: u16,
|
||||
bit_count: u16,
|
||||
bytes_in_res: u32,
|
||||
id: u16,
|
||||
};
|
||||
|
||||
// Version info structures
|
||||
const VS_FIXEDFILEINFO = extern struct {
|
||||
signature: u32,
|
||||
struct_version: u32,
|
||||
file_version_ms: u32,
|
||||
file_version_ls: u32,
|
||||
product_version_ms: u32,
|
||||
product_version_ls: u32,
|
||||
file_flags_mask: u32,
|
||||
file_flags: u32,
|
||||
file_os: u32,
|
||||
file_type: u32,
|
||||
file_subtype: u32,
|
||||
file_date_ms: u32,
|
||||
file_date_ls: u32,
|
||||
};
|
||||
|
||||
const VS_VERSIONINFO = struct {
|
||||
length: u16,
|
||||
value_length: u16,
|
||||
type: u16,
|
||||
key: []const u16, // "VS_VERSION_INFO"
|
||||
padding1: []const u8,
|
||||
fixed_file_info: VS_FIXEDFILEINFO,
|
||||
padding2: []const u8,
|
||||
children: []const u8,
|
||||
};
|
||||
|
||||
// Helper methods to safely access headers
|
||||
fn getDosHeader(self: *const PEFile) *DOSHeader {
|
||||
return @ptrCast(@alignCast(self.data.items.ptr + self.dos_header_offset));
|
||||
@@ -276,6 +371,9 @@ pub const PEFile = struct {
|
||||
const updated_optional_header = self.getOptionalHeader();
|
||||
updated_optional_header.size_of_image = alignSize(new_section.virtual_address + new_section.virtual_size, updated_optional_header.section_alignment);
|
||||
updated_optional_header.size_of_initialized_data += new_section.size_of_raw_data;
|
||||
|
||||
// Update PE checksum - critical for Windows to accept the executable
|
||||
self.updateChecksum();
|
||||
}
|
||||
|
||||
/// Find the .bun section and return its data
|
||||
@@ -330,6 +428,59 @@ pub const PEFile = struct {
|
||||
return error.BunSectionNotFound;
|
||||
}
|
||||
|
||||
/// Calculate PE checksum using the standard Windows algorithm
|
||||
pub fn calculateChecksum(self: *const PEFile) u32 {
|
||||
const data = self.data.items;
|
||||
const file_size = data.len;
|
||||
|
||||
// Find checksum field offset
|
||||
const checksum_offset = self.optional_header_offset + @offsetOf(OptionalHeader64, "checksum");
|
||||
|
||||
var checksum: u64 = 0;
|
||||
var i: usize = 0;
|
||||
|
||||
// Process file as 16-bit words
|
||||
while (i + 1 < file_size) : (i += 2) {
|
||||
// Skip the checksum field itself (4 bytes)
|
||||
if (i >= checksum_offset and i < checksum_offset + 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add 16-bit word to checksum
|
||||
const word = std.mem.readInt(u16, data[i..][0..2], .little);
|
||||
checksum += word;
|
||||
|
||||
// Handle overflow - fold back the carry
|
||||
if (checksum > 0xFFFF) {
|
||||
checksum = (checksum & 0xFFFF) + (checksum >> 16);
|
||||
}
|
||||
}
|
||||
|
||||
// If file size is odd, last byte is treated as if followed by 0x00
|
||||
if (file_size & 1 != 0) {
|
||||
checksum += data[file_size - 1];
|
||||
if (checksum > 0xFFFF) {
|
||||
checksum = (checksum & 0xFFFF) + (checksum >> 16);
|
||||
}
|
||||
}
|
||||
|
||||
// Final fold
|
||||
checksum = (checksum & 0xFFFF) + (checksum >> 16);
|
||||
checksum = (checksum + (checksum >> 16)) & 0xFFFF;
|
||||
|
||||
// Add file size to checksum
|
||||
checksum += file_size;
|
||||
|
||||
return @intCast(checksum);
|
||||
}
|
||||
|
||||
/// Update the PE checksum field
|
||||
pub fn updateChecksum(self: *PEFile) void {
|
||||
const checksum = self.calculateChecksum();
|
||||
const optional_header = self.getOptionalHeader();
|
||||
optional_header.checksum = checksum;
|
||||
}
|
||||
|
||||
/// Write the modified PE file
|
||||
pub fn write(self: *const PEFile, writer: anytype) !void {
|
||||
try writer.writeAll(self.data.items);
|
||||
@@ -363,6 +514,696 @@ pub const PEFile = struct {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resource editing functionality
|
||||
fn getResourceSection(self: *const PEFile) ?*SectionHeader {
|
||||
const section_headers = self.getSectionHeaders();
|
||||
for (section_headers) |*section| {
|
||||
if (strings.eqlComptime(section.name[0..6], ".rsrc\x00")) {
|
||||
return section;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn getResourceDirectory(self: *const PEFile) !?*ResourceDirectoryTable {
|
||||
const rsrc_section = self.getResourceSection() orelse return null;
|
||||
|
||||
if (rsrc_section.pointer_to_raw_data >= self.data.items.len or
|
||||
rsrc_section.pointer_to_raw_data + rsrc_section.size_of_raw_data > self.data.items.len)
|
||||
{
|
||||
return error.InvalidResourceSection;
|
||||
}
|
||||
|
||||
return @ptrCast(@alignCast(self.data.items.ptr + rsrc_section.pointer_to_raw_data));
|
||||
}
|
||||
|
||||
fn findResourceEntry(self: *const PEFile, dir_offset: u32, resource_type: u32, resource_id: u32, language_id: u16) !?*ResourceDataEntry {
|
||||
const rsrc_section = self.getResourceSection() orelse return null;
|
||||
const rsrc_base = rsrc_section.pointer_to_raw_data;
|
||||
|
||||
// Level 1: Type
|
||||
const type_dir: *ResourceDirectoryTable = @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + dir_offset));
|
||||
const type_entries = @as([*]ResourceDirectoryEntry, @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + dir_offset + @sizeOf(ResourceDirectoryTable))));
|
||||
|
||||
const total_entries = type_dir.number_of_name_entries + type_dir.number_of_id_entries;
|
||||
var type_entry: ?*ResourceDirectoryEntry = null;
|
||||
|
||||
for (0..total_entries) |i| {
|
||||
if ((type_entries[i].name_or_id & 0x7FFFFFFF) == resource_type) {
|
||||
type_entry = &type_entries[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (type_entry == null) return null;
|
||||
if ((type_entry.?.offset_to_data & 0x80000000) == 0) return null; // Must be directory
|
||||
|
||||
// Level 2: Name/ID
|
||||
const name_dir_offset = type_entry.?.offset_to_data & 0x7FFFFFFF;
|
||||
const name_dir: *ResourceDirectoryTable = @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + name_dir_offset));
|
||||
const name_entries = @as([*]ResourceDirectoryEntry, @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + name_dir_offset + @sizeOf(ResourceDirectoryTable))));
|
||||
|
||||
var name_entry: ?*ResourceDirectoryEntry = null;
|
||||
for (0..name_dir.number_of_name_entries + name_dir.number_of_id_entries) |i| {
|
||||
if ((name_entries[i].name_or_id & 0x7FFFFFFF) == resource_id) {
|
||||
name_entry = &name_entries[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (name_entry == null) return null;
|
||||
if ((name_entry.?.offset_to_data & 0x80000000) == 0) return null; // Must be directory
|
||||
|
||||
// Level 3: Language
|
||||
const lang_dir_offset = name_entry.?.offset_to_data & 0x7FFFFFFF;
|
||||
const lang_dir: *ResourceDirectoryTable = @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + lang_dir_offset));
|
||||
const lang_entries = @as([*]ResourceDirectoryEntry, @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + lang_dir_offset + @sizeOf(ResourceDirectoryTable))));
|
||||
|
||||
for (0..lang_dir.number_of_named_entries + lang_dir.number_of_id_entries) |i| {
|
||||
if ((lang_entries[i].name_or_id & 0x7FFFFFFF) == language_id) {
|
||||
if ((lang_entries[i].offset_to_data & 0x80000000) == 0) {
|
||||
// This is a data entry
|
||||
return @ptrCast(@alignCast(self.data.items.ptr + rsrc_base + lang_entries[i].offset_to_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn applyWindowsSettings(self: *PEFile, settings: *const bun.options.WindowsSettings, allocator: Allocator) !void {
|
||||
// Handle hide console first (simple modification)
|
||||
if (settings.hide_console) {
|
||||
const optional_header = self.getOptionalHeader();
|
||||
// Change subsystem from IMAGE_SUBSYSTEM_WINDOWS_CUI (3) to IMAGE_SUBSYSTEM_WINDOWS_GUI (2)
|
||||
if (optional_header.subsystem == 3) {
|
||||
optional_header.subsystem = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// If no resource modifications needed, return early
|
||||
if (settings.icon == null and settings.version == null and settings.description == null and
|
||||
settings.publisher == null and settings.title == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Find or create resource section
|
||||
var rsrc_section = self.getResourceSection();
|
||||
if (rsrc_section == null) {
|
||||
try self.createResourceSection();
|
||||
rsrc_section = self.getResourceSection() orelse return error.FailedToCreateResourceSection;
|
||||
}
|
||||
|
||||
// Build new resource directory
|
||||
var resource_builder = ResourceBuilder.init(allocator);
|
||||
defer resource_builder.deinit();
|
||||
|
||||
// Load and process icon if provided
|
||||
if (settings.icon) |icon_path| {
|
||||
// Simple approach - just read the file
|
||||
const icon_data = std.fs.cwd().readFileAlloc(allocator, icon_path, std.math.maxInt(usize)) catch {
|
||||
return error.FileNotFound;
|
||||
};
|
||||
defer allocator.free(icon_data);
|
||||
|
||||
try resource_builder.setIcon(icon_data);
|
||||
}
|
||||
|
||||
// Build version info if any version fields provided
|
||||
if (settings.version != null or settings.description != null or
|
||||
settings.publisher != null or settings.title != null)
|
||||
{
|
||||
const version_str = if (settings.version) |v| v else "1.0.0.0";
|
||||
try resource_builder.setVersionInfo(
|
||||
version_str,
|
||||
settings.description,
|
||||
settings.publisher,
|
||||
settings.title,
|
||||
);
|
||||
}
|
||||
|
||||
// Build the resource data
|
||||
const resource_data = try resource_builder.build(rsrc_section.?.virtual_address);
|
||||
defer allocator.free(resource_data);
|
||||
|
||||
// Update the resource section
|
||||
try self.updateResourceSection(rsrc_section.?, resource_data);
|
||||
|
||||
// Update PE checksum after all modifications
|
||||
self.updateChecksum();
|
||||
}
|
||||
|
||||
fn createResourceSection(self: *PEFile) !void {
|
||||
const section_name = ".rsrc\x00\x00\x00";
|
||||
const optional_header = self.getOptionalHeader();
|
||||
|
||||
// Check if we can add another section
|
||||
if (self.num_sections >= 95) { // PE limit is 96 sections
|
||||
return error.TooManySections;
|
||||
}
|
||||
|
||||
// Find the last section to determine where to place the new one
|
||||
var last_section_end: u32 = 0;
|
||||
var last_virtual_end: u32 = 0;
|
||||
|
||||
const section_headers = self.getSectionHeaders();
|
||||
for (section_headers) |section| {
|
||||
const section_file_end = section.pointer_to_raw_data + section.size_of_raw_data;
|
||||
const section_virtual_end = section.virtual_address + alignSize(section.virtual_size, optional_header.section_alignment);
|
||||
|
||||
if (section_file_end > last_section_end) {
|
||||
last_section_end = section_file_end;
|
||||
}
|
||||
if (section_virtual_end > last_virtual_end) {
|
||||
last_virtual_end = section_virtual_end;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new section header
|
||||
const new_section = SectionHeader{
|
||||
.name = section_name.*,
|
||||
.virtual_size = 0x1000, // Start with 4KB
|
||||
.virtual_address = alignSize(last_virtual_end, optional_header.section_alignment),
|
||||
.size_of_raw_data = alignSize(0x1000, optional_header.file_alignment),
|
||||
.pointer_to_raw_data = alignSize(last_section_end, optional_header.file_alignment),
|
||||
.pointer_to_relocations = 0,
|
||||
.pointer_to_line_numbers = 0,
|
||||
.number_of_relocations = 0,
|
||||
.number_of_line_numbers = 0,
|
||||
.characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ,
|
||||
};
|
||||
|
||||
// Resize data to accommodate new section
|
||||
const new_data_size = new_section.pointer_to_raw_data + new_section.size_of_raw_data;
|
||||
try self.data.resize(new_data_size);
|
||||
|
||||
// Zero out the new section data
|
||||
@memset(self.data.items[last_section_end..new_data_size], 0);
|
||||
|
||||
// Write the section header
|
||||
const new_section_offset = self.section_headers_offset + @sizeOf(SectionHeader) * self.num_sections;
|
||||
const new_section_ptr: *SectionHeader = @ptrCast(@alignCast(self.data.items.ptr + new_section_offset));
|
||||
new_section_ptr.* = new_section;
|
||||
|
||||
// Update PE header
|
||||
const pe_header = self.getPEHeader();
|
||||
pe_header.number_of_sections += 1;
|
||||
self.num_sections += 1;
|
||||
|
||||
// Update optional header
|
||||
const updated_optional_header = self.getOptionalHeader();
|
||||
updated_optional_header.size_of_image = alignSize(new_section.virtual_address + new_section.virtual_size, updated_optional_header.section_alignment);
|
||||
|
||||
// Update resource directory RVA
|
||||
updated_optional_header.data_directories[2].virtual_address = new_section.virtual_address;
|
||||
updated_optional_header.data_directories[2].size = new_section.virtual_size;
|
||||
}
|
||||
|
||||
fn updateResourceSection(self: *PEFile, section: *SectionHeader, data: []const u8) !void {
|
||||
const optional_header = self.getOptionalHeader();
|
||||
|
||||
// Calculate aligned size
|
||||
const aligned_size = alignSize(@intCast(data.len), optional_header.file_alignment);
|
||||
|
||||
// Check if we need to resize the section
|
||||
if (aligned_size > section.size_of_raw_data) {
|
||||
// This is complex - would need to move all following sections
|
||||
// For now, just error if the resource data is too large
|
||||
return error.ResourceDataTooLarge;
|
||||
}
|
||||
|
||||
// Update section data
|
||||
const section_offset = section.pointer_to_raw_data;
|
||||
@memcpy(self.data.items[section_offset..][0..data.len], data);
|
||||
|
||||
// Zero out remaining space
|
||||
if (data.len < section.size_of_raw_data) {
|
||||
@memset(self.data.items[section_offset + data.len .. section_offset + section.size_of_raw_data], 0);
|
||||
}
|
||||
|
||||
// Update section header
|
||||
section.virtual_size = @intCast(data.len);
|
||||
|
||||
// Update data directory
|
||||
optional_header.data_directories[2].size = @intCast(data.len);
|
||||
}
|
||||
};
|
||||
|
||||
// Resource builder for creating Windows PE resources
|
||||
const ResourceBuilder = struct {
|
||||
allocator: Allocator,
|
||||
root: ResourceTable,
|
||||
|
||||
const ResourceTable = struct {
|
||||
entries: std.ArrayList(ResourceTableEntry),
|
||||
};
|
||||
|
||||
const ResourceTableEntry = struct {
|
||||
id: u32,
|
||||
subtable: ?*ResourceTable = null,
|
||||
data: ?[]const u8 = null,
|
||||
data_offset: ?u32 = null,
|
||||
data_size: ?u32 = null,
|
||||
code_page: u32 = PEFile.CODE_PAGE_ID_EN_US,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator) ResourceBuilder {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.root = .{
|
||||
.entries = std.ArrayList(ResourceTableEntry).init(allocator),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ResourceBuilder) void {
|
||||
// Recursively free all entries
|
||||
self.freeTable(&self.root);
|
||||
}
|
||||
|
||||
fn freeTable(self: *ResourceBuilder, table: *ResourceTable) void {
|
||||
for (table.entries.items) |*entry| {
|
||||
if (entry.subtable) |subtable| {
|
||||
self.freeTable(subtable);
|
||||
self.allocator.destroy(subtable);
|
||||
}
|
||||
if (entry.data) |data| {
|
||||
self.allocator.free(data);
|
||||
}
|
||||
}
|
||||
table.entries.deinit();
|
||||
}
|
||||
|
||||
pub fn setIcon(self: *ResourceBuilder, icon_data: []const u8) !void {
|
||||
// Parse ICO file header
|
||||
if (icon_data.len < @sizeOf(PEFile.IconDirectory)) {
|
||||
return error.InvalidIconFile;
|
||||
}
|
||||
|
||||
const icon_dir = std.mem.bytesAsValue(PEFile.IconDirectory, icon_data[0..@sizeOf(PEFile.IconDirectory)]).*;
|
||||
if (icon_dir.reserved != 0 or icon_dir.type != 1) {
|
||||
return error.InvalidIconFormat;
|
||||
}
|
||||
|
||||
// Get or create RT_ICON table
|
||||
const icon_table = try self.getOrCreateTable(&self.root, PEFile.RT_ICON);
|
||||
|
||||
// Find first free icon ID
|
||||
var first_free_icon_id: u32 = 1;
|
||||
for (icon_table.entries.items) |entry| {
|
||||
if (entry.id >= first_free_icon_id) {
|
||||
first_free_icon_id = entry.id + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Read icon entries
|
||||
var offset: usize = @sizeOf(PEFile.IconDirectory);
|
||||
var group_icon_data = std.ArrayList(u8).init(self.allocator);
|
||||
defer group_icon_data.deinit();
|
||||
|
||||
// Write GRPICONDIR header
|
||||
try group_icon_data.appendSlice(std.mem.asBytes(&icon_dir));
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < icon_dir.count) : (i += 1) {
|
||||
if (offset + @sizeOf(PEFile.IconDirectoryEntry) > icon_data.len) {
|
||||
return error.InvalidIconFile;
|
||||
}
|
||||
|
||||
const entry = std.mem.bytesAsValue(PEFile.IconDirectoryEntry, icon_data[offset..][0..@sizeOf(PEFile.IconDirectoryEntry)]).*;
|
||||
offset += @sizeOf(PEFile.IconDirectoryEntry);
|
||||
|
||||
// Read the actual icon image data
|
||||
if (entry.image_offset + entry.bytes_in_res > icon_data.len) {
|
||||
return error.InvalidIconFile;
|
||||
}
|
||||
|
||||
const image_data = icon_data[entry.image_offset..][0..entry.bytes_in_res];
|
||||
const icon_id = first_free_icon_id + @as(u32, @intCast(i));
|
||||
|
||||
// Add individual icon to RT_ICON table
|
||||
const id_table = try self.getOrCreateTable(icon_table, icon_id);
|
||||
|
||||
// At the language level, add data directly instead of creating another table
|
||||
const data_copy = try self.allocator.dupe(u8, image_data);
|
||||
try id_table.entries.append(.{
|
||||
.id = PEFile.LANGUAGE_ID_EN_US,
|
||||
.data = data_copy,
|
||||
.data_size = @intCast(data_copy.len),
|
||||
.code_page = PEFile.CODE_PAGE_ID_EN_US,
|
||||
});
|
||||
|
||||
// Create GRPICONDIRENTRY for group icon
|
||||
const grp_entry = PEFile.GroupIconDirectoryEntry{
|
||||
.width = entry.width,
|
||||
.height = entry.height,
|
||||
.color_count = entry.color_count,
|
||||
.reserved = entry.reserved,
|
||||
.planes = entry.planes,
|
||||
.bit_count = entry.bit_count,
|
||||
.bytes_in_res = entry.bytes_in_res,
|
||||
.id = @intCast(icon_id),
|
||||
};
|
||||
try group_icon_data.appendSlice(std.mem.asBytes(&grp_entry));
|
||||
}
|
||||
|
||||
// Get or create RT_GROUP_ICON table
|
||||
const group_table = try self.getOrCreateTable(&self.root, PEFile.RT_GROUP_ICON);
|
||||
const name_table = try self.getOrCreateTable(group_table, 1); // MAINICON ID
|
||||
|
||||
// At the language level, add data directly instead of creating another table
|
||||
const group_data_copy = try group_icon_data.toOwnedSlice();
|
||||
try name_table.entries.append(.{
|
||||
.id = PEFile.LANGUAGE_ID_EN_US,
|
||||
.data = group_data_copy,
|
||||
.data_size = @intCast(group_data_copy.len),
|
||||
.code_page = PEFile.CODE_PAGE_ID_EN_US,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to write a string as UTF-16LE with null terminator
|
||||
fn writeUtf16String(data: *std.ArrayList(u8), str: []const u8) !void {
|
||||
// Calculate the length first
|
||||
const utf16_len = strings.elementLengthUTF8IntoUTF16([]const u8, str);
|
||||
|
||||
// Ensure we have capacity for the UTF-16 data plus null terminator
|
||||
const start_len = data.items.len;
|
||||
try data.ensureUnusedCapacity((utf16_len + 1) * 2);
|
||||
|
||||
// Resize to make room for UTF-16 data
|
||||
data.items.len = start_len + (utf16_len * 2);
|
||||
|
||||
// Convert UTF-8 to UTF-16LE in place
|
||||
// We need to use a temporary buffer due to alignment requirements
|
||||
var utf16_buf: [1024]u16 = undefined;
|
||||
const utf16_result = strings.convertUTF8toUTF16InBuffer(utf16_buf[0..utf16_len], str);
|
||||
|
||||
// Copy UTF-16 bytes to the output
|
||||
const utf16_bytes = std.mem.sliceAsBytes(utf16_result);
|
||||
@memcpy(data.items[start_len..][0..utf16_bytes.len], utf16_bytes);
|
||||
|
||||
// Add null terminator
|
||||
try data.append(0);
|
||||
try data.append(0);
|
||||
}
|
||||
|
||||
// Helper to align to 32-bit boundary
|
||||
fn alignTo32Bit(data: *std.ArrayList(u8)) !void {
|
||||
while (data.items.len % 4 != 0) {
|
||||
try data.append(0);
|
||||
}
|
||||
}
|
||||
|
||||
const VersionHeader = extern struct {
|
||||
wLength: u16,
|
||||
wValueLength: u16,
|
||||
wType: u16,
|
||||
};
|
||||
|
||||
pub fn setVersionInfo(self: *ResourceBuilder, version: []const u8, description: ?[]const u8, company: ?[]const u8, product: ?[]const u8) !void {
|
||||
// Parse version string
|
||||
var version_parts: [4]u16 = .{ 1, 0, 0, 0 };
|
||||
var iter = std.mem.tokenizeScalar(u8, version, '.');
|
||||
var i: usize = 0;
|
||||
while (iter.next()) |part| : (i += 1) {
|
||||
if (i >= 4) break;
|
||||
version_parts[i] = std.fmt.parseInt(u16, part, 10) catch 0;
|
||||
}
|
||||
|
||||
const file_version_ms = (@as(u32, version_parts[0]) << 16) | version_parts[1];
|
||||
const file_version_ls = (@as(u32, version_parts[2]) << 16) | version_parts[3];
|
||||
|
||||
// Build VS_VERSIONINFO structure
|
||||
var data = std.ArrayList(u8).init(self.allocator);
|
||||
defer data.deinit();
|
||||
|
||||
// VS_VERSIONINFO root structure
|
||||
const vs_version_info_start = data.items.len;
|
||||
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = @sizeOf(PEFile.VS_FIXEDFILEINFO), .wType = 0 }));
|
||||
try writeUtf16String(&data, "VS_VERSION_INFO");
|
||||
try alignTo32Bit(&data);
|
||||
|
||||
// VS_FIXEDFILEINFO
|
||||
const fixed_info = PEFile.VS_FIXEDFILEINFO{
|
||||
.signature = PEFile.VS_FFI_SIGNATURE,
|
||||
.struct_version = PEFile.VS_FFI_STRUCVERSION,
|
||||
.file_version_ms = file_version_ms,
|
||||
.file_version_ls = file_version_ls,
|
||||
.product_version_ms = file_version_ms,
|
||||
.product_version_ls = file_version_ls,
|
||||
.file_flags_mask = PEFile.VS_FFI_FILEFLAGSMASK,
|
||||
.file_flags = 0,
|
||||
.file_os = PEFile.VOS_NT_WINDOWS32,
|
||||
.file_type = PEFile.VFT_APP,
|
||||
.file_subtype = 0,
|
||||
.file_date_ms = 0,
|
||||
.file_date_ls = 0,
|
||||
};
|
||||
try data.appendSlice(std.mem.asBytes(&fixed_info));
|
||||
try alignTo32Bit(&data);
|
||||
|
||||
// StringFileInfo
|
||||
const string_file_info_start = data.items.len;
|
||||
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = 0, .wType = 1 }));
|
||||
try writeUtf16String(&data, "StringFileInfo");
|
||||
try alignTo32Bit(&data);
|
||||
|
||||
// StringTable for 040904B0 (US English, Unicode)
|
||||
const string_table_start = data.items.len;
|
||||
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = 0, .wType = 1 }));
|
||||
try writeUtf16String(&data, "040904B0");
|
||||
try alignTo32Bit(&data);
|
||||
|
||||
// Add string entries
|
||||
const version_strings = [_]struct { key: []const u8, value: ?[]const u8 }{
|
||||
.{ .key = "CompanyName", .value = company },
|
||||
.{ .key = "FileDescription", .value = description },
|
||||
.{ .key = "FileVersion", .value = version },
|
||||
.{ .key = "ProductName", .value = product },
|
||||
.{ .key = "ProductVersion", .value = version },
|
||||
};
|
||||
|
||||
for (version_strings) |str| {
|
||||
if (str.value) |value| {
|
||||
const string_start = data.items.len;
|
||||
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = 0, .wType = 1 }));
|
||||
try writeUtf16String(&data, str.key);
|
||||
try alignTo32Bit(&data);
|
||||
|
||||
// Write value and update header
|
||||
const value_start = data.items.len;
|
||||
try writeUtf16String(&data, value);
|
||||
const value_len = @divExact(data.items.len - value_start, 2); // Length in WORDs, including null
|
||||
|
||||
// Update string header
|
||||
const string_len = data.items.len - string_start;
|
||||
if (string_len > std.math.maxInt(u16)) return error.StringTooLong;
|
||||
if (value_len > std.math.maxInt(u16)) return error.ValueTooLong;
|
||||
std.mem.writeInt(u16, data.items[string_start..][0..2], @intCast(string_len), .little);
|
||||
std.mem.writeInt(u16, data.items[string_start + 2 ..][0..2], @intCast(value_len), .little);
|
||||
|
||||
try alignTo32Bit(&data);
|
||||
}
|
||||
}
|
||||
|
||||
// Update StringTable header
|
||||
const string_table_len = data.items.len - string_table_start;
|
||||
if (string_table_len > std.math.maxInt(u16)) return error.StringTableTooLong;
|
||||
std.mem.writeInt(u16, data.items[string_table_start..][0..2], @intCast(string_table_len), .little);
|
||||
|
||||
// Update StringFileInfo header
|
||||
const string_file_info_len = data.items.len - string_file_info_start;
|
||||
if (string_file_info_len > std.math.maxInt(u16)) return error.StringFileInfoTooLong;
|
||||
std.mem.writeInt(u16, data.items[string_file_info_start..][0..2], @intCast(string_file_info_len), .little);
|
||||
|
||||
// VarFileInfo
|
||||
const var_file_info_start = data.items.len;
|
||||
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = 0, .wType = 1 }));
|
||||
try writeUtf16String(&data, "VarFileInfo");
|
||||
try alignTo32Bit(&data);
|
||||
|
||||
// Translation
|
||||
const translation_start = data.items.len;
|
||||
try data.appendSlice(std.mem.asBytes(&VersionHeader{ .wLength = 0, .wValueLength = 4, .wType = 0 }));
|
||||
try writeUtf16String(&data, "Translation");
|
||||
try alignTo32Bit(&data);
|
||||
|
||||
// Language and code page
|
||||
try data.appendSlice(&[_]u8{ 0x09, 0x04, 0xB0, 0x04 }); // 0x0409, 0x04B0
|
||||
|
||||
// Update Translation header
|
||||
const translation_len = data.items.len - translation_start;
|
||||
if (translation_len > std.math.maxInt(u16)) return error.TranslationTooLong;
|
||||
std.mem.writeInt(u16, data.items[translation_start..][0..2], @intCast(translation_len), .little);
|
||||
|
||||
// Update VarFileInfo header
|
||||
const var_file_info_len = data.items.len - var_file_info_start;
|
||||
if (var_file_info_len > std.math.maxInt(u16)) return error.VarFileInfoTooLong;
|
||||
std.mem.writeInt(u16, data.items[var_file_info_start..][0..2], @intCast(var_file_info_len), .little);
|
||||
|
||||
// Update VS_VERSIONINFO header
|
||||
const vs_version_info_len = data.items.len - vs_version_info_start;
|
||||
if (vs_version_info_len > std.math.maxInt(u16)) return error.VersionInfoTooLong;
|
||||
std.mem.writeInt(u16, data.items[vs_version_info_start..][0..2], @intCast(vs_version_info_len), .little);
|
||||
|
||||
// Add to resource table
|
||||
const version_table = try self.getOrCreateTable(&self.root, PEFile.RT_VERSION);
|
||||
const id_table = try self.getOrCreateTable(version_table, 1);
|
||||
|
||||
const version_bytes = try data.toOwnedSlice();
|
||||
try id_table.entries.append(.{
|
||||
.id = PEFile.LANGUAGE_ID_EN_US,
|
||||
.data = version_bytes,
|
||||
.data_size = @intCast(version_bytes.len),
|
||||
.code_page = PEFile.CODE_PAGE_ID_EN_US,
|
||||
});
|
||||
}
|
||||
|
||||
fn getOrCreateTable(self: *ResourceBuilder, parent: *ResourceTable, id: u32) !*ResourceTable {
|
||||
// Look for existing entry
|
||||
for (parent.entries.items) |*entry| {
|
||||
if (entry.id == id) {
|
||||
if (entry.subtable) |subtable| {
|
||||
return subtable;
|
||||
}
|
||||
return error.ExpectedDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new subtable
|
||||
const new_table = try self.allocator.create(ResourceTable);
|
||||
new_table.* = .{
|
||||
.entries = std.ArrayList(ResourceTableEntry).init(self.allocator),
|
||||
};
|
||||
|
||||
try parent.entries.append(.{
|
||||
.id = id,
|
||||
.subtable = new_table,
|
||||
});
|
||||
|
||||
return new_table;
|
||||
}
|
||||
|
||||
pub fn build(self: *ResourceBuilder, virtual_address: u32) ![]u8 {
|
||||
var tables = std.ArrayList(u8).init(self.allocator);
|
||||
defer tables.deinit();
|
||||
var data_entries = std.ArrayList(u8).init(self.allocator);
|
||||
defer data_entries.deinit();
|
||||
var data_bytes = std.ArrayList(u8).init(self.allocator);
|
||||
defer data_bytes.deinit();
|
||||
|
||||
// Calculate total sizes first
|
||||
var total_table_size: u32 = 0;
|
||||
var total_data_entries: u32 = 0;
|
||||
self.calculateTableSizes(&self.root, &total_table_size, &total_data_entries);
|
||||
|
||||
// Now build with known offsets
|
||||
var tables_offset: u32 = 0;
|
||||
var data_entries_offset = total_table_size;
|
||||
var data_offset = total_table_size + (total_data_entries * @sizeOf(PEFile.ResourceDataEntry));
|
||||
|
||||
try self.writeTableRecursive(&tables, &data_entries, &data_bytes, virtual_address, &self.root, &tables_offset, &data_entries_offset, &data_offset);
|
||||
|
||||
// Combine all parts
|
||||
var output = std.ArrayList(u8).init(self.allocator);
|
||||
try output.appendSlice(tables.items);
|
||||
try output.appendSlice(data_entries.items);
|
||||
try output.appendSlice(data_bytes.items);
|
||||
|
||||
return output.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn calculateTableSizes(self: *const ResourceBuilder, table: *const ResourceTable, table_size: *u32, data_entries: *u32) void {
|
||||
const entry_count = table.entries.items.len;
|
||||
const size_increase = @sizeOf(PEFile.ResourceDirectoryTable) + entry_count * @sizeOf(PEFile.ResourceDirectoryEntry);
|
||||
table_size.* += @as(u32, @intCast(size_increase));
|
||||
|
||||
for (table.entries.items) |*entry| {
|
||||
if (entry.subtable) |subtable| {
|
||||
self.calculateTableSizes(subtable, table_size, data_entries);
|
||||
} else if (entry.data != null) {
|
||||
data_entries.* += 1; // Count the number of data entries, not their size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn writeTableRecursive(
|
||||
self: *ResourceBuilder,
|
||||
tables: *std.ArrayList(u8),
|
||||
data_entries: *std.ArrayList(u8),
|
||||
data_bytes: *std.ArrayList(u8),
|
||||
virtual_address: u32,
|
||||
table: *const ResourceTable,
|
||||
tables_offset: *u32,
|
||||
data_entries_offset: *u32,
|
||||
data_offset: *u32,
|
||||
) !void {
|
||||
_ = tables.items.len; // dir_start - may be used for debugging
|
||||
|
||||
// Write directory header
|
||||
const dir_header = PEFile.ResourceDirectoryTable{
|
||||
.characteristics = 0,
|
||||
.time_date_stamp = 0,
|
||||
.major_version = 0,
|
||||
.minor_version = 0,
|
||||
.number_of_name_entries = 0,
|
||||
.number_of_id_entries = @intCast(table.entries.items.len),
|
||||
};
|
||||
try tables.appendSlice(std.mem.asBytes(&dir_header));
|
||||
|
||||
// Calculate where subdirectories will be placed
|
||||
var subdirs = std.ArrayList(struct { entry: *const ResourceTableEntry, offset: u32 }).init(self.allocator);
|
||||
defer subdirs.deinit();
|
||||
|
||||
var next_table_offset = tables_offset.* + @as(u32, @intCast(tables.items.len + table.entries.items.len * @sizeOf(PEFile.ResourceDirectoryEntry)));
|
||||
|
||||
// Write directory entries
|
||||
for (table.entries.items) |*entry| {
|
||||
if (entry.subtable) |subtable| {
|
||||
// Calculate subdirectory size
|
||||
var subdir_size: u32 = 0;
|
||||
var subdir_data_entries: u32 = 0;
|
||||
self.calculateTableSizes(subtable, &subdir_size, &subdir_data_entries);
|
||||
|
||||
const dir_entry = PEFile.ResourceDirectoryEntry{
|
||||
.name_or_id = entry.id,
|
||||
.offset_to_data = 0x80000000 | (next_table_offset - tables_offset.*),
|
||||
};
|
||||
try tables.appendSlice(std.mem.asBytes(&dir_entry));
|
||||
|
||||
try subdirs.append(.{ .entry = entry, .offset = next_table_offset });
|
||||
next_table_offset += subdir_size;
|
||||
} else if (entry.data) |_| {
|
||||
// Calculate offset to the ResourceDataEntry from start of resource section
|
||||
const data_entry_offset = data_entries_offset.* + @as(u32, @intCast(data_entries.items.len));
|
||||
const dir_entry = PEFile.ResourceDirectoryEntry{
|
||||
.name_or_id = entry.id,
|
||||
.offset_to_data = data_entry_offset, // No high bit for data entry (points to ResourceDataEntry)
|
||||
};
|
||||
try tables.appendSlice(std.mem.asBytes(&dir_entry));
|
||||
|
||||
// Write the data entry
|
||||
const data_byte_offset = data_offset.* + @as(u32, @intCast(data_bytes.items.len));
|
||||
const res_data_entry = PEFile.ResourceDataEntry{
|
||||
.offset_to_data = virtual_address + data_byte_offset,
|
||||
.size = entry.data_size.?,
|
||||
.code_page = entry.code_page,
|
||||
.reserved = 0,
|
||||
};
|
||||
try data_entries.appendSlice(std.mem.asBytes(&res_data_entry));
|
||||
try data_bytes.appendSlice(entry.data.?);
|
||||
}
|
||||
}
|
||||
|
||||
tables_offset.* = next_table_offset;
|
||||
|
||||
// Write subdirectories
|
||||
for (subdirs.items) |subdir| {
|
||||
try self.writeTableRecursive(tables, data_entries, data_bytes, virtual_address, subdir.entry.subtable.?, tables_offset, data_entries_offset, data_offset);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Align size to the nearest multiple of alignment
|
||||
|
||||
20
src/sys.zig
20
src/sys.zig
@@ -2331,9 +2331,25 @@ pub fn pidfd_open(pid: std.os.linux.pid_t, flags: u32) Maybe(i32) {
|
||||
unreachable;
|
||||
}
|
||||
|
||||
pub fn lseek(fd: bun.FileDescriptor, offset: i64, whence: usize) Maybe(usize) {
|
||||
pub fn lseek(fd: bun.FileDescriptor, offset: i64, whence: c_int) Maybe(usize) {
|
||||
if (comptime Environment.isWindows) {
|
||||
var new_ptr: std.os.windows.LARGE_INTEGER = undefined;
|
||||
const rc = kernel32.SetFilePointerEx(fd.cast(), @as(windows.LARGE_INTEGER, @bitCast(offset)), &new_ptr, @as(u32, @intCast(whence)));
|
||||
if (rc == windows.FALSE) {
|
||||
return Maybe(usize).errnoSysFd(0, .lseek, fd) orelse Maybe(usize){ .result = 0 };
|
||||
}
|
||||
return Maybe(usize){ .result = @as(usize, @intCast(new_ptr)) };
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const rc = syscall.lseek(fd.cast(), offset, whence);
|
||||
const rc = switch (Environment.os) {
|
||||
.linux => syscall.lseek(fd.cast(), offset, @as(usize, @intCast(whence))),
|
||||
.mac => blk: {
|
||||
const result = syscall.lseek(fd.cast(), offset, whence);
|
||||
break :blk @as(usize, @intCast(result));
|
||||
},
|
||||
else => @compileError("not implemented"),
|
||||
};
|
||||
if (Maybe(usize).errnoSysFd(rc, .lseek, fd)) |err| {
|
||||
if (err.getErrno() == .INTR) continue;
|
||||
return err;
|
||||
|
||||
@@ -3642,19 +3642,6 @@ pub fn editWin32BinarySubsystem(fd: bun.sys.File, subsystem: Subsystem) !void {
|
||||
try fd.writer().writeInt(u16, @intFromEnum(subsystem), .little);
|
||||
}
|
||||
|
||||
pub const rescle = struct {
|
||||
extern fn rescle__setIcon([*:0]const u16, [*:0]const u16) c_int;
|
||||
|
||||
pub fn setIcon(exe_path: [*:0]const u16, icon: [*:0]const u16) !void {
|
||||
comptime bun.assert(bun.Environment.isWindows);
|
||||
const status = rescle__setIcon(exe_path, icon);
|
||||
return switch (status) {
|
||||
0 => {},
|
||||
else => error.IconEditError,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub extern "kernel32" fn CloseHandle(hObject: HANDLE) callconv(.winapi) BOOL;
|
||||
pub extern "kernel32" fn GetFinalPathNameByHandleW(hFile: HANDLE, lpszFilePath: [*]u16, cchFilePath: DWORD, dwFlags: DWORD) callconv(.winapi) DWORD;
|
||||
pub extern "kernel32" fn DeleteFileW(lpFileName: [*:0]const u16) callconv(.winapi) BOOL;
|
||||
|
||||
538
test/bundler/windows-resources-compile.test.ts
Normal file
538
test/bundler/windows-resources-compile.test.ts
Normal file
@@ -0,0 +1,538 @@
|
||||
import { spawn } from "bun";
|
||||
import { windowsResourceInternals } from "bun:internal-for-testing";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("Windows Resource Editing", () => {
|
||||
describe("unit tests", () => {
|
||||
test("parseIconFile parses valid ICO", () => {
|
||||
const icon = createTestIcon();
|
||||
const result = windowsResourceInternals.parseIconFile(icon);
|
||||
|
||||
expect(result.groupIconData).toBeInstanceOf(Uint8Array);
|
||||
expect(result.icons).toBeArrayOfSize(1);
|
||||
expect(result.icons[0].id).toBe(1);
|
||||
expect(result.icons[0].data).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
test("parseIconFile rejects invalid data", () => {
|
||||
expect(() => windowsResourceInternals.parseIconFile(Buffer.from("not an icon"))).toThrow();
|
||||
expect(() => windowsResourceInternals.parseIconFile(Buffer.from([0, 0]))).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// Test icon data (minimal valid ICO file)
|
||||
const createTestIcon = () => {
|
||||
// ICO header (6 bytes)
|
||||
const header = Buffer.from([
|
||||
0x00,
|
||||
0x00, // Reserved
|
||||
0x01,
|
||||
0x00, // Type (1 = ICO)
|
||||
0x01,
|
||||
0x00, // Count (1 icon)
|
||||
]);
|
||||
|
||||
// Directory entry (16 bytes)
|
||||
const dirEntry = Buffer.from([
|
||||
0x10, // Width (16)
|
||||
0x10, // Height (16)
|
||||
0x00, // Color count (0 = 256 colors)
|
||||
0x00, // Reserved
|
||||
0x01,
|
||||
0x00, // Planes
|
||||
0x08,
|
||||
0x00, // Bit count
|
||||
0x28,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00, // Bytes in resource (296)
|
||||
0x16,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00, // Image offset (22)
|
||||
]);
|
||||
|
||||
// Minimal BMP data (just a header for simplicity)
|
||||
const bmpHeader = Buffer.alloc(40);
|
||||
bmpHeader.writeUInt32LE(40, 0); // Header size
|
||||
bmpHeader.writeInt32LE(16, 4); // Width
|
||||
bmpHeader.writeInt32LE(32, 8); // Height (double for AND mask)
|
||||
bmpHeader.writeUInt16LE(1, 12); // Planes
|
||||
bmpHeader.writeUInt16LE(8, 14); // Bit count
|
||||
bmpHeader.writeUInt32LE(0, 16); // Compression
|
||||
bmpHeader.writeUInt32LE(256, 20); // Image size
|
||||
|
||||
// Create minimal image data
|
||||
const imageData = Buffer.alloc(256); // 16x16x8bpp
|
||||
|
||||
return Buffer.concat([header, dirEntry, bmpHeader, imageData]);
|
||||
};
|
||||
|
||||
describe("compile with icon", () => {
|
||||
test("--windows-icon sets executable icon", async () => {
|
||||
const dir = tempDirWithFiles("windows-icon", {
|
||||
"index.js": `console.log("Hello from Bun!");`,
|
||||
"test.ico": createTestIcon(),
|
||||
});
|
||||
|
||||
const proc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
isWindows ? "" : "--target=bun-windows-x64-v1.2.19",
|
||||
"--windows-icon",
|
||||
join(dir, "test.ico"),
|
||||
join(dir, "index.js"),
|
||||
"--outfile",
|
||||
join(dir, "test.exe"),
|
||||
].filter(x => x),
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.log("stdout:", stdout);
|
||||
console.log("stderr:", stderr);
|
||||
console.log("exitCode:", exitCode);
|
||||
}
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).not.toContain("Failed to set");
|
||||
|
||||
// Verify executable exists
|
||||
const exePath = join(dir, "test.exe");
|
||||
expect(await Bun.file(exePath).exists()).toBe(true);
|
||||
|
||||
// Check file size
|
||||
const fileInfo = await Bun.file(exePath);
|
||||
const fileSize = fileInfo.size;
|
||||
|
||||
// Parse and verify resources
|
||||
// Force a small delay to ensure file system operations are complete
|
||||
await Bun.sleep(100);
|
||||
|
||||
// Use Node.js fs to read the file to avoid any potential Bun caching issues
|
||||
const fs = require("fs");
|
||||
const exeBuffer = fs.readFileSync(exePath);
|
||||
const exeData = exeBuffer.buffer.slice(exeBuffer.byteOffset, exeBuffer.byteOffset + exeBuffer.byteLength);
|
||||
|
||||
const resources = windowsResourceInternals.parseResources(new Uint8Array(exeData));
|
||||
|
||||
// Should have icon resources
|
||||
expect(resources.icons.length).toBeGreaterThan(0);
|
||||
expect(resources.groupIcons.length).toBe(1);
|
||||
|
||||
// Verify icon data matches what we embedded
|
||||
const originalIcon = windowsResourceInternals.parseIconFile(createTestIcon());
|
||||
expect(resources.icons[0].data).toEqual(originalIcon.icons[0].data);
|
||||
});
|
||||
|
||||
test("invalid icon file shows error", async () => {
|
||||
const dir = tempDirWithFiles("windows-icon-invalid", {
|
||||
"index.js": `console.log("Hello!");`,
|
||||
"bad.ico": Buffer.from("not an icon"),
|
||||
});
|
||||
|
||||
const proc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
isWindows ? "" : "--target=bun-windows-x64-v1.2.19",
|
||||
"--windows-icon",
|
||||
join(dir, "bad.ico"),
|
||||
join(dir, "index.js"),
|
||||
"--outfile",
|
||||
join(dir, "test.exe"),
|
||||
].filter(x => x),
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Invalid");
|
||||
});
|
||||
});
|
||||
|
||||
describe("compile with version info", () => {
|
||||
test("--windows-version and --windows-description set version info", async () => {
|
||||
const dir = tempDirWithFiles("windows-version", {
|
||||
"index.js": `console.log("Hello from MyApp!");`,
|
||||
});
|
||||
|
||||
const proc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
isWindows ? "" : "--target=bun-windows-x64-v1.2.19",
|
||||
"--windows-version",
|
||||
"1.2.3.4",
|
||||
"--windows-description",
|
||||
"My Test Application",
|
||||
join(dir, "index.js"),
|
||||
"--outfile",
|
||||
join(dir, "myapp.exe"),
|
||||
].filter(x => x),
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).not.toContain("Failed to set");
|
||||
|
||||
// Verify executable exists
|
||||
const exePath = join(dir, "myapp.exe");
|
||||
expect(await Bun.file(exePath).exists()).toBe(true);
|
||||
|
||||
// Parse and verify resources
|
||||
await Bun.sleep(100);
|
||||
const fs = require("fs");
|
||||
const exeBuffer = fs.readFileSync(exePath);
|
||||
const exeData = exeBuffer.buffer.slice(exeBuffer.byteOffset, exeBuffer.byteOffset + exeBuffer.byteLength);
|
||||
const resources = windowsResourceInternals.parseResources(new Uint8Array(exeData));
|
||||
|
||||
// Should have version info
|
||||
expect(resources.versionInfo).not.toBeNull();
|
||||
expect(resources.versionInfo.fileVersion).toBe("1.2.3.4");
|
||||
expect(resources.versionInfo.fileDescription).toBe("My Test Application");
|
||||
});
|
||||
|
||||
test("all Windows options together", async () => {
|
||||
const dir = tempDirWithFiles("windows-all", {
|
||||
"index.js": `console.log("Complete app!");`,
|
||||
"app.ico": createTestIcon(),
|
||||
});
|
||||
|
||||
const proc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
isWindows ? "" : "--target=bun-windows-x64-v1.2.19",
|
||||
"--windows-icon",
|
||||
join(dir, "app.ico"),
|
||||
"--windows-version",
|
||||
"10.5.3.1",
|
||||
"--windows-description",
|
||||
"Complete Test Application",
|
||||
"--windows-hide-console",
|
||||
join(dir, "index.js"),
|
||||
"--outfile",
|
||||
join(dir, "super.exe"),
|
||||
].filter(x => x),
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
|
||||
const exePath = join(dir, "super.exe");
|
||||
expect(await Bun.file(exePath).exists()).toBe(true);
|
||||
|
||||
// Parse and verify all resources
|
||||
await Bun.sleep(100);
|
||||
const fs = require("fs");
|
||||
const exeBuffer = fs.readFileSync(exePath);
|
||||
const exeData = exeBuffer.buffer.slice(exeBuffer.byteOffset, exeBuffer.byteOffset + exeBuffer.byteLength);
|
||||
const resources = windowsResourceInternals.parseResources(new Uint8Array(exeData));
|
||||
|
||||
// Verify icon
|
||||
expect(resources.icons.length).toBeGreaterThan(0);
|
||||
expect(resources.groupIcons.length).toBe(1);
|
||||
|
||||
// Verify version info
|
||||
expect(resources.versionInfo).not.toBeNull();
|
||||
expect(resources.versionInfo.fileVersion).toBe("10.5.3.1");
|
||||
expect(resources.versionInfo.fileDescription).toBe("Complete Test Application");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cross-platform compilation", () => {
|
||||
test("can set Windows resources when compiling for Windows from non-Windows", async () => {
|
||||
// Skip if already on Windows
|
||||
if (isWindows) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = tempDirWithFiles("windows-cross", {
|
||||
"index.js": `console.log("Cross-compiled!");`,
|
||||
"icon.ico": createTestIcon(),
|
||||
});
|
||||
|
||||
const proc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
"--target=bun-windows-x64-v1.2.19",
|
||||
"--windows-icon",
|
||||
join(dir, "icon.ico"),
|
||||
"--windows-version",
|
||||
"2.0.0.0",
|
||||
"--windows-description",
|
||||
"Cross Platform App",
|
||||
join(dir, "index.js"),
|
||||
"--outfile",
|
||||
join(dir, "cross.exe"),
|
||||
],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Should produce a Windows executable
|
||||
const exePath = join(dir, "cross.exe");
|
||||
expect(await Bun.file(exePath).exists()).toBe(true);
|
||||
|
||||
// Verify resources are properly embedded even when cross-compiling
|
||||
await Bun.sleep(100);
|
||||
const fs = require("fs");
|
||||
const exeBuffer = fs.readFileSync(exePath);
|
||||
const exeData = exeBuffer.buffer.slice(exeBuffer.byteOffset, exeBuffer.byteOffset + exeBuffer.byteLength);
|
||||
const resources = windowsResourceInternals.parseResources(new Uint8Array(exeData));
|
||||
|
||||
expect(resources.icons.length).toBeGreaterThan(0);
|
||||
expect(resources.versionInfo).not.toBeNull();
|
||||
expect(resources.versionInfo.fileVersion).toBe("2.0.0.0");
|
||||
expect(resources.versionInfo.fileDescription).toBe("Cross Platform App");
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
test("--windows-description requires --compile", async () => {
|
||||
const dir = tempDirWithFiles("windows-no-compile", {
|
||||
"index.js": `console.log("test");`,
|
||||
});
|
||||
|
||||
const proc = spawn({
|
||||
cmd: [bunExe(), "build", "--windows-description", "Test", join(dir, "index.js")],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("--windows-description requires --compile");
|
||||
});
|
||||
|
||||
test("--windows-icon requires --compile", async () => {
|
||||
const dir = tempDirWithFiles("windows-icon-no-compile", {
|
||||
"index.js": `console.log("test");`,
|
||||
"test.ico": createTestIcon(),
|
||||
});
|
||||
|
||||
const proc = spawn({
|
||||
cmd: [bunExe(), "build", "--windows-icon", join(dir, "test.ico"), join(dir, "index.js")],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("--windows-icon requires --compile");
|
||||
});
|
||||
|
||||
test("--windows-version requires --compile", async () => {
|
||||
const dir = tempDirWithFiles("windows-version-no-compile", {
|
||||
"index.js": `console.log("test");`,
|
||||
});
|
||||
|
||||
const proc = spawn({
|
||||
cmd: [bunExe(), "build", "--windows-version", "1.0.0.0", join(dir, "index.js")],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("--windows-version requires --compile");
|
||||
});
|
||||
|
||||
test("invalid version format shows error", async () => {
|
||||
const dir = tempDirWithFiles("windows-bad-version", {
|
||||
"index.js": `console.log("test");`,
|
||||
});
|
||||
|
||||
const proc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
"--target=bun-windows-x64-v1.2.19",
|
||||
"--windows-version",
|
||||
"not-a-version",
|
||||
join(dir, "index.js"),
|
||||
],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("Invalid");
|
||||
});
|
||||
});
|
||||
|
||||
describe("version parsing", () => {
|
||||
test("accepts valid version formats", async () => {
|
||||
const dir = tempDirWithFiles("windows-version-formats", {
|
||||
"index.js": `console.log("Version test");`,
|
||||
});
|
||||
|
||||
const testCases = [
|
||||
{ version: "1.0.0.0", expected: "1.0.0.0" },
|
||||
{ version: "255.255.65535.65535", expected: "255.255.65535.65535" },
|
||||
{ version: "0.0.0.1", expected: "0.0.0.1" },
|
||||
];
|
||||
|
||||
for (const { version, expected } of testCases) {
|
||||
await using proc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
"--target=bun-windows-x64-v1.2.19",
|
||||
`--outfile=version-${version.replace(/\./g, "-")}.exe`,
|
||||
`--windows-version=${version}`,
|
||||
join(dir, "index.js"),
|
||||
],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
|
||||
// Verify version in resources
|
||||
const exePath = join(dir, `version-${version.replace(/\./g, "-")}.exe`);
|
||||
await Bun.sleep(100);
|
||||
const fs = require("fs");
|
||||
const exeBuffer = fs.readFileSync(exePath);
|
||||
const exeData = exeBuffer.buffer.slice(exeBuffer.byteOffset, exeBuffer.byteOffset + exeBuffer.byteLength);
|
||||
const resources = windowsResourceInternals.parseResources(new Uint8Array(exeData));
|
||||
|
||||
expect(resources.versionInfo?.fileVersion).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects invalid version formats", async () => {
|
||||
const dir = tempDirWithFiles("windows-bad-versions", {
|
||||
"index.js": `console.log("test");`,
|
||||
});
|
||||
|
||||
const invalidVersions = [
|
||||
"1", // too few parts
|
||||
"1.2", // too few parts
|
||||
"1.2.3", // too few parts
|
||||
"1.2.3.4.5", // too many parts
|
||||
"a.b.c.d", // non-numeric
|
||||
"1.2.3.-1", // negative number
|
||||
"65536.0.0.0", // overflow
|
||||
];
|
||||
|
||||
for (const version of invalidVersions) {
|
||||
await using proc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
"--target=bun-windows-x64-v1.2.19",
|
||||
`--windows-version=${version}`,
|
||||
join(dir, "index.js"),
|
||||
],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("Invalid");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Run actual executable on Windows
|
||||
if (isWindows) {
|
||||
describe("runtime verification", () => {
|
||||
test("executable with resources runs correctly", async () => {
|
||||
const dir = tempDirWithFiles("windows-runtime", {
|
||||
"app.js": `console.log("Running with resources!");`,
|
||||
"app.ico": createTestIcon(),
|
||||
});
|
||||
|
||||
// Build with resources
|
||||
await using buildProc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
"--windows-icon",
|
||||
join(dir, "app.ico"),
|
||||
"--windows-version",
|
||||
"1.0.0.0",
|
||||
"--windows-description",
|
||||
"Runtime Test App",
|
||||
join(dir, "app.js"),
|
||||
"--outfile",
|
||||
join(dir, "app.exe"),
|
||||
],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
expect(await buildProc.exited).toBe(0);
|
||||
|
||||
// Run the executable
|
||||
await using runProc = spawn({
|
||||
cmd: [join(dir, "app.exe")],
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, exitCode] = await Promise.all([new Response(runProc.stdout).text(), runProc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.trim()).toBe("Running with resources!");
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user