diff --git a/debian/changelog b/debian/changelog index 9b26350d..02076836 100644 --- a/debian/changelog +++ b/debian/changelog @@ -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) diff --git a/src/components/InstalledAppsModal.vue b/src/components/InstalledAppsModal.vue index ae49e1c5..ee3c6b41 100644 --- a/src/components/InstalledAppsModal.vue +++ b/src/components/InstalledAppsModal.vue @@ -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); }); diff --git a/src/composables/useCatalog.ts b/src/composables/useCatalog.ts index 886a99e0..cdf3766f 100644 --- a/src/composables/useCatalog.ts +++ b/src/composables/useCatalog.ts @@ -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; + 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) {