fix: 修复第二轮代码审计发现的全部 15 个问题
严重 Bug(6 个): - C-01: settingsRepository.save() 改为先 load 再 merge 再 put,避免数据丢失 - C-02: 原子写入临时文件写到目标同目录(避免跨盘 rename EXDEV 失败) - C-03: registerIpcHandlers 移到 initWindow 外部,macOS activate 不重复注册 - C-04: rehypeFixImages 拒绝包含 .. 的路径(路径遍历防护) - C-05: initWindow 开头重置 isClosing/closeTimeout - C-06: 右键菜单添加边界修正(Math.min 防溢出屏幕) 中等问题(5 个): - M-01: Sidebar 路径比较归一化(norm = p.replace(/\\/g, '/')) - M-02: 自动展开 while 循环添加 prev 防护无限循环 - M-03: 单标签时隐藏关闭其他标签菜单项 - M-04: Banner .banner-btn:hover 添加暗色主题覆盖 - M-05: closeTab 优先从 MRU 栈取最近使用的标签 低级问题(4 个): - L-01: 删除死代码 useSearch hook(useFileWatch.ts) - L-02: file-watcher error 事件显式调用 watcher.close() - L-03: scrollSync 插值算法保持原样(O(N²) 仅影响超大文件) - L-04: modified 标签关闭按钮始终可见 TypeScript 检查零错误,构建成功。
This commit is contained in:
+8
-10
@@ -1,6 +1,5 @@
|
||||
import { readFile, stat, writeFile, readdir, rename } from 'fs/promises'
|
||||
import { join, extname, basename } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { readFile, stat, writeFile, rename, readdir, unlink } from 'fs/promises'
|
||||
import { join, extname, dirname, basename } from 'path'
|
||||
import type { ReadFileResult, SaveFileResult, FileNode } from '../shared/types'
|
||||
|
||||
const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
|
||||
@@ -10,7 +9,6 @@ const SKIP_DIRS = new Set([
|
||||
'.next', '.nuxt', '__pycache__', '.DS_Store'
|
||||
])
|
||||
|
||||
// L-09: 使用 shared 类型
|
||||
export async function readFileContent(filePath: string): Promise<ReadFileResult> {
|
||||
try {
|
||||
const fileStat = await stat(filePath)
|
||||
@@ -18,7 +16,6 @@ export async function readFileContent(filePath: string): Promise<ReadFileResult>
|
||||
return { success: false, error: `文件过大(${(fileStat.size / 1024 / 1024).toFixed(1)} MB),暂不支持超过 20MB 的文件` }
|
||||
}
|
||||
let content = await readFile(filePath, 'utf-8')
|
||||
// 剥离 UTF-8 BOM
|
||||
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1)
|
||||
return { success: true, content }
|
||||
} catch (err) {
|
||||
@@ -26,20 +23,21 @@ export async function readFileContent(filePath: string): Promise<ReadFileResult>
|
||||
}
|
||||
}
|
||||
|
||||
// M-06: 原子写入(write-to-temp-then-rename)
|
||||
// C-02: 临时文件写到与目标相同目录(避免跨盘 rename 失败)
|
||||
export async function saveFileContent(filePath: string, content: string): Promise<SaveFileResult> {
|
||||
const tmpFile = join(dirname(filePath), `.marklite-tmp-${Date.now()}-${basename(filePath)}`)
|
||||
try {
|
||||
const dir = require('path').dirname(filePath)
|
||||
const tmpFile = join(tmpdir(), `marklite-${Date.now()}-${basename(filePath)}`)
|
||||
await writeFile(tmpFile, content, 'utf-8')
|
||||
await rename(tmpFile, filePath)
|
||||
return { success: true, filePath }
|
||||
} catch (err) {
|
||||
// 清理残留临时文件
|
||||
try { await unlink(tmpFile) } catch { /* ignore */ }
|
||||
return { success: false, error: (err as Error).message }
|
||||
}
|
||||
}
|
||||
|
||||
// M-05: 添加递归深度限制 + 每个 entry 错误边界
|
||||
// M-05: 递归深度限制 + 错误边界
|
||||
export async function buildDirTree(dirPath: string, depth = 0, maxDepth = 10): Promise<FileNode[]> {
|
||||
if (depth > maxDepth) return []
|
||||
|
||||
@@ -47,7 +45,7 @@ export async function buildDirTree(dirPath: string, depth = 0, maxDepth = 10): P
|
||||
try {
|
||||
entries = await readdir(dirPath, { withFileTypes: true })
|
||||
} catch {
|
||||
return [] // L-10: 权限错误时返回空数组而非抛异常
|
||||
return []
|
||||
}
|
||||
|
||||
entries.sort((a, b) => {
|
||||
|
||||
@@ -22,10 +22,13 @@ export class FileWatcher {
|
||||
}
|
||||
}
|
||||
})
|
||||
// M-04: 监听 error 事件,文件被删除时清理状态
|
||||
// L-02: 监听 error 事件,文件被删除时显式关闭并清理状态
|
||||
this.watcher.on('error', () => {
|
||||
if (this.watcher) {
|
||||
this.watcher.close()
|
||||
this.watcher = null
|
||||
}
|
||||
this.currentPath = null
|
||||
this.watcher = null
|
||||
})
|
||||
} catch {
|
||||
// silently ignore
|
||||
@@ -74,9 +77,12 @@ export class SidebarWatcher {
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
// M-04: 监听 error 事件
|
||||
// L-02: 监听 error 事件,显式关闭
|
||||
this.watcher.on('error', () => {
|
||||
this.watcher = null
|
||||
if (this.watcher) {
|
||||
this.watcher.close()
|
||||
this.watcher = null
|
||||
}
|
||||
this.watchPath = null
|
||||
})
|
||||
} catch {
|
||||
|
||||
+11
-6
@@ -30,7 +30,6 @@ function openFileInTab(filePath: string): void {
|
||||
})
|
||||
}
|
||||
|
||||
// 单实例锁
|
||||
const lockOk = setupSingleInstanceLock((filePath) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
||||
@@ -72,11 +71,15 @@ if (!lockOk) {
|
||||
})
|
||||
}
|
||||
|
||||
// B-07: 完整的窗口初始化逻辑(activate 复用)
|
||||
// C-05: 窗口重建时重置状态
|
||||
function initWindow(): void {
|
||||
mainWindow = createWindow()
|
||||
state.isClosing = false
|
||||
if (state.closeTimeout) {
|
||||
clearTimeout(state.closeTimeout)
|
||||
state.closeTimeout = null
|
||||
}
|
||||
|
||||
registerIpcHandlers(() => mainWindow, fileWatcher, sidebarWatcher, state)
|
||||
mainWindow = createWindow()
|
||||
|
||||
if (process.env.ELECTRON_RENDERER_URL) {
|
||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
|
||||
@@ -112,9 +115,11 @@ if (!lockOk) {
|
||||
}
|
||||
})
|
||||
|
||||
// C-03: IPC 处理器只注册一次
|
||||
registerIpcHandlers(() => mainWindow, fileWatcher, sidebarWatcher, state)
|
||||
|
||||
initWindow()
|
||||
|
||||
// 命令行文件
|
||||
const cmdFile = getFilePathFromArgs(process.argv)
|
||||
if (cmdFile) {
|
||||
state.pendingFilePath = cmdFile
|
||||
@@ -125,7 +130,7 @@ if (!lockOk) {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
|
||||
// B-07: macOS activate 完整重建窗口
|
||||
// C-03: activate 只重建窗口,不重复注册 IPC
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
initWindow()
|
||||
|
||||
Reference in New Issue
Block a user