Your IP : 216.73.216.224


Current Path : /home/hotlineuser/mobius/hotline/
Upload File :
Current File : //home/hotlineuser/mobius/hotline/field.go

package hotline

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"errors"
	"slices"
)

// FieldType represents a Hotline protocol field type identifier
type FieldType [2]byte

// List of Hotline protocol field types taken from the official 1.9 protocol document
var (
	FieldError               = FieldType{0x00, 0x64} // 100
	FieldData                = FieldType{0x00, 0x65} // 101
	FieldUserName            = FieldType{0x00, 0x66} // 102
	FieldUserID              = FieldType{0x00, 0x67} // 103
	FieldUserIconID          = FieldType{0x00, 0x68} // 104
	FieldUserLogin           = FieldType{0x00, 0x69} // 105
	FieldUserPassword        = FieldType{0x00, 0x6A} // 106
	FieldRefNum              = FieldType{0x00, 0x6B} // 107
	FieldTransferSize        = FieldType{0x00, 0x6C} // 108
	FieldChatOptions         = FieldType{0x00, 0x6D} // 109
	FieldUserAccess          = FieldType{0x00, 0x6E} // 110
	FieldUserFlags           = FieldType{0x00, 0x70} // 112
	FieldOptions             = FieldType{0x00, 0x71} // 113
	FieldChatID              = FieldType{0x00, 0x72} // 114
	FieldChatSubject         = FieldType{0x00, 0x73} // 115
	FieldWaitingCount        = FieldType{0x00, 0x74} // 116
	FieldBannerType          = FieldType{0x00, 0x98} // 152
	FieldNoServerAgreement   = FieldType{0x00, 0x98} // 152
	FieldVersion             = FieldType{0x00, 0xA0} // 160
	FieldCommunityBannerID   = FieldType{0x00, 0xA1} // 161
	FieldServerName          = FieldType{0x00, 0xA2} // 162
	FieldFileNameWithInfo    = FieldType{0x00, 0xC8} // 200
	FieldFileName            = FieldType{0x00, 0xC9} // 201
	FieldFilePath            = FieldType{0x00, 0xCA} // 202
	FieldFileResumeData      = FieldType{0x00, 0xCB} // 203
	FieldFileTransferOptions = FieldType{0x00, 0xCC} // 204
	FieldFileTypeString      = FieldType{0x00, 0xCD} // 205
	FieldFileCreatorString   = FieldType{0x00, 0xCE} // 206
	FieldFileSize            = FieldType{0x00, 0xCF} // 207
	FieldFileCreateDate      = FieldType{0x00, 0xD0} // 208
	FieldFileModifyDate      = FieldType{0x00, 0xD1} // 209
	FieldFileComment         = FieldType{0x00, 0xD2} // 210
	FieldFileNewName         = FieldType{0x00, 0xD3} // 211
	FieldFileNewPath         = FieldType{0x00, 0xD4} // 212
	FieldFileType            = FieldType{0x00, 0xD5} // 213
	FieldQuotingMsg          = FieldType{0x00, 0xD6} // 214
	FieldAutomaticResponse   = FieldType{0x00, 0xD7} // 215
	FieldFolderItemCount     = FieldType{0x00, 0xDC} // 220
	FieldUsernameWithInfo    = FieldType{0x01, 0x2C} // 300
	FieldNewsArtListData     = FieldType{0x01, 0x41} // 321
	FieldNewsCatName         = FieldType{0x01, 0x42} // 322
	FieldNewsCatListData15   = FieldType{0x01, 0x43} // 323
	FieldNewsPath            = FieldType{0x01, 0x45} // 325
	FieldNewsArtID           = FieldType{0x01, 0x46} // 326
	FieldNewsArtDataFlav     = FieldType{0x01, 0x47} // 327
	FieldNewsArtTitle        = FieldType{0x01, 0x48} // 328
	FieldNewsArtPoster       = FieldType{0x01, 0x49} // 329
	FieldNewsArtDate         = FieldType{0x01, 0x4A} // 330
	FieldNewsArtPrevArt      = FieldType{0x01, 0x4B} // 331
	FieldNewsArtNextArt      = FieldType{0x01, 0x4C} // 332
	FieldNewsArtData         = FieldType{0x01, 0x4D} // 333
	FieldNewsArtParentArt    = FieldType{0x01, 0x4F} // 335
	FieldNewsArt1stChildArt  = FieldType{0x01, 0x50} // 336
	FieldNewsArtRecurseDel   = FieldType{0x01, 0x51} // 337

	// These fields are documented, but seemingly unused.
	// FieldUserAlias           = FieldType{0x00, 0x6F} // 111
	// FieldNewsArtFlags        = FieldType{0x01, 0x4E} // 334
)

