/* * 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 #include #include #include #include #include #include #include /* 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; }