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)); } }); } } // 【demo】 // 如果传入了 allowedExtensions,则只使用传入的;否则使用全部 KNOWN_SIGNATURES // const imageValidator = new FileValidator({ // maxSizeMB: 5, // allowedExtensions: ['png', 'jpg', 'jpeg'], // }); // imageValidator // .validate(file) // .then(() => { // statusDiv.textContent = `检测通过: ${file.name}`; // statusDiv.style.color = 'green'; // console.log('图片校验通过,开始上传...'); // // upload(file)... // }) // .catch((err) => { // statusDiv.textContent = `检测失败: ${err}`; // statusDiv.style.color = 'red'; // });