UI *UI
- Inbox chan *Transaction
+ Inbox chan *Transaction
}
func NewClient(cfgPath string, logger *zap.SugaredLogger) *Client {
entry := selectedNode.GetReference().(*FileNameWithInfo)
- if bytes.Equal(entry.Type, []byte("fldr")) {
- c.Logger.Infow("get new directory listing", "name", string(entry.Name))
+ if bytes.Equal(entry.Type[:], []byte("fldr")) {
+ c.Logger.Infow("get new directory listing", "name", string(entry.name))
- c.filePath = append(c.filePath, string(entry.Name))
+ c.filePath = append(c.filePath, string(entry.name))
f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
}
} else {
// TODO: initiate file download
- c.Logger.Infow("download file", "name", string(entry.Name))
+ c.Logger.Infow("download file", "name", string(entry.name))
}
}
for _, f := range t.Fields {
var fn FileNameWithInfo
- _, _ = fn.Read(f.Data)
+ err = fn.UnmarshalBinary(f.Data)
+ if err != nil {
+ return nil, nil
+ }
- if bytes.Equal(fn.Type, []byte("fldr")) {
- node := tview.NewTreeNode(fmt.Sprintf("[blue::]📁 %s[-:-:-]", fn.Name))
+ if bytes.Equal(fn.Type[:], []byte("fldr")) {
+ node := tview.NewTreeNode(fmt.Sprintf("[blue::]📁 %s[-:-:-]", fn.name))
node.SetReference(&fn)
root.AddChild(node)
} else {
- size := binary.BigEndian.Uint32(fn.FileSize) / 1024
+ size := binary.BigEndian.Uint32(fn.FileSize[:]) / 1024
- node := tview.NewTreeNode(fmt.Sprintf(" %-40s %10v KB", fn.Name, size))
+ node := tview.NewTreeNode(fmt.Sprintf(" %-40s %10v KB", fn.name, size))
node.SetReference(&fn)
root.AddChild(node)
}
agreement := string(t.GetField(fieldData).Data)
agreement = strings.ReplaceAll(agreement, "\r", "\n")
- c.UI.agreeModal = tview.NewModal().
+ agreeModal := tview.NewModal().
SetText(agreement).
AddButtons([]string{"Agree", "Disagree"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
},
)
- c.Logger.Debug("show agreement page")
- c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
- c.UI.Pages.ShowPage("agreement ")
- c.UI.App.Draw()
+ c.UI.Pages.AddPage("agreement", agreeModal, false, true)
return res, err
}
return err
}
- if bytes.Compare(replyBuf, ServerHandshake) == 0 {
+ if bytes.Equal(replyBuf, ServerHandshake) {
return nil
}
var n int
var err error
- if n, err = c.Connection.Write(t.Payload()); err != nil {
+ b, err := t.MarshalBinary()
+ if err != nil {
+ return err
+ }
+ if n, err = c.Connection.Write(b); err != nil {
return err
}
c.Logger.Debugw("Sent Transaction",
ID *[]byte
Icon *[]byte
Flags *[]byte
- UserName *[]byte
+ UserName []byte
Account *Account
IdleTime *int
Server *Server
if field.ID == nil {
cc.Server.Logger.Infow(
"Missing required field",
- "Account", cc.Account.Login, "UserName", string(*cc.UserName), "RequestType", handler.Name, "FieldID", reqField.ID,
+ "Account", cc.Account.Login, "UserName", string(cc.UserName), "RequestType", handler.Name, "FieldID", reqField.ID,
)
return nil
}
if len(field.Data) < reqField.minLen {
cc.Server.Logger.Infow(
"Field does not meet minLen",
- "Account", cc.Account.Login, "UserName", string(*cc.UserName), "RequestType", handler.Name, "FieldID", reqField.ID,
+ "Account", cc.Account.Login, "UserName", string(cc.UserName), "RequestType", handler.Name, "FieldID", reqField.ID,
)
return nil
}
if !authorize(cc.Account.Access, handler.Access) {
cc.Server.Logger.Infow(
"Unauthorized Action",
- "Account", cc.Account.Login, "UserName", string(*cc.UserName), "RequestType", handler.Name,
+ "Account", cc.Account.Login, "UserName", string(cc.UserName), "RequestType", handler.Name,
)
cc.Server.outbox <- cc.NewErrReply(transaction, handler.DenyMsg)
cc.Server.Logger.Infow(
"Received Transaction",
"login", cc.Account.Login,
- "name", string(*cc.UserName),
+ "name", string(cc.UserName),
"RequestType", handler.Name,
)
} else {
cc.Server.Logger.Errorw(
"Unimplemented transaction type received",
- "UserName", string(*cc.UserName), "RequestID", requestNum,
+ "UserName", string(cc.UserName), "RequestID", requestNum,
)
}
tranNotifyChangeUser,
NewField(fieldUserID, *cc.ID),
NewField(fieldUserFlags, *cc.Flags),
- NewField(fieldUserName, *cc.UserName),
+ NewField(fieldUserName, cc.UserName),
NewField(fieldUserIconID, *cc.Icon),
)
ID *[]byte
Icon *[]byte
Flags *[]byte
- UserName *[]byte
+ UserName []byte
Account *Account
IdleTime *int
Server *Server
return fh
}
+func (fh *FileHeader) Read(p []byte) (n int, err error) {
+ p = concat.Slices(
+ fh.Size,
+ fh.Type,
+ fh.FilePath,
+ )
+ return len(p), nil
+}
+
func (fh *FileHeader) Payload() []byte {
return concat.Slices(
fh.Size,
package hotline
import (
+ "bytes"
"encoding/binary"
- "github.com/jhalter/mobius/concat"
)
-// FileNameWithInfo field content is presented in this structure:
-// Type 4 Folder (‘fldr’) or other
-// Creator 4
-// File size 4
-// 4 Reserved?
-// Name script 2
-// Name size 2
-// Name data size
type FileNameWithInfo struct {
- Type []byte // file type code
- Creator []byte // File creator code
- FileSize []byte // File Size in bytes
- RSVD []byte
- NameScript []byte // TODO: What is this?
- NameSize []byte // Length of name field
- Name []byte // File name
+ fileNameWithInfoHeader
+ name []byte // File name
}
-func (f FileNameWithInfo) Payload() []byte {
- name := f.Name
- nameSize := make([]byte, 2)
- binary.BigEndian.PutUint16(nameSize, uint16(len(name)))
-
- return concat.Slices(
- f.Type,
- f.Creator,
- f.FileSize,
- []byte{0, 0, 0, 0},
- f.NameScript,
- nameSize,
- f.Name,
- )
+// fileNameWithInfoHeader contains the fixed length fields of FileNameWithInfo
+type fileNameWithInfoHeader struct {
+ Type [4]byte // file type code
+ Creator [4]byte // File creator code
+ FileSize [4]byte // File Size in bytes
+ RSVD [4]byte
+ NameScript [2]byte // ??
+ NameSize [2]byte // Length of name field
}
-func (f *FileNameWithInfo) Read(p []byte) (n int, err error) {
- // TODO: check p for expected len
- f.Type = p[0:4]
- f.Creator = p[4:8]
- f.FileSize = p[8:12]
- f.RSVD = p[12:16]
- f.NameScript = p[16:18]
- f.NameSize = p[18:20]
- f.Name = p[20:]
-
- return len(p), err
+func (f *fileNameWithInfoHeader) nameLen() int {
+ return int(binary.BigEndian.Uint16(f.NameSize[:]))
}
+
+func (f *FileNameWithInfo) MarshalBinary() (data []byte, err error) {
+ var buf bytes.Buffer
+ err = binary.Write(&buf, binary.LittleEndian, f.fileNameWithInfoHeader)
+ if err != nil {
+ return data, err
+ }
+
+ _, err = buf.Write(f.name)
+ if err != nil {
+ return data, err
+ }
+
+ return buf.Bytes(), err
+}
+
+func (f *FileNameWithInfo) UnmarshalBinary(data []byte) error {
+ err := binary.Read(bytes.NewReader(data), binary.BigEndian, &f.fileNameWithInfoHeader)
+ if err != nil {
+ return err
+ }
+ headerLen := binary.Size(f.fileNameWithInfoHeader)
+ f.name = data[headerLen : headerLen+f.nameLen()]
+
+ return err
+}
+
import (
"github.com/stretchr/testify/assert"
+ "reflect"
"testing"
)
-func TestFileNameWithInfo_Read(t *testing.T) {
+func TestFileNameWithInfo_MarshalBinary(t *testing.T) {
type fields struct {
- Type []byte
- Creator []byte
- FileSize []byte
- NameScript []byte
- NameSize []byte
- Name []byte
+ fileNameWithInfoHeader fileNameWithInfoHeader
+ name []byte
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantData []byte
+ wantErr bool
+ }{
+ {
+ name: "returns expected bytes",
+ fields: fields{
+ fileNameWithInfoHeader: fileNameWithInfoHeader{
+ Type: [4]byte{0x54, 0x45, 0x58, 0x54}, // TEXT
+ Creator: [4]byte{0x54, 0x54, 0x58, 0x54}, // TTXT
+ FileSize: [4]byte{0x00, 0x43, 0x16, 0xd3}, // File Size
+ RSVD: [4]byte{0, 0, 0, 0},
+ NameScript: [2]byte{0, 0},
+ NameSize: [2]byte{0x00, 0x03},
+ },
+ name: []byte("foo"),
+ },
+ wantData: []byte{
+ 0x54, 0x45, 0x58, 0x54,
+ 0x54, 0x54, 0x58, 0x54,
+ 0x00, 0x43, 0x16, 0xd3,
+ 0, 0, 0, 0,
+ 0, 0,
+ 0x00, 0x03,
+ 0x66, 0x6f, 0x6f,
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ f := &FileNameWithInfo{
+ fileNameWithInfoHeader: tt.fields.fileNameWithInfoHeader,
+ name: tt.fields.name,
+ }
+ gotData, err := f.MarshalBinary()
+ if (err != nil) != tt.wantErr {
+ t.Errorf("MarshalBinary() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(gotData, tt.wantData) {
+ t.Errorf("MarshalBinary() gotData = %v, want %v", gotData, tt.wantData)
+ }
+ })
+ }
+}
+
+func TestFileNameWithInfo_UnmarshalBinary(t *testing.T) {
+ type fields struct {
+ fileNameWithInfoHeader fileNameWithInfoHeader
+ name []byte
}
type args struct {
- p []byte
+ data []byte
}
tests := []struct {
name string
fields fields
args args
- want *FileNameWithInfo
- wantN int
+ want *FileNameWithInfo
wantErr bool
}{
{
- name: "reads bytes into struct",
- fields: fields{},
+ name: "writes bytes into struct",
args: args{
- p: []byte{
+ data: []byte{
0x54, 0x45, 0x58, 0x54, // TEXT
0x54, 0x54, 0x58, 0x54, // TTXT
0x00, 0x43, 0x16, 0xd3, // File Size
},
},
want: &FileNameWithInfo{
- Type: []byte("TEXT"),
- Creator: []byte("TTXT"),
- FileSize: []byte{0x00, 0x43, 0x16, 0xd3},
- RSVD: []byte{0, 0, 0, 0},
- NameScript: []byte{0, 0},
- NameSize: []byte{0x00, 0x0e},
- Name: []byte("Audion.app.zip"),
+ fileNameWithInfoHeader: fileNameWithInfoHeader{
+ Type: [4]byte{0x54, 0x45, 0x58, 0x54}, // TEXT
+ Creator: [4]byte{0x54, 0x54, 0x58, 0x54}, // TTXT
+ FileSize: [4]byte{0x00, 0x43, 0x16, 0xd3}, // File Size
+ RSVD: [4]byte{0, 0, 0, 0},
+ NameScript: [2]byte{0, 0},
+ NameSize: [2]byte{0x00, 0x0e},
+ },
+ name: []byte("Audion.app.zip"),
},
- wantN: 34,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &FileNameWithInfo{
- Type: tt.fields.Type,
- Creator: tt.fields.Creator,
- FileSize: tt.fields.FileSize,
- NameScript: tt.fields.NameScript,
- NameSize: tt.fields.NameSize,
- Name: tt.fields.Name,
+ fileNameWithInfoHeader: tt.fields.fileNameWithInfoHeader,
+ name: tt.fields.name,
}
- gotN, err := f.Read(tt.args.p)
- if (err != nil) != tt.wantErr {
- t.Errorf("Read() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if gotN != tt.wantN {
- t.Errorf("Read() gotN = %v, want %v", gotN, tt.wantN)
+ if err := f.UnmarshalBinary(tt.args.data); (err != nil) != tt.wantErr {
+ t.Errorf("UnmarshalBinary() error = %v, wantErr %v", err, tt.wantErr)
}
if !assert.Equal(t, tt.want, f) {
t.Errorf("Read() got = %v, want %v", f, tt.want)
-
}
})
}
-}
+}
\ No newline at end of file
}
type FilePath struct {
- PathItemCount []byte
- PathItems []FilePathItem
+ ItemCount []byte
+ Items []FilePathItem
}
-func NewFilePath(b []byte) FilePath {
- if b == nil {
- return FilePath{}
- }
-
- fp := FilePath{PathItemCount: b[0:2]}
+func (fp *FilePath) UnmarshalBinary(b []byte) error {
+ fp.ItemCount = b[0:2]
- // number of items in the path
- pathItemLen := binary.BigEndian.Uint16(b[0:2])
pathData := b[2:]
- for i := uint16(0); i < pathItemLen; i++ {
+ for i := uint16(0); i < fp.Len(); i++ {
segLen := pathData[2]
- fp.PathItems = append(fp.PathItems, NewFilePathItem(pathData[:segLen+3]))
+ fp.Items = append(fp.Items, NewFilePathItem(pathData[:segLen+3]))
pathData = pathData[3+segLen:]
}
- return fp
+ return nil
+}
+
+func (fp *FilePath) Len() uint16 {
+ return binary.BigEndian.Uint16(fp.ItemCount)
}
func (fp *FilePath) String() string {
var out []string
- for _, i := range fp.PathItems {
+ for _, i := range fp.Items {
out = append(out, string(i.Name))
}
return strings.Join(out, pathSeparator)
--- /dev/null
+package hotline
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestFilePath_UnmarshalBinary(t *testing.T) {
+ type fields struct {
+ ItemCount []byte
+ Items []FilePathItem
+ }
+ type args struct {
+ b []byte
+ }
+ tests := []struct {
+ name string
+ args args
+ want FilePath
+ wantErr bool
+ }{
+ {
+ name: "unmarshals bytes into struct",
+ args: args{b: []byte{
+ 0x00, 0x02,
+ 0x00, 0x00,
+ 0x0f,
+ 0x46, 0x69, 0x72, 0x73, 0x74, 0x20, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x20, 0x44, 0x69, 0x72,
+ 0x00, 0x00,
+ 0x08,
+ 0x41, 0x20, 0x53, 0x75, 0x62, 0x44, 0x69, 0x72,
+ }},
+ want: FilePath{
+ ItemCount: []byte{0x00, 0x02},
+ Items: []FilePathItem{
+ {
+ Len: 0x0f,
+ Name: []byte("First Level Dir"),
+ },
+ {
+ Len: 0x08,
+ Name: []byte("A SubDir"),
+ },
+ },
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var fp FilePath
+ if err := fp.UnmarshalBinary(tt.args.b); (err != nil) != tt.wantErr {
+ t.Errorf("UnmarshalBinary() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ if !assert.Equal(t, tt.want, fp) {
+ t.Errorf("Read() got = %v, want %v", fp, tt.want)
+ }
+ })
+ }
+}
for _, file := range files {
var fileType []byte
+ var fnwi FileNameWithInfo
fileCreator := make([]byte, 4)
- fileSize := make([]byte, 4)
+ //fileSize := make([]byte, 4)
if !file.IsDir() {
fileType = []byte(fileTypeFromFilename(file.Name()))
fileCreator = []byte(fileCreatorFromFilename(file.Name()))
- binary.BigEndian.PutUint32(fileSize, uint32(file.Size()))
+ binary.BigEndian.PutUint32(fnwi.FileSize[:], uint32(file.Size()))
+ copy(fnwi.Type[:], fileType[:])
+ copy(fnwi.Creator[:], fileCreator[:])
} else {
fileType = []byte("fldr")
if err != nil {
return fields, err
}
- binary.BigEndian.PutUint32(fileSize, uint32(len(dir)))
+ binary.BigEndian.PutUint32(fnwi.FileSize[:], uint32(len(dir)))
+ copy(fnwi.Type[:], fileType[:])
+ copy(fnwi.Creator[:], fileCreator[:])
}
- fields = append(fields, NewField(
- fieldFileNameWithInfo,
- FileNameWithInfo{
- Type: fileType,
- Creator: fileCreator,
- FileSize: fileSize,
- NameScript: []byte{0, 0},
- Name: []byte(file.Name()),
- }.Payload(),
- ))
+ nameSize := make([]byte, 2)
+ binary.BigEndian.PutUint16(nameSize, uint16(len(file.Name())))
+ copy(fnwi.NameSize[:], nameSize[:])
+
+ fnwi.name = []byte(file.Name())
+
+ b, err := fnwi.MarshalBinary()
+ if err != nil {
+ return nil, err
+ }
+ fields = append(fields, NewField(fieldFileNameWithInfo, b))
}
return fields, nil
}
func ReadFilePath(filePathFieldData []byte) string {
- fp := NewFilePath(filePathFieldData)
+ var fp FilePath
+ err := fp.UnmarshalBinary(filePathFieldData)
+ if err != nil {
+ // TODO
+ }
return fp.String()
}
// FlatFileHeader is the first section of a "Flattened File Object". All fields have static values.
type FlatFileHeader struct {
- Format []byte // Always "FILP"
- Version []byte // Always 1
- RSVD []byte // Always empty zeros
- ForkCount []byte // Always 2
+ Format [4]byte // Always "FILP"
+ Version [2]byte // Always 1
+ RSVD [16]byte // Always empty zeros
+ ForkCount [2]byte // Always 2
}
// NewFlatFileHeader returns a FlatFileHeader struct
func NewFlatFileHeader() FlatFileHeader {
return FlatFileHeader{
- Format: []byte("FILP"),
- Version: []byte{0, 1},
- RSVD: make([]byte, 16),
- ForkCount: []byte{0, 2},
+ Format: [4]byte{0x46, 0x49, 0x4c, 0x50}, // FILP
+ Version: [2]byte{0, 1},
+ RSVD: [16]byte{},
+ ForkCount: [2]byte{0, 2},
}
}
//dataSize := binary.BigEndian.Uint32(dataSizeField)
ffo := flattenedFileObject{
- FlatFileHeader: FlatFileHeader{
- Format: bytes[0:4],
- Version: bytes[4:6],
- RSVD: bytes[6:22],
- ForkCount: bytes[22:24],
- },
+ FlatFileHeader: NewFlatFileHeader(),
FlatFileInformationForkHeader: FlatFileInformationForkHeader{
ForkType: bytes[24:28],
CompressionType: bytes[28:32],
func (f flattenedFileObject) Payload() []byte {
var out []byte
- out = append(out, f.FlatFileHeader.Format...)
- out = append(out, f.FlatFileHeader.Version...)
- out = append(out, f.FlatFileHeader.RSVD...)
- out = append(out, f.FlatFileHeader.ForkCount...)
+ out = append(out, f.FlatFileHeader.Format[:]...)
+ out = append(out, f.FlatFileHeader.Version[:]...)
+ out = append(out, f.FlatFileHeader.RSVD[:]...)
+ out = append(out, f.FlatFileHeader.ForkCount[:]...)
out = append(out, []byte("INFO")...)
out = append(out, []byte{0, 0, 0, 0}...)
return out
}
-func NewFlattenedFileObject(filePath string, fileName string) (flattenedFileObject, error) {
+func NewFlattenedFileObject(filePath, fileName string) (*flattenedFileObject, error) {
file, err := os.Open(fmt.Sprintf("%v/%v", filePath, fileName))
if err != nil {
- return flattenedFileObject{}, err
+ return nil, err
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
- return flattenedFileObject{}, err
+ return nil, err
}
dataSize := make([]byte, 4)
binary.BigEndian.PutUint32(dataSize, uint32(fileInfo.Size()))
- return flattenedFileObject{
+ return &flattenedFileObject{
FlatFileHeader: NewFlatFileHeader(),
FlatFileInformationFork: NewFlatFileInformationFork(fileName),
FlatFileDataForkHeader: FlatFileDataForkHeader{
import (
"bytes"
"encoding/hex"
+ "github.com/davecgh/go-spew/spew"
+ "reflect"
"testing"
)
ffo := ReadFlattenedFileObject(testData)
- format := ffo.FlatFileHeader.Format
+ format := ffo.FlatFileHeader.Format[:]
want := []byte("FILP")
if !bytes.Equal(format, want) {
t.Errorf("ReadFlattenedFileObject() = %q, want %q", format, want)
// t.Errorf("%q, want %q", comment, want)
// }
//}
+
+func TestNewFlattenedFileObject(t *testing.T) {
+ type args struct {
+ filePath string
+ fileName string
+ }
+ tests := []struct {
+ name string
+ args args
+ want *flattenedFileObject
+ wantErr bool
+ }{
+ {
+ name: "when file path is valid",
+ args: args{
+ filePath: "./test/config/Files/",
+ fileName: "testfile.txt",
+ },
+ want: &flattenedFileObject{
+ FlatFileHeader: NewFlatFileHeader(),
+ FlatFileInformationForkHeader: FlatFileInformationForkHeader{},
+ FlatFileInformationFork: NewFlatFileInformationFork("testfile.txt"),
+ FlatFileDataForkHeader: FlatFileDataForkHeader{
+ ForkType: []byte("DATA"),
+ CompressionType: []byte{0, 0, 0, 0},
+ RSVD: []byte{0, 0, 0, 0},
+ DataSize: []byte{0x00, 0x00, 0x00, 0x17},
+ },
+ FileData: nil,
+ },
+ wantErr: false,
+ },
+ {
+ name: "when file path is invalid",
+ args: args{
+ filePath: "./nope/",
+ fileName: "also-nope.txt",
+ },
+ want: nil,
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := NewFlattenedFileObject(tt.args.filePath, tt.args.fileName)
+ spew.Dump(got)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("NewFlattenedFileObject() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("NewFlattenedFileObject() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
\ No newline at end of file
"bytes"
"crypto/rand"
"encoding/binary"
- "log"
"sort"
"time"
)
}
type NewsCategoryListData15 struct {
- Type []byte `yaml:"Type"` //Size 2 ; Bundle (2) or category (3)
+ Type []byte `yaml:"Type"` //Size 2 ; Bundle (2) or category (3)
+ Count []byte // Article or SubCategory count Size 2
+ NameSize byte
Name string `yaml:"Name"` //
Articles map[uint32]*NewsArtData `yaml:"Articles"` // Optional, if Type is Category
SubCats map[string]NewsCategoryListData15 `yaml:"SubCats"`
- Count []byte // Article or SubCategory count Size 2
+ GUID []byte // Size 16
AddSN []byte // Size 4
DeleteSN []byte // Size 4
- GUID []byte // Size 16
}
func (newscat *NewsCategoryListData15) GetNewsArtListData() NewsArtListData {
return nald
}
-// NewsArtData repsents a single news article
+// NewsArtData represents single news article
type NewsArtData struct {
Title string `yaml:"Title"`
Poster string `yaml:"Poster"`
return out
}
-// NewsArtList is a summarized ver sion of a NewArtData record for display in list view
+// NewsArtList is a summarized version of a NewArtData record for display in list view
type NewsArtList struct {
ID []byte // Size 4
TimeStamp []byte // Year (2 bytes), milliseconds (2 bytes) and seconds (4 bytes)
// Article size 2
}
-func (newscat *NewsCategoryListData15) Payload() []byte {
+func (newscat *NewsCategoryListData15) MarshalBinary() (data []byte, err error) {
count := make([]byte, 2)
binary.BigEndian.PutUint16(count, uint16(len(newscat.Articles)+len(newscat.SubCats)))
out := append(newscat.Type, count...)
if bytes.Equal(newscat.Type, []byte{0, 3}) {
- // Generate a random GUID
+ // Generate a random GUID // TODO: does this need to be random?
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
- log.Fatal(err)
+ return data, err
}
out = append(out, b...) // GUID
out = append(out, newscat.nameLen()...)
out = append(out, []byte(newscat.Name)...)
- return out
+ return out, err
}
// ReadNewsCategoryListData parses a byte slice into a NewsCategoryListData15 struct
return []byte{uint8(len(newscat.Name))}
}
-type NewsPath struct {
- Paths []string
-}
-
-func (np *NewsPath) Payload() []byte {
- var out []byte
-
- count := make([]byte, 2)
- binary.BigEndian.PutUint16(count, uint16(len(np.Paths)))
-
- out = append(out, count...)
- for _, p := range np.Paths {
- pLen := byte(len(p))
- out = append(out, []byte{0, 0}...) // what is this?
- out = append(out, pLen)
- out = append(out, []byte(p)...)
- }
-
- return out
-}
+//type NewsPath struct {
+// Paths []string
+//}
+//
+//func (np *NewsPath) Payload() []byte {
+// var out []byte
+//
+// count := make([]byte, 2)
+// binary.BigEndian.PutUint16(count, uint16(len(np.Paths)))
+//
+// out = append(out, count...)
+// for _, p := range np.Paths {
+// pLen := byte(len(p))
+// out = append(out, []byte{0, 0}...) // what is this?
+// out = append(out, pLen)
+// out = append(out, []byte(p)...)
+// }
+//
+// return out
+//}
func ReadNewsPath(newsPath []byte) []string {
if len(newsPath) == 0 {
--- /dev/null
+package hotline
+
+import (
+ "bytes"
+ "reflect"
+ "testing"
+)
+
+func TestNewsCategoryListData15_MarshalBinary(t *testing.T) {
+ type fields struct {
+ Type []byte
+ Name string
+ Articles map[uint32]*NewsArtData
+ SubCats map[string]NewsCategoryListData15
+ Count []byte
+ AddSN []byte
+ DeleteSN []byte
+ GUID []byte
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantData []byte
+ wantErr bool
+ }{
+ {
+ name: "returns expected bytes when type is a bundle",
+ fields: fields{
+ Type: []byte{0x00, 0x02},
+ Articles: map[uint32]*NewsArtData{
+ uint32(1): {
+ Title: "",
+ Poster: "",
+ Data: "",
+ },
+ },
+ Name: "foo",
+ },
+ wantData: []byte{
+ 0x00, 0x02,
+ 0x00, 0x01,
+ 0x03,
+ 0x66, 0x6f, 0x6f,
+ },
+ wantErr: false,
+ },
+ {
+ name: "returns expected bytes when type is a category",
+ fields: fields{
+ Type: []byte{0x00, 0x03},
+ Articles: map[uint32]*NewsArtData{
+ uint32(1): {
+ Title: "",
+ Poster: "",
+ Data: "",
+ },
+ },
+ Name: "foo",
+ },
+ wantData: []byte{
+ 0x00, 0x03,
+ 0x00, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x00, 0x00, 0x02,
+ 0x03,
+ 0x66, 0x6f, 0x6f,
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ newscat := &NewsCategoryListData15{
+ Type: tt.fields.Type,
+ Name: tt.fields.Name,
+ Articles: tt.fields.Articles,
+ SubCats: tt.fields.SubCats,
+ Count: tt.fields.Count,
+ AddSN: tt.fields.AddSN,
+ DeleteSN: tt.fields.DeleteSN,
+ GUID: tt.fields.GUID,
+ }
+ gotData, err := newscat.MarshalBinary()
+ if bytes.Equal(newscat.Type, []byte{0, 3}) {
+ // zero out the random GUID before comparison
+ for i := 4; i < 20; i++ {
+ gotData[i] = 0
+ }
+ }
+ if (err != nil) != tt.wantErr {
+ t.Errorf("MarshalBinary() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(gotData, tt.wantData) {
+ t.Errorf("MarshalBinary() gotData = %v, want %v", gotData, tt.wantData)
+ }
+ })
+ }
+}
)
type Server struct {
- Interface string
Port int
Accounts map[string]*Account
Agreement []byte
if client == nil {
return errors.New("invalid client")
}
- userName := string(*client.UserName)
+ userName := string(client.UserName)
login := client.Account.Login
handler := TransactionHandlers[requestNum]
+ b, err := t.MarshalBinary()
+ if err != nil {
+ return err
+ }
var n int
- if n, err = client.Connection.Write(t.Payload()); err != nil {
+ if n, err = client.Connection.Write(b); err != nil {
return err
}
s.Logger.Debugw("Sent Transaction",
const (
agreementFile = "Agreement.txt"
- messageBoardFile = "MessageBoard.txt"
- threadedNewsFile = "ThreadedNews.yaml"
)
// NewServer constructs a new Server from a config dir
tranNotifyChangeUser,
NewField(fieldUserID, *c.ID),
NewField(fieldUserFlags, *c.Flags),
- NewField(fieldUserName, *c.UserName),
+ NewField(fieldUserName, c.UserName),
NewField(fieldUserIconID, *c.Icon),
)
}
ID: &[]byte{0, 0},
Icon: &[]byte{0, 0},
Flags: &[]byte{0, 0},
- UserName: &[]byte{},
+ UserName: []byte{},
Connection: conn,
Server: s,
Version: &[]byte{},
ID: *c.ID,
Icon: *c.Icon,
Flags: *c.Flags,
- Name: string(*c.UserName),
+ Name: string(c.UserName),
}
connectedUsers = append(connectedUsers, NewField(fieldUsernameWithInfo, user.Payload()))
}
// If authentication fails, send error reply and close connection
if !c.Authenticate(login, encodedPassword) {
- rep := c.NewErrReply(clientLogin, "Incorrect login.")
- if _, err := conn.Write(rep.Payload()); err != nil {
+ t := c.NewErrReply(clientLogin, "Incorrect login.")
+ b, err := t.MarshalBinary()
+ if err != nil {
+ return err
+ }
+ if _, err := conn.Write(b); err != nil {
return err
}
return fmt.Errorf("incorrect login")
}
if clientLogin.GetField(fieldUserName).Data != nil {
- *c.UserName = clientLogin.GetField(fieldUserName).Data
+ c.UserName = clientLogin.GetField(fieldUserName).Data
}
if clientLogin.GetField(fieldUserIconID).Data != nil {
//
// This notifies the server to send the next item header
- fh := NewFilePath(fileTransfer.FilePath)
+ var fh FilePath
+ _ = fh.UnmarshalBinary(fileTransfer.FilePath)
fullFilePath := fmt.Sprintf("%v/%v", s.Config.FileRoot+fh.String(), string(fileTransfer.FileName))
basePathLen := len(fullFilePath)
return fields, nil
}
-func (t Transaction) Payload() []byte {
+func (t Transaction) MarshalBinary() (data []byte, err error) {
payloadSize := t.Size()
fieldCount := make([]byte, 2)
payloadSize, // this is the dataSize field, but seeming the same as totalSize
fieldCount,
fieldPayload,
- )
+ ), err
}
// Size returns the total size of the transaction payload
"encoding/binary"
"errors"
"fmt"
- "github.com/davecgh/go-spew/spew"
"gopkg.in/yaml.v2"
"io/ioutil"
"math/big"
func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
// Truncate long usernames
- trunc := fmt.Sprintf("%13s", *cc.UserName)
+ trunc := fmt.Sprintf("%13s", cc.UserName)
formattedMsg := fmt.Sprintf("\r%.14s: %s", trunc, t.GetField(fieldData).Data)
// By holding the option key, Hotline chat allows users to send /me formatted messages like:
// *** Halcyon does stuff
// This is indicated by the presence of the optional field fieldChatOptions in the transaction payload
if t.GetField(fieldChatOptions).Data != nil {
- formattedMsg = fmt.Sprintf("\r*** %s %s", *cc.UserName, t.GetField(fieldData).Data)
+ formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(fieldData).Data)
}
if bytes.Equal(t.GetField(fieldData).Data, []byte("/stats")) {
tranServerMsg,
&ID.Data,
NewField(fieldData, msg.Data),
- NewField(fieldUserName, *cc.UserName),
+ NewField(fieldUserName, cc.UserName),
NewField(fieldUserID, *cc.ID),
NewField(fieldOptions, []byte{0, 1}),
),
tranServerMsg,
cc.ID,
NewField(fieldData, *otherClient.AutoReply),
- NewField(fieldUserName, *otherClient.UserName),
+ NewField(fieldUserName, otherClient.UserName),
NewField(fieldUserID, *otherClient.ID),
NewField(fieldOptions, []byte{0, 1}),
),
func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
fileName := string(t.GetField(fieldFileName).Data)
filePath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFilePath).Data)
- spew.Dump(cc.Server.Config.FileRoot)
ffo, err := NewFlattenedFileObject(filePath, fileName)
if err != nil {
// fieldFilePath is only present for nested paths
if t.GetField(fieldFilePath).Data != nil {
- newFp := NewFilePath(t.GetField(fieldFilePath).Data)
+ var newFp FilePath
+ newFp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
newFolderPath += newFp.String()
}
newFolderPath += "/" + string(t.GetField(fieldFileName).Data)
tranNotifyChangeUser,
NewField(fieldUserID, *c.ID),
NewField(fieldUserFlags, *c.Flags),
- NewField(fieldUserName, *c.UserName),
+ NewField(fieldUserName, c.UserName),
NewField(fieldUserIconID, *c.Icon),
)
}
template = fmt.Sprintf(
template,
- *clientConn.UserName,
+ clientConn.UserName,
clientConn.Account.Name,
clientConn.Account.Login,
clientConn.Connection.RemoteAddr().String(),
res = append(res, cc.NewReply(t,
NewField(fieldData, []byte(template)),
- NewField(fieldUserName, *clientConn.UserName),
+ NewField(fieldUserName, clientConn.UserName),
))
return res, err
}
cc.NotifyOthers(
*NewTransaction(
tranNotifyChangeUser, nil,
- NewField(fieldUserName, *cc.UserName),
+ NewField(fieldUserName, cc.UserName),
NewField(fieldUserID, *cc.ID),
NewField(fieldUserIconID, *cc.Icon),
NewField(fieldUserFlags, *cc.Flags),
bs := make([]byte, 2)
binary.BigEndian.PutUint16(bs, *cc.Server.NextGuestID)
- *cc.UserName = t.GetField(fieldUserName).Data
+ cc.UserName = t.GetField(fieldUserName).Data
*cc.ID = bs
*cc.Icon = t.GetField(fieldUserIconID).Data
newsTemplate = cc.Server.Config.NewsDelimiter
}
- newsPost := fmt.Sprintf(newsTemplate+"\r", *cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(fieldData).Data)
+ newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(fieldData).Data)
newsPost = strings.Replace(newsPost, "\n", "\r", -1)
// update news in memory
var fieldData []Field
for _, k := range keys {
cat := cats[k]
+ b, _ := cat.MarshalBinary()
fieldData = append(fieldData, NewField(
fieldNewsCatListData15,
- cat.Payload(),
+ b,
))
}
newArt := NewsArtData{
Title: string(t.GetField(fieldNewsArtTitle).Data),
- Poster: string(*cc.UserName),
+ Poster: string(cc.UserName),
Date: NewsDate(),
PrevArt: []byte{0, 0, 0, 0},
NextArt: []byte{0, 0, 0, 0},
cc.Server.FileTransfers[data] = fileTransfer
cc.Transfers[FolderDownload] = append(cc.Transfers[FolderDownload], fileTransfer)
- fp := NewFilePath(t.GetField(fieldFilePath).Data)
+ var fp FilePath
+ fp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
fullFilePath := fmt.Sprintf("%v%v", cc.Server.Config.FileRoot+fp.String(), string(fileTransfer.FileName))
transferSize, err := CalcTotalSize(fullFilePath)
icon = t.GetField(fieldUserIconID).Data
}
*cc.Icon = icon
- *cc.UserName = t.GetField(fieldUserName).Data
+ cc.UserName = t.GetField(fieldUserName).Data
// the options field is only passed by the client versions > 1.2.3.
options := t.GetField(fieldOptions).Data
NewField(fieldUserID, *cc.ID),
NewField(fieldUserIconID, *cc.Icon),
NewField(fieldUserFlags, *cc.Flags),
- NewField(fieldUserName, *cc.UserName),
+ NewField(fieldUserName, cc.UserName),
)
return res, err
tranInviteToChat,
&targetID,
NewField(fieldChatID, newChatID),
- NewField(fieldUserName, *cc.UserName),
+ NewField(fieldUserName, cc.UserName),
NewField(fieldUserID, *cc.ID),
),
)
res = append(res,
cc.NewReply(t,
NewField(fieldChatID, newChatID),
- NewField(fieldUserName, *cc.UserName),
+ NewField(fieldUserName, cc.UserName),
NewField(fieldUserID, *cc.ID),
NewField(fieldUserIconID, *cc.Icon),
NewField(fieldUserFlags, *cc.Flags),
tranInviteToChat,
&targetID,
NewField(fieldChatID, chatID),
- NewField(fieldUserName, *cc.UserName),
+ NewField(fieldUserName, cc.UserName),
NewField(fieldUserID, *cc.ID),
),
)
cc.NewReply(
t,
NewField(fieldChatID, chatID),
- NewField(fieldUserName, *cc.UserName),
+ NewField(fieldUserName, cc.UserName),
NewField(fieldUserID, *cc.ID),
NewField(fieldUserIconID, *cc.Icon),
NewField(fieldUserFlags, *cc.Flags),
privChat := cc.Server.PrivateChats[chatInt]
- resMsg := append(*cc.UserName, []byte(" declined invitation to chat")...)
+ resMsg := append(cc.UserName, []byte(" declined invitation to chat")...)
for _, c := range sortedClients(privChat.ClientConn) {
res = append(res,
tranNotifyChatChangeUser,
c.ID,
NewField(fieldChatID, chatID),
- NewField(fieldUserName, *cc.UserName),
+ NewField(fieldUserName, cc.UserName),
NewField(fieldUserID, *cc.ID),
NewField(fieldUserIconID, *cc.Icon),
NewField(fieldUserFlags, *cc.Flags),
ID: *c.ID,
Icon: *c.Icon,
Flags: *c.Flags,
- Name: string(*c.UserName),
+ Name: string(c.UserName),
}
replyFields = append(replyFields, NewField(fieldUsernameWithInfo, user.Payload()))
name: "sends chat subject to private chat members",
args: args{
cc: &ClientConn{
- UserName: &[]byte{0x00, 0x01},
+ UserName: []byte{0x00, 0x01},
Server: &Server{
PrivateChats: map[uint32]*PrivateChat{
uint32(1): {
ID: &[]byte{0, 1},
Icon: &[]byte{0, 2},
Flags: &[]byte{0, 3},
- UserName: &[]byte{0, 4},
+ UserName: []byte{0, 4},
},
},
},
name: "sends chat msg transaction to all clients",
args: args{
cc: &ClientConn{
- UserName: &[]byte{0x00, 0x01},
+ UserName: []byte{0x00, 0x01},
Server: &Server{
Clients: map[uint16]*ClientConn{
uint16(1): {
},
wantErr: false,
},
+ {
+ name: "sends chat msg as emote if fieldChatOptions is set",
+ args: args{
+ cc: &ClientConn{
+ UserName: []byte("Testy McTest"),
+ Server: &Server{
+ Clients: map[uint16]*ClientConn{
+ uint16(1): {
+ Account: &Account{
+ Access: &[]byte{255, 255, 255, 255, 255, 255, 255, 255},
+ },
+ ID: &[]byte{0, 1},
+ },
+ uint16(2): {
+ Account: &Account{
+ Access: &[]byte{255, 255, 255, 255, 255, 255, 255, 255},
+ },
+ ID: &[]byte{0, 2},
+ },
+ },
+ },
+ },
+ t: &Transaction{
+ Fields: []Field{
+ NewField(fieldData, []byte("performed action")),
+ NewField(fieldChatOptions, []byte{0x00, 0x01}),
+ },
+ },
+ },
+ want: []Transaction{
+ {
+ clientID: &[]byte{0, 1},
+ Flags: 0x00,
+ IsReply: 0x00,
+ Type: []byte{0, 0x6a},
+ ID: []byte{0x9a, 0xcb, 0x04, 0x42}, // Random ID from rand.Seed(1)
+ ErrorCode: []byte{0, 0, 0, 0},
+ Fields: []Field{
+ NewField(fieldData, []byte("\r*** Testy McTest performed action")),
+ },
+ },
+ {
+ clientID: &[]byte{0, 2},
+ Flags: 0x00,
+ IsReply: 0x00,
+ Type: []byte{0, 0x6a},
+ ID: []byte{0xf0, 0xc5, 0x34, 0x1e}, // Random ID from rand.Seed(1)
+ ErrorCode: []byte{0, 0, 0, 0},
+ Fields: []Field{
+ NewField(fieldData, []byte("\r*** Testy McTest performed action")),
+ },
+ },
+ },
+ wantErr: false,
+ },
{
name: "only sends chat msg to clients with accessReadChat permission",
args: args{
cc: &ClientConn{
- UserName: &[]byte{0x00, 0x01},
+ UserName: []byte{0x00, 0x01},
Server: &Server{
Clients: map[uint16]*ClientConn{
uint16(1): {
})
}
}
+
+func TestHandleGetFileInfo(t *testing.T) {
+ rand.Seed(1) // reset seed between tests to make transaction IDs predictable
+
+ type args struct {
+ cc *ClientConn
+ t *Transaction
+ }
+ tests := []struct {
+ name string
+ args args
+ wantRes []Transaction
+ wantErr bool
+ }{
+ {
+ name: "returns expected fields when a valid file is requested",
+ args: args{
+ cc: &ClientConn{
+ ID: &[]byte{0x00, 0x01},
+ Server: &Server{
+ Config: &Config{
+ FileRoot: "./test/config/Files/",
+ },
+ },
+ },
+ t: NewTransaction(
+ tranGetFileInfo, nil,
+ NewField(fieldFileName, []byte("testfile.txt")),
+ NewField(fieldFilePath, []byte{0x00, 0x00}),
+ //NewField(fieldFilePath, []byte{
+ // 0x00, 0x03,
+ // 0x00, 0x00,
+ // 0x04,
+ // 0x74, 0x65, 0x73, 0x74,
+ // 0x00, 0x00,
+ // 0x06,
+ // 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+ //
+ // 0x00, 0x00,
+ // 0x05,
+ // 0x46, 0x69, 0x6c, 0x65, 73},
+ //),
+ ),
+ },
+ wantRes: []Transaction{
+ {
+ clientID: &[]byte{0, 1},
+ Flags: 0x00,
+ IsReply: 0x01,
+ Type: []byte{0, 0xce},
+ ID: []byte{0x9a, 0xcb, 0x04, 0x42}, // Random ID from rand.Seed(1)
+ ErrorCode: []byte{0, 0, 0, 0},
+ Fields: []Field{
+ NewField(fieldFileName, []byte("testfile.txt")),
+ NewField(fieldFileTypeString, []byte("TEXT")),
+ NewField(fieldFileCreatorString, []byte("TTXT")),
+ NewField(fieldFileComment, []byte("TODO")),
+ NewField(fieldFileType, []byte("TEXT")),
+ NewField(fieldFileCreateDate, []byte{0x07, 0x70, 0x00, 0x00, 0xba, 0x74, 0x24, 0x73}),
+ NewField(fieldFileModifyDate, []byte{0x07, 0x70, 0x00, 0x00, 0xba, 0x74, 0x24, 0x73}),
+ NewField(fieldFileSize, []byte{0x0, 0x0, 0x0, 0x17}),
+ },
+ },
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ rand.Seed(1) // reset seed between tests to make transaction IDs predictable
+
+ gotRes, err := HandleGetFileInfo(tt.args.cc, tt.args.t)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("HandleGetFileInfo() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !assert.Equal(t, tt.wantRes, gotRes) {
+ t.Errorf("HandleGetFileInfo() gotRes = %v, want %v", gotRes, tt.wantRes)
+ }
+ })
+ }
+}
{
name: "when buf contains all bytes for a single transaction",
args: args{
- buf: sampleTransaction.Payload(),
+ buf: func() []byte {
+ b, _ := sampleTransaction.MarshalBinary()
+ return b
+ }(),
},
want: sampleTransaction,
- want1: len(sampleTransaction.Payload()),
+ want1: func() int {
+ b, _ := sampleTransaction.MarshalBinary()
+ return len(b)
+ }(),
wantErr: false,
},
{
name: "when len(buf) is less than the length of the transaction",
args: args{
- buf: sampleTransaction.Payload()[:len(sampleTransaction.Payload())-1],
+ buf: func() []byte {
+ b, _ := sampleTransaction.MarshalBinary()
+ return b[:len(b)-1]
+ }(),
},
want: nil,
want1: 0,
App *tview.Application
Pages *tview.Pages
userList *tview.TextView
- agreeModal *tview.Modal
trackerList *tview.List
HLClient *Client
}
chatInput: chatInput,
userList: userList,
trackerList: tview.NewList(),
- agreeModal: tview.NewModal(),
HLClient: c,
}
}