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