master
1//! https://devblogs.microsoft.com/oldnewthing/20120720-00/?p=7083
2//! https://learn.microsoft.com/en-us/previous-versions/ms997538(v=msdn.10)
3//! https://learn.microsoft.com/en-us/windows/win32/menurc/newheader
4//! https://learn.microsoft.com/en-us/windows/win32/menurc/resdir
5//! https://learn.microsoft.com/en-us/windows/win32/menurc/localheader
6
7const std = @import("std");
8const builtin = @import("builtin");
9const native_endian = builtin.cpu.arch.endian();
10
11pub const ReadError = std.mem.Allocator.Error || error{ InvalidHeader, InvalidImageType, ImpossibleDataSize, UnexpectedEOF, ReadFailed };
12
13pub fn read(allocator: std.mem.Allocator, reader: *std.Io.Reader, max_size: u64) ReadError!IconDir {
14 return readInner(allocator, reader, max_size) catch |err| switch (err) {
15 error.OutOfMemory,
16 error.InvalidHeader,
17 error.InvalidImageType,
18 error.ImpossibleDataSize,
19 error.ReadFailed,
20 => |e| return e,
21 error.EndOfStream => error.UnexpectedEOF,
22 };
23}
24
25// TODO: This seems like a somewhat strange pattern, could be a better way
26// to do this. Maybe it makes more sense to handle the translation
27// at the call site instead of having a helper function here.
28fn readInner(allocator: std.mem.Allocator, reader: *std.Io.Reader, max_size: u64) !IconDir {
29 const reserved = try reader.takeInt(u16, .little);
30 if (reserved != 0) {
31 return error.InvalidHeader;
32 }
33
34 const image_type = reader.takeEnum(ImageType, .little) catch |err| switch (err) {
35 error.InvalidEnumTag => return error.InvalidImageType,
36 else => |e| return e,
37 };
38
39 const num_images = try reader.takeInt(u16, .little);
40
41 // To avoid over-allocation in the case of a file that says it has way more
42 // entries than it actually does, we use an ArrayList with a conservatively
43 // limited initial capacity instead of allocating the entire slice at once.
44 const initial_capacity = @min(num_images, 8);
45 var entries = try std.ArrayList(Entry).initCapacity(allocator, initial_capacity);
46 errdefer entries.deinit(allocator);
47
48 var i: usize = 0;
49 while (i < num_images) : (i += 1) {
50 var entry: Entry = undefined;
51 entry.width = try reader.takeByte();
52 entry.height = try reader.takeByte();
53 entry.num_colors = try reader.takeByte();
54 entry.reserved = try reader.takeByte();
55 switch (image_type) {
56 .icon => {
57 entry.type_specific_data = .{ .icon = .{
58 .color_planes = try reader.takeInt(u16, .little),
59 .bits_per_pixel = try reader.takeInt(u16, .little),
60 } };
61 },
62 .cursor => {
63 entry.type_specific_data = .{ .cursor = .{
64 .hotspot_x = try reader.takeInt(u16, .little),
65 .hotspot_y = try reader.takeInt(u16, .little),
66 } };
67 },
68 }
69 entry.data_size_in_bytes = try reader.takeInt(u32, .little);
70 entry.data_offset_from_start_of_file = try reader.takeInt(u32, .little);
71 // Validate that the offset/data size is feasible
72 if (@as(u64, entry.data_offset_from_start_of_file) + entry.data_size_in_bytes > max_size) {
73 return error.ImpossibleDataSize;
74 }
75 // and that the data size is large enough for at least the header of an image
76 // Note: This avoids needing to deal with a miscompilation from the Win32 RC
77 // compiler when the data size of an image is specified as zero but there
78 // is data to-be-read at the offset. The Win32 RC compiler will output
79 // an ICON/CURSOR resource with a bogus size in its header but with no actual
80 // data bytes in it, leading to an invalid .res. Similarly, if, for example,
81 // there is valid PNG data at the image's offset, but the size is specified
82 // as fewer bytes than the PNG header, then the Win32 RC compiler will still
83 // treat it as a PNG (e.g. unconditionally set num_planes to 1) but the data
84 // of the resource will only be 1 byte so treating it as a PNG doesn't make
85 // sense (especially not when you have to read past the data size to determine
86 // that it's a PNG).
87 if (entry.data_size_in_bytes < 16) {
88 return error.ImpossibleDataSize;
89 }
90 try entries.append(allocator, entry);
91 }
92
93 return .{
94 .image_type = image_type,
95 .entries = try entries.toOwnedSlice(allocator),
96 .allocator = allocator,
97 };
98}
99
100pub const ImageType = enum(u16) {
101 icon = 1,
102 cursor = 2,
103};
104
105pub const IconDir = struct {
106 image_type: ImageType,
107 /// Note: entries.len will always fit into a u16, since the field containing the
108 /// number of images in an ico file is a u16.
109 entries: []Entry,
110 allocator: std.mem.Allocator,
111
112 pub fn deinit(self: IconDir) void {
113 self.allocator.free(self.entries);
114 }
115
116 pub const res_header_byte_len = 6;
117
118 pub fn getResDataSize(self: IconDir) u32 {
119 // maxInt(u16) * Entry.res_byte_len = 917,490 which is well within the u32 range.
120 // Note: self.entries.len is limited to maxInt(u16)
121 return @intCast(IconDir.res_header_byte_len + self.entries.len * Entry.res_byte_len);
122 }
123
124 pub fn writeResData(self: IconDir, writer: *std.Io.Writer, first_image_id: u16) !void {
125 try writer.writeInt(u16, 0, .little);
126 try writer.writeInt(u16, @intFromEnum(self.image_type), .little);
127 // We know that entries.len must fit into a u16
128 try writer.writeInt(u16, @as(u16, @intCast(self.entries.len)), .little);
129
130 var image_id = first_image_id;
131 for (self.entries) |entry| {
132 try entry.writeResData(writer, image_id);
133 image_id += 1;
134 }
135 }
136};
137
138pub const Entry = struct {
139 // Icons are limited to u8 sizes, cursors can have u16,
140 // so we store as u16 and truncate when needed.
141 width: u16,
142 height: u16,
143 num_colors: u8,
144 /// This should always be zero, but whatever value it is gets
145 /// carried over so we need to store it
146 reserved: u8,
147 type_specific_data: union(ImageType) {
148 icon: struct {
149 color_planes: u16,
150 bits_per_pixel: u16,
151 },
152 cursor: struct {
153 hotspot_x: u16,
154 hotspot_y: u16,
155 },
156 },
157 data_size_in_bytes: u32,
158 data_offset_from_start_of_file: u32,
159
160 pub const res_byte_len = 14;
161
162 pub fn writeResData(self: Entry, writer: *std.Io.Writer, id: u16) !void {
163 switch (self.type_specific_data) {
164 .icon => |icon_data| {
165 try writer.writeInt(u8, @as(u8, @truncate(self.width)), .little);
166 try writer.writeInt(u8, @as(u8, @truncate(self.height)), .little);
167 try writer.writeInt(u8, self.num_colors, .little);
168 try writer.writeInt(u8, self.reserved, .little);
169 try writer.writeInt(u16, icon_data.color_planes, .little);
170 try writer.writeInt(u16, icon_data.bits_per_pixel, .little);
171 try writer.writeInt(u32, self.data_size_in_bytes, .little);
172 },
173 .cursor => |cursor_data| {
174 try writer.writeInt(u16, self.width, .little);
175 try writer.writeInt(u16, self.height, .little);
176 try writer.writeInt(u16, cursor_data.hotspot_x, .little);
177 try writer.writeInt(u16, cursor_data.hotspot_y, .little);
178 try writer.writeInt(u32, self.data_size_in_bytes + 4, .little);
179 },
180 }
181 try writer.writeInt(u16, id, .little);
182 }
183};
184
185test "icon" {
186 const data = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00\x10\x00\x10\x00\x00\x00\x16\x00\x00\x00" ++ [_]u8{0} ** 16;
187 var fbs: std.Io.Reader = .fixed(data);
188 const icon = try read(std.testing.allocator, &fbs, data.len);
189 defer icon.deinit();
190
191 try std.testing.expectEqual(ImageType.icon, icon.image_type);
192 try std.testing.expectEqual(@as(usize, 1), icon.entries.len);
193}
194
195test "icon too many images" {
196 // Note that with verifying that all data sizes are within the file bounds and >= 16,
197 // it's not possible to hit EOF when looking for more RESDIR structures, since they are
198 // themselves 16 bytes long, so we'll always hit ImpossibleDataSize instead.
199 const data = "\x00\x00\x01\x00\x02\x00\x10\x10\x00\x00\x01\x00\x10\x00\x10\x00\x00\x00\x16\x00\x00\x00" ++ [_]u8{0} ** 16;
200 var fbs: std.Io.Reader = .fixed(data);
201 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, &fbs, data.len));
202}
203
204test "icon data size past EOF" {
205 const data = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00\x10\x00\x10\x01\x00\x00\x16\x00\x00\x00" ++ [_]u8{0} ** 16;
206 var fbs: std.Io.Reader = .fixed(data);
207 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, &fbs, data.len));
208}
209
210test "icon data offset past EOF" {
211 const data = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00\x10\x00\x10\x00\x00\x00\x17\x00\x00\x00" ++ [_]u8{0} ** 16;
212 var fbs: std.Io.Reader = .fixed(data);
213 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, &fbs, data.len));
214}
215
216test "icon data size too small" {
217 const data = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00\x10\x00\x0F\x00\x00\x00\x16\x00\x00\x00";
218 var fbs: std.Io.Reader = .fixed(data);
219 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, &fbs, data.len));
220}
221
222pub const ImageFormat = enum(u2) {
223 dib,
224 png,
225 riff,
226
227 const riff_header = std.mem.readInt(u32, "RIFF", native_endian);
228 const png_signature = std.mem.readInt(u64, "\x89PNG\r\n\x1a\n", native_endian);
229 const ihdr_code = std.mem.readInt(u32, "IHDR", native_endian);
230 const acon_form_type = std.mem.readInt(u32, "ACON", native_endian);
231
232 pub fn detect(header_bytes: *const [16]u8) ImageFormat {
233 if (std.mem.readInt(u32, header_bytes[0..4], native_endian) == riff_header) return .riff;
234 if (std.mem.readInt(u64, header_bytes[0..8], native_endian) == png_signature) return .png;
235 return .dib;
236 }
237
238 pub fn validate(format: ImageFormat, header_bytes: *const [16]u8) bool {
239 return switch (format) {
240 .png => std.mem.readInt(u32, header_bytes[12..16], native_endian) == ihdr_code,
241 .riff => std.mem.readInt(u32, header_bytes[8..12], native_endian) == acon_form_type,
242 .dib => true,
243 };
244 }
245};
246
247/// Contains only the fields of BITMAPINFOHEADER (WinGDI.h) that are both:
248/// - relevant to what we need, and
249/// - are shared between all versions of BITMAPINFOHEADER (V4, V5).
250pub const BitmapHeader = extern struct {
251 bcSize: u32,
252 bcWidth: i32,
253 bcHeight: i32,
254 bcPlanes: u16,
255 bcBitCount: u16,
256
257 pub fn version(self: *const BitmapHeader) Version {
258 return Version.get(self.bcSize);
259 }
260
261 /// https://en.wikipedia.org/wiki/BMP_file_format#DIB_header_(bitmap_information_header)
262 pub const Version = enum(u3) {
263 unknown,
264 @"win2.0", // Windows 2.0 or later
265 @"nt3.1", // Windows NT, 3.1x or later
266 @"nt4.0", // Windows NT 4.0, 95 or later
267 @"nt5.0", // Windows NT 5.0, 98 or later
268
269 pub fn get(header_size: u32) Version {
270 return switch (header_size) {
271 len(.@"win2.0") => .@"win2.0",
272 len(.@"nt3.1") => .@"nt3.1",
273 len(.@"nt4.0") => .@"nt4.0",
274 len(.@"nt5.0") => .@"nt5.0",
275 else => .unknown,
276 };
277 }
278
279 pub fn len(comptime v: Version) comptime_int {
280 return switch (v) {
281 .@"win2.0" => 12,
282 .@"nt3.1" => 40,
283 .@"nt4.0" => 108,
284 .@"nt5.0" => 124,
285 .unknown => unreachable,
286 };
287 }
288
289 pub fn nameForErrorDisplay(v: Version) []const u8 {
290 return switch (v) {
291 .unknown => "unknown",
292 .@"win2.0" => "Windows 2.0 (BITMAPCOREHEADER)",
293 .@"nt3.1" => "Windows NT, 3.1x (BITMAPINFOHEADER)",
294 .@"nt4.0" => "Windows NT 4.0, 95 (BITMAPV4HEADER)",
295 .@"nt5.0" => "Windows NT 5.0, 98 (BITMAPV5HEADER)",
296 };
297 }
298 };
299};