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 }