lib/closure/src/schema/value.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 pub const name_bytes_max: usize = 64;
  4 pub const descriptor_bytes_max: usize = 192;
  5 pub const digest_bytes: usize = 32;
  6 pub const digest_hex_bytes: usize = digest_bytes * 2;
  7 
  8 pub const TextError = error{
  9     EmptyText,
 10     TextTooLong,
 11     InvalidText,
 12 };
 13 
 14 pub const DigestParseError = error{
 15     InvalidDigestLength,
 16     InvalidDigestByte,
 17 };
 18 
 19 pub const Name = FixedText(name_bytes_max);
 20 pub const Descriptor = FixedText(descriptor_bytes_max);
 21 
 22 pub fn FixedText(comptime bytes_max: usize) type {
 23     return extern struct {
 24         len: u16,
 25         bytes: [bytes_max]u8,
 26 
 27         const Self = @This();
 28 
 29         pub const Error: type = TextError;
 30 
 31         pub fn init(input: []const u8) Error!Self {
 32             if (input.len == 0) return error.EmptyText;
 33             if (input.len > bytes_max) return error.TextTooLong;
 34             var value = Self{
 35                 .len = @intCast(input.len),
 36                 .bytes = @splat(0),
 37             };
 38             for (input, 0..) |byte, index| {
 39                 if (byte < 0x20 or byte == 0x7f) {
 40                     return error.InvalidText;
 41                 }
 42                 value.bytes[index] = byte;
 43             }
 44             return value;
 45         }
 46 
 47         pub fn empty() Self {
 48             return .{
 49                 .len = 0,
 50                 .bytes = @splat(0),
 51             };
 52         }
 53 
 54         pub fn slice(self: *const Self) []const u8 {
 55             return self.bytes[0..self.len];
 56         }
 57 
 58         pub fn isEmpty(self: *const Self) bool {
 59             return self.len == 0;
 60         }
 61 
 62         pub fn eql(self: *const Self, other: *const Self) bool {
 63             return std.mem.eql(u8, self.slice(), other.slice());
 64         }
 65     };
 66 }
 67 
 68 pub const Digest = extern struct {
 69     bytes: [digest_bytes]u8,
 70 
 71     pub const ParseError: type = DigestParseError;
 72 
 73     pub fn zero() Digest {
 74         return .{ .bytes = @splat(0) };
 75     }
 76 
 77     pub fn fromHex(input: []const u8) ParseError!Digest {
 78         if (input.len != digest_hex_bytes) {
 79             return error.InvalidDigestLength;
 80         }
 81         var output = Digest.zero();
 82         for (0..digest_bytes) |index| {
 83             const high = try nibble(input[index * 2]);
 84             const low = try nibble(input[index * 2 + 1]);
 85             output.bytes[index] = (high << 4) | low;
 86         }
 87         return output;
 88     }
 89 
 90     pub fn isKnown(self: *const Digest) bool {
 91         for (self.bytes) |byte| {
 92             if (byte != 0) return true;
 93         }
 94         return false;
 95     }
 96 
 97     pub fn eql(self: *const Digest, other: *const Digest) bool {
 98         return std.mem.eql(u8, &self.bytes, &other.bytes);
 99     }
100 
101     pub fn hex(self: *const Digest) [digest_hex_bytes]u8 {
102         return std.fmt.bytesToHex(self.bytes, .lower);
103     }
104 };
105 
106 pub const ProfileRef = extern struct {
107     required: bool,
108     id: Name,
109     source: Name,
110     body_sha256: Digest,
111 
112     pub fn known(self: *const ProfileRef) bool {
113         if (!self.required) return true;
114         return !self.id.isEmpty() and
115             !self.source.isEmpty() and
116             self.body_sha256.isKnown();
117     }
118 
119     pub fn absent() ProfileRef {
120         return .{
121             .required = false,
122             .id = Name.empty(),
123             .source = Name.empty(),
124             .body_sha256 = Digest.zero(),
125         };
126     }
127 };
128 
129 pub const ProfileSet = extern struct {
130     executable: ProfileRef,
131     service_trust: ProfileRef,
132     model_origin: ProfileRef,
133     bootstrap: ProfileRef,
134 
135     pub fn known(self: *const ProfileSet) bool {
136         return self.executable.known() and
137             self.service_trust.known() and
138             self.model_origin.known() and
139             self.bootstrap.known();
140     }
141 };
142 
143 fn nibble(byte: u8) Digest.ParseError!u8 {
144     if (byte >= '0' and byte <= '9') return byte - '0';
145     if (byte >= 'a' and byte <= 'f') return byte - 'a' + 10;
146     return error.InvalidDigestByte;
147 }
148 
149 test "fixed text rejects empty oversized and control input" {
150     try std.testing.expectError(error.EmptyText, Name.init(""));
151     var oversized: [name_bytes_max + 1]u8 = @splat('a');
152     try std.testing.expectError(
153         error.TextTooLong,
154         Name.init(&oversized),
155     );
156     try std.testing.expectError(
157         error.InvalidText,
158         Name.init("bad\nname"),
159     );
160 }
161 
162 test "digest round trips canonical lowercase hexadecimal" {
163     const source =
164         "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
165     const digest = try Digest.fromHex(source);
166     try std.testing.expect(digest.isKnown());
167     try std.testing.expectEqualStrings(source, &digest.hex());
168     try std.testing.expectError(
169         error.InvalidDigestByte,
170         Digest.fromHex(
171             "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeg",
172         ),
173     );
174 }