@zogien/native-sdk 0.11.0-fork.3 → 0.11.0-fork.4

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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/build/app.zig +3 -0
  3. package/build/cli.zig +22 -0
  4. package/package.json +10 -10
  5. package/packages/core/package-lock.json +2 -2
  6. package/packages/core/package.json +1 -1
  7. package/schemas/app.schema.json +9 -1
  8. package/skill-data/ts-core/SKILL.md +2 -0
  9. package/src/app_runner/app_assets.zig +85 -1
  10. package/src/app_runner/root.zig +63 -3
  11. package/src/app_runner/ts_core_main.zig +8 -0
  12. package/src/platform/macos/appkit_host.h +9 -0
  13. package/src/platform/macos/appkit_host.m +366 -0
  14. package/src/platform/macos/root.zig +29 -6
  15. package/src/platform/root.zig +1 -0
  16. package/src/platform/types.zig +9 -0
  17. package/src/platform/windows/root.zig +37 -0
  18. package/src/platform/windows/webview2_host.cpp +371 -26
  19. package/src/primitives/app_manifest/root.zig +1 -0
  20. package/src/primitives/app_manifest/tests.zig +35 -0
  21. package/src/primitives/app_manifest/types.zig +12 -0
  22. package/src/primitives/app_manifest/validation.zig +44 -6
  23. package/src/root.zig +1 -0
  24. package/src/runtime/channel_sources.zig +49 -1
  25. package/src/runtime/effects.zig +12 -1
  26. package/src/runtime/flow.zig +6 -0
  27. package/src/runtime/flow_reference.zig +6 -0
  28. package/src/runtime/root.zig +4 -0
  29. package/src/runtime/single_instance.zig +893 -0
  30. package/src/tooling/manifest.zig +111 -3
  31. package/src/tooling/package.zig +98 -8
  32. package/src/tooling/raw_manifest.zig +7 -0
  33. package/src/tooling/root.zig +2 -0
  34. package/src/tooling/templates.zig +44 -2
  35. package/src/tooling/update.zig +228 -10
  36. package/src/tooling/windows_installer.zig +245 -0
  37. package/src/tooling/zip_writer.zig +211 -0
  38. package/src/updater/engine.zig +936 -0
  39. package/src/updater/feed.zig +261 -12
  40. package/src/updater/macos.zig +187 -0
  41. package/src/updater/root.zig +41 -0
  42. package/src/updater/windows.zig +470 -0
  43. package/src/windows_install/config.zig +247 -0
  44. package/src/windows_install/main.zig +690 -0
  45. package/src/windows_install/payload.zig +146 -0
  46. package/src/windows_install/switch.zig +184 -0
  47. package/src/windows_install/tests.zig +11 -0
  48. package/src/windows_install/win32.zig +248 -0
  49. package/tools/native-sdk/main.zig +10 -9
