1
Fork 0
turbonss/src/sections.zig

530 lines
18 KiB
Zig
Raw Normal View History

2022-02-28 10:31:14 +02:00
const std = @import("std");
2022-03-02 11:05:20 +02:00
const fmt = std.fmt;
const math = std.math;
2022-02-28 10:31:14 +02:00
const sort = std.sort;
2022-03-02 11:05:20 +02:00
const unicode = std.unicode;
2022-02-28 10:31:14 +02:00
const Allocator = std.mem.Allocator;
2022-03-02 06:02:04 +02:00
const ArrayListUnmanaged = std.ArrayListUnmanaged;
2022-03-02 06:50:15 +02:00
const ArrayList = std.ArrayList;
2022-03-02 11:05:20 +02:00
const MultiArrayList = std.MultiArrayList;
2022-02-28 10:31:14 +02:00
const StringHashMap = std.StringHashMap;
const AutoHashMap = std.AutoHashMap;
const BufSet = std.BufSet;
const pad = @import("padding.zig");
2022-03-02 06:50:15 +02:00
const compress = @import("compress.zig");
2022-03-03 18:05:46 +02:00
const shellImport = @import("shell.zig");
2022-02-28 10:31:14 +02:00
const userImport = @import("user.zig");
const groupImport = @import("group.zig");
2022-03-02 11:05:20 +02:00
const cmph = @import("cmph.zig");
const bdz = @import("bdz.zig");
2022-02-28 10:31:14 +02:00
const User = userImport.User;
const Group = groupImport.Group;
2022-03-05 06:08:01 +02:00
const ShellSections = shellImport.ShellWriter.ShellSections;
2022-02-28 10:31:14 +02:00
const Corpus = struct {
2022-03-01 05:47:44 +02:00
arena: std.heap.ArenaAllocator,
2022-02-28 10:31:14 +02:00
// sorted by name, by unicode codepoint
users: []User,
// sorted by gid
groups: []Group,
2022-03-02 11:05:20 +02:00
// convenience users and groups by column
usersMulti: MultiArrayList(User),
groupsMulti: MultiArrayList(Group),
2022-02-28 10:31:14 +02:00
// pointing to `users` and `groups` slices above.
name2user: StringHashMap(*const User),
uid2user: AutoHashMap(u32, *const User),
name2group: StringHashMap(*const Group),
gid2group: AutoHashMap(u32, *const Group),
groupname2users: StringHashMap([]*const User),
username2groups: StringHashMap([]*const Group),
2022-02-28 10:31:14 +02:00
pub const initErr = Allocator.Error ||
error{InvalidUtf8} ||
error{Duplicate} ||
error{NotFound};
pub fn init(
2022-03-01 05:47:44 +02:00
baseAllocator: Allocator,
2022-02-28 10:31:14 +02:00
usersConst: []const User,
groupsConst: []const Group,
) initErr!Corpus {
2022-03-01 05:47:44 +02:00
var arena = std.heap.ArenaAllocator.init(baseAllocator);
var allocator = arena.allocator();
2022-03-05 05:33:31 +02:00
errdefer arena.deinit();
2022-03-01 05:47:44 +02:00
2022-02-28 10:31:14 +02:00
var users = try allocator.alloc(User, usersConst.len);
var groups = try allocator.alloc(Group, groupsConst.len);
2022-03-02 06:18:19 +02:00
for (usersConst) |*user, i|
users[i] = try user.clone(allocator);
for (groupsConst) |*group, i|
groups[i] = try group.clone(allocator);
2022-02-28 10:31:14 +02:00
sort.sort(User, users, {}, cmpUser);
sort.sort(Group, groups, {}, cmpGroup);
2022-03-02 11:05:20 +02:00
var usersMulti = MultiArrayList(User){};
try usersMulti.ensureTotalCapacity(allocator, users.len);
for (users) |user|
usersMulti.appendAssumeCapacity(user);
var groupsMulti = MultiArrayList(Group){};
try groupsMulti.ensureTotalCapacity(allocator, groups.len);
for (groups) |group|
groupsMulti.appendAssumeCapacity(group);
2022-02-28 10:31:14 +02:00
var name2user = StringHashMap(*const User).init(allocator);
var uid2user = AutoHashMap(u32, *const User).init(allocator);
var name2group = StringHashMap(*const Group).init(allocator);
var gid2group = AutoHashMap(u32, *const Group).init(allocator);
2022-03-01 15:25:38 +02:00
for (users) |*user| {
2022-02-28 10:31:14 +02:00
var res1 = try name2user.getOrPut(user.name);
2022-03-02 06:18:19 +02:00
if (res1.found_existing)
2022-02-28 10:31:14 +02:00
return error.Duplicate;
2022-03-01 15:25:38 +02:00
res1.value_ptr.* = user;
2022-02-28 10:31:14 +02:00
var res2 = try uid2user.getOrPut(user.uid);
2022-03-02 06:18:19 +02:00
if (res2.found_existing)
2022-02-28 10:31:14 +02:00
return error.Duplicate;
2022-03-01 15:25:38 +02:00
res2.value_ptr.* = user;
2022-02-28 10:31:14 +02:00
}
2022-03-01 15:25:38 +02:00
for (groups) |*group| {
2022-02-28 10:31:14 +02:00
var res1 = try name2group.getOrPut(group.name);
2022-03-02 06:18:19 +02:00
if (res1.found_existing)
2022-02-28 10:31:14 +02:00
return error.Duplicate;
2022-03-01 15:25:38 +02:00
res1.value_ptr.* = group;
2022-02-28 10:31:14 +02:00
var res2 = try gid2group.getOrPut(group.gid);
2022-03-02 06:18:19 +02:00
if (res2.found_existing)
2022-02-28 10:31:14 +02:00
return error.Duplicate;
2022-03-01 15:25:38 +02:00
res2.value_ptr.* = group;
2022-02-28 10:31:14 +02:00
}
2022-03-02 11:05:20 +02:00
var groupname2users = StringHashMap([]*const User).init(allocator);
// uses baseAllocator, because it will be freed before
// returning from this function. This keeps the arena clean.
var username2groups = StringHashMap(
ArrayListUnmanaged(*const Group),
).init(baseAllocator);
2022-03-05 05:33:31 +02:00
defer username2groups.deinit();
2022-03-04 10:37:07 +02:00
2022-03-01 15:25:38 +02:00
for (groups) |*group| {
2022-03-02 11:05:20 +02:00
var members = try allocator.alloc(*const User, group.members.count());
members.len = 0;
2022-02-28 10:31:14 +02:00
var it = group.members.iterator();
while (it.next()) |memberName| {
if (name2user.get(memberName.*)) |user| {
2022-03-02 11:05:20 +02:00
members.len += 1;
members[members.len - 1] = user;
2022-02-28 10:31:14 +02:00
} else {
return error.NotFound;
}
2022-03-01 11:01:45 +02:00
var groupsOfMember = try username2groups.getOrPut(memberName.*);
2022-03-02 06:18:19 +02:00
if (!groupsOfMember.found_existing)
2022-03-02 06:02:04 +02:00
groupsOfMember.value_ptr.* = ArrayListUnmanaged(*const Group){};
try groupsOfMember.value_ptr.*.append(allocator, group);
2022-02-28 10:31:14 +02:00
}
var result = try groupname2users.getOrPut(group.name);
2022-03-02 06:18:19 +02:00
if (result.found_existing)
2022-02-28 10:31:14 +02:00
return error.Duplicate;
result.value_ptr.* = members;
}
2022-03-02 11:05:20 +02:00
var it1 = groupname2users.valueIterator();
while (it1.next()) |groupUsers| {
sort.sort(*const User, groupUsers.*, {}, cmpUserPtr);
2022-03-02 06:02:04 +02:00
}
2022-03-02 11:05:20 +02:00
var it2 = username2groups.valueIterator();
while (it2.next()) |userGroups|
sort.sort(*const Group, userGroups.items, {}, cmpGroupPtr);
var username2groups_final = StringHashMap([]*const Group).init(allocator);
2022-03-04 10:37:07 +02:00
var it = username2groups.iterator();
while (it.next()) |elem| {
const username = elem.key_ptr.*;
const usergroups = elem.value_ptr.*.toOwnedSlice(allocator);
try username2groups_final.put(username, usergroups);
}
2022-02-28 10:31:14 +02:00
return Corpus{
2022-03-01 05:47:44 +02:00
.arena = arena,
2022-02-28 10:31:14 +02:00
.users = users,
.groups = groups,
2022-03-02 11:05:20 +02:00
.usersMulti = usersMulti,
.groupsMulti = groupsMulti,
2022-02-28 10:31:14 +02:00
.name2user = name2user,
.uid2user = uid2user,
.name2group = name2group,
.gid2group = gid2group,
2022-03-02 11:05:20 +02:00
.groupname2users = groupname2users,
.username2groups = username2groups_final,
2022-02-28 10:31:14 +02:00
};
}
2022-03-01 05:47:44 +02:00
pub fn deinit(self: *Corpus) void {
self.arena.deinit();
self.* = undefined;
}
2022-02-28 10:31:14 +02:00
};
pub fn bdzGid(allocator: Allocator, corpus: *const Corpus) cmph.Error![]const u8 {
return try cmph.pack_u32(allocator, corpus.groupsMulti.items(.gid));
}
2022-03-02 06:50:15 +02:00
pub fn bdzGroupname(allocator: Allocator, corpus: *const Corpus) cmph.Error![]const u8 {
return try cmph.pack_str(allocator, corpus.groupsMulti.items(.name));
}
2022-03-02 06:50:15 +02:00
pub fn bdzUid(allocator: Allocator, corpus: *const Corpus) cmph.Error![]const u8 {
return try cmph.pack_u32(allocator, corpus.usersMulti.items(.uid));
}
2022-03-02 06:50:15 +02:00
pub fn bdzUsername(allocator: Allocator, corpus: *const Corpus) cmph.Error![]const u8 {
return try cmph.pack_str(allocator, corpus.usersMulti.items(.name));
}
2022-03-03 18:05:46 +02:00
// TODO(motiejus) there are a few problems:
// - memory management for shell sections is a mess. Make it easier by ...
// - shell module should accept a list of shells and spit out two slices
// (allocated with a given allocator). There is too much dancing around
// here.
const shellSectionsErr = Allocator.Error || error{Overflow};
pub fn shellSections(
allocator: Allocator,
corpus: *const Corpus,
) shellSectionsErr!ShellSections {
var popcon = shellImport.ShellWriter.init(allocator);
for (corpus.usersMulti.items(.shell)) |shell| {
try popcon.put(shell);
2022-03-03 18:05:46 +02:00
}
2022-03-05 06:08:01 +02:00
return popcon.toOwnedSections(shellImport.max_shells);
}
2022-03-03 18:05:46 +02:00
pub const UserGids = struct {
// username -> offset in blob
name2offset: StringHashMap(u32),
// compressed user gids blob. A blob contains N <= users.len items,
// an item is:
// len: varint
// gid: [varint]varint,
// ... and the gid list is delta-compressed.
blob: []u8,
pub fn deinit(self: *UserGids, allocator: Allocator) void {
self.name2offset.deinit();
allocator.free(self.blob);
self.* = undefined;
2022-03-03 18:05:46 +02:00
}
};
2022-03-02 06:50:15 +02:00
const userGidsErr = Allocator.Error || error{Overflow};
pub fn userGids(allocator: Allocator, corpus: *const Corpus) userGidsErr!UserGids {
var blob = ArrayList(u8).init(allocator);
2022-03-05 05:33:31 +02:00
errdefer blob.deinit();
var name2offset = StringHashMap(u32).init(allocator);
2022-03-05 05:33:31 +02:00
errdefer name2offset.deinit();
2022-03-06 13:11:06 +02:00
// zero'th entry is empty, so groupless users can refer to it.
try compress.appendUvarint(&blob, 0);
2022-03-05 05:33:31 +02:00
var scratch = try allocator.alloc(u32, 256);
defer allocator.free(scratch);
for (corpus.users) |user| {
2022-03-06 13:11:06 +02:00
if (corpus.username2groups.get(user.name)) |usergroups| {
try name2offset.putNoClobber(user.name, try math.cast(u32, blob.items.len));
scratch = try allocator.realloc(scratch, usergroups.len);
scratch.len = usergroups.len;
for (usergroups) |group, i|
scratch[i] = group.gid;
compress.deltaCompress(u32, scratch) catch |err| switch (err) {
error.NotSorted => unreachable,
};
try compress.appendUvarint(&blob, usergroups.len);
for (scratch) |gid|
try compress.appendUvarint(&blob, gid);
} else {
try name2offset.putNoClobber(user.name, 0);
}
2022-03-04 10:37:07 +02:00
}
return UserGids{
.name2offset = name2offset,
.blob = blob.toOwnedSlice(),
};
}
2022-03-05 06:08:01 +02:00
pub fn usersSection(
allocator: Allocator,
corpus: *const Corpus,
2022-03-05 10:19:42 +02:00
gids: *const UserGids,
shells: *const ShellSections,
2022-03-06 13:11:06 +02:00
) error{ OutOfMemory, Overflow }![]const u8 {
2022-03-05 10:19:42 +02:00
// as of writing each user takes 15 bytes + strings + padding, padded to
2022-03-06 13:11:06 +02:00
// 8 bytes. 24 is an optimistic lower bound for an average record size.
2022-03-05 10:19:42 +02:00
var buf = try ArrayList(u8).initCapacity(allocator, 24 * corpus.users.len);
2022-03-06 13:11:06 +02:00
for (corpus.users) |user| {
const offset = gids.name2offset.get(user.name).?;
std.debug.assert(offset & 7 == 0);
try userImport.PackedUserConst.packTo(
&buf,
user,
@truncate(u29, @shrExact(offset, 3)),
shells.indices.get,
);
}
2022-03-05 10:19:42 +02:00
return buf.toOwnedSlice();
2022-03-05 06:08:01 +02:00
}
pub fn groupMembers(allocator: Allocator, corpus: *const Corpus) Allocator.Error!void {
var buf: [compress.maxVarintLen64]u8 = undefined;
var offsets = ArrayListUnmanaged(usize).initCapacity(
allocator,
corpus.groups.len,
);
var bytes = ArrayList(u8).init(allocator);
var offset: usize = 0;
for (corpus.groups) |group, i| {
offsets[i] = offset;
const users = corpus.groupname2users.get(group.name).?;
const len = compress.putVarint(&buf, users.len);
offset += len;
try bytes.appendSlice(buf[0..len]);
for (users) |user| {
// TODO: offset into the User's record
_ = user;
2022-03-02 06:50:15 +02:00
}
}
}
2022-03-02 06:50:15 +02:00
2022-02-28 10:31:14 +02:00
// cmpUser compares two users for sorting. By username's utf8 codepoints, ascending.
fn cmpUser(_: void, a: User, b: User) bool {
var utf8_a = (unicode.Utf8View.init(a.name) catch unreachable).iterator();
var utf8_b = (unicode.Utf8View.init(b.name) catch unreachable).iterator();
while (utf8_a.nextCodepoint()) |codepoint_a| {
if (utf8_b.nextCodepoint()) |codepoint_b| {
2022-03-02 06:18:19 +02:00
if (codepoint_a == codepoint_b) {
continue;
} else {
2022-02-28 10:31:14 +02:00
return codepoint_a < codepoint_b;
}
}
2022-03-02 06:18:19 +02:00
// a is a prefix of b. It is thus shorter.
return false;
2022-02-28 10:31:14 +02:00
}
// b is a prefix of a
return true;
}
2022-03-02 06:02:04 +02:00
fn cmpUserPtr(context: void, a: *const User, b: *const User) bool {
return cmpUser(context, a.*, b.*);
}
2022-02-28 10:31:14 +02:00
fn cmpGroup(_: void, a: Group, b: Group) bool {
return a.gid < b.gid;
}
2022-03-02 06:02:04 +02:00
fn cmpGroupPtr(context: void, a: *const Group, b: *const Group) bool {
return cmpGroup(context, a.*, b.*);
}
2022-02-28 10:31:14 +02:00
const testing = std.testing;
2022-03-02 11:05:20 +02:00
fn testCorpus(allocator: Allocator) !Corpus {
2022-03-01 15:25:38 +02:00
const users = [_]User{ User{
2022-03-02 11:05:20 +02:00
.uid = 128,
.gid = 128,
2022-02-28 10:31:14 +02:00
.name = "vidmantas",
.gecos = "Vidmantas Kaminskas",
.home = "/home/vidmantas",
.shell = "/bin/bash",
}, User{
.uid = 0,
2022-03-02 11:05:20 +02:00
.gid = math.maxInt(u32),
2022-02-28 10:31:14 +02:00
.name = "Name" ** 8,
.gecos = "Gecos" ** 51,
.home = "Home" ** 16,
.shell = "She.LllL" ** 8,
}, User{
.uid = 1002,
.gid = 1002,
.name = "svc-bar",
.gecos = "",
.home = "/",
.shell = "/",
2022-03-04 10:37:07 +02:00
}, User{
.uid = 65534,
.gid = 65534,
.name = "nobody",
.gecos = "nobody",
.home = "/nonexistent",
.shell = "/usr/sbin/nologin",
2022-02-28 10:31:14 +02:00
} };
2022-03-01 11:01:45 +02:00
var members1 = try groupImport.someMembers(
allocator,
&[_][]const u8{"vidmantas"},
);
2022-03-01 05:47:44 +02:00
defer members1.deinit();
2022-03-01 11:01:45 +02:00
var members2 = try groupImport.someMembers(
allocator,
2022-03-01 15:25:38 +02:00
&[_][]const u8{ "svc-bar", "vidmantas" },
2022-03-01 11:01:45 +02:00
);
2022-03-01 05:47:44 +02:00
defer members2.deinit();
2022-03-01 11:01:45 +02:00
var members3 = try groupImport.someMembers(
allocator,
&[_][]const u8{ "svc-bar", "Name" ** 8, "vidmantas" },
);
2022-03-01 05:47:44 +02:00
defer members3.deinit();
2022-03-01 15:25:38 +02:00
const groups = [_]Group{ Group{
2022-03-02 11:05:20 +02:00
.gid = 128,
2022-02-28 10:31:14 +02:00
.name = "vidmantas",
2022-03-01 05:47:44 +02:00
.members = members1,
2022-02-28 10:31:14 +02:00
}, Group{
.gid = 9999,
2022-03-01 11:01:45 +02:00
.name = "all",
2022-03-01 05:47:44 +02:00
.members = members3,
2022-03-01 15:25:38 +02:00
}, Group{
2022-03-02 11:05:20 +02:00
.gid = 0,
2022-03-01 15:25:38 +02:00
.name = "service-account",
.members = members2,
2022-02-28 10:31:14 +02:00
} };
2022-03-02 11:05:20 +02:00
return try Corpus.init(allocator, users[0..], groups[0..]);
}
test "test corpus" {
var corpus = try testCorpus(testing.allocator);
2022-03-01 05:47:44 +02:00
defer corpus.deinit();
2022-03-02 11:05:20 +02:00
2022-03-01 05:47:44 +02:00
try testing.expectEqualStrings(corpus.users[0].name, "Name" ** 8);
2022-03-04 10:37:07 +02:00
try testing.expectEqualStrings(corpus.users[1].name, "nobody");
try testing.expectEqualStrings(corpus.users[2].name, "svc-bar");
try testing.expectEqualStrings(corpus.users[3].name, "vidmantas");
2022-03-01 11:01:45 +02:00
2022-03-01 15:25:38 +02:00
try testing.expectEqual(corpus.name2user.get("404"), null);
2022-03-02 11:05:20 +02:00
try testing.expectEqual(corpus.name2user.get("vidmantas").?.uid, 128);
2022-03-01 15:25:38 +02:00
try testing.expectEqual(corpus.uid2user.get(42), null);
2022-03-02 11:05:20 +02:00
try testing.expectEqual(corpus.uid2user.get(128).?.gid, 128);
2022-03-01 15:25:38 +02:00
try testing.expectEqual(corpus.name2group.get("404"), null);
2022-03-02 11:05:20 +02:00
try testing.expectEqual(corpus.name2group.get("vidmantas").?.gid, 128);
2022-03-01 15:25:38 +02:00
try testing.expectEqual(corpus.gid2group.get(42), null);
2022-03-02 11:05:20 +02:00
try testing.expectEqual(corpus.gid2group.get(128).?.gid, 128);
2022-03-01 15:25:38 +02:00
const membersOfAll = corpus.groupname2users.get("all").?;
2022-03-02 06:02:04 +02:00
try testing.expectEqualStrings(membersOfAll[0].name, "Name" ** 8);
try testing.expectEqualStrings(membersOfAll[1].name, "svc-bar");
try testing.expectEqualStrings(membersOfAll[2].name, "vidmantas");
2022-03-02 06:02:45 +02:00
try testing.expectEqual(corpus.groupname2users.get("404"), null);
2022-03-01 15:25:38 +02:00
const groupsOfVidmantas = corpus.username2groups.get("vidmantas").?;
2022-03-02 11:05:20 +02:00
try testing.expectEqual(groupsOfVidmantas[0].gid, 0);
try testing.expectEqual(groupsOfVidmantas[1].gid, 128);
2022-03-01 15:25:38 +02:00
try testing.expectEqual(groupsOfVidmantas[2].gid, 9999);
2022-03-04 10:37:07 +02:00
try testing.expectEqual(corpus.username2groups.get("nobody"), null);
try testing.expectEqual(corpus.username2groups.get("doesnotexist"), null);
2022-02-28 10:31:14 +02:00
}
2022-03-03 18:05:46 +02:00
test "test sections" {
const allocator = testing.allocator;
var corpus = try testCorpus(allocator);
defer corpus.deinit();
const bdz_gid = try bdzGid(allocator, &corpus);
2022-03-03 18:05:46 +02:00
defer allocator.free(bdz_gid);
const bdz_groupname = try bdzGroupname(allocator, &corpus);
2022-03-03 18:05:46 +02:00
defer allocator.free(bdz_groupname);
const bdz_uid = try bdzUid(allocator, &corpus);
2022-03-03 18:05:46 +02:00
defer allocator.free(bdz_uid);
const bdz_username = try bdzUsername(allocator, &corpus);
2022-03-03 18:05:46 +02:00
defer allocator.free(bdz_username);
var shell_sections = try shellSections(allocator, &corpus);
2022-03-05 06:08:01 +02:00
defer shell_sections.deinit();
2022-03-04 10:37:07 +02:00
var user_gids = try userGids(allocator, &corpus);
defer user_gids.deinit(allocator);
2022-03-05 10:19:42 +02:00
var users_section = try usersSection(
allocator,
&corpus,
&user_gids,
&shell_sections,
);
defer allocator.free(users_section);
}
test "userGids" {
const allocator = testing.allocator;
var corpus = try testCorpus(allocator);
defer corpus.deinit();
var user_gids = try userGids(allocator, &corpus);
defer user_gids.deinit(allocator);
2022-03-06 09:57:44 +02:00
for (corpus.users) |user| {
const groups = corpus.username2groups.get(user.name);
const offset = user_gids.name2offset.get(user.name);
if (groups == null) {
try testing.expect(offset == null);
continue;
}
var vit = try compress.VarintSliceIterator(user_gids.blob[offset.?..]);
var it = compress.DeltaDecompressionIterator(&vit);
try testing.expectEqual(it.remaining(), groups.?.len);
var i: usize = 0;
while (try it.next()) |gid| : (i += 1) {
try testing.expectEqual(gid, groups.?[i].gid);
}
}
2022-03-03 18:05:46 +02:00
}
2022-03-02 11:05:20 +02:00
test "pack gids" {
const allocator = testing.allocator;
var corpus = try testCorpus(allocator);
defer corpus.deinit();
const cmph_gid = try cmph.pack_u32(allocator, corpus.groupsMulti.items(.gid));
defer allocator.free(cmph_gid);
const k1 = bdz.search_u32(cmph_gid, 0);
const k2 = bdz.search_u32(cmph_gid, 128);
const k3 = bdz.search_u32(cmph_gid, 9999);
var hashes = &[_]u32{ k1, k2, k3 };
sort.sort(u32, hashes, {}, comptime sort.asc(u32));
for (hashes) |hash, i|
try testing.expectEqual(i, hash);
}
2022-02-28 10:31:14 +02:00
fn testUser(name: []const u8) User {
var result = std.mem.zeroes(User);
result.name = name;
return result;
}
test "users compare function" {
const a = testUser("a");
const b = testUser("b");
const bb = testUser("bb");
try testing.expect(cmpUser({}, a, b));
try testing.expect(!cmpUser({}, b, a));
try testing.expect(cmpUser({}, a, bb));
try testing.expect(!cmpUser({}, bb, a));
try testing.expect(cmpUser({}, b, bb));
try testing.expect(!cmpUser({}, bb, b));
}