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    // ===== Images - Specialized =====
29    DjVu,
30    Xcf,
31    Pcx,
32    Pict,
33    Psp,
34    Hdr,
35    Rwz,
36    Btf,
37    Mng,
38    // ===== Images - RAW =====
39    Cr2,
40    Cr3,
41    Crw,
42    Nef,
43    Arw,
44    Sr2,
45    Srf,
46    Orf,
47    Rw2,
48    Dng,
49    Raf,
50    Pef,
51    Dcr,
52    Mrw,
53    Erf,
54    Fff,
55    Iiq,
56    Rwl,
57    Mef,
58    Srw,
59    X3f,
60    Gpr,
61    Arq,
62    ThreeFR,
63    Crm,
64    // ===== Video =====
65    Mp4,
66    QuickTime,
67    Avi,
68    Mkv,
69    WebM,
70    Wmv,
71    Asf,
72    Flv,
73    Mxf,
74    M2ts,
75    Mpeg,
76    ThreeGP,
77    RealMedia,
78    R3d,
79    Dvb,
80    Lrv,
81    Mqv,
82    F4v,
83    Wtv,
84    DvrMs,
85    // ===== Audio =====
86    Mp3,
87    Flac,
88    Ogg,
89    Wav,
90    Aiff,
91    Aac,
92    Opus,
93    Mpc,
94    Ape,
95    WavPack,
96    Ofr,
97    Dsf,
98    Audible,
99    RealAudio,
100    Wma,
101    M4a,
102    // ===== Documents =====
103    Pdf,
104    PostScript,
105    Doc,
106    Docx,
107    Xls,
108    Xlsx,
109    Ppt,
110    Pptx,
111    InDesign,
112    Rtf,
113    // ===== Archives =====
114    Zip,
115    Rar,
116    SevenZ,
117    Gzip,
118    // ===== Metadata / Other =====
119    Xmp,
120    Mie,
121    Exv,
122    Vrd,
123    Icc,
124    Html,
125    Exe,
126    Font,
127    Swf,
128    Dicom,
129    Fits,
130}
131
132/// Indicates the read/write capability for a file type.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum Support {
135    Read,
136    ReadWrite,
137    ReadWriteCreate,
138}
139
140impl FileType {
141    /// Human-readable file type description.
142    pub fn description(self) -> &'static str {
143        match self {
144            // Standard images
145            FileType::Jpeg => "JPEG image",
146            FileType::Tiff => "TIFF image",
147            FileType::Png => "PNG image",
148            FileType::Gif => "GIF image",
149            FileType::Bmp => "BMP image",
150            FileType::WebP => "WebP image",
151            FileType::Heif => "HEIF/HEIC image",
152            FileType::Avif => "AVIF image",
153            FileType::Psd => "Adobe Photoshop Document",
154            FileType::Jp2 => "JPEG 2000 image",
155            FileType::J2c => "JPEG 2000 Codestream",
156            FileType::Jxl => "JPEG XL image",
157            FileType::Jxr => "JPEG XR / HD Photo",
158            FileType::Flif => "Free Lossless Image Format",
159            FileType::Bpg => "Better Portable Graphics",
160            FileType::Exr => "OpenEXR image",
161            FileType::Ico => "Windows Icon",
162            // Specialized images
163            FileType::DjVu => "DjVu document",
164            FileType::Xcf => "GIMP image",
165            FileType::Pcx => "PCX image",
166            FileType::Pict => "Apple PICT",
167            FileType::Psp => "Paint Shop Pro image",
168            FileType::Hdr => "Radiance HDR",
169            FileType::Rwz => "Rawzor compressed image",
170            FileType::Btf => "BigTIFF image",
171            FileType::Mng => "MNG animation",
172            // RAW
173            FileType::Cr2 => "Canon CR2 RAW",
174            FileType::Cr3 => "Canon CR3 RAW",
175            FileType::Crw => "Canon CRW RAW",
176            FileType::Nef => "Nikon NEF RAW",
177            FileType::Arw => "Sony ARW RAW",
178            FileType::Sr2 => "Sony SR2 RAW",
179            FileType::Srf => "Sony SRF RAW",
180            FileType::Orf => "Olympus ORF RAW",
181            FileType::Rw2 => "Panasonic RW2 RAW",
182            FileType::Dng => "Adobe Digital Negative",
183            FileType::Raf => "Fujifilm RAF RAW",
184            FileType::Pef => "Pentax PEF RAW",
185            FileType::Dcr => "Kodak DCR RAW",
186            FileType::Mrw => "Minolta MRW RAW",
187            FileType::Erf => "Epson ERF RAW",
188            FileType::Fff => "Hasselblad FFF RAW",
189            FileType::Iiq => "Phase One IIQ RAW",
190            FileType::Rwl => "Leica RWL RAW",
191            FileType::Mef => "Mamiya MEF RAW",
192            FileType::Srw => "Samsung SRW RAW",
193            FileType::X3f => "Sigma X3F RAW",
194            FileType::Gpr => "GoPro GPR RAW",
195            FileType::Arq => "Sony ARQ RAW",
196            FileType::ThreeFR => "Hasselblad 3FR RAW",
197            FileType::Crm => "Canon Cinema RAW",
198            // Video
199            FileType::Mp4 => "MP4 video",
200            FileType::QuickTime => "QuickTime video",
201            FileType::Avi => "AVI video",
202            FileType::Mkv => "Matroska video",
203            FileType::WebM => "WebM video",
204            FileType::Wmv => "Windows Media Video",
205            FileType::Asf => "Advanced Systems Format",
206            FileType::Flv => "Flash Video",
207            FileType::Mxf => "Material Exchange Format",
208            FileType::M2ts => "MPEG-2 Transport Stream",
209            FileType::Mpeg => "MPEG video",
210            FileType::ThreeGP => "3GPP multimedia",
211            FileType::RealMedia => "RealMedia",
212            FileType::R3d => "Redcode RAW video",
213            FileType::Dvb => "Digital Video Broadcasting",
214            FileType::Lrv => "GoPro Low-Res Video",
215            FileType::Mqv => "Sony Movie",
216            FileType::F4v => "Adobe Flash Video",
217            FileType::Wtv => "Windows Recorded TV",
218            FileType::DvrMs => "Microsoft DVR",
219            // Audio
220            FileType::Mp3 => "MP3 audio",
221            FileType::Flac => "FLAC audio",
222            FileType::Ogg => "Ogg Vorbis audio",
223            FileType::Wav => "WAV audio",
224            FileType::Aiff => "AIFF audio",
225            FileType::Aac => "AAC audio",
226            FileType::Opus => "Opus audio",
227            FileType::Mpc => "Musepack audio",
228            FileType::Ape => "Monkey's Audio",
229            FileType::WavPack => "WavPack audio",
230            FileType::Ofr => "OptimFROG audio",
231            FileType::Dsf => "DSD Stream File",
232            FileType::Audible => "Audible audiobook",
233            FileType::RealAudio => "RealAudio",
234            FileType::Wma => "Windows Media Audio",
235            FileType::M4a => "MPEG-4 Audio",
236            // Documents
237            FileType::Pdf => "PDF document",
238            FileType::PostScript => "PostScript",
239            FileType::Doc => "Microsoft Word (legacy)",
240            FileType::Docx => "Microsoft Word",
241            FileType::Xls => "Microsoft Excel (legacy)",
242            FileType::Xlsx => "Microsoft Excel",
243            FileType::Ppt => "Microsoft PowerPoint (legacy)",
244            FileType::Pptx => "Microsoft PowerPoint",
245            FileType::InDesign => "Adobe InDesign",
246            FileType::Rtf => "Rich Text Format",
247            // Archives
248            FileType::Zip => "ZIP archive",
249            FileType::Rar => "RAR archive",
250            FileType::SevenZ => "7-Zip archive",
251            FileType::Gzip => "GZIP archive",
252            // Metadata / Other
253            FileType::Xmp => "XMP sidecar",
254            FileType::Mie => "MIE metadata",
255            FileType::Exv => "Exiv2 metadata",
256            FileType::Vrd => "Canon VRD recipe",
257            FileType::Icc => "ICC color profile",
258            FileType::Html => "HTML document",
259            FileType::Exe => "Windows executable",
260            FileType::Font => "Font file",
261            FileType::Swf => "Shockwave Flash",
262            FileType::Dicom => "DICOM medical image",
263            FileType::Fits => "FITS astronomical image",
264        }
265    }
266
267    /// MIME type for this file type.
268    pub fn mime_type(self) -> &'static str {
269        match self {
270            FileType::Jpeg => "image/jpeg",
271            FileType::Tiff | FileType::Btf => "image/tiff",
272            FileType::Png => "image/png",
273            FileType::Gif => "image/gif",
274            FileType::Bmp => "image/bmp",
275            FileType::WebP => "image/webp",
276            FileType::Heif => "image/heif",
277            FileType::Avif => "image/avif",
278            FileType::Psd => "image/vnd.adobe.photoshop",
279            FileType::Jp2 => "image/jp2",
280            FileType::J2c => "image/x-j2c",
281            FileType::Jxl => "image/jxl",
282            FileType::Jxr => "image/jxr",
283            FileType::Flif => "image/flif",
284            FileType::Bpg => "image/bpg",
285            FileType::Exr => "image/x-exr",
286            FileType::Ico => "image/x-icon",
287            FileType::DjVu => "image/vnd.djvu",
288            FileType::Xcf => "image/x-xcf",
289            FileType::Pcx => "image/x-pcx",
290            FileType::Pict => "image/x-pict",
291            FileType::Psp => "image/x-psp",
292            FileType::Hdr => "image/vnd.radiance",
293            FileType::Rwz => "image/x-rawzor",
294            FileType::Mng => "video/x-mng",
295            // RAW → use specific MIME where available
296            FileType::Cr2 => "image/x-canon-cr2",
297            FileType::Cr3 | FileType::Crm => "image/x-canon-cr3",
298            FileType::Crw => "image/x-canon-crw",
299            FileType::Nef => "image/x-nikon-nef",
300            FileType::Arw | FileType::Arq => "image/x-sony-arw",
301            FileType::Sr2 => "image/x-sony-sr2",
302            FileType::Srf => "image/x-sony-srf",
303            FileType::Orf => "image/x-olympus-orf",
304            FileType::Rw2 => "image/x-panasonic-rw2",
305            FileType::Dng | FileType::Gpr => "image/x-adobe-dng",
306            FileType::Raf => "image/x-fuji-raf",
307            FileType::Pef => "image/x-pentax-pef",
308            FileType::Dcr => "image/x-kodak-dcr",
309            FileType::Mrw => "image/x-minolta-mrw",
310            FileType::Erf => "image/x-epson-erf",
311            FileType::Fff | FileType::ThreeFR => "image/x-hasselblad-fff",
312            FileType::Iiq => "image/x-phaseone-iiq",
313            FileType::Rwl => "image/x-leica-rwl",
314            FileType::Mef => "image/x-mamiya-mef",
315            FileType::Srw => "image/x-samsung-srw",
316            FileType::X3f => "image/x-sigma-x3f",
317            // Video
318            FileType::Mp4 | FileType::F4v => "video/mp4",
319            FileType::QuickTime | FileType::Mqv => "video/quicktime",
320            FileType::Avi => "video/x-msvideo",
321            FileType::Mkv => "video/x-matroska",
322            FileType::WebM => "video/webm",
323            FileType::Wmv => "video/x-ms-wmv",
324            FileType::Asf => "video/x-ms-asf",
325            FileType::Flv => "video/x-flv",
326            FileType::Mxf => "application/mxf",
327            FileType::M2ts => "video/mp2t",
328            FileType::Mpeg => "video/mpeg",
329            FileType::ThreeGP => "video/3gpp",
330            FileType::RealMedia => "application/vnd.rn-realmedia",
331            FileType::R3d => "video/x-red-r3d",
332            FileType::Dvb => "video/dvb",
333            FileType::Lrv => "video/mp4",
334            FileType::Wtv => "video/x-ms-wtv",
335            FileType::DvrMs => "video/x-ms-dvr",
336            // Audio
337            FileType::Mp3 => "audio/mpeg",
338            FileType::Flac => "audio/flac",
339            FileType::Ogg | FileType::Opus => "audio/ogg",
340            FileType::Wav => "audio/wav",
341            FileType::Aiff => "audio/aiff",
342            FileType::Aac => "audio/aac",
343            FileType::Mpc => "audio/x-musepack",
344            FileType::Ape => "audio/x-ape",
345            FileType::WavPack => "audio/x-wavpack",
346            FileType::Ofr => "audio/x-ofr",
347            FileType::Dsf => "audio/dsf",
348            FileType::Audible => "audio/x-pn-audibleaudio",
349            FileType::RealAudio => "audio/x-pn-realaudio",
350            FileType::Wma => "audio/x-ms-wma",
351            FileType::M4a => "audio/mp4",
352            // Documents
353            FileType::Pdf => "application/pdf",
354            FileType::PostScript => "application/postscript",
355            FileType::Doc => "application/msword",
356            FileType::Docx => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
357            FileType::Xls => "application/vnd.ms-excel",
358            FileType::Xlsx => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
359            FileType::Ppt => "application/vnd.ms-powerpoint",
360            FileType::Pptx => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
361            FileType::InDesign => "application/x-indesign",
362            FileType::Rtf => "application/rtf",
363            // Archives
364            FileType::Zip => "application/zip",
365            FileType::Rar => "application/x-rar-compressed",
366            FileType::SevenZ => "application/x-7z-compressed",
367            FileType::Gzip => "application/gzip",
368            // Metadata / Other
369            FileType::Xmp => "application/rdf+xml",
370            FileType::Mie => "application/x-mie",
371            FileType::Exv => "application/x-exv",
372            FileType::Vrd => "application/x-canon-vrd",
373            FileType::Icc => "application/vnd.icc.profile",
374            FileType::Html => "text/html",
375            FileType::Exe => "application/x-dosexec",
376            FileType::Font => "font/sfnt",
377            FileType::Swf => "application/x-shockwave-flash",
378            FileType::Dicom => "application/dicom",
379            FileType::Fits => "application/fits",
380        }
381    }
382
383    /// Common file extensions for this type.
384    pub fn extensions(self) -> &'static [&'static str] {
385        match self {
386            FileType::Jpeg => &["jpg", "jpeg", "jpe", "jif", "jfif"],
387            FileType::Tiff => &["tif", "tiff"],
388            FileType::Png => &["png"],
389            FileType::Gif => &["gif"],
390            FileType::Bmp => &["bmp", "dib"],
391            FileType::WebP => &["webp"],
392            FileType::Heif => &["heif", "heic", "hif"],
393            FileType::Avif => &["avif"],
394            FileType::Psd => &["psd", "psb", "psdt"],
395            FileType::Jp2 => &["jp2", "jpf", "jpm", "jpx", "jph"],
396            FileType::J2c => &["j2c", "j2k", "jpc"],
397            FileType::Jxl => &["jxl"],
398            FileType::Jxr => &["jxr", "hdp", "wdp"],
399            FileType::Flif => &["flif"],
400            FileType::Bpg => &["bpg"],
401            FileType::Exr => &["exr"],
402            FileType::Ico => &["ico", "cur"],
403            FileType::DjVu => &["djvu", "djv"],
404            FileType::Xcf => &["xcf"],
405            FileType::Pcx => &["pcx"],
406            FileType::Pict => &["pict", "pct"],
407            FileType::Psp => &["psp", "pspimage"],
408            FileType::Hdr => &["hdr"],
409            FileType::Rwz => &["rwz"],
410            FileType::Btf => &["btf"],
411            FileType::Mng => &["mng", "jng"],
412            // RAW
413            FileType::Cr2 => &["cr2"],
414            FileType::Cr3 => &["cr3"],
415            FileType::Crw => &["crw", "ciff"],
416            FileType::Nef => &["nef", "nrw"],
417            FileType::Arw => &["arw"],
418            FileType::Sr2 => &["sr2"],
419            FileType::Srf => &["srf"],
420            FileType::Orf => &["orf", "ori"],
421            FileType::Rw2 => &["rw2"],
422            FileType::Dng => &["dng"],
423            FileType::Raf => &["raf"],
424            FileType::Pef => &["pef"],
425            FileType::Dcr => &["dcr"],
426            FileType::Mrw => &["mrw"],
427            FileType::Erf => &["erf"],
428            FileType::Fff => &["fff"],
429            FileType::Iiq => &["iiq"],
430            FileType::Rwl => &["rwl"],
431            FileType::Mef => &["mef"],
432            FileType::Srw => &["srw"],
433            FileType::X3f => &["x3f"],
434            FileType::Gpr => &["gpr"],
435            FileType::Arq => &["arq"],
436            FileType::ThreeFR => &["3fr"],
437            FileType::Crm => &["crm"],
438            // Video
439            FileType::Mp4 => &["mp4", "m4v"],
440            FileType::QuickTime => &["mov", "qt"],
441            FileType::Avi => &["avi"],
442            FileType::Mkv => &["mkv", "mks"],
443            FileType::WebM => &["webm"],
444            FileType::Wmv => &["wmv"],
445            FileType::Asf => &["asf"],
446            FileType::Flv => &["flv"],
447            FileType::Mxf => &["mxf"],
448            FileType::M2ts => &["m2ts", "mts", "m2t", "ts"],
449            FileType::Mpeg => &["mpg", "mpeg", "m2v", "mpv"],
450            FileType::ThreeGP => &["3gp", "3gpp", "3g2", "3gp2"],
451            FileType::RealMedia => &["rm", "rv", "rmvb"],
452            FileType::R3d => &["r3d"],
453            FileType::Dvb => &["dvb"],
454            FileType::Lrv => &["lrv", "lrf"],
455            FileType::Mqv => &["mqv"],
456            FileType::F4v => &["f4v", "f4a", "f4b", "f4p"],
457            FileType::Wtv => &["wtv"],
458            FileType::DvrMs => &["dvr-ms"],
459            // Audio
460            FileType::Mp3 => &["mp3"],
461            FileType::Flac => &["flac"],
462            FileType::Ogg => &["ogg", "oga", "ogv"],
463            FileType::Wav => &["wav"],
464            FileType::Aiff => &["aiff", "aif", "aifc"],
465            FileType::Aac => &["aac"],
466            FileType::Opus => &["opus"],
467            FileType::Mpc => &["mpc"],
468            FileType::Ape => &["ape"],
469            FileType::WavPack => &["wv", "wvp"],
470            FileType::Ofr => &["ofr"],
471            FileType::Dsf => &["dsf"],
472            FileType::Audible => &["aa", "aax"],
473            FileType::RealAudio => &["ra"],
474            FileType::Wma => &["wma"],
475            FileType::M4a => &["m4a", "m4b", "m4p"],
476            // Documents
477            FileType::Pdf => &["pdf"],
478            FileType::PostScript => &["ps", "eps", "epsf"],
479            FileType::Doc => &["doc", "dot"],
480            FileType::Docx => &["docx", "docm"],
481            FileType::Xls => &["xls", "xlt"],
482            FileType::Xlsx => &["xlsx", "xlsm", "xlsb"],
483            FileType::Ppt => &["ppt", "pps", "pot"],
484            FileType::Pptx => &["pptx", "pptm"],
485            FileType::InDesign => &["ind", "indd", "indt"],
486            FileType::Rtf => &["rtf"],
487            // Archives
488            FileType::Zip => &["zip"],
489            FileType::Rar => &["rar"],
490            FileType::SevenZ => &["7z"],
491            FileType::Gzip => &["gz", "gzip"],
492            // Metadata / Other
493            FileType::Xmp => &["xmp", "inx", "xml"],
494            FileType::Mie => &["mie"],
495            FileType::Exv => &["exv"],
496            FileType::Vrd => &["vrd", "dr4"],
497            FileType::Icc => &["icc", "icm"],
498            FileType::Html => &["html", "htm", "xhtml", "svg"],
499            FileType::Exe => &["exe", "dll", "elf", "so", "dylib", "a", "macho", "o"],
500            FileType::Font => &["ttf", "otf", "woff", "woff2", "ttc", "dfont", "afm", "pfa", "pfb", "pfm"],
501            FileType::Swf => &["swf"],
502            FileType::Dicom => &["dcm"],
503            FileType::Fits => &["fits", "fit", "fts"],
504        }
505    }
506
507    /// Read/Write/Create support level.
508    pub fn support(self) -> Support {
509        match self {
510            // R/W/C
511            FileType::Xmp | FileType::Mie | FileType::Exv => Support::ReadWriteCreate,
512            // R/W
513            FileType::Jpeg
514            | FileType::Tiff
515            | FileType::Png
516            | FileType::Gif
517            | FileType::WebP
518            | FileType::Heif
519            | FileType::Avif
520            | FileType::Psd
521            | FileType::Jp2
522            | FileType::Jxl
523            | FileType::Jxr
524            | FileType::Flif
525            | FileType::Cr2
526            | FileType::Cr3
527            | FileType::Crw
528            | FileType::Nef
529            | FileType::Arw
530            | FileType::Arq
531            | FileType::Sr2
532            | FileType::Orf
533            | FileType::Rw2
534            | FileType::Dng
535            | FileType::Raf
536            | FileType::Pef
537            | FileType::Erf
538            | FileType::Fff
539            | FileType::Iiq
540            | FileType::Rwl
541            | FileType::Mef
542            | FileType::Srw
543            | FileType::X3f
544            | FileType::Gpr
545            | FileType::Crm
546            | FileType::Mp4
547            | FileType::QuickTime
548            | FileType::ThreeGP
549            | FileType::Dvb
550            | FileType::Lrv
551            | FileType::Mqv
552            | FileType::F4v
553            | FileType::Pdf
554            | FileType::PostScript
555            | FileType::InDesign
556            | FileType::Vrd
557            | FileType::Audible => Support::ReadWrite,
558            // R only
559            _ => Support::Read,
560        }
561    }
562
563    /// Returns an iterator over all file types.
564    pub fn all() -> &'static [FileType] {
565        ALL_FILE_TYPES
566    }
567}
568
569static ALL_FILE_TYPES: &[FileType] = &[
570    // Images - Standard
571    FileType::Jpeg, FileType::Tiff, FileType::Png, FileType::Gif, FileType::Bmp,
572    FileType::WebP, FileType::Heif, FileType::Avif, FileType::Psd, FileType::Jp2,
573    FileType::J2c, FileType::Jxl, FileType::Jxr, FileType::Flif, FileType::Bpg,
574    FileType::Exr, FileType::Ico,
575    // Images - Specialized
576    FileType::DjVu, FileType::Xcf, FileType::Pcx, FileType::Pict, FileType::Psp,
577    FileType::Hdr, FileType::Rwz, FileType::Btf, FileType::Mng,
578    // RAW
579    FileType::Cr2, FileType::Cr3, FileType::Crw, FileType::Nef, FileType::Arw,
580    FileType::Sr2, FileType::Srf, FileType::Orf, FileType::Rw2, FileType::Dng,
581    FileType::Raf, FileType::Pef, FileType::Dcr, FileType::Mrw, FileType::Erf,
582    FileType::Fff, FileType::Iiq, FileType::Rwl, FileType::Mef, FileType::Srw,
583    FileType::X3f, FileType::Gpr, FileType::Arq, FileType::ThreeFR, FileType::Crm,
584    // Video
585    FileType::Mp4, FileType::QuickTime, FileType::Avi, FileType::Mkv, FileType::WebM,
586    FileType::Wmv, FileType::Asf, FileType::Flv, FileType::Mxf, FileType::M2ts,
587    FileType::Mpeg, FileType::ThreeGP, FileType::RealMedia, FileType::R3d,
588    FileType::Dvb, FileType::Lrv, FileType::Mqv, FileType::F4v, FileType::Wtv,
589    FileType::DvrMs,
590    // Audio
591    FileType::Mp3, FileType::Flac, FileType::Ogg, FileType::Wav, FileType::Aiff,
592    FileType::Aac, FileType::Opus, FileType::Mpc, FileType::Ape, FileType::WavPack,
593    FileType::Ofr, FileType::Dsf, FileType::Audible, FileType::RealAudio,
594    FileType::Wma, FileType::M4a,
595    // Documents
596    FileType::Pdf, FileType::PostScript, FileType::Doc, FileType::Docx,
597    FileType::Xls, FileType::Xlsx, FileType::Ppt, FileType::Pptx,
598    FileType::InDesign, FileType::Rtf,
599    // Archives
600    FileType::Zip, FileType::Rar, FileType::SevenZ, FileType::Gzip,
601    // Metadata / Other
602    FileType::Xmp, FileType::Mie, FileType::Exv, FileType::Vrd, FileType::Icc,
603    FileType::Html, FileType::Exe, FileType::Font, FileType::Swf,
604    FileType::Dicom, FileType::Fits,
605];
606
607/// Detect file type from magic bytes (first 64+ bytes of a file).
608pub fn detect_from_magic(header: &[u8]) -> Option<FileType> {
609    if header.len() < 4 {
610        return None;
611    }
612
613    // ===== Images =====
614
615    // JPEG: FF D8 FF
616    if header.starts_with(&[0xFF, 0xD8, 0xFF]) {
617        return Some(FileType::Jpeg);
618    }
619
620    // PNG: 89 50 4E 47 0D 0A 1A 0A
621    if header.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
622        return Some(FileType::Png);
623    }
624
625    // GIF: "GIF87a" or "GIF89a"
626    if header.starts_with(b"GIF8") && header.len() >= 6 && (header[4] == b'7' || header[4] == b'9') {
627        return Some(FileType::Gif);
628    }
629
630    // TIFF / TIFF-based RAW: "II" or "MM" + magic 42
631    if header.len() >= 4 {
632        let is_le = header[0] == b'I' && header[1] == b'I' && header[2] == 0x2A && header[3] == 0x00;
633        let is_be = header[0] == b'M' && header[1] == b'M' && header[2] == 0x00 && header[3] == 0x2A;
634        if is_le || is_be {
635            // CR2: "II" + "CR" at offset 8
636            if header.len() >= 10 && is_le && header[8] == b'C' && header[9] == b'R' {
637                return Some(FileType::Cr2);
638            }
639            // ORF: "IIRO" or "IIRS" (Olympus)
640            if header.len() >= 4 && is_le && header[0] == b'I' && header[1] == b'I' {
641                if header.len() >= 8 {
642                    // Check for ORF signature at specific offsets (Olympus uses standard TIFF with specific patterns)
643                    // For now, fall through to generic TIFF and detect by extension
644                }
645            }
646            // BigTIFF: magic 43 instead of 42
647            // (handled below)
648            return Some(FileType::Tiff);
649        }
650        // BigTIFF: "II" + 0x2B or "MM" + 0x002B
651        let is_btf_le = header[0] == b'I' && header[1] == b'I' && header[2] == 0x2B && header[3] == 0x00;
652        let is_btf_be = header[0] == b'M' && header[1] == b'M' && header[2] == 0x00 && header[3] == 0x2B;
653        if is_btf_le || is_btf_be {
654            return Some(FileType::Btf);
655        }
656    }
657
658    // BMP: "BM"
659    if header.starts_with(b"BM") && header.len() >= 6 {
660        return Some(FileType::Bmp);
661    }
662
663    // RIFF container: WebP, AVI, WAV
664    if header.len() >= 12 && header.starts_with(b"RIFF") {
665        match &header[8..12] {
666            b"WEBP" => return Some(FileType::WebP),
667            b"AVI " => return Some(FileType::Avi),
668            b"WAVE" => return Some(FileType::Wav),
669            _ => {}
670        }
671    }
672
673    // PSD: "8BPS"
674    if header.starts_with(b"8BPS") {
675        return Some(FileType::Psd);
676    }
677
678    // JPEG 2000: 00 00 00 0C 6A 50 20 20 (jp2 signature box)
679    if header.len() >= 12 && header.starts_with(&[0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20]) {
680        return Some(FileType::Jp2);
681    }
682
683    // JPEG 2000 codestream: FF 4F FF 51
684    if header.starts_with(&[0xFF, 0x4F, 0xFF, 0x51]) {
685        return Some(FileType::J2c);
686    }
687
688    // JPEG XL: FF 0A (bare codestream) or 00 00 00 0C 4A 58 4C 20 (container)
689    if header.len() >= 2 && header[0] == 0xFF && header[1] == 0x0A {
690        return Some(FileType::Jxl);
691    }
692    if header.len() >= 12 && header.starts_with(&[0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20]) {
693        return Some(FileType::Jxl);
694    }
695
696    // FLIF: "FLIF"
697    if header.starts_with(b"FLIF") {
698        return Some(FileType::Flif);
699    }
700
701    // BPG: 0x425047FB
702    if header.starts_with(&[0x42, 0x50, 0x47, 0xFB]) {
703        return Some(FileType::Bpg);
704    }
705
706    // OpenEXR: 76 2F 31 01
707    if header.starts_with(&[0x76, 0x2F, 0x31, 0x01]) {
708        return Some(FileType::Exr);
709    }
710
711    // ICO: 00 00 01 00 (icon) or 00 00 02 00 (cursor)
712    if header.len() >= 4 && header[0] == 0 && header[1] == 0 && (header[2] == 1 || header[2] == 2) && header[3] == 0 {
713        return Some(FileType::Ico);
714    }
715
716    // DjVu: "AT&TFORM"
717    if header.len() >= 8 && header.starts_with(b"AT&TFORM") {
718        return Some(FileType::DjVu);
719    }
720
721    // GIMP XCF: "gimp xcf"
722    if header.starts_with(b"gimp xcf") {
723        return Some(FileType::Xcf);
724    }
725
726    // MNG: 8A 4D 4E 47 0D 0A 1A 0A
727    if header.starts_with(&[0x8A, 0x4D, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
728        return Some(FileType::Mng);
729    }
730
731    // JNG: 8B 4A 4E 47 0D 0A 1A 0A
732    if header.starts_with(&[0x8B, 0x4A, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
733        return Some(FileType::Mng);
734    }
735
736    // Radiance HDR: "#?RADIANCE"
737    if header.len() >= 10 && header.starts_with(b"#?RADIANCE") {
738        return Some(FileType::Hdr);
739    }
740
741    // ===== RAW formats with unique magic =====
742
743    // Fujifilm RAF: "FUJIFILMCCD-RAW"
744    if header.len() >= 15 && header.starts_with(b"FUJIFILMCCD-RAW") {
745        return Some(FileType::Raf);
746    }
747
748    // Canon CRW: "II" + 0x1A00 + "HEAPCCDR"
749    if header.len() >= 14 && header[0] == b'I' && header[1] == b'I'
750        && header[2] == 0x1A && header[3] == 0x00
751        && &header[6..14] == b"HEAPCCDR"
752    {
753        return Some(FileType::Crw);
754    }
755
756    // Minolta MRW: 00 4D 52 4D
757    if header.starts_with(&[0x00, 0x4D, 0x52, 0x4D]) {
758        return Some(FileType::Mrw);
759    }
760
761    // Sigma X3F: "FOVb"
762    if header.starts_with(b"FOVb") {
763        return Some(FileType::X3f);
764    }
765
766    // Panasonic RW2: "IIU" (special TIFF variant)
767    if header.len() >= 4 && header[0] == b'I' && header[1] == b'I' && header[2] == 0x55 && header[3] == 0x00 {
768        return Some(FileType::Rw2);
769    }
770
771    // ===== Video / QuickTime container =====
772
773    // QuickTime / MP4 / HEIF / AVIF / CR3: check for ftyp box
774    if header.len() >= 12 && &header[4..8] == b"ftyp" {
775        let brand = &header[8..12];
776        // HEIF/HEIC
777        if brand == b"heic" || brand == b"mif1" || brand == b"heim" || brand == b"heis"
778            || brand == b"msf1"
779        {
780            return Some(FileType::Heif);
781        }
782        // AVIF
783        if brand == b"avif" || brand == b"avis" {
784            return Some(FileType::Avif);
785        }
786        // Canon CR3
787        if brand == b"crx " {
788            return Some(FileType::Cr3);
789        }
790        // QuickTime
791        if brand == b"qt  " {
792            return Some(FileType::QuickTime);
793        }
794        // 3GP
795        if brand == b"3gp4" || brand == b"3gp5" || brand == b"3gp6" || brand == b"3g2a" {
796            return Some(FileType::ThreeGP);
797        }
798        // M4A/M4V
799        if brand == b"M4A " || brand == b"M4B " || brand == b"M4P " {
800            return Some(FileType::M4a);
801        }
802        if brand == b"M4V " || brand == b"M4VH" || brand == b"M4VP" {
803            return Some(FileType::Mp4);
804        }
805        // F4V
806        if brand == b"F4V " || brand == b"F4P " {
807            return Some(FileType::F4v);
808        }
809        // Default ftyp → MP4
810        return Some(FileType::Mp4);
811    }
812
813    // QuickTime without ftyp: check for common atom types at offset 4
814    if header.len() >= 8 {
815        let atom_type = &header[4..8];
816        if atom_type == b"moov" || atom_type == b"mdat" || atom_type == b"wide"
817            || atom_type == b"free" || atom_type == b"pnot" || atom_type == b"skip"
818        {
819            return Some(FileType::QuickTime);
820        }
821    }
822
823    // Matroska/WebM: EBML header 0x1A45DFA3
824    if header.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) {
825        return Some(FileType::Mkv);
826        // Note: WebM vs MKV distinction requires reading the DocType element inside EBML
827    }
828
829    // FLV: "FLV\x01"
830    if header.starts_with(b"FLV\x01") {
831        return Some(FileType::Flv);
832    }
833
834    // ASF/WMV/WMA: 30 26 B2 75 8E 66 CF 11
835    if header.len() >= 16 && header.starts_with(&[0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11]) {
836        return Some(FileType::Asf);
837        // WMV/WMA distinction is done by content/extension
838    }
839
840    // MXF: 06 0E 2B 34 02 05 01 01
841    if header.len() >= 8 && header.starts_with(&[0x06, 0x0E, 0x2B, 0x34]) {
842        return Some(FileType::Mxf);
843    }
844
845    // ICC Profile: "acsp" at offset 36 (must be before MPEG check which has loose matching)
846    if header.len() >= 40 && &header[36..40] == b"acsp" {
847        return Some(FileType::Icc);
848    }
849
850    // MPEG: 00 00 01 Bx (system header) or 00 00 01 BA (pack start)
851    if header.len() >= 4 && header[0] == 0 && header[1] == 0 && header[2] == 1
852        && (header[3] == 0xBA || header[3] == 0xBB || (header[3] & 0xF0) == 0xE0)
853    {
854        return Some(FileType::Mpeg);
855    }
856
857    // MPEG-2 TS: 0x47 sync byte every 188 or 192 bytes
858    if header.len() >= 1 && header[0] == 0x47 {
859        if header.len() >= 376 && header[188] == 0x47 {
860            return Some(FileType::M2ts);
861        }
862        if header.len() >= 384 && header[192] == 0x47 {
863            return Some(FileType::M2ts);
864        }
865    }
866
867    // RealMedia: ".RMF"
868    if header.starts_with(b".RMF") {
869        return Some(FileType::RealMedia);
870    }
871
872    // RED R3D: "RED1" or "RED2"
873    if header.starts_with(b"RED1") || header.starts_with(b"RED2") {
874        return Some(FileType::R3d);
875    }
876
877    // ===== Audio =====
878
879    // MP3: ID3 tag or MPEG sync word
880    if header.starts_with(b"ID3") {
881        return Some(FileType::Mp3);
882    }
883    // MPEG audio sync: 0xFF + 0xE0 mask (after other FF-starting formats)
884    if header.len() >= 2 && header[0] == 0xFF && (header[1] & 0xE0) == 0xE0 {
885        return Some(FileType::Mp3);
886    }
887
888    // FLAC: "fLaC"
889    if header.starts_with(b"fLaC") {
890        return Some(FileType::Flac);
891    }
892
893    // OGG: "OggS"
894    if header.starts_with(b"OggS") {
895        return Some(FileType::Ogg);
896    }
897
898    // AIFF: "FORM" + "AIFF" or "AIFC"
899    if header.len() >= 12 && header.starts_with(b"FORM") {
900        if &header[8..12] == b"AIFF" || &header[8..12] == b"AIFC" {
901            return Some(FileType::Aiff);
902        }
903    }
904
905    // APE: "MAC "
906    if header.starts_with(b"MAC ") {
907        return Some(FileType::Ape);
908    }
909
910    // Musepack: "MP+" or "MPCK"
911    if header.starts_with(b"MP+") || header.starts_with(b"MPCK") {
912        return Some(FileType::Mpc);
913    }
914
915    // WavPack: "wvpk"
916    if header.starts_with(b"wvpk") {
917        return Some(FileType::WavPack);
918    }
919
920    // DSD/DSF: "DSD "
921    if header.starts_with(b"DSD ") {
922        return Some(FileType::Dsf);
923    }
924
925    // OptimFROG: "OFR "
926    if header.starts_with(b"OFR ") {
927        return Some(FileType::Ofr);
928    }
929
930    // RealAudio: ".ra\xFD"
931    if header.len() >= 4 && header[0] == b'.' && header[1] == b'r' && header[2] == b'a' && header[3] == 0xFD {
932        return Some(FileType::RealAudio);
933    }
934
935    // ===== Documents =====
936
937    // PDF: "%PDF-"
938    if header.starts_with(b"%PDF-") {
939        return Some(FileType::Pdf);
940    }
941
942    // PostScript: "%!PS" or "%!Adobe"
943    if header.starts_with(b"%!PS") || header.starts_with(b"%!Adobe") {
944        return Some(FileType::PostScript);
945    }
946
947    // MS Office legacy (DOC/XLS/PPT): OLE2 compound binary D0 CF 11 E0
948    if header.starts_with(&[0xD0, 0xCF, 0x11, 0xE0]) {
949        return Some(FileType::Doc); // Distinguishing DOC/XLS/PPT requires deeper parsing
950    }
951
952    // RTF: "{\rtf"
953    if header.starts_with(b"{\\rtf") {
954        return Some(FileType::Rtf);
955    }
956
957    // InDesign: 06 06 ED F5 D8 1D 46 E5
958    if header.len() >= 8 && header.starts_with(&[0x06, 0x06, 0xED, 0xF5, 0xD8, 0x1D, 0x46, 0xE5]) {
959        return Some(FileType::InDesign);
960    }
961
962    // ===== Archives =====
963
964    // ZIP (and DOCX/XLSX/PPTX/EPUB/etc.): "PK\x03\x04"
965    if header.starts_with(&[0x50, 0x4B, 0x03, 0x04]) {
966        return Some(FileType::Zip);
967        // DOCX/XLSX/PPTX are ZIP files; distinguishing requires checking [Content_Types].xml
968    }
969
970    // RAR: "Rar!\x1A\x07"
971    if header.len() >= 6 && header.starts_with(b"Rar!\x1A\x07") {
972        return Some(FileType::Rar);
973    }
974
975    // 7-Zip: "7z\xBC\xAF\x27\x1C"
976    if header.len() >= 6 && header.starts_with(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]) {
977        return Some(FileType::SevenZ);
978    }
979
980    // GZIP: 1F 8B
981    if header.len() >= 2 && header[0] == 0x1F && header[1] == 0x8B {
982        return Some(FileType::Gzip);
983    }
984
985    // ===== Other =====
986
987    // SWF: "FWS" (uncompressed) or "CWS" (compressed) or "ZWS"
988    if header.len() >= 3
989        && ((header[0] == b'F' || header[0] == b'C' || header[0] == b'Z')
990            && header[1] == b'W'
991            && header[2] == b'S')
992    {
993        return Some(FileType::Swf);
994    }
995
996    // DICOM: "DICM" at offset 128
997    if header.len() >= 132 && &header[128..132] == b"DICM" {
998        return Some(FileType::Dicom);
999    }
1000
1001    // FITS: "SIMPLE  ="
1002    if header.len() >= 9 && header.starts_with(b"SIMPLE  =") {
1003        return Some(FileType::Fits);
1004    }
1005
1006    // MIE: "~\x10\x04" + version
1007    if header.len() >= 4 && header[0] == 0x7E && header[1] == 0x10 && header[2] == 0x04 {
1008        return Some(FileType::Mie);
1009    }
1010
1011    // XMP sidecar (starts with XML PI or xpacket)
1012    if header.starts_with(b"<?xpacket") || header.starts_with(b"<x:xmpmeta") {
1013        return Some(FileType::Xmp);
1014    }
1015
1016    // XML-based formats: look deeper to classify
1017    if header.starts_with(b"<?xml") || header.starts_with(b"<svg") {
1018        let preview = &header[..header.len().min(512)];
1019        if preview.windows(4).any(|w| w == b"<svg") {
1020            return Some(FileType::Html); // SVG handled via HTML path
1021        }
1022        if preview.windows(5).any(|w| w == b"<html" || w == b"<HTML") {
1023            return Some(FileType::Html); // XHTML
1024        }
1025        if preview.windows(10).any(|w| w == b"<x:xmpmeta") || preview.windows(9).any(|w| w == b"<?xpacket") {
1026            return Some(FileType::Xmp);
1027        }
1028        if preview.windows(4).any(|w| w == b"<rdf" || w == b"<RDF") {
1029            return Some(FileType::Xmp);
1030        }
1031        // Default XML → XMP (most XML files ExifTool handles contain XMP)
1032        return Some(FileType::Xmp);
1033    }
1034
1035    // HTML
1036    if header.starts_with(b"<!DOCTYPE html") || header.starts_with(b"<!doctype html")
1037        || header.starts_with(b"<!DOCTYPE HTML") || header.starts_with(b"<html") || header.starts_with(b"<HTML")
1038    {
1039        return Some(FileType::Html);
1040    }
1041
1042    // PE executable: "MZ"
1043    if header.starts_with(b"MZ") {
1044        return Some(FileType::Exe);
1045    }
1046
1047    // TrueType font: 00 01 00 00 or "true" or "typ1"
1048    if (header.starts_with(&[0x00, 0x01, 0x00, 0x00]) || header.starts_with(b"true") || header.starts_with(b"typ1"))
1049        && header.len() >= 12
1050    {
1051        return Some(FileType::Font);
1052    }
1053
1054    // TrueType Collection: "ttcf"
1055    if header.starts_with(b"ttcf") {
1056        return Some(FileType::Font);
1057    }
1058
1059    // OpenType font: "OTTO"
1060    if header.starts_with(b"OTTO") {
1061        return Some(FileType::Font);
1062    }
1063
1064    // WOFF: "wOFF"
1065    if header.starts_with(b"wOFF") {
1066        return Some(FileType::Font);
1067    }
1068
1069    // WOFF2: "wOF2"
1070    if header.starts_with(b"wOF2") {
1071        return Some(FileType::Font);
1072    }
1073
1074    None
1075}
1076
1077/// Detect file type from file extension.
1078pub fn detect_from_extension(ext: &str) -> Option<FileType> {
1079    let ext_lower = ext.to_ascii_lowercase();
1080    let ext_lower = ext_lower.trim_start_matches('.');
1081
1082    for &ft in ALL_FILE_TYPES {
1083        for known_ext in ft.extensions() {
1084            if ext_lower == *known_ext {
1085                return Some(ft);
1086            }
1087        }
1088    }
1089
1090    None
1091}
1092
1093impl std::fmt::Display for FileType {
1094    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1095        write!(f, "{}", self.description())
1096    }
1097}
1098
1099impl std::fmt::Display for Support {
1100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1101        match self {
1102            Support::Read => write!(f, "R"),
1103            Support::ReadWrite => write!(f, "R/W"),
1104            Support::ReadWriteCreate => write!(f, "R/W/C"),
1105        }
1106    }
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111    use super::*;
1112
1113    #[test]
1114    fn test_detect_jpeg() {
1115        let header = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, b'J', b'F', b'I', b'F'];
1116        assert_eq!(detect_from_magic(&header), Some(FileType::Jpeg));
1117    }
1118
1119    #[test]
1120    fn test_detect_png() {
1121        let header = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
1122        assert_eq!(detect_from_magic(&header), Some(FileType::Png));
1123    }
1124
1125    #[test]
1126    fn test_detect_tiff_le() {
1127        let header = [b'I', b'I', 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00];
1128        assert_eq!(detect_from_magic(&header), Some(FileType::Tiff));
1129    }
1130
1131    #[test]
1132    fn test_detect_tiff_be() {
1133        let header = [b'M', b'M', 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08];
1134        assert_eq!(detect_from_magic(&header), Some(FileType::Tiff));
1135    }
1136
1137    #[test]
1138    fn test_detect_cr2() {
1139        let header = [b'I', b'I', 0x2A, 0x00, 0x10, 0x00, 0x00, 0x00, b'C', b'R'];
1140        assert_eq!(detect_from_magic(&header), Some(FileType::Cr2));
1141    }
1142
1143    #[test]
1144    fn test_detect_pdf() {
1145        let header = b"%PDF-1.7 some more data here";
1146        assert_eq!(detect_from_magic(header), Some(FileType::Pdf));
1147    }
1148
1149    #[test]
1150    fn test_detect_webp() {
1151        let header = b"RIFF\x00\x00\x00\x00WEBP";
1152        assert_eq!(detect_from_magic(header), Some(FileType::WebP));
1153    }
1154
1155    #[test]
1156    fn test_detect_heif() {
1157        let mut header = [0u8; 16];
1158        header[4..8].copy_from_slice(b"ftyp");
1159        header[8..12].copy_from_slice(b"heic");
1160        assert_eq!(detect_from_magic(&header), Some(FileType::Heif));
1161    }
1162
1163    #[test]
1164    fn test_detect_avif() {
1165        let mut header = [0u8; 16];
1166        header[4..8].copy_from_slice(b"ftyp");
1167        header[8..12].copy_from_slice(b"avif");
1168        assert_eq!(detect_from_magic(&header), Some(FileType::Avif));
1169    }
1170
1171    #[test]
1172    fn test_detect_cr3() {
1173        let mut header = [0u8; 16];
1174        header[4..8].copy_from_slice(b"ftyp");
1175        header[8..12].copy_from_slice(b"crx ");
1176        assert_eq!(detect_from_magic(&header), Some(FileType::Cr3));
1177    }
1178
1179    #[test]
1180    fn test_detect_flac() {
1181        assert_eq!(detect_from_magic(b"fLaC\x00\x00"), Some(FileType::Flac));
1182    }
1183
1184    #[test]
1185    fn test_detect_ogg() {
1186        assert_eq!(detect_from_magic(b"OggS\x00\x02"), Some(FileType::Ogg));
1187    }
1188
1189    #[test]
1190    fn test_detect_mp3_id3() {
1191        assert_eq!(detect_from_magic(b"ID3\x04\x00"), Some(FileType::Mp3));
1192    }
1193
1194    #[test]
1195    fn test_detect_rar() {
1196        assert_eq!(detect_from_magic(b"Rar!\x1A\x07\x01\x00"), Some(FileType::Rar));
1197    }
1198
1199    #[test]
1200    fn test_detect_7z() {
1201        assert_eq!(detect_from_magic(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]), Some(FileType::SevenZ));
1202    }
1203
1204    #[test]
1205    fn test_detect_gzip() {
1206        assert_eq!(detect_from_magic(&[0x1F, 0x8B, 0x08, 0x00]), Some(FileType::Gzip));
1207    }
1208
1209    #[test]
1210    fn test_detect_raf() {
1211        assert_eq!(detect_from_magic(b"FUJIFILMCCD-RAW 0201"), Some(FileType::Raf));
1212    }
1213
1214    #[test]
1215    fn test_detect_psd() {
1216        assert_eq!(detect_from_magic(b"8BPS\x00\x01"), Some(FileType::Psd));
1217    }
1218
1219    #[test]
1220    fn test_detect_from_extension() {
1221        assert_eq!(detect_from_extension("jpg"), Some(FileType::Jpeg));
1222        assert_eq!(detect_from_extension(".JPEG"), Some(FileType::Jpeg));
1223        assert_eq!(detect_from_extension("cr2"), Some(FileType::Cr2));
1224        assert_eq!(detect_from_extension("cr3"), Some(FileType::Cr3));
1225        assert_eq!(detect_from_extension("nef"), Some(FileType::Nef));
1226        assert_eq!(detect_from_extension("arw"), Some(FileType::Arw));
1227        assert_eq!(detect_from_extension("dng"), Some(FileType::Dng));
1228        assert_eq!(detect_from_extension("raf"), Some(FileType::Raf));
1229        assert_eq!(detect_from_extension("mp4"), Some(FileType::Mp4));
1230        assert_eq!(detect_from_extension("mov"), Some(FileType::QuickTime));
1231        assert_eq!(detect_from_extension("flac"), Some(FileType::Flac));
1232        assert_eq!(detect_from_extension("docx"), Some(FileType::Docx));
1233        assert_eq!(detect_from_extension("xlsx"), Some(FileType::Xlsx));
1234        assert_eq!(detect_from_extension("3fr"), Some(FileType::ThreeFR));
1235        assert_eq!(detect_from_extension("xyz"), None);
1236    }
1237
1238    #[test]
1239    fn test_all_types_have_extensions() {
1240        for &ft in FileType::all() {
1241            assert!(!ft.extensions().is_empty(), "{:?} has no extensions", ft);
1242        }
1243    }
1244
1245    #[test]
1246    fn test_all_types_have_mime() {
1247        for &ft in FileType::all() {
1248            assert!(!ft.mime_type().is_empty(), "{:?} has no MIME type", ft);
1249        }
1250    }
1251
1252    #[test]
1253    fn test_total_format_count() {
1254        assert!(FileType::all().len() >= 100, "Expected 100+ formats, got {}", FileType::all().len());
1255    }
1256}