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
+234
View File
@@ -0,0 +1,234 @@
package rdpgfx
import (
"bytes"
"encoding/binary"
"testing"
)
// fakeStore 记录 Persist 调用并回放固定条目,用于验证持久缓存桥。
type fakeStore struct {
persisted []GfxCacheEntry
export []GfxCacheEntry
}
func (s *fakeStore) Persist(key uint64, w, h int, bpp uint16, data []byte) {
cp := make([]byte, len(data))
copy(cp, data)
s.persisted = append(s.persisted, GfxCacheEntry{Key: key, Width: w, Height: h, Bpp: bpp, Data: cp})
}
func (s *fakeStore) Export() []GfxCacheEntry { return s.export }
func (s *fakeStore) Get(key uint64) (GfxCacheEntry, bool) {
for _, e := range s.export {
if e.Key == key {
return e, true
}
}
return GfxCacheEntry{}, false
}
func (s *fakeStore) Keys() []uint64 {
out := make([]uint64, 0, len(s.export))
for _, e := range s.export {
out = append(out, e.Key)
}
return out
}
// newCacheTestHandler 返回带捕获 sendFn 的最小处理器(不经 NewGfxHandler,
// 避免拉起解码/写循环 goroutine)。
func newCacheTestHandler() (*GfxHandler, *[][]byte) {
sent := &[][]byte{}
g := &GfxHandler{
surfaces: make(map[uint16]*surface),
cacheEntries: make(map[uint16]cacheEntry),
sendFn: func(b []byte) {
cp := make([]byte, len(b))
copy(cp, b)
*sent = append(*sent, cp)
},
}
return g, sent
}
func TestSendCacheImportOffer(t *testing.T) {
g, sent := newCacheTestHandler()
st := &fakeStore{export: []GfxCacheEntry{
{Key: 0x1122334455667788, Width: 8, Height: 2, Data: bytes.Repeat([]byte{0xAB}, 8*2*4)},
{Key: 0x0102030405060708 >> 0, Width: 4, Height: 4, Data: bytes.Repeat([]byte{0xCD}, 4*4*4)},
}}
g.cacheStore = st
// 空上报守卫:未调用 Export 前直接发送应只发一次,重复调用被闩住
g.sendCacheImportOffer()
if len(*sent) != 1 {
t.Fatalf("期望 1 条 PDU,实得 %d", len(*sent))
}
g.sendCacheImportOffer()
if len(*sent) != 1 {
t.Fatalf("importOfferSent 闩失效:实得 %d 条", len(*sent))
}
pdu := (*sent)[0]
if got := binary.LittleEndian.Uint16(pdu[0:]); got != cmdidCacheImportOffer {
t.Fatalf("cmdId=0x%X,期望 0x%X", got, cmdidCacheImportOffer)
}
wantLen := 8 + 2 + 12*2
if got := binary.LittleEndian.Uint32(pdu[4:]); int(got) != wantLen {
t.Fatalf("pduLength=%d,期望 %d", got, wantLen)
}
if got := binary.LittleEndian.Uint16(pdu[8:]); got != 2 {
t.Fatalf("cacheEntriesCount=%d,期望 2", got)
}
// 条目 1:key u64 + bitmapLength u32
if got := binary.LittleEndian.Uint64(pdu[10:]); got != 0x1122334455667788 {
t.Fatalf("entry0 key=0x%X", got)
}
if got := binary.LittleEndian.Uint32(pdu[18:]); got != 8*2*4 {
t.Fatalf("entry0 bitmapLength=%d,期望 %d", got, 8*2*4)
}
if got := binary.LittleEndian.Uint64(pdu[22:]); got != 0x0102030405060708 {
t.Fatalf("entry1 key=0x%X", got)
}
if len(g.offeredCache) != 2 {
t.Fatalf("offeredCache 应保留 2 条待映射,实得 %d", len(g.offeredCache))
}
}
func TestSendCacheImportOfferEmpty(t *testing.T) {
g, sent := newCacheTestHandler()
g.cacheStore = &fakeStore{export: nil}
g.sendCacheImportOffer()
if len(*sent) != 0 {
t.Fatalf("空存储不应发送 PDU")
}
g.cacheStore = nil
g.importOfferSent = false
g.sendCacheImportOffer()
if len(*sent) != 0 {
t.Fatalf("无 store 不应发送 PDU")
}
}
func TestOnCacheImportReply(t *testing.T) {
g, _ := newCacheTestHandler()
e0 := GfxCacheEntry{Key: 0xA, Width: 4, Height: 2, Data: bytes.Repeat([]byte{1}, 4*2*4)}
e1 := GfxCacheEntry{Key: 0xB, Width: 2, Height: 2, Data: bytes.Repeat([]byte{2}, 2*2*4)}
g.offeredCache = []GfxCacheEntry{e0, e1}
// 前缀语义:前 2 条导入,槽位 7 与 9
data := make([]byte, 2, 2+4)
binary.LittleEndian.PutUint16(data, 2)
data = binary.LittleEndian.AppendUint16(data, 7)
data = binary.LittleEndian.AppendUint16(data, 9)
g.onCacheImportReply(data)
ce, ok := g.cacheEntries[7]
if !ok || ce.key != 0xA || ce.width != 4 || ce.height != 2 || !bytes.Equal(ce.data, e0.Data) {
t.Fatalf("槽位 7 条目不符: %+v", ce)
}
ce, ok = g.cacheEntries[9]
if !ok || ce.key != 0xB || !bytes.Equal(ce.data, e1.Data) {
t.Fatalf("槽位 9 条目不符: %+v", ce)
}
if g.offeredCache != nil {
t.Fatalf("Reply 后 offeredCache 应清空")
}
}
func TestOnCacheImportReplyClamp(t *testing.T) {
g, _ := newCacheTestHandler()
bad := GfxCacheEntry{Key: 0xC, Width: 4, Height: 2, Data: []byte{1, 2, 3}} // 长度不符
ok1 := GfxCacheEntry{Key: 0xD, Width: 2, Height: 2, Data: bytes.Repeat([]byte{3}, 2*2*4)}
g.offeredCache = []GfxCacheEntry{bad, ok1}
// n=5 超过上报数(截到 2);条目 0 长度不符必须被拒(防花屏)
data := make([]byte, 2, 2+10)
binary.LittleEndian.PutUint16(data, 5)
for _, s := range []uint16{3, 4} {
data = binary.LittleEndian.AppendUint16(data, s)
}
g.onCacheImportReply(data)
if _, hit := g.cacheEntries[3]; hit {
t.Fatalf("长度不符的条目不应入缓存")
}
if ce, hit := g.cacheEntries[4]; !hit || ce.key != 0xD {
t.Fatalf("槽位 4 应为有效条目")
}
// 槽位数组截断:只有 1 个完整槽位
g2, _ := newCacheTestHandler()
g2.offeredCache = []GfxCacheEntry{ok1, ok1}
short := []byte{2, 0, 6, 0} // n=2 但只有 1 个槽位
g2.onCacheImportReply(short)
if _, hit := g2.cacheEntries[6]; !hit {
t.Fatalf("截断时应导入完整部分")
}
if len(g2.cacheEntries) != 1 {
t.Fatalf("截断时不应导入缺失槽位,实得 %d 条", len(g2.cacheEntries))
}
}
func TestSurfaceToCachePersists(t *testing.T) {
g, _ := newCacheTestHandler()
st := &fakeStore{}
g.cacheStore = st
// 8×4 表面,每行像素值 = 行号(BGRA 同值)
sw, sh := 8, 4
sdata := make([]byte, sw*sh*4)
for row := range sh {
for col := 0; col < sw; col++ {
o := (row*sw + col) * 4
sdata[o], sdata[o+1], sdata[o+2], sdata[o+3] = byte(row), byte(row), byte(row), 0xFF
}
}
g.surfaces[1] = &surface{width: uint16(sw), height: uint16(sh), data: sdata}
key := uint64(0x1122334455667788)
p := make([]byte, 0, 20)
p = binary.LittleEndian.AppendUint16(p, 1) // surfId
p = binary.LittleEndian.AppendUint64(p, key) // cacheKey
p = binary.LittleEndian.AppendUint16(p, 3) // slot
p = binary.LittleEndian.AppendUint16(p, 2) // left
p = binary.LittleEndian.AppendUint16(p, 1) // top
p = binary.LittleEndian.AppendUint16(p, 6) // right
p = binary.LittleEndian.AppendUint16(p, 3) // bottom
g.onSurfaceToCache(p)
ce := g.cacheEntries[3]
if ce.width != 4 || ce.height != 2 {
t.Fatalf("缓存条目尺寸 %dx%d,期望 4x2", ce.width, ce.height)
}
if ce.key != key {
t.Fatalf("缓存条目 key=0x%X,期望 0x%X", ce.key, key)
}
// 第一行来自表面第 1 行(值为 1),第二行来自第 2 行(值为 2)
if ce.data[0] != 1 || ce.data[(4*1)*4] != 2 {
t.Fatalf("缓存像素内容不符: [0]=%d [row1]=%d", ce.data[0], ce.data[(4*1)*4])
}
if len(st.persisted) != 1 {
t.Fatalf("Persist 应被调用 1 次,实得 %d", len(st.persisted))
}
pv := st.persisted[0]
if pv.Key != key || pv.Width != 4 || pv.Height != 2 || !bytes.Equal(pv.Data, ce.data) {
t.Fatalf("持久化条目不符: %+v", pv)
}
// key=0 不持久化(视为无效键)
p0 := append([]byte(nil), p...)
binary.LittleEndian.PutUint64(p0[2:], 0)
g.onSurfaceToCache(p0)
if len(st.persisted) != 1 {
t.Fatalf("key=0 不应触发 Persist,实得 %d", len(st.persisted))
}
}
func TestMaxCacheImportEntries(t *testing.T) {
if maxCacheImportEntries >= 0x1556 {
t.Fatalf("maxCacheImportEntries=%d 必须小于规范上限 0x1556", maxCacheImportEntries)
}
}