Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,15 @@ Usage:


```js
define([
'filevalidator',
],
import { verifyFileType } from 'filevalidator';

function(filevalidator) {
const fileInput = document.getElementById('file-input');

var fileInput = document.getElementById('file-input');

fileInput.addEventListener('change', function(e) {
var file = e.currentTarget.files[0];
filevalidator.verifyFileType(file, ['mp3', 'wav'], function(valid) {
alert('Valid mp3 or wave file: ' + !!valid);
});
fileInput.addEventListener('change', function(e) {
const file = e.currentTarget.files[0];
verifyFileType(file, ['mp3', 'wav'], function(valid) {
alert('Valid mp3 or wave file: ' + !!valid);
});

});
```

172 changes: 80 additions & 92 deletions filevalidator.js
Original file line number Diff line number Diff line change
@@ -1,99 +1,87 @@
define([
'underscore',
],
'use strict';

function(_) {
'use strict';
// https://en.wikipedia.org/wiki/List_of_file_signatures
const fileSignatures = {
'mp3': [
// MPEG-1 Layer 3 file without an ID3 tag or with an ID3v1 tag (which's appended at the end of the file)
[0xFF, 0xFB],
// MP3 file with an ID3v2 container
[0x49, 0x44, 0x33],
// Other MP3 files (FF Fx and FF Ex – they may cause false-positives)
// Headers taken from https://www.garykessler.net/library/file_sigs.html
[0xFF, 0xE0], [0xFF, 0xE1], [0xFF, 0xE2], [0xFF, 0xE3], [0xFF, 0xE4],
[0xFF, 0xE5], [0xFF, 0xE6], [0xFF, 0xE7], [0xFF, 0xE8], [0xFF, 0xE9],
[0xFF, 0xEA], [0xFF, 0xEB], [0xFF, 0xEC], [0xFF, 0xED], [0xFF, 0xEE], [0xFF, 0xEF],
[0xFF, 0xF0], [0xFF, 0xF1], [0xFF, 0xF2], [0xFF, 0xF3], [0xFF, 0xF4],
[0xFF, 0xF5], [0xFF, 0xF6], [0xFF, 0xF7], [0xFF, 0xF8], [0xFF, 0xF9],
[0xFF, 0xFA], [0xFF, 0xFB], [0xFF, 0xFC], [0xFF, 0xFD], [0xFF, 0xFE], [0xFF, 0xFF]
],
'wav': [
// Waveform Audio File Format
// Empty slots can be any byte. Can't look at only first 4 or else .avi files match
[0x52, 0x49, 0x46, 0x46, , , , , 0x57, 0x41, 0x56, 0x45]
],
};

// https://en.wikipedia.org/wiki/List_of_file_signatures
var fileSignatures = {
'mp3': [
// MPEG-1 Layer 3 file without an ID3 tag or with an ID3v1 tag (which's appended at the end of the file)
[0xFF, 0xFB],
// MP3 file with an ID3v2 container
[0x49, 0x44, 0x33],
// Other MP3 files (FF Fx and FF Ex – they may cause false-positives)
// Headers taken from https://www.garykessler.net/library/file_sigs.html
[0xFF, 0xE0], [0xFF, 0xE1], [0xFF, 0xE2], [0xFF, 0xE3], [0xFF, 0xE4],
[0xFF, 0xE5], [0xFF, 0xE6], [0xFF, 0xE7], [0xFF, 0xE8], [0xFF, 0xE9],
[0xFF, 0xEA], [0xFF, 0xEB], [0xFF, 0xEC], [0xFF, 0xED], [0xFF, 0xEE], [0xFF, 0xEF],
[0xFF, 0xF0], [0xFF, 0xF1], [0xFF, 0xF2], [0xFF, 0xF3], [0xFF, 0xF4],
[0xFF, 0xF5], [0xFF, 0xF6], [0xFF, 0xF7], [0xFF, 0xF8], [0xFF, 0xF9],
[0xFF, 0xFA], [0xFF, 0xFB], [0xFF, 0xFC], [0xFF, 0xFD], [0xFF, 0xFE], [0xFF, 0xFF]
],
'wav': [
// Waveform Audio File Format
// Empty slots can be any byte. Can't look at only first 4 or else .avi files match
[0x52, 0x49, 0x46, 0x46, , , , , 0x57, 0x41, 0x56, 0x45]
],
};
/**
* Compare an Uint8Array with an expected file signature.
* Can't do a direct equality check since some signatures (e.gfor wav files) have wildcard slots.
* @param {array} sig - pattern from fileSignatures
* @param {Uint8Array} actual - bytes from file (should already be sliced to match length of sig)
* @returns {boolean} - do they match
*/
const compareSignature = function(sig, actual) {
if (sig.length !== actual.length) return false;
for (var i = 0, l = sig.length; i < l; i++) {
if (sig[i] !== actual[i] && typeof sig[i] !== 'undefined') return false;
}
return true;
};

/**
* Compare an Uint8Array with an expected file signature.
* Can't do a direct equality check since some signatures (e.gfor wav files) have wildcard slots.
* @param {array} sig - pattern from fileSignatures
* @param {Uint8Array} actual - bytes from file (should already be sliced to match length of sig)
* @returns {boolean} - do they match
*/
var compareSignature = function(sig, actual) {
if (sig.length !== actual.length) return false;
for (var i = 0, l = sig.length; i < l; i++) {
if (sig[i] !== actual[i] && typeof sig[i] !== 'undefined') return false;
}
return true;
};
/**
* @param {Uint8Array} uint8
* @param {string} type
* @returns {boolean}
*/
const matchesFileType = function(uint8, type) {
return fileSignatures[type].find(function(sig) {
return compareSignature(sig, uint8.subarray(0, sig.length));
});
};

/**
* @param {Uint8Array} uint8
* @param {string} type
* @returns {boolean}
*/
var matchesFileType = function(uint8, type) {
return _.find(fileSignatures[type], function(sig) {
return compareSignature(sig, uint8.subarray(0, sig.length));
});
};

/**
* Detect, through file signature / mime sniffing detection, if a given File
* matches an expected type or types. The types supported are the keys in
* fileSignatures above.
* @param {File} file
* @param {(string|string[])} types - e.g. 'mp3' or ['mp3', 'wav']
* @param {function} - callback which is passed a boolean
*/
var verifyFileType = function(file, types, cb) {
if (_.isString(types)) types = [types];

// Calculate the longest file signature for any of the requested
// types, so we know how many bytes of this file to look at.
var bytesNeeded = types.reduce(function(prevMax, type, idx, arr) {
var sigs = fileSignatures[type];
return Math.max.apply(this, [prevMax].concat(sigs.map(function(sig) {
return sig.length;
})));
}, 0);
/**
* Detect, through file signature / mime sniffing detection, if a given File
* matches an expected type or types. The types supported are the keys in
* fileSignatures above.
* @param {File} file
* @param {(string|string[])} types - e.g. 'mp3' or ['mp3', 'wav']
* @param {function} - callback which is passed a boolean
*/
const verifyFileType = function(file, types, cb) {
if (typeof types === 'string') types = [types];

// Load file into ArrayBuffer and see if its first few bytes match
// the signature of any of our requested types. Let callback know.
var reader = new FileReader();
reader.onload = function(e) {
// Load only as many bytes from the array buffer as necessary
var arrayBuffer = e.currentTarget.result;
var bytes = new Uint8Array(arrayBuffer, 0, bytesNeeded);
var match = _.find(types, function(type) {
return matchesFileType(bytes, type);
});
cb(match);
};
reader.readAsArrayBuffer(file);
};
// Calculate the longest file signature for any of the requested
// types, so we know how many bytes of this file to look at.
const bytesNeeded = types.reduce(function(prevMax, type, idx, arr) {
var sigs = fileSignatures[type];
return Math.max.apply(this, [prevMax].concat(sigs.map(function(sig) {
return sig.length;
})));
}, 0);

// Expose public interface
var FileDetector = {
verifyFileType: verifyFileType
};

return FileDetector;
});
// Load file into ArrayBuffer and see if its first few bytes match
// the signature of any of our requested types. Let callback know.
const reader = new FileReader();
reader.addEventListener('load', function(e) {
// Load only as many bytes from the array buffer as necessary
var arrayBuffer = e.currentTarget.result;
var bytes = new Uint8Array(arrayBuffer, 0, bytesNeeded);
var match = _.find(types, function(type) {
return matchesFileType(bytes, type);
});
cb(match);
});
reader.readAsArrayBuffer(file);
};

export { verifyFileType };
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"name": "filevalidator",
"version": "1.0.1",
"version": "2.0.0",
"description": "File signature validation in JavaScript",
"type": "module",
"main": "filevalidator.js",
"author": "Phil Freo <phil@philfreo.com> (http://philfreo.com/)",
"license": "MIT"
Expand Down