Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
/**
|
|
* Typprüfung über Magic Bytes.
|
|
*
|
|
* **Die Endung wird nie angesehen**, und der vom Browser gemeldete Typ nur, um
|
|
* ihn gegen den erkannten zu prüfen. Siehe Zusicherung 4 der README.
|
|
*
|
|
* Bewusst eine eigene Signaturtabelle statt `file-type`: die Liste der
|
|
* erlaubten Typen ist kurz und geschlossen, und was nicht darin steht, wird
|
|
* abgelehnt. Eine Bibliothek, die hundert Formate erkennt, hilft bei einer
|
|
* Positivliste nicht - sie erweitert nur die Angriffsfläche und die Menge
|
|
* dessen, was aktuell gehalten werden muss. Der eigentliche Schutz ist ohnehin
|
|
* die Neukodierung danach, siehe `process.ts`.
|
|
*
|
|
* Ausdrücklich verboten, auch wenn es bequem wäre: SVG (führt Skripte aus),
|
|
* HTML, ZIP und andere Archive, Office-Dokumente, alles Ausführbare. Sie
|
|
* fallen automatisch durch, weil sie nicht in der Tabelle stehen.
|
|
*/
|
|
|
|
export const ALLOWED_TYPES = [
|
|
'image/png',
|
|
'image/jpeg',
|
|
'image/webp',
|
|
'application/pdf',
|
|
'text/plain',
|
|
] as const
|
|
|
|
export type AllowedType = (typeof ALLOWED_TYPES)[number]
|
|
|
|
export function isAllowedType(value: string): value is AllowedType {
|
|
return (ALLOWED_TYPES as readonly string[]).includes(value)
|
|
}
|
|
|
|
/** Wie viele Bytes vom Anfang für die Erkennung reichen. */
|
|
export const MAGIC_WINDOW = 32
|
|
|
|
type Signature = {
|
|
type: AllowedType
|
|
/** Bytes ab Position 0, `null` an einer Stelle heisst „beliebig". */
|
|
bytes: (number | null)[]
|
|
/** Zusätzliche Bedingung, etwa der Containertyp bei WebP. */
|
|
extra?: (buffer: Uint8Array) => boolean
|
|
}
|
|
|
|
const ascii = (text: string): number[] => [...text].map((c) => c.charCodeAt(0))
|
|
|
|
const SIGNATURES: Signature[] = [
|
|
{ type: 'image/png', bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
|
|
// JFIF, Exif und die übrigen JPEG-Varianten beginnen alle so.
|
|
{ type: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },
|
|
{
|
|
// RIFF-Container. Ohne die zweite Prüfung wäre auch eine WAV-Datei ein Treffer.
|
|
type: 'image/webp',
|
|
bytes: ascii('RIFF'),
|
|
extra: (buffer) => ascii('WEBP').every((byte, index) => buffer[8 + index] === byte),
|
|
},
|
|
// Die Spezifikation erlaubt Müll vor %PDF-. Hier nicht: ein Vorspann ist der
|
|
// klassische Weg, eine Datei für zwei Programme gleichzeitig lesbar zu machen.
|
|
{ type: 'application/pdf', bytes: ascii('%PDF-') },
|
|
]
|
|
|
|
export type Detection =
|
|
| { ok: true; type: AllowedType }
|
|
| { ok: false; reason: 'unknown-type' | 'not-utf8' | 'empty' }
|
|
|
|
/**
|
|
* Erkennt den Typ am Inhalt.
|
|
*
|
|
* Reine Textdateien haben keine Signatur. Sie werden deshalb zuletzt geprüft
|
|
* und nur dann angenommen, wenn der ganze Inhalt gültiges UTF-8 ohne
|
|
* Steuerzeichen ist - sonst wäre `text/plain` das Schlupfloch, durch das jedes
|
|
* Binärformat passt.
|
|
*/
|
|
export function detectType(content: Uint8Array): Detection {
|
|
if (content.length === 0) return { ok: false, reason: 'empty' }
|
|
|
|
for (const signature of SIGNATURES) {
|
|
const passt = signature.bytes.every(
|
|
(byte, index) => byte === null || content[index] === byte,
|
|
)
|
|
if (passt && (signature.extra?.(content) ?? true)) {
|
|
return { ok: true, type: signature.type }
|
|
}
|
|
}
|
|
|
|
return looksLikeText(content) ? { ok: true, type: 'text/plain' } : { ok: false, reason: 'unknown-type' }
|
|
}
|
|
|
|
/**
|
|
* Gültiges UTF-8, keine Nullbytes, keine Steuerzeichen ausser Tabulator,
|
|
* Zeilenumbruch und Wagenrücklauf.
|
|
*/
|
|
export function looksLikeText(content: Uint8Array): boolean {
|
|
for (const byte of content) {
|
|
if (byte === 0x09 || byte === 0x0a || byte === 0x0d) continue
|
|
if (byte < 0x20 || byte === 0x7f) return false
|
|
}
|
|
|
|
try {
|
|
new TextDecoder('utf-8', { fatal: true }).decode(content)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Der gemeldete Typ muss zum erkannten passen.
|
|
*
|
|
* Nicht, weil der gemeldete etwas wert wäre - er kommt vom Browser und ist frei
|
|
* wählbar -, sondern weil eine Abweichung ein Hinweis ist: wer eine PNG-Datei
|
|
* hochlädt und sie als PDF ankündigt, hat etwas vor.
|
|
*/
|
|
export function matchesDeclared(detected: AllowedType, declared: string | null): boolean {
|
|
if (!declared) return true
|
|
const normalisiert = declared.split(';')[0]!.trim().toLowerCase()
|
|
if (normalisiert === '' || normalisiert === 'application/octet-stream') return true
|
|
return normalisiert === detected
|
|
}
|