// cliprdr_image_test.go — 剪贴板图片链路的协议层单元测试(native 可跑) package cliprdr_test import ( "bytes" "encoding/binary" "image/png" "sync" "testing" "git.zeroonesoft.cn/golib/rdplib/core" "git.zeroonesoft.cn/golib/rdplib/plugin/cliprdr" ) // captureSender 捕获 handler 发出的 PDU,供断言 type captureSender struct { mu sync.Mutex pdus [][]byte } func (s *captureSender) GetType() (string, uint32) { return "cliprdr", 0 } func (s *captureSender) Sender(f core.ChannelSender) {} func (s *captureSender) Process(data []byte) {} func (s *captureSender) SendToChannel(name string, data []byte) (int, error) { s.mu.Lock() defer s.mu.Unlock() pdu := make([]byte, len(data)) copy(pdu, data) s.pdus = append(s.pdus, pdu) return len(data), nil } func (s *captureSender) findByType(msgType uint16) [][]byte { s.mu.Lock() defer s.mu.Unlock() var out [][]byte for _, p := range s.pdus { if len(p) >= 8 && binary.LittleEndian.Uint16(p[0:]) == msgType { out = append(out, p) } } return out } // fakeDIB 构造一张 2x2 的 32bpp BI_RGB 底行优先 DIB func fakeDIB() []byte { const w, h = 2, 2 dib := make([]byte, 40+w*4*h) binary.LittleEndian.PutUint32(dib[0:4], 40) // biSize binary.LittleEndian.PutUint32(dib[4:8], w) // width binary.LittleEndian.PutUint32(dib[8:12], h) // height(正 = 底行优先) binary.LittleEndian.PutUint16(dib[12:14], 1) // planes binary.LittleEndian.PutUint16(dib[14:16], 32) // bpp pix := dib[40:] // 视觉顶部 = DIB 最后一行:红、绿 off := (h - 1) * w * 4 binary.LittleEndian.PutUint32(pix[off:off+4], 0x00FF0000) // BGRA → 红 binary.LittleEndian.PutUint32(pix[off+4:off+8], 0x0000FF00) // 绿 // 视觉底部 = DIB 首行:蓝、白 binary.LittleEndian.PutUint32(pix[0:4], 0x000000FF) binary.LittleEndian.PutUint32(pix[4:8], 0x00FFFFFF) return dib } // capsPDU 构造 CB_CLIP_CAPS(声明长格式名),handler 需先收到它才会按长名解析 func capsPDU() []byte { body := make([]byte, 0, 20) body = binary.LittleEndian.AppendUint16(body, 1) // cCapSets body = binary.LittleEndian.AppendUint16(body, 0) // pad body = binary.LittleEndian.AppendUint16(body, cliprdr.CB_CAPSTYPE_GENERAL) body = binary.LittleEndian.AppendUint16(body, cliprdr.CB_CAPSTYPE_GENERAL_LEN) body = binary.LittleEndian.AppendUint32(body, cliprdr.CB_CAPS_VERSION_2) body = binary.LittleEndian.AppendUint32(body, cliprdr.CB_USE_LONG_FORMAT_NAMES) p := pduHeader(cliprdr.CB_CLIP_CAPS, 0, len(body)) return append(p, body...) } func TestDIBToPNG(t *testing.T) { pngBytes, err := cliprdr.DIBToPNG(fakeDIB()) if err != nil { t.Fatalf("DIBToPNG: %v", err) } img, err := png.Decode(bytes.NewReader(pngBytes)) if err != nil { t.Fatalf("decoded PNG invalid: %v", err) } if img.Bounds().Dx() != 2 || img.Bounds().Dy() != 2 { t.Fatalf("unexpected size %v", img.Bounds()) } r, g, b, _ := img.At(0, 0).RGBA() // RGBA() 分量为 16 位 if r != 0xFFFF || g != 0 || b != 0 { t.Fatalf("pixel(0,0) want red, got r=%x g=%x b=%x", r, g, b) } } func TestFormatDataResponseDIBConvertedToPNG(t *testing.T) { h := cliprdr.NewHandler(nil, nil) sender := &captureSender{} h.Sender(sender) var gotPNG []byte h.SetImageCallbacks(func(p []byte) { gotPNG = p }, nil) // 先声明能力(长格式名),再让服务端提供仅含 CF_DIB 的 FormatList h.Process(capsPDU()) h.Process(mkFormatListLong([]cliprdr.CliprdrFormat{{FormatId: cliprdr.CF_DIB}})) h.Process(responsePDU(cliprdr.CF_DIB, fakeDIB())) if len(gotPNG) == 0 { t.Fatal("expected PNG bytes via onRemoteClipboardImage") } if _, err := png.Decode(bytes.NewReader(gotPNG)); err != nil { t.Fatalf("converted PNG invalid: %v", err) } } func TestFormatListPNGTriggersRequest(t *testing.T) { h := cliprdr.NewHandler(nil, nil) sender := &captureSender{} h.Sender(sender) h.Process(capsPDU()) h.Process(mkFormatListLong([]cliprdr.CliprdrFormat{ {FormatId: 0xC3F0, FormatName: "PNG"}, {FormatId: cliprdr.CF_UNICODETEXT, FormatName: ""}, })) requests := sender.findByType(0x0004) // CB_FORMAT_DATA_REQUEST for _, p := range requests { id := binary.LittleEndian.Uint32(p[8:12]) if id == 0xC3F0 { // 应按服务端给出的 PNG 注册 ID 发起请求 return } } t.Fatal("expected PNG format data request for server-offered PNG format") } func TestFormatDataRequestPNGServedFromProvider(t *testing.T) { h := cliprdr.NewHandler(nil, nil) sender := &captureSender{} h.Sender(sender) h.SetImageCallbacks(nil, func() []byte { return []byte("FAKEPNG") }) h.Process(requestPDU(cliprdr.CF_PNG)) responses := sender.findByType(0x0005) // CB_FORMAT_DATA_RESPONSE for _, p := range responses { if binary.LittleEndian.Uint16(p[2:]) == 1 && bytes.Equal(p[8:15], []byte("FAKEPNG")) { return } } t.Fatalf("expected PNG data served from provider, got %d responses", len(responses)) } // --- helpers: 构造标准 CLIPRDR PDU --- func pduHeader(msgType, msgFlags uint16, bodyLen int) []byte { h := make([]byte, 8) binary.LittleEndian.PutUint16(h[0:], msgType) binary.LittleEndian.PutUint16(h[2:], msgFlags) binary.LittleEndian.PutUint32(h[4:], uint32(bodyLen)) return h } func requestPDU(formatId uint32) []byte { p := pduHeader(0x0004, 0, 4) return binary.LittleEndian.AppendUint32(p, formatId) } func responsePDU(formatId uint32, data []byte) []byte { _ = formatId // 响应 PDU 本身不含格式 ID,格式由我方请求记录 p := pduHeader(0x0005, 1, len(data)) return append(p, data...) } func mkFormatListLong(formats []cliprdr.CliprdrFormat) []byte { var body bytes.Buffer for _, f := range formats { binary.Write(&body, binary.LittleEndian, f.FormatId) body.Write(encodeUTF16(f.FormatName)) body.Write([]byte{0, 0}) } p := pduHeader(0x0002, 0, body.Len()) return append(p, body.Bytes()...) } func encodeUTF16(s string) []byte { u := []uint16{} for _, r := range s { u = append(u, uint16(r)) } out := make([]byte, len(u)*2) for i, v := range u { binary.LittleEndian.PutUint16(out[i*2:], v) } return out }