Skip to main content

exiftool_rs/
file_type.rs

1/// Supported file types and their detection logic.
2///
3/// Mirrors ExifTool's %fileTypeLookup, %magicNumber, and %mimeType.
4/// Covers all 150+ formats supported by ExifTool.
5///
6/// Known file types that exiftool can process.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8#[allow(non_camel_case_types)]
9pub enum FileType {
10    // ===== Images - Standard =====
11    Jpeg,
12    Tiff,
13    Png,
14    Gif,
15    Bmp,
16    WebP,
17    Heif,
18    Avif,
19    Psd,
20    Jp2,
21    J2c,
22    Jxl,
23    Jxr,
24    Flif,
25    Bpg,
26    Exr,
27    Ico,
28    Jps,
29    // ===== Images - Specialized =====
30    DjVu,
31    Xcf,
32    Pcx,
33    Pict,
34    Psp,
35    Hdr,
36    Rwz,
37    Btf,
38    Mng,
39    PhotoCd,
40    // ===== Images - RAW =====
41    Cr2,
42    Cr3,
43    Crw,
44    Nef,
45    Arw,
46    Sr2,
47    Srf,
48    Orf,
49    Rw2,
50    Dng,
51    Raf,
52    Pef,
53    Dcr,
54    Mrw,
55    Erf,
56    Fff,
57    Iiq,
58    Rwl,
59    Mef,
60    Srw,
61    X3f,
62    Gpr,
63    Arq,
64    ThreeFR,
65    Crm,
66    // ===== Video =====
67    Mp4,
68    QuickTime,
69    Avi,
70    Mkv,
71    WebM,
72    Wmv,
73    Asf,
74    Flv,
75    Mxf,
76    Czi,
77    M2ts,
78    Mpeg,
79    ThreeGP,
80    RealMedia,
81    R3d,
82    Dvb,
83    Lrv,
84    Mqv,
85    F4v,
86    Wtv,
87    DvrMs,
88    // ===== Audio =====
89    Mp3,
90    Flac,
91    Ogg,
92    Wav,
93    Aiff,
94    Aac,
95    Opus,
96    Mpc,
97    Ape,
98    WavPack,
99    Ofr,
100    Dsf,
101    Audible,
102    RealAudio,
103    Wma,
104    M4a,
105    Dss,
106    // ===== Documents =====
107    Pdf,
108    PostScript,
109    Doc,
110    Docx,
111    Xls,
112    Xlsx,
113    Ppt,
114    Pptx,
115    Numbers,
116    Pages,
117    Key,
118    InDesign,
119    Rtf,
120    // ===== Archives =====
121    Zip,
122    Rar,
123    SevenZ,
124    Gzip,
125    // ===== Metadata / Other =====
126    Xmp,
127    Mie,
128    Exv,
129    Vrd,
130    Dr4,
131    Icc,
132    Html,
133    Exe,
134    Font,
135    Swf,
136    Dicom,
137    Fits,
138    Fit,
139    // ===== Newly added =====
140    Mrc,
141    Moi,
142    MacOs,
143    Json,
144    Pcap,
145    Pcapng,
146    Svg,
147    // ===== New formats =====
148    Pgf,
149    Xisf,
150    Torrent,
151    Mobi,
152    SonyPmp,
153    // ===== Additional formats =====
154    Plist,
155    Aae,
156    KyoceraRaw,
157    // ===== Lytro Light Field Picture =====
158    Lfp,
159    // ===== Portable Float Map =====
160    PortableFloatMap,
161    // ===== FLIR thermal =====
162    Fpf,
163    // ===== OpenDocument =====
164    Ods,
165    Odt,
166    Odp,
167    Odg,
168    Odf,
169    Odb,
170    Odi,
171    Odc,
172    // ===== CaptureOne Enhanced Image Package =====
173    Eip,
174    // ===== Leica Image Format =====
175    Lif,
176    // ===== Plain text formats =====
177    Txt,
178    Csv,
179    Vcard,
180    Ics,
181    Url,
182    Lnk,
183    Ppm,
184    Dpx,
185    Ram,
186    // ===== Formats with readers but previously lacking a FileType identity =====
187    Miff,
188    Tnef,
189    Wpg,
190    Dv,
191    Itc,
192    Iso,
193    Afm,
194    Pfa,
195    Pfb,
196    Dfont,
197    Xml,
198    Inx,
199    Eps,
200}
201
202/// Indicates the read/write capability for a file type.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum Support {
205    Read,
206    ReadWrite,
207    ReadWriteCreate,
208}
209
210impl FileType {
211    /// Human-readable file type description.
212    pub fn description(self) -> &'static str {
213        match self {
214            // Standard images
215            FileType::Jpeg => "JPEG image",
216            FileType::Tiff => "TIFF image",
217            FileType::Png => "PNG image",
218            FileType::Gif => "GIF image",
219            FileType::Bmp => "BMP image",
220            FileType::WebP => "WebP image",
221            FileType::Heif => "HEIF/HEIC image",
222            FileType::Avif => "AVIF image",
223            FileType::Psd => "Adobe Photoshop Document",
224            FileType::Jp2 => "JPEG 2000 image",
225            FileType::J2c => "JPEG 2000 Codestream",
226            FileType::Jxl => "JPEG XL image",
227            FileType::Jxr => "JPEG XR / HD Photo",
228            FileType::Flif => "Free Lossless Image Format",
229            FileType::Bpg => "Better Portable Graphics",
230            FileType::Exr => "OpenEXR image",
231            FileType::Ico => "Windows Icon",
232            FileType::Jps => "JPEG Stereo image",
233            // Specialized images
234            FileType::DjVu => "DjVu document",
235            FileType::Xcf => "GIMP image",
236            FileType::Pcx => "PCX image",
237            FileType::Pict => "Apple PICT",
238            FileType::Psp => "Paint Shop Pro image",
239            FileType::Pgf => "Progressive Graphics File",
240            FileType::Xisf => "PixInsight XISF image",
241            FileType::Torrent => "BitTorrent descriptor",
242            FileType::Mobi => "Mobipocket Book",
243            FileType::SonyPmp => "Sony PMP video",
244            FileType::Plist => "PLIST",
245            FileType::Aae => "AAE",
246            FileType::KyoceraRaw => "Kyocera Contax N RAW",
247            FileType::Lfp => "LFP",
248            FileType::PortableFloatMap => "Portable Float Map",
249            FileType::Hdr => "Radiance HDR",
250            FileType::Rwz => "Rawzor compressed image",
251            FileType::Btf => "BigTIFF image",
252            FileType::Mng => "MNG animation",
253            FileType::PhotoCd => "Kodak Photo CD",
254            // RAW
255            FileType::Cr2 => "Canon CR2 RAW",
256            FileType::Cr3 => "Canon CR3 RAW",
257            FileType::Crw => "Canon CRW RAW",
258            FileType::Nef => "Nikon NEF RAW",
259            FileType::Arw => "Sony ARW RAW",
260            FileType::Sr2 => "Sony SR2 RAW",
261            FileType::Srf => "Sony SRF RAW",
262            FileType::Orf => "Olympus ORF RAW",
263            FileType::Rw2 => "Panasonic RW2 RAW",
264            FileType::Dng => "Adobe Digital Negative",
265            FileType::Raf => "Fujifilm RAF RAW",
266            FileType::Pef => "Pentax PEF RAW",
267            FileType::Dcr => "Kodak DCR RAW",
268            FileType::Mrw => "Minolta MRW RAW",
269            FileType::Erf => "Epson ERF RAW",
270            FileType::Fff => "Hasselblad FFF RAW",
271            FileType::Iiq => "Phase One IIQ RAW",
272            FileType::Rwl => "Leica RWL RAW",
273            FileType::Mef => "Mamiya MEF RAW",
274            FileType::Srw => "Samsung SRW RAW",
275            FileType::X3f => "Sigma X3F RAW",
276            FileType::Gpr => "GoPro GPR RAW",
277            FileType::Arq => "Sony ARQ RAW",
278            FileType::ThreeFR => "Hasselblad 3FR RAW",
279            FileType::Crm => "Canon Cinema RAW",
280            // Video
281            FileType::Mp4 => "MP4 video",
282            FileType::QuickTime => "QuickTime video",
283            FileType::Avi => "AVI",
284            FileType::Mkv => "Matroska video",
285            FileType::WebM => "WebM video",
286            FileType::Wmv => "Windows Media Video",
287            FileType::Asf => "Advanced Systems Format",
288            FileType::Flv => "Flash Video",
289            FileType::Mxf => "Material Exchange Format",
290            FileType::Czi => "CZI",
291            FileType::M2ts => "MPEG-2 Transport Stream",
292            FileType::Mpeg => "MPEG video",
293            FileType::ThreeGP => "3GPP multimedia",
294            FileType::RealMedia => "RealMedia",
295            FileType::R3d => "Redcode RAW video",
296            FileType::Dvb => "Digital Video Broadcasting",
297            FileType::Lrv => "GoPro Low-Res Video",
298            FileType::Mqv => "Sony Movie",
299            FileType::F4v => "Adobe Flash Video",
300            FileType::Wtv => "Windows Recorded TV",
301            FileType::DvrMs => "Microsoft DVR",
302            // Audio
303            FileType::Mp3 => "MP3 audio",
304            FileType::Flac => "FLAC audio",
305            FileType::Ogg => "Ogg Vorbis audio",
306            FileType::Wav => "WAV audio",
307            FileType::Aiff => "AIFF",
308            FileType::Aac => "AAC audio",
309            FileType::Opus => "Opus audio",
310            FileType::Mpc => "Musepack audio",
311            FileType::Ape => "Monkey's Audio",
312            FileType::WavPack => "WavPack audio",
313            FileType::Ofr => "OptimFROG audio",
314            FileType::Dsf => "DSD Stream File",
315            FileType::Audible => "Audible audiobook",
316            FileType::RealAudio => "RealAudio",
317            FileType::Wma => "Windows Media Audio",
318            FileType::M4a => "MPEG-4 Audio",
319            FileType::Dss => "DSS",
320            // Documents
321            FileType::Pdf => "PDF document",
322            FileType::PostScript => "PostScript",
323            FileType::Doc => "Microsoft Word (legacy)",
324            FileType::Docx => "Microsoft Word",
325            FileType::Xls => "Microsoft Excel (legacy)",
326            FileType::Xlsx => "Microsoft Excel",
327            FileType::Ppt => "Microsoft PowerPoint (legacy)",
328            FileType::Pptx => "Microsoft PowerPoint",
329            FileType::Numbers => "Apple Numbers",
330            FileType::Pages => "Apple Pages",
331            FileType::Key => "Apple Keynote",
332            FileType::InDesign => "Adobe InDesign",
333            FileType::Rtf => "Rich Text Format",
334            // Archives
335            FileType::Zip => "ZIP archive",
336            FileType::Rar => "RAR archive",
337            FileType::SevenZ => "7-Zip archive",
338            FileType::Gzip => "GZIP",
339            // Metadata / Other
340            FileType::Xmp => "XMP sidecar",
341            FileType::Mie => "MIE metadata",
342            FileType::Exv => "Exiv2 metadata",
343            FileType::Vrd => "VRD",
344            FileType::Dr4 => "DR4",
345            FileType::Icc => "ICC color profile",
346            FileType::Html => "HTML document",
347            FileType::Exe => "Windows executable",
348            FileType::Font => "Font file",
349            FileType::Swf => "Shockwave Flash",
350            FileType::Dicom => "DICOM medical image",
351            FileType::Fits => "FITS astronomical image",
352            FileType::Fit => "Garmin Flexible and Interoperable Data Transfer",
353            FileType::Mrc => "MRC image",
354            FileType::Moi => "MOI",
355            FileType::MacOs => "MacOS",
356            FileType::Json => "JSON",
357            FileType::Pcap => "PCAP",
358            FileType::Pcapng => "PCAPNG",
359            FileType::Svg => "SVG",
360            FileType::Fpf => "FPF",
361            FileType::Ods => "ODS",
362            FileType::Odt => "ODT",
363            FileType::Odp => "ODP",
364            FileType::Odg => "ODG",
365            FileType::Odf => "ODF",
366            FileType::Odb => "ODB",
367            FileType::Odi => "ODI",
368            FileType::Odc => "ODC",
369            FileType::Eip => "EIP",
370            FileType::Lif => "Leica Image Format",
371            FileType::Txt => "Text",
372            FileType::Csv => "Comma-Separated Values",
373            FileType::Vcard => "vCard",
374            FileType::Ics => "iCalendar",
375            FileType::Url => "Windows URL Shortcut",
376            FileType::Lnk => "Windows Shortcut",
377            FileType::Ppm => "Portable Pixel Map",
378            FileType::Dpx => "Digital Picture Exchange",
379            FileType::Ram => "Real Audio Metadata",
380            FileType::Miff => "Magick Image File Format",
381            FileType::Tnef => "Transport Neutral Encapsulation Format",
382            FileType::Wpg => "WordPerfect Graphics",
383            FileType::Dv => "Digital Video",
384            FileType::Itc => "iTunes Cover Flow",
385            FileType::Iso => "ISO 9660 disk image",
386            FileType::Afm => "Adobe Font Metrics",
387            FileType::Pfa => "PostScript Font ASCII",
388            FileType::Pfb => "PostScript Font Binary",
389            FileType::Dfont => "Macintosh Font",
390            FileType::Xml => "Extensible Markup Language",
391            FileType::Inx => "Adobe InDesign Interchange",
392            FileType::Eps => "Encapsulated PostScript",
393        }
394    }
395
396    /// ExifTool's short `FileType` value (the `%fileTypeLookup` key), e.g. "JPEG",
397    /// "NEF", "MOV" — distinct from the human-readable [`Self::description`].
398    pub fn code(self) -> &'static str {
399        match self {
400            FileType::Jpeg => "JPEG",
401            FileType::Tiff => "TIFF",
402            FileType::Png => "PNG",
403            FileType::Gif => "GIF",
404            FileType::Bmp => "BMP",
405            FileType::WebP => "WEBP",
406            FileType::Heif => "HEIF",
407            FileType::Avif => "AVIF",
408            FileType::Psd => "PSD",
409            FileType::Jp2 => "JP2",
410            FileType::J2c => "J2C",
411            FileType::Jxl => "JXL",
412            FileType::Jxr => "JXR",
413            FileType::Flif => "FLIF",
414            FileType::Bpg => "BPG",
415            FileType::Exr => "EXR",
416            FileType::Ico => "ICO",
417            FileType::Jps => "JPS",
418            FileType::DjVu => "DJVU",
419            FileType::Xcf => "XCF",
420            FileType::Pcx => "PCX",
421            FileType::Pict => "PICT",
422            FileType::Psp => "PSP",
423            FileType::Pgf => "PGF",
424            FileType::Xisf => "XISF",
425            FileType::Torrent => "Torrent",
426            FileType::Mobi => "MOBI",
427            FileType::SonyPmp => "PMP",
428            FileType::Plist => "PLIST",
429            FileType::Aae => "AAE",
430            FileType::KyoceraRaw => "RAW",
431            FileType::Lfp => "LFP",
432            FileType::PortableFloatMap => "PFM",
433            FileType::Hdr => "HDR",
434            FileType::Rwz => "RWZ",
435            FileType::Btf => "BTF",
436            FileType::Mng => "MNG",
437            FileType::PhotoCd => "PCD",
438            FileType::Cr2 => "CR2",
439            FileType::Cr3 => "CR3",
440            FileType::Crw => "CRW",
441            FileType::Nef => "NEF",
442            FileType::Arw => "ARW",
443            FileType::Sr2 => "SR2",
444            FileType::Srf => "SRF",
445            FileType::Orf => "ORF",
446            FileType::Rw2 => "RW2",
447            FileType::Dng => "DNG",
448            FileType::Raf => "RAF",
449            FileType::Pef => "PEF",
450            FileType::Dcr => "DCR",
451            FileType::Mrw => "MRW",
452            FileType::Erf => "ERF",
453            FileType::Fff => "FFF",
454            FileType::Iiq => "IIQ",
455            FileType::Rwl => "RWL",
456            FileType::Mef => "MEF",
457            FileType::Srw => "SRW",
458            FileType::X3f => "X3F",
459            FileType::Gpr => "GPR",
460            FileType::Arq => "ARQ",
461            FileType::ThreeFR => "3FR",
462            FileType::Crm => "CRM",
463            FileType::Mp4 => "MP4",
464            FileType::QuickTime => "MOV",
465            FileType::Avi => "AVI",
466            FileType::Mkv => "MKV",
467            FileType::WebM => "WEBM",
468            FileType::Wmv => "WMV",
469            FileType::Asf => "ASF",
470            FileType::Flv => "FLV",
471            FileType::Mxf => "MXF",
472            FileType::Czi => "CZI",
473            FileType::M2ts => "M2TS",
474            FileType::Mpeg => "MPEG",
475            FileType::ThreeGP => "3GP",
476            FileType::RealMedia => "RM",
477            FileType::R3d => "R3D",
478            FileType::Dvb => "DVB",
479            FileType::Lrv => "LRV",
480            FileType::Mqv => "MQV",
481            FileType::F4v => "F4V",
482            FileType::Wtv => "WTV",
483            FileType::DvrMs => "DVR-MS",
484            FileType::Mp3 => "MP3",
485            FileType::Flac => "FLAC",
486            FileType::Ogg => "OGG",
487            FileType::Wav => "WAV",
488            FileType::Aiff => "AIFF",
489            FileType::Aac => "AAC",
490            FileType::Opus => "OPUS",
491            FileType::Mpc => "MPC",
492            FileType::Ape => "APE",
493            FileType::WavPack => "WV",
494            FileType::Ofr => "OFR",
495            FileType::Dsf => "DSF",
496            FileType::Audible => "AA",
497            FileType::RealAudio => "RA",
498            FileType::Wma => "WMA",
499            FileType::M4a => "M4A",
500            FileType::Dss => "DSS",
501            FileType::Pdf => "PDF",
502            FileType::PostScript => "PS",
503            FileType::Doc => "DOC",
504            FileType::Docx => "DOCX",
505            FileType::Xls => "XLS",
506            FileType::Xlsx => "XLSX",
507            FileType::Ppt => "PPT",
508            FileType::Pptx => "PPTX",
509            FileType::Numbers => "NUMBERS",
510            FileType::Pages => "PAGES",
511            FileType::Key => "KEY",
512            FileType::InDesign => "INDD",
513            FileType::Rtf => "RTF",
514            FileType::Zip => "ZIP",
515            FileType::Rar => "RAR",
516            FileType::SevenZ => "7Z",
517            FileType::Gzip => "GZIP",
518            FileType::Xmp => "XMP",
519            FileType::Mie => "MIE",
520            FileType::Exv => "EXV",
521            FileType::Vrd => "VRD",
522            FileType::Dr4 => "DR4",
523            FileType::Icc => "ICC",
524            FileType::Html => "HTML",
525            FileType::Exe => "EXE",
526            FileType::Font => "TTF",
527            FileType::Swf => "SWF",
528            FileType::Dicom => "DICOM",
529            FileType::Fits => "FITS",
530            FileType::Fit => "FIT",
531            FileType::Mrc => "MRC",
532            FileType::Moi => "MOI",
533            FileType::MacOs => "MacOS",
534            FileType::Json => "JSON",
535            FileType::Pcap => "PCAP",
536            FileType::Pcapng => "PCAPNG",
537            FileType::Svg => "SVG",
538            FileType::Fpf => "FPF",
539            FileType::Ods => "ODS",
540            FileType::Odt => "ODT",
541            FileType::Odp => "ODP",
542            FileType::Odg => "ODG",
543            FileType::Odf => "ODF",
544            FileType::Odb => "ODB",
545            FileType::Odi => "ODI",
546            FileType::Odc => "ODC",
547            FileType::Eip => "EIP",
548            FileType::Lif => "LIF",
549            FileType::Txt => "TXT",
550            FileType::Csv => "CSV",
551            FileType::Vcard => "VCard",
552            FileType::Ics => "ICS",
553            FileType::Url => "URL",
554            FileType::Lnk => "LNK",
555            FileType::Ppm => "PPM",
556            FileType::Dpx => "DPX",
557            FileType::Ram => "RAM",
558            FileType::Miff => "MIFF",
559            FileType::Tnef => "TNEF",
560            FileType::Wpg => "WPG",
561            FileType::Dv => "DV",
562            FileType::Itc => "ITC",
563            FileType::Iso => "ISO",
564            FileType::Afm => "AFM",
565            FileType::Pfa => "PFA",
566            FileType::Pfb => "PFB",
567            FileType::Dfont => "DFONT",
568            FileType::Xml => "XML",
569            FileType::Inx => "INX",
570            FileType::Eps => "EPS",
571        }
572    }
573
574    /// MIME type for this file type.
575    pub fn mime_type(self) -> &'static str {
576        match self {
577            FileType::Jpeg => "image/jpeg",
578            FileType::Tiff => "image/tiff",
579            FileType::Btf => "image/x-tiff-big",
580            FileType::Png => "image/png",
581            FileType::Gif => "image/gif",
582            FileType::Bmp => "image/bmp",
583            FileType::WebP => "image/webp",
584            FileType::Heif => "image/heif",
585            FileType::Avif => "image/avif",
586            FileType::Psd => "application/vnd.adobe.photoshop",
587            FileType::Jp2 => "image/jp2",
588            FileType::J2c => "image/x-j2c",
589            FileType::Jxl => "image/jxl",
590            FileType::Jxr => "image/jxr",
591            FileType::Flif => "image/flif",
592            FileType::Bpg => "image/bpg",
593            FileType::Exr => "image/x-exr",
594            FileType::Ico => "image/x-icon",
595            FileType::Jps => "image/x-jps",
596            FileType::DjVu => "image/vnd.djvu",
597            FileType::Xcf => "image/x-xcf",
598            FileType::Pcx => "image/pcx",
599            FileType::Pict => "image/pict",
600            FileType::Psp => "image/x-paintshoppro",
601            FileType::Hdr => "image/vnd.radiance",
602            FileType::Rwz => "image/x-rawzor",
603            FileType::Mng => "video/x-mng",
604            FileType::PhotoCd => "image/x-photo-cd",
605            // RAW → use specific MIME where available
606            FileType::Cr2 => "image/x-canon-cr2",
607            FileType::Cr3 | FileType::Crm => "image/x-canon-cr3",
608            FileType::Crw => "image/x-canon-crw",
609            FileType::Nef => "image/x-nikon-nef",
610            FileType::Arw | FileType::Arq => "image/x-sony-arw",
611            FileType::Sr2 => "image/x-sony-sr2",
612            FileType::Srf => "image/x-sony-srf",
613            FileType::Orf => "image/x-olympus-orf",
614            FileType::Rw2 => "image/x-panasonic-rw2",
615            FileType::Dng | FileType::Gpr => "image/x-adobe-dng",
616            FileType::Raf => "image/x-fujifilm-raf",
617            FileType::Pef => "image/x-pentax-pef",
618            FileType::Dcr => "image/x-kodak-dcr",
619            FileType::Mrw => "image/x-minolta-mrw",
620            FileType::Erf => "image/x-epson-erf",
621            FileType::Fff | FileType::ThreeFR => "image/x-hasselblad-fff",
622            FileType::Iiq => "image/x-raw",
623            FileType::Rwl => "image/x-leica-rwl",
624            FileType::Mef => "image/x-mamiya-mef",
625            FileType::Srw => "image/x-samsung-srw",
626            FileType::X3f => "image/x-sigma-x3f",
627            // Video
628            FileType::Mp4 | FileType::F4v => "video/mp4",
629            FileType::QuickTime | FileType::Mqv => "video/quicktime",
630            FileType::Avi => "video/x-msvideo",
631            FileType::Mkv => "video/x-matroska",
632            FileType::WebM => "video/webm",
633            FileType::Wmv => "video/x-ms-wmv",
634            FileType::Asf => "video/x-ms-asf",
635            FileType::Flv => "video/x-flv",
636            FileType::Mxf => "application/mxf",
637            FileType::Czi => "image/x-zeiss-czi",
638            FileType::M2ts => "video/m2ts",
639            FileType::Mpeg => "video/mpeg",
640            FileType::ThreeGP => "video/3gpp",
641            FileType::RealMedia => "application/vnd.rn-realmedia",
642            FileType::R3d => "video/x-red-r3d",
643            FileType::Dvb => "video/dvb",
644            FileType::Lrv => "video/mp4",
645            FileType::Wtv => "video/x-ms-wtv",
646            FileType::DvrMs => "video/x-ms-dvr",
647            // Audio
648            FileType::Mp3 => "audio/mpeg",
649            FileType::Flac => "audio/flac",
650            FileType::Ogg | FileType::Opus => "audio/ogg",
651            FileType::Wav => "audio/x-wav",
652            FileType::Aiff => "audio/x-aiff",
653            FileType::Aac => "audio/aac",
654            FileType::Mpc => "audio/x-musepack",
655            FileType::Ape => "audio/x-monkeys-audio",
656            FileType::WavPack => "audio/x-wavpack",
657            FileType::Ofr => "audio/x-ofr",
658            FileType::Dsf => "audio/dsf",
659            FileType::Audible => "audio/audible",
660            FileType::RealAudio => "audio/x-pn-realaudio",
661            FileType::Wma => "audio/x-ms-wma",
662            FileType::M4a => "audio/mp4",
663            FileType::Dss => "audio/x-dss",
664            // Documents
665            FileType::Pdf => "application/pdf",
666            FileType::PostScript => "application/postscript",
667            FileType::Doc => "application/msword",
668            FileType::Docx => {
669                "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
670            }
671            FileType::Xls => "application/vnd.ms-excel",
672            FileType::Xlsx => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
673            FileType::Ppt => "application/vnd.ms-powerpoint",
674            FileType::Pptx => {
675                "application/vnd.openxmlformats-officedocument.presentationml.presentation"
676            }
677            FileType::Numbers => "application/x-iwork-numbers-sffnumbers",
678            FileType::Pages => "application/x-iwork-pages-sffpages",
679            FileType::Key => "application/x-iwork-keynote-sffkey",
680            FileType::InDesign => "application/x-indesign",
681            FileType::Rtf => "text/rtf",
682            // Archives
683            FileType::Zip => "application/zip",
684            FileType::Rar => "application/x-rar-compressed",
685            FileType::SevenZ => "application/x-7z-compressed",
686            FileType::Gzip => "application/x-gzip",
687            // Metadata / Other
688            FileType::Xmp => "application/rdf+xml",
689            FileType::Mie => "application/x-mie",
690            FileType::Exv => "application/x-exv",
691            FileType::Vrd => "application/octet-stream",
692            FileType::Dr4 => "application/octet-stream",
693            FileType::Icc => "application/vnd.iccprofile",
694            FileType::Html => "text/html",
695            FileType::Exe => "application/x-dosexec",
696            FileType::Font => "application/font-ttf",
697            FileType::Swf => "application/x-shockwave-flash",
698            FileType::Dicom => "application/dicom",
699            FileType::Fits => "image/fits",
700            FileType::Fit => "application/fit",
701            FileType::Mrc => "image/x-mrc",
702            FileType::Moi => "application/octet-stream",
703            FileType::MacOs => "application/unknown",
704            FileType::Json => "application/json",
705            FileType::Pcap => "application/vnd.tcpdump.pcap",
706            FileType::Pcapng => "application/vnd.tcpdump.pcap",
707            FileType::Svg => "image/svg+xml",
708            FileType::Pgf => "image/pgf",
709            FileType::Xisf => "image/x-xisf",
710            FileType::Torrent => "application/x-bittorrent",
711            FileType::Mobi => "application/x-mobipocket-ebook",
712            FileType::SonyPmp => "image/x-sony-pmp",
713            FileType::Plist => "application/x-plist",
714            FileType::Aae => "application/vnd.apple.photos",
715            FileType::KyoceraRaw => "image/x-raw",
716            FileType::Lfp => "image/x-lytro-lfp",
717            FileType::PortableFloatMap => "image/x-pfm",
718            FileType::Fpf => "image/x-flir-fpf",
719            FileType::Ods => "application/vnd.oasis.opendocument.spreadsheet",
720            FileType::Odt => "application/vnd.oasis.opendocument.text",
721            FileType::Odp => "application/vnd.oasis.opendocument.presentation",
722            FileType::Odg => "application/vnd.oasis.opendocument.graphics",
723            FileType::Odf => "application/vnd.oasis.opendocument.formula",
724            FileType::Odb => "application/vnd.oasis.opendocument.database",
725            FileType::Odi => "application/vnd.oasis.opendocument.image",
726            FileType::Odc => "application/vnd.oasis.opendocument.chart",
727            FileType::Eip => "application/x-captureone",
728            FileType::Lif => "image/x-leica-lif",
729            FileType::Txt => "text/plain",
730            FileType::Csv => "text/csv",
731            FileType::Vcard => "text/vcard",
732            FileType::Ics => "text/calendar",
733            FileType::Url => "application/x-mswinurl",
734            FileType::Lnk => "application/octet-stream",
735            FileType::Ppm => "image/x-portable-pixmap",
736            FileType::Dpx => "image/x-dpx",
737            FileType::Ram => "audio/x-pn-realaudio",
738            FileType::Miff => "application/x-magick-image",
739            FileType::Tnef => "application/vnd.ms-tnef",
740            FileType::Wpg => "image/x-wpg",
741            FileType::Dv => "video/x-dv",
742            FileType::Itc => "application/itunes",
743            FileType::Iso => "application/x-iso9660-image",
744            FileType::Afm => "application/x-font-afm",
745            FileType::Pfa => "application/x-font-type1",
746            FileType::Pfb => "application/x-font-type1",
747            FileType::Dfont => "application/x-dfont",
748            FileType::Xml => "application/xml",
749            FileType::Inx => "application/x-indesign-interchange",
750            FileType::Eps => "application/postscript",
751        }
752    }
753
754    /// Common file extensions for this type.
755    pub fn extensions(self) -> &'static [&'static str] {
756        match self {
757            FileType::Jpeg => &["jpg", "jpeg", "jpe", "jif", "jfif"],
758            FileType::Tiff => &["tif", "tiff"],
759            FileType::Png => &["png"],
760            FileType::Gif => &["gif"],
761            FileType::Bmp => &["bmp", "dib"],
762            FileType::WebP => &["webp"],
763            FileType::Heif => &["heif", "heic", "hif"],
764            FileType::Avif => &["avif"],
765            FileType::Psd => &["psd", "psb", "psdt"],
766            FileType::Jp2 => &["jp2", "jpf", "jpm", "jpx", "jph"],
767            FileType::J2c => &["j2c", "j2k", "jpc"],
768            FileType::Jxl => &["jxl"],
769            FileType::Jxr => &["jxr", "hdp", "wdp"],
770            FileType::Flif => &["flif"],
771            FileType::Bpg => &["bpg"],
772            FileType::Exr => &["exr"],
773            FileType::Ico => &["ico", "cur"],
774            FileType::Jps => &["jps"],
775            FileType::DjVu => &["djvu", "djv"],
776            FileType::Xcf => &["xcf"],
777            FileType::Pcx => &["pcx"],
778            FileType::Pict => &["pict", "pct"],
779            FileType::Psp => &["psp", "pspimage"],
780            FileType::Hdr => &["hdr"],
781            FileType::Rwz => &["rwz"],
782            FileType::Btf => &["btf"],
783            FileType::Mng => &["mng", "jng"],
784            FileType::PhotoCd => &["pcd"],
785            // RAW
786            FileType::Cr2 => &["cr2"],
787            FileType::Cr3 => &["cr3"],
788            FileType::Crw => &["crw", "ciff"],
789            FileType::Nef => &["nef", "nrw"],
790            FileType::Arw => &["arw"],
791            FileType::Sr2 => &["sr2"],
792            FileType::Srf => &["srf"],
793            FileType::Orf => &["orf", "ori"],
794            FileType::Rw2 => &["rw2"],
795            FileType::Dng => &["dng"],
796            FileType::Raf => &["raf"],
797            FileType::Pef => &["pef"],
798            FileType::Dcr => &["dcr"],
799            FileType::Mrw => &["mrw"],
800            FileType::Erf => &["erf"],
801            FileType::Fff => &["fff"],
802            FileType::Iiq => &["iiq"],
803            FileType::Rwl => &["rwl"],
804            FileType::Mef => &["mef"],
805            FileType::Srw => &["srw"],
806            FileType::X3f => &["x3f"],
807            FileType::Gpr => &["gpr"],
808            FileType::Arq => &["arq"],
809            FileType::ThreeFR => &["3fr"],
810            FileType::Crm => &["crm"],
811            // Video
812            FileType::Mp4 => &["mp4", "m4v"],
813            FileType::QuickTime => &["mov", "qt"],
814            FileType::Avi => &["avi"],
815            FileType::Mkv => &["mkv", "mks"],
816            FileType::WebM => &["webm"],
817            FileType::Wmv => &["wmv"],
818            FileType::Asf => &["asf"],
819            FileType::Flv => &["flv"],
820            FileType::Mxf => &["mxf"],
821            FileType::Czi => &["czi"],
822            FileType::M2ts => &["mts", "m2ts", "m2t", "ts"],
823            FileType::Mpeg => &["mpg", "mpeg", "m2v", "mpv"],
824            FileType::ThreeGP => &["3gp", "3gpp", "3g2", "3gp2"],
825            FileType::RealMedia => &["rm", "rv", "rmvb"],
826            FileType::R3d => &["r3d"],
827            FileType::Dvb => &["dvb"],
828            FileType::Lrv => &["lrv", "lrf"],
829            FileType::Mqv => &["mqv"],
830            FileType::F4v => &["f4v", "f4a", "f4b", "f4p"],
831            FileType::Wtv => &["wtv"],
832            FileType::DvrMs => &["dvr-ms"],
833            // Audio
834            FileType::Mp3 => &["mp3"],
835            FileType::Flac => &["flac"],
836            FileType::Ogg => &["ogg", "oga", "ogv"],
837            FileType::Wav => &["wav"],
838            FileType::Aiff => &["aiff", "aif", "aifc"],
839            FileType::Aac => &["aac"],
840            FileType::Opus => &["opus"],
841            FileType::Mpc => &["mpc"],
842            FileType::Ape => &["ape"],
843            FileType::WavPack => &["wv", "wvp"],
844            FileType::Ofr => &["ofr"],
845            FileType::Dsf => &["dsf"],
846            FileType::Audible => &["aa", "aax"],
847            FileType::RealAudio => &["ra"],
848            FileType::Wma => &["wma"],
849            FileType::M4a => &["m4a", "m4b", "m4p"],
850            FileType::Dss => &["dss"],
851            // Documents
852            FileType::Pdf => &["pdf"],
853            FileType::PostScript => &["ps", "eps", "epsf"],
854            FileType::Doc => &["doc", "dot"],
855            FileType::Docx => &["docx", "docm"],
856            FileType::Xls => &["xls", "xlt"],
857            FileType::Xlsx => &["xlsx", "xlsm", "xlsb"],
858            FileType::Ppt => &["ppt", "pps", "pot"],
859            FileType::Pptx => &["pptx", "pptm"],
860            FileType::Numbers => &["numbers", "nmbtemplate"],
861            FileType::Pages => &["pages"],
862            FileType::Key => &["key", "kth"],
863            FileType::InDesign => &["indd", "ind", "indt"],
864            FileType::Rtf => &["rtf"],
865            // Archives
866            FileType::Zip => &["zip"],
867            FileType::Rar => &["rar"],
868            FileType::SevenZ => &["7z"],
869            FileType::Gzip => &["gz", "gzip"],
870            // Metadata / Other
871            FileType::Xmp => &["xmp"],
872            FileType::Mie => &["mie"],
873            FileType::Exv => &["exv"],
874            FileType::Vrd => &["vrd"],
875            FileType::Dr4 => &["dr4"],
876            FileType::Icc => &["icc", "icm"],
877            FileType::Html => &["html", "htm", "xhtml", "svg"],
878            FileType::Exe => &["exe", "dll", "elf", "so", "dylib", "a", "macho", "o"],
879            FileType::Font => &["ttf", "otf", "woff", "woff2", "ttc"],
880            FileType::Swf => &["swf"],
881            FileType::Dicom => &["dcm"],
882            FileType::Fits => &["fits", "fts"],
883            FileType::Fit => &["fit"],
884            FileType::Mrc => &["mrc"],
885            FileType::Moi => &["moi"],
886            FileType::MacOs => &["macos"],
887            FileType::Json => &["json"],
888            FileType::Pcap => &["pcap", "cap"],
889            FileType::Pcapng => &["pcapng", "ntar"],
890            FileType::Svg => &["svg"],
891            FileType::Pgf => &["pgf"],
892            FileType::Xisf => &["xisf"],
893            FileType::Torrent => &["torrent"],
894            FileType::Mobi => &["mobi", "azw", "azw3"],
895            FileType::SonyPmp => &["pmp"],
896            FileType::Plist => &["plist"],
897            FileType::Aae => &["aae"],
898            FileType::KyoceraRaw => &["raw"],
899            FileType::PortableFloatMap => &["pfm"],
900            FileType::Fpf => &["fpf"],
901            FileType::Ods => &["ods"],
902            FileType::Odt => &["odt"],
903            FileType::Odp => &["odp"],
904            FileType::Odg => &["odg"],
905            FileType::Odf => &["odf"],
906            FileType::Odb => &["odb"],
907            FileType::Odi => &["odi"],
908            FileType::Odc => &["odc"],
909            FileType::Lfp => &["lfp", "lfr"],
910            FileType::Eip => &["eip"],
911            FileType::Lif => &["lif"],
912            FileType::Txt => &["txt", "log", "igc"],
913            FileType::Csv => &["csv"],
914            FileType::Vcard => &["vcf", "vcard"],
915            FileType::Ics => &["ics"],
916            FileType::Url => &["url"],
917            FileType::Lnk => &["lnk"],
918            FileType::Ppm => &["ppm"],
919            FileType::Dpx => &["dpx"],
920            FileType::Ram => &["ram"],
921            FileType::Miff => &["miff", "mif"],
922            FileType::Tnef => &["tnef"],
923            FileType::Wpg => &["wpg"],
924            FileType::Dv => &["dv"],
925            FileType::Itc => &["itc"],
926            FileType::Iso => &["iso"],
927            FileType::Afm => &["afm", "acfm", "amfm"],
928            FileType::Pfa => &["pfa"],
929            FileType::Pfb => &["pfb"],
930            FileType::Dfont => &["dfont"],
931            FileType::Xml => &["xml"],
932            FileType::Inx => &["inx"],
933            FileType::Eps => &["eps", "epsf", "eps3", "epsi", "ept"],
934        }
935    }
936
937    /// Read/Write/Create support level.
938    pub fn support(self) -> Support {
939        match self {
940            // R/W/C
941            FileType::Xmp | FileType::Mie | FileType::Exv => Support::ReadWriteCreate,
942            // R/W
943            FileType::Jpeg
944            | FileType::Tiff
945            | FileType::Png
946            | FileType::Gif
947            | FileType::WebP
948            | FileType::Heif
949            | FileType::Avif
950            | FileType::Psd
951            | FileType::Jp2
952            | FileType::Jxl
953            | FileType::Jxr
954            | FileType::Flif
955            | FileType::Cr2
956            | FileType::Cr3
957            | FileType::Crw
958            | FileType::Nef
959            | FileType::Arw
960            | FileType::Arq
961            | FileType::Sr2
962            | FileType::Orf
963            | FileType::Rw2
964            | FileType::Dng
965            | FileType::Raf
966            | FileType::Pef
967            | FileType::Erf
968            | FileType::Fff
969            | FileType::Iiq
970            | FileType::Rwl
971            | FileType::Mef
972            | FileType::Srw
973            | FileType::X3f
974            | FileType::Gpr
975            | FileType::Crm
976            | FileType::Mp4
977            | FileType::QuickTime
978            | FileType::ThreeGP
979            | FileType::Dvb
980            | FileType::Lrv
981            | FileType::Mqv
982            | FileType::F4v
983            | FileType::Pdf
984            | FileType::PostScript
985            | FileType::InDesign
986            | FileType::Vrd
987            | FileType::Dr4
988            | FileType::Audible => Support::ReadWrite,
989            // R only
990            _ => Support::Read,
991        }
992    }
993
994    /// Returns an iterator over all file types.
995    pub fn all() -> &'static [FileType] {
996        ALL_FILE_TYPES
997    }
998}
999
1000static ALL_FILE_TYPES: &[FileType] = &[
1001    // Images - Standard
1002    FileType::Jpeg,
1003    FileType::Tiff,
1004    FileType::Png,
1005    FileType::Gif,
1006    FileType::Bmp,
1007    FileType::WebP,
1008    FileType::Heif,
1009    FileType::Avif,
1010    FileType::Psd,
1011    FileType::Jp2,
1012    FileType::J2c,
1013    FileType::Jxl,
1014    FileType::Jxr,
1015    FileType::Flif,
1016    FileType::Bpg,
1017    FileType::Exr,
1018    FileType::Ico,
1019    FileType::Jps,
1020    // Images - Specialized
1021    FileType::DjVu,
1022    FileType::Xcf,
1023    FileType::Pcx,
1024    FileType::Pict,
1025    FileType::Psp,
1026    FileType::Hdr,
1027    FileType::Rwz,
1028    FileType::Btf,
1029    FileType::Mng,
1030    FileType::PhotoCd,
1031    FileType::Lif,
1032    FileType::Txt,
1033    FileType::Csv,
1034    FileType::Vcard,
1035    FileType::Ics,
1036    FileType::Url,
1037    FileType::Lnk,
1038    FileType::Ppm,
1039    FileType::Dpx,
1040    FileType::Ram,
1041    FileType::Miff,
1042    FileType::Tnef,
1043    FileType::Wpg,
1044    FileType::Dv,
1045    FileType::Itc,
1046    FileType::Iso,
1047    FileType::Afm,
1048    FileType::Pfa,
1049    FileType::Pfb,
1050    FileType::Dfont,
1051    FileType::Xml,
1052    FileType::Inx,
1053    FileType::Eps,
1054    // RAW
1055    FileType::Cr2,
1056    FileType::Cr3,
1057    FileType::Crw,
1058    FileType::Nef,
1059    FileType::Arw,
1060    FileType::Sr2,
1061    FileType::Srf,
1062    FileType::Orf,
1063    FileType::Rw2,
1064    FileType::Dng,
1065    FileType::Raf,
1066    FileType::Pef,
1067    FileType::Dcr,
1068    FileType::Mrw,
1069    FileType::Erf,
1070    FileType::Fff,
1071    FileType::Iiq,
1072    FileType::Rwl,
1073    FileType::Mef,
1074    FileType::Srw,
1075    FileType::X3f,
1076    FileType::Gpr,
1077    FileType::Arq,
1078    FileType::ThreeFR,
1079    FileType::Crm,
1080    // Video
1081    FileType::Mp4,
1082    FileType::QuickTime,
1083    FileType::Avi,
1084    FileType::Mkv,
1085    FileType::WebM,
1086    FileType::Wmv,
1087    FileType::Asf,
1088    FileType::Flv,
1089    FileType::Mxf,
1090    FileType::M2ts,
1091    FileType::Mpeg,
1092    FileType::ThreeGP,
1093    FileType::RealMedia,
1094    FileType::R3d,
1095    FileType::Dvb,
1096    FileType::Lrv,
1097    FileType::Mqv,
1098    FileType::F4v,
1099    FileType::Wtv,
1100    FileType::DvrMs,
1101    // Audio
1102    FileType::Mp3,
1103    FileType::Flac,
1104    FileType::Ogg,
1105    FileType::Wav,
1106    FileType::Aiff,
1107    FileType::Aac,
1108    FileType::Opus,
1109    FileType::Mpc,
1110    FileType::Ape,
1111    FileType::WavPack,
1112    FileType::Ofr,
1113    FileType::Dsf,
1114    FileType::Audible,
1115    FileType::RealAudio,
1116    FileType::Wma,
1117    FileType::M4a,
1118    FileType::Dss,
1119    // Documents
1120    FileType::Pdf,
1121    FileType::PostScript,
1122    FileType::Doc,
1123    FileType::Docx,
1124    FileType::Xls,
1125    FileType::Xlsx,
1126    FileType::Ppt,
1127    FileType::Pptx,
1128    FileType::Numbers,
1129    FileType::Pages,
1130    FileType::Key,
1131    FileType::InDesign,
1132    FileType::Rtf,
1133    // Archives
1134    FileType::Zip,
1135    FileType::Rar,
1136    FileType::SevenZ,
1137    FileType::Gzip,
1138    // Metadata / Other
1139    FileType::Xmp,
1140    FileType::Mie,
1141    FileType::Exv,
1142    FileType::Vrd,
1143    FileType::Dr4,
1144    FileType::Icc,
1145    FileType::Html,
1146    FileType::Exe,
1147    FileType::Font,
1148    FileType::Swf,
1149    FileType::Dicom,
1150    FileType::Fits,
1151    FileType::Fit,
1152    FileType::Mrc,
1153    FileType::Moi,
1154    FileType::MacOs,
1155    FileType::Json,
1156    FileType::Pcap,
1157    FileType::Pcapng,
1158    FileType::Svg,
1159    FileType::Pgf,
1160    FileType::Xisf,
1161    FileType::Torrent,
1162    FileType::Mobi,
1163    FileType::SonyPmp,
1164    FileType::Plist,
1165    FileType::Aae,
1166    FileType::KyoceraRaw,
1167    FileType::PortableFloatMap,
1168    FileType::Fpf,
1169    FileType::Lfp,
1170    // OpenDocument
1171    FileType::Ods,
1172    FileType::Odt,
1173    FileType::Odp,
1174    FileType::Odg,
1175    FileType::Odf,
1176    FileType::Odb,
1177    FileType::Odi,
1178    FileType::Odc,
1179    // CaptureOne
1180    FileType::Eip,
1181];
1182
1183/// Detect file type from magic bytes (first 64+ bytes of a file).
1184pub fn detect_from_magic(header: &[u8]) -> Option<FileType> {
1185    if header.len() < 4 {
1186        return None;
1187    }
1188
1189    // ===== Images =====
1190
1191    // JPEG: FF D8 FF
1192    if header.starts_with(&[0xFF, 0xD8, 0xFF]) {
1193        return Some(FileType::Jpeg);
1194    }
1195
1196    // PNG: 89 50 4E 47 0D 0A 1A 0A
1197    if header.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
1198        return Some(FileType::Png);
1199    }
1200
1201    // Lytro LFP: 89 4C 46 50 0D 0A 1A 0A  ("\x89LFP\r\n\x1a\n")
1202    if header.starts_with(&[0x89, 0x4C, 0x46, 0x50, 0x0D, 0x0A, 0x1A, 0x0A]) {
1203        return Some(FileType::Lfp);
1204    }
1205
1206    // GIF: "GIF87a" or "GIF89a"
1207    if header.starts_with(b"GIF8") && header.len() >= 6 && (header[4] == b'7' || header[4] == b'9')
1208    {
1209        return Some(FileType::Gif);
1210    }
1211
1212    // Canon DR4: "IIII" + 04 00 04 00 (or 05 00 04 00)
1213    if header.len() >= 8
1214        && header.starts_with(b"IIII")
1215        && (header[4] == 0x04 || header[4] == 0x05)
1216        && header[5] == 0x00
1217        && header[6] == 0x04
1218        && header[7] == 0x00
1219    {
1220        return Some(FileType::Dr4);
1221    }
1222
1223    // Canon VRD: "CANON OPTIONAL DATA\0"
1224    if header.starts_with(b"CANON OPTIONAL DATA\0") {
1225        return Some(FileType::Vrd);
1226    }
1227
1228    // TIFF / TIFF-based RAW: "II" or "MM" + magic 42
1229    if header.len() >= 4 {
1230        let is_le =
1231            header[0] == b'I' && header[1] == b'I' && header[2] == 0x2A && header[3] == 0x00;
1232        let is_be =
1233            header[0] == b'M' && header[1] == b'M' && header[2] == 0x00 && header[3] == 0x2A;
1234        if is_le || is_be {
1235            // CR2: "II" + "CR" at offset 8
1236            if header.len() >= 10 && is_le && header[8] == b'C' && header[9] == b'R' {
1237                return Some(FileType::Cr2);
1238            }
1239            // IIQ: "IIII" (LE) or "MMMM" (BE) at offset 8
1240            if header.len() >= 12 && is_le && &header[8..12] == b"IIII" {
1241                return Some(FileType::Iiq);
1242            }
1243            if header.len() >= 12 && is_be && &header[8..12] == b"MMMM" {
1244                return Some(FileType::Iiq);
1245            }
1246            // ORF: "IIRO" or "IIRS" (Olympus)
1247            if header.len() >= 4
1248                && is_le
1249                && header[0] == b'I'
1250                && header[1] == b'I'
1251                && header.len() >= 8
1252            {
1253                // Check for ORF signature at specific offsets (Olympus uses standard TIFF with specific patterns)
1254                // For now, fall through to generic TIFF and detect by extension
1255            }
1256            // BigTIFF: magic 43 instead of 42
1257            // (handled below)
1258            return Some(FileType::Tiff);
1259        }
1260        // BigTIFF: "II" + 0x2B or "MM" + 0x002B
1261        let is_btf_le =
1262            header[0] == b'I' && header[1] == b'I' && header[2] == 0x2B && header[3] == 0x00;
1263        let is_btf_be =
1264            header[0] == b'M' && header[1] == b'M' && header[2] == 0x00 && header[3] == 0x2B;
1265        if is_btf_le || is_btf_be {
1266            return Some(FileType::Btf);
1267        }
1268    }
1269
1270    // BMP: "BM"
1271    if header.starts_with(b"BM") && header.len() >= 6 {
1272        return Some(FileType::Bmp);
1273    }
1274
1275    // RIFF container: WebP, AVI, WAV
1276    if header.len() >= 12 && header.starts_with(b"RIFF") {
1277        match &header[8..12] {
1278            b"WEBP" => return Some(FileType::WebP),
1279            b"AVI " => return Some(FileType::Avi),
1280            b"WAVE" => return Some(FileType::Wav),
1281            _ => {}
1282        }
1283    }
1284
1285    // PSD: "8BPS"
1286    if header.starts_with(b"8BPS") {
1287        return Some(FileType::Psd);
1288    }
1289
1290    // JPEG 2000: 00 00 00 0C 6A 50 20 20 (jp2 signature box)
1291    if header.len() >= 12 && header.starts_with(&[0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20]) {
1292        return Some(FileType::Jp2);
1293    }
1294
1295    // JPEG 2000 codestream: FF 4F FF 51
1296    if header.starts_with(&[0xFF, 0x4F, 0xFF, 0x51]) {
1297        return Some(FileType::J2c);
1298    }
1299
1300    // JPEG XL: FF 0A (bare codestream) or 00 00 00 0C 4A 58 4C 20 (container)
1301    if header.len() >= 2 && header[0] == 0xFF && header[1] == 0x0A {
1302        return Some(FileType::Jxl);
1303    }
1304    if header.len() >= 12 && header.starts_with(&[0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20]) {
1305        return Some(FileType::Jxl);
1306    }
1307
1308    // FLIF: "FLIF"
1309    if header.starts_with(b"FLIF") {
1310        return Some(FileType::Flif);
1311    }
1312
1313    // BPG: 0x425047FB
1314    if header.starts_with(&[0x42, 0x50, 0x47, 0xFB]) {
1315        return Some(FileType::Bpg);
1316    }
1317
1318    // OpenEXR: 76 2F 31 01
1319    if header.starts_with(&[0x76, 0x2F, 0x31, 0x01]) {
1320        return Some(FileType::Exr);
1321    }
1322
1323    // ICO: 00 00 01 00 (icon) or 00 00 02 00 (cursor)
1324    if header.len() >= 4
1325        && header[0] == 0
1326        && header[1] == 0
1327        && (header[2] == 1 || header[2] == 2)
1328        && header[3] == 0
1329    {
1330        return Some(FileType::Ico);
1331    }
1332
1333    // DjVu: "AT&TFORM"
1334    if header.len() >= 8 && header.starts_with(b"AT&TFORM") {
1335        return Some(FileType::DjVu);
1336    }
1337
1338    // GIMP XCF: "gimp xcf"
1339    if header.starts_with(b"gimp xcf") {
1340        return Some(FileType::Xcf);
1341    }
1342
1343    // MNG: 8A 4D 4E 47 0D 0A 1A 0A
1344    if header.starts_with(&[0x8A, 0x4D, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
1345        return Some(FileType::Mng);
1346    }
1347
1348    // JNG: 8B 4A 4E 47 0D 0A 1A 0A
1349    if header.starts_with(&[0x8B, 0x4A, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
1350        return Some(FileType::Mng);
1351    }
1352
1353    // Radiance HDR: "#?RADIANCE"
1354    if header.len() >= 10 && header.starts_with(b"#?RADIANCE") {
1355        return Some(FileType::Hdr);
1356    }
1357
1358    // Portable Float Map: "PF\n" (color) or "Pf\n" (grayscale)
1359    if header.len() >= 3
1360        && header[0] == b'P'
1361        && (header[1] == b'F' || header[1] == b'f')
1362        && header[2] == b'\n'
1363    {
1364        return Some(FileType::PortableFloatMap);
1365    }
1366
1367    // FLIR FPF: "FPF Public Image Format\0"
1368    if header.starts_with(b"FPF Public Image Format\0") {
1369        return Some(FileType::Fpf);
1370    }
1371
1372    // LIF (Leica Image Format): 0x70 0x00 0x00 0x00 + 4 bytes size + 0x2A + 4 bytes + '<' 0x00
1373    if header.len() >= 15
1374        && header[0] == 0x70
1375        && header[1] == 0x00
1376        && header[2] == 0x00
1377        && header[3] == 0x00
1378        && header[8] == 0x2A
1379        && header[13] == b'<'
1380        && header[14] == 0x00
1381    {
1382        return Some(FileType::Lif);
1383    }
1384
1385    // Rawzor: "rawzor"
1386    if header.starts_with(b"rawzor") {
1387        return Some(FileType::Rwz);
1388    }
1389
1390    // JPEG XR / HD Photo: "II" + 0xBC byte at offset 2 (TIFF-like but identifier 0xBC)
1391    if header.len() >= 4 && header[0] == b'I' && header[1] == b'I' && header[2] == 0xBC {
1392        return Some(FileType::Jxr);
1393    }
1394
1395    // ===== RAW formats with unique magic =====
1396
1397    // Fujifilm RAF: "FUJIFILMCCD-RAW"
1398    if header.len() >= 15 && header.starts_with(b"FUJIFILMCCD-RAW") {
1399        return Some(FileType::Raf);
1400    }
1401
1402    // Canon CRW: "II" + 0x1A00 + "HEAPCCDR"
1403    if header.len() >= 14
1404        && header[0] == b'I'
1405        && header[1] == b'I'
1406        && header[2] == 0x1A
1407        && header[3] == 0x00
1408        && &header[6..14] == b"HEAPCCDR"
1409    {
1410        return Some(FileType::Crw);
1411    }
1412
1413    // Minolta MRW: 00 4D 52 4D
1414    if header.starts_with(&[0x00, 0x4D, 0x52, 0x4D]) {
1415        return Some(FileType::Mrw);
1416    }
1417
1418    // Sigma X3F: "FOVb"
1419    if header.starts_with(b"FOVb") {
1420        return Some(FileType::X3f);
1421    }
1422
1423    // Panasonic RW2: "IIU" (special TIFF variant)
1424    if header.len() >= 4
1425        && header[0] == b'I'
1426        && header[1] == b'I'
1427        && header[2] == 0x55
1428        && header[3] == 0x00
1429    {
1430        return Some(FileType::Rw2);
1431    }
1432
1433    // ===== Video / QuickTime container =====
1434
1435    // QuickTime / MP4 / HEIF / AVIF / CR3: check for ftyp box
1436    if header.len() >= 12 && &header[4..8] == b"ftyp" {
1437        let brand = &header[8..12];
1438        // HEIF/HEIC
1439        if brand == b"heic"
1440            || brand == b"mif1"
1441            || brand == b"heim"
1442            || brand == b"heis"
1443            || brand == b"msf1"
1444        {
1445            return Some(FileType::Heif);
1446        }
1447        // AVIF
1448        if brand == b"avif" || brand == b"avis" {
1449            return Some(FileType::Avif);
1450        }
1451        // Canon CR3
1452        if brand == b"crx " {
1453            return Some(FileType::Cr3);
1454        }
1455        // QuickTime
1456        if brand == b"qt  " {
1457            return Some(FileType::QuickTime);
1458        }
1459        // 3GP
1460        if brand == b"3gp4" || brand == b"3gp5" || brand == b"3gp6" || brand == b"3g2a" {
1461            return Some(FileType::ThreeGP);
1462        }
1463        // M4A/M4V
1464        if brand == b"M4A " || brand == b"M4B " || brand == b"M4P " {
1465            return Some(FileType::M4a);
1466        }
1467        if brand == b"M4V " || brand == b"M4VH" || brand == b"M4VP" {
1468            return Some(FileType::Mp4);
1469        }
1470        // F4V
1471        if brand == b"F4V " || brand == b"F4P " {
1472            return Some(FileType::F4v);
1473        }
1474        // Default ftyp → MP4
1475        return Some(FileType::Mp4);
1476    }
1477
1478    // QuickTime without ftyp: check for common atom types at offset 4
1479    if header.len() >= 8 {
1480        let atom_type = &header[4..8];
1481        if atom_type == b"moov"
1482            || atom_type == b"mdat"
1483            || atom_type == b"wide"
1484            || atom_type == b"free"
1485            || atom_type == b"pnot"
1486            || atom_type == b"skip"
1487        {
1488            return Some(FileType::QuickTime);
1489        }
1490    }
1491
1492    // Matroska/WebM: EBML header 0x1A45DFA3
1493    if header.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) {
1494        return Some(FileType::Mkv);
1495        // Note: WebM vs MKV distinction requires reading the DocType element inside EBML
1496    }
1497
1498    // FLV: "FLV\x01"
1499    if header.starts_with(b"FLV\x01") {
1500        return Some(FileType::Flv);
1501    }
1502
1503    // ASF/WMV/WMA: 30 26 B2 75 8E 66 CF 11
1504    if header.len() >= 16 && header.starts_with(&[0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11]) {
1505        return Some(FileType::Asf);
1506        // WMV/WMA distinction is done by content/extension
1507    }
1508
1509    // MXF: 06 0E 2B 34 02 05 01 01
1510    if header.len() >= 8 && header.starts_with(&[0x06, 0x0E, 0x2B, 0x34]) {
1511        return Some(FileType::Mxf);
1512    }
1513
1514    // ZISRAW/CZI: "ZISRAWFILE" magic
1515    if header.len() >= 10 && header.starts_with(b"ZISRAWFILE") {
1516        return Some(FileType::Czi);
1517    }
1518
1519    // ICC Profile: "acsp" at offset 36 (must be before MPEG check which has loose matching)
1520    if header.len() >= 40 && &header[36..40] == b"acsp" {
1521        return Some(FileType::Icc);
1522    }
1523
1524    // MPEG: 00 00 01 Bx (system header) or 00 00 01 BA (pack start)
1525    if header.len() >= 4
1526        && header[0] == 0
1527        && header[1] == 0
1528        && header[2] == 1
1529        && (header[3] == 0xBA || header[3] == 0xBB || (header[3] & 0xF0) == 0xE0)
1530    {
1531        return Some(FileType::Mpeg);
1532    }
1533
1534    // MPEG-2 TS: 0x47 sync byte every 188 or 192 bytes
1535    if !header.is_empty() && header[0] == 0x47 {
1536        if header.len() >= 376 && header[188] == 0x47 {
1537            return Some(FileType::M2ts);
1538        }
1539        if header.len() >= 384 && header[192] == 0x47 {
1540            return Some(FileType::M2ts);
1541        }
1542    }
1543
1544    // RealMedia: ".RMF"
1545    if header.starts_with(b".RMF") {
1546        return Some(FileType::RealMedia);
1547    }
1548
1549    // RED R3D: "RED1" or "RED2"
1550    if header.starts_with(b"RED1") || header.starts_with(b"RED2") {
1551        return Some(FileType::R3d);
1552    }
1553
1554    // ===== Audio =====
1555
1556    // MP3: ID3 tag or MPEG sync word
1557    if header.starts_with(b"ID3") {
1558        return Some(FileType::Mp3);
1559    }
1560    // AAC ADTS: sync=0xFFF (12 bits), then layer bits 13-14 must be 00
1561    // 0xFF F0 or 0xFF F1 (MPEG-2 AAC) or 0xFF F8/F9 (MPEG-4 AAC with CRC/no-CRC)
1562    // Distinguishing from MP3: layer bits are 00 for AAC, non-zero for MP3
1563    if header.len() >= 2
1564        && header[0] == 0xFF
1565        && (header[1] == 0xF0 || header[1] == 0xF1 || header[1] == 0xF8 || header[1] == 0xF9)
1566    {
1567        return Some(FileType::Aac);
1568    }
1569    // MPEG audio sync: 0xFF + 0xE0 mask (after other FF-starting formats)
1570    if header.len() >= 2 && header[0] == 0xFF && (header[1] & 0xE0) == 0xE0 {
1571        return Some(FileType::Mp3);
1572    }
1573
1574    // FLAC: "fLaC"
1575    if header.starts_with(b"fLaC") {
1576        return Some(FileType::Flac);
1577    }
1578
1579    // OGG: "OggS"
1580    if header.starts_with(b"OggS") {
1581        return Some(FileType::Ogg);
1582    }
1583
1584    // AIFF: "FORM" + "AIFF" or "AIFC"
1585    if header.len() >= 12
1586        && header.starts_with(b"FORM")
1587        && (&header[8..12] == b"AIFF" || &header[8..12] == b"AIFC")
1588    {
1589        return Some(FileType::Aiff);
1590    }
1591
1592    // APE: "MAC "
1593    if header.starts_with(b"MAC ") {
1594        return Some(FileType::Ape);
1595    }
1596
1597    // Kyocera Contax N RAW: 'ARECOYK' at offset 0x19
1598    if header.len() >= 0x20 && &header[0x19..0x20] == b"ARECOYK" {
1599        return Some(FileType::KyoceraRaw);
1600    }
1601
1602    // Musepack: "MP+" or "MPCK"
1603    if header.starts_with(b"MP+") || header.starts_with(b"MPCK") {
1604        return Some(FileType::Mpc);
1605    }
1606
1607    // WavPack: "wvpk"
1608    if header.starts_with(b"wvpk") {
1609        return Some(FileType::WavPack);
1610    }
1611
1612    // DSD/DSF: "DSD "
1613    if header.starts_with(b"DSD ") {
1614        return Some(FileType::Dsf);
1615    }
1616
1617    // OptimFROG: "OFR "
1618    if header.starts_with(b"OFR ") {
1619        return Some(FileType::Ofr);
1620    }
1621
1622    // RealAudio: ".ra\xFD"
1623    if header.len() >= 4
1624        && header[0] == b'.'
1625        && header[1] == b'r'
1626        && header[2] == b'a'
1627        && header[3] == 0xFD
1628    {
1629        return Some(FileType::RealAudio);
1630    }
1631
1632    // DSS (Olympus Digital Speech Standard): "\x02dss" or "\x03ds2"
1633    if header.len() >= 4
1634        && (header[0] == 0x02 || header[0] == 0x03)
1635        && (header[1] == b'd')
1636        && (header[2] == b's')
1637        && (header[3] == b's' || header[3] == b'2')
1638    {
1639        return Some(FileType::Dss);
1640    }
1641
1642    // ===== Documents =====
1643
1644    // PDF: "%PDF-"
1645    if header.starts_with(b"%PDF-") {
1646        return Some(FileType::Pdf);
1647    }
1648
1649    // AFM (Adobe Font Metrics): "StartFontMetrics" / "StartCompFontMetrics" / "StartMasterFontMetrics"
1650    if header.starts_with(b"StartFontMetrics")
1651        || header.starts_with(b"StartCompFontMetrics")
1652        || header.starts_with(b"StartMasterFontMetrics")
1653    {
1654        return Some(FileType::Afm);
1655    }
1656
1657    // PFA (PostScript Type 1 ASCII font): %!PS-AdobeFont- / %!PS-Bitstream  / %!FontType1-
1658    // (must precede the generic PostScript "%!PS" test)
1659    if header.starts_with(b"%!PS-AdobeFont-")
1660        || header.starts_with(b"%!PS-Bitstream ")
1661        || header.starts_with(b"%!FontType1-")
1662    {
1663        return Some(FileType::Pfa);
1664    }
1665
1666    // PFB (PostScript Type 1 Binary font): 0x80 0x01 <len32> then the PFA text at offset 6
1667    if header.len() >= 21
1668        && header[0] == 0x80
1669        && header[1] == 0x01
1670        && (header[6..].starts_with(b"%!PS-AdobeFont-")
1671            || header[6..].starts_with(b"%!PS-Bitstream ")
1672            || header[6..].starts_with(b"%!FontType1-"))
1673    {
1674        return Some(FileType::Pfb);
1675    }
1676
1677    // EPS: DOS binary EPS header (C5 D0 D3 C6), or "%!PS-Adobe-N.N EPSF-N.N"
1678    if header.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6]) {
1679        return Some(FileType::Eps);
1680    }
1681    if header.starts_with(b"%!PS-Adobe-")
1682        && header[..header.len().min(32)]
1683            .windows(4)
1684            .any(|w| w == b"EPSF")
1685    {
1686        return Some(FileType::Eps);
1687    }
1688
1689    // PostScript: "%!PS" or "%!Adobe"
1690    if header.starts_with(b"%!PS") || header.starts_with(b"%!Adobe") {
1691        return Some(FileType::PostScript);
1692    }
1693
1694    // MIFF (Magick Image File Format): "id=ImageMagick"
1695    if header.starts_with(b"id=ImageMagick") {
1696        return Some(FileType::Miff);
1697    }
1698
1699    // TNEF (Transport Neutral Encapsulation Format): signature 0x223e9f78 (little-endian)
1700    if header.starts_with(&[0x78, 0x9f, 0x3e, 0x22]) {
1701        return Some(FileType::Tnef);
1702    }
1703
1704    // WPG (WordPerfect Graphics): 0xff "WPC"
1705    if header.starts_with(&[0xff, 0x57, 0x50, 0x43]) {
1706        return Some(FileType::Wpg);
1707    }
1708
1709    // DV (Digital Video): 1F 07 00 [3F|BF]
1710    if header.len() >= 4
1711        && header[0] == 0x1f
1712        && header[1] == 0x07
1713        && header[2] == 0x00
1714        && (header[3] == 0x3f || header[3] == 0xbf)
1715    {
1716        return Some(FileType::Dv);
1717    }
1718
1719    // ITC (iTunes Cover Flow): ".{4}itch"
1720    if header.len() >= 8 && &header[4..8] == b"itch" {
1721        return Some(FileType::Itc);
1722    }
1723
1724    // MS Office legacy (DOC/XLS/PPT): OLE2 compound binary D0 CF 11 E0
1725    if header.starts_with(&[0xD0, 0xCF, 0x11, 0xE0]) {
1726        return Some(FileType::Doc); // Distinguishing DOC/XLS/PPT requires deeper parsing
1727    }
1728
1729    // RTF: "{\rtf"
1730    if header.starts_with(b"{\\rtf") {
1731        return Some(FileType::Rtf);
1732    }
1733
1734    // InDesign: 06 06 ED F5 D8 1D 46 E5
1735    if header.len() >= 8 && header.starts_with(&[0x06, 0x06, 0xED, 0xF5, 0xD8, 0x1D, 0x46, 0xE5]) {
1736        return Some(FileType::InDesign);
1737    }
1738
1739    // ===== Archives =====
1740
1741    // ZIP (and DOCX/XLSX/PPTX/EPUB/etc.): "PK\x03\x04"
1742    if header.starts_with(&[0x50, 0x4B, 0x03, 0x04]) {
1743        return Some(FileType::Zip);
1744        // DOCX/XLSX/PPTX are ZIP files; distinguishing requires checking [Content_Types].xml
1745    }
1746
1747    // RAR: "Rar!\x1A\x07"
1748    if header.len() >= 6 && header.starts_with(b"Rar!\x1A\x07") {
1749        return Some(FileType::Rar);
1750    }
1751
1752    // 7-Zip: "7z\xBC\xAF\x27\x1C"
1753    if header.len() >= 6 && header.starts_with(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]) {
1754        return Some(FileType::SevenZ);
1755    }
1756
1757    // GZIP: 1F 8B
1758    if header.len() >= 2 && header[0] == 0x1F && header[1] == 0x8B {
1759        return Some(FileType::Gzip);
1760    }
1761
1762    // PCAPNG: 0x0A 0x0D 0x0D 0x0A (Section Header Block)
1763    if header.len() >= 4
1764        && header[0] == 0x0A
1765        && header[1] == 0x0D
1766        && header[2] == 0x0D
1767        && header[3] == 0x0A
1768    {
1769        return Some(FileType::Pcapng);
1770    }
1771
1772    // PCAP: D4 C3 B2 A1 (little-endian) or A1 B2 C3 D4 (big-endian)
1773    if header.len() >= 4
1774        && ((header[0] == 0xD4 && header[1] == 0xC3 && header[2] == 0xB2 && header[3] == 0xA1)
1775            || (header[0] == 0xA1 && header[1] == 0xB2 && header[2] == 0xC3 && header[3] == 0xD4))
1776    {
1777        return Some(FileType::Pcap);
1778    }
1779
1780    // ===== Other =====
1781
1782    // SWF: "FWS" (uncompressed) or "CWS" (compressed) or "ZWS"
1783    if header.len() >= 3
1784        && ((header[0] == b'F' || header[0] == b'C' || header[0] == b'Z')
1785            && header[1] == b'W'
1786            && header[2] == b'S')
1787    {
1788        return Some(FileType::Swf);
1789    }
1790
1791    // DICOM: "DICM" at offset 128
1792    if header.len() >= 132 && &header[128..132] == b"DICM" {
1793        return Some(FileType::Dicom);
1794    }
1795
1796    // MRC: axis values at offset 64-75 (each 1/2/3) and "MAP" at offset 208
1797    if header.len() >= 214 {
1798        let ax1 = u32::from_le_bytes([header[64], header[65], header[66], header[67]]);
1799        let ax2 = u32::from_le_bytes([header[68], header[69], header[70], header[71]]);
1800        let ax3 = u32::from_le_bytes([header[72], header[73], header[74], header[75]]);
1801        if (1..=3).contains(&ax1)
1802            && (1..=3).contains(&ax2)
1803            && (1..=3).contains(&ax3)
1804            && &header[208..211] == b"MAP"
1805        {
1806            let ms0 = header[212];
1807            let ms1 = header[213];
1808            if (ms1 == 0x41 || ms1 == 0x44) && ms0 == 0x44 || (ms0 == 0x11 && ms1 == 0x11) {
1809                return Some(FileType::Mrc);
1810            }
1811        }
1812    }
1813
1814    // Garmin FIT: ".FIT" at offset 8 (must precede the .fit extension fallback,
1815    // which would otherwise collide with the FITS astronomical format)
1816    if header.len() >= 12 && &header[8..12] == b".FIT" {
1817        return Some(FileType::Fit);
1818    }
1819
1820    // FITS: "SIMPLE  ="
1821    if header.len() >= 9 && header.starts_with(b"SIMPLE  =") {
1822        return Some(FileType::Fits);
1823    }
1824
1825    // MIE: "~\x10\x04" + version
1826    if header.len() >= 4 && header[0] == 0x7E && header[1] == 0x10 && header[2] == 0x04 {
1827        return Some(FileType::Mie);
1828    }
1829
1830    // XMP sidecar (starts with XML PI or xpacket)
1831    if header.starts_with(b"<?xpacket") || header.starts_with(b"<x:xmpmeta") {
1832        return Some(FileType::Xmp);
1833    }
1834
1835    // XML-based formats: look deeper to classify
1836    if header.starts_with(b"<?xml") || header.starts_with(b"<svg") {
1837        let preview = &header[..header.len().min(512)];
1838        if preview.windows(4).any(|w| w == b"<svg") {
1839            return Some(FileType::Svg);
1840        }
1841        if preview.windows(5).any(|w| w == b"<html" || w == b"<HTML") {
1842            return Some(FileType::Html); // XHTML
1843        }
1844        if preview.windows(10).any(|w| w == b"<x:xmpmeta")
1845            || preview.windows(9).any(|w| w == b"<?xpacket")
1846        {
1847            return Some(FileType::Xmp);
1848        }
1849        if preview.windows(4).any(|w| w == b"<rdf" || w == b"<RDF") {
1850            return Some(FileType::Xmp);
1851        }
1852        // Apple PLIST
1853        if preview.windows(7).any(|w| w == b"<plist")
1854            || preview.windows(20).any(|w| w == b"DTD PLIST")
1855        {
1856            return Some(FileType::Plist);
1857        }
1858        // Adobe InDesign Interchange: "<?aid" processing instruction
1859        if preview.windows(5).any(|w| w == b"<?aid") {
1860            return Some(FileType::Inx);
1861        }
1862        // Default XML → XML (ExifTool reports plain XML; XMP needs rdf/xmpmeta)
1863        return Some(FileType::Xml);
1864    }
1865
1866    // HTML
1867    if header.starts_with(b"<!DOCTYPE html")
1868        || header.starts_with(b"<!doctype html")
1869        || header.starts_with(b"<!DOCTYPE HTML")
1870        || header.starts_with(b"<html")
1871        || header.starts_with(b"<HTML")
1872    {
1873        return Some(FileType::Html);
1874    }
1875
1876    // ELF executable: \x7FELF
1877    if header.starts_with(&[0x7F, b'E', b'L', b'F']) {
1878        return Some(FileType::Exe);
1879    }
1880
1881    // Mach-O 32-bit: FEEDFACE (big) or CEFAEDFE (little)
1882    if header.starts_with(&[0xFE, 0xED, 0xFA, 0xCE])
1883        || header.starts_with(&[0xCE, 0xFA, 0xED, 0xFE])
1884    {
1885        return Some(FileType::Exe);
1886    }
1887
1888    // Mach-O 64-bit: FEEDFACF (big) or CFFAEDFE (little)
1889    if header.starts_with(&[0xFE, 0xED, 0xFA, 0xCF])
1890        || header.starts_with(&[0xCF, 0xFA, 0xED, 0xFE])
1891    {
1892        return Some(FileType::Exe);
1893    }
1894
1895    // Mach-O Universal/Fat binary: CAFEBABE
1896    if header.starts_with(&[0xCA, 0xFE, 0xBA, 0xBE]) {
1897        return Some(FileType::Exe);
1898    }
1899
1900    // PE executable: "MZ"
1901    if header.starts_with(b"MZ") {
1902        return Some(FileType::Exe);
1903    }
1904
1905    // TrueType font: 00 01 00 00 or "true" or "typ1"
1906    if (header.starts_with(&[0x00, 0x01, 0x00, 0x00])
1907        || header.starts_with(b"true")
1908        || header.starts_with(b"typ1"))
1909        && header.len() >= 12
1910    {
1911        return Some(FileType::Font);
1912    }
1913
1914    // TrueType Collection: "ttcf"
1915    if header.starts_with(b"ttcf") {
1916        return Some(FileType::Font);
1917    }
1918
1919    // OpenType font: "OTTO"
1920    if header.starts_with(b"OTTO") {
1921        return Some(FileType::Font);
1922    }
1923
1924    // WOFF: "wOFF"
1925    if header.starts_with(b"wOFF") {
1926        return Some(FileType::Font);
1927    }
1928
1929    // WOFF2: "wOF2"
1930    if header.starts_with(b"wOF2") {
1931        return Some(FileType::Font);
1932    }
1933
1934    // MOI: starts with "V6" (camcorder info file)
1935    if header.len() >= 2 && header[0] == b'V' && header[1] == b'6' {
1936        return Some(FileType::Moi);
1937    }
1938
1939    // PGF: "PGF"
1940    if header.starts_with(b"PGF") {
1941        return Some(FileType::Pgf);
1942    }
1943
1944    // XISF: "XISF0100"
1945    if header.starts_with(b"XISF0100") {
1946        return Some(FileType::Xisf);
1947    }
1948
1949    // Paint Shop Pro: "Paint Shop Pro Image File\n\x1a\0\0\0\0\0"
1950    if header.len() >= 27 && header.starts_with(b"Paint Shop Pro Image File\n\x1a") {
1951        return Some(FileType::Psp);
1952    }
1953
1954    // Sony PMP: magic at offset 8-11 is 00 00 00 7C (header length = 124)
1955    // and byte 4 is 0x00 (part of file size field)
1956    if header.len() >= 12
1957        && header[8] == 0x00
1958        && header[9] == 0x00
1959        && header[10] == 0x00
1960        && header[11] == 0x7C
1961    {
1962        return Some(FileType::SonyPmp);
1963    }
1964
1965    None
1966}
1967
1968/// Detect file type from file extension.
1969pub fn detect_from_extension(ext: &str) -> Option<FileType> {
1970    let ext_lower = ext.to_ascii_lowercase();
1971    let ext_lower = ext_lower.trim_start_matches('.');
1972
1973    for &ft in ALL_FILE_TYPES {
1974        for known_ext in ft.extensions() {
1975            if ext_lower == *known_ext {
1976                return Some(ft);
1977            }
1978        }
1979    }
1980
1981    None
1982}
1983
1984impl std::fmt::Display for FileType {
1985    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1986        write!(f, "{}", self.description())
1987    }
1988}
1989
1990impl std::fmt::Display for Support {
1991    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1992        match self {
1993            Support::Read => write!(f, "R"),
1994            Support::ReadWrite => write!(f, "R/W"),
1995            Support::ReadWriteCreate => write!(f, "R/W/C"),
1996        }
1997    }
1998}
1999
2000#[cfg(test)]
2001mod tests {
2002    use super::*;
2003
2004    #[test]
2005    fn test_detect_jpeg() {
2006        let header = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, b'J', b'F', b'I', b'F'];
2007        assert_eq!(detect_from_magic(&header), Some(FileType::Jpeg));
2008    }
2009
2010    #[test]
2011    fn test_detect_png() {
2012        let header = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
2013        assert_eq!(detect_from_magic(&header), Some(FileType::Png));
2014    }
2015
2016    #[test]
2017    fn test_detect_tiff_le() {
2018        let header = [b'I', b'I', 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00];
2019        assert_eq!(detect_from_magic(&header), Some(FileType::Tiff));
2020    }
2021
2022    #[test]
2023    fn test_detect_tiff_be() {
2024        let header = [b'M', b'M', 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08];
2025        assert_eq!(detect_from_magic(&header), Some(FileType::Tiff));
2026    }
2027
2028    #[test]
2029    fn test_detect_cr2() {
2030        let header = [b'I', b'I', 0x2A, 0x00, 0x10, 0x00, 0x00, 0x00, b'C', b'R'];
2031        assert_eq!(detect_from_magic(&header), Some(FileType::Cr2));
2032    }
2033
2034    #[test]
2035    fn test_detect_pdf() {
2036        let header = b"%PDF-1.7 some more data here";
2037        assert_eq!(detect_from_magic(header), Some(FileType::Pdf));
2038    }
2039
2040    #[test]
2041    fn test_detect_webp() {
2042        let header = b"RIFF\x00\x00\x00\x00WEBP";
2043        assert_eq!(detect_from_magic(header), Some(FileType::WebP));
2044    }
2045
2046    #[test]
2047    fn test_detect_heif() {
2048        let mut header = [0u8; 16];
2049        header[4..8].copy_from_slice(b"ftyp");
2050        header[8..12].copy_from_slice(b"heic");
2051        assert_eq!(detect_from_magic(&header), Some(FileType::Heif));
2052    }
2053
2054    #[test]
2055    fn test_detect_avif() {
2056        let mut header = [0u8; 16];
2057        header[4..8].copy_from_slice(b"ftyp");
2058        header[8..12].copy_from_slice(b"avif");
2059        assert_eq!(detect_from_magic(&header), Some(FileType::Avif));
2060    }
2061
2062    #[test]
2063    fn test_detect_cr3() {
2064        let mut header = [0u8; 16];
2065        header[4..8].copy_from_slice(b"ftyp");
2066        header[8..12].copy_from_slice(b"crx ");
2067        assert_eq!(detect_from_magic(&header), Some(FileType::Cr3));
2068    }
2069
2070    #[test]
2071    fn test_detect_flac() {
2072        assert_eq!(detect_from_magic(b"fLaC\x00\x00"), Some(FileType::Flac));
2073    }
2074
2075    #[test]
2076    fn test_detect_ogg() {
2077        assert_eq!(detect_from_magic(b"OggS\x00\x02"), Some(FileType::Ogg));
2078    }
2079
2080    #[test]
2081    fn test_detect_mp3_id3() {
2082        assert_eq!(detect_from_magic(b"ID3\x04\x00"), Some(FileType::Mp3));
2083    }
2084
2085    #[test]
2086    fn test_detect_rar() {
2087        assert_eq!(
2088            detect_from_magic(b"Rar!\x1A\x07\x01\x00"),
2089            Some(FileType::Rar)
2090        );
2091    }
2092
2093    #[test]
2094    fn test_detect_7z() {
2095        assert_eq!(
2096            detect_from_magic(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]),
2097            Some(FileType::SevenZ)
2098        );
2099    }
2100
2101    #[test]
2102    fn test_detect_gzip() {
2103        assert_eq!(
2104            detect_from_magic(&[0x1F, 0x8B, 0x08, 0x00]),
2105            Some(FileType::Gzip)
2106        );
2107    }
2108
2109    #[test]
2110    fn test_detect_raf() {
2111        assert_eq!(
2112            detect_from_magic(b"FUJIFILMCCD-RAW 0201"),
2113            Some(FileType::Raf)
2114        );
2115    }
2116
2117    #[test]
2118    fn test_detect_psd() {
2119        assert_eq!(detect_from_magic(b"8BPS\x00\x01"), Some(FileType::Psd));
2120    }
2121
2122    #[test]
2123    fn test_detect_from_extension() {
2124        assert_eq!(detect_from_extension("jpg"), Some(FileType::Jpeg));
2125        assert_eq!(detect_from_extension(".JPEG"), Some(FileType::Jpeg));
2126        assert_eq!(detect_from_extension("cr2"), Some(FileType::Cr2));
2127        assert_eq!(detect_from_extension("cr3"), Some(FileType::Cr3));
2128        assert_eq!(detect_from_extension("nef"), Some(FileType::Nef));
2129        assert_eq!(detect_from_extension("arw"), Some(FileType::Arw));
2130        assert_eq!(detect_from_extension("dng"), Some(FileType::Dng));
2131        assert_eq!(detect_from_extension("raf"), Some(FileType::Raf));
2132        assert_eq!(detect_from_extension("mp4"), Some(FileType::Mp4));
2133        assert_eq!(detect_from_extension("mov"), Some(FileType::QuickTime));
2134        assert_eq!(detect_from_extension("flac"), Some(FileType::Flac));
2135        assert_eq!(detect_from_extension("docx"), Some(FileType::Docx));
2136        assert_eq!(detect_from_extension("xlsx"), Some(FileType::Xlsx));
2137        assert_eq!(detect_from_extension("3fr"), Some(FileType::ThreeFR));
2138        assert_eq!(detect_from_extension("xyz"), None);
2139    }
2140
2141    #[test]
2142    fn test_all_types_have_extensions() {
2143        for &ft in FileType::all() {
2144            assert!(!ft.extensions().is_empty(), "{:?} has no extensions", ft);
2145        }
2146    }
2147
2148    #[test]
2149    fn test_all_types_have_mime() {
2150        for &ft in FileType::all() {
2151            assert!(!ft.mime_type().is_empty(), "{:?} has no MIME type", ft);
2152        }
2153    }
2154
2155    #[test]
2156    fn test_total_format_count() {
2157        assert!(
2158            FileType::all().len() >= 100,
2159            "Expected 100+ formats, got {}",
2160            FileType::all().len()
2161        );
2162    }
2163
2164    #[test]
2165    fn test_detect_mp4_ftyp_isom() {
2166        let mut header = [0u8; 16];
2167        // size=16, type=ftyp, brand=isom
2168        header[0..4].copy_from_slice(&[0x00, 0x00, 0x00, 0x10]);
2169        header[4..8].copy_from_slice(b"ftyp");
2170        header[8..12].copy_from_slice(b"isom");
2171        assert_eq!(detect_from_magic(&header), Some(FileType::Mp4));
2172    }
2173
2174    #[test]
2175    fn test_detect_mp4_ftyp_mp42() {
2176        let mut header = [0u8; 16];
2177        header[0..4].copy_from_slice(&[0x00, 0x00, 0x00, 0x10]);
2178        header[4..8].copy_from_slice(b"ftyp");
2179        header[8..12].copy_from_slice(b"mp42");
2180        assert_eq!(detect_from_magic(&header), Some(FileType::Mp4));
2181    }
2182
2183    #[test]
2184    fn test_detect_mkv_ebml() {
2185        // Matroska/WebM EBML header
2186        let header = [0x1A, 0x45, 0xDF, 0xA3, 0x93, 0x42, 0x82, 0x88];
2187        assert_eq!(detect_from_magic(&header), Some(FileType::Mkv));
2188    }
2189
2190    #[test]
2191    fn test_detect_flif() {
2192        assert_eq!(
2193            detect_from_magic(b"FLIF\x30\x00\x01\x00"),
2194            Some(FileType::Flif)
2195        );
2196    }
2197
2198    #[test]
2199    fn test_detect_bpg() {
2200        assert_eq!(
2201            detect_from_magic(&[0x42, 0x50, 0x47, 0xFB, 0x00, 0x00]),
2202            Some(FileType::Bpg)
2203        );
2204    }
2205
2206    #[test]
2207    fn test_detect_exe_mz() {
2208        // PE/MZ header
2209        assert_eq!(
2210            detect_from_magic(b"MZ\x90\x00\x03\x00\x00\x00"),
2211            Some(FileType::Exe)
2212        );
2213    }
2214
2215    #[test]
2216    fn test_detect_elf() {
2217        assert_eq!(
2218            detect_from_magic(&[0x7F, b'E', b'L', b'F', 0x02, 0x01]),
2219            Some(FileType::Exe)
2220        );
2221    }
2222
2223    #[test]
2224    fn test_detect_macho_64_le() {
2225        // Mach-O 64-bit little-endian
2226        assert_eq!(
2227            detect_from_magic(&[0xCF, 0xFA, 0xED, 0xFE, 0x07, 0x00]),
2228            Some(FileType::Exe)
2229        );
2230    }
2231
2232    #[test]
2233    fn test_detect_macho_32_be() {
2234        // Mach-O 32-bit big-endian
2235        assert_eq!(
2236            detect_from_magic(&[0xFE, 0xED, 0xFA, 0xCE, 0x00, 0x00]),
2237            Some(FileType::Exe)
2238        );
2239    }
2240
2241    #[test]
2242    fn test_detect_macho_fat() {
2243        // Mach-O Universal/Fat binary
2244        assert_eq!(
2245            detect_from_magic(&[0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00]),
2246            Some(FileType::Exe)
2247        );
2248    }
2249
2250    #[test]
2251    fn test_detect_dicom() {
2252        // DICOM: "DICM" at offset 128
2253        let mut header = vec![0u8; 136];
2254        header[128..132].copy_from_slice(b"DICM");
2255        assert_eq!(detect_from_magic(&header), Some(FileType::Dicom));
2256    }
2257}