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