( file: FileUpload, )
| 18 | * those accepted anything at all as long as it was *called* a PNG — a shell |
| 19 | * script renamed `evil.png` was stored and then served from this origin. |
| 20 | * Returns undefined when the header matches no image this service accepts. |
| 21 | */ |
| 22 | /** |
| 23 | * What a buffer actually is, by its magic bytes — the one thing a client |
| 24 | * cannot lie about. Exported because chat attachments arrive by a different |
| 25 | * road (base64 data URLs, not multipart) and need the same answer. |
| 26 | */ |
| 27 | export function sniff(buffer: Buffer): string | undefined { |
| 28 | if ( |
| 29 | buffer.length >= 8 && |
| 30 | buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) |
| 31 | ) { |
| 32 | return 'image/png'; |
| 33 | } |
| 34 | if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) { |
| 35 | return 'image/jpeg'; |
| 36 | } |
| 37 | if ( |
| 38 | buffer.length >= 12 && |
| 39 | buffer.subarray(0, 4).toString('ascii') === 'RIFF' && |
| 40 | buffer.subarray(8, 12).toString('ascii') === 'WEBP' |
| 41 | ) { |
| 42 | return 'image/webp'; |
| 43 | } |
| 44 | // GIF87a / GIF89a. Not accepted for avatars or covers, but the agent can |
| 45 | // be handed one, so the sniffer has to be able to name it. |
| 46 | if (buffer.length >= 6 && buffer.subarray(0, 3).toString('ascii') === 'GIF') { |
| 47 | return 'image/gif'; |
| 48 | } |
| 49 | return undefined; |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * Validates a file upload (size, type) and returns a Buffer. |
| 54 | * @param file - FileUpload object from GraphQL |
| 55 | * @returns Promise<Buffer> - The file data in buffer format |
| 56 | * @throws BadRequestException - If validation fails |
| 57 | */ |
| 58 | export async function validateAndBufferFile( |
| 59 | file: FileUpload, |
| 60 | ): Promise<{ buffer: Buffer; mimetype: string }> { |
no outgoing calls
no test coverage detected