From 043e4f0a08fa9e1966b9733c58f33ca93d45da50 Mon Sep 17 00:00:00 2001 From: Corentin THOMASSET Date: Mon, 13 Nov 2023 11:44:23 +0100 Subject: [PATCH] fix(base64-file-converter): fix downloading of index.html content without data preambula (#750) * fix(base64-file-converter): fix downloading of index.html content without data preambula * feat(base64-file-converter): infer mime type from base64 signature --------- Co-authored-by: akharlov --- src/composable/downloadBase64.test.ts | 32 +++++++++++++++++ src/composable/downloadBase64.ts | 52 ++++++++++++++++++++------- 2 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 src/composable/downloadBase64.test.ts diff --git a/src/composable/downloadBase64.test.ts b/src/composable/downloadBase64.test.ts new file mode 100644 index 00000000..3f2e3f7e --- /dev/null +++ b/src/composable/downloadBase64.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { getMimeTypeFromBase64 } from './downloadBase64'; + +describe('downloadBase64', () => { + describe('getMimeTypeFromBase64', () => { + it('when the base64 string has a data URI, it returns the mime type', () => { + expect(getMimeTypeFromBase64({ base64String: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/png' }); + expect(getMimeTypeFromBase64({ base64String: 'data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/jpg' }); + }); + + it('when the base64 string has no data URI, it try to infer the mime type from the signature', () => { + // https://en.wikipedia.org/wiki/List_of_file_signatures + + // PNG + expect(getMimeTypeFromBase64({ base64String: 'iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/png' }); + + // GIF + expect(getMimeTypeFromBase64({ base64String: 'R0lGODdh' })).to.deep.equal({ mimeType: 'image/gif' }); + expect(getMimeTypeFromBase64({ base64String: 'R0lGODlh' })).to.deep.equal({ mimeType: 'image/gif' }); + + // JPG + expect(getMimeTypeFromBase64({ base64String: '/9j/' })).to.deep.equal({ mimeType: 'image/jpg' }); + + // PDF + expect(getMimeTypeFromBase64({ base64String: 'JVBERi0' })).to.deep.equal({ mimeType: 'application/pdf' }); + }); + + it('when the base64 string has no data URI and no signature, it returns an undefined mimeType', () => { + expect(getMimeTypeFromBase64({ base64String: 'JVBERi' })).to.deep.equal({ mimeType: undefined }); + }); + }); +}); diff --git a/src/composable/downloadBase64.ts b/src/composable/downloadBase64.ts index 3904315a..37b0428d 100644 --- a/src/composable/downloadBase64.ts +++ b/src/composable/downloadBase64.ts @@ -1,32 +1,60 @@ import { extension as getExtensionFromMime } from 'mime-types'; import type { Ref } from 'vue'; +import _ from 'lodash'; -function getFileExtensionFromBase64({ - base64String, +export { getMimeTypeFromBase64, useDownloadFileFromBase64 }; + +const commonMimeTypesSignatures = { + 'JVBERi0': 'application/pdf', + 'R0lGODdh': 'image/gif', + 'R0lGODlh': 'image/gif', + 'iVBORw0KGgo': 'image/png', + '/9j/': 'image/jpg', +}; + +function getMimeTypeFromBase64({ base64String }: { base64String: string }) { + const [,mimeTypeFromBase64] = base64String.match(/data:(.*?);base64/i) ?? []; + + if (mimeTypeFromBase64) { + return { mimeType: mimeTypeFromBase64 }; + } + + const inferredMimeType = _.find(commonMimeTypesSignatures, (_mimeType, signature) => base64String.startsWith(signature)); + + if (inferredMimeType) { + return { mimeType: inferredMimeType }; + } + + return { mimeType: undefined }; +} + +function getFileExtensionFromMimeType({ + mimeType, defaultExtension = 'txt', }: { - base64String: string + mimeType: string | undefined defaultExtension?: string }) { - const hasMimeType = base64String.match(/data:(.*?);base64/i); - - if (hasMimeType) { - return getExtensionFromMime(hasMimeType[1]) || defaultExtension; + if (mimeType) { + return getExtensionFromMime(mimeType) ?? defaultExtension; } return defaultExtension; } -export function useDownloadFileFromBase64({ source, filename }: { source: Ref; filename?: string }) { +function useDownloadFileFromBase64({ source, filename }: { source: Ref; filename?: string }) { return { download() { - const base64String = source.value; - - if (base64String === '') { + if (source.value === '') { throw new Error('Base64 string is empty'); } - const cleanFileName = filename ?? `file.${getFileExtensionFromBase64({ base64String })}`; + const { mimeType } = getMimeTypeFromBase64({ base64String: source.value }); + const base64String = mimeType + ? source.value + : `data:text/plain;base64,${source.value}`; + + const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`; const a = document.createElement('a'); a.href = base64String;