diff --git a/resources/UNICORN-UPPERCASE.PNG b/resources/UNICORN-UPPERCASE.PNG new file mode 100644 index 0000000..6169ed9 Binary files /dev/null and b/resources/UNICORN-UPPERCASE.PNG differ diff --git a/src/utils.ts b/src/utils.ts index 2efdb97..5261454 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -43,7 +43,7 @@ export async function getFileType( */ const magicType = await fileTypeFromBuffer(fileContents) if (magicType) { - return Object.assign({ ext: magicType.ext }, parseMimeType(magicType.mime)) + return Object.assign({ ext: magicType.ext.toLowerCase() }, parseMimeType(magicType.mime)) } return null @@ -61,7 +61,7 @@ export function computeFileTypeFromName( * and pull type/subtype from the headers content type. */ return Object.assign( - { ext: extname(clientName).replace(/^\./, '') }, + { ext: extname(clientName).replace(/^\./, '').toLowerCase() }, parseMimeType(headers['content-type']) ) } diff --git a/tests/body_parser.spec.ts b/tests/body_parser.spec.ts index 747af5b..2e0a0a2 100644 --- a/tests/body_parser.spec.ts +++ b/tests/body_parser.spec.ts @@ -26,7 +26,12 @@ import { AppFactory } from '@adonisjs/application/factories' import { Multipart } from '../src/multipart/main.ts' import { type MultipartFile } from '../src/multipart/file.ts' import { BodyParserMiddlewareFactory } from '../factories/middleware_factory.ts' -import { packageFilePath, packageFileSize, unicornFilePath } from './helpers.ts' +import { + packageFilePath, + packageFileSize, + unicornFilePath, + unicornUppercaseFilePath, +} from './helpers.ts' test.group('BodyParser Middleware', () => { test('do not parse get requests', async ({ assert }) => { @@ -1189,6 +1194,40 @@ test.group('BodyParser Middleware | multipart', () => { ]) }) + test('validate uppercase file extension', async ({ assert }) => { + const server = createServer(async (req, res) => { + const request = new RequestFactory().merge({ req, res }).create() + const response = new ResponseFactory().merge({ req, res }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + const middleware = new BodyParserMiddlewareFactory().create() + + await middleware.handle(ctx, async () => { + res.writeHead(200, { 'content-type': 'application/json' }) + const pkgFile = ctx.request.file('image')! + pkgFile.sizeLimit = 100_000 + pkgFile.validate() + + pkgFile.allowedExtensions = ['png'] + pkgFile.validate() + + res.end( + JSON.stringify({ + tmpPath: pkgFile.tmpPath!, + size: pkgFile.size, + validated: pkgFile.validated, + isValid: pkgFile.isValid, + errors: pkgFile.errors, + }) + ) + }) + }) + + const { body } = await supertest(server).post('/').attach('image', unicornUppercaseFilePath) + assert.exists(body.tmpPath) + assert.isTrue(body.validated) + assert.isTrue(body.isValid) + }) + test('calling validate multiple times must be a noop', async ({ assert }) => { const server = createServer(async (req, res) => { const request = new RequestFactory().merge({ req, res }).create() diff --git a/tests/helpers.ts b/tests/helpers.ts index 3ac190f..4be990a 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -18,6 +18,9 @@ export const largePdfFile = fileURLToPath( export const xlsFilePath = fileURLToPath(new URL('../resources/sample.xls', import.meta.url)) export const xlsxFilePath = fileURLToPath(new URL('../resources/sample.xlsx', import.meta.url)) export const unicornFilePath = fileURLToPath(new URL('../resources/unicorn.png', import.meta.url)) +export const unicornUppercaseFilePath = fileURLToPath( + new URL('../resources/UNICORN-UPPERCASE.PNG', import.meta.url) +) export const unicornNoExtFilePath = fileURLToPath( new URL('../resources/unicorn-wo-ext', import.meta.url) ) diff --git a/tests/multipart.spec.ts b/tests/multipart.spec.ts index cd7889b..cfebb1f 100644 --- a/tests/multipart.spec.ts +++ b/tests/multipart.spec.ts @@ -32,6 +32,7 @@ import { packageFileSize, unicornFilePath, unicornNoExtFilePath, + unicornUppercaseFilePath, } from './helpers.ts' const BASE_URL = new URL('./tmp/', import.meta.url) @@ -639,6 +640,42 @@ test.group('Multipart', () => { ]) }) + test('does not report extension validation errors when file extension is uppercase', async ({ + assert, + }) => { + let files: null | Record = null + const server = createServer(async (req, res) => { + const request = new RequestFactory().merge({ req, res }).create() + const response = new ResponseFactory().merge({ req, res }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + const multipart = new Multipart(ctx, { maxFields: 1000, limit: 100_000 }) + + multipart.onFile( + '*', + { + extnames: ['png'], + }, + (part, reporter) => { + return new Promise((resolve, reject) => { + part.on('error', reject) + part.on('end', resolve) + part.on('data', reporter) + }) + } + ) + + await multipart.process() + files = ctx.request['__raw_files'] || null + res.end() + }) + + await supertest(server).post('/').attach('image', unicornUppercaseFilePath) + + assert.property(files, 'image') + assert.isTrue(files!.image instanceof MultipartFile && files!.image.isValid) + assert.equal(files!.image instanceof MultipartFile && files!.image.state, 'consumed') + }) + test('do not run validations when deferValidations is set to true', async ({ assert }) => { let files: null | Record = null const server = createServer(async (req, res) => { @@ -928,6 +965,50 @@ test.group('Multipart', () => { assert.lengthOf(report.errors, 0) }) + test('validates uppercase extension', async ({ assert }) => { + let files: null | Record = null + const server = createServer(async (req, res) => { + const request = new RequestFactory().merge({ req, res }).create() + const response = new ResponseFactory().merge({ req, res }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + const multipart = new Multipart(ctx, { maxFields: 1000, limit: 100_000 }) + + multipart.onFile( + '*', + { + extnames: ['png'], + }, + (part, reporter) => { + return new Promise((resolve, reject) => { + part.on('error', reject) + part.on('end', resolve) + part.on('data', reporter) + }) + } + ) + + try { + await multipart.process() + } catch (error) { + console.log(error) + } + files = ctx.request['__raw_files'] || null + res.end() + }) + + await supertest(server).post('/').attach('image', unicornUppercaseFilePath) + + assert.property(files, 'image') + assert.instanceOf(files!.image, MultipartFile) + + const image = files!.image as MultipartFile + + assert.isTrue(image.isValid) + assert.equal(image.state, 'consumed') + assert.equal(image.extname, 'png') + assert.lengthOf(image.errors, 0) + }) + test('process medium sized files', async ({ assert }) => { let files: null | Record = null