| 1 | //! A buffered DER encoder. |
| 2 | //! |
| 3 | //! Prefers calling container's `fn encodeDer(self: @This(), encoder: *der.Encoder)`. |
| 4 | //! That function should encode values, lengths, then tags. |
| 5 | buffer: ArrayListReverse, |
| 6 | /// The field tag set by a parent container. |
| 7 | /// This is needed because we might visit an implicitly tagged container with a `fn encodeDer`. |
| 8 | field_tag: ?FieldTag = null, |
| 9 | |
| 10 | pub fn init(allocator: std.mem.Allocator) Encoder { |
| 11 | return Encoder{ .buffer = ArrayListReverse.init(allocator) }; |
| 12 | } |
| 13 | |
| 14 | pub fn deinit(self: *Encoder) void { |
| 15 | self.buffer.deinit(); |
| 16 | } |
| 17 | |
| 18 | /// Encode any value. |
| 19 | pub fn any(self: *Encoder, val: anytype) !void { |
| 20 | const T = @TypeOf(val); |
| 21 | try self.anyTag(Tag.fromZig(T), val); |
| 22 | } |
| 23 | |
| 24 | test any { |
| 25 | const Enum = enum(u8) { |
| 26 | x = 0, |
| 27 | }; |
| 28 | |
| 29 | try checkAnyEncoding(u16, 0, &.{ 0x02, 0x01, 0x00 }); |
| 30 | try checkAnyEncoding(Enum, .x, &.{ 0x0a, 0x01, 0x00 }); |
| 31 | } |
| 32 | |
| 33 | fn checkAnyEncoding(T: type, value: T, expected: []const u8) !void { |
| 34 | var encoder: @This() = .init(std.testing.allocator); |
| 35 | defer encoder.deinit(); |
| 36 | |
| 37 | try encoder.any(value); |
| 38 | const encoded = try encoder.buffer.toOwnedSlice(); |
| 39 | defer std.testing.allocator.free(encoded); |
| 40 | |
| 41 | try std.testing.expectEqualSlices(u8, expected, encoded); |
| 42 | } |
| 43 | |
| 44 | fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { |
| 45 | const T = @TypeOf(val); |
| 46 | if (std.meta.hasFn(T, "encodeDer")) return try val.encodeDer(self); |
| 47 | const outer_field_tag = self.field_tag; |
| 48 | const start = self.buffer.data.len; |
| 49 | const merged_tag = self.mergedTag(tag_); |
| 50 | |
| 51 | switch (@typeInfo(T)) { |
| 52 | .@"struct" => |info| { |
| 53 | inline for (0..info.field_names.len) |i| { |
| 54 | const f_idx = info.field_names.len - i - 1; |
| 55 | const f_name = info.field_names[f_idx]; |
| 56 | const f_type = info.field_types[f_idx]; |
| 57 | const f_attrs = info.field_attrs[f_idx]; |
| 58 | const field_val = @field(val, f_name); |
| 59 | const field_tag = FieldTag.fromContainer(T, f_name); |
| 60 | |
| 61 | // > The encoding of a set value or sequence value shall not include an encoding for any |
| 62 | // > component value which is equal to its default value. |
| 63 | const is_default = if (f_attrs.@"comptime") false else if (f_attrs.defaultValue(f_type)) |default_val| brk: { |
| 64 | break :brk std.mem.eql(u8, std.mem.asBytes(&default_val), std.mem.asBytes(&field_val)); |
| 65 | } else false; |
| 66 | const is_null_optional = if (@typeInfo(f_type) == .optional) field_val == null else false; |
| 67 | |
| 68 | if (!is_default and !is_null_optional) { |
| 69 | const start2 = self.buffer.data.len; |
| 70 | self.field_tag = field_tag; |
| 71 | // will merge with self.field_tag. |
| 72 | // may mutate self.field_tag. |
| 73 | try self.anyTag(Tag.fromZig(f_type), field_val); |
| 74 | if (field_tag) |ft| { |
| 75 | if (ft.explicit) { |
| 76 | try self.length(self.buffer.data.len - start2); |
| 77 | try self.tag(ft.toTag()); |
| 78 | self.field_tag = null; |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | self.field_tag = outer_field_tag; |
| 84 | }, |
| 85 | .bool => try self.buffer.prependSlice(&[_]u8{if (val) 0xff else 0}), |
| 86 | .int => try self.int(T, val), |
| 87 | .@"enum" => |e| { |
| 88 | if (@hasDecl(T, "oids")) { |
| 89 | return self.any(T.oids.enumToOid(val)); |
| 90 | } else { |
| 91 | try self.int(e.tag_type, @backingInt(val)); |
| 92 | } |
| 93 | }, |
| 94 | .optional => if (val) |v| return try self.anyTag(tag_, v) else return, |
| 95 | .null => {}, |
| 96 | else => @compileError("cannot encode type " ++ @typeName(T)), |
| 97 | } |
| 98 | |
| 99 | try self.length(self.buffer.data.len - start); |
| 100 | try self.tag(merged_tag); |
| 101 | } |
| 102 | |
| 103 | /// Encode a tag. |
| 104 | pub fn tag(self: *Encoder, tag_: Tag) !void { |
| 105 | const t = self.mergedTag(tag_); |
| 106 | var buf: [Tag.max_encoded_len]u8 = undefined; |
| 107 | try self.buffer.prependSlice(t.encodeToSlice(&buf)); |
| 108 | } |
| 109 | |
| 110 | fn mergedTag(self: *Encoder, tag_: Tag) Tag { |
| 111 | var res = tag_; |
| 112 | if (self.field_tag) |ft| { |
| 113 | if (!ft.explicit) { |
| 114 | res.number = @fromBackingInt(@intCast(ft.number)); |
| 115 | res.class = ft.class; |
| 116 | } |
| 117 | } |
| 118 | return res; |
| 119 | } |
| 120 | |
| 121 | /// Encode a length. |
| 122 | pub fn length(self: *Encoder, len: usize) !void { |
| 123 | if (len < 128) return self.buffer.prependSlice(&.{@intCast(len)}); |
| 124 | const len32 = std.math.cast(u32, len) orelse return error.InvalidLength; |
| 125 | var buf: [@sizeOf(u32) + 1]u8 = undefined; |
| 126 | std.mem.writeInt(u32, buf[1..], len32, .big); |
| 127 | var first: usize = 1; |
| 128 | while (buf[first] == 0) first += 1; |
| 129 | buf[first - 1] = @intCast((buf.len - first) | 0x80); |
| 130 | return self.buffer.prependSlice(buf[first - 1 ..]); |
| 131 | } |
| 132 | |
| 133 | /// Encode a tag and length-prefixed bytes. |
| 134 | pub fn tagBytes(self: *Encoder, tag_: Tag, bytes: []const u8) !void { |
| 135 | try self.buffer.prependSlice(bytes); |
| 136 | try self.length(bytes.len); |
| 137 | try self.tag(tag_); |
| 138 | } |
| 139 | |
| 140 | /// Write raw bytes. The encoder builds its output back-to-front, so chained |
| 141 | /// calls should be made in reverse of the desired on-wire order. |
| 142 | pub fn prependBytes(self: *Encoder, bytes: []const u8) !void { |
| 143 | return self.buffer.prependSlice(bytes); |
| 144 | } |
| 145 | |
| 146 | fn int(self: *Encoder, comptime T: type, value: T) !void { |
| 147 | const info = @typeInfo(T).int; |
| 148 | const Unsigned = @Int(.unsigned, info.bits); |
| 149 | const pad: u8 = if (info.signedness == .signed and value < 0) 0xff else 0; |
| 150 | var buf: [@sizeOf(Unsigned) + 1]u8 = undefined; |
| 151 | buf[0] = pad; |
| 152 | std.mem.writeInt(Unsigned, buf[1..], @bitCast(value), .big); |
| 153 | |
| 154 | var first: usize = 0; |
| 155 | while (first + 1 < buf.len and buf[first] == pad and (buf[first + 1] ^ pad) & 0x80 == 0) first += 1; |
| 156 | try self.buffer.prependSlice(buf[first..]); |
| 157 | } |
| 158 | |
| 159 | test int { |
| 160 | const allocator = std.testing.allocator; |
| 161 | var encoder = Encoder.init(allocator); |
| 162 | defer encoder.deinit(); |
| 163 | |
| 164 | try encoder.int(u8, 0); |
| 165 | try std.testing.expectEqualSlices(u8, &.{0}, encoder.buffer.data); |
| 166 | |
| 167 | encoder.buffer.clearAndFree(); |
| 168 | try encoder.int(u16, 0x00ff); |
| 169 | try std.testing.expectEqualSlices(u8, &.{ 0, 0xff }, encoder.buffer.data); |
| 170 | |
| 171 | encoder.buffer.clearAndFree(); |
| 172 | try encoder.int(u32, 0xffff); |
| 173 | try std.testing.expectEqualSlices(u8, &.{ 0, 0xff, 0xff }, encoder.buffer.data); |
| 174 | |
| 175 | encoder.buffer.clearAndFree(); |
| 176 | try encoder.int(u32, 0x01020304); |
| 177 | try std.testing.expectEqualSlices(u8, &.{ 0x01, 0x02, 0x03, 0x04 }, encoder.buffer.data); |
| 178 | |
| 179 | encoder.buffer.clearAndFree(); |
| 180 | try encoder.int(u8, 127); |
| 181 | try std.testing.expectEqualSlices(u8, &.{0x7f}, encoder.buffer.data); |
| 182 | |
| 183 | encoder.buffer.clearAndFree(); |
| 184 | try encoder.int(u16, 128); |
| 185 | try std.testing.expectEqualSlices(u8, &.{ 0, 0x80 }, encoder.buffer.data); |
| 186 | |
| 187 | encoder.buffer.clearAndFree(); |
| 188 | try encoder.int(u16, 256); |
| 189 | try std.testing.expectEqualSlices(u8, &.{ 0x01, 0x00 }, encoder.buffer.data); |
| 190 | |
| 191 | encoder.buffer.clearAndFree(); |
| 192 | try encoder.int(u8, 128); |
| 193 | try std.testing.expectEqualSlices(u8, &.{ 0, 0x80 }, encoder.buffer.data); |
| 194 | |
| 195 | encoder.buffer.clearAndFree(); |
| 196 | try encoder.int(u8, 255); |
| 197 | try std.testing.expectEqualSlices(u8, &.{ 0, 0xff }, encoder.buffer.data); |
| 198 | |
| 199 | encoder.buffer.clearAndFree(); |
| 200 | try encoder.int(u16, 0x8000); |
| 201 | try std.testing.expectEqualSlices(u8, &.{ 0, 0x80, 0 }, encoder.buffer.data); |
| 202 | |
| 203 | encoder.buffer.clearAndFree(); |
| 204 | try encoder.int(i8, -1); |
| 205 | try std.testing.expectEqualSlices(u8, &.{0xff}, encoder.buffer.data); |
| 206 | |
| 207 | encoder.buffer.clearAndFree(); |
| 208 | try encoder.int(i8, -128); |
| 209 | try std.testing.expectEqualSlices(u8, &.{0x80}, encoder.buffer.data); |
| 210 | |
| 211 | encoder.buffer.clearAndFree(); |
| 212 | try encoder.int(i16, -129); |
| 213 | try std.testing.expectEqualSlices(u8, &.{ 0xff, 0x7f }, encoder.buffer.data); |
| 214 | } |
| 215 | |
| 216 | test length { |
| 217 | const allocator = std.testing.allocator; |
| 218 | var encoder = Encoder.init(allocator); |
| 219 | defer encoder.deinit(); |
| 220 | |
| 221 | try encoder.length(127); |
| 222 | try std.testing.expectEqualSlices(u8, &.{0x7f}, encoder.buffer.data); |
| 223 | |
| 224 | encoder.buffer.clearAndFree(); |
| 225 | try encoder.length(128); |
| 226 | try std.testing.expectEqualSlices(u8, &.{ 0x81, 0x80 }, encoder.buffer.data); |
| 227 | |
| 228 | encoder.buffer.clearAndFree(); |
| 229 | try encoder.length(255); |
| 230 | try std.testing.expectEqualSlices(u8, &.{ 0x81, 0xff }, encoder.buffer.data); |
| 231 | |
| 232 | encoder.buffer.clearAndFree(); |
| 233 | try encoder.length(256); |
| 234 | try std.testing.expectEqualSlices(u8, &.{ 0x82, 0x01, 0x00 }, encoder.buffer.data); |
| 235 | |
| 236 | encoder.buffer.clearAndFree(); |
| 237 | try encoder.length(65535); |
| 238 | try std.testing.expectEqualSlices(u8, &.{ 0x82, 0xff, 0xff }, encoder.buffer.data); |
| 239 | |
| 240 | encoder.buffer.clearAndFree(); |
| 241 | try encoder.length(65536); |
| 242 | try std.testing.expectEqualSlices(u8, &.{ 0x83, 0x01, 0x00, 0x00 }, encoder.buffer.data); |
| 243 | } |
| 244 | |
| 245 | const std = @import("std"); |
| 246 | const Oid = @import("../Oid.zig"); |
| 247 | const asn1 = @import("../../asn1.zig"); |
| 248 | const ArrayListReverse = @import("./ArrayListReverse.zig"); |
| 249 | const Tag = asn1.Tag; |
| 250 | const FieldTag = asn1.FieldTag; |
| 251 | const Encoder = @This(); |