From 7911287261b5e4f3e8242e49ef44f40225c4d5fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Motiejus=20Jak=C5=A1tys?= Date: Wed, 29 Jun 2022 13:36:41 +0300 Subject: [PATCH] gecos validator --- src/User.zig | 42 ++++++++++++++--------------- src/validate.zig | 69 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 70 insertions(+), 41 deletions(-) diff --git a/src/User.zig b/src/User.zig index 27a7c81..cc915b9 100644 --- a/src/User.zig +++ b/src/User.zig @@ -46,7 +46,7 @@ pub fn clone( // fromLine accepts a line of /etc/passwd (with or without the EOL) and makes a // User. -fn fromLine(allocator: Allocator, err_ctx: *ErrCtx, line: []const u8) error{ InvalidRecord, OutOfMemory }!User { +fn fromLine(allocator: Allocator, err: *ErrCtx, line: []const u8) error{ InvalidRecord, OutOfMemory }!User { var it = mem.split(u8, line, ":"); const name = it.next(); _ = it.next(); // password @@ -58,7 +58,7 @@ fn fromLine(allocator: Allocator, err_ctx: *ErrCtx, line: []const u8) error{ Inv // all fields are set if (shell == null) { - err_ctx.print("too few user fields in line: {s}", .{line}); + err.print("too few user fields in line: {s}", .{line}); return error.InvalidRecord; } @@ -67,19 +67,19 @@ fn fromLine(allocator: Allocator, err_ctx: *ErrCtx, line: []const u8) error{ Inv return error.InvalidRecord; const uid = fmt.parseInt(u32, uids.?, 10) catch - return err_ctx.returnf("bad uid: {s}", .{uids.?}, error.InvalidRecord); + return err.returnf("bad uid: {s}", .{uids.?}, error.InvalidRecord); const gid = fmt.parseInt(u32, gids.?, 10) catch - return err_ctx.returnf("bad uid: {s}", .{gids.?}, error.InvalidRecord); + return err.returnf("bad uid: {s}", .{gids.?}, error.InvalidRecord); - validate.utf8(name.?) catch - return err_ctx.returnf("name is invalid utf8: '{s}'", .{name.?}, error.InvalidRecord); - validate.utf8(gecos.?) catch - return err_ctx.returnf("gecos is invalid utf8: '{s}'", .{gecos.?}, error.InvalidRecord); + validate.name(name.?, err) catch + return err.returnf("invalid name '{s}'", .{name.?}, error.InvalidRecord); + validate.gecos(gecos.?, err) catch + return err.returnf("invalid gecos '{s}'", .{gecos.?}, error.InvalidRecord); validate.utf8(home.?) catch - return err_ctx.returnf("home is invalid utf8: '{s}'", .{home.?}, error.InvalidRecord); + return err.returnf("home is invalid utf8: '{s}'", .{home.?}, error.InvalidRecord); validate.utf8(shell.?) catch - return err_ctx.returnf("shell is invalid utf8: '{s}'", .{shell.?}, error.InvalidRecord); + return err.returnf("shell is invalid utf8: '{s}'", .{shell.?}, error.InvalidRecord); const user = User{ .uid = uid, @@ -138,7 +138,7 @@ pub fn deinit(self: *User, allocator: Allocator) void { self.* = undefined; } -pub fn fromReader(allocator: Allocator, err_ctx: *ErrCtx, reader: anytype) ![]User { +pub fn fromReader(allocator: Allocator, err: *ErrCtx, reader: anytype) ![]User { var users = ArrayList(User).init(allocator); errdefer { for (users.items) |*user| user.deinit(allocator); @@ -147,7 +147,7 @@ pub fn fromReader(allocator: Allocator, err_ctx: *ErrCtx, reader: anytype) ![]Us var buf: [max_line_len + 1]u8 = undefined; while (try reader.readUntilDelimiterOrEof(buf[0..], '\n')) |line| { - var user = try fromLine(allocator, err_ctx, line); + var user = try fromLine(allocator, err, line); try users.append(user); } return users.toOwnedSlice(); @@ -176,7 +176,7 @@ pub const max_user = User{ const from_line_examples = [_]struct { line: []const u8, - want: error{InvalidUser}!User, + want: error{InvalidRecord}!User, wantErrStr: []const u8 = &[_]u8{}, }{ .{ @@ -191,20 +191,20 @@ const from_line_examples = [_]struct { }, }, .{ - .line = "\x00emas:x:0:0:root:/root:/bin/bash", - .want = error.InvalidUser, - .wantErrStr = "foobar", + .line = "žemas:x:0:0:root:/root:/bin/bash", + .want = error.InvalidRecord, + .wantErrStr = "invalid name 'žemas': invalid character at position 0", }, }; test "User.fromLine" { - if (true) return error.SkipZigTest; + //if (true) return error.SkipZigTest; const allocator = testing.allocator; for (from_line_examples) |tt| { - var err_ctx = ErrCtx{}; + var err = ErrCtx{}; if (tt.want) |want_user| { - var got = try fromLine(allocator, &err_ctx, tt.line); + var got = try fromLine(allocator, &err, tt.line); defer got.deinit(allocator); try testing.expectEqual(want_user.uid, got.uid); try testing.expectEqual(want_user.gid, got.gid); @@ -215,11 +215,11 @@ test "User.fromLine" { } else |want_err| { try testing.expectError( want_err, - fromLine(allocator, &err_ctx, tt.line), + fromLine(allocator, &err, tt.line), ); try testing.expectEqualStrings( tt.wantErrStr, - err_ctx.unwrap().constSlice(), + err.unwrap().constSlice(), ); } } diff --git a/src/validate.zig b/src/validate.zig index 4d2da72..d68ac58 100644 --- a/src/validate.zig +++ b/src/validate.zig @@ -36,6 +36,19 @@ pub fn name(s: []const u8, err: *ErrCtx) error{InvalidRecord}!void { } } +// gecos must be utf8 without ':' +pub fn gecos(s: []const u8, errc: *ErrCtx) error{InvalidRecord}!void { + var utf8view = std.unicode.Utf8View.init(s) catch |err| switch (err) { + error.InvalidUtf8 => return errc.returnf("invalid utf8", .{}, error.InvalidRecord), + }; + + var it = utf8view.iterator(); + while (it.nextCodepoint()) |codepoint| { + if (codepoint == ':') + return errc.returnf(": is not allowed", .{}, error.InvalidRecord); + } +} + const testing = std.testing; test "validate name" { @@ -43,33 +56,49 @@ test "validate name" { name: []const u8, wantErr: ?[]const u8 = null, }{ - .{ - .name = "all-good", - }, - .{ - .name = "...", - }, - .{ - .name = "", - .wantErr = "cannot be empty", - }, - .{ - .name = "-no-start-dash", - .wantErr = "invalid character at position 0", - }, - .{ - .name = "Herbasž", - .wantErr = "invalid character at position 7", - }, + .{ .name = "all-good" }, + .{ .name = "..." }, + .{ .name = "", .wantErr = "cannot be empty" }, + .{ .name = "-no-start-dash", .wantErr = "invalid character at position 0" }, + .{ .name = "Herbasž", .wantErr = "invalid character at position 7" }, }; for (examples) |tt| { var err = ErrCtx{}; + const got = name(tt.name, &err); if (tt.wantErr) |wantErr| { - try testing.expectError(error.InvalidRecord, name(tt.name, &err)); + try testing.expectError(error.InvalidRecord, got); try testing.expectEqualStrings(wantErr, err.unwrap().constSlice()); } else { - try name(tt.name, &err); + // TODO: how to assert `got` is a non-error in a single line? + if (got) |_| {} else |_| return error.TestUnExpectedError; + try testing.expectEqualStrings("", err.unwrap().constSlice()); + } + } +} + +test "validate gecos" { + const examples = [_]struct { + gecos: []const u8, + wantErr: ?[]const u8 = null, + }{ + .{ .gecos = "all-good" }, + .{ .gecos = "..." }, + .{ .gecos = "" }, + .{ .gecos = "Vidmantas Kaminskas Ž" }, + .{ .gecos = "Not\xffUnicode", .wantErr = "invalid utf8" }, + .{ .gecos = "Has:Colon", .wantErr = ": is not allowed" }, + }; + for (examples) |tt| { + var err = ErrCtx{}; + + const got = gecos(tt.gecos, &err); + if (tt.wantErr) |wantErr| { + try testing.expectError(error.InvalidRecord, got); + try testing.expectEqualStrings(wantErr, err.unwrap().constSlice()); + } else { + // TODO: how to assert `got` is a non-error in a single line? + if (got) |_| {} else |_| return error.TestUnExpectedError; try testing.expectEqualStrings("", err.unwrap().constSlice()); } }