feat: ports - netsurf with the monkey frontend

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ae69wJS7sueQMUwNxV3nX
This commit is contained in:
2026-08-07 21:26:32 +02:00
co-authored by Claude Opus 5
parent d14c8565d3
commit f4dce0cdd6
15 changed files with 953 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
/*
* Minimal <arpa/inet.h> for the MontaukOS NetSurf port.
*
* NetSurf uses inet_pton() only to recognise IP literals in URLs
* (content/urldb.c, utils/utils.c) -- it never opens a socket with the result.
* A self-contained parser is therefore enough, and keeps this out of the
* MontaukOS libc, where a partial implementation would be misleading.
*
* static inline throughout: these headers are included by a handful of
* translation units and NetSurf builds with -Werror, so a non-static
* definition would either collide at link time or warn when unused.
*/
#ifndef _MONTAUK_COMPAT_ARPA_INET_H_
#define _MONTAUK_COMPAT_ARPA_INET_H_
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdint.h>
#include <string.h>
/* Parse a dotted-quad into 4 network-order bytes. Returns 1 on success. */
static inline int montauk_inet_pton4(const char *src, uint8_t *dst)
{
uint32_t octet = 0;
int digits = 0;
int seen = 0;
uint8_t out[4];
for (;;) {
char c = *src++;
if (c >= '0' && c <= '9') {
if (digits == 3)
return 0;
/* a leading zero is not accepted (023 is ambiguous) */
if (digits == 1 && out[seen] == 0)
return 0;
octet = octet * 10 + (uint32_t)(c - '0');
if (octet > 255)
return 0;
out[seen] = (uint8_t)octet;
digits++;
} else if (c == '.' || c == '\0') {
if (digits == 0)
return 0;
if (c == '\0') {
if (seen != 3)
return 0;
memcpy(dst, out, 4);
return 1;
}
if (seen == 3)
return 0;
seen++;
octet = 0;
digits = 0;
out[seen] = 0;
} else {
return 0;
}
}
}
/*
* Parse an IPv6 literal, including "::" compression and a trailing
* dotted-quad (e.g. ::ffff:192.0.2.1). Returns 1 on success.
*/
static inline int montauk_inet_pton6(const char *src, uint8_t *dst)
{
uint8_t out[16];
int head = 0; /* bytes written before "::" */
int tail = 0; /* bytes buffered after "::" */
uint8_t tailbuf[16];
int seen_compress = 0;
uint8_t *cur = out;
int *curlen = &head;
memset(out, 0, sizeof(out));
if (src[0] == ':') {
if (src[1] != ':')
return 0;
src += 2;
seen_compress = 1;
cur = tailbuf;
curlen = &tail;
if (*src == '\0')
goto done;
}
for (;;) {
uint32_t group = 0;
int digits = 0;
/* a dotted-quad may only appear as the final component */
if (strchr(src, '.') != NULL) {
const char *p = src;
int dots = 0;
while (*p != '\0' && *p != ':') {
if (*p == '.')
dots++;
p++;
}
if (dots == 3 && *p == '\0') {
if (*curlen + 4 > 16)
return 0;
if (montauk_inet_pton4(src, cur + *curlen) == 0)
return 0;
*curlen += 4;
break;
}
}
while (*src != '\0' && *src != ':') {
char c = *src;
uint32_t v;
if (c >= '0' && c <= '9')
v = (uint32_t)(c - '0');
else if (c >= 'a' && c <= 'f')
v = (uint32_t)(c - 'a' + 10);
else if (c >= 'A' && c <= 'F')
v = (uint32_t)(c - 'A' + 10);
else
return 0;
group = group * 16 + v;
if (++digits > 4)
return 0;
src++;
}
if (digits == 0)
return 0;
if (*curlen + 2 > 16)
return 0;
cur[(*curlen)++] = (uint8_t)(group >> 8);
cur[(*curlen)++] = (uint8_t)(group & 0xff);
if (*src == '\0')
break;
src++; /* consume ':' */
if (*src == ':') {
if (seen_compress)
return 0;
seen_compress = 1;
src++;
cur = tailbuf;
curlen = &tail;
if (*src == '\0')
break;
} else if (*src == '\0') {
/* trailing single colon */
return 0;
}
}
done:
if (seen_compress) {
if (head + tail > 15) /* "::" must stand for >= 1 zero group */
return 0;
memmove(out + 16 - tail, tailbuf, (size_t)tail);
memset(out + head, 0, (size_t)(16 - tail - head));
} else if (head != 16) {
return 0;
}
memcpy(dst, out, 16);
return 1;
}
/*
* inet_aton(). Returns non-zero on success, as BSD does.
*
* Only the full dotted-quad form is accepted. Historic inet_aton also takes
* "a", "a.b", "a.b.c" and octal/hex octets, but content/urldb.c immediately
* rejects anything without three dots (see the comment at its call site), so
* the narrower behaviour matches how NetSurf actually uses it -- and refusing
* octal-looking octets like "1.2.3.04" is the safer reading for a URL host.
*/
static inline int inet_aton(const char *cp, struct in_addr *addr)
{
uint8_t out[4];
if (cp == NULL || addr == NULL)
return 0;
if (montauk_inet_pton4(cp, out) != 1)
return 0;
memcpy(&addr->s_addr, out, 4);
return 1;
}
static inline int inet_pton(int af, const char *src, void *dst)
{
if (src == NULL || dst == NULL)
return -1;
if (af == AF_INET)
return montauk_inet_pton4(src, (uint8_t *) dst);
if (af == AF_INET6)
return montauk_inet_pton6(src, (uint8_t *) dst);
return -1; /* EAFNOSUPPORT */
}
#endif
+28
View File
@@ -0,0 +1,28 @@
/*
* Stub <curl/curl.h> for the MontaukOS NetSurf port.
*
* This is NOT a curl implementation and nothing here can perform a fetch.
*
* NetSurf is built with NETSURF_USE_CURL=NO, but that configuration is not
* well exercised upstream: content/fetch.c includes content/fetchers/curl.h
* unconditionally and only guards the *use* of fetch_curl_register() behind
* #ifdef WITH_CURL. That header in turn includes <curl/curl.h> and declares
* `extern CURLM *fetch_curl_multi`, so the type must exist for the
* translation unit to compile even though no curl code is built.
*
* Supplying this stub keeps the port free of patches against upstream. The
* alternative -- guarding the include in content/fetch.c -- is a one-line
* change but starts a patch series that every future NetSurf update has to
* be rebased onto.
*
* When fetch_montauk lands it registers itself the same way fetch_curl would,
* and this file stays exactly as it is.
*/
#ifndef _MONTAUK_COMPAT_CURL_CURL_H_
#define _MONTAUK_COMPAT_CURL_CURL_H_
/* Opaque: only ever named in a declaration that is never defined or used. */
typedef void CURLM;
typedef void CURL;
#endif
+235
View File
@@ -0,0 +1,235 @@
/*
* iconv() for the MontaukOS NetSurf port, implemented over libparserutils'
* charset codecs. See iconv.h for why.
*
* Structure: one codec decodes the source charset to UCS-4, a second encodes
* UCS-4 to the destination charset. iconv() pumps a small stack buffer of
* UCS-4 between them so neither side needs an unbounded intermediate.
*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <parserutils/charset/codec.h>
#include <parserutils/errors.h>
#include "iconv.h"
/* UCS-4 code points staged between decode and encode, per inner iteration. */
#define STAGE_POINTS 64
struct montauk_iconv {
parserutils_charset_codec *from; /* source charset -> UCS-4 */
parserutils_charset_codec *to; /* UCS-4 -> dest charset */
};
/*
* Strip glibc's "//TRANSLIT" / "//IGNORE" suffixes, which libparserutils does
* not understand. Returns a malloc'd bare charset name.
*/
static char *strip_suffix(const char *name)
{
const char *sep;
size_t len;
char *out;
if (name == NULL)
return NULL;
sep = strstr(name, "//");
len = (sep != NULL) ? (size_t) (sep - name) : strlen(name);
out = malloc(len + 1);
if (out == NULL)
return NULL;
memcpy(out, name, len);
out[len] = '\0';
return out;
}
static parserutils_charset_codec *make_codec(const char *name)
{
parserutils_charset_codec *codec = NULL;
parserutils_charset_codec_optparams params;
char *bare;
bare = strip_suffix(name);
if (bare == NULL)
return NULL;
if (parserutils_charset_codec_create(bare, &codec) != PARSERUTILS_OK) {
free(bare);
return NULL;
}
free(bare);
/*
* LOOSE: substitute unrepresentable characters instead of aborting.
* NetSurf renders best-effort; a hard failure mid-document is worse
* than a replacement character.
*/
params.error_mode.mode = PARSERUTILS_CHARSET_CODEC_ERROR_LOOSE;
(void) parserutils_charset_codec_setopt(codec,
PARSERUTILS_CHARSET_CODEC_ERROR_MODE, &params);
return codec;
}
iconv_t iconv_open(const char *tocode, const char *fromcode)
{
struct montauk_iconv *cd;
cd = calloc(1, sizeof(*cd));
if (cd == NULL) {
errno = ENOMEM;
return (iconv_t) -1;
}
cd->from = make_codec(fromcode);
cd->to = make_codec(tocode);
if (cd->from == NULL || cd->to == NULL) {
if (cd->from != NULL)
parserutils_charset_codec_destroy(cd->from);
if (cd->to != NULL)
parserutils_charset_codec_destroy(cd->to);
free(cd);
errno = EINVAL; /* unsupported conversion */
return (iconv_t) -1;
}
return (iconv_t) cd;
}
int iconv_close(iconv_t handle)
{
struct montauk_iconv *cd = (struct montauk_iconv *) handle;
if (cd == NULL || handle == (iconv_t) -1) {
errno = EBADF;
return -1;
}
parserutils_charset_codec_destroy(cd->from);
parserutils_charset_codec_destroy(cd->to);
free(cd);
return 0;
}
size_t iconv(iconv_t handle, char **inbuf, size_t *inbytesleft,
char **outbuf, size_t *outbytesleft)
{
struct montauk_iconv *cd = (struct montauk_iconv *) handle;
size_t converted = 0;
if (cd == NULL || handle == (iconv_t) -1) {
errno = EBADF;
return (size_t) -1;
}
/* Reset request: iconv(cd, NULL, NULL, ...) returns to initial state. */
if (inbuf == NULL || *inbuf == NULL) {
parserutils_charset_codec_reset(cd->from);
parserutils_charset_codec_reset(cd->to);
return 0;
}
while (*inbytesleft > 0) {
uint8_t stage[STAGE_POINTS * 4];
const uint8_t *src = (const uint8_t *) *inbuf;
size_t srclen = *inbytesleft;
uint8_t *mid = stage;
size_t midlen = sizeof(stage);
size_t staged;
const uint8_t *midsrc;
size_t midsrclen;
uint8_t *dst;
size_t dstlen;
parserutils_error perr;
/* ---- source charset -> UCS-4 ---- */
perr = parserutils_charset_codec_decode(cd->from,
&src, &srclen, &mid, &midlen);
staged = sizeof(stage) - midlen;
if (staged == 0) {
/* nothing decoded: classify why and stop */
if (perr == PARSERUTILS_NEEDDATA)
errno = EINVAL; /* incomplete tail sequence */
else if (perr == PARSERUTILS_INVALID)
errno = EILSEQ; /* malformed input */
else if (perr == PARSERUTILS_NOMEM)
errno = E2BIG;
else
errno = EILSEQ;
return (size_t) -1;
}
/* ---- UCS-4 -> destination charset ---- */
midsrc = stage;
midsrclen = staged;
dst = (uint8_t *) *outbuf;
dstlen = *outbytesleft;
perr = parserutils_charset_codec_encode(cd->to,
&midsrc, &midsrclen, &dst, &dstlen);
/*
* Advance the caller's input only by what was both decoded AND
* encoded. If the encoder ran out of output room mid-stage,
* the undrained UCS-4 must not be counted as consumed -- the
* caller will grow its buffer and call again.
*/
{
size_t encoded_points = (staged - midsrclen) / 4;
size_t consumed_in;
*outbytesleft = dstlen;
*outbuf = (char *) dst;
if (midsrclen == 0) {
/* whole stage drained: input consumed as decoded */
consumed_in = (size_t) ((const char *) src - *inbuf);
} else {
/*
* Partial drain. Re-decode from the original
* position next time; consume nothing this
* round beyond what actually made it out.
*/
if (encoded_points == 0) {
errno = E2BIG;
return (size_t) -1;
}
consumed_in = 0;
}
if (consumed_in > 0) {
*inbuf += consumed_in;
*inbytesleft -= consumed_in;
}
converted += encoded_points;
if (midsrclen != 0) {
/* output was too small to take the rest */
errno = E2BIG;
return (size_t) -1;
}
}
if (perr == PARSERUTILS_NOMEM) {
errno = E2BIG;
return (size_t) -1;
}
if (*outbytesleft == 0 && *inbytesleft > 0) {
errno = E2BIG;
return (size_t) -1;
}
}
return converted;
}
+41
View File
@@ -0,0 +1,41 @@
/*
* <iconv.h> for the MontaukOS NetSurf port.
*
* MontaukOS has no iconv. Rather than port GNU libiconv, this implements the
* three-function iconv API on top of libparserutils' charset codecs, which are
* already in the ports sysroot.
*
* That is a good fit rather than a bodge: libparserutils decodes a charset to
* UCS-4 and encodes UCS-4 back out, so iconv() is decode-then-encode through a
* UCS-4 staging buffer. The supported set is whatever libparserutils supports
* -- UTF-8, UTF-16, ASCII, ISO-8859-x and the Windows-125x range -- which is
* exactly the set the HTML parser can handle anyway, so nothing is lost that
* would otherwise have worked.
*
* NetSurf touches iconv only from utils/utf8.c.
*
* NOT SUPPORTED: the "//TRANSLIT" and "//IGNORE" suffixes glibc accepts. They
* are stripped and ignored; conversion uses libparserutils' LOOSE error mode,
* which substitutes rather than failing.
*/
#ifndef _MONTAUK_COMPAT_ICONV_H_
#define _MONTAUK_COMPAT_ICONV_H_
#include <stddef.h>
typedef void *iconv_t;
#ifdef __cplusplus
extern "C" {
#endif
iconv_t iconv_open(const char *tocode, const char *fromcode);
size_t iconv(iconv_t cd, char **inbuf, size_t *inbytesleft,
char **outbuf, size_t *outbytesleft);
int iconv_close(iconv_t cd);
#ifdef __cplusplus
}
#endif
#endif
+21
View File
@@ -0,0 +1,21 @@
/*
* Minimal <netinet/in.h> for the MontaukOS NetSurf port.
* Only the address structures urldb.c and utils.c use for IP-literal parsing.
*/
#ifndef _MONTAUK_COMPAT_NETINET_IN_H_
#define _MONTAUK_COMPAT_NETINET_IN_H_
#include <stdint.h>
struct in_addr {
uint32_t s_addr;
};
struct in6_addr {
uint8_t s6_addr[16];
};
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
#endif
+107
View File
@@ -0,0 +1,107 @@
/*
* Minimal <sys/select.h> for the MontaukOS NetSurf port.
*
* MontaukOS has no select(). Readiness on a socket is expressed through the
* kernel's waitset (SYS_WAITSET_*), which is a different shape entirely, and
* stdio descriptors are not waitable objects at all.
*
* NetSurf's core uses select() in exactly one place -- content/fetch.c's
* fetch_fdset() collects descriptors from the registered fetchers, and the
* frontend's main loop selects over them plus its own input. With
* NETSURF_USE_CURL=NO no fetcher registers a descriptor, so the only fd that
* ever appears is the monkey frontend's stdin (fd 0).
*
* That makes a useful shim possible:
*
* - if any read descriptor is requested, report them ready immediately and
* let the caller block in read(). monkey is a line-driven REPL, so
* blocking on stdin is what it wants anyway.
* - otherwise sleep for the timeout so an idle loop does not spin.
*
* LIMITATION: because a requested read fd is always reported ready, scheduled
* callbacks do not fire while the process sits idle waiting for a command.
* That is fine for driving monkey by hand or by script, and it is wrong for
* anything with real network activity. When fetch_montauk lands it will
* register descriptors here and this needs replacing with a genuine
* waitset-backed implementation.
*/
#ifndef _MONTAUK_COMPAT_SYS_SELECT_H_
#define _MONTAUK_COMPAT_SYS_SELECT_H_
#include <sys/time.h>
#include <unistd.h>
#include <string.h>
#define FD_SETSIZE 64
typedef struct {
unsigned long fds_bits[(FD_SETSIZE + (8 * sizeof(unsigned long)) - 1) /
(8 * sizeof(unsigned long))];
} fd_set;
#define __NFDBITS (8 * (int) sizeof(unsigned long))
#define FD_ZERO(s) memset((s), 0, sizeof(fd_set))
#define FD_SET(fd, s) \
((void) ((s)->fds_bits[(fd) / __NFDBITS] |= \
(1UL << ((fd) % __NFDBITS))))
#define FD_CLR(fd, s) \
((void) ((s)->fds_bits[(fd) / __NFDBITS] &= \
~(1UL << ((fd) % __NFDBITS))))
#define FD_ISSET(fd, s) \
(((s)->fds_bits[(fd) / __NFDBITS] & (1UL << ((fd) % __NFDBITS))) != 0)
static inline int montauk_fdset_empty(const fd_set *s)
{
unsigned i;
if (s == NULL)
return 1;
for (i = 0; i < sizeof(s->fds_bits) / sizeof(s->fds_bits[0]); i++) {
if (s->fds_bits[i] != 0)
return 0;
}
return 1;
}
static inline int montauk_fdset_count(const fd_set *s, int nfds)
{
int fd, n = 0;
if (s == NULL)
return 0;
for (fd = 0; fd < nfds && fd < FD_SETSIZE; fd++) {
if (FD_ISSET(fd, s))
n++;
}
return n;
}
static inline int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout)
{
int ready;
if (writefds != NULL)
FD_ZERO(writefds);
if (exceptfds != NULL)
FD_ZERO(exceptfds);
if (montauk_fdset_empty(readfds) == 0) {
/* leave readfds as-is: every requested fd is "ready" */
return montauk_fdset_count(readfds, nfds);
}
ready = 0;
if (timeout != NULL) {
unsigned long usec = (unsigned long) timeout->tv_sec * 1000000UL +
(unsigned long) timeout->tv_usec;
if (usec != 0)
usleep(usec);
}
return ready;
}
#endif
+42
View File
@@ -0,0 +1,42 @@
/*
* Minimal <sys/socket.h> for the MontaukOS NetSurf port.
*
* NetSurf includes this via utils/inet.h. MontaukOS has no BSD socket layer
* (the kernel exposes sockets as IPC handles through montauk::socket), so this
* supplies only the few declarations NetSurf's non-curl paths actually touch:
* the address families and socklen_t. Nothing here opens a socket.
*/
#ifndef _MONTAUK_COMPAT_SYS_SOCKET_H_
#define _MONTAUK_COMPAT_SYS_SOCKET_H_
#include <stdint.h>
#include <stddef.h>
#define AF_UNSPEC 0
#define AF_INET 2
#define AF_INET6 10
typedef uint32_t socklen_t;
#define SOCK_STREAM 1
#define SOCK_DGRAM 2
/*
* socket() exists only so desktop/gui_factory.c compiles: it installs
* gui_default_socket_open() into the fetch table, and that default is reached
* only by a fetcher that opens sockets -- i.e. curl, which is not built here.
*
* It deliberately FAILS rather than pretending. MontaukOS sockets are IPC
* handles obtained through montauk::socket() and are not interchangeable with
* BSD descriptors, so any caller reaching this wants the real fetcher instead.
* fetch_montauk will provide its own socket_open in the fetch table.
*/
static inline int socket(int domain, int type, int protocol)
{
(void) domain;
(void) type;
(void) protocol;
return -1;
}
#endif