freedos-micro-python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. freedos_micro_python/__init__.py +8 -0
  2. freedos_micro_python/cli.py +106 -0
  3. freedos_micro_python/gen_qstrdefs.py +275 -0
  4. freedos_micro_python/port/arch/bpstruct.h +2 -0
  5. freedos_micro_python/port/arch/cc.h +4 -0
  6. freedos_micro_python/port/arch/epstruct.h +1 -0
  7. freedos_micro_python/port/base64_uc386dos.c +164 -0
  8. freedos_micro_python/port/file_uc386dos.c +228 -0
  9. freedos_micro_python/port/lib/axtls/crypto/crypto.h +45 -0
  10. freedos_micro_python/port/lwip-arch-cc.h +46 -0
  11. freedos_micro_python/port/lwip_uc386dos.c +248 -0
  12. freedos_micro_python/port/lwipopts.h +117 -0
  13. freedos_micro_python/port/math_gamma.c +63 -0
  14. freedos_micro_python/port/modtime_uc386dos.c +60 -0
  15. freedos_micro_python/port/modtls_axtls_uc386dos.c +461 -0
  16. freedos_micro_python/port/mpconfigport.h +358 -0
  17. freedos_micro_python/port/mphal_uc386dos.c +103 -0
  18. freedos_micro_python/port/mphalport.h +11 -0
  19. freedos_micro_python/port/os_uc386dos.c +264 -0
  20. freedos_micro_python/port/path_uc386dos.c +307 -0
  21. freedos_micro_python/port/pktdrv_uc386dos.c +650 -0
  22. freedos_micro_python/port/qstrdefsport.h +2 -0
  23. freedos_micro_python/port/shutil_uc386dos.c +111 -0
  24. freedos_micro_python/port/tempfile_uc386dos.c +129 -0
  25. freedos_micro_python/port/time_real_uc386dos.c +77 -0
  26. freedos_micro_python/port/uc386_net_uc386dos.c +126 -0
  27. freedos_micro_python/port/urllib_parse_uc386dos.c +360 -0
  28. freedos_micro_python/port/urllib_uc386dos.c +29 -0
  29. freedos_micro_python/scripts/build.sh +641 -0
  30. freedos_micro_python/scripts/build_port.sh +241 -0
  31. freedos_micro_python/scripts/fetch.sh +238 -0
  32. freedos_micro_python-0.1.0.dist-info/METADATA +131 -0
  33. freedos_micro_python-0.1.0.dist-info/RECORD +37 -0
  34. freedos_micro_python-0.1.0.dist-info/WHEEL +5 -0
  35. freedos_micro_python-0.1.0.dist-info/entry_points.txt +2 -0
  36. freedos_micro_python-0.1.0.dist-info/licenses/LICENSE +25 -0
  37. freedos_micro_python-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ """freedos_micro_python - MicroPython port for FreeDOS / i386 via uc386."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("freedos_micro_python")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0+unknown"
@@ -0,0 +1,106 @@
1
+ """freedos-micropython CLI: builds the FreeDOS MicroPython binary.
2
+
3
+ Subcommands:
4
+ fetch Run scripts/fetch.sh to clone upstream MicroPython.
5
+ build Run scripts/build.sh (per-TU triage build, generates qstrdefs).
6
+ port Run scripts/build_port.sh (multi-TU build → micropython.bin).
7
+
8
+ Each subcommand is a thin wrapper around the bundled shell script. The
9
+ wrapper:
10
+
11
+ 1. Locates uc386's lib/ via the installed package and exports
12
+ UC386_LIB_INCLUDE so the scripts don't have to compute it.
13
+ 2. Picks a working directory: uses --workdir if given, else cwd.
14
+ 3. Execs the script with stdin/stdout/stderr passed through.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import os
21
+ import subprocess
22
+ import sys
23
+ from importlib.resources import files
24
+ from pathlib import Path
25
+
26
+
27
+ def _uc386_lib_include() -> Path:
28
+ import uc386
29
+ return Path(uc386.__file__).resolve().parent / "lib" / "include"
30
+
31
+
32
+ def _scripts_dir() -> Path:
33
+ return Path(str(files("freedos_micro_python") / "scripts"))
34
+
35
+
36
+ def _port_dir() -> Path:
37
+ return Path(str(files("freedos_micro_python") / "port"))
38
+
39
+
40
+ def _ensure_port_symlink(workdir: Path) -> None:
41
+ """The shipped scripts reference port files as `uc386-dos/foo.c`.
42
+ Make sure `workdir/uc386-dos` points at the bundled port dir so
43
+ those references resolve regardless of where the user is running
44
+ from."""
45
+ link = workdir / "uc386-dos"
46
+ target = _port_dir()
47
+ if link.is_symlink():
48
+ if link.resolve() == target.resolve():
49
+ return
50
+ link.unlink()
51
+ elif link.exists():
52
+ # Real directory at uc386-dos/ — leave it alone, user knows
53
+ # what they're doing.
54
+ return
55
+ link.symlink_to(target)
56
+
57
+
58
+ def _run_script(name: str, workdir: Path, extra_args: list[str]) -> int:
59
+ script = _scripts_dir() / name
60
+ if not script.exists():
61
+ print(f"freedos-micropython: bundled script not found: {script}",
62
+ file=sys.stderr)
63
+ return 1
64
+ env = os.environ.copy()
65
+ env["UC386_LIB_INCLUDE"] = str(_uc386_lib_include())
66
+ env["FREEDOS_MP_PORT_DIR"] = str(_port_dir())
67
+ env["FREEDOS_MP_SCRIPTS_DIR"] = str(_scripts_dir())
68
+ workdir.mkdir(parents=True, exist_ok=True)
69
+ _ensure_port_symlink(workdir)
70
+ return subprocess.run(
71
+ [str(script), *extra_args],
72
+ cwd=workdir, env=env,
73
+ ).returncode
74
+
75
+
76
+ def main(argv: list[str] | None = None) -> int:
77
+ ap = argparse.ArgumentParser(
78
+ prog="freedos-micropython",
79
+ description=(
80
+ "Build the FreeDOS / i386 MicroPython port end-to-end "
81
+ "through the uc386 C23 compiler. Produces a flat .bin "
82
+ "runnable under uc386.dos_emu (or a PMODE/W .exe via "
83
+ "uc386's exe.py harness)."
84
+ ),
85
+ )
86
+ ap.add_argument(
87
+ "--workdir", type=Path, default=Path.cwd(),
88
+ help="Directory the build runs in (upstream/ and build/ "
89
+ "land here). Default: current dir.",
90
+ )
91
+ sub = ap.add_subparsers(dest="cmd", required=True)
92
+ sub.add_parser("fetch", help="Clone upstream micropython into ./upstream")
93
+ sub.add_parser("build", help="Per-TU triage build (generates qstrdefs)")
94
+ sub.add_parser("port", help="Multi-TU build → build/micropython.bin")
95
+
96
+ ns, extra = ap.parse_known_args(argv)
97
+ script_map = {
98
+ "fetch": "fetch.sh",
99
+ "build": "build.sh",
100
+ "port": "build_port.sh",
101
+ }
102
+ return _run_script(script_map[ns.cmd], ns.workdir, extra)
103
+
104
+
105
+ if __name__ == "__main__":
106
+ raise SystemExit(main())
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env python3
2
+ """Reverse-mangle MP_QSTR_<sanitized> macro names back to the
3
+ original qstr source string.
4
+
5
+ Upstream's `tools/makeqstrdata.py:qstr_escape` walks each qstr and
6
+ replaces every non-`[A-Za-z0-9]` byte with `_<name>_`, where
7
+ `<name>` comes from `codepoint2name` (HTML entity names + a small
8
+ custom map) or `0x%02x` for the rest. Our triage build greps for
9
+ `MP_QSTR_<sanitized>` references in the source; the *macro* name
10
+ is what's been sanitized — the original qstr string for `\\n` is
11
+ 1 byte, but it appears in source as `MP_QSTR__0x0a_` (5-byte
12
+ sanitized form). If we emit the sanitized form as the qstr's
13
+ 4th-field string, `qstr_find_strn("\\n", 1)` misses, AND output
14
+ qstrs (e.g. `print()`'s trailing newline) render as the literal
15
+ text `_0x0a_` instead of a newline.
16
+
17
+ This script reads MP_QSTR_<x> tokens on stdin (one per line, may
18
+ contain dups), reverses the escape, and emits one
19
+ `QDEF1(macro, 0, len, "<orig>")` line per UNIQUE token sorted by
20
+ the **original string** (ASCII byte order). Sort key matters:
21
+ qstr_find_strn does `strncmp(probe_str, pool->qstrs[mid], n)` —
22
+ the comparison key at runtime is the un-escaped string, so the
23
+ pool's `is_sorted=true` invariant requires that order. Sorting
24
+ by macro name happens to coincide for pure-identifier qstrs
25
+ (`print`, `__name__`) but breaks for escaped ones
26
+ (`MP_QSTR__0x0a_` lex-orders near `_`, while its actual string
27
+ `\\n` = 0x0A would sort before space).
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import sys
32
+
33
+ # Pull codepoint2name from upstream verbatim — same source of truth
34
+ # as makeqstrdata.qstr_escape so the inverse is exact. Caller is
35
+ # expected to set sys.path so `upstream.py.makeqstrdata` resolves.
36
+ sys.path.insert(0, "upstream/py")
37
+ from makeqstrdata import codepoint2name # type: ignore[import-not-found]
38
+
39
+ # Inverse map: HTML entity name -> single-character byte string.
40
+ # Two filters:
41
+ # 1. Codepoint must be < 256 — qstrs are byte sequences, so
42
+ # escapes for high-codepoint Unicode chars never appear in real
43
+ # source. Without this, `_omega_` etc. would false-match.
44
+ # 2. The decoded char must NOT itself be an identifier char
45
+ # (`[A-Za-z0-9_]`). Upstream's `qstr_escape` only produces
46
+ # `_<name>_` wrappers for NON-identifier chars (the regex
47
+ # `RE_NO_ESCAPE = r"[A-Za-z0-9_]"` passes identifier chars
48
+ # through unchanged). So `_<name>_` in a macro tail can only
49
+ # have come from escaping a punctuation/whitespace byte —
50
+ # never from an alphanumeric escape. This filter eliminates
51
+ # false matches like `__not__` (a real Python dunder, not an
52
+ # escape of `¬` U+00AC) and `__and__` (likewise, not `∧`).
53
+ _IDENT_CHARS = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_")
54
+ # Restrict to ASCII printable punctuation (32–126, excluding the
55
+ # identifier subset). Control chars (`\n`, `\t`, …) and high-byte
56
+ # chars (¬ U+00AC, ∧ U+2227, Α U+0391, …) are handled either via
57
+ # the `0x%02x` literal path or are simply unreachable in real qstr
58
+ # source — `__not__` is a real Python dunder, not an escape for `¬`.
59
+ name2char = {
60
+ name: chr(cp)
61
+ for cp, name in codepoint2name.items()
62
+ if 32 <= cp <= 126 and chr(cp) not in _IDENT_CHARS
63
+ }
64
+
65
+
66
+ def unescape(macro_tail: str) -> str:
67
+ """Reverse qstr_escape on the part after `MP_QSTR_`.
68
+
69
+ Walks left-to-right. Plain `[A-Za-z0-9]` runs pass through.
70
+ A `_<name>_` group decodes to a single byte: `name` is either
71
+ an HTML entity name (`lt`, `gt`, `space`, `hyphen`, ...) or a
72
+ `0x%02x` literal. The leading `_` and trailing `_` framing the
73
+ name come from `qstr_escape`'s `"_" + name + "_"` template.
74
+
75
+ Edge case: an underscore that's part of the *original* string
76
+ (e.g. `__name__`) ALSO gets sanitized — but to itself, since
77
+ `_` is in `RE_NO_ESCAPE` upstream:
78
+
79
+ RE_NO_ESCAPE = re.compile(r"[A-Za-z0-9_]")
80
+
81
+ So plain `_` runs through verbatim. The only `_<name>_` groups
82
+ that exist are the genuine escape sequences. Disambiguating
83
+ works because every escape `name` is at minimum 2 chars
84
+ (`lt`, `gt`, `0x..`) — a lone `_` is never a wrapper.
85
+ """
86
+ out: list[str] = []
87
+ i = 0
88
+ n = len(macro_tail)
89
+ while i < n:
90
+ c = macro_tail[i]
91
+ if c != "_":
92
+ out.append(c)
93
+ i += 1
94
+ continue
95
+ # Found a `_` — could be a literal underscore in the source
96
+ # qstr (e.g. `__name__`) or the start of `_<name>_` where
97
+ # `<name>` is an entry in `codepoint2name` (HTML entity name)
98
+ # or `0xNN` (hex byte literal). Some entity names CONTAIN
99
+ # underscores themselves: `brace_open`, `brace_close`,
100
+ # `paren_open`, `bracket_open`, etc. So a naive
101
+ # `find("_", i+1)` would split `_brace_open_` on the inner
102
+ # `_` and produce `brace` as the candidate name (which
103
+ # doesn't match anything, so we'd silently drop the escape).
104
+ # Iterate forward over EVERY `_` that follows and accept the
105
+ # first candidate that's a known name (longest valid by
106
+ # construction — names don't share prefixes that are also
107
+ # names). Fall back to literal `_` if no closing `_` matches.
108
+ matched = False
109
+ j = i + 1
110
+ while True:
111
+ k = macro_tail.find("_", j)
112
+ if k == -1:
113
+ break
114
+ candidate = macro_tail[i + 1 : k]
115
+ if candidate in name2char:
116
+ out.append(name2char[candidate])
117
+ i = k + 1
118
+ matched = True
119
+ break
120
+ if (
121
+ len(candidate) == 4
122
+ and candidate[:2] == "0x"
123
+ and all(ch in "0123456789abcdef" for ch in candidate[2:])
124
+ ):
125
+ out.append(chr(int(candidate[2:], 16)))
126
+ i = k + 1
127
+ matched = True
128
+ break
129
+ j = k + 1
130
+ if not matched:
131
+ out.append("_")
132
+ i += 1
133
+ return "".join(out)
134
+
135
+
136
+ def compute_hash(qbytes: bytes, bytes_hash: int) -> int:
137
+ """djb2 hash, mirrored from upstream `tools/makeqstrdata.py:compute_hash`.
138
+ Required at `MICROPY_QSTR_BYTES_IN_HASH > 0` (CORE_FEATURES and
139
+ above): `qstr_find_strn`'s post-binary-search filter does
140
+ `pool->hashes[at] == str_hash` before memcmp. If the QDEF emits 0
141
+ for the hash but the runtime computes a real hash, every static
142
+ lookup misses and `print`/`__name__` NameError at runtime.
143
+
144
+ Mirrors upstream's `(hash & mask) or 1` zero-fix; mask width is
145
+ `8 * bytes_hash`, with `bytes_hash == 0` falling back to a 16-bit
146
+ mask so the value still fits in `qstr_hash_t = uint16_t` if the
147
+ port flips to a wider hash."""
148
+ h = 5381
149
+ for b in qbytes:
150
+ h = (h * 33) ^ b
151
+ mask = (1 << (8 * (bytes_hash or 2))) - 1
152
+ return (h & mask) or 1
153
+
154
+
155
+ def c_string(s: str) -> str:
156
+ """Render `s` as a C string literal — escape `"`, `\\`, and any
157
+ byte outside printable ASCII."""
158
+ parts: list[str] = []
159
+ for ch in s:
160
+ b = ord(ch)
161
+ if ch == "\\":
162
+ parts.append("\\\\")
163
+ elif ch == '"':
164
+ parts.append('\\"')
165
+ elif ch == "\n":
166
+ parts.append("\\n")
167
+ elif ch == "\t":
168
+ parts.append("\\t")
169
+ elif ch == "\r":
170
+ parts.append("\\r")
171
+ elif 0x20 <= b < 0x7F:
172
+ parts.append(ch)
173
+ else:
174
+ parts.append(f"\\x{b:02x}")
175
+ return '"' + "".join(parts) + '"'
176
+
177
+
178
+ def main() -> int:
179
+ # Optional `--bytes-hash N` selects the qstr-hash width (matches
180
+ # `MICROPY_QSTR_BYTES_IN_HASH` in mpconfigport.h). Default 1 ==
181
+ # CORE_FEATURES; 0 == MINIMUM (hash field is unused at runtime
182
+ # but we still emit a non-zero value so a future ROM-level bump
183
+ # works without regenerating qstrdefs).
184
+ bytes_hash = 1
185
+ args = sys.argv[1:]
186
+ i = 0
187
+ while i < len(args):
188
+ if args[i] == "--bytes-hash" and i + 1 < len(args):
189
+ bytes_hash = int(args[i + 1])
190
+ i += 2
191
+ else:
192
+ sys.stderr.write(f"gen_qstrdefs: unknown arg {args[i]!r}\n")
193
+ return 2
194
+
195
+ # Pull the static + unsorted qstr lists from upstream so the
196
+ # qstrs the runtime needs to fit in a `byte` (e.g. the
197
+ # mp_binary_op_method_name table at py/objtype.c:483 — entries
198
+ # like `__add__` are stored as 1-byte qstr ids) end up in the
199
+ # static (QDEF0) pool with id < 256. Without this, enabling
200
+ # MICROPY_PY_ALL_SPECIAL_METHODS truncates `MP_QSTR___add__`'s
201
+ # >256 id to its low byte and `V(2)+V(3)` dispatches to
202
+ # whatever qstr happens to live at id-modulo-256 (we saw
203
+ # `FLOAT32` instead of `__add__`).
204
+ from makeqstrdata import ( # type: ignore[import-not-found]
205
+ static_qstr_list,
206
+ unsorted_qstr_list,
207
+ )
208
+ static_pool = set(static_qstr_list) | set(unsorted_qstr_list)
209
+
210
+ seen: dict[str, str] = {} # macro -> original
211
+ for line in sys.stdin:
212
+ macro = line.strip()
213
+ if not macro.startswith("MP_QSTR_"):
214
+ continue
215
+ if macro in seen:
216
+ continue
217
+ # `MP_QSTR_` is 8 chars; the rest is the sanitized qstr.
218
+ seen[macro] = unescape(macro[8:])
219
+
220
+ # Emit QDEF0 entries first (in upstream's defined order) so they
221
+ # get fixed id slots < 256. Then QDEF1 entries sorted by the
222
+ # original string (ASCII byte order) — that's the runtime's
223
+ # binary-search key.
224
+ out_w = sys.stdout.write
225
+
226
+ static_emitted: set[str] = set()
227
+ # Walk upstream's static_qstr_list verbatim so the .mpy ABI
228
+ # ID assignments stay stable across uc386 + upstream builds.
229
+ for original in static_qstr_list:
230
+ # Find the macro name for this string from `seen`. If our
231
+ # grep didn't capture it, synthesize the macro: upstream's
232
+ # qstr_escape can be re-applied via the `unescape` inverse,
233
+ # but we already have a forward `qstr_escape` in
234
+ # makeqstrdata, so use that.
235
+ from makeqstrdata import qstr_escape # type: ignore
236
+ macro = "MP_QSTR_" + qstr_escape(original)
237
+ qhash = compute_hash(original.encode("utf-8"), bytes_hash)
238
+ out_w(
239
+ f"QDEF0({macro}, {qhash}, {len(original)}, "
240
+ f"{c_string(original)})\n"
241
+ )
242
+ static_emitted.add(macro)
243
+
244
+ # Then unsorted_qstr_list — also QDEF0 (low ids), but ordering
245
+ # doesn't matter for .mpy compat (these aren't part of the
246
+ # public ABI list).
247
+ for original in sorted(unsorted_qstr_list):
248
+ from makeqstrdata import qstr_escape # type: ignore
249
+ macro = "MP_QSTR_" + qstr_escape(original)
250
+ if macro in static_emitted:
251
+ continue
252
+ qhash = compute_hash(original.encode("utf-8"), bytes_hash)
253
+ out_w(
254
+ f"QDEF0({macro}, {qhash}, {len(original)}, "
255
+ f"{c_string(original)})\n"
256
+ )
257
+ static_emitted.add(macro)
258
+
259
+ # Everything else goes to QDEF1, sorted for the binary-search
260
+ # invariant.
261
+ for macro, original in sorted(
262
+ seen.items(), key=lambda item: (item[1].encode("utf-8"), item[0])
263
+ ):
264
+ if macro in static_emitted or original in static_pool:
265
+ continue
266
+ qhash = compute_hash(original.encode("utf-8"), bytes_hash)
267
+ out_w(
268
+ f"QDEF1({macro}, {qhash}, {len(original)}, "
269
+ f"{c_string(original)})\n"
270
+ )
271
+ return 0
272
+
273
+
274
+ if __name__ == "__main__":
275
+ sys.exit(main())
@@ -0,0 +1,2 @@
1
+ // Empty pack-struct begin marker — uc_core doesn't pack structs by
2
+ // default and lwIP's wire-shape headers fall through fine without it.
@@ -0,0 +1,4 @@
1
+ // Stub that forwards to the real arch/cc.h shim (lwip-arch-cc.h).
2
+ // lwIP's headers do `#include "arch/cc.h"`; we put that path on
3
+ // the search list (uc386-dos/) so this file resolves there.
4
+ #include "../lwip-arch-cc.h"
@@ -0,0 +1 @@
1
+ // Empty pack-struct end marker.
@@ -0,0 +1,164 @@
1
+ // uc386-dos `base64` module — thin port-supplied stdlib shim.
2
+ //
3
+ // MicroPython ships base64 routines in `binascii` (b2a_base64 /
4
+ // a2b_base64) but most CPython programs reach for `import base64;
5
+ // base64.b64encode(...)` directly. Rather than freezing a Python
6
+ // wrapper, we ship a tiny C module with inline RFC 4648 base64 +
7
+ // base16 encoders/decoders. ~80 lines, no allocations beyond the
8
+ // vstr that holds the result.
9
+ //
10
+ // Surface:
11
+ // base64.b64encode(data) → bytes (no trailing newline)
12
+ // base64.b64decode(s) → bytes
13
+ // base64.b16encode(data) → uppercase hex bytes
14
+ // base64.b16decode(s) → bytes (case-insensitive accepted)
15
+
16
+ #include <string.h>
17
+
18
+ #include "py/runtime.h"
19
+ #include "py/objstr.h"
20
+ #include "py/binary.h"
21
+
22
+ static const char b64_alphabet[] =
23
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
24
+
25
+ static int b64_decode_char(int c) {
26
+ if (c >= 'A' && c <= 'Z') return c - 'A';
27
+ if (c >= 'a' && c <= 'z') return c - 'a' + 26;
28
+ if (c >= '0' && c <= '9') return c - '0' + 52;
29
+ if (c == '+') return 62;
30
+ if (c == '/') return 63;
31
+ return -1; // invalid (also catches '=' padding)
32
+ }
33
+
34
+ static mp_obj_t base64_b64encode(mp_obj_t data_in) {
35
+ mp_buffer_info_t buf;
36
+ mp_get_buffer_raise(data_in, &buf, MP_BUFFER_READ);
37
+ const unsigned char *src = (const unsigned char *)buf.buf;
38
+ size_t n = buf.len;
39
+
40
+ vstr_t vstr;
41
+ vstr_init_len(&vstr, ((n + 2) / 3) * 4);
42
+ char *out = vstr.buf;
43
+
44
+ size_t i = 0;
45
+ for (; i + 3 <= n; i += 3) {
46
+ unsigned int v = ((unsigned int)src[i] << 16) |
47
+ ((unsigned int)src[i + 1] << 8) |
48
+ (unsigned int)src[i + 2];
49
+ *out++ = b64_alphabet[(v >> 18) & 0x3F];
50
+ *out++ = b64_alphabet[(v >> 12) & 0x3F];
51
+ *out++ = b64_alphabet[(v >> 6) & 0x3F];
52
+ *out++ = b64_alphabet[v & 0x3F];
53
+ }
54
+ size_t rem = n - i;
55
+ if (rem == 1) {
56
+ unsigned int v = (unsigned int)src[i] << 16;
57
+ *out++ = b64_alphabet[(v >> 18) & 0x3F];
58
+ *out++ = b64_alphabet[(v >> 12) & 0x3F];
59
+ *out++ = '=';
60
+ *out++ = '=';
61
+ } else if (rem == 2) {
62
+ unsigned int v = ((unsigned int)src[i] << 16) |
63
+ ((unsigned int)src[i + 1] << 8);
64
+ *out++ = b64_alphabet[(v >> 18) & 0x3F];
65
+ *out++ = b64_alphabet[(v >> 12) & 0x3F];
66
+ *out++ = b64_alphabet[(v >> 6) & 0x3F];
67
+ *out++ = '=';
68
+ }
69
+ return mp_obj_new_bytes_from_vstr(&vstr);
70
+ }
71
+ static MP_DEFINE_CONST_FUN_OBJ_1(base64_b64encode_obj, base64_b64encode);
72
+
73
+ static mp_obj_t base64_b64decode(mp_obj_t s_in) {
74
+ mp_buffer_info_t buf;
75
+ mp_get_buffer_raise(s_in, &buf, MP_BUFFER_READ);
76
+ const unsigned char *src = (const unsigned char *)buf.buf;
77
+ size_t n = buf.len;
78
+
79
+ vstr_t vstr;
80
+ vstr_init(&vstr, (n * 3) / 4 + 4);
81
+ int bits = 0;
82
+ int collected = 0;
83
+ for (size_t i = 0; i < n; i++) {
84
+ int c = src[i];
85
+ if (c == '\r' || c == '\n' || c == ' ' || c == '\t') {
86
+ continue;
87
+ }
88
+ if (c == '=') {
89
+ break; // padding — no more data
90
+ }
91
+ int v = b64_decode_char(c);
92
+ if (v < 0) {
93
+ mp_raise_ValueError(MP_ERROR_TEXT("invalid base64 character"));
94
+ }
95
+ bits = (bits << 6) | v;
96
+ collected += 6;
97
+ if (collected >= 8) {
98
+ collected -= 8;
99
+ vstr_add_byte(&vstr, (bits >> collected) & 0xFF);
100
+ }
101
+ }
102
+ return mp_obj_new_bytes_from_vstr(&vstr);
103
+ }
104
+ static MP_DEFINE_CONST_FUN_OBJ_1(base64_b64decode_obj, base64_b64decode);
105
+
106
+ static mp_obj_t base64_b16encode(mp_obj_t data_in) {
107
+ mp_buffer_info_t buf;
108
+ mp_get_buffer_raise(data_in, &buf, MP_BUFFER_READ);
109
+ const unsigned char *src = (const unsigned char *)buf.buf;
110
+ static const char hex[] = "0123456789ABCDEF";
111
+
112
+ vstr_t vstr;
113
+ vstr_init_len(&vstr, buf.len * 2);
114
+ char *out = vstr.buf;
115
+ for (size_t i = 0; i < buf.len; i++) {
116
+ *out++ = hex[(src[i] >> 4) & 0x0F];
117
+ *out++ = hex[src[i] & 0x0F];
118
+ }
119
+ return mp_obj_new_bytes_from_vstr(&vstr);
120
+ }
121
+ static MP_DEFINE_CONST_FUN_OBJ_1(base64_b16encode_obj, base64_b16encode);
122
+
123
+ static int hex_digit(int c) {
124
+ if (c >= '0' && c <= '9') return c - '0';
125
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
126
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
127
+ return -1;
128
+ }
129
+
130
+ static mp_obj_t base64_b16decode(mp_obj_t s_in) {
131
+ mp_buffer_info_t buf;
132
+ mp_get_buffer_raise(s_in, &buf, MP_BUFFER_READ);
133
+ const unsigned char *src = (const unsigned char *)buf.buf;
134
+ if (buf.len & 1) {
135
+ mp_raise_ValueError(MP_ERROR_TEXT("odd-length base16 input"));
136
+ }
137
+ vstr_t vstr;
138
+ vstr_init_len(&vstr, buf.len / 2);
139
+ char *out = vstr.buf;
140
+ for (size_t i = 0; i < buf.len; i += 2) {
141
+ int hi = hex_digit(src[i]);
142
+ int lo = hex_digit(src[i + 1]);
143
+ if (hi < 0 || lo < 0) {
144
+ mp_raise_ValueError(MP_ERROR_TEXT("invalid base16 character"));
145
+ }
146
+ *out++ = (hi << 4) | lo;
147
+ }
148
+ return mp_obj_new_bytes_from_vstr(&vstr);
149
+ }
150
+ static MP_DEFINE_CONST_FUN_OBJ_1(base64_b16decode_obj, base64_b16decode);
151
+
152
+ static const mp_rom_map_elem_t mp_module_base64_globals_table[] = {
153
+ { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_base64) },
154
+ { MP_ROM_QSTR(MP_QSTR_b64encode), MP_ROM_PTR(&base64_b64encode_obj) },
155
+ { MP_ROM_QSTR(MP_QSTR_b64decode), MP_ROM_PTR(&base64_b64decode_obj) },
156
+ { MP_ROM_QSTR(MP_QSTR_b16encode), MP_ROM_PTR(&base64_b16encode_obj) },
157
+ { MP_ROM_QSTR(MP_QSTR_b16decode), MP_ROM_PTR(&base64_b16decode_obj) },
158
+ };
159
+ static MP_DEFINE_CONST_DICT(mp_module_base64_globals, mp_module_base64_globals_table);
160
+
161
+ const mp_obj_module_t mp_module_base64 = {
162
+ .base = { &mp_type_module },
163
+ .globals = (mp_obj_dict_t *)&mp_module_base64_globals,
164
+ };