@zogien/native-sdk 0.11.0-fork.3 → 0.11.0-fork.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/app.zig +53 -34
- package/build/cli.zig +22 -0
- package/package.json +10 -10
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/schemas/app.schema.json +9 -1
- package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
- package/skill-data/ts-core/SKILL.md +2 -0
- package/src/app_runner/app_assets.zig +85 -1
- package/src/app_runner/root.zig +63 -3
- package/src/app_runner/ts_core_main.zig +8 -0
- package/src/platform/linux/cef_host.cpp +22 -0
- package/src/platform/linux/gtk_host.c +317 -34
- package/src/platform/linux/gtk_host.h +37 -0
- package/src/platform/linux/root.zig +114 -20
- package/src/platform/macos/appkit_host.h +53 -0
- package/src/platform/macos/appkit_host.m +667 -67
- package/src/platform/macos/cef_host.mm +21 -0
- package/src/platform/macos/root.zig +146 -29
- package/src/platform/null_platform.zig +136 -0
- package/src/platform/root.zig +7 -0
- package/src/platform/types.zig +90 -0
- package/src/platform/windows/dialog_test.cpp +303 -0
- package/src/platform/windows/host_loop_test.cpp +169 -0
- package/src/platform/windows/root.zig +262 -39
- package/src/platform/windows/session_end_test.cpp +303 -0
- package/src/platform/windows/webview2_host.cpp +1435 -306
- package/src/primitives/app_manifest/root.zig +1 -0
- package/src/primitives/app_manifest/tests.zig +35 -0
- package/src/primitives/app_manifest/types.zig +12 -0
- package/src/primitives/app_manifest/validation.zig +44 -6
- package/src/root.zig +8 -0
- package/src/runtime/builtin_bridge.zig +108 -13
- package/src/runtime/channel_sources.zig +49 -1
- package/src/runtime/core.zig +24 -3
- package/src/runtime/dialog_tests.zig +367 -0
- package/src/runtime/dialogs.zig +385 -0
- package/src/runtime/effects.zig +83 -3
- package/src/runtime/effects_host_tests.zig +69 -1
- package/src/runtime/flow.zig +20 -6
- package/src/runtime/flow_reference.zig +20 -6
- package/src/runtime/platform_bridge_tests.zig +1 -0
- package/src/runtime/root.zig +9 -0
- package/src/runtime/single_instance.zig +893 -0
- package/src/runtime/system_services.zig +64 -82
- package/src/runtime/ui_app.zig +67 -0
- package/src/tooling/manifest.zig +111 -3
- package/src/tooling/package.zig +98 -8
- package/src/tooling/raw_manifest.zig +7 -0
- package/src/tooling/root.zig +2 -0
- package/src/tooling/templates.zig +44 -2
- package/src/tooling/update.zig +228 -10
- package/src/tooling/windows_installer.zig +245 -0
- package/src/tooling/zip_writer.zig +211 -0
- package/src/updater/engine.zig +936 -0
- package/src/updater/feed.zig +261 -12
- package/src/updater/macos.zig +187 -0
- package/src/updater/root.zig +41 -0
- package/src/updater/windows.zig +470 -0
- package/src/windows_install/config.zig +247 -0
- package/src/windows_install/main.zig +690 -0
- package/src/windows_install/payload.zig +146 -0
- package/src/windows_install/switch.zig +184 -0
- package/src/windows_install/tests.zig +11 -0
- package/src/windows_install/win32.zig +248 -0
- package/tools/native-sdk/main.zig +10 -9
|
@@ -1597,6 +1597,9 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path:
|
|
|
1597
1597
|
\\ package.addArgs(&.{ "--web-layer", if (web_layer) "include" else "exclude" });
|
|
1598
1598
|
\\ if (cef_auto_install) package.addArg("--cef-auto-install");
|
|
1599
1599
|
\\ if (app_config.updates_enabled and package_target == .macos and b.graph.host.result.os.tag == .macos) package.addArg("--update-archive");
|
|
1600
|
+
\\ // Windows updates reach only installed apps, so an app with updates
|
|
1601
|
+
\\ // ships the per-user setup program beside the archive its feed names.
|
|
1602
|
+
\\ if (app_config.updates_enabled and package_target == .windows) package.addArgs(&.{ "--installer", "--update-archive" });
|
|
1600
1603
|
\\ package.step.dependOn(&package_exe.step);
|
|
1601
1604
|
\\ package.step.dependOn(&frontend_build.step);
|
|
1602
1605
|
\\ const package_step = b.step("package", "Create a local package artifact");
|
|
@@ -2440,8 +2443,9 @@ fn runnerZig() []const u8 {
|
|
|
2440
2443
|
\\ .bundle_id = self.bundle_id,
|
|
2441
2444
|
\\ .icon_path = self.icon_path,
|
|
2442
2445
|
\\ .update_feed_url = manifestUpdateString("feed_url"),
|
|
2443
|
-
\\ .update_public_key =
|
|
2446
|
+
\\ .update_public_key = manifestUpdatePublicKeys(),
|
|
2444
2447
|
\\ .update_check_on_start = manifestUpdateCheckOnStart(),
|
|
2448
|
+
\\ .update_mode = manifestUpdateMode(),
|
|
2445
2449
|
\\ };
|
|
2446
2450
|
\\ const windows = manifestWindowOptions(buffers);
|
|
2447
2451
|
\\ if (windows.len > 0) {
|
|
@@ -2518,6 +2522,36 @@ fn runnerZig() []const u8 {
|
|
|
2518
2522
|
\\ return app_manifest.updates.check_on_start;
|
|
2519
2523
|
\\}
|
|
2520
2524
|
\\
|
|
2525
|
+
\\/// The trusted update keys the updater verifies feeds against:
|
|
2526
|
+
\\/// `public_key` followed by any `public_keys`, comma-joined.
|
|
2527
|
+
\\fn manifestUpdatePublicKeys() []const u8 {
|
|
2528
|
+
\\ const primary = comptime manifestUpdateString("public_key");
|
|
2529
|
+
\\ if (comptime primary.len == 0) return "";
|
|
2530
|
+
\\ if (comptime !@hasField(@TypeOf(app_manifest.updates), "public_keys")) return primary;
|
|
2531
|
+
\\ comptime var keys: []const u8 = primary;
|
|
2532
|
+
\\ inline for (app_manifest.updates.public_keys) |key| keys = keys ++ "," ++ key;
|
|
2533
|
+
\\ return keys;
|
|
2534
|
+
\\}
|
|
2535
|
+
\\
|
|
2536
|
+
\\fn manifestUpdateMode() native_sdk.platform.UpdateMode {
|
|
2537
|
+
\\ if (comptime !@hasField(@TypeOf(app_manifest), "updates")) return .host;
|
|
2538
|
+
\\ if (comptime !@hasField(@TypeOf(app_manifest.updates), "mode")) return .host;
|
|
2539
|
+
\\ return comptime std.meta.stringToEnum(native_sdk.platform.UpdateMode, app_manifest.updates.mode) orelse
|
|
2540
|
+
\\ @compileError("app manifest updates.mode must be \"host\" or \"app\"");
|
|
2541
|
+
\\}
|
|
2542
|
+
\\
|
|
2543
|
+
\\/// Points the app-driven updater at this installation (see
|
|
2544
|
+
\\/// `updater.configureApp`).
|
|
2545
|
+
\\fn configureUpdates(app_info: *native_sdk.platform.AppInfo, environ_map: *std.process.Environ.Map) void {
|
|
2546
|
+
\\ const host: native_sdk.updater.Host = comptime if (std.mem.eql(u8, build_options.platform, "windows"))
|
|
2547
|
+
\\ .windows
|
|
2548
|
+
\\ else if (std.mem.eql(u8, build_options.platform, "macos") and std.mem.eql(u8, build_options.web_engine, "system"))
|
|
2549
|
+
\\ .macos_appkit
|
|
2550
|
+
\\ else
|
|
2551
|
+
\\ .other;
|
|
2552
|
+
\\ native_sdk.updater.configureApp(host, app_info, environ_map);
|
|
2553
|
+
\\}
|
|
2554
|
+
\\
|
|
2521
2555
|
\\const CommandStorage = struct {
|
|
2522
2556
|
\\ commands: [native_sdk.app_manifest.max_commands]native_sdk.Command = undefined,
|
|
2523
2557
|
\\
|
|
@@ -3062,6 +3096,7 @@ fn runnerZig() []const u8 {
|
|
|
3062
3096
|
\\fn runMacos(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
|
|
3063
3097
|
\\ var buffers: StateBuffers = undefined;
|
|
3064
3098
|
\\ var app_info = options.appInfo(&buffers);
|
|
3099
|
+
\\ configureUpdates(&app_info, init.environ_map);
|
|
3065
3100
|
\\ const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
|
|
3066
3101
|
\\ // Heap wrapper, latch-gated free: worker threads hold this address
|
|
3067
3102
|
\\ // as the channel wake context and an abandoned wake call may
|
|
@@ -3120,6 +3155,7 @@ fn runnerZig() []const u8 {
|
|
|
3120
3155
|
\\fn runLinux(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
|
|
3121
3156
|
\\ var buffers: StateBuffers = undefined;
|
|
3122
3157
|
\\ var app_info = options.appInfo(&buffers);
|
|
3158
|
+
\\ configureUpdates(&app_info, init.environ_map);
|
|
3123
3159
|
\\ const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
|
|
3124
3160
|
\\ // Heap wrapper, latch-gated free: worker threads hold this address
|
|
3125
3161
|
\\ // as the channel wake context and an abandoned wake call may
|
|
@@ -3178,6 +3214,7 @@ fn runnerZig() []const u8 {
|
|
|
3178
3214
|
\\fn runWindows(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
|
|
3179
3215
|
\\ var buffers: StateBuffers = undefined;
|
|
3180
3216
|
\\ var app_info = options.appInfo(&buffers);
|
|
3217
|
+
\\ configureUpdates(&app_info, init.environ_map);
|
|
3181
3218
|
\\ const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
|
|
3182
3219
|
\\ // Heap wrapper, latch-gated free: worker threads hold this address
|
|
3183
3220
|
\\ // as the channel wake context and an abandoned wake call may
|
|
@@ -4272,6 +4309,7 @@ test "writeDefaultApp emits Vite project files" {
|
|
|
4272
4309
|
// the packaged artifact structurally agrees with the compiled exe.
|
|
4273
4310
|
try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package.addArgs(&.{ \"--web-layer\", if (web_layer) \"include\" else \"exclude\" })") != null);
|
|
4274
4311
|
try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package.addArg(\"--update-archive\")") != null);
|
|
4312
|
+
try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package_target == .windows) package.addArgs(&.{ \"--installer\", \"--update-archive\" })") != null);
|
|
4275
4313
|
try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "options.addOption(bool, \"web_layer\", web_layer)") != null);
|
|
4276
4314
|
try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "std.json.parseFromSliceLeaky(InferenceManifest") != null);
|
|
4277
4315
|
try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "fn appManifestModule") != null);
|
|
@@ -4336,8 +4374,12 @@ test "writeDefaultApp emits Vite project files" {
|
|
|
4336
4374
|
try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, "fn appInfo(self: RunOptions, buffers: *StateBuffers)") != null);
|
|
4337
4375
|
try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".version = manifestStringField(\"version\")") != null);
|
|
4338
4376
|
try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".update_feed_url = manifestUpdateString(\"feed_url\")") != null);
|
|
4339
|
-
try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".update_public_key =
|
|
4377
|
+
try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".update_public_key = manifestUpdatePublicKeys()") != null);
|
|
4340
4378
|
try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".update_check_on_start = manifestUpdateCheckOnStart()") != null);
|
|
4379
|
+
try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".update_mode = manifestUpdateMode()") != null);
|
|
4380
|
+
// Ejected apps configure the app-driven updater on every desktop
|
|
4381
|
+
// platform.
|
|
4382
|
+
try std.testing.expectEqual(@as(usize, 3), std.mem.count(u8, runner_zig_text, "configureUpdates(&app_info, init.environ_map);"));
|
|
4341
4383
|
try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, "fn manifestWindowOptions") != null);
|
|
4342
4384
|
try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, "info.windows = windows") != null);
|
|
4343
4385
|
try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, "for (restored_windows, 0..)") != null);
|
package/src/tooling/update.zig
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
const manifest_tool = @import("manifest.zig");
|
|
3
3
|
const update_feed = @import("update_feed");
|
|
4
|
+
const install_config = @import("windows_install_config");
|
|
4
5
|
|
|
5
6
|
const Ed25519 = std.crypto.sign.Ed25519;
|
|
6
7
|
|
|
@@ -50,9 +51,14 @@ fn sign(allocator: std.mem.Allocator, io: std.Io, args: []const []const u8) !voi
|
|
|
50
51
|
var target: ?[]const u8 = null;
|
|
51
52
|
var release_notes: []const u8 = "";
|
|
52
53
|
var output_path: []const u8 = "native-update.json";
|
|
54
|
+
var url_policy: update_feed.UrlPolicy = .{};
|
|
53
55
|
var index: usize = 0;
|
|
54
56
|
while (index < args.len) : (index += 1) {
|
|
55
57
|
const name = args[index];
|
|
58
|
+
if (std.mem.eql(u8, name, "--allow-loopback-http")) {
|
|
59
|
+
url_policy.allow_loopback_http = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
56
62
|
index += 1;
|
|
57
63
|
if (index >= args.len) return error.InvalidArguments;
|
|
58
64
|
const value = args[index];
|
|
@@ -61,15 +67,16 @@ fn sign(allocator: std.mem.Allocator, io: std.Io, args: []const []const u8) !voi
|
|
|
61
67
|
const archive = archive_path orelse return error.InvalidArguments;
|
|
62
68
|
const url = archive_url orelse return error.InvalidArguments;
|
|
63
69
|
const release_target = target orelse return error.InvalidArguments;
|
|
64
|
-
if (!update_feed.
|
|
65
|
-
if (!
|
|
70
|
+
if (!update_feed.urlAllowed(url, url_policy)) return error.InvalidArchiveUrl;
|
|
71
|
+
if (!update_feed.isTarget(release_target)) return error.InvalidTarget;
|
|
66
72
|
if (!std.mem.endsWith(u8, archive, ".zip")) return error.InvalidArchive;
|
|
67
73
|
const metadata = try manifest_tool.readMetadata(allocator, io, manifest_path orelse manifest_tool.defaultPath(io) orelse "app.json");
|
|
68
74
|
defer metadata.deinit(allocator);
|
|
69
75
|
if (!metadata.updates.enabled()) return error.UpdatesNotConfigured;
|
|
70
76
|
_ = manifest_tool.parseVersion(metadata.version) catch return error.InvalidManifestVersion;
|
|
71
77
|
if (release_notes.len > 16 * 1024) return error.ReleaseNotesTooLarge;
|
|
72
|
-
|
|
78
|
+
const validate = if (std.mem.startsWith(u8, release_target, "windows-")) &validateWindowsUpdateArchive else &validateUpdateArchive;
|
|
79
|
+
validate(allocator, io, archive, metadata.id, metadata.version, metadata.name, release_target) catch |err| switch (err) {
|
|
73
80
|
error.UpdateArchitectureMismatch => return error.UpdateArchitectureMismatch,
|
|
74
81
|
else => return error.InvalidArchive,
|
|
75
82
|
};
|
|
@@ -83,12 +90,14 @@ fn sign(allocator: std.mem.Allocator, io: std.Io, args: []const []const u8) !voi
|
|
|
83
90
|
var extra: [1]u8 = undefined;
|
|
84
91
|
if (try key_reader.interface.readSliceShort(&extra) != 0) return error.InvalidPrivateKey;
|
|
85
92
|
const key_pair = try Ed25519.KeyPair.generateDeterministic(seed);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
// The release's installs trust the manifest's `public_key` and any
|
|
94
|
+
// `public_keys`, so a release that rotates keys is signed with the key
|
|
95
|
+
// the installs it replaces already know.
|
|
96
|
+
if (metadata.updates.public_key == null) return error.UpdatesNotConfigured;
|
|
97
|
+
if (!metadata.updates.publicKeysAreValid()) return error.InvalidUpdatePublicKey;
|
|
98
|
+
var signing_public_key_base64: [std.base64.standard.Encoder.calcSize(Ed25519.PublicKey.encoded_length)]u8 = undefined;
|
|
99
|
+
const signing_public_key = std.base64.standard.Encoder.encode(&signing_public_key_base64, &key_pair.public_key.toBytes());
|
|
100
|
+
if (!metadata.updates.trustsPublicKey(signing_public_key)) return error.UpdateKeyMismatch;
|
|
92
101
|
|
|
93
102
|
var archive_file = try std.Io.Dir.cwd().openFile(io, archive, .{});
|
|
94
103
|
defer archive_file.close(io);
|
|
@@ -139,7 +148,7 @@ fn sign(allocator: std.mem.Allocator, io: std.Io, args: []const []const u8) !voi
|
|
|
139
148
|
defer output_writer.deinit();
|
|
140
149
|
try std.json.Stringify.value(.{ .payload = payload_base64, .signature = &signature_base64 }, .{ .whitespace = .indent_2 }, &output_writer.writer);
|
|
141
150
|
try output_writer.writer.writeByte('\n');
|
|
142
|
-
var verified = try update_feed.
|
|
151
|
+
var verified = try update_feed.verifyEnvelopeWithPolicy(allocator, output_writer.written(), signing_public_key, url_policy);
|
|
143
152
|
defer verified.deinit(allocator);
|
|
144
153
|
if (!std.mem.eql(u8, verified.release.bundle_id, metadata.id) or
|
|
145
154
|
!std.mem.eql(u8, verified.release.version, metadata.version) or
|
|
@@ -149,6 +158,9 @@ fn sign(allocator: std.mem.Allocator, io: std.Io, args: []const []const u8) !voi
|
|
|
149
158
|
try atomic.file.writeStreamingAll(io, output_writer.written());
|
|
150
159
|
try atomic.replace(io);
|
|
151
160
|
std.debug.print("signed {s} {s} update ({d} bytes) into {s}\n", .{ metadata.displayName(), metadata.version, archive_stat.size, output_path });
|
|
161
|
+
if (!update_feed.archiveUrlIsValid(url)) {
|
|
162
|
+
std.debug.print("note: the archive URL is loopback http://; installed apps accept this feed only while NATIVE_SDK_UPDATES_FEED_URL points them at it\n", .{});
|
|
163
|
+
}
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
const max_info_plist_bytes: u64 = 1024 * 1024;
|
|
@@ -218,6 +230,72 @@ fn validateUpdateArchive(
|
|
|
218
230
|
!plistStringValueEquals(plist, "CFBundlePackageType", "APPL")) return error.InvalidArchive;
|
|
219
231
|
}
|
|
220
232
|
|
|
233
|
+
/// A Windows update archive is the install image `native package
|
|
234
|
+
/// --update-archive` writes: the app's install configuration naming this
|
|
235
|
+
/// app and version, its launcher, and `bin\<name>.exe` built for the
|
|
236
|
+
/// target architecture.
|
|
237
|
+
fn validateWindowsUpdateArchive(
|
|
238
|
+
allocator: std.mem.Allocator,
|
|
239
|
+
io: std.Io,
|
|
240
|
+
archive_path: []const u8,
|
|
241
|
+
expected_bundle_id: []const u8,
|
|
242
|
+
expected_version: []const u8,
|
|
243
|
+
expected_name: []const u8,
|
|
244
|
+
expected_target: []const u8,
|
|
245
|
+
) !void {
|
|
246
|
+
const expected_machine: u16 = if (std.mem.eql(u8, expected_target, "windows-aarch64")) 0xaa64 else if (std.mem.eql(u8, expected_target, "windows-x86_64")) 0x8664 else return error.InvalidTarget;
|
|
247
|
+
var file = try std.Io.Dir.cwd().openFile(io, archive_path, .{});
|
|
248
|
+
defer file.close(io);
|
|
249
|
+
var reader_buffer: [64 * 1024]u8 = undefined;
|
|
250
|
+
var reader = file.reader(io, &reader_buffer);
|
|
251
|
+
var iterator = try std.zip.Iterator.init(&reader);
|
|
252
|
+
var filename_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
|
|
253
|
+
var executable_name_buffer: [256]u8 = undefined;
|
|
254
|
+
const executable_name = try std.fmt.bufPrint(&executable_name_buffer, "bin/{s}.exe", .{expected_name});
|
|
255
|
+
var config_bytes: ?[]u8 = null;
|
|
256
|
+
defer if (config_bytes) |bytes| allocator.free(bytes);
|
|
257
|
+
var has_launcher = false;
|
|
258
|
+
var executable_machine: ?u16 = null;
|
|
259
|
+
var entries: usize = 0;
|
|
260
|
+
|
|
261
|
+
while (try iterator.next()) |entry| {
|
|
262
|
+
const filename = try zipEntryFilename(&reader, entry, &filename_buffer);
|
|
263
|
+
if (!zipEntryPathIsSafe(filename)) return error.InvalidArchive;
|
|
264
|
+
// Installed apps unpack with std.zip, which reads only these.
|
|
265
|
+
if (entry.compression_method != .store and entry.compression_method != .deflate) return error.InvalidArchive;
|
|
266
|
+
entries += 1;
|
|
267
|
+
if (entries > 1_000_000) return error.InvalidArchive;
|
|
268
|
+
if (std.mem.eql(u8, filename, install_config.file_name)) {
|
|
269
|
+
if (config_bytes != null) return error.InvalidArchive;
|
|
270
|
+
config_bytes = try readZipEntryAlloc(allocator, &reader, entry, filename, install_config.max_config_bytes);
|
|
271
|
+
} else if (std.mem.eql(u8, filename, "native-launcher.exe")) {
|
|
272
|
+
has_launcher = true;
|
|
273
|
+
} else if (std.ascii.eqlIgnoreCase(filename, executable_name)) {
|
|
274
|
+
var header: [4096]u8 = undefined;
|
|
275
|
+
const prefix = try readZipEntryPrefix(&reader, entry, filename, &header);
|
|
276
|
+
executable_machine = peMachine(prefix) orelse return error.InvalidArchive;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const bytes = config_bytes orelse return error.InvalidArchive;
|
|
281
|
+
var parsed = install_config.parse(allocator, bytes) catch return error.InvalidArchive;
|
|
282
|
+
defer parsed.deinit();
|
|
283
|
+
const config = parsed.value;
|
|
284
|
+
if (!std.mem.eql(u8, config.bundle_id, expected_bundle_id) or
|
|
285
|
+
!std.mem.eql(u8, config.version, expected_version) or
|
|
286
|
+
!std.mem.eql(u8, config.name, expected_name)) return error.InvalidArchive;
|
|
287
|
+
if (!has_launcher) return error.InvalidArchive;
|
|
288
|
+
const machine = executable_machine orelse return error.InvalidArchive;
|
|
289
|
+
if (machine != expected_machine) return error.UpdateArchitectureMismatch;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn peMachine(header: []const u8) ?u16 {
|
|
293
|
+
if (header.len < 0x40 or header[0] != 'M' or header[1] != 'Z') return null;
|
|
294
|
+
const pe_offset: usize = std.mem.readInt(u32, header[0x3c..0x40], .little);
|
|
295
|
+
if (pe_offset + 6 > header.len or !std.mem.eql(u8, header[pe_offset..][0..4], "PE\x00\x00")) return null;
|
|
296
|
+
return std.mem.readInt(u16, header[pe_offset + 4 ..][0..2], .little);
|
|
297
|
+
}
|
|
298
|
+
|
|
221
299
|
const max_macho_architectures: usize = 64;
|
|
222
300
|
const max_macho_header_bytes: usize = 8 + max_macho_architectures * 32;
|
|
223
301
|
const cpu_type_x86_64: u32 = 0x01000007;
|
|
@@ -574,3 +652,143 @@ test "update archive validation requires one matching app bundle" {
|
|
|
574
652
|
try std.testing.expectError(error.InvalidArchive, validateUpdateArchive(std.testing.allocator, std.testing.io, valid_path, "com.example.other", "1.2.3", "demo", "macos-aarch64"));
|
|
575
653
|
try std.testing.expectError(error.InvalidArchive, validateUpdateArchive(std.testing.allocator, std.testing.io, valid_path, "com.example.demo", "1.2.4", "demo", "macos-aarch64"));
|
|
576
654
|
}
|
|
655
|
+
|
|
656
|
+
test "a release that rotates update keys is signed with a key its installs already trust" {
|
|
657
|
+
const allocator = std.testing.allocator;
|
|
658
|
+
const io = std.testing.io;
|
|
659
|
+
var tmp = std.testing.tmpDir(.{});
|
|
660
|
+
defer tmp.cleanup();
|
|
661
|
+
const root = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}", .{tmp.sub_path});
|
|
662
|
+
defer allocator.free(root);
|
|
663
|
+
|
|
664
|
+
const old_key = try Ed25519.KeyPair.generateDeterministic([_]u8{0x51} ** Ed25519.KeyPair.seed_length);
|
|
665
|
+
const unrelated_key = try Ed25519.KeyPair.generateDeterministic([_]u8{0x53} ** Ed25519.KeyPair.seed_length);
|
|
666
|
+
const old_seed = old_key.secret_key.seed();
|
|
667
|
+
const unrelated_seed = unrelated_key.secret_key.seed();
|
|
668
|
+
try tmp.dir.writeFile(io, .{ .sub_path = "old.key", .data = &old_seed });
|
|
669
|
+
try tmp.dir.writeFile(io, .{ .sub_path = "unrelated.key", .data = &unrelated_seed });
|
|
670
|
+
// The new release names its new key as `public_key` and keeps the old
|
|
671
|
+
// one in `public_keys`; installs of the previous release know only
|
|
672
|
+
// the old one.
|
|
673
|
+
try tmp.dir.writeFile(io, .{ .sub_path = "app.json", .data =
|
|
674
|
+
\\{
|
|
675
|
+
\\ "id": "com.example.demo",
|
|
676
|
+
\\ "name": "demo",
|
|
677
|
+
\\ "version": "1.2.3",
|
|
678
|
+
\\ "updates": {
|
|
679
|
+
\\ "feed_url": "https://example.com/releases/{target}/native-update.json",
|
|
680
|
+
\\ "public_key": "IBLLkMpg6OXY2vZuInLSIz4EhtVX6MZhQe2JIBd9frc=",
|
|
681
|
+
\\ "public_keys": ["wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo="]
|
|
682
|
+
\\ }
|
|
683
|
+
\\}
|
|
684
|
+
});
|
|
685
|
+
var plist_buffer: [1024]u8 = undefined;
|
|
686
|
+
const plist = try updateInfoPlist("com.example.demo", "1.2.3", "demo", &plist_buffer);
|
|
687
|
+
const executable = thinMachO64(cpu_type_arm64);
|
|
688
|
+
const archive_entries = [_]StoredZipEntry{
|
|
689
|
+
.{ .name = "Demo.app/Contents/Info.plist", .data = plist },
|
|
690
|
+
.{ .name = "Demo.app/Contents/MacOS/demo", .data = &executable },
|
|
691
|
+
};
|
|
692
|
+
const archive = try storedZipAlloc(allocator, &archive_entries);
|
|
693
|
+
defer allocator.free(archive);
|
|
694
|
+
try tmp.dir.writeFile(io, .{ .sub_path = "demo.zip", .data = archive });
|
|
695
|
+
|
|
696
|
+
const manifest_path = try std.fmt.allocPrint(allocator, "{s}/app.json", .{root});
|
|
697
|
+
defer allocator.free(manifest_path);
|
|
698
|
+
const archive_path = try std.fmt.allocPrint(allocator, "{s}/demo.zip", .{root});
|
|
699
|
+
defer allocator.free(archive_path);
|
|
700
|
+
const old_key_path = try std.fmt.allocPrint(allocator, "{s}/old.key", .{root});
|
|
701
|
+
defer allocator.free(old_key_path);
|
|
702
|
+
const unrelated_key_path = try std.fmt.allocPrint(allocator, "{s}/unrelated.key", .{root});
|
|
703
|
+
defer allocator.free(unrelated_key_path);
|
|
704
|
+
const output_path = try std.fmt.allocPrint(allocator, "{s}/native-update.json", .{root});
|
|
705
|
+
defer allocator.free(output_path);
|
|
706
|
+
|
|
707
|
+
try sign(allocator, io, &.{ "--private-key", old_key_path, "--manifest", manifest_path, "--archive", archive_path, "--url", "https://example.com/demo-1.2.3.zip", "--target", "macos-aarch64", "--output", output_path });
|
|
708
|
+
const feed = try std.Io.Dir.cwd().readFileAlloc(io, output_path, allocator, .limited(update_feed.max_envelope_bytes));
|
|
709
|
+
defer allocator.free(feed);
|
|
710
|
+
var verified = try update_feed.verifyEnvelope(allocator, feed, "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=");
|
|
711
|
+
defer verified.deinit(allocator);
|
|
712
|
+
try std.testing.expectEqualStrings("1.2.3", verified.release.version);
|
|
713
|
+
try std.testing.expectError(error.InvalidUpdateSignature, update_feed.verifyEnvelope(allocator, feed, "IBLLkMpg6OXY2vZuInLSIz4EhtVX6MZhQe2JIBd9frc="));
|
|
714
|
+
|
|
715
|
+
try std.testing.expectError(error.UpdateKeyMismatch, sign(allocator, io, &.{ "--private-key", unrelated_key_path, "--manifest", manifest_path, "--archive", archive_path, "--url", "https://example.com/demo-1.2.3.zip", "--target", "macos-aarch64", "--output", output_path }));
|
|
716
|
+
|
|
717
|
+
// A key that does not decode is reported as such, not as a mismatch.
|
|
718
|
+
try tmp.dir.writeFile(io, .{ .sub_path = "app.json", .data =
|
|
719
|
+
\\{
|
|
720
|
+
\\ "id": "com.example.demo",
|
|
721
|
+
\\ "name": "demo",
|
|
722
|
+
\\ "version": "1.2.3",
|
|
723
|
+
\\ "updates": {
|
|
724
|
+
\\ "feed_url": "https://example.com/releases/{target}/native-update.json",
|
|
725
|
+
\\ "public_key": "IBLLkMpg6OXY2vZuInLSIz4EhtVX6MZhQe2JIBd9frc=",
|
|
726
|
+
\\ "public_keys": ["wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=", "not-a-key"]
|
|
727
|
+
\\ }
|
|
728
|
+
\\}
|
|
729
|
+
});
|
|
730
|
+
try std.testing.expectError(error.InvalidUpdatePublicKey, sign(allocator, io, &.{ "--private-key", old_key_path, "--manifest", manifest_path, "--archive", archive_path, "--url", "https://example.com/demo-1.2.3.zip", "--target", "macos-aarch64", "--output", output_path }));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
test "a Windows release signs its installed image and names loopback http only when asked" {
|
|
734
|
+
const allocator = std.testing.allocator;
|
|
735
|
+
const io = std.testing.io;
|
|
736
|
+
var tmp = std.testing.tmpDir(.{});
|
|
737
|
+
defer tmp.cleanup();
|
|
738
|
+
const root = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}", .{tmp.sub_path});
|
|
739
|
+
defer allocator.free(root);
|
|
740
|
+
|
|
741
|
+
const key = try Ed25519.KeyPair.generateDeterministic([_]u8{0x51} ** Ed25519.KeyPair.seed_length);
|
|
742
|
+
const seed = key.secret_key.seed();
|
|
743
|
+
try tmp.dir.writeFile(io, .{ .sub_path = "release.key", .data = &seed });
|
|
744
|
+
try tmp.dir.writeFile(io, .{ .sub_path = "app.json", .data =
|
|
745
|
+
\\{
|
|
746
|
+
\\ "id": "com.example.demo",
|
|
747
|
+
\\ "name": "demo",
|
|
748
|
+
\\ "version": "1.2.3",
|
|
749
|
+
\\ "updates": {
|
|
750
|
+
\\ "feed_url": "https://example.com/releases/{target}/native-update.json",
|
|
751
|
+
\\ "public_key": "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo="
|
|
752
|
+
\\ }
|
|
753
|
+
\\}
|
|
754
|
+
});
|
|
755
|
+
var config_writer = std.Io.Writer.Allocating.init(allocator);
|
|
756
|
+
defer config_writer.deinit();
|
|
757
|
+
try install_config.write(.{ .bundle_id = "com.example.demo", .name = "demo", .display_name = "Demo", .version = "1.2.3", .executable = "demo.exe" }, &config_writer.writer);
|
|
758
|
+
var executable = [_]u8{0} ** 0x100;
|
|
759
|
+
executable[0] = 'M';
|
|
760
|
+
executable[1] = 'Z';
|
|
761
|
+
std.mem.writeInt(u32, executable[0x3c..0x40], 0x80, .little);
|
|
762
|
+
@memcpy(executable[0x80..0x84], "PE\x00\x00");
|
|
763
|
+
std.mem.writeInt(u16, executable[0x84..0x86], 0x8664, .little);
|
|
764
|
+
const archive = try storedZipAlloc(allocator, &.{
|
|
765
|
+
.{ .name = install_config.file_name, .data = config_writer.written() },
|
|
766
|
+
.{ .name = "native-launcher.exe", .data = "launcher" },
|
|
767
|
+
.{ .name = "bin/demo.exe", .data = &executable },
|
|
768
|
+
});
|
|
769
|
+
defer allocator.free(archive);
|
|
770
|
+
try tmp.dir.writeFile(io, .{ .sub_path = "demo.zip", .data = archive });
|
|
771
|
+
|
|
772
|
+
const manifest_path = try std.fmt.allocPrint(allocator, "{s}/app.json", .{root});
|
|
773
|
+
defer allocator.free(manifest_path);
|
|
774
|
+
const archive_path = try std.fmt.allocPrint(allocator, "{s}/demo.zip", .{root});
|
|
775
|
+
defer allocator.free(archive_path);
|
|
776
|
+
const key_path = try std.fmt.allocPrint(allocator, "{s}/release.key", .{root});
|
|
777
|
+
defer allocator.free(key_path);
|
|
778
|
+
const output_path = try std.fmt.allocPrint(allocator, "{s}/native-update.json", .{root});
|
|
779
|
+
defer allocator.free(output_path);
|
|
780
|
+
const common = [_][]const u8{ "--private-key", key_path, "--manifest", manifest_path, "--archive", archive_path, "--output", output_path };
|
|
781
|
+
|
|
782
|
+
try sign(allocator, io, &(common ++ [_][]const u8{ "--url", "https://example.com/demo-1.2.3.zip", "--target", "windows-x86_64" }));
|
|
783
|
+
try std.testing.expectError(error.UpdateArchitectureMismatch, sign(allocator, io, &(common ++ [_][]const u8{ "--url", "https://example.com/demo-1.2.3.zip", "--target", "windows-aarch64" })));
|
|
784
|
+
|
|
785
|
+
const local_url = "http://127.0.0.1:8080/demo-1.2.3.zip";
|
|
786
|
+
try std.testing.expectError(error.InvalidArchiveUrl, sign(allocator, io, &(common ++ [_][]const u8{ "--url", local_url, "--target", "windows-x86_64" })));
|
|
787
|
+
try sign(allocator, io, &(common ++ [_][]const u8{ "--url", local_url, "--target", "windows-x86_64", "--allow-loopback-http" }));
|
|
788
|
+
const feed = try std.Io.Dir.cwd().readFileAlloc(io, output_path, allocator, .limited(update_feed.max_envelope_bytes));
|
|
789
|
+
defer allocator.free(feed);
|
|
790
|
+
try std.testing.expectError(error.InvalidUpdatePayload, update_feed.verifyEnvelope(allocator, feed, "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo="));
|
|
791
|
+
var verified = try update_feed.verifyEnvelopeWithPolicy(allocator, feed, "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=", .{ .allow_loopback_http = true });
|
|
792
|
+
defer verified.deinit(allocator);
|
|
793
|
+
try std.testing.expectEqualStrings(local_url, verified.release.archive_url);
|
|
794
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
//! `native package --target windows --installer`: turns the Windows
|
|
2
|
+
//! artifact directory into a per-user setup program.
|
|
3
|
+
//!
|
|
4
|
+
//! The artifact directory becomes the install image: it gains
|
|
5
|
+
//! `native-install.json` (the install configuration) and
|
|
6
|
+
//! `native-launcher.exe` (the launcher the setup program copies to the
|
|
7
|
+
//! stable `<root>\<name>.exe`). setup.exe is the launcher program with the
|
|
8
|
+
//! configuration and a ZIP of the image appended, followed by a trailer
|
|
9
|
+
//! carrying their SHA-256 (src/windows_install/payload.zig).
|
|
10
|
+
|
|
11
|
+
const std = @import("std");
|
|
12
|
+
const manifest_tool = @import("manifest.zig");
|
|
13
|
+
const package = @import("package.zig");
|
|
14
|
+
const zip_writer = @import("zip_writer.zig");
|
|
15
|
+
const install_config = @import("windows_install_config");
|
|
16
|
+
const setup_payload = @import("windows_install_payload");
|
|
17
|
+
|
|
18
|
+
pub const launcher_file_in_image = "native-launcher.exe";
|
|
19
|
+
pub const launcher_x86_64 = @embedFile("windows_launcher_x86_64");
|
|
20
|
+
pub const launcher_aarch64 = @embedFile("windows_launcher_aarch64");
|
|
21
|
+
|
|
22
|
+
pub const Arch = enum {
|
|
23
|
+
x86_64,
|
|
24
|
+
aarch64,
|
|
25
|
+
|
|
26
|
+
pub fn launcher(self: Arch) []const u8 {
|
|
27
|
+
return switch (self) {
|
|
28
|
+
.x86_64 => launcher_x86_64,
|
|
29
|
+
.aarch64 => launcher_aarch64,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
pub const Options = struct {
|
|
35
|
+
/// The Windows artifact directory `native package` just staged.
|
|
36
|
+
artifact_dir: []const u8,
|
|
37
|
+
metadata: manifest_tool.Metadata,
|
|
38
|
+
/// The app executable's file name in `bin\`.
|
|
39
|
+
executable: []const u8,
|
|
40
|
+
arch: Arch,
|
|
41
|
+
output_path: []const u8,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/// The setup program's file name: `<name>-<version>-windows-<arch>-setup.exe`.
|
|
45
|
+
pub fn setupFileName(allocator: std.mem.Allocator, metadata: manifest_tool.Metadata, arch: Arch) ![]const u8 {
|
|
46
|
+
return std.fmt.allocPrint(allocator, "{s}-{s}-windows-{s}-setup.exe", .{ metadata.name, metadata.version, @tagName(arch) });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// The update archive's file name: `<name>-<version>-windows-<arch>-update.zip`.
|
|
50
|
+
pub fn updateArchiveFileName(allocator: std.mem.Allocator, metadata: manifest_tool.Metadata, arch: Arch) ![]const u8 {
|
|
51
|
+
return std.fmt.allocPrint(allocator, "{s}-{s}-windows-{s}-update.zip", .{ metadata.name, metadata.version, @tagName(arch) });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub fn createSetup(allocator: std.mem.Allocator, io: std.Io, options: Options) !void {
|
|
55
|
+
var arena_state = std.heap.ArenaAllocator.init(allocator);
|
|
56
|
+
defer arena_state.deinit();
|
|
57
|
+
const arena = arena_state.allocator();
|
|
58
|
+
const image = try buildImage(arena, io, options);
|
|
59
|
+
|
|
60
|
+
const program = options.arch.launcher();
|
|
61
|
+
const trailer = setup_payload.encode(.{
|
|
62
|
+
.config_offset = program.len,
|
|
63
|
+
.config_size = image.config.len,
|
|
64
|
+
.zip_offset = program.len + image.config.len,
|
|
65
|
+
.zip_size = image.zip.len,
|
|
66
|
+
.sha256 = setup_payload.digest(image.config, image.zip),
|
|
67
|
+
});
|
|
68
|
+
try writeAtomically(io, options.output_path, &.{ program, image.config, image.zip, &trailer });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The ZIP of the install image the updater downloads: the same image a
|
|
72
|
+
/// setup program carries, signed into a feed by `native update sign`.
|
|
73
|
+
pub fn createUpdateArchive(allocator: std.mem.Allocator, io: std.Io, options: Options) !void {
|
|
74
|
+
var arena_state = std.heap.ArenaAllocator.init(allocator);
|
|
75
|
+
defer arena_state.deinit();
|
|
76
|
+
const image = try buildImage(arena_state.allocator(), io, options);
|
|
77
|
+
try writeAtomically(io, options.output_path, &.{image.zip});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const Image = struct {
|
|
81
|
+
config: []const u8,
|
|
82
|
+
zip: []const u8,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/// Adds the install configuration and launcher to the artifact directory
|
|
86
|
+
/// and zips it, without the portable registration script.
|
|
87
|
+
fn buildImage(arena: std.mem.Allocator, io: std.Io, options: Options) !Image {
|
|
88
|
+
var artifact = try std.Io.Dir.cwd().openDir(io, options.artifact_dir, .{});
|
|
89
|
+
defer artifact.close(io);
|
|
90
|
+
const has_icon = if (artifact.access(io, "app-icon.ico", .{})) |_| true else |_| false;
|
|
91
|
+
const config = try installConfig(arena, options.metadata, options.executable, has_icon);
|
|
92
|
+
try install_config.validate(config);
|
|
93
|
+
|
|
94
|
+
var config_writer: std.Io.Writer.Allocating = .init(arena);
|
|
95
|
+
try install_config.write(config, &config_writer.writer);
|
|
96
|
+
const config_bytes = config_writer.written();
|
|
97
|
+
try artifact.writeFile(io, .{ .sub_path = install_config.file_name, .data = config_bytes });
|
|
98
|
+
try artifact.writeFile(io, .{ .sub_path = launcher_file_in_image, .data = options.arch.launcher() });
|
|
99
|
+
|
|
100
|
+
const collected = try zip_writer.collectDirectory(arena, io, options.artifact_dir);
|
|
101
|
+
var image: std.ArrayList(zip_writer.Entry) = .empty;
|
|
102
|
+
for (collected) |entry| {
|
|
103
|
+
// The portable artifact's registration script points at bin\; an
|
|
104
|
+
// installed app registers through the setup program instead.
|
|
105
|
+
if (std.mem.startsWith(u8, entry.name, "install/")) continue;
|
|
106
|
+
try image.append(arena, entry);
|
|
107
|
+
}
|
|
108
|
+
var zip: std.Io.Writer.Allocating = .init(arena);
|
|
109
|
+
try zip_writer.write(arena, &zip.writer, image.items);
|
|
110
|
+
return .{ .config = config_bytes, .zip = zip.written() };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn writeAtomically(io: std.Io, path: []const u8, parts: []const []const u8) !void {
|
|
114
|
+
if (std.fs.path.dirname(path)) |parent| try std.Io.Dir.cwd().createDirPath(io, parent);
|
|
115
|
+
var atomic = try std.Io.Dir.cwd().createFileAtomic(io, path, .{ .make_path = true, .replace = true });
|
|
116
|
+
defer atomic.deinit(io);
|
|
117
|
+
var buffer: [64 * 1024]u8 = undefined;
|
|
118
|
+
var writer = atomic.file.writer(io, &buffer);
|
|
119
|
+
for (parts) |part| try writer.interface.writeAll(part);
|
|
120
|
+
try writer.interface.flush();
|
|
121
|
+
try atomic.replace(io);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// The install configuration for an app: its identity, executable and
|
|
125
|
+
/// icon, and the HKCU\Software\Classes values its file associations and
|
|
126
|
+
/// URL schemes need, pointing at the installed launcher so a pending
|
|
127
|
+
/// version switch also applies when a document or link opens the app.
|
|
128
|
+
pub fn installConfig(allocator: std.mem.Allocator, metadata: manifest_tool.Metadata, executable: []const u8, has_icon: bool) !install_config.InstallConfig {
|
|
129
|
+
var registry: std.ArrayList(install_config.RegistryValue) = .empty;
|
|
130
|
+
var cleanup: std.ArrayList(install_config.RegistryCleanup) = .empty;
|
|
131
|
+
const open_command = "\"" ++ install_config.launcher_placeholder ++ "\" \"%1\"";
|
|
132
|
+
const document_icon = if (has_icon) install_config.install_placeholder ++ "\\app\\app-icon.ico" else install_config.launcher_placeholder;
|
|
133
|
+
|
|
134
|
+
for (metadata.file_associations) |association| {
|
|
135
|
+
const prog_id = try package.windowsProgId(allocator, metadata, association);
|
|
136
|
+
const prog_key = try std.fmt.allocPrint(allocator, "Software\\Classes\\{s}", .{prog_id});
|
|
137
|
+
for (association.extensions) |extension| {
|
|
138
|
+
const extension_key = try std.fmt.allocPrint(allocator, "Software\\Classes\\.{s}", .{package.trimExtensionDot(extension)});
|
|
139
|
+
try registry.append(allocator, .{ .key = extension_key, .value = prog_id });
|
|
140
|
+
try cleanup.append(allocator, .{ .key = extension_key, .name = "", .if_value = prog_id });
|
|
141
|
+
}
|
|
142
|
+
try registry.append(allocator, .{ .key = prog_key, .value = association.name });
|
|
143
|
+
try registry.append(allocator, .{ .key = prog_key, .name = "FriendlyTypeName", .value = association.name });
|
|
144
|
+
try registry.append(allocator, .{ .key = try std.fmt.allocPrint(allocator, "{s}\\DefaultIcon", .{prog_key}), .value = document_icon });
|
|
145
|
+
try registry.append(allocator, .{ .key = try std.fmt.allocPrint(allocator, "{s}\\shell\\open\\command", .{prog_key}), .value = open_command });
|
|
146
|
+
try cleanup.append(allocator, .{ .key = prog_key });
|
|
147
|
+
}
|
|
148
|
+
for (metadata.url_schemes) |url_scheme| {
|
|
149
|
+
const scheme_key = try std.fmt.allocPrint(allocator, "Software\\Classes\\{s}", .{url_scheme.scheme});
|
|
150
|
+
try registry.append(allocator, .{ .key = scheme_key, .value = try std.fmt.allocPrint(allocator, "URL:{s}", .{url_scheme.scheme}) });
|
|
151
|
+
try registry.append(allocator, .{ .key = scheme_key, .name = "URL Protocol", .value = "" });
|
|
152
|
+
try registry.append(allocator, .{ .key = try std.fmt.allocPrint(allocator, "{s}\\shell\\open\\command", .{scheme_key}), .value = open_command });
|
|
153
|
+
try cleanup.append(allocator, .{ .key = scheme_key });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return .{
|
|
157
|
+
.bundle_id = metadata.id,
|
|
158
|
+
.name = metadata.name,
|
|
159
|
+
.display_name = metadata.displayName(),
|
|
160
|
+
.version = metadata.version,
|
|
161
|
+
.executable = executable,
|
|
162
|
+
.icon = if (has_icon) "app-icon.ico" else "",
|
|
163
|
+
.registry = try registry.toOwnedSlice(allocator),
|
|
164
|
+
.registry_cleanup = try cleanup.toOwnedSlice(allocator),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
test "an app's associations and URL schemes register through the installed launcher" {
|
|
169
|
+
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
170
|
+
defer arena_state.deinit();
|
|
171
|
+
const arena = arena_state.allocator();
|
|
172
|
+
const associations = [_]manifest_tool.FileAssociationMetadata{.{ .name = "Markdown", .extensions = &.{"md"} }};
|
|
173
|
+
const schemes = [_]manifest_tool.UrlSchemeMetadata{.{ .scheme = "acme-notes" }};
|
|
174
|
+
const metadata: manifest_tool.Metadata = .{
|
|
175
|
+
.id = "dev.example.notes",
|
|
176
|
+
.name = "notes",
|
|
177
|
+
.display_name = "Acme Notes",
|
|
178
|
+
.version = "1.2.3",
|
|
179
|
+
.file_associations = &associations,
|
|
180
|
+
.url_schemes = &schemes,
|
|
181
|
+
};
|
|
182
|
+
const config = try installConfig(arena, metadata, "notes.exe", true);
|
|
183
|
+
try install_config.validate(config);
|
|
184
|
+
try std.testing.expectEqualStrings("Acme Notes", config.display_name);
|
|
185
|
+
try std.testing.expectEqualStrings("app-icon.ico", config.icon);
|
|
186
|
+
try std.testing.expectEqualStrings("Software\\Classes\\.md", config.registry[0].key);
|
|
187
|
+
try std.testing.expectEqualStrings("dev.example.notes.Markdown", config.registry[0].value);
|
|
188
|
+
try std.testing.expectEqualStrings("\"{launcher}\" \"%1\"", config.registry[4].value);
|
|
189
|
+
try std.testing.expectEqualStrings("Software\\Classes\\acme-notes\\shell\\open\\command", config.registry[7].key);
|
|
190
|
+
// The extension's default value goes only while it still names this
|
|
191
|
+
// app; the app's own ProgID and scheme keys go whole.
|
|
192
|
+
try std.testing.expectEqualStrings("dev.example.notes.Markdown", config.registry_cleanup[0].if_value.?);
|
|
193
|
+
try std.testing.expect(config.registry_cleanup[1].name == null);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
test "setup.exe carries the launcher, its configuration and the install image" {
|
|
197
|
+
const allocator = std.testing.allocator;
|
|
198
|
+
const io = std.testing.io;
|
|
199
|
+
var cwd = std.Io.Dir.cwd();
|
|
200
|
+
const root = ".zig-cache/test-windows-setup";
|
|
201
|
+
try cwd.deleteTree(io, root);
|
|
202
|
+
defer cwd.deleteTree(io, root) catch {};
|
|
203
|
+
try cwd.createDirPath(io, root ++ "/artifact/bin");
|
|
204
|
+
try cwd.createDirPath(io, root ++ "/artifact/install");
|
|
205
|
+
try cwd.writeFile(io, .{ .sub_path = root ++ "/artifact/bin/notes.exe", .data = "app" });
|
|
206
|
+
try cwd.writeFile(io, .{ .sub_path = root ++ "/artifact/install/register-file-types.ps1", .data = "script" });
|
|
207
|
+
try cwd.writeFile(io, .{ .sub_path = root ++ "/artifact/app-icon.ico", .data = "icon" });
|
|
208
|
+
|
|
209
|
+
const metadata: manifest_tool.Metadata = .{ .id = "dev.example.notes", .name = "notes", .version = "1.2.3" };
|
|
210
|
+
try createSetup(allocator, io, .{ .artifact_dir = root ++ "/artifact", .metadata = metadata, .executable = "notes.exe", .arch = .x86_64, .output_path = root ++ "/notes-setup.exe" });
|
|
211
|
+
|
|
212
|
+
const setup = try cwd.readFileAlloc(io, root ++ "/notes-setup.exe", allocator, .unlimited);
|
|
213
|
+
defer allocator.free(setup);
|
|
214
|
+
try std.testing.expect(std.mem.startsWith(u8, setup, launcher_x86_64));
|
|
215
|
+
const trailer = setup_payload.find(setup[setup.len - setup_payload.trailer_len ..], setup.len).?;
|
|
216
|
+
try std.testing.expectEqual(@as(u64, launcher_x86_64.len), trailer.config_offset);
|
|
217
|
+
const config_bytes = setup[@intCast(trailer.config_offset)..][0..@intCast(trailer.config_size)];
|
|
218
|
+
const zip_bytes = setup[@intCast(trailer.zip_offset)..][0..@intCast(trailer.zip_size)];
|
|
219
|
+
try std.testing.expectEqualSlices(u8, &trailer.sha256, &setup_payload.digest(config_bytes, zip_bytes));
|
|
220
|
+
var parsed = try install_config.parse(allocator, config_bytes);
|
|
221
|
+
defer parsed.deinit();
|
|
222
|
+
try std.testing.expectEqualStrings("notes", parsed.value.name);
|
|
223
|
+
|
|
224
|
+
try cwd.writeFile(io, .{ .sub_path = root ++ "/image.zip", .data = zip_bytes });
|
|
225
|
+
try cwd.createDirPath(io, root ++ "/extracted");
|
|
226
|
+
var extracted = try cwd.openDir(io, root ++ "/extracted", .{});
|
|
227
|
+
defer extracted.close(io);
|
|
228
|
+
var archive = try cwd.openFile(io, root ++ "/image.zip", .{});
|
|
229
|
+
defer archive.close(io);
|
|
230
|
+
var buffer: [4096]u8 = undefined;
|
|
231
|
+
var reader = archive.reader(io, &buffer);
|
|
232
|
+
try std.zip.extract(extracted, &reader, .{});
|
|
233
|
+
const launcher = try extracted.readFileAlloc(io, launcher_file_in_image, allocator, .unlimited);
|
|
234
|
+
defer allocator.free(launcher);
|
|
235
|
+
try std.testing.expectEqualSlices(u8, launcher_x86_64, launcher);
|
|
236
|
+
try std.testing.expectError(error.FileNotFound, extracted.access(io, "install/register-file-types.ps1", .{}));
|
|
237
|
+
try extracted.access(io, "bin/notes.exe", .{});
|
|
238
|
+
try extracted.access(io, install_config.file_name, .{});
|
|
239
|
+
|
|
240
|
+
// The updater downloads the very image the setup program carries.
|
|
241
|
+
try createUpdateArchive(allocator, io, .{ .artifact_dir = root ++ "/artifact", .metadata = metadata, .executable = "notes.exe", .arch = .x86_64, .output_path = root ++ "/notes-update.zip" });
|
|
242
|
+
const update_archive = try cwd.readFileAlloc(io, root ++ "/notes-update.zip", allocator, .unlimited);
|
|
243
|
+
defer allocator.free(update_archive);
|
|
244
|
+
try std.testing.expectEqualSlices(u8, zip_bytes, update_archive);
|
|
245
|
+
}
|