init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user