/* * OmniNX Installer - Filesystem operations with file logging * Based on HATS Installer * * All tree walkers share one path buffer per call (no malloc per entry) and * visit every entry exactly once. Moving within the SD card uses f_rename and * only falls back to copying when a rename fails. */ #include "fs.h" #include #include #include #include #include #define FS_BUFFER_SIZE 0x100000 // 1MB copy buffer, allocated once #define LOG_BUFFER_SIZE 1024 // fits two 255-char paths plus text #define LOG_SYNC_EVERY 16 // Log file handle static FIL log_file; static bool log_enabled = false; static u32 log_lines; static char log_buf[LOG_BUFFER_SIZE]; // Shared copy buffer (lazy, released with fs_release_buffers) static u8 *copy_buf; // Initialize log file void log_init(const char *path) { int res = f_open(&log_file, path, FA_WRITE | FA_CREATE_ALWAYS); if (res == FR_OK) { log_enabled = true; log_lines = 0; log_write("=== OmniNX Installer Log ===\n\n"); } } // Close log file void log_close(void) { if (log_enabled) { f_sync(&log_file); f_close(&log_file); log_enabled = false; } } // Write to log file (variadic version) void log_write(const char *fmt, ...) { if (!log_enabled) return; va_list args; va_start(args, fmt); // BDK formatter (no newlib printf). Callers keep paths <= 255 chars. u32 len = s_vprintf(log_buf, fmt, args); va_end(args); UINT bw; f_write(&log_file, log_buf, len, &bw); if (++log_lines % LOG_SYNC_EVERY == 0) f_sync(&log_file); } bool log_is_enabled(void) { return log_enabled; } void fs_release_buffers(void) { if (copy_buf) { free(copy_buf); copy_buf = NULL; } } // Convert FatFS error code to string const char *fs_error_str(int err) { switch (err) { case FR_OK: return "OK"; case FR_DISK_ERR: return "DISK_ERR: Low level disk error"; case FR_INT_ERR: return "INT_ERR: Internal error"; case FR_NOT_READY: return "NOT_READY: Drive not ready"; case FR_NO_FILE: return "NO_FILE: File not found"; case FR_NO_PATH: return "NO_PATH: Path not found"; case FR_INVALID_NAME: return "INVALID_NAME: Invalid path name"; case FR_DENIED: return "DENIED: Access denied"; case FR_EXIST: return "EXIST: Already exists"; case FR_INVALID_OBJECT: return "INVALID_OBJECT: Invalid object"; case FR_WRITE_PROTECTED: return "WRITE_PROTECTED: Write protected"; case FR_INVALID_DRIVE: return "INVALID_DRIVE: Invalid drive"; case FR_NOT_ENABLED: return "NOT_ENABLED: Volume not mounted"; case FR_NO_FILESYSTEM: return "NO_FILESYSTEM: No valid FAT"; case FR_MKFS_ABORTED: return "MKFS_ABORTED: mkfs aborted"; case FR_TIMEOUT: return "TIMEOUT: Timeout"; case FR_LOCKED: return "LOCKED: File locked"; case FR_NOT_ENOUGH_CORE: return "NOT_ENOUGH_CORE: Out of memory"; case FR_TOO_MANY_OPEN_FILES: return "TOO_MANY_OPEN_FILES"; case FR_INVALID_PARAMETER: return "INVALID_PARAMETER"; default: return "UNKNOWN_ERROR"; } } /* ---- path helpers ---------------------------------------------------- */ // Append "/name" to path (len = current length). Returns new length or 0 if too long. static size_t path_push(char *path, size_t len, const char *name) { size_t nlen = strlen(name); size_t slash = (len > 0 && path[len - 1] != '/') ? 1 : 0; if (len + slash + nlen + 1 > FS_PATH_MAX) return 0; if (slash) path[len++] = '/'; memcpy(path + len, name, nlen + 1); return len + nlen; } static size_t path_set(char *path, const char *src) { size_t len = strlen(src); if (len + 1 > FS_PATH_MAX) return 0; memcpy(path, src, len + 1); // Normalise "sd:/foo/" -> "sd:/foo" (keep "sd:/") while (len > 4 && path[len - 1] == '/') path[--len] = '\0'; return len; } static const char *path_basename(const char *path) { const char *slash = strrchr(path, '/'); return slash ? slash + 1 : path; } static bool is_dot_entry(const char *name) { return name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0')); } static void clear_readonly(const char *path, BYTE attr) { if (attr & AM_RDO) f_chmod(path, attr & ~AM_RDO, AM_RDO); } /* ---- single file ----------------------------------------------------- */ // Copy a single file with logging int file_copy(const char *src, const char *dst) { FIL fin, fout; FILINFO fno; int res; res = f_open(&fin, src, FA_READ | FA_OPEN_EXISTING); if (res != FR_OK) { log_write("COPY %s: open src %s\n", src, fs_error_str(res)); return res; } if (f_stat(src, &fno) != FR_OK) fno.fattrib = 0; u64 remaining = f_size(&fin); res = f_open(&fout, dst, FA_WRITE | FA_CREATE_ALWAYS); if (res != FR_OK) { f_close(&fin); log_write("COPY %s: open dst %s\n", dst, fs_error_str(res)); return res; } if (!copy_buf) copy_buf = malloc(FS_BUFFER_SIZE); if (!copy_buf) { f_close(&fin); f_close(&fout); log_write("COPY %s: out of memory\n", src); return FR_NOT_ENOUGH_CORE; } UINT br, bw; while (remaining > 0) { UINT to_copy = (remaining > FS_BUFFER_SIZE) ? FS_BUFFER_SIZE : (UINT)remaining; res = f_read(&fin, copy_buf, to_copy, &br); if (res == FR_OK && br != to_copy) res = FR_DISK_ERR; if (res != FR_OK) break; res = f_write(&fout, copy_buf, to_copy, &bw); if (res == FR_OK && bw != to_copy) res = FR_DISK_ERR; if (res != FR_OK) break; remaining -= to_copy; } f_close(&fin); f_close(&fout); if (res == FR_OK) f_chmod(dst, fno.fattrib, 0x3A); log_write("COPY %s -> %s: %s\n", src, dst, fs_error_str(res)); return res; } // Move a single file; replaces dst. Falls back to copy + delete if rename fails. int fs_move_file(const char *src, const char *dst) { FILINFO fno; if (f_stat(dst, &fno) == FR_OK) { if (fno.fattrib & AM_DIR) { log_write("MOVE %s: destination is a folder\n", dst); return FR_DENIED; } clear_readonly(dst, fno.fattrib); f_unlink(dst); } int res = f_rename(src, dst); if (res == FR_OK) { log_write("MOVE %s -> %s\n", src, dst); return FR_OK; } res = file_copy(src, dst); if (res == FR_OK) f_unlink(src); return res; } /* ---- trees ----------------------------------------------------------- */ static int delete_tree_rec(char *path, size_t len, fs_progress_cb cb, void *ctx) { DIR dir; FILINFO fno; int res = f_opendir(&dir, path); if (res != FR_OK) return res; while ((res = f_readdir(&dir, &fno)) == FR_OK && fno.fname[0]) { if (is_dot_entry(fno.fname)) continue; size_t sub = path_push(path, len, fno.fname); if (!sub) { res = FR_INVALID_NAME; break; } if (fno.fattrib & AM_DIR) { res = delete_tree_rec(path, sub, cb, ctx); if (res == FR_OK) { clear_readonly(path, fno.fattrib); res = f_unlink(path); } } else { clear_readonly(path, fno.fattrib); res = f_unlink(path); } log_write(" DEL %s: %s\n", path, fs_error_str(res)); path[len] = '\0'; if (res != FR_OK) break; if (cb) cb(ctx); } f_closedir(&dir); return res; } // Delete a file or a whole folder. A missing path counts as success. int fs_delete_tree(const char *target, fs_progress_cb cb, void *ctx) { char path[FS_PATH_MAX]; FILINFO fno; size_t len = path_set(path, target); if (!len) return FR_INVALID_NAME; int res = f_stat(path, &fno); if (res == FR_NO_FILE || res == FR_NO_PATH) return FR_OK; if (res != FR_OK) return res; if (fno.fattrib & AM_DIR) { res = delete_tree_rec(path, len, cb, ctx); if (res != FR_OK) { log_write("DELETE %s: %s\n", target, fs_error_str(res)); return res; } } clear_readonly(path, fno.fattrib); res = f_unlink(path); log_write("DELETE %s: %s\n", target, fs_error_str(res)); return res; } int folder_delete(const char *path) { return fs_delete_tree(path, NULL, NULL); } static int copy_tree_rec(char *src, size_t slen, char *dst, size_t dlen) { DIR dir; FILINFO fno; int res = f_mkdir(dst); if (res != FR_OK && res != FR_EXIST) return res; res = f_opendir(&dir, src); if (res != FR_OK) return res; while ((res = f_readdir(&dir, &fno)) == FR_OK && fno.fname[0]) { if (is_dot_entry(fno.fname)) continue; size_t s = path_push(src, slen, fno.fname); size_t d = path_push(dst, dlen, fno.fname); if (!s || !d) { res = FR_INVALID_NAME; break; } if (fno.fattrib & AM_DIR) res = copy_tree_rec(src, s, dst, d); else res = file_copy(src, dst); src[slen] = '\0'; dst[dlen] = '\0'; if (res != FR_OK) break; } f_closedir(&dir); return res; } // Recursively copy folder src into dst_parent (creates dst_parent/) int folder_copy(const char *src_dir, const char *dst_parent) { char src[FS_PATH_MAX]; char dst[FS_PATH_MAX]; size_t slen = path_set(src, src_dir); size_t dlen = path_set(dst, dst_parent); if (!slen || !dlen || !(dlen = path_push(dst, dlen, path_basename(src)))) return FR_INVALID_NAME; int res = copy_tree_rec(src, slen, dst, dlen); log_write("COPY TREE %s -> %s: %s\n", src_dir, dst, fs_error_str(res)); return res; } static int move_tree_rec(char *src, size_t slen, char *dst, size_t dlen, fs_progress_cb cb, void *ctx) { DIR dir; FILINFO fno; FILINFO dst_info; int res = f_opendir(&dir, src); if (res != FR_OK) return res; while ((res = f_readdir(&dir, &fno)) == FR_OK && fno.fname[0]) { if (is_dot_entry(fno.fname)) continue; size_t s = path_push(src, slen, fno.fname); size_t d = path_push(dst, dlen, fno.fname); if (!s || !d) { res = FR_INVALID_NAME; break; } bool dst_exists = f_stat(dst, &dst_info) == FR_OK; if (fno.fattrib & AM_DIR) { if (!dst_exists) { // Whole subtree moves in one step res = f_rename(src, dst); if (res != FR_OK) res = copy_tree_rec(src, s, dst, d); log_write("MOVE DIR %s -> %s: %s\n", src, dst, fs_error_str(res)); } else if (dst_info.fattrib & AM_DIR) { res = move_tree_rec(src, s, dst, d, cb, ctx); } else { log_write("MOVE %s: destination is a file\n", dst); res = FR_DENIED; } } else { res = fs_move_file(src, dst); } src[slen] = '\0'; dst[dlen] = '\0'; if (res != FR_OK) break; if (cb) cb(ctx); } f_closedir(&dir); return res; } // Move folder src_dir into dst_parent (dst_parent/), merging with an existing folder. int fs_move_tree(const char *src_dir, const char *dst_parent, fs_progress_cb cb, void *ctx) { char src[FS_PATH_MAX]; char dst[FS_PATH_MAX]; FILINFO fno; size_t slen = path_set(src, src_dir); size_t dlen = path_set(dst, dst_parent); if (!slen || !dlen || !(dlen = path_push(dst, dlen, path_basename(src)))) return FR_INVALID_NAME; int res = f_stat(dst, &fno); if (res == FR_NO_FILE) { // Target missing: one rename moves everything res = f_rename(src, dst); log_write("MOVE DIR %s -> %s: %s\n", src, dst, fs_error_str(res)); if (res == FR_OK) return FR_OK; res = copy_tree_rec(src, slen, dst, dlen); log_write("COPY TREE %s -> %s: %s\n", src, dst, fs_error_str(res)); return res; } if (res != FR_OK) return res; if (!(fno.fattrib & AM_DIR)) return FR_DENIED; return move_tree_rec(src, slen, dst, dlen, cb, ctx); } // Count entries below path, stopping once limit is reached (limit 0 = no limit) static u32 count_rec(char *path, size_t len, u32 count, u32 limit) { DIR dir; FILINFO fno; if (f_opendir(&dir, path) != FR_OK) return count; while (f_readdir(&dir, &fno) == FR_OK && fno.fname[0]) { if (is_dot_entry(fno.fname)) continue; count++; if (limit && count >= limit) break; if (fno.fattrib & AM_DIR) { size_t sub = path_push(path, len, fno.fname); if (sub) { count = count_rec(path, sub, count, limit); path[len] = '\0'; } if (limit && count >= limit) break; } } f_closedir(&dir); return count; } u32 fs_count_items(const char *target, u32 limit) { char path[FS_PATH_MAX]; size_t len = path_set(path, target); return len ? count_rec(path, len, 0, limit) : 0; }