@@ -0,0 +1,470 @@
1
+ //! The Windows half of the updater (engine.zig) for apps installed by
2
+ //! the SDK's per-user setup program (src/windows_install):
3
+ //!
4
+ //! <root>\<name>.exe the launcher
5
+ //! <root>\app\ the running version (this executable is app\bin\<name>.exe)
6
+ //! <root>\app.next\ where a downloaded release is unpacked and staged
7
+ //!
8
+ //! Fetches and downloads go through WinHTTP with the user's proxy
9
+ //! settings. A staged release must carry this app's bundle id and
10
+ //! executable, the version its signed feed named, and an executable for
11
+ //! this architecture; the launcher moves it into place at the next
12
+ //! launch, or right after this process exits when the app asks to
13
+ //! restart.
14
+
15
+ const std = @import("std");
16
+ const builtin = @import("builtin");
17
+ const engine = @import("engine.zig");
18
+ const feed = @import("feed.zig");
19
+ const install_config = @import("../windows_install/config.zig");
20
+ const install_switch = @import("../windows_install/switch.zig");
21
+ const win = @import("../windows_install/win32.zig");
22
+
23
+ const download_name = ".native-update.zip";
24
+ const launcher_in_image = "native-launcher.exe";
25
+
26
+ const Layout = struct {
27
+ root_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined,
28
+ root_len: usize = 0,
29
+ name_buffer: [install_config.max_text_bytes]u8 = undefined,
30
+ name_len: usize = 0,
31
+ executable_buffer: [install_config.max_text_bytes]u8 = undefined,
32
+ executable_len: usize = 0,
33
+ bundle_buffer: [128]u8 = undefined,
34
+ bundle_len: usize = 0,
35
+ unsupported: ?engine.Unsupported = .not_installed,
36
+
37
+ fn root(self: *const Layout) []const u8 {
38
+ return self.root_buffer[0..self.root_len];
39
+ }
40
+ fn name(self: *const Layout) []const u8 {
41
+ return self.name_buffer[0..self.name_len];
42
+ }
43
+ fn executable(self: *const Layout) []const u8 {
44
+ return self.executable_buffer[0..self.executable_len];
45
+ }
46
+ fn bundleId(self: *const Layout) []const u8 {
47
+ return self.bundle_buffer[0..self.bundle_len];
48
+ }
49
+ };
50
+
51
+ var layout: Layout = .{};
52
+
53
+ /// Finds the installation this executable runs from and returns the
54
+ /// backend. An executable outside an SDK installation (a portable copy,
55
+ /// a development build) reports `not_installed`.
56
+ pub fn backend(bundle_id: []const u8) engine.Backend {
57
+ layout = .{};
58
+ detect(bundle_id) catch {};
59
+ return .{
60
+ .context = &layout,
61
+ .eligibility = eligibility,
62
+ .staged_version = stagedVersion,
63
+ .take_last_update = takeLastUpdate,
64
+ .fetch = fetch,
65
+ .stage = stage,
66
+ .discard = discard,
67
+ .prepare_restart = prepareRestart,
68
+ };
69
+ }
70
+
71
+ fn detect(bundle_id: []const u8) !void {
72
+ var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
73
+ defer threaded.deinit();
74
+ const io = threaded.io();
75
+ var executable_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
76
+ const executable_path = executable_buffer[0..try std.process.executablePath(io, &executable_buffer)];
77
+ const bin_dir = std.fs.path.dirname(executable_path) orelse return;
78
+ const app_dir = std.fs.path.dirname(bin_dir) orelse return;
79
+ const root = std.fs.path.dirname(app_dir) orelse return;
80
+ if (!std.ascii.eqlIgnoreCase(std.fs.path.basename(bin_dir), "bin")) return;
81
+ if (!std.ascii.eqlIgnoreCase(std.fs.path.basename(app_dir), install_switch.current)) return;
82
+
83
+ var root_dir = try std.Io.Dir.cwd().openDir(io, root, .{});
84
+ defer root_dir.close(io);
85
+ var config_buffer: [install_config.max_config_bytes]u8 = undefined;
86
+ const config_bytes = try root_dir.readFile(io, install_switch.current ++ "\\" ++ install_config.file_name, &config_buffer);
87
+ var parsed = try install_config.parse(std.heap.page_allocator, config_bytes);
88
+ defer parsed.deinit();
89
+ const config = parsed.value;
90
+ if (!std.mem.eql(u8, config.bundle_id, bundle_id)) return;
91
+ if (!std.ascii.eqlIgnoreCase(config.executable, std.fs.path.basename(executable_path))) return;
92
+ var launcher_buffer: [install_config.max_text_bytes + 8]u8 = undefined;
93
+ const launcher = try std.fmt.bufPrint(&launcher_buffer, "{s}.exe", .{config.name});
94
+ root_dir.access(io, launcher, .{}) catch return;
95
+
96
+ layout.root_len = copyInto(&layout.root_buffer, root);
97
+ layout.name_len = copyInto(&layout.name_buffer, config.name);
98
+ layout.executable_len = copyInto(&layout.executable_buffer, config.executable);
99
+ layout.bundle_len = copyInto(&layout.bundle_buffer, config.bundle_id);
100
+ layout.unsupported = null;
101
+
102
+ // Writing beside app\ is how a release gets staged.
103
+ root_dir.writeFile(io, .{ .sub_path = ".native-write-probe", .data = "" }) catch {
104
+ layout.unsupported = .read_only;
105
+ return;
106
+ };
107
+ root_dir.deleteFile(io, ".native-write-probe") catch {};
108
+ refreshLauncher(io, root_dir, launcher);
109
+ }
110
+
111
+ /// A release can ship a newer launcher than the one setup installed.
112
+ /// The launcher exits as soon as it has started the app, so the app
113
+ /// replaces it here when the copies differ; a launcher still exiting
114
+ /// keeps its place until the next launch.
115
+ fn refreshLauncher(io: std.Io, root_dir: std.Io.Dir, launcher: []const u8) void {
116
+ const bundled = root_dir.readFileAlloc(io, install_switch.current ++ "\\" ++ launcher_in_image, std.heap.page_allocator, .limited(16 * 1024 * 1024)) catch return;
117
+ defer std.heap.page_allocator.free(bundled);
118
+ const installed = root_dir.readFileAlloc(io, launcher, std.heap.page_allocator, .limited(16 * 1024 * 1024)) catch return;
119
+ defer std.heap.page_allocator.free(installed);
120
+ if (std.mem.eql(u8, bundled, installed)) return;
121
+ var temp_buffer: [install_config.max_text_bytes + 16]u8 = undefined;
122
+ const temp = std.fmt.bufPrint(&temp_buffer, "{s}.new", .{launcher}) catch return;
123
+ root_dir.writeFile(io, .{ .sub_path = temp, .data = bundled }) catch return;
124
+ root_dir.rename(temp, root_dir, launcher, io) catch {
125
+ root_dir.deleteFile(io, temp) catch {};
126
+ };
127
+ }
128
+
129
+ fn eligibility(context: *anyopaque) ?engine.Unsupported {
130
+ const self: *Layout = @ptrCast(@alignCast(context));
131
+ return self.unsupported;
132
+ }
133
+
134
+ fn stagedVersion(context: *anyopaque, output: []u8) ?[]const u8 {
135
+ const self: *Layout = @ptrCast(@alignCast(context));
136
+ if (self.unsupported != null) return null;
137
+ var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
138
+ defer threaded.deinit();
139
+ const io = threaded.io();
140
+ var root_dir = std.Io.Dir.cwd().openDir(io, self.root(), .{}) catch return null;
141
+ defer root_dir.close(io);
142
+ root_dir.access(io, install_switch.staged ++ "\\" ++ install_switch.staged_marker, .{}) catch return null;
143
+ var config_buffer: [install_config.max_config_bytes]u8 = undefined;
144
+ const config_bytes = root_dir.readFile(io, install_switch.staged ++ "\\" ++ install_config.file_name, &config_buffer) catch return null;
145
+ var parsed = install_config.parse(std.heap.page_allocator, config_bytes) catch return null;
146
+ defer parsed.deinit();
147
+ if (!std.mem.eql(u8, parsed.value.bundle_id, self.bundleId())) return null;
148
+ return output[0..copyInto(output, parsed.value.version)];
149
+ }
150
+
151
+ fn takeLastUpdate(context: *anyopaque, output: []u8) ?[]const u8 {
152
+ const self: *Layout = @ptrCast(@alignCast(context));
153
+ if (self.root_len == 0) return null;
154
+ var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
155
+ defer threaded.deinit();
156
+ const io = threaded.io();
157
+ var root_dir = std.Io.Dir.cwd().openDir(io, self.root(), .{}) catch return null;
158
+ defer root_dir.close(io);
159
+ var buffer: [256]u8 = undefined;
160
+ const text = root_dir.readFile(io, install_switch.update_result, &buffer) catch return null;
161
+ root_dir.deleteFile(io, install_switch.update_result) catch {};
162
+ const line = std.mem.trim(u8, text, " \r\n");
163
+ const prefix = "updated ";
164
+ if (!std.mem.startsWith(u8, line, prefix)) return null;
165
+ return output[0..copyInto(output, line[prefix.len..])];
166
+ }
167
+
168
+ fn discard(context: *anyopaque) void {
169
+ const self: *Layout = @ptrCast(@alignCast(context));
170
+ var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
171
+ defer threaded.deinit();
172
+ const io = threaded.io();
173
+ var root_dir = std.Io.Dir.cwd().openDir(io, self.root(), .{}) catch return;
174
+ defer root_dir.close(io);
175
+ install_switch.clearStaged(root_dir, io) catch {};
176
+ }
177
+
178
+ fn prepareRestart(context: *anyopaque) bool {
179
+ const self: *Layout = @ptrCast(@alignCast(context));
180
+ var launcher_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
181
+ const launcher = std.fmt.bufPrint(&launcher_buffer, "{s}\\{s}.exe", .{ self.root(), self.name() }) catch return false;
182
+ var arguments_buffer: [64]u8 = undefined;
183
+ const arguments = std.fmt.bufPrint(&arguments_buffer, "--apply-after {d}", .{win.GetCurrentProcessId()}) catch return false;
184
+ return startDetached(launcher, arguments);
185
+ }
186
+
187
+ fn startDetached(executable: []const u8, arguments: []const u8) bool {
188
+ var command_buffer: [std.Io.Dir.max_path_bytes + 128]u16 = undefined;
189
+ var command: std.ArrayList(u16) = .initBuffer(&command_buffer);
190
+ var executable_buffer: [std.Io.Dir.max_path_bytes]u16 = undefined;
191
+ const executable_len = std.unicode.wtf8ToWtf16Le(&executable_buffer, executable) catch return false;
192
+ if (executable_len >= executable_buffer.len) return false;
193
+ executable_buffer[executable_len] = 0;
194
+ command.appendBounded('"') catch return false;
195
+ command.appendSliceBounded(executable_buffer[0..executable_len]) catch return false;
196
+ command.appendSliceBounded(&.{ '"', ' ' }) catch return false;
197
+ var arguments_buffer: [128]u16 = undefined;
198
+ const arguments_len = std.unicode.wtf8ToWtf16Le(&arguments_buffer, arguments) catch return false;
199
+ command.appendSliceBounded(arguments_buffer[0..arguments_len]) catch return false;
200
+ command.appendBounded(0) catch return false;
201
+ var startup: win.STARTUPINFOW = .{};
202
+ var process: win.PROCESS_INFORMATION = undefined;
203
+ const command_line: [*:0]u16 = @ptrCast(command.items.ptr);
204
+ if (win.CreateProcessW(@ptrCast(&executable_buffer), command_line, null, null, 0, win.CREATE_UNICODE_ENVIRONMENT, null, null, &startup, &process) == 0) return false;
205
+ _ = win.CloseHandle(process.hThread);
206
+ _ = win.CloseHandle(process.hProcess);
207
+ return true;
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // HTTP
212
+
213
+ fn fetch(context: *anyopaque, url: []const u8, policy: feed.UrlPolicy, output: []u8) engine.FetchError![]const u8 {
214
+ _ = context;
215
+ var sink: BufferSink = .{ .buffer = output };
216
+ get(url, policy, sink.writer(), null) catch |err| return switch (err) {
217
+ error.HttpStatus => error.HttpStatus,
218
+ error.TooLarge => error.TooLarge,
219
+ else => error.Network,
220
+ };
221
+ return output[0..sink.len];
222
+ }
223
+
224
+ fn stage(context: *anyopaque, release: *const engine.Release, policy: feed.UrlPolicy, progress: *engine.Progress) engine.StageError!void {
225
+ const self: *Layout = @ptrCast(@alignCast(context));
226
+ var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
227
+ defer threaded.deinit();
228
+ const io = threaded.io();
229
+ var root_dir = std.Io.Dir.cwd().openDir(io, self.root(), .{}) catch return error.Disk;
230
+ defer root_dir.close(io);
231
+ defer root_dir.deleteFile(io, download_name) catch {};
232
+
233
+ {
234
+ var file = root_dir.createFile(io, download_name, .{ .truncate = true }) catch return error.Disk;
235
+ defer file.close(io);
236
+ var buffer: [64 * 1024]u8 = undefined;
237
+ var writer = file.writer(io, &buffer);
238
+ var sink: FileSink = .{ .file_writer = &writer, .limit = release.archive_bytes, .progress = progress };
239
+ get(release.url(), policy, sink.writer(), progress) catch |err| return switch (err) {
240
+ error.HttpStatus => error.HttpStatus,
241
+ error.TooLarge => error.ArchiveSize,
242
+ error.Cancelled => error.Cancelled,
243
+ error.Disk => error.Disk,
244
+ else => error.Network,
245
+ };
246
+ writer.interface.flush() catch return error.Disk;
247
+ if (sink.received != release.archive_bytes) return error.ArchiveSize;
248
+ var digest: [32]u8 = undefined;
249
+ sink.hasher.final(&digest);
250
+ const actual = std.fmt.bytesToHex(digest, .lower);
251
+ if (!std.crypto.timing_safe.eql([64]u8, actual, release.sha256)) return error.ArchiveDigest;
252
+ }
253
+
254
+ install_switch.clearStaged(root_dir, io) catch return error.Disk;
255
+ root_dir.createDirPath(io, install_switch.staged) catch return error.Disk;
256
+ {
257
+ var staged = root_dir.openDir(io, install_switch.staged, .{}) catch return error.Disk;
258
+ defer staged.close(io);
259
+ var archive = root_dir.openFile(io, download_name, .{}) catch return error.Disk;
260
+ defer archive.close(io);
261
+ var buffer: [64 * 1024]u8 = undefined;
262
+ var reader = archive.reader(io, &buffer);
263
+ std.zip.extract(staged, &reader, .{}) catch {
264
+ install_switch.clearStaged(root_dir, io) catch {};
265
+ return error.ArchiveInvalid;
266
+ };
267
+ }
268
+ validateStaged(io, root_dir, self, release) catch {
269
+ install_switch.clearStaged(root_dir, io) catch {};
270
+ return error.ArchiveInvalid;
271
+ };
272
+ install_switch.markStaged(root_dir, io) catch return error.Disk;
273
+ }
274
+
275
+ /// A staged release must be this app, at the version the signed feed
276
+ /// named, with its executable built for this architecture.
277
+ fn validateStaged(io: std.Io, root_dir: std.Io.Dir, self: *const Layout, release: *const engine.Release) !void {
278
+ var config_buffer: [install_config.max_config_bytes]u8 = undefined;
279
+ const config_bytes = try root_dir.readFile(io, install_switch.staged ++ "\\" ++ install_config.file_name, &config_buffer);
280
+ var parsed = try install_config.parse(std.heap.page_allocator, config_bytes);
281
+ defer parsed.deinit();
282
+ const config = parsed.value;
283
+ if (!std.mem.eql(u8, config.bundle_id, self.bundleId())) return error.WrongApp;
284
+ if (!std.mem.eql(u8, config.version, release.version())) return error.WrongVersion;
285
+ if (!std.mem.eql(u8, config.name, self.name()) or !std.ascii.eqlIgnoreCase(config.executable, self.executable())) return error.WrongExecutable;
286
+ try root_dir.access(io, install_switch.staged ++ "\\" ++ launcher_in_image, .{});
287
+ var path_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
288
+ const executable = try std.fmt.bufPrint(&path_buffer, "{s}\\bin\\{s}", .{ install_switch.staged, config.executable });
289
+ var header: [4096]u8 = undefined;
290
+ const header_len = (try root_dir.readFile(io, executable, &header)).len;
291
+ if (peMachine(header[0..header_len]) != expected_machine) return error.WrongArchitecture;
292
+ }
293
+
294
+ const expected_machine: u16 = switch (builtin.cpu.arch) {
295
+ .aarch64 => 0xaa64,
296
+ else => 0x8664,
297
+ };
298
+
299
+ pub fn peMachine(header: []const u8) ?u16 {
300
+ if (header.len < 0x40 or header[0] != 'M' or header[1] != 'Z') return null;
301
+ const pe_offset: usize = std.mem.readInt(u32, header[0x3c..0x40], .little);
302
+ if (pe_offset + 6 > header.len or !std.mem.eql(u8, header[pe_offset..][0..4], "PE\x00\x00")) return null;
303
+ return std.mem.readInt(u16, header[pe_offset + 4 ..][0..2], .little);
304
+ }
305
+
306
+ const Sink = struct {
307
+ context: *anyopaque,
308
+ write: *const fn (context: *anyopaque, bytes: []const u8) GetError!void,
309
+ };
310
+
311
+ const BufferSink = struct {
312
+ buffer: []u8,
313
+ len: usize = 0,
314
+
315
+ fn writer(self: *BufferSink) Sink {
316
+ return .{ .context = self, .write = write };
317
+ }
318
+ fn write(context: *anyopaque, bytes: []const u8) GetError!void {
319
+ const self: *BufferSink = @ptrCast(@alignCast(context));
320
+ if (bytes.len > self.buffer.len - self.len) return error.TooLarge;
321
+ @memcpy(self.buffer[self.len..][0..bytes.len], bytes);
322
+ self.len += bytes.len;
323
+ }
324
+ };
325
+
326
+ const FileSink = struct {
327
+ file_writer: *std.Io.File.Writer,
328
+ limit: u64,
329
+ progress: *engine.Progress,
330
+ received: u64 = 0,
331
+ hasher: std.crypto.hash.sha2.Sha256 = .init(.{}),
332
+
333
+ fn writer(self: *FileSink) Sink {
334
+ return .{ .context = self, .write = write };
335
+ }
336
+ fn write(context: *anyopaque, bytes: []const u8) GetError!void {
337
+ const self: *FileSink = @ptrCast(@alignCast(context));
338
+ if (bytes.len > self.limit - self.received) return error.TooLarge;
339
+ self.file_writer.interface.writeAll(bytes) catch return error.Disk;
340
+ self.hasher.update(bytes);
341
+ self.received += bytes.len;
342
+ self.progress.report(self.received);
343
+ }
344
+ };
345
+
346
+ const GetError = error{ Network, HttpStatus, TooLarge, Cancelled, Disk, InvalidUrl };
347
+
348
+ /// GETs `url` through WinHTTP, honoring the user's proxy settings, and
349
+ /// streams the body to `sink`. Redirects may not leave HTTPS (WinHTTP's
350
+ /// default); a loopback http:// request, allowed only for testing, may
351
+ /// be redirected anywhere, so its final URL is held to `policy`.
352
+ fn get(url: []const u8, policy: feed.UrlPolicy, sink: Sink, progress: ?*engine.Progress) GetError!void {
353
+ if (!feed.urlAllowed(url, policy)) return error.InvalidUrl;
354
+ var url_buffer: [feed.max_archive_url_bytes + 1]u16 = undefined;
355
+ const url_len = std.unicode.wtf8ToWtf16Le(&url_buffer, url) catch return error.InvalidUrl;
356
+ if (url_len >= url_buffer.len) return error.InvalidUrl;
357
+ url_buffer[url_len] = 0;
358
+ var parts: http.URL_COMPONENTS = .{ .dwHostNameLength = std.math.maxInt(u32), .dwUrlPathLength = std.math.maxInt(u32), .dwExtraInfoLength = std.math.maxInt(u32) };
359
+ if (http.WinHttpCrackUrl(@ptrCast(&url_buffer), @intCast(url_len), 0, &parts) == 0) return error.InvalidUrl;
360
+ var host_buffer: [256]u16 = undefined;
361
+ if (parts.dwHostNameLength + 1 > host_buffer.len) return error.InvalidUrl;
362
+ @memcpy(host_buffer[0..parts.dwHostNameLength], parts.lpszHostName.?[0..parts.dwHostNameLength]);
363
+ host_buffer[parts.dwHostNameLength] = 0;
364
+ var object_buffer: [feed.max_archive_url_bytes + 1]u16 = undefined;
365
+ const object_len = parts.dwUrlPathLength + parts.dwExtraInfoLength;
366
+ if (object_len + 1 > object_buffer.len) return error.InvalidUrl;
367
+ if (parts.dwUrlPathLength > 0) @memcpy(object_buffer[0..parts.dwUrlPathLength], parts.lpszUrlPath.?[0..parts.dwUrlPathLength]);
368
+ if (parts.dwExtraInfoLength > 0) @memcpy(object_buffer[parts.dwUrlPathLength..object_len], parts.lpszExtraInfo.?[0..parts.dwExtraInfoLength]);
369
+ object_buffer[object_len] = 0;
370
+
371
+ const agent = std.unicode.utf8ToUtf16LeStringLiteral("native-sdk-updater");
372
+ const session = http.WinHttpOpen(agent, http.WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, null, null, 0) orelse
373
+ http.WinHttpOpen(agent, http.WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, null, null, 0) orelse return error.Network;
374
+ defer _ = http.WinHttpCloseHandle(session);
375
+ _ = http.WinHttpSetTimeouts(session, 30_000, 30_000, 30_000, 60_000);
376
+ const connection = http.WinHttpConnect(session, @ptrCast(&host_buffer), parts.nPort, 0) orelse return error.Network;
377
+ defer _ = http.WinHttpCloseHandle(connection);
378
+ const secure = parts.nScheme == http.INTERNET_SCHEME_HTTPS;
379
+ const request = http.WinHttpOpenRequest(connection, std.unicode.utf8ToUtf16LeStringLiteral("GET"), @ptrCast(&object_buffer), null, null, null, if (secure) http.WINHTTP_FLAG_SECURE else 0) orelse return error.Network;
380
+ defer _ = http.WinHttpCloseHandle(request);
381
+ if (http.WinHttpSendRequest(request, null, 0, null, 0, 0, 0) == 0) return error.Network;
382
+ if (http.WinHttpReceiveResponse(request, null) == 0) return error.Network;
383
+
384
+ if (!secure) {
385
+ var final_buffer: [feed.max_archive_url_bytes + 1]u16 = undefined;
386
+ var final_size: u32 = @sizeOf(@TypeOf(final_buffer));
387
+ if (http.WinHttpQueryOption(request, http.WINHTTP_OPTION_URL, &final_buffer, &final_size) == 0) return error.InvalidUrl;
388
+ var final_utf8: [feed.max_archive_url_bytes * 3]u8 = undefined;
389
+ const final_units = @min(final_size / 2, final_buffer.len);
390
+ const final_len = std.unicode.wtf16LeToWtf8(&final_utf8, final_buffer[0..final_units]);
391
+ if (!feed.urlAllowed(std.mem.trimEnd(u8, final_utf8[0..final_len], &.{0}), policy)) return error.InvalidUrl;
392
+ }
393
+ var status: u32 = 0;
394
+ var status_size: u32 = @sizeOf(u32);
395
+ if (http.WinHttpQueryHeaders(request, http.WINHTTP_QUERY_STATUS_CODE | http.WINHTTP_QUERY_FLAG_NUMBER, null, &status, &status_size, null) == 0) return error.Network;
396
+ if (status != 200) return error.HttpStatus;
397
+
398
+ var chunk: [64 * 1024]u8 = undefined;
399
+ while (true) {
400
+ if (progress) |active| if (active.cancelled()) return error.Cancelled;
401
+ var read: u32 = 0;
402
+ if (http.WinHttpReadData(request, &chunk, chunk.len, &read) == 0) return error.Network;
403
+ if (read == 0) break;
404
+ try sink.write(sink.context, chunk[0..read]);
405
+ }
406
+ }
407
+
408
+ const http = struct {
409
+ const HINTERNET = *opaque {};
410
+ const BOOL = i32;
411
+
412
+ const URL_COMPONENTS = extern struct {
413
+ dwStructSize: u32 = @sizeOf(URL_COMPONENTS),
414
+ lpszScheme: ?[*]u16 = null,
415
+ dwSchemeLength: u32 = 0,
416
+ nScheme: i32 = 0,
417
+ lpszHostName: ?[*]u16 = null,
418
+ dwHostNameLength: u32 = 0,
419
+ nPort: u16 = 0,
420
+ lpszUserName: ?[*]u16 = null,
421
+ dwUserNameLength: u32 = 0,
422
+ lpszPassword: ?[*]u16 = null,
423
+ dwPasswordLength: u32 = 0,
424
+ lpszUrlPath: ?[*]u16 = null,
425
+ dwUrlPathLength: u32 = 0,
426
+ lpszExtraInfo: ?[*]u16 = null,
427
+ dwExtraInfoLength: u32 = 0,
428
+ };
429
+
430
+ const WINHTTP_ACCESS_TYPE_DEFAULT_PROXY: u32 = 0;
431
+ const WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY: u32 = 4;
432
+ const WINHTTP_FLAG_SECURE: u32 = 0x0080_0000;
433
+ const WINHTTP_QUERY_STATUS_CODE: u32 = 19;
434
+ const WINHTTP_QUERY_FLAG_NUMBER: u32 = 0x2000_0000;
435
+ const WINHTTP_OPTION_URL: u32 = 34;
436
+ const INTERNET_SCHEME_HTTPS: i32 = 2;
437
+
438
+ extern "winhttp" fn WinHttpOpen(pszAgentW: ?[*:0]const u16, dwAccessType: u32, pszProxyW: ?[*:0]const u16, pszProxyBypassW: ?[*:0]const u16, dwFlags: u32) callconv(.winapi) ?HINTERNET;
439
+ extern "winhttp" fn WinHttpCrackUrl(pwszUrl: [*:0]const u16, dwUrlLength: u32, dwFlags: u32, lpUrlComponents: *URL_COMPONENTS) callconv(.winapi) BOOL;
440
+ extern "winhttp" fn WinHttpConnect(hSession: HINTERNET, pswzServerName: [*:0]const u16, nServerPort: u16, dwReserved: u32) callconv(.winapi) ?HINTERNET;
441
+ extern "winhttp" fn WinHttpOpenRequest(hConnect: HINTERNET, pwszVerb: ?[*:0]const u16, pwszObjectName: ?[*:0]const u16, pwszVersion: ?[*:0]const u16, pwszReferrer: ?[*:0]const u16, ppwszAcceptTypes: ?*const ?[*:0]const u16, dwFlags: u32) callconv(.winapi) ?HINTERNET;
442
+ extern "winhttp" fn WinHttpSetTimeouts(hInternet: HINTERNET, nResolveTimeout: i32, nConnectTimeout: i32, nSendTimeout: i32, nReceiveTimeout: i32) callconv(.winapi) BOOL;
443
+ extern "winhttp" fn WinHttpSendRequest(hRequest: HINTERNET, lpszHeaders: ?[*]const u16, dwHeadersLength: u32, lpOptional: ?*anyopaque, dwOptionalLength: u32, dwTotalLength: u32, dwContext: usize) callconv(.winapi) BOOL;
444
+ extern "winhttp" fn WinHttpReceiveResponse(hRequest: HINTERNET, lpReserved: ?*anyopaque) callconv(.winapi) BOOL;
445
+ extern "winhttp" fn WinHttpQueryHeaders(hRequest: HINTERNET, dwInfoLevel: u32, pwszName: ?[*:0]const u16, lpBuffer: ?*anyopaque, lpdwBufferLength: *u32, lpdwIndex: ?*u32) callconv(.winapi) BOOL;
446
+ extern "winhttp" fn WinHttpQueryOption(hInternet: HINTERNET, dwOption: u32, lpBuffer: ?*anyopaque, lpdwBufferLength: *u32) callconv(.winapi) BOOL;
447
+ extern "winhttp" fn WinHttpReadData(hRequest: HINTERNET, lpBuffer: [*]u8, dwNumberOfBytesToRead: u32, lpdwNumberOfBytesRead: *u32) callconv(.winapi) BOOL;
448
+ extern "winhttp" fn WinHttpCloseHandle(hInternet: HINTERNET) callconv(.winapi) BOOL;
449
+ };
450
+
451
+ fn copyInto(buffer: []u8, value: []const u8) usize {
452
+ const length = @min(buffer.len, value.len);
453
+ @memcpy(buffer[0..length], value[0..length]);
454
+ return length;
455
+ }
456
+
457
+ test "the backend compiles for every Windows build" {
458
+ if (comptime builtin.os.tag == .windows) _ = &backend;
459
+ }
460
+
461
+ test "the PE machine field names the executable's architecture" {
462
+ var header = [_]u8{0} ** 0x100;
463
+ header[0] = 'M';
464
+ header[1] = 'Z';
465
+ std.mem.writeInt(u32, header[0x3c..0x40], 0x80, .little);
466
+ @memcpy(header[0x80..0x84], "PE\x00\x00");
467
+ std.mem.writeInt(u16, header[0x84..0x86], 0xaa64, .little);
468
+ try std.testing.expectEqual(@as(?u16, 0xaa64), peMachine(&header));
469
+ try std.testing.expectEqual(@as(?u16, null), peMachine("MZ-short"));
470
+ }