163 lines
5.6 KiB
Zig
163 lines
5.6 KiB
Zig
const std = @import("std");
|
|
const ascii = std.ascii;
|
|
const BoundedArray = std.BoundedArray;
|
|
|
|
const ErrCtx = @import("ErrCtx.zig");
|
|
|
|
pub fn downCast(comptime T: type, n: u64) error{InvalidRecord}!T {
|
|
return std.math.cast(T, n) orelse return error.InvalidRecord;
|
|
}
|
|
|
|
pub fn utf8(s: []const u8) error{InvalidRecord}!void {
|
|
if (!std.unicode.utf8ValidateSlice(s)) {
|
|
return error.InvalidRecord;
|
|
}
|
|
}
|
|
|
|
fn debugChar(c: u8) BoundedArray(u8, 4) {
|
|
var res = BoundedArray(u8, 4).init(0) catch unreachable;
|
|
if (ascii.isPrint(c)) {
|
|
res.writer().print("'{c}'", .{c}) catch unreachable;
|
|
} else res.writer().print("0x{X}", .{c}) catch unreachable;
|
|
return res;
|
|
}
|
|
|
|
// # adduser žmogus
|
|
// adduser: To avoid problems, the username should consist only of letters,
|
|
// digits, underscores, periods, at signs and dashes, and not start with a dash
|
|
// (as defined by IEEE Std 1003.1-2001). For compatibility with Samba machine
|
|
// accounts $ is also supported at the end of the username
|
|
pub fn name(s: []const u8, err: *ErrCtx) error{InvalidRecord}!void {
|
|
if (s.len == 0)
|
|
return err.returnf("cannot be empty", .{}, error.InvalidRecord);
|
|
|
|
const c0 = s[0];
|
|
if (!(ascii.isAlNum(c0) or c0 == '_' or c0 == '.' or c0 == '@'))
|
|
return err.returnf(
|
|
"invalid character {s} at position 0",
|
|
.{debugChar(c0).constSlice()},
|
|
error.InvalidRecord,
|
|
);
|
|
|
|
for (s[1..]) |c, i| {
|
|
if (!(ascii.isAlNum(c) or c == '_' or c == '.' or c == '@' or c == '-'))
|
|
return err.returnf(
|
|
"invalid character {s} at position {d}",
|
|
.{ debugChar(c).constSlice(), i + 2 },
|
|
error.InvalidRecord,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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("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 0xD at position {d}",
|
|
.{i + 2},
|
|
error.InvalidRecord,
|
|
);
|
|
}
|
|
}
|
|
|
|
const testing = std.testing;
|
|
|
|
test "validate name" {
|
|
const examples = [_]struct {
|
|
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 0xC5 at position 7" },
|
|
};
|
|
for (examples) |tt| {
|
|
var err = ErrCtx{};
|
|
|
|
const got = name(tt.name, &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());
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = "colon 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());
|
|
}
|
|
}
|
|
}
|
|
|
|
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 0xD at position 6" },
|
|
.{ .path = "/Herbasž", .wantErr = "invalid character 0xD 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());
|
|
}
|
|
}
|
|
}
|