diff --git a/.gitignore b/.gitignore index 38b3718..1b80de7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ zig-cache/ zig-out/ core src/worker_name.txt +*.wasm diff --git a/src/demo.wasm b/src/demo.wasm deleted file mode 100755 index 716016a..0000000 Binary files a/src/demo.wasm and /dev/null differ diff --git a/src/main.zig b/src/main.zig index d469058..ea95d27 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,7 +5,7 @@ var x_auth_key: [:0]const u8 = undefined; const cf_api_base = "https://api.cloudflare.com/client/v4"; -pub fn main() !void { +pub fn main() !u8 { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -21,7 +21,7 @@ pub fn main() !void { // We might actually want a "run this wasm" upload vs a "these are my // js files" upload. But for now we'll optimize for wasm const script = - \\import demoWasm from "./24526702f6c3ed7fb02b15125f614dd38804525f-demo.wasm"; + \\import demoWasm from "demo.wasm"; \\var src_default = { \\ async fetch(request, _env2, ctx) { \\ const stdout = new TransformStream(); @@ -56,7 +56,8 @@ pub fn main() !void { \\ src_default as default \\}; ; - const wasm = @embedFile("demo.wasm"); + var wasm = try loadWasm(allocator, script); + defer wasm.deinit(); // stdout is for the actual output of your application, for example if you // are implementing gzip, then only the compressed bytes should be sent to @@ -79,18 +80,91 @@ pub fn main() !void { .{if (worker_exists) "Worker exists, will not re-enable" else "Worker is new. Will enable after code update"}, ); - try putNewWorker(allocator, &client, .{ + var worker = Worker{ .account_id = accountid.?, .name = worker_name, - .wasm_file_data = wasm, + .wasm_file_data = wasm.data, .main_module = script, - }); + }; + putNewWorker(allocator, &client, &worker) catch |err| { + if (worker.errors == null) return err; + const stderr = std.io.getStdErr().writer(); + try stderr.print("{d} errors returned from CloudFlare:\n\n", .{worker.errors.?.len}); + for (worker.errors.?) |cf_err| { + try stderr.print("{s}\n", .{cf_err}); + allocator.free(cf_err); + } + return 1; + }; const subdomain = try getSubdomain(allocator, &client, accountid.?); defer allocator.free(subdomain); try stdout.print("Worker available at: https://{s}.{s}.workers.dev/\n", .{ worker_name, subdomain }); if (!worker_exists) try enableWorker(allocator, &client, accountid.?, worker_name); try bw.flush(); // don't forget to flush! + return 0; +} + +const Wasm = struct { + allocator: std.mem.Allocator, + name: []const u8, + data: []const u8, + + const Self = @This(); + + pub fn deinit(self: *Self) void { + self.allocator.free(self.name); + self.allocator.free(self.data); + } +}; + +fn loadWasm(allocator: std.mem.Allocator, script: []const u8) !Wasm { + // Looking for a string like this: import demoWasm from "demo.wasm" + // JavaScript may or may not have ; characters. We're not doing + // a full JS parsing here, so this may not be the most robust + + var inx: usize = 0; + + var name: ?[]const u8 = null; + while (true) { + inx = std.mem.indexOf(u8, script[inx..], "import ") orelse if (inx == 0) return error.NoImportFound else break; + inx += "import ".len; + + // oh god, we're not doing this: https://262.ecma-international.org/5.1/#sec-7.5 + // advance to next token - we don't care what the name is + while (inx < script.len and script[inx] != ' ') inx += 1; + // continue past space(s) + while (inx < script.len and script[inx] == ' ') inx += 1; + // We expect "from " to be next + if (!std.mem.startsWith(u8, script[inx..], "from ")) continue; + inx += "from ".len; + // continue past space(s) + while (inx < script.len and script[inx] == ' ') inx += 1; + // We now expect the name of our file... + if (script[inx] != '"' and script[inx] != '\'') continue; // we're not where we think we are + const quote = script[inx]; // there are two possibilities here + // we don't need to advance inx any more, as we're on the name, and if + // we loop, that's ok + inx += 1; // move off the quote onto the name + const end_quote_inx = std.mem.indexOfScalar(u8, script[inx..], quote); + if (end_quote_inx == null) continue; + const candidate_name = script[inx .. inx + end_quote_inx.?]; + if (std.mem.endsWith(u8, candidate_name, ".wasm")) { + if (name != null) // we are importing two wasm files, and we are now lost + return error.MultipleWasmImportsUnsupported; + name = candidate_name; + } + } + if (name == null) return error.NoWasmImportFound; + + const nm = try allocator.dupe(u8, name.?); + errdefer allocator.free(nm); + const data = try std.fs.cwd().readFileAlloc(allocator, nm, std.math.maxInt(usize)); + return Wasm{ + .allocator = allocator, + .name = nm, + .data = data, + }; } fn getAccountId(allocator: std.mem.Allocator, client: *std.http.Client) ![:0]const u8 { @@ -167,8 +241,9 @@ const Worker = struct { name: []const u8, main_module: []const u8, wasm_file_data: []const u8, + errors: ?[][]const u8 = null, }; -fn putNewWorker(allocator: std.mem.Allocator, client: *std.http.Client, worker: Worker) !void { +fn putNewWorker(allocator: std.mem.Allocator, client: *std.http.Client, worker: *Worker) !void { const put_script = cf_api_base ++ "/accounts/{s}/workers/scripts/{s}?include_subdomain_availability=true&excludeScript=true"; const url = try std.fmt.allocPrint(allocator, put_script, .{ worker.account_id, worker.name }); defer allocator.free(url); @@ -226,10 +301,27 @@ fn putNewWorker(allocator: std.mem.Allocator, client: *std.http.Client, worker: // std.debug.print("Url is {s}\n", .{url}); if (req.response.status != .ok) { std.debug.print("Status is {}\n", .{req.response.status}); + if (req.response.status == .bad_request) + worker.errors = getErrors(allocator, &req) catch null; return error.RequestFailed; } } +fn getErrors(allocator: std.mem.Allocator, req: *std.http.Client.Request) !?[][]const u8 { + var json_reader = std.json.reader(allocator, req.reader()); + defer json_reader.deinit(); + var body = try std.json.parseFromTokenSource(std.json.Value, allocator, &json_reader, .{}); + defer body.deinit(); + const arr = body.value.object.get("errors").?.array.items; + if (arr.len == 0) return null; + var error_list = try std.ArrayList([]const u8).initCapacity(allocator, arr.len); + defer error_list.deinit(); + for (arr) |item| { + error_list.appendAssumeCapacity(item.object.get("message").?.string); + } + return try error_list.toOwnedSlice(); +} + fn workerExists(allocator: std.mem.Allocator, client: *std.http.Client, account_id: []const u8, name: []const u8) !bool { const existence_check = cf_api_base ++ "/accounts/{s}/workers/services/{s}"; const url = try std.fmt.allocPrint(allocator, existence_check, .{ account_id, name });