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};