type Field struct {
	Type      FieldType // Type of field
	FieldSize [2]byte   // Size of the data field
	Data      []byte    // Field data

	readOffset int // Internal offset to track read progress
}

func NewField(fieldType FieldType, data []byte) Field {
	f := Field{
		Type: fieldType,
		Data: make([]byte, len(data)),
	}

	// Copy instead of assigning to avoid data race when the field is read in another go routine.
	copy(f.Data, data)

	binary.BigEndian.PutUint16(f.FieldSize[:], uint16(len(data)))
	return f
}

// FieldScanner implements bufio.SplitFunc for parsing byte slices into complete tokens
func FieldScanner(data []byte, _ bool) (advance int, token []byte, err error) {
	if len(data) < minFieldLen {
		return 0, nil, nil
	}

	// neededSize represents the length of bytes that are part of the field token.
	neededSize := minFieldLen + int(binary.BigEndian.Uint16(data[2:4]))
	if neededSize > len(data) {
		return 0, nil, nil
	}

	return neededSize, data[0:neededSize], nil
}

// DecodeInt decodes the field bytes to an int.
// The official Hotline clients will send uint32s as 2 bytes if possible, but
// some third party clients such as Frogblast and Heildrun will always send 4 bytes
func (f *Field) DecodeInt() (int, error) {
	switch len(f.Data) {
	case 2:
		return int(binary.BigEndian.Uint16(f.Data)), nil
	case 4:
		return int(binary.BigEndian.Uint32(f.Data)), nil
	}

	return 0, errors.New("unknown byte length")
}

func (f *Field) DecodeObfuscatedString() string {
	return string(EncodeString(f.Data))
}

// DecodeNewsPath decodes the field data to a news path.
// Example News Path data for a Category nested under two Bundles:
// 00000000  00 03 00 00 10 54 6f 70  20 4c 65 76 65 6c 20 42  |.....Top Level B|
// 00000010  75 6e 64 6c 65 00 00 13  53 65 63 6f 6e 64 20 4c  |undle...Second L|
// 00000020  65 76 65 6c 20 42 75 6e  64 6c 65 00 00 0f 4e 65  |evel Bundle...Ne|
// 00000030  73 74 65 64 20 43 61 74  65 67 6f 72 79           |sted Category|
func (f *Field) DecodeNewsPath() ([]string, error) {
	if len(f.Data) == 0 {
		return []string{}, nil
	}

	pathCount := binary.BigEndian.Uint16(f.Data[0:2])

	scanner := bufio.NewScanner(bytes.NewReader(f.Data[2:]))
	scanner.Split(newsPathScanner)

	var paths []string

	for i := uint16(0); i < pathCount; i++ {
		scanner.Scan()
		paths = append(paths, scanner.Text())
	}

	return paths, nil
}

// Read implements io.Reader for Field
func (f *Field) Read(p []byte) (int, error) {
	buf := slices.Concat(f.Type[:], f.FieldSize[:], f.Data)
	return readFrom(p, &f.readOffset, buf)
}

// Write implements io.Writer for Field
func (f *Field) Write(p []byte) (int, error) {
	if len(p) < minFieldLen {
		return 0, errors.New("input slice too short")
	}

	copy(f.Type[:], p[0:2])
	copy(f.FieldSize[:], p[2:4])

	dataSize := int(binary.BigEndian.Uint16(f.FieldSize[:]))
	if len(p) < minFieldLen+dataSize {
		return 0, errors.New("input slice too short for data size")
	}

	f.Data = make([]byte, dataSize)
	copy(f.Data, p[4:4+dataSize])

	return minFieldLen + dataSize, nil
}

func GetField(id FieldType, fields *[]Field) *Field {
	for _, field := range *fields {
		if id == field.Type {
			return &field
		}
	}
	return nil
}