flat: ok
This commit is contained in:
@@ -1,210 +0,0 @@
|
||||
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 语法校验
|
||||
Reference in New Issue
Block a user