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}