- 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 logicmaster
parent
725959da9f
commit
515132b6b8
16 changed files with 1733 additions and 123 deletions
@ -1 +1,3 @@ |
||||
/build/ |
||||
*.o |
||||
/thirdparty |
||||
@ -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(); |
||||
} |
||||
@ -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; |
||||
} |
||||
@ -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 {} |
||||
Loading…
Reference in new issue