zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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