master
  1//! Globally unique hierarchical identifier made of a sequence of integers.
  2//!
  3//! Commonly used to identify standards, algorithms, certificate extensions,
  4//! organizations, or policy documents.
  5encoded: []const u8,
  6
  7pub const InitError = std.fmt.ParseIntError || error{MissingPrefix} || std.Io.Writer.Error;
  8
  9pub fn fromDot(dot_notation: []const u8, out: []u8) InitError!Oid {
 10    var split = std.mem.splitScalar(u8, dot_notation, '.');
 11    const first_str = split.next() orelse return error.MissingPrefix;
 12    const second_str = split.next() orelse return error.MissingPrefix;
 13
 14    const first = try std.fmt.parseInt(u8, first_str, 10);
 15    const second = try std.fmt.parseInt(u8, second_str, 10);
 16
 17    var writer: std.Io.Writer = .fixed(out);
 18
 19    try writer.writeByte(first * 40 + second);
 20
 21    var i: usize = 1;
 22    while (split.next()) |s| {
 23        var parsed = try std.fmt.parseUnsigned(Arc, s, 10);
 24        const n_bytes = if (parsed == 0) 0 else std.math.log(Arc, encoding_base, parsed);
 25
 26        for (0..n_bytes) |j| {
 27            const place = std.math.pow(Arc, encoding_base, n_bytes - @as(Arc, @intCast(j)));
 28            const digit: u8 = @intCast(@divFloor(parsed, place));
 29
 30            try writer.writeByte(digit | 0x80);
 31            parsed -= digit * place;
 32
 33            i += 1;
 34        }
 35        try writer.writeByte(@intCast(parsed));
 36        i += 1;
 37    }
 38
 39    return .{ .encoded = writer.buffered() };
 40}
 41
 42test fromDot {
 43    var buf: [256]u8 = undefined;
 44    for (test_cases) |t| {
 45        const actual = try fromDot(t.dot_notation, &buf);
 46        try std.testing.expectEqualSlices(u8, t.encoded, actual.encoded);
 47    }
 48}
 49
 50pub fn toDot(self: Oid, writer: anytype) @TypeOf(writer).Error!void {
 51    const encoded = self.encoded;
 52    const first = @divTrunc(encoded[0], 40);
 53    const second = encoded[0] - first * 40;
 54    try writer.print("{d}.{d}", .{ first, second });
 55
 56    var i: usize = 1;
 57    while (i != encoded.len) {
 58        const n_bytes: usize = brk: {
 59            var res: usize = 1;
 60            var j: usize = i;
 61            while (encoded[j] & 0x80 != 0) {
 62                res += 1;
 63                j += 1;
 64            }
 65            break :brk res;
 66        };
 67
 68        var n: usize = 0;
 69        for (0..n_bytes) |j| {
 70            const place = std.math.pow(usize, encoding_base, n_bytes - j - 1);
 71            n += place * (encoded[i] & 0b01111111);
 72            i += 1;
 73        }
 74        try writer.print(".{d}", .{n});
 75    }
 76}
 77
 78test toDot {
 79    var buf: [256]u8 = undefined;
 80
 81    for (test_cases) |t| {
 82        var stream: std.Io.Writer = .fixed(&buf);
 83        try toDot(Oid{ .encoded = t.encoded }, &stream);
 84        try std.testing.expectEqualStrings(t.dot_notation, stream.written());
 85    }
 86}
 87
 88const TestCase = struct {
 89    encoded: []const u8,
 90    dot_notation: []const u8,
 91
 92    pub fn init(comptime hex: []const u8, dot_notation: []const u8) TestCase {
 93        return .{ .encoded = &hexToBytes(hex), .dot_notation = dot_notation };
 94    }
 95};
 96
 97const test_cases = [_]TestCase{
 98    // https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier
 99    TestCase.init("2b0601040182371514", "1.3.6.1.4.1.311.21.20"),
100    // https://luca.ntop.org/Teaching/Appunti/asn1.html
101    TestCase.init("2a864886f70d", "1.2.840.113549"),
102    // https://www.sysadmins.lv/blog-en/how-to-encode-object-identifier-to-an-asn1-der-encoded-string.aspx
103    TestCase.init("2a868d20", "1.2.100000"),
104    TestCase.init("2a864886f70d01010b", "1.2.840.113549.1.1.11"),
105    TestCase.init("2b6570", "1.3.101.112"),
106};
107
108pub const asn1_tag = asn1.Tag.init(.oid, false, .universal);
109
110pub fn decodeDer(decoder: *der.Decoder) !Oid {
111    const ele = try decoder.element(asn1_tag.toExpected());
112    return Oid{ .encoded = decoder.view(ele) };
113}
114
115pub fn encodeDer(self: Oid, encoder: *der.Encoder) !void {
116    try encoder.tagBytes(asn1_tag, self.encoded);
117}
118
119fn encodedLen(dot_notation: []const u8) usize {
120    var buf: [256]u8 = undefined;
121    const oid = fromDot(dot_notation, &buf) catch unreachable;
122    return oid.encoded.len;
123}
124
125/// Returns encoded bytes of OID.
126fn encodeComptime(comptime dot_notation: []const u8) [encodedLen(dot_notation)]u8 {
127    @setEvalBranchQuota(4000);
128    comptime var buf: [256]u8 = undefined;
129    const oid = comptime fromDot(dot_notation, &buf) catch unreachable;
130    return oid.encoded[0..oid.encoded.len].*;
131}
132
133test encodeComptime {
134    try std.testing.expectEqual(
135        hexToBytes("2b0601040182371514"),
136        comptime encodeComptime("1.3.6.1.4.1.311.21.20"),
137    );
138}
139
140pub fn fromDotComptime(comptime dot_notation: []const u8) Oid {
141    const tmp = comptime encodeComptime(dot_notation);
142    return Oid{ .encoded = &tmp };
143}
144
145/// Maps of:
146/// - Oid -> enum
147/// - Enum -> oid
148pub fn StaticMap(comptime Enum: type) type {
149    const enum_info = @typeInfo(Enum).@"enum";
150    const EnumToOid = std.EnumArray(Enum, []const u8);
151    const ReturnType = struct {
152        oid_to_enum: std.StaticStringMap(Enum),
153        enum_to_oid: EnumToOid,
154
155        pub fn oidToEnum(self: @This(), encoded: []const u8) ?Enum {
156            return self.oid_to_enum.get(encoded);
157        }
158
159        pub fn enumToOid(self: @This(), value: Enum) Oid {
160            const bytes = self.enum_to_oid.get(value);
161            return .{ .encoded = bytes };
162        }
163    };
164
165    return struct {
166        pub fn initComptime(comptime key_pairs: anytype) ReturnType {
167            const struct_info = @typeInfo(@TypeOf(key_pairs)).@"struct";
168            const error_msg = "Each field of '" ++ @typeName(Enum) ++ "' must map to exactly one OID";
169            if (!enum_info.is_exhaustive or enum_info.fields.len != struct_info.fields.len) {
170                @compileError(error_msg);
171            }
172
173            comptime var enum_to_oid = EnumToOid.initUndefined();
174
175            const KeyPair = struct { []const u8, Enum };
176            comptime var static_key_pairs: [enum_info.fields.len]KeyPair = undefined;
177
178            comptime for (enum_info.fields, 0..) |f, i| {
179                if (!@hasField(@TypeOf(key_pairs), f.name)) {
180                    @compileError("Field '" ++ f.name ++ "' missing Oid.StaticMap entry");
181                }
182                const encoded = &encodeComptime(@field(key_pairs, f.name));
183                const tag: Enum = @enumFromInt(f.value);
184                static_key_pairs[i] = .{ encoded, tag };
185                enum_to_oid.set(tag, encoded);
186            };
187
188            const oid_to_enum = std.StaticStringMap(Enum).initComptime(static_key_pairs);
189            if (oid_to_enum.values().len != enum_info.fields.len) @compileError(error_msg);
190
191            return ReturnType{ .oid_to_enum = oid_to_enum, .enum_to_oid = enum_to_oid };
192        }
193    };
194}
195
196/// Strictly for testing.
197fn hexToBytes(comptime hex: []const u8) [hex.len / 2]u8 {
198    var res: [hex.len / 2]u8 = undefined;
199    _ = std.fmt.hexToBytes(&res, hex) catch unreachable;
200    return res;
201}
202
203const std = @import("std");
204const Oid = @This();
205const Arc = u32;
206const encoding_base = 128;
207const Allocator = std.mem.Allocator;
208const der = @import("der.zig");
209const asn1 = @import("../asn1.zig");