@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.
- package/README.md +1 -1
- package/build/app.zig +3 -0
- 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/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/macos/appkit_host.h +9 -0
- package/src/platform/macos/appkit_host.m +366 -0
- package/src/platform/macos/root.zig +29 -6
- package/src/platform/root.zig +1 -0
- package/src/platform/types.zig +9 -0
- package/src/platform/windows/root.zig +37 -0
- package/src/platform/windows/webview2_host.cpp +371 -26
- 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 +1 -0
- package/src/runtime/channel_sources.zig +49 -1
- package/src/runtime/effects.zig +12 -1
- package/src/runtime/flow.zig +6 -0
- package/src/runtime/flow_reference.zig +6 -0
- package/src/runtime/root.zig +4 -0
- package/src/runtime/single_instance.zig +893 -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
package/src/updater/feed.zig
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
|
+
const builtin = @import("builtin");
|
|
2
3
|
|
|
3
4
|
const Ed25519 = std.crypto.sign.Ed25519;
|
|
4
5
|
|
|
@@ -7,6 +8,59 @@ pub const max_payload_bytes: usize = 32 * 1024;
|
|
|
7
8
|
pub const max_release_notes_bytes: usize = 16 * 1024;
|
|
8
9
|
pub const max_archive_url_bytes: usize = 4096;
|
|
9
10
|
pub const max_archive_bytes: u64 = 8 * 1024 * 1024 * 1024;
|
|
11
|
+
pub const max_feed_url_bytes: usize = 4096;
|
|
12
|
+
/// A manifest trusts its `public_key` plus up to three `public_keys`, so
|
|
13
|
+
/// a release can still be verified by installs that only know the key it
|
|
14
|
+
/// is rotating away from.
|
|
15
|
+
pub const max_public_keys: usize = 4;
|
|
16
|
+
|
|
17
|
+
/// The release targets a feed may name, one per installable build.
|
|
18
|
+
pub const targets = [_][]const u8{ "macos-aarch64", "macos-x86_64", "windows-x86_64", "windows-aarch64" };
|
|
19
|
+
|
|
20
|
+
pub fn isTarget(value: []const u8) bool {
|
|
21
|
+
for (targets) |target| {
|
|
22
|
+
if (std.mem.eql(u8, value, target)) return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// The target this build installs, or null where the updater has no
|
|
28
|
+
/// release format.
|
|
29
|
+
pub fn currentTarget() ?[]const u8 {
|
|
30
|
+
return comptime switch (builtin.os.tag) {
|
|
31
|
+
.macos => switch (builtin.cpu.arch) {
|
|
32
|
+
.aarch64 => "macos-aarch64",
|
|
33
|
+
.x86_64 => "macos-x86_64",
|
|
34
|
+
else => null,
|
|
35
|
+
},
|
|
36
|
+
.windows => switch (builtin.cpu.arch) {
|
|
37
|
+
.x86_64 => "windows-x86_64",
|
|
38
|
+
.aarch64 => "windows-aarch64",
|
|
39
|
+
else => null,
|
|
40
|
+
},
|
|
41
|
+
else => null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Loosens the HTTPS rule for local testing: with `allow_loopback_http`,
|
|
46
|
+
/// http:// URLs to 127.0.0.1, [::1] or localhost pass as well. The Ed25519
|
|
47
|
+
/// signature stays the authority over what installs.
|
|
48
|
+
pub const UrlPolicy = struct {
|
|
49
|
+
allow_loopback_http: bool = false,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
pub fn urlAllowed(value: []const u8, policy: UrlPolicy) bool {
|
|
53
|
+
if (archiveUrlIsValid(value)) return true;
|
|
54
|
+
if (!policy.allow_loopback_http) return false;
|
|
55
|
+
if (value.len == 0 or value.len > max_archive_url_bytes) return false;
|
|
56
|
+
for (value) |byte| if (byte <= 0x20 or byte == 0x7f) return false;
|
|
57
|
+
const uri = std.Uri.parse(value) catch return false;
|
|
58
|
+
if (!std.mem.eql(u8, uri.scheme, "http")) return false;
|
|
59
|
+
const host = uri.host orelse return false;
|
|
60
|
+
var host_buffer: [64]u8 = undefined;
|
|
61
|
+
const host_name = host.toRaw(&host_buffer) catch return false;
|
|
62
|
+
return std.mem.eql(u8, host_name, "127.0.0.1") or std.mem.eql(u8, host_name, "[::1]") or std.mem.eql(u8, host_name, "::1") or std.ascii.eqlIgnoreCase(host_name, "localhost");
|
|
63
|
+
}
|
|
10
64
|
|
|
11
65
|
pub const Envelope = struct {
|
|
12
66
|
payload: []const u8,
|
|
@@ -34,15 +88,32 @@ pub const VerifiedRelease = struct {
|
|
|
34
88
|
}
|
|
35
89
|
};
|
|
36
90
|
|
|
91
|
+
/// Verifies a feed envelope against the app's trusted keys:
|
|
92
|
+
/// `public_keys_base64` is one base64 Ed25519 public key, or several
|
|
93
|
+
/// joined with commas (the manifest's `public_key` followed by its
|
|
94
|
+
/// `public_keys`). The feed verifies when any of them signed the payload;
|
|
95
|
+
/// a malformed key fails closed. Fields this version does not know are
|
|
96
|
+
/// ignored, in the envelope and in the signed payload, so feeds can grow
|
|
97
|
+
/// without breaking installed apps.
|
|
37
98
|
pub fn verifyEnvelope(
|
|
38
99
|
allocator: std.mem.Allocator,
|
|
39
100
|
envelope_bytes: []const u8,
|
|
40
|
-
|
|
101
|
+
public_keys_base64: []const u8,
|
|
102
|
+
) !VerifiedRelease {
|
|
103
|
+
return verifyEnvelopeWithPolicy(allocator, envelope_bytes, public_keys_base64, .{});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// `verifyEnvelope` with the release's archive URL held to `policy`.
|
|
107
|
+
pub fn verifyEnvelopeWithPolicy(
|
|
108
|
+
allocator: std.mem.Allocator,
|
|
109
|
+
envelope_bytes: []const u8,
|
|
110
|
+
public_keys_base64: []const u8,
|
|
111
|
+
policy: UrlPolicy,
|
|
41
112
|
) !VerifiedRelease {
|
|
42
113
|
if (envelope_bytes.len == 0 or envelope_bytes.len > max_envelope_bytes) return error.InvalidUpdateFeed;
|
|
43
114
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
|
44
115
|
defer arena.deinit();
|
|
45
|
-
const envelope = std.json.parseFromSliceLeaky(Envelope, arena.allocator(), envelope_bytes, .{ .ignore_unknown_fields =
|
|
116
|
+
const envelope = std.json.parseFromSliceLeaky(Envelope, arena.allocator(), envelope_bytes, .{ .ignore_unknown_fields = true }) catch return error.InvalidUpdateFeed;
|
|
46
117
|
|
|
47
118
|
const payload_size = std.base64.standard.Decoder.calcSizeForSlice(envelope.payload) catch return error.InvalidUpdateFeed;
|
|
48
119
|
if (payload_size == 0 or payload_size > max_payload_bytes) return error.InvalidUpdateFeed;
|
|
@@ -55,17 +126,18 @@ pub fn verifyEnvelope(
|
|
|
55
126
|
if (signature_size != signature_bytes.len) return error.InvalidUpdateSignature;
|
|
56
127
|
std.base64.standard.Decoder.decode(&signature_bytes, envelope.signature) catch return error.InvalidUpdateSignature;
|
|
57
128
|
|
|
58
|
-
var
|
|
59
|
-
const
|
|
60
|
-
if (public_key_size != public_key_bytes.len) return error.InvalidUpdateKey;
|
|
61
|
-
std.base64.standard.Decoder.decode(&public_key_bytes, public_key_base64) catch return error.InvalidUpdateKey;
|
|
62
|
-
const public_key = Ed25519.PublicKey.fromBytes(public_key_bytes) catch return error.InvalidUpdateKey;
|
|
129
|
+
var public_keys: [max_public_keys]Ed25519.PublicKey = undefined;
|
|
130
|
+
const public_key_count = try parsePublicKeys(public_keys_base64, &public_keys);
|
|
63
131
|
const signature = Ed25519.Signature.fromBytes(signature_bytes);
|
|
64
|
-
|
|
132
|
+
const signed = for (public_keys[0..public_key_count]) |public_key| {
|
|
133
|
+
signature.verifyStrict(payload, public_key) catch continue;
|
|
134
|
+
break true;
|
|
135
|
+
} else false;
|
|
136
|
+
if (!signed) return error.InvalidUpdateSignature;
|
|
65
137
|
|
|
66
|
-
var parsed = std.json.parseFromSlice(Release, allocator, payload, .{ .ignore_unknown_fields =
|
|
138
|
+
var parsed = std.json.parseFromSlice(Release, allocator, payload, .{ .ignore_unknown_fields = true }) catch return error.InvalidUpdatePayload;
|
|
67
139
|
errdefer parsed.deinit();
|
|
68
|
-
try
|
|
140
|
+
try validateReleaseWithPolicy(parsed.value, policy);
|
|
69
141
|
return .{
|
|
70
142
|
.payload = payload,
|
|
71
143
|
.release = parsed.value,
|
|
@@ -73,11 +145,79 @@ pub fn verifyEnvelope(
|
|
|
73
145
|
};
|
|
74
146
|
}
|
|
75
147
|
|
|
148
|
+
/// Decodes a comma-separated list of base64 Ed25519 public keys.
|
|
149
|
+
pub fn parsePublicKeys(list: []const u8, output: *[max_public_keys]Ed25519.PublicKey) !usize {
|
|
150
|
+
if (list.len == 0) return error.InvalidUpdateKey;
|
|
151
|
+
var count: usize = 0;
|
|
152
|
+
var parts = std.mem.splitScalar(u8, list, ',');
|
|
153
|
+
while (parts.next()) |part| {
|
|
154
|
+
if (count == output.len) return error.InvalidUpdateKey;
|
|
155
|
+
var key_bytes: [Ed25519.PublicKey.encoded_length]u8 = undefined;
|
|
156
|
+
const key_size = std.base64.standard.Decoder.calcSizeForSlice(part) catch return error.InvalidUpdateKey;
|
|
157
|
+
if (key_size != key_bytes.len) return error.InvalidUpdateKey;
|
|
158
|
+
std.base64.standard.Decoder.decode(&key_bytes, part) catch return error.InvalidUpdateKey;
|
|
159
|
+
output[count] = Ed25519.PublicKey.fromBytes(key_bytes) catch return error.InvalidUpdateKey;
|
|
160
|
+
count += 1;
|
|
161
|
+
}
|
|
162
|
+
return count;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Expands a manifest feed_url for one installed app: `{target}` becomes
|
|
166
|
+
/// the release target the build installs (for example `macos-aarch64`)
|
|
167
|
+
/// and `{current_version}` its version, so one manifest serves a feed
|
|
168
|
+
/// per target, or lets a feed server answer per version. Placeholders
|
|
169
|
+
/// belong to the path and query; any other brace, or one in the scheme
|
|
170
|
+
/// or host, is invalid.
|
|
171
|
+
pub fn expandFeedUrl(output: []u8, template: []const u8, target: []const u8, current_version: []const u8) ![]const u8 {
|
|
172
|
+
return expandFeedUrlWithPolicy(output, template, target, current_version, .{});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// `expandFeedUrl` with the result held to `policy`.
|
|
176
|
+
pub fn expandFeedUrlWithPolicy(output: []u8, template: []const u8, target: []const u8, current_version: []const u8, policy: UrlPolicy) ![]const u8 {
|
|
177
|
+
const scheme_len: usize = if (std.mem.startsWith(u8, template, "https://"))
|
|
178
|
+
"https://".len
|
|
179
|
+
else if (policy.allow_loopback_http and std.mem.startsWith(u8, template, "http://"))
|
|
180
|
+
"http://".len
|
|
181
|
+
else
|
|
182
|
+
return error.InvalidUpdateFeedUrl;
|
|
183
|
+
const authority_end = std.mem.findAnyPos(u8, template, scheme_len, "/?#") orelse template.len;
|
|
184
|
+
if (std.mem.findAny(u8, template[0..authority_end], "{}") != null) return error.InvalidUpdateFeedUrl;
|
|
185
|
+
if (!isTarget(target)) return error.InvalidUpdateFeedUrl;
|
|
186
|
+
_ = try parseVersion(current_version);
|
|
187
|
+
|
|
188
|
+
var length: usize = 0;
|
|
189
|
+
var index: usize = 0;
|
|
190
|
+
while (index < template.len) {
|
|
191
|
+
var replacement: []const u8 = template[index .. index + 1];
|
|
192
|
+
var consumed: usize = 1;
|
|
193
|
+
if (template[index] == '{') {
|
|
194
|
+
if (std.mem.startsWith(u8, template[index..], "{target}")) {
|
|
195
|
+
replacement = target;
|
|
196
|
+
consumed = "{target}".len;
|
|
197
|
+
} else if (std.mem.startsWith(u8, template[index..], "{current_version}")) {
|
|
198
|
+
replacement = current_version;
|
|
199
|
+
consumed = "{current_version}".len;
|
|
200
|
+
} else return error.InvalidUpdateFeedUrl;
|
|
201
|
+
} else if (template[index] == '}') return error.InvalidUpdateFeedUrl;
|
|
202
|
+
if (replacement.len > output.len - length) return error.InvalidUpdateFeedUrl;
|
|
203
|
+
@memcpy(output[length..][0..replacement.len], replacement);
|
|
204
|
+
length += replacement.len;
|
|
205
|
+
index += consumed;
|
|
206
|
+
}
|
|
207
|
+
const expanded = output[0..length];
|
|
208
|
+
if (expanded.len > max_feed_url_bytes or !urlAllowed(expanded, policy)) return error.InvalidUpdateFeedUrl;
|
|
209
|
+
return expanded;
|
|
210
|
+
}
|
|
211
|
+
|
|
76
212
|
pub fn validateRelease(release: Release) !void {
|
|
213
|
+
return validateReleaseWithPolicy(release, .{});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pub fn validateReleaseWithPolicy(release: Release, policy: UrlPolicy) !void {
|
|
77
217
|
if (release.bundle_id.len == 0 or release.bundle_id.len > 128) return error.InvalidUpdatePayload;
|
|
78
218
|
_ = try parseVersion(release.version);
|
|
79
|
-
if (!
|
|
80
|
-
if (!
|
|
219
|
+
if (!isTarget(release.target)) return error.InvalidUpdatePayload;
|
|
220
|
+
if (!urlAllowed(release.archive_url, policy)) return error.InvalidUpdatePayload;
|
|
81
221
|
if (release.archive_bytes == 0 or release.archive_bytes > max_archive_bytes) return error.InvalidUpdatePayload;
|
|
82
222
|
if (release.sha256.len != 64) return error.InvalidUpdatePayload;
|
|
83
223
|
for (release.sha256) |byte| if (!(std.ascii.isDigit(byte) or (byte >= 'a' and byte <= 'f'))) return error.InvalidUpdatePayload;
|
|
@@ -213,3 +353,112 @@ test "archive URLs fit the runtime output buffer" {
|
|
|
213
353
|
try std.testing.expect(!archiveUrlIsValid("http://example.com/release.zip"));
|
|
214
354
|
try std.testing.expect(archiveUrlIsValid("https://example.com/releases/demo.zip?channel=stable"));
|
|
215
355
|
}
|
|
356
|
+
|
|
357
|
+
fn testEnvelopeAlloc(allocator: std.mem.Allocator, key_pair: Ed25519.KeyPair, payload: []const u8) ![]u8 {
|
|
358
|
+
const signature = try key_pair.sign(payload, null);
|
|
359
|
+
const payload_base64 = try allocator.alloc(u8, std.base64.standard.Encoder.calcSize(payload.len));
|
|
360
|
+
defer allocator.free(payload_base64);
|
|
361
|
+
var signature_base64: [std.base64.standard.Encoder.calcSize(Ed25519.Signature.encoded_length)]u8 = undefined;
|
|
362
|
+
return std.fmt.allocPrint(allocator, "{{\"payload\":\"{s}\",\"signature\":\"{s}\"}}", .{
|
|
363
|
+
std.base64.standard.Encoder.encode(payload_base64, payload),
|
|
364
|
+
std.base64.standard.Encoder.encode(&signature_base64, &signature.toBytes()),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn testPublicKey(key_pair: Ed25519.KeyPair, buffer: *[std.base64.standard.Encoder.calcSize(Ed25519.PublicKey.encoded_length)]u8) []const u8 {
|
|
369
|
+
return std.base64.standard.Encoder.encode(buffer, &key_pair.public_key.toBytes());
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const test_payload =
|
|
373
|
+
\\{"bundle_id":"com.example.demo","version":"1.2.3","target":"macos-aarch64","archive_url":"https://example.com/demo.zip","archive_bytes":42,"sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
|
|
374
|
+
;
|
|
375
|
+
|
|
376
|
+
test "a feed verifies when any trusted key signed it" {
|
|
377
|
+
const allocator = std.testing.allocator;
|
|
378
|
+
const old_key = try Ed25519.KeyPair.generateDeterministic([_]u8{0x11} ** Ed25519.KeyPair.seed_length);
|
|
379
|
+
const new_key = try Ed25519.KeyPair.generateDeterministic([_]u8{0x22} ** Ed25519.KeyPair.seed_length);
|
|
380
|
+
var old_buffer: [44]u8 = undefined;
|
|
381
|
+
var new_buffer: [44]u8 = undefined;
|
|
382
|
+
const old_public = testPublicKey(old_key, &old_buffer);
|
|
383
|
+
const new_public = testPublicKey(new_key, &new_buffer);
|
|
384
|
+
const envelope = try testEnvelopeAlloc(allocator, old_key, test_payload);
|
|
385
|
+
defer allocator.free(envelope);
|
|
386
|
+
|
|
387
|
+
const both = try std.fmt.allocPrint(allocator, "{s},{s}", .{ new_public, old_public });
|
|
388
|
+
defer allocator.free(both);
|
|
389
|
+
var verified = try verifyEnvelope(allocator, envelope, both);
|
|
390
|
+
verified.deinit(allocator);
|
|
391
|
+
try std.testing.expectError(error.InvalidUpdateSignature, verifyEnvelope(allocator, envelope, new_public));
|
|
392
|
+
|
|
393
|
+
const malformed = try std.fmt.allocPrint(allocator, "{s},not-a-key", .{old_public});
|
|
394
|
+
defer allocator.free(malformed);
|
|
395
|
+
try std.testing.expectError(error.InvalidUpdateKey, verifyEnvelope(allocator, envelope, malformed));
|
|
396
|
+
const trailing = try std.fmt.allocPrint(allocator, "{s},", .{old_public});
|
|
397
|
+
defer allocator.free(trailing);
|
|
398
|
+
try std.testing.expectError(error.InvalidUpdateKey, verifyEnvelope(allocator, envelope, trailing));
|
|
399
|
+
const too_many = try std.fmt.allocPrint(allocator, "{s},{s},{s},{s},{s}", .{ new_public, new_public, new_public, new_public, old_public });
|
|
400
|
+
defer allocator.free(too_many);
|
|
401
|
+
try std.testing.expectError(error.InvalidUpdateKey, verifyEnvelope(allocator, envelope, too_many));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
test "fields a newer feed adds are ignored" {
|
|
405
|
+
const allocator = std.testing.allocator;
|
|
406
|
+
const key_pair = try Ed25519.KeyPair.generateDeterministic([_]u8{0x33} ** Ed25519.KeyPair.seed_length);
|
|
407
|
+
var key_buffer: [44]u8 = undefined;
|
|
408
|
+
const payload =
|
|
409
|
+
\\{"bundle_id":"com.example.demo","version":"1.2.3","target":"macos-x86_64","archive_url":"https://example.com/demo.zip","archive_bytes":42,"sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","channel":"beta"}
|
|
410
|
+
;
|
|
411
|
+
const envelope = try testEnvelopeAlloc(allocator, key_pair, payload);
|
|
412
|
+
defer allocator.free(envelope);
|
|
413
|
+
const extended = try std.fmt.allocPrint(allocator, "{s},\"format\":2}}", .{envelope[0 .. envelope.len - 1]});
|
|
414
|
+
defer allocator.free(extended);
|
|
415
|
+
var verified = try verifyEnvelope(allocator, extended, testPublicKey(key_pair, &key_buffer));
|
|
416
|
+
defer verified.deinit(allocator);
|
|
417
|
+
try std.testing.expectEqualStrings("macos-x86_64", verified.release.target);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
test "feed URLs expand the target and installed version" {
|
|
421
|
+
var buffer: [max_feed_url_bytes]u8 = undefined;
|
|
422
|
+
try std.testing.expectEqualStrings(
|
|
423
|
+
"https://example.com/releases/macos-aarch64/native-update.json?from=1.2.3",
|
|
424
|
+
try expandFeedUrl(&buffer, "https://example.com/releases/{target}/native-update.json?from={current_version}", "macos-aarch64", "1.2.3"),
|
|
425
|
+
);
|
|
426
|
+
try std.testing.expectEqualStrings(
|
|
427
|
+
"https://example.com/native-update.json",
|
|
428
|
+
try expandFeedUrl(&buffer, "https://example.com/native-update.json", "macos-x86_64", "1.0.0"),
|
|
429
|
+
);
|
|
430
|
+
try std.testing.expectError(error.InvalidUpdateFeedUrl, expandFeedUrl(&buffer, "https://example.com/{arch}/feed.json", "macos-aarch64", "1.2.3"));
|
|
431
|
+
try std.testing.expectError(error.InvalidUpdateFeedUrl, expandFeedUrl(&buffer, "https://example.com/{target/feed.json", "macos-aarch64", "1.2.3"));
|
|
432
|
+
try std.testing.expectError(error.InvalidUpdateFeedUrl, expandFeedUrl(&buffer, "https://example.com/target}/feed.json", "macos-aarch64", "1.2.3"));
|
|
433
|
+
try std.testing.expectError(error.InvalidUpdateFeedUrl, expandFeedUrl(&buffer, "https://{target}.example.com/feed.json", "macos-aarch64", "1.2.3"));
|
|
434
|
+
try std.testing.expectError(error.InvalidUpdateFeedUrl, expandFeedUrl(&buffer, "http://example.com/{target}/feed.json", "macos-aarch64", "1.2.3"));
|
|
435
|
+
try std.testing.expectError(error.InvalidUpdateFeedUrl, expandFeedUrl(&buffer, "https://example.com/{target}/feed.json", "linux-x86_64", "1.2.3"));
|
|
436
|
+
try std.testing.expectError(error.InvalidVersion, expandFeedUrl(&buffer, "https://example.com/{target}/feed.json", "macos-aarch64", "1.02.3"));
|
|
437
|
+
var small: [24]u8 = undefined;
|
|
438
|
+
try std.testing.expectError(error.InvalidUpdateFeedUrl, expandFeedUrl(&small, "https://example.com/{target}/feed.json", "macos-aarch64", "1.2.3"));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
test "only builds with a release format have an update target" {
|
|
442
|
+
const target = currentTarget();
|
|
443
|
+
if ((builtin.os.tag == .macos or builtin.os.tag == .windows) and (builtin.cpu.arch == .aarch64 or builtin.cpu.arch == .x86_64)) {
|
|
444
|
+
try std.testing.expect(isTarget(target.?));
|
|
445
|
+
} else {
|
|
446
|
+
try std.testing.expect(target == null);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
test "plain HTTP passes only to loopback hosts and only when testing asks for it" {
|
|
451
|
+
try std.testing.expect(!urlAllowed("http://127.0.0.1:8080/feed.json", .{}));
|
|
452
|
+
try std.testing.expect(urlAllowed("http://127.0.0.1:8080/feed.json", .{ .allow_loopback_http = true }));
|
|
453
|
+
try std.testing.expect(urlAllowed("http://localhost/feed.json", .{ .allow_loopback_http = true }));
|
|
454
|
+
try std.testing.expect(urlAllowed("http://[::1]:9000/feed.json", .{ .allow_loopback_http = true }));
|
|
455
|
+
try std.testing.expect(!urlAllowed("http://example.com/feed.json", .{ .allow_loopback_http = true }));
|
|
456
|
+
try std.testing.expect(!urlAllowed("http://127.0.0.1.example.com/feed.json", .{ .allow_loopback_http = true }));
|
|
457
|
+
try std.testing.expect(urlAllowed("https://example.com/feed.json", .{}));
|
|
458
|
+
var buffer: [max_feed_url_bytes]u8 = undefined;
|
|
459
|
+
try std.testing.expectEqualStrings(
|
|
460
|
+
"http://127.0.0.1:8080/windows-x86_64/feed.json",
|
|
461
|
+
try expandFeedUrlWithPolicy(&buffer, "http://127.0.0.1:8080/{target}/feed.json", "windows-x86_64", "1.0.0", .{ .allow_loopback_http = true }),
|
|
462
|
+
);
|
|
463
|
+
try std.testing.expectError(error.InvalidUpdateFeedUrl, expandFeedUrl(&buffer, "http://127.0.0.1:8080/{target}/feed.json", "windows-x86_64", "1.0.0"));
|
|
464
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//! The macOS half of the updater (engine.zig) for apps whose manifest sets
|
|
2
|
+
//! `updates.mode` to `app`. The AppKit host does the Cocoa work
|
|
3
|
+
//! (appkit_host.m, `native_sdk_macos_updates_*`): fetching through the
|
|
4
|
+
//! user's network settings, unpacking with ditto, checking the bundle and
|
|
5
|
+
//! its signing team, and the helper that swaps the bundle once this
|
|
6
|
+
//! process exits. This file checks the archive's signed size and SHA-256
|
|
7
|
+
//! and maps results onto the engine's.
|
|
8
|
+
//!
|
|
9
|
+
//! A release stages beside the app as `.<App.app>.native-update/` and
|
|
10
|
+
//! installs when the app restarts into it or quits; the version that
|
|
11
|
+
//! starts next reports `last updated`.
|
|
12
|
+
|
|
13
|
+
const std = @import("std");
|
|
14
|
+
const engine = @import("engine.zig");
|
|
15
|
+
const feed = @import("feed.zig");
|
|
16
|
+
|
|
17
|
+
extern fn native_sdk_macos_updates_eligibility() c_int;
|
|
18
|
+
extern fn native_sdk_macos_updates_staged_version(bundle_id: [*]const u8, bundle_id_len: usize, output: [*]u8, capacity: usize) usize;
|
|
19
|
+
extern fn native_sdk_macos_updates_take_last(bundle_id: [*]const u8, bundle_id_len: usize, output: [*]u8, capacity: usize) usize;
|
|
20
|
+
extern fn native_sdk_macos_updates_fetch(url: [*]const u8, url_len: usize, allow_loopback_http: c_int, output: [*]u8, capacity: usize, output_len: *usize) c_int;
|
|
21
|
+
extern fn native_sdk_macos_updates_download(url: [*]const u8, url_len: usize, allow_loopback_http: c_int, limit: u64, path_output: [*]u8, path_capacity: usize, path_len: *usize, context: ?*anyopaque, progress: *const fn (?*anyopaque, u64) callconv(.c) void, cancelled: *const fn (?*anyopaque) callconv(.c) c_int) c_int;
|
|
22
|
+
extern fn native_sdk_macos_updates_stage(archive_path: [*]const u8, archive_path_len: usize, bundle_id: [*]const u8, bundle_id_len: usize, version: [*]const u8, version_len: usize) c_int;
|
|
23
|
+
extern fn native_sdk_macos_updates_discard() void;
|
|
24
|
+
extern fn native_sdk_macos_updates_install(bundle_id: [*]const u8, bundle_id_len: usize, from_version: [*]const u8, from_version_len: usize, relaunch: c_int) c_int;
|
|
25
|
+
|
|
26
|
+
// Transfer results, as appkit_host.m numbers them.
|
|
27
|
+
const transfer_network = 1;
|
|
28
|
+
const transfer_http_status = 2;
|
|
29
|
+
const transfer_too_large = 3;
|
|
30
|
+
const transfer_cancelled = 4;
|
|
31
|
+
|
|
32
|
+
const Install = struct {
|
|
33
|
+
bundle_buffer: [128]u8 = undefined,
|
|
34
|
+
bundle_len: usize = 0,
|
|
35
|
+
version_buffer: [64]u8 = undefined,
|
|
36
|
+
version_len: usize = 0,
|
|
37
|
+
|
|
38
|
+
fn bundleId(self: *const Install) []const u8 {
|
|
39
|
+
return self.bundle_buffer[0..self.bundle_len];
|
|
40
|
+
}
|
|
41
|
+
fn version(self: *const Install) []const u8 {
|
|
42
|
+
return self.version_buffer[0..self.version_len];
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
var install: Install = .{};
|
|
47
|
+
|
|
48
|
+
/// The backend for this app; `version` is the running version, which the
|
|
49
|
+
/// install records as the one it replaced.
|
|
50
|
+
pub fn backend(bundle_id: []const u8, version: []const u8) engine.Backend {
|
|
51
|
+
install = .{};
|
|
52
|
+
install.bundle_len = copyInto(&install.bundle_buffer, bundle_id);
|
|
53
|
+
install.version_len = copyInto(&install.version_buffer, version);
|
|
54
|
+
return .{
|
|
55
|
+
.context = &install,
|
|
56
|
+
.eligibility = eligibility,
|
|
57
|
+
.staged_version = stagedVersion,
|
|
58
|
+
.take_last_update = takeLastUpdate,
|
|
59
|
+
.fetch = fetch,
|
|
60
|
+
.stage = stage,
|
|
61
|
+
.discard = discard,
|
|
62
|
+
.prepare_restart = prepareRestart,
|
|
63
|
+
.install_on_exit = installOnExit,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn get(context: *anyopaque) *Install {
|
|
68
|
+
return @ptrCast(@alignCast(context));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn eligibility(context: *anyopaque) ?engine.Unsupported {
|
|
72
|
+
_ = context;
|
|
73
|
+
return switch (native_sdk_macos_updates_eligibility()) {
|
|
74
|
+
0 => null,
|
|
75
|
+
1 => .not_installed,
|
|
76
|
+
else => .read_only,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn stagedVersion(context: *anyopaque, output: []u8) ?[]const u8 {
|
|
81
|
+
const bundle_id = get(context).bundleId();
|
|
82
|
+
const length = native_sdk_macos_updates_staged_version(bundle_id.ptr, bundle_id.len, output.ptr, output.len);
|
|
83
|
+
return if (length == 0) null else output[0..length];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn takeLastUpdate(context: *anyopaque, output: []u8) ?[]const u8 {
|
|
87
|
+
const bundle_id = get(context).bundleId();
|
|
88
|
+
const length = native_sdk_macos_updates_take_last(bundle_id.ptr, bundle_id.len, output.ptr, output.len);
|
|
89
|
+
return if (length == 0) null else output[0..length];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn fetch(context: *anyopaque, url: []const u8, policy: feed.UrlPolicy, output: []u8) engine.FetchError![]const u8 {
|
|
93
|
+
_ = context;
|
|
94
|
+
var length: usize = 0;
|
|
95
|
+
return switch (native_sdk_macos_updates_fetch(url.ptr, url.len, @intFromBool(policy.allow_loopback_http), output.ptr, output.len, &length)) {
|
|
96
|
+
0 => output[0..length],
|
|
97
|
+
transfer_http_status => error.HttpStatus,
|
|
98
|
+
transfer_too_large => error.TooLarge,
|
|
99
|
+
else => error.Network,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fn stage(context: *anyopaque, release: *const engine.Release, policy: feed.UrlPolicy, progress: *engine.Progress) engine.StageError!void {
|
|
104
|
+
const self = get(context);
|
|
105
|
+
const url = release.url();
|
|
106
|
+
var path_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
|
|
107
|
+
var path_len: usize = 0;
|
|
108
|
+
switch (native_sdk_macos_updates_download(url.ptr, url.len, @intFromBool(policy.allow_loopback_http), release.archive_bytes, &path_buffer, path_buffer.len, &path_len, progress, reportProgress, isCancelled)) {
|
|
109
|
+
0 => {},
|
|
110
|
+
transfer_network => return error.Network,
|
|
111
|
+
transfer_http_status => return error.HttpStatus,
|
|
112
|
+
transfer_too_large => return error.ArchiveSize,
|
|
113
|
+
transfer_cancelled => return error.Cancelled,
|
|
114
|
+
else => return error.Disk,
|
|
115
|
+
}
|
|
116
|
+
const path = path_buffer[0..path_len];
|
|
117
|
+
|
|
118
|
+
var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
|
|
119
|
+
defer threaded.deinit();
|
|
120
|
+
const io = threaded.io();
|
|
121
|
+
defer std.Io.Dir.cwd().deleteFile(io, path) catch {};
|
|
122
|
+
try verifyArchive(io, path, release);
|
|
123
|
+
|
|
124
|
+
const version = release.version();
|
|
125
|
+
const bundle_id = self.bundleId();
|
|
126
|
+
return switch (native_sdk_macos_updates_stage(path.ptr, path.len, bundle_id.ptr, bundle_id.len, version.ptr, version.len)) {
|
|
127
|
+
0 => {},
|
|
128
|
+
1 => error.ArchiveInvalid,
|
|
129
|
+
else => error.Disk,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// The archive must be exactly the size and SHA-256 the signed feed named.
|
|
134
|
+
fn verifyArchive(io: std.Io, path: []const u8, release: *const engine.Release) engine.StageError!void {
|
|
135
|
+
var file = std.Io.Dir.cwd().openFile(io, path, .{}) catch return error.Disk;
|
|
136
|
+
defer file.close(io);
|
|
137
|
+
const size = (file.stat(io) catch return error.Disk).size;
|
|
138
|
+
if (size != release.archive_bytes) return error.ArchiveSize;
|
|
139
|
+
var buffer: [64 * 1024]u8 = undefined;
|
|
140
|
+
var reader = file.reader(io, &buffer);
|
|
141
|
+
var hasher = std.crypto.hash.sha2.Sha256.init(.{});
|
|
142
|
+
var chunk: [64 * 1024]u8 = undefined;
|
|
143
|
+
while (true) {
|
|
144
|
+
const count = reader.interface.readSliceShort(&chunk) catch return error.Disk;
|
|
145
|
+
if (count == 0) break;
|
|
146
|
+
hasher.update(chunk[0..count]);
|
|
147
|
+
}
|
|
148
|
+
var digest: [32]u8 = undefined;
|
|
149
|
+
hasher.final(&digest);
|
|
150
|
+
const actual = std.fmt.bytesToHex(digest, .lower);
|
|
151
|
+
if (!std.crypto.timing_safe.eql([64]u8, actual, release.sha256)) return error.ArchiveDigest;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn discard(context: *anyopaque) void {
|
|
155
|
+
_ = context;
|
|
156
|
+
native_sdk_macos_updates_discard();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn prepareRestart(context: *anyopaque) bool {
|
|
160
|
+
return startInstall(get(context), true);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn installOnExit(context: *anyopaque) void {
|
|
164
|
+
_ = startInstall(get(context), false);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn startInstall(self: *const Install, relaunch: bool) bool {
|
|
168
|
+
const bundle_id = self.bundleId();
|
|
169
|
+
const from = self.version();
|
|
170
|
+
return native_sdk_macos_updates_install(bundle_id.ptr, bundle_id.len, from.ptr, from.len, @intFromBool(relaunch)) == 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn reportProgress(context: ?*anyopaque, received: u64) callconv(.c) void {
|
|
174
|
+
const progress: *engine.Progress = @ptrCast(@alignCast(context.?));
|
|
175
|
+
progress.report(received);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn isCancelled(context: ?*anyopaque) callconv(.c) c_int {
|
|
179
|
+
const progress: *engine.Progress = @ptrCast(@alignCast(context.?));
|
|
180
|
+
return @intFromBool(progress.cancelled());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fn copyInto(buffer: []u8, value: []const u8) usize {
|
|
184
|
+
const length = @min(buffer.len, value.len);
|
|
185
|
+
@memcpy(buffer[0..length], value[0..length]);
|
|
186
|
+
return length;
|
|
187
|
+
}
|
package/src/updater/root.zig
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const platform_types = @import("../platform/types.zig");
|
|
3
|
+
|
|
1
4
|
pub const feed = @import("feed.zig");
|
|
2
5
|
pub const c_api = @import("c_api.zig");
|
|
6
|
+
pub const engine = @import("engine.zig");
|
|
7
|
+
/// The Windows backend for SDK-installed apps; analyzed only by Windows builds.
|
|
8
|
+
pub const windows = @import("windows.zig");
|
|
9
|
+
/// The macOS backend for app-driven updates; analyzed only by builds that
|
|
10
|
+
/// link the AppKit host.
|
|
11
|
+
pub const macos = @import("macos.zig");
|
|
12
|
+
|
|
13
|
+
/// The desktop host a runner was built for.
|
|
14
|
+
pub const Host = enum { windows, macos_appkit, other };
|
|
15
|
+
|
|
16
|
+
/// Points the app-driven updater at this installation; each runner calls
|
|
17
|
+
/// it once at startup. Windows installs, and macOS apps whose manifest sets
|
|
18
|
+
/// `updates.mode` to `app`, get a backend; elsewhere the updater reports
|
|
19
|
+
/// why it cannot update. App mode on macOS also turns the host's own update
|
|
20
|
+
/// UI off. Setting NATIVE_SDK_UPDATES_FEED_URL replaces the manifest's feed
|
|
21
|
+
/// URL to test an update end to end, and may then name a loopback http://
|
|
22
|
+
/// server; the manifest's Ed25519 keys stay the authority over what
|
|
23
|
+
/// installs.
|
|
24
|
+
pub fn configureApp(comptime host: Host, app_info: *platform_types.AppInfo, environ_map: *std.process.Environ.Map) void {
|
|
25
|
+
const override = environ_map.get("NATIVE_SDK_UPDATES_FEED_URL");
|
|
26
|
+
const app_mode = app_info.update_mode == .app;
|
|
27
|
+
const backend: ?engine.Backend = switch (host) {
|
|
28
|
+
.windows => windows.backend(app_info.bundle_id),
|
|
29
|
+
.macos_appkit => if (app_mode) macos.backend(app_info.bundle_id, app_info.version) else null,
|
|
30
|
+
.other => null,
|
|
31
|
+
};
|
|
32
|
+
engine.configure(.{
|
|
33
|
+
.feed_url = override orelse app_info.update_feed_url,
|
|
34
|
+
.public_keys = app_info.update_public_key,
|
|
35
|
+
.bundle_id = app_info.bundle_id,
|
|
36
|
+
.version = app_info.version,
|
|
37
|
+
.target = feed.currentTarget(),
|
|
38
|
+
.policy = .{ .allow_loopback_http = override != null },
|
|
39
|
+
.backend = backend,
|
|
40
|
+
.check_on_start = app_info.update_check_on_start,
|
|
41
|
+
});
|
|
42
|
+
if (host == .macos_appkit and app_mode) app_info.update_feed_url = "";
|
|
43
|
+
}
|
|
3
44
|
|
|
4
45
|
pub const Envelope = feed.Envelope;
|
|
5
46
|
pub const Release = feed.Release;
|