diff --git a/src/User.zig b/src/User.zig index cc915b9..c6cdd2c 100644 --- a/src/User.zig +++ b/src/User.zig @@ -57,14 +57,12 @@ fn fromLine(allocator: Allocator, err: *ErrCtx, line: []const u8) error{ Invalid const shell = it.next(); // all fields are set - if (shell == null) { - err.print("too few user fields in line: {s}", .{line}); - return error.InvalidRecord; - } + if (shell == null) + return err.returnf("too few user fields in line: {s}", .{line}, error.InvalidRecord); // the line must be exhaustive. if (it.next() != null) - return error.InvalidRecord; + return error.returnf("too many fields in line", .{}, error.InvalidRecord); const uid = fmt.parseInt(u32, uids.?, 10) catch return err.returnf("bad uid: {s}", .{uids.?}, error.InvalidRecord); @@ -76,10 +74,10 @@ fn fromLine(allocator: Allocator, err: *ErrCtx, line: []const u8) error{ Invalid 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.returnf("home is invalid utf8: '{s}'", .{home.?}, error.InvalidRecord); - validate.utf8(shell.?) catch - return err.returnf("shell is invalid utf8: '{s}'", .{shell.?}, error.InvalidRecord); + validate.path(home.?, err) catch + return err.returnf("invalid home '{s}'", .{home.?}, error.InvalidRecord); + validate.path(shell.?, err) catch + return err.returnf("invalid shell '{s}'", .{shell.?}, error.InvalidRecord); const user = User{ .uid = uid, diff --git a/src/validate.zig b/src/validate.zig index d68ac58..ef070b0 100644 --- a/src/validate.zig +++ b/src/validate.zig @@ -45,7 +45,24 @@ pub fn gecos(s: []const u8, errc: *ErrCtx) error{InvalidRecord}!void { var it = utf8view.iterator(); while (it.nextCodepoint()) |codepoint| { if (codepoint == ':') - return errc.returnf(": is not allowed", .{}, error.InvalidRecord); + return errc.returnf("colon is not allowed", .{}, error.InvalidRecord); + } +} + +pub fn path(s: []const u8, err: *ErrCtx) error{InvalidRecord}!void { + if (s.len == 0) + return err.returnf("cannot be empty", .{}, error.InvalidRecord); + + if (s[0] != '/') + return err.returnf("must start with '/'", .{}, error.InvalidRecord); + + for (s[1..]) |c, i| { + if (!(ascii.isAlNum(c) or c == '/' or c == '_' or c == '.' or c == '@' or c == '-')) + return err.returnf( + "invalid character at position {d}", + .{i + 2}, + error.InvalidRecord, + ); } } @@ -87,7 +104,7 @@ test "validate gecos" { .{ .gecos = "" }, .{ .gecos = "Vidmantas Kaminskas Ž" }, .{ .gecos = "Not\xffUnicode", .wantErr = "invalid utf8" }, - .{ .gecos = "Has:Colon", .wantErr = ": is not allowed" }, + .{ .gecos = "Has:Colon", .wantErr = "colon is not allowed" }, }; for (examples) |tt| { var err = ErrCtx{}; @@ -103,3 +120,30 @@ test "validate gecos" { } } } + +test "validate path" { + const examples = [_]struct { + path: []const u8, + wantErr: ?[]const u8 = null, + }{ + .{ .path = "/path/ok" }, + .{ .path = "/" }, + .{ .path = "foo", .wantErr = "must start with '/'" }, + .{ .path = "", .wantErr = "cannot be empty" }, + .{ .path = "/path:motiejus", .wantErr = "invalid character at position 6" }, + .{ .path = "/Herbasž", .wantErr = "invalid character at position 8" }, + }; + for (examples) |tt| { + var err = ErrCtx{}; + + const got = path(tt.path, &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()); + } + } +}