Last active
July 19, 2023 20:52
-
-
Save jonchurch/077bbbb3f0cc199f5b0db64940fe1d55 to your computer and use it in GitHub Desktop.
Typescript for checking a file's magic bytes to determine what mimetype it should have
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Reads the first 8 bytes (magic bytes) from the provided file. | |
* | |
* @param {File} file - The file from which the magic bytes are to be read. | |
* @returns {Promise<Uint8Array>} A promise that resolves to a `Uint8Array` containing the first 8 bytes of the file. | |
* @throws {Error} Throws an error if the file cannot be read or if there's another reading issue. | |
* | |
* @example | |
* const file = new File(["content"], "filename.txt"); | |
* sniffMagicBytes(file).then(bytes => { | |
* console.log(bytes); | |
* }).catch(error => { | |
* console.error(error); | |
* }); | |
*/ | |
export const sniffMagicBytes = (file: File): Promise<Uint8Array> => new Promise((resolve, reject) => { | |
const reader = new FileReader() | |
reader.onload = function(e) { | |
if (e.target && e.target.result ) { | |
// TS doesn't know e.target.result will be an ArrayBuffer in the onload callback | |
// its type is controlled by what readAs* method we call on the reader | |
if (e.target.result instanceof ArrayBuffer) { | |
const arrayBuffer = e.target.result | |
const uint8Array = new Uint8Array(arrayBuffer, 0, 8) | |
resolve(uint8Array) | |
} | |
} else { | |
reject(new Error("Unable to read file.")) | |
} | |
} | |
reader.onerror = () => { | |
reject(new Error('Error reading file')) | |
} | |
reader.readAsArrayBuffer(file.slice(0,8)) | |
}) | |
export function isPNGSignature(bytes: Uint8Array): boolean { | |
// PNG signature in bytes: 137 80 78 71 13 10 26 10 | |
const pngSignature: Uint8Array = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); | |
// Ensure the provided Uint8Array has at least 8 bytes | |
if (bytes.length < 8) { | |
return false; | |
} | |
for (let i = 0; i < 8; i++) { | |
if (bytes[i] !== pngSignature[i]) { | |
return false; | |
} | |
} | |
return true; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This type of deep introspection of a supplied file is useful because
<input type="file" accept="image/png">
doesn't actually sniff mimetypes in the browser's filepicker. It will rely on extensions, or OS level metadata when extension is hidden (can't confirm this, but I do suspect it!). Makes sense, would you want a browser to sniff all files on your local disk just to determine what they are under the hood? No.Strangely enough, though, even after picking a file the browser doesn't seem to sniff the magic bytes. It will happily report
File.type === 'image/png'
even for a JPG that you simply renamed with a PNG extension. So we end up not being able to trust mimetypes in this situation.Everyone will tell you not to do this on the frontend. There are many good reasons for that. Chief among them is likely just what the heck do you tell your user?
Frontend validation of mimetypes is best suited for giving useful information to the user. In this case, I really don't know what I'd tell my user. "Your file is not a PNG, despite all appearances" or "Something is wrong with your image, please provide a valid PNG"
That being said, this code does work, you can do this, and it might be the right solution for you. So here you go, internet.