-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathencoding.affine
More file actions
207 lines (190 loc) · 7.43 KB
/
Copy pathencoding.affine
File metadata and controls
207 lines (190 loc) · 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 hyperpolymath
//
// AffineScript Standard Library — Hexadecimal encoding/decoding.
//
// Roadmap: stdlib #25 ("Base64/hex") in docs/stdlib-roadmap.adoc — the
// hex half. Base64 is deferred until a `Bytes` type (stdlib #30) lands,
// since correct base64 is byte-oriented rather than integer-oriented.
//
// Lowercase, non-negative, 32-bit hexadecimal conversion built on the
// same string/number primitive layer as stdlib/string.affine —
// `len` / `string_sub` / `string_find` / `++` plus integer bit-ops
// (`&`, `>>`, `<<`, `|`), which resolve on the interpreter and Deno-ESM
// backends (verified by tests/codegen-deno/encoding_smoke). The
// linear-memory wasm backend does not yet expose `string_sub` /
// `string_find` (the same limitation as stdlib/string.affine). Integer
// `/` is deliberately avoided — it lowers to floating-point division on
// the Deno-ESM backend.
/// Lowercase hex alphabet; the character at index `v` is the digit for
/// nibble value `v` (0..15).
fn hex_alphabet() -> String = "0123456789abcdef";
/// Map a nibble to its lowercase hex digit as a one-character string.
/// Only the low four bits are used, so callers need not pre-mask.
pub fn hex_digit(nibble: Int) -> String =
string_sub(hex_alphabet(), nibble & 15, 1);
/// Encode a non-negative integer as lowercase hex with no `0x` prefix.
/// `to_hex(0) == "0"`, `to_hex(255) == "ff"`, `to_hex(4096) == "1000"`.
pub fn to_hex(n: Int) -> String {
if n == 0 {
return "0";
}
let mut acc = "";
let mut v = n;
while v > 0 {
acc = hex_digit(v & 15) ++ acc;
v = v >> 4;
}
acc
}
/// Encode `n` as lowercase hex left-padded with zeros to at least
/// `width` digits. `to_hex_padded(255, 4) == "00ff"`.
pub fn to_hex_padded(n: Int, width: Int) -> String {
let mut s = to_hex(n);
while len(s) < width {
s = "0" ++ s;
}
s
}
/// Value (0..15) of a single hex digit, accepting either case; returns
/// `-1` for a non-hex character.
pub fn hex_value(digit: String) -> Int {
let lower = string_find(hex_alphabet(), digit);
if lower >= 0 {
lower
} else {
string_find("0123456789ABCDEF", digit)
}
}
/// Decode a hex string (either case) to an integer. The input is assumed
/// to be valid hex; an invalid digit contributes `-1` from `hex_value`.
/// `from_hex("ff") == 255`, `from_hex("1000") == 4096`.
pub fn from_hex(s: String) -> Int {
let mut acc = 0;
let mut i = 0;
let n = len(s);
while i < n {
acc = (acc << 4) | hex_value(string_sub(s, i, 1));
i = i + 1;
}
acc
}
// ---------------------------------------------------------------------------
// Base64 (RFC 4648)
//
// Unlike hex, base64 is byte-oriented: it groups the *bytes* of its input
// three at a time and re-packs them as four 6-bit code points. AffineScript
// has no `Bytes` type yet (stdlib #30), but the compiler exposes the byte
// view of a `String` directly — `string_char_code_at(s, i)` returns the
// 0..255 code of the i-th byte and `string_from_char_code(c)` builds a
// one-byte string from `c & 0xff` (both on the interpreter and Deno-ESM
// backends; see the implementations in lib/interp.ml / lib/codegen_deno.ml).
// So `to_base64` / `from_base64` operate on a String whose characters are
// Latin-1 bytes (0..255) — the natural representation for ASCII text and for
// the byte buffers a `Bytes` type would later wrap. When stdlib #30 lands,
// these gain `Bytes` overloads; the String forms stay as the text path.
//
// As in the hex half, integer `/` and `%` are avoided (they lower to
// floating-point division on the Deno-ESM backend); all group/remainder
// math uses `&`, `<<`, `>>`, `|`.
/// Standard base64 alphabet (RFC 4648 §4). Index `v` (0..63) is the digit
/// for sextet value `v`.
fn b64_alphabet() -> String =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// URL- and filename-safe base64 alphabet (RFC 4648 §5): `+` and `/`
/// replaced by `-` and `_`.
fn b64_url_alphabet() -> String =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
/// Map a sextet (0..63) to its base64 digit under `alphabet`.
fn b64_digit(alphabet: String, sextet: Int) -> String =
string_sub(alphabet, sextet & 63, 1);
/// Value (0..63) of a single base64 digit under `alphabet`, or `-1` for a
/// character not in the alphabet (e.g. the `=` pad or whitespace).
fn b64_value(alphabet: String, digit: String) -> Int =
string_find(alphabet, digit);
/// Encode the bytes of `input` as base64 using `alphabet`, appending `=`
/// padding when `pad` is true. Shared core for the standard and URL-safe
/// front doors below.
fn b64_encode_with(alphabet: String, pad: Bool, input: String) -> String {
let n = len(input);
let mut out = "";
let mut i = 0;
// Whole 3-byte groups -> 4 sextets, no padding.
while i + 3 <= n {
let b0 = string_char_code_at(input, i);
let b1 = string_char_code_at(input, i + 1);
let b2 = string_char_code_at(input, i + 2);
out = out
++ b64_digit(alphabet, b0 >> 2)
++ b64_digit(alphabet, ((b0 & 3) << 4) | (b1 >> 4))
++ b64_digit(alphabet, ((b1 & 15) << 2) | (b2 >> 6))
++ b64_digit(alphabet, b2 & 63);
i = i + 3;
}
// Tail: either 1 or 2 leftover bytes.
let rem = n - i;
if rem == 1 {
let b0 = string_char_code_at(input, i);
out = out
++ b64_digit(alphabet, b0 >> 2)
++ b64_digit(alphabet, (b0 & 3) << 4);
if pad {
out = out ++ "==";
}
} else {
if rem == 2 {
let b0 = string_char_code_at(input, i);
let b1 = string_char_code_at(input, i + 1);
out = out
++ b64_digit(alphabet, b0 >> 2)
++ b64_digit(alphabet, ((b0 & 3) << 4) | (b1 >> 4))
++ b64_digit(alphabet, (b1 & 15) << 2);
if pad {
out = out ++ "=";
}
}
}
out
}
/// Standard base64 (RFC 4648 §4) with `=` padding.
/// `to_base64("") == ""`, `to_base64("f") == "Zg=="`,
/// `to_base64("foobar") == "Zm9vYmFy"`.
pub fn to_base64(input: String) -> String =
b64_encode_with(b64_alphabet(), true, input);
/// URL- and filename-safe base64 (RFC 4648 §5) without padding — the form
/// used by JWT segments. `to_base64_url("f") == "Zg"`.
pub fn to_base64_url(input: String) -> String =
b64_encode_with(b64_url_alphabet(), false, input);
/// Decode base64 text under `alphabet` to the original bytes. Characters
/// not in `alphabet` (the `=` pad, newlines, whitespace) are skipped, so a
/// single decoder handles padded and unpadded input and tolerates wrapped
/// lines. Shared core for the standard and URL-safe front doors below.
fn b64_decode_with(alphabet: String, s: String) -> String {
let n = len(s);
let mut out = "";
// Accumulate sextets into a bit buffer; emit a byte per 8 bits.
let mut buf = 0;
let mut bits = 0;
let mut i = 0;
while i < n {
let v = b64_value(alphabet, string_sub(s, i, 1));
if v >= 0 {
buf = (buf << 6) | v;
bits = bits + 6;
if bits >= 8 {
bits = bits - 8;
out = out ++ string_from_char_code((buf >> bits) & 255);
}
}
i = i + 1;
}
out
}
/// Decode standard base64 (RFC 4648 §4) to the original bytes. Accepts
/// padded or unpadded input. `from_base64("Zm9vYmFy") == "foobar"`.
pub fn from_base64(s: String) -> String =
b64_decode_with(b64_alphabet(), s);
/// Decode URL-safe base64 (RFC 4648 §5) to the original bytes. Accepts
/// padded or unpadded input. `from_base64_url("Zg") == "f"`.
pub fn from_base64_url(s: String) -> String =
b64_decode_with(b64_url_alphabet(), s);