mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-20 21:50:11 +08:00
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:
Vendored
+1
-1
@@ -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>
|
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
||||||
|
|
||||||
|
|||||||
@@ -455,6 +455,11 @@ const props = defineProps<{
|
|||||||
const hasOrigin = (app: App, origin: "spark" | "apm"): boolean =>
|
const hasOrigin = (app: App, origin: "spark" | "apm"): boolean =>
|
||||||
app.origins?.includes(origin) ?? app.origin === origin;
|
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 同时装两种来源时各计一次)
|
// APM / Spark 分别统计实际安装的包数量(同一 pkgname 同时装两种来源时各计一次)
|
||||||
// 搜索过滤后的全量(不叠加来源筛选),用于顶部统计徽章实时同步搜索结果,
|
// 搜索过滤后的全量(不叠加来源筛选),用于顶部统计徽章实时同步搜索结果,
|
||||||
// 避免搜索时列表缩减而徽章数字仍显示全量造成误导。
|
// 避免搜索时列表缩减而徽章数字仍显示全量造成误导。
|
||||||
@@ -506,9 +511,9 @@ const filteredApps = computed(() => {
|
|||||||
|
|
||||||
// 返回新数组排序:APM 应用始终排在前面(默认全部视图也遵守此规则)
|
// 返回新数组排序:APM 应用始终排在前面(默认全部视图也遵守此规则)
|
||||||
return [...matched].sort((a, b) => {
|
return [...matched].sort((a, b) => {
|
||||||
const aApm = hasOrigin(a, "apm") ? 0 : 1;
|
const pa = originPriority(a);
|
||||||
const bApm = hasOrigin(b, "apm") ? 0 : 1;
|
const pb = originPriority(b);
|
||||||
if (aApm !== bApm) return aApm - bApm;
|
if (pa !== pb) return pa - pb;
|
||||||
// 同类内保持原有的字母序,体验更一致
|
// 同类内保持原有的字母序,体验更一致
|
||||||
return a.pkgname.localeCompare(b.pkgname);
|
return a.pkgname.localeCompare(b.pkgname);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 () => {
|
export const loadSidebarConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const arch = window.apm_store.arch || "amd64";
|
const arch = window.apm_store.arch || "amd64";
|
||||||
@@ -86,7 +115,8 @@ export const loadSidebarConfig = async () => {
|
|||||||
const entries = Array.isArray(data) ? data : data.entries || [];
|
const entries = Array.isArray(data) ? data : data.entries || [];
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.id && entry.name) {
|
// 严格校验远程配置,避免畸形/恶意字段进入渲染层(Vue 模板自动转义已兜底 XSS)
|
||||||
|
if (!isValidSidebarEntry(entry)) continue;
|
||||||
const existing = entryMap.get(entry.id);
|
const existing = entryMap.get(entry.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// 多仓库共有入口,合并来源
|
// 多仓库共有入口,合并来源
|
||||||
@@ -97,14 +127,13 @@ export const loadSidebarConfig = async () => {
|
|||||||
entryMap.set(entry.id, {
|
entryMap.set(entry.id, {
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
icon: entry.icon || "",
|
icon: entry.icon ?? "",
|
||||||
type: entry.type || "category",
|
type: entry.type ?? "category",
|
||||||
value: entry.value || entry.id,
|
value: entry.value ?? entry.id,
|
||||||
origins: [mode],
|
origins: [mode],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`读取 ${mode} sidebar-config.json 失败:`, e);
|
console.warn(`读取 ${mode} sidebar-config.json 失败:`, e);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user