/* logic/fsx.c — 文件管理器业务逻辑实现(纯 C:仅 libc + 标准头,无 meow_ui 依赖)。 * * 实现要点: * - 所有写操作(delete/rename/mkdir)先 fsx_within_sd() 复核目标路径,拒绝 ".." 越界与卡外路径; * - 路径一律经 fsx_join() 由「已验证 base + sanitize 后的 name」构造,杜绝手工拼串引入穿越; * - 目录枚举用 bounded 数组(FSX_MAX_ENTRIES)+ 客户端虚拟化只画可见行——不一次全读进内存; * - 符号链接 v1 不追(is_dir=0,作普通文件展示),防止链出卡外。 */ #include "fsx.h" #include #include #include #include #include #include #include #include #include /* ---- 路径安全 ---- */ int fsx_within_sd(const char *path) { if (!path) return 0; size_t rl = strlen(FSX_SD_ROOT); if (strncmp(path, FSX_SD_ROOT, rl) != 0) return 0; char c = path[rl]; if (c != '\0' && c != '/') return 0; /* "…/sd_0evil" 这类前缀伪装拒绝 */ /* 逐 '/'-分隔分量扫描:出现 ".." 即拒绝(防 "../../" 逃逸)。 */ const char *p = path; while (*p) { while (*p == '/') p++; if (!*p) break; const char *start = p; while (*p && *p != '/') p++; size_t len = (size_t)(p - start); if (len == 2 && start[0] == '.' && start[1] == '.') return 0; } return 1; } int fsx_is_root(const char *path) { return path && strcmp(path, FSX_SD_ROOT) == 0; } int fsx_dirname(const char *path, char *out, int outsz) { if (!path || !out || outsz <= 0) return -1; if (fsx_is_root(path)) { size_t rl = strlen(FSX_SD_ROOT); if ((int)rl >= outsz) return -1; memcpy(out, FSX_SD_ROOT, rl + 1); return 0; } const char *slash = strrchr(path, '/'); if (!slash) return -1; /* 非法相对路径 */ size_t n = (size_t)(slash - path); if (n == 0) n = 1; /* "/xxx" → "/"(一般不出现) */ if ((int)(n + 1) > outsz) return -1; memcpy(out, path, n); out[n] = '\0'; return 0; } const char *fsx_basename(const char *path) { if (!path) return ""; const char *slash = strrchr(path, '/'); return slash ? slash + 1 : path; } int fsx_join(const char *base, const char *name, char *out, int outsz) { if (!base || !name || !out || outsz <= 0) return -1; /* name 必须是被清洗后的单分量:无 '/'、非 ".."、非空。 */ if (!name[0] || name[0] == '/' || strcmp(name, "..") == 0 || strchr(name, '/')) return -1; size_t bl = strlen(base); /* base 尾随 '/' 去掉,但保持根 "/usr/data/mnt/sd_0" 不截成 ""。 */ char b[FSX_MAX_PATH]; if (bl >= sizeof b) return -1; memcpy(b, base, bl + 1); while (bl > 0 && b[bl - 1] == '/' && bl != strlen(FSX_SD_ROOT)) b[--bl] = '\0'; int n = snprintf(out, (size_t)outsz, "%s/%s", b, name); if (n < 0 || n >= outsz) return -1; return 0; } int fsx_sanitize_name(const char *name, char *out, int outsz) { if (!name || !out || outsz <= 0) return -1; const char *s = name; /* 跳过前导空白 */ while (*s == ' ' || *s == '\t') s++; if (!*s) return -1; /* 全空白 */ if (strlen(s) > 120) return -1; /* 过长 */ if (s[0] == '.') return -1; /* 隐藏/点位起始拒 */ for (const char *p = s; *p; p++) { unsigned char ch = (unsigned char)*p; if (ch < 0x20) return -1; /* 控制字符 */ if (*p == '/' || *p == '\\' || *p == ':') return -1; if (p[0] == '.' && p[1] == '.') return -1; /* ".." 分量拒 */ } size_t n = strlen(s); /* 去尾随空白 */ while (n > 0 && (s[n - 1] == ' ' || s[n - 1] == '\t')) n--; if (n == 0) return -1; if ((int)n + 1 > outsz) return -1; memcpy(out, s, n); out[n] = '\0'; return 0; } /* ---- 展示格式化 ---- */ void fsx_fmt_size(long long size, char *out, int outsz) { if (!out || outsz <= 0) return; if (size < 0) size = 0; const char *u[] = { "B", "KB", "MB", "GB" }; double v = (double)size; int i = 0; while (v >= 1024.0 && i < 3) { v /= 1024.0; i++; } if (i == 0) snprintf(out, (size_t)outsz, "%lld B", size); else snprintf(out, (size_t)outsz, "%.1f %s", v, u[i]); } void fsx_fmt_time(long long mtime, char *out, int outsz) { if (!out || outsz <= 0) return; time_t t = (time_t)mtime; struct tm tm; if (!localtime_r(&t, &tm)) { snprintf(out, (size_t)outsz, "-"); return; } strftime(out, (size_t)outsz, "%Y-%m-%d %H:%M", &tm); } /* ---- 目录枚举 / 排序 / 虚拟化 ---- */ static int entry_cmp(const void *a, const void *b) { const fsx_entry *ea = (const fsx_entry *)a; const fsx_entry *eb = (const fsx_entry *)b; if (ea->is_dir != eb->is_dir) return eb->is_dir - ea->is_dir; /* 目录优先 */ return strcmp(ea->name, eb->name); /* 名称升序 */ } void fsx_sort_entries(fsx_entry *entries, int n) { if (n > 1) qsort(entries, (size_t)n, sizeof *entries, entry_cmp); } int fsx_scan_dir(const char *path, fsx_entry *entries, int max, int *out_count) { if (out_count) *out_count = 0; if (!path) return FSX_ERR_NOENT; DIR *d = opendir(path); if (!d) return -errno; /* ENOENT(-2)/EACCES(-13)/ENOTDIR(-20) */ int n = 0; struct dirent *de; char jbuf[FSX_MAX_PATH]; while ((de = readdir(d)) != NULL) { if (de->d_name[0] == '.') continue; /* 跳过 . .. 及隐藏项 */ if (n >= max) break; /* 超大目录截断(客户端标注) */ fsx_entry *e = &entries[n]; memset(e, 0, sizeof *e); if (fsx_join(path, de->d_name, jbuf, sizeof jbuf) != 0) continue; strncpy(e->name, de->d_name, FSX_MAX_NAME - 1); e->name[FSX_MAX_NAME - 1] = '\0'; strncpy(e->path, jbuf, FSX_MAX_PATH - 1); e->path[FSX_MAX_PATH - 1] = '\0'; struct stat st; if (lstat(jbuf, &st) == 0) { e->is_link = S_ISLNK(st.st_mode); if (S_ISDIR(st.st_mode)) { e->is_dir = 1; } else { e->is_dir = 0; e->size = st.st_size; } e->mtime = st.st_mtime; } else { e->is_dir = 0; e->size = 0; e->mtime = 0; } n++; } closedir(d); fsx_sort_entries(entries, n); if (out_count) *out_count = n; return 0; } int fsx_max_scroll(int count, int item_h, int viewport_h) { int m = count * item_h - viewport_h; return m > 0 ? m : 0; } void fsx_visible_range(int count, int item_h, int scroll, int viewport_h, int *first, int *last) { if (first) *first = 0; if (last) *last = 0; if (count <= 0 || item_h <= 0 || viewport_h <= 0) return; if (scroll < 0) scroll = 0; int f = scroll / item_h; if (f < 0) f = 0; if (f > count) f = count; int l = (scroll + viewport_h) / item_h + 1; if (l > count) l = count; if (first) *first = f; if (last) *last = l; } /* ---- 写/查操作 ---- */ int fsx_delete(const char *path) { if (!path || !fsx_within_sd(path)) return -EACCES; struct stat st; if (lstat(path, &st) != 0) return -errno; if (S_ISDIR(st.st_mode)) { return rmdir(path) == 0 ? 0 : -errno; } return unlink(path) == 0 ? 0 : -errno; } int fsx_rename_into(const char *path, const char *new_name) { if (!path || !fsx_within_sd(path)) return -EACCES; char nb[FSX_MAX_NAME], db[FSX_MAX_PATH], np[FSX_MAX_PATH]; if (fsx_sanitize_name(new_name, nb, sizeof nb) != 0) return -EINVAL; if (fsx_dirname(path, db, sizeof db) != 0) return -EINVAL; if (fsx_join(db, nb, np, sizeof np) != 0) return -EINVAL; if (!fsx_within_sd(np)) return -EACCES; struct stat st; if (lstat(np, &st) == 0) return -EEXIST; /* 目标已存在,拒绝覆盖 */ return rename(path, np) == 0 ? 0 : -errno; } int fsx_mkdir(const char *base, const char *name) { if (!base || !fsx_within_sd(base)) return -EACCES; char nb[FSX_MAX_NAME], np[FSX_MAX_PATH]; if (fsx_sanitize_name(name, nb, sizeof nb) != 0) return -EINVAL; if (fsx_join(base, nb, np, sizeof np) != 0) return -EINVAL; if (!fsx_within_sd(np)) return -EACCES; return mkdir(np, 0777) == 0 ? 0 : -errno; } int fsx_stat_path(const char *path, fsx_entry *out) { if (!path || !out) return -EINVAL; struct stat st; if (lstat(path, &st) != 0) return -errno; memset(out, 0, sizeof *out); strncpy(out->name, fsx_basename(path), FSX_MAX_NAME - 1); out->name[FSX_MAX_NAME - 1] = '\0'; strncpy(out->path, path, FSX_MAX_PATH - 1); out->path[FSX_MAX_PATH - 1] = '\0'; out->is_link = S_ISLNK(st.st_mode); out->is_dir = S_ISDIR(st.st_mode); out->size = st.st_size; out->mtime = st.st_mtime; return 0; } int fsx_is_text_path(const char *path) { if (!path) return 0; const char *dot = strrchr(path, '.'); if (!dot) return 0; static const char *exts[] = { ".txt", ".log", ".md", ".json", ".ini", ".cfg", ".conf", ".csv", ".html", ".xml", ".srt", ".lrc", ".sh", ".c", ".h", ".py", ".js", ".ts", ".css", ".nfo", ".m3u", ".m3u8", ".list", ".license", ".readme", ".gitignore", NULL }; for (int i = 0; exts[i]; i++) if (strcasecmp(dot, exts[i]) == 0) return 1; return 0; }