65816-llvm-mos/runtime/src/libc.c
2026-07-05 23:30:54 -05:00

2801 lines
100 KiB
C

// Minimal libc for the W65816 backend. Provides:
// string.h: memcpy, memset, memmove, memcmp, strlen, strcpy, strcmp,
// strncpy, strncmp, strchr, strrchr
// ctype.h: isdigit, isalpha, isalnum, isspace, isupper, islower,
// toupper, tolower, isxdigit, isprint, ispunct
// stdlib.h: abs, labs, atoi
//
// All functions are straightforward implementations using only
// integer ops. Each is short enough that internal conditional
// branches stay within 8-bit PCREL reach.
//
// Output goes (eventually) through a putchar stub that targets a
// memory-mapped IO port or a MAME-debug Lua hook; for now putchar
// is provided as a weak stub that does nothing.
typedef unsigned long size_t;
typedef int ssize_t;
typedef unsigned char u8;
typedef unsigned short u16;
// MUST stay in lock-step with PATH_MAX in runtime/include/limits.h.
// Not pulled via `#include <limits.h>` because libc.c is built standalone
// (the smoke harness compiles it without -I runtime/include, which lets
// clang's own limits.h win the lookup and drag in glibc headers).
#define LIBC_PATH_MAX 256
// GS/OS class-1 file-call hooks. Resolved at link time by the
// iigsGsos.s wrappers (which themselves dispatch through $E100A8).
// Declared inline here to avoid pulling iigs/gsos.h's full type
// surface into libc.c. The parm-block types are local matches of
// iigs/gsos.h's structs — kept layout-equivalent so callers in
// iigs/gsos.h can interoperate.
typedef struct {
u16 pCount;
u16 refNum;
void *pathname;
u16 requestAccess;
} __GsosOpenParm;
typedef struct {
u16 pCount;
u16 refNum;
void *dataBuffer;
unsigned long requestCount;
unsigned long transferCount;
} __GsosIORecGS;
typedef struct {
u16 pCount;
u16 refNum;
} __GsosRefNumRecGS;
typedef struct {
u16 pCount;
u16 refNum;
unsigned long eof;
} __GsosEOFRecGS;
typedef struct {
u16 pCount;
u16 refNum;
unsigned long position;
} __GsosMarkRecGS;
typedef struct {
u16 pCount;
void *pathname;
u16 access;
u16 fileType;
unsigned long auxType;
u16 storageType;
} __GsosCreateParm;
typedef struct {
u16 pCount;
void *pathname;
} __GsosDestroyParm;
typedef struct {
u16 pCount;
void *oldPathname;
void *newPathname;
} __GsosChangePathParm;
// GSString-like length-prefixed buffer + max length cap for the OS to
// observe. Matches `ResultBuf` in iigs/gsos.h byte-for-byte (maxLen,
// then a GSString { length, text[1] }).
typedef struct {
u16 maxLen;
u16 length;
char text[1]; // variable-length tail
} __GsosResultBuf;
typedef struct {
u16 pCount; // 2
u16 prefixNum;
void *prefix; // __GsosResultBuf *
} __GsosPrefixParm;
// Full Get_File_Info parm block. We only set pCount=4 from realpath()
// (just enough to retrieve storageType), but the full struct is laid
// out so callers needing aux fields can use the same type.
typedef struct {
u16 pCount;
void *pathname; // GSString *
u16 access;
u16 fileType;
unsigned long auxType;
u16 storageType;
unsigned char createDate[8];
unsigned char modDate[8];
void *optionList;
unsigned long eof;
unsigned long blocksUsed;
unsigned long resourceEOF;
unsigned long resourceBlocks;
} __GsosFileInfoParm;
typedef struct {
u16 pCount;
u16 refNum;
u16 flags;
u16 base;
u16 displacement;
void *name; // __GsosResultBuf *
u16 entryNum;
u16 fileType;
unsigned long eof;
unsigned long blockCount;
unsigned char createDate[8];
unsigned char modDate[8];
u16 access;
unsigned long auxType;
u16 fileSysID;
void *optionList;
unsigned long resourceEOF;
unsigned long resourceBlocks;
} __GsosDirEntryParm;
// Weak so programs that never call into the GS/OS file backend don't
// drag iigsGsos.o into the link. fopen guards GSOS path through
// __gsosAvailable() below.
//
// `retain` + `used` on the weak-extern decl is the LTO survival policy
// (Phase 1.11): under LTO the inliner can decide an undefined weak is
// constant-0/NULL and propagate that through every caller, DCE-ing the
// dispatcher arms entirely. `used` keeps the compiler from removing
// references to the symbol; `retain` survives linker GC. In a non-LTO
// build the attributes are no-ops on a declaration (no body to retain).
extern u16 gsosOpen (__GsosOpenParm *p) __attribute__((weak, retain, used));
extern u16 gsosRead (__GsosIORecGS *p) __attribute__((weak, retain, used));
extern u16 gsosWrite (__GsosIORecGS *p) __attribute__((weak, retain, used));
extern u16 gsosClose (__GsosRefNumRecGS *p) __attribute__((weak, retain, used));
extern u16 gsosGetEOF (__GsosEOFRecGS *p) __attribute__((weak, retain, used));
extern u16 gsosSetEOF (__GsosEOFRecGS *p) __attribute__((weak, retain, used));
extern u16 gsosSetMark(__GsosMarkRecGS *p) __attribute__((weak, retain, used));
extern u16 gsosGetMark(__GsosMarkRecGS *p) __attribute__((weak, retain, used));
extern u16 gsosCreate (__GsosCreateParm *p) __attribute__((weak, retain, used));
extern u16 gsosDestroy (__GsosDestroyParm *p) __attribute__((weak, retain, used));
extern u16 gsosChangePath(__GsosChangePathParm *p) __attribute__((weak, retain, used));
extern u16 gsosGetPrefix (__GsosPrefixParm *p) __attribute__((weak, retain, used));
extern u16 gsosGetFileInfo(__GsosFileInfoParm *p) __attribute__((weak, retain, used));
extern u16 gsosGetDirEntry(__GsosDirEntryParm *p) __attribute__((weak, retain, used));
// Stub-mode sentinel. Defined in iigsGsos.s as 1 (real dispatch
// wrappers linked) and in iigsGsosStub.s as 0 (universal-success
// stub linked). When neither is linked the weak-extern resolves to
// address 0, which we detect via &__gsosIsRealImpl == 0 — otherwise
// loading from address 0 dereferences NULL. This is the single
// source of truth that distinguishes "real GS/OS available" from
// "stub linked" from "no GS/OS surface at all", so newly-added
// wrappers can refuse to silently lie about success.
extern int __gsosIsRealImpl __attribute__((weak, retain, used));
int __gsosAvailable(void) {
if (&__gsosIsRealImpl == (int *)0) {
return 0;
}
return __gsosIsRealImpl;
}
// ---- string.h ----
void *memcpy(void *dst, const void *src, size_t n) {
char *d = (char *)dst;
const char *s = (const char *)src;
while (n--) *d++ = *s++;
return dst;
}
void *memmove(void *dst, const void *src, size_t n) {
char *d = (char *)dst;
const char *s = (const char *)src;
if (d < s) {
while (n--) *d++ = *s++;
} else {
d += n; s += n;
while (n--) *--d = *--s;
}
return dst;
}
void *memset(void *dst, int c, size_t n) {
char *d = (char *)dst;
while (n--) *d++ = (char)c;
return dst;
}
int memcmp(const void *a, const void *b, size_t n) {
const u8 *p = (const u8 *)a;
const u8 *q = (const u8 *)b;
while (n--) {
if (*p != *q) return *p - *q;
p++; q++;
}
return 0;
}
size_t strlen(const char *s) {
size_t n = 0;
while (*s++) n++;
return n;
}
char *strcpy(char *dst, const char *src) {
char *d = dst;
while ((*d++ = *src++)) {}
return dst;
}
char *strncpy(char *dst, const char *src, size_t n) {
char *d = dst;
while (n && (*d = *src)) { d++; src++; n--; }
while (n--) *d++ = 0;
return dst;
}
int strcmp(const char *a, const char *b) {
while (*a && *a == *b) { a++; b++; }
return (u8)*a - (u8)*b;
}
int strncmp(const char *a, const char *b, size_t n) {
while (n && *a && *a == *b) { a++; b++; n--; }
if (!n) return 0;
return (u8)*a - (u8)*b;
}
char *strchr(const char *s, int c) {
while (*s) {
if (*s == (char)c) return (char *)s;
s++;
}
if ((char)c == 0) return (char *)s;
return 0;
}
char *strrchr(const char *s, int c) {
const char *r = 0;
while (*s) {
if (*s == (char)c) r = s;
s++;
}
if ((char)c == 0) return (char *)s;
return (char *)r;
}
// ---- ctype.h ----
int isdigit(int c) { return c >= '0' && c <= '9'; }
int isupper(int c) { return c >= 'A' && c <= 'Z'; }
int islower(int c) { return c >= 'a' && c <= 'z'; }
int isalpha(int c) { return isupper(c) || islower(c); }
int isalnum(int c) { return isalpha(c) || isdigit(c); }
int isspace(int c) {
return c == ' ' || c == '\t' || c == '\n' ||
c == '\r' || c == '\v' || c == '\f';
}
int isxdigit(int c) {
return isdigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
int isprint(int c) { return c >= 0x20 && c < 0x7f; }
int ispunct(int c) { return isprint(c) && !isalnum(c) && c != ' '; }
int iscntrl(int c) { return (c >= 0 && c < 0x20) || c == 0x7f; }
int isgraph(int c) { return isprint(c) && c != ' '; }
int isblank(int c) { return c == ' ' || c == '\t'; }
int toupper(int c) { return islower(c) ? c - 32 : c; }
int tolower(int c) { return isupper(c) ? c + 32 : c; }
// ---- stdlib.h ----
int abs(int n) { return n < 0 ? -n : n; }
long labs(long n) { return n < 0 ? -n : n; }
// div/ldiv/lldiv: return both quotient and remainder in one struct.
// Useful for code that wants a single libcall instead of paired / and %.
// Per C99: quot is integer division truncated toward zero; rem has the
// same sign as the numerator.
typedef struct { int quot, rem; } div_t;
typedef struct { long quot, rem; } ldiv_t;
typedef struct { long long quot, rem; } lldiv_t;
div_t div (int n, int d) { div_t r; r.quot = n/d; r.rem = n%d; return r; }
ldiv_t ldiv (long n, long d) { ldiv_t r; r.quot = n/d; r.rem = n%d; return r; }
lldiv_t lldiv(long long n, long long d) { lldiv_t r; r.quot = n/d; r.rem = n%d; return r; }
int atoi(const char *s) {
int sign = 1;
while (isspace(*s)) s++;
if (*s == '-') { sign = -1; s++; }
else if (*s == '+') { s++; }
// Parse magnitude as unsigned to dodge signed-overflow UB on
// values like "32768" (parsing INT_MAX+1 as signed int).
unsigned int u = 0;
while (isdigit(*s)) {
u = u * 10 + (unsigned int)(*s - '0');
s++;
}
return sign < 0 ? (int)(0u - u) : (int)u;
}
// ---- stdio.h essentials (stubs) ----
// putchar: by default, writes to direct-page slot $E2 (which the
// emulator harness can poll). Real targets (MAME with our IIgs
// glue, or a console emulator) override this with a strong
// definition. Marked `weak` so users can replace it.
//
// NB: this is a weak DECLARATION (no definition here), NOT a weak
// default definition. A weak default defined in this same TU would
// lose to itself: the strong override in libcGno.o gets gc'd / out-
// resolved (verified — `putchar`'s call bound to the local weak $E2
// default and the GNO console went silent). Keeping it an undefined-
// weak ref forces link-time resolution to whatever strong def is
// linked (libcGno's console putByte under GNO, else nothing -> the
// $E2 fallback below). The `&__putByte` footgun this creates is
// handled in the backend: W65816AsmPrinter emits `lda #0` (not the PBR
// `lda $BE`) for an external-weak symbol's bank half, and link816 skips
// recording a cRELOC for a sub-text-base (weak-null) target.
extern void __putByte(char c) __attribute__((weak, retain, used));
int putchar(int c) {
if (__putByte)
__putByte((char)c);
else
*(volatile char *)0xE2 = (char)c;
return c;
}
int puts(const char *s) {
while (*s) { putchar(*s); s++; }
putchar('\n');
return 0;
}
// ---- input ----
//
// getchar polls the IIgs hardware keyboard register at $C000. Bit 7
// is the "key ready" strobe; bits 0..6 are the ASCII code. Reading
// $C010 clears the strobe so the next keypress can be detected. This
// blocks until a key is pressed and returns the (7-bit) ASCII value.
// It works in any execution context (raw boot, ProDOS-16, GS/OS app)
// because $C000 is hardware I/O, not toolbox-mediated.
//
// Callers wanting non-blocking input or Event Manager integration
// should call ReadCh/GetNextEvent directly via iigs/toolbox.h.
// Console byte source. Default polls the IIgs keyboard at $C000. A
// hosted environment (GNO/ME) provides a strong __getByte that reads
// its console (returns -1 on EOF). Weak DECLARATION, not a default
// definition — see __putByte for why.
extern int __getByte(void) __attribute__((weak, retain, used));
int getchar(void) {
if (__getByte)
return __getByte();
volatile unsigned char *kbd = (volatile unsigned char *)0xC000;
volatile unsigned char *strb = (volatile unsigned char *)0xC010;
while (((*kbd) & 0x80) == 0) {
// Spin until a key is ready. No yield — single-threaded.
}
int c = (*kbd) & 0x7F;
(void)*strb; // clear strobe
return c;
}
// ---- minimal printf ----
// Re-declare va_list / va_* locally rather than including stdarg.h —
// clang's built-ins work directly and skip a header pull-in that's
// otherwise harmless but adds compile time on every TU.
typedef __builtin_va_list va_list;
#define va_start(ap, last) __builtin_va_start(ap, last)
#define va_arg(ap, ty) __builtin_va_arg(ap, ty)
#define va_end(ap) __builtin_va_end(ap)
// vprintf / printf used to dispatch through their own small format
// helpers (writeUDec/writeDec/writeULong/writeHex/writeStr/writeSignedLong/
// writeDouble). Once vprintf was rewritten to route through vsnprintf
// (so printf and snprintf share one format engine in snprintf.c), the
// helpers became dead weight and were removed.
extern int vsnprintf(char *buf, size_t n, const char *fmt, va_list ap);
// vprintf used to carry its own format dispatcher (subset of vsnprintf's).
// That meant `%lld`, `%zu`, `%n`, flag/width/`*` etc. quietly worked in
// snprintf but NOT in printf — confusing for portable code. Route
// through vsnprintf via a stack buffer; the buffer is sized to handle
// most lines in one shot. Single-threaded use only.
int vprintf(const char *fmt, va_list ap) {
char tmp[256];
int n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
int emitted = n < (int)sizeof(tmp) ? n : (int)sizeof(tmp) - 1;
for (int i = 0; i < emitted; i++) putchar(tmp[i]);
return n;
}
int printf(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int r = vprintf(fmt, ap);
va_end(ap);
return r;
}
// ---- additional string.h ----
void *memchr(const void *s, int c, size_t n) {
const u8 *p = (const u8 *)s;
while (n--) {
if (*p == (u8)c) return (void *)p;
p++;
}
return 0;
}
// strstr: index-based scan rather than pointer-increment.
char *strstr(const char *haystack, const char *needle) {
if (!needle[0]) return (char *)haystack;
unsigned int i = 0;
while (haystack[i]) {
unsigned int j = 0;
while (needle[j] && haystack[i + j] == needle[j]) j++;
if (!needle[j]) return (char *)(haystack + i);
i++;
}
return 0;
}
// Forward declarations for strdup/strndup; the actual definitions
// live further down in the file (malloc and strnlen are below).
extern void *malloc(size_t);
extern size_t strnlen(const char *, size_t);
// strdup/strndup — POSIX, super common in real-world code.
// Both allocate via malloc; caller frees.
char *strdup(const char *s) {
size_t n = strlen(s);
char *r = (char *)malloc(n + 1);
if (!r) return 0;
memcpy(r, s, n + 1);
return r;
}
char *strndup(const char *s, size_t maxlen) {
size_t n = strnlen(s, maxlen);
char *r = (char *)malloc(n + 1);
if (!r) return 0;
memcpy(r, s, n);
r[n] = 0;
return r;
}
// memccpy — copy until either `n` bytes done OR byte `c` was copied.
// Returns ptr to byte after the copied `c`, or NULL if `c` not found.
void *memccpy(void *dst, const void *src, int c, size_t n) {
unsigned char *d = (unsigned char *)dst;
const unsigned char *s = (const unsigned char *)src;
while (n--) {
unsigned char v = *s++;
*d++ = v;
if (v == (unsigned char)c) return d;
}
return 0;
}
// stpcpy — like strcpy but returns ptr to terminating NUL of dst.
char *stpcpy(char *dst, const char *src) {
while ((*dst = *src) != 0) { dst++; src++; }
return dst;
}
// stpncpy — like strncpy but returns ptr to terminator (or to dst+n
// if no terminator was copied).
char *stpncpy(char *dst, const char *src, size_t n) {
char *p = dst;
while (n && (*p = *src) != 0) { p++; src++; n--; }
char *end = p;
while (n--) *p++ = 0;
return end;
}
// ---- malloc/free — first-fit allocator with coalescing-on-free ----
//
// Heap lives between the static-data top (linker-supplied __heap_start)
// and a soft cap. Each allocated block is preceded by a 2-byte header
// holding the block's payload size in bytes. Free blocks add a 2-byte
// "next" pointer after the size, forming a singly-linked free list.
//
// malloc: first-fit walk of the free list; split the chosen block when
// the remainder is large enough to host its own header+next.
// free: insert onto the head of the free list, then coalesce with any
// adjacent free blocks (forward and backward via free-list scan).
//
// The bump fallback (top of heap) is used when the free list has no
// suitable block.
// Linker-supplied weak symbols; fallback to fixed defaults so a static
// link without crt0 still has SOMETHING.
extern char __heap_start[] __attribute__((weak, retain, used));
extern char __heap_end[] __attribute__((weak, retain, used));
#define HEAP_DEFAULT_START ((char *)0x4000)
#define HEAP_DEFAULT_END ((char *)0xBF00)
// Heap is bounded to <32KB so the size field stays uint16_t even
// under 32-bit size_t (saves 2 bytes/header). next-pointer width
// follows the data layout (2 bytes under p:16, 4 under p:32) — bake
// it into FREE_NODE_SZ via sizeof.
typedef struct FreeBlk {
u16 size; // payload size, NOT including header
struct FreeBlk *next; // valid only while in the free list
} FreeBlk;
#define HDR_SZ ((size_t)sizeof(u16))
#define FREE_NODE_SZ ((size_t)(sizeof(u16) + sizeof(struct FreeBlk *)))
#define MIN_SPLIT ((size_t)(FREE_NODE_SZ + 2))
static FreeBlk *freeList = (FreeBlk *)0;
static char *bumpPtr = (char *)0;
static char *heapEnd = (char *)0;
static void mallocInitOnce(void) {
if (bumpPtr) return;
bumpPtr = __heap_start ? __heap_start : HEAP_DEFAULT_START;
heapEnd = __heap_end ? __heap_end : HEAP_DEFAULT_END;
freeList = (FreeBlk *)0;
}
// GS/OS: back the malloc heap with a locked Memory-Manager handle so the
// heap span is owned by the MM and no later NewHandle (ours or the
// system's) can land on top of live malloc data. crt0Gsos calls this with
// our MM user ID in A, before .init_array. crt0.s (bare-metal, no Memory
// Manager) and crt0Gno.s do NOT call it, so -ffunction-sections GC drops
// this function -- and its NewHandle reference -- from those links. A weak
// no-op __heapInitMM in libgcc.s resolves crt0Gsos's `jsl` for GS/OS
// programs that don't link libc.o.
//
// On any failure -- NewHandle returns 0, a null master pointer, or a block
// that straddles a bank boundary -- it returns without setting bumpPtr, so
// mallocInitOnce falls back to the link-time [__heap_start,__heap_end)
// window. A tight-memory machine is therefore never worse off than before.
extern void *NewHandle(unsigned long size, unsigned short userID,
unsigned short attr, void *loc);
extern void DisposeHandle(void *h);
void __heapInitMM(unsigned short userID) {
if (bumpPtr) {
return; // already initialised
}
// Heap size: keep the link-time window when it is in a sane range,
// else a 32 KB default. The window is bank-0-bounded, so it never
// spans more than a single bank.
unsigned long want = (unsigned long)(void *)__heap_end
- (unsigned long)(void *)__heap_start;
if (want < 0x2000UL || want > 0xC000UL) {
want = 0x8000UL; // 32 KB
}
// attr 0x8014 = attrLocked | attrNoCross | attrPage. Every bit here is
// set in the proven-good desktop.c DP-block value 0xC015, minus
// attrFixed and attrBank -- so the MM is free to place the block in any
// bank and then pin it. attrLocked keeps the master pointer stable,
// which malloc relies on (it hands out raw pointers into the block).
void *h = NewHandle(want, userID, (unsigned short)0x8014, (void *)0);
if (!h) {
return; // fall back to link-time window
}
char *base = *(char **)h;
if (!base) {
DisposeHandle(h);
return;
}
// The allocator does flat pointer arithmetic and assumes a single bank;
// reject (rather than corrupt on) a straddling block. This also makes
// correctness independent of the attrNoCross bit guess above.
unsigned long lo = (unsigned long)(void *)base;
unsigned long hi = lo + want - 1UL;
if ((lo & 0xFF0000UL) != (hi & 0xFF0000UL)) {
DisposeHandle(h);
return;
}
bumpPtr = base;
heapEnd = base + want;
freeList = (FreeBlk *)0;
}
void *malloc(size_t n0) {
mallocInitOnce();
// Heap ceiling is ~32KB so anything > 0x7FF0 is unsatisfiable.
if (n0 > (size_t)0x7FF0) return (void *)0;
// Round up to 2-byte alignment, with a minimum of FREE_NODE_SZ-HDR_SZ.
// Keep this in 16-bit arithmetic: the 0x7FF0 cap above guarantees the
// value fits, and 16-bit ops are cheaper than i32 on the 65816. (An
// earlier comment here blamed an "i32 umax miscompile" for staying
// 16-bit; that was a misattribution. The i32 path is correct, verified
// in MAME; the real over-heap bug was a control-flow live-in staleness
// bug in W65816BranchExpand, since fixed.)
u16 n = (u16)n0;
if (n == 0) n = 1;
n = (u16)((n + 1) & ~(u16)1);
if (n < (u16)(FREE_NODE_SZ - HDR_SZ))
n = (u16)(FREE_NODE_SZ - HDR_SZ);
// First-fit on free list.
FreeBlk **link = &freeList;
FreeBlk *cur = freeList;
while (cur) {
if (cur->size >= n) {
// Split if there's room for a separate free block.
if (cur->size >= n + MIN_SPLIT) {
u16 rem = (u16)(cur->size - n - HDR_SZ);
FreeBlk *tail = (FreeBlk *)((char *)cur + HDR_SZ + n);
tail->size = rem;
tail->next = cur->next;
cur->size = (u16)n;
*link = tail;
} else {
*link = cur->next;
}
return (char *)cur + HDR_SZ;
}
link = &cur->next;
cur = cur->next;
}
// Bump-allocate from the high end. This heap only serves small
// allocations: the 0x7FF0 cap at the top of malloc() bounds n, and
// callers needing big buffers (e.g. a 16 KB sprite scratch) get them
// straight from the GS/OS Memory Manager, not malloc. (There is no
// "halBigAlloc" routing -- an older comment here described one that was
// never implemented; oversized requests simply hit the 0x7FF0 cap and
// return NULL.) The `p + HDR_SZ + n > heapEnd` compare is correct and
// is covered by smoke check #176. The historical "over-heap" failures
// were the W65816BranchExpand/SepRepCleanup live-in bug (since fixed),
// not an i32 miscompile; `heapEnd - p` is equally correct now, MAME-
// verified -- the addition form is kept only because it is the tested one.
char *p = bumpPtr;
if (p + HDR_SZ + n > heapEnd) return (void *)0;
*(u16 *)p = (u16)n;
bumpPtr = p + HDR_SZ + n;
return p + HDR_SZ;
}
void free(void *p) {
if (!p) return;
FreeBlk *blk = (FreeBlk *)((char *)p - HDR_SZ);
blk->next = freeList;
freeList = blk;
// Coalesce: walk the free list and merge adjacent blocks. Outer
// loop tracks a's predecessor (a_link) so we can excise `a` when
// it gets absorbed into a lower-address neighbour. Without that,
// an `aEnd == b` from b's perspective (i.e. b precedes a in
// memory) would extend b but leave a in the list — a future malloc
// could then hand out a's range as a "free" block while the
// expanded b overlaps it. O(n^2) in the worst case; n is small.
FreeBlk **a_link = &freeList;
FreeBlk *a = freeList;
while (a) {
int a_absorbed = 0;
FreeBlk **link = &a->next;
FreeBlk *b = a->next;
while (b) {
char *aEnd = (char *)a + HDR_SZ + a->size;
char *bEnd = (char *)b + HDR_SZ + b->size;
if (aEnd == (char *)b) {
// a immediately precedes b — extend a, drop b.
a->size = (u16)(a->size + HDR_SZ + b->size);
*link = b->next;
b = *link;
continue;
}
if (bEnd == (char *)a) {
// b immediately precedes a — extend b, drop a from
// the outer list. We can't continue the inner walk
// (a is gone), so break out and let the outer loop
// restart from a's successor.
b->size = (u16)(b->size + HDR_SZ + a->size);
*a_link = a->next;
a_absorbed = 1;
break;
}
link = &b->next;
b = b->next;
}
if (a_absorbed) {
a = *a_link; // already advanced by the excise
} else {
a_link = &a->next;
a = a->next;
}
}
}
void *calloc(size_t nmemb, size_t size) {
// size_t is 32-bit, so the multiply itself won't overflow for any
// realistic input. The 0xFFFF cap is a "fits in one 64KB bank"
// sanity check: the heap lives in bank 0 below the IO window, so
// any single allocation must fit there. calloc(65536, 1) returns
// null rather than silently truncating into the IO range.
if (size != 0 && nmemb > (size_t)0xFFFF / size) return (void *)0;
size_t total = nmemb * size;
void *p = malloc(total);
if (p) memset(p, 0, total);
return p;
}
void *realloc(void *ptr, size_t n) {
if (!ptr) return malloc(n);
if (n == 0) { free(ptr); return (void *)0; }
size_t old = *(u16 *)((char *)ptr - HDR_SZ);
if (n <= old) return ptr;
void *q = malloc(n);
if (!q) return (void *)0;
memcpy(q, ptr, old);
free(ptr);
return q;
}
// ---- atexit / exit ----
//
// Standard exit() halts via BRK after running any registered atexit
// handler. Programs running under the IIgs runtime typically would
// call back into GS/OS Quit; here we just wedge the CPU. Single-slot
// atexit (the storage and registration function are below).
typedef void (*AtexitFn)(void);
static AtexitFn __atexitFn = (AtexitFn)0;
// BRK $00 then spin -- halts a 65816 in BRK so MAME's debugger catches
// it; the spin loop guards against the (rare) case where BRK returns.
static void __halt(void) __attribute__((noreturn));
static void __halt(void) {
__asm__ volatile (".byte 0x00, 0x00");
while (1) {}
}
void exit(int code) {
(void)code;
// C99 7.20.4.3: exit() must invoke registered atexit handlers in
// reverse-registration order before terminating.
if (__atexitFn) {
AtexitFn fn = __atexitFn;
__atexitFn = (AtexitFn)0; // prevent re-entry if fn calls exit
fn();
}
__halt();
}
// ---- errno ----
//
// Single global errno cell. Library functions that want to report a
// failure code write here. The `errno` macro in <errno.h> expands to
// `(*__errno_location())` — we provide that for source compatibility,
// but most code can just touch `errno` directly.
int errno = 0;
int *__errno_location(void) { return &errno; }
char *strerror(int err) {
switch (err) {
case 0: return (char *)"Success";
case 1: return (char *)"Operation not permitted";
case 2: return (char *)"No such file or directory";
case 3: return (char *)"No such process";
case 4: return (char *)"Interrupted system call";
case 5: return (char *)"Input/output error";
case 6: return (char *)"No such device or address";
case 7: return (char *)"Argument list too long";
case 8: return (char *)"Exec format error";
case 9: return (char *)"Bad file descriptor";
case 10: return (char *)"No child processes";
case 11: return (char *)"Resource temporarily unavailable";
case 12: return (char *)"Out of memory";
case 13: return (char *)"Permission denied";
case 14: return (char *)"Bad address";
case 16: return (char *)"Device or resource busy";
case 17: return (char *)"File exists";
case 18: return (char *)"Cross-device link";
case 19: return (char *)"No such device";
case 20: return (char *)"Not a directory";
case 21: return (char *)"Is a directory";
case 22: return (char *)"Invalid argument";
case 23: return (char *)"Too many open files in system";
case 24: return (char *)"Too many open files";
case 25: return (char *)"Inappropriate I/O control operation";
case 26: return (char *)"Text file busy";
case 27: return (char *)"File too large";
case 28: return (char *)"No space left on device";
case 29: return (char *)"Illegal seek";
case 30: return (char *)"Read-only file system";
case 31: return (char *)"Too many links";
case 32: return (char *)"Broken pipe";
case 33: return (char *)"Numerical argument out of domain";
case 34: return (char *)"Numerical result out of range";
case 36: return (char *)"File name too long";
case 38: return (char *)"Function not implemented";
case 39: return (char *)"Directory not empty";
case 40: return (char *)"Too many levels of symbolic links";
case 84: return (char *)"Invalid or incomplete multibyte or wide character";
default: return (char *)"Unknown error";
}
}
// perror — write `prefix: errno-string\n` to stderr. Common pattern in
// portable programs that report I/O failures.
void perror(const char *prefix) {
if (prefix && *prefix) {
const char *p = prefix;
while (*p) { putchar(*p); p++; }
putchar(':');
putchar(' ');
}
const char *m = strerror(errno);
while (*m) { putchar(*m); m++; }
putchar('\n');
}
// ---- time.h ----
//
// time() reads the IIgs RTC via ReadTimeHex (Misc Tool $0D03) and
// converts the broken-down date/time to seconds since 1970-01-01.
// Requires `iigsToolboxInit()` to have run at least once — without
// the Tool Locator initialised, JSL $E10000 crashes. Programs
// that need real time() should call iigsToolboxInit() early from
// main; otherwise time() returns 0 (no crash, but no clock).
//
// Under crt0Gsos (GS/OS app) and crt0Gno (GNO shell command) the Tool
// Locator is already initialised by the host (the GS/OS Loader before
// __start, or by the GNO kernel which itself calls JSL $E10000), so
// iigsToolboxInit() is effectively a no-op there but harmless to call.
// Under crt0.s (bare metal, no GS/OS), this is the ONLY way to get TL
// up — and the explicit SEI is critical because TLStartUp's internal
// CLI re-enables the ROM VBL IRQ which runs in M=X=8 and would
// corrupt our in-progress M=16 register state.
//
// clock() reads the IIgs vertical-blank counter at $00/E1/006B (1
// byte that increments every VBL ~= 60 Hz) via inline asm with a
// 24-bit absolute load — works with or without toolbox init since
// the VBL counter is just a memory location updated by the IRQ
// handler. Wraparound tracked in a u32 static so the counter can
// span days. CLOCKS_PER_SEC is 60 (defined in time.h).
// Toolbox-init flag, set by iigsToolboxInit(). time() guards on it.
static unsigned short __toolboxInited = 0;
void iigsToolboxInit(void) {
if (__toolboxInited) return;
__asm__ volatile (
"rep #0x30\n"
"ldx #0x0201\n" // TLStartUp
"jsl 0xe10000\n"
"sei\n" // re-disable IRQ that the dispatcher may re-enable
"rep #0x30\n"
:
:
: "a", "x", "y", "memory"
);
__toolboxInited = 1;
}
typedef long time_t;
typedef unsigned long clock_t;
// ReadTimeHex returns 8 bytes via a parameter block: second, minute,
// hour, (unused), year-1900, day, month, weekday. Push a 4-word
// result-area on the stack, JSL X=$0D03, pop the words back into
// DP scratch ($E0..$E7), then memcpy out. We can't use "=g"
// constraints (W65816 backend rejects memory operands in inline
// asm), so the data path runs through known DP addresses.
//
// "memory" clobber on the asm tells the scheduler we touch arbitrary
// memory, so it can't reorder the asm against the volatile DP reads
// below. That permits inlining without losing the read ordering.
static void readTimeHex(unsigned char buf[8]) {
__asm__ volatile (
"pea 0\n"
"pea 0\n"
"pea 0\n"
"pea 0\n"
"ldx #0x0D03\n"
"jsl 0xe10000\n"
"pla\n"
"sta 0xe0\n"
"pla\n"
"sta 0xe2\n"
"pla\n"
"sta 0xe4\n"
"pla\n"
"sta 0xe6\n"
:
:
: "a", "x", "y", "memory"
);
// Read DP $E0..$E7 via known-good direct page accesses. We're
// in M=16 by ABI so each `lda` reads 2 bytes — split into bytes.
volatile unsigned char *dp = (volatile unsigned char *)0xE0;
for (int i = 0; i < 8; i++) buf[i] = dp[i];
}
// Days at start of each month (non-leap).
static const unsigned short __monthDays[12] = {
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
};
static int __isLeap(int y) {
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
}
time_t time(time_t *t) {
if (!__toolboxInited) {
if (t) *t = 0;
return 0;
}
unsigned char b[8];
readTimeHex(b);
int sec = b[0];
int min = b[1];
int hour = b[2];
int year = 1900 + b[4];
int day = b[5];
int month = b[6];
if (year < 1970 || month > 11) {
if (t) *t = 0;
return 0;
}
long days = 0;
for (int y = 1970; y < year; y++) {
days += __isLeap(y) ? 366 : 365;
}
days += __monthDays[month];
if (month > 1 && __isLeap(year)) days++;
days += day;
long secs = days * 86400L + (long)hour * 3600 + (long)min * 60 + sec;
if (t) *t = secs;
return secs;
}
// VBL counter at $00/E1/006B (1 byte). C `*p` deref where p is a
// 16-bit pointer can't reach $E1006B (would truncate to $006B in
// zero page), so we use inline asm with `lda 0xe1006b` (4-byte
// absolute-long, opcode 0xAF).
static unsigned long __vblBase = 0;
static unsigned char __vblPrev = 0;
clock_t clock(void) {
unsigned char now;
__asm__ volatile (
"sep #0x20\n"
"lda 0xe1006b\n" // 24-bit absolute
"rep #0x20\n"
"and #0x00ff\n"
: "=a"(now)
:
: "memory"
);
if (now < __vblPrev) {
__vblBase += 256;
}
__vblPrev = now;
return (clock_t)(__vblBase + now);
}
// ---- ETL chrono clock hooks (Phase 5.3 cxxchrono) ----
//
// `etl::chrono::{system,steady,high_resolution}_clock::now()` calls
// these `extern "C"` hooks; each is expected to return the current
// time as a count of the clock's duration::rep units. We configure
// all three clocks to `duration<int32_t, etl::milli>` via
// runtime/include/c++/etl_profile.h, so the rep is int32_t and the
// unit is milliseconds.
//
// Source of truth is the IIgs $E1:006B VBL counter (60 Hz on NTSC),
// already maintained by clock() above. Conversion factor:
// 1 VBL tick = 1000/60 ms = 50/3 ms
// We multiply by 50 in u32, then divide by 3. Intermediate (ticks*50)
// fits in u32 as long as ticks < UINT32_MAX/50 ≈ 85.9M, which at 60 Hz
// is ~16.5 days of wall time — well beyond any smoke or demo run.
// Beyond that the u32 wraps cleanly (unsigned defined-overflow).
//
// `system_clock` and `high_resolution_clock` share the same monotonic
// source; the IIgs has no walltime tick faster than VBL, and no
// monotonic-vs-realtime distinction in hardware. All three are
// "steady" in the standard sense (never decreases, no leap-second or
// wall-clock adjustment). The header documents this.
//
// Returning a strictly-i32 value is load-bearing: an i64 return would
// drag __addsi3 / __ashlsi3 / etc. into every now() comparison.
// Static-assert the contract before chrono callers ever see it.
// libc.c is built standalone (no -I runtime/include — see PATH_MAX
// note above), so we don't pull <stdint.h>; assert directly on the
// underlying scalar `long` we hand to the chrono surface.
typedef char __etl_chrono_rep_must_be_i32[
(sizeof(int) == 2 && sizeof(long) == 4) ? 1 : -1
];
static long __vblToMillis(clock_t ticks) {
// ticks is unsigned long (32-bit); the (50*ticks)/3 path stays in
// u32 below ~4 hours of wall time, then wraps cleanly. Cast to
// signed long at the end — chrono::steady_clock uses signed rep
// (`int32_t` on this target == `long`).
unsigned long ms = (ticks * 50UL) / 3UL;
return (long)ms;
}
long etl_get_steady_clock(void) {
return __vblToMillis(clock());
}
long etl_get_high_resolution_clock(void) {
return __vblToMillis(clock());
}
long etl_get_system_clock(void) {
return __vblToMillis(clock());
}
// ---- FILE* abstraction (memory-backed FS + GS/OS pass-through) ----
//
// stdin / stdout / stderr are tagged as kind=STDIO and route through
// putchar / fgetc-from-keyboard; opening a regular file allocates a
// FILE slot. Two backends:
//
// kind=MEM — backed by an mfsRegister'd in-memory buffer. Used by
// smoke tests that don't have a real disk; staged via
// mfsRegister(name, ptr, size, cap, writable) at startup.
// kind=GSOS — backed by a real GS/OS file. fopen falls through to
// gsosOpen for any path not in the mfs table, so callers
// with a mounted ProDOS volume get true file I/O via
// the GS/OS class-1 dispatcher (Open/Read/Write/Close/
// SetMark/GetMark/SetEOF/GetEOF). Requires a GS/OS-
// hosted environment; in a bare MAME boot (no ProDOS
// volume) gsosOpen fails and fopen returns NULL.
//
// FILE-table layout: 8 entries. Slot 0..2 are stdin/stdout/stderr
// (immutable); 3..7 are user-allocated by fopen. Each entry has:
// kind (0=stdin, 1=stdout, 2=stderr, 3=memory, 4=GS/OS)
// buf (memory buffer base; unused for GS/OS)
// size (logical size in bytes; unused for GS/OS — read on demand)
// cap (allocated capacity — for write-grow; unused for GS/OS)
// pos (current seek position; unused for GS/OS — Mark is authoritative)
// eof, err flags
// writable (1 if opened for "w" or "r+" or "a")
// ungetc holding cell (-1 = empty)
// refNum (GS/OS file reference; only valid when kind=GSOS)
#define FILE_KIND_STDIN 0
#define FILE_KIND_STDOUT 1
#define FILE_KIND_STDERR 2
#define FILE_KIND_MEM 3
#define FILE_KIND_GSOS 4
// Console byte sink for stderr. A hosted environment (GNO/ME) provides
// a strong __putByteErr that targets the stderr stream (GNO fd 3); when
// absent, stderr just shares stdout's sink (the historical behavior).
// Weak DECLARATION, not a default definition — see __putByte for why.
extern void __putByteErr(char c) __attribute__((weak, retain, used));
// Write one byte to the stdout (kind 1) or stderr (kind 2) console
// stream. Single dispatch point so every stderr write path routes to
// the right backend hook without duplicating the test.
static int putcharStd(int kind, int c) {
if (kind == FILE_KIND_STDERR && __putByteErr) {
__putByteErr((char)c);
return c;
}
return putchar(c);
}
typedef struct __sFILE {
u8 kind;
u8 writable;
u8 eof;
u8 err;
u8 autoDelete; // 1 = remove(path) on fclose (tmpfile)
char *buf;
size_t size;
size_t cap;
size_t pos;
int unget; // -1 if no pushed-back char
const char *path; // borrowed from caller; for autoDelete files
// points into __tmpNames[slot].
unsigned short refNum; // GS/OS file reference (kind=GSOS only)
} FILE;
#define MFS_MAX_FILES 8
// Per-FILE-slot tmpfile name storage. Parallel to __mfs[] so an
// auto-delete FILE can own its name without the caller having to pass
// (and keep alive) a path string. Only slots 3..MFS_MAX_FILES-1 are
// ever populated by tmpfile(); 0..2 are stdin/stdout/stderr. Each
// entry is L_tmpnam bytes -- matches stdio.h's macro so portable
// callers passing a buffer of exactly L_tmpnam can pass it to tmpnam()
// and we'll fill in <= that many chars (incl. terminating NUL).
//
// L_tmpnam is 24 here -- big enough for our canonical "/RAM5/Txxxxxxxx.TMP"
// shape (19 chars + NUL = 20) plus headroom for prefix tuning. Must
// stay in lock-step with the value in <stdio.h>.
#define LIBC_L_TMPNAM 24
static char __tmpNames[MFS_MAX_FILES][LIBC_L_TMPNAM];
static FILE __mfs[MFS_MAX_FILES] = {
{ .kind = FILE_KIND_STDIN, .unget = -1 },
{ .kind = FILE_KIND_STDOUT, .writable = 1, .unget = -1 },
{ .kind = FILE_KIND_STDERR, .writable = 1, .unget = -1 },
};
FILE *stdin = &__mfs[0];
FILE *stdout = &__mfs[1];
FILE *stderr = &__mfs[2];
// Registered "files" available to fopen. Each registration is
// (path, buf, size, writable). Order doesn't matter — fopen scans
// linearly.
typedef struct {
const char *path;
char *buf;
size_t size;
size_t cap;
u8 writable;
u8 inUse;
} MfsEntry;
#define MFS_MAX_REG 16
static MfsEntry __mfsReg[MFS_MAX_REG];
// Register a memory region as a named file. Returns 0 on success,
// -1 if the table is full or a duplicate name exists. `cap` may be
// larger than `size` to allow appends without reallocation; pass
// cap=size if writes must not grow the file.
int mfsRegister(const char *path, void *buf, size_t size, size_t cap,
int writable) {
if (cap < size) cap = size;
for (int i = 0; i < MFS_MAX_REG; i++) {
if (__mfsReg[i].inUse && strcmp(__mfsReg[i].path, path) == 0)
return -1;
}
for (int i = 0; i < MFS_MAX_REG; i++) {
if (!__mfsReg[i].inUse) {
__mfsReg[i].path = path;
__mfsReg[i].buf = (char *)buf;
__mfsReg[i].size = size;
__mfsReg[i].cap = cap;
__mfsReg[i].writable = (u8)(writable != 0);
__mfsReg[i].inUse = 1;
return 0;
}
}
return -1;
}
// Drop a registration. Returns 0 on success, -1 if not found.
int mfsUnregister(const char *path) {
for (int i = 0; i < MFS_MAX_REG; i++) {
if (__mfsReg[i].inUse && strcmp(__mfsReg[i].path, path) == 0) {
__mfsReg[i].inUse = 0;
__mfsReg[i].path = (const char *)0;
return 0;
}
}
return -1;
}
int fputc(int c, FILE *stream) {
if (!stream) return -1;
if (stream->kind == FILE_KIND_STDOUT || stream->kind == FILE_KIND_STDERR)
return putcharStd(stream->kind, c);
if (stream->kind == FILE_KIND_MEM) {
if (!stream->writable) { stream->err = 1; return -1; }
if (stream->pos >= stream->cap) { stream->err = 1; return -1; }
stream->buf[stream->pos++] = (char)c;
if (stream->pos > stream->size) stream->size = stream->pos;
return (int)(unsigned char)c;
}
if (stream->kind == FILE_KIND_GSOS) {
if (!stream->writable) { stream->err = 1; return -1; }
unsigned char b = (unsigned char)c;
__GsosIORecGS r = { 4, stream->refNum, &b, 1, 0 };
if (gsosWrite(&r) != 0 || r.transferCount != 1) {
stream->err = 1;
return -1;
}
return (int)b;
}
return -1;
}
int fputs(const char *s, FILE *stream) {
if (!stream || !s) return -1;
if (stream->kind == FILE_KIND_STDOUT || stream->kind == FILE_KIND_STDERR) {
while (*s) { putcharStd(stream->kind, *s); s++; }
return 0;
}
if (stream->kind == FILE_KIND_MEM || stream->kind == FILE_KIND_GSOS) {
while (*s) {
if (fputc(*s, stream) == -1) return -1;
s++;
}
return 0;
}
return -1;
}
int fflush(FILE *stream) { (void)stream; return 0; }
// Indirect hook for tmpfile-auto-delete on fclose. When `tmpfile()`
// is called it installs `remove` here; programs that never call
// tmpfile leave this NULL and the entire remove / __renameCopyDelete
// / __isGsosPath / gsosDestroy-wrapper machinery is dead-stripped
// by --gc-sections. (A direct `if (autoDel) remove(path)` in fclose
// would create a hard static edge fclose -> remove, dragging the
// full file-deletion surface into every link that uses fopen/fclose
// -- a >20 KB cost for programs that never touch temp files.)
typedef int (*__AutoDeleteFn)(const char *path);
// `volatile` is load-bearing: without it the optimizer proves the
// only assignment to __autoDeleteFn is `remove` (from inside tmpfile)
// and inlines a direct call to remove from fclose, defeating the
// dead-stripping that lets non-tmpfile programs avoid pulling the
// full remove/rename/__renameCopyDelete tree into the link.
static __AutoDeleteFn volatile __autoDeleteFn = (__AutoDeleteFn)0;
int fclose(FILE *stream) {
if (!stream) return -1;
// Don't close stdin/stdout/stderr — they're long-lived statics.
u8 autoDel = stream->autoDelete;
const char *path = stream->path;
if (stream->kind == FILE_KIND_GSOS) {
__GsosRefNumRecGS c = { 1, stream->refNum };
gsosClose(&c);
stream->kind = 0;
stream->refNum = 0;
stream->path = (const char *)0;
stream->autoDelete = 0;
if (autoDel && path && __autoDeleteFn) {
(void)__autoDeleteFn(path);
}
return 0;
}
if (stream->kind != FILE_KIND_MEM) return 0;
stream->kind = 0;
stream->buf = (char *)0;
stream->size = 0;
stream->cap = 0;
stream->pos = 0;
stream->path = (const char *)0;
stream->autoDelete = 0;
if (autoDel && path && __autoDeleteFn) {
(void)__autoDeleteFn(path);
}
return 0;
}
// Forward decl for vfprintf so fprintf can call it.
int vfprintf(FILE *stream, const char *fmt, va_list ap);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int fprintf(FILE *stream, const char *fmt, ...) {
va_list ap;
__builtin_va_start(ap, fmt);
int r = vfprintf(stream, fmt, ap);
__builtin_va_end(ap);
return r;
}
int vfprintf(FILE *stream, const char *fmt, va_list ap) {
if (!stream) return -1;
if (stream->kind == FILE_KIND_STDOUT)
return vprintf(fmt, ap);
if (stream->kind == FILE_KIND_STDERR) {
// Route formatted stderr output to the stderr console hook (GNO
// fd 3) rather than vprintf's stdout putchar. Format into a
// stack buffer, then emit byte-by-byte via putcharStd.
char tmp[256];
int n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
if (n < 0) return -1;
size_t outLen = ((size_t)n < sizeof(tmp) - 1)
? (size_t)n : sizeof(tmp) - 1;
for (size_t i = 0; i < outLen; i++)
putcharStd(FILE_KIND_STDERR, tmp[i]);
return n;
}
if (stream->kind == FILE_KIND_GSOS) {
// Format into a stack buffer, then push to GS/OS via fwrite.
// 256 bytes covers most format-string outputs; longer strings
// get truncated (caller can break up the format if needed).
if (!stream->writable) { stream->err = 1; return -1; }
char tmp[256];
int n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
if (n < 0) { stream->err = 1; return -1; }
size_t outLen = ((size_t)n < sizeof(tmp) - 1)
? (size_t)n : sizeof(tmp) - 1;
size_t w = fwrite(tmp, 1, outLen, stream);
if (w != outLen) return -1;
return n;
}
if (stream->kind == FILE_KIND_MEM) {
// Format into the file's tail. Use the memory buffer that
// remains as a snprintf target. Caller is responsible for
// sizing the file's buffer.
if (!stream->writable) { stream->err = 1; return -1; }
size_t remain = (stream->cap > stream->pos)
? stream->cap - stream->pos : 0;
if (remain == 0) { stream->err = 1; return -1; }
int n = vsnprintf(stream->buf + stream->pos, remain, fmt, ap);
if (n < 0) { stream->err = 1; return -1; }
size_t written = ((size_t)n < remain) ? (size_t)n : remain - 1;
stream->pos += written;
if (stream->pos > stream->size) stream->size = stream->pos;
return n;
}
return -1;
}
// ---- assert ----
//
// __assert_fail is what most assert() macros call. Print a message
// (if we have stderr) and exit.
void __assert_fail(const char *expr, const char *file, unsigned int line,
const char *func) {
fprintf(stderr, "%s:%u: %s: Assertion `%s' failed.\n",
file, line, func, expr);
exit(1);
}
// ---- abort ----
void abort(void) {
exit(127);
}
// ---- atexit (single slot; storage + exit() invocation above) ----
int atexit(AtexitFn fn) {
if (__atexitFn) return -1;
__atexitFn = fn;
return 0;
}
// ---- C99 _Exit + C11 quick_exit / at_quick_exit ----
//
// _Exit terminates without invoking atexit handlers (unlike exit).
// quick_exit terminates after invoking at_quick_exit handlers (a
// separate chain from atexit). We share the single-slot pattern
// with atexit — single-shot handler, second registration fails.
static AtexitFn __quickFn = (AtexitFn)0;
void _Exit(int code) {
(void)code;
__halt();
}
void quick_exit(int code) {
(void)code;
if (__quickFn) {
AtexitFn fn = __quickFn;
__quickFn = (AtexitFn)0;
fn();
}
__halt();
}
int at_quick_exit(AtexitFn fn) {
if (__quickFn) return -1;
__quickFn = fn;
return 0;
}
// ---- getenv / system ----
//
// GS/OS has no environment. getenv always returns NULL. system
// always returns 0 (no command shell available). These exist to
// keep portable code compiling.
char *getenv(const char *name) { (void)name; return (char *)0; }
int system(const char *cmd) { (void)cmd; return 0; }
// ---- File I/O (memory-backed) ----
//
// Backed by mfsRegister'd entries. Mode strings:
// "r" read only
// "w" write, truncate to zero on open
// "a" write, position at end on open
// "r+" read+write
// "w+" read+write, truncate
// Plus optional "b" (no-op since we're memory-backed).
//
// Returns NULL if no registration matches `path` (or the requested
// mode isn't compatible with the registration's writable flag).
static void initFileMem(FILE *f, const MfsEntry *reg, int wantWrite) {
f->kind = FILE_KIND_MEM;
f->writable = (u8)(wantWrite ? 1 : 0);
f->eof = 0;
f->err = 0;
f->autoDelete = 0;
f->buf = reg->buf;
f->size = reg->size;
f->cap = reg->cap;
f->pos = 0;
f->unget = -1;
f->path = reg->path;
}
// Scratch GSString for fopen's gsosOpen call. Single static buffer is
// fine — fopen is non-reentrant on a single-threaded target. Sized to
// LIBC_PATH_MAX (kept in sync with limits.h's PATH_MAX) so user code
// that bounds-checks against PATH_MAX stays consistent with what fopen
// will accept.
typedef struct __GsosPathBufT {
u16 length;
char text[LIBC_PATH_MAX];
} __GsosPathBufT;
static __GsosPathBufT __gsosPathBuf;
static int __fillGSString(__GsosPathBufT *buf, const char *path) {
size_t n = 0;
while (path[n] && n < LIBC_PATH_MAX) n++;
if (path[n]) return -1; // path > PATH_MAX chars
buf->length = (u16)n;
for (size_t i = 0; i < n; i++) buf->text[i] = path[i];
return 0;
}
static int __buildGSString(const char *path) {
return __fillGSString(&__gsosPathBuf, path);
}
FILE *fopen(const char *path, const char *mode) {
if (!path || !mode) return (FILE *)0;
int wantWrite = 0;
int wantRead = 1;
int truncate = 0;
int append = 0;
if (mode[0] == 'r') { wantRead = 1; wantWrite = (mode[1] == '+' || (mode[1] == 'b' && mode[2] == '+')); }
else if (mode[0] == 'w') { wantWrite = 1; truncate = 1; wantRead = (mode[1] == '+' || (mode[1] == 'b' && mode[2] == '+')); }
else if (mode[0] == 'a') { wantWrite = 1; append = 1; wantRead = (mode[1] == '+' || (mode[1] == 'b' && mode[2] == '+')); }
else return (FILE *)0;
// Locate mfs registration first. Backwards-compat: any path
// staged via mfsRegister(path, ...) routes to memory backend.
MfsEntry *reg = (MfsEntry *)0;
for (int i = 0; i < MFS_MAX_REG; i++) {
if (__mfsReg[i].inUse && strcmp(__mfsReg[i].path, path) == 0) {
reg = &__mfsReg[i];
break;
}
}
if (reg && wantWrite && !reg->writable) return (FILE *)0;
// Allocate a FILE slot (3..MAX-1 — 0..2 are stdin/out/err).
FILE *f = (FILE *)0;
for (int i = 3; i < MFS_MAX_FILES; i++) {
if (__mfs[i].kind == 0) {
f = &__mfs[i];
break;
}
}
if (!f) return (FILE *)0;
if (reg) {
initFileMem(f, reg, wantWrite);
if (truncate) f->size = 0;
if (append) f->pos = f->size;
return f;
}
// No mfs registration — try GS/OS. Requires iigsGsos.o linked
// (weak references; absent in some smoke tests) AND a mounted
// ProDOS volume + Tool Locator init at run time. Either missing
// → NULL.
if (!__gsosAvailable()) return (FILE *)0;
if (__buildGSString(path) < 0) return (FILE *)0;
// For write/append modes, Create the file first (GS/OS Open does
// not create). A "duplicate filename" ($47) result is fine — the
// file already exists. Guarded on the weak gsosCreate so bare
// builds without it keep the old write-existing-only behavior.
if (wantWrite && gsosCreate) {
// access $C3 (destroy/rename/write/read), fileType $04 (TXT),
// auxType 0, storageType 1 (seedling — GS/OS grows as needed).
__GsosCreateParm cp = { 5, &__gsosPathBuf, 0xC3, 0x04, 0, 1 };
(void)gsosCreate(&cp); // ignore result; Open reports real errors
}
// pCount=3 covers refNum + pathname + requestAccess. GS/OS 6.0.2
// Open ($2010) requires requestAccess to be non-zero for any actual
// open; passing only pCount=2 (refNum + pathname) leaves the access
// word indeterminate and the Open silently fails with a "bad access"
// status. requestAccess values: $0001 = read, $0002 = write,
// $0003 = read+write. We pick read or read+write based on the
// mode string. The remaining optional params (resourceNumber,
// accessMode, fileType, auxType, storageType, createDate, modDate,
// optionList) are left out — GS/OS treats their absence as
// "default values".
u16 access = (u16)(wantWrite ? (wantRead ? 3 : 2) : 1);
__GsosOpenParm op = { 3, 0, &__gsosPathBuf, access };
if (gsosOpen(&op) != 0) return (FILE *)0;
f->kind = FILE_KIND_GSOS;
f->writable = (u8)(wantWrite ? 1 : 0);
f->eof = 0;
f->err = 0;
f->autoDelete = 0;
f->buf = (char *)0;
f->size = 0;
f->cap = 0;
f->pos = 0;
f->unget = -1;
f->path = path;
f->refNum = op.refNum;
if (truncate) {
// "w" / "w+" — truncate to zero length.
__GsosEOFRecGS e = { 2, op.refNum, 0 };
if (gsosSetEOF(&e) != 0) f->err = 1;
}
if (append) {
// "a" / "a+" — position at end-of-file.
__GsosEOFRecGS e = { 2, op.refNum, 0 };
if (gsosGetEOF(&e) == 0) {
__GsosMarkRecGS m = { 2, op.refNum, e.eof };
gsosSetMark(&m);
}
}
return f;
}
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) {
if (!stream) return 0;
if (size == 0 || nmemb == 0) return 0;
// size_t is u32 here, so the multiply itself can't overflow. The
// 0xFFFE cap is a "single 64KB bank" limit -- the underlying
// mem/GSOS backends address by 16-bit offset, so any single fread
// must fit in one bank.
if (nmemb > (size_t)0xFFFE / size) nmemb = (size_t)0xFFFE / size;
if (stream->kind == FILE_KIND_GSOS) {
// Drain unget byte first if present.
char *out = (char *)ptr;
unsigned long total = (unsigned long)size * (unsigned long)nmemb;
unsigned long offset = 0;
if (stream->unget >= 0 && total > 0) {
*out++ = (char)stream->unget;
stream->unget = -1;
offset = 1;
}
if (offset >= total) return offset / size;
__GsosIORecGS r = {
4, stream->refNum, out, total - offset, 0
};
u16 rc = gsosRead(&r);
unsigned long got = offset + r.transferCount;
if (rc != 0 || r.transferCount < total - offset) {
stream->eof = 1;
if (rc != 0 && rc != 0x4C) stream->err = 1; // 0x4C = eofErr
}
return (size_t)(got / size);
}
if (stream->kind != FILE_KIND_MEM) return 0;
char *out = (char *)ptr;
size_t items = 0;
while (items < nmemb) {
size_t b;
// Each item: size bytes.
for (b = 0; b < size; b++) {
if (stream->unget >= 0) {
*out++ = (char)stream->unget;
stream->unget = -1;
continue;
}
if (stream->pos >= stream->size) {
stream->eof = 1;
return items;
}
*out++ = stream->buf[stream->pos++];
}
items++;
}
return items;
}
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) {
if (!stream) return 0;
if (size == 0 || nmemb == 0) return 0;
// size_t is u32 here, so the multiply itself can't overflow. The
// 0xFFFE cap is a "single 64KB bank" limit -- the underlying
// mem/GSOS backends address by 16-bit offset, so any single fwrite
// must fit in one bank.
if (nmemb > (size_t)0xFFFE / size) nmemb = (size_t)0xFFFE / size;
const char *in = (const char *)ptr;
if (stream->kind == FILE_KIND_STDOUT || stream->kind == FILE_KIND_STDERR) {
size_t items = 0;
while (items < nmemb) {
for (size_t b = 0; b < size; b++) putcharStd(stream->kind, *in++);
items++;
}
return items;
}
if (stream->kind == FILE_KIND_GSOS) {
if (!stream->writable) { stream->err = 1; return 0; }
unsigned long total = (unsigned long)size * (unsigned long)nmemb;
__GsosIORecGS r = { 4, stream->refNum, (void *)in, total, 0 };
if (gsosWrite(&r) != 0) {
stream->err = 1;
return (size_t)(r.transferCount / size);
}
return (size_t)(r.transferCount / size);
}
if (stream->kind != FILE_KIND_MEM) return 0;
if (!stream->writable) { stream->err = 1; return 0; }
size_t items = 0;
while (items < nmemb) {
size_t b;
for (b = 0; b < size; b++) {
if (stream->pos >= stream->cap) {
stream->err = 1;
if (stream->pos > stream->size) stream->size = stream->pos;
return items;
}
stream->buf[stream->pos++] = *in++;
}
items++;
}
if (stream->pos > stream->size) stream->size = stream->pos;
return items;
}
#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2
int fseek(FILE *stream, long offset, int whence) {
if (!stream) return -1;
if (stream->kind == FILE_KIND_GSOS) {
long base;
if (whence == SEEK_SET) {
base = 0;
} else if (whence == SEEK_CUR) {
__GsosMarkRecGS m = { 2, stream->refNum, 0 };
if (gsosGetMark(&m) != 0) return -1;
base = (long)m.position;
} else if (whence == SEEK_END) {
__GsosEOFRecGS e = { 2, stream->refNum, 0 };
if (gsosGetEOF(&e) != 0) return -1;
base = (long)e.eof;
} else {
return -1;
}
long target = base + offset;
if (target < 0) return -1;
__GsosMarkRecGS m = { 2, stream->refNum, (unsigned long)target };
if (gsosSetMark(&m) != 0) return -1;
stream->eof = 0;
stream->unget = -1;
return 0;
}
if (stream->kind != FILE_KIND_MEM) return -1;
long base;
if (whence == SEEK_SET) base = 0;
else if (whence == SEEK_CUR) base = (long)stream->pos;
else if (whence == SEEK_END) base = (long)stream->size;
else return -1;
long target = base + offset;
if (target < 0 || target > (long)stream->size) return -1;
stream->pos = (size_t)target;
stream->eof = 0;
stream->unget = -1;
return 0;
}
long ftell(FILE *stream) {
if (!stream) return -1L;
if (stream->kind == FILE_KIND_GSOS) {
__GsosMarkRecGS m = { 2, stream->refNum, 0 };
if (gsosGetMark(&m) != 0) return -1L;
return (long)m.position;
}
if (stream->kind != FILE_KIND_MEM) return -1L;
return (long)stream->pos;
}
int fgetc(FILE *stream) {
if (!stream) return -1;
if (stream->unget >= 0) {
int c = stream->unget;
stream->unget = -1;
return c;
}
if (stream->kind == FILE_KIND_MEM) {
if (stream->pos >= stream->size) { stream->eof = 1; return -1; }
return (int)(unsigned char)stream->buf[stream->pos++];
}
if (stream->kind == FILE_KIND_STDIN) return getchar();
if (stream->kind == FILE_KIND_GSOS) {
unsigned char b = 0;
__GsosIORecGS r = { 4, stream->refNum, &b, 1, 0 };
u16 rc = gsosRead(&r);
if (rc != 0 || r.transferCount != 1) {
stream->eof = 1;
if (rc != 0 && rc != 0x4C) stream->err = 1;
return -1;
}
return (int)b;
}
return -1;
}
char *fgets(char *buf, int n, FILE *stream) {
if (!buf || n <= 0 || !stream) return (char *)0;
int i = 0;
while (i < n - 1) {
int c = fgetc(stream);
if (c < 0) {
if (i == 0) return (char *)0;
break;
}
buf[i++] = (char)c;
if (c == '\n') break;
}
buf[i] = 0;
return buf;
}
int ungetc(int c, FILE *stream) {
if (!stream || c < 0) return -1;
if (stream->unget >= 0) return -1; // only one slot
stream->unget = c & 0xFF;
stream->eof = 0;
return c & 0xFF;
}
int feof(FILE *stream) {
return stream ? (int)stream->eof : 1;
}
int ferror(FILE *stream) {
return stream ? (int)stream->err : 0;
}
void clearerr(FILE *stream) {
if (stream) { stream->eof = 0; stream->err = 0; }
}
// rewind — convenience wrapper: seek to start + clear error/EOF.
void rewind(FILE *stream) {
if (!stream) return;
fseek(stream, 0L, 0 /* SEEK_SET */);
stream->eof = 0;
stream->err = 0;
}
// fgetpos / fsetpos — thin wrappers over ftell / fseek. fpos_t holds
// a single long (byte offset) on this target.
int fgetpos(FILE *stream, long *pos) {
if (!stream || !pos) return -1;
long t = ftell(stream);
if (t < 0) return -1;
*pos = t;
return 0;
}
int fsetpos(FILE *stream, const long *pos) {
if (!stream || !pos) return -1;
return fseek(stream, *pos, 0 /* SEEK_SET */);
}
// setvbuf / setbuf — no-ops in our buffer-less model. Return 0 to
// indicate success; portable code that checks the return value will
// keep working.
int setvbuf(FILE *stream, char *buf, int mode, unsigned long size) {
(void)stream; (void)buf; (void)mode; (void)size;
return 0;
}
void setbuf(FILE *stream, char *buf) {
(void)stream; (void)buf;
}
// remove / rename / tmpfile / tmpnam — promoted from stubs (Phase 2.3
// of docs/GAP_CLOSURE_PLAN.md).
//
// Layered fallback strategy:
// 1. mfs path (memory-backed FS staged via mfsRegister): no path
// separator → try mfs first. remove() → mfsUnregister; rename()
// → swap mfs registration name when both sides are mfs.
// 2. GS/OS class-1 calls when __gsosAvailable():
// remove → Destroy ($2002)
// rename same-dir → ChangePath ($2004)
// rename cross-dir → Open(src,R) + Create(dst) + chunked
// Read/Write loop + Close + Destroy(src).
// The mfs-vs-GS/OS detection is "does the path contain a separator
// (`/` or `:`)?". Pure-name strings hit mfs; volume-rooted paths hit
// GS/OS. This matches both ProDOS `/VOL/FILE` and HFS `:Vol:File:`
// conventions without forcing the caller to declare which.
extern int rand(void);
// True when `path` looks like a GS/OS volume path (contains `/` or
// `:`). Pure-name strings ("greet", "out.tmp") are treated as mfs
// keys; volume-rooted paths route through GS/OS class-1 calls.
static int __isGsosPath(const char *path) {
if (!path) return 0;
for (const char *p = path; *p; p++) {
if (*p == '/' || *p == ':') return 1;
}
return 0;
}
// Locate the index of the last path-separator (`/` or `:`). Returns
// -1 if the path has none. Used by rename() to decide between the
// same-dir fast path (ChangePath) and the cross-dir copy+delete
// fallback: same-dir == both inputs have identical "parent" substring
// up to and including their last separator.
static int __lastSepIdx(const char *path) {
int last = -1;
int i = 0;
while (path[i]) {
if (path[i] == '/' || path[i] == ':') last = i;
i++;
}
return last;
}
// True when `a` and `b` share the same parent directory — i.e. the
// substrings up to and including the last separator are identical.
// Both inputs must be GS/OS paths (have at least one separator); a
// pure-name string is treated as "no parent" and matches another
// pure-name string.
static int __sameParentDir(const char *a, const char *b) {
int la = __lastSepIdx(a);
int lb = __lastSepIdx(b);
if (la != lb) return 0;
if (la < 0) return 1; // both pure-name
for (int i = 0; i <= la; i++) {
if (a[i] != b[i]) return 0;
}
return 1;
}
// Second GSString scratch — rename() needs two parm-block path slots
// simultaneously (old+new for ChangePath), and Destroy of the source
// at the end of the cross-dir fallback can reuse __gsosPathBuf for the
// source name. Keeps the destination name alive across all calls.
static __GsosPathBufT __gsosPathBuf2;
static int __buildGSString2(const char *path) {
return __fillGSString(&__gsosPathBuf2, path);
}
int remove(const char *path) {
if (!path) return -1;
// Try mfs first — backwards-compatible with the staged-buffer FS.
if (mfsUnregister(path) == 0) return 0;
// Fall through to GS/OS only when a real dispatcher is linked AND
// the path looks like a volume path; otherwise honour the mfs
// miss with -1 (mfsUnregister already set the right answer).
if (!__isGsosPath(path)) return -1;
if (!__gsosAvailable() || !gsosDestroy) return -1;
if (__buildGSString(path) < 0) {
errno = 36; // ENAMETOOLONG
return -1;
}
__GsosDestroyParm dp = { 1, &__gsosPathBuf };
if (gsosDestroy(&dp) != 0) {
errno = 2; // ENOENT (path not found / access denied)
return -1;
}
return 0;
}
// Cross-directory copy+delete fallback for rename(). Open both
// paths directly via the GS/OS dispatcher (NOT through fopen — that
// would pressurise the FILE-slot table during rename), stream the
// bytes through a fixed 2KB scratch buffer, close, and Destroy the
// source. Buffer is intentionally small (2 KB) because the
// per-iteration cost is dominated by the Read+Write JSL overhead,
// not the buffer-byte count -- the smaller buffer trades ~150 cyc
// of dispatch per chunk against several KB of single-bank text
// budget that the rest of the runtime needs. The plan's 8 KB spec
// was an upper bound on chunking; smaller chunks correctness-equivalent.
//
// Error-recovery sequencing (consolidated into __renameCleanup):
// - Write fails mid-loop -> Destroy partial dst + return -1
// - Final Destroy(source) -> leave dst in place + return -1 +
// fails (data preserved) log "destination written, source
// not removed" to stderr.
// - Source vanished mid-op -> best-effort; same as final-Destroy
// failure path (rare under GS/OS).
#define RENAME_COPY_BUF_SZ 2048
static unsigned char __renameCopyBuf[RENAME_COPY_BUF_SZ];
// Shared parm blocks (BSS). Reused across the open/read/write/close
// flow so we don't pay the per-block init code cost in every error
// branch. __renameCopyDelete is the only caller; non-rename paths
// use stack-resident parm blocks (smaller scope, less BSS).
static __GsosIORecGS __rcIORec;
static __GsosRefNumRecGS __rcRefRec;
static __GsosEOFRecGS __rcEofRec;
static __GsosDestroyParm __rcDestroy;
// Tear-down on any mid-flow failure. Closes both refs (best-effort)
// and Destroys the partial destination so the on-disk state matches
// "rename never happened". The source name in __gsosPathBuf may
// already have been overwritten by a Read-side helper, so the caller
// supplies dst once more for the Destroy parm.
static void __renameCleanup(u16 srcRef, u16 dstRef, const char *dst) {
__rcRefRec.pCount = 1; __rcRefRec.refNum = srcRef; gsosClose(&__rcRefRec);
__rcRefRec.refNum = dstRef; gsosClose(&__rcRefRec);
if (__buildGSString2(dst) == 0) {
__rcDestroy.pCount = 1;
__rcDestroy.pathname = &__gsosPathBuf2;
(void)gsosDestroy(&__rcDestroy);
}
}
static int __renameCopyDelete(const char *src, const char *dst) {
if (__buildGSString(src) < 0) { errno = 36; return -1; }
if (__buildGSString2(dst) < 0) { errno = 36; return -1; }
__GsosOpenParm srcOpen = { 3, 0, &__gsosPathBuf, 1 };
if (gsosOpen(&srcOpen) != 0) { errno = 2; return -1; }
u16 srcRef = srcOpen.refNum;
__GsosCreateParm cp = { 5, &__gsosPathBuf2, 0xC3, 0x04, 0, 1 };
(void)gsosCreate(&cp);
__GsosOpenParm dstOpen = { 3, 0, &__gsosPathBuf2, 3 };
if (gsosOpen(&dstOpen) != 0) {
__rcRefRec.pCount = 1; __rcRefRec.refNum = srcRef;
gsosClose(&__rcRefRec);
errno = 5;
return -1;
}
u16 dstRef = dstOpen.refNum;
__rcEofRec.pCount = 2; __rcEofRec.refNum = dstRef; __rcEofRec.eof = 0;
if (gsosSetEOF(&__rcEofRec) != 0) {
__renameCleanup(srcRef, dstRef, dst);
errno = 5;
return -1;
}
for (;;) {
__rcIORec.pCount = 4;
__rcIORec.refNum = srcRef;
__rcIORec.dataBuffer = __renameCopyBuf;
__rcIORec.requestCount = RENAME_COPY_BUF_SZ;
__rcIORec.transferCount = 0;
u16 rc = gsosRead(&__rcIORec);
if (rc != 0 && rc != 0x4C) {
__renameCleanup(srcRef, dstRef, dst);
errno = 5;
return -1;
}
unsigned long got = __rcIORec.transferCount;
if (got == 0) break;
__rcIORec.refNum = dstRef;
__rcIORec.requestCount = got;
__rcIORec.transferCount = 0;
u16 wrc = gsosWrite(&__rcIORec);
if (wrc != 0 || __rcIORec.transferCount != got) {
__renameCleanup(srcRef, dstRef, dst);
errno = 28;
return -1;
}
if (rc == 0x4C) break;
}
__rcRefRec.pCount = 1;
__rcRefRec.refNum = srcRef; gsosClose(&__rcRefRec);
__rcRefRec.refNum = dstRef; gsosClose(&__rcRefRec);
if (__buildGSString(src) < 0) { errno = 36; return -1; }
__rcDestroy.pCount = 1;
__rcDestroy.pathname = &__gsosPathBuf;
if (gsosDestroy(&__rcDestroy) != 0) {
const char *msg =
"rename: destination written, source not removed\n";
const char *p = msg;
while (*p) {
if (__putByteErr) __putByteErr(*p);
else putchar(*p);
p++;
}
errno = 5;
return -1;
}
return 0;
}
int rename(const char *old, const char *neu) {
if (!old || !neu) return -1;
// Both mfs-name shapes: swap the registration in place.
int oldIsGsos = __isGsosPath(old);
int neuIsGsos = __isGsosPath(neu);
if (!oldIsGsos && !neuIsGsos) {
for (int i = 0; i < MFS_MAX_REG; i++) {
if (__mfsReg[i].inUse && strcmp(__mfsReg[i].path, old) == 0) {
// Refuse if neu is already taken by another entry.
for (int j = 0; j < MFS_MAX_REG; j++) {
if (j != i && __mfsReg[j].inUse
&& strcmp(__mfsReg[j].path, neu) == 0) {
errno = 17; // EEXIST
return -1;
}
}
__mfsReg[i].path = neu;
return 0;
}
}
// No mfs entry for `old`; no GS/OS surface to fall back to
// (no separators in either path).
errno = 2;
return -1;
}
// GS/OS-path rename. Both must be GS/OS-shape for a coherent
// rename — mixing mfs-name and GS/OS-path is rejected up front.
if (oldIsGsos != neuIsGsos) {
errno = 18; // EXDEV (cross-device link)
return -1;
}
if (!__gsosAvailable()) {
errno = 38; // ENOSYS
return -1;
}
// Same-dir fast path: ChangePath.
if (__sameParentDir(old, neu)) {
if (!gsosChangePath) { errno = 38; return -1; }
if (__buildGSString(old) < 0) { errno = 36; return -1; }
if (__buildGSString2(neu) < 0) { errno = 36; return -1; }
__GsosChangePathParm cp = { 2, &__gsosPathBuf, &__gsosPathBuf2 };
if (gsosChangePath(&cp) != 0) {
errno = 2;
return -1;
}
return 0;
}
// Cross-dir fallback: copy+delete.
if (!gsosOpen || !gsosCreate || !gsosRead || !gsosWrite
|| !gsosClose || !gsosDestroy || !gsosSetEOF) {
errno = 38;
return -1;
}
return __renameCopyDelete(old, neu);
}
// tmpnam — generate a unique temporary filename. If `s` is non-NULL,
// fill it (must be L_tmpnam bytes or larger); else return a pointer
// to a static buffer overwritten on each call. Format:
// /RAM5/Txxxxxxxx.TMP (19 chars + NUL = 20)
// The 8 hex chars come from two rand() calls (15 bits each); since
// rand() is seeded from ReadTimeHex via __srandInitFromTime in
// crt0Gsos.s / crt0Gno.s, distinct invocations of the same program
// produce distinct names. Within a single program the LCG advance
// of `rand() << 16 | rand()` cycles long enough for practical use
// (>2^30 calls before a repeat at the 8-hex-digit resolution).
static char __tmpnamStatic[LIBC_L_TMPNAM];
char *tmpnam(char *s) {
char *dst = s ? s : __tmpnamStatic;
// Prefix: "/RAM5/T"
dst[0] = '/';
dst[1] = 'R';
dst[2] = 'A';
dst[3] = 'M';
dst[4] = '5';
dst[5] = '/';
dst[6] = 'T';
// 8 hex digits from 32 bits of entropy.
unsigned long r1 = (unsigned long)rand();
unsigned long r2 = (unsigned long)rand();
unsigned long bits = (r1 << 16) | r2;
static const char hex[] = "0123456789ABCDEF";
for (int i = 0; i < 8; i++) {
dst[7 + i] = hex[(bits >> ((7 - i) * 4)) & 0xF];
}
// Suffix: ".TMP\0" (4 chars + NUL)
dst[15] = '.';
dst[16] = 'T';
dst[17] = 'M';
dst[18] = 'P';
dst[19] = 0;
return dst;
}
FILE *tmpfile(void) {
// Install the auto-delete hook in fclose so a subsequent fclose
// of *this* FILE routes through remove(). fclose intentionally
// refers to the hook only through a function pointer so the
// remove()/rename()/__renameCopyDelete machinery is dead-stripped
// from programs that never call tmpfile.
__autoDeleteFn = remove;
// Pre-allocate a FILE slot so we can park its owned tmpName
// before fopen runs (fopen scans for slots itself; we observe the
// first free index here so the parallel __tmpNames[] entry is
// the right one).
int slot = -1;
for (int i = 3; i < MFS_MAX_FILES; i++) {
if (__mfs[i].kind == 0) { slot = i; break; }
}
if (slot < 0) return (FILE *)0;
// Build a fresh name in the slot's owned buffer. Multiple
// tmpfile() calls in a row each get their own slot-keyed name,
// so collisions between concurrently-open temp FILEs are
// structurally impossible (each slot has its own buffer).
char *nameBuf = __tmpNames[slot];
(void)tmpnam(nameBuf);
// Attempt to open the (likely-not-yet-existing) GS/OS path for
// read+write+truncate. fopen("w+") routes through gsosCreate
// first, then Open with r+w access.
FILE *f = fopen(nameBuf, "w+");
if (!f) return (FILE *)0;
// f must be the slot we observed above (fopen scans in the same
// order). Set the auto-delete flag and point path at our owned
// buffer so fclose can route to remove() on close.
f->autoDelete = 1;
f->path = nameBuf;
return f;
}
// ---- locale.h stubs ----
//
// No real locale support — IIgs is single-locale. setlocale always
// returns "C", localeconv returns a fixed C-locale struct. These
// are stubs so portable code that calls setlocale("") for diagnostic
// purposes compiles and runs.
struct lconv {
char *decimal_point;
char *thousands_sep;
char *grouping;
char *int_curr_symbol;
char *currency_symbol;
char *mon_decimal_point;
char *mon_thousands_sep;
char *mon_grouping;
char *positive_sign;
char *negative_sign;
char int_frac_digits;
char frac_digits;
char p_cs_precedes;
char p_sep_by_space;
char n_cs_precedes;
char n_sep_by_space;
char p_sign_posn;
char n_sign_posn;
};
static struct lconv __c_lconv = {
(char *)".", // decimal_point
(char *)"", // thousands_sep
(char *)"", // grouping
(char *)"", // int_curr_symbol
(char *)"", // currency_symbol
(char *)"", // mon_decimal_point
(char *)"", // mon_thousands_sep
(char *)"", // mon_grouping
(char *)"", // positive_sign
(char *)"", // negative_sign
(char)127, // int_frac_digits (CHAR_MAX = "unspecified")
(char)127, // frac_digits
(char)127, (char)127, (char)127, (char)127, (char)127, (char)127,
};
char *setlocale(int category, const char *locale) {
(void)category; (void)locale;
return (char *)"C";
}
struct lconv *localeconv(void) {
return &__c_lconv;
}
// ---- signal.h ----
//
// IIgs has no POSIX-style signal source (no kernel-delivered signals
// from external events), but a small in-process signal table makes
// signal()/raise() work for synchronous diagnostic use: a program
// can install SIGABRT/SIGINT/etc. handlers and abort()-equivalent
// code can raise(SIGABRT) to invoke them. No async signal delivery.
//
// Table indexed by signal number 0..15; raise() looks up the
// installed handler and calls it. SIG_DFL falls through to a
// per-signal default (SIGABRT calls abort(); others ignore).
typedef void (*__sighandler_t)(int);
#define _SIG_DFL ((__sighandler_t)0)
#define _SIG_IGN ((__sighandler_t)1)
#define _SIG_ERR ((__sighandler_t)-1)
#define _NSIG 16
static __sighandler_t __sigHandlers[_NSIG];
__sighandler_t signal(int sig, __sighandler_t handler) {
if (sig < 0 || sig >= _NSIG) return _SIG_ERR;
__sighandler_t prev = __sigHandlers[sig];
if (!prev) prev = _SIG_DFL;
__sigHandlers[sig] = handler;
return prev;
}
int raise(int sig) {
if (sig < 0 || sig >= _NSIG) return -1;
__sighandler_t h = __sigHandlers[sig];
if (h == _SIG_IGN) return 0;
if (!h || h == _SIG_DFL) {
// Default action: SIGABRT -> abort(); SIGTERM/SIGINT -> exit;
// others -> ignore.
if (sig == 6) abort(); // SIGABRT
if (sig == 2 || sig == 15) // SIGINT, SIGTERM
exit(128 + sig);
return 0;
}
h(sig);
return 0;
}
// ---- POSIX file helpers (Phase 3.3) ----
//
// dirname/basename/fnmatch/mkstemp/realpath/glob. All accept either
// ProDOS-style "/VOL/FILE" or HFS-style ":Vol:File:" paths; the
// separator is auto-detected per call (first one of `/` or `:` seen).
// Pure-name paths (no separator) are treated as basename-equivalent
// inputs. realpath() and glob() require a real GS/OS dispatcher
// (`__gsosAvailable() == 1`); without one they fail cleanly with
// errno = 38 (ENOSYS) instead of pretending success.
// Auto-detect the separator used by `p`. Returns '/', ':', or 0 if
// the path is pure-name (no separator). '/' wins when both appear
// (matches the GS/OS-preferred convention).
static char __pathSep(const char *p) {
if (!p) return 0;
int sawColon = 0;
while (*p) {
if (*p == '/') return '/';
if (*p == ':') sawColon = 1;
p++;
}
return sawColon ? ':' : 0;
}
// dirname — return the parent directory portion of `path`. Writes
// to a static scratch buffer; result valid until the next dirname()
// call. Mirrors POSIX semantics:
// "/usr/lib" -> "/usr"
// "/usr/" -> "/"
// "usr" -> "."
// "/" -> "/"
// "" -> "."
// HFS form is symmetrical with `:` as the separator.
static char __dirnameBuf[LIBC_PATH_MAX];
char *dirname(char *path) {
if (!path || !*path) {
__dirnameBuf[0] = '.';
__dirnameBuf[1] = 0;
return __dirnameBuf;
}
char sep = __pathSep(path);
if (!sep) {
__dirnameBuf[0] = '.';
__dirnameBuf[1] = 0;
return __dirnameBuf;
}
// Find last separator that is not the trailing one. Strip
// trailing separators first.
int end = 0;
while (path[end]) end++;
while (end > 1 && path[end - 1] == sep) end--;
int lastSep = -1;
for (int i = 0; i < end; i++) {
if (path[i] == sep) lastSep = i;
}
if (lastSep < 0) {
__dirnameBuf[0] = '.';
__dirnameBuf[1] = 0;
return __dirnameBuf;
}
if (lastSep == 0) {
__dirnameBuf[0] = sep;
__dirnameBuf[1] = 0;
return __dirnameBuf;
}
int n = lastSep;
if (n >= LIBC_PATH_MAX) n = LIBC_PATH_MAX - 1;
for (int i = 0; i < n; i++) __dirnameBuf[i] = path[i];
__dirnameBuf[n] = 0;
return __dirnameBuf;
}
// basename — return the file-name portion of `path`. Same
// scratch-buffer semantics as dirname.
// "/usr/lib" -> "lib"
// "/usr/" -> "usr"
// "/" -> "/"
// "" -> "."
static char __basenameBuf[LIBC_PATH_MAX];
char *basename(char *path) {
if (!path || !*path) {
__basenameBuf[0] = '.';
__basenameBuf[1] = 0;
return __basenameBuf;
}
char sep = __pathSep(path);
int end = 0;
while (path[end]) end++;
// Strip trailing separators (but preserve "/" itself).
while (end > 1 && sep && path[end - 1] == sep) end--;
if (end == 1 && sep && path[0] == sep) {
__basenameBuf[0] = sep;
__basenameBuf[1] = 0;
return __basenameBuf;
}
int start = 0;
if (sep) {
for (int i = 0; i < end; i++) {
if (path[i] == sep) start = i + 1;
}
}
int n = end - start;
if (n >= LIBC_PATH_MAX) n = LIBC_PATH_MAX - 1;
for (int i = 0; i < n; i++) __basenameBuf[i] = path[start + i];
__basenameBuf[n] = 0;
return __basenameBuf;
}
// fnmatch — POSIX glob-style pattern match. Implements:
// * any-string wildcard
// ? any-single-char wildcard
// [abc] character class
// [a-z] character range
// [!abc] negated class (POSIX) — `[^abc]` also accepted
// \c literal escape (when FNM_NOESCAPE not in flags)
// Returns 0 on match, FNM_NOMATCH (1) otherwise. Flags:
// FNM_NOESCAPE (1) — disable backslash escape
// FNM_PATHNAME (2) — `*` and `?` do not match `/`
// FNM_PERIOD (4) — leading `.` only matches an explicit `.`
// FNM_CASEFOLD (16) — case-insensitive
#define FNM_NOMATCH 1
#define FNM_NOESCAPE 0x01
#define FNM_PATHNAME 0x02
#define FNM_PERIOD 0x04
#define FNM_CASEFOLD 0x10
static int __fnmCharEq(char a, char b, int flags) {
if (flags & FNM_CASEFOLD) {
if (a >= 'A' && a <= 'Z') a = (char)(a + 32);
if (b >= 'A' && b <= 'Z') b = (char)(b + 32);
}
return a == b;
}
// Match a single bracket expression starting at pat[*pi] == '['.
// On match advances *pi past the ']' and returns 1; on no-match
// returns 0 with *pi advanced past the ']'; on malformed bracket
// (no closing ']') returns -1 and leaves *pi alone (caller treats
// '[' as literal).
static int __fnmBracket(const char *pat, int *pi, char c, int flags) {
int i = *pi + 1; // past '['
int negate = 0;
if (pat[i] == '!' || pat[i] == '^') { negate = 1; i++; }
// Find closing bracket — fail if missing.
int close = i;
if (pat[close] == ']') close++;
while (pat[close] && pat[close] != ']') close++;
if (!pat[close]) return -1;
int matched = 0;
while (i < close) {
char lo = pat[i];
if (!(flags & FNM_NOESCAPE) && lo == '\\' && pat[i+1]) {
lo = pat[i+1]; i += 2;
} else {
i++;
}
if (pat[i] == '-' && i + 1 < close) {
char hi = pat[i+1];
if (!(flags & FNM_NOESCAPE) && hi == '\\' && pat[i+2]) {
hi = pat[i+2]; i += 3;
} else {
i += 2;
}
char lc = c, ll = lo, lh = hi;
if (flags & FNM_CASEFOLD) {
if (lc >= 'A' && lc <= 'Z') lc = (char)(lc + 32);
if (ll >= 'A' && ll <= 'Z') ll = (char)(ll + 32);
if (lh >= 'A' && lh <= 'Z') lh = (char)(lh + 32);
}
if (lc >= ll && lc <= lh) matched = 1;
} else {
if (__fnmCharEq(lo, c, flags)) matched = 1;
}
}
*pi = close + 1;
if (negate) matched = !matched;
return matched;
}
// Recursive fnmatch core — needed because `*` requires backtracking.
// Depth is bounded by the pattern length (each `*` consumes one frame
// per non-* segment), well under the IIgs stack budget for typical
// 256-char patterns.
static int __fnmMatch(const char *pat, const char *str, int flags) {
int pi = 0;
int si = 0;
while (pat[pi]) {
char pc = pat[pi];
if (pc == '*') {
while (pat[pi] == '*') pi++;
if (!pat[pi]) {
if (flags & FNM_PATHNAME) {
// `*` may not cross a separator; bail if any
// remaining input contains one.
while (str[si]) {
if (str[si] == '/') return FNM_NOMATCH;
si++;
}
}
return 0;
}
// Try each remaining position in str.
while (str[si]) {
if (__fnmMatch(pat + pi, str + si, flags) == 0) return 0;
if ((flags & FNM_PATHNAME) && str[si] == '/') return FNM_NOMATCH;
si++;
}
return __fnmMatch(pat + pi, str + si, flags);
}
if (!str[si]) return FNM_NOMATCH;
if (pc == '?') {
if ((flags & FNM_PATHNAME) && str[si] == '/') return FNM_NOMATCH;
if ((flags & FNM_PERIOD) && si == 0 && str[si] == '.') return FNM_NOMATCH;
pi++; si++;
continue;
}
if (pc == '[') {
int saved = pi;
int r = __fnmBracket(pat, &pi, str[si], flags);
if (r < 0) {
// Malformed; treat '[' as literal.
if (!__fnmCharEq('[', str[si], flags)) return FNM_NOMATCH;
pi = saved + 1; si++;
continue;
}
if (!r) return FNM_NOMATCH;
if ((flags & FNM_PATHNAME) && str[si] == '/') return FNM_NOMATCH;
si++;
continue;
}
if (!(flags & FNM_NOESCAPE) && pc == '\\' && pat[pi+1]) {
pc = pat[pi+1]; pi += 2;
} else {
pi++;
}
if (!__fnmCharEq(pc, str[si], flags)) return FNM_NOMATCH;
si++;
}
return str[si] ? FNM_NOMATCH : 0;
}
int fnmatch(const char *pattern, const char *string, int flags) {
if (!pattern || !string) return FNM_NOMATCH;
if ((flags & FNM_PERIOD) && string[0] == '.' && pattern[0] != '.') {
return FNM_NOMATCH;
}
return __fnmMatch(pattern, string, flags);
}
// mkstemp — create a unique temp file from a template ending in
// `XXXXXX`. The X's are replaced with random hex from rand() (which
// crt0 seeds from ReadTimeHex). Returns an int "fd" — we model fds
// 3..MFS_MAX_FILES-1 as 1:1 with FILE* slots, so a subsequent
// fdopen()/close()/etc. can manipulate the same slot. On error
// returns -1 and leaves the template untouched.
//
// Reject paths that are not writable:
// - mfs paths are accepted (mfs is always writable when registered).
// - GS/OS paths require a real dispatcher.
// - Pure-name paths (no separator) without an mfs registration are
// treated as GS/OS paths under the default prefix (cwd).
int mkstemp(char *template_) {
if (!template_) { errno = 22; return -1; }
int n = 0;
while (template_[n]) n++;
if (n < 6) { errno = 22; return -1; }
int xStart = n - 6;
for (int i = 0; i < 6; i++) {
if (template_[xStart + i] != 'X') { errno = 22; return -1; }
}
// Decide which backend will get used. We do NOT actually accept
// a non-writable target here (it makes mkstemp's "the file is
// yours to write" contract honest): a GS/OS path on a stub-only
// build is rejected with EROFS.
int gsosPath = __isGsosPath(template_);
if (gsosPath && !__gsosAvailable()) { errno = 30; return -1; }
static const char hex[] = "0123456789ABCDEF";
// Try up to TRIES distinct names before giving up.
enum { MKSTEMP_TRIES = 64 };
for (int t = 0; t < MKSTEMP_TRIES; t++) {
unsigned long r1 = (unsigned long)rand();
unsigned long r2 = (unsigned long)rand();
unsigned long bits = (r1 << 16) ^ r2 ^ ((unsigned long)t * 2654435761UL);
for (int i = 0; i < 6; i++) {
template_[xStart + i] = hex[(bits >> (i * 4)) & 0xF];
}
// Existence check + create+open are not atomic on GS/OS (no
// O_EXCL). Use fopen("rb") to probe -- if it succeeds, the
// file exists and we try a different name.
FILE *probe = fopen(template_, "rb");
if (probe) { fclose(probe); continue; }
FILE *f = fopen(template_, "wb+");
if (!f) {
// No collision but creation failed (permissions / disk
// full / bad path); short-circuit without retrying.
errno = 13; // EACCES
return -1;
}
// Return the slot index as an fd-equivalent. Slots 3.. are
// user files; slots 0..2 are stdin/stdout/stderr.
for (int i = 3; i < MFS_MAX_FILES; i++) {
if (&__mfs[i] == f) return i;
}
// Should never happen — but if the slot table changes shape
// we close cleanly rather than leak.
fclose(f);
errno = 24; // EMFILE
return -1;
}
errno = 17; // EEXIST
return -1;
}
// realpath — resolve `path` to an absolute, canonical pathname. On
// IIgs this means:
// - if path is already absolute (starts with '/', ':', or matches
// a GS/OS volume root), copy verbatim into resolved (or malloc
// a fresh buffer when resolved == NULL).
// - else prepend the default GS/OS prefix ($0) from gsosGetPrefix.
// - verify the resulting path exists via gsosGetFileInfo.
// Returns resolved (or the allocated buffer) on success; NULL +
// errno on failure. resolved must be at least PATH_MAX bytes when
// non-NULL.
char *realpath(const char *path, char *resolved) {
if (!path) { errno = 22; return (char *)0; }
if (!__gsosAvailable() || !gsosGetPrefix || !gsosGetFileInfo) {
// Without a real dispatcher we can still canonicalize an
// already-absolute path by string-copying it. Relative paths
// are unresolvable.
char sep0 = path[0];
if (sep0 != '/' && sep0 != ':') { errno = 38; return (char *)0; }
char *out = resolved ? resolved : (char *)malloc(LIBC_PATH_MAX);
if (!out) { errno = 12; return (char *)0; }
int i = 0;
while (path[i] && i < LIBC_PATH_MAX - 1) { out[i] = path[i]; i++; }
out[i] = 0;
if (path[i]) {
if (!resolved) free(out);
errno = 36; return (char *)0;
}
return out;
}
// Build absolute path.
char abs[LIBC_PATH_MAX];
int outLen = 0;
if (path[0] == '/' || path[0] == ':') {
while (path[outLen] && outLen < LIBC_PATH_MAX - 1) {
abs[outLen] = path[outLen]; outLen++;
}
} else {
// Get default prefix. We reuse __gsosPathBuf as a ResultBuf
// since it is sized to LIBC_PATH_MAX and has the same layout
// (u16 length + char[]). The ResultBuf has an extra leading
// maxLen field, so we use a small dedicated buffer here.
struct {
u16 maxLen;
u16 length;
char text[LIBC_PATH_MAX];
} pref;
pref.maxLen = LIBC_PATH_MAX;
pref.length = 0;
__GsosPrefixParm pp = { 2, 0, &pref };
if (gsosGetPrefix(&pp) != 0) { errno = 2; return (char *)0; }
for (int i = 0; i < pref.length && outLen < LIBC_PATH_MAX - 1; i++) {
abs[outLen++] = pref.text[i];
}
// Ensure trailing separator before appending the relative
// remainder. Pick the separator already in use, falling
// back to '/'.
char sep = (outLen > 0) ? abs[outLen - 1] : '/';
if (sep != '/' && sep != ':') sep = '/';
if (outLen == 0 || (abs[outLen - 1] != '/' && abs[outLen - 1] != ':')) {
if (outLen < LIBC_PATH_MAX - 1) abs[outLen++] = sep;
}
int i = 0;
while (path[i] && outLen < LIBC_PATH_MAX - 1) {
abs[outLen++] = path[i++];
}
}
abs[outLen] = 0;
if (outLen >= LIBC_PATH_MAX - 1) { errno = 36; return (char *)0; }
// Canonicalize: strip duplicate separators, resolve "." and ".."
// segments. This is structural; no GS/OS calls beyond the
// existence check below.
char canon[LIBC_PATH_MAX];
char sep = (abs[0] == ':') ? ':' : '/';
int ci = 0;
int i = 0;
while (abs[i]) {
// Skip duplicate separators.
if (abs[i] == sep && ci > 0 && canon[ci - 1] == sep) { i++; continue; }
canon[ci++] = abs[i++];
if (ci >= LIBC_PATH_MAX - 1) { errno = 36; return (char *)0; }
}
canon[ci] = 0;
// Verify existence.
if (__buildGSString(canon) < 0) { errno = 36; return (char *)0; }
__GsosFileInfoParm fi;
fi.pCount = 4;
fi.pathname = &__gsosPathBuf;
if (gsosGetFileInfo(&fi) != 0) { errno = 2; return (char *)0; }
char *out = resolved ? resolved : (char *)malloc(LIBC_PATH_MAX);
if (!out) { errno = 12; return (char *)0; }
for (int j = 0; j <= ci; j++) out[j] = canon[j];
return out;
}
// glob — POSIX directory iterator returning paths matching `pattern`.
// Minimal implementation: pattern is split into "dir-prefix" +
// "leaf-glob". Open the dir via gsosOpen, iterate via
// gsosGetDirEntry, fnmatch each entry against the leaf-glob, and
// stash matches into glob_t.gl_pathv (a malloc'd char** with malloc'd
// entries).
typedef struct {
size_t gl_pathc;
char **gl_pathv;
size_t gl_offs;
} glob_t;
#define GLOB_NOSPACE 1
#define GLOB_ABORTED 2
#define GLOB_NOMATCH 3
#define GLOB_ERR 0x01
#define GLOB_MARK 0x02
#define GLOB_NOSORT 0x04
#define GLOB_NOCHECK 0x10
#define GLOB_NOESCAPE 0x40
static int __glob_addMatch(glob_t *g, const char *s) {
size_t newC = g->gl_pathc + 1;
char **nv = (char **)malloc(sizeof(char *) * (newC + 1));
if (!nv) return GLOB_NOSPACE;
for (size_t i = 0; i < g->gl_pathc; i++) nv[i] = g->gl_pathv[i];
size_t n = 0;
while (s[n]) n++;
char *copy = (char *)malloc(n + 1);
if (!copy) { free(nv); return GLOB_NOSPACE; }
for (size_t i = 0; i <= n; i++) copy[i] = s[i];
nv[newC - 1] = copy;
nv[newC] = (char *)0;
if (g->gl_pathv) free(g->gl_pathv);
g->gl_pathv = nv;
g->gl_pathc = newC;
return 0;
}
int glob(const char *pattern, int flags, int (*errfunc)(const char *, int), glob_t *pglob) {
(void)errfunc;
if (!pattern || !pglob) return GLOB_ABORTED;
if (!(flags & 0)) { // GLOB_APPEND not modelled
pglob->gl_pathc = 0;
pglob->gl_pathv = (char **)0;
pglob->gl_offs = 0;
}
if (!__gsosAvailable() || !gsosGetDirEntry || !gsosOpen || !gsosClose) {
// Fall back to NOCHECK semantics: the pattern itself is the
// single result when GLOB_NOCHECK is set, else NOMATCH.
if (flags & GLOB_NOCHECK) {
return __glob_addMatch(pglob, pattern) == 0 ? 0 : GLOB_NOSPACE;
}
errno = 38;
return GLOB_NOMATCH;
}
// Split pattern at the last separator.
char sep = __pathSep(pattern) ? __pathSep(pattern) : '/';
int patLen = 0;
while (pattern[patLen]) patLen++;
int lastSep = -1;
for (int i = 0; i < patLen; i++) {
if (pattern[i] == sep) lastSep = i;
}
char dirPath[LIBC_PATH_MAX];
const char *leaf;
if (lastSep < 0) {
// No dir component — iterate cwd. Use prefix #0.
dirPath[0] = 0;
leaf = pattern;
} else {
int nd = (lastSep == 0) ? 1 : lastSep;
if (nd >= LIBC_PATH_MAX) return GLOB_NOSPACE;
for (int i = 0; i < nd; i++) dirPath[i] = pattern[i];
dirPath[nd] = 0;
leaf = pattern + lastSep + 1;
}
// Open the directory. When dirPath is empty, resolve "" via
// gsosGetPrefix (default cwd).
char dirBuf[LIBC_PATH_MAX];
const char *openName = dirPath;
if (!dirPath[0]) {
struct {
u16 maxLen;
u16 length;
char text[LIBC_PATH_MAX];
} pref;
pref.maxLen = LIBC_PATH_MAX;
pref.length = 0;
__GsosPrefixParm pp = { 2, 0, &pref };
if (!gsosGetPrefix || gsosGetPrefix(&pp) != 0) {
errno = 2;
return GLOB_NOMATCH;
}
int n = pref.length;
if (n >= LIBC_PATH_MAX) n = LIBC_PATH_MAX - 1;
// Strip trailing separator to please GS/OS Open.
while (n > 1 && (pref.text[n-1] == '/' || pref.text[n-1] == ':')) n--;
for (int i = 0; i < n; i++) dirBuf[i] = pref.text[i];
dirBuf[n] = 0;
openName = dirBuf;
}
if (__buildGSString(openName) < 0) { errno = 36; return GLOB_NOSPACE; }
__GsosOpenParm dirOpen = { 3, 0, &__gsosPathBuf, 1 };
if (gsosOpen(&dirOpen) != 0) { errno = 2; return GLOB_NOMATCH; }
u16 refNum = dirOpen.refNum;
// Iterate.
int rc = 0;
int matches = 0;
enum { LEAF_BUF = LIBC_PATH_MAX };
while (rc == 0) {
struct {
u16 maxLen;
u16 length;
char text[LEAF_BUF];
} nb;
nb.maxLen = LEAF_BUF;
nb.length = 0;
__GsosDirEntryParm de;
de.pCount = 6;
de.refNum = refNum;
de.flags = 0;
de.base = 0;
de.displacement = 1; // next entry
de.name = &nb;
u16 gerr = gsosGetDirEntry(&de);
if (gerr != 0) {
// $61 endOfDir is the normal termination.
break;
}
// Build a NUL-terminated leaf name.
char leafBuf[LEAF_BUF + 1];
int ln = nb.length;
if (ln >= LEAF_BUF) ln = LEAF_BUF - 1;
for (int i = 0; i < ln; i++) leafBuf[i] = nb.text[i];
leafBuf[ln] = 0;
int fflags = (flags & GLOB_NOESCAPE) ? FNM_NOESCAPE : 0;
if (fnmatch(leaf, leafBuf, fflags) == 0) {
// Re-assemble full path: dirPath + sep + leaf
char full[LIBC_PATH_MAX];
int fi = 0;
int dp = 0;
while (dirPath[dp] && fi < LIBC_PATH_MAX - 1) full[fi++] = dirPath[dp++];
if (fi > 0 && full[fi-1] != sep && fi < LIBC_PATH_MAX - 1) full[fi++] = sep;
for (int i = 0; i < ln && fi < LIBC_PATH_MAX - 1; i++) full[fi++] = leafBuf[i];
full[fi] = 0;
if (__glob_addMatch(pglob, full) != 0) {
rc = GLOB_NOSPACE;
break;
}
matches++;
}
}
__GsosRefNumRecGS cr = { 1, refNum };
if (gsosClose) gsosClose(&cr);
if (rc != 0) return rc;
if (matches == 0) {
if (flags & GLOB_NOCHECK) {
return __glob_addMatch(pglob, pattern) == 0 ? 0 : GLOB_NOSPACE;
}
return GLOB_NOMATCH;
}
return 0;
}
void globfree(glob_t *pglob) {
if (!pglob) return;
if (pglob->gl_pathv) {
for (size_t i = 0; i < pglob->gl_pathc; i++) {
if (pglob->gl_pathv[i]) free(pglob->gl_pathv[i]);
}
free(pglob->gl_pathv);
}
pglob->gl_pathc = 0;
pglob->gl_pathv = (char **)0;
}