package infra import ( "bytes" "encoding/binary" "fmt" "os" ) const ( rtIcon = 3 rtGroupIcon = 14 scnCntInitializedData = 0x00000040 scnMemRead = 0x40000000 dataDirResource = 2 resourceTableSlot = 2 ) type icoEntry struct { width uint8 height uint8 colors uint8 reserved uint8 planes uint16 bitCount uint16 size uint32 offset uint32 data []byte } func parseICO(buf []byte) ([]icoEntry, error) { if len(buf) < 6 { return nil, fmt.Errorf("ico too short") } if binary.LittleEndian.Uint16(buf[0:2]) != 0 { return nil, fmt.Errorf("ico reserved field must be 0") } if binary.LittleEndian.Uint16(buf[2:4]) != 1 { return nil, fmt.Errorf("not an icon (type != 1)") } count := int(binary.LittleEndian.Uint16(buf[4:6])) if count == 0 { return nil, fmt.Errorf("ico has 0 images") } if len(buf) < 6+16*count { return nil, fmt.Errorf("ico header truncated") } entries := make([]icoEntry, count) for i := 0; i < count; i++ { off := 6 + 16*i e := icoEntry{ width: buf[off], height: buf[off+1], colors: buf[off+2], reserved: buf[off+3], planes: binary.LittleEndian.Uint16(buf[off+4 : off+6]), bitCount: binary.LittleEndian.Uint16(buf[off+6 : off+8]), size: binary.LittleEndian.Uint32(buf[off+8 : off+12]), offset: binary.LittleEndian.Uint32(buf[off+12 : off+16]), } if int(e.offset)+int(e.size) > len(buf) { return nil, fmt.Errorf("ico image %d out of bounds", i) } e.data = buf[e.offset : e.offset+e.size] entries[i] = e } return entries, nil } type peLayout struct { data []byte peOff int is64 bool numSections int sizeOptHdr int sectionAlign uint32 fileAlign uint32 sectionsStart int sizeOfImage uint32 dataDirCount uint32 dataDirOff int checksumOff int sizeOfImageOff int } func parsePE(buf []byte) (*peLayout, error) { if len(buf) < 64 || buf[0] != 'M' || buf[1] != 'Z' { return nil, fmt.Errorf("not a PE file (no MZ signature)") } peOff := int(binary.LittleEndian.Uint32(buf[0x3C:0x40])) if peOff+24 > len(buf) { return nil, fmt.Errorf("PE header out of bounds") } if string(buf[peOff:peOff+4]) != "PE\x00\x00" { return nil, fmt.Errorf("not a PE file (no PE signature at e_lfanew)") } coff := peOff + 4 numSections := int(binary.LittleEndian.Uint16(buf[coff+2 : coff+4])) sizeOptHdr := int(binary.LittleEndian.Uint16(buf[coff+16 : coff+18])) optOff := coff + 20 if optOff+sizeOptHdr > len(buf) { return nil, fmt.Errorf("optional header truncated") } magic := binary.LittleEndian.Uint16(buf[optOff : optOff+2]) var is64 bool switch magic { case 0x10B: is64 = false case 0x20B: is64 = true default: return nil, fmt.Errorf("unknown optional header magic 0x%X", magic) } var sectionAlignOff, fileAlignOff, sizeOfImageOff, checksumOff, numRvaOff, dataDirOff int if is64 { sectionAlignOff = optOff + 32 fileAlignOff = optOff + 36 sizeOfImageOff = optOff + 56 checksumOff = optOff + 64 numRvaOff = optOff + 108 dataDirOff = optOff + 112 } else { sectionAlignOff = optOff + 32 fileAlignOff = optOff + 36 sizeOfImageOff = optOff + 56 checksumOff = optOff + 64 numRvaOff = optOff + 92 dataDirOff = optOff + 96 } dataDirCount := binary.LittleEndian.Uint32(buf[numRvaOff : numRvaOff+4]) return &peLayout{ data: buf, peOff: peOff, is64: is64, numSections: numSections, sizeOptHdr: sizeOptHdr, sectionAlign: binary.LittleEndian.Uint32(buf[sectionAlignOff : sectionAlignOff+4]), fileAlign: binary.LittleEndian.Uint32(buf[fileAlignOff : fileAlignOff+4]), sectionsStart: optOff + sizeOptHdr, sizeOfImage: binary.LittleEndian.Uint32(buf[sizeOfImageOff : sizeOfImageOff+4]), dataDirCount: dataDirCount, dataDirOff: dataDirOff, checksumOff: checksumOff, sizeOfImageOff: sizeOfImageOff, }, nil } type sectionHdr struct { name string virtualSize uint32 virtualAddress uint32 sizeOfRawData uint32 pointerToRawData uint32 characteristics uint32 headerOff int } func (p *peLayout) sections() []sectionHdr { out := make([]sectionHdr, p.numSections) for i := 0; i < p.numSections; i++ { off := p.sectionsStart + 40*i nameBytes := p.data[off : off+8] end := bytes.IndexByte(nameBytes, 0) if end < 0 { end = 8 } out[i] = sectionHdr{ name: string(nameBytes[:end]), virtualSize: binary.LittleEndian.Uint32(p.data[off+8 : off+12]), virtualAddress: binary.LittleEndian.Uint32(p.data[off+12 : off+16]), sizeOfRawData: binary.LittleEndian.Uint32(p.data[off+16 : off+20]), pointerToRawData: binary.LittleEndian.Uint32(p.data[off+20 : off+24]), characteristics: binary.LittleEndian.Uint32(p.data[off+36 : off+40]), headerOff: off, } } return out } func (p *peLayout) hasRsrc() bool { if p.dataDirCount > resourceTableSlot { off := p.dataDirOff + resourceTableSlot*8 va := binary.LittleEndian.Uint32(p.data[off : off+4]) size := binary.LittleEndian.Uint32(p.data[off+4 : off+8]) if va != 0 && size != 0 { return true } } for _, s := range p.sections() { if s.name == ".rsrc" { return true } } return false } func alignUp(v, a uint32) uint32 { if a == 0 { return v } return (v + a - 1) &^ (a - 1) } func buildResourceSection(entries []icoEntry, baseRVA uint32) ([]byte, error) { n := uint32(len(entries)) rootDirSize := uint32(16 + 2*8) rtIconDirSize := uint32(16 + n*8) perIconNameDirSize := uint32(16 + 8) rtGroupDirSize := uint32(16 + 8) groupNameDirSize := uint32(16 + 8) leafEntrySize := uint32(16) totalLeafEntries := n + 1 leavesSize := leafEntrySize * totalLeafEntries dirsSize := rootDirSize + rtIconDirSize + n*perIconNameDirSize + rtGroupDirSize + groupNameDirSize dirsAndLeaves := dirsSize + leavesSize groupDataSize := uint32(6 + 14*n) dataStartOff := dirsAndLeaves groupDataOff := dataStartOff groupDataPadded := alignUp(groupDataSize, 4) iconDataOffsets := make([]uint32, n) cursor := groupDataOff + groupDataPadded for i, e := range entries { iconDataOffsets[i] = cursor cursor += alignUp(uint32(len(e.data)), 4) } totalSize := cursor out := make([]byte, totalSize) leafOff := dirsSize groupLeafOff := leafOff iconLeavesStart := leafOff + leafEntrySize rootOff := uint32(0) rtGroupSubOff := rootDirSize rtIconSubOff := rtGroupSubOff + rtGroupDirSize groupNameSubOff := rtIconSubOff + rtIconDirSize perIconNamesStart := groupNameSubOff + groupNameDirSize writeDirHeader := func(off uint32, idEntries uint16) { binary.LittleEndian.PutUint32(out[off:off+4], 0) binary.LittleEndian.PutUint32(out[off+4:off+8], 0) binary.LittleEndian.PutUint16(out[off+8:off+10], 0) binary.LittleEndian.PutUint16(out[off+10:off+12], 0) binary.LittleEndian.PutUint16(out[off+12:off+14], 0) binary.LittleEndian.PutUint16(out[off+14:off+16], idEntries) } writeIDEntry := func(off uint32, id uint32, target uint32, isDir bool) { binary.LittleEndian.PutUint32(out[off:off+4], id) val := target if isDir { val |= 0x80000000 } binary.LittleEndian.PutUint32(out[off+4:off+8], val) } writeDirHeader(rootOff, 2) writeIDEntry(rootOff+16, rtIcon, rtIconSubOff, true) writeIDEntry(rootOff+24, rtGroupIcon, rtGroupSubOff, true) writeDirHeader(rtGroupSubOff, 1) writeIDEntry(rtGroupSubOff+16, 1, groupNameSubOff, true) writeDirHeader(groupNameSubOff, 1) writeIDEntry(groupNameSubOff+16, 0, groupLeafOff, false) writeDirHeader(rtIconSubOff, uint16(n)) for i := uint32(0); i < n; i++ { writeIDEntry(rtIconSubOff+16+i*8, i+1, perIconNamesStart+i*perIconNameDirSize, true) } for i := uint32(0); i < n; i++ { nameDirOff := perIconNamesStart + i*perIconNameDirSize writeDirHeader(nameDirOff, 1) leafForIcon := iconLeavesStart + i*leafEntrySize writeIDEntry(nameDirOff+16, 0, leafForIcon, false) } writeLeaf := func(off uint32, dataOff uint32, size uint32) { binary.LittleEndian.PutUint32(out[off:off+4], baseRVA+dataOff) binary.LittleEndian.PutUint32(out[off+4:off+8], size) binary.LittleEndian.PutUint32(out[off+8:off+12], 0) binary.LittleEndian.PutUint32(out[off+12:off+16], 0) } writeLeaf(groupLeafOff, groupDataOff, groupDataSize) for i := uint32(0); i < n; i++ { writeLeaf(iconLeavesStart+i*leafEntrySize, iconDataOffsets[i], uint32(len(entries[i].data))) } binary.LittleEndian.PutUint16(out[groupDataOff:groupDataOff+2], 0) binary.LittleEndian.PutUint16(out[groupDataOff+2:groupDataOff+4], 1) binary.LittleEndian.PutUint16(out[groupDataOff+4:groupDataOff+6], uint16(n)) for i, e := range entries { eo := groupDataOff + 6 + uint32(i)*14 out[eo] = e.width out[eo+1] = e.height out[eo+2] = e.colors out[eo+3] = e.reserved binary.LittleEndian.PutUint16(out[eo+4:eo+6], e.planes) binary.LittleEndian.PutUint16(out[eo+6:eo+8], e.bitCount) binary.LittleEndian.PutUint32(out[eo+8:eo+12], e.size) binary.LittleEndian.PutUint16(out[eo+12:eo+14], uint16(i+1)) } for i, e := range entries { copy(out[iconDataOffsets[i]:], e.data) } return out, nil } // SetExeIcon embebe el icono del archivo .ico en el .exe sobreescribiendo // el archivo. Funciona para PE32 y PE32+ que aun no tienen seccion .rsrc // (caso comun de binarios Go compilados sin icono). Si el .exe ya tiene // recursos retorna error. func SetExeIcon(exePath, icoPath string) error { icoBuf, err := os.ReadFile(icoPath) if err != nil { return fmt.Errorf("read ico: %w", err) } entries, err := parseICO(icoBuf) if err != nil { return fmt.Errorf("parse ico: %w", err) } exeBuf, err := os.ReadFile(exePath) if err != nil { return fmt.Errorf("read exe: %w", err) } pe, err := parsePE(exeBuf) if err != nil { return fmt.Errorf("parse pe: %w", err) } if pe.hasRsrc() { return fmt.Errorf("exe already has .rsrc resources; not supported") } if pe.dataDirCount <= resourceTableSlot { return fmt.Errorf("optional header DataDirectory has only %d entries (need >= %d)", pe.dataDirCount, resourceTableSlot+1) } sections := pe.sections() if len(sections) == 0 { return fmt.Errorf("exe has no sections") } last := sections[len(sections)-1] newSecHdrOff := pe.sectionsStart + 40*pe.numSections if newSecHdrOff+40 > int(last.pointerToRawData) { return fmt.Errorf("not enough space in PE headers for new section header") } newRVA := alignUp(last.virtualAddress+last.virtualSize, pe.sectionAlign) newRawOff := alignUp(last.pointerToRawData+last.sizeOfRawData, pe.fileAlign) rsrc, err := buildResourceSection(entries, newRVA) if err != nil { return fmt.Errorf("build resource section: %w", err) } rawSize := alignUp(uint32(len(rsrc)), pe.fileAlign) virtSize := uint32(len(rsrc)) out := make([]byte, 0, int(newRawOff)+int(rawSize)) out = append(out, exeBuf[:newRawOff]...) if int(newRawOff) > len(exeBuf) { out = append(out, make([]byte, int(newRawOff)-len(exeBuf))...) } out = append(out, rsrc...) if rawSize > uint32(len(rsrc)) { out = append(out, make([]byte, rawSize-uint32(len(rsrc)))...) } hdr := make([]byte, 40) copy(hdr[0:8], ".rsrc\x00\x00\x00") binary.LittleEndian.PutUint32(hdr[8:12], virtSize) binary.LittleEndian.PutUint32(hdr[12:16], newRVA) binary.LittleEndian.PutUint32(hdr[16:20], rawSize) binary.LittleEndian.PutUint32(hdr[20:24], newRawOff) binary.LittleEndian.PutUint32(hdr[36:40], scnCntInitializedData|scnMemRead) copy(out[newSecHdrOff:newSecHdrOff+40], hdr) binary.LittleEndian.PutUint16(out[pe.peOff+4+2:pe.peOff+4+4], uint16(pe.numSections+1)) rsrcEntryOff := pe.dataDirOff + resourceTableSlot*8 binary.LittleEndian.PutUint32(out[rsrcEntryOff:rsrcEntryOff+4], newRVA) binary.LittleEndian.PutUint32(out[rsrcEntryOff+4:rsrcEntryOff+8], virtSize) newSizeOfImage := alignUp(newRVA+virtSize, pe.sectionAlign) binary.LittleEndian.PutUint32(out[pe.sizeOfImageOff:pe.sizeOfImageOff+4], newSizeOfImage) binary.LittleEndian.PutUint32(out[pe.checksumOff:pe.checksumOff+4], 0) if err := os.WriteFile(exePath, out, 0o755); err != nil { return fmt.Errorf("write exe: %w", err) } return nil }