lib/reticulum/src/destination/name.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const reticulum = @import("../root.zig");
3
4 pub const Limits = struct {
5 name_bytes_max: u16,
6 };
7
8 pub const NameError = error{
9 DotInAppName,
10 DotInAspect,
11 NameTooLong,
12 OutputTooSmall,
13 };
14
15 const identity_hex_bytes: usize = reticulum.hash.truncated_bytes * 2;
16
17 const Plan = struct {
18 base_bytes: usize,
19 expanded_bytes: usize,
20 };
21
22 fn checkedAdd(left: usize, right: usize) NameError!usize {
23 return std.math.add(usize, left, right) catch error.NameTooLong;
24 }
25
26 fn plan(
27 limits: Limits,
28 app: []const u8,
29 aspects: []const []const u8,
30 includes_identity: bool,
31 ) NameError!Plan {
32 if (std.mem.indexOfScalar(u8, app, '.') != null) return error.DotInAppName;
33 const maximum: usize = limits.name_bytes_max;
34 if (app.len > maximum) return error.NameTooLong;
35 if (aspects.len > maximum) return error.NameTooLong;
36
37 var base_bytes = app.len;
38 const aspect_count: u16 = @intCast(aspects.len);
39 var index: u16 = 0;
40 while (index < aspect_count) : (index += 1) {
41 const aspect = aspects[index];
42 if (std.mem.indexOfScalar(u8, aspect, '.') != null) {
43 return error.DotInAspect;
44 }
45 base_bytes = try checkedAdd(base_bytes, 1);
46 base_bytes = try checkedAdd(base_bytes, aspect.len);
47 if (base_bytes > maximum) return error.NameTooLong;
48 }
49
50 var expanded_bytes = base_bytes;
51 if (includes_identity) {
52 expanded_bytes = try checkedAdd(expanded_bytes, 1 + identity_hex_bytes);
53 }
54 if (expanded_bytes > maximum) return error.NameTooLong;
55 return .{ .base_bytes = base_bytes, .expanded_bytes = expanded_bytes };
56 }
57
58 fn append(output: []u8, cursor: *usize, bytes: []const u8) void {
59 std.debug.assert(cursor.* <= output.len);
60 std.debug.assert(bytes.len <= output.len - cursor.*);
61 @memcpy(output[cursor.*..][0..bytes.len], bytes);
62 cursor.* += bytes.len;
63 }
64
65 fn writeBase(app: []const u8, aspects: []const []const u8, output: []u8) usize {
66 var cursor: usize = 0;
67 append(output, &cursor, app);
68 for (aspects) |aspect| {
69 append(output, &cursor, ".");
70 append(output, &cursor, aspect);
71 }
72 return cursor;
73 }
74
75 /// Writes the dotted name into `output` and returns it: the application name,
76 /// then each aspect after a dot, then the identity hash in lowercase hex after
77 /// another dot when the caller passes one, so a caller shows a person the full
78 /// name of a destination (*expanded name*), following Reticulum@1.5.0
79 /// RNS/Destination.py:96-112. The call returns `error.DotInAppName` or
80 /// `error.DotInAspect` for a label holding a dot, `error.NameTooLong` when the
81 /// name passes the caller's byte maximum, and `error.OutputTooSmall` when
82 /// `output` is shorter than the name.
83 pub fn expandName(
84 limits: Limits,
85 app: []const u8,
86 aspects: []const []const u8,
87 identity_hash: ?[reticulum.hash.truncated_bytes]u8,
88 output: []u8,
89 ) NameError![]u8 {
90 const layout = try plan(limits, app, aspects, identity_hash != null);
91 if (output.len < layout.expanded_bytes) return error.OutputTooSmall;
92 var cursor = writeBase(app, aspects, output);
93 std.debug.assert(cursor == layout.base_bytes);
94 if (identity_hash) |identity| {
95 const encoded = std.fmt.bytesToHex(identity, .lower);
96 append(output, &cursor, ".");
97 append(output, &cursor, &encoded);
98 }
99 std.debug.assert(cursor == layout.expanded_bytes);
100 return output[0..cursor];
101 }
102
103 /// Returns the first 10 bytes of the SHA-256 digest of the dotted name (*name
104 /// hash*), with no identity bytes in it, so a caller works out the ten bytes an
105 /// announce publishes for a name, the same for every owner of that name,
106 /// following Reticulum@1.5.0 RNS/Destination.py:120. The call returns
107 /// `error.DotInAppName`, `error.DotInAspect`, or `error.NameTooLong` on the
108 /// same terms as expanding a name.
109 pub fn nameHash(
110 limits: Limits,
111 app: []const u8,
112 aspects: []const []const u8,
113 ) NameError![reticulum.hash.name_bytes]u8 {
114 _ = try plan(limits, app, aspects, false);
115 var hasher = reticulum.hash.Hasher.init();
116 hasher.update(app);
117 for (aspects) |aspect| {
118 hasher.update(".");
119 hasher.update(aspect);
120 }
121 return hasher.finalName();
122 }
123
124 /// Returns the first 16 bytes of one digest taken over the name hash followed
125 /// by the identity hash, and over the name hash alone when the caller passes no
126 /// identity, so a sender works out the 16-byte address to put in a packet
127 /// header from the name and the owner's identity hash, following
128 /// Reticulum@1.5.0 RNS/Destination.py:116-130. Folding the owner's identity
129 /// hash in gives two parties using the same dotted name two different
130 /// addresses. The call returns `error.DotInAppName`, `error.DotInAspect`, or
131 /// `error.NameTooLong` on the same terms as expanding a name.
132 pub fn hash(
133 limits: Limits,
134 app: []const u8,
135 aspects: []const []const u8,
136 identity_hash: ?[reticulum.hash.truncated_bytes]u8,
137 ) NameError![reticulum.hash.truncated_bytes]u8 {
138 const name_hash = try nameHash(limits, app, aspects);
139 var hasher = reticulum.hash.Hasher.init();
140 hasher.update(&name_hash);
141 if (identity_hash) |identity| hasher.update(&identity);
142 return hasher.finalTruncated();
143 }