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 }