diff --git a/build.zig b/build.zig index 808e210..badefc4 100644 --- a/build.zig +++ b/build.zig @@ -52,4 +52,20 @@ pub fn build(b: *std.Build) void { // This will evaluate the `run` step rather than the default, which is "install". const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); } diff --git a/src/main.zig b/src/main.zig index a9bfec0..ca9e6dc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,16 +1,12 @@ const std = @import("std"); +const validate = @import("validate.zig"); const allocator = std.heap.page_allocator; var client: std.http.Client = .{ .allocator = allocator }; const Command = enum { list, logs, create, delete, start }; const ForwardedUser = "X-Forwarded-User"; -const PluginError = error{ - InvalidUser, - InvalidCommand, - NoContainerName, - InvalidImage, -}; +const PluginError = error{ InvalidUser, InvalidCommand, NoContainerName, InvalidImage, Unprocessable }; const CodefirstAuth = struct { proxyScheme: []const u8, @@ -104,7 +100,25 @@ pub fn run() !void { .proxyHost = std.os.getenv("PROXYHOST") orelse "dockerproxy:8080", .user = std.os.getenv("DRONE_REPO_OWNER") orelse return PluginError.InvalidUser, }; - const containerName = std.os.getenv("PLUGIN_CONTAINER") orelse return PluginError.NoContainerName; + var containerNameEnv = std.os.getenv("PLUGIN_CONTAINER") orelse return PluginError.NoContainerName; + var shortUser = try std.mem.replaceOwned(u8, allocator, auth.user, ".", ""); + defer allocator.free(shortUser); + var containerName = try std.fmt.allocPrint(allocator, "{s}-{s}", .{ shortUser, containerNameEnv }); + defer allocator.free(containerName); + + var idx: usize = 0; + validate.validateDockerContainerName(containerName, &idx) catch |err| { + switch (err) { + validate.NamingError.IllegalChar => { + std.log.err("Invalid container name at index {}: {}", .{ idx, err }); + }, + else => { + std.log.err("Invalid container name: {}", .{err}); + return PluginError.Unprocessable; + }, + } + return PluginError.Unprocessable; + }; var envs = std.ArrayList([]const u8).init(allocator); defer envs.deinit(); diff --git a/src/validate.zig b/src/validate.zig new file mode 100644 index 0000000..f391280 --- /dev/null +++ b/src/validate.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +const ascii = std.ascii; + +pub const NamingError = error{ Empty, IllegalChar, TooLong }; + +pub fn validateDockerContainerName(name: []const u8, error_index: *usize) !void { + // [a-zA-Z0-9][a-zA-Z0-9_.-] + if (name.len == 0) { + return NamingError.Empty; + } + if (name.len > 30) { + return NamingError.TooLong; + } + + if (!ascii.isAlphanumeric(name[0])) { + error_index.* = 0; + return NamingError.IllegalChar; + } + for (name, 0..) |c, idx| { + if (!ascii.isAlphanumeric(c) and c != '.' and c != '_' and c != '-') { + error_index.* = idx; + return NamingError.IllegalChar; + } + } +} + +test "valid name" { + var error_index: usize = 0; + try validateDockerContainerName("hello", &error_index); + try std.testing.expect(error_index == 0); +} + +test "indicate non ascii letter" { + var error_index: usize = 0; + try std.testing.expectError(NamingError.IllegalChar, validateDockerContainerName("hello😀", &error_index)); + try std.testing.expect(error_index == 5); +}