前端——利用File signature精准校验文件类型

我心飞翔 分类:javascript

背景:在进行文件上传时,往往需要对上传文件的类型进行限制。最简单也是最常用的文件类型校验方法,是直接校验文件的拓展名,但由于拓展名可以手动随意修改,因此这种方式并不保险。那么有没有什么方法可以准确地判断出上传文件的类型呢?

File signature

File signature是指在文件中用于标识文件格式的字节,通常在文件的开头放置一小段字节(大多数为2-4个字节)。不同的文件类型都有着对应的文件签名,通过 List of file signatures 和 All File Signatures我们可以查询到各个文件类型的签名。

通过File signature校验文件类型

检验思路

  1. 获取文件,并将文件转化为ArrayBuffer

文件可通过网络获取或本地获取,对应的转化方法:

/**
* 获取网络文件并转化为ArrayBuffer
*/
function loadFile(fileUrl) {
 return new Promise((resolve, reject) => {
   const xhr = new XMLHttpRequest();
   xhr.onload = function () {
     if (xhr.status === 200) {
       resolve(xhr.response);
     }
   };
   xhr.onerror = reject;
   xhr.open('GET', fileUrl, true);
   xhr.responseType = 'arraybuffer';
   xhr.send('');
 })
}
 
/**
* 获取本地文件并转化为ArrayBuffer
*/

//input[type="file"].onchange 回调事件
function onFileChange(file) {
  return new Promise(async (reslove, reject) => {
    if (!file) {
      return reslove(false);
    }
   const FR = new FileReader();
   const fileChunk = file.slice(offset, size + offset);
   FR.readAsArrayBuffer(fileChunk);
   FR.onload = (e) => {
       //e.target.result便是所需ArrayBuffer
       const ArrayBuffer = e.target.result;
      //do something
    }
  });
}
 
  1. 将buffer转化为16进制字符串

通过TypedArray进行转化:

Array.prototype.map.call(new Uint8Array(ArrayBuffer), (x) => x.toString(16).padStart(2, '0')).join('');
 

但TypedArray将会使用系统默认的字节顺序(详情见TypedArray or DataView: Understanding byte order),而使用DataView 则会默认使用从大到小的顺序:

const view = new DataView(ArrayBuffer);
let hex= '';
for (let i = 0; i < view.byteLength; i += 1) {
  let byte = Number(view.getUint8(i)).toString(16).toUpperCase();
  byte.length === 1 && (byte = '0' + byte);
  hex += byte;
}
 
  1. 根据文件后缀名截取对应文件签名并进行比对

如针对pdf的文件签名

    {
      extension: 'ppt',
      signature: '006E1EF0',
      size: 4,
      offset: 512,
    },
 

可截取16进制字符串中的第offset到offset+size个个byte与signature进行比对

const fileSignature = hex.slice(offset * 2, (size + offset) * 2);
 

优化

可根据文件的后缀名得到对应文件签名的offset与size,直接截取文件中文件签名的内容,而无需加载文件的全部二进制数据。

完整代码

//文件签名列表,作为文件上传类型的白名单
const fileSignatures = {
  pdf: [
    {
      extension: 'pdf',
      signature: '25504446',
      size: 4,
      offset: 0,
    },
  ],
  //....
};


function getfileSignature(file, size, offset) {
  return new Promise((resolve, reject) => {
    try {
      const FR = new FileReader();
      const fileChunk = file.slice(offset, size + offset);
      FR.readAsArrayBuffer(fileChunk);
      FR.onload = (e) => {
        const view = new DataView(e.target.result as ArrayBufferLike);
        let fileSignature = '';
        for (let i = 0; i < view.byteLength; i += 1) {
          let byte = Number(view.getUint8(i)).toString(16).toUpperCase();
          byte.length === 1 && (byte = '0' + byte);
          fileSignature += byte;
        }

        resolve(fileSignature);
      };
    } catch (e) {
      reject(e);
    }
  });
}

function judgeFileType(file) {
  return new Promise(async (reslove, reject) => {
    if (!file) {
      return reslove(false);
    }
    try {
      const fileInfoArr = file.name.split('.');
      const fileType = fileInfoArr[fileInfoArr.length - 1];

      if (fileSignatures[fileType]) {
        for (let i = 0; i < fileSignatures[fileType].length; i++) {
          const fileSignature = await getfileSignature(file, fileSignatures[fileType][i].size, fileSignatures[fileType][i].offset);
          if (fileSignature === fileSignatures[fileType][i].signature) {
            return reslove(true);
          } else if (i === fileSignatures[fileType].length - 1) {
            return reslove(false);
          }
        }
      } else {
        return reslove(false);
      }
    } catch (e) {
      reject(e);
    }
  });
}
 

作者:掘金-前端——利用File signature精准校验文件类型

回复

我来回复
  • 暂无回复内容