const KNOWN_SIGNATURES = { png: "89504E470D0A1A0A", jpg: "FFD8FF", jpeg: "FFD8FF", gif: "47494638", webp: "52494646", docx: "504B0304", xlsx: "504B0304", pptx: "504B0304", doc: "D0CF11E0", xls: "D0CF11E0", ppt: "D0CF11E0", pdf: "25504446", txt: "TYPE_TEXT", csv: "TYPE_TEXT", md: "TYPE_TEXT", json: "TYPE_TEXT" }; export class FileValidator { version = "1.0.0"; signs = Object.keys(KNOWN_SIGNATURES); constructor(options = {}) { this.maxSizeMB = options.maxSizeMB || 10; if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) { this.allowedConfig = {}; options.allowedExtensions.forEach(ext => { const key = ext.toLowerCase(); if (KNOWN_SIGNATURES[key]) { this.allowedConfig[key] = KNOWN_SIGNATURES[key] } else { console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`) } }) } else { this.allowedConfig = { ...KNOWN_SIGNATURES } } } _isValidUTF8(buffer) { try { const decoder = new TextDecoder("utf-8", { fatal: true }); decoder.decode(buffer); return true } catch (e) { return false } } _bufferToHex(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), x => ("00" + x.toString(16)).slice(-2)).join("") .toUpperCase() } _countCSVRows(buffer) { const decoder = new TextDecoder("utf-8"); const text = decoder.decode(buffer); 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++ } } if (len > 0 && text[len - 1] !== "\n") { rowCount++ } return rowCount } _validateTextContent(buffer, extension) { let contentStr = ""; try { const decoder = new TextDecoder("utf-8", { fatal: true }); contentStr = decoder.decode(buffer) } catch (e) { console.warn("UTF-8 解码失败", e); return false } if (contentStr.includes("\0")) { return false } if (extension === "json") { try { JSON.parse(contentStr) } catch (e) { console.warn("无效的 JSON 格式"); return false } } return true } validate(file) { return new Promise((resolve, reject) => { if (!file || !file.name) return reject("无效的文件对象"); if (file.size > this.maxSizeMB * 1024 * 1024) { return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`) } const fileName = file.name.toLowerCase(); const extension = fileName.substring(fileName.lastIndexOf(".") + 1); const expectedMagic = this.allowedConfig[extension]; if (!expectedMagic) { return reject(`不支持的文件格式: .${extension}`) } const reader = new FileReader; reader.onload = e => { const buffer = e.target.result; let isSafe = false; if (expectedMagic === "TYPE_TEXT") { if (this._validateTextContent(buffer, extension)) { isSafe = true } else { if (extension === "json") { return reject(`文件异常:不是有效的 JSON 文件`) } return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`) } if (extension === "csv" && this.csvMaxRows > 0) { const rows = this._countCSVRows(buffer); if (rows > this.csvMaxRows) { return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`) } } } else { const fileHeader = this._bufferToHex(buffer.slice(0, 8)); if (fileHeader.startsWith(expectedMagic)) { isSafe = true } else { return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`) } } if (isSafe) resolve(true) }; reader.onerror = () => reject("文件读取失败,无法校验"); if (expectedMagic === "TYPE_TEXT" && extension === "json") { reader.readAsArrayBuffer(file) } else { reader.readAsArrayBuffer(file.slice(0, 2048)) } }) } } // FileValidator 使用文档 // FileValidator 是一个用于浏览器端的 JavaScript 文件校验类。它提供了比简单的后缀名检查更安全的文件验证机制。 // 主要特性 // 真实类型检测:通过读取文件二进制头部的“魔数”来验证文件类型,防止后缀名伪造。 // 文本内容安全:检测文本文件是否为有效的 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'] // 仅允许这些格式 // }); // // 2. 获取文件对象 (通常来自 input[type="file"]) // const fileInput = document.getElementById('file-upload'); // fileInput.addEventListener('change', async (event) => { // const file = event.target.files[0]; // if (!file) return; // try { // // 3. 执行校验 // 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 语法校验