From 515132b6b8e414afe75c23cc4ef598761a5456a2 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 11 Sep 2026 10:22:09 +0800 Subject: [PATCH] feat(kernel): add ATA disk driver and FAT16 filesystem implementation - Implemented low-level ATA PIO driver with read/write/flush operations - Added FAT16 filesystem layer with directory and file management - Created bridge between C disk operations and PHP filesystem API - Integrated disk availability detection and LBA sector access - Implemented file operations including open, read, write, and close - Added support for directory creation, file listing and renaming - Included proper error handling and disk initialization logic --- examples/typephp-os/.gitignore | 2 + examples/typephp-os/Makefile | 17 +- examples/typephp-os/README.md | 25 +- examples/typephp-os/ROADMAP.md | 7 +- examples/typephp-os/freestanding/abi/libc.c | 64 -- .../freestanding/abi/unimplemented.S | 45 -- examples/typephp-os/freestanding/ata.c | 201 ++++++ examples/typephp-os/freestanding/bridge.cc | 56 ++ .../typephp-os/freestanding/filesystem_abi.cc | 383 +++++++++++ examples/typephp-os/project.yml | 8 + examples/typephp-os/src/fat16.php | 651 ++++++++++++++++++ examples/typephp-os/src/filesystem.php | 296 ++++++++ examples/typephp-os/src/fs_bridge.stub.php | 4 + examples/typephp-os/src/hardware.stub.php | 12 + examples/typephp-os/src/kernel.php | 76 ++ examples/typephp-os/tools/test-qemu.sh | 9 +- 16 files changed, 1733 insertions(+), 123 deletions(-) create mode 100644 examples/typephp-os/freestanding/ata.c create mode 100644 examples/typephp-os/freestanding/filesystem_abi.cc create mode 100644 examples/typephp-os/src/fat16.php create mode 100644 examples/typephp-os/src/filesystem.php create mode 100644 examples/typephp-os/src/fs_bridge.stub.php diff --git a/examples/typephp-os/.gitignore b/examples/typephp-os/.gitignore index 84c048a7..884faf23 100644 --- a/examples/typephp-os/.gitignore +++ b/examples/typephp-os/.gitignore @@ -1 +1,3 @@ /build/ +*.o +/thirdparty \ No newline at end of file diff --git a/examples/typephp-os/Makefile b/examples/typephp-os/Makefile index 3ad4cf8c..d94e78d1 100644 --- a/examples/typephp-os/Makefile +++ b/examples/typephp-os/Makefile @@ -5,13 +5,14 @@ PAYLOAD_ELF := $(BUILD)/kernel64.elf PAYLOAD_BIN := $(BUILD)/kernel64.bin PAYLOAD_OBJECT := $(OBJECT_DIR)/kernel64-payload.o KERNEL := $(BUILD)/typephp-os.elf +DISK := $(BUILD)/typephp-os.img BOOTFLAGS := -m32 -O2 -ffreestanding -fno-pic -fno-pie \ -fno-stack-protector -Wall -Wextra -.PHONY: all payload objects run test clean +.PHONY: all payload objects disk run test clean -all: $(KERNEL) +all: $(KERNEL) $(DISK) payload: objects cd ../.. && ./bin/tpc.php --nano examples/typephp-os/project.yml @@ -20,6 +21,15 @@ payload: objects # payload. Ordinary .c/.cc/.S files stay in project.yml sources. objects: $(BOOTSTRAP_OBJECT) +disk: $(DISK) + +$(DISK): | $(BUILD) + truncate -s 32M $@ + mkfs.fat -F 16 -n TYPEPHPOS $@ + +$(BUILD): + mkdir -p $@ + $(OBJECT_DIR): mkdir -p $@ @@ -36,10 +46,11 @@ $(KERNEL): payload run: all qemu-system-x86_64 -m 128M -kernel $(KERNEL) \ + -drive file=$(DISK),format=raw,if=ide,index=0 \ -display none -serial stdio -monitor none -no-reboot -no-shutdown test: all - bash tools/test-qemu.sh $(KERNEL) $(BUILD)/qemu.log + bash tools/test-qemu.sh $(KERNEL) $(DISK) $(BUILD)/qemu.log clean: rm -rf $(BUILD) diff --git a/examples/typephp-os/README.md b/examples/typephp-os/README.md index 3864fc0f..88b545d9 100644 --- a/examples/typephp-os/README.md +++ b/examples/typephp-os/README.md @@ -23,8 +23,7 @@ project owns the freestanding boundary under `freestanding/abi`: implemented C/POSIX and C++ ABI functions live there, while APIs required for linking but not implemented by the kernel are exported as panic stubs. Consequently an unsupported operation fails immediately with its ABI symbol instead of -silently returning fabricated data. Filesystem APIs are the next subsystem to -replace with real implementations; sockets are outside the current scope. +silently returning fabricated data. Sockets are outside the current scope. Cross-project portability uses general feature switches only: @@ -45,7 +44,7 @@ only excluded capability is dynamic PHP execution through ZendVM. ## Build and run Required host tools are TypePHP's PHP/Composer dependencies, GCC/G++, GNU -binutils, GNU make, and `qemu-system-x86_64`. +binutils, GNU make, `dosfstools`, and `qemu-system-x86_64`. From this directory, build and boot with: @@ -58,7 +57,8 @@ The build produces two useful files: - `build/kernel64.elf`: the 64-bit TypePHP + ordinary Nano payload produced by tpc; -- `build/typephp-os.elf`: the final Multiboot kernel accepted by QEMU. +- `build/typephp-os.elf`: the final Multiboot kernel accepted by QEMU; +- `build/typephp-os.img`: a persistent 32 MiB FAT16 disk image. Run the automated serial-output smoke test with: @@ -84,6 +84,7 @@ To invoke QEMU manually: ```shell qemu-system-x86_64 -m 128M \ -kernel examples/typephp-os/build/typephp-os.elf \ + -drive file=examples/typephp-os/build/typephp-os.img,format=raw,if=ide,index=0 \ -display none -serial stdio -monitor none -no-reboot -no-shutdown ``` @@ -103,7 +104,14 @@ The QEMU smoke test currently verifies: - real Zend allocation, GC, string, array, object, class, and exception data; - PHPX `Variant`, `Str`, `Array`, custom classes, and a typed `std::vector`; +- OpenLibm implementations of the ordinary double-precision math ABI; - built-in date handling and `sleep()` through Zend Bridge; +- ATA PIO sector I/O and a TypePHP FAT16 implementation with 8.3 root files + and directories; +- PHP's unchanged plain file stream and `php_stat()` paths, including + `file_put_contents()`, `file_get_contents()`, `is_dir()`, `mkdir()`, and + `scandir()`, forwarded through the POSIX ABI to TypePHP; +- normal PHP output through multi-argument `echo` and `PHPWRITE`; - a TypePHP prime calculation for 0–100; - a resident loop that prints the RTC-derived UTC time and `Hello TypePHP-OS!` every two seconds. @@ -112,7 +120,8 @@ The 64-bit payload currently occupies about 7 MiB, so the first 16 MiB is reserved before physical memory is handed to Zend MM. Returning entire Zend chunks to a future physical page allocator remains later work. -Filesystem ABI entries already link as explicit panic stubs. The next -milestone will replace them with an in-kernel filesystem and make PHP's local -file stream operations real. Network sockets, dynamic module loading, -`include`/`require`/`eval`, and external process execution remain unavailable. +The first filesystem milestone deliberately supports only the FAT16 root +directory and DOS 8.3 names. Nested path traversal, long filenames, +timestamps, permissions, and a general block-device layer remain future work. +Network sockets, dynamic module loading, `include`/`require`/`eval`, and +external process execution remain unavailable. diff --git a/examples/typephp-os/ROADMAP.md b/examples/typephp-os/ROADMAP.md index ddb4f9a1..02eddc32 100644 --- a/examples/typephp-os/ROADMAP.md +++ b/examples/typephp-os/ROADMAP.md @@ -36,8 +36,11 @@ behavior. classes, generated user classes, construction, properties, and destruction. 4. **Time service — complete.** CMOS wall clock, monotonic host hook, built-in date formatting, and the standard extension's `sleep()` path. -5. **Filesystem — next.** Physical block device, a small filesystem, file - descriptors, directory/stat operations, and PHP's local file stream API. +5. **Filesystem — first slice complete.** ATA PIO, a persistent FAT16 image, + TypePHP-owned cluster/directory/file logic, file descriptors, POSIX + directory/stat operations, and PHP's local file stream API. The current + implementation is intentionally limited to root-level DOS 8.3 names; + nested directories and long filenames are next. 6. **Native Class memory.** Exercise Wren GC through Zend MM and verify tracing of PHPX fields under sustained allocation. 7. **Kernel services.** Interrupt-driven timer, keyboard, physical-page diff --git a/examples/typephp-os/freestanding/abi/libc.c b/examples/typephp-os/freestanding/abi/libc.c index 84462567..251b8ce0 100644 --- a/examples/typephp-os/freestanding/abi/libc.c +++ b/examples/typephp-os/freestanding/abi/libc.c @@ -98,11 +98,6 @@ __attribute__((weak, noreturn)) void typephp_os_panic(const char *message) } } -void phpx_no_exception_abort(const char *message) -{ - typephp_os_panic(message); -} - int php_nano_host_random_bytes(void *bytes, size_t size) { static uint64_t state = UINT64_C(0x9e3779b97f4a7c15); @@ -599,65 +594,6 @@ int _setjmp(jmp_buf environment) return 0; } -double pow(double base, double exponent) -{ - long power = (long) exponent; - if ((double) power != exponent) { - return 0.0; - } - double result = 1.0; - unsigned long magnitude = power < 0 ? (unsigned long) (-power) : (unsigned long) power; - while (magnitude != 0) { - if ((magnitude & 1u) != 0) { - result *= base; - } - base *= base; - magnitude >>= 1u; - } - return power < 0 ? 1.0 / result : result; -} - -double fmod(double value, double divisor) -{ - if (divisor == 0.0) { - return 0.0 / 0.0; - } - double quotient = value / divisor; - if (quotient >= 0.0) { - quotient = (double) (unsigned long) quotient; - } else { - quotient = (double) (long) quotient; - } - return value - quotient * divisor; -} - -double ceil(double value) -{ - long integral = (long) value; - return value > (double) integral ? (double) integral + 1.0 : (double) integral; -} - -double floor(double value) -{ - long integral = (long) value; - return value < (double) integral ? (double) integral - 1.0 : (double) integral; -} - -double trunc(double value) -{ - return (double) (long) value; -} - -double round(double value) -{ - return value < 0.0 ? ceil(value - 0.5) : floor(value + 0.5); -} - -double fabs(double value) -{ - return value < 0.0 ? -value : value; -} - int abs(int value) { return value < 0 ? -value : value; diff --git a/examples/typephp-os/freestanding/abi/unimplemented.S b/examples/typephp-os/freestanding/abi/unimplemented.S index 8094fef0..08b47948 100644 --- a/examples/typephp-os/freestanding/abi/unimplemented.S +++ b/examples/typephp-os/freestanding/abi/unimplemented.S @@ -17,12 +17,8 @@ .popsection .endm -TYPEPHP_OS_UNIMPLEMENTED access -TYPEPHP_OS_UNIMPLEMENTED chdir TYPEPHP_OS_UNIMPLEMENTED chmod TYPEPHP_OS_UNIMPLEMENTED chown -TYPEPHP_OS_UNIMPLEMENTED close -TYPEPHP_OS_UNIMPLEMENTED closedir TYPEPHP_OS_UNIMPLEMENTED fcntl TYPEPHP_OS_UNIMPLEMENTED fdopen TYPEPHP_OS_UNIMPLEMENTED feof @@ -32,10 +28,7 @@ TYPEPHP_OS_UNIMPLEMENTED fopen TYPEPHP_OS_UNIMPLEMENTED fclose TYPEPHP_OS_UNIMPLEMENTED fread TYPEPHP_OS_UNIMPLEMENTED fseek -TYPEPHP_OS_UNIMPLEMENTED fstat -TYPEPHP_OS_UNIMPLEMENTED fsync TYPEPHP_OS_UNIMPLEMENTED ftell -TYPEPHP_OS_UNIMPLEMENTED ftruncate TYPEPHP_OS_UNIMPLEMENTED getgid TYPEPHP_OS_UNIMPLEMENTED getgrnam TYPEPHP_OS_UNIMPLEMENTED getgroups @@ -45,49 +38,11 @@ TYPEPHP_OS_UNIMPLEMENTED getpwuid_r TYPEPHP_OS_UNIMPLEMENTED getuid TYPEPHP_OS_UNIMPLEMENTED gnu_dev_major TYPEPHP_OS_UNIMPLEMENTED gnu_dev_minor -TYPEPHP_OS_UNIMPLEMENTED isatty -TYPEPHP_OS_UNIMPLEMENTED lseek -TYPEPHP_OS_UNIMPLEMENTED lstat -TYPEPHP_OS_UNIMPLEMENTED mkdir TYPEPHP_OS_UNIMPLEMENTED mkstemp -TYPEPHP_OS_UNIMPLEMENTED open -TYPEPHP_OS_UNIMPLEMENTED opendir -TYPEPHP_OS_UNIMPLEMENTED read -TYPEPHP_OS_UNIMPLEMENTED readdir TYPEPHP_OS_UNIMPLEMENTED readlink -TYPEPHP_OS_UNIMPLEMENTED rename TYPEPHP_OS_UNIMPLEMENTED rewind -TYPEPHP_OS_UNIMPLEMENTED rewinddir -TYPEPHP_OS_UNIMPLEMENTED rmdir -TYPEPHP_OS_UNIMPLEMENTED stat TYPEPHP_OS_UNIMPLEMENTED umask -TYPEPHP_OS_UNIMPLEMENTED unlink TYPEPHP_OS_UNIMPLEMENTED utime -TYPEPHP_OS_UNIMPLEMENTED write - -TYPEPHP_OS_UNIMPLEMENTED acos -TYPEPHP_OS_UNIMPLEMENTED acosh -TYPEPHP_OS_UNIMPLEMENTED asin -TYPEPHP_OS_UNIMPLEMENTED asinh -TYPEPHP_OS_UNIMPLEMENTED atan -TYPEPHP_OS_UNIMPLEMENTED atan2 -TYPEPHP_OS_UNIMPLEMENTED atanh -TYPEPHP_OS_UNIMPLEMENTED copysign -TYPEPHP_OS_UNIMPLEMENTED cos -TYPEPHP_OS_UNIMPLEMENTED cosh -TYPEPHP_OS_UNIMPLEMENTED exp -TYPEPHP_OS_UNIMPLEMENTED expm1 -TYPEPHP_OS_UNIMPLEMENTED hypot -TYPEPHP_OS_UNIMPLEMENTED log -TYPEPHP_OS_UNIMPLEMENTED log10 -TYPEPHP_OS_UNIMPLEMENTED log1p -TYPEPHP_OS_UNIMPLEMENTED log2 -TYPEPHP_OS_UNIMPLEMENTED nextafter -TYPEPHP_OS_UNIMPLEMENTED sin -TYPEPHP_OS_UNIMPLEMENTED sinh -TYPEPHP_OS_UNIMPLEMENTED sqrt -TYPEPHP_OS_UNIMPLEMENTED tan -TYPEPHP_OS_UNIMPLEMENTED tanh TYPEPHP_OS_UNIMPLEMENTED mblen TYPEPHP_OS_UNIMPLEMENTED strerror diff --git a/examples/typephp-os/freestanding/ata.c b/examples/typephp-os/freestanding/ata.c new file mode 100644 index 00000000..ae3888ca --- /dev/null +++ b/examples/typephp-os/freestanding/ata.c @@ -0,0 +1,201 @@ +/* + +----------------------------------------------------------------------+ + | TypePHP OS | + +----------------------------------------------------------------------+ + | Minimal polling ATA PIO block device for the QEMU primary IDE disk. | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#include +#include + +enum { + ATA_DATA = 0x1f0, + ATA_ERROR = 0x1f1, + ATA_SECTOR_COUNT = 0x1f2, + ATA_LBA_LOW = 0x1f3, + ATA_LBA_MID = 0x1f4, + ATA_LBA_HIGH = 0x1f5, + ATA_DRIVE = 0x1f6, + ATA_STATUS = 0x1f7, + ATA_COMMAND = 0x1f7, + ATA_ALT_STATUS = 0x3f6, + + ATA_STATUS_ERROR = 0x01, + ATA_STATUS_DATA_REQUEST = 0x08, + ATA_STATUS_DEVICE_FAULT = 0x20, + ATA_STATUS_BUSY = 0x80, + + ATA_COMMAND_READ_SECTORS = 0x20, + ATA_COMMAND_WRITE_SECTORS = 0x30, + ATA_COMMAND_CACHE_FLUSH = 0xe7, + ATA_SECTOR_SIZE = 512, + ATA_TIMEOUT = 10000000, + PCI_CONFIG_ADDRESS = 0xcf8, + PCI_CONFIG_DATA = 0xcfc, +}; + +static int ata_initialized; + +static void ata_delay_400ns(void); + +static inline void outb(uint16_t port, uint8_t value) +{ + __asm__ volatile("outb %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline uint8_t inb(uint16_t port) +{ + uint8_t value; + __asm__ volatile("inb %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static inline void outw(uint16_t port, uint16_t value) +{ + __asm__ volatile("outw %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline uint16_t inw(uint16_t port) +{ + uint16_t value; + __asm__ volatile("inw %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static inline void outl(uint16_t port, uint32_t value) +{ + __asm__ volatile("outl %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline uint32_t inl(uint16_t port) +{ + uint32_t value; + __asm__ volatile("inl %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static void ata_controller_init(void) +{ + if (ata_initialized) { + return; + } + /* QEMU's direct Multiboot path does not run a PC firmware that enables + * the PIIX IDE PCI function. Enable I/O decoding for 00:01.1 while + * leaving the controller in its reset legacy compatibility mode. */ + outl(PCI_CONFIG_ADDRESS, UINT32_C(0x80000904)); + uint32_t command = inl(PCI_CONFIG_DATA); + outl(PCI_CONFIG_DATA, command | UINT32_C(0x00000001)); + outb(ATA_ALT_STATUS, 0x00); + ata_delay_400ns(); + ata_initialized = 1; +} + +static void ata_delay_400ns(void) +{ + (void) inb(ATA_ALT_STATUS); + (void) inb(ATA_ALT_STATUS); + (void) inb(ATA_ALT_STATUS); + (void) inb(ATA_ALT_STATUS); +} + +static int ata_wait_not_busy(void) +{ + for (uint32_t attempt = 0; attempt < ATA_TIMEOUT; ++attempt) { + const uint8_t status = inb(ATA_STATUS); + if (status == 0xff) { + return 0; + } + if ((status & ATA_STATUS_BUSY) == 0) { + return (status & (ATA_STATUS_ERROR | ATA_STATUS_DEVICE_FAULT)) == 0; + } + __asm__ volatile("pause"); + } + return 0; +} + +static int ata_wait_data(void) +{ + for (uint32_t attempt = 0; attempt < ATA_TIMEOUT; ++attempt) { + const uint8_t status = inb(ATA_STATUS); + if ((status & (ATA_STATUS_ERROR | ATA_STATUS_DEVICE_FAULT)) != 0) { + (void) inb(ATA_ERROR); + return 0; + } + if ((status & ATA_STATUS_BUSY) == 0 + && (status & ATA_STATUS_DATA_REQUEST) != 0) { + return 1; + } + __asm__ volatile("pause"); + } + return 0; +} + +static int ata_select_lba(uint32_t lba) +{ + ata_controller_init(); + if (lba >= UINT32_C(0x10000000) || !ata_wait_not_busy()) { + return 0; + } + outb(ATA_DRIVE, (uint8_t) (0xe0u | ((lba >> 24u) & 0x0fu))); + ata_delay_400ns(); + outb(ATA_SECTOR_COUNT, 1); + outb(ATA_LBA_LOW, (uint8_t) lba); + outb(ATA_LBA_MID, (uint8_t) (lba >> 8u)); + outb(ATA_LBA_HIGH, (uint8_t) (lba >> 16u)); + return 1; +} + +int typephp_os_disk_available(void) +{ + ata_controller_init(); + const uint8_t status = inb(ATA_STATUS); + /* QEMU leaves the status register at zero until the first command when + * booting a Multiboot kernel directly, without SeaBIOS ATA probing. */ + return status != 0xff; +} + +int typephp_os_disk_read_sector(uint32_t lba, unsigned char *data) +{ + if (data == 0 || !ata_select_lba(lba)) { + return 0; + } + outb(ATA_COMMAND, ATA_COMMAND_READ_SECTORS); + ata_delay_400ns(); + if (!ata_wait_data()) { + return 0; + } + for (size_t offset = 0; offset < ATA_SECTOR_SIZE; offset += 2) { + const uint16_t word = inw(ATA_DATA); + data[offset] = (unsigned char) word; + data[offset + 1] = (unsigned char) (word >> 8u); + } + return ata_wait_not_busy(); +} + +int typephp_os_disk_write_sector(uint32_t lba, const unsigned char *data) +{ + if (data == 0 || !ata_select_lba(lba)) { + return 0; + } + outb(ATA_COMMAND, ATA_COMMAND_WRITE_SECTORS); + ata_delay_400ns(); + if (!ata_wait_data()) { + return 0; + } + for (size_t offset = 0; offset < ATA_SECTOR_SIZE; offset += 2) { + outw(ATA_DATA, (uint16_t) (data[offset] | ((uint16_t) data[offset + 1] << 8u))); + } + return ata_wait_not_busy(); +} + +int typephp_os_disk_flush(void) +{ + if (!ata_wait_not_busy()) { + return 0; + } + outb(ATA_COMMAND, ATA_COMMAND_CACHE_FLUSH); + ata_delay_400ns(); + return ata_wait_not_busy(); +} diff --git a/examples/typephp-os/freestanding/bridge.cc b/examples/typephp-os/freestanding/bridge.cc index a9d28981..9ce3c6ef 100644 --- a/examples/typephp-os/freestanding/bridge.cc +++ b/examples/typephp-os/freestanding/bridge.cc @@ -2,6 +2,7 @@ #include #include #include +#include void php_main(); @@ -14,6 +15,33 @@ extern "C" void kernel_put_char_c(int ascii, int color); extern "C" void kernel_halt_c(); extern "C" unsigned long physical_memory_megabytes(); extern "C" unsigned long physical_chunk_smoke_test(); +extern "C" int typephp_os_disk_available(); +extern "C" int typephp_os_disk_read_sector(uint32_t lba, unsigned char *data); +extern "C" int typephp_os_disk_write_sector(uint32_t lba, const unsigned char *data); +extern "C" int typephp_os_disk_flush(); + +extern "C" ZEND_NORETURN void phpx_no_exception_abort(const char *fallback) +{ + if (EG(exception) != nullptr) { + zval value; + zval *message = zend_read_property_ex( + zend_get_exception_base(EG(exception)), + EG(exception), + ZSTR_KNOWN(ZEND_STR_MESSAGE), + true, + &value); + typephp_os_write("Uncaught ", sizeof("Uncaught ") - 1); + typephp_os_write(ZSTR_VAL(EG(exception)->ce->name), ZSTR_LEN(EG(exception)->ce->name)); + typephp_os_write(": ", sizeof(": ") - 1); + if (Z_TYPE_P(message) == IS_STRING) { + typephp_os_write(Z_STRVAL_P(message), Z_STRLEN_P(message)); + } else { + typephp_os_write(fallback, strlen(fallback)); + } + typephp_os_write("\n", 1); + } + typephp_os_panic(fallback); +} static void exception_model_smoke_test() { @@ -82,3 +110,31 @@ void php_kernel_halt() { kernel_halt_c(); } + +php::Bool php_kernel_disk_available() +{ + return typephp_os_disk_available() != 0; +} + +php::Str php_kernel_disk_read_sector(php::Int lba) +{ + unsigned char data[512]; + if (lba < 0 || lba > UINT32_MAX + || !typephp_os_disk_read_sector(static_cast(lba), data)) { + return php::Str(); + } + return php::Str(reinterpret_cast(data), sizeof(data)); +} + +php::Bool php_kernel_disk_write_sector(php::Int lba, php::Str data) +{ + return lba >= 0 && lba <= UINT32_MAX && data.length() == 512 + && typephp_os_disk_write_sector( + static_cast(lba), + reinterpret_cast(data.data())) != 0; +} + +php::Bool php_kernel_disk_flush() +{ + return typephp_os_disk_flush() != 0; +} diff --git a/examples/typephp-os/freestanding/filesystem_abi.cc b/examples/typephp-os/freestanding/filesystem_abi.cc new file mode 100644 index 00000000..e1ee5527 --- /dev/null +++ b/examples/typephp-os/freestanding/filesystem_abi.cc @@ -0,0 +1,383 @@ +/* + +----------------------------------------------------------------------+ + | TypePHP OS | + +----------------------------------------------------------------------+ + | POSIX ABI forwarding layer for the TypePHP FAT16 implementation. | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +php::Object installed_filesystem; + +bool filesystem_installed() +{ + return installed_filesystem.isObject(); +} + +php::Int fs_open(php::Str path, php::Int flags) +{ + return filesystem_installed() ? php_kernelfilesystem__open(installed_filesystem, path, flags) : -EIO; +} + +php::Int fs_close(php::Int fd) +{ + return filesystem_installed() ? php_kernelfilesystem__close(installed_filesystem, fd) : -EIO; +} + +php::Str fs_read(php::Int fd, php::Int count) +{ + return filesystem_installed() ? php_kernelfilesystem__read(installed_filesystem, fd, count) : php::Str{}; +} + +php::Int fs_write(php::Int fd, php::Str data) +{ + return filesystem_installed() ? php_kernelfilesystem__write(installed_filesystem, fd, data) : -EIO; +} + +php::Int fs_seek(php::Int fd, php::Int offset, php::Int whence) +{ + return filesystem_installed() ? php_kernelfilesystem__seek(installed_filesystem, fd, offset, whence) : -EIO; +} + +php::Int fs_flush(php::Int fd) +{ + return filesystem_installed() ? php_kernelfilesystem__flush(installed_filesystem, fd) : -EIO; +} + +php::Int fs_truncate(php::Int fd, php::Int size) +{ + return filesystem_installed() ? php_kernelfilesystem__truncate(installed_filesystem, fd, size) : -EIO; +} + +php::Int fs_size(php::Str path) +{ + return filesystem_installed() ? php_kernelfilesystem__filesize(installed_filesystem, path) : -EIO; +} + +php::Int fs_fd_size(php::Int fd) +{ + return filesystem_installed() ? php_kernelfilesystem__fdsize(installed_filesystem, fd) : -EIO; +} + +php::Int fs_type(php::Str path) +{ + return filesystem_installed() + ? php_kernelfilesystem__pathtype(installed_filesystem, path) + : 0; +} + +php::Int fs_mkdir(php::Str path) +{ + return filesystem_installed() ? php_kernelfilesystem__makedirectory(installed_filesystem, path) : -EIO; +} + +php::Int fs_rmdir(php::Str path) +{ + return filesystem_installed() ? php_kernelfilesystem__removedirectory(installed_filesystem, path) : -EIO; +} + +php::Int fs_unlink(php::Str path) +{ + return filesystem_installed() ? php_kernelfilesystem__removefile(installed_filesystem, path) : -EIO; +} + +php::Int fs_rename(php::Str old_path, php::Str new_path) +{ + return filesystem_installed() + ? php_kernelfilesystem__rename(installed_filesystem, old_path, new_path) + : -EIO; +} + +php::Str fs_entries(php::Str path) +{ + return filesystem_installed() ? php_kernelfilesystem__entries(installed_filesystem, path) : php::Str{}; +} + +int posix_result(php::Int result) +{ + if (result >= 0) { + return static_cast(result); + } + errno = static_cast(-result); + return -1; +} + +off_t posix_offset(php::Int result) +{ + if (result >= 0) { + return static_cast(result); + } + errno = static_cast(-result); + return static_cast(-1); +} + +void fill_stat(struct stat *value, php::Int type, php::Int size) +{ + std::memset(value, 0, sizeof(*value)); + value->st_mode = (type == 2 ? S_IFDIR | 0777 : S_IFREG | 0666); + value->st_nlink = 1; + value->st_size = static_cast(size); + value->st_blksize = 512; + value->st_blocks = static_cast((size + 511) / 512); +} + +struct DirectoryState { + php::Str entries; + std::size_t offset = 0; + struct dirent entry{}; + + explicit DirectoryState(php::Str value) : entries(std::move(value)) {} +}; + +} // namespace + +php::Bool php_kernel_fs_install(php::Object filesystem) +{ + installed_filesystem = filesystem; + return installed_filesystem.isObject(); +} + +extern "C" int open(const char *path, int flags, ...) +{ + if (path == nullptr) { + errno = EFAULT; + return -1; + } + return posix_result(fs_open(php::Str(path), flags)); +} + +extern "C" int close(int fd) +{ + if (fd >= 0 && fd <= 2) { + return 0; + } + return posix_result(fs_close(fd)); +} + +extern "C" ssize_t read(int fd, void *buffer, size_t count) +{ + if (buffer == nullptr && count != 0) { + errno = EFAULT; + return -1; + } + if (fd == STDIN_FILENO) { + return 0; + } + const php::Int size = fs_fd_size(fd); + if (size < 0) { + errno = static_cast(-size); + return -1; + } + php::Str data = fs_read(fd, static_cast(count)); + std::memcpy(buffer, data.data(), data.length()); + return static_cast(data.length()); +} + +extern "C" ssize_t write(int fd, const void *buffer, size_t count) +{ + if (buffer == nullptr && count != 0) { + errno = EFAULT; + return -1; + } + if (fd == STDOUT_FILENO || fd == STDERR_FILENO) { + typephp_os_write(static_cast(buffer), count); + return static_cast(count); + } + return static_cast(posix_result(fs_write( + fd, php::Str(static_cast(buffer), count)))); +} + +extern "C" off_t lseek(int fd, off_t offset, int whence) +{ + return posix_offset(fs_seek(fd, offset, whence)); +} + +extern "C" int fsync(int fd) +{ + return posix_result(fs_flush(fd)); +} + +extern "C" int ftruncate(int fd, off_t size) +{ + return posix_result(fs_truncate(fd, size)); +} + +extern "C" int stat(const char *path, struct stat *value) +{ + if (path == nullptr || value == nullptr) { + errno = EFAULT; + return -1; + } + php::Str php_path(path); + const php::Int type = fs_type(php_path); + if (type == 0) { + errno = ENOENT; + return -1; + } + const php::Int size = fs_size(php_path); + if (size < 0) { + errno = static_cast(-size); + return -1; + } + fill_stat(value, type, size); + return 0; +} + +extern "C" int lstat(const char *path, struct stat *value) +{ + return stat(path, value); +} + +extern "C" int fstat(int fd, struct stat *value) +{ + if (value == nullptr) { + errno = EFAULT; + return -1; + } + if (fd >= 0 && fd <= 2) { + fill_stat(value, 1, 0); + value->st_mode = S_IFCHR | 0666; + return 0; + } + const php::Int size = fs_fd_size(fd); + if (size < 0) { + errno = static_cast(-size); + return -1; + } + fill_stat(value, 1, size); + return 0; +} + +extern "C" int mkdir(const char *path, mode_t mode) +{ + (void) mode; + if (path == nullptr) { + errno = EFAULT; + return -1; + } + return posix_result(fs_mkdir(php::Str(path))); +} + +extern "C" int rmdir(const char *path) +{ + if (path == nullptr) { + errno = EFAULT; + return -1; + } + return posix_result(fs_rmdir(php::Str(path))); +} + +extern "C" int unlink(const char *path) +{ + if (path == nullptr) { + errno = EFAULT; + return -1; + } + return posix_result(fs_unlink(php::Str(path))); +} + +extern "C" int rename(const char *old_path, const char *new_path) +{ + if (old_path == nullptr || new_path == nullptr) { + errno = EFAULT; + return -1; + } + return posix_result(fs_rename(php::Str(old_path), php::Str(new_path))); +} + +extern "C" int access(const char *path, int mode) +{ + (void) mode; + if (path == nullptr) { + errno = EFAULT; + return -1; + } + return fs_type(php::Str(path)) == 0 ? (errno = ENOENT, -1) : 0; +} + +extern "C" int chdir(const char *path) +{ + if (path != nullptr && (std::strcmp(path, "/") == 0 || std::strcmp(path, ".") == 0)) { + return 0; + } + errno = ENOTDIR; + return -1; +} + +extern "C" int isatty(int fd) +{ + return fd >= 0 && fd <= 2; +} + +extern "C" DIR *opendir(const char *path) +{ + if (path == nullptr || fs_type(php::Str(path)) != 2) { + errno = ENOTDIR; + return nullptr; + } + return reinterpret_cast(new DirectoryState(fs_entries(php::Str(path)))); +} + +extern "C" struct dirent *readdir(DIR *directory) +{ + if (directory == nullptr) { + errno = EBADF; + return nullptr; + } + auto *state = reinterpret_cast(directory); + if (state->offset >= state->entries.length()) { + return nullptr; + } + std::size_t length = 0; + while (state->offset + length < state->entries.length() + && state->entries.data()[state->offset + length] != '\n') { + ++length; + } + if (length >= sizeof(state->entry.d_name)) { + errno = ENAMETOOLONG; + return nullptr; + } + std::memset(&state->entry, 0, sizeof(state->entry)); + std::memcpy(state->entry.d_name, state->entries.data() + state->offset, length); + state->entry.d_name[length] = '\0'; + state->entry.d_ino = static_cast(state->offset + 1); + state->entry.d_reclen = sizeof(state->entry); + state->entry.d_type = DT_UNKNOWN; + state->offset += length + 1; + return &state->entry; +} + +extern "C" void rewinddir(DIR *directory) +{ + if (directory != nullptr) { + reinterpret_cast(directory)->offset = 0; + } +} + +extern "C" int closedir(DIR *directory) +{ + if (directory == nullptr) { + errno = EBADF; + return -1; + } + delete reinterpret_cast(directory); + return 0; +} diff --git a/examples/typephp-os/project.yml b/examples/typephp-os/project.yml index 40645e7d..75c4a080 100644 --- a/examples/typephp-os/project.yml +++ b/examples/typephp-os/project.yml @@ -6,16 +6,22 @@ sources: - freestanding/kernel.c - freestanding/memory.c - freestanding/time.c + - freestanding/ata.c - freestanding/zend-allocator.c - freestanding/bridge.cc + - freestanding/filesystem_abi.cc - freestanding/abi/libc.c - freestanding/abi/cxx.cpp - freestanding/abi/unimplemented.S + - thirdparty/openlibm/src build-dir: build/generated output: build/kernel64.elf cxx-std: c++17 include-paths: - freestanding/abi + - thirdparty/openlibm/include + - thirdparty/openlibm/src + - thirdparty/openlibm/amd64 defines: - TYPEPHP_NO_MAIN=1 - PHPX_NO_EXCEPTION=1 @@ -24,6 +30,8 @@ defines: - __NO_CTYPE=1 - ZEND_MM_ERROR=0 - ZEND_MM_CUSTOM=0 + - ASSEMBLER=1 + - __BSD_VISIBLE=1 cxx-flags: - -ffreestanding - -fno-builtin diff --git a/examples/typephp-os/src/fat16.php b/examples/typephp-os/src/fat16.php new file mode 100644 index 00000000..09f5a4f8 --- /dev/null +++ b/examples/typephp-os/src/fat16.php @@ -0,0 +1,651 @@ +device = $device; + } + + private function quotient(int $value, int $divisor): int + { + return (int) (($value - ($value % $divisor)) / $divisor); + } + + private function ceilQuotient(int $value, int $divisor): int + { + return $this->quotient($value + $divisor - 1, $divisor); + } + + private function byteAt(string $data, int $offset): int + { + return ord($data[$offset]); + } + + private function readU16(string $data, int $offset): int + { + return $this->byteAt($data, $offset) + + ($this->byteAt($data, $offset + 1) << 8); + } + + private function readU32(string $data, int $offset): int + { + return $this->byteAt($data, $offset) + + ($this->byteAt($data, $offset + 1) << 8) + + ($this->byteAt($data, $offset + 2) << 16) + + ($this->byteAt($data, $offset + 3) << 24); + } + + private function writeU16(string &$data, int $offset, int $value): void + { + $data[$offset] = chr($value & 0xff); + $data[$offset + 1] = chr(($value >> 8) & 0xff); + } + + private function writeU32(string &$data, int $offset, int $value): void + { + $data[$offset] = chr($value & 0xff); + $data[$offset + 1] = chr(($value >> 8) & 0xff); + $data[$offset + 2] = chr(($value >> 16) & 0xff); + $data[$offset + 3] = chr(($value >> 24) & 0xff); + } + + private function blankSector(): string + { + $result = ''; + for ($index = 0; $index < 512; $index++) { + $result .= chr(0); + } + return $result; + } + + private function sectorData(string $data): string + { + $result = $data; + while (strlen($result) < 512) { + $result .= chr(0); + } + return $result; + } + + public function lastError(): string + { + return $this->error; + } + + public function mount(): bool + { + $boot = $this->device->readSector(0); + if (strlen($boot) !== 512) { + $this->error = 'unable to read FAT boot sector'; + return false; + } + if ($this->byteAt($boot, 510) !== 0x55 || $this->byteAt($boot, 511) !== 0xaa) { + $this->error = 'invalid FAT boot signature'; + return false; + } + + $this->bytesPerSector = $this->readU16($boot, 11); + $this->sectorsPerCluster = $this->byteAt($boot, 13); + $this->reservedSectors = $this->readU16($boot, 14); + $this->fatCount = $this->byteAt($boot, 16); + $this->rootEntryCount = $this->readU16($boot, 17); + $this->totalSectors = $this->readU16($boot, 19); + if ($this->totalSectors === 0) { + $this->totalSectors = $this->readU32($boot, 32); + } + $this->sectorsPerFat = $this->readU16($boot, 22); + + if ($this->bytesPerSector !== 512 || $this->sectorsPerCluster <= 0 + || $this->reservedSectors <= 0 || $this->fatCount <= 0 + || $this->rootEntryCount <= 0 || $this->sectorsPerFat <= 0) { + $this->error = 'unsupported FAT16 geometry'; + return false; + } + + $this->rootSectorCount = $this->ceilQuotient($this->rootEntryCount * 32, 512); + $this->firstFatSector = $this->reservedSectors; + $this->firstRootSector = $this->firstFatSector + $this->fatCount * $this->sectorsPerFat; + $this->firstDataSector = $this->firstRootSector + $this->rootSectorCount; + $this->clusterCount = $this->quotient( + $this->totalSectors - $this->firstDataSector, + $this->sectorsPerCluster + ); + if ($this->clusterCount < 4085 || $this->clusterCount >= 65525) { + $this->error = 'disk is not FAT16'; + return false; + } + $this->error = ''; + return true; + } + + private function validNameCharacter(int $character): bool + { + return ($character >= 65 && $character <= 90) + || ($character >= 48 && $character <= 57) + || $character === 45 || $character === 95; + } + + private function fatName(string $name): string + { + $upper = strtoupper($name); + $base = ''; + $extension = ''; + $inExtension = false; + for ($index = 0; $index < strlen($upper); $index++) { + $character = ord($upper[$index]); + if ($character === 46 && !$inExtension) { + $inExtension = true; + continue; + } + if (!$this->validNameCharacter($character)) { + return ''; + } + if ($inExtension) { + if (strlen($extension) >= 3) { + return ''; + } + $extension .= $upper[$index]; + } else { + if (strlen($base) >= 8) { + return ''; + } + $base .= $upper[$index]; + } + } + if (strlen($base) === 0) { + return ''; + } + while (strlen($base) < 8) { + $base .= ' '; + } + while (strlen($extension) < 3) { + $extension .= ' '; + } + return $base . $extension; + } + + private function displayName(string $entry): string + { + $base = rtrim(substr($entry, 0, 8)); + $extension = rtrim(substr($entry, 8, 3)); + return strlen($extension) === 0 ? $base : $base . '.' . $extension; + } + + /* A directory location is encoded as sector * 512 + byte offset. */ + private function rootEntryLocation(string $fatName): int + { + for ($sectorIndex = 0; $sectorIndex < $this->rootSectorCount; $sectorIndex++) { + $sectorNumber = $this->firstRootSector + $sectorIndex; + $sector = $this->device->readSector($sectorNumber); + if (strlen($sector) !== 512) { + return -1; + } + for ($offset = 0; $offset < 512; $offset += 32) { + $first = $this->byteAt($sector, $offset); + if ($first === 0x00) { + return -1; + } + if ($first !== 0xe5 && substr($sector, $offset, 11) === $fatName) { + return $sectorNumber * 512 + $offset; + } + } + } + return -1; + } + + private function freeRootEntryLocation(): int + { + for ($sectorIndex = 0; $sectorIndex < $this->rootSectorCount; $sectorIndex++) { + $sectorNumber = $this->firstRootSector + $sectorIndex; + $sector = $this->device->readSector($sectorNumber); + if (strlen($sector) !== 512) { + return -1; + } + for ($offset = 0; $offset < 512; $offset += 32) { + $first = $this->byteAt($sector, $offset); + if ($first === 0x00 || $first === 0xe5) { + return $sectorNumber * 512 + $offset; + } + } + } + return -1; + } + + private function readEntry(int $location): string + { + $sectorNumber = $this->quotient($location, 512); + $offset = $location % 512; + return substr($this->device->readSector($sectorNumber), $offset, 32); + } + + private function writeEntry(int $location, string $fatName, int $attributes, int $cluster, int $size): bool + { + $sectorNumber = $this->quotient($location, 512); + $offset = $location % 512; + $sector = $this->device->readSector($sectorNumber); + if (strlen($sector) !== 512) { + return false; + } + for ($index = 0; $index < 32; $index++) { + $sector[$offset + $index] = chr(0); + } + for ($index = 0; $index < 11; $index++) { + $sector[$offset + $index] = $fatName[$index]; + } + $sector[$offset + 11] = chr($attributes); + $this->writeU16($sector, $offset + 26, $cluster); + $this->writeU32($sector, $offset + 28, $size); + return $this->device->writeSector($sectorNumber, $sector); + } + + private function clusterSector(int $cluster): int + { + return $this->firstDataSector + ($cluster - 2) * $this->sectorsPerCluster; + } + + private function readFat(int $cluster): int + { + $fatOffset = $cluster * 2; + $sectorNumber = $this->firstFatSector + $this->quotient($fatOffset, 512); + $sector = $this->device->readSector($sectorNumber); + if (strlen($sector) !== 512) { + return 0xffff; + } + return $this->readU16($sector, $fatOffset % 512); + } + + private function writeFat(int $cluster, int $value): bool + { + $fatOffset = $cluster * 2; + $relativeSector = $this->quotient($fatOffset, 512); + $offset = $fatOffset % 512; + for ($fat = 0; $fat < $this->fatCount; $fat++) { + $sectorNumber = $this->firstFatSector + $fat * $this->sectorsPerFat + $relativeSector; + $sector = $this->device->readSector($sectorNumber); + if (strlen($sector) !== 512) { + return false; + } + $this->writeU16($sector, $offset, $value); + if (!$this->device->writeSector($sectorNumber, $sector)) { + return false; + } + } + return true; + } + + private function freeChain(int $cluster): bool + { + $visited = 0; + while ($cluster >= 2 && $cluster < 0xfff8 && $visited < $this->clusterCount) { + $next = $this->readFat($cluster); + if (!$this->writeFat($cluster, 0)) { + return false; + } + $cluster = $next; + $visited++; + } + return true; + } + + private function findFreeCluster(int $start): int + { + $lastCluster = $this->clusterCount + 1; + for ($cluster = $start; $cluster <= $lastCluster; $cluster++) { + if ($this->readFat($cluster) === 0) { + return $cluster; + } + } + return -1; + } + + private function clearCluster(int $cluster): bool + { + $blank = $this->blankSector(); + $firstSector = $this->clusterSector($cluster); + for ($index = 0; $index < $this->sectorsPerCluster; $index++) { + if (!$this->device->writeSector($firstSector + $index, $blank)) { + return false; + } + } + return true; + } + + public function hasRootEntry(string $name): bool + { + $fatName = $this->fatName($name); + return strlen($fatName) === 11 && $this->rootEntryLocation($fatName) >= 0; + } + + public function rootEntryType(string $name): int + { + $fatName = $this->fatName($name); + if (strlen($fatName) !== 11) { + return 0; + } + $location = $this->rootEntryLocation($fatName); + if ($location < 0) { + return 0; + } + $entry = $this->readEntry($location); + return ($this->byteAt($entry, 11) & 0x10) !== 0 ? 2 : 1; + } + + public function rootFileSize(string $name): int + { + $fatName = $this->fatName($name); + if (strlen($fatName) !== 11) { + return -1; + } + $location = $this->rootEntryLocation($fatName); + if ($location < 0) { + return -1; + } + $entry = $this->readEntry($location); + return ($this->byteAt($entry, 11) & 0x10) !== 0 + ? 0 + : $this->readU32($entry, 28); + } + + public function makeRootDirectory(string $name): bool + { + $fatName = $this->fatName($name); + if (strlen($fatName) !== 11 || $this->rootEntryLocation($fatName) >= 0) { + return false; + } + $location = $this->freeRootEntryLocation(); + $cluster = $this->findFreeCluster(2); + if ($location < 0 || $cluster < 0 || !$this->writeFat($cluster, 0xffff) + || !$this->clearCluster($cluster)) { + return false; + } + + $sector = $this->device->readSector($this->clusterSector($cluster)); + $dot = '. '; + $dotDot = '.. '; + for ($index = 0; $index < 11; $index++) { + $sector[$index] = $dot[$index]; + $sector[32 + $index] = $dotDot[$index]; + } + $sector[11] = chr(0x10); + $sector[43] = chr(0x10); + $this->writeU16($sector, 26, $cluster); + $this->writeU16($sector, 58, 0); + if (!$this->device->writeSector($this->clusterSector($cluster), $sector) + || !$this->writeEntry($location, $fatName, 0x10, $cluster, 0)) { + $this->writeFat($cluster, 0); + return false; + } + return $this->device->flush(); + } + + public function writeRootFile(string $name, string $contents): bool + { + $fatName = $this->fatName($name); + if (strlen($fatName) !== 11) { + return false; + } + $location = $this->rootEntryLocation($fatName); + if ($location >= 0) { + $oldEntry = $this->readEntry($location); + if (($this->byteAt($oldEntry, 11) & 0x10) !== 0 + || !$this->freeChain($this->readU16($oldEntry, 26))) { + return false; + } + } else { + $location = $this->freeRootEntryLocation(); + if ($location < 0) { + return false; + } + } + + $clusterSize = $this->sectorsPerCluster * 512; + $needed = $this->ceilQuotient(strlen($contents), $clusterSize); + $clusters = std::vector(Type::Int); + $candidate = 2; + for ($index = 0; $index < $needed; $index++) { + $candidate = $this->findFreeCluster($candidate); + if ($candidate < 0) { + for ($rollback = 0; $rollback < count($clusters); $rollback++) { + $this->writeFat($clusters[$rollback], 0); + } + return false; + } + $clusters[] = $candidate; + if (!$this->writeFat($candidate, 0xffff)) { + return false; + } + $candidate++; + } + + for ($index = 0; $index < count($clusters); $index++) { + $cluster = $clusters[$index]; + $next = $index + 1 < count($clusters) ? $clusters[$index + 1] : 0xffff; + if (!$this->writeFat($cluster, $next)) { + return false; + } + for ($sectorIndex = 0; $sectorIndex < $this->sectorsPerCluster; $sectorIndex++) { + $dataOffset = $index * $clusterSize + $sectorIndex * 512; + $part = substr($contents, $dataOffset, 512); + if (!$this->device->writeSector( + $this->clusterSector($cluster) + $sectorIndex, + $this->sectorData($part) + )) { + return false; + } + } + } + + $firstCluster = count($clusters) === 0 ? 0 : $clusters[0]; + return $this->writeEntry($location, $fatName, 0x20, $firstCluster, strlen($contents)) + && $this->device->flush(); + } + + public function readRootFile(string $name): string + { + $fatName = $this->fatName($name); + if (strlen($fatName) !== 11) { + return ''; + } + $location = $this->rootEntryLocation($fatName); + if ($location < 0) { + return ''; + } + $entry = $this->readEntry($location); + if (($this->byteAt($entry, 11) & 0x10) !== 0) { + return ''; + } + $remaining = $this->readU32($entry, 28); + $cluster = $this->readU16($entry, 26); + $result = ''; + $visited = 0; + while ($remaining > 0 && $cluster >= 2 && $cluster < 0xfff8 + && $visited < $this->clusterCount) { + $firstSector = $this->clusterSector($cluster); + for ($index = 0; $index < $this->sectorsPerCluster && $remaining > 0; $index++) { + $sector = $this->device->readSector($firstSector + $index); + if (strlen($sector) !== 512) { + return ''; + } + $length = $remaining < 512 ? $remaining : 512; + $result .= substr($sector, 0, $length); + $remaining -= $length; + } + $cluster = $this->readFat($cluster); + $visited++; + } + return $remaining === 0 ? $result : ''; + } + + public function removeRootFile(string $name): bool + { + $fatName = $this->fatName($name); + if (strlen($fatName) !== 11) { + return false; + } + $location = $this->rootEntryLocation($fatName); + if ($location < 0) { + return false; + } + $entry = $this->readEntry($location); + if (($this->byteAt($entry, 11) & 0x10) !== 0 + || !$this->freeChain($this->readU16($entry, 26))) { + return false; + } + $sectorNumber = $this->quotient($location, 512); + $sector = $this->device->readSector($sectorNumber); + $sector[$location % 512] = chr(0xe5); + return $this->device->writeSector($sectorNumber, $sector) + && $this->device->flush(); + } + + public function renameRootEntry(string $oldName, string $newName): bool + { + $oldFatName = $this->fatName($oldName); + $newFatName = $this->fatName($newName); + if (strlen($oldFatName) !== 11 || strlen($newFatName) !== 11 + || $this->rootEntryLocation($newFatName) >= 0) { + return false; + } + $location = $this->rootEntryLocation($oldFatName); + if ($location < 0) { + return false; + } + $sectorNumber = $this->quotient($location, 512); + $offset = $location % 512; + $sector = $this->device->readSector($sectorNumber); + for ($index = 0; $index < 11; $index++) { + $sector[$offset + $index] = $newFatName[$index]; + } + return $this->device->writeSector($sectorNumber, $sector) + && $this->device->flush(); + } + + public function removeRootDirectory(string $name): bool + { + $fatName = $this->fatName($name); + if (strlen($fatName) !== 11) { + return false; + } + $location = $this->rootEntryLocation($fatName); + if ($location < 0) { + return false; + } + $entry = $this->readEntry($location); + if (($this->byteAt($entry, 11) & 0x10) === 0) { + return false; + } + $cluster = $this->readU16($entry, 26); + $firstSector = $this->device->readSector($this->clusterSector($cluster)); + for ($offset = 64; $offset < 512; $offset += 32) { + $first = $this->byteAt($firstSector, $offset); + if ($first !== 0x00 && $first !== 0xe5) { + return false; + } + } + if (!$this->freeChain($cluster)) { + return false; + } + $sectorNumber = $this->quotient($location, 512); + $sector = $this->device->readSector($sectorNumber); + $sector[$location % 512] = chr(0xe5); + return $this->device->writeSector($sectorNumber, $sector) + && $this->device->flush(); + } + + public function rootEntryNames(): string + { + $result = ''; + for ($sectorIndex = 0; $sectorIndex < $this->rootSectorCount; $sectorIndex++) { + $sector = $this->device->readSector($this->firstRootSector + $sectorIndex); + if (strlen($sector) !== 512) { + return ''; + } + for ($offset = 0; $offset < 512; $offset += 32) { + $first = $this->byteAt($sector, $offset); + if ($first === 0x00) { + return $result; + } + $attributes = $this->byteAt($sector, $offset + 11); + if ($first === 0xe5 || $attributes === 0x0f || ($attributes & 0x08) !== 0) { + continue; + } + $result .= $this->displayName(substr($sector, $offset, 11)) . chr(10); + } + } + return $result; + } + + public function rootListing(): string + { + $result = ''; + for ($sectorIndex = 0; $sectorIndex < $this->rootSectorCount; $sectorIndex++) { + $sector = $this->device->readSector($this->firstRootSector + $sectorIndex); + if (strlen($sector) !== 512) { + return ''; + } + for ($offset = 0; $offset < 512; $offset += 32) { + $first = $this->byteAt($sector, $offset); + if ($first === 0x00) { + return $result; + } + $attributes = $this->byteAt($sector, $offset + 11); + if ($first === 0xe5 || $attributes === 0x0f || ($attributes & 0x08) !== 0) { + continue; + } + if (strlen($result) !== 0) { + $result .= ', '; + } + $result .= $this->displayName(substr($sector, $offset, 11)); + if (($attributes & 0x10) !== 0) { + $result .= '/'; + } + } + } + return $result; + } +} diff --git a/examples/typephp-os/src/filesystem.php b/examples/typephp-os/src/filesystem.php new file mode 100644 index 00000000..3d3155c7 --- /dev/null +++ b/examples/typephp-os/src/filesystem.php @@ -0,0 +1,296 @@ +name = $name; + $this->contents = $contents; + $this->offset = $offset; + $this->writable = $writable; + $this->dirty = false; + } +} + +/* + * Process-level filesystem state. The POSIX shim forwards here, so PHP's + * unchanged plain-stream implementation ultimately executes TypePHP code. + */ +final class KernelFileSystem +{ + private Fat16Volume $volume; + private bool $mounted = false; + private array $files = []; + + public function initialize(): bool + { + if ($this->mounted) { + return true; + } + $device = new AtaBlockDevice(); + if (!$device->available()) { + return false; + } + $this->volume = new Fat16Volume($device); + $volume = $this->volume->toObject(Fat16Volume::class); + $this->mounted = $volume->mount(); + return $this->mounted; + } + + private function rootName(string $path): string + { + $name = $path; + while (strlen($name) > 0 && $name[0] === '/') { + $name = substr($name, 1); + } + if (strlen($name) === 0) { + return ''; + } + for ($index = 0; $index < strlen($name); $index++) { + if ($name[$index] === '/') { + return ''; + } + } + return $name; + } + + private function opened(int $fd): KernelOpenFile + { + return $this->files[$fd]->toObject(KernelOpenFile::class); + } + + public function open(string $path, int $flags): int + { + if (!$this->initialize()) { + return -5; + } + $name = $this->rootName($path); + if (strlen($name) === 0) { + return -21; + } + $volume = $this->volume->toObject(Fat16Volume::class); + $type = $volume->rootEntryType($name); + if ($type === 2) { + return -21; + } + $create = ($flags & 64) !== 0; + $truncate = ($flags & 512) !== 0; + $append = ($flags & 1024) !== 0; + $writable = ($flags & 3) !== 0; + if ($type === 0 && !$create) { + return -2; + } + $contents = $type === 1 ? $volume->readRootFile($name) : ''; + if ($truncate && $writable) { + $contents = ''; + } + $offset = $append ? strlen($contents) : 0; + $fd = 3; + while (isset($this->files[$fd])) { + $fd++; + if ($fd >= 64) { + return -24; + } + } + $file = new KernelOpenFile($name, $contents, $offset, $writable); + $file->dirty = $type === 0 || ($truncate && $writable); + $this->files[$fd] = $file; + return $fd; + } + + public function close(int $fd): int + { + if (!isset($this->files[$fd])) { + return -9; + } + $file = $this->opened($fd); + $volume = $this->volume->toObject(Fat16Volume::class); + if ($file->dirty && !$volume->writeRootFile($file->name, $file->contents)) { + return -5; + } + unset($this->files[$fd]); + return 0; + } + + public function read(int $fd, int $count): string + { + if (!isset($this->files[$fd]) || $count <= 0) { + return ''; + } + $file = $this->opened($fd); + $data = substr($file->contents, $file->offset, $count); + $file->offset += strlen($data); + return $data; + } + + public function write(int $fd, string $data): int + { + if (!isset($this->files[$fd])) { + return -9; + } + $file = $this->opened($fd); + if (!$file->writable) { + return -9; + } + while (strlen($file->contents) < $file->offset) { + $file->contents .= chr(0); + } + $prefix = substr($file->contents, 0, $file->offset); + $after = $file->offset + strlen($data); + $suffix = $after < strlen($file->contents) ? substr($file->contents, $after) : ''; + $file->contents = $prefix . $data . $suffix; + $file->offset += strlen($data); + $file->dirty = true; + return strlen($data); + } + + public function seek(int $fd, int $offset, int $whence): int + { + if (!isset($this->files[$fd])) { + return -9; + } + $file = $this->opened($fd); + $position = $offset; + if ($whence === 1) { + $position = $file->offset + $offset; + } elseif ($whence === 2) { + $position = strlen($file->contents) + $offset; + } elseif ($whence !== 0) { + return -22; + } + if ($position < 0) { + return -22; + } + $file->offset = $position; + return $position; + } + + public function flush(int $fd): int + { + if (!isset($this->files[$fd])) { + return -9; + } + $file = $this->opened($fd); + if ($file->dirty) { + $volume = $this->volume->toObject(Fat16Volume::class); + if (!$volume->writeRootFile($file->name, $file->contents)) { + return -5; + } + $file->dirty = false; + } + return 0; + } + + public function truncate(int $fd, int $size): int + { + if (!isset($this->files[$fd]) || $size < 0) { + return -22; + } + $file = $this->opened($fd); + if (!$file->writable) { + return -9; + } + if ($size < strlen($file->contents)) { + $file->contents = substr($file->contents, 0, $size); + } else { + while (strlen($file->contents) < $size) { + $file->contents .= chr(0); + } + } + $file->dirty = true; + return 0; + } + + public function fileSize(string $path): int + { + if (!$this->initialize()) { + return -5; + } + if ($path === '/' || $path === '') { + return 0; + } + $name = $this->rootName($path); + $volume = $this->volume->toObject(Fat16Volume::class); + if (strlen($name) === 0 || !$volume->hasRootEntry($name)) { + return -2; + } + return $volume->rootFileSize($name); + } + + public function pathType(string $path): int + { + if (!$this->initialize()) { + return 0; + } + if ($path === '/' || $path === '' || $path === '.') { + return 2; + } + $name = $this->rootName($path); + $volume = $this->volume->toObject(Fat16Volume::class); + return strlen($name) === 0 ? 0 : $volume->rootEntryType($name); + } + + public function fdSize(int $fd): int + { + return isset($this->files[$fd]) ? strlen($this->opened($fd)->contents) : -9; + } + + public function makeDirectory(string $path): int + { + $name = $this->rootName($path); + if (!$this->initialize() || strlen($name) === 0) { + return -22; + } + $volume = $this->volume->toObject(Fat16Volume::class); + if ($volume->hasRootEntry($name)) { + return -17; + } + return $volume->makeRootDirectory($name) ? 0 : -5; + } + + public function removeDirectory(string $path): int + { + $name = $this->rootName($path); + if (!$this->initialize() || strlen($name) === 0) { + return -22; + } + $volume = $this->volume->toObject(Fat16Volume::class); + return $volume->removeRootDirectory($name) ? 0 : -39; + } + + public function removeFile(string $path): int + { + $name = $this->rootName($path); + if (!$this->initialize() || strlen($name) === 0) { + return -22; + } + $volume = $this->volume->toObject(Fat16Volume::class); + return $volume->removeRootFile($name) ? 0 : -2; + } + + public function rename(string $oldPath, string $newPath): int + { + $oldName = $this->rootName($oldPath); + $newName = $this->rootName($newPath); + if (!$this->initialize() || strlen($oldName) === 0 || strlen($newName) === 0) { + return -22; + } + $volume = $this->volume->toObject(Fat16Volume::class); + return $volume->renameRootEntry($oldName, $newName) ? 0 : -2; + } + + public function entries(string $path): string + { + if (!$this->initialize() || ($path !== '/' && $path !== '' && $path !== '.')) { + return ''; + } + $volume = $this->volume->toObject(Fat16Volume::class); + return ".\n..\n" . $volume->rootEntryNames(); + } +} diff --git a/examples/typephp-os/src/fs_bridge.stub.php b/examples/typephp-os/src/fs_bridge.stub.php new file mode 100644 index 00000000..a1840a1c --- /dev/null +++ b/examples/typephp-os/src/fs_bridge.stub.php @@ -0,0 +1,4 @@ + 1.414 && $root < 1.415 + && $power > 2.999 && $power < 3.001 + && $identity > 0.999 && $identity < 1.001) { + writeLine('OpenLibm math: OK', 10); + return; + } + writeLine('OpenLibm math: FAILED', 12); +} + +function runFilesystemSelfCheck(): void +{ + $device = new AtaBlockDevice(); + if (!$device->available()) { + writeLine('FAT16 disk: unavailable', 12); + return; + } + + $volume = new Fat16Volume($device); + if (!$volume->mount()) { + writeLine('FAT16 mount: ' . $volume->lastError(), 12); + return; + } + if (!$volume->hasRootEntry('DATA')) { + $volume->makeRootDirectory('DATA'); + } + $expected = 'Hello from TypePHP FAT16!'; + if (!$volume->writeRootFile('HELLO.TXT', $expected)) { + writeLine('FAT16 write: FAILED', 12); + return; + } + $actual = $volume->readRootFile('HELLO.TXT'); + if ($actual !== $expected) { + writeLine('FAT16 read: FAILED', 12); + return; + } + writeLine('FAT16 file: ' . $actual, 10); + writeLine('FAT16 root: ' . $volume->rootListing(), 10); + + /* Exercise the unchanged PHP standard extension and plain file-stream + * implementation through the POSIX-to-TypePHP bridge. */ + $filesystem = new KernelFileSystem(); + if (!$filesystem->initialize()) { + writeLine('PHP file stream: mount FAILED', 12); + return; + } + if (!kernel_fs_install($filesystem)) { + writeLine('PHP file stream: install FAILED', 12); + return; + } + $streamExpected = 'PHP stream through TypePHP FAT16'; + $written = file_put_contents('/STREAM.TXT', $streamExpected); + $streamActual = file_get_contents('/STREAM.TXT'); + if ($written !== strlen($streamExpected) || $streamActual !== $streamExpected) { + writeLine('PHP file stream: FAILED', 12); + return; + } + if (!is_dir('/DOCS') && !mkdir('/DOCS')) { + writeLine('PHP directory: FAILED', 12); + return; + } + $entries = scandir('/'); + if ($entries === false) { + writeLine('PHP directory scan: FAILED', 12); + return; + } + echo 'PHP file stream: ', $streamActual, "\n"; + echo 'PHP directory scan: ', implode(', ', $entries), "\n"; +} + function runPrimeDemo(int $limit): void { $primes = std::vector(Type::Int); @@ -99,6 +173,8 @@ function runPrimeDemo(int $limit): void function main(): void { runFeatureSelfCheck(); + runMathSelfCheck(); + runFilesystemSelfCheck(); runPrimeDemo(100); $greeting = new KernelGreeting(); diff --git a/examples/typephp-os/tools/test-qemu.sh b/examples/typephp-os/tools/test-qemu.sh index 775a4795..177bb626 100755 --- a/examples/typephp-os/tools/test-qemu.sh +++ b/examples/typephp-os/tools/test-qemu.sh @@ -3,10 +3,12 @@ set -euo pipefail kernel=${1:?kernel ELF is required} -log=${2:?log path is required} +disk=${2:?FAT16 disk image is required} +log=${3:?log path is required} if timeout 8 qemu-system-x86_64 \ -kernel "${kernel}" \ + -drive file="${disk}",format=raw,if=ide,index=0 \ -display none \ -serial stdio \ -monitor none \ @@ -31,6 +33,11 @@ grep -Eq '^RAM MiB: [1-9][0-9]*' "${log}" grep -q "Zend MiB: 2" "${log}" grep -q "Zend string/array: OK" "${log}" grep -q "Kernel is!" "${log}" +grep -q "OpenLibm math: OK" "${log}" +grep -q "FAT16 file: Hello from TypePHP FAT16!" "${log}" +grep -q "FAT16 root: DATA/, HELLO.TXT" "${log}" +grep -q "PHP file stream: PHP stream through TypePHP FAT16" "${log}" +grep -q "PHP directory scan: ., .., DATA, DOCS, HELLO.TXT, STREAM.TXT" "${log}" grep -q "Calculate primes: 0-100" "${log}" grep -q "Prime count: 25" "${log}" grep -q "Prime list: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97" "${log}"