Files
ts-mobile-go/go/_patches/github.com/honeybbq/teamspeak-go/transport/quicklz.go
T
2026-07-20 19:01:03 +08:00

194 lines
3.9 KiB
Go

package transport
import (
"encoding/binary"
"errors"
)
var (
errQlzDataTooShort = errors.New("data too short")
errQlzUnsupportedLevel = errors.New("only QuickLZ level 1 is supported")
errQlzDataTooShortForHeader = errors.New("data too short for header")
)
// TableSize is the QuickLZ level-1 hash table size.
const TableSize = 4096
type Qlz struct {
hashtable [TableSize]int
}
type qlzState struct {
control uint32
sourcePos int
destPos int
nextHashed int
}
func NewQlz() *Qlz {
return &Qlz{}
}
func getDecompressedSize(data []byte) int {
if (data[0] & 0x02) != 0 {
return int(binary.LittleEndian.Uint32(data[5:9]))
}
return int(data[2])
}
func (q *Qlz) Decompress(data []byte) ([]byte, error) {
headerLen, decompressedSize, flags, err := parseQlzHeader(data)
if err != nil {
return nil, err
}
dest := make([]byte, decompressedSize)
if (flags & 0x01) == 0 {
copy(dest, data[headerLen:headerLen+decompressedSize])
return dest, nil
}
for i := range q.hashtable {
q.hashtable[i] = 0
}
state := qlzState{
control: 1,
sourcePos: headerLen,
}
for q.ensureControl(data, &state) {
if (state.control & 1) != 0 {
if !q.processReference(data, dest, &state) {
break
}
} else {
if q.processLiteral(data, dest, decompressedSize, &state) {
break
}
}
}
return dest, nil
}
func parseQlzHeader(data []byte) (int, int, byte, error) {
if len(data) < 3 {
return 0, 0, 0, errQlzDataTooShort
}
flags := data[0]
level := (flags >> 2) & 0x03
if level != 1 {
return 0, 0, 0, errQlzUnsupportedLevel
}
headerLen := 3
if (flags & 0x02) != 0 {
headerLen = 9
}
if len(data) < headerLen {
return 0, 0, 0, errQlzDataTooShortForHeader
}
return headerLen, getDecompressedSize(data), flags, nil
}
func (q *Qlz) ensureControl(data []byte, st *qlzState) bool {
if st.control != 1 {
return true
}
if st.sourcePos+4 > len(data) {
return false
}
st.control = binary.LittleEndian.Uint32(data[st.sourcePos : st.sourcePos+4])
st.sourcePos += 4
return true
}
func (q *Qlz) processReference(data, dest []byte, st *qlzState) bool {
st.control >>= 1
if st.sourcePos+2 > len(data) {
return false
}
b1 := data[st.sourcePos]
b2 := data[st.sourcePos+1]
st.sourcePos += 2
hash := int(b1>>4) | (int(b2) << 4)
matchlen := int(b1 & 0x0F)
if matchlen != 0 {
matchlen += 2
} else {
if st.sourcePos >= len(data) {
return false
}
matchlen = int(data[st.sourcePos])
st.sourcePos++
}
offset := q.hashtable[hash]
for i := range matchlen {
if st.destPos < len(dest) && offset+i < st.destPos {
dest[st.destPos] = dest[offset+i]
st.destPos++
}
}
end := st.destPos + 1 - matchlen
q.updateHashtable(dest, &st.nextHashed, end)
st.nextHashed = st.destPos
return true
}
func (q *Qlz) processLiteral(data, dest []byte, decompressedSize int, st *qlzState) bool {
if st.destPos >= max(decompressedSize, 10)-10 {
for st.destPos < decompressedSize {
if st.control == 1 {
st.sourcePos += 4
if st.sourcePos > len(data) {
break
}
st.control = binary.LittleEndian.Uint32(data[st.sourcePos-4 : st.sourcePos])
}
if st.sourcePos >= len(data) {
break
}
dest[st.destPos] = data[st.sourcePos]
st.destPos++
st.sourcePos++
st.control >>= 1
}
return true
}
if st.sourcePos >= len(data) || st.destPos >= len(dest) {
return true
}
dest[st.destPos] = data[st.sourcePos]
st.destPos++
st.sourcePos++
st.control >>= 1
end := max(st.destPos-2, 0)
q.updateHashtable(dest, &st.nextHashed, end)
if st.nextHashed < end {
st.nextHashed = end
}
return false
}
func (q *Qlz) updateHashtable(dest []byte, nextHashed *int, end int) {
for *nextHashed < end {
if *nextHashed+3 > len(dest) {
break
}
v := uint32(dest[*nextHashed]) | (uint32(dest[*nextHashed+1]) << 8) | (uint32(dest[*nextHashed+2]) << 16)
hash := ((v >> 12) ^ v) & 0xFFF
q.hashtable[hash] = *nextHashed
*nextHashed++
}
}