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