You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. 'use strict';
  2. const toBytes = s => Array.from(s).map(c => c.charCodeAt(0));
  3. const xpiZipFilename = toBytes('META-INF/mozilla.rsa');
  4. const oxmlContentTypes = toBytes('[Content_Types].xml');
  5. const oxmlRels = toBytes('_rels/.rels');
  6. module.exports = input => {
  7. const buf = new Uint8Array(input);
  8. if (!(buf && buf.length > 1)) {
  9. return null;
  10. }
  11. const check = (header, opts) => {
  12. opts = Object.assign({
  13. offset: 0
  14. }, opts);
  15. for (let i = 0; i < header.length; i++) {
  16. // If a bitmask is set
  17. if (opts.mask) {
  18. // If header doesn't equal `buf` with bits masked off
  19. if (header[i] !== (opts.mask[i] & buf[i + opts.offset])) {
  20. return false;
  21. }
  22. } else if (header[i] !== buf[i + opts.offset]) {
  23. return false;
  24. }
  25. }
  26. return true;
  27. };
  28. if (check([0xFF, 0xD8, 0xFF])) {
  29. return {
  30. ext: 'jpg',
  31. mime: 'image/jpeg'
  32. };
  33. }
  34. if (check([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])) {
  35. return {
  36. ext: 'png',
  37. mime: 'image/png'
  38. };
  39. }
  40. if (check([0x47, 0x49, 0x46])) {
  41. return {
  42. ext: 'gif',
  43. mime: 'image/gif'
  44. };
  45. }
  46. if (check([0x57, 0x45, 0x42, 0x50], {offset: 8})) {
  47. return {
  48. ext: 'webp',
  49. mime: 'image/webp'
  50. };
  51. }
  52. if (check([0x46, 0x4C, 0x49, 0x46])) {
  53. return {
  54. ext: 'flif',
  55. mime: 'image/flif'
  56. };
  57. }
  58. // Needs to be before `tif` check
  59. if (
  60. (check([0x49, 0x49, 0x2A, 0x0]) || check([0x4D, 0x4D, 0x0, 0x2A])) &&
  61. check([0x43, 0x52], {offset: 8})
  62. ) {
  63. return {
  64. ext: 'cr2',
  65. mime: 'image/x-canon-cr2'
  66. };
  67. }
  68. if (
  69. check([0x49, 0x49, 0x2A, 0x0]) ||
  70. check([0x4D, 0x4D, 0x0, 0x2A])
  71. ) {
  72. return {
  73. ext: 'tif',
  74. mime: 'image/tiff'
  75. };
  76. }
  77. if (check([0x42, 0x4D])) {
  78. return {
  79. ext: 'bmp',
  80. mime: 'image/bmp'
  81. };
  82. }
  83. if (check([0x49, 0x49, 0xBC])) {
  84. return {
  85. ext: 'jxr',
  86. mime: 'image/vnd.ms-photo'
  87. };
  88. }
  89. if (check([0x38, 0x42, 0x50, 0x53])) {
  90. return {
  91. ext: 'psd',
  92. mime: 'image/vnd.adobe.photoshop'
  93. };
  94. }
  95. // Zip-based file formats
  96. // Need to be before the `zip` check
  97. if (check([0x50, 0x4B, 0x3, 0x4])) {
  98. if (
  99. check([0x6D, 0x69, 0x6D, 0x65, 0x74, 0x79, 0x70, 0x65, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x65, 0x70, 0x75, 0x62, 0x2B, 0x7A, 0x69, 0x70], {offset: 30})
  100. ) {
  101. return {
  102. ext: 'epub',
  103. mime: 'application/epub+zip'
  104. };
  105. }
  106. // Assumes signed `.xpi` from addons.mozilla.org
  107. if (check(xpiZipFilename, {offset: 30})) {
  108. return {
  109. ext: 'xpi',
  110. mime: 'application/x-xpinstall'
  111. };
  112. }
  113. // https://github.com/file/file/blob/master/magic/Magdir/msooxml
  114. if (check(oxmlContentTypes, {offset: 30}) || check(oxmlRels, {offset: 30})) {
  115. const sliced = buf.subarray(4, 4 + 2000);
  116. const nextZipHeaderIndex = arr => arr.findIndex((el, i, arr) => arr[i] === 0x50 && arr[i + 1] === 0x4B && arr[i + 2] === 0x3 && arr[i + 3] === 0x4);
  117. const header2Pos = nextZipHeaderIndex(sliced);
  118. if (header2Pos !== -1) {
  119. const slicedAgain = buf.subarray(header2Pos + 8, header2Pos + 8 + 1000);
  120. const header3Pos = nextZipHeaderIndex(slicedAgain);
  121. if (header3Pos !== -1) {
  122. const offset = 8 + header2Pos + header3Pos + 30;
  123. if (check(toBytes('word/'), {offset})) {
  124. return {
  125. ext: 'docx',
  126. mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
  127. };
  128. }
  129. if (check(toBytes('ppt/'), {offset})) {
  130. return {
  131. ext: 'pptx',
  132. mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
  133. };
  134. }
  135. if (check(toBytes('xl/'), {offset})) {
  136. return {
  137. ext: 'xlsx',
  138. mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  139. };
  140. }
  141. }
  142. }
  143. }
  144. }
  145. if (
  146. check([0x50, 0x4B]) &&
  147. (buf[2] === 0x3 || buf[2] === 0x5 || buf[2] === 0x7) &&
  148. (buf[3] === 0x4 || buf[3] === 0x6 || buf[3] === 0x8)
  149. ) {
  150. return {
  151. ext: 'zip',
  152. mime: 'application/zip'
  153. };
  154. }
  155. if (check([0x75, 0x73, 0x74, 0x61, 0x72], {offset: 257})) {
  156. return {
  157. ext: 'tar',
  158. mime: 'application/x-tar'
  159. };
  160. }
  161. if (
  162. check([0x52, 0x61, 0x72, 0x21, 0x1A, 0x7]) &&
  163. (buf[6] === 0x0 || buf[6] === 0x1)
  164. ) {
  165. return {
  166. ext: 'rar',
  167. mime: 'application/x-rar-compressed'
  168. };
  169. }
  170. if (check([0x1F, 0x8B, 0x8])) {
  171. return {
  172. ext: 'gz',
  173. mime: 'application/gzip'
  174. };
  175. }
  176. if (check([0x42, 0x5A, 0x68])) {
  177. return {
  178. ext: 'bz2',
  179. mime: 'application/x-bzip2'
  180. };
  181. }
  182. if (check([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C])) {
  183. return {
  184. ext: '7z',
  185. mime: 'application/x-7z-compressed'
  186. };
  187. }
  188. if (check([0x78, 0x01])) {
  189. return {
  190. ext: 'dmg',
  191. mime: 'application/x-apple-diskimage'
  192. };
  193. }
  194. if (check([0x33, 0x67, 0x70, 0x35]) || // 3gp5
  195. (
  196. check([0x0, 0x0, 0x0]) && check([0x66, 0x74, 0x79, 0x70], {offset: 4}) &&
  197. (
  198. check([0x6D, 0x70, 0x34, 0x31], {offset: 8}) || // MP41
  199. check([0x6D, 0x70, 0x34, 0x32], {offset: 8}) || // MP42
  200. check([0x69, 0x73, 0x6F, 0x6D], {offset: 8}) || // ISOM
  201. check([0x69, 0x73, 0x6F, 0x32], {offset: 8}) || // ISO2
  202. check([0x6D, 0x6D, 0x70, 0x34], {offset: 8}) || // MMP4
  203. check([0x4D, 0x34, 0x56], {offset: 8}) || // M4V
  204. check([0x64, 0x61, 0x73, 0x68], {offset: 8}) // DASH
  205. )
  206. )) {
  207. return {
  208. ext: 'mp4',
  209. mime: 'video/mp4'
  210. };
  211. }
  212. if (check([0x4D, 0x54, 0x68, 0x64])) {
  213. return {
  214. ext: 'mid',
  215. mime: 'audio/midi'
  216. };
  217. }
  218. // https://github.com/threatstack/libmagic/blob/master/magic/Magdir/matroska
  219. if (check([0x1A, 0x45, 0xDF, 0xA3])) {
  220. const sliced = buf.subarray(4, 4 + 4096);
  221. const idPos = sliced.findIndex((el, i, arr) => arr[i] === 0x42 && arr[i + 1] === 0x82);
  222. if (idPos !== -1) {
  223. const docTypePos = idPos + 3;
  224. const findDocType = type => Array.from(type).every((c, i) => sliced[docTypePos + i] === c.charCodeAt(0));
  225. if (findDocType('matroska')) {
  226. return {
  227. ext: 'mkv',
  228. mime: 'video/x-matroska'
  229. };
  230. }
  231. if (findDocType('webm')) {
  232. return {
  233. ext: 'webm',
  234. mime: 'video/webm'
  235. };
  236. }
  237. }
  238. }
  239. if (check([0x0, 0x0, 0x0, 0x14, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20]) ||
  240. check([0x66, 0x72, 0x65, 0x65], {offset: 4}) ||
  241. check([0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20], {offset: 4}) ||
  242. check([0x6D, 0x64, 0x61, 0x74], {offset: 4}) || // MJPEG
  243. check([0x77, 0x69, 0x64, 0x65], {offset: 4})) {
  244. return {
  245. ext: 'mov',
  246. mime: 'video/quicktime'
  247. };
  248. }
  249. if (
  250. check([0x52, 0x49, 0x46, 0x46]) &&
  251. check([0x41, 0x56, 0x49], {offset: 8})
  252. ) {
  253. return {
  254. ext: 'avi',
  255. mime: 'video/x-msvideo'
  256. };
  257. }
  258. if (check([0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9])) {
  259. return {
  260. ext: 'wmv',
  261. mime: 'video/x-ms-wmv'
  262. };
  263. }
  264. if (check([0x0, 0x0, 0x1, 0xBA])) {
  265. return {
  266. ext: 'mpg',
  267. mime: 'video/mpeg'
  268. };
  269. }
  270. // Check for MP3 header at different starting offsets
  271. for (let start = 0; start < 2 && start < (buf.length - 16); start++) {
  272. if (
  273. check([0x49, 0x44, 0x33], {offset: start}) || // ID3 header
  274. check([0xFF, 0xE2], {offset: start, mask: [0xFF, 0xE2]}) // MPEG 1 or 2 Layer 3 header
  275. ) {
  276. return {
  277. ext: 'mp3',
  278. mime: 'audio/mpeg'
  279. };
  280. }
  281. }
  282. if (
  283. check([0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x41], {offset: 4}) ||
  284. check([0x4D, 0x34, 0x41, 0x20])
  285. ) {
  286. return {
  287. ext: 'm4a',
  288. mime: 'audio/m4a'
  289. };
  290. }
  291. // Needs to be before `ogg` check
  292. if (check([0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64], {offset: 28})) {
  293. return {
  294. ext: 'opus',
  295. mime: 'audio/opus'
  296. };
  297. }
  298. if (check([0x4F, 0x67, 0x67, 0x53])) {
  299. return {
  300. ext: 'ogg',
  301. mime: 'audio/ogg'
  302. };
  303. }
  304. if (check([0x66, 0x4C, 0x61, 0x43])) {
  305. return {
  306. ext: 'flac',
  307. mime: 'audio/x-flac'
  308. };
  309. }
  310. if (
  311. check([0x52, 0x49, 0x46, 0x46]) &&
  312. check([0x57, 0x41, 0x56, 0x45], {offset: 8})
  313. ) {
  314. return {
  315. ext: 'wav',
  316. mime: 'audio/x-wav'
  317. };
  318. }
  319. if (check([0x23, 0x21, 0x41, 0x4D, 0x52, 0x0A])) {
  320. return {
  321. ext: 'amr',
  322. mime: 'audio/amr'
  323. };
  324. }
  325. if (check([0x25, 0x50, 0x44, 0x46])) {
  326. return {
  327. ext: 'pdf',
  328. mime: 'application/pdf'
  329. };
  330. }
  331. if (check([0x4D, 0x5A])) {
  332. return {
  333. ext: 'exe',
  334. mime: 'application/x-msdownload'
  335. };
  336. }
  337. if (
  338. (buf[0] === 0x43 || buf[0] === 0x46) &&
  339. check([0x57, 0x53], {offset: 1})
  340. ) {
  341. return {
  342. ext: 'swf',
  343. mime: 'application/x-shockwave-flash'
  344. };
  345. }
  346. if (check([0x7B, 0x5C, 0x72, 0x74, 0x66])) {
  347. return {
  348. ext: 'rtf',
  349. mime: 'application/rtf'
  350. };
  351. }
  352. if (check([0x00, 0x61, 0x73, 0x6D])) {
  353. return {
  354. ext: 'wasm',
  355. mime: 'application/wasm'
  356. };
  357. }
  358. if (
  359. check([0x77, 0x4F, 0x46, 0x46]) &&
  360. (
  361. check([0x00, 0x01, 0x00, 0x00], {offset: 4}) ||
  362. check([0x4F, 0x54, 0x54, 0x4F], {offset: 4})
  363. )
  364. ) {
  365. return {
  366. ext: 'woff',
  367. mime: 'font/woff'
  368. };
  369. }
  370. if (
  371. check([0x77, 0x4F, 0x46, 0x32]) &&
  372. (
  373. check([0x00, 0x01, 0x00, 0x00], {offset: 4}) ||
  374. check([0x4F, 0x54, 0x54, 0x4F], {offset: 4})
  375. )
  376. ) {
  377. return {
  378. ext: 'woff2',
  379. mime: 'font/woff2'
  380. };
  381. }
  382. if (
  383. check([0x4C, 0x50], {offset: 34}) &&
  384. (
  385. check([0x00, 0x00, 0x01], {offset: 8}) ||
  386. check([0x01, 0x00, 0x02], {offset: 8}) ||
  387. check([0x02, 0x00, 0x02], {offset: 8})
  388. )
  389. ) {
  390. return {
  391. ext: 'eot',
  392. mime: 'application/octet-stream'
  393. };
  394. }
  395. if (check([0x00, 0x01, 0x00, 0x00, 0x00])) {
  396. return {
  397. ext: 'ttf',
  398. mime: 'font/ttf'
  399. };
  400. }
  401. if (check([0x4F, 0x54, 0x54, 0x4F, 0x00])) {
  402. return {
  403. ext: 'otf',
  404. mime: 'font/otf'
  405. };
  406. }
  407. if (check([0x00, 0x00, 0x01, 0x00])) {
  408. return {
  409. ext: 'ico',
  410. mime: 'image/x-icon'
  411. };
  412. }
  413. if (check([0x46, 0x4C, 0x56, 0x01])) {
  414. return {
  415. ext: 'flv',
  416. mime: 'video/x-flv'
  417. };
  418. }
  419. if (check([0x25, 0x21])) {
  420. return {
  421. ext: 'ps',
  422. mime: 'application/postscript'
  423. };
  424. }
  425. if (check([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00])) {
  426. return {
  427. ext: 'xz',
  428. mime: 'application/x-xz'
  429. };
  430. }
  431. if (check([0x53, 0x51, 0x4C, 0x69])) {
  432. return {
  433. ext: 'sqlite',
  434. mime: 'application/x-sqlite3'
  435. };
  436. }
  437. if (check([0x4E, 0x45, 0x53, 0x1A])) {
  438. return {
  439. ext: 'nes',
  440. mime: 'application/x-nintendo-nes-rom'
  441. };
  442. }
  443. if (check([0x43, 0x72, 0x32, 0x34])) {
  444. return {
  445. ext: 'crx',
  446. mime: 'application/x-google-chrome-extension'
  447. };
  448. }
  449. if (
  450. check([0x4D, 0x53, 0x43, 0x46]) ||
  451. check([0x49, 0x53, 0x63, 0x28])
  452. ) {
  453. return {
  454. ext: 'cab',
  455. mime: 'application/vnd.ms-cab-compressed'
  456. };
  457. }
  458. // Needs to be before `ar` check
  459. if (check([0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0x0A, 0x64, 0x65, 0x62, 0x69, 0x61, 0x6E, 0x2D, 0x62, 0x69, 0x6E, 0x61, 0x72, 0x79])) {
  460. return {
  461. ext: 'deb',
  462. mime: 'application/x-deb'
  463. };
  464. }
  465. if (check([0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E])) {
  466. return {
  467. ext: 'ar',
  468. mime: 'application/x-unix-archive'
  469. };
  470. }
  471. if (check([0xED, 0xAB, 0xEE, 0xDB])) {
  472. return {
  473. ext: 'rpm',
  474. mime: 'application/x-rpm'
  475. };
  476. }
  477. if (
  478. check([0x1F, 0xA0]) ||
  479. check([0x1F, 0x9D])
  480. ) {
  481. return {
  482. ext: 'Z',
  483. mime: 'application/x-compress'
  484. };
  485. }
  486. if (check([0x4C, 0x5A, 0x49, 0x50])) {
  487. return {
  488. ext: 'lz',
  489. mime: 'application/x-lzip'
  490. };
  491. }
  492. if (check([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1])) {
  493. return {
  494. ext: 'msi',
  495. mime: 'application/x-msi'
  496. };
  497. }
  498. if (check([0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x02])) {
  499. return {
  500. ext: 'mxf',
  501. mime: 'application/mxf'
  502. };
  503. }
  504. if (check([0x47], {offset: 4}) && (check([0x47], {offset: 192}) || check([0x47], {offset: 196}))) {
  505. return {
  506. ext: 'mts',
  507. mime: 'video/mp2t'
  508. };
  509. }
  510. if (check([0x42, 0x4C, 0x45, 0x4E, 0x44, 0x45, 0x52])) {
  511. return {
  512. ext: 'blend',
  513. mime: 'application/x-blender'
  514. };
  515. }
  516. if (check([0x42, 0x50, 0x47, 0xFB])) {
  517. return {
  518. ext: 'bpg',
  519. mime: 'image/bpg'
  520. };
  521. }
  522. return null;
  523. };