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
master
韩天峰 4 weeks ago
parent 725959da9f
commit 515132b6b8
  1. 2
      examples/typephp-os/.gitignore
  2. 17
      examples/typephp-os/Makefile
  3. 25
      examples/typephp-os/README.md
  4. 7
      examples/typephp-os/ROADMAP.md
  5. 64
      examples/typephp-os/freestanding/abi/libc.c
  6. 45
      examples/typephp-os/freestanding/abi/unimplemented.S
  7. 201
      examples/typephp-os/freestanding/ata.c
  8. 56
      examples/typephp-os/freestanding/bridge.cc
  9. 383
      examples/typephp-os/freestanding/filesystem_abi.cc
  10. 8
      examples/typephp-os/project.yml
  11. 651
      examples/typephp-os/src/fat16.php
  12. 296
      examples/typephp-os/src/filesystem.php
  13. 4
      examples/typephp-os/src/fs_bridge.stub.php
  14. 12
      examples/typephp-os/src/hardware.stub.php
  15. 76
      examples/typephp-os/src/kernel.php
  16. 9
      examples/typephp-os/tools/test-qemu.sh

@ -1 +1,3 @@
/build/
*.o
/thirdparty

@ -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)

@ -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<int>`;
- 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.

@ -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

@ -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;

@ -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

@ -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 <stddef.h>
#include <stdint.h>
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();
}

@ -2,6 +2,7 @@
#include <php_nano_extension.h>
#include <phpx.h>
#include <phpx_helper.h>
#include <cstdint>
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<uint32_t>(lba), data)) {
return php::Str();
}
return php::Str(reinterpret_cast<const char *>(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<uint32_t>(lba),
reinterpret_cast<const unsigned char *>(data.data())) != 0;
}
php::Bool php_kernel_disk_flush()
{
return typephp_os_disk_flush() != 0;
}

@ -0,0 +1,383 @@
/*
+----------------------------------------------------------------------+
| TypePHP OS |
+----------------------------------------------------------------------+
| POSIX ABI forwarding layer for the TypePHP FAT16 implementation. |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
*/
#include <phpx.h>
#include <php_kernel64_func_decl.h>
#include <typephp_os_abi.h>
#include <cerrno>
#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <dirent.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <utility>
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<int>(result);
}
errno = static_cast<int>(-result);
return -1;
}
off_t posix_offset(php::Int result)
{
if (result >= 0) {
return static_cast<off_t>(result);
}
errno = static_cast<int>(-result);
return static_cast<off_t>(-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<off_t>(size);
value->st_blksize = 512;
value->st_blocks = static_cast<blkcnt_t>((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<int>(-size);
return -1;
}
php::Str data = fs_read(fd, static_cast<php::Int>(count));
std::memcpy(buffer, data.data(), data.length());
return static_cast<ssize_t>(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<const char *>(buffer), count);
return static_cast<ssize_t>(count);
}
return static_cast<ssize_t>(posix_result(fs_write(
fd, php::Str(static_cast<const char *>(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<int>(-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<int>(-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<DIR *>(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<DirectoryState *>(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<ino_t>(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<DirectoryState *>(directory)->offset = 0;
}
}
extern "C" int closedir(DIR *directory)
{
if (directory == nullptr) {
errno = EBADF;
return -1;
}
delete reinterpret_cast<DirectoryState *>(directory);
return 0;
}

@ -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

@ -0,0 +1,651 @@
<?php
final class AtaBlockDevice
{
public function available(): bool
{
return kernel_disk_available();
}
public function readSector(int $lba): string
{
return kernel_disk_read_sector($lba);
}
public function writeSector(int $lba, string $data): bool
{
return kernel_disk_write_sector($lba, $data);
}
public function flush(): bool
{
return kernel_disk_flush();
}
}
/*
* Deliberately small FAT16 implementation. The ATA driver only transports
* sectors; filesystem layout, allocation, directory entries, and file data
* are implemented in TypePHP so this code can later be shared with hosted
* and in-memory block devices.
*/
final class Fat16Volume
{
private AtaBlockDevice $device;
private int $bytesPerSector = 0;
private int $sectorsPerCluster = 0;
private int $reservedSectors = 0;
private int $fatCount = 0;
private int $rootEntryCount = 0;
private int $sectorsPerFat = 0;
private int $totalSectors = 0;
private int $firstFatSector = 0;
private int $firstRootSector = 0;
private int $rootSectorCount = 0;
private int $firstDataSector = 0;
private int $clusterCount = 0;
private string $error = '';
public function __construct(AtaBlockDevice $device)
{
$this->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;
}
}

@ -0,0 +1,296 @@
<?php
final class KernelOpenFile
{
public string $name;
public string $contents;
public int $offset;
public bool $writable;
public bool $dirty;
public function __construct(string $name, string $contents, int $offset, bool $writable)
{
$this->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();
}
}

@ -0,0 +1,4 @@
<?php
/** Install the TypePHP filesystem object behind the POSIX ABI bridge. */
function kernel_fs_install(KernelFileSystem $filesystem): bool {}

@ -20,3 +20,15 @@ function kernel_chunk_smoke_test(): int { return 0; }
#[NativeFunction]
function kernel_halt(): void {}
#[NativeFunction]
function kernel_disk_available(): bool { return false; }
#[NativeFunction]
function kernel_disk_read_sector(int $lba): string { return ''; }
#[NativeFunction]
function kernel_disk_write_sector(int $lba, string $data): bool { return false; }
#[NativeFunction]
function kernel_disk_flush(): bool { return false; }

@ -69,6 +69,80 @@ function runFeatureSelfCheck(): void
}
}
function runMathSelfCheck(): void
{
$root = sqrt(2.0);
$power = pow(9.0, 0.5);
$identity = sin(0.5) * sin(0.5) + cos(0.5) * cos(0.5);
if ($root > 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();

@ -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}"

Loading…
Cancel
Save