flat 文件校验

This commit is contained in:
Apcallover
2025-12-19 12:06:35 +08:00
parent fdd5577c85
commit ce597b182d

View File

@@ -1,272 +1,210 @@
/**
* FileValidator.js
* 封装好的文件安全校验类
*/
// ==========================================
// 1. 预定义:已知文件类型的魔数 (Signature Database)
// ==========================================
const KNOWN_SIGNATURES = { const KNOWN_SIGNATURES = {
// === 图片 === png: "89504E470D0A1A0A",
png: '89504E470D0A1A0A', jpg: "FFD8FF",
jpg: 'FFD8FF', jpeg: "FFD8FF",
jpeg: 'FFD8FF', gif: "47494638",
gif: '47494638', webp: "52494646",
webp: '52494646', // RIFF Header docx: "504B0304",
xlsx: "504B0304",
// === 文档 (Office 新版 - ZIP 格式) === pptx: "504B0304",
docx: '504B0304', doc: "D0CF11E0",
xlsx: '504B0304', xls: "D0CF11E0",
pptx: '504B0304', ppt: "D0CF11E0",
pdf: "25504446",
// === 文档 (Office 旧版 - OLECF 格式) === txt: "TYPE_TEXT",
doc: 'D0CF11E0', csv: "TYPE_TEXT",
xls: 'D0CF11E0', md: "TYPE_TEXT",
ppt: 'D0CF11E0', json: "TYPE_TEXT"
// === 其他 ===
pdf: '25504446',
// === 纯文本 (无固定魔数,需特殊算法检测) ===
txt: 'TYPE_TEXT',
csv: 'TYPE_TEXT',
md: 'TYPE_TEXT',
json: 'TYPE_TEXT',
}; };
// ==========================================
// 2. 核心类定义
// ==========================================
export class FileValidator { export class FileValidator {
/** version = "1.0.0";
* 构造函数 signs = Object.keys(KNOWN_SIGNATURES);
* @param {Object} options 配置项
* @param {number} [options.maxSizeMB=10] 最大文件大小 (MB)
* @param {string[]} [options.allowedExtensions] 允许的扩展名列表 (如 ['jpg', 'png']),默认允许全部已知类型
*/
version = '1.0.0';
constructor(options = {}) { constructor(options = {}) {
// 配置大小 (默认 10MB)
this.maxSizeMB = options.maxSizeMB || 10; this.maxSizeMB = options.maxSizeMB || 10;
// 配置允许的类型
// 如果传入了 allowedExtensions则只使用传入的否则使用全部 KNOWN_SIGNATURES
if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) { if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) {
this.allowedConfig = {}; this.allowedConfig = {};
options.allowedExtensions.forEach((ext) => { options.allowedExtensions.forEach(ext => {
const key = ext.toLowerCase(); const key = ext.toLowerCase();
if (KNOWN_SIGNATURES[key]) { if (KNOWN_SIGNATURES[key]) {
this.allowedConfig[key] = KNOWN_SIGNATURES[key]; this.allowedConfig[key] = KNOWN_SIGNATURES[key]
} else { } else {
console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`); console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`)
} }
}); })
} else { } else {
this.allowedConfig = { this.allowedConfig = {
...KNOWN_SIGNATURES ...KNOWN_SIGNATURES
}; }
} }
} }
/**
* 改进版:检查是否为有效的 UTF-8 文本
*/
_isValidUTF8(buffer) { _isValidUTF8(buffer) {
try { try {
// fatal: true 会在遇到无效编码时抛出错误,而不是用 替换 const decoder = new TextDecoder("utf-8", {
const decoder = new TextDecoder('utf-8', {
fatal: true fatal: true
}); });
decoder.decode(buffer); decoder.decode(buffer);
return true; return true
} catch (e) { } catch (e) {
return false; return false
} }
} }
/**
* 辅助ArrayBuffer 转 Hex 字符串
*/
_bufferToHex(buffer) { _bufferToHex(buffer) {
return Array.prototype.map return Array.prototype.map.call(new Uint8Array(buffer), x => ("00" + x.toString(16)).slice(-2)).join("")
.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2)) .toUpperCase()
.join('')
.toUpperCase();
} }
/**
* 【新增】统计 CSV 行数(严谨版:忽略引号内的换行符)
* 性能:对于 10MB 文件,现代浏览器处理通常在 100ms 以内
*/
_countCSVRows(buffer) { _countCSVRows(buffer) {
const decoder = new TextDecoder('utf-8'); const decoder = new TextDecoder("utf-8");
const text = decoder.decode(buffer); const text = decoder.decode(buffer);
let rowCount = 0; let rowCount = 0;
let inQuote = false; let inQuote = false;
let len = text.length; let len = text.length;
// 遍历每一个字符
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const char = text[i]; const char = text[i];
// 切换引号状态
if (char === '"') { if (char === '"') {
inQuote = !inQuote; inQuote = !inQuote
} } else if (char === "\n" && !inQuote) {
// 只有在非引号状态下的换行符,才算作一行结束 rowCount++
else if (char === '\n' && !inQuote) {
rowCount++;
} }
} }
if (len > 0 && text[len - 1] !== "\n") {
// 处理最后一行没有换行符的情况(且文件不为空) rowCount++
if (len > 0 && text[len - 1] !== '\n') {
rowCount++;
} }
return rowCount
return rowCount;
} }
/**
* 【核心】:校验纯文本内容
* 1. 检查是否包含乱码 (非 UTF-8)
* 2. 针对特定格式 (JSON) 进行语法解析
*/
_validateTextContent(buffer, extension) { _validateTextContent(buffer, extension) {
// 1. 尝试解码为 UTF-8 let contentStr = "";
let contentStr = '';
try { try {
const decoder = new TextDecoder('utf-8', { const decoder = new TextDecoder("utf-8", {
fatal: true fatal: true
}); });
contentStr = decoder.decode(buffer); contentStr = decoder.decode(buffer)
} catch (e) { } catch (e) {
// 如果解码失败,说明包含非文本的二进制数据 console.warn("UTF-8 解码失败", e);
console.warn('UTF-8 解码失败', e); return false
return false;
} }
if (contentStr.includes("\0")) {
// 2. 检查是否存在过多的空字符 (二进制文件特征) return false
// 某些二进制文件可能勉强通过 UTF-8 解码,但会包含大量 \0
if (contentStr.includes('\u0000')) {
return false;
} }
if (extension === "json") {
// 3. 针对特定后缀进行语法校验 (可选,更严格)
if (extension === 'json') {
try { try {
JSON.parse(contentStr); JSON.parse(contentStr)
} catch (e) { } catch (e) {
console.warn('无效的 JSON 格式'); console.warn("无效的 JSON 格式");
return false; return false
} }
} }
return true
// 如果是 CSV可以简单检查行数可选
// if (extension === 'csv') { ... }
return true;
} }
/**
* 执行校验
* @param {File} file 文件对象
* @returns {Promise<boolean>}
*/
validate(file) { validate(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 1. 基础对象检查 if (!file || !file.name) return reject("无效的文件对象");
if (!file || !file.name) return reject('无效的文件对象');
// 2. 大小检查
if (file.size > this.maxSizeMB * 1024 * 1024) { if (file.size > this.maxSizeMB * 1024 * 1024) {
return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`); return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`)
} }
// 3. 后缀名检查
const fileName = file.name.toLowerCase(); const fileName = file.name.toLowerCase();
const extension = fileName.substring(fileName.lastIndexOf('.') + 1); const extension = fileName.substring(fileName.lastIndexOf(".") + 1);
// 检查是否在配置的白名单中
const expectedMagic = this.allowedConfig[extension]; const expectedMagic = this.allowedConfig[extension];
if (!expectedMagic) { if (!expectedMagic) {
return reject(`不支持的文件格式: .${extension}`); return reject(`不支持的文件格式: .${extension}`)
} }
const reader = new FileReader;
// 4. 读取二进制头进行魔数校验 reader.onload = e => {
const reader = new FileReader();
reader.onload = (e) => {
const buffer = e.target.result; const buffer = e.target.result;
let isSafe = false; let isSafe = false;
if (expectedMagic === "TYPE_TEXT") {
// 分支处理:纯文本 vs 二进制
if (expectedMagic === 'TYPE_TEXT') {
if (this._validateTextContent(buffer, extension)) { if (this._validateTextContent(buffer, extension)) {
isSafe = true; isSafe = true
} else { } else {
// 细化报错信息 if (extension === "json") {
if (extension === 'json') { return reject(`文件异常:不是有效的 JSON 文件`)
return reject(`文件异常:不是有效的 JSON 文件`);
} }
return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`); return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`)
} }
if (extension === "csv" && this.csvMaxRows > 0) {
// 【新增】专门针对 CSV 的行数检查
if (extension === 'csv' && this.csvMaxRows > 0) {
const rows = this._countCSVRows(buffer); const rows = this._countCSVRows(buffer);
// 注意:这里通常把表头也算作 1 行,如果不算表头可以将 limit + 1
if (rows > this.csvMaxRows) { if (rows > this.csvMaxRows) {
return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`); return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`)
} }
} }
} else { } else {
// 获取文件头 Hex (读取足够长的字节以覆盖最长的魔数PNG需8字节)
const fileHeader = this._bufferToHex(buffer.slice(0, 8)); const fileHeader = this._bufferToHex(buffer.slice(0, 8));
// 使用 startsWith 匹配
if (fileHeader.startsWith(expectedMagic)) { if (fileHeader.startsWith(expectedMagic)) {
isSafe = true; isSafe = true
} else { } else {
return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`); return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`)
} }
} }
if (isSafe) resolve(true)
if (isSafe) resolve(true);
}; };
reader.onerror = () => reject("文件读取失败,无法校验");
reader.onerror = () => reject('文件读取失败,无法校验'); if (expectedMagic === "TYPE_TEXT" && extension === "json") {
reader.readAsArrayBuffer(file)
// 读取前 1KB 进行判断
if (expectedMagic === 'TYPE_TEXT' && extension === 'json') {
// JSON 必须读全量才能 parse建议限制 JSON 文件大小
reader.readAsArrayBuffer(file);
} else { } else {
// 图片/普通文本 读取前 2KB 足够判断头部和编码特征 reader.readAsArrayBuffer(file.slice(0, 2048))
reader.readAsArrayBuffer(file.slice(0, 2048));
} }
}); })
} }
} }
// 【demo】 // FileValidator 使用文档
// 如果传入了 allowedExtensions则只使用传入的否则使用全部 KNOWN_SIGNATURES // FileValidator 是一个用于浏览器端的 JavaScript 文件校验类。它提供了比简单的后缀名检查更安全的文件验证机制。
// const imageValidator = new FileValidator({ // 主要特性
// maxSizeMB: 5, // 真实类型检测:通过读取文件二进制头部的“魔数”来验证文件类型,防止后缀名伪造。
// allowedExtensions: ['png', 'jpg', 'jpeg'], // 文本内容安全:检测文本文件是否为有效的 UTF-8 编码,防止乱码或二进制文件伪装。
// JSON 语法校验:针对 JSON 文件,会自动尝试解析以确保格式正确。
// CSV 行数限制:支持限制 CSV 文件的最大行数(需手动配置)。
// 大小限制内置文件大小检查MB
// 按需配置:支持自定义允许的文件扩展名列表。
// 使用方法
// import { FileValidator } from './fileValidator.js';
// 1. 初始化校验器
// const validator = new FileValidator({
// maxSizeMB: 5, // 限制最大 5MB
// allowedExtensions: ['jpg', 'png', 'pdf', 'docx'] // 仅允许这些格式
// }); // });
// imageValidator // // 2. 获取文件对象 (通常来自 input[type="file"])
// .validate(file) // const fileInput = document.getElementById('file-upload');
// .then(() => {
// statusDiv.textContent = `检测通过: ${file.name}`; // fileInput.addEventListener('change', async (event) => {
// statusDiv.style.color = 'green'; // const file = event.target.files[0];
// console.log('图片校验通过,开始上传...');
// // upload(file)... // if (!file) return;
// })
// .catch((err) => { // try {
// statusDiv.textContent = `检测失败: ${err}`; // // 3. 执行校验
// statusDiv.style.color = 'red'; // await validator.validate(file);
// }); // console.log('✅ 文件校验通过,可以上传');
// // 在这里执行你的上传逻辑...
// } catch (errorMessage) {
// console.error('❌ 校验失败:', errorMessage);
// alert(errorMessage);
// }
// });
// 配置项:
// maxSizeMB: 允许的最大文件大小,单位 MB。
// allowedExtensions: 允许的文件后缀列表(不区分大小写)。如果不传,则允许所有内置支持的格式。
// // 允许所有支持的格式,限制 20MB
// const v1 = new FileValidator({ maxSizeMB: 20 });
// // 仅允许图片
// const v2 = new FileValidator({
// allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp']
// });
// 设置 CSV 最大允许 1000 行
// validator.csvMaxRows = 1000;
// 分类,扩展名,检测方式
// 图片,"png, jpg, jpeg, gif, webp",二进制头签名 (Magic Number)
// 文档,"pdf, docx, xlsx, pptx, doc, xls, ppt",二进制头签名 (Magic Number)
// 文本,"txt, csv, md",UTF-8 编码检测 + 无 Null 字节检测
// 数据,json,UTF-8 编码检测 + JSON.parse 语法校验