mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
app-module - Apps now have support for: - environment variables (inherit from parent app) - current workdir - window size (when running in terminal) - Add type aliases like `AppId` and `PackageId` - Remove data duplication between ledger and app instance records: create `AppManifestSet` - `AppManifest` usage safety improved: pass by value where needed, or use `AppId` instead of a manifest. Terminal and shell - Moved shell commands into coreutils-module as proper apps. - Refine stack size for terminal and shell because apps now run with their own task and stack (including in-memory apps) Platform - Fix for listing root dir on ESP32: wrap posix directory functions and wire in file systems. - Moved all platform-specific file system init code to platform subprojects Other - Fix for screenshot capture (issue with paths) - Fixes for coreutils app correctness - Memory optimizations (use more PSRAM where possible) in various locations such as app-module, shell and terminal. - `ls` and `help` commands now use terminal width and show columns of data by default - Created fs helpers at `TactilityKernel/include/tactility/filesystem/fs.h` (partially ported from `ShellFs`)
433 lines
14 KiB
C++
433 lines
14 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
#include <tactility/filesystem/fs.h>
|
|
|
|
#include <tactility/log.h>
|
|
#include <tactility/paths.h>
|
|
|
|
#include <dirent.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include <cerrno>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
constexpr auto* TAG = "fs";
|
|
|
|
// Bound on how deep directory_remove_tree()/path_tree_size() will recurse: a fixed-size buffer is
|
|
// spent per level, so a pathological tree is refused rather than overflowing the stack.
|
|
constexpr int MAX_RECURSION_DEPTH = 16;
|
|
|
|
// Entries collected per directory pass, and the longest filename handled, for
|
|
// directory_remove_tree()/path_tree_size(). dirent allows 255-byte names; storing them at full
|
|
// width would cost 16KB per recursion level, so the cap is lower and over-long names are skipped
|
|
// rather than silently mishandled.
|
|
constexpr int MAX_ENTRIES_PER_PASS = 64;
|
|
constexpr size_t MAX_ENTRY_NAME = 64;
|
|
|
|
namespace {
|
|
|
|
// Checked before creating, not after: on some mounts (e.g. the SD card's FATFS mount root),
|
|
// mkdir() on an already-existing directory doesn't fail with EEXIST, so an mkdir-first/
|
|
// errno-check approach never recognizes it as already there (see also file::
|
|
// findOrCreateDirectoryInternal() in Tactility/Source/file/File.cpp, which works around the same
|
|
// quirk the same way).
|
|
bool make_directory(const char* path) {
|
|
struct stat info;
|
|
if (stat(path, &info) == 0) {
|
|
return S_ISDIR(info.st_mode);
|
|
}
|
|
if (mkdir(path, 0755) == 0) {
|
|
return true;
|
|
}
|
|
return errno == EEXIST;
|
|
}
|
|
|
|
error_t directory_remove_tree_at(const char* path, int depth) {
|
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
// Names are capped at MAX_ENTRY_NAME rather than dirent's full 256 bytes, which would make this
|
|
// a 16KB allocation per recursion level. Anything longer is skipped rather than stored
|
|
// truncated: a truncated name resolves to a *different* path, and deleting the wrong file is a
|
|
// far worse outcome than refusing to delete an unusually named one.
|
|
struct Collected {
|
|
char names[MAX_ENTRIES_PER_PASS][MAX_ENTRY_NAME];
|
|
int count;
|
|
bool overflowed;
|
|
bool skipped_long_name;
|
|
};
|
|
|
|
// Looped rather than re-calling itself at the same depth: MAX_RECURSION_DEPTH only bounds
|
|
// recursion into subdirectories below, so a same-depth call here would grow the stack by one
|
|
// frame per MAX_ENTRIES_PER_PASS entries, unbounded by anything - a directory with a few
|
|
// thousand files could overflow it.
|
|
for (;;) {
|
|
auto* collected = static_cast<Collected*>(calloc(1, sizeof(Collected)));
|
|
if (collected == nullptr) {
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
{
|
|
DIR* dir = opendir(path);
|
|
if (dir == nullptr) {
|
|
free(collected);
|
|
// Not a directory: fall through to removing it as a plain file.
|
|
return file_remove(path);
|
|
}
|
|
|
|
for (struct dirent* entry = readdir(dir); entry != nullptr; entry = readdir(dir)) {
|
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
|
continue;
|
|
}
|
|
if (collected->count >= MAX_ENTRIES_PER_PASS) {
|
|
collected->overflowed = true;
|
|
break;
|
|
}
|
|
|
|
const size_t name_length = strlen(entry->d_name);
|
|
if (name_length >= MAX_ENTRY_NAME) {
|
|
collected->skipped_long_name = true;
|
|
continue;
|
|
}
|
|
|
|
memcpy(collected->names[collected->count], entry->d_name, name_length + 1);
|
|
collected->count++;
|
|
}
|
|
closedir(dir);
|
|
}
|
|
|
|
error_t result = ERROR_NONE;
|
|
for (int i = 0; i < collected->count; i++) {
|
|
char child_path[FILE_MAX_PATH_STRING_LENGTH];
|
|
// Reject truncated paths
|
|
const int written = snprintf(child_path, sizeof(child_path), "%s/%s", path, collected->names[i]);
|
|
if (written < 0 || static_cast<size_t>(written) >= sizeof(child_path)) {
|
|
result = ERROR_BUFFER_OVERFLOW;
|
|
continue;
|
|
}
|
|
|
|
const error_t child_result = directory_exists(child_path)
|
|
? directory_remove_tree_at(child_path, depth + 1)
|
|
: file_remove(child_path);
|
|
|
|
if (child_result != ERROR_NONE) {
|
|
result = child_result;
|
|
}
|
|
}
|
|
|
|
const bool overflowed = collected->overflowed;
|
|
const bool skipped_long_name = collected->skipped_long_name;
|
|
free(collected);
|
|
|
|
// A directory with more entries than one pass could hold needs another sweep.
|
|
if (overflowed && result == ERROR_NONE && !skipped_long_name) {
|
|
continue;
|
|
}
|
|
if (result != ERROR_NONE) {
|
|
return result;
|
|
}
|
|
if (skipped_long_name) {
|
|
// The directory still holds entries this code declined to touch, so removing it would
|
|
// fail anyway; report the real reason rather than a bare "not empty".
|
|
LOG_W(TAG, "'%s' contains a name longer than %u bytes; not removed",
|
|
path, (unsigned)MAX_ENTRY_NAME);
|
|
return ERROR_NOT_EMPTY;
|
|
}
|
|
|
|
return directory_remove(path);
|
|
}
|
|
}
|
|
|
|
uint64_t path_tree_size_at(const char* path, int depth) {
|
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
return 0;
|
|
}
|
|
if (!directory_exists(path)) {
|
|
struct stat info;
|
|
return (stat(path, &info) == 0) ? static_cast<uint64_t>(info.st_size) : 0;
|
|
}
|
|
|
|
struct Accumulator {
|
|
uint64_t bytes;
|
|
};
|
|
Accumulator accumulator { 0 };
|
|
|
|
directory_list(path, &accumulator, [](const DirectoryEntry* entry, void* context) {
|
|
// Only files are summed here; directories are recursed into by the caller below.
|
|
if (!entry->is_directory) {
|
|
static_cast<Accumulator*>(context)->bytes += entry->size;
|
|
}
|
|
});
|
|
|
|
// Collect subdirectory names first, then recurse.
|
|
// Same name-length cap as directory_remove_tree_at: a truncated name would size a different path.
|
|
struct Children {
|
|
char names[32][MAX_ENTRY_NAME];
|
|
int count;
|
|
};
|
|
auto* children = static_cast<Children*>(calloc(1, sizeof(Children)));
|
|
if (children == nullptr) {
|
|
return accumulator.bytes;
|
|
}
|
|
|
|
directory_list(path, children, [](const DirectoryEntry* entry, void* context) {
|
|
auto* list = static_cast<Children*>(context);
|
|
if (!entry->is_directory || list->count >= static_cast<int>(sizeof(list->names) / sizeof(list->names[0]))) {
|
|
return;
|
|
}
|
|
const size_t name_length = strlen(entry->name);
|
|
if (name_length >= MAX_ENTRY_NAME) {
|
|
return;
|
|
}
|
|
memcpy(list->names[list->count], entry->name, name_length + 1);
|
|
list->count++;
|
|
});
|
|
|
|
uint64_t total = accumulator.bytes;
|
|
for (int i = 0; i < children->count; i++) {
|
|
char child_path[FILE_MAX_PATH_STRING_LENGTH];
|
|
snprintf(child_path, sizeof(child_path), "%s/%s", path, children->names[i]);
|
|
total += path_tree_size_at(child_path, depth + 1);
|
|
}
|
|
|
|
free(children);
|
|
return total;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
|
|
bool path_exists(const char* path) {
|
|
struct stat info;
|
|
return stat(path, &info) == 0;
|
|
}
|
|
|
|
bool directory_exists(const char* path) {
|
|
struct stat info;
|
|
return stat(path, &info) == 0 && S_ISDIR(info.st_mode);
|
|
}
|
|
|
|
error_t file_read_binary(const char* path, uint8_t* bytes, size_t* out_size) {
|
|
FILE* file = fopen(path, "rb");
|
|
if (file == nullptr) {
|
|
return ERROR_NOT_FOUND;
|
|
}
|
|
|
|
fseek(file, 0, SEEK_END);
|
|
const long size = ftell(file);
|
|
fseek(file, 0, SEEK_SET);
|
|
|
|
if (size < 0 || static_cast<size_t>(size) > *out_size) {
|
|
fclose(file);
|
|
return ERROR_BUFFER_OVERFLOW;
|
|
}
|
|
|
|
const size_t read = fread(bytes, 1, static_cast<size_t>(size), file);
|
|
fclose(file);
|
|
|
|
*out_size = read;
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t directory_list(const char* path, void* context, void (*callback)(const DirectoryEntry* entry, void* context)) {
|
|
DIR* dir = opendir(path);
|
|
if (dir == nullptr) {
|
|
return ERROR_NOT_FOUND;
|
|
}
|
|
|
|
for (struct dirent* entry = readdir(dir); entry != nullptr; entry = readdir(dir)) {
|
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
|
continue;
|
|
}
|
|
|
|
char child_path[FILE_MAX_PATH_STRING_LENGTH];
|
|
const int written = (strcmp(path, "/") == 0)
|
|
? snprintf(child_path, sizeof(child_path), "/%s", entry->d_name)
|
|
: snprintf(child_path, sizeof(child_path), "%s/%s", path, entry->d_name);
|
|
|
|
DirectoryEntry item { entry->d_name, false, 0 };
|
|
if (written > 0 && static_cast<size_t>(written) < sizeof(child_path)) {
|
|
struct stat info;
|
|
if (stat(child_path, &info) == 0) {
|
|
item.is_directory = S_ISDIR(info.st_mode);
|
|
item.size = static_cast<size_t>(info.st_size);
|
|
}
|
|
}
|
|
|
|
callback(&item, context);
|
|
}
|
|
|
|
closedir(dir);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t directory_make(const char* path, bool create_parents) {
|
|
if (!create_parents) {
|
|
// Checked before creating; see makeDirectory()'s own comment for why.
|
|
struct stat info;
|
|
if (stat(path, &info) == 0) {
|
|
return ERROR_ALREADY_EXISTS;
|
|
}
|
|
if (mkdir(path, 0755) != 0) {
|
|
return (errno == EEXIST) ? ERROR_ALREADY_EXISTS : ERROR_RESOURCE;
|
|
}
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
// Walk the path creating each component, temporarily truncating at every separator.
|
|
char working[FILE_MAX_PATH_STRING_LENGTH];
|
|
// Reject truncated paths
|
|
const int written = snprintf(working, sizeof(working), "%s", path);
|
|
if (written < 0 || static_cast<size_t>(written) >= sizeof(working)) {
|
|
return ERROR_BUFFER_OVERFLOW;
|
|
}
|
|
|
|
for (char* p = working + 1; *p != '\0'; p++) {
|
|
if (*p != '/') {
|
|
continue;
|
|
}
|
|
*p = '\0';
|
|
if (!make_directory(working)) {
|
|
return ERROR_RESOURCE;
|
|
}
|
|
*p = '/';
|
|
}
|
|
|
|
if (!make_directory(working)) {
|
|
return ERROR_RESOURCE;
|
|
}
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t file_remove(const char* path) {
|
|
if (unlink(path) != 0) {
|
|
return (errno == ENOENT) ? ERROR_NOT_FOUND : ERROR_RESOURCE;
|
|
}
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t directory_remove(const char* path) {
|
|
if (rmdir(path) != 0) {
|
|
if (errno == ENOENT) {
|
|
return ERROR_NOT_FOUND;
|
|
}
|
|
return (errno == ENOTEMPTY || errno == EEXIST) ? ERROR_NOT_EMPTY : ERROR_RESOURCE;
|
|
}
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t directory_remove_tree(const char* path) {
|
|
return directory_remove_tree_at(path, 0);
|
|
}
|
|
|
|
error_t file_copy(const char* source, const char* target, bool overwrite) {
|
|
// Reject a copy when the source and target are the same file.
|
|
if (strcmp(source, target) == 0) {
|
|
return ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
// Source and target are the same, but don't have the same name:
|
|
struct stat source_info, target_info;
|
|
if (
|
|
stat(source, &source_info) == 0 &&
|
|
stat(target, &target_info) == 0 &&
|
|
source_info.st_ino != 0 &&
|
|
source_info.st_dev == target_info.st_dev &&
|
|
source_info.st_ino == target_info.st_ino
|
|
) {
|
|
return ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!overwrite && path_exists(target)) {
|
|
return ERROR_ALREADY_EXISTS;
|
|
}
|
|
|
|
// Copies in chunks, carrying the file offset between them, since source and target can live
|
|
// on different mounts.
|
|
long offset = 0;
|
|
for (;;) {
|
|
char chunk[512];
|
|
size_t read = 0;
|
|
|
|
{
|
|
FILE* source_file = fopen(source, "rb");
|
|
if (source_file == nullptr) {
|
|
return ERROR_NOT_FOUND;
|
|
}
|
|
if (fseek(source_file, offset, SEEK_SET) != 0) {
|
|
fclose(source_file);
|
|
return ERROR_RESOURCE;
|
|
}
|
|
read = fread(chunk, 1, sizeof(chunk), source_file);
|
|
fclose(source_file);
|
|
}
|
|
|
|
if (read == 0) {
|
|
break;
|
|
}
|
|
|
|
{
|
|
// "wb" for the first chunk so an overwrite truncates; "ab" to extend after that.
|
|
FILE* target_file = fopen(target, (offset == 0) ? "wb" : "ab");
|
|
if (target_file == nullptr) {
|
|
return ERROR_RESOURCE;
|
|
}
|
|
const size_t written = fwrite(chunk, 1, read, target_file);
|
|
fclose(target_file);
|
|
if (written != read) {
|
|
return ERROR_RESOURCE;
|
|
}
|
|
}
|
|
|
|
offset += static_cast<long>(read);
|
|
|
|
if (read < sizeof(chunk)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// A zero-length source never entered the write branch above, so create the target explicitly.
|
|
if (offset == 0) {
|
|
return file_touch(target);
|
|
}
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t file_move(const char* source, const char* target, bool overwrite) {
|
|
if (!overwrite && path_exists(target)) {
|
|
return ERROR_ALREADY_EXISTS;
|
|
}
|
|
|
|
if (rename(source, target) == 0) {
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
// rename() only works within a filesystem; across mounts it fails with EXDEV and the move has
|
|
// to be done the long way.
|
|
const error_t copied = file_copy(source, target, overwrite);
|
|
if (copied != ERROR_NONE) {
|
|
return copied;
|
|
}
|
|
return file_remove(source);
|
|
}
|
|
|
|
error_t file_touch(const char* path) {
|
|
// "ab" creates the file when missing and leaves existing content alone.
|
|
FILE* file = fopen(path, "ab");
|
|
if (file == nullptr) {
|
|
return ERROR_RESOURCE;
|
|
}
|
|
fclose(file);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
uint64_t path_tree_size(const char* path) {
|
|
return path_tree_size_at(path, 0);
|
|
}
|
|
|
|
} // extern "C"
|