fix: 排序魔法数字提取常量 + sidebar-config 远程校验类型守卫

- InstalledAppsModal 提取 ORIGIN_PRIORITY 具名常量与 originPriority(),
  消除排序比较函数中的 0/1 魔法数字(保留 hasOrigin 双来源语义)
- useCatalog.loadSidebarConfig 新增 isValidSidebarEntry 类型守卫,对远程
  sidebar-config.json 入口做字段类型/长度/type 白名单校验,防止畸形数据进入
  渲染层(type 白名单与 typedefinition.ts 的 SidebarEntry.type 严格一致)
- debian/changelog 测试版本号 5.2.1.1-test
- 验证:vue-tsc 0 / eslint 0 / dpkg-buildpackage 打包成功

审查项实证:
- 改进③(filteredApps 多次 filter) 误报:当前已为单 filter 遍历(.24 已改),
  审查贴旧片段且退化 a.origin 会破坏双来源,不改
- 改进①/② 为真改进,已采纳并保真实现
This commit is contained in:
xiyidaiwa
2026-08-13 20:43:43 +08:00
parent 0f62a7e36a
commit 7be90f13ce
3 changed files with 54 additions and 20 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
spark-store (5.2.1.0) UNRELEASED; urgency=medium
spark-store (5.2.1.1-test) UNRELEASED; urgency=medium
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
+8 -3
View File
@@ -455,6 +455,11 @@ const props = defineProps<{
const hasOrigin = (app: App, origin: "spark" | "apm"): boolean =>
app.origins?.includes(origin) ?? app.origin === origin;
// 来源排序优先级:APM 始终排在前面(值越小越靠前),与排序比较函数共用,避免魔法数字
const ORIGIN_PRIORITY: Record<"spark" | "apm", number> = { apm: 0, spark: 1 };
const originPriority = (app: App): number =>
hasOrigin(app, "apm") ? ORIGIN_PRIORITY.apm : ORIGIN_PRIORITY.spark;
// APM / Spark 分别统计实际安装的包数量(同一 pkgname 同时装两种来源时各计一次)
// 搜索过滤后的全量(不叠加来源筛选),用于顶部统计徽章实时同步搜索结果,
// 避免搜索时列表缩减而徽章数字仍显示全量造成误导。
@@ -506,9 +511,9 @@ const filteredApps = computed(() => {
// 返回新数组排序:APM 应用始终排在前面(默认全部视图也遵守此规则)
return [...matched].sort((a, b) => {
const aApm = hasOrigin(a, "apm") ? 0 : 1;
const bApm = hasOrigin(b, "apm") ? 0 : 1;
if (aApm !== bApm) return aApm - bApm;
const pa = originPriority(a);
const pb = originPriority(b);
if (pa !== pb) return pa - pb;
// 同类内保持原有的字母序,体验更一致
return a.pkgname.localeCompare(b.pkgname);
});
+45 -16
View File
@@ -69,6 +69,35 @@ export const loadCategories = async () => {
}
};
// 远程 sidebar-config.json 入口的类型守卫:在写入渲染层前拦截异常字段。
// 字段类型/长度限制防止畸形数据(如超长 name 撑破布局、非法 type 触发未知分支)。
// type 白名单必须与 global/typedefinition.ts 的 SidebarEntry.type 保持一致。
const VALID_SIDEBAR_TYPES = ["category", "search", "link", "homeList"] as const;
const isValidSidebarEntry = (e: unknown): e is SidebarEntry => {
if (typeof e !== "object" || e === null) return false;
const entry = e as Record<string, unknown>;
if (
typeof entry.id !== "string" ||
entry.id.length === 0 ||
entry.id.length > 64
)
return false;
if (
typeof entry.name !== "string" ||
entry.name.length === 0 ||
entry.name.length > 128
)
return false;
if (entry.icon !== undefined && typeof entry.icon !== "string") return false;
if (
entry.type !== undefined &&
!VALID_SIDEBAR_TYPES.includes(entry.type as (typeof VALID_SIDEBAR_TYPES)[number])
)
return false;
if (entry.value !== undefined && typeof entry.value !== "string") return false;
return true;
};
export const loadSidebarConfig = async () => {
try {
const arch = window.apm_store.arch || "amd64";
@@ -86,23 +115,23 @@ export const loadSidebarConfig = async () => {
const entries = Array.isArray(data) ? data : data.entries || [];
for (const entry of entries) {
if (entry.id && entry.name) {
const existing = entryMap.get(entry.id);
if (existing) {
// 多仓库共有入口,合并来源
if (existing.origins && !existing.origins.includes(mode)) {
existing.origins.push(mode);
}
} else {
entryMap.set(entry.id, {
id: entry.id,
name: entry.name,
icon: entry.icon || "",
type: entry.type || "category",
value: entry.value || entry.id,
origins: [mode],
});
// 严格校验远程配置,避免畸形/恶意字段进入渲染层(Vue 模板自动转义已兜底 XSS)
if (!isValidSidebarEntry(entry)) continue;
const existing = entryMap.get(entry.id);
if (existing) {
// 多仓库共有入口,合并来源
if (existing.origins && !existing.origins.includes(mode)) {
existing.origins.push(mode);
}
} else {
entryMap.set(entry.id, {
id: entry.id,
name: entry.name,
icon: entry.icon ?? "",
type: entry.type ?? "category",
value: entry.value ?? entry.id,
origins: [mode],
});
}
}
} catch (e) {