zig-mobile-runner 0.1.0
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/CHANGELOG.md +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/schemas/README.md +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- package/viewer/styles.css +585 -0
package/src/ios_shim.zig
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const selectors = @import("selector.zig");
|
|
3
|
+
const trace = @import("trace.zig");
|
|
4
|
+
const types = @import("types.zig");
|
|
5
|
+
|
|
6
|
+
pub const CommandKind = enum {
|
|
7
|
+
snapshot,
|
|
8
|
+
screenshot,
|
|
9
|
+
tap,
|
|
10
|
+
type_text,
|
|
11
|
+
erase_text,
|
|
12
|
+
hide_keyboard,
|
|
13
|
+
swipe,
|
|
14
|
+
press_back,
|
|
15
|
+
app_state,
|
|
16
|
+
settle,
|
|
17
|
+
accept_system_alert,
|
|
18
|
+
query,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
pub const Command = struct {
|
|
22
|
+
kind: CommandKind,
|
|
23
|
+
selector: ?[]const u8 = null,
|
|
24
|
+
text: ?[]const u8 = null,
|
|
25
|
+
x: ?i32 = null,
|
|
26
|
+
y: ?i32 = null,
|
|
27
|
+
x1: ?i32 = null,
|
|
28
|
+
y1: ?i32 = null,
|
|
29
|
+
x2: ?i32 = null,
|
|
30
|
+
y2: ?i32 = null,
|
|
31
|
+
duration_ms: ?u32 = null,
|
|
32
|
+
max_chars: ?u32 = null,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
pub fn writeCommandJson(writer: anytype, command: Command) !void {
|
|
36
|
+
try writer.writeAll("{\"cmd\":");
|
|
37
|
+
try trace.writeJsonString(writer, commandName(command.kind));
|
|
38
|
+
if (command.selector) |selector| {
|
|
39
|
+
try writer.writeAll(",\"selector\":");
|
|
40
|
+
try trace.writeJsonString(writer, selector);
|
|
41
|
+
}
|
|
42
|
+
if (command.text) |text| {
|
|
43
|
+
try writer.writeAll(",\"text\":");
|
|
44
|
+
try trace.writeJsonString(writer, text);
|
|
45
|
+
}
|
|
46
|
+
if (command.x) |value| try writer.print(",\"x\":{d}", .{value});
|
|
47
|
+
if (command.y) |value| try writer.print(",\"y\":{d}", .{value});
|
|
48
|
+
if (command.x1) |value| try writer.print(",\"x1\":{d}", .{value});
|
|
49
|
+
if (command.y1) |value| try writer.print(",\"y1\":{d}", .{value});
|
|
50
|
+
if (command.x2) |value| try writer.print(",\"x2\":{d}", .{value});
|
|
51
|
+
if (command.y2) |value| try writer.print(",\"y2\":{d}", .{value});
|
|
52
|
+
if (command.duration_ms) |value| try writer.print(",\"durationMs\":{d}", .{value});
|
|
53
|
+
if (command.max_chars) |value| try writer.print(",\"maxChars\":{d}", .{value});
|
|
54
|
+
try writer.writeAll("}\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub fn parseSnapshotNodes(allocator: std.mem.Allocator, content: []const u8) ![]types.UiNode {
|
|
58
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
59
|
+
defer parsed.deinit();
|
|
60
|
+
if (parsed.value != .object) return error.IosShimResponseMustBeObject;
|
|
61
|
+
const status = fieldString(parsed.value.object, "status") orelse return error.IosShimMissingStatus;
|
|
62
|
+
if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
|
|
63
|
+
const nodes_value = parsed.value.object.get("nodes") orelse return error.IosShimMissingNodes;
|
|
64
|
+
if (nodes_value != .array) return error.IosShimNodesMustBeArray;
|
|
65
|
+
|
|
66
|
+
var nodes = std.ArrayList(types.UiNode).empty;
|
|
67
|
+
errdefer {
|
|
68
|
+
for (nodes.items) |*node| node.deinit(allocator);
|
|
69
|
+
nodes.deinit(allocator);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (nodes_value.array.items) |node_value| {
|
|
73
|
+
if (node_value != .object) return error.IosShimNodeMustBeObject;
|
|
74
|
+
const object = node_value.object;
|
|
75
|
+
const stable_id_source = fieldString(object, "id") orelse return error.IosShimNodeMissingId;
|
|
76
|
+
const class_name_source = fieldString(object, "type") orelse "XCUIElementTypeOther";
|
|
77
|
+
const bounds_value = object.get("bounds") orelse return error.IosShimNodeMissingBounds;
|
|
78
|
+
if (bounds_value != .object) return error.IosShimBoundsMustBeObject;
|
|
79
|
+
|
|
80
|
+
const text = nonEmptyFieldString(object, "label") orelse nonEmptyFieldString(object, "value");
|
|
81
|
+
const content_desc = fieldString(object, "identifier");
|
|
82
|
+
try nodes.append(allocator, .{
|
|
83
|
+
.stable_id = try allocator.dupe(u8, stable_id_source),
|
|
84
|
+
.class_name = try allocator.dupe(u8, class_name_source),
|
|
85
|
+
.text = try dupeOptional(allocator, text),
|
|
86
|
+
.content_desc = try dupeOptional(allocator, content_desc),
|
|
87
|
+
.resource_id = try dupeOptional(allocator, content_desc),
|
|
88
|
+
.bounds = .{
|
|
89
|
+
.x = try intField(bounds_value.object, "x"),
|
|
90
|
+
.y = try intField(bounds_value.object, "y"),
|
|
91
|
+
.width = try intField(bounds_value.object, "width"),
|
|
92
|
+
.height = try intField(bounds_value.object, "height"),
|
|
93
|
+
},
|
|
94
|
+
.enabled = boolField(object, "enabled", true),
|
|
95
|
+
.visible = boolField(object, "visible", true),
|
|
96
|
+
.selected = boolField(object, "selected", false),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return try nodes.toOwnedSlice(allocator);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub fn parseOkResponse(content: []const u8) !void {
|
|
104
|
+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
105
|
+
defer arena.deinit();
|
|
106
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), content, .{});
|
|
107
|
+
if (parsed.value != .object) return error.IosShimResponseMustBeObject;
|
|
108
|
+
const status = fieldString(parsed.value.object, "status") orelse return error.IosShimMissingStatus;
|
|
109
|
+
if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub fn parseQueryResponse(content: []const u8) !bool {
|
|
113
|
+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
114
|
+
defer arena.deinit();
|
|
115
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), content, .{});
|
|
116
|
+
if (parsed.value != .object) return error.IosShimResponseMustBeObject;
|
|
117
|
+
const status = fieldString(parsed.value.object, "status") orelse return error.IosShimMissingStatus;
|
|
118
|
+
if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
|
|
119
|
+
const exists = parsed.value.object.get("exists") orelse return error.IosShimMissingExists;
|
|
120
|
+
if (exists != .bool) return error.IosShimExistsMustBeBool;
|
|
121
|
+
return exists.bool;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pub fn parseAppStateRunning(content: []const u8) !bool {
|
|
125
|
+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
126
|
+
defer arena.deinit();
|
|
127
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), content, .{});
|
|
128
|
+
if (parsed.value != .object) return error.IosShimResponseMustBeObject;
|
|
129
|
+
const status = fieldString(parsed.value.object, "status") orelse return error.IosShimMissingStatus;
|
|
130
|
+
if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
|
|
131
|
+
const state = parsed.value.object.get("state") orelse return error.IosShimMissingState;
|
|
132
|
+
return switch (state) {
|
|
133
|
+
.integer => |value| value >= 3,
|
|
134
|
+
.string => |value| std.mem.eql(u8, value, "running") or
|
|
135
|
+
std.mem.eql(u8, value, "runningForeground") or
|
|
136
|
+
std.mem.eql(u8, value, "runningBackground"),
|
|
137
|
+
else => error.IosShimStateMustBeIntegerOrString,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
pub fn parseScreenshotPng(allocator: std.mem.Allocator, content: []const u8) ![]u8 {
|
|
142
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
143
|
+
defer parsed.deinit();
|
|
144
|
+
if (parsed.value != .object) return error.IosShimResponseMustBeObject;
|
|
145
|
+
const status = fieldString(parsed.value.object, "status") orelse return error.IosShimMissingStatus;
|
|
146
|
+
if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
|
|
147
|
+
const format = fieldString(parsed.value.object, "format") orelse return error.IosShimMissingScreenshotFormat;
|
|
148
|
+
if (!std.mem.eql(u8, format, "png")) return error.IosShimUnsupportedScreenshotFormat;
|
|
149
|
+
const encoded = fieldString(parsed.value.object, "base64") orelse return error.IosShimMissingScreenshotData;
|
|
150
|
+
const size = try std.base64.standard.Decoder.calcSizeForSlice(encoded);
|
|
151
|
+
const bytes = try allocator.alloc(u8, size);
|
|
152
|
+
errdefer allocator.free(bytes);
|
|
153
|
+
try std.base64.standard.Decoder.decode(bytes, encoded);
|
|
154
|
+
return bytes;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
pub fn selectorString(allocator: std.mem.Allocator, wanted: selectors.Selector) !?[]u8 {
|
|
158
|
+
var count: usize = 0;
|
|
159
|
+
var prefix: []const u8 = "";
|
|
160
|
+
var value: []const u8 = "";
|
|
161
|
+
|
|
162
|
+
if (wanted.id) |actual| {
|
|
163
|
+
count += 1;
|
|
164
|
+
prefix = "resourceId=";
|
|
165
|
+
value = actual;
|
|
166
|
+
}
|
|
167
|
+
if (wanted.text) |actual| {
|
|
168
|
+
count += 1;
|
|
169
|
+
prefix = "text=";
|
|
170
|
+
value = actual;
|
|
171
|
+
}
|
|
172
|
+
if (wanted.text_contains) |actual| {
|
|
173
|
+
count += 1;
|
|
174
|
+
prefix = "textContains=";
|
|
175
|
+
value = actual;
|
|
176
|
+
}
|
|
177
|
+
if (wanted.content_desc) |actual| {
|
|
178
|
+
count += 1;
|
|
179
|
+
prefix = "identifier=";
|
|
180
|
+
value = actual;
|
|
181
|
+
}
|
|
182
|
+
if (wanted.content_desc_contains) |actual| {
|
|
183
|
+
count += 1;
|
|
184
|
+
prefix = "identifierContains=";
|
|
185
|
+
value = actual;
|
|
186
|
+
}
|
|
187
|
+
if (wanted.class_name) |actual| {
|
|
188
|
+
count += 1;
|
|
189
|
+
prefix = "type=";
|
|
190
|
+
value = actual;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (count != 1) return null;
|
|
194
|
+
return try std.fmt.allocPrint(allocator, "{s}{s}", .{ prefix, value });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fn commandName(kind: CommandKind) []const u8 {
|
|
198
|
+
return switch (kind) {
|
|
199
|
+
.snapshot => "snapshot",
|
|
200
|
+
.screenshot => "screenshot",
|
|
201
|
+
.tap => "tap",
|
|
202
|
+
.type_text => "type",
|
|
203
|
+
.erase_text => "eraseText",
|
|
204
|
+
.hide_keyboard => "hideKeyboard",
|
|
205
|
+
.swipe => "swipe",
|
|
206
|
+
.press_back => "pressBack",
|
|
207
|
+
.app_state => "appState",
|
|
208
|
+
.settle => "settle",
|
|
209
|
+
.accept_system_alert => "acceptSystemAlert",
|
|
210
|
+
.query => "query",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fn fieldString(object: std.json.ObjectMap, key: []const u8) ?[]const u8 {
|
|
215
|
+
const value = object.get(key) orelse return null;
|
|
216
|
+
if (value != .string) return null;
|
|
217
|
+
return value.string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn nonEmptyFieldString(object: std.json.ObjectMap, key: []const u8) ?[]const u8 {
|
|
221
|
+
const value = fieldString(object, key) orelse return null;
|
|
222
|
+
if (value.len == 0) return null;
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn boolField(object: std.json.ObjectMap, key: []const u8, default: bool) bool {
|
|
227
|
+
const value = object.get(key) orelse return default;
|
|
228
|
+
if (value != .bool) return default;
|
|
229
|
+
return value.bool;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fn intField(object: std.json.ObjectMap, key: []const u8) !i32 {
|
|
233
|
+
const value = object.get(key) orelse return error.IosShimBoundsMissingField;
|
|
234
|
+
if (value != .integer) return error.IosShimBoundsFieldMustBeInteger;
|
|
235
|
+
if (value.integer < std.math.minInt(i32) or value.integer > std.math.maxInt(i32)) return error.IosShimBoundsFieldMustBeInteger;
|
|
236
|
+
return @intCast(value.integer);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fn dupeOptional(allocator: std.mem.Allocator, value: ?[]const u8) !?[]const u8 {
|
|
240
|
+
if (value) |actual| return try allocator.dupe(u8, actual);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const types = @import("types.zig");
|
|
3
|
+
|
|
4
|
+
pub fn parsePngViewport(bytes: []const u8) ?types.Viewport {
|
|
5
|
+
const signature = "\x89PNG\r\n\x1a\n";
|
|
6
|
+
if (bytes.len < 24 or !std.mem.eql(u8, bytes[0..8], signature)) return null;
|
|
7
|
+
if (!std.mem.eql(u8, bytes[12..16], "IHDR")) return null;
|
|
8
|
+
|
|
9
|
+
return .{
|
|
10
|
+
.width = readBigEndianU32(bytes[16..20]),
|
|
11
|
+
.height = readBigEndianU32(bytes[20..24]),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn readBigEndianU32(bytes: []const u8) u32 {
|
|
16
|
+
return (@as(u32, bytes[0]) << 24) |
|
|
17
|
+
(@as(u32, bytes[1]) << 16) |
|
|
18
|
+
(@as(u32, bytes[2]) << 8) |
|
|
19
|
+
@as(u32, bytes[3]);
|
|
20
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
pub fn field(params: ?std.json.Value, key: []const u8) ?std.json.Value {
|
|
4
|
+
const value = params orelse return null;
|
|
5
|
+
if (value != .object) return null;
|
|
6
|
+
return value.object.get(key);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
pub fn requiredString(params: ?std.json.Value, key: []const u8, missing_error: anyerror, type_error: anyerror) ![]const u8 {
|
|
10
|
+
const value = field(params, key) orelse return missing_error;
|
|
11
|
+
return stringValue(value, type_error);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub fn requiredStringFromObject(object: std.json.ObjectMap, key: []const u8, missing_error: anyerror, type_error: anyerror) ![]const u8 {
|
|
15
|
+
const value = object.get(key) orelse return missing_error;
|
|
16
|
+
return stringValue(value, type_error);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub fn optionalStringFromObject(object: std.json.ObjectMap, key: []const u8, type_error: anyerror) !?[]const u8 {
|
|
20
|
+
const value = object.get(key) orelse return null;
|
|
21
|
+
return try stringValue(value, type_error);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub fn requiredI32(params: ?std.json.Value, key: []const u8, missing_error: anyerror, type_error: anyerror) !i32 {
|
|
25
|
+
const value = field(params, key) orelse return missing_error;
|
|
26
|
+
return i32Value(value, type_error);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub fn requiredI32FromObject(object: std.json.ObjectMap, key: []const u8, missing_error: anyerror, type_error: anyerror) !i32 {
|
|
30
|
+
const value = object.get(key) orelse return missing_error;
|
|
31
|
+
return i32Value(value, type_error);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub fn optionalU64(params: ?std.json.Value, key: []const u8, default_value: u64, type_error: anyerror) !u64 {
|
|
35
|
+
const value = field(params, key) orelse return default_value;
|
|
36
|
+
return u64Value(value, type_error);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub fn optionalU64FromObject(object: std.json.ObjectMap, key: []const u8, default_value: u64, type_error: anyerror) !u64 {
|
|
40
|
+
const value = object.get(key) orelse return default_value;
|
|
41
|
+
return u64Value(value, type_error);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn optionalBool(params: ?std.json.Value, key: []const u8, default_value: bool, type_error: anyerror) !bool {
|
|
45
|
+
const value = field(params, key) orelse return default_value;
|
|
46
|
+
return boolValue(value, type_error);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn optionalBoolFromObject(object: std.json.ObjectMap, key: []const u8, default_value: bool, type_error: anyerror) !bool {
|
|
50
|
+
const value = object.get(key) orelse return default_value;
|
|
51
|
+
return boolValue(value, type_error);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn stringValue(value: std.json.Value, type_error: anyerror) ![]const u8 {
|
|
55
|
+
return switch (value) {
|
|
56
|
+
.string => |actual| actual,
|
|
57
|
+
else => type_error,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn i32Value(value: std.json.Value, type_error: anyerror) !i32 {
|
|
62
|
+
return switch (value) {
|
|
63
|
+
.integer => |actual| @as(i32, @intCast(actual)),
|
|
64
|
+
else => type_error,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn u64Value(value: std.json.Value, type_error: anyerror) !u64 {
|
|
69
|
+
return switch (value) {
|
|
70
|
+
.integer => |actual| @as(u64, @intCast(actual)),
|
|
71
|
+
else => type_error,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn boolValue(value: std.json.Value, type_error: anyerror) !bool {
|
|
76
|
+
return switch (value) {
|
|
77
|
+
.bool => |actual| actual,
|
|
78
|
+
else => type_error,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/json_rpc.zig
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const errors = @import("errors.zig");
|
|
3
|
+
const methods = @import("json_rpc_methods.zig");
|
|
4
|
+
const protocol = @import("json_rpc_protocol.zig");
|
|
5
|
+
const trace = @import("trace.zig");
|
|
6
|
+
|
|
7
|
+
pub const ServeOptions = struct {
|
|
8
|
+
transport: []const u8 = "stdio",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
pub fn serveStdio(allocator: std.mem.Allocator, device: anytype) !void {
|
|
12
|
+
try serveStdioWithTrace(allocator, device, null);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
pub fn serveStdioWithTrace(allocator: std.mem.Allocator, device: anytype, live_trace: ?*trace.TraceWriter) !void {
|
|
16
|
+
var stdin = std.fs.File.stdin().deprecatedReader();
|
|
17
|
+
const stdout = std.fs.File.stdout().deprecatedWriter();
|
|
18
|
+
|
|
19
|
+
while (true) {
|
|
20
|
+
const line = stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 16 * 1024 * 1024) catch |err| {
|
|
21
|
+
try protocol.writeError(stdout, null, -32700, @errorName(err));
|
|
22
|
+
continue;
|
|
23
|
+
};
|
|
24
|
+
const owned_line = line orelse break;
|
|
25
|
+
defer allocator.free(owned_line);
|
|
26
|
+
const trimmed = std.mem.trim(u8, owned_line, " \t\r\n");
|
|
27
|
+
if (trimmed.len == 0) continue;
|
|
28
|
+
try dispatchLineWithTrace(allocator, device, trimmed, stdout, live_trace);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub fn serveTcp(allocator: std.mem.Allocator, device: anytype, port: u16) !void {
|
|
33
|
+
try serveTcpWithTrace(allocator, device, port, null);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn serveTcpWithTrace(allocator: std.mem.Allocator, device: anytype, port: u16, live_trace: ?*trace.TraceWriter) !void {
|
|
37
|
+
const address = try std.net.Address.parseIp("127.0.0.1", port);
|
|
38
|
+
var server = try address.listen(.{ .reuse_address = true });
|
|
39
|
+
defer server.deinit();
|
|
40
|
+
|
|
41
|
+
while (true) {
|
|
42
|
+
var connection = try server.accept();
|
|
43
|
+
defer connection.stream.close();
|
|
44
|
+
try serveTcpConnection(allocator, device, connection.stream, live_trace);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn serveTcpConnection(allocator: std.mem.Allocator, device: anytype, stream: std.net.Stream, live_trace: ?*trace.TraceWriter) !void {
|
|
49
|
+
var write_buffer: [8192]u8 = undefined;
|
|
50
|
+
var stream_writer = stream.writer(&write_buffer);
|
|
51
|
+
const writer = &stream_writer.interface;
|
|
52
|
+
|
|
53
|
+
var line = std.ArrayList(u8).empty;
|
|
54
|
+
defer line.deinit(allocator);
|
|
55
|
+
|
|
56
|
+
var read_buffer: [4096]u8 = undefined;
|
|
57
|
+
while (true) {
|
|
58
|
+
const n = try stream.read(&read_buffer);
|
|
59
|
+
if (n == 0) break;
|
|
60
|
+
for (read_buffer[0..n]) |ch| {
|
|
61
|
+
if (ch == '\n') {
|
|
62
|
+
const trimmed = std.mem.trim(u8, line.items, " \t\r\n");
|
|
63
|
+
if (trimmed.len != 0) {
|
|
64
|
+
try dispatchLineWithTrace(allocator, device, trimmed, writer, live_trace);
|
|
65
|
+
try writer.flush();
|
|
66
|
+
}
|
|
67
|
+
line.clearRetainingCapacity();
|
|
68
|
+
} else {
|
|
69
|
+
try line.append(allocator, ch);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (line.items.len != 0) {
|
|
75
|
+
const trimmed = std.mem.trim(u8, line.items, " \t\r\n");
|
|
76
|
+
if (trimmed.len != 0) {
|
|
77
|
+
try dispatchLineWithTrace(allocator, device, trimmed, writer, live_trace);
|
|
78
|
+
try writer.flush();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub fn dispatchLine(allocator: std.mem.Allocator, device: anytype, line: []const u8, writer: anytype) !void {
|
|
84
|
+
try dispatchLineWithTrace(allocator, device, line, writer, null);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub fn dispatchLineWithTrace(
|
|
88
|
+
allocator: std.mem.Allocator,
|
|
89
|
+
device: anytype,
|
|
90
|
+
line: []const u8,
|
|
91
|
+
writer: anytype,
|
|
92
|
+
live_trace: ?*trace.TraceWriter,
|
|
93
|
+
) !void {
|
|
94
|
+
const parsed = std.json.parseFromSlice(std.json.Value, allocator, line, .{}) catch |err| {
|
|
95
|
+
try protocol.writeError(writer, null, -32700, @errorName(err));
|
|
96
|
+
return;
|
|
97
|
+
};
|
|
98
|
+
defer parsed.deinit();
|
|
99
|
+
|
|
100
|
+
if (parsed.value != .object) {
|
|
101
|
+
try protocol.writeError(writer, null, -32600, "request must be an object");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const object = parsed.value.object;
|
|
105
|
+
const id = object.get("id");
|
|
106
|
+
const method_value = object.get("method") orelse {
|
|
107
|
+
try protocol.writeError(writer, id, -32600, "missing method");
|
|
108
|
+
return;
|
|
109
|
+
};
|
|
110
|
+
if (method_value != .string) {
|
|
111
|
+
try protocol.writeError(writer, id, -32600, "method must be a string");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const params = object.get("params");
|
|
115
|
+
|
|
116
|
+
if (live_trace) |tw| try recordRpcEvent(tw, "rpc.request", method_value.string, id);
|
|
117
|
+
methods.dispatchMethod(allocator, device, method_value.string, params, id, writer, live_trace) catch |err| {
|
|
118
|
+
if (live_trace) |tw| try recordRpcErrorEvent(tw, method_value.string, id, err);
|
|
119
|
+
const classified = errors.classify(err);
|
|
120
|
+
try protocol.writeErrorWithPublicCode(writer, id, -32000, @errorName(err), classified.code);
|
|
121
|
+
return;
|
|
122
|
+
};
|
|
123
|
+
if (live_trace) |tw| try recordRpcEvent(tw, "rpc.response", method_value.string, id);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn recordRpcEvent(tw: *trace.TraceWriter, kind: []const u8, method: []const u8, id: ?std.json.Value) !void {
|
|
127
|
+
var payload = std.ArrayList(u8).empty;
|
|
128
|
+
defer payload.deinit(tw.allocator);
|
|
129
|
+
const writer = payload.writer(tw.allocator);
|
|
130
|
+
try writer.writeAll("{\"method\":");
|
|
131
|
+
try trace.writeJsonString(writer, method);
|
|
132
|
+
try writer.writeAll(",\"id\":");
|
|
133
|
+
try protocol.writeId(writer, id);
|
|
134
|
+
try writer.writeAll("}");
|
|
135
|
+
try tw.recordEvent(kind, payload.items);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn recordRpcErrorEvent(tw: *trace.TraceWriter, method: []const u8, id: ?std.json.Value, err: anyerror) !void {
|
|
139
|
+
var payload = std.ArrayList(u8).empty;
|
|
140
|
+
defer payload.deinit(tw.allocator);
|
|
141
|
+
const writer = payload.writer(tw.allocator);
|
|
142
|
+
try writer.writeAll("{\"method\":");
|
|
143
|
+
try trace.writeJsonString(writer, method);
|
|
144
|
+
try writer.writeAll(",\"id\":");
|
|
145
|
+
try protocol.writeId(writer, id);
|
|
146
|
+
try writer.writeAll(",\"error\":");
|
|
147
|
+
try trace.writeJsonString(writer, @errorName(err));
|
|
148
|
+
try writer.writeAll("}");
|
|
149
|
+
try tw.recordEvent("rpc.error", payload.items);
|
|
150
|
+
}
|