Files
mimiclaw/main/tools/tool_files.c

262 lines
8.2 KiB
C

#include "tools/tool_files.h"
#include "mimi_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <dirent.h>
#include <sys/stat.h>
#include "esp_log.h"
#include "cJSON.h"
#include <stdbool.h>
static const char *TAG = "tool_files";
#define MAX_FILE_SIZE (32 * 1024)
/**
* Validate that a path starts with /spiffs/ and contains no ".." traversal.
*/
static bool validate_path(const char *path)
{
if (!path) return false;
if (strncmp(path, "/spiffs/", 8) != 0) return false;
if (strstr(path, "..") != NULL) return false;
return true;
}
/* ── read_file ─────────────────────────────────────────────── */
esp_err_t tool_read_file_execute(const char *input_json, char *output, size_t output_size)
{
cJSON *root = cJSON_Parse(input_json);
if (!root) {
snprintf(output, output_size, "Error: invalid JSON input");
return ESP_ERR_INVALID_ARG;
}
const char *path = cJSON_GetStringValue(cJSON_GetObjectItem(root, "path"));
if (!validate_path(path)) {
snprintf(output, output_size, "Error: path must start with /spiffs/ and must not contain '..'");
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
FILE *f = fopen(path, "r");
if (!f) {
snprintf(output, output_size, "Error: file not found: %s", path);
cJSON_Delete(root);
return ESP_ERR_NOT_FOUND;
}
size_t max_read = output_size - 1;
if (max_read > MAX_FILE_SIZE) max_read = MAX_FILE_SIZE;
size_t n = fread(output, 1, max_read, f);
output[n] = '\0';
fclose(f);
ESP_LOGI(TAG, "read_file: %s (%d bytes)", path, (int)n);
cJSON_Delete(root);
return ESP_OK;
}
/* ── write_file ────────────────────────────────────────────── */
esp_err_t tool_write_file_execute(const char *input_json, char *output, size_t output_size)
{
cJSON *root = cJSON_Parse(input_json);
if (!root) {
snprintf(output, output_size, "Error: invalid JSON input");
return ESP_ERR_INVALID_ARG;
}
const char *path = cJSON_GetStringValue(cJSON_GetObjectItem(root, "path"));
const char *content = cJSON_GetStringValue(cJSON_GetObjectItem(root, "content"));
if (!validate_path(path)) {
snprintf(output, output_size, "Error: path must start with /spiffs/ and must not contain '..'");
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
if (!content) {
snprintf(output, output_size, "Error: missing 'content' field");
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
FILE *f = fopen(path, "w");
if (!f) {
snprintf(output, output_size, "Error: cannot open file for writing: %s", path);
cJSON_Delete(root);
return ESP_FAIL;
}
size_t len = strlen(content);
size_t written = fwrite(content, 1, len, f);
fclose(f);
if (written != len) {
snprintf(output, output_size, "Error: wrote %d of %d bytes to %s", (int)written, (int)len, path);
cJSON_Delete(root);
return ESP_FAIL;
}
snprintf(output, output_size, "OK: wrote %d bytes to %s", (int)written, path);
ESP_LOGI(TAG, "write_file: %s (%d bytes)", path, (int)written);
cJSON_Delete(root);
return ESP_OK;
}
/* ── edit_file ─────────────────────────────────────────────── */
esp_err_t tool_edit_file_execute(const char *input_json, char *output, size_t output_size)
{
cJSON *root = cJSON_Parse(input_json);
if (!root) {
snprintf(output, output_size, "Error: invalid JSON input");
return ESP_ERR_INVALID_ARG;
}
const char *path = cJSON_GetStringValue(cJSON_GetObjectItem(root, "path"));
const char *old_str = cJSON_GetStringValue(cJSON_GetObjectItem(root, "old_string"));
const char *new_str = cJSON_GetStringValue(cJSON_GetObjectItem(root, "new_string"));
if (!validate_path(path)) {
snprintf(output, output_size, "Error: path must start with /spiffs/ and must not contain '..'");
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
if (!old_str || !new_str) {
snprintf(output, output_size, "Error: missing 'old_string' or 'new_string' field");
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
/* Read existing file */
FILE *f = fopen(path, "r");
if (!f) {
snprintf(output, output_size, "Error: file not found: %s", path);
cJSON_Delete(root);
return ESP_ERR_NOT_FOUND;
}
fseek(f, 0, SEEK_END);
long file_size = ftell(f);
fseek(f, 0, SEEK_SET);
if (file_size <= 0 || file_size > MAX_FILE_SIZE) {
snprintf(output, output_size, "Error: file too large or empty (%ld bytes)", file_size);
fclose(f);
cJSON_Delete(root);
return ESP_ERR_INVALID_SIZE;
}
/* Allocate buffer for the result (old content + possible expansion) */
size_t old_len = strlen(old_str);
size_t new_len = strlen(new_str);
size_t max_result = file_size + (new_len > old_len ? new_len - old_len : 0) + 1;
char *buf = malloc(file_size + 1);
char *result = malloc(max_result);
if (!buf || !result) {
free(buf);
free(result);
fclose(f);
snprintf(output, output_size, "Error: out of memory");
cJSON_Delete(root);
return ESP_ERR_NO_MEM;
}
size_t n = fread(buf, 1, file_size, f);
buf[n] = '\0';
fclose(f);
/* Find and replace first occurrence */
char *pos = strstr(buf, old_str);
if (!pos) {
snprintf(output, output_size, "Error: old_string not found in %s", path);
free(buf);
free(result);
cJSON_Delete(root);
return ESP_ERR_NOT_FOUND;
}
size_t prefix_len = pos - buf;
memcpy(result, buf, prefix_len);
memcpy(result + prefix_len, new_str, new_len);
size_t suffix_start = prefix_len + old_len;
size_t suffix_len = n - suffix_start;
memcpy(result + prefix_len + new_len, buf + suffix_start, suffix_len);
size_t total = prefix_len + new_len + suffix_len;
result[total] = '\0';
free(buf);
/* Write back */
f = fopen(path, "w");
if (!f) {
snprintf(output, output_size, "Error: cannot open file for writing: %s", path);
free(result);
cJSON_Delete(root);
return ESP_FAIL;
}
fwrite(result, 1, total, f);
fclose(f);
free(result);
snprintf(output, output_size, "OK: edited %s (replaced %d bytes with %d bytes)", path, (int)old_len, (int)new_len);
ESP_LOGI(TAG, "edit_file: %s", path);
cJSON_Delete(root);
return ESP_OK;
}
/* ── list_dir ──────────────────────────────────────────────── */
esp_err_t tool_list_dir_execute(const char *input_json, char *output, size_t output_size)
{
cJSON *root = cJSON_Parse(input_json);
const char *prefix = NULL;
if (root) {
cJSON *pfx = cJSON_GetObjectItem(root, "prefix");
if (pfx && cJSON_IsString(pfx)) {
prefix = pfx->valuestring;
}
}
DIR *dir = opendir(MIMI_SPIFFS_BASE);
if (!dir) {
snprintf(output, output_size, "Error: cannot open /spiffs directory");
cJSON_Delete(root);
return ESP_FAIL;
}
size_t off = 0;
struct dirent *ent;
int count = 0;
while ((ent = readdir(dir)) != NULL && off < output_size - 1) {
/* Build full path: SPIFFS entries are just filenames with embedded slashes */
char full_path[512];
snprintf(full_path, sizeof(full_path), "%s/%s", MIMI_SPIFFS_BASE, ent->d_name);
if (prefix && strncmp(full_path, prefix, strlen(prefix)) != 0) {
continue;
}
off += snprintf(output + off, output_size - off, "%s\n", full_path);
count++;
}
closedir(dir);
if (count == 0) {
snprintf(output, output_size, "(no files found)");
}
ESP_LOGI(TAG, "list_dir: %d files (prefix=%s)", count, prefix ? prefix : "(none)");
cJSON_Delete(root);
return ESP_OK;
}