// 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 ` 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; } 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. Going through `unsigned long` here triggers an i32 umax // pattern that our backend currently miscompiles; staying 16-bit dodges // that path entirely. 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. Big allocations (e.g. the 16 KB sprite // codegen scratch) are routed through halBigAlloc -> the Memory Manager, // NOT this heap, so n is always small here and the historical // `p + HDR_SZ + n > heapEnd` over-heap miscompile (only manifested for // oversized n) is never exercised. A `heapEnd - p` reformulation went // through the same i32 path and spuriously failed small mallocs, so keep // the original simple compare. 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 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` 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 ; 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 . #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; }