running_process_platform_internal/platform/window_icon/
ico.rs1const ICONDIR_LEN: usize = 6;
22const ICONDIRENTRY_LEN: usize = 16;
24const TYPE_ICON: u16 = 1;
27
28#[derive(Debug, PartialEq, Eq)]
30pub enum IcoError {
31 Truncated,
33 NotAnIcon,
35 NoImages,
37 EntryOutOfBounds,
39}
40
41impl std::fmt::Display for IcoError {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 let text = match self {
44 Self::Truncated => "icon data is truncated",
45 Self::NotAnIcon => "data is not an icon (bad ICONDIR header)",
46 Self::NoImages => "icon directory contains no images",
47 Self::EntryOutOfBounds => "an icon entry points outside the supplied data",
48 };
49 f.write_str(text)
50 }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub struct ImageSpan {
56 pub offset: usize,
58 pub len: usize,
60}
61
62fn u16_at(bytes: &[u8], at: usize) -> u16 {
63 u16::from_le_bytes([bytes[at], bytes[at + 1]])
64}
65
66fn u32_at(bytes: &[u8], at: usize) -> u32 {
67 u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
68}
69
70pub fn best_image(bytes: &[u8]) -> Result<ImageSpan, IcoError> {
77 if bytes.len() < ICONDIR_LEN {
78 return Err(IcoError::Truncated);
79 }
80 if u16_at(bytes, 0) != 0 || u16_at(bytes, 2) != TYPE_ICON {
83 return Err(IcoError::NotAnIcon);
84 }
85 let count = u16_at(bytes, 4) as usize;
86 if count == 0 {
87 return Err(IcoError::NoImages);
88 }
89
90 let directory_end = ICONDIR_LEN
92 .checked_add(
93 count
94 .checked_mul(ICONDIRENTRY_LEN)
95 .ok_or(IcoError::Truncated)?,
96 )
97 .ok_or(IcoError::Truncated)?;
98 if bytes.len() < directory_end {
99 return Err(IcoError::Truncated);
100 }
101
102 let mut best: Option<(u32, u16, ImageSpan)> = None;
103 for index in 0..count {
104 let entry = ICONDIR_LEN + index * ICONDIRENTRY_LEN;
105 let width = match bytes[entry] {
109 0 => 256u32,
110 w => u32::from(w),
111 };
112 let height = match bytes[entry + 1] {
113 0 => 256u32,
114 h => u32::from(h),
115 };
116 let bit_count = u16_at(bytes, entry + 6);
117 let len = u32_at(bytes, entry + 8) as usize;
118 let offset = u32_at(bytes, entry + 12) as usize;
119
120 let end = offset.checked_add(len).ok_or(IcoError::EntryOutOfBounds)?;
124 if len == 0 || end > bytes.len() || offset < directory_end {
125 return Err(IcoError::EntryOutOfBounds);
126 }
127
128 let pixels = width * height;
129 let candidate = (pixels, bit_count, ImageSpan { offset, len });
130 match &best {
131 Some((best_pixels, best_depth, _))
132 if (*best_pixels, *best_depth) >= (pixels, bit_count) => {}
133 _ => best = Some(candidate),
134 }
135 }
136
137 best.map(|(_, _, span)| span).ok_or(IcoError::NoImages)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn ico(entries: &[(u8, u8, u16, &[u8])]) -> Vec<u8> {
146 let count = entries.len();
147 let mut out = Vec::new();
148 out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&TYPE_ICON.to_le_bytes());
150 out.extend_from_slice(&(count as u16).to_le_bytes());
151
152 let mut offset = ICONDIR_LEN + count * ICONDIRENTRY_LEN;
153 let mut payloads = Vec::new();
154 for (w, h, bits, data) in entries {
155 out.push(*w);
156 out.push(*h);
157 out.push(0); out.push(0); out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&bits.to_le_bytes());
161 out.extend_from_slice(&(data.len() as u32).to_le_bytes());
162 out.extend_from_slice(&(offset as u32).to_le_bytes());
163 offset += data.len();
164 payloads.push(*data);
165 }
166 for data in payloads {
167 out.extend_from_slice(data);
168 }
169 out
170 }
171
172 #[test]
173 fn a_single_image_is_found() {
174 let bytes = ico(&[(32, 32, 32, b"IMAGEDATA")]);
175 let span = best_image(&bytes).expect("valid icon");
176 assert_eq!(&bytes[span.offset..span.offset + span.len], b"IMAGEDATA");
177 }
178
179 #[test]
180 fn the_largest_image_wins() {
181 let bytes = ico(&[
182 (16, 16, 32, b"small"),
183 (64, 64, 32, b"BIGGEST"),
184 (32, 32, 32, b"mid"),
185 ]);
186 let span = best_image(&bytes).unwrap();
187 assert_eq!(&bytes[span.offset..span.offset + span.len], b"BIGGEST");
188 }
189
190 #[test]
192 fn a_zero_dimension_means_256_and_therefore_wins() {
193 let bytes = ico(&[(64, 64, 32, b"sixtyfour"), (0, 0, 32, b"TWOFIFTYSIX")]);
194 let span = best_image(&bytes).unwrap();
195 assert_eq!(
196 &bytes[span.offset..span.offset + span.len],
197 b"TWOFIFTYSIX",
198 "a 0 dimension byte encodes 256 and should outrank 64x64"
199 );
200 }
201
202 #[test]
203 fn colour_depth_breaks_a_size_tie() {
204 let bytes = ico(&[(32, 32, 4, b"shallow"), (32, 32, 32, b"DEEPEST")]);
205 let span = best_image(&bytes).unwrap();
206 assert_eq!(&bytes[span.offset..span.offset + span.len], b"DEEPEST");
207 }
208
209 #[test]
210 fn empty_input_is_truncated_not_a_panic() {
211 assert_eq!(best_image(&[]), Err(IcoError::Truncated));
212 assert_eq!(best_image(&[0, 0, 1]), Err(IcoError::Truncated));
213 }
214
215 #[test]
216 fn a_non_icon_is_refused() {
217 let mut bytes = ico(&[(32, 32, 32, b"data")]);
219 bytes[2] = 2;
220 assert_eq!(best_image(&bytes), Err(IcoError::NotAnIcon));
221
222 let mut bytes = ico(&[(32, 32, 32, b"data")]);
224 bytes[0] = 9;
225 assert_eq!(best_image(&bytes), Err(IcoError::NotAnIcon));
226 }
227
228 #[test]
229 fn a_directory_claiming_no_images_is_refused() {
230 let bytes = ico(&[]);
231 assert_eq!(best_image(&bytes), Err(IcoError::NoImages));
232 }
233
234 #[test]
237 fn an_entry_running_past_the_buffer_is_refused() {
238 let mut bytes = ico(&[(32, 32, 32, b"data")]);
239 let len_at = ICONDIR_LEN + 8;
240 bytes[len_at..len_at + 4].copy_from_slice(&0xFFFF_u32.to_le_bytes());
241 assert_eq!(best_image(&bytes), Err(IcoError::EntryOutOfBounds));
242 }
243
244 #[test]
245 fn an_entry_offset_past_the_buffer_is_refused() {
246 let mut bytes = ico(&[(32, 32, 32, b"data")]);
247 let offset_at = ICONDIR_LEN + 12;
248 bytes[offset_at..offset_at + 4].copy_from_slice(&0xFFFF_u32.to_le_bytes());
249 assert_eq!(best_image(&bytes), Err(IcoError::EntryOutOfBounds));
250 }
251
252 #[test]
255 fn an_offset_inside_the_directory_is_refused() {
256 let mut bytes = ico(&[(32, 32, 32, b"data")]);
257 let offset_at = ICONDIR_LEN + 12;
258 bytes[offset_at..offset_at + 4].copy_from_slice(&2u32.to_le_bytes());
259 assert_eq!(best_image(&bytes), Err(IcoError::EntryOutOfBounds));
260 }
261
262 #[test]
263 fn a_zero_length_entry_is_refused() {
264 let mut bytes = ico(&[(32, 32, 32, b"data")]);
265 let len_at = ICONDIR_LEN + 8;
266 bytes[len_at..len_at + 4].copy_from_slice(&0u32.to_le_bytes());
267 assert_eq!(best_image(&bytes), Err(IcoError::EntryOutOfBounds));
268 }
269
270 #[test]
272 fn a_count_exceeding_the_buffer_is_truncated() {
273 let mut bytes = ico(&[(32, 32, 32, b"data")]);
274 bytes[4..6].copy_from_slice(&1000u16.to_le_bytes());
275 assert_eq!(best_image(&bytes), Err(IcoError::Truncated));
276 }
277
278 #[test]
281 fn corrupted_icons_never_panic() {
282 let original = ico(&[(16, 16, 32, b"aa"), (32, 32, 32, b"bbbb")]);
283 for index in 0..original.len() {
284 for replacement in [0u8, 0xFF, 0x7F] {
285 let mut bytes = original.clone();
286 bytes[index] = replacement;
287 let _ = best_image(&bytes);
289 }
290 let _ = best_image(&original[..index]);
292 }
293 }
294}