Skip to content

Instantly share code, notes, and snippets.

@jonchurch
Last active July 19, 2023 20:52
Show Gist options
  • Save jonchurch/077bbbb3f0cc199f5b0db64940fe1d55 to your computer and use it in GitHub Desktop.
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
/**
* 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;
}
@jonchurch
Copy link
Author

jonchurch commented Jul 19, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment