main zmx / src / label.zig
Max Rydahl Andersen  ·  2026-06-29
  1const std = @import("std");
  2
  3pub const LabelError = error{
  4    LabelKeyEmpty,
  5    LabelKeyInvalidChar,
  6    LabelValueInvalidChar,
  7    LabelKeyReservedName,
  8};
  9
 10const reserved_keys = [_][]const u8{ "name", "start_dir", "cmd" };
 11
 12fn isAlnum(c: u8) bool {
 13    return (c >= 'a' and c <= 'z') or
 14        (c >= 'A' and c <= 'Z') or
 15        (c >= '0' and c <= '9');
 16}
 17
 18pub fn assertLabel(key: []const u8, value: []const u8) LabelError!void {
 19    if (key.len == 0) {
 20        return LabelError.LabelKeyEmpty;
 21    }
 22
 23    for (reserved_keys) |rk| {
 24        if (std.mem.eql(u8, key, rk)) return error.LabelKeyReservedName;
 25    }
 26
 27    for (key) |ch| {
 28        if (!isAlnum(ch) and ch != '-' and ch != '_' and ch != '.') {
 29            return LabelError.LabelKeyInvalidChar;
 30        }
 31    }
 32
 33    for (value) |ch| {
 34        if (!isAlnum(ch) and ch != '-' and ch != '_' and ch != '.') {
 35            return LabelError.LabelValueInvalidChar;
 36        }
 37    }
 38}
 39
 40pub fn labelsToU8(alloc: std.mem.Allocator, labels: std.StringHashMapUnmanaged([]u8)) ![]u8 {
 41    var out = std.ArrayList(u8).empty;
 42    var keys = std.ArrayList([]const u8).empty;
 43    defer keys.deinit(alloc);
 44
 45    var it = labels.iterator();
 46    while (it.next()) |entry| {
 47        try keys.append(alloc, entry.key_ptr.*);
 48    }
 49    std.mem.sort([]const u8, keys.items, {}, struct {
 50        fn lessThan(_: void, a: []const u8, b: []const u8) bool {
 51            return std.mem.order(u8, a, b) == .lt;
 52        }
 53    }.lessThan);
 54
 55    var idx: usize = 1;
 56    for (keys.items) |key| {
 57        defer idx += 1;
 58        const value = labels.get(key).?;
 59        try out.appendSlice(alloc, key);
 60        try out.append(alloc, '=');
 61        try out.appendSlice(alloc, value);
 62        if (idx < keys.items.len) {
 63            try out.append(alloc, ' ');
 64        }
 65    }
 66    return out.toOwnedSlice(alloc);
 67}
 68
 69pub const LabelIterator = struct {
 70    labels: []const u8,
 71    idx: usize = 0,
 72
 73    const LabelKeyValue = struct {
 74        key: []const u8,
 75        value: []const u8,
 76    };
 77
 78    pub fn init(labels: []const u8) LabelIterator {
 79        return .{
 80            .labels = labels,
 81        };
 82    }
 83
 84    pub fn next(self: *LabelIterator) ?LabelKeyValue {
 85        const labels = self.labels;
 86        while (self.idx < labels.len) {
 87            var eql_idx = self.idx;
 88            // scan to '=' char
 89            while (eql_idx < labels.len and labels[eql_idx] != '=') eql_idx += 1;
 90            if (eql_idx == labels.len) break;
 91
 92            var space_idx = eql_idx + 1;
 93            // scan to ' ' char
 94            while (space_idx < labels.len and labels[space_idx] != ' ') space_idx += 1;
 95
 96            const kv = LabelKeyValue{
 97                .key = labels[self.idx..eql_idx],
 98                .value = labels[eql_idx + 1 .. space_idx],
 99            };
100            // move the pointer so next() will start where it left off
101            self.idx = if (space_idx < labels.len) space_idx + 1 else labels.len;
102            return kv;
103        }
104
105        return null;
106    }
107};
108
109pub fn getLabelValueFromPairs(single_kv: []const u8, labels: []const u8) error{LabelKeyNotFound}![]const u8 {
110    var iter = LabelIterator.init(labels);
111    while (iter.next()) |kv| {
112        if (std.mem.eql(u8, single_kv, kv.key)) {
113            return kv.value;
114        }
115    }
116    return error.LabelKeyNotFound;
117}
118
119test "getLabelValueFromPairs" {
120    try std.testing.expect(std.mem.eql(u8, "zmx", try getLabelValueFromPairs("project", "project=zmx env=prd")));
121    try std.testing.expect(std.mem.eql(u8, "zmx", try getLabelValueFromPairs("project", "env=prd status=done project=zmx")));
122    try std.testing.expectError(error.LabelKeyNotFound, getLabelValueFromPairs("sha", "env=prd status=done project=zmx"));
123}
124
125test "assertLabel" {
126    try assertLabel("key", "");
127    try assertLabel("1337", "");
128    try assertLabel("key.key_key-key", "");
129    try std.testing.expectError(error.LabelKeyEmpty, assertLabel("", "value"));
130    try std.testing.expectError(error.LabelKeyInvalidChar, assertLabel("key key", ""));
131    try std.testing.expectError(error.LabelKeyInvalidChar, assertLabel("key:key", ""));
132    try std.testing.expectError(error.LabelKeyInvalidChar, assertLabel("key/key", ""));
133
134    try assertLabel("key", "");
135    try assertLabel("key", "1337");
136    try assertLabel("key", "value");
137    try assertLabel("key", "value.value_value-value");
138    try std.testing.expectError(error.LabelValueInvalidChar, assertLabel("key", "value value"));
139    try std.testing.expectError(error.LabelValueInvalidChar, assertLabel("key", "value:value"));
140    try std.testing.expectError(error.LabelValueInvalidChar, assertLabel("key", "value/value"));
141
142    try std.testing.expectError(error.LabelKeyReservedName, assertLabel("name", "dev"));
143    try std.testing.expectError(error.LabelKeyReservedName, assertLabel("start_dir", "dev"));
144    try std.testing.expectError(error.LabelKeyReservedName, assertLabel("cmd", "dev"));
145}