| 1 | const std = @import("std"); |
| 2 | const extras = @import("extras"); |
| 3 | |
| 4 | const base = 36; |
| 5 | const tmin = 1; |
| 6 | const tmax = 26; |
| 7 | const skew = 38; |
| 8 | const damp = 700; |
| 9 | const initial_bias = 72; |
| 10 | const initial_n = 128; |
| 11 | const delimiter = '-'; |
| 12 | |
| 13 | pub fn decode(map: *extras.ManyArrayList(u8), map_n: usize) !void { |
| 14 | var input = map.items(map_n); |
| 15 | var offset: usize = 0; |
| 16 | const initial_len = input.len; |
| 17 | // std.debug.assert(std.mem.startsWith(u8, map.items(n), "xn--")); |
| 18 | offset += 4; |
| 19 | input = input[offset..initial_len]; |
| 20 | |
| 21 | // let n = initial_n |
| 22 | var n: u21 = 128; |
| 23 | // let i = 0 |
| 24 | var i: u32 = 0; |
| 25 | // let bias = initial_bias |
| 26 | var bias: u32 = initial_bias; |
| 27 | // let output = an empty string indexed from 0 |
| 28 | // output is ascii so output_length as codepoint or bytes is the same |
| 29 | var output_length: u32 = 0; |
| 30 | // consume all code points before the last delimiter (if there is one) and copy them to output, fail on any non-basic code point |
| 31 | // if more than zero code points were consumed then consume one more (which will be the last delimiter) |
| 32 | if (std.mem.lastIndexOfScalar(u8, input, delimiter)) |idx| { |
| 33 | // we're passing input as what we're appending so we have to ensure the backing allocation is not invalidated by calling append |
| 34 | try map.list.ensureUnusedCapacity(map.allocator, idx); |
| 35 | input = map.items(map_n)[offset..initial_len]; |
| 36 | try map.appendSlice(map_n, input[0..idx]); |
| 37 | offset += idx + 1; |
| 38 | input = input[idx + 1 ..]; |
| 39 | output_length += @intCast(idx); |
| 40 | } |
| 41 | // while the input is not exhausted do begin |
| 42 | while (offset < initial_len) { |
| 43 | // let oldi = i |
| 44 | const oldi = i; |
| 45 | // let w = 1 |
| 46 | var w: u32 = 1; |
| 47 | // for k = base to infinity in steps of base do begin |
| 48 | var k: u32 = base; |
| 49 | while (true) : (k += base) { |
| 50 | // consume a code point, or fail if there was none to consume |
| 51 | if (input.len == 0) return error.InvalidPunycode; |
| 52 | const sl = input[0 .. std.unicode.utf8ByteSequenceLength(input[0]) catch unreachable]; |
| 53 | offset += sl.len; |
| 54 | input = input[sl.len..]; |
| 55 | // let digit = the code point's digit-value, fail if it has none |
| 56 | const cp = std.unicode.utf8Decode(sl) catch unreachable; |
| 57 | const digit: u32 = blk: { |
| 58 | if (cp >= 'A' and cp <= 'Z') break :blk cp - 'A'; |
| 59 | if (cp >= 'a' and cp <= 'z') break :blk cp - 'a'; |
| 60 | if (cp >= '0' and cp <= '9') break :blk cp - '0' + 26; |
| 61 | return error.InvalidPunycode; |
| 62 | }; |
| 63 | // let i = i + digit * w, fail on overflow |
| 64 | i = std.math.add(u32, i, std.math.mul(u32, digit, w) catch return error.InvalidPunycode) catch return error.InvalidPunycode; |
| 65 | // let t = tmin if k <= bias {+ tmin}, or |
| 66 | // tmax if k >= bias + tmax, or k - bias otherwise |
| 67 | const t = if (k <= bias) tmin else if (k >= bias + tmax) tmax else k - bias; |
| 68 | // if digit < t then break |
| 69 | if (digit < t) break; |
| 70 | // let w = w * (base - t), fail on overflow |
| 71 | w = w * (base - t); |
| 72 | } |
| 73 | // let bias = adapt(i - oldi, length(output) + 1, test oldi is 0?) |
| 74 | bias = adapt(i - oldi, output_length + 1, oldi == 0); |
| 75 | // let n = n + i div (length(output) + 1), fail on overflow |
| 76 | n = std.math.add(u21, n, @intCast(i / (output_length + 1))) catch return error.InvalidPunycode; |
| 77 | // let i = i mod (length(output) + 1) |
| 78 | i = i % (output_length + 1); |
| 79 | // {if n is a basic code point then fail} |
| 80 | std.debug.assert(n > std.math.maxInt(u7)); |
| 81 | // insert n into output at position i |
| 82 | if (i == output_length) { |
| 83 | var buf: [4]u8 = undefined; |
| 84 | const len = std.unicode.utf8Encode(n, &buf) catch unreachable; |
| 85 | try map.appendSlice(map_n, buf[0..len]); |
| 86 | input = map.items(map_n)[offset..initial_len]; |
| 87 | output_length += 1; |
| 88 | } else { |
| 89 | var ioff: usize = 0; |
| 90 | var cpi: usize = 0; |
| 91 | var it = std.unicode.Utf8View.initUnchecked(map.items(map_n)[initial_len..]).iterator(); |
| 92 | while (it.nextCodepointSlice()) |sl| { |
| 93 | if (cpi == i) break; |
| 94 | ioff += sl.len; |
| 95 | cpi += 1; |
| 96 | } |
| 97 | var buf: [4]u8 = undefined; |
| 98 | const len = std.unicode.utf8Encode(n, &buf) catch unreachable; |
| 99 | try map.replace(map_n, initial_len + ioff, 0, buf[0..len]); |
| 100 | input = map.items(map_n)[offset..initial_len]; |
| 101 | output_length += 1; |
| 102 | } |
| 103 | // increment i |
| 104 | i += 1; |
| 105 | } |
| 106 | |
| 107 | try map.replace(map_n, 0, initial_len, ""); |
| 108 | } |
| 109 | |
| 110 | fn adapt(delta_: u32, numpoints: u32, firsttime: bool) u32 { |
| 111 | var delta = delta_; |
| 112 | // if firsttime then let delta = delta div damp |
| 113 | // else let delta = delta div 2 |
| 114 | delta = if (firsttime) delta / damp else delta / 2; |
| 115 | // let delta = delta + (delta div numpoints) |
| 116 | delta = delta + (delta / numpoints); |
| 117 | // let k = 0 |
| 118 | var k: u32 = 0; |
| 119 | // while delta > ((base - tmin) * tmax) div 2 do begin |
| 120 | while (delta > ((base - tmin) * tmax) / 2) { |
| 121 | // let delta = delta div (base - tmin) |
| 122 | delta /= base - tmin; |
| 123 | // let k = k + base |
| 124 | k += base; |
| 125 | } |
| 126 | // return k + (((base - tmin + 1) * delta) div (delta + skew)) |
| 127 | return k + (((base - tmin + 1) * delta) / (delta + skew)); |
| 128 | } |
| 129 | |
| 130 | pub fn encode(map: *extras.ManyArrayList(u8), map_n: usize) !void { |
| 131 | const initial_len = map.lengths.items[map_n]; |
| 132 | var input = map.items(map_n)[0..initial_len]; |
| 133 | if (extras.matchesAll(u8, input, std.ascii.isAscii)) return; |
| 134 | try map.appendSlice(map_n, "xn--"); |
| 135 | input = map.items(map_n)[0..initial_len]; |
| 136 | const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; |
| 137 | |
| 138 | var input_len_in_cp: u32 = 0; |
| 139 | var it = std.unicode.Utf8View.initUnchecked(input).iterator(); |
| 140 | while (it.nextCodepointSlice()) |_| input_len_in_cp += 1; |
| 141 | it.i = 0; |
| 142 | |
| 143 | // let n = initial_n |
| 144 | var n: u21 = initial_n; |
| 145 | // let delta = 0 |
| 146 | var delta: u32 = 0; |
| 147 | // let bias = initial_bias |
| 148 | var bias: u32 = initial_bias; |
| 149 | // let h = b = the number of basic code points in the input |
| 150 | // copy them to the output in order, followed by a delimiter if b > 0 |
| 151 | var h: u32 = 0; |
| 152 | var b: u32 = 0; |
| 153 | { |
| 154 | var i: usize = 0; |
| 155 | while (i < input.len) : (i += 1) { |
| 156 | if (std.ascii.isAscii(input[i])) b += 1; |
| 157 | } |
| 158 | try map.list.ensureUnusedCapacity(map.allocator, b + 1); |
| 159 | input = map.items(map_n)[0..initial_len]; |
| 160 | it.bytes = input; |
| 161 | i = 0; |
| 162 | while (i < input.len) : (i += 1) { |
| 163 | if (std.ascii.isAscii(input[i])) map.appendSlice(map_n, &.{input[i]}) catch unreachable; |
| 164 | } |
| 165 | if (b > 0) map.appendSlice(map_n, &.{delimiter}) catch unreachable; |
| 166 | h = b; |
| 167 | } |
| 168 | // {if the input contains a non-basic code point < n then fail} |
| 169 | // while h < length(input) do begin |
| 170 | while (h < input_len_in_cp) { |
| 171 | // let m = the minimum {non-basic} code point >= n in the input |
| 172 | const m = get_m(it.bytes, n); |
| 173 | // let delta = delta + (m - n) * (h + 1), fail on overflow |
| 174 | delta = delta + (m - n) * (h + 1); |
| 175 | if (false) return error.InvalidPunycode; |
| 176 | // let n = m |
| 177 | n = m; |
| 178 | // for each code point c in the input (in order) do begin |
| 179 | it.i = 0; |
| 180 | while (it.nextCodepointSlice()) |sl| { |
| 181 | const c = std.unicode.utf8Decode(sl) catch unreachable; |
| 182 | // if c < n {or c is basic} then increment delta, fail on overflow |
| 183 | if (c < n) { |
| 184 | delta = std.math.add(u32, delta, 1) catch return error.InvalidPunycode; |
| 185 | } |
| 186 | // if c == n then begin |
| 187 | if (c == n) { |
| 188 | // let q = delta |
| 189 | var q = delta; |
| 190 | // for k = base to infinity in steps of base do begin |
| 191 | var k: u23 = base; |
| 192 | while (true) : (k += base) { |
| 193 | // let t = tmin if k <= bias {+ tmin}, or |
| 194 | // tmax if k >= bias + tmax, or k - bias otherwise |
| 195 | const t = if (k <= bias) tmin else if (k >= bias + tmax) tmax else k - bias; |
| 196 | // if q < t then break |
| 197 | if (q < t) break; |
| 198 | // output the code point for digit t + ((q - t) mod (base - t)) |
| 199 | try map.appendSlice(map_n, &.{alphabet[t + ((q - t) % (base - t))]}); |
| 200 | it.bytes = map.items(map_n)[0..initial_len]; |
| 201 | // let q = (q - t) div (base - t) |
| 202 | q = (q - t) / (base - t); |
| 203 | } |
| 204 | // output the code point for digit q |
| 205 | try map.appendSlice(map_n, &.{alphabet[q]}); |
| 206 | it.bytes = map.items(map_n)[0..initial_len]; |
| 207 | // let bias = adapt(delta, h + 1, test h equals b?) |
| 208 | bias = adapt(delta, h + 1, h == b); |
| 209 | // let delta = 0 |
| 210 | delta = 0; |
| 211 | // increment h |
| 212 | h += 1; |
| 213 | } |
| 214 | } |
| 215 | // increment delta and n |
| 216 | delta += 1; |
| 217 | n += 1; |
| 218 | } |
| 219 | // clear input from map_n item |
| 220 | map.replace(map_n, 0, initial_len, "") catch unreachable; |
| 221 | } |
| 222 | |
| 223 | fn get_m(input: []const u8, n: u21) u21 { |
| 224 | var min: u21 = std.math.maxInt(u21); |
| 225 | var it = std.unicode.Utf8View.initUnchecked(input).iterator(); |
| 226 | while (it.nextCodepointSlice()) |sl| { |
| 227 | const cp = std.unicode.utf8Decode(sl) catch unreachable; |
| 228 | if (cp < n) continue; |
| 229 | min = @min(min, cp); |
| 230 | } |
| 231 | return min; |
| 232 | } |