init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib

This commit is contained in:
w11
2026-09-19 18:55:43 +08:00
commit 1096fbce9d
94 changed files with 36790 additions and 0 deletions
+527
View File
@@ -0,0 +1,527 @@
package cliprdr
import (
"bytes"
"encoding/binary"
"fmt"
"testing"
)
// capturedPDU 是 fakeSender 捕获的一条完整 CLIPRDR PDU。
type capturedPDU struct {
msgType uint16
flags uint16
body []byte
}
// fakeSender 捕获所有发出通道的数据,供断言。
type fakeSender struct{ pdus []capturedPDU }
func (f *fakeSender) SendToChannel(_ string, data []byte) (int, error) {
cp := make([]byte, len(data))
copy(cp, data)
if len(cp) < 8 {
return len(cp), nil
}
f.pdus = append(f.pdus, capturedPDU{
msgType: binary.LittleEndian.Uint16(cp[0:]),
flags: binary.LittleEndian.Uint16(cp[2:]),
body: cp[8:],
})
return len(cp), nil
}
func (f *fakeSender) last() capturedPDU { return f.pdus[len(f.pdus)-1] }
// newFileTestHandler 构造带 fakeSender 且 serverFileClip=true 的 handler
// (模拟服务器 caps 已声明 CB_STREAM_FILECLIP_ENABLED)。
func newFileTestHandler(t *testing.T) (*CliprdrHandler, *fakeSender) {
t.Helper()
h := NewHandler(nil, nil)
fs := &fakeSender{}
h.Sender(fs)
caps := &bytes.Buffer{}
binary.Write(caps, binary.LittleEndian, uint16(1)) // cCapSets
binary.Write(caps, binary.LittleEndian, uint16(0)) // pad
binary.Write(caps, binary.LittleEndian, uint16(CB_CAPSTYPE_GENERAL))
binary.Write(caps, binary.LittleEndian, uint16(12))
binary.Write(caps, binary.LittleEndian, uint32(CB_CAPS_VERSION_2))
binary.Write(caps, binary.LittleEndian, uint32(CB_USE_LONG_FORMAT_NAMES|CB_STREAM_FILECLIP_ENABLED))
h.processClipCaps(caps.Bytes())
if !h.serverFileClip {
t.Fatal("serverFileClip should be set after caps with CB_STREAM_FILECLIP_ENABLED")
}
return h, fs
}
func TestBuildParseDropfilesRoundTrip(t *testing.T) {
names := []string{`C:\Users\测试\报告 最终版.docx`, "plain.txt", "数据 - 副本 (2).xlsx"}
b := BuildDropfiles(names)
if binary.LittleEndian.Uint32(b[0:]) != dropfilesHeaderLen {
t.Fatalf("pFiles = %d, want %d", binary.LittleEndian.Uint32(b[0:]), dropfilesHeaderLen)
}
if binary.LittleEndian.Uint32(b[16:]) != 1 {
t.Fatal("fWide should be 1 (Unicode)")
}
got, err := ParseDropfiles(b)
if err != nil {
t.Fatal(err)
}
if len(got) != len(names) {
t.Fatalf("got %d names, want %d: %q", len(got), len(names), got)
}
for i := range names {
if got[i] != names[i] {
t.Fatalf("name[%d] = %q, want %q", i, got[i], names[i])
}
}
}
func TestParseDropfilesASCII(t *testing.T) {
// fWide=0 的 ASCII 变体(FreeRDP 老服务器可能使用)
list := []byte("a.txt\x00b.bin\x00\x00")
body := make([]byte, dropfilesHeaderLen+len(list))
binary.LittleEndian.PutUint32(body[0:], dropfilesHeaderLen)
binary.LittleEndian.PutUint32(body[16:], 0)
copy(body[dropfilesHeaderLen:], list)
got, err := ParseDropfiles(body)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 || got[0] != "a.txt" || got[1] != "b.bin" {
t.Fatalf("got %q", got)
}
}
func TestParseDropfilesRejectsBad(t *testing.T) {
if _, err := ParseDropfiles(make([]byte, 8)); err == nil {
t.Fatal("short body should fail")
}
bad := make([]byte, dropfilesHeaderLen)
binary.LittleEndian.PutUint32(bad[0:], 1<<20) // pFiles 越界
if _, err := ParseDropfiles(bad); err == nil {
t.Fatal("out-of-range pFiles should fail")
}
}
func serverFormatListWithHDrop(t *testing.T, h *CliprdrHandler) {
t.Helper()
// long-name Format List:CF_HDROP(id 15) + CF_UNICODETEXT
b := &bytes.Buffer{}
binary.Write(b, binary.LittleEndian, uint32(CF_HDROP))
b.Write(encodeUTF16LE("")) // 标准格式空名
b.Write([]byte{0, 0})
binary.Write(b, binary.LittleEndian, uint32(CF_UNICODETEXT))
b.Write(encodeUTF16LE(""))
b.Write([]byte{0, 0})
h.processFormatList(b.Bytes(), 0)
}
func TestServerFormatListTriggersHDropRequest(t *testing.T) {
h, fs := newFileTestHandler(t)
serverFormatListWithHDrop(t, h)
// 第一条:Format List Response OK;第二条:Format Data Request(CF_HDROP)
if len(fs.pdus) < 2 {
t.Fatalf("expected FORMAT_LIST_RESPONSE + FORMAT_DATA_REQUEST, got %d", len(fs.pdus))
}
req := fs.pdus[1]
if req.msgType != CB_FORMAT_DATA_REQUEST {
t.Fatalf("msgType=%#x, want FORMAT_DATA_REQUEST", req.msgType)
}
if id := binary.LittleEndian.Uint32(req.body); id != CF_HDROP {
t.Fatalf("requested format=%d, want %d", id, CF_HDROP)
}
if h.lastRequestedFormat != CF_HDROP {
t.Fatalf("lastRequestedFormat=%d", h.lastRequestedFormat)
}
}
func TestRemoteFilesReceivedViaFormatDataResponse(t *testing.T) {
h, _ := newFileTestHandler(t)
var gotNames []string
h.SetFileCallbacks(func(names []string) { gotNames = names }, nil, nil)
serverFormatListWithHDrop(t, h)
payload := BuildDropfiles([]string{`D:\share\a.pdf`, `D:\share\b.pdf`})
h.processFormatDataResponse(payload, CB_RESPONSE_OK)
if len(gotNames) != 2 || gotNames[0] != `D:\share\a.pdf` {
t.Fatalf("onRemoteFiles got %q", gotNames)
}
if names := h.RemoteFileNames(); len(names) != 2 {
t.Fatalf("RemoteFileNames = %q", names)
}
}
func TestFormatDataRequestServesStagedFiles(t *testing.T) {
h, fs := newFileTestHandler(t)
h.SetLocalFiles([]LocalFile{{Name: "x.bin", Data: []byte{1, 2, 3}}})
req := make([]byte, 4)
binary.LittleEndian.PutUint32(req, CF_FILE_GROUP_DESCRIPTORW)
h.processFormatDataRequest(req)
resp := fs.last()
if resp.msgType != CB_FORMAT_DATA_RESPONSE || resp.flags != CB_RESPONSE_OK {
t.Fatalf("msgType=%#x flags=%#x", resp.msgType, resp.flags)
}
names, sizes, err := ParseFileGroupDescriptorW(resp.body)
if err != nil || len(names) != 1 || names[0] != "x.bin" || sizes[0] != 3 {
t.Fatalf("FileGroupDescriptorW parse=%q %v err=%v", names, sizes, err)
}
}
func TestFormatDataRequestNoFilesFails(t *testing.T) {
h, fs := newFileTestHandler(t)
req := make([]byte, 4)
binary.LittleEndian.PutUint32(req, CF_FILE_GROUP_DESCRIPTORW)
h.processFormatDataRequest(req)
if fs.last().flags != CB_RESPONSE_FAIL {
t.Fatal("expected FAIL when no staged files")
}
}
// TestBuildParseFileGroupDescriptorWRoundTrip:编码→解码保持名称与大小。
func TestBuildParseFileGroupDescriptorWRoundTrip(t *testing.T) {
files := []LocalFile{
{Name: "报 告 最终版.docx", Data: make([]byte, 5)},
{Name: "小.txt", Data: []byte("hi")},
}
payload := BuildFileGroupDescriptorW(files)
if len(payload) != 4+2*fileDescriptorSize {
t.Fatalf("payload=%d bytes, want %d", len(payload), 4+2*fileDescriptorSize)
}
names, sizes, err := ParseFileGroupDescriptorW(payload)
if err != nil {
t.Fatal(err)
}
if len(names) != 2 {
t.Fatalf("names=%q", names)
}
if names[0] != "报 告 最终版.docx" || names[1] != "小.txt" {
t.Fatalf("names=%q", names)
}
if sizes[0] != 5 || sizes[1] != 2 {
t.Fatalf("sizes=%v", sizes)
}
}
// TestServerFileDescriptorListFlow:服务器通告 "FileGroupDescriptorW" 时
// 应以其 ID 请求格式数据,并用描述符填充远端文件列表(含大小)。
func TestServerFileDescriptorListFlow(t *testing.T) {
h, fs := newFileTestHandler(t)
var gotNames []string
h.SetFileCallbacks(func(names []string) { gotNames = names }, nil, nil)
// 服务器 Format List:FileGroupDescriptorW(id 0xC181) + FileContents + DropEffect
b := &bytes.Buffer{}
writeNamed := func(id uint32, name string) {
binary.Write(b, binary.LittleEndian, id)
b.Write(encodeUTF16LE(name))
b.Write([]byte{0, 0})
}
writeNamed(0xC181, FormatNameFileGroupDescriptorW)
writeNamed(0xC182, FormatNameFileContents)
writeNamed(0xC17E, FormatNameDropEffect)
h.processFormatList(b.Bytes(), 0)
req := fs.last()
if req.msgType != CB_FORMAT_DATA_REQUEST ||
binary.LittleEndian.Uint32(req.body) != 0xC181 {
t.Fatalf("expected FormatDataRequest(0xC181), got %#x id=%d",
req.msgType, binary.LittleEndian.Uint32(req.body))
}
payload := BuildFileGroupDescriptorW([]LocalFile{{Name: `C:\doc\a.pdf`, Data: make([]byte, 9)}})
h.processFormatDataResponse(payload, CB_RESPONSE_OK)
if len(gotNames) != 1 || gotNames[0] != `C:\doc\a.pdf` {
t.Fatalf("onRemoteFiles got %q", gotNames)
}
// 已知大小:RequestRemoteFile 应直接发 RANGE(0) 而非 SIZE
if err := h.RequestRemoteFile(0); err != nil {
t.Fatal(err)
}
fr := fs.last()
if binary.LittleEndian.Uint32(fr.body[8:]) != FILECONTENTS_RANGE ||
binary.LittleEndian.Uint32(fr.body[12:]) != 0 {
t.Fatalf("want direct RANGE(0), flags=%d pos=%d",
binary.LittleEndian.Uint32(fr.body[8:]), binary.LittleEndian.Uint32(fr.body[12:]))
}
}
// TestDropEffectRequest:资源管理器右键菜单查询 "Preferred DropEffect",
// 有暂存文件时必须回 OK + DROPEFFECT_COPY,否则"粘贴"被置灰。
func TestDropEffectRequest(t *testing.T) {
h, fs := newFileTestHandler(t)
req := make([]byte, 4)
binary.LittleEndian.PutUint32(req, CF_DROP_EFFECT)
h.processFormatDataRequest(req)
if fs.last().flags != CB_RESPONSE_FAIL {
t.Fatal("no staged files: expected FAIL for DropEffect")
}
h.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
h.processFormatDataRequest(req)
resp := fs.last()
if resp.msgType != CB_FORMAT_DATA_RESPONSE || resp.flags != CB_RESPONSE_OK {
t.Fatalf("msgType=%#x flags=%#x", resp.msgType, resp.flags)
}
if v := binary.LittleEndian.Uint32(resp.body); v != DROPEFFECT_COPY {
t.Fatalf("DropEffect=%d, want %d", v, DROPEFFECT_COPY)
}
}
func TestFormatListAdvertisesDropEffectWithFiles(t *testing.T) {
h, fs := newFileTestHandler(t)
h.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
body := fs.last().body
if !bytes.Contains(body, encodeUTF16LE(FormatNameDropEffect)) {
t.Fatal("staged Format List should advertise Preferred DropEffect")
}
}
func TestFileContentsRequestServesSizeAndRange(t *testing.T) {
h, fs := newFileTestHandler(t)
data := bytes.Repeat([]byte{0xA5}, 300*1024) // 跨两个 RANGE 段
h.SetLocalFiles([]LocalFile{{Name: "big.bin", Data: data}})
// SIZE 请求
szReq := make([]byte, 24)
binary.LittleEndian.PutUint32(szReq[0:], 7) // streamId
binary.LittleEndian.PutUint32(szReq[4:], 0) // lindex
binary.LittleEndian.PutUint32(szReq[8:], FILECONTENTS_SIZE)
binary.LittleEndian.PutUint32(szReq[20:], 8)
h.processFileContentsRequest(szReq)
resp := fs.last()
if resp.msgType != CB_FILECONTENTS_RESPONSE || resp.flags != CB_RESPONSE_OK {
t.Fatalf("SIZE: msgType=%#x flags=%#x", resp.msgType, resp.flags)
}
if sid := binary.LittleEndian.Uint32(resp.body); sid != 7 {
t.Fatalf("SIZE: streamId=%d", sid)
}
if size := binary.LittleEndian.Uint64(resp.body[4:]); size != uint64(len(data)) {
t.Fatalf("SIZE: got %d want %d", size, len(data))
}
// RANGE 请求(尾段裁剪到文件末尾)
rng := make([]byte, 24)
binary.LittleEndian.PutUint32(rng[0:], 7)
binary.LittleEndian.PutUint32(rng[8:], FILECONTENTS_RANGE)
binary.LittleEndian.PutUint32(rng[12:], 290*1024) // pos
binary.LittleEndian.PutUint32(rng[20:], 1<<20) // 请求远超末尾
h.processFileContentsRequest(rng)
resp = fs.last()
got := resp.body[4:]
if len(got) != 10*1024 {
t.Fatalf("RANGE: got %d bytes, want clamped %d", len(got), 10*1024)
}
if got[0] != 0xA5 {
t.Fatal("RANGE: wrong payload")
}
// 越界 lindex → FAIL 且带 streamId
bad := make([]byte, 24)
binary.LittleEndian.PutUint32(bad[0:], 9)
binary.LittleEndian.PutUint32(bad[4:], 5)
binary.LittleEndian.PutUint32(bad[8:], FILECONTENTS_SIZE)
h.processFileContentsRequest(bad)
resp = fs.last()
if resp.flags != CB_RESPONSE_FAIL || len(resp.body) != 4 ||
binary.LittleEndian.Uint32(resp.body) != 9 {
t.Fatalf("unknown lindex: flags=%#x body=% X", resp.flags, resp.body)
}
}
// TestRemoteFileTransferChain 走完 SIZE→RANGE→完成的完整拉取链,
// 并验证服务器裁剪返回段时按实际长度推进。
func TestRemoteFileTransferChain(t *testing.T) {
h, fs := newFileTestHandler(t)
var resultName string
var resultData []byte
var progGot, progTotal int64
h.SetFileCallbacks(func(names []string) {},
func(index int, name string, data []byte) { resultName, resultData = name, data },
func(index int, received, total int64) { progGot, progTotal = received, total })
serverFormatListWithHDrop(t, h)
payload := BuildDropfiles([]string{`E:\doc\说明 书.pdf`})
h.processFormatDataResponse(payload, CB_RESPONSE_OK)
file := bytes.Repeat([]byte{0x5A}, fileRangeChunk+1000)
if err := h.RequestRemoteFile(0); err != nil {
t.Fatal(err)
}
// 1) SIZE 请求
req := fs.last()
if req.msgType != CB_FILECONTENTS_REQUEST ||
binary.LittleEndian.Uint32(req.body[8:]) != FILECONTENTS_SIZE {
t.Fatalf("step1: msgType=%#x flags=%#x", req.msgType, binary.LittleEndian.Uint32(req.body[8:]))
}
sid := binary.LittleEndian.Uint32(req.body[0:])
// SIZE 响应 → 应发出 RANGE(0, fileRangeChunk)
h.processFileContentsResponse(append(u32le(sid), u64le(uint64(len(file)))...), CB_RESPONSE_OK)
req = fs.last()
if binary.LittleEndian.Uint32(req.body[8:]) != FILECONTENTS_RANGE ||
binary.LittleEndian.Uint32(req.body[12:]) != 0 ||
binary.LittleEndian.Uint32(req.body[20:]) != fileRangeChunk {
t.Fatalf("step2: want RANGE(0,%d), got pos=%d cb=%d", fileRangeChunk,
binary.LittleEndian.Uint32(req.body[12:]), binary.LittleEndian.Uint32(req.body[20:]))
}
// RANGE 响应(服务器只回一半)→ 应从实际到达位置续传
h.processFileContentsResponse(append(u32le(sid), file[:fileRangeChunk/2]...), CB_RESPONSE_OK)
req = fs.last()
if p := binary.LittleEndian.Uint32(req.body[12:]); p != fileRangeChunk/2 {
t.Fatalf("step3: want resume pos=%d, got %d", fileRangeChunk/2, p)
}
if progGot != fileRangeChunk/2 || progTotal != int64(len(file)) {
t.Fatalf("progress got=%d total=%d", progGot, progTotal)
}
// 剩余数据 → 完成回调
h.processFileContentsResponse(append(u32le(sid), file[fileRangeChunk/2:]...), CB_RESPONSE_OK)
if resultName != `E:\doc\说明 书.pdf` {
t.Fatalf("result name=%q", resultName)
}
if !bytes.Equal(resultData, file) {
t.Fatalf("result %d bytes, want %d", len(resultData), len(file))
}
}
func TestRemoteFileTransferFailure(t *testing.T) {
h, fs := newFileTestHandler(t)
var failData []byte
var failName string
h.SetFileCallbacks(func(names []string) {},
func(index int, name string, data []byte) { failName, failData = name, data },
nil)
serverFormatListWithHDrop(t, h)
h.processFormatDataResponse(BuildDropfiles([]string{"gone.txt"}), CB_RESPONSE_OK)
if err := h.RequestRemoteFile(0); err != nil {
t.Fatal(err)
}
sid := binary.LittleEndian.Uint32(fs.last().body[0:])
h.processFileContentsResponse(u32le(sid), CB_RESPONSE_FAIL)
if failName != "gone.txt" || failData != nil {
t.Fatalf("failure delivery: name=%q data=%v", failName, failData)
}
}
// TestServerLocalDropEffectId:Windows 用它本地的 "Preferred DropEffect"
// 注册格式 ID(实测 0xC17E)查询而非我方广告 ID,需按 DropEffect 应答。
func TestServerLocalDropEffectId(t *testing.T) {
h, fs := newFileTestHandler(t)
h.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
req := make([]byte, 4)
binary.LittleEndian.PutUint32(req, 0xC17E)
h.processFormatDataRequest(req)
resp := fs.last()
if resp.flags != CB_RESPONSE_OK || len(resp.body) != 4 ||
binary.LittleEndian.Uint32(resp.body) != DROPEFFECT_COPY {
t.Fatalf("heuristic DropEffect reply failed: flags=%#x body=% X", resp.flags, resp.body)
}
// 服务器通告过名称后,精确匹配其本地 ID
h2, fs2 := newFileTestHandler(t)
b := &bytes.Buffer{}
binary.Write(b, binary.LittleEndian, uint32(0xC17E))
b.Write(encodeUTF16LE(FormatNameDropEffect))
b.Write([]byte{0, 0})
h2.processFormatList(b.Bytes(), 0)
h2.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
h2.processFormatDataRequest(req)
if fs2.last().flags != CB_RESPONSE_OK {
t.Fatal("learned serverDropEffectId should be answered with OK")
}
// 无文件时 FAIL
h3, fs3 := newFileTestHandler(t)
b3 := &bytes.Buffer{}
binary.Write(b3, binary.LittleEndian, uint32(0xC17E))
b3.Write(encodeUTF16LE(FormatNameDropEffect))
b3.Write([]byte{0, 0})
h3.processFormatList(b3.Bytes(), 0)
h3.processFormatDataRequest(req)
if fs3.last().flags != CB_RESPONSE_FAIL {
t.Fatal("no staged files: server-local DropEffect should FAIL")
}
}
func TestRequestRemoteFileOutOfRange(t *testing.T) {
h, _ := newFileTestHandler(t)
if err := h.RequestRemoteFile(3); err == nil {
t.Fatal("expected error for out-of-range index")
}
}
func TestFormatListAdvertisesFileFormatsWhenStaged(t *testing.T) {
h, fs := newFileTestHandler(t) // long-name 模式
h.sendFormatList()
body := fs.last().body
if bytes.Contains(body, encodeUTF16LE(FormatNameFileGroupDescriptorW)) {
t.Fatal("file formats must not be advertised before staging files")
}
h.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
body = fs.last().body
if !bytes.Contains(body, encodeUTF16LE(FormatNameFileGroupDescriptorW)) ||
!bytes.Contains(body, encodeUTF16LE(FormatNameFileContents)) ||
!bytes.Contains(body, encodeUTF16LE(FormatNameDropEffect)) {
t.Fatal("staged Format List should advertise FileGroupDescriptorW/FileContents/DropEffect")
}
// short-name 模式(名称为原始 ASCII 字节)
h2, fs2 := newFileTestHandler(t)
h2.useLongFormatNames = false
h2.SetLocalFiles([]LocalFile{{Name: "a.txt", Data: []byte("hi")}})
body = fs2.last().body
if !bytes.Contains(body, []byte("FileGroupDescriptorW")) {
t.Fatalf("short-name Format List should carry FileGroupDescriptorW: % X", body[:min(80, len(body))])
}
}
func u64le(v uint64) []byte {
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, v)
return b
}
// TestParseFileGroupDescriptorWLargeDirectory:远端复制含数千文件的目录
// 是正常操作(曾因固定 1024 项上限整单被拒);超过防御性上限才拒绝。
func TestParseFileGroupDescriptorWLargeDirectory(t *testing.T) {
build := func(n uint32) []byte {
body := make([]byte, 4+int(n)*fileDescriptorSize)
binary.LittleEndian.PutUint32(body[0:], n)
for i := uint32(0); i < n; i++ {
d := body[4+int(i)*fileDescriptorSize:]
binary.LittleEndian.PutUint32(d[0:], FD_ATTRIBUTES|FD_FILESIZE|FD_PROGRESSUI)
binary.LittleEndian.PutUint32(d[68:], 7) // 大小低 32 位
copy(d[72:], asciiUTF16(fmt.Sprintf("f%d.txt", i)))
}
return body
}
// 5000 项:必须接受
names, _, err := ParseFileGroupDescriptorW(build(5000))
if err != nil || len(names) != 5000 {
t.Fatalf("5000 items: err=%v names=%d", err, len(names))
}
if names[4999] != "f4999.txt" {
t.Fatalf("last name=%q", names[4999])
}
// 超过 maxDescriptorItems:拒绝
if _, _, err := ParseFileGroupDescriptorW(build(maxDescriptorItems + 1)); err == nil {
t.Fatal("over-limit cItems should be rejected")
}
// cItems 声明与载荷不符:拒绝
short := build(3)[:4+2*fileDescriptorSize]
binary.LittleEndian.PutUint32(short[0:], 3)
if _, _, err := ParseFileGroupDescriptorW(short); err == nil {
t.Fatal("truncated payload should be rejected")
}
}
// asciiUTF16 把纯 ASCII 名编码为 UTF-16LE(NUL 结尾),供构造描述符用。
func asciiUTF16(s string) []byte {
b := make([]byte, 0, (len(s)+1)*2)
for _, c := range []byte(s) {
b = append(b, c, 0)
}
return append(b, 0, 0)
}