icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

reader.go (7406B)


      1 package exe
      2 
      3 import (
      4 	"bytes"
      5 	"cmp"
      6 	"debug/pe"
      7 	"encoding/binary"
      8 	"fmt"
      9 	"image"
     10 	"io"
     11 	"slices"
     12 
     13 	"github.com/jackmordaunt/icns/v4/ico"
     14 )
     15 
     16 // Icons returns the icons a Windows binary carries, lowest ordinal first,
     17 // which is the order Explorer draws them in.
     18 func Icons(r io.ReaderAt) ([]Group, error) {
     19 	file, err := pe.NewFile(r)
     20 	if err != nil {
     21 		return nil, fmt.Errorf("reading binary: %w", err)
     22 	}
     23 	defer file.Close()
     24 	section := file.Section(".rsrc")
     25 	if section == nil {
     26 		return nil, ErrNoIcons
     27 	}
     28 	data, err := section.Data()
     29 	if err != nil {
     30 		return nil, fmt.Errorf("reading resource section: %w", err)
     31 	}
     32 	// Section.Data returns the bytes on disk, which may be padded out past
     33 	// the size the section declares.
     34 	if int(section.VirtualSize) < len(data) {
     35 		data = data[:section.VirtualSize]
     36 	}
     37 	res := resources{data: data, base: section.VirtualAddress}
     38 	images, err := res.leaves(typeIcon)
     39 	if err != nil {
     40 		return nil, err
     41 	}
     42 	groups, err := res.leaves(typeIconGroup)
     43 	if err != nil {
     44 		return nil, err
     45 	}
     46 	if len(groups) == 0 {
     47 		return nil, ErrNoIcons
     48 	}
     49 	out := make([]Group, 0, len(groups))
     50 	for _, entry := range groups {
     51 		group, err := assemble(entry, images)
     52 		if err != nil {
     53 			return nil, err
     54 		}
     55 		out = append(out, group)
     56 	}
     57 	slices.SortStableFunc(out, func(a, b Group) int {
     58 		return cmp.Compare(a.ID, b.ID)
     59 	})
     60 	return out, nil
     61 }
     62 
     63 // Decode returns the largest icon in the binary that can be decoded.
     64 func Decode(r io.ReaderAt) (image.Image, error) {
     65 	groups, err := Icons(r)
     66 	if err != nil {
     67 		return nil, err
     68 	}
     69 	return groups[0].Decode()
     70 }
     71 
     72 // Decode returns the largest icon in the group that can be decoded.
     73 func (g Group) Decode() (image.Image, error) {
     74 	return ico.Decode(bytes.NewReader(g.ico))
     75 }
     76 
     77 // leaf is one resource: the ordinal it is filed under and its bytes.
     78 type leaf struct {
     79 	id   uint16
     80 	data []byte
     81 }
     82 
     83 // resources walks the tree in a resource section. The tree is three levels
     84 // deep, by type, then by name or ordinal, then by language, and every offset
     85 // inside it is measured from the start of the section.
     86 type resources struct {
     87 	data []byte
     88 	base uint32
     89 }
     90 
     91 // leaves returns every resource of a type, taking the first language of each.
     92 func (res resources) leaves(kind uint32) ([]leaf, error) {
     93 	types, err := res.entries(0)
     94 	if err != nil {
     95 		return nil, err
     96 	}
     97 	var out []leaf
     98 	for _, t := range types {
     99 		if t.name != kind || !t.directory {
    100 			continue
    101 		}
    102 		named, err := res.entries(t.offset)
    103 		if err != nil {
    104 			return nil, err
    105 		}
    106 		for _, n := range named {
    107 			if !n.directory {
    108 				continue
    109 			}
    110 			languages, err := res.entries(n.offset)
    111 			if err != nil {
    112 				return nil, err
    113 			}
    114 			for _, l := range languages {
    115 				if l.directory {
    116 					continue
    117 				}
    118 				data, err := res.at(l.offset)
    119 				if err != nil {
    120 					return nil, err
    121 				}
    122 				out = append(out, leaf{id: uint16(n.name), data: data})
    123 				// One language is enough: the images are the same icon.
    124 				break
    125 			}
    126 		}
    127 	}
    128 	return out, nil
    129 }
    130 
    131 // entry is one row of a resource directory.
    132 type entry struct {
    133 	// name is the ordinal the resource is filed under, or the offset of its
    134 	// name when it has one rather than a number.
    135 	name uint32
    136 	// offset is where the row points, from the start of the section.
    137 	offset uint32
    138 	// directory reports whether the row points at another directory rather
    139 	// than at the bytes of a resource.
    140 	directory bool
    141 }
    142 
    143 // entries reads the rows of the resource directory at offset.
    144 func (res resources) entries(offset uint32) ([]entry, error) {
    145 	if int(offset)+directoryHeaderSize > len(res.data) {
    146 		return nil, fmt.Errorf("%w: a directory lies at %d, outside the section", ErrMalformed, offset)
    147 	}
    148 	var (
    149 		header = res.data[offset:]
    150 		named  = int(binary.LittleEndian.Uint16(header[12:14]))
    151 		ids    = int(binary.LittleEndian.Uint16(header[14:16]))
    152 		count  = named + ids
    153 		at     = int(offset) + directoryHeaderSize
    154 	)
    155 	if at+count*directoryEntrySize > len(res.data) {
    156 		return nil, fmt.Errorf("%w: a directory of %d entries runs past the section", ErrMalformed, count)
    157 	}
    158 	out := make([]entry, 0, count)
    159 	for i := 0; i < count; i++ {
    160 		row := res.data[at+i*directoryEntrySize:]
    161 		var (
    162 			name   = binary.LittleEndian.Uint32(row[0:4])
    163 			target = binary.LittleEndian.Uint32(row[4:8])
    164 		)
    165 		out = append(out, entry{
    166 			// The high bit marks a name held as a string rather than an
    167 			// ordinal, which icons are not filed under.
    168 			name:      name &^ 0x80000000,
    169 			offset:    target &^ 0x80000000,
    170 			directory: target&0x80000000 != 0,
    171 		})
    172 	}
    173 	return out, nil
    174 }
    175 
    176 // at reads the resource the data entry at offset points to. The entry holds
    177 // an address in the loaded image, which the section's own address turns back
    178 // into a position in the file.
    179 func (res resources) at(offset uint32) ([]byte, error) {
    180 	if int(offset)+dataEntrySize > len(res.data) {
    181 		return nil, fmt.Errorf("%w: a data entry lies at %d, outside the section", ErrMalformed, offset)
    182 	}
    183 	var (
    184 		row     = res.data[offset:]
    185 		address = binary.LittleEndian.Uint32(row[0:4])
    186 		size    = binary.LittleEndian.Uint32(row[4:8])
    187 	)
    188 	if address < res.base {
    189 		return nil, fmt.Errorf("%w: a resource lies at %d, before the section", ErrMalformed, address)
    190 	}
    191 	start := address - res.base
    192 	if int(start)+int(size) > len(res.data) {
    193 		return nil, fmt.Errorf("%w: a resource of %d bytes at %d runs past the section", ErrMalformed, size, start)
    194 	}
    195 	return res.data[start : start+size], nil
    196 }
    197 
    198 // assemble turns a group icon directory and the images it names back into an
    199 // ico file. The two differ only in the last field of a row, where the group
    200 // names a resource and an ico gives the position of the image.
    201 func assemble(group leaf, images []leaf) (Group, error) {
    202 	if len(group.data) < groupHeaderSize {
    203 		return Group{}, fmt.Errorf("%w: icon group %d holds %d bytes", ErrMalformed, group.id, len(group.data))
    204 	}
    205 	count := int(binary.LittleEndian.Uint16(group.data[4:6]))
    206 	if groupHeaderSize+count*groupEntrySize > len(group.data) {
    207 		return Group{}, fmt.Errorf("%w: icon group %d lists %d icons it does not hold", ErrMalformed, group.id, count)
    208 	}
    209 	var (
    210 		rows   = make([]byte, 0, groupHeaderSize+count*icoEntrySize)
    211 		body   []byte
    212 		sizes  []int
    213 		offset = groupHeaderSize + count*icoEntrySize
    214 	)
    215 	rows = binary.LittleEndian.AppendUint16(rows, 0)
    216 	rows = binary.LittleEndian.AppendUint16(rows, 1)
    217 	rows = binary.LittleEndian.AppendUint16(rows, uint16(count))
    218 	for i := 0; i < count; i++ {
    219 		row := group.data[groupHeaderSize+i*groupEntrySize:]
    220 		id := binary.LittleEndian.Uint16(row[12:14])
    221 		index := slices.IndexFunc(images, func(l leaf) bool { return l.id == id })
    222 		if index < 0 {
    223 			return Group{}, fmt.Errorf("%w: icon group %d names image %d, which is not present", ErrMalformed, group.id, id)
    224 		}
    225 		pixels := images[index].data
    226 		// The two rows agree up to the length of the image, which is taken
    227 		// from the resource itself rather than from the field that names it.
    228 		rows = append(rows, row[:8]...)
    229 		rows = binary.LittleEndian.AppendUint32(rows, uint32(len(pixels)))
    230 		rows = binary.LittleEndian.AppendUint32(rows, uint32(offset))
    231 		body = append(body, pixels...)
    232 		offset += len(pixels)
    233 
    234 		side := int(row[0])
    235 		if side == 0 {
    236 			side = 256
    237 		}
    238 		sizes = append(sizes, side)
    239 	}
    240 	slices.SortStableFunc(sizes, func(a, b int) int { return cmp.Compare(b, a) })
    241 	return Group{
    242 		ID:    group.id,
    243 		Sizes: sizes,
    244 		ico:   append(rows, body...),
    245 	}, nil
    246 }