flat: 合并代码

This commit is contained in:
Apcallover
2025-12-19 11:35:00 +08:00
parent 4dfc7bdfd8
commit fdd5577c85
6 changed files with 146 additions and 208 deletions

View File

@@ -68,6 +68,22 @@ export class FileValidator {
}
}
/**
* 改进版:检查是否为有效的 UTF-8 文本
*/
_isValidUTF8(buffer) {
try {
// fatal: true 会在遇到无效编码时抛出错误,而不是用 替换
const decoder = new TextDecoder('utf-8', {
fatal: true
});
decoder.decode(buffer);
return true;
} catch (e) {
return false;
}
}
/**
* 辅助ArrayBuffer 转 Hex 字符串
*/
@@ -79,23 +95,79 @@ export class FileValidator {
}
/**
* 辅助:纯文本抽样检测
* 【新增】统计 CSV 行数(严谨版:忽略引号内的换行符)
* 性能:对于 10MB 文件,现代浏览器处理通常在 100ms 以内
*/
_isCleanText(buffer) {
const bytes = new Uint8Array(buffer);
const checkLen = Math.min(bytes.length, 1000);
let suspiciousCount = 0;
_countCSVRows(buffer) {
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(buffer);
for (let i = 0; i < checkLen; i++) {
const byte = bytes[i];
// 允许常见控制符: 9(Tab), 10(LF), 13(CR)
// 0-31 范围内其他的通常是二进制控制符
if (byte < 32 && ![9, 10, 13].includes(byte)) {
suspiciousCount++;
let rowCount = 0;
let inQuote = false;
let len = text.length;
// 遍历每一个字符
for (let i = 0; i < len; i++) {
const char = text[i];
// 切换引号状态
if (char === '"') {
inQuote = !inQuote;
}
// 只有在非引号状态下的换行符,才算作一行结束
else if (char === '\n' && !inQuote) {
rowCount++;
}
}
// 如果可疑字符占比 < 5%,认为是纯文本
return suspiciousCount / checkLen < 0.05;
// 处理最后一行没有换行符的情况(且文件不为空)
if (len > 0 && text[len - 1] !== '\n') {
rowCount++;
}
return rowCount;
}
/**
* 【核心】:校验纯文本内容
* 1. 检查是否包含乱码 (非 UTF-8)
* 2. 针对特定格式 (JSON) 进行语法解析
*/
_validateTextContent(buffer, extension) {
// 1. 尝试解码为 UTF-8
let contentStr = '';
try {
const decoder = new TextDecoder('utf-8', {
fatal: true
});
contentStr = decoder.decode(buffer);
} catch (e) {
// 如果解码失败,说明包含非文本的二进制数据
console.warn('UTF-8 解码失败', e);
return false;
}
// 2. 检查是否存在过多的空字符 (二进制文件特征)
// 某些二进制文件可能勉强通过 UTF-8 解码,但会包含大量 \0
if (contentStr.includes('\u0000')) {
return false;
}
// 3. 针对特定后缀进行语法校验 (可选,更严格)
if (extension === 'json') {
try {
JSON.parse(contentStr);
} catch (e) {
console.warn('无效的 JSON 格式');
return false;
}
}
// 如果是 CSV可以简单检查行数可选
// if (extension === 'csv') { ... }
return true;
}
/**
@@ -132,10 +204,23 @@ export class FileValidator {
// 分支处理:纯文本 vs 二进制
if (expectedMagic === 'TYPE_TEXT') {
if (this._isCleanText(buffer)) {
if (this._validateTextContent(buffer, extension)) {
isSafe = true;
} else {
return reject(`文件异常:.${extension} 文件包含非法二进制内容`);
// 细化报错信息
if (extension === 'json') {
return reject(`文件异常:不是有效的 JSON 文件`);
}
return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`);
}
// 【新增】专门针对 CSV 的行数检查
if (extension === 'csv' && this.csvMaxRows > 0) {
const rows = this._countCSVRows(buffer);
// 注意:这里通常把表头也算作 1 行,如果不算表头可以将 limit + 1
if (rows > this.csvMaxRows) {
return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`);
}
}
} else {
// 获取文件头 Hex (读取足够长的字节以覆盖最长的魔数PNG需8字节)
@@ -155,7 +240,13 @@ export class FileValidator {
reader.onerror = () => reject('文件读取失败,无法校验');
// 读取前 1KB 进行判断
reader.readAsArrayBuffer(file.slice(0, 1024));
if (expectedMagic === 'TYPE_TEXT' && extension === 'json') {
// JSON 必须读全量才能 parse建议限制 JSON 文件大小
reader.readAsArrayBuffer(file);
} else {
// 图片/普通文本 读取前 2KB 足够判断头部和编码特征
reader.readAsArrayBuffer(file.slice(0, 2048));
}
});
}
}