UNPKG

detect-file-type

Version:
439 lines (333 loc) 11.4 kB
"use strict"; var _fs = _interopRequireDefault(require("fs")); var _signatures = _interopRequireDefault(require("../signatures.json")); var _jschardet = _interopRequireDefault(require("jschardet")); var _iconvLite = _interopRequireDefault(require("iconv-lite")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** @type {(function(Buffer):FileTypeResult)[]} */ const customFunctions = []; const noopCallback = () => {}; const DEFAULT_BUFFER_SIZE = 500; const iconvOptions = { stripBOM: true }; let validatedSignaturesCache = false; /** * @typedef {Object} FileTypeResult * @property {string} ext * @property {string} mime * @property {string=} iana */ /** */ class DetectFileType { /** * @param {string} filePath * @param {number=} bufferLength * @param {function(Error=,FileTypeResult)} callback */ static fromFile(filePath, bufferLength, callback) { if (typeof bufferLength === 'function') { callback = bufferLength; bufferLength = undefined; } DetectFileType._getFileSize(filePath, (err, fileSize) => { if (err) { return callback(err); } _fs.default.open(filePath, 'r', (err, fd) => { if (err) { return callback(err); } DetectFileType.fromFd(fd, Math.min(bufferLength || DEFAULT_BUFFER_SIZE, fileSize), callback); }); }); } /** * @param {number} fd * @param {number=} bufferLength * @param {function(Error=,FileTypeResult)} callback */ static fromFd(fd, bufferLength, callback) { if (typeof bufferLength === 'function') { callback = bufferLength; bufferLength = undefined; } let bufferSize = bufferLength; if (!bufferSize) { bufferSize = DEFAULT_BUFFER_SIZE; } const buffer = new Buffer(bufferSize); _fs.default.read(fd, buffer, 0, bufferSize, 0, (err, data) => { _fs.default.close(fd, noopCallback); if (err) { return callback(err); } DetectFileType.fromBuffer(buffer, callback); }); } /** * @param {Buffer} buffer * @param {function(Error=,FileTypeResult)} callback */ static fromBuffer(buffer, callback) { let result = null; if (!validatedSignaturesCache) { validatedSignaturesCache = DetectFileType._validateSigantures(); } if (Array.isArray(validatedSignaturesCache)) { return callback(validatedSignaturesCache); } if (!(buffer instanceof Buffer)) buffer = Buffer.from(buffer); _signatures.default.every(signature => { let detection = DetectFileType._detect(buffer, signature.rules); if (!detection && signature.recode_text === true) { let textBuffer = DetectFileType._getTextBuffer(buffer); if (textBuffer !== null) { detection = DetectFileType._detect(textBuffer, signature.rules); } } if (buffer.textRecoded !== undefined) delete buffer.textRecoded; if (detection) { result = DetectFileType._getRuleDetection({}, signature, detection); return false; } return true; }); if (result === null) { customFunctions.every(fn => { const fnResult = fn(buffer); if (fnResult) { result = fnResult; return false; } ; return true; }); } callback(null, result); } static addSignature(signature) { validatedSignaturesCache = false; _signatures.default.push(signature); } /** @param {function(Buffer):FileTypeResult} fn */ static addCustomFunction(fn) { customFunctions.push(fn); } /** @private */ static _detect(buffer, rules, type, searchData, tryTextBuffer) { if (!type) { type = 'and'; } let detectedRule = true; const ruleEvaluator = rule => { let result = true; // Process search rule if (typeof rule.search === 'object') { let searchRule = rule.search; // Elevate bytes into a buffer if (!(searchRule.bytes instanceof Buffer)) searchRule.bytes = Buffer.from(searchRule.bytes, typeof searchRule.bytes === 'string' ? 'hex' : null); // Figure out start/end let start = searchRule.start || 0; let end = searchRule.end; // Offset start/end based on a previous search if (searchRule.hasOwnProperty('search_ref')) { const index = searchData ? searchData.get(searchRule.search_ref) : -1; if (index === -1) { start = -1; } else { start += index; end += index; } } // Limit end to buffer length (otherwise an error is thrown) end = Math.min(typeof end === 'number' ? end : buffer.length, buffer.length); // Search for those bytes let index = start === -1 ? -1 : buffer.indexOf(searchRule.bytes, undefined, undefined, start, end); if (index < 0) { detectedRule = this._getRuleDetection(detectedRule, false); return this._isReturnFalse(detectedRule, type); } searchData = searchData || new Map(); searchData.set(searchRule.id, index); } if (rule.type === 'or') { result = this._detect(buffer, rule.rules, 'or', searchData); if (!result && rule.recode_text === true) { let textBuffer = this._getTextBuffer(buffer); if (textBuffer !== null) { result = this._detect(textBuffer, rule.rules, 'or', searchData); } } } else if (rule.type === 'and') { result = this._detect(buffer, rule.rules, 'and', searchData); if (!result && rule.recode_text === true) { let textBuffer = this._getTextBuffer(buffer); if (textBuffer !== null) { result = this._detect(textBuffer, rule.rules, 'and', searchData); } } } else if (rule.type === 'default') { result = rule; } else { // Elevate bytes into a buffer if (!(rule.bytes instanceof Buffer)) rule.bytes = Buffer.from(rule.bytes, typeof rule.bytes === 'string' ? 'hex' : null); // Figure out start/end let start = rule.start || 0; let end = rule.end; // Offset start/end based on a previous search if (rule.hasOwnProperty('search_ref')) { const index = searchData ? searchData.get(rule.search_ref) : -1; if (index === -1) { start = -1; } else { start += index; end += index; } } // Limit end to buffer length (otherwise an error is thrown) end = Math.min(typeof end === 'number' ? end : buffer.length, buffer.length); if (start < 0) { result = false; } else if (rule.type === 'equal') { result = buffer.compare(rule.bytes, undefined, undefined, start, end) === 0; } else if (rule.type === 'notEqual') { result = buffer.compare(rule.bytes, undefined, undefined, rule.start || 0, end) !== 0; } else if (rule.type === 'contains') { result = buffer.slice(rule.start || 0, rule.end || buffer.length).includes(rule.bytes); } else if (rule.type === 'notContains') { result = !buffer.slice(rule.start || 0, rule.end || buffer.length).includes(rule.bytes); } } if (result === true) result = rule; detectedRule = this._getRuleDetection(detectedRule, result); return this._isReturnFalse(detectedRule, type); }; rules.every(ruleEvaluator); return detectedRule; } /** @private */ static _isReturnFalse(isDetected, type) { if (!isDetected && type === 'and') { return false; } if (isDetected && type === 'or') { return false; } return true; } /** @private */ static _validateRuleType(rule) { const types = ['or', 'and', 'contains', 'notContains', 'equal', 'notEqual', 'default']; return types.indexOf(rule.type) !== -1; } /** @private */ static _validateSigantures() { let invalidSignatures = _signatures.default.map(signature => { return this._validateSignature(signature); }).filter(Boolean); if (invalidSignatures.length) { return invalidSignatures; } return true; } /** @private */ static _validateSignature(signature) { if (!('type' in signature)) { return { message: 'signature does not contain "type" field', signature }; } if (!('rules' in signature)) { return { message: 'signature does not contain "rules" field', signature }; } const validations = this._validateRules(signature.rules); if (!('ext' in signature) && !validations.hasExt) { return { message: 'signature does not contain "ext" field', signature }; } if (!('mime' in signature) && !validations.hasMime) { return { message: 'signature does not contain "mime" field', signature }; } if (Array.isArray(validations)) { return { message: 'signature has invalid rule', signature, rules: validations }; } } /** @private */ static _validateRules(rules) { let validations = rules.map(rule => { let isRuleTypeValid = this._validateRuleType(rule); if (!isRuleTypeValid) { return { message: 'rule type not supported', rule }; } if ((rule.type === 'or' || rule.type === 'and') && !('rules' in rule)) { return { message: 'rule should contains "rules" field', rule }; } if (rule.type === 'or' || rule.type === 'and') { return this._validateRules(rule.rules); } return { hasExt: 'ext' in rule, hasMime: 'mime' in rule }; }); let invalid = validations.filter(x => x.message); let valid = validations.filter(x => !x.message); if (!invalid) return invalid; return { hasExt: valid.some(x => x.hasExt), hasMime: valid.some(x => x.hasMime) }; } /** @private */ static _getFileSize(filePath, callback) { _fs.default.stat(filePath, (err, stat) => { if (err) { return callback(err); } return callback(null, stat.size); }); } /** @private */ static _getRuleDetection() { let v = false; for (let i = 0, len = arguments.length; i < len; i++) { let detection = arguments[i]; if (typeof detection === 'boolean') { v = detection ? v || detection : false; } else { v = typeof v === 'boolean' ? {} : v; if ('ext' in detection) v.ext = detection.ext; if ('mime' in detection) v.mime = detection.mime; if ('iana' in detection) v.iana = detection.iana; } } return v; } static _getTextBuffer(buffer) { if (buffer.textRecoded === undefined) { let textBuffer = null; try { let detected = _jschardet.default.detect(buffer); if (detected) { textBuffer = Buffer.from(_iconvLite.default.decode(buffer, detected.encoding, iconvOptions)); if (buffer.equals(textBuffer)) textBuffer = null; } } catch (ignored) {} buffer.textRecoded = textBuffer; } return buffer.textRecoded; } } /** @type {typeof DetectFileType} */ module.exports = DetectFileType;