/* vfs.c - An embedded CNC Controller with rs274/ngc (g-code) support Virtual File System handler Part of grblHAL Copyright (c) 2022-2026 Terje Io grblHAL is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. grblHAL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with grblHAL. If not, see . */ #include #include #include "hal.h" #include "vfs.h" #ifndef VFS_CWD_LENGTH #define VFS_CWD_LENGTH 100 #endif #ifdef ARDUINO_SAM_DUE #undef feof #endif static vfs_mount_t *get_rootfs (void); static inline vfs_mount_t *path_is_mount_dir (const char *path) { size_t plen = strlen(path); vfs_mount_t *mount = get_rootfs(); if(*path == '/' && plen == 1) return mount; size_t mlen; if((mount = mount->next)) do { mlen = strlen(mount->path) - 1; if(mlen == plen && !strncmp(mount->path, path, mlen)) break; } while((mount = mount->next)); return mount; } // NULL file system static vfs_file_t *fs_open (const char *filename, const char *mode) { return NULL; } static void fs_close (vfs_file_t *file) { } static size_t fs_read (void *buffer, size_t size, size_t count, vfs_file_t *file) { return 0; } static size_t fs_write (const void *buffer, size_t size, size_t count, vfs_file_t *file) { return 0; } static size_t fs_tell (vfs_file_t *file) { return 0; } static int fs_seek (vfs_file_t *file, size_t offset) { return -1; } static bool fs_eof (vfs_file_t *file) { return true; } static int fs_unlink (const char *filename) { return -1; } static int fs_dirop (const char *path) { return -1; } static vfs_dir_t *fs_opendir (const char *path) { static vfs_dir_t *dir = NULL; if(dir == NULL) dir = calloc(sizeof(vfs_dir_t) + 3, 1); if(dir) { vfs_mount_t **mount = (vfs_mount_t **)&dir->handle; *mount = get_rootfs()->next; } return !strcmp(path, "/") ? dir : NULL; } static char *fs_readdir (vfs_dir_t *dir, vfs_dirent_t *dirent) { vfs_mount_t **mount = (vfs_mount_t **)&dir->handle; vfs_errno = 0; if(*mount) { strcpy(dirent->name, (*mount)->path); *(strchr(dirent->name, '\0') - 1) = '\0'; dirent->st_mode = (*mount)->mode; dirent->st_mode.directory = true; while((*mount = (*mount)->next)) { if(!(*mount)->mode.hidden) break; } } else *dirent->name = '\0'; return *dirent->name ? dirent->name : NULL; } static void fs_closedir (vfs_dir_t *dir) { } static int fs_stat (const char *filename, vfs_stat_t *st) { char path[64]; vfs_mount_t *mount; if((mount = path_is_mount_dir(strcat(strcpy(path, "/"), filename)))) { if(!(mount->vfs->fstat && (vfs_errno = mount->vfs->fstat("/", st) == 0))) { vfs_errno = 0; st->st_size = 1024; st->st_mode = mount->mode; st->st_mode.directory = true; #if ESP_PLATFORM st->st_mtim = mount->st_mtim; #else st->st_mtime = mount->st_mtime; #endif } } else vfs_errno = -1; return vfs_errno; } static int fs_chdir (const char *path) { return !strcmp(path, "/") ? 0 : -1; } static char *fs_getcwd (char *buf, size_t size) { return "/"; } static const vfs_t fs_null = { .fopen = fs_open, .fclose = fs_close, .fread = fs_read, .fwrite = fs_write, .ftell = fs_tell, .fseek = fs_seek, .feof = fs_eof, .funlink = fs_unlink, .fmkdir = fs_dirop, .fchdir = fs_chdir, .frmdir = fs_dirop, .fopendir = fs_opendir, .readdir = fs_readdir, .fclosedir = fs_closedir, .fstat = fs_stat, .fgetcwd = fs_getcwd }; // End NULL file system static vfs_mount_t root = { .path = "/", .vfs = &fs_null, .mode = { .directory = true, .read_only = true, .hidden = true }, .next = NULL }; static vfs_mount_t *cwdmount = &root; static char cwd[VFS_CWD_LENGTH] = "/"; volatile int vfs_errno = 0; vfs_events_t vfs = {0}; static vfs_mount_t *get_rootfs (void) { return &root; } // Strip trailing directory separator, FatFS dont't like it (WinSCP adds it) char *vfs_fixpath (char *path) { char *s = path + strlen(path) - 1; if(s > path && *s == '/' && s != strchr(path, '/')) *s = '\0'; return path; } static vfs_mount_t *get_mount (const char *path) { vfs_errno = 0; if(*path != '/') return cwdmount; vfs_mount_t *mount; if((mount = path_is_mount_dir(path)) == NULL) { mount = root.next; if(mount) do { if(!strncmp(mount->path, path, strlen(mount->path))) break; } while((mount = mount->next)); if(mount == NULL) mount = &root; } return mount; } static const char *get_filename (vfs_mount_t *mount, const char *filename) { if(*filename == '/') { size_t len = strlen(mount->path); return filename + (len == 1 ? 0 : len - 1); } else return filename; } vfs_file_t *vfs_open (const char *filename, const char *mode) { vfs_file_t *file = NULL; vfs_mount_t *mount = get_mount(filename); if(mount && (file = mount->vfs->fopen(get_filename(mount, filename), mode))) { file->fs = mount->vfs; file->status.update = !mount->mode.hidden && !!strchr(mode, 'w'); } return file; } void vfs_close (vfs_file_t *file) { vfs_errno = 0; ((vfs_t *)(file->fs))->fclose(file); if(file->status.update && vfs.on_fs_changed) vfs.on_fs_changed((vfs_t *)file->fs); } size_t vfs_read (void *buffer, size_t size, size_t count, vfs_file_t *file) { vfs_errno = 0; return ((vfs_t *)(file->fs))->fread(buffer, size, count, file); } size_t vfs_write (const void *buffer, size_t size, size_t count, vfs_file_t *file) { vfs_errno = 0; return ((vfs_t *)(file->fs))->fwrite(buffer, size, count, file); } int vfs_puts (const char *s, vfs_file_t *file) { size_t count = strlen(s), ret; vfs_errno = 0; if((ret = ((vfs_t *)(file->fs))->fwrite(s, sizeof(char), count, file)) != count) return -1; return (int)ret; } size_t vfs_tell (vfs_file_t *file) { vfs_errno = 0; return ((vfs_t *)(file->fs))->ftell(file); } int vfs_seek (vfs_file_t *file, size_t offset) { vfs_errno = 0; return ((vfs_t *)(file->fs))->fseek(file, offset); } bool vfs_eof (vfs_file_t *file) { vfs_errno = 0; return ((vfs_t *)(file->fs))->feof(file); } int vfs_rename (const char *from, const char *to) { int ret; vfs_mount_t *mount_from = get_mount(from), *mount_to = get_mount(to); if((ret = mount_from == mount_to ? mount_from->vfs->frename(get_filename(mount_from, from), get_filename(mount_to, to)) : -1) != -1 && vfs.on_fs_changed && !mount_from->mode.hidden) { vfs.on_fs_changed(mount_from->vfs); if(mount_from != mount_to && !mount_to->mode.hidden) vfs.on_fs_changed(mount_to->vfs); } return ret; } int vfs_unlink (const char *filename) { int ret; vfs_mount_t *mount = get_mount(filename); // TODO: test for dir? if((ret = mount ? mount->vfs->funlink(get_filename(mount, filename)) : -1) != -1 && vfs.on_fs_changed && !mount->mode.hidden) vfs.on_fs_changed(mount->vfs); return ret; } int vfs_mkdir (const char *path) { int ret; vfs_mount_t *mount = get_mount(path); if((ret = mount ? mount->vfs->fmkdir(get_filename(mount, path)) : -1) != -1 && vfs.on_fs_changed && !mount->mode.hidden) vfs.on_fs_changed(mount->vfs); return ret; } int vfs_rmdir (const char *path) { int ret; vfs_mount_t *mount = get_mount(path); if((ret = mount ? mount->vfs->frmdir(get_filename(mount, path)) : -1) != -1 && vfs.on_fs_changed && !mount->mode.hidden) vfs.on_fs_changed(mount->vfs); return ret; } int vfs_chdir (const char *path) { int ret; char *p; vfs_errno = 0; if(!strcmp("..", path) && strcmp("/", (path = cwd)) && (p = strrchr(cwd, '/'))) *(p + (p == cwd ? 1 : 0)) = '\0'; if(*path != '/' && strcmp(cwd, "/")) { if(strcmp(path, "..")) { if(strlen(cwd) > 1) strcat(cwd, "/"); strcat(cwd, path); } else { char *s = strrchr(cwd, '/'); if(s) *s = '\0'; } } else { if(*path == '/') strcpy(cwd, path); else strcat(strcpy(cwd, "/"), path); vfs_fixpath(cwd); if((cwdmount = get_mount(cwd)) && strchr(cwd + 1, '/') == NULL && cwdmount != &root) { strcpy(cwd, cwdmount->path); vfs_fixpath(cwd); return 0; } } if((ret = cwdmount ? cwdmount->vfs->fchdir(path) : -1) != 0) // + strlen(mount->path));)) vfs_fixpath(cwd); return ret; } vfs_dir_t *vfs_opendir (const char *path) { vfs_dir_t *dir = NULL; vfs_mount_t *mount = get_mount(path), *add_mount; vfs_mount_ll_entry_t *ml = NULL, *mln; if(mount && (dir = mount->vfs->fopendir(get_filename(mount, path)))) { dir->fs = mount->vfs; dir->mounts = NULL; if((add_mount = root.next)) do { if(add_mount != mount && !strncmp(add_mount->path, path, strlen(path))) { if(!add_mount->mode.hidden && (mln = malloc(sizeof(vfs_mount_ll_entry_t)))) { mln->mount = add_mount; mln->next = NULL; if(dir->mounts == NULL) dir->mounts = ml = mln; else { ml->next = mln; ml = mln; } } } } while((add_mount = add_mount->next)); } return dir; } vfs_dirent_t *vfs_readdir (vfs_dir_t *dir) { static vfs_dirent_t dirent; vfs_errno = 0; ((vfs_t *)dir->fs)->readdir(dir, &dirent); if(*dirent.name == '\0' && dir->mounts) { char *s; vfs_mount_ll_entry_t *ml = dir->mounts; strcpy(dirent.name, ml->mount->path + 1); if((s = strrchr(dirent.name, '/'))) *s = '\0'; dirent.st_mode = ml->mount->mode; dirent.st_mode.directory = true; dir->mounts = dir->mounts->next; free(ml); } return *dirent.name == '\0' ? NULL : &dirent; } void vfs_closedir (vfs_dir_t *dir) { vfs_errno = 0; while(dir->mounts) { vfs_mount_ll_entry_t *ml = dir->mounts; dir->mounts = dir->mounts->next; free(ml); } ((vfs_t *)dir->fs)->fclosedir(dir); } char *vfs_getcwd (char *buf, size_t len) { char *cwds = cwdmount->vfs->fgetcwd ? cwdmount->vfs->fgetcwd(NULL, len) : cwd; vfs_errno = 0; if(buf == NULL) buf = (char *)malloc(strlen(cwds) + 1); if(buf) strcpy(buf, cwds); return buf ? buf : cwds; } int vfs_chmod (const char *filename, vfs_st_mode_t attr, vfs_st_mode_t mask) { vfs_mount_t *mount = get_mount(filename); return mount && mount->vfs->fchmod ? mount->vfs->fchmod(get_filename(mount, filename), attr, mask) : -1; } int vfs_stat (const char *filename, vfs_stat_t *st) { char tmp[VFS_CWD_LENGTH], *p; if(!strcmp("..", filename)) { strcpy(tmp, cwd); if((p = strrchr(tmp, '/'))) *(p + (p == tmp ? 1 : 0)) = '\0'; filename = tmp; } vfs_mount_t *mount = get_mount(filename); int ret = mount ? mount->vfs->fstat(get_filename(mount, filename), st) : -1; if(ret == -1 && (!strcmp("/", filename) || (strchr(filename, '/') == NULL && !strcmp("/", cwd)))) { strcat(cwd, filename); mount = get_mount(cwd); cwd[1] = '\0'; if(mount) { st->st_size = 0; st->st_mode.mode = 0; st->st_mode.directory = true; #if defined(ESP_PLATFORM) st->st_mtim = mount->st_mtim; #else st->st_mtime = mount->st_mtime; #endif ret = 0; } } return ret; } int vfs_utime (const char *filename, struct tm *modified) { vfs_mount_t *mount = get_mount(filename); return mount && mount->vfs->futime ? mount->vfs->futime(get_filename(mount, filename), modified) : -1; } vfs_free_t *vfs_fgetfree (const char *path) { static vfs_free_t free; vfs_mount_t *mount = get_mount(path); if(mount && mount->vfs->fgetfree && mount->vfs->fgetfree(&free)) return &free; return NULL; } static bool vfs_get_time (struct tm *time) { memset(time, 0, sizeof(struct tm)); // 2025-01-01:00:00:00 time->tm_year = 2025 - 1900; time->tm_mday = 1; return true; } bool vfs_mount (const char *path, const vfs_t *fs, vfs_st_mode_t mode) { vfs_mount_t *mount; if(!strcmp(path, "/")) { root.vfs = fs; root.mode = mode; } else if((mount = (vfs_mount_t *)calloc(sizeof(vfs_mount_t), 1))) { struct tm tm; strcpy(mount->path, path); if(mount->path[strlen(path) - 1] != '/') strcat(mount->path, "/"); mount->vfs = fs; mount->mode = mode; mount->next = NULL; if(hal.rtc.get_datetime && hal.rtc.get_datetime(&tm)) { #ifdef ESP_PLATFORM mount->st_mtim = mktime(&tm); #else mount->st_mtime = mktime(&tm); #endif } vfs_mount_t *lmount = &root; while(lmount->next) lmount = lmount->next; lmount->next = mount; } if(hal.rtc.get_datetime == NULL) hal.rtc.get_datetime = vfs_get_time; if(fs && vfs.on_mount) vfs.on_mount(path, fs, mode); return fs != NULL; } bool vfs_unmount (const char *path) { // TODO: close open files? if(!strcmp(path, "/")) { root.vfs = &fs_null; root.mode = (vfs_st_mode_t){ .directory = true, .read_only = true, .hidden = true }; } else { vfs_mount_t *mount = get_mount(path); if(mount) { vfs_mount_t *pmount = &root; while(pmount->next && pmount->next != mount) pmount = pmount->next; if(pmount->next == mount) pmount->next = mount->next; free(mount); } } if(vfs.on_unmount) vfs.on_unmount(path); return true; } vfs_drive_t *vfs_get_drive (const char *path) { static vfs_drive_t drive; vfs_mount_t *mount = get_mount(path); drive.name = mount->vfs->fs_name; drive.path = (const char *)mount->path; drive.mode = mount->mode; drive.removable = mount->vfs->removable; drive.fs = mount->vfs; return &drive; } vfs_drives_t *vfs_drives_open (void) { vfs_drives_t *handle; vfs_mount_t *mount = &root; if((handle = malloc(sizeof(vfs_drives_t)))) { handle->mount = NULL; do { if(mount->mode.hidden) mount = mount->next; else handle->mount = mount; } while(mount && handle->mount == NULL); if(handle->mount == NULL) { free(handle); handle = NULL; } } return handle; } vfs_drive_t *vfs_drives_read (vfs_drives_t *handle, bool add_hidden) { static vfs_drive_t drive; bool ok; if((ok = handle->mount != NULL)) { drive.name = handle->mount->vfs->fs_name; drive.path = (const char *)handle->mount->path; drive.mode = handle->mount->mode; drive.removable = handle->mount->vfs->removable; drive.fs = handle->mount->vfs; handle->mount = handle->mount->next; if(handle->mount) do { if((!handle->mount->mode.hidden || add_hidden) && handle->mount->vfs->fs_name) break; } while((handle->mount = handle->mount->next)); } return ok ? &drive : NULL; } void vfs_drives_close (vfs_drives_t *handle) { free(handle); } vfs_free_t *vfs_drive_getfree (vfs_drive_t *drive) { static vfs_free_t free; const vfs_t *fs = drive->fs; return fs->fgetfree && fs->fgetfree(&free) ? &free : NULL; } int vfs_drive_format (vfs_drive_t *drive) { const vfs_t *fs = drive->fs; return (vfs_errno = fs->format ? fs->format() : -1); }