Skip to main content

tpt_mime_pure/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3#![cfg_attr(not(feature = "std"), no_std)]
4
5//! Pure Rust MIME type detection via magic bytes and file extension fallback.
6//! See [`detect`], [`detect_by_extension`], and [`MimeType`].
7
8/// A detected MIME type.
9///
10/// This enum is `#[non_exhaustive]` — new variants may be added in future releases
11/// without a breaking change.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[non_exhaustive]
14pub enum MimeType {
15    // Images
16    /// `image/jpeg`
17    Jpeg,
18    /// `image/png`
19    Png,
20    /// `image/gif`
21    Gif,
22    /// `image/webp`
23    WebP,
24    /// `image/bmp`
25    Bmp,
26    /// `image/x-icon`
27    Ico,
28    /// `image/tiff`
29    Tiff,
30    // Video
31    /// `video/mp4`
32    Mp4,
33    /// `video/quicktime` (MOV)
34    QuickTime,
35    /// `video/3gpp`
36    ThreeGp,
37    /// `video/x-matroska`
38    Mkv,
39    /// `video/webm`
40    WebM,
41    /// `video/x-msvideo`
42    Avi,
43    // Images (container formats sharing the ISO-BMFF `ftyp` box)
44    /// `image/heic`
45    Heic,
46    /// `image/heif`
47    Heif,
48    /// `image/avif`
49    Avif,
50    // Audio
51    /// `audio/mpeg`
52    Mp3,
53    /// `audio/wav`
54    Wav,
55    /// `audio/flac`
56    Flac,
57    /// `audio/ogg`
58    Ogg,
59    // Documents & archives
60    /// `application/pdf`
61    Pdf,
62    /// `application/zip`
63    Zip,
64    /// `application/gzip`
65    Gzip,
66    /// `application/x-tar`
67    Tar,
68    /// `application/x-sqlite3`
69    Sqlite,
70    // Binary / code
71    /// `application/wasm`
72    Wasm,
73    /// `application/x-elf`
74    Elf,
75    /// `application/x-msdownload`
76    PeExe,
77}
78
79impl MimeType {
80    /// The MIME type string, e.g. `"image/jpeg"`.
81    pub const fn as_str(&self) -> &'static str {
82        match self {
83            Self::Jpeg => "image/jpeg",
84            Self::Png => "image/png",
85            Self::Gif => "image/gif",
86            Self::WebP => "image/webp",
87            Self::Bmp => "image/bmp",
88            Self::Ico => "image/x-icon",
89            Self::Tiff => "image/tiff",
90            Self::Mp4 => "video/mp4",
91            Self::QuickTime => "video/quicktime",
92            Self::ThreeGp => "video/3gpp",
93            Self::Mkv => "video/x-matroska",
94            Self::WebM => "video/webm",
95            Self::Avi => "video/x-msvideo",
96            Self::Heic => "image/heic",
97            Self::Heif => "image/heif",
98            Self::Avif => "image/avif",
99            Self::Mp3 => "audio/mpeg",
100            Self::Wav => "audio/wav",
101            Self::Flac => "audio/flac",
102            Self::Ogg => "audio/ogg",
103            Self::Pdf => "application/pdf",
104            Self::Zip => "application/zip",
105            Self::Gzip => "application/gzip",
106            Self::Tar => "application/x-tar",
107            Self::Sqlite => "application/x-sqlite3",
108            Self::Wasm => "application/wasm",
109            Self::Elf => "application/x-elf",
110            Self::PeExe => "application/x-msdownload",
111        }
112    }
113
114    /// The canonical file extension (without leading dot), e.g. `"jpg"`.
115    pub const fn extension(&self) -> &'static str {
116        match self {
117            Self::Jpeg => "jpg",
118            Self::Png => "png",
119            Self::Gif => "gif",
120            Self::WebP => "webp",
121            Self::Bmp => "bmp",
122            Self::Ico => "ico",
123            Self::Tiff => "tiff",
124            Self::Mp4 => "mp4",
125            Self::QuickTime => "mov",
126            Self::ThreeGp => "3gp",
127            Self::Mkv => "mkv",
128            Self::WebM => "webm",
129            Self::Avi => "avi",
130            Self::Heic => "heic",
131            Self::Heif => "heif",
132            Self::Avif => "avif",
133            Self::Mp3 => "mp3",
134            Self::Wav => "wav",
135            Self::Flac => "flac",
136            Self::Ogg => "ogg",
137            Self::Pdf => "pdf",
138            Self::Zip => "zip",
139            Self::Gzip => "gz",
140            Self::Tar => "tar",
141            Self::Sqlite => "db",
142            Self::Wasm => "wasm",
143            Self::Elf => "elf",
144            Self::PeExe => "exe",
145        }
146    }
147}
148
149/// Detect MIME type from the leading bytes of a file.
150///
151/// Checks up to the first 512 bytes against known magic byte signatures.
152/// Returns `None` if no signature matches.
153///
154/// # Example
155///
156/// ```
157/// use tpt_mime_pure::{detect, MimeType};
158///
159/// let jpeg_header = &[0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10];
160/// assert_eq!(detect(jpeg_header), Some(MimeType::Jpeg));
161/// ```
162pub fn detect(bytes: &[u8]) -> Option<MimeType> {
163    let b = bytes;
164    let len = b.len();
165
166    macro_rules! starts_with {
167        ($sig:expr) => {
168            len >= $sig.len() && b[..$sig.len()] == $sig[..]
169        };
170    }
171
172    macro_rules! at_offset {
173        ($offset:expr, $sig:expr) => {
174            len >= $offset + $sig.len() && b[$offset..$offset + $sig.len()] == $sig[..]
175        };
176    }
177
178    // JPEG: FF D8 FF
179    if starts_with!([0xFF, 0xD8, 0xFF]) {
180        return Some(MimeType::Jpeg);
181    }
182    // PNG: 89 50 4E 47 0D 0A 1A 0A
183    if starts_with!([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
184        return Some(MimeType::Png);
185    }
186    // GIF: 47 49 46 38
187    if starts_with!([0x47, 0x49, 0x46, 0x38]) {
188        return Some(MimeType::Gif);
189    }
190    // WebP: "RIFF" at 0, "WEBP" at offset 8
191    if starts_with!([0x52, 0x49, 0x46, 0x46]) && at_offset!(8, [0x57, 0x45, 0x42, 0x50]) {
192        return Some(MimeType::WebP);
193    }
194    // PDF: %PDF
195    if starts_with!([0x25, 0x50, 0x44, 0x46]) {
196        return Some(MimeType::Pdf);
197    }
198    // WASM: \0asm
199    if starts_with!([0x00, 0x61, 0x73, 0x6D]) {
200        return Some(MimeType::Wasm);
201    }
202    // ELF: \x7FELF
203    if starts_with!([0x7F, 0x45, 0x4C, 0x46]) {
204        return Some(MimeType::Elf);
205    }
206    // PE/EXE: MZ
207    if starts_with!([0x4D, 0x5A]) {
208        return Some(MimeType::PeExe);
209    }
210    // GZIP: 1F 8B
211    if starts_with!([0x1F, 0x8B]) {
212        return Some(MimeType::Gzip);
213    }
214    // ZIP (also DOCX/XLSX): PK\x03\x04
215    if starts_with!([0x50, 0x4B, 0x03, 0x04]) {
216        return Some(MimeType::Zip);
217    }
218    // SQLite: "SQLite format 3\0"
219    if starts_with!([
220        0x53, 0x51, 0x4C, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x20, 0x33,
221        0x00
222    ]) {
223        return Some(MimeType::Sqlite);
224    }
225    // FLAC: fLaC
226    if starts_with!([0x66, 0x4C, 0x61, 0x43]) {
227        return Some(MimeType::Flac);
228    }
229    // OGG: OggS
230    if starts_with!([0x4F, 0x67, 0x67, 0x53]) {
231        return Some(MimeType::Ogg);
232    }
233    // MP3: ID3 tag or sync word FF FB/FA/F3/F2
234    if starts_with!([0x49, 0x44, 0x33]) {
235        return Some(MimeType::Mp3);
236    }
237    if len >= 2 && b[0] == 0xFF && (b[1] == 0xFB || b[1] == 0xFA || b[1] == 0xF3 || b[1] == 0xF2) {
238        return Some(MimeType::Mp3);
239    }
240    // MKV / WebM: EBML magic 1A 45 DF A3. Both share the same EBML header but
241    // declare a different DocType ("webm" vs "matroska"). Scan the leading bytes
242    // for the DocType element (id 0x42 0x82) and inspect its value.
243    if starts_with!([0x1A, 0x45, 0xDF, 0xA3]) {
244        let limit = len.min(512);
245        let mut idx = 0;
246        let mut kind = MimeType::Mkv;
247        while idx + 3 <= limit {
248            if b[idx] == 0x42 && b[idx + 1] == 0x82 {
249                let data_len = (b[idx + 2] & 0x7F) as usize;
250                if idx + 3 + data_len <= limit {
251                    let doc_type = &b[idx + 3..idx + 3 + data_len];
252                    if doc_type == b"webm" {
253                        kind = MimeType::WebM;
254                        break;
255                    } else if doc_type == b"matroska" {
256                        kind = MimeType::Mkv;
257                        break;
258                    }
259                }
260            }
261            idx += 1;
262        }
263        return Some(kind);
264    }
265    // MP4 / MOV / 3GP / HEIC / HEIF / AVIF: ISO-BMFF `ftyp` box at offset 4.
266    // The 4-byte major-brand string at offset 8 distinguishes the variants;
267    // unknown brands fall back to MP4.
268    if at_offset!(4, [0x66, 0x74, 0x79, 0x70]) {
269        if len >= 12 {
270            let brand = &b[8..12];
271            return Some(match brand {
272                b"heic" | b"heix" | b"hevc" | b"hevx" => MimeType::Heic,
273                b"mif1" => MimeType::Heif,
274                b"avif" | b"avis" => MimeType::Avif,
275                b"qt  " => MimeType::QuickTime,
276                b"3gp4" | b"3gp5" | b"3gp6" | b"3gr6" | b"3gs6" | b"3gpp" => MimeType::ThreeGp,
277                _ => MimeType::Mp4,
278            });
279        }
280        return Some(MimeType::Mp4);
281    }
282    // WAV: RIFF at 0, WAVE at offset 8
283    if starts_with!([0x52, 0x49, 0x46, 0x46]) && at_offset!(8, [0x57, 0x41, 0x56, 0x45]) {
284        return Some(MimeType::Wav);
285    }
286    // AVI: RIFF at 0, AVI  at offset 8
287    if starts_with!([0x52, 0x49, 0x46, 0x46]) && at_offset!(8, [0x41, 0x56, 0x49, 0x20]) {
288        return Some(MimeType::Avi);
289    }
290    // TAR: "ustar" at offset 257
291    if at_offset!(257, [0x75, 0x73, 0x74, 0x61, 0x72]) {
292        return Some(MimeType::Tar);
293    }
294    // BMP: BM
295    if starts_with!([0x42, 0x4D]) {
296        return Some(MimeType::Bmp);
297    }
298    // ICO: 00 00 01 00
299    if starts_with!([0x00, 0x00, 0x01, 0x00]) {
300        return Some(MimeType::Ico);
301    }
302    // TIFF: II (little-endian) or MM (big-endian)
303    if starts_with!([0x49, 0x49, 0x2A, 0x00]) || starts_with!([0x4D, 0x4D, 0x00, 0x2A]) {
304        return Some(MimeType::Tiff);
305    }
306
307    None
308}
309
310/// Detect MIME type from a file extension (without leading dot, case-insensitive).
311///
312/// Returns `None` if the extension is not recognised.
313///
314/// # Example
315///
316/// ```
317/// use tpt_mime_pure::{detect_by_extension, MimeType};
318///
319/// assert_eq!(detect_by_extension("jpg"), Some(MimeType::Jpeg));
320/// assert_eq!(detect_by_extension("PDF"), Some(MimeType::Pdf));
321/// assert_eq!(detect_by_extension("xyz"), None);
322/// ```
323pub fn detect_by_extension(ext: &str) -> Option<MimeType> {
324    // Compare ASCII case-insensitively without allocation
325    let eq = |a: &str| {
326        a.len() == ext.len()
327            && a.bytes()
328                .zip(ext.bytes())
329                .all(|(a, b)| a == b.to_ascii_lowercase())
330    };
331
332    if eq("jpg") || eq("jpeg") {
333        return Some(MimeType::Jpeg);
334    }
335    if eq("png") {
336        return Some(MimeType::Png);
337    }
338    if eq("gif") {
339        return Some(MimeType::Gif);
340    }
341    if eq("webp") {
342        return Some(MimeType::WebP);
343    }
344    if eq("bmp") {
345        return Some(MimeType::Bmp);
346    }
347    if eq("ico") {
348        return Some(MimeType::Ico);
349    }
350    if eq("tiff") || eq("tif") {
351        return Some(MimeType::Tiff);
352    }
353    if eq("mp4") || eq("m4v") {
354        return Some(MimeType::Mp4);
355    }
356    if eq("mov") || eq("qt") {
357        return Some(MimeType::QuickTime);
358    }
359    if eq("3gp") || eq("3gpp") {
360        return Some(MimeType::ThreeGp);
361    }
362    if eq("mkv") || eq("mk3d") {
363        return Some(MimeType::Mkv);
364    }
365    if eq("heic") {
366        return Some(MimeType::Heic);
367    }
368    if eq("heif") {
369        return Some(MimeType::Heif);
370    }
371    if eq("avif") {
372        return Some(MimeType::Avif);
373    }
374    if eq("webm") {
375        return Some(MimeType::WebM);
376    }
377    if eq("avi") {
378        return Some(MimeType::Avi);
379    }
380    if eq("mp3") {
381        return Some(MimeType::Mp3);
382    }
383    if eq("wav") {
384        return Some(MimeType::Wav);
385    }
386    if eq("flac") {
387        return Some(MimeType::Flac);
388    }
389    if eq("ogg") || eq("oga") {
390        return Some(MimeType::Ogg);
391    }
392    if eq("pdf") {
393        return Some(MimeType::Pdf);
394    }
395    if eq("zip") || eq("docx") || eq("xlsx") || eq("pptx") || eq("jar") {
396        return Some(MimeType::Zip);
397    }
398    if eq("gz") || eq("gzip") {
399        return Some(MimeType::Gzip);
400    }
401    if eq("tar") {
402        return Some(MimeType::Tar);
403    }
404    if eq("db") || eq("sqlite") || eq("sqlite3") {
405        return Some(MimeType::Sqlite);
406    }
407    if eq("wasm") {
408        return Some(MimeType::Wasm);
409    }
410    if eq("elf") {
411        return Some(MimeType::Elf);
412    }
413    if eq("exe") || eq("dll") {
414        return Some(MimeType::PeExe);
415    }
416
417    None
418}
419
420/// Read up to 512 bytes from `path` and detect the MIME type.
421///
422/// Falls back to `None` if the magic bytes are not recognised.
423/// Requires the `std` feature (enabled by default).
424#[cfg(feature = "std")]
425pub fn detect_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Option<MimeType>> {
426    use std::io::Read;
427    let mut buf = [0u8; 512];
428    let mut f = std::fs::File::open(path)?;
429    let n = f.read(&mut buf)?;
430    Ok(detect(&buf[..n]))
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn jpeg() {
439        assert_eq!(detect(&[0xFF, 0xD8, 0xFF, 0xE0]), Some(MimeType::Jpeg));
440    }
441
442    #[test]
443    fn png() {
444        assert_eq!(
445            detect(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
446            Some(MimeType::Png)
447        );
448    }
449
450    #[test]
451    fn gif() {
452        assert_eq!(
453            detect(&[0x47, 0x49, 0x46, 0x38, 0x39, 0x61]),
454            Some(MimeType::Gif)
455        );
456    }
457
458    #[test]
459    fn pdf() {
460        assert_eq!(detect(b"%PDF-1.4"), Some(MimeType::Pdf));
461    }
462
463    #[test]
464    fn wasm() {
465        assert_eq!(
466            detect(&[0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]),
467            Some(MimeType::Wasm)
468        );
469    }
470
471    #[test]
472    fn elf() {
473        assert_eq!(detect(&[0x7F, 0x45, 0x4C, 0x46, 0x02]), Some(MimeType::Elf));
474    }
475
476    #[test]
477    fn pe_exe() {
478        assert_eq!(detect(&[0x4D, 0x5A, 0x90, 0x00]), Some(MimeType::PeExe));
479    }
480
481    #[test]
482    fn gzip() {
483        assert_eq!(detect(&[0x1F, 0x8B, 0x08, 0x00]), Some(MimeType::Gzip));
484    }
485
486    #[test]
487    fn zip() {
488        assert_eq!(detect(&[0x50, 0x4B, 0x03, 0x04, 0x14]), Some(MimeType::Zip));
489    }
490
491    #[test]
492    fn flac() {
493        assert_eq!(detect(b"fLaC\x00\x00\x00\x22"), Some(MimeType::Flac));
494    }
495
496    #[test]
497    fn ogg() {
498        assert_eq!(detect(b"OggS\x00\x02"), Some(MimeType::Ogg));
499    }
500
501    #[test]
502    fn mp3_id3() {
503        assert_eq!(detect(b"ID3\x04\x00"), Some(MimeType::Mp3));
504    }
505
506    #[test]
507    fn mp3_sync() {
508        assert_eq!(detect(&[0xFF, 0xFB, 0x90, 0x00]), Some(MimeType::Mp3));
509    }
510
511    #[test]
512    fn unknown_returns_none() {
513        assert_eq!(detect(b"hello world"), None);
514    }
515
516    #[test]
517    fn empty_returns_none() {
518        assert_eq!(detect(&[]), None);
519    }
520
521    #[test]
522    fn ext_jpg() {
523        assert_eq!(detect_by_extension("jpg"), Some(MimeType::Jpeg));
524        assert_eq!(detect_by_extension("jpeg"), Some(MimeType::Jpeg));
525        assert_eq!(detect_by_extension("JPG"), Some(MimeType::Jpeg));
526    }
527
528    #[test]
529    fn ext_pdf() {
530        assert_eq!(detect_by_extension("pdf"), Some(MimeType::Pdf));
531        assert_eq!(detect_by_extension("PDF"), Some(MimeType::Pdf));
532    }
533
534    #[test]
535    fn ext_unknown() {
536        assert_eq!(detect_by_extension("xyz"), None);
537        assert_eq!(detect_by_extension(""), None);
538    }
539
540    #[test]
541    fn ext_docx_is_zip() {
542        assert_eq!(detect_by_extension("docx"), Some(MimeType::Zip));
543    }
544
545    #[test]
546    fn mime_str_and_ext() {
547        assert_eq!(MimeType::Jpeg.as_str(), "image/jpeg");
548        assert_eq!(MimeType::Jpeg.extension(), "jpg");
549        assert_eq!(MimeType::Wasm.as_str(), "application/wasm");
550        assert_eq!(MimeType::Wasm.extension(), "wasm");
551    }
552
553    #[test]
554    fn webp() {
555        let mut bytes = [0u8; 12];
556        bytes[0..4].copy_from_slice(&[0x52, 0x49, 0x46, 0x46]);
557        bytes[8..12].copy_from_slice(&[0x57, 0x45, 0x42, 0x50]);
558        assert_eq!(detect(&bytes), Some(MimeType::WebP));
559    }
560
561    #[test]
562    fn wav() {
563        let mut bytes = [0u8; 12];
564        bytes[0..4].copy_from_slice(&[0x52, 0x49, 0x46, 0x46]);
565        bytes[8..12].copy_from_slice(&[0x57, 0x41, 0x56, 0x45]);
566        assert_eq!(detect(&bytes), Some(MimeType::Wav));
567    }
568
569    #[test]
570    fn bmp() {
571        assert_eq!(detect(&[0x42, 0x4D, 0x00, 0x00]), Some(MimeType::Bmp));
572    }
573
574    #[test]
575    fn ico() {
576        assert_eq!(detect(&[0x00, 0x00, 0x01, 0x00, 0x01]), Some(MimeType::Ico));
577    }
578
579    #[test]
580    fn tiff_le() {
581        assert_eq!(detect(&[0x49, 0x49, 0x2A, 0x00]), Some(MimeType::Tiff));
582    }
583
584    #[test]
585    fn tiff_be() {
586        assert_eq!(detect(&[0x4D, 0x4D, 0x00, 0x2A]), Some(MimeType::Tiff));
587    }
588
589    #[test]
590    fn webm_via_ebml_doctype() {
591        let bytes = [
592            0x1A, 0x45, 0xDF, 0xA3, // EBML header id
593            0x8F, // size
594            0x42, 0x82, 0x84, b'w', b'e', b'b', b'm', 0, 0, 0, 0, 0, 0, 0, 0,
595        ];
596        assert_eq!(detect(&bytes), Some(MimeType::WebM));
597    }
598
599    #[test]
600    fn mkv_via_ebml_doctype() {
601        let bytes = [
602            0x1A, 0x45, 0xDF, 0xA3, // EBML header id
603            0x8F, // size
604            0x42, 0x82, 0x88, b'm', b'a', b't', b'r', b'o', b's', b'k', b'a', 0, 0, 0, 0,
605        ];
606        assert_eq!(detect(&bytes), Some(MimeType::Mkv));
607    }
608
609    #[test]
610    fn heic_via_ftyp_brand() {
611        let bytes = [
612            0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'h', b'e', b'i', b'c', 0x00, 0x00,
613            0x00, 0x00,
614        ];
615        assert_eq!(detect(&bytes), Some(MimeType::Heic));
616    }
617
618    #[test]
619    fn heif_via_ftyp_brand() {
620        let bytes = [
621            0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'm', b'i', b'f', b'1', 0x00, 0x00,
622            0x00, 0x00,
623        ];
624        assert_eq!(detect(&bytes), Some(MimeType::Heif));
625    }
626
627    #[test]
628    fn avif_via_ftyp_brand() {
629        let bytes = [
630            0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'a', b'v', b'i', b'f', 0x00, 0x00,
631            0x00, 0x00,
632        ];
633        assert_eq!(detect(&bytes), Some(MimeType::Avif));
634    }
635
636    #[test]
637    fn mov_via_ftyp_brand() {
638        let bytes = [
639            0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'q', b't', b' ', b' ', 0x00, 0x00,
640            0x00, 0x00,
641        ];
642        assert_eq!(detect(&bytes), Some(MimeType::QuickTime));
643    }
644
645    #[test]
646    fn threegp_via_ftyp_brand() {
647        let bytes = [
648            0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'3', b'g', b'p', b'4', 0x00, 0x00,
649            0x00, 0x00,
650        ];
651        assert_eq!(detect(&bytes), Some(MimeType::ThreeGp));
652    }
653
654    #[test]
655    fn mp4_fallback_for_unknown_brand() {
656        let bytes = [
657            0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'i', b's', b'o', b'm', 0x00, 0x00,
658            0x00, 0x00,
659        ];
660        assert_eq!(detect(&bytes), Some(MimeType::Mp4));
661    }
662
663    #[test]
664    fn ext_new_variants() {
665        assert_eq!(detect_by_extension("heic"), Some(MimeType::Heic));
666        assert_eq!(detect_by_extension("heif"), Some(MimeType::Heif));
667        assert_eq!(detect_by_extension("avif"), Some(MimeType::Avif));
668        assert_eq!(detect_by_extension("mov"), Some(MimeType::QuickTime));
669        assert_eq!(detect_by_extension("3gp"), Some(MimeType::ThreeGp));
670    }
671}