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
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
pub const Options = struct {
|
|
4
|
+
redact: bool = false,
|
|
5
|
+
omit_screenshots: bool = false,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
pub fn redactEntry(allocator: std.mem.Allocator, archive_path: []const u8, bytes: []const u8, options: Options) ![]u8 {
|
|
9
|
+
if (std.mem.eql(u8, archive_path, "trace.json")) {
|
|
10
|
+
return try redactTraceManifest(allocator, bytes, options);
|
|
11
|
+
}
|
|
12
|
+
if (std.mem.endsWith(u8, archive_path, ".json") or std.mem.eql(u8, archive_path, "events.jsonl")) {
|
|
13
|
+
return redactJsonishText(allocator, bytes);
|
|
14
|
+
}
|
|
15
|
+
if (isTextPath(archive_path)) {
|
|
16
|
+
return redactFreeText(allocator, bytes);
|
|
17
|
+
}
|
|
18
|
+
return try allocator.dupe(u8, bytes);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn redactTraceManifest(allocator: std.mem.Allocator, bytes: []const u8, options: Options) ![]u8 {
|
|
22
|
+
var parsed = try std.json.parseFromSlice(std.json.Value, allocator, bytes, .{});
|
|
23
|
+
defer parsed.deinit();
|
|
24
|
+
if (parsed.value != .object) return try redactJsonishText(allocator, bytes);
|
|
25
|
+
|
|
26
|
+
const arena_allocator = parsed.arena.allocator();
|
|
27
|
+
var redaction = std.json.ObjectMap.init(arena_allocator);
|
|
28
|
+
try redaction.put("enabled", .{ .bool = true });
|
|
29
|
+
try redaction.put("screenshots", .{ .string = if (options.omit_screenshots) "omitted" else "placeholder" });
|
|
30
|
+
try redaction.put("screenRecordings", .{ .string = "omitted" });
|
|
31
|
+
try redaction.put("textArtifacts", .{ .string = "scrubbed" });
|
|
32
|
+
try redaction.put("screenshotsOmitted", .{ .bool = options.omit_screenshots });
|
|
33
|
+
try redaction.put("screenshotsRedacted", .{ .bool = !options.omit_screenshots });
|
|
34
|
+
try redaction.put("screenRecordingsOmitted", .{ .bool = true });
|
|
35
|
+
try parsed.value.object.put("redaction", .{ .object = redaction });
|
|
36
|
+
|
|
37
|
+
var out = std.ArrayList(u8).empty;
|
|
38
|
+
errdefer out.deinit(allocator);
|
|
39
|
+
try writeJsonValueRedacted(out.writer(allocator), parsed.value, null);
|
|
40
|
+
try out.writer(allocator).writeByte('\n');
|
|
41
|
+
return try out.toOwnedSlice(allocator);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn redactJsonishText(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
|
|
45
|
+
const parsed = std.json.parseFromSlice(std.json.Value, allocator, bytes, .{}) catch {
|
|
46
|
+
return redactFreeText(allocator, bytes);
|
|
47
|
+
};
|
|
48
|
+
defer parsed.deinit();
|
|
49
|
+
var out = std.ArrayList(u8).empty;
|
|
50
|
+
errdefer out.deinit(allocator);
|
|
51
|
+
try writeJsonValueRedacted(out.writer(allocator), parsed.value, null);
|
|
52
|
+
try out.writer(allocator).writeByte('\n');
|
|
53
|
+
return try out.toOwnedSlice(allocator);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn writeJsonValueRedacted(writer: anytype, value: std.json.Value, key_context: ?[]const u8) !void {
|
|
57
|
+
switch (value) {
|
|
58
|
+
.null => try writer.writeAll("null"),
|
|
59
|
+
.bool => |actual| try writer.writeAll(if (actual) "true" else "false"),
|
|
60
|
+
.integer => |actual| try writer.print("{d}", .{actual}),
|
|
61
|
+
.float => |actual| try writer.print("{d}", .{actual}),
|
|
62
|
+
.number_string => |actual| try writer.writeAll(actual),
|
|
63
|
+
.string => |actual| try writeRedactedJsonStringForKey(writer, key_context orelse "", actual),
|
|
64
|
+
.array => |array| {
|
|
65
|
+
try writer.writeAll("[");
|
|
66
|
+
for (array.items, 0..) |item, index| {
|
|
67
|
+
if (index > 0) try writer.writeAll(",");
|
|
68
|
+
try writeJsonValueRedacted(writer, item, key_context);
|
|
69
|
+
}
|
|
70
|
+
try writer.writeAll("]");
|
|
71
|
+
},
|
|
72
|
+
.object => |object| {
|
|
73
|
+
try writer.writeAll("{");
|
|
74
|
+
var first = true;
|
|
75
|
+
var iterator = object.iterator();
|
|
76
|
+
while (iterator.next()) |entry| {
|
|
77
|
+
if (!first) try writer.writeAll(",");
|
|
78
|
+
first = false;
|
|
79
|
+
try writeJsonString(writer, entry.key_ptr.*);
|
|
80
|
+
try writer.writeAll(":");
|
|
81
|
+
try writeJsonValueRedacted(writer, entry.value_ptr.*, entry.key_ptr.*);
|
|
82
|
+
}
|
|
83
|
+
try writer.writeAll("}");
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn writeRedactedJsonStringForKey(writer: anytype, key: []const u8, value: []const u8) !void {
|
|
89
|
+
if (isSensitiveLabel(key)) {
|
|
90
|
+
try writeJsonString(writer, "[REDACTED:secret]");
|
|
91
|
+
} else if (looksLikeBearerOrToken(value)) {
|
|
92
|
+
try writeJsonString(writer, "[REDACTED:token]");
|
|
93
|
+
} else if (looksLikeEmail(value)) {
|
|
94
|
+
try writeJsonString(writer, "[REDACTED:email]");
|
|
95
|
+
} else {
|
|
96
|
+
try writeJsonString(writer, value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn redactFreeText(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
|
|
101
|
+
var out = std.ArrayList(u8).empty;
|
|
102
|
+
errdefer out.deinit(allocator);
|
|
103
|
+
var sensitive_tag = false;
|
|
104
|
+
var index: usize = 0;
|
|
105
|
+
while (index < bytes.len) {
|
|
106
|
+
if (bytes[index] == '<') {
|
|
107
|
+
sensitive_tag = tagLooksSensitive(bytes[index..@min(bytes.len, index + 240)]);
|
|
108
|
+
} else if (bytes[index] == '>') {
|
|
109
|
+
sensitive_tag = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (sensitive_tag and (startsWithAt(bytes, index, "text=\"") or startsWithAt(bytes, index, "content-desc=\""))) {
|
|
113
|
+
const prefix_len: usize = if (startsWithAt(bytes, index, "text=\"")) 6 else 14;
|
|
114
|
+
try out.writer(allocator).writeAll(bytes[index .. index + prefix_len]);
|
|
115
|
+
try out.writer(allocator).writeAll("[REDACTED:secret]");
|
|
116
|
+
index += prefix_len;
|
|
117
|
+
while (index < bytes.len and bytes[index] != '"') : (index += 1) {}
|
|
118
|
+
if (index < bytes.len) {
|
|
119
|
+
try out.writer(allocator).writeByte('"');
|
|
120
|
+
index += 1;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (emailEnd(bytes, index)) |end| {
|
|
126
|
+
try out.writer(allocator).writeAll("[REDACTED:email]");
|
|
127
|
+
index = end;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (bearerEnd(bytes, index)) |end| {
|
|
131
|
+
try out.writer(allocator).writeAll("[REDACTED:token]");
|
|
132
|
+
index = end;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
try out.writer(allocator).writeByte(bytes[index]);
|
|
136
|
+
index += 1;
|
|
137
|
+
}
|
|
138
|
+
return try out.toOwnedSlice(allocator);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn emailEnd(bytes: []const u8, start: usize) ?usize {
|
|
142
|
+
if (start > 0 and !isDelimiter(bytes[start - 1])) return null;
|
|
143
|
+
var end = start;
|
|
144
|
+
while (end < bytes.len and !isDelimiter(bytes[end])) : (end += 1) {}
|
|
145
|
+
const value = bytes[start..end];
|
|
146
|
+
return if (looksLikeEmail(value)) end else null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn bearerEnd(bytes: []const u8, start: usize) ?usize {
|
|
150
|
+
if (!startsWithIgnoreCase(bytes[start..], "bearer ")) return null;
|
|
151
|
+
var end = start;
|
|
152
|
+
while (end < bytes.len and bytes[end] != '"' and bytes[end] != '\'' and bytes[end] != '<' and bytes[end] != '>') : (end += 1) {}
|
|
153
|
+
return end;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn tagLooksSensitive(bytes: []const u8) bool {
|
|
157
|
+
const end = std.mem.indexOfScalar(u8, bytes, '>') orelse bytes.len;
|
|
158
|
+
return isSensitiveLabel(bytes[0..end]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn isScreenshotPath(path: []const u8) bool {
|
|
162
|
+
return std.mem.endsWith(u8, path, ".png") or std.mem.endsWith(u8, path, ".jpg") or std.mem.endsWith(u8, path, ".jpeg");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pub fn isPlaceholderScreenshotPath(path: []const u8) bool {
|
|
166
|
+
return std.mem.endsWith(u8, path, ".png");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
pub fn isVisualArtifactPath(path: []const u8) bool {
|
|
170
|
+
return isScreenshotPath(path) or std.mem.endsWith(u8, path, ".mp4") or std.mem.endsWith(u8, path, ".webm");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pub const redacted_screenshot_png = [_]u8{
|
|
174
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
175
|
+
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
|
176
|
+
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
|
177
|
+
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
|
|
178
|
+
0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41,
|
|
179
|
+
0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
|
|
180
|
+
0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00,
|
|
181
|
+
0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
|
|
182
|
+
0x42, 0x60, 0x82,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
fn isTextPath(path: []const u8) bool {
|
|
186
|
+
return std.mem.endsWith(u8, path, ".xml") or std.mem.endsWith(u8, path, ".html") or std.mem.endsWith(u8, path, ".txt") or std.mem.endsWith(u8, path, ".log");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn writeJsonString(writer: anytype, value: []const u8) !void {
|
|
190
|
+
try writer.writeAll("\"");
|
|
191
|
+
for (value) |ch| {
|
|
192
|
+
switch (ch) {
|
|
193
|
+
'"' => try writer.writeAll("\\\""),
|
|
194
|
+
'\\' => try writer.writeAll("\\\\"),
|
|
195
|
+
'\n' => try writer.writeAll("\\n"),
|
|
196
|
+
'\r' => try writer.writeAll("\\r"),
|
|
197
|
+
'\t' => try writer.writeAll("\\t"),
|
|
198
|
+
0...7, 11, 12, 14...31 => try writer.print("\\u{x:0>4}", .{ch}),
|
|
199
|
+
else => try writer.writeByte(ch),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
try writer.writeAll("\"");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn isSensitiveLabel(value: []const u8) bool {
|
|
206
|
+
const needles = [_][]const u8{
|
|
207
|
+
"password",
|
|
208
|
+
"token",
|
|
209
|
+
"secret",
|
|
210
|
+
"authorization",
|
|
211
|
+
"auth",
|
|
212
|
+
"cookie",
|
|
213
|
+
"apikey",
|
|
214
|
+
"api_key",
|
|
215
|
+
"bearer",
|
|
216
|
+
};
|
|
217
|
+
for (needles) |needle| {
|
|
218
|
+
if (indexOfIgnoreCase(value, needle) != null) return true;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fn looksLikeEmail(value: []const u8) bool {
|
|
224
|
+
if (value.len < 5 or value.len > 254) return false;
|
|
225
|
+
if (std.mem.indexOfAny(u8, value, " \t\r\n<>\"'") != null) return false;
|
|
226
|
+
const at = std.mem.indexOfScalar(u8, value, '@') orelse return false;
|
|
227
|
+
if (at == 0 or at + 3 >= value.len) return false;
|
|
228
|
+
return std.mem.indexOfScalar(u8, value[at + 1 ..], '.') != null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn looksLikeBearerOrToken(value: []const u8) bool {
|
|
232
|
+
if (indexOfIgnoreCase(value, "bearer ") != null) return true;
|
|
233
|
+
if (value.len < 40) return false;
|
|
234
|
+
const first_dot = std.mem.indexOfScalar(u8, value, '.') orelse return false;
|
|
235
|
+
const second_dot = std.mem.indexOfScalarPos(u8, value, first_dot + 1, '.') orelse return false;
|
|
236
|
+
return second_dot + 1 < value.len;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fn indexOfIgnoreCase(haystack: []const u8, needle: []const u8) ?usize {
|
|
240
|
+
if (needle.len == 0 or needle.len > haystack.len) return null;
|
|
241
|
+
var index: usize = 0;
|
|
242
|
+
while (index + needle.len <= haystack.len) : (index += 1) {
|
|
243
|
+
var matched = true;
|
|
244
|
+
for (needle, 0..) |needle_ch, offset| {
|
|
245
|
+
if (std.ascii.toLower(haystack[index + offset]) != std.ascii.toLower(needle_ch)) {
|
|
246
|
+
matched = false;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (matched) return index;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn startsWithAt(bytes: []const u8, index: usize, needle: []const u8) bool {
|
|
256
|
+
return index + needle.len <= bytes.len and std.mem.eql(u8, bytes[index .. index + needle.len], needle);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn startsWithIgnoreCase(haystack: []const u8, needle: []const u8) bool {
|
|
260
|
+
if (needle.len > haystack.len) return false;
|
|
261
|
+
for (needle, 0..) |needle_ch, index| {
|
|
262
|
+
if (std.ascii.toLower(haystack[index]) != std.ascii.toLower(needle_ch)) return false;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
fn isDelimiter(ch: u8) bool {
|
|
268
|
+
return switch (ch) {
|
|
269
|
+
' ', '\t', '\r', '\n', '<', '>', '"', '\'', '=', ',', ';', ')' => true,
|
|
270
|
+
else => false,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
pub fn writeFile(
|
|
4
|
+
allocator: std.mem.Allocator,
|
|
5
|
+
trace_dir: []const u8,
|
|
6
|
+
archive_path: []const u8,
|
|
7
|
+
out_file: *std.fs.File,
|
|
8
|
+
) !void {
|
|
9
|
+
if (isUnsafeArchivePath(archive_path)) return error.UnsafeArchivePath;
|
|
10
|
+
const fs_path = try std.fs.path.join(allocator, &.{ trace_dir, archive_path });
|
|
11
|
+
defer allocator.free(fs_path);
|
|
12
|
+
|
|
13
|
+
var in_file = try std.fs.cwd().openFile(fs_path, .{});
|
|
14
|
+
defer in_file.close();
|
|
15
|
+
const stat = try in_file.stat();
|
|
16
|
+
|
|
17
|
+
var header = [_]u8{0} ** 512;
|
|
18
|
+
try writeHeader(&header, archive_path, stat.size);
|
|
19
|
+
|
|
20
|
+
try out_file.writeAll(&header);
|
|
21
|
+
var buffer: [16 * 1024]u8 = undefined;
|
|
22
|
+
var remaining = stat.size;
|
|
23
|
+
while (remaining > 0) {
|
|
24
|
+
const read_len = @min(buffer.len, remaining);
|
|
25
|
+
const n = try in_file.read(buffer[0..read_len]);
|
|
26
|
+
if (n == 0) return error.UnexpectedEndOfStream;
|
|
27
|
+
try out_file.writeAll(buffer[0..n]);
|
|
28
|
+
remaining -= n;
|
|
29
|
+
}
|
|
30
|
+
try writePadding(out_file, stat.size);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn writeBytes(archive_path: []const u8, bytes: []const u8, out_file: *std.fs.File) !void {
|
|
34
|
+
if (isUnsafeArchivePath(archive_path)) return error.UnsafeArchivePath;
|
|
35
|
+
var header = [_]u8{0} ** 512;
|
|
36
|
+
try writeHeader(&header, archive_path, bytes.len);
|
|
37
|
+
|
|
38
|
+
try out_file.writeAll(&header);
|
|
39
|
+
try out_file.writeAll(bytes);
|
|
40
|
+
try writePadding(out_file, bytes.len);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn writeHeader(header: *[512]u8, archive_path: []const u8, size: u64) !void {
|
|
44
|
+
try writeTarName(header, archive_path);
|
|
45
|
+
writeOctal(header[100..108], 0o644);
|
|
46
|
+
writeOctal(header[108..116], 0);
|
|
47
|
+
writeOctal(header[116..124], 0);
|
|
48
|
+
writeOctal(header[124..136], size);
|
|
49
|
+
writeOctal(header[136..148], 0);
|
|
50
|
+
@memset(header[148..156], ' ');
|
|
51
|
+
header[156] = '0';
|
|
52
|
+
@memcpy(header[257..263], "ustar\x00");
|
|
53
|
+
@memcpy(header[263..265], "00");
|
|
54
|
+
writeOctal(header[329..337], 0);
|
|
55
|
+
writeOctal(header[337..345], 0);
|
|
56
|
+
const checksum = tarChecksum(header);
|
|
57
|
+
writeChecksum(header[148..156], checksum);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn writePadding(out_file: *std.fs.File, size: u64) !void {
|
|
61
|
+
const padding = (512 - (size % 512)) % 512;
|
|
62
|
+
if (padding > 0) try out_file.writeAll((&([_]u8{0} ** 512))[0..padding]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn isUnsafeArchivePath(archive_path: []const u8) bool {
|
|
66
|
+
return std.mem.startsWith(u8, archive_path, "/") or std.mem.indexOf(u8, archive_path, "..") != null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn writeTarName(header: *[512]u8, archive_path: []const u8) !void {
|
|
70
|
+
if (archive_path.len <= 100) {
|
|
71
|
+
@memcpy(header[0..archive_path.len], archive_path);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var split_index: ?usize = null;
|
|
76
|
+
var index: usize = archive_path.len;
|
|
77
|
+
while (index > 0) {
|
|
78
|
+
index -= 1;
|
|
79
|
+
if (archive_path[index] != '/') continue;
|
|
80
|
+
const prefix = archive_path[0..index];
|
|
81
|
+
const name = archive_path[index + 1 ..];
|
|
82
|
+
if (prefix.len <= 155 and name.len <= 100) {
|
|
83
|
+
split_index = index;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const actual_split = split_index orelse return error.ArchivePathTooLong;
|
|
88
|
+
const prefix = archive_path[0..actual_split];
|
|
89
|
+
const name = archive_path[actual_split + 1 ..];
|
|
90
|
+
@memcpy(header[0..name.len], name);
|
|
91
|
+
@memcpy(header[345 .. 345 + prefix.len], prefix);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn writeOctal(field: []u8, value: u64) void {
|
|
95
|
+
@memset(field, 0);
|
|
96
|
+
const digits_len = field.len - 1;
|
|
97
|
+
var remaining = value;
|
|
98
|
+
var index = digits_len;
|
|
99
|
+
while (index > 0) {
|
|
100
|
+
index -= 1;
|
|
101
|
+
field[index] = @as(u8, @intCast('0' + (remaining & 7)));
|
|
102
|
+
remaining >>= 3;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn writeChecksum(field: []u8, value: u64) void {
|
|
107
|
+
@memset(field, 0);
|
|
108
|
+
var remaining = value;
|
|
109
|
+
var index: usize = 6;
|
|
110
|
+
while (index > 0) {
|
|
111
|
+
index -= 1;
|
|
112
|
+
field[index] = @as(u8, @intCast('0' + (remaining & 7)));
|
|
113
|
+
remaining >>= 3;
|
|
114
|
+
}
|
|
115
|
+
field[6] = 0;
|
|
116
|
+
field[7] = ' ';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn tarChecksum(header: *const [512]u8) u64 {
|
|
120
|
+
var sum: u64 = 0;
|
|
121
|
+
for (header) |byte| sum += byte;
|
|
122
|
+
return sum;
|
|
123
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const android = @import("android.zig");
|
|
3
|
+
const device_registry = @import("device_registry.zig");
|
|
4
|
+
const ios = @import("ios.zig");
|
|
5
|
+
const run_options = @import("run_options.zig");
|
|
6
|
+
const types = @import("types.zig");
|
|
7
|
+
|
|
8
|
+
pub const IosDevicesScope = enum {
|
|
9
|
+
simulator,
|
|
10
|
+
physical,
|
|
11
|
+
all,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
|
|
15
|
+
var platform: run_options.Platform = .android;
|
|
16
|
+
var ios_devices_scope: IosDevicesScope = .simulator;
|
|
17
|
+
var adb_path: []const u8 = "adb";
|
|
18
|
+
var xcrun_path: []const u8 = "xcrun";
|
|
19
|
+
var json = false;
|
|
20
|
+
|
|
21
|
+
while (args.next()) |arg| {
|
|
22
|
+
if (std.mem.eql(u8, arg, "--platform")) {
|
|
23
|
+
platform = try parsePlatform(args.next() orelse return error.MissingPlatform);
|
|
24
|
+
} else if (std.mem.eql(u8, arg, "--ios-device-type")) {
|
|
25
|
+
ios_devices_scope = try parseIosDevicesScope(args.next() orelse return error.MissingIosDeviceType);
|
|
26
|
+
} else if (std.mem.eql(u8, arg, "--adb")) {
|
|
27
|
+
adb_path = args.next() orelse return error.MissingAdbPath;
|
|
28
|
+
} else if (std.mem.eql(u8, arg, "--xcrun")) {
|
|
29
|
+
xcrun_path = args.next() orelse return error.MissingXcrunPath;
|
|
30
|
+
} else if (std.mem.eql(u8, arg, "--json")) {
|
|
31
|
+
json = true;
|
|
32
|
+
} else {
|
|
33
|
+
return error.UnknownFlag;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const devices = switch (platform) {
|
|
38
|
+
.android => try android.listDevices(allocator, adb_path),
|
|
39
|
+
.ios => try listIosDevicesForScope(allocator, xcrun_path, ios_devices_scope),
|
|
40
|
+
};
|
|
41
|
+
defer {
|
|
42
|
+
for (devices) |device| device.deinit(allocator);
|
|
43
|
+
allocator.free(devices);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const stdout = std.fs.File.stdout().deprecatedWriter();
|
|
47
|
+
if (json) return try device_registry.writeJson(stdout, registryPlatform(platform), devices);
|
|
48
|
+
if (devices.len == 0) return try stdout.print("No {s} devices found.\n", .{@tagName(platform)});
|
|
49
|
+
for (devices) |device| {
|
|
50
|
+
try stdout.print("{s}\t{s}\n", .{ device.serial, device.state });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn parsePlatform(value: []const u8) !run_options.Platform {
|
|
55
|
+
if (std.mem.eql(u8, value, "android")) return .android;
|
|
56
|
+
if (std.mem.eql(u8, value, "ios")) return .ios;
|
|
57
|
+
return error.UnsupportedPlatform;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub fn parseIosDevicesScope(value: []const u8) !IosDevicesScope {
|
|
61
|
+
if (std.mem.eql(u8, value, "simulator")) return .simulator;
|
|
62
|
+
if (std.mem.eql(u8, value, "physical")) return .physical;
|
|
63
|
+
if (std.mem.eql(u8, value, "all")) return .all;
|
|
64
|
+
return error.UnsupportedIosDeviceType;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn listIosDevicesForScope(allocator: std.mem.Allocator, xcrun_path: []const u8, scope: IosDevicesScope) ![]types.DeviceInfo {
|
|
68
|
+
return switch (scope) {
|
|
69
|
+
.simulator => try ios.listDevices(allocator, xcrun_path),
|
|
70
|
+
.physical => try ios.listPhysicalDevices(allocator, xcrun_path),
|
|
71
|
+
.all => blk: {
|
|
72
|
+
const simulators = try ios.listDevices(allocator, xcrun_path);
|
|
73
|
+
errdefer {
|
|
74
|
+
for (simulators) |device| device.deinit(allocator);
|
|
75
|
+
allocator.free(simulators);
|
|
76
|
+
}
|
|
77
|
+
const physical = try ios.listPhysicalDevices(allocator, xcrun_path);
|
|
78
|
+
errdefer {
|
|
79
|
+
for (physical) |device| device.deinit(allocator);
|
|
80
|
+
allocator.free(physical);
|
|
81
|
+
}
|
|
82
|
+
const combined = try allocator.alloc(types.DeviceInfo, simulators.len + physical.len);
|
|
83
|
+
@memcpy(combined[0..simulators.len], simulators);
|
|
84
|
+
@memcpy(combined[simulators.len..], physical);
|
|
85
|
+
allocator.free(simulators);
|
|
86
|
+
allocator.free(physical);
|
|
87
|
+
break :blk combined;
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub fn registryPlatform(platform: run_options.Platform) device_registry.Platform {
|
|
93
|
+
return switch (platform) {
|
|
94
|
+
.android => .android,
|
|
95
|
+
.ios => .ios,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
const cli_output = @import("cli_output.zig");
|
|
4
|
+
const config = @import("config.zig");
|
|
5
|
+
const config_paths = @import("config_paths.zig");
|
|
6
|
+
const doctor = @import("doctor.zig");
|
|
7
|
+
|
|
8
|
+
pub const ParsedArgs = struct {
|
|
9
|
+
options: doctor.Options = .{},
|
|
10
|
+
json: bool = false,
|
|
11
|
+
strict: bool = false,
|
|
12
|
+
config_path: ?[]const u8 = null,
|
|
13
|
+
explicit_config: bool = false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
pub fn parseArgs(args: []const []const u8) !ParsedArgs {
|
|
17
|
+
var parsed = ParsedArgs{};
|
|
18
|
+
var index: usize = 0;
|
|
19
|
+
while (index < args.len) : (index += 1) {
|
|
20
|
+
const arg = args[index];
|
|
21
|
+
if (std.mem.eql(u8, arg, "--adb")) {
|
|
22
|
+
index += 1;
|
|
23
|
+
parsed.options.adb_path = if (index < args.len) args[index] else return error.MissingAdbPath;
|
|
24
|
+
} else if (std.mem.eql(u8, arg, "--android-shim")) {
|
|
25
|
+
index += 1;
|
|
26
|
+
parsed.options.android_shim_path = if (index < args.len) args[index] else return error.MissingAndroidShimPath;
|
|
27
|
+
} else if (std.mem.eql(u8, arg, "--xcrun")) {
|
|
28
|
+
index += 1;
|
|
29
|
+
parsed.options.xcrun_path = if (index < args.len) args[index] else return error.MissingXcrunPath;
|
|
30
|
+
} else if (std.mem.eql(u8, arg, "--ios-shim")) {
|
|
31
|
+
index += 1;
|
|
32
|
+
parsed.options.ios_shim_path = if (index < args.len) args[index] else return error.MissingIosShimPath;
|
|
33
|
+
} else if (std.mem.eql(u8, arg, "--zig")) {
|
|
34
|
+
index += 1;
|
|
35
|
+
parsed.options.zig_path = if (index < args.len) args[index] else return error.MissingZigPath;
|
|
36
|
+
} else if (std.mem.eql(u8, arg, "--json")) {
|
|
37
|
+
parsed.json = true;
|
|
38
|
+
} else if (std.mem.eql(u8, arg, "--strict")) {
|
|
39
|
+
parsed.strict = true;
|
|
40
|
+
} else if (std.mem.eql(u8, arg, "--config")) {
|
|
41
|
+
index += 1;
|
|
42
|
+
parsed.config_path = if (index < args.len) args[index] else return error.MissingConfigPath;
|
|
43
|
+
parsed.explicit_config = true;
|
|
44
|
+
} else {
|
|
45
|
+
return error.UnknownFlag;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
|
|
52
|
+
var raw_args = std.ArrayList([]const u8).empty;
|
|
53
|
+
defer raw_args.deinit(allocator);
|
|
54
|
+
while (args.next()) |arg| try raw_args.append(allocator, arg);
|
|
55
|
+
|
|
56
|
+
const parsed = try parseArgs(raw_args.items);
|
|
57
|
+
try runParsed(allocator, parsed);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn runParsed(allocator: std.mem.Allocator, parsed: ParsedArgs) !void {
|
|
61
|
+
var options = parsed.options;
|
|
62
|
+
const actual_config_path = parsed.config_path orelse config_paths.default_path;
|
|
63
|
+
var config_check: ?doctor.Check = null;
|
|
64
|
+
defer if (config_check) |check| check.deinit(allocator);
|
|
65
|
+
var owned_option_paths = std.ArrayList([]const u8).empty;
|
|
66
|
+
defer {
|
|
67
|
+
for (owned_option_paths.items) |path| allocator.free(path);
|
|
68
|
+
owned_option_paths.deinit(allocator);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var loaded_config = config_paths.loadIfPresent(allocator, parsed.config_path) catch |err| blk: {
|
|
72
|
+
const field_path = try config.errorFieldPathForFile(allocator, actual_config_path, err);
|
|
73
|
+
defer if (field_path) |value| allocator.free(value);
|
|
74
|
+
config_check = try doctor.checkConfigError(allocator, actual_config_path, err, field_path);
|
|
75
|
+
break :blk null;
|
|
76
|
+
};
|
|
77
|
+
defer if (loaded_config) |*cfg| cfg.deinit(allocator);
|
|
78
|
+
if (loaded_config) |cfg| {
|
|
79
|
+
const config_root = try config_paths.rootForPath(allocator, actual_config_path);
|
|
80
|
+
defer allocator.free(config_root);
|
|
81
|
+
if (parsed.explicit_config) config_check = try doctor.checkConfigLoaded(allocator, actual_config_path, cfg.scripts);
|
|
82
|
+
if (std.mem.eql(u8, options.adb_path, "adb")) {
|
|
83
|
+
if (cfg.tools.adb_path) |path| options.adb_path = try config_paths.ownCommandPath(allocator, &owned_option_paths, config_root, path);
|
|
84
|
+
}
|
|
85
|
+
if (options.android_shim_path == null) {
|
|
86
|
+
if (cfg.tools.android_shim_path) |path| options.android_shim_path = try config_paths.ownFilePath(allocator, &owned_option_paths, config_root, path);
|
|
87
|
+
}
|
|
88
|
+
if (cfg.android.smoke_scenario) |path| options.android_smoke_scenario = try config_paths.ownFilePath(allocator, &owned_option_paths, config_root, path);
|
|
89
|
+
if (std.mem.eql(u8, options.xcrun_path, "xcrun")) {
|
|
90
|
+
if (cfg.tools.xcrun_path) |path| options.xcrun_path = try config_paths.ownCommandPath(allocator, &owned_option_paths, config_root, path);
|
|
91
|
+
}
|
|
92
|
+
if (options.ios_shim_path == null) {
|
|
93
|
+
if (cfg.tools.ios_shim_path) |path| options.ios_shim_path = try config_paths.ownFilePath(allocator, &owned_option_paths, config_root, path);
|
|
94
|
+
}
|
|
95
|
+
if (cfg.ios.smoke_scenario) |path| options.ios_smoke_scenario = try config_paths.ownFilePath(allocator, &owned_option_paths, config_root, path);
|
|
96
|
+
if (std.mem.eql(u8, options.zig_path, "zig")) {
|
|
97
|
+
if (cfg.tools.zig_path) |path| options.zig_path = try config_paths.ownCommandPath(allocator, &owned_option_paths, config_root, path);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const checks = try doctor.run(allocator, options);
|
|
102
|
+
defer {
|
|
103
|
+
for (checks) |check| check.deinit(allocator);
|
|
104
|
+
allocator.free(checks);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const stdout = std.fs.File.stdout().deprecatedWriter();
|
|
108
|
+
if (parsed.json) {
|
|
109
|
+
try cli_output.writeDoctorJson(stdout, config_check, checks);
|
|
110
|
+
} else {
|
|
111
|
+
try cli_output.writeDoctorText(stdout, config_check, checks);
|
|
112
|
+
}
|
|
113
|
+
if (parsed.strict and !cli_output.doctorChecksHealthy(config_check, checks)) std.process.exit(1);
|
|
114
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
const cli_output = @import("cli_output.zig");
|
|
4
|
+
const importer = @import("importer.zig");
|
|
5
|
+
|
|
6
|
+
pub const ParsedArgs = struct {
|
|
7
|
+
format: []const u8,
|
|
8
|
+
source_path: []const u8,
|
|
9
|
+
out_path: ?[]const u8 = null,
|
|
10
|
+
name: ?[]const u8 = null,
|
|
11
|
+
app_id: ?[]const u8 = null,
|
|
12
|
+
force: bool = false,
|
|
13
|
+
json: bool = false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
pub fn parseArgs(args: []const []const u8) !ParsedArgs {
|
|
17
|
+
if (args.len == 0) return error.MissingImportFormat;
|
|
18
|
+
if (args.len == 1) return error.MissingImportPath;
|
|
19
|
+
|
|
20
|
+
var parsed = ParsedArgs{
|
|
21
|
+
.format = args[0],
|
|
22
|
+
.source_path = args[1],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
var index: usize = 2;
|
|
26
|
+
while (index < args.len) : (index += 1) {
|
|
27
|
+
const arg = args[index];
|
|
28
|
+
if (std.mem.eql(u8, arg, "--out")) {
|
|
29
|
+
index += 1;
|
|
30
|
+
parsed.out_path = if (index < args.len) args[index] else return error.MissingImportOut;
|
|
31
|
+
} else if (std.mem.eql(u8, arg, "--name")) {
|
|
32
|
+
index += 1;
|
|
33
|
+
parsed.name = if (index < args.len) args[index] else return error.MissingImportName;
|
|
34
|
+
} else if (std.mem.eql(u8, arg, "--app-id")) {
|
|
35
|
+
index += 1;
|
|
36
|
+
parsed.app_id = if (index < args.len) args[index] else return error.MissingAppId;
|
|
37
|
+
} else if (std.mem.eql(u8, arg, "--force")) {
|
|
38
|
+
parsed.force = true;
|
|
39
|
+
} else if (std.mem.eql(u8, arg, "--json")) {
|
|
40
|
+
parsed.json = true;
|
|
41
|
+
} else {
|
|
42
|
+
return error.UnknownFlag;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (parsed.out_path == null) return error.MissingImportOut;
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
|
|
50
|
+
var raw_args = std.ArrayList([]const u8).empty;
|
|
51
|
+
defer raw_args.deinit(allocator);
|
|
52
|
+
while (args.next()) |arg| try raw_args.append(allocator, arg);
|
|
53
|
+
|
|
54
|
+
const parsed = try parseArgs(raw_args.items);
|
|
55
|
+
if (!std.mem.eql(u8, parsed.format, "flow-yaml")) return error.UnsupportedImportFormat;
|
|
56
|
+
|
|
57
|
+
const result = try importer.importFlowYamlFile(allocator, parsed.source_path, parsed.out_path.?, .{
|
|
58
|
+
.name = parsed.name,
|
|
59
|
+
.app_id = parsed.app_id,
|
|
60
|
+
.force = parsed.force,
|
|
61
|
+
});
|
|
62
|
+
defer result.deinit(allocator);
|
|
63
|
+
|
|
64
|
+
const stdout = std.fs.File.stdout().deprecatedWriter();
|
|
65
|
+
if (parsed.json) return try cli_output.writeImportJson(stdout, parsed.format, parsed.source_path, result);
|
|
66
|
+
try stdout.print("wrote {s}\n", .{result.out_path});
|
|
67
|
+
try stdout.writeAll("next: zmr validate ");
|
|
68
|
+
try cli_output.writeShellArg(stdout, result.out_path);
|
|
69
|
+
try stdout.writeAll("\n");
|
|
70
|
+
}
|