text/plain
•
11.78 KB
•
485 lines
#include <dirent.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include "driver/sdspi_host.h"
#include "driver/spi_common.h"
#include "pinout.h"
#include "sdmmc_cmd.h"
#include "storage.h"
#include "tusb_msc_storage.h"
#include "usb_msc.h"
#define MOUNT_POINT "/sdcard"
// Decode a single UTF-8 codepoint from src, storing it in *cp.
// Returns number of bytes consumed, or 0 on invalid sequence.
static int utf8_decode(const char* src, uint32_t* cp) {
const unsigned char* s = (const unsigned char*)src;
if (s[0] < 0x80) {
*cp = s[0];
return 1;
} else if ((s[0] & 0xE0) == 0xC0) {
if ((s[1] & 0xC0) != 0x80) {
return 0;
}
*cp = ((uint32_t)(s[0] & 0x1F) << 6) | (s[1] & 0x3F);
if (*cp < 0x80) {
return 0; // overlong
}
return 2;
} else if ((s[0] & 0xF0) == 0xE0) {
if ((s[1] & 0xC0) != 0x80 || (s[2] & 0xC0) != 0x80) {
return 0;
}
*cp = ((uint32_t)(s[0] & 0x0F) << 12) | ((uint32_t)(s[1] & 0x3F) << 6) |
(s[2] & 0x3F);
if (*cp < 0x800) {
return 0; // overlong
}
return 3;
} else if ((s[0] & 0xF8) == 0xF0) {
if ((s[1] & 0xC0) != 0x80 || (s[2] & 0xC0) != 0x80 ||
(s[3] & 0xC0) != 0x80) {
return 0;
}
*cp = ((uint32_t)(s[0] & 0x07) << 18) |
((uint32_t)(s[1] & 0x3F) << 12) | ((uint32_t)(s[2] & 0x3F) << 6) |
(s[3] & 0x3F);
if (*cp < 0x10000 || *cp > 0x10FFFF) {
return 0;
}
return 4;
}
return 0;
}
// Check if a UTF-8 byte sequence contains multi-byte characters.
static bool has_multibyte_utf8(const unsigned char* buf, size_t len) {
for (size_t i = 0; i < len; i++) {
if (buf[i] >= 0x80) {
return true;
}
}
return false;
}
// Check if a byte sequence is valid UTF-8 with at least one multi-byte char.
static bool is_valid_multibyte_utf8(const unsigned char* buf, size_t len) {
if (!has_multibyte_utf8(buf, len)) {
return false;
}
size_t i = 0;
while (i < len) {
uint32_t cp;
int n = utf8_decode((const char*)&buf[i], &cp);
if (n == 0) {
return false;
}
i += (size_t)n;
}
return true;
}
// Detect if a UTF-8 string was double-encoded via Latin-1.
// Returns true if all non-ASCII codepoints are <= U+00FF and the extracted
// low bytes form valid multi-byte UTF-8.
static bool is_double_encoded_utf8(const char* str) {
unsigned char decoded[STORAGE_MAX_NAME];
size_t dlen = 0;
const char* p = str;
while (*p) {
uint32_t cp;
int n = utf8_decode(p, &cp);
if (n == 0) {
return false; // invalid UTF-8 input
}
if (cp < 0x80) {
if (dlen >= sizeof(decoded) - 1) {
return false;
}
decoded[dlen++] = (unsigned char)cp;
} else if (cp <= 0xFF) {
if (dlen >= sizeof(decoded) - 1) {
return false;
}
decoded[dlen++] = (unsigned char)cp;
} else {
// Codepoint > U+00FF — not double-encoded
return false;
}
p += n;
}
return is_valid_multibyte_utf8(decoded, dlen);
}
// Decode a double-encoded UTF-8 string in place.
// Extracts the low byte of each codepoint (all guaranteed <= U+00FF)
// which gives back the original UTF-8 bytes.
// The result is always shorter or equal length, so in-place is safe.
static void decode_double_encoded_utf8(char* str) {
unsigned char decoded[STORAGE_MAX_NAME];
size_t dlen = 0;
const char* p = str;
while (*p) {
uint32_t cp;
int n = utf8_decode(p, &cp);
if (n == 0) {
break;
}
decoded[dlen++] = (unsigned char)cp;
p += n;
}
memcpy(str, decoded, dlen);
str[dlen] = '\0';
}
static const char* TAG = "storage";
static bool mounted = false;
static sdmmc_card_t* card = NULL;
static void msc_mount_changed_cb(tinyusb_msc_event_t* event) {
mounted = event->mount_changed_data.is_mounted;
printf("%s: MSC mount changed: %s\n", TAG,
mounted ? "mounted" : "unmounted");
}
bool storage_init(void) {
esp_err_t ret;
// SPI bus configuration
spi_bus_config_t bus_cfg = {
.mosi_io_num = SD_PIN_MOSI,
.miso_io_num = SD_PIN_MISO,
.sclk_io_num = SD_PIN_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4096,
};
// Initialize SPI bus (SPI3_HOST to avoid conflict with display on SPI2)
ret = spi_bus_initialize(SPI3_HOST, &bus_cfg, SDSPI_DEFAULT_DMA);
if (ret != ESP_OK) {
printf("%s: Failed to initialize SPI bus: %s\n", TAG,
esp_err_to_name(ret));
return false;
}
// Initialize the SPI SD slot
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = SD_PIN_CS;
slot_config.host_id = SPI3_HOST;
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
host.slot = SPI3_HOST;
// Initialize SD card at the lower level
sdspi_dev_handle_t sdspi_handle;
ret = sdspi_host_init();
if (ret != ESP_OK) {
printf("%s: Failed to init sdspi host: %s\n", TAG,
esp_err_to_name(ret));
spi_bus_free(SPI3_HOST);
return false;
}
ret = sdspi_host_init_device(&slot_config, &sdspi_handle);
if (ret != ESP_OK) {
printf("%s: Failed to init sdspi device: %s\n", TAG,
esp_err_to_name(ret));
sdspi_host_deinit();
spi_bus_free(SPI3_HOST);
return false;
}
// Update host slot to the device handle (must be done before card init)
host.slot = sdspi_handle;
// Allocate and initialize the card
card = calloc(1, sizeof(sdmmc_card_t));
if (!card) {
printf("%s: Failed to allocate card\n", TAG);
sdspi_host_deinit();
spi_bus_free(SPI3_HOST);
return false;
}
ret = sdmmc_card_init(&host, card);
if (ret != ESP_OK) {
printf("%s: Failed to initialize SD card: %s\n", TAG,
esp_err_to_name(ret));
free(card);
card = NULL;
sdspi_host_deinit();
spi_bus_free(SPI3_HOST);
return false;
}
sdmmc_card_print_info(stdout, card);
// Initialize TinyUSB MSC with this card
const tinyusb_msc_sdmmc_config_t msc_config = {
.card = card,
.callback_mount_changed = msc_mount_changed_cb,
.mount_config =
{
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 16 * 1024,
},
};
ret = tinyusb_msc_storage_init_sdmmc(&msc_config);
if (ret != ESP_OK) {
printf("%s: Failed to init MSC storage: %s\n", TAG,
esp_err_to_name(ret));
free(card);
card = NULL;
sdspi_host_deinit();
spi_bus_free(SPI3_HOST);
return false;
}
// Mount via TinyUSB MSC storage (handles VFS+FATFS registration)
ret = tinyusb_msc_storage_mount(MOUNT_POINT);
if (ret != ESP_OK) {
printf("%s: Failed to mount filesystem: %s\n", TAG,
esp_err_to_name(ret));
tinyusb_msc_storage_deinit();
free(card);
card = NULL;
sdspi_host_deinit();
spi_bus_free(SPI3_HOST);
return false;
}
// Initialize TinyUSB driver for USB MSC
usb_msc_init();
mounted = true;
printf("%s: SD card mounted at %s\n", TAG, MOUNT_POINT);
return true;
}
void storage_unmount(void) {
if (!mounted) {
return;
}
tinyusb_msc_storage_unmount();
mounted = false;
}
bool storage_remount(void) {
if (mounted) {
return true;
}
esp_err_t ret = tinyusb_msc_storage_mount(MOUNT_POINT);
if (ret != ESP_OK) {
printf("%s: Failed to remount: %s\n", TAG, esp_err_to_name(ret));
return false;
}
mounted = true;
return true;
}
bool storage_mkdir(const char* path) {
if (!mounted || !path) {
return false;
}
char full_path[STORAGE_MAX_PATH];
snprintf(full_path, sizeof(full_path), "%s/%s", MOUNT_POINT, path);
struct stat st;
if (stat(full_path, &st) == 0) {
return S_ISDIR(st.st_mode);
}
return mkdir(full_path, 0755) == 0;
}
bool storage_remove(const char* path) {
if (!mounted || !path) {
return false;
}
const char* rel = (path[0] == '/') ? path + 1 : path;
char full_path[STORAGE_MAX_PATH];
snprintf(full_path, sizeof(full_path), "%s/%s", MOUNT_POINT, rel);
if (remove(full_path) != 0) {
struct stat st;
if (stat(full_path, &st) == 0) {
printf("%s: Failed to remove %s\n", TAG, full_path);
return false;
}
}
return true;
}
bool storage_is_mounted(void) {
return mounted;
}
// Scan a directory for double-encoded UTF-8 filenames and rename them.
// Returns true if any renames were performed.
static bool fix_double_encoded_filenames(const char* full_path) {
DIR* dir = opendir(full_path);
if (!dir) {
return false;
}
// Collect names that need fixing (rename while dir is open causes dupes)
char old_names[32][STORAGE_MAX_NAME];
char new_names[32][STORAGE_MAX_NAME];
int fix_count = 0;
struct dirent* ent;
while ((ent = readdir(dir)) != NULL && fix_count < 32) {
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
}
if (is_double_encoded_utf8(ent->d_name)) {
strncpy(old_names[fix_count], ent->d_name, STORAGE_MAX_NAME - 1);
old_names[fix_count][STORAGE_MAX_NAME - 1] = '\0';
strncpy(new_names[fix_count], ent->d_name, STORAGE_MAX_NAME - 1);
new_names[fix_count][STORAGE_MAX_NAME - 1] = '\0';
decode_double_encoded_utf8(new_names[fix_count]);
fix_count++;
}
}
closedir(dir);
if (fix_count == 0) {
return false;
}
// Perform renames with directory closed
bool any_renamed = false;
for (int i = 0; i < fix_count; i++) {
char old_path[STORAGE_MAX_PATH * 2];
char new_path[STORAGE_MAX_PATH * 2];
snprintf(old_path, sizeof(old_path), "%s/%s", full_path, old_names[i]);
snprintf(new_path, sizeof(new_path), "%s/%s", full_path, new_names[i]);
// Skip if target already exists
struct stat tmp;
if (stat(new_path, &tmp) == 0) {
continue;
}
if (rename(old_path, new_path) == 0) {
printf("%s: Fixed double-encoded filename: %s -> %s\n", TAG,
old_names[i], new_names[i]);
any_renamed = true;
}
}
return any_renamed;
}
int storage_list_dir(const char* path, storage_entry_t* entries, int max) {
static bool fixing_in_progress = false;
if (!mounted || !entries || max <= 0) {
return -1;
}
char full_path[STORAGE_MAX_PATH];
if (path == NULL || path[0] == '\0' || strcmp(path, "/") == 0) {
snprintf(full_path, sizeof(full_path), "%s", MOUNT_POINT);
} else {
snprintf(full_path, sizeof(full_path), "%s/%s", MOUNT_POINT, path);
}
// Fix double-encoded filenames before listing (guard against recursion)
if (!fixing_in_progress) {
fixing_in_progress = true;
if (fix_double_encoded_filenames(full_path)) {
// Renames happened — re-list with clean directory state
int result = storage_list_dir(path, entries, max);
fixing_in_progress = false;
return result;
}
fixing_in_progress = false;
}
DIR* dir = opendir(full_path);
if (!dir) {
printf("%s: Failed to open directory %s\n", TAG, full_path);
return -1;
}
int count = 0;
struct dirent* ent;
while ((ent = readdir(dir)) != NULL && count < max) {
// Skip . and ..
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
}
strncpy(entries[count].name, ent->d_name, STORAGE_MAX_NAME - 1);
entries[count].name[STORAGE_MAX_NAME - 1] = '\0';
// Get file info
char file_path[STORAGE_MAX_PATH * 2];
snprintf(file_path, sizeof(file_path), "%s/%s", full_path, ent->d_name);
struct stat st;
if (stat(file_path, &st) == 0) {
entries[count].type =
S_ISDIR(st.st_mode) ? STORAGE_TYPE_DIR : STORAGE_TYPE_FILE;
entries[count].size = (size_t)st.st_size;
} else {
entries[count].type = STORAGE_TYPE_FILE;
entries[count].size = 0;
}
count++;
}
closedir(dir);
return count;
}
storage_file_t storage_open(const char* path, const char* mode) {
if (!mounted || !path || !mode) {
return NULL;
}
char full_path[STORAGE_MAX_PATH];
snprintf(full_path, sizeof(full_path), "%s/%s", MOUNT_POINT, path);
FILE* f = fopen(full_path, mode);
if (!f) {
printf("%s: Failed to open %s\n", TAG, full_path);
}
return (storage_file_t)f;
}
size_t storage_read(storage_file_t file, void* buf, size_t size) {
if (!file || !buf) {
return 0;
}
return fread(buf, 1, size, (FILE*)file);
}
size_t storage_write(storage_file_t file, const void* buf, size_t size) {
if (!file || !buf) {
return 0;
}
return fwrite(buf, 1, size, (FILE*)file);
}
int storage_close(storage_file_t file) {
if (!file) {
return -1;
}
return fclose((FILE*)file);
}