c71e249a4e
First substantive commit: the entire Sprinter C compiler tree on top of
the bare README+gitignore initial commit.
What's in here:
bin/sprinter-cc — driver script invoking SDCC + linker + mkexe
libc/ — Sprinter-specific libc layer over ESTEX/BIOS
(conio, gfx, io, mem, stdio + headers)
runtime/ — crt0 variants (default/small/banked/minimal)
+ heap + bank trampolines
toolchain/ — mkexe (SprintEXE packer, C + tests)
examples/ — 30 demo programs (gfx, file I/O, env, time, …)
lib/Makefile — builds the libc archive (sprinter.lib)
docs/ — converted Sprinter manuals + asm reference samples
third_party/ — solid-c reference compiler dump + sdcc setup script
release_docs/ — packaging / release notes
gitignore overhaul:
• Drop dangerous blanket patterns: *.asm (would hide docs/samples/*.asm)
and *.exe (case-insensitive match was hiding third_party/solid-c/*.EXE
on macOS APFS). Replaced with examples/*/*.{asm,exe,…} and lib/*.lib.
• Restore tracking of toolchain/mkexe/tests/{one,big}.bin — those are
INPUT fixtures, not build outputs.
• Collapse the duplicated SDCC/C/Sdcc sections into one section per
concern (build outputs / vendored / OS-junk).
• Add .sprinter-cc-*/, build/ (catches lib/build/ too), .claude/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
191 lines
6.1 KiB
C
191 lines
6.1 KiB
C
/*
|
||
* stat.c — POSIX stat() and fstat() over ESTEX metadata.
|
||
*
|
||
* fstat(fd, &st) -> ESTEX GET_D_T ($17) for mtime + lseek/SEEK_END
|
||
* for size.
|
||
* stat(path, &st) -> open(O_RDONLY) + fstat() + close. This works
|
||
* for any regular file; directory paths fail at
|
||
* open() — F_FIRST-based stat had unreliable
|
||
* semantics for exact filenames.
|
||
*
|
||
* Sprinter / DSS doesn't track POSIX owner/group/inode, so we synth a
|
||
* minimal mode (S_IFREG | rw user perm).
|
||
*/
|
||
|
||
#include <sys/stat.h>
|
||
#include <fcntl.h>
|
||
#include <unistd.h>
|
||
#include <time.h>
|
||
#include <errno.h>
|
||
#include <stdint.h>
|
||
#include <string.h>
|
||
#include <dir.h>
|
||
|
||
/* ESTEX GET_D_T: A=fd, C=$17 → D=day, E=month, IX=year, H=hour, L=min,
|
||
* B=sec. CF=1 + A=errcode on failure.
|
||
*
|
||
* Writes directly into *out using `ex (sp), hl` to swap the saved out
|
||
* pointer with HL=hour:min after RST — no static scratch needed.
|
||
* `out->dow` is left untouched (GET_D_T doesn't return it). */
|
||
static int get_dt_for_handle(int fd, datetime_t *out) __naked
|
||
{
|
||
(void)fd; (void)out;
|
||
__asm
|
||
;; __sdcccall(1): fd in HL (low byte), out in DE.
|
||
push ix ; save caller IX
|
||
push de ; stash out pointer
|
||
ld a, l ; A = fd
|
||
ld c, #0x17 ; ESTEX GET_D_T
|
||
rst #0x10
|
||
jr c, _gdt_err
|
||
;; D=day E=month IX=year H=hour L=min B=sec
|
||
ex (sp), hl ; TOS<->HL: HL=out, TOS=hour:min
|
||
ld (hl), d ; +0 day
|
||
inc hl
|
||
ld (hl), e ; +1 month
|
||
inc hl
|
||
push ix ; year onto stack
|
||
pop de ; DE = year
|
||
ld (hl), e ; +2 year low
|
||
inc hl
|
||
ld (hl), d ; +3 year high
|
||
inc hl
|
||
pop de ; D=hour E=min (from earlier ex (sp))
|
||
ld (hl), d ; +4 hour
|
||
inc hl
|
||
ld (hl), e ; +5 min
|
||
inc hl
|
||
ld (hl), b ; +6 sec (+7 dow left untouched)
|
||
pop ix ; restore caller IX
|
||
ld de, #0
|
||
ret
|
||
_gdt_err:
|
||
pop hl ; discard stashed out pointer
|
||
pop ix ; restore caller IX
|
||
call __errno_set
|
||
ld de, #-1
|
||
ret
|
||
__endasm;
|
||
}
|
||
|
||
int fstat(int fd, struct stat *buf)
|
||
{
|
||
/* Size via lseek trick. */
|
||
long cur = lseek(fd, 0L, SEEK_CUR);
|
||
if (cur < 0) return -1;
|
||
long end = lseek(fd, 0L, SEEK_END);
|
||
if (end < 0) return -1;
|
||
(void)lseek(fd, cur, SEEK_SET);
|
||
buf->st_size = (uint32_t)end;
|
||
|
||
/* Date/time via ESTEX. */
|
||
datetime_t ft;
|
||
if (get_dt_for_handle(fd, &ft) < 0) return -1;
|
||
{
|
||
struct tm tm;
|
||
tm.tm_sec = ft.second;
|
||
tm.tm_min = ft.minute;
|
||
tm.tm_hour = ft.hour;
|
||
tm.tm_mday = ft.day;
|
||
tm.tm_mon = (unsigned char)(ft.month - 1);
|
||
tm.tm_year = (int)ft.year - 1900;
|
||
tm.tm_isdst = 0;
|
||
tm.tm_hundredth = 0;
|
||
buf->st_mtime = mktime(&tm);
|
||
}
|
||
buf->st_mode = S_IFREG | S_IRUSR | S_IWUSR;
|
||
return 0;
|
||
}
|
||
|
||
/* Convert ESTEX DOS-style date+time to time_t epoch (used by stat()
|
||
* when going through F_FIRST for directory entries). */
|
||
static time_t dos_to_epoch(uint16_t date, uint16_t dtime)
|
||
{
|
||
struct tm tm;
|
||
tm.tm_sec = (unsigned char)((dtime & 0x1F) << 1);
|
||
tm.tm_min = (unsigned char)((dtime >> 5) & 0x3F);
|
||
tm.tm_hour = (unsigned char)((dtime >> 11) & 0x1F);
|
||
tm.tm_mday = (unsigned char)(date & 0x1F);
|
||
tm.tm_mon = (unsigned char)(((date >> 5) & 0x0F) - 1);
|
||
tm.tm_year = (int)((date >> 9) & 0x7F) + 80; /* DOS year base = 1980 */
|
||
tm.tm_isdst = 0;
|
||
tm.tm_hundredth = 0;
|
||
return mktime(&tm);
|
||
}
|
||
|
||
/* Returns 1 if path is "." or "..", else 0. Reads at most 3 bytes;
|
||
* NULL-safe. ~28 bytes / 34–117 T-states depending on input. */
|
||
static char is_dot_or_dotdot(const char *path) __naked
|
||
{
|
||
(void)path;
|
||
__asm
|
||
ld a, h
|
||
or a, l
|
||
jr Z, _idd_fail ; NULL → 0
|
||
ld a, (hl)
|
||
sub a, #0x2E
|
||
jr NZ, _idd_fail ; path[0] != '.'
|
||
inc hl
|
||
ld a, (hl)
|
||
or a, a
|
||
jr Z, _idd_ok ; ".\0" → 1
|
||
sub a, #0x2E
|
||
jr NZ, _idd_fail ; path[1] not '.' and not '\0'
|
||
inc hl
|
||
ld a, (hl)
|
||
or a, a
|
||
jr Z, _idd_ok ; "..\0" → 1
|
||
_idd_fail:
|
||
xor a, a
|
||
ret
|
||
_idd_ok:
|
||
ld a, #1
|
||
ret
|
||
__endasm;
|
||
}
|
||
|
||
int stat(const char *path, struct stat *buf)
|
||
{
|
||
/* Regular file: open + fstat. */
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd >= 0) {
|
||
int r = fstat(fd, buf);
|
||
close(fd);
|
||
return r;
|
||
}
|
||
int saved = errno;
|
||
|
||
/* Try ffirst directly — works for ordinary subdirectories. */
|
||
ffblk_t ffb;
|
||
if (ffirst(path, &ffb, FA_DIREC) == 0 && (ffb.found_attr & FA_DIREC)) {
|
||
buf->st_size = ffb.size;
|
||
buf->st_mtime = dos_to_epoch(ffb.date, ffb.time);
|
||
buf->st_mode = S_IFDIR | S_IRWXU;
|
||
return 0;
|
||
}
|
||
|
||
/* Verified 2026-05-29: ESTEX F_FIRST rejects bare "." and ".." with
|
||
* EINAME (16), same as open(). But they DO appear in the "*.*"
|
||
* directory listing with FA_DIREC. Iterate to find them. */
|
||
if (is_dot_or_dotdot(path)) {
|
||
if (ffirst("*.*", &ffb, FA_DIREC) == 0) {
|
||
do {
|
||
if (strcmp(ffb.found_name, path) == 0) {
|
||
buf->st_size = ffb.size;
|
||
buf->st_mtime = dos_to_epoch(ffb.date, ffb.time);
|
||
buf->st_mode = S_IFDIR | S_IRWXU;
|
||
return 0;
|
||
}
|
||
} while (fnext(&ffb) == 0);
|
||
}
|
||
/* Last-resort synthetic entry — FS variant didn't expose them. */
|
||
buf->st_mode = S_IFDIR | S_IRWXU;
|
||
buf->st_size = 0;
|
||
buf->st_mtime = 0;
|
||
return 0;
|
||
}
|
||
|
||
errno = saved;
|
||
return -1;
|
||
}
|