// Package cliprdr handler.go implements a cross-platform CLIPRDR // (Clipboard Virtual Channel Extension, MS-RDPECLIP) handler for // bidirectional text clipboard sharing between RDP client and server. // // Only text formats (CF_UNICODETEXT / CF_TEXT) are supported. package cliprdr import ( "bytes" "encoding/binary" "log/slog" "strings" "sync" "unicode/utf16" "git.zeroonesoft.cn/golib/rdplib/core" ) // CliprdrHandler implements plugin.ChannelTransport for the "cliprdr" // static virtual channel. It uses callbacks for clipboard integration // so that any UI toolkit can wire in its own clipboard access. type CliprdrHandler struct { channelSender core.ChannelSender useLongFormatNames bool // serverCapsReceived is set when the server's CB_CLIP_CAPS PDU has been // processed. Per MS-RDPECLIP §1.3.2.1 the server sends CB_CLIP_CAPS // before CB_MONITOR_READY, so FORMAT_LIST should only be sent once both // have arrived. serverCapsReceived bool // monitorReady is set when CB_MONITOR_READY has been received. monitorReady bool // onRemoteClipboardChanged is called with the text when the server's // clipboard content arrives. onRemoteClipboardChanged func(text string) // getLocalClipboardText is called to retrieve the current local // clipboard text when the server requests it. getLocalClipboardText func() string // onRemoteClipboardImage is called with PNG-encoded bytes when the // server's clipboard image (PNG / CF_DIB) arrives. onRemoteClipboardImage func(png []byte) // getLocalClipboardImage is called to retrieve the current local // clipboard image (PNG-encoded) when the server requests the "PNG" // registered format. Returns nil when no image is available. getLocalClipboardImage func() []byte // onRemoteClipboardHTML is called with the HTML content when the server's // clipboard "HTML Format" data arrives (fragment already extracted from // the CF_HTML envelope). onRemoteClipboardHTML func(html string) // getLocalClipboardHTML is called to retrieve the current local clipboard // HTML (raw fragment/document, without the CF_HTML envelope) when the // server requests the "HTML Format" registered format. // Returns "" when no HTML is available. getLocalClipboardHTML func() string // remoteHTMLFormatId 记录服务器 Format List 中 "HTML Format" 的格式 ID, // 用于识别 Format Data Response(服务端注册格式的 ID 由服务端分配)。 remoteHTMLFormatId uint32 // serverDropEffectId 记录服务器 Format List 中 "Preferred DropEffect" // 的服务器本地 ID(注册格式 ID 各端自定,按名称对齐)。 serverDropEffectId uint32 // lastRequestedFormat 记录我方最后一次 Format Data Request 的格式, // 以正确解读 Format Data Response(PNG 原样、DIB 转码、文本按 UTF-16)。 lastRequestedFormat uint32 // suppressNextLocalChange prevents an echo loop: // server→client clipboard update triggers a local clipboard change // event which would otherwise be sent back to the server. suppressNextLocalChange bool // --- File clipboard (CF_HDROP + FileContentsRequest/Response) ----------- // // Browser clipboards cannot hold files, so files are "staged" explicitly // by the UI (file picker / drag-drop): staged files are advertised as // CF_HDROP in Format List; the server fetches their bytes through // FileContentsRequest. Files copied on the server arrive as a CF_HDROP // path list, and the UI pulls bytes per file via RequestRemoteFile. mu sync.Mutex // serverFileClip is set when the server advertised CB_STREAM_FILECLIP_ENABLED. // CF_HDROP is only advertised/requested when both sides support file streaming. serverFileClip bool // localFiles are files staged by the local UI, advertised as CF_HDROP. localFiles []LocalFile // remoteFiles are the paths from the server's last file list payload. remoteFiles []string // remoteFileSizes are the sizes from the server's FileGroupDescriptorW // (-1 when unknown; only valid alongside remoteFiles from descriptors). remoteFileSizes []int64 // streams tracks in-flight outgoing FileContentsRequest transfers // (remote → local download) keyed by our StreamId. streams map[uint32]*fileStream // nextStreamID is the StreamId counter for outgoing FileContentsRequests. nextStreamID uint32 onRemoteFiles func(names []string) onFileData func(index int, name string, data []byte) onFileProgress func(index int, received, total int64) } // NewHandler creates a CliprdrHandler. // // - onRemote is called when the server clipboard text is received. // - getLocal is called to retrieve the current local clipboard text. // // Either callback may be nil. Image callbacks can be attached later via // SetImageCallbacks. func NewHandler(onRemote func(text string), getLocal func() string) *CliprdrHandler { return &CliprdrHandler{ onRemoteClipboardChanged: onRemote, getLocalClipboardText: getLocal, streams: map[uint32]*fileStream{}, } } // SetImageCallbacks attaches image clipboard callbacks (both optional). // // - onRemoteImage receives PNG-encoded bytes of the server clipboard image. // - getLocalImage returns PNG-encoded bytes of the local clipboard image // (nil when unavailable), used to answer server requests for "PNG". func (h *CliprdrHandler) SetImageCallbacks(onRemoteImage func(png []byte), getLocalImage func() []byte) { h.onRemoteClipboardImage = onRemoteImage h.getLocalClipboardImage = getLocalImage } // SetHTMLCallbacks attaches HTML clipboard callbacks (both optional). // // - onRemoteHTML receives the HTML content when the server's clipboard // "HTML Format" data arrives (fragment extracted from the CF_HTML envelope). // - getLocalHTML returns the local clipboard HTML (raw HTML, no CF_HTML // envelope; empty when unavailable), used to answer server requests for // the "HTML Format" registered format. func (h *CliprdrHandler) SetHTMLCallbacks(onRemoteHTML func(html string), getLocalHTML func() string) { h.onRemoteClipboardHTML = onRemoteHTML h.getLocalClipboardHTML = getLocalHTML } // --- plugin.ChannelTransport interface ------------------------------------ func (h *CliprdrHandler) GetType() (string, uint32) { return ChannelName, ChannelOption } func (h *CliprdrHandler) Sender(f core.ChannelSender) { h.channelSender = f } // Process handles a reassembled CLIPRDR PDU from the server. func (h *CliprdrHandler) Process(s []byte) { if len(s) < 8 { return } r := bytes.NewReader(s) msgType, _ := core.ReadUint16LE(r) msgFlags, _ := core.ReadUint16LE(r) dataLen, _ := core.ReadUInt32LE(r) body := make([]byte, dataLen) if dataLen > 0 { n, _ := r.Read(body) body = body[:n] } slog.Debug("cliprdr recv", "msgType", msgType, "msgFlags", msgFlags, "dataLen", dataLen) switch msgType { case CB_CLIP_CAPS: h.processClipCaps(body) case CB_MONITOR_READY: h.processMonitorReady() case CB_FORMAT_LIST: h.processFormatList(body, msgFlags) case CB_FORMAT_LIST_RESPONSE: h.processFormatListResponse(msgFlags) case CB_FORMAT_DATA_REQUEST: h.processFormatDataRequest(body) case CB_FORMAT_DATA_RESPONSE: h.processFormatDataResponse(body, msgFlags) case CB_FILECONTENTS_REQUEST: h.processFileContentsRequest(body) case CB_FILECONTENTS_RESPONSE: h.processFileContentsResponse(body, msgFlags) case CB_LOCK_CLIPDATA, CB_UNLOCK_CLIPDATA: // ignored default: slog.Debug("cliprdr: unhandled msgType", "msgType", msgType) } } // --- Clipboard Capabilities (MS-RDPECLIP 2.2.2.1) ------------------------- func (h *CliprdrHandler) processClipCaps(body []byte) { if len(body) < 4 { return } cCapSets := binary.LittleEndian.Uint16(body[0:2]) // pad1 at [2:4] offset := 4 for i := 0; i < int(cCapSets); i++ { if offset+4 > len(body) { break } capType := binary.LittleEndian.Uint16(body[offset:]) capLen := binary.LittleEndian.Uint16(body[offset+2:]) if capType == CB_CAPSTYPE_GENERAL && capLen >= 12 { generalFlags := binary.LittleEndian.Uint32(body[offset+8:]) h.useLongFormatNames = generalFlags&CB_USE_LONG_FORMAT_NAMES != 0 h.mu.Lock() h.serverFileClip = generalFlags&CB_STREAM_FILECLIP_ENABLED != 0 fileClip := h.serverFileClip h.mu.Unlock() slog.Debug("cliprdr: server caps", "generalFlags", generalFlags, "longNames", h.useLongFormatNames, "fileClip", fileClip) } offset += int(capLen) } h.serverCapsReceived = true // If CB_MONITOR_READY already arrived before CB_CLIP_CAPS (non-standard // ordering), send the FORMAT_LIST now that we have correct capabilities. if h.monitorReady { h.sendFormatList() } } func (h *CliprdrHandler) sendClipCaps() { b := &bytes.Buffer{} // General capability set: type(2) + length(2) + version(4) + flags(4) binary.Write(b, binary.LittleEndian, uint16(CB_CAPSTYPE_GENERAL)) binary.Write(b, binary.LittleEndian, uint16(12)) binary.Write(b, binary.LittleEndian, uint32(CB_CAPS_VERSION_2)) // CB_STREAM_FILECLIP_ENABLED + CB_FILECLIP_NO_FILE_PATHS:声明支持 // FileGroupDescriptorW/FileContents 流式文件传输(mstsc 同款), // 服务器未开时其会忽略这些位。 binary.Write(b, binary.LittleEndian, uint32(CB_USE_LONG_FORMAT_NAMES|CB_STREAM_FILECLIP_ENABLED|CB_FILECLIP_NO_FILE_PATHS)) // cCapabilitySets(2) + pad1(2) + capabilitySet body := &bytes.Buffer{} binary.Write(body, binary.LittleEndian, uint16(1)) binary.Write(body, binary.LittleEndian, uint16(0)) body.Write(b.Bytes()) h.sendPDU(CB_CLIP_CAPS, 0, body.Bytes()) } // --- Monitor Ready (MS-RDPECLIP 2.2.2.2) ---------------------------------- func (h *CliprdrHandler) processMonitorReady() { slog.Debug("cliprdr: server Monitor Ready") h.monitorReady = true h.sendClipCaps() // Per MS-RDPECLIP §1.3.2.1 the server sends CB_CLIP_CAPS before // CB_MONITOR_READY. Only send FORMAT_LIST after server caps are known // so useLongFormatNames is set correctly. If CB_CLIP_CAPS hasn't been // received yet (non-standard ordering), defer until processClipCaps fires. if h.serverCapsReceived { h.sendFormatList() } } // --- Format List (MS-RDPECLIP 2.2.3.1) ------------------------------------ func (h *CliprdrHandler) sendFormatList() { h.mu.Lock() staged := len(h.localFiles) > 0 fileClip := h.serverFileClip h.mu.Unlock() // 仅当双方都声明 CB_STREAM_FILECLIP_ENABLED 且本地已暂存文件时 // 才广告 CF_HDROP,避免服务器把普通粘贴当文件处理。 advertiseFiles := staged && fileClip b := &bytes.Buffer{} if h.useLongFormatNames { // Long Format Name: formatId(4) + wszFormatName(null-terminated UTF-16LE) writeFormat := func(id uint32, name string) { binary.Write(b, binary.LittleEndian, id) b.Write(encodeUTF16LE(name)) b.Write([]byte{0, 0}) // null terminator } writeFormat(CF_UNICODETEXT, "") // 空名 = 标准格式 writeFormat(CF_PNG, FormatNamePNG) // 注册格式 "PNG" writeFormat(CF_HTML_FORMAT_ID, FormatNameHTML) // 注册格式 "HTML Format" if advertiseFiles { // 文件传输三件套(mstsc 同款,名称必须一致): // 描述符列表 + 流式内容 + 复制语义 writeFormat(CF_FILE_GROUP_DESCRIPTORW, FormatNameFileGroupDescriptorW) writeFormat(CF_FILE_CONTENTS, FormatNameFileContents) writeFormat(CF_DROP_EFFECT, FormatNameDropEffect) } } else { // Short Format Name: formatId(4) + formatName[32] writeShort := func(id uint32, name string) { binary.Write(b, binary.LittleEndian, id) nameBuf := make([]byte, 32) copy(nameBuf, name) b.Write(nameBuf) } writeShort(CF_UNICODETEXT, "") writeShort(CF_PNG, FormatNamePNG) writeShort(CF_HTML_FORMAT_ID, FormatNameHTML) if advertiseFiles { writeShort(CF_FILE_GROUP_DESCRIPTORW, FormatNameFileGroupDescriptorW) writeShort(CF_FILE_CONTENTS, FormatNameFileContents) writeShort(CF_DROP_EFFECT, FormatNameDropEffect) } } h.sendPDU(CB_FORMAT_LIST, 0, b.Bytes()) } func (h *CliprdrHandler) processFormatList(body []byte, msgFlags uint16) { formats := h.parseFormatList(body, msgFlags) slog.Debug("cliprdr: server Format List", "formats", formats) // 记录服务器侧 "Preferred DropEffect" 的本地 ID(注册格式 ID 各端自定) for _, f := range formats { if strings.EqualFold(f.FormatName, FormatNameDropEffect) { h.mu.Lock() h.serverDropEffectId = f.FormatId h.mu.Unlock() } } // Always respond OK h.sendPDU(CB_FORMAT_LIST_RESPONSE, CB_RESPONSE_OK, nil) // 请求优先级:文件描述符(远端复制了文件)> CF_HDROP > PNG > CF_DIB > // HTML > 文本。资源管理器复制文件时通常还附带文件名文本,文件列表 // 必须最先识别。 for _, f := range formats { if strings.EqualFold(f.FormatName, FormatNameFileGroupDescriptorW) { h.mu.Lock() fileClip := h.serverFileClip h.mu.Unlock() if fileClip { slog.Debug("cliprdr: server offers files (FileGroupDescriptorW)") h.lastRequestedFormat = CF_FILE_GROUP_DESCRIPTORW h.sendFormatDataRequest(f.FormatId) return } } } for _, f := range formats { if f.FormatId == uint32(CF_HDROP) || strings.EqualFold(f.FormatName, FormatNameHDrop) { h.mu.Lock() fileClip := h.serverFileClip h.mu.Unlock() if fileClip { slog.Debug("cliprdr: server offers files (CF_HDROP)") h.lastRequestedFormat = CF_HDROP h.sendFormatDataRequest(CF_HDROP) return } } } for _, f := range formats { if f.FormatId == CF_PNG || strings.EqualFold(f.FormatName, FormatNamePNG) || strings.EqualFold(f.FormatName, "image/png") { h.lastRequestedFormat = f.FormatId h.sendFormatDataRequest(f.FormatId) return } } for _, f := range formats { if f.FormatId == CF_DIB { h.lastRequestedFormat = CF_DIB h.sendFormatDataRequest(CF_DIB) return } } // HTML Format:远端复制的富文本(浏览器/Office),保格式优于纯文本 for _, f := range formats { if f.FormatId == CF_HTML_FORMAT_ID || strings.EqualFold(f.FormatName, FormatNameHTML) { h.remoteHTMLFormatId = f.FormatId h.lastRequestedFormat = f.FormatId h.sendFormatDataRequest(f.FormatId) return } } for _, f := range formats { if f.FormatId == CF_UNICODETEXT { h.lastRequestedFormat = CF_UNICODETEXT h.sendFormatDataRequest(CF_UNICODETEXT) return } } for _, f := range formats { if f.FormatId == CF_TEXT { h.lastRequestedFormat = CF_TEXT h.sendFormatDataRequest(CF_TEXT) return } } } func (h *CliprdrHandler) parseFormatList(body []byte, msgFlags uint16) []CliprdrFormat { var formats []CliprdrFormat if h.useLongFormatNames && (msgFlags&CB_ASCII_NAMES == 0) { // Long Format Names (MS-RDPECLIP 2.2.3.1.1.1) offset := 0 for offset+4 <= len(body) { fmtId := binary.LittleEndian.Uint32(body[offset:]) offset += 4 // Read null-terminated UTF-16LE string nameEnd := offset for nameEnd+1 < len(body) { if body[nameEnd] == 0 && body[nameEnd+1] == 0 { break } nameEnd += 2 } name := decodeUTF16LE(body[offset:nameEnd]) offset = nameEnd + 2 formats = append(formats, CliprdrFormat{fmtId, name}) } } else { // Short Format Names (MS-RDPECLIP 2.2.3.1.1.2) offset := 0 for offset+36 <= len(body) { fmtId := binary.LittleEndian.Uint32(body[offset:]) nameBytes := body[offset+4 : offset+36] var name string if msgFlags&CB_ASCII_NAMES != 0 { name = strings.TrimRight(string(nameBytes), "\x00") } else { name = decodeUTF16LE(nameBytes) name = strings.TrimRight(name, "\x00") } formats = append(formats, CliprdrFormat{fmtId, name}) offset += 36 } } return formats } func (h *CliprdrHandler) processFormatListResponse(msgFlags uint16) { if msgFlags&CB_RESPONSE_OK != 0 { slog.Debug("cliprdr: Format List Response OK") } else { slog.Warn("cliprdr: Format List Response FAIL") } } // --- Format Data Request / Response (MS-RDPECLIP 2.2.5) -------------------- func (h *CliprdrHandler) sendFormatDataRequest(formatId uint32) { b := make([]byte, 4) binary.LittleEndian.PutUint32(b, formatId) h.sendPDU(CB_FORMAT_DATA_REQUEST, 0, b) slog.Debug("cliprdr: sent Format Data Request", "formatId", formatId) } func (h *CliprdrHandler) processFormatDataRequest(body []byte) { if len(body) < 4 { h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil) return } requestedFormat := binary.LittleEndian.Uint32(body[0:4]) slog.Debug("cliprdr: server requests format", "formatId", requestedFormat) switch requestedFormat { case CF_PNG: var png []byte if h.getLocalClipboardImage != nil { png = h.getLocalClipboardImage() } if len(png) == 0 { h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil) return } h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, png) case CF_HTML_FORMAT_ID: // 本地 HTML(无 CF_HTML 信封)→ 包装成 "HTML Format" 字节流 html := "" if h.getLocalClipboardHTML != nil { html = h.getLocalClipboardHTML() } if html == "" { h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil) return } h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, EncodeCFHTML(html)) case CF_FILE_GROUP_DESCRIPTORW: // 远端粘贴文件:回 FILE_GROUP_DESCRIPTORW(名称+大小+属性), // 字节内容由服务器随后以 CB_FILECONTENTS_REQUEST 按 lIndex 拉取 h.mu.Lock() files := h.localFiles h.mu.Unlock() if len(files) == 0 { h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil) return } h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, BuildFileGroupDescriptorW(files)) case CF_FILE_CONTENTS: // FileContents 的具体字节一律走 CB_FILECONTENTS_REQUEST(含流式 // 偏移/长度),对整格式 Format Data Request 只能回答不可流式的空体 h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil) case CF_DROP_EFFECT: // 有暂存文件时回答 DROPEFFECT_COPY(否则 FAIL)。 // 该查询决定资源管理器右键菜单"粘贴"是否可用。 h.mu.Lock() hasFiles := len(h.localFiles) > 0 h.mu.Unlock() if !hasFiles { h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil) return } h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, buildDropEffectReply()) case CF_UNICODETEXT: text := "" if h.getLocalClipboardText != nil { text = h.getLocalClipboardText() } h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, encodeUTF16LE(text+"\x00")) case CF_TEXT: text := "" if h.getLocalClipboardText != nil { text = h.getLocalClipboardText() } h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, []byte(text+"\x00")) default: // 注册格式 ID 各端自定:服务器会用它本地的 "Preferred DropEffect" // ID 查询(不一定先通告)。该查询决定资源管理器"粘贴"是否可用。 h.mu.Lock() serverDropEffect := h.serverDropEffectId hasFiles := len(h.localFiles) > 0 h.mu.Unlock() // DropEffect 应答:精确命中服务器 ID,或未知注册格式 ID(≥0xC000) // 且有暂存文件时按 DropEffect 猜测应答 if (serverDropEffect != 0 && requestedFormat == serverDropEffect) || (requestedFormat >= 0xC000 && hasFiles) { if !hasFiles { h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil) return } slog.Debug("cliprdr: replying DropEffect", "formatId", requestedFormat) h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_OK, buildDropEffectReply()) return } h.sendPDU(CB_FORMAT_DATA_RESPONSE, CB_RESPONSE_FAIL, nil) } } func (h *CliprdrHandler) processFormatDataResponse(body []byte, msgFlags uint16) { if msgFlags&CB_RESPONSE_OK == 0 { slog.Warn("cliprdr: Format Data Response FAIL") return } // 按我方请求的格式解读响应 switch { case h.lastRequestedFormat == CF_FILE_GROUP_DESCRIPTORW: names, sizes, err := ParseFileGroupDescriptorW(body) if err != nil { slog.Warn("cliprdr: FileGroupDescriptorW parse failed", "err", err, "len", len(body)) return } h.mu.Lock() h.remoteFiles = names h.remoteFileSizes = sizes h.mu.Unlock() if h.onRemoteFiles != nil { h.onRemoteFiles(names) } case h.lastRequestedFormat == CF_HDROP: names, err := ParseDropfiles(body) if err != nil { slog.Warn("cliprdr: CF_HDROP parse failed", "err", err, "len", len(body)) return } h.mu.Lock() h.remoteFiles = names h.mu.Unlock() if h.onRemoteFiles != nil { h.onRemoteFiles(names) } case h.lastRequestedFormat != 0 && h.lastRequestedFormat == h.remoteHTMLFormatId: // "HTML Format"(服务端注册格式的 ID 与请求时记录的一致) html := DecodeCFHTML(body) if html == "" { return } if h.onRemoteClipboardHTML != nil { h.suppressNextLocalChange = true h.onRemoteClipboardHTML(html) } case h.lastRequestedFormat == CF_PNG: if len(body) == 0 { return } if h.onRemoteClipboardImage != nil { h.suppressNextLocalChange = true png := make([]byte, len(body)) copy(png, body) h.onRemoteClipboardImage(png) } case h.lastRequestedFormat == CF_DIB || h.lastRequestedFormat == CF_DIBV5: png, err := DIBToPNG(body) if err != nil { slog.Warn("cliprdr: DIB→PNG conversion failed", "err", err) return } if h.onRemoteClipboardImage != nil { h.suppressNextLocalChange = true h.onRemoteClipboardImage(png) } default: // CF_UNICODETEXT / CF_TEXT → UTF-16LE 文本 text := decodeUTF16LE(body) text = strings.TrimRight(text, "\x00") if text != "" && h.onRemoteClipboardChanged != nil { slog.Debug("cliprdr: received text", "len", len(text)) h.suppressNextLocalChange = true h.onRemoteClipboardChanged(text) } } } // --- Public API for local clipboard changes -------------------------------- // OnLocalClipboardChanged notifies the server that the local clipboard // content has changed. Call this from the UI when the system clipboard // changes (e.g. via polling or a platform clipboard-change signal). func (h *CliprdrHandler) OnLocalClipboardChanged() { if h.suppressNextLocalChange { h.suppressNextLocalChange = false return } if h.channelSender != nil { h.sendFormatList() slog.Debug("cliprdr: local clipboard changed, sent Format List") } } // --- Send helpers ---------------------------------------------------------- func (h *CliprdrHandler) sendPDU(msgType, msgFlags uint16, body []byte) { if h.channelSender == nil { return } sendClipPDU(h.channelSender, msgType, msgFlags, body) } // --- UTF-16LE helpers ------------------------------------------------------ func decodeUTF16LE(b []byte) string { if len(b) < 2 { return "" } // Trim to even length if len(b)%2 != 0 { b = b[:len(b)-1] } u16 := make([]uint16, len(b)/2) for i := range u16 { u16[i] = binary.LittleEndian.Uint16(b[i*2:]) } return string(utf16.Decode(u16)) } func encodeUTF16LE(s string) []byte { runes := []rune(s) u16 := utf16.Encode(runes) b := make([]byte, len(u16)*2) for i, v := range u16 { binary.LittleEndian.PutUint16(b[i*2:], v) } return b }