From 998ac6bb4624e1a7ba7373d18fa43694e7efd913 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 3 Aug 2024 17:46:00 +0200 Subject: [PATCH] feat(#155): manual MIME checking for empty and single byte files --- src/routes/viewer.ts | 4 ++-- src/utils/path.ts | 29 +++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index 2ec50964..5464b7d7 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -43,7 +43,7 @@ router.get(/.*/, async (req: Request, res: Response) => { body = renderDirectory(path); } else { const data = readFileSync(path); - const mime = pmime(path); + const mime = await pmime(path); if (!shouldRender(mime)) { res.setHeader('Content-Type', mime).send(data); return; @@ -99,7 +99,7 @@ router.post(/.*/, async (req: Request, res: Response) => { let { content } = req.body; if (reload) { - const mime = pmime(path); + const mime = await pmime(path); if (!shouldRender(mime)) { res.status(400).send('Reload is only permitted on rendered files'); return; diff --git a/src/utils/path.ts b/src/utils/path.ts index 99fb4926..7c0397f6 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -1,9 +1,34 @@ -import { execSync } from 'child_process'; +import { exec } from 'child_process'; import { homedir } from 'os'; import { basename as pbasename, dirname as pdirname, parse as pparse } from 'path'; import config from '../config.js'; +import { stat, readFile } from 'fs/promises'; +import { promisify } from 'util'; -export const pmime = (path: string) => execSync(`file --mime-type -b '${path}'`).toString().trim(); +const execPromise = promisify(exec); +export const pmime = async (path: string) => { + const [{ stdout: mime }, stats] = await Promise.all([ + execPromise(`file --mime-type -b '${path}'`), + stat(path), + ]); + // empty files can also be `application/x-empty` + // -> we unify to `inode/x-empty` + if (stats.size == 0) return 'inode/x-empty'; + // single byte files don't work well for mime recognition as they will + // always be guessed as application/octet-stream + // -> we return `text/plain` if the single byte is a printable character + if (stats.size == 1) { + const content = await readFile(path); + const char = content.at(0); + if ( + char !== undefined && + // tab line feed carriage return printable character range + (char === 0x09 || char === 0x0a || char === 0x0d || (char >= 0x20 && char <= 0x7e)) + ) + return 'text/plain'; + } + return mime.trim(); +}; export const pcomponents = (path: string) => { const parsed = pparse(path);