Coverage Report

Created: 2025-11-11 07:15

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/hooks.rs
Line
Count
Source
1
//! This module provides a way to register decoding hooks for image formats not directly supported
2
//! by this crate.
3
4
use std::{
5
    collections::HashMap,
6
    ffi::{OsStr, OsString},
7
    io::{BufRead, BufReader, Read, Seek},
8
    sync::RwLock,
9
};
10
11
use crate::{ImageDecoder, ImageResult};
12
13
pub(crate) trait ReadSeek: Read + Seek {}
14
impl<T: Read + Seek> ReadSeek for T {}
15
16
pub(crate) static DECODING_HOOKS: RwLock<Option<HashMap<OsString, DecodingHook>>> =
17
    RwLock::new(None);
18
19
pub(crate) type DetectionHook = (&'static [u8], &'static [u8], OsString);
20
pub(crate) static GUESS_FORMAT_HOOKS: RwLock<Vec<DetectionHook>> = RwLock::new(Vec::new());
21
22
/// A wrapper around a type-erased trait object that implements `Read` and `Seek`.
23
pub struct GenericReader<'a>(pub(crate) BufReader<Box<dyn ReadSeek + 'a>>);
24
impl Read for GenericReader<'_> {
25
0
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
26
0
        self.0.read(buf)
27
0
    }
28
0
    fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
29
0
        self.0.read_vectored(bufs)
30
0
    }
31
0
    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> std::io::Result<usize> {
32
0
        self.0.read_to_end(buf)
33
0
    }
34
0
    fn read_to_string(&mut self, buf: &mut String) -> std::io::Result<usize> {
35
0
        self.0.read_to_string(buf)
36
0
    }
37
0
    fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
38
0
        self.0.read_exact(buf)
39
0
    }
40
}
41
impl BufRead for GenericReader<'_> {
42
0
    fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
43
0
        self.0.fill_buf()
44
0
    }
45
0
    fn consume(&mut self, amt: usize) {
46
0
        self.0.consume(amt)
47
0
    }
48
0
    fn read_until(&mut self, byte: u8, buf: &mut Vec<u8>) -> std::io::Result<usize> {
49
0
        self.0.read_until(byte, buf)
50
0
    }
51
0
    fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> {
52
0
        self.0.read_line(buf)
53
0
    }
54
}
55
impl Seek for GenericReader<'_> {
56
0
    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
57
0
        self.0.seek(pos)
58
0
    }
59
0
    fn rewind(&mut self) -> std::io::Result<()> {
60
0
        self.0.rewind()
61
0
    }
62
0
    fn stream_position(&mut self) -> std::io::Result<u64> {
63
0
        self.0.stream_position()
64
0
    }
65
66
    // TODO: Add `seek_relative` once MSRV is at least 1.80.0
67
}
68
69
/// A function to produce an `ImageDecoder` for a given image format.
70
pub type DecodingHook =
71
    Box<dyn for<'a> Fn(GenericReader<'a>) -> ImageResult<Box<dyn ImageDecoder + 'a>> + Send + Sync>;
72
73
/// Register a new decoding hook or returns false if one already exists for the given format.
74
0
pub fn register_decoding_hook(extension: OsString, hook: DecodingHook) -> bool {
75
0
    let mut hooks = DECODING_HOOKS.write().unwrap();
76
0
    if hooks.is_none() {
77
0
        *hooks = Some(HashMap::new());
78
0
    }
79
0
    match hooks.as_mut().unwrap().entry(extension) {
80
0
        std::collections::hash_map::Entry::Vacant(entry) => {
81
0
            entry.insert(hook);
82
0
            true
83
        }
84
0
        std::collections::hash_map::Entry::Occupied(_) => false,
85
    }
86
0
}
87
88
/// Returns whether a decoding hook has been registered for the given format.
89
0
pub fn decoding_hook_registered(extension: &OsStr) -> bool {
90
0
    DECODING_HOOKS
91
0
        .read()
92
0
        .unwrap()
93
0
        .as_ref()
94
0
        .map(|hooks| hooks.contains_key(extension))
95
0
        .unwrap_or(false)
96
0
}
97
98
/// Registers a format detection hook.
99
///
100
/// The signature field holds the magic bytes from the start of the file that must be matched to
101
/// detect the format. The mask field is optional and can be used to specify which bytes in the
102
/// signature should be ignored during the detection.
103
///
104
/// # Examples
105
///
106
/// ## Using the mask to ignore some bytes
107
///
108
/// ```
109
/// # use image::hooks::register_format_detection_hook;
110
/// // WebP signature is 'riff' followed by 4 bytes of length and then by 'webp'.
111
/// // This requires a mask to ignore the length.
112
/// register_format_detection_hook("webp".into(),
113
///      &[b'r', b'i', b'f', b'f', 0, 0, 0, 0, b'w', b'e', b'b', b'p'],
114
/// Some(&[0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff]),
115
/// );
116
/// ```
117
///
118
/// ## Multiple signatures
119
///
120
/// ```
121
/// # use image::hooks::register_format_detection_hook;
122
/// // JPEG XL has two different signatures: https://en.wikipedia.org/wiki/JPEG_XL
123
/// // This function should be called twice to register them both.
124
/// register_format_detection_hook("jxl".into(), &[0xff, 0x0a], None);
125
/// register_format_detection_hook("jxl".into(),
126
///      &[0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0x0d, 0x0a, 0x87, 0x0a], None,
127
/// );
128
/// ```
129
///
130
0
pub fn register_format_detection_hook(
131
0
    extension: OsString,
132
0
    signature: &'static [u8],
133
0
    mask: Option<&'static [u8]>,
134
0
) {
135
0
    GUESS_FORMAT_HOOKS
136
0
        .write()
137
0
        .unwrap()
138
0
        .push((signature, mask.unwrap_or(&[]), extension));
139
0
}