2739 lines
97 KiB
C
2739 lines
97 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;
|
|
}
|
|
|
|
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 <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;
|
|
}
|