272 lines
6.1 KiB
Go
272 lines
6.1 KiB
Go
//go:build windows
|
|
// +build windows
|
|
|
|
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"log"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"overlord-client/cmd/agent/wininterop"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
var (
|
|
feShell32 = windows.NewLazySystemDLL("shell32.dll")
|
|
feOle32 = windows.NewLazySystemDLL("ole32.dll")
|
|
pSHGetFileInfoW = feShell32.NewProc("SHGetFileInfoW")
|
|
pSHCreateItemFromParsingName = feShell32.NewProc("SHCreateItemFromParsingName")
|
|
pCoInitializeEx = feOle32.NewProc("CoInitializeEx")
|
|
pCoUninitialize = feOle32.NewProc("CoUninitialize")
|
|
)
|
|
|
|
const (
|
|
shgfiIcon = 0x000000100
|
|
shgfiSmallIcon = 0x000000001
|
|
shgfiLargeIcon = 0x000000000
|
|
shgfiUseFileAttributes = 0x000000010
|
|
fileAttributeNormal = 0x00000080
|
|
coinitApartmentThreaded = 0x2
|
|
coinitDisableOLE1DDE = 0x4
|
|
|
|
siigbfResizeToFit = 0x00
|
|
siigbfBiggerSizeOk = 0x01
|
|
siigbfMemoryOnly = 0x02
|
|
siigbfIconOnly = 0x04
|
|
siigbfThumbnailOnly = 0x08
|
|
siigbfInCacheOnly = 0x10
|
|
)
|
|
|
|
type shFileInfoW struct {
|
|
HIcon uintptr
|
|
IIcon int32
|
|
DwAttributes uint32
|
|
SzDisplayName [260]uint16
|
|
SzTypeName [80]uint16
|
|
}
|
|
|
|
var iidIShellItemImageFactory = windows.GUID{
|
|
Data1: 0xbcc18b79, Data2: 0xba16, Data3: 0x442f,
|
|
Data4: [8]byte{0x80, 0xc4, 0x8a, 0x59, 0xc3, 0x0c, 0x46, 0x3b},
|
|
}
|
|
|
|
type iShellItemImageFactoryVtbl struct {
|
|
QueryInterface uintptr
|
|
AddRef uintptr
|
|
Release uintptr
|
|
GetImage uintptr
|
|
}
|
|
|
|
type iShellItemImageFactory struct {
|
|
Vtbl *iShellItemImageFactoryVtbl
|
|
}
|
|
|
|
type comJob struct {
|
|
fn func()
|
|
done chan struct{}
|
|
}
|
|
|
|
var (
|
|
comStartOnce sync.Once
|
|
comQueue chan comJob
|
|
)
|
|
|
|
func runOnCOMThread(fn func()) {
|
|
comStartOnce.Do(func() {
|
|
comQueue = make(chan comJob, 256)
|
|
go func() {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
pCoInitializeEx.Call(0, uintptr(coinitApartmentThreaded|coinitDisableOLE1DDE))
|
|
defer pCoUninitialize.Call()
|
|
for job := range comQueue {
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Printf("com worker: recovered panic: %v", r)
|
|
}
|
|
}()
|
|
job.fn()
|
|
}()
|
|
close(job.done)
|
|
}
|
|
}()
|
|
})
|
|
done := make(chan struct{})
|
|
comQueue <- comJob{fn: fn, done: done}
|
|
<-done
|
|
}
|
|
|
|
func hBitmapToRGBA(hBitmap uintptr) *image.RGBA {
|
|
var bm bitmapObj
|
|
pGetObjectW.Call(hBitmap, unsafe.Sizeof(bm), uintptr(unsafe.Pointer(&bm)))
|
|
if bm.BmWidth <= 0 || bm.BmHeight <= 0 {
|
|
return nil
|
|
}
|
|
w := int(bm.BmWidth)
|
|
h := int(bm.BmHeight)
|
|
|
|
hdc, _, _ := pCreateCompatibleDC.Call(0)
|
|
if hdc == 0 {
|
|
return nil
|
|
}
|
|
defer pDeleteDC.Call(hdc)
|
|
|
|
bih := bitmapInfoHeader{
|
|
BiSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
|
|
BiWidth: int32(w),
|
|
BiHeight: -int32(h),
|
|
BiPlanes: 1,
|
|
BiBitCount: 32,
|
|
}
|
|
pixels := make([]byte, w*h*4)
|
|
pGetDIBits.Call(hdc, hBitmap, 0, uintptr(h),
|
|
uintptr(unsafe.Pointer(&pixels[0])),
|
|
uintptr(unsafe.Pointer(&bih)),
|
|
0,
|
|
)
|
|
|
|
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
off := (y*w + x) * 4
|
|
img.SetRGBA(x, y, color.RGBA{
|
|
R: pixels[off+2],
|
|
G: pixels[off+1],
|
|
B: pixels[off+0],
|
|
A: pixels[off+3],
|
|
})
|
|
}
|
|
}
|
|
return img
|
|
}
|
|
|
|
func extractFileIconPNG(targetPath, ext string) ([]byte, error) {
|
|
var queryPath string
|
|
flags := uint32(shgfiIcon | shgfiLargeIcon)
|
|
if targetPath != "" {
|
|
queryPath = targetPath
|
|
} else if ext != "" {
|
|
queryPath = "x." + strings.TrimPrefix(strings.ToLower(ext), ".")
|
|
flags |= shgfiUseFileAttributes
|
|
} else {
|
|
return nil, fmt.Errorf("path or ext required")
|
|
}
|
|
|
|
pathPtr, err := windows.UTF16PtrFromString(queryPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var out []byte
|
|
var outErr error
|
|
runOnCOMThread(func() {
|
|
var sfi shFileInfoW
|
|
ret, _, _ := pSHGetFileInfoW.Call(
|
|
uintptr(unsafe.Pointer(pathPtr)),
|
|
uintptr(fileAttributeNormal),
|
|
uintptr(unsafe.Pointer(&sfi)),
|
|
unsafe.Sizeof(sfi),
|
|
uintptr(flags),
|
|
)
|
|
if ret == 0 || sfi.HIcon == 0 {
|
|
outErr = fmt.Errorf("SHGetFileInfo failed")
|
|
return
|
|
}
|
|
defer pDestroyIcon.Call(sfi.HIcon)
|
|
|
|
var ii iconInfo
|
|
r, _, _ := pGetIconInfo.Call(sfi.HIcon, uintptr(unsafe.Pointer(&ii)))
|
|
if r == 0 {
|
|
outErr = fmt.Errorf("GetIconInfo failed")
|
|
return
|
|
}
|
|
if ii.HbmMask != 0 {
|
|
defer pDeleteObject.Call(uintptr(ii.HbmMask))
|
|
}
|
|
if ii.HbmColor == 0 {
|
|
outErr = fmt.Errorf("no color bitmap")
|
|
return
|
|
}
|
|
defer pDeleteObject.Call(uintptr(ii.HbmColor))
|
|
|
|
img := hBitmapToRGBA(uintptr(ii.HbmColor))
|
|
if img == nil {
|
|
outErr = fmt.Errorf("bitmap conversion failed")
|
|
return
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := png.Encode(&buf, img); err != nil {
|
|
outErr = err
|
|
return
|
|
}
|
|
out = buf.Bytes()
|
|
})
|
|
return out, outErr
|
|
}
|
|
|
|
func extractFileThumbnailJPEG(path string, edge int) ([]byte, int, int, error) {
|
|
if edge <= 0 {
|
|
edge = 128
|
|
}
|
|
pathPtr, err := windows.UTF16PtrFromString(path)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
var out []byte
|
|
var outW, outH int
|
|
var outErr error
|
|
runOnCOMThread(func() {
|
|
var itemPtr uintptr
|
|
hr, _, _ := pSHCreateItemFromParsingName.Call(
|
|
uintptr(unsafe.Pointer(pathPtr)),
|
|
0,
|
|
uintptr(unsafe.Pointer(&iidIShellItemImageFactory)),
|
|
uintptr(unsafe.Pointer(&itemPtr)),
|
|
)
|
|
if hr != 0 || itemPtr == 0 {
|
|
outErr = fmt.Errorf("SHCreateItemFromParsingName failed: 0x%x", hr)
|
|
return
|
|
}
|
|
factory := (*iShellItemImageFactory)(wininterop.Pointer(itemPtr))
|
|
defer callRelease(factory.Vtbl.Release, itemPtr)
|
|
|
|
flags := uint32(siigbfThumbnailOnly | siigbfBiggerSizeOk)
|
|
var hBitmap uintptr
|
|
hr2 := callGetImage(factory.Vtbl.GetImage, itemPtr, int32(edge), int32(edge), flags, &hBitmap)
|
|
if hr2 != 0 || hBitmap == 0 {
|
|
outErr = fmt.Errorf("GetImage failed: 0x%x", hr2)
|
|
return
|
|
}
|
|
defer pDeleteObject.Call(hBitmap)
|
|
|
|
img := hBitmapToRGBA(hBitmap)
|
|
if img == nil {
|
|
outErr = fmt.Errorf("bitmap conversion failed")
|
|
return
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 80}); err != nil {
|
|
outErr = err
|
|
return
|
|
}
|
|
out = buf.Bytes()
|
|
outW = img.Bounds().Dx()
|
|
outH = img.Bounds().Dy()
|
|
})
|
|
return out, outW, outH, outErr
|
|
}
|