// fields, but shxd sneaks in FieldChatSubject (115) so it's important to filter explicitly for the expected
// field type. Probably a good idea to do everywhere.
if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
- u, err := ReadUser(field.Data)
- if err != nil {
- return res, err
+ var user User
+ if _, err := user.Write(field.Data); err != nil {
+ return res, fmt.Errorf("unable to read user data: %w", err)
}
- users = append(users, *u)
+
+ users = append(users, user)
}
}
c.UserList = users
import (
"encoding/binary"
+ "io"
"slices"
)
type FileHeader struct {
- Size []byte // Total size of FileHeader payload
- Type []byte // 0 for file, 1 for dir
- FilePath []byte // encoded file path
+ Size [2]byte // Total size of FileHeader payload
+ Type [2]byte // 0 for file, 1 for dir
+ FilePath []byte // encoded file path
}
func NewFileHeader(fileName string, isDir bool) FileHeader {
fh := FileHeader{
- Size: make([]byte, 2),
- Type: []byte{0x00, 0x00},
+ Type: [2]byte{0x00, 0x00},
FilePath: EncodeFilePath(fileName),
}
if isDir {
- fh.Type = []byte{0x00, 0x01}
+ fh.Type = [2]byte{0x00, 0x01}
}
encodedPathLen := uint16(len(fh.FilePath) + len(fh.Type))
- binary.BigEndian.PutUint16(fh.Size, encodedPathLen)
+ binary.BigEndian.PutUint16(fh.Size[:], encodedPathLen)
return fh
}
-func (fh *FileHeader) Payload() []byte {
- return slices.Concat(
- fh.Size,
- fh.Type,
+func (fh *FileHeader) Read(p []byte) (int, error) {
+ return copy(p, slices.Concat(
+ fh.Size[:],
+ fh.Type[:],
fh.FilePath,
- )
+ ),
+ ), io.EOF
}
package hotline
import (
+ "io"
"reflect"
"testing"
)
isDir: false,
},
want: FileHeader{
- Size: []byte{0x00, 0x0a},
- Type: []byte{0x00, 0x00},
+ Size: [2]byte{0x00, 0x0a},
+ Type: [2]byte{0x00, 0x00},
FilePath: EncodeFilePath("foo"),
},
},
isDir: true,
},
want: FileHeader{
- Size: []byte{0x00, 0x0a},
- Type: []byte{0x00, 0x01},
+ Size: [2]byte{0x00, 0x0a},
+ Type: [2]byte{0x00, 0x01},
FilePath: EncodeFilePath("foo"),
},
},
func TestFileHeader_Payload(t *testing.T) {
type fields struct {
- Size []byte
- Type []byte
+ Size [2]byte
+ Type [2]byte
FilePath []byte
}
tests := []struct {
{
name: "has expected payload bytes",
fields: fields{
- Size: []byte{0x00, 0x0a},
- Type: []byte{0x00, 0x00},
+ Size: [2]byte{0x00, 0x0a},
+ Type: [2]byte{0x00, 0x00},
FilePath: EncodeFilePath("foo"),
},
want: []byte{
Type: tt.fields.Type,
FilePath: tt.fields.FilePath,
}
- if got := fh.Payload(); !reflect.DeepEqual(got, tt.want) {
+ got, _ := io.ReadAll(fh)
+ if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Read() = %v, want %v", got, tt.want)
}
})
package hotline
import (
+ "bytes"
"encoding/binary"
"errors"
"fmt"
f.ffo.FlatFileHeader.ForkCount[1] = 3
- if err := f.ffo.FlatFileInformationFork.UnmarshalBinary(b); err != nil {
+ _, err = io.Copy(&f.ffo.FlatFileInformationFork, bytes.NewReader(b))
+ if err != nil {
return nil, err
}
+
} else {
f.ffo.FlatFileInformationFork = FlatFileInformationFork{
Platform: []byte("AMAC"), // TODO: Remove hardcode to support "AWIN" Platform (maybe?)
package hotline
import (
+ "bytes"
"encoding/binary"
"io"
+ "slices"
)
type flattenedFileObject struct {
func (ffo *flattenedFileObject) TransferSize(offset int64) []byte {
// get length of the flattenedFileObject, including the info fork
- payloadSize := len(ffo.BinaryMarshal())
+ b, _ := io.ReadAll(ffo)
+ payloadSize := len(b)
// length of data fork
dataSize := binary.BigEndian.Uint32(ffo.FlatFileDataForkHeader.DataSize[:])
DataSize [4]byte
}
-func (ffif *FlatFileInformationFork) MarshalBinary() []byte {
- var b []byte
- b = append(b, ffif.Platform...)
- b = append(b, ffif.TypeSignature...)
- b = append(b, ffif.CreatorSignature...)
- b = append(b, ffif.Flags...)
- b = append(b, ffif.PlatformFlags...)
- b = append(b, ffif.RSVD...)
- b = append(b, ffif.CreateDate...)
- b = append(b, ffif.ModifyDate...)
- b = append(b, ffif.NameScript...)
- b = append(b, ffif.ReadNameSize()...)
- b = append(b, ffif.Name...)
- b = append(b, ffif.CommentSize...)
- b = append(b, ffif.Comment...)
+func (ffif *FlatFileInformationFork) Read(p []byte) (int, error) {
+ return copy(p,
+ slices.Concat(
+ ffif.Platform,
+ ffif.TypeSignature,
+ ffif.CreatorSignature,
+ ffif.Flags,
+ ffif.PlatformFlags,
+ ffif.RSVD,
+ ffif.CreateDate,
+ ffif.ModifyDate,
+ ffif.NameScript,
+ ffif.ReadNameSize(),
+ ffif.Name,
+ ffif.CommentSize,
+ ffif.Comment,
+ ),
+ ), io.EOF
+}
+
+// Write implements the io.Writeer interface for FlatFileInformationFork
+func (ffif *FlatFileInformationFork) Write(p []byte) (int, error) {
+ nameSize := p[70:72]
+ bs := binary.BigEndian.Uint16(nameSize)
+ total := 72 + bs
+
+ ffif.Platform = p[0:4]
+ ffif.TypeSignature = p[4:8]
+ ffif.CreatorSignature = p[8:12]
+ ffif.Flags = p[12:16]
+ ffif.PlatformFlags = p[16:20]
+ ffif.RSVD = p[20:52]
+ ffif.CreateDate = p[52:60]
+ ffif.ModifyDate = p[60:68]
+ ffif.NameScript = p[68:70]
+ ffif.NameSize = p[70:72]
+ ffif.Name = p[72:total]
+
+ if len(p) > int(total) {
+ ffif.CommentSize = p[total : total+2]
+ commentLen := binary.BigEndian.Uint16(ffif.CommentSize)
+
+ commentStartPos := int(total) + 2
+ commentEndPos := int(total) + 2 + int(commentLen)
+
+ ffif.Comment = p[commentStartPos:commentEndPos]
- return b
+ total = uint16(commentEndPos)
+ }
+
+ return int(total), nil
}
func (ffif *FlatFileInformationFork) UnmarshalBinary(b []byte) error {
return nil
}
-func (ffo *flattenedFileObject) BinaryMarshal() []byte {
- var out []byte
- out = append(out, ffo.FlatFileHeader.Format[:]...)
- out = append(out, ffo.FlatFileHeader.Version[:]...)
- out = append(out, ffo.FlatFileHeader.RSVD[:]...)
- out = append(out, ffo.FlatFileHeader.ForkCount[:]...)
-
- out = append(out, []byte("INFO")...)
- out = append(out, []byte{0, 0, 0, 0}...)
- out = append(out, make([]byte, 4)...)
- out = append(out, ffo.FlatFileInformationFork.DataSize()...)
-
- out = append(out, ffo.FlatFileInformationFork.Platform...)
- out = append(out, ffo.FlatFileInformationFork.TypeSignature...)
- out = append(out, ffo.FlatFileInformationFork.CreatorSignature...)
- out = append(out, ffo.FlatFileInformationFork.Flags...)
- out = append(out, ffo.FlatFileInformationFork.PlatformFlags...)
- out = append(out, ffo.FlatFileInformationFork.RSVD...)
- out = append(out, ffo.FlatFileInformationFork.CreateDate...)
- out = append(out, ffo.FlatFileInformationFork.ModifyDate...)
- out = append(out, ffo.FlatFileInformationFork.NameScript...)
- out = append(out, ffo.FlatFileInformationFork.ReadNameSize()...)
- out = append(out, ffo.FlatFileInformationFork.Name...)
- out = append(out, ffo.FlatFileInformationFork.CommentSize...)
- out = append(out, ffo.FlatFileInformationFork.Comment...)
-
- out = append(out, ffo.FlatFileDataForkHeader.ForkType[:]...)
- out = append(out, ffo.FlatFileDataForkHeader.CompressionType[:]...)
- out = append(out, ffo.FlatFileDataForkHeader.RSVD[:]...)
- out = append(out, ffo.FlatFileDataForkHeader.DataSize[:]...)
-
- return out
+// Read implements the io.Reader interface for flattenedFileObject
+func (ffo *flattenedFileObject) Read(p []byte) (int, error) {
+ return copy(p, slices.Concat(
+ ffo.FlatFileHeader.Format[:],
+ ffo.FlatFileHeader.Version[:],
+ ffo.FlatFileHeader.RSVD[:],
+ ffo.FlatFileHeader.ForkCount[:],
+ []byte("INFO"),
+ []byte{0, 0, 0, 0},
+ make([]byte, 4),
+ ffo.FlatFileInformationFork.DataSize(),
+ ffo.FlatFileInformationFork.Platform,
+ ffo.FlatFileInformationFork.TypeSignature,
+ ffo.FlatFileInformationFork.CreatorSignature,
+ ffo.FlatFileInformationFork.Flags,
+ ffo.FlatFileInformationFork.PlatformFlags,
+ ffo.FlatFileInformationFork.RSVD,
+ ffo.FlatFileInformationFork.CreateDate,
+ ffo.FlatFileInformationFork.ModifyDate,
+ ffo.FlatFileInformationFork.NameScript,
+ ffo.FlatFileInformationFork.ReadNameSize(),
+ ffo.FlatFileInformationFork.Name,
+ ffo.FlatFileInformationFork.CommentSize,
+ ffo.FlatFileInformationFork.Comment,
+ ffo.FlatFileDataForkHeader.ForkType[:],
+ ffo.FlatFileDataForkHeader.CompressionType[:],
+ ffo.FlatFileDataForkHeader.RSVD[:],
+ ffo.FlatFileDataForkHeader.DataSize[:],
+ ),
+ ), io.EOF
}
-func (ffo *flattenedFileObject) ReadFrom(r io.Reader) (int, error) {
- var n int
+func (ffo *flattenedFileObject) ReadFrom(r io.Reader) (int64, error) {
+ var n int64
if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileHeader); err != nil {
return n, err
return n, err
}
- if err := ffo.FlatFileInformationFork.UnmarshalBinary(ffifBuf); err != nil {
+ _, err := io.Copy(&ffo.FlatFileInformationFork, bytes.NewReader(ffifBuf))
+ if err != nil {
return n, err
}
"bytes"
"crypto/rand"
"encoding/binary"
+ "io"
+ "slices"
"sort"
)
}
type NewsCategoryListData15 struct {
- Type []byte `yaml:"Type"` // Size 2 ; Bundle (2) or category (3)
- Count []byte // Article or SubCategory count Size 2
+ Type [2]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
var newsArtsPayload []byte
for i, art := range newscat.Articles {
- ID := make([]byte, 4)
- binary.BigEndian.PutUint32(ID, i)
+ id := make([]byte, 4)
+ binary.BigEndian.PutUint32(id, i)
newArt := NewsArtList{
- ID: ID,
+ ID: id,
TimeStamp: art.Date,
ParentID: art.ParentArt,
Flags: []byte{0, 0, 0, 0},
}
nald := NewsArtListData{
- ID: []byte{0, 0, 0, 0},
+ ID: [4]byte{0, 0, 0, 0},
Count: len(newsArts),
Name: []byte{},
Description: []byte{},
}
type NewsArtListData struct {
- ID []byte `yaml:"ID"` // Size 4
- Name []byte `yaml:"Name"`
- Description []byte `yaml:"Description"` // not used?
- NewsArtList []byte // List of articles Optional (if article count > 0)
+ ID [4]byte `yaml:"ID"` // Size 4
+ Name []byte `yaml:"Name"`
+ Description []byte `yaml:"Description"` // not used?
+ NewsArtList []byte // List of articles Optional (if article count > 0)
Count int
}
-func (nald *NewsArtListData) Payload() []byte {
+func (nald *NewsArtListData) Read(p []byte) (int, error) {
count := make([]byte, 4)
binary.BigEndian.PutUint32(count, uint32(nald.Count))
- out := append(nald.ID, count...)
- out = append(out, []byte{uint8(len(nald.Name))}...)
- out = append(out, nald.Name...)
- out = append(out, []byte{uint8(len(nald.Description))}...)
- out = append(out, nald.Description...)
- out = append(out, nald.NewsArtList...)
-
- return out
+ return copy(
+ p,
+ slices.Concat(
+ nald.ID[:],
+ count,
+ []byte{uint8(len(nald.Name))},
+ nald.Name,
+ []byte{uint8(len(nald.Description))},
+ nald.Description,
+ nald.NewsArtList,
+ ),
+ ),
+ io.EOF
}
// NewsArtList is a summarized version of a NewArtData record for display in list view
count := make([]byte, 2)
binary.BigEndian.PutUint16(count, uint16(len(newscat.Articles)+len(newscat.SubCats)))
- out := append(newscat.Type, count...)
+ out := append(newscat.Type[:], count...)
- if bytes.Equal(newscat.Type, []byte{0, 3}) {
+ if bytes.Equal(newscat.Type[:], []byte{0, 3}) {
// Generate a random GUID // TODO: does this need to be random?
b := make([]byte, 16)
_, err := rand.Read(b)
return out, err
}
-// ReadNewsCategoryListData parses a byte slice into a NewsCategoryListData15 struct
-// For use on the client side
-func ReadNewsCategoryListData(payload []byte) NewsCategoryListData15 {
- ncld := NewsCategoryListData15{
- Type: payload[0:2],
- Count: payload[2:4],
- }
-
- if bytes.Equal(ncld.Type, []byte{0, 3}) {
- ncld.GUID = payload[4:20]
- ncld.AddSN = payload[20:24]
- ncld.AddSN = payload[24:28]
- ncld.Name = string(payload[29:])
- } else {
- ncld.Name = string(payload[5:])
- }
-
- return ncld
-}
-
func (newscat *NewsCategoryListData15) nameLen() []byte {
return []byte{uint8(len(newscat.Name))}
}
package hotline
import (
- "bytes"
"reflect"
"testing"
)
func TestNewsCategoryListData15_MarshalBinary(t *testing.T) {
type fields struct {
- Type []byte
+ Type [2]byte
Name string
Articles map[uint32]*NewsArtData
SubCats map[string]NewsCategoryListData15
{
name: "returns expected bytes when type is a bundle",
fields: fields{
- Type: []byte{0x00, 0x02},
+ Type: [2]byte{0x00, 0x02},
Articles: map[uint32]*NewsArtData{
uint32(1): {
Title: "",
{
name: "returns expected bytes when type is a category",
fields: fields{
- Type: []byte{0x00, 0x03},
+ Type: [2]byte{0x00, 0x03},
Articles: map[uint32]*NewsArtData{
uint32(1): {
Title: "",
GUID: tt.fields.GUID,
}
gotData, err := newscat.MarshalBinary()
- if bytes.Equal(newscat.Type, []byte{0, 3}) {
+ if newscat.Type == [2]byte{0, 3} {
// zero out the random GUID before comparison
for i := 4; i < 20; i++ {
gotData[i] = 0
var connectedUsers []Field
for _, c := range sortedClients(s.Clients) {
- user := User{
+ b, err := io.ReadAll(&User{
ID: *c.ID,
Icon: c.Icon,
Flags: c.Flags,
Name: string(c.UserName),
+ })
+ if err != nil {
+ return nil
}
- connectedUsers = append(connectedUsers, NewField(FieldUsernameWithInfo, user.Payload()))
+ connectedUsers = append(connectedUsers, NewField(FieldUsernameWithInfo, b))
}
return connectedUsers
}
// if file transfer options are included, that means this is a "quick preview" request from a 1.5+ client
if fileTransfer.options == nil {
- // Start by sending flat file object to client
- if _, err := rwc.Write(fw.ffo.BinaryMarshal()); err != nil {
+ _, err = io.Copy(rwc, fw.ffo)
+ if err != nil {
return err
}
}
}
fileHeader := NewFileHeader(subPath, info.IsDir())
-
- // Send the fileWrapper header to client
- if _, err := rwc.Write(fileHeader.Payload()); err != nil {
- s.Logger.Errorf("error sending file header: %v", err)
- return err
+ if _, err := io.Copy(rwc, &fileHeader); err != nil {
+ return fmt.Errorf("error sending file header: %w", err)
}
// Read the client's Next Action request
}
// Send ffo bytes to client
- if _, err := rwc.Write(hlFile.ffo.BinaryMarshal()); err != nil {
- s.Logger.Error(err)
+ _, err = io.Copy(rwc, hlFile.ffo)
+ if err != nil {
return err
}
if err != nil {
return res, err
}
- _, err = w.Write(hlFile.ffo.FlatFileInformationFork.MarshalBinary())
+ _, err = io.Copy(w, &hlFile.ffo.FlatFileInformationFork)
if err != nil {
return res, err
}
cats := cc.Server.GetNewsCatByPath(pathStrs)
cats[name] = NewsCategoryListData15{
Name: name,
- Type: []byte{0, 3},
+ Type: [2]byte{0, 3},
Articles: map[uint32]*NewsArtData{},
SubCats: make(map[string]NewsCategoryListData15),
}
cats := cc.Server.GetNewsCatByPath(pathStrs)
cats[name] = NewsCategoryListData15{
Name: name,
- Type: []byte{0, 2},
+ Type: [2]byte{0, 2},
Articles: map[uint32]*NewsArtData{},
SubCats: make(map[string]NewsCategoryListData15),
}
nald := cat.GetNewsArtListData()
- res = append(res, cc.NewReply(t, NewField(FieldNewsArtListData, nald.Payload())))
+ b, err := io.ReadAll(&nald)
+ if err != nil {
+
+ }
+
+ res = append(res, cc.NewReply(t, NewField(FieldNewsArtListData, b)))
return res, err
}
}
}
- if bytes.Equal(cats[delName].Type, []byte{0, 3}) {
+ if cats[delName].Type == [2]byte{0, 3} {
if !cc.Authorize(accessNewsDeleteCat) {
return append(res, cc.NewErrReply(t, "You are not allowed to delete news categories.")), nil
}
replyFields := []Field{NewField(FieldChatSubject, []byte(privChat.Subject))}
for _, c := range sortedClients(privChat.ClientConn) {
- user := User{
+
+ b, err := io.ReadAll(&User{
ID: *c.ID,
Icon: c.Icon,
Flags: c.Flags,
Name: string(c.UserName),
+ })
+ if err != nil {
+ return res, nil
}
-
- replyFields = append(replyFields, NewField(FieldUsernameWithInfo, user.Payload()))
+ replyFields = append(replyFields, NewField(FieldUsernameWithInfo, b))
}
res = append(res, cc.NewReply(t, replyFields...))
Server: &Server{
ThreadedNews: &ThreadedNews{Categories: map[string]NewsCategoryListData15{
"test": {
- Type: []byte{0, 3},
+ Type: [2]byte{0, 3},
Count: nil,
NameSize: 0,
Name: "zz",
Server: &Server{
ThreadedNews: &ThreadedNews{Categories: map[string]NewsCategoryListData15{
"testcat": {
- Type: []byte{0, 2},
+ Type: [2]byte{0, 2},
Count: nil,
NameSize: 0,
Name: "test",
}(),
ThreadedNews: &ThreadedNews{Categories: map[string]NewsCategoryListData15{
"testcat": {
- Type: []byte{0, 2},
+ Type: [2]byte{0, 2},
Count: nil,
NameSize: 0,
Name: "test",
}(),
ThreadedNews: &ThreadedNews{Categories: map[string]NewsCategoryListData15{
"test": {
- Type: []byte{0, 2},
+ Type: [2]byte{0, 2},
Count: nil,
NameSize: 0,
Name: "test",
}
// Write the information fork
- _, err := infoFork.Write(ffo.FlatFileInformationFork.MarshalBinary())
+ _, err := io.Copy(infoFork, &ffo.FlatFileInformationFork)
if err != nil {
return err
}
return nil
}
+// TODO: read the banner once on startup instead of once per banner fetch
func (s *Server) bannerDownload(w io.Writer) error {
bannerBytes, err := os.ReadFile(filepath.Join(s.ConfigDir, s.Config.BannerFile))
if err != nil {
},
}
fakeFileData := []byte{1, 2, 3}
- b := testFile.BinaryMarshal()
+ b, _ := io.ReadAll(&testFile)
b = append(b, fakeFileData...)
return bytes.NewReader(b)
}(),
import (
"encoding/binary"
+ "io"
+ "slices"
)
// User flags are stored as a 2 byte bitmap and represent various user states
Name string // Variable length user name
}
-func (u User) Payload() []byte {
+func (u *User) Read(p []byte) (int, error) {
nameLen := make([]byte, 2)
binary.BigEndian.PutUint16(nameLen, uint16(len(u.Name)))
out = append(out, nameLen...)
out = append(out, u.Name...)
- return out
+ return copy(p, slices.Concat(
+ u.ID,
+ u.Icon,
+ u.Flags,
+ nameLen,
+ []byte(u.Name),
+ )), io.EOF
}
-func ReadUser(b []byte) (*User, error) {
- u := &User{
- ID: b[0:2],
- Icon: b[2:4],
- Flags: b[4:6],
- Name: string(b[8:]),
- }
- return u, nil
+func (u *User) Write(p []byte) (int, error) {
+ namelen := int(binary.BigEndian.Uint16(p[6:8]))
+ u.ID = p[0:2]
+ u.Icon = p[2:4]
+ u.Flags = p[4:6]
+ u.Name = string(p[8 : 8+namelen])
+
+ return 8 + namelen, nil
}
// decodeString decodes an obfuscated user string from a client
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got, err := ReadUser(tt.args.b)
+ var user User
+ _, err := user.Write(tt.args.b)
if (err != nil) != tt.wantErr {
t.Errorf("ReadUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
- if !assert.Equal(t, tt.want, got) {
- t.Errorf("ReadUser() got = %v, want %v", got, tt.want)
+ if !assert.Equal(t, tt.want, &user) {
+ t.Errorf("ReadUser() got = %v, want %v", user, tt.want)
}
})
}