libc: сплит «1 функция = 1 модуль» — вся библиотека, wildcard-сборка

- bios/conio/env/errno/gfx/io/mem/mouse/stdio/stdlib/string/sys/time/
  video разложены по модулям: общие статики и helpers — в internal
  _-модулях (_conio.h/_mouse.h/_gfx.h/_palette.h/_atexit.h/_time.h)
- lib/Makefile: LIBC_C = wildcard libc/*/*.c — гранулярность файлов
  = гранулярность DCE линкера
- эффект _CODE: gfx_text 6986→2568 Б, gfx_mous −1745, gfx_demo/d16
  −542; ранее timedir −3270, ls −3098, stattest −2995
- комментарии оставшихся модулей переведены на русский; puts: убран
  мёртвый pchars; videomode_raw разложен на get/set
- docs/libc-split-asm-cases.md — правила asm-связок между модулями;
  docs/libc-roadmap.md — план этапа
- восстановлен examples/mdview/SAMPLE.MD (нужен make floppy)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 16:08:58 +03:00
parent 46553f4e07
commit 4a081501d8
237 changed files with 4830 additions and 3933 deletions
+14 -96
View File
@@ -1,15 +1,9 @@
/*
* stat.c — POSIX stat() and fstat() over ESTEX metadata.
* stat — POSIX stat() поверх метаданных ESTEX.
*
* 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).
* stat(path, &st) -> open(O_RDONLY) + fstat() + close. Работает для
* любого обычного файла; для директорий — fallback
* через F_FIRST (и перебор "*.*" для "."/"..").
*/
#include <sys/stat.h>
@@ -21,84 +15,8 @@
#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). */
/* Конвертация DOS-формата даты+времени ESTEX в time_t (используется
* на пути через F_FIRST для записей директорий). */
static time_t dos_to_epoch(uint16_t date, uint16_t dtime)
{
struct tm tm;
@@ -113,8 +31,8 @@ static time_t dos_to_epoch(uint16_t date, uint16_t dtime)
return mktime(&tm);
}
/* Returns 1 if path is "." or "..", else 0. Reads at most 3 bytes;
* NULL-safe. ~28 bytes / 34117 T-states depending on input. */
/* Возвращает 1, если path "." или "..", иначе 0. Читает максимум
* 3 байта; NULL-безопасно. ~28 байт / 34117 тактов от входа. */
static char is_dot_or_dotdot(const char *path) __naked
{
(void)path;
@@ -146,7 +64,7 @@ static char is_dot_or_dotdot(const char *path) __naked
int stat(const char *path, struct stat *buf)
{
/* Regular file: open + fstat. */
/* Обычный файл: open + fstat. */
int fd = open(path, O_RDONLY);
if (fd >= 0) {
int r = fstat(fd, buf);
@@ -155,7 +73,7 @@ int stat(const char *path, struct stat *buf)
}
int saved = errno;
/* Try ffirst directly — works for ordinary subdirectories. */
/* Пробуем ffirst напрямую — работает для обычных подпапок. */
ffblk_t ffb;
if (ffirst(path, &ffb, FA_DIREC) == 0 && (ffb.found_attr & FA_DIREC)) {
buf->st_size = ffb.size;
@@ -164,9 +82,9 @@ int stat(const char *path, struct stat *buf)
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. */
/* Проверено 2026-05-29: ESTEX F_FIRST отвергает голые "." и ".."
* с EINAME (16), как и open(). Но в листинге "*.*" они ЕСТЬ с
* FA_DIREC — ищем перебором. */
if (is_dot_or_dotdot(path)) {
if (ffirst("*.*", &ffb, FA_DIREC) == 0) {
do {
@@ -178,7 +96,7 @@ int stat(const char *path, struct stat *buf)
}
} 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;