]> git.r.bdr.sh - rbdr/mobius/commitdiff
Add initial support for resource and info forks
authorJeff Halter <redacted>
Tue, 21 Jun 2022 03:14:32 +0000 (20:14 -0700)
committerJeff Halter <redacted>
Tue, 21 Jun 2022 03:14:32 +0000 (20:14 -0700)
27 files changed:
cmd/mobius-hotline-server/main.go
cmd/mobius-hotline-server/mobius/config/config.yaml
go.sum
hotline/access.go
hotline/client.go
hotline/client_conn.go
hotline/config.go
hotline/file_name_with_info.go
hotline/file_path.go
hotline/file_store.go
hotline/file_types.go
hotline/file_wrapper.go [new file with mode: 0644]
hotline/files.go
hotline/flattened_file_object.go
hotline/flattened_file_object_test.go
hotline/handshake.go
hotline/server.go
hotline/server_blackbox_test.go
hotline/server_test.go
hotline/stats.go
hotline/test/config/Files/testfile-1k [new file with mode: 0644]
hotline/test/config/Files/testfile-8b [new file with mode: 0644]
hotline/transaction.go
hotline/transaction_handlers.go
hotline/transaction_handlers_test.go
hotline/transfer.go
hotline/transfer_test.go

index 2f914c4d1f6a34242dfd180b50ddd67ecb2a1598..bf41da1580093b55c89a6695485ad935da4a594f 100644 (file)
@@ -29,7 +29,22 @@ const (
 func main() {
        rand.Seed(time.Now().UnixNano())
 
-       ctx, cancelRoot := context.WithCancel(context.Background())
+       ctx, cancel := context.WithCancel(context.Background())
+
+       // TODO: implement graceful shutdown by closing context
+       // c := make(chan os.Signal, 1)
+       // signal.Notify(c, os.Interrupt)
+       // defer func() {
+       //      signal.Stop(c)
+       //      cancel()
+       // }()
+       // go func() {
+       //      select {
+       //      case <-c:
+       //              cancel()
+       //      case <-ctx.Done():
+       //      }
+       // }()
 
        basePort := flag.Int("bind", defaultPort, "Bind address and port")
        statsPort := flag.String("stats-port", "", "Enable stats HTTP endpoint on address and port")
@@ -80,7 +95,7 @@ func main() {
                logger.Fatalw("Configuration directory not found", "path", configDir)
        }
 
-       srv, err := hotline.NewServer(*configDir, "", *basePort, logger, &hotline.OSFileStore{})
+       srv, err := hotline.NewServer(*configDir, *basePort, logger, &hotline.OSFileStore{})
        if err != nil {
                logger.Fatal(err)
        }
@@ -99,7 +114,7 @@ func main() {
        }
 
        // Serve Hotline requests until program exit
-       logger.Fatal(srv.ListenAndServe(ctx, cancelRoot))
+       logger.Fatal(srv.ListenAndServe(ctx, cancel))
 }
 
 type statHandler struct {
index a7a3ad97b9a7d06fd6fe84c1fde7d691555928ac..380c39a4a1632f9586cc44ee0f5b48d39503433d 100644 (file)
@@ -11,3 +11,4 @@ NewsDateFormat: ""
 MaxDownloads: 0
 MaxDownloadsPerClient: 0
 MaxConnectionsPerIP: 0
+PreserveResourceForks: true
diff --git a/go.sum b/go.sum
index 1e5b05d8778982389231d4c03dd0e75d3cee416e..a210380840c012913d6dd0d9be0a69ca739e1996 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -44,6 +44,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/ryboe/q v1.0.16 h1:BIRQwmpdD/YE4HTI8ZDIaRTx+m7Qk3WCVlEcDCHQ5U0=
+github.com/ryboe/q v1.0.16/go.mod h1:27Qxobs9LgGMjiUKOnVMUT6IlHKsUwjjh0HeXrsY3Kg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
index 69bf53ee62a17cbbffb53101c7d74b9684521fce..f2e904d3eeb3fbe67d9b7c518a15b4c3eb54ff67 100644 (file)
@@ -6,8 +6,6 @@ import (
 )
 
 const (
-       accessAlwaysAllow = -1 // Some transactions are always allowed
-
        // File System Maintenance
        accessDeleteFile   = 0
        accessUploadFile   = 1
@@ -37,13 +35,13 @@ const (
        accessUploadAnywhere = 25
        // accessAnyName          = 26
        // accessNoAgreement      = 27
-       // accessSetFileComment   = 28
-       // accessSetFolderComment = 29
-       accessViewDropBoxes = 30
-       accessMakeAlias     = 31
-       accessBroadcast     = 32
-       accessNewsDeleteArt = 33
-       accessNewsCreateCat = 34
+       accessSetFileComment   = 28
+       accessSetFolderComment = 29
+       accessViewDropBoxes    = 30
+       accessMakeAlias        = 31
+       accessBroadcast        = 32
+       accessNewsDeleteArt    = 33
+       accessNewsCreateCat    = 34
        // accessNewsDeleteCat    = 35
        accessNewsCreateFldr = 36
        // accessNewsDeleteFldr   = 37
@@ -58,9 +56,6 @@ func (bits *accessBitmap) Set(i int) {
 // authorize checks if 64 bit access slice contain has accessBit set
 // TODO: refactor to use accessBitmap type
 func authorize(access *[]byte, accessBit int) bool {
-       if accessBit == accessAlwaysAllow {
-               return true
-       }
        bits := big.NewInt(int64(binary.BigEndian.Uint64(*access)))
 
        return bits.Bit(63-accessBit) == 1
index 367656027ebacaf475d4242f0527fd08d92ef13d..3a6584af9384c5239efeae6d1246ee0c0923f71b 100644 (file)
@@ -119,7 +119,7 @@ func (db *DebugBuffer) Write(p []byte) (int, error) {
        return db.TextView.Write(p)
 }
 
-// Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
+// Sync is a noop function that dataFile to satisfy the zapcore.WriteSyncer interface
 func (db *DebugBuffer) Sync() error {
        return nil
 }
index c1ae4e3c8fb9f5f4fb0d5b4bb09cffaba2e30da3..a8ace87fe42c65cfa04c0e95923b1e5b60cf963e 100644 (file)
@@ -5,6 +5,7 @@ import (
        "golang.org/x/crypto/bcrypt"
        "io"
        "math/big"
+       "sort"
 )
 
 type byClientID []*ClientConn
@@ -164,9 +165,9 @@ func (cc *ClientConn) Authorize(access int) bool {
                return true
        }
 
-       accessBitmap := big.NewInt(int64(binary.BigEndian.Uint64(*cc.Account.Access)))
+       i := big.NewInt(int64(binary.BigEndian.Uint64(*cc.Account.Access)))
 
-       return accessBitmap.Bit(63-access) == 1
+       return i.Bit(63-access) == 1
 }
 
 // Disconnect notifies other clients that a client has disconnected
@@ -222,3 +223,13 @@ func (cc *ClientConn) NewErrReply(t *Transaction, errMsg string) Transaction {
                },
        }
 }
+
+// sortedClients is a utility function that takes a map of *ClientConn and returns a sorted slice of the values.
+// The purpose of this is to ensure that the ordering of client connections is deterministic so that test assertions work.
+func sortedClients(unsortedClients map[uint16]*ClientConn) (clients []*ClientConn) {
+       for _, c := range unsortedClients {
+               clients = append(clients, c)
+       }
+       sort.Sort(byClientID(clients))
+       return clients
+}
index cd1d850713280b35562cf3e450e0a227d61b4ec9..55d68e9ad3db153fdf6c9c50a176d34c220c6578 100644 (file)
@@ -12,4 +12,5 @@ type Config struct {
        MaxDownloads              int      `yaml:"MaxDownloads"`                            // Global simultaneous download limit
        MaxDownloadsPerClient     int      `yaml:"MaxDownloadsPerClient"`                   // Per client simultaneous download limit
        MaxConnectionsPerIP       int      `yaml:"MaxConnectionsPerIP"`                     // Max connections per IP
+       PreserveResourceForks     bool     `yaml:"PreserveResourceForks"`                   // Enable preservation of file info and resource forks in sidecar files
 }
index d9add8cc6a4f16142c3c6063279a91af68a491f0..f230856d27e57c366da4860ae629ad02e340412c 100644 (file)
@@ -12,7 +12,7 @@ type FileNameWithInfo struct {
 
 // fileNameWithInfoHeader contains the fixed length fields of FileNameWithInfo
 type fileNameWithInfoHeader struct {
-       Type       [4]byte // file type code
+       Type       [4]byte // File type code
        Creator    [4]byte // File creator code
        FileSize   [4]byte // File Size in bytes
        RSVD       [4]byte
@@ -49,4 +49,3 @@ func (f *FileNameWithInfo) UnmarshalBinary(data []byte) error {
 
        return err
 }
-
index 3ff243557f9b96a69a2d5be6a952fc48f1f03614..cdd95b9a33149f588378cec2c2014574dea4d108 100644 (file)
@@ -87,15 +87,6 @@ func (fp *FilePath) String() string {
        return path.Join(out...)
 }
 
-func ReadFilePath(filePathFieldData []byte) string {
-       var fp FilePath
-       err := fp.UnmarshalBinary(filePathFieldData)
-       if err != nil {
-               // TODO
-       }
-       return fp.String()
-}
-
 func readPath(fileRoot string, filePath, fileName []byte) (fullPath string, err error) {
        var fp FilePath
        if filePath != nil {
index 2ba9d7af6b9e3ea8444f1ff4a74c4d60b1e62292..3c46faeb2d8e115eab6d70bc719194e7366bb383 100644 (file)
@@ -4,19 +4,21 @@ import (
        "github.com/stretchr/testify/mock"
        "io/fs"
        "os"
+       "time"
 )
 
 type FileStore interface {
+       Create(name string) (*os.File, error)
        Mkdir(name string, perm os.FileMode) error
-       Stat(name string) (os.FileInfo, error)
        Open(name string) (*os.File, error)
-       Symlink(oldname, newname string) error
+       OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error)
        Remove(name string) error
-       Create(name string) (*os.File, error)
+       RemoveAll(path string) error
+       Rename(oldpath string, newpath string) error
+       Stat(name string) (fs.FileInfo, error)
+       Symlink(oldname, newname string) error
        WriteFile(name string, data []byte, perm fs.FileMode) error
-       // TODO: implement these
-       // Rename(oldpath string, newpath string) error
-       // RemoveAll(path string) error
+       ReadFile(name string) ([]byte, error)
 }
 
 type OSFileStore struct{}
@@ -37,6 +39,10 @@ func (fs *OSFileStore) Symlink(oldname, newname string) error {
        return os.Symlink(oldname, newname)
 }
 
+func (fs *OSFileStore) RemoveAll(name string) error {
+       return os.RemoveAll(name)
+}
+
 func (fs *OSFileStore) Remove(name string) error {
        return os.Remove(name)
 }
@@ -49,6 +55,18 @@ func (fs *OSFileStore) WriteFile(name string, data []byte, perm fs.FileMode) err
        return os.WriteFile(name, data, perm)
 }
 
+func (fs *OSFileStore) Rename(oldpath string, newpath string) error {
+       return os.Rename(oldpath, newpath)
+}
+
+func (fs *OSFileStore) ReadFile(name string) ([]byte, error) {
+       return os.ReadFile(name)
+}
+
+func (fs *OSFileStore) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
+       return os.OpenFile(name, flag, perm)
+}
+
 type MockFileStore struct {
        mock.Mock
 }
@@ -72,11 +90,21 @@ func (mfs *MockFileStore) Open(name string) (*os.File, error) {
        return args.Get(0).(*os.File), args.Error(1)
 }
 
+func (mfs *MockFileStore) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
+       args := mfs.Called(name, flag, perm)
+       return args.Get(0).(*os.File), args.Error(1)
+}
+
 func (mfs *MockFileStore) Symlink(oldname, newname string) error {
        args := mfs.Called(oldname, newname)
        return args.Error(0)
 }
 
+func (mfs *MockFileStore) RemoveAll(name string) error {
+       args := mfs.Called(name)
+       return args.Error(0)
+}
+
 func (mfs *MockFileStore) Remove(name string) error {
        args := mfs.Called(name)
        return args.Error(0)
@@ -91,3 +119,47 @@ func (mfs *MockFileStore) WriteFile(name string, data []byte, perm fs.FileMode)
        args := mfs.Called(name, data, perm)
        return args.Error(0)
 }
+
+func (mfs *MockFileStore) Rename(oldpath, newpath string) error {
+       args := mfs.Called(oldpath, newpath)
+       return args.Error(0)
+}
+
+func (mfs *MockFileStore) ReadFile(name string) ([]byte, error) {
+       args := mfs.Called(name)
+       return args.Get(0).([]byte), args.Error(1)
+}
+
+type MockFileInfo struct {
+       mock.Mock
+}
+
+func (mfi *MockFileInfo) Name() string {
+       args := mfi.Called()
+       return args.String(0)
+}
+
+func (mfi *MockFileInfo) Size() int64 {
+       args := mfi.Called()
+       return args.Get(0).(int64)
+}
+
+func (mfi *MockFileInfo) Mode() fs.FileMode {
+       args := mfi.Called()
+       return args.Get(0).(fs.FileMode)
+}
+
+func (mfi *MockFileInfo) ModTime() time.Time {
+       _ = mfi.Called()
+       return time.Now()
+}
+
+func (mfi *MockFileInfo) IsDir() bool {
+       args := mfi.Called()
+       return args.Bool(0)
+}
+
+func (mfi *MockFileInfo) Sys() interface{} {
+       _ = mfi.Called()
+       return nil
+}
index e9f45bb59ce1e5653cf6f48d319fe462029bd2eb..216232384936bfcc535933235069e67f05b0b9a0 100644 (file)
@@ -63,9 +63,12 @@ var fileTypes = map[string]fileType{
 
 // A small number of type codes are displayed in the GetInfo window with a friendly name instead of the 4 letter code
 var friendlyCreatorNames = map[string]string{
+       "APPL": "Application Program",
+       "HTbm": "Hotline Bookmark",
        "fldr": "Folder",
        "flda": "Folder Alias",
        "HTft": "Incomplete File",
        "SIT!": "StuffIt Archive",
        "TEXT": "Text File",
+       "HTLC": "Hotline",
 }
diff --git a/hotline/file_wrapper.go b/hotline/file_wrapper.go
new file mode 100644 (file)
index 0000000..3773e79
--- /dev/null
@@ -0,0 +1,297 @@
+package hotline
+
+import (
+       "encoding/binary"
+       "errors"
+       "fmt"
+       "io"
+       "io/fs"
+       "os"
+       "path"
+       "strings"
+)
+
+const (
+       incompleteFileSuffix = ".incomplete"
+       infoForkNameTemplate = "%s.info_%s" // template string for info fork filenames
+       rsrcForkNameTemplate = "%s.rsrc_%s" // template string for resource fork filenames
+)
+
+// fileWrapper encapsulates the data, info, and resource forks of a Hotline file and provides methods to manage the files.
+type fileWrapper struct {
+       fs             FileStore
+       name           string // name of the file
+       path           string // path to file directory
+       dataPath       string // path to the file data fork
+       dataOffset     int64
+       rsrcPath       string // path to the file resource fork
+       infoPath       string // path to the file information fork
+       incompletePath string // path to partially transferred temp file
+       saveMetaData   bool   // if true, enables saving of info and resource forks in sidecar files
+       infoFork       *FlatFileInformationFork
+       ffo            *flattenedFileObject
+}
+
+func newFileWrapper(fs FileStore, path string, dataOffset int64) (*fileWrapper, error) {
+       pathSegs := strings.Split(path, pathSeparator)
+       dir := strings.Join(pathSegs[:len(pathSegs)-1], pathSeparator)
+       fName := pathSegs[len(pathSegs)-1]
+       f := fileWrapper{
+               fs:             fs,
+               name:           fName,
+               path:           dir,
+               dataPath:       path,
+               dataOffset:     dataOffset,
+               rsrcPath:       fmt.Sprintf(rsrcForkNameTemplate, dir+"/", fName),
+               infoPath:       fmt.Sprintf(infoForkNameTemplate, dir+"/", fName),
+               incompletePath: dir + "/" + fName + incompleteFileSuffix,
+               ffo:            &flattenedFileObject{},
+       }
+
+       var err error
+       f.ffo, err = f.flattenedFileObject()
+       if err != nil {
+               return nil, err
+       }
+
+       return &f, nil
+}
+
+func (f *fileWrapper) totalSize() []byte {
+       var s int64
+       size := make([]byte, 4)
+
+       info, err := f.fs.Stat(f.dataPath)
+       if err == nil {
+               s += info.Size() - f.dataOffset
+       }
+
+       info, err = f.fs.Stat(f.rsrcPath)
+       if err == nil {
+               s += info.Size()
+       }
+
+       binary.BigEndian.PutUint32(size, uint32(s))
+
+       return size
+}
+
+func (f *fileWrapper) rsrcForkSize() (s [4]byte) {
+       info, err := f.fs.Stat(f.rsrcPath)
+       if err != nil {
+               return s
+       }
+
+       binary.BigEndian.PutUint32(s[:], uint32(info.Size()))
+       return s
+}
+
+func (f *fileWrapper) rsrcForkHeader() FlatFileForkHeader {
+       return FlatFileForkHeader{
+               ForkType:        [4]byte{0x4D, 0x41, 0x43, 0x52}, // "MACR"
+               CompressionType: [4]byte{},
+               RSVD:            [4]byte{},
+               DataSize:        f.rsrcForkSize(),
+       }
+}
+
+func (f *fileWrapper) incompleteDataName() string {
+       return f.name + incompleteFileSuffix
+}
+
+func (f *fileWrapper) rsrcForkName() string {
+       return fmt.Sprintf(rsrcForkNameTemplate, "", f.name)
+}
+
+func (f *fileWrapper) infoForkName() string {
+       return fmt.Sprintf(infoForkNameTemplate, "", f.name)
+}
+
+func (f *fileWrapper) creatorCode() []byte {
+       if f.ffo.FlatFileInformationFork.CreatorSignature != nil {
+               return f.infoFork.CreatorSignature
+       }
+       return []byte(fileTypeFromFilename(f.name).CreatorCode)
+}
+
+func (f *fileWrapper) typeCode() []byte {
+       if f.infoFork != nil {
+               return f.infoFork.TypeSignature
+       }
+       return []byte(fileTypeFromFilename(f.name).TypeCode)
+}
+
+func (f *fileWrapper) rsrcForkWriter() (io.Writer, error) {
+       file, err := os.OpenFile(f.rsrcPath, os.O_CREATE|os.O_WRONLY, 0644)
+       if err != nil {
+               return nil, err
+       }
+
+       return file, nil
+}
+
+func (f *fileWrapper) infoForkWriter() (io.Writer, error) {
+       file, err := os.OpenFile(f.infoPath, os.O_CREATE|os.O_WRONLY, 0644)
+       if err != nil {
+               return nil, err
+       }
+
+       return file, nil
+}
+
+func (f *fileWrapper) incFileWriter() (io.Writer, error) {
+       file, err := os.OpenFile(f.incompletePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+       if err != nil {
+               return nil, err
+       }
+
+       return file, nil
+}
+
+func (f *fileWrapper) dataForkReader() (io.Reader, error) {
+       return f.fs.Open(f.dataPath)
+}
+
+func (f *fileWrapper) rsrcForkFile() (*os.File, error) {
+       return f.fs.Open(f.rsrcPath)
+}
+
+func (f *fileWrapper) dataFile() (os.FileInfo, error) {
+       if fi, err := f.fs.Stat(f.dataPath); err == nil {
+               return fi, nil
+       }
+       if fi, err := f.fs.Stat(f.incompletePath); err == nil {
+               return fi, nil
+       }
+
+       return nil, errors.New("file or directory not found")
+}
+
+// move a fileWrapper and its associated metadata files to newPath
+func (f *fileWrapper) move(newPath string) error {
+       err := f.fs.Rename(f.dataPath, path.Join(newPath, f.name))
+       if err != nil {
+               // TODO
+       }
+
+       err = f.fs.Rename(f.incompletePath, path.Join(newPath, f.incompleteDataName()))
+       if err != nil {
+               // TODO
+       }
+
+       err = f.fs.Rename(f.rsrcPath, path.Join(newPath, f.rsrcForkName()))
+       if err != nil {
+               // TODO
+       }
+
+       err = f.fs.Rename(f.infoPath, path.Join(newPath, f.infoForkName()))
+       if err != nil {
+               // TODO
+       }
+
+       return nil
+}
+
+// delete a fileWrapper and its associated metadata files if they exist
+func (f *fileWrapper) delete() error {
+       err := f.fs.RemoveAll(f.dataPath)
+       if err != nil {
+               // TODO
+       }
+
+       err = f.fs.Remove(f.incompletePath)
+       if err != nil {
+               // TODO
+       }
+
+       err = f.fs.Remove(f.rsrcPath)
+       if err != nil {
+               // TODO
+       }
+
+       err = f.fs.Remove(f.infoPath)
+       if err != nil {
+               // TODO
+       }
+
+       return nil
+}
+
+func (f *fileWrapper) flattenedFileObject() (*flattenedFileObject, error) {
+       dataSize := make([]byte, 4)
+       mTime := make([]byte, 8)
+
+       ft := defaultFileType
+
+       fileInfo, err := f.fs.Stat(f.dataPath)
+       if err != nil && !errors.Is(err, fs.ErrNotExist) {
+               return nil, err
+       }
+       if errors.Is(err, fs.ErrNotExist) {
+               fileInfo, err = f.fs.Stat(f.incompletePath)
+               if err == nil {
+                       mTime = toHotlineTime(fileInfo.ModTime())
+                       binary.BigEndian.PutUint32(dataSize, uint32(fileInfo.Size()-f.dataOffset))
+                       ft, _ = fileTypeFromInfo(fileInfo)
+               }
+       } else {
+               mTime = toHotlineTime(fileInfo.ModTime())
+               binary.BigEndian.PutUint32(dataSize, uint32(fileInfo.Size()-f.dataOffset))
+               ft, _ = fileTypeFromInfo(fileInfo)
+       }
+
+       f.ffo.FlatFileHeader = FlatFileHeader{
+               Format:    [4]byte{0x46, 0x49, 0x4c, 0x50}, // "FILP"
+               Version:   [2]byte{0, 1},
+               RSVD:      [16]byte{},
+               ForkCount: [2]byte{0, 2},
+       }
+
+       _, err = f.fs.Stat(f.infoPath)
+       if err == nil {
+               b, err := f.fs.ReadFile(f.infoPath)
+               if err != nil {
+                       return nil, err
+               }
+
+               f.ffo.FlatFileHeader.ForkCount[1] = 3
+
+               if err := f.ffo.FlatFileInformationFork.UnmarshalBinary(b); err != nil {
+                       return nil, err
+               }
+       } else {
+               f.ffo.FlatFileInformationFork = FlatFileInformationFork{
+                       Platform:         []byte("AMAC"), // TODO: Remove hardcode to support "AWIN" Platform (maybe?)
+                       TypeSignature:    []byte(ft.TypeCode),
+                       CreatorSignature: []byte(ft.CreatorCode),
+                       Flags:            []byte{0, 0, 0, 0},
+                       PlatformFlags:    []byte{0, 0, 1, 0}, // TODO: What is this?
+                       RSVD:             make([]byte, 32),
+                       CreateDate:       mTime, // some filesystems don't support createTime
+                       ModifyDate:       mTime,
+                       NameScript:       []byte{0, 0},
+                       Name:             []byte(f.name),
+                       NameSize:         []byte{0, 0},
+                       CommentSize:      []byte{0, 0},
+                       Comment:          []byte{},
+               }
+               binary.BigEndian.PutUint16(f.ffo.FlatFileInformationFork.NameSize, uint16(len(f.name)))
+       }
+
+       f.ffo.FlatFileInformationForkHeader = FlatFileForkHeader{
+               ForkType:        [4]byte{0x49, 0x4E, 0x46, 0x4F}, // "INFO"
+               CompressionType: [4]byte{},
+               RSVD:            [4]byte{},
+               DataSize:        f.ffo.FlatFileInformationFork.Size(),
+       }
+
+       f.ffo.FlatFileDataForkHeader = FlatFileForkHeader{
+               ForkType:        [4]byte{0x44, 0x41, 0x54, 0x41}, // "DATA"
+               CompressionType: [4]byte{},
+               RSVD:            [4]byte{},
+               DataSize:        [4]byte{dataSize[0], dataSize[1], dataSize[2], dataSize[3]},
+       }
+       f.ffo.FlatFileResForkHeader = f.rsrcForkHeader()
+
+       return f.ffo, nil
+}
index 4f55eba13f29946a2cec4feed35040271e4276f2..b66580fcfef1d5e119823ce842406c2935f54e82 100644 (file)
@@ -10,8 +10,6 @@ import (
        "strings"
 )
 
-const incompleteFileSuffix = ".incomplete"
-
 func downcaseFileExtension(filename string) string {
        splitStr := strings.Split(filename, ".")
        ext := strings.ToLower(
@@ -29,7 +27,7 @@ func fileTypeFromFilename(fn string) fileType {
        return defaultFileType
 }
 
-func fileTypeFromInfo(info os.FileInfo) (ft fileType, err error) {
+func fileTypeFromInfo(info fs.FileInfo) (ft fileType, err error) {
        if info.IsDir() {
                ft.CreatorCode = "n/a "
                ft.TypeCode = "fldr"
@@ -41,7 +39,7 @@ func fileTypeFromInfo(info os.FileInfo) (ft fileType, err error) {
 }
 
 func getFileNameList(filePath string) (fields []Field, err error) {
-       files, err := ioutil.ReadDir(filePath)
+       files, err := os.ReadDir(filePath)
        if err != nil {
                return fields, nil
        }
@@ -49,9 +47,18 @@ func getFileNameList(filePath string) (fields []Field, err error) {
        for _, file := range files {
                var fnwi FileNameWithInfo
 
+               if strings.HasPrefix(file.Name(), ".") {
+                       continue
+               }
+
                fileCreator := make([]byte, 4)
 
-               if file.Mode()&os.ModeSymlink != 0 {
+               fileInfo, err := file.Info()
+               if err != nil {
+                       return fields, err
+               }
+
+               if fileInfo.Mode()&os.ModeSymlink != 0 {
                        resolvedPath, err := os.Readlink(filePath + "/" + file.Name())
                        if err != nil {
                                return fields, err
@@ -70,7 +77,15 @@ func getFileNameList(filePath string) (fields []Field, err error) {
                                if err != nil {
                                        return fields, err
                                }
-                               binary.BigEndian.PutUint32(fnwi.FileSize[:], uint32(len(dir)))
+
+                               var c uint32
+                               for _, f := range dir {
+                                       if !strings.HasPrefix(f.Name(), ".") {
+                                               c += 1
+                                       }
+                               }
+
+                               binary.BigEndian.PutUint32(fnwi.FileSize[:], c)
                                copy(fnwi.Type[:], []byte("fldr")[:])
                                copy(fnwi.Creator[:], fileCreator[:])
                        } else {
@@ -84,17 +99,31 @@ func getFileNameList(filePath string) (fields []Field, err error) {
                        if err != nil {
                                return fields, err
                        }
-                       binary.BigEndian.PutUint32(fnwi.FileSize[:], uint32(len(dir)))
+
+                       var c uint32
+                       for _, f := range dir {
+                               if !strings.HasPrefix(f.Name(), ".") {
+                                       c += 1
+                               }
+                       }
+
+                       binary.BigEndian.PutUint32(fnwi.FileSize[:], c)
                        copy(fnwi.Type[:], []byte("fldr")[:])
                        copy(fnwi.Creator[:], fileCreator[:])
                } else {
-                       // the Hotline protocol does not support file sizes > 4GiB due to the 4 byte field size, so skip them
-                       if file.Size() > 4294967296 {
+                       // the Hotline protocol does not support fileWrapper sizes > 4GiB due to the 4 byte field size, so skip them
+                       if fileInfo.Size() > 4294967296 {
                                continue
                        }
-                       binary.BigEndian.PutUint32(fnwi.FileSize[:], uint32(file.Size()))
-                       copy(fnwi.Type[:], []byte(fileTypeFromFilename(file.Name()).TypeCode)[:])
-                       copy(fnwi.Creator[:], []byte(fileTypeFromFilename(file.Name()).CreatorCode)[:])
+
+                       hlFile, err := newFileWrapper(&OSFileStore{}, filePath+"/"+file.Name(), 0)
+                       if err != nil {
+                               return nil, err
+                       }
+
+                       copy(fnwi.FileSize[:], hlFile.totalSize()[:])
+                       copy(fnwi.Type[:], hlFile.ffo.FlatFileInformationFork.TypeSignature[:])
+                       copy(fnwi.Creator[:], hlFile.ffo.FlatFileInformationFork.CreatorSignature[:])
                }
 
                strippedName := strings.Replace(file.Name(), ".incomplete", "", -1)
@@ -143,12 +172,14 @@ func CalcTotalSize(filePath string) ([]byte, error) {
 func CalcItemCount(filePath string) ([]byte, error) {
        var itemcount uint16
        err := filepath.Walk(filePath, func(path string, info os.FileInfo, err error) error {
-               itemcount += 1
-
                if err != nil {
                        return err
                }
 
+               if !strings.HasPrefix(info.Name(), ".") {
+                       itemcount += 1
+               }
+
                return nil
        })
        if err != nil {
@@ -178,19 +209,3 @@ func EncodeFilePath(filePath string) []byte {
 
        return bytes
 }
-
-// effectiveFile wraps os.Open to check for the presence of a partial file transfer as a fallback
-func effectiveFile(filePath string) (*os.File, error) {
-       file, err := os.Open(filePath)
-       if err != nil && !errors.Is(err, fs.ErrNotExist) {
-               return nil, err
-       }
-
-       if errors.Is(err, fs.ErrNotExist) {
-               file, err = os.OpenFile(filePath+incompleteFileSuffix, os.O_APPEND|os.O_WRONLY, 0644)
-               if err != nil {
-                       return nil, err
-               }
-       }
-       return file, nil
-}
index 6b8d3d9f788a948416eda05d0a8f1fd7c5b6801d..6ccd9d7071000e456b2078c5d71414206d422906 100644 (file)
@@ -2,15 +2,15 @@ package hotline
 
 import (
        "encoding/binary"
-       "os"
+       "io"
 )
 
 type flattenedFileObject struct {
        FlatFileHeader                FlatFileHeader
-       FlatFileInformationForkHeader FlatFileInformationForkHeader
+       FlatFileInformationForkHeader FlatFileForkHeader
        FlatFileInformationFork       FlatFileInformationFork
-       FlatFileDataForkHeader        FlatFileDataForkHeader
-       FileData                      []byte
+       FlatFileDataForkHeader        FlatFileForkHeader
+       FlatFileResForkHeader         FlatFileForkHeader
 }
 
 // FlatFileHeader is the first section of a "Flattened File Object".  All fields have static values.
@@ -18,25 +18,7 @@ type FlatFileHeader struct {
        Format    [4]byte  // Always "FILP"
        Version   [2]byte  // Always 1
        RSVD      [16]byte // Always empty zeros
-       ForkCount [2]byte  // Number of forks
-}
-
-// NewFlatFileHeader returns a FlatFileHeader struct
-func NewFlatFileHeader() FlatFileHeader {
-       return FlatFileHeader{
-               Format:    [4]byte{0x46, 0x49, 0x4c, 0x50}, // FILP
-               Version:   [2]byte{0, 1},
-               RSVD:      [16]byte{},
-               ForkCount: [2]byte{0, 2},
-       }
-}
-
-// FlatFileInformationForkHeader is the second section of a "Flattened File Object"
-type FlatFileInformationForkHeader struct {
-       ForkType        [4]byte // Always "INFO"
-       CompressionType [4]byte // Always 0; Compression was never implemented in the Hotline protocol
-       RSVD            [4]byte // Always zeros
-       DataSize        [4]byte // Size of the flat file information fork
+       ForkCount [2]byte  // Number of forks, either 2 or 3 if there is a resource fork
 }
 
 type FlatFileInformationFork struct {
@@ -48,10 +30,10 @@ type FlatFileInformationFork struct {
        RSVD             []byte
        CreateDate       []byte
        ModifyDate       []byte
-       NameScript       []byte // TODO: what is this?
+       NameScript       []byte
        NameSize         []byte // Length of file name (Maximum 128 characters)
        Name             []byte // File name
-       CommentSize      []byte // Length of file comment
+       CommentSize      []byte // Length of the comment
        Comment          []byte // File comment
 }
 
@@ -73,34 +55,63 @@ func NewFlatFileInformationFork(fileName string, modifyTime []byte, typeSignatur
 }
 
 func (ffif *FlatFileInformationFork) friendlyType() []byte {
-
        if name, ok := friendlyCreatorNames[string(ffif.TypeSignature)]; ok {
                return []byte(name)
        }
+       return ffif.TypeSignature
+}
+
+func (ffif *FlatFileInformationFork) friendlyCreator() []byte {
+       if name, ok := friendlyCreatorNames[string(ffif.CreatorSignature)]; ok {
+               return []byte(name)
+       }
        return ffif.CreatorSignature
 }
 
+func (ffif *FlatFileInformationFork) setComment(comment []byte) error {
+       ffif.Comment = comment
+       binary.BigEndian.PutUint16(ffif.CommentSize, uint16(len(comment)))
+
+       // TODO: return err if comment is too long
+       return nil
+}
+
 // DataSize calculates the size of the flat file information fork, which is
 // 72 bytes for the fixed length fields plus the length of the Name + Comment
 func (ffif *FlatFileInformationFork) DataSize() []byte {
        size := make([]byte, 4)
 
-       // TODO: Can I do math directly on two byte slices?
-       dataSize := len(ffif.Name) + len(ffif.Comment) + 74
+       dataSize := len(ffif.Name) + len(ffif.Comment) + 74 // 74 = len of fixed size headers
 
        binary.BigEndian.PutUint32(size, uint32(dataSize))
 
        return size
 }
 
-func (ffo *flattenedFileObject) TransferSize() []byte {
+func (ffif *FlatFileInformationFork) Size() [4]byte {
+       size := [4]byte{}
+
+       dataSize := len(ffif.Name) + len(ffif.Comment) + 74 // 74 = len of fixed size headers
+
+       binary.BigEndian.PutUint32(size[:], uint32(dataSize))
+
+       return size
+}
+
+func (ffo *flattenedFileObject) TransferSize(offset int64) []byte {
+       // get length of the flattenedFileObject, including the info fork
        payloadSize := len(ffo.BinaryMarshal())
+
+       // length of data fork
        dataSize := binary.BigEndian.Uint32(ffo.FlatFileDataForkHeader.DataSize[:])
 
-       transferSize := make([]byte, 4)
-       binary.BigEndian.PutUint32(transferSize, dataSize+uint32(payloadSize))
+       // length of resource fork
+       resForkSize := binary.BigEndian.Uint32(ffo.FlatFileResForkHeader.DataSize[:])
+
+       size := make([]byte, 4)
+       binary.BigEndian.PutUint32(size[:], dataSize+resForkSize+uint32(payloadSize)-uint32(offset))
 
-       return transferSize
+       return size
 }
 
 func (ffif *FlatFileInformationFork) ReadNameSize() []byte {
@@ -110,13 +121,32 @@ func (ffif *FlatFileInformationFork) ReadNameSize() []byte {
        return size
 }
 
-type FlatFileDataForkHeader struct {
-       ForkType        [4]byte
+type FlatFileForkHeader struct {
+       ForkType        [4]byte // Either INFO, DATA or MACR
        CompressionType [4]byte
        RSVD            [4]byte
        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...)
+
+       return b
+}
+
 func (ffif *FlatFileInformationFork) UnmarshalBinary(b []byte) error {
        nameSize := b[70:72]
        bs := binary.BigEndian.Uint16(nameSize)
@@ -147,72 +177,72 @@ func (ffif *FlatFileInformationFork) UnmarshalBinary(b []byte) error {
        return nil
 }
 
-func (f *flattenedFileObject) BinaryMarshal() []byte {
+func (ffo *flattenedFileObject) BinaryMarshal() []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, 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, f.FlatFileInformationFork.DataSize()...)
-
-       out = append(out, f.FlatFileInformationFork.Platform...)
-       out = append(out, f.FlatFileInformationFork.TypeSignature...)
-       out = append(out, f.FlatFileInformationFork.CreatorSignature...)
-       out = append(out, f.FlatFileInformationFork.Flags...)
-       out = append(out, f.FlatFileInformationFork.PlatformFlags...)
-       out = append(out, f.FlatFileInformationFork.RSVD...)
-       out = append(out, f.FlatFileInformationFork.CreateDate...)
-       out = append(out, f.FlatFileInformationFork.ModifyDate...)
-       out = append(out, f.FlatFileInformationFork.NameScript...)
-       out = append(out, f.FlatFileInformationFork.ReadNameSize()...)
-       out = append(out, f.FlatFileInformationFork.Name...)
-       out = append(out, f.FlatFileInformationFork.CommentSize...)
-       out = append(out, f.FlatFileInformationFork.Comment...)
-
-       out = append(out, f.FlatFileDataForkHeader.ForkType[:]...)
-       out = append(out, f.FlatFileDataForkHeader.CompressionType[:]...)
-       out = append(out, f.FlatFileDataForkHeader.RSVD[:]...)
-       out = append(out, f.FlatFileDataForkHeader.DataSize[:]...)
+       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
 }
 
-func NewFlattenedFileObject(fileRoot string, filePath, fileName []byte, dataOffset int64) (*flattenedFileObject, error) {
-       fullFilePath, err := readPath(fileRoot, filePath, fileName)
-       if err != nil {
-               return nil, err
+func (ffo *flattenedFileObject) ReadFrom(r io.Reader) (int, error) {
+       var n int
+
+       if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileHeader); err != nil {
+               return n, err
        }
-       file, err := effectiveFile(fullFilePath)
-       if err != nil {
-               return nil, err
+
+       if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileInformationForkHeader); err != nil {
+               return n, err
        }
 
-       defer func(file *os.File) { _ = file.Close() }(file)
+       dataLen := binary.BigEndian.Uint32(ffo.FlatFileInformationForkHeader.DataSize[:])
+       ffifBuf := make([]byte, dataLen)
+       if _, err := io.ReadFull(r, ffifBuf); err != nil {
+               return n, err
+       }
 
-       fileInfo, err := file.Stat()
-       if err != nil {
-               return nil, err
+       if err := ffo.FlatFileInformationFork.UnmarshalBinary(ffifBuf); err != nil {
+               return n, err
        }
 
-       dataSize := make([]byte, 4)
-       binary.BigEndian.PutUint32(dataSize, uint32(fileInfo.Size()-dataOffset))
+       if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileDataForkHeader); err != nil {
+               return n, err
+       }
 
-       mTime := toHotlineTime(fileInfo.ModTime())
+       return n, nil
+}
 
-       ft, _ := fileTypeFromInfo(fileInfo)
+func (ffo *flattenedFileObject) dataSize() int64 {
+       return int64(binary.BigEndian.Uint32(ffo.FlatFileDataForkHeader.DataSize[:]))
+}
 
-       return &flattenedFileObject{
-               FlatFileHeader:          NewFlatFileHeader(),
-               FlatFileInformationFork: NewFlatFileInformationFork(string(fileName), mTime, ft.TypeCode, ft.CreatorCode),
-               FlatFileDataForkHeader: FlatFileDataForkHeader{
-                       ForkType:        [4]byte{0x44, 0x41, 0x54, 0x41}, // "DATA"
-                       CompressionType: [4]byte{},
-                       RSVD:            [4]byte{},
-                       DataSize:        [4]byte{dataSize[0], dataSize[1], dataSize[2], dataSize[3]},
-               },
-       }, nil
+func (ffo *flattenedFileObject) rsrcSize() int64 {
+       return int64(binary.BigEndian.Uint32(ffo.FlatFileResForkHeader.DataSize[:]))
 }
index 4b3fdf65f0b015aa421fa04a3abca7ec8395d98b..596ca2c2151ae9dd979da117000d57c83108cf5a 100644 (file)
@@ -3,69 +3,9 @@ package hotline
 import (
        "fmt"
        "github.com/stretchr/testify/assert"
-       "os"
        "testing"
 )
 
-func TestNewFlattenedFileObject(t *testing.T) {
-       type args struct {
-               fileRoot string
-               filePath []byte
-               fileName []byte
-       }
-       tests := []struct {
-               name    string
-               args    args
-               want    *flattenedFileObject
-               wantErr assert.ErrorAssertionFunc
-       }{
-               {
-                       name: "with valid file",
-                       args: args{
-                               fileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(),
-                               fileName: []byte("testfile.txt"),
-                               filePath: []byte{0, 0},
-                       },
-                       want: &flattenedFileObject{
-                               FlatFileHeader:                NewFlatFileHeader(),
-                               FlatFileInformationForkHeader: FlatFileInformationForkHeader{},
-                               FlatFileInformationFork:       NewFlatFileInformationFork("testfile.txt", make([]byte, 8), "", ""),
-                               FlatFileDataForkHeader: FlatFileDataForkHeader{
-                                       ForkType:        [4]byte{0x4d, 0x41, 0x43, 0x52}, // DATA
-                                       CompressionType: [4]byte{0, 0, 0, 0},
-                                       RSVD:            [4]byte{0, 0, 0, 0},
-                                       DataSize:        [4]byte{0x00, 0x00, 0x00, 0x17},
-                               },
-                               FileData: nil,
-                       },
-                       wantErr: assert.NoError,
-               },
-               {
-                       name: "when file path is invalid",
-                       args: args{
-                               fileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(),
-                               fileName: []byte("nope.txt"),
-                       },
-                       want:    nil,
-                       wantErr: assert.Error,
-               },
-       }
-       for _, tt := range tests {
-               t.Run(tt.name, func(t *testing.T) {
-                       got, err := NewFlattenedFileObject(tt.args.fileRoot, tt.args.filePath, tt.args.fileName, 0)
-                       if tt.wantErr(t, err, fmt.Sprintf("NewFlattenedFileObject(%v, %v, %v)", tt.args.fileRoot, tt.args.filePath, tt.args.fileName)) {
-                               return
-                       }
-
-                       // Clear the file timestamp fields to work around problems running the tests in multiple timezones
-                       // TODO: revisit how to test this by mocking the stat calls
-                       got.FlatFileInformationFork.CreateDate = make([]byte, 8)
-                       got.FlatFileInformationFork.ModifyDate = make([]byte, 8)
-                       assert.Equalf(t, tt.want, got, "NewFlattenedFileObject(%v, %v, %v)", tt.args.fileRoot, tt.args.filePath, tt.args.fileName)
-               })
-       }
-}
-
 func TestFlatFileInformationFork_UnmarshalBinary(t *testing.T) {
        type args struct {
                b []byte
index 39d9cab4d489e5fbd78daa24d71225d9c240f4c9..9ccdc624001975de75aca11971d36c660cfde830 100644 (file)
@@ -14,6 +14,8 @@ type handshake struct {
        SubVersion  [2]byte
 }
 
+var trtp = [4]byte{0x54, 0x52, 0x54, 0x50}
+
 // Handshake
 // After establishing TCP connection, both client and server start the handshake process
 // in order to confirm that each of them comply with requirements of the other.
@@ -33,9 +35,9 @@ type handshake struct {
 // Description         Size    Data    Note
 // Protocol ID         4               TRTP
 // Error code          4                               Error code returned by the server (0 = no error)
-func Handshake(conn io.ReadWriter) error {
+func Handshake(rw io.ReadWriter) error {
        handshakeBuf := make([]byte, 12)
-       if _, err := io.ReadFull(conn, handshakeBuf); err != nil {
+       if _, err := io.ReadFull(rw, handshakeBuf); err != nil {
                return err
        }
 
@@ -45,10 +47,10 @@ func Handshake(conn io.ReadWriter) error {
                return err
        }
 
-       if h.Protocol != [4]byte{0x54, 0x52, 0x54, 0x50} {
+       if h.Protocol != trtp {
                return errors.New("invalid handshake")
        }
 
-       _, err := conn.Write([]byte{84, 82, 84, 80, 0, 0, 0, 0})
+       _, err := rw.Write([]byte{84, 82, 84, 80, 0, 0, 0, 0})
        return err
 }
index 76cc248ba57088ebf950a449c4af9551d49361d2..9df89f2b219ac0a7f24e57c48cbc8a6b2a967cb0 100644 (file)
@@ -1,6 +1,7 @@
 package hotline
 
 import (
+       "bufio"
        "bytes"
        "context"
        "encoding/binary"
@@ -8,6 +9,7 @@ import (
        "fmt"
        "github.com/go-playground/validator/v10"
        "go.uber.org/zap"
+       "gopkg.in/yaml.v3"
        "io"
        "io/fs"
        "io/ioutil"
@@ -18,14 +20,21 @@ import (
        "path"
        "path/filepath"
        "runtime/debug"
-       "sort"
        "strings"
        "sync"
        "time"
-
-       "gopkg.in/yaml.v3"
 )
 
+type contextKey string
+
+var contextKeyReq = contextKey("req")
+
+type requestCtx struct {
+       remoteAddr string
+       login      string
+       name       string
+}
+
 const (
        userIdleSeconds        = 300 // time in seconds before an inactive user is marked idle
        idleCheckInterval      = 10  // time in seconds to check for idle users
@@ -39,7 +48,6 @@ type Server struct {
        Accounts      map[string]*Account
        Agreement     []byte
        Clients       map[uint16]*ClientConn
-       FlatNews      []byte
        ThreadedNews  *ThreadedNews
        FileTransfers map[uint32]*FileTransfer
        Config        *Config
@@ -50,15 +58,13 @@ type Server struct {
        TrackerPassID [4]byte
        Stats         *Stats
 
-       FS FileStore
-
-       // newsReader io.Reader
-       // newsWriter io.WriteCloser
+       FS FileStore // Storage backend to use for File storage
 
        outbox chan Transaction
+       mux    sync.Mutex
 
-       mux         sync.Mutex
        flatNewsMux sync.Mutex
+       FlatNews    []byte
 }
 
 type PrivateChat struct {
@@ -82,7 +88,7 @@ func (s *Server) ListenAndServe(ctx context.Context, cancelRoot context.CancelFu
                        s.Logger.Fatal(err)
                }
 
-               s.Logger.Fatal(s.Serve(ctx, cancelRoot, ln))
+               s.Logger.Fatal(s.Serve(ctx, ln))
        }()
 
        wg.Add(1)
@@ -93,7 +99,7 @@ func (s *Server) ListenAndServe(ctx context.Context, cancelRoot context.CancelFu
 
                }
 
-               s.Logger.Fatal(s.ServeFileTransfers(ln))
+               s.Logger.Fatal(s.ServeFileTransfers(ctx, ln))
        }()
 
        wg.Wait()
@@ -101,7 +107,7 @@ func (s *Server) ListenAndServe(ctx context.Context, cancelRoot context.CancelFu
        return nil
 }
 
-func (s *Server) ServeFileTransfers(ln net.Listener) error {
+func (s *Server) ServeFileTransfers(ctx context.Context, ln net.Listener) error {
        for {
                conn, err := ln.Accept()
                if err != nil {
@@ -109,7 +115,16 @@ func (s *Server) ServeFileTransfers(ln net.Listener) error {
                }
 
                go func() {
-                       if err := s.handleFileTransfer(conn); err != nil {
+                       defer func() { _ = conn.Close() }()
+
+                       err = s.handleFileTransfer(
+                               context.WithValue(ctx, contextKeyReq, requestCtx{
+                                       remoteAddr: conn.RemoteAddr().String(),
+                               }),
+                               conn,
+                       )
+
+                       if err != nil {
                                s.Logger.Errorw("file transfer error", "reason", err)
                        }
                }()
@@ -153,8 +168,7 @@ func (s *Server) sendTransaction(t Transaction) error {
        return nil
 }
 
-func (s *Server) Serve(ctx context.Context, cancelRoot context.CancelFunc, ln net.Listener) error {
-
+func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
        for {
                conn, err := ln.Accept()
                if err != nil {
@@ -172,7 +186,8 @@ func (s *Server) Serve(ctx context.Context, cancelRoot context.CancelFunc, ln ne
                        }
                }()
                go func() {
-                       if err := s.handleNewConnection(conn, conn.RemoteAddr().String()); err != nil {
+                       if err := s.handleNewConnection(ctx, conn, conn.RemoteAddr().String()); err != nil {
+                               s.Logger.Infow("New client connection established", "RemoteAddr", conn.RemoteAddr())
                                if err == io.EOF {
                                        s.Logger.Infow("Client disconnected", "RemoteAddr", conn.RemoteAddr())
                                } else {
@@ -188,7 +203,7 @@ const (
 )
 
 // NewServer constructs a new Server from a config dir
-func NewServer(configDir, netInterface string, netPort int, logger *zap.SugaredLogger, FS FileStore) (*Server, error) {
+func NewServer(configDir string, netPort int, logger *zap.SugaredLogger, FS FileStore) (*Server, error) {
        server := Server{
                Port:          netPort,
                Accounts:      make(map[string]*Account),
@@ -500,7 +515,7 @@ func dontPanic(logger *zap.SugaredLogger) {
 }
 
 // handleNewConnection takes a new net.Conn and performs the initial login sequence
-func (s *Server) handleNewConnection(conn net.Conn, remoteAddr string) error {
+func (s *Server) handleNewConnection(ctx context.Context, conn net.Conn, remoteAddr string) error {
        defer dontPanic(s.Logger)
 
        if err := Handshake(conn); err != nil {
@@ -616,7 +631,7 @@ func (s *Server) handleNewConnection(conn net.Conn, remoteAddr string) error {
                        c.Server.Logger.Errorw("Error handling transaction", "err", err)
                }
 
-               // iterate over all of the transactions that were parsed from the byte slice and handle them
+               // iterate over all the transactions that were parsed from the byte slice and handle them
                for _, t := range transactions {
                        if err := c.handleTransaction(&t); err != nil {
                                c.Server.Logger.Errorw("Error handling transaction", "err", err)
@@ -626,7 +641,7 @@ func (s *Server) handleNewConnection(conn net.Conn, remoteAddr string) error {
 }
 
 // NewTransactionRef generates a random ID for the file transfer.  The Hotline client includes this ID
-// in the file transfer request payload, and the file transfer server will use it to map the request
+// in the transfer request payload, and the file transfer server will use it to map the request
 // to a transfer
 func (s *Server) NewTransactionRef() []byte {
        transactionRef := make([]byte, 4)
@@ -657,18 +672,11 @@ const dlFldrActionResumeFile = 2
 const dlFldrActionNextFile = 3
 
 // handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection
-func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
-       defer func() {
-
-               if err := conn.Close(); err != nil {
-                       s.Logger.Errorw("error closing connection", "error", err)
-               }
-       }()
-
+func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error {
        defer dontPanic(s.Logger)
 
        txBuf := make([]byte, 16)
-       if _, err := io.ReadFull(conn, txBuf); err != nil {
+       if _, err := io.ReadFull(rwc, txBuf); err != nil {
                return err
        }
 
@@ -691,6 +699,11 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                return errors.New("invalid transaction ID")
        }
 
+       rLogger := s.Logger.With(
+               "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
+               "xferID", transferRefNum,
+       )
+
        switch fileTransfer.Type {
        case FileDownload:
                s.Stats.DownloadCounter += 1
@@ -705,50 +718,65 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                        dataOffset = int64(binary.BigEndian.Uint32(fileTransfer.fileResumeData.ForkInfoList[0].DataSize[:]))
                }
 
-               ffo, err := NewFlattenedFileObject(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName, dataOffset)
+               fw, err := newFileWrapper(s.FS, fullFilePath, 0)
                if err != nil {
                        return err
                }
 
-               s.Logger.Infow("File download started", "filePath", fullFilePath, "transactionRef", fileTransfer.ReferenceNumber)
+               rLogger.Infow("File download started", "filePath", fullFilePath, "transactionRef", fileTransfer.ReferenceNumber)
 
+               wr := bufio.NewWriterSize(rwc, 1460)
+
+               // 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 := conn.Write(ffo.BinaryMarshal()); err != nil {
+                       if _, err := wr.Write(fw.ffo.BinaryMarshal()); err != nil {
                                return err
                        }
                }
 
-               file, err := s.FS.Open(fullFilePath)
+               file, err := fw.dataForkReader()
                if err != nil {
                        return err
                }
 
-               sendBuffer := make([]byte, 1048576)
-               var totalSent int64
-               for {
-                       var bytesRead int
-                       if bytesRead, err = file.ReadAt(sendBuffer, dataOffset+totalSent); err == io.EOF {
-                               if _, err := conn.Write(sendBuffer[:bytesRead]); err != nil {
-                                       return err
-                               }
-                               break
-                       }
+               if err := sendFile(wr, file, int(dataOffset)); err != nil {
+                       return err
+               }
+
+               if err := wr.Flush(); err != nil {
+                       return err
+               }
+
+               // if the client requested to resume transfer, do not send the resource fork, or it will be appended into the fileWrapper data
+               if fileTransfer.fileResumeData == nil {
+                       err = binary.Write(wr, binary.BigEndian, fw.rsrcForkHeader())
                        if err != nil {
                                return err
                        }
-                       totalSent += int64(bytesRead)
-
-                       fileTransfer.BytesSent += bytesRead
-
-                       if _, err := conn.Write(sendBuffer[:bytesRead]); err != nil {
+                       if err := wr.Flush(); err != nil {
                                return err
                        }
                }
+
+               rFile, err := fw.rsrcForkFile()
+               if err != nil {
+                       return nil
+               }
+
+               err = sendFile(wr, rFile, int(dataOffset))
+
+               if err := wr.Flush(); err != nil {
+                       return err
+               }
+
        case FileUpload:
                s.Stats.UploadCounter += 1
 
-               destinationFile := s.Config.FileRoot + ReadFilePath(fileTransfer.FilePath) + "/" + string(fileTransfer.FileName)
+               destinationFile, err := readPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName)
+               if err != nil {
+                       return err
+               }
 
                var file *os.File
 
@@ -756,10 +784,10 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                // 1) Upload a new file
                // 2) Resume a partially transferred file
                // 3) Replace a fully uploaded file
-               // Unfortunately we have to infer which case applies by inspecting what is already on the file system
+               //  We have to infer which case applies by inspecting what is already on the filesystem
 
                // 1) Check for existing file:
-               _, err := os.Stat(destinationFile)
+               _, err = os.Stat(destinationFile)
                if err == nil {
                        // If found, that means this upload is intended to replace the file
                        if err = os.Remove(destinationFile); err != nil {
@@ -768,23 +796,41 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                        file, err = os.Create(destinationFile + incompleteFileSuffix)
                }
                if errors.Is(err, fs.ErrNotExist) {
-                       // If not found, open or create a new incomplete file
+                       // If not found, open or create a new .incomplete file
                        file, err = os.OpenFile(destinationFile+incompleteFileSuffix, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
                        if err != nil {
                                return err
                        }
                }
 
+               f, err := newFileWrapper(s.FS, destinationFile, 0)
+               if err != nil {
+                       return err
+               }
+
                defer func() { _ = file.Close() }()
 
                s.Logger.Infow("File upload started", "transactionRef", fileTransfer.ReferenceNumber, "dstFile", destinationFile)
 
-               // TODO: replace io.Discard with a real file when ready to implement storing of resource fork data
-               if err := receiveFile(conn, file, io.Discard); err != nil {
+               rForkWriter := io.Discard
+               iForkWriter := io.Discard
+               if s.Config.PreserveResourceForks {
+                       rForkWriter, err = f.rsrcForkWriter()
+                       if err != nil {
+                               return err
+                       }
+
+                       iForkWriter, err = f.infoForkWriter()
+                       if err != nil {
+                               return err
+                       }
+               }
+
+               if err := receiveFile(rwc, file, rForkWriter, iForkWriter); err != nil {
                        return err
                }
 
-               if err := os.Rename(destinationFile+".incomplete", destinationFile); err != nil {
+               if err := s.FS.Rename(destinationFile+".incomplete", destinationFile); err != nil {
                        return err
                }
 
@@ -793,26 +839,26 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                // Folder Download flow:
                // 1. Get filePath from the transfer
                // 2. Iterate over files
-               // 3. For each file:
-               //       Send file header to client
+               // 3. For each fileWrapper:
+               //       Send fileWrapper header to client
                // The client can reply in 3 ways:
                //
-               // 1. If type is an odd number (unknown type?), or file download for the current file is completed:
-               //              client sends []byte{0x00, 0x03} to tell the server to continue to the next file
+               // 1. If type is an odd number (unknown type?), or fileWrapper download for the current fileWrapper is completed:
+               //              client sends []byte{0x00, 0x03} to tell the server to continue to the next fileWrapper
                //
-               // 2. If download of a file is to be resumed:
+               // 2. If download of a fileWrapper is to be resumed:
                //              client sends:
                //                      []byte{0x00, 0x02} // download folder action
                //                      [2]byte // Resume data size
-               //                      []byte file resume data (see myField_FileResumeData)
+               //                      []byte fileWrapper resume data (see myField_FileResumeData)
                //
-               // 3. Otherwise, download of the file is requested and client sends []byte{0x00, 0x01}
+               // 3. Otherwise, download of the fileWrapper is requested and client sends []byte{0x00, 0x01}
                //
                // When download is requested (case 2 or 3), server replies with:
-               //                      [4]byte - file size
+               //                      [4]byte - fileWrapper size
                //                      []byte  - Flattened File Object
                //
-               // After every file download, client could request next file with:
+               // After every fileWrapper download, client could request next fileWrapper with:
                //                      []byte{0x00, 0x03}
                //
                // This notifies the server to send the next item header
@@ -827,18 +873,29 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                s.Logger.Infow("Start folder download", "path", fullFilePath, "ReferenceNumber", fileTransfer.ReferenceNumber)
 
                nextAction := make([]byte, 2)
-               if _, err := io.ReadFull(conn, nextAction); err != nil {
+               if _, err := io.ReadFull(rwc, nextAction); err != nil {
                        return err
                }
 
                i := 0
                err = filepath.Walk(fullFilePath+"/", func(path string, info os.FileInfo, err error) error {
                        s.Stats.DownloadCounter += 1
+                       i += 1
 
                        if err != nil {
                                return err
                        }
-                       i += 1
+
+                       // skip dot files
+                       if strings.HasPrefix(info.Name(), ".") {
+                               return nil
+                       }
+
+                       hlFile, err := newFileWrapper(s.FS, path, 0)
+                       if err != nil {
+                               return err
+                       }
+
                        subPath := path[basePathLen+1:]
                        s.Logger.Infow("Sending fileheader", "i", i, "path", path, "fullFilePath", fullFilePath, "subPath", subPath, "IsDir", info.IsDir())
 
@@ -848,14 +905,14 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
 
                        fileHeader := NewFileHeader(subPath, info.IsDir())
 
-                       // Send the file header to client
-                       if _, err := conn.Write(fileHeader.Payload()); err != nil {
+                       // 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
                        }
 
                        // Read the client's Next Action request
-                       if _, err := io.ReadFull(conn, nextAction); err != nil {
+                       if _, err := io.ReadFull(rwc, nextAction); err != nil {
                                return err
                        }
 
@@ -865,19 +922,19 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
 
                        switch nextAction[1] {
                        case dlFldrActionResumeFile:
-                               // client asked to resume this file
-                               var frd FileResumeData
                                // get size of resumeData
-                               if _, err := io.ReadFull(conn, nextAction); err != nil {
+                               resumeDataByteLen := make([]byte, 2)
+                               if _, err := io.ReadFull(rwc, resumeDataByteLen); err != nil {
                                        return err
                                }
 
-                               resumeDataLen := binary.BigEndian.Uint16(nextAction)
+                               resumeDataLen := binary.BigEndian.Uint16(resumeDataByteLen)
                                resumeDataBytes := make([]byte, resumeDataLen)
-                               if _, err := io.ReadFull(conn, resumeDataBytes); err != nil {
+                               if _, err := io.ReadFull(rwc, resumeDataBytes); err != nil {
                                        return err
                                }
 
+                               var frd FileResumeData
                                if err := frd.UnmarshalBinary(resumeDataBytes); err != nil {
                                        return err
                                }
@@ -891,26 +948,20 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                                return nil
                        }
 
-                       splitPath := strings.Split(path, "/")
-
-                       ffo, err := NewFlattenedFileObject(strings.Join(splitPath[:len(splitPath)-1], "/"), nil, []byte(info.Name()), dataOffset)
-                       if err != nil {
-                               return err
-                       }
                        s.Logger.Infow("File download started",
                                "fileName", info.Name(),
                                "transactionRef", fileTransfer.ReferenceNumber,
-                               "TransferSize", fmt.Sprintf("%x", ffo.TransferSize()),
+                               "TransferSize", fmt.Sprintf("%x", hlFile.ffo.TransferSize(dataOffset)),
                        )
 
                        // Send file size to client
-                       if _, err := conn.Write(ffo.TransferSize()); err != nil {
+                       if _, err := rwc.Write(hlFile.ffo.TransferSize(dataOffset)); err != nil {
                                s.Logger.Error(err)
                                return err
                        }
 
                        // Send ffo bytes to client
-                       if _, err := conn.Write(ffo.BinaryMarshal()); err != nil {
+                       if _, err := rwc.Write(hlFile.ffo.BinaryMarshal()); err != nil {
                                s.Logger.Error(err)
                                return err
                        }
@@ -920,38 +971,31 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                                return err
                        }
 
-                       // // Copy N bytes from file to connection
-                       // _, err = io.CopyN(conn, file, int64(binary.BigEndian.Uint32(ffo.FlatFileDataForkHeader.DataSize[:])))
-                       // if err != nil {
-                       //      return err
-                       // }
-                       // file.Close()
-                       sendBuffer := make([]byte, 1048576)
-                       var totalSent int64
-                       for {
-                               var bytesRead int
-                               if bytesRead, err = file.ReadAt(sendBuffer, dataOffset+totalSent); err == io.EOF {
-                                       if _, err := conn.Write(sendBuffer[:bytesRead]); err != nil {
-                                               return err
-                                       }
-                                       break
-                               }
+                       // wr := bufio.NewWriterSize(rwc, 1460)
+                       err = sendFile(rwc, file, int(dataOffset))
+                       if err != nil {
+                               return err
+                       }
+
+                       if nextAction[1] != 2 && hlFile.ffo.FlatFileHeader.ForkCount[1] == 3 {
+                               err = binary.Write(rwc, binary.BigEndian, hlFile.rsrcForkHeader())
                                if err != nil {
-                                       panic(err)
+                                       return err
                                }
-                               totalSent += int64(bytesRead)
 
-                               fileTransfer.BytesSent += bytesRead
+                               rFile, err := hlFile.rsrcForkFile()
+                               if err != nil {
+                                       return err
+                               }
 
-                               if _, err := conn.Write(sendBuffer[:bytesRead]); err != nil {
+                               err = sendFile(rwc, rFile, int(dataOffset))
+                               if err != nil {
                                        return err
                                }
                        }
 
-                       // TODO: optionally send resource fork header and resource fork data
-
                        // Read the client's Next Action request.  This is always 3, I think?
-                       if _, err := io.ReadFull(conn, nextAction); err != nil {
+                       if _, err := io.ReadFull(rwc, nextAction); err != nil {
                                return err
                        }
 
@@ -963,6 +1007,7 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                if err != nil {
                        return err
                }
+
                s.Logger.Infow(
                        "Folder upload started",
                        "transactionRef", fileTransfer.ReferenceNumber,
@@ -979,7 +1024,7 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                }
 
                // Begin the folder upload flow by sending the "next file action" to client
-               if _, err := conn.Write([]byte{0, dlFldrActionNextFile}); err != nil {
+               if _, err := rwc.Write([]byte{0, dlFldrActionNextFile}); err != nil {
                        return err
                }
 
@@ -989,19 +1034,19 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                        s.Stats.UploadCounter += 1
 
                        var fu folderUpload
-                       if _, err := io.ReadFull(conn, fu.DataSize[:]); err != nil {
+                       if _, err := io.ReadFull(rwc, fu.DataSize[:]); err != nil {
                                return err
                        }
-
-                       if _, err := io.ReadFull(conn, fu.IsFolder[:]); err != nil {
+                       if _, err := io.ReadFull(rwc, fu.IsFolder[:]); err != nil {
                                return err
                        }
-                       if _, err := io.ReadFull(conn, fu.PathItemCount[:]); err != nil {
+                       if _, err := io.ReadFull(rwc, fu.PathItemCount[:]); err != nil {
                                return err
                        }
-                       fu.FileNamePath = make([]byte, binary.BigEndian.Uint16(fu.DataSize[:])-4)
 
-                       if _, err := io.ReadFull(conn, fu.FileNamePath); err != nil {
+                       fu.FileNamePath = make([]byte, binary.BigEndian.Uint16(fu.DataSize[:])-4) // -4 to subtract the path separator bytes
+
+                       if _, err := io.ReadFull(rwc, fu.FileNamePath); err != nil {
                                return err
                        }
 
@@ -1021,14 +1066,14 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                                }
 
                                // Tell client to send next file
-                               if _, err := conn.Write([]byte{0, dlFldrActionNextFile}); err != nil {
+                               if _, err := rwc.Write([]byte{0, dlFldrActionNextFile}); err != nil {
                                        return err
                                }
                        } else {
                                nextAction := dlFldrActionSendFile
 
                                // Check if we have the full file already.  If so, send dlFldrAction_NextFile to client to skip.
-                               _, err := os.Stat(dstPath + "/" + fu.FormattedPath())
+                               _, err = os.Stat(dstPath + "/" + fu.FormattedPath())
                                if err != nil && !errors.Is(err, fs.ErrNotExist) {
                                        return err
                                }
@@ -1037,7 +1082,7 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                                }
 
                                //  Check if we have a partial file already.  If so, send dlFldrAction_ResumeFile to client to resume upload.
-                               inccompleteFile, err := os.Stat(dstPath + "/" + fu.FormattedPath() + incompleteFileSuffix)
+                               incompleteFile, err := os.Stat(dstPath + "/" + fu.FormattedPath() + incompleteFileSuffix)
                                if err != nil && !errors.Is(err, fs.ErrNotExist) {
                                        return err
                                }
@@ -1045,7 +1090,7 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                                        nextAction = dlFldrActionResumeFile
                                }
 
-                               if _, err := conn.Write([]byte{0, uint8(nextAction)}); err != nil {
+                               if _, err := rwc.Write([]byte{0, uint8(nextAction)}); err != nil {
                                        return err
                                }
 
@@ -1054,31 +1099,29 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                                        continue
                                case dlFldrActionResumeFile:
                                        offset := make([]byte, 4)
-                                       binary.BigEndian.PutUint32(offset, uint32(inccompleteFile.Size()))
+                                       binary.BigEndian.PutUint32(offset, uint32(incompleteFile.Size()))
 
                                        file, err := os.OpenFile(dstPath+"/"+fu.FormattedPath()+incompleteFileSuffix, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
                                        if err != nil {
                                                return err
                                        }
 
-                                       fileResumeData := NewFileResumeData([]ForkInfoList{
-                                               *NewForkInfoList(offset),
-                                       })
+                                       fileResumeData := NewFileResumeData([]ForkInfoList{*NewForkInfoList(offset)})
 
                                        b, _ := fileResumeData.BinaryMarshal()
 
                                        bs := make([]byte, 2)
                                        binary.BigEndian.PutUint16(bs, uint16(len(b)))
 
-                                       if _, err := conn.Write(append(bs, b...)); err != nil {
+                                       if _, err := rwc.Write(append(bs, b...)); err != nil {
                                                return err
                                        }
 
-                                       if _, err := io.ReadFull(conn, fileSize); err != nil {
+                                       if _, err := io.ReadFull(rwc, fileSize); err != nil {
                                                return err
                                        }
 
-                                       if err := receiveFile(conn, file, ioutil.Discard); err != nil {
+                                       if err := receiveFile(rwc, file, ioutil.Discard, ioutil.Discard); err != nil {
                                                s.Logger.Error(err)
                                        }
 
@@ -1088,29 +1131,48 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
                                        }
 
                                case dlFldrActionSendFile:
-                                       if _, err := io.ReadFull(conn, fileSize); err != nil {
+                                       if _, err := io.ReadFull(rwc, fileSize); err != nil {
                                                return err
                                        }
 
                                        filePath := dstPath + "/" + fu.FormattedPath()
-                                       s.Logger.Infow("Starting file transfer", "path", filePath, "fileNum", i+1, "totalFiles", "zz", "fileSize", binary.BigEndian.Uint32(fileSize))
 
-                                       newFile, err := s.FS.Create(filePath + ".incomplete")
+                                       hlFile, err := newFileWrapper(s.FS, filePath, 0)
                                        if err != nil {
                                                return err
                                        }
 
-                                       if err := receiveFile(conn, newFile, ioutil.Discard); err != nil {
-                                               s.Logger.Error(err)
+                                       s.Logger.Infow("Starting file transfer", "path", filePath, "fileNum", i+1, "fileSize", binary.BigEndian.Uint32(fileSize))
+
+                                       incWriter, err := hlFile.incFileWriter()
+                                       if err != nil {
+                                               return err
+                                       }
+
+                                       rForkWriter := io.Discard
+                                       iForkWriter := io.Discard
+                                       if s.Config.PreserveResourceForks {
+                                               iForkWriter, err = hlFile.infoForkWriter()
+                                               if err != nil {
+                                                       return err
+                                               }
+
+                                               rForkWriter, err = hlFile.rsrcForkWriter()
+                                               if err != nil {
+                                                       return err
+                                               }
                                        }
-                                       _ = newFile.Close()
+                                       if err := receiveFile(rwc, incWriter, rForkWriter, iForkWriter); err != nil {
+                                               return err
+                                       }
+                                       // _ = newFile.Close()
                                        if err := os.Rename(filePath+".incomplete", filePath); err != nil {
                                                return err
                                        }
                                }
 
-                               // Tell client to send next file
-                               if _, err := conn.Write([]byte{0, dlFldrActionNextFile}); err != nil {
+                               // Tell client to send next fileWrapper
+                               if _, err := rwc.Write([]byte{0, dlFldrActionNextFile}); err != nil {
                                        return err
                                }
                        }
@@ -1120,13 +1182,3 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error {
 
        return nil
 }
-
-// sortedClients is a utility function that takes a map of *ClientConn and returns a sorted slice of the values.
-// The purpose of this is to ensure that the ordering of client connections is deterministic so that test assertions work.
-func sortedClients(unsortedClients map[uint16]*ClientConn) (clients []*ClientConn) {
-       for _, c := range unsortedClients {
-               clients = append(clients, c)
-       }
-       sort.Sort(byClientID(clients))
-       return clients
-}
index 17ad7f49418779b40db14832e2ab15ef1277106d..138a17f285ca782e64c8c2c79811c893ef9789e4 100644 (file)
@@ -1,6 +1,8 @@
 package hotline
 
 import (
+       "bytes"
+       "encoding/hex"
        "github.com/stretchr/testify/assert"
        "go.uber.org/zap"
        "go.uber.org/zap/zapcore"
@@ -25,19 +27,53 @@ func NewTestLogger() *zap.SugaredLogger {
        return l.Sugar()
 }
 
+// assertTransferBytesEqual takes a string with a hexdump in the same format that `hexdump -C` produces and compares with
+// a hexdump for the bytes in got, after stripping the create/modify timestamps.
+// I don't love this, but as git does not  preserve file create/modify timestamps, we either need to fully mock the
+// filesystem interactions or work around in this way.
+// TODO: figure out a better solution
+func assertTransferBytesEqual(t *testing.T, wantHexDump string, got []byte) bool {
+       if wantHexDump == "" {
+               return true
+       }
+
+       var clean []byte
+       clean = append(clean, got[:92]...)         // keep the first 92 bytes
+       clean = append(clean, make([]byte, 16)...) // replace the next 16 bytes for create/modify timestamps
+       clean = append(clean, got[108:]...)        // keep the rest
+
+       return assert.Equal(t, wantHexDump, hex.Dump(clean))
+}
+
 // tranAssertEqual compares equality of transactions slices after stripping out the random ID
 func tranAssertEqual(t *testing.T, tran1, tran2 []Transaction) bool {
        var newT1 []Transaction
        var newT2 []Transaction
+
        for _, trans := range tran1 {
                trans.ID = []byte{0, 0, 0, 0}
+               var fs []Field
+               for _, field := range trans.Fields {
+                       if bytes.Equal(field.ID, []byte{0x00, 0x6b}) {
+                               continue
+                       }
+                       fs = append(fs, field)
+               }
+               trans.Fields = fs
                newT1 = append(newT1, trans)
        }
 
        for _, trans := range tran2 {
                trans.ID = []byte{0, 0, 0, 0}
+               var fs []Field
+               for _, field := range trans.Fields {
+                       if bytes.Equal(field.ID, []byte{0x00, 0x6b}) {
+                               continue
+                       }
+                       fs = append(fs, field)
+               }
+               trans.Fields = fs
                newT2 = append(newT2, trans)
-
        }
 
        return assert.Equal(t, newT1, newT2)
index 5937adc11666cb3c4947f742a80ca801a7ac4013..d3e43250aeb840af203270a4312c04a997a274ba 100644 (file)
 package hotline
 
-//
-// import (
-//     "bytes"
-//     "fmt"
-//     "github.com/google/go-cmp/cmp"
-//     "io/ioutil"
-//     "math/big"
-//     "net"
-//     "strings"
-//     "sync"
-//     "testing"
-// )
-//
-// type transactionTest struct {
-//     description string      // Human understandable description
-//     account     Account     // Account struct for a user that will test transaction will execute under
-//     request     Transaction // transaction that will be sent by the client to the server
-//     want        Transaction // transaction that the client expects to receive in response
-//     setup       func()      // Optional setup required for the test scenario
-//     teardown    func()      // Optional teardown for test scenario
-// }
-//
-// func (tt *transactionTest) Setup(srv *Server) error {
-//     if err := srv.NewUser(tt.account.Login, tt.account.Name, NegatedUserString([]byte(tt.account.Password)), tt.account.Access); err != nil {
-//             return err
-//     }
-//
-//     if tt.setup != nil {
-//             tt.setup()
-//     }
-//
-//     return nil
-// }
-//
-// func (tt *transactionTest) Teardown(srv *Server) error {
-//     if err := srv.DeleteUser(tt.account.Login); err != nil {
-//             return err
-//     }
-//
-//     if tt.teardown != nil {
-//             tt.teardown()
-//     }
-//
-//     return nil
-// }
-//
-// // StartTestServer
-// func StartTestServer() (srv *Server, lnPort int) {
-//     hotlineServer, _ := NewServer("test/config/")
-//     ln, err := net.Listen("tcp", ":0")
-//
-//     if err != nil {
-//             panic(err)
-//     }
-//     go func() {
-//             for {
-//                     conn, _ := ln.Accept()
-//                     go hotlineServer.HandleConnection(conn)
-//             }
-//     }()
-//     return hotlineServer, ln.Addr().(*net.TCPAddr).Port
-// }
-//
-// func StartTestClient(serverPort int, login, passwd string) (*Client, error) {
-//     c := NewClient("")
-//
-//     err := c.JoinServer(fmt.Sprintf(":%v", serverPort), login, passwd)
-//     if err != nil {
-//             return nil, err
-//     }
-//
-//     return c, nil
-// }
-//
-// func StartTestServerWithClients(clientCount int) ([]*Client, int) {
-//     _, serverPort := StartTestServer()
-//
-//     var clients []*Client
-//     for i := 0; i < clientCount; i++ {
-//             client, err := StartTestClient(serverPort, "admin", "")
-//             if err != nil {
-//                     panic(err)
-//             }
-//             clients = append(clients, client)
-//     }
-//     clients[0].ReadN(2)
-//
-//     return clients, serverPort
-// }
-//
+import (
+       "bytes"
+       "context"
+       "fmt"
+       "github.com/stretchr/testify/assert"
+       "go.uber.org/zap"
+       "io"
+       "os"
+       "sync"
+       "testing"
+)
 
-// //func TestHandleTranAgreed(t *testing.T) {
-// //  clients, _ := StartTestServerWithClients(2)
-// //
-// //  chatMsg := "Test Chat"
-// //
-// //  // Assert that both clients should receive the user join notification
-// //  var wg sync.WaitGroup
-// //  for _, client := range clients {
-// //          wg.Add(1)
-// //          go func(wg *sync.WaitGroup, c *Client) {
-// //                  defer wg.Done()
-// //
-// //                  receivedMsg := c.ReadTransactions()[0].GetField(fieldData).Data
-// //
-// //                  want := []byte(fmt.Sprintf("test: %s\r", chatMsg))
-// //                  if bytes.Compare(receivedMsg, want) != 0 {
-// //                          t.Errorf("%q, want %q", receivedMsg, want)
-// //                  }
-// //          }(&wg, client)
-// //  }
-// //
-// //  trans := clients[1].ReadTransactions()
-// //  spew.Dump(trans)
-// //
-// //  // Send the agreement
-// //  clients[1].Connection.Write(
-// //          NewTransaction(
-// //                  tranAgreed, 0,
-// //                  []Field{
-// //                          NewField(fieldUserName, []byte("testUser")),
-// //                          NewField(fieldUserIconID, []byte{0x00,0x07}),
-// //                  },
-// //          ).Payload(),
-// //  )
-// //
-// //  wg.Wait()
-// //}
-//
-// func TestChatSend(t *testing.T) {
-//     //srvPort := StartTestServer()
-//     //
-//     //senderClient := NewClient("senderClient")
-//     //senderClient.JoinServer(fmt.Sprintf(":%v", srvPort), "", "")
-//     //
-//     //receiverClient := NewClient("receiverClient")
-//     //receiverClient.JoinServer(fmt.Sprintf(":%v", srvPort), "", "")
-//
-//     clients, _ := StartTestServerWithClients(2)
-//
-//     chatMsg := "Test Chat"
-//
-//     // Both clients should receive the chatMsg
-//     var wg sync.WaitGroup
-//     for _, client := range clients {
-//             wg.Add(1)
-//             go func(wg *sync.WaitGroup, c *Client) {
-//                     defer wg.Done()
-//
-//                     receivedMsg := c.ReadTransactions()[0].GetField(fieldData).Data
-//
-//                     want := []byte(fmt.Sprintf("         test:  %s\r", chatMsg))
-//                     if bytes.Compare(receivedMsg, want) != 0 {
-//                             t.Errorf("%q, want %q", receivedMsg, want)
-//                     }
-//             }(&wg, client)
-//     }
-//
-//     // Send the chatMsg
-//     clients[1].Send(
-//             NewTransaction(
-//                     tranChatSend, 0,
-//                     []Field{
-//                             NewField(fieldData, []byte(chatMsg)),
-//                     },
-//             ),
-//     )
-//
-//     wg.Wait()
-// }
-//
-// func TestSetClientUserInfo(t *testing.T) {
-//     clients, _ := StartTestServerWithClients(2)
-//
-//     newIcon := []byte{0x00, 0x01}
-//     newUserName := "newName"
-//
-//     // Both clients should receive the chatMsg
-//     var wg sync.WaitGroup
-//     for _, client := range clients {
-//             wg.Add(1)
-//             go func(wg *sync.WaitGroup, c *Client) {
-//                     defer wg.Done()
-//
-//                     tran := c.ReadTransactions()[0]
-//
-//                     want := []byte(newUserName)
-//                     got := tran.GetField(fieldUserName).Data
-//                     if bytes.Compare(got, want) != 0 {
-//                             t.Errorf("%q, want %q", got, want)
-//                     }
-//             }(&wg, client)
-//     }
-//
-//     _, err := clients[1].Connection.Write(
-//             NewTransaction(
-//                     tranSetClientUserInfo, 0,
-//                     []Field{
-//                             NewField(fieldUserIconID, newIcon),
-//                             NewField(fieldUserName, []byte(newUserName)),
-//                     },
-//             ).Payload(),
-//     )
-//     if err != nil {
-//             t.Errorf("%v", err)
-//     }
-//
-//     wg.Wait()
-// }
-//
-// // TestSendInstantMsg tests that client A can send an instant message to client B
-// //
-// func TestSendInstantMsg(t *testing.T) {
-//     clients, _ := StartTestServerWithClients(2)
-//
-//     instantMsg := "Test IM"
-//
-//     var wg sync.WaitGroup
-//     wg.Add(1)
-//     go func(wg *sync.WaitGroup, c *Client) {
-//             defer wg.Done()
-//
-//             tran := c.WaitForTransaction(tranServerMsg)
-//
-//             receivedMsg := tran.GetField(fieldData).Data
-//             want := []byte(fmt.Sprintf("%s", instantMsg))
-//             if bytes.Compare(receivedMsg, want) != 0 {
-//                     t.Errorf("%q, want %q", receivedMsg, want)
-//             }
-//     }(&wg, clients[0])
-//
-//     _ = clients[1].Send(
-//             NewTransaction(tranGetUserNameList, 0, []Field{}),
-//     )
-//     //connectedUsersTran := clients[1].ReadTransactions()[0]
-//     ////connectedUsers := connectedUsersTran.Fields[0].Data[0:2]
-//     //spew.Dump(connectedUsersTran.Fields)
-//     //firstUserID := connectedUsersTran.Fields[0].Data[0:2]
-//     //
-//     //spew.Dump(firstUserID)
-//
-//     // Send the IM
-//     err := clients[1].Send(
-//             NewTransaction(
-//                     tranSendInstantMsg, 0,
-//                     []Field{
-//                             NewField(fieldData, []byte(instantMsg)),
-//                             NewField(fieldUserName, clients[1].UserName),
-//                             NewField(fieldUserID, []byte{0, 2}),
-//                             NewField(fieldOptions, []byte{0, 1}),
-//                     },
-//             ),
-//     )
-//     if err != nil {
-//             t.Error(err)
-//     }
-//
-//     wg.Wait()
-// }
-//
-// func TestOldPostNews(t *testing.T) {
-//     clients, _ := StartTestServerWithClients(2)
-//
-//     newsPost := "Test News Post"
-//
-//     var wg sync.WaitGroup
-//     wg.Add(1)
-//     go func(wg *sync.WaitGroup, c *Client) {
-//             defer wg.Done()
-//
-//             receivedMsg := c.ReadTransactions()[0].GetField(fieldData).Data
-//
-//             if strings.Contains(string(receivedMsg), newsPost) == false {
-//                     t.Errorf("news post missing")
-//             }
-//     }(&wg, clients[0])
-//
-//     clients[1].Connection.Write(
-//             NewTransaction(
-//                     tranOldPostNews, 0,
-//                     []Field{
-//                             NewField(fieldData, []byte(newsPost)),
-//                     },
-//             ).Payload(),
-//     )
-//
-//     wg.Wait()
-// }
-//
-// // TODO: Fixme
-// //func TestGetFileNameList(t *testing.T) {
-// //  clients, _ := StartTestServerWithClients(2)
-// //
-// //  clients[0].Connection.Write(
-// //          NewTransaction(
-// //                  tranGetFileNameList, 0,
-// //                  []Field{},
-// //          ).Payload(),
-// //  )
-// //
-// //  ts := clients[0].ReadTransactions()
-// //  testfileSit := ReadFileNameWithInfo(ts[0].Fields[1].Data)
-// //
-// //  want := "testfile.sit"
-// //  got := testfileSit.Name
-// //  diff := cmp.Diff(want, got)
-// //  if diff != "" {
-// //          t.Fatalf(diff)
-// //  }
-// //  if testfileSit.Name != "testfile.sit" {
-// //          t.Errorf("news post missing")
-// //          t.Errorf("%q, want %q", testfileSit.Name, "testfile.sit")
-// //  }
-// //}
-//
-// func TestNewsCategoryList(t *testing.T) {
-//     clients, _ := StartTestServerWithClients(2)
-//     client := clients[0]
-//
-//     client.Send(
-//             NewTransaction(
-//                     tranGetNewsCatNameList, 0,
-//                     []Field{},
-//             ),
-//     )
-//
-//     ts := client.ReadTransactions()
-//     cats := ts[0].GetFields(fieldNewsCatListData15)
-//
-//     newsCat := ReadNewsCategoryListData(cats[0].Data)
-//     want := "TestBundle"
-//     got := newsCat.Name
-//     diff := cmp.Diff(want, got)
-//     if diff != "" {
-//             t.Fatalf(diff)
-//     }
-//
-//     newsBundle := ReadNewsCategoryListData(cats[1].Data)
-//     want = "TestCat"
-//     got = newsBundle.Name
-//     diff = cmp.Diff(want, got)
-//     if diff != "" {
-//             t.Fatalf(diff)
-//     }
-// }
-//
-// func TestNestedNewsCategoryList(t *testing.T) {
-//     clients, _ := StartTestServerWithClients(2)
-//     client := clients[0]
-//     newsPath := NewsPath{
-//             []string{
-//                     "TestBundle",
-//                     "NestedBundle",
-//             },
-//     }
-//
-//     _, err := client.Connection.Write(
-//             NewTransaction(
-//                     tranGetNewsCatNameList, 0,
-//                     []Field{
-//                             NewField(
-//                                     fieldNewsPath,
-//                                     newsPath.Payload(),
-//                             ),
-//                     },
-//             ).Payload(),
-//     )
-//     if err != nil {
-//             t.Errorf("%v", err)
-//     }
-//
-//     ts := client.ReadTransactions()
-//     cats := ts[0].GetFields(fieldNewsCatListData15)
-//
-//     newsCat := ReadNewsCategoryListData(cats[0].Data)
-//     want := "NestedCat"
-//     got := newsCat.Name
-//     diff := cmp.Diff(want, got)
-//     if diff != "" {
-//             t.Fatalf(diff)
-//     }
-// }
-//
-// func TestFileDownload(t *testing.T) {
-//     clients, _ := StartTestServerWithClients(2)
-//     client := clients[0]
-//
-//     type want struct {
-//             fileSize     []byte
-//             transferSize []byte
-//             waitingCount []byte
-//             refNum       []byte
-//     }
-//     var tests = []struct {
-//             fileName string
-//             want     want
-//     }{
-//             {
-//                     fileName: "testfile.sit",
-//                     want: want{
-//                             fileSize:     []byte{0x0, 0x0, 0x0, 0x13},
-//                             transferSize: []byte{0x0, 0x0, 0x0, 0xa1},
-//                     },
-//             },
-//             {
-//                     fileName: "testfile.txt",
-//                     want: want{
-//                             fileSize:     []byte{0x0, 0x0, 0x0, 0x17},
-//                             transferSize: []byte{0x0, 0x0, 0x0, 0xa5},
-//                     },
-//             },
-//     }
-//
-//     for _, test := range tests {
-//             _, err := client.Connection.Write(
-//                     NewTransaction(
-//                             tranDownloadFile, 0,
-//                             []Field{
-//                                     NewField(fieldFileName, []byte(test.fileName)),
-//                                     NewField(fieldFilePath, []byte("")),
-//                             },
-//                     ).Payload(),
-//             )
-//             if err != nil {
-//                     t.Errorf("%v", err)
-//             }
-//             tran := client.ReadTransactions()[0]
-//
-//             if got := tran.GetField(fieldFileSize).Data; bytes.Compare(got, test.want.fileSize) != 0 {
-//                     t.Errorf("TestFileDownload: fileSize got %#v, want %#v", got, test.want.fileSize)
-//             }
-//
-//             if got := tran.GetField(fieldTransferSize).Data; bytes.Compare(got, test.want.transferSize) != 0 {
-//                     t.Errorf("TestFileDownload: fieldTransferSize: %s: got %#v, want %#v", test.fileName, got, test.want.transferSize)
-//             }
-//     }
-// }
-//
-// func TestFileUpload(t *testing.T) {
-//     clients, _ := StartTestServerWithClients(2)
-//     client := clients[0]
-//
-//     var tests = []struct {
-//             fileName string
-//             want     Transaction
-//     }{
-//             {
-//                     fileName: "testfile.sit",
-//                     want: Transaction{
-//                             Fields: []Field{
-//                                     NewField(fieldRefNum, []byte{0x16, 0x3f, 0x5f, 0xf}),
-//                             },
-//                     },
-//             },
-//     }
-//
-//     for _, test := range tests {
-//             err := client.Send(
-//                     NewTransaction(
-//                             tranUploadFile, 0,
-//                             []Field{
-//                                     NewField(fieldFileName, []byte(test.fileName)),
-//                                     NewField(fieldFilePath, []byte("")),
-//                             },
-//                     ),
-//             )
-//             if err != nil {
-//                     t.Errorf("%v", err)
-//             }
-//             tran := client.ReadTransactions()[0]
-//
-//             for _, f := range test.want.Fields {
-//                     got := tran.GetField(f.Uint16ID()).Data
-//                     want := test.want.GetField(fieldRefNum).Data
-//                     if bytes.Compare(got, want) != 0 {
-//                             t.Errorf("xxx: yyy got %#v, want %#v", got, want)
-//                     }
-//             }
-//     }
-// }
-//
-// // TODO: Make canonical
-// func TestNewUser(t *testing.T) {
-//     srv, port := StartTestServer()
-//
-//     var tests = []struct {
-//             description string
-//             setup       func()
-//             teardown    func()
-//             account     Account
-//             request     Transaction
-//             want        Transaction
-//     }{
-//             {
-//                     description: "a valid new account",
-//                     teardown: func() {
-//                             _ = srv.DeleteUser("testUser")
-//                     },
-//                     account: Account{
-//                             Login:    "test",
-//                             Name:     "unnamed",
-//                             Password: "test",
-//                             Access:   []byte{255, 255, 255, 255, 255, 255, 255, 255},
-//                     },
-//                     request: NewTransaction(
-//                             tranNewUser, 0,
-//                             []Field{
-//                                     NewField(fieldUserLogin, []byte(NegatedUserString([]byte("testUser")))),
-//                                     NewField(fieldUserName, []byte("testUserName")),
-//                                     NewField(fieldUserPassword, []byte(NegatedUserString([]byte("testPw")))),
-//                                     NewField(fieldUserAccess, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}),
-//                             },
-//                     ),
-//                     want: Transaction{
-//                             Fields: []Field{},
-//                     },
-//             },
-//             {
-//                     description: "a newUser request from a user without the required access",
-//                     teardown: func() {
-//                             _ = srv.DeleteUser("testUser")
-//                     },
-//                     account: Account{
-//                             Login:    "test",
-//                             Name:     "unnamed",
-//                             Password: "test",
-//                             Access:   []byte{0, 0, 0, 0, 0, 0, 0, 0},
-//                     },
-//                     request: NewTransaction(
-//                             tranNewUser, 0,
-//                             []Field{
-//                                     NewField(fieldUserLogin, []byte(NegatedUserString([]byte("testUser")))),
-//                                     NewField(fieldUserName, []byte("testUserName")),
-//                                     NewField(fieldUserPassword, []byte(NegatedUserString([]byte("testPw")))),
-//                                     NewField(fieldUserAccess, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}),
-//                             },
-//                     ),
-//                     want: Transaction{
-//                             Fields: []Field{
-//                                     NewField(fieldError, []byte("You are not allowed to create new accounts.")),
-//                             },
-//                     },
-//             },
-//             {
-//                     description: "a request to create a user that already exists",
-//                     teardown: func() {
-//                             _ = srv.DeleteUser("testUser")
-//                     },
-//                     account: Account{
-//                             Login:    "test",
-//                             Name:     "unnamed",
-//                             Password: "test",
-//                             Access:   []byte{255, 255, 255, 255, 255, 255, 255, 255},
-//                     },
-//                     request: NewTransaction(
-//                             tranNewUser, 0,
-//                             []Field{
-//                                     NewField(fieldUserLogin, []byte(NegatedUserString([]byte("guest")))),
-//                                     NewField(fieldUserName, []byte("testUserName")),
-//                                     NewField(fieldUserPassword, []byte(NegatedUserString([]byte("testPw")))),
-//                                     NewField(fieldUserAccess, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}),
-//                             },
-//                     ),
-//                     want: Transaction{
-//                             Fields: []Field{
-//                                     NewField(fieldError, []byte("Cannot create account guest because there is already an account with that login.")),
-//                             },
-//                     },
-//             },
-//     }
-//
-//     for _, test := range tests {
-//             if test.setup != nil {
-//                     test.setup()
-//             }
-//
-//             if err := srv.NewUser(test.account.Login, test.account.Name, NegatedUserString([]byte(test.account.Password)), test.account.Access); err != nil {
-//                     t.Errorf("%v", err)
-//             }
-//
-//             c := NewClient("")
-//             err := c.JoinServer(fmt.Sprintf(":%v", port), test.account.Login, test.account.Password)
-//             if err != nil {
-//                     t.Errorf("login failed: %v", err)
-//             }
-//
-//             if err := c.Send(test.request); err != nil {
-//                     t.Errorf("%v", err)
-//             }
-//
-//             tran := c.ReadTransactions()[0]
-//             for _, want := range test.want.Fields {
-//                     got := tran.GetField(want.Uint16ID())
-//                     if bytes.Compare(got.Data, want.Data) != 0 {
-//                             t.Errorf("%v: field mismatch:  want: %#v got: %#v", test.description, want.Data, got.Data)
-//                     }
-//             }
-//
-//             srv.DeleteUser(test.account.Login)
-//
-//             if test.teardown != nil {
-//                     test.teardown()
-//             }
-//     }
-// }
-//
-// func TestDeleteUser(t *testing.T) {
-//     srv, port := StartTestServer()
-//
-//     var tests = []transactionTest{
-//             {
-//                     description: "a deleteUser request from a user without the required access",
-//                     account: Account{
-//                             Login:    "test",
-//                             Name:     "unnamed",
-//                             Password: "test",
-//                             Access:   []byte{0, 0, 0, 0, 0, 0, 0, 0},
-//                     },
-//                     request: NewTransaction(
-//                             tranDeleteUser, 0,
-//                             []Field{
-//                                     NewField(fieldUserLogin, []byte(NegatedUserString([]byte("foo")))),
-//                             },
-//                     ),
-//                     want: Transaction{
-//                             Fields: []Field{
-//                                     NewField(fieldError, []byte("You are not allowed to delete accounts.")),
-//                             },
-//                     },
-//             },
-//             {
-//                     description: "a valid deleteUser request",
-//                     setup: func() {
-//                             _ = srv.NewUser("foo", "foo", "foo", []byte{0, 0, 0, 0, 0, 0, 0, 0})
-//                     },
-//                     account: Account{
-//                             Login:    "test",
-//                             Name:     "unnamed",
-//                             Password: "test",
-//                             Access:   []byte{255, 255, 255, 255, 255, 255, 255, 255},
-//                     },
-//                     request: NewTransaction(
-//                             tranDeleteUser, 0,
-//                             []Field{
-//                                     NewField(fieldUserLogin, []byte(NegatedUserString([]byte("foo")))),
-//                             },
-//                     ),
-//                     want: Transaction{
-//                             Fields: []Field{},
-//                     },
-//             },
-//     }
-//
-//     for _, test := range tests {
-//             test.Setup(srv)
-//
-//             c := NewClient("")
-//             err := c.JoinServer(fmt.Sprintf(":%v", port), test.account.Login, test.account.Password)
-//             if err != nil {
-//                     t.Errorf("login failed: %v", err)
-//             }
-//
-//             if err := c.Send(test.request); err != nil {
-//                     t.Errorf("%v", err)
-//             }
-//
-//             tran := c.ReadTransactions()[0]
-//             for _, want := range test.want.Fields {
-//                     got := tran.GetField(want.Uint16ID())
-//                     if bytes.Compare(got.Data, want.Data) != 0 {
-//                             t.Errorf("%v: field mismatch:  want: %#v got: %#v", test.description, want.Data, got.Data)
-//                     }
-//             }
-//
-//             test.Teardown(srv)
-//     }
-// }
-//
-// func TestDeleteFile(t *testing.T) {
-//     srv, port := StartTestServer()
-//
-//     var tests = []transactionTest{
-//             {
-//                     description: "a request without the required access",
-//                     account: Account{
-//                             Login:    "test",
-//                             Name:     "unnamed",
-//                             Password: "test",
-//                             Access:   []byte{0, 0, 0, 0, 0, 0, 0, 0},
-//                     },
-//                     request: NewTransaction(
-//                             tranDeleteFile, 0,
-//                             []Field{
-//                                     NewField(fieldFileName, []byte("testFile")),
-//                                     NewField(fieldFilePath, []byte("")),
-//                             },
-//                     ),
-//                     want: Transaction{
-//                             Fields: []Field{},
-//                     },
-//             },
-//             {
-//                     description: "a valid deleteFile request",
-//                     setup: func() {
-//                             _ = ioutil.WriteFile(srv.Config.FileRoot+"testFile", []byte{0x00}, 0666)
-//                     },
-//                     account: Account{
-//                             Login:    "test",
-//                             Name:     "unnamed",
-//                             Password: "test",
-//                             Access:   []byte{255, 255, 255, 255, 255, 255, 255, 255},
-//                     },
-//                     request: NewTransaction(
-//                             tranDeleteFile, 0,
-//                             []Field{
-//                                     NewField(fieldFileName, []byte("testFile")),
-//                                     NewField(fieldFilePath, []byte("")),
-//                             },
-//                     ),
-//                     want: Transaction{
-//                             Fields: []Field{},
-//                     },
-//             },
-//             {
-//                     description: "an invalid request for a file that does not exist",
-//                     account: Account{
-//                             Login:    "test",
-//                             Name:     "unnamed",
-//                             Password: "test",
-//                             Access:   []byte{255, 255, 255, 255, 255, 255, 255, 255},
-//                     },
-//                     request: NewTransaction(
-//                             tranDeleteFile, 0,
-//                             []Field{
-//                                     NewField(fieldFileName, []byte("testFile")),
-//                                     NewField(fieldFilePath, []byte("")),
-//                             },
-//                     ),
-//                     want: Transaction{
-//                             Fields: []Field{
-//                                     NewField(fieldError, []byte("Cannot delete file testFile because it does not exist or cannot be found.")),
-//                             },
-//                     },
-//             },
-//     }
-//
-//     for _, test := range tests {
-//             test.Setup(srv)
-//
-//             c := NewClient("")
-//
-//             if err := c.JoinServer(fmt.Sprintf(":%v", port), test.account.Login, test.account.Password); err != nil {
-//                     t.Errorf("login failed: %v", err)
-//             }
-//
-//             if err := c.Send(test.request); err != nil {
-//                     t.Errorf("%v", err)
-//             }
-//
-//             tran := c.ReadTransactions()[0]
-//             for _, want := range test.want.Fields {
-//                     got := tran.GetField(want.Uint16ID())
-//                     if bytes.Compare(got.Data, want.Data) != 0 {
-//                             t.Errorf("%v: field mismatch:  want: %#v got: %#v", test.description, want.Data, got.Data)
-//                     }
-//             }
-//
-//             test.Teardown(srv)
-//     }
-// }
-//
-// func Test_authorize(t *testing.T) {
-//     accessBitmap := big.NewInt(int64(0))
-//     accessBitmap.SetBit(accessBitmap, accessCreateFolder, 1)
-//     fmt.Printf("%v %b %x\n", accessBitmap, accessBitmap, accessBitmap)
-//     fmt.Printf("%b\n", 0b10000)
-//
-//     type args struct {
-//             access    *[]byte
-//             reqAccess int
-//     }
-//     tests := []struct {
-//             name string
-//             args args
-//             want bool
-//     }{
-//             {
-//                     name: "fooz",
-//                     args: args{
-//                             access: &[]byte{4, 0, 0, 0, 0, 0, 0, 0x02},
-//                             reqAccess: accessDownloadFile,
-//                     },
-//                     want: true,
-//             },
-//     }
-//     for _, tt := range tests {
-//             t.Run(tt.name, func(t *testing.T) {
-//                     if got := authorize(tt.args.access, tt.args.reqAccess); got != tt.want {
-//                             t.Errorf("authorize() = %v, want %v", got, tt.want)
-//                     }
-//             })
-//     }
-// }
+type mockReadWriter struct {
+       RBuf bytes.Buffer
+       WBuf *bytes.Buffer
+}
+
+func (mrw mockReadWriter) Read(p []byte) (n int, err error) {
+       return mrw.RBuf.Read(p)
+}
+
+func (mrw mockReadWriter) Write(p []byte) (n int, err error) {
+       return mrw.WBuf.Write(p)
+}
+
+func TestServer_handleFileTransfer(t *testing.T) {
+       type fields struct {
+               Port          int
+               Accounts      map[string]*Account
+               Agreement     []byte
+               Clients       map[uint16]*ClientConn
+               ThreadedNews  *ThreadedNews
+               FileTransfers map[uint32]*FileTransfer
+               Config        *Config
+               ConfigDir     string
+               Logger        *zap.SugaredLogger
+               PrivateChats  map[uint32]*PrivateChat
+               NextGuestID   *uint16
+               TrackerPassID [4]byte
+               Stats         *Stats
+               FS            FileStore
+               outbox        chan Transaction
+               mux           sync.Mutex
+               flatNewsMux   sync.Mutex
+               FlatNews      []byte
+       }
+       type args struct {
+               ctx context.Context
+               rwc io.ReadWriter
+       }
+       tests := []struct {
+               name     string
+               fields   fields
+               args     args
+               wantErr  assert.ErrorAssertionFunc
+               wantDump string
+       }{
+               {
+                       name: "with invalid protocol",
+                       args: args{
+                               ctx: func() context.Context {
+                                       ctx := context.Background()
+                                       ctx = context.WithValue(ctx, contextKeyReq, requestCtx{})
+                                       return ctx
+                               }(),
+                               rwc: func() io.ReadWriter {
+                                       mrw := mockReadWriter{}
+                                       mrw.WBuf = &bytes.Buffer{}
+                                       mrw.RBuf.Write(
+                                               []byte{
+                                                       0, 0, 0, 0,
+                                                       0, 0, 0, 5,
+                                                       0, 0, 0x01, 0,
+                                                       0, 0, 0, 0,
+                                               },
+                                       )
+                                       return mrw
+                               }(),
+                       },
+                       wantErr: assert.Error,
+               },
+               {
+                       name: "with invalid transfer ID",
+                       args: args{
+                               ctx: func() context.Context {
+                                       ctx := context.Background()
+                                       ctx = context.WithValue(ctx, contextKeyReq, requestCtx{})
+                                       return ctx
+                               }(),
+                               rwc: func() io.ReadWriter {
+                                       mrw := mockReadWriter{}
+                                       mrw.WBuf = &bytes.Buffer{}
+                                       mrw.RBuf.Write(
+                                               []byte{
+                                                       0x48, 0x54, 0x58, 0x46,
+                                                       0, 0, 0, 5,
+                                                       0, 0, 0x01, 0,
+                                                       0, 0, 0, 0,
+                                               },
+                                       )
+                                       return mrw
+                               }(),
+                       },
+                       wantErr: assert.Error,
+               },
+               {
+                       name: "file download",
+                       fields: fields{
+                               FS: &OSFileStore{},
+                               Config: &Config{
+                                       FileRoot: func() string {
+                                               path, _ := os.Getwd()
+                                               return path + "/test/config/Files"
+                                       }()},
+                               Logger: NewTestLogger(),
+                               Stats:  &Stats{},
+                               FileTransfers: map[uint32]*FileTransfer{
+                                       uint32(5): {
+                                               ReferenceNumber: []byte{0, 0, 0, 5},
+                                               Type:            FileDownload,
+                                               FileName:        []byte("testfile-8b"),
+                                               FilePath:        []byte{},
+                                       },
+                               },
+                       },
+                       args: args{
+                               ctx: func() context.Context {
+                                       ctx := context.Background()
+                                       ctx = context.WithValue(ctx, contextKeyReq, requestCtx{})
+                                       return ctx
+                               }(),
+                               rwc: func() io.ReadWriter {
+                                       mrw := mockReadWriter{}
+                                       mrw.WBuf = &bytes.Buffer{}
+                                       mrw.RBuf.Write(
+                                               []byte{
+                                                       0x48, 0x54, 0x58, 0x46,
+                                                       0, 0, 0, 5,
+                                                       0, 0, 0x01, 0,
+                                                       0, 0, 0, 0,
+                                               },
+                                       )
+                                       return mrw
+                               }(),
+                       },
+                       wantErr: assert.NoError,
+                       wantDump: `00000000  46 49 4c 50 00 01 00 00  00 00 00 00 00 00 00 00  |FILP............|
+00000010  00 00 00 00 00 00 00 02  49 4e 46 4f 00 00 00 00  |........INFO....|
+00000020  00 00 00 00 00 00 00 55  41 4d 41 43 54 45 58 54  |.......UAMACTEXT|
+00000030  54 54 58 54 00 00 00 00  00 00 01 00 00 00 00 00  |TTXT............|
+00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
+00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
+00000060  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 0b  |................|
+00000070  74 65 73 74 66 69 6c 65  2d 38 62 00 00 44 41 54  |testfile-8b..DAT|
+00000080  41 00 00 00 00 00 00 00  00 00 00 00 08 7c 39 e0  |A............|9.|
+00000090  bc 64 e2 cd de 4d 41 43  52 00 00 00 00 00 00 00  |.d...MACR.......|
+000000a0  00 00 00 00 00                                    |.....|
+`,
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       s := &Server{
+                               Port:          tt.fields.Port,
+                               Accounts:      tt.fields.Accounts,
+                               Agreement:     tt.fields.Agreement,
+                               Clients:       tt.fields.Clients,
+                               ThreadedNews:  tt.fields.ThreadedNews,
+                               FileTransfers: tt.fields.FileTransfers,
+                               Config:        tt.fields.Config,
+                               ConfigDir:     tt.fields.ConfigDir,
+                               Logger:        tt.fields.Logger,
+                               Stats:         tt.fields.Stats,
+                               FS:            tt.fields.FS,
+                       }
+                       tt.wantErr(t, s.handleFileTransfer(tt.args.ctx, tt.args.rwc), fmt.Sprintf("handleFileTransfer(%v, %v)", tt.args.ctx, tt.args.rwc))
+
+                       assertTransferBytesEqual(t, tt.wantDump, tt.args.rwc.(mockReadWriter).WBuf.Bytes())
+               })
+       }
+}
index 4b9119d6a21aa60516fc9af8e3c2e323d4d7400b..4fa683ad0161c51447c9076d393cf6c06c97c040 100644 (file)
@@ -1,34 +1,17 @@
 package hotline
 
 import (
-       "fmt"
        "time"
 )
 
 type Stats struct {
-       LoginCount      int       `yaml:"login count"`
-       StartTime       time.Time `yaml:"start time"`
-       DownloadCounter int
-       UploadCounter   int
-}
-
-func (s *Stats) String() string {
-       template := `
-Server Stats:
-  Start Time:          %v
-  Uptime:                      %s
-  Login Count: %v
-`
-       d := time.Since(s.StartTime)
-       d = d.Round(time.Minute)
-       h := d / time.Hour
-       d -= h * time.Hour
-       m := d / time.Minute
+       CurrentlyConnected  int
+       DownloadsInProgress int
+       UploadsInProgress   int
+       ConnectionPeak      int
+       DownloadCounter     int
+       UploadCounter       int
 
-       return fmt.Sprintf(
-               template,
-               s.StartTime.Format(time.RFC1123Z),
-               fmt.Sprintf("%02d:%02d", h, m),
-               s.LoginCount,
-       )
+       LoginCount int       `yaml:"login count"`
+       StartTime  time.Time `yaml:"start time"`
 }
diff --git a/hotline/test/config/Files/testfile-1k b/hotline/test/config/Files/testfile-1k
new file mode 100644 (file)
index 0000000..31758a0
Binary files /dev/null and b/hotline/test/config/Files/testfile-1k differ
diff --git a/hotline/test/config/Files/testfile-8b b/hotline/test/config/Files/testfile-8b
new file mode 100644 (file)
index 0000000..ecb617f
--- /dev/null
@@ -0,0 +1 @@
+|9à¼dâÍÞ
\ No newline at end of file
index 9eb879474e43b7e018d605350906825a482cd088..077e6f3050f0a75942bbac83f25921df87831943 100644 (file)
@@ -39,7 +39,7 @@ const (
        tranGetFileInfo          = 206
        tranSetFileInfo          = 207
        tranMoveFile             = 208
-       tranMakeFileAlias        = 209 // TODO: implement file alias command
+       tranMakeFileAlias        = 209
        tranDownloadFldr         = 210
        // tranDownloadInfo         = 211 TODO: implement file transfer queue
        // tranDownloadBanner     = 212 TODO: figure out what this is used for
index 1b6911493862cbf177e42c8a22794abca7a5e20c..c3b8c592bf2f5d8ea55a533ce5c6251544d9175e 100644 (file)
@@ -249,10 +249,6 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
                formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(fieldData).Data)
        }
 
-       if bytes.Equal(t.GetField(fieldData).Data, []byte("/stats")) {
-               formattedMsg = strings.Replace(cc.Server.Stats.String(), "\n", "\r", -1)
-       }
-
        chatID := t.GetField(fieldChatID).Data
        // a non-nil chatID indicates the message belongs to a private chat
        if chatID != nil {
@@ -347,26 +343,30 @@ func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
        fileName := t.GetField(fieldFileName).Data
        filePath := t.GetField(fieldFilePath).Data
 
-       ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName, 0)
+       fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
+       if err != nil {
+               return res, err
+       }
+
+       fw, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
        if err != nil {
                return res, err
        }
 
        res = append(res, cc.NewReply(t,
-               NewField(fieldFileName, fileName),
-               NewField(fieldFileTypeString, ffo.FlatFileInformationFork.friendlyType()),
-               NewField(fieldFileCreatorString, ffo.FlatFileInformationFork.CreatorSignature),
-               NewField(fieldFileComment, ffo.FlatFileInformationFork.Comment),
-               NewField(fieldFileType, ffo.FlatFileInformationFork.TypeSignature),
-               NewField(fieldFileCreateDate, ffo.FlatFileInformationFork.CreateDate),
-               NewField(fieldFileModifyDate, ffo.FlatFileInformationFork.ModifyDate),
-               NewField(fieldFileSize, ffo.FlatFileDataForkHeader.DataSize[:]),
+               NewField(fieldFileName, []byte(fw.name)),
+               NewField(fieldFileTypeString, fw.ffo.FlatFileInformationFork.friendlyType()),
+               NewField(fieldFileCreatorString, fw.ffo.FlatFileInformationFork.friendlyCreator()),
+               NewField(fieldFileComment, fw.ffo.FlatFileInformationFork.Comment),
+               NewField(fieldFileType, fw.ffo.FlatFileInformationFork.TypeSignature),
+               NewField(fieldFileCreateDate, fw.ffo.FlatFileInformationFork.CreateDate),
+               NewField(fieldFileModifyDate, fw.ffo.FlatFileInformationFork.ModifyDate),
+               NewField(fieldFileSize, fw.totalSize()),
        ))
        return res, err
 }
 
 // HandleSetFileInfo updates a file or folder name and/or comment from the Get Info window
-// TODO: Implement support for comments
 // Fields used in the request:
 // * 201       File name
 // * 202       File path       Optional
@@ -382,36 +382,77 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
                return res, err
        }
 
+       fi, err := cc.Server.FS.Stat(fullFilePath)
+       if err != nil {
+               return res, err
+       }
+
+       hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
+       if err != nil {
+               return res, err
+       }
+       if t.GetField(fieldFileComment).Data != nil {
+               switch mode := fi.Mode(); {
+               case mode.IsDir():
+                       if !authorize(cc.Account.Access, accessSetFolderComment) {
+                               res = append(res, cc.NewErrReply(t, "You are not allowed to set comments for folders."))
+                               return res, err
+                       }
+               case mode.IsRegular():
+                       if !authorize(cc.Account.Access, accessSetFileComment) {
+                               res = append(res, cc.NewErrReply(t, "You are not allowed to set comments for files."))
+                               return res, err
+                       }
+               }
+
+               hlFile.ffo.FlatFileInformationFork.setComment(t.GetField(fieldFileComment).Data)
+               w, err := hlFile.infoForkWriter()
+               if err != nil {
+                       return res, err
+               }
+               _, err = w.Write(hlFile.ffo.FlatFileInformationFork.MarshalBinary())
+               if err != nil {
+                       return res, err
+               }
+       }
+
        fullNewFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, t.GetField(fieldFileNewName).Data)
        if err != nil {
                return nil, err
        }
 
-       // fileComment := t.GetField(fieldFileComment).Data
        fileNewName := t.GetField(fieldFileNewName).Data
 
        if fileNewName != nil {
-               fi, err := cc.Server.FS.Stat(fullFilePath)
-               if err != nil {
-                       return res, err
-               }
                switch mode := fi.Mode(); {
                case mode.IsDir():
                        if !authorize(cc.Account.Access, accessRenameFolder) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to rename folders."))
                                return res, err
                        }
+                       err = os.Rename(fullFilePath, fullNewFilePath)
+                       if os.IsNotExist(err) {
+                               res = append(res, cc.NewErrReply(t, "Cannot rename folder "+string(fileName)+" because it does not exist or cannot be found."))
+                               return res, err
+                       }
                case mode.IsRegular():
                        if !authorize(cc.Account.Access, accessRenameFile) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to rename files."))
                                return res, err
                        }
-               }
-
-               err = os.Rename(fullFilePath, fullNewFilePath)
-               if os.IsNotExist(err) {
-                       res = append(res, cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found."))
-                       return res, err
+                       fileDir, err := readPath(cc.Server.Config.FileRoot, filePath, []byte{})
+                       if err != nil {
+                               return nil, err
+                       }
+                       hlFile.name = string(fileNewName)
+                       err = hlFile.move(fileDir)
+                       if os.IsNotExist(err) {
+                               res = append(res, cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found."))
+                               return res, err
+                       }
+                       if err != nil {
+                               panic(err)
+                       }
                }
        }
 
@@ -433,13 +474,17 @@ func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
                return res, err
        }
 
-       cc.Server.Logger.Debugw("Delete file", "src", fullFilePath)
+       hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
+       if err != nil {
+               return res, err
+       }
 
-       fi, err := os.Stat(fullFilePath)
+       fi, err := hlFile.dataFile()
        if err != nil {
                res = append(res, cc.NewErrReply(t, "Cannot delete file "+string(fileName)+" because it does not exist or cannot be found."))
                return res, nil
        }
+
        switch mode := fi.Mode(); {
        case mode.IsDir():
                if !authorize(cc.Account.Access, accessDeleteFolder) {
@@ -453,7 +498,7 @@ func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
                }
        }
 
-       if err := os.RemoveAll(fullFilePath); err != nil {
+       if err := hlFile.delete(); err != nil {
                return res, err
        }
 
@@ -464,13 +509,26 @@ func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
 // HandleMoveFile moves files or folders. Note: seemingly not documented
 func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        fileName := string(t.GetField(fieldFileName).Data)
-       filePath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFilePath).Data)
-       fileNewPath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFileNewPath).Data)
+
+       filePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(fieldFilePath).Data, t.GetField(fieldFileName).Data)
+       if err != nil {
+               return res, err
+       }
+
+       fileNewPath, err := readPath(cc.Server.Config.FileRoot, t.GetField(fieldFileNewPath).Data, nil)
+       if err != nil {
+               return res, err
+       }
 
        cc.Server.Logger.Debugw("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
 
-       fp := filePath + "/" + fileName
-       fi, err := os.Stat(fp)
+       hlFile, err := newFileWrapper(cc.Server.FS, filePath, 0)
+
+       fi, err := hlFile.dataFile()
+       if err != nil {
+               res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
+               return res, err
+       }
        if err != nil {
                return res, err
        }
@@ -486,16 +544,10 @@ func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err erro
                        return res, err
                }
        }
-
-       err = os.Rename(filePath+"/"+fileName, fileNewPath+"/"+fileName)
-       if os.IsNotExist(err) {
-               res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
+       if err := hlFile.move(fileNewPath); err != nil {
                return res, err
        }
-       if err != nil {
-               return []Transaction{}, err
-       }
-       // TODO: handle other possible errors; e.g. file delete fails due to file permission issue
+       // TODO: handle other possible errors; e.g. fileWrapper delete fails due to fileWrapper permission issue
 
        res = append(res, cc.NewReply(t))
        return res, err
@@ -673,11 +725,11 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
                login := DecodeUserString(getField(fieldUserLogin, &subFields).Data)
 
-               // check if the login exists; if so, we know we are updating an existing user
+               // check if the login dataFile; if so, we know we are updating an existing user
                if acc, ok := cc.Server.Accounts[login]; ok {
                        cc.Server.Logger.Infow("UpdateUser", "login", login)
 
-                       // account exists, so this is an update action
+                       // account dataFile, so this is an update action
                        if !authorize(cc.Account.Access, accessModifyUser) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
                                return res, err
@@ -737,7 +789,7 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
 
        login := DecodeUserString(t.GetField(fieldUserLogin).Data)
 
-       // If the account already exists, reply with an error
+       // If the account already dataFile, reply with an error
        if _, ok := cc.Server.Accounts[login]; ok {
                res = append(res, cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login."))
                return res, err
@@ -1310,7 +1362,6 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err
 
        fileName := t.GetField(fieldFileName).Data
        filePath := t.GetField(fieldFilePath).Data
-
        resumeData := t.GetField(fieldFileResumeData).Data
 
        var dataOffset int64
@@ -1319,16 +1370,16 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err
                if err := frd.UnmarshalBinary(t.GetField(fieldFileResumeData).Data); err != nil {
                        return res, err
                }
+               // TODO: handle rsrc fork offset
                dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:]))
        }
 
-       var fp FilePath
-       err = fp.UnmarshalBinary(filePath)
+       fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
        if err != nil {
                return res, err
        }
 
-       ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName, dataOffset)
+       hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, dataOffset)
        if err != nil {
                return res, err
        }
@@ -1343,6 +1394,7 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err
                Type:            FileDownload,
        }
 
+       // TODO: refactor to remove this
        if resumeData != nil {
                var frd FileResumeData
                if err := frd.UnmarshalBinary(t.GetField(fieldFileResumeData).Data); err != nil {
@@ -1351,14 +1403,14 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err
                ft.fileResumeData = &frd
        }
 
-       xferSize := ffo.TransferSize()
+       xferSize := hlFile.ffo.TransferSize(0)
 
        // Optional field for when a HL v1.5+ client requests file preview
        // Used only for TEXT, JPEG, GIFF, BMP or PICT files
        // The value will always be 2
        if t.GetField(fieldFileTransferOptions).Data != nil {
                ft.options = t.GetField(fieldFileTransferOptions).Data
-               xferSize = ffo.FlatFileDataForkHeader.DataSize[:]
+               xferSize = hlFile.ffo.FlatFileDataForkHeader.DataSize[:]
        }
 
        cc.Server.mux.Lock()
@@ -1371,7 +1423,7 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err
                NewField(fieldRefNum, transactionRef),
                NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
                NewField(fieldTransferSize, xferSize),
-               NewField(fieldFileSize, ffo.FlatFileDataForkHeader.DataSize[:]),
+               NewField(fieldFileSize, hlFile.ffo.FlatFileDataForkHeader.DataSize[:]),
        ))
 
        return res, err
@@ -1516,7 +1568,7 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
        replyT := cc.NewReply(t, NewField(fieldRefNum, transactionRef))
 
-       // client has requested to resume a partially transfered file
+       // client has requested to resume a partially transferred file
        if transferOptions != nil {
                fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
                if err != nil {
@@ -1827,7 +1879,7 @@ func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, er
        return res, err
 }
 
-// HandleMakeAlias makes a file alias using the specified path.
+// HandleMakeAlias makes a filer alias using the specified path.
 // Fields used in the request:
 // 201 File name
 // 202 File path
index 01ae862fc964e2dcb8276a91189037195cf89b3b..ebb4a95c303f7e43980e6e32ec4f798fff5df747 100644 (file)
@@ -9,6 +9,7 @@ import (
        "os"
        "strings"
        "testing"
+       "time"
 )
 
 func TestHandleSetChatSubject(t *testing.T) {
@@ -625,6 +626,7 @@ func TestHandleGetFileInfo(t *testing.T) {
                                cc: &ClientConn{
                                        ID: &[]byte{0x00, 0x01},
                                        Server: &Server{
+                                               FS: &OSFileStore{},
                                                Config: &Config{
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
@@ -672,7 +674,7 @@ func TestHandleGetFileInfo(t *testing.T) {
                                return
                        }
 
-                       // Clear the file timestamp fields to work around problems running the tests in multiple timezones
+                       // Clear the fileWrapper timestamp fields to work around problems running the tests in multiple timezones
                        // TODO: revisit how to test this by mocking the stat calls
                        gotRes[0].Fields[5].Data = make([]byte, 8)
                        gotRes[0].Fields[6].Data = make([]byte, 8)
@@ -1398,7 +1400,7 @@ func TestHandleDeleteUser(t *testing.T) {
                wantErr assert.ErrorAssertionFunc
        }{
                {
-                       name: "when user exists",
+                       name: "when user dataFile",
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
@@ -1748,6 +1750,7 @@ func TestHandleDownloadFile(t *testing.T) {
                                                }(),
                                        },
                                        Server: &Server{
+                                               FS:            &OSFileStore{},
                                                FileTransfers: make(map[uint32]*FileTransfer),
                                                Config: &Config{
                                                        FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(),
@@ -1779,12 +1782,99 @@ func TestHandleDownloadFile(t *testing.T) {
                        },
                        wantErr: assert.NoError,
                },
+               {
+                       name: "when client requests to resume 1k test file at offset 256",
+                       args: args{
+                               cc: &ClientConn{
+                                       Transfers: make(map[int][]*FileTransfer),
+                                       Account: &Account{
+                                               Access: func() *[]byte {
+                                                       var bits accessBitmap
+                                                       bits.Set(accessDownloadFile)
+                                                       access := bits[:]
+                                                       return &access
+                                               }(),
+                                       },
+                                       Server: &Server{
+                                               FS: &OSFileStore{},
+                                               // FS: func() *MockFileStore {
+                                               //      path, _ := os.Getwd()
+                                               //      testFile, err := os.Open(path + "/test/config/Files/testfile-1k")
+                                               //      if err != nil {
+                                               //              panic(err)
+                                               //      }
+                                               //
+                                               //      mfi := &MockFileInfo{}
+                                               //      mfi.On("Mode").Return(fs.FileMode(0))
+                                               //      mfs := &MockFileStore{}
+                                               //      mfs.On("Stat", "/fakeRoot/Files/testfile.txt").Return(mfi, nil)
+                                               //      mfs.On("Open", "/fakeRoot/Files/testfile.txt").Return(testFile, nil)
+                                               //      mfs.On("Stat", "/fakeRoot/Files/.info_testfile.txt").Return(nil, errors.New("no"))
+                                               //      mfs.On("Stat", "/fakeRoot/Files/.rsrc_testfile.txt").Return(nil, errors.New("no"))
+                                               //
+                                               //      return mfs
+                                               // }(),
+                                               FileTransfers: make(map[uint32]*FileTransfer),
+                                               Config: &Config{
+                                                       FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(),
+                                               },
+                                               Accounts: map[string]*Account{},
+                                       },
+                               },
+                               t: NewTransaction(
+                                       accessDownloadFile,
+                                       &[]byte{0, 1},
+                                       NewField(fieldFileName, []byte("testfile-1k")),
+                                       NewField(fieldFilePath, []byte{0x00, 0x00}),
+                                       NewField(
+                                               fieldFileResumeData,
+                                               func() []byte {
+                                                       frd := FileResumeData{
+                                                               Format:    [4]byte{},
+                                                               Version:   [2]byte{},
+                                                               RSVD:      [34]byte{},
+                                                               ForkCount: [2]byte{0, 2},
+                                                               ForkInfoList: []ForkInfoList{
+                                                                       {
+                                                                               Fork:     [4]byte{0x44, 0x41, 0x54, 0x41}, // "DATA"
+                                                                               DataSize: [4]byte{0, 0, 0x01, 0x00},       // request offset 256
+                                                                               RSVDA:    [4]byte{},
+                                                                               RSVDB:    [4]byte{},
+                                                                       },
+                                                                       {
+                                                                               Fork:     [4]byte{0x4d, 0x41, 0x43, 0x52}, // "MACR"
+                                                                               DataSize: [4]byte{0, 0, 0, 0},
+                                                                               RSVDA:    [4]byte{},
+                                                                               RSVDB:    [4]byte{},
+                                                                       },
+                                                               },
+                                                       }
+                                                       b, _ := frd.BinaryMarshal()
+                                                       return b
+                                               }(),
+                                       ),
+                               ),
+                       },
+                       wantRes: []Transaction{
+                               {
+                                       Flags:     0x00,
+                                       IsReply:   0x01,
+                                       Type:      []byte{0, 0x2},
+                                       ID:        []byte{0x9a, 0xcb, 0x04, 0x42},
+                                       ErrorCode: []byte{0, 0, 0, 0},
+                                       Fields: []Field{
+                                               NewField(fieldRefNum, []byte{0x52, 0xfd, 0xfc, 0x07}),
+                                               NewField(fieldWaitingCount, []byte{0x00, 0x00}),
+                                               NewField(fieldTransferSize, []byte{0x00, 0x00, 0x03, 0x8d}),
+                                               NewField(fieldFileSize, []byte{0x00, 0x00, 0x03, 0x00}),
+                                       },
+                               },
+                       },
+                       wantErr: assert.NoError,
+               },
        }
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
-                       // reset the rand seed so that the random fieldRefNum will be deterministic
-                       rand.Seed(1)
-
                        gotRes, err := HandleDownloadFile(tt.args.cc, tt.args.t)
                        if !tt.wantErr(t, err, fmt.Sprintf("HandleDownloadFile(%v, %v)", tt.args.cc, tt.args.t)) {
                                return
@@ -2244,3 +2334,153 @@ func TestHandleSendInstantMsg(t *testing.T) {
                })
        }
 }
+
+func TestHandleDeleteFile(t *testing.T) {
+       type args struct {
+               cc *ClientConn
+               t  *Transaction
+       }
+       tests := []struct {
+               name    string
+               args    args
+               wantRes []Transaction
+               wantErr assert.ErrorAssertionFunc
+       }{
+               {
+                       name: "when user does not have required permission to delete a folder",
+                       args: args{
+                               cc: &ClientConn{
+                                       Account: &Account{
+                                               Access: func() *[]byte {
+                                                       var bits accessBitmap
+                                                       access := bits[:]
+                                                       return &access
+                                               }(),
+                                       },
+                                       Server: &Server{
+                                               Config: &Config{
+                                                       FileRoot: func() string {
+                                                               return "/fakeRoot/Files"
+                                                       }(),
+                                               },
+                                               FS: func() *MockFileStore {
+                                                       mfi := &MockFileInfo{}
+                                                       mfi.On("Mode").Return(fs.FileMode(0))
+                                                       mfi.On("Size").Return(int64(100))
+                                                       mfi.On("ModTime").Return(time.Parse(time.Layout, time.Layout))
+                                                       mfi.On("IsDir").Return(false)
+                                                       mfi.On("Name").Return("testfile")
+
+                                                       mfs := &MockFileStore{}
+                                                       mfs.On("Stat", "/fakeRoot/Files/aaa/testfile").Return(mfi, nil)
+                                                       mfs.On("Stat", "/fakeRoot/Files/aaa/.info_testfile").Return(nil, errors.New("err"))
+                                                       mfs.On("Stat", "/fakeRoot/Files/aaa/.rsrc_testfile").Return(nil, errors.New("err"))
+
+                                                       return mfs
+                                               }(),
+                                               Accounts: map[string]*Account{},
+                                       },
+                               },
+                               t: NewTransaction(
+                                       tranDeleteFile, &[]byte{0, 1},
+                                       NewField(fieldFileName, []byte("testfile")),
+                                       NewField(fieldFilePath, []byte{
+                                               0x00, 0x01,
+                                               0x00, 0x00,
+                                               0x03,
+                                               0x61, 0x61, 0x61,
+                                       }),
+                               ),
+                       },
+                       wantRes: []Transaction{
+                               {
+                                       Flags:     0x00,
+                                       IsReply:   0x01,
+                                       Type:      []byte{0, 0x00},
+                                       ID:        []byte{0x9a, 0xcb, 0x04, 0x42},
+                                       ErrorCode: []byte{0, 0, 0, 1},
+                                       Fields: []Field{
+                                               NewField(fieldError, []byte("You are not allowed to delete files.")),
+                                       },
+                               },
+                       },
+                       wantErr: assert.NoError,
+               },
+               {
+                       name: "deletes all associated metadata files",
+                       args: args{
+                               cc: &ClientConn{
+                                       Account: &Account{
+                                               Access: func() *[]byte {
+                                                       var bits accessBitmap
+                                                       bits.Set(accessDeleteFile)
+                                                       access := bits[:]
+                                                       return &access
+                                               }(),
+                                       },
+                                       Server: &Server{
+                                               Config: &Config{
+                                                       FileRoot: func() string {
+                                                               return "/fakeRoot/Files"
+                                                       }(),
+                                               },
+                                               FS: func() *MockFileStore {
+                                                       mfi := &MockFileInfo{}
+                                                       mfi.On("Mode").Return(fs.FileMode(0))
+                                                       mfi.On("Size").Return(int64(100))
+                                                       mfi.On("ModTime").Return(time.Parse(time.Layout, time.Layout))
+                                                       mfi.On("IsDir").Return(false)
+                                                       mfi.On("Name").Return("testfile")
+
+                                                       mfs := &MockFileStore{}
+                                                       mfs.On("Stat", "/fakeRoot/Files/aaa/testfile").Return(mfi, nil)
+                                                       mfs.On("Stat", "/fakeRoot/Files/aaa/.info_testfile").Return(nil, errors.New("err"))
+                                                       mfs.On("Stat", "/fakeRoot/Files/aaa/.rsrc_testfile").Return(nil, errors.New("err"))
+
+                                                       mfs.On("RemoveAll", "/fakeRoot/Files/aaa/testfile").Return(nil)
+                                                       mfs.On("Remove", "/fakeRoot/Files/aaa/testfile.incomplete").Return(nil)
+                                                       mfs.On("Remove", "/fakeRoot/Files/aaa/.rsrc_testfile").Return(nil)
+                                                       mfs.On("Remove", "/fakeRoot/Files/aaa/.info_testfile").Return(nil)
+
+                                                       return mfs
+                                               }(),
+                                               Accounts: map[string]*Account{},
+                                       },
+                               },
+                               t: NewTransaction(
+                                       tranDeleteFile, &[]byte{0, 1},
+                                       NewField(fieldFileName, []byte("testfile")),
+                                       NewField(fieldFilePath, []byte{
+                                               0x00, 0x01,
+                                               0x00, 0x00,
+                                               0x03,
+                                               0x61, 0x61, 0x61,
+                                       }),
+                               ),
+                       },
+                       wantRes: []Transaction{
+                               {
+                                       Flags:     0x00,
+                                       IsReply:   0x01,
+                                       Type:      []byte{0x0, 0xcc},
+                                       ID:        []byte{0x0, 0x0, 0x0, 0x0},
+                                       ErrorCode: []byte{0, 0, 0, 0},
+                                       Fields:    []Field(nil),
+                               },
+                       },
+                       wantErr: assert.NoError,
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       gotRes, err := HandleDeleteFile(tt.args.cc, tt.args.t)
+                       if !tt.wantErr(t, err, fmt.Sprintf("HandleDeleteFile(%v, %v)", tt.args.cc, tt.args.t)) {
+                               return
+                       }
+
+                       tranAssertEqual(t, tt.wantRes, gotRes)
+
+                       tt.args.cc.Server.FS.(*MockFileStore).AssertExpectations(t)
+               })
+       }
+}
index 4e6f79f0e3380c4337e6dda59f54a3696a3218b3..73331970e33648900b0642de8754a234da933f0f 100644 (file)
@@ -29,89 +29,72 @@ func (tf *transfer) Write(b []byte) (int, error) {
        return len(b), nil
 }
 
-const fileCopyBufSize = 524288 // 512k
-func receiveFile(conn io.Reader, targetFile io.Writer, resForkFile io.Writer) error {
-       ffhBuf := make([]byte, 24)
-       if _, err := io.ReadFull(conn, ffhBuf); err != nil {
-               return err
-       }
-
-       var ffh FlatFileHeader
-       err := binary.Read(bytes.NewReader(ffhBuf), binary.BigEndian, &ffh)
-       if err != nil {
-               return err
-       }
-
-       ffifhBuf := make([]byte, 16)
-       if _, err := io.ReadFull(conn, ffifhBuf); err != nil {
-               return err
-       }
-
-       var ffifh FlatFileInformationForkHeader
-       err = binary.Read(bytes.NewReader(ffifhBuf), binary.BigEndian, &ffifh)
-       if err != nil {
-               return err
-       }
+const fileCopyBufSize = 4096
 
-       var ffif FlatFileInformationFork
-
-       dataLen := binary.BigEndian.Uint32(ffifh.DataSize[:])
-       ffifBuf := make([]byte, dataLen)
-       if _, err := io.ReadFull(conn, ffifBuf); err != nil {
-               return err
-       }
-       if err := ffif.UnmarshalBinary(ffifBuf); err != nil {
+func receiveFile(r io.Reader, targetFile, resForkFile, infoFork io.Writer) error {
+       var ffo flattenedFileObject
+       if _, err := ffo.ReadFrom(r); err != nil {
                return err
        }
 
-       var ffdfh FlatFileDataForkHeader
-       ffdfhBuf := make([]byte, 16)
-       if _, err := io.ReadFull(conn, ffdfhBuf); err != nil {
-               return err
-       }
-       err = binary.Read(bytes.NewReader(ffdfhBuf), binary.BigEndian, &ffdfh)
+       // Write the information fork
+       _, err := infoFork.Write(ffo.FlatFileInformationFork.MarshalBinary())
        if err != nil {
                return err
        }
 
-       // this will be zero if the file only has a resource fork
-       fileSize := int(binary.BigEndian.Uint32(ffdfh.DataSize[:]))
-
+       // read and write the data fork
        bw := bufio.NewWriterSize(targetFile, fileCopyBufSize)
-       _, err = io.CopyN(bw, conn, int64(fileSize))
-       if err != nil {
+       if _, err = io.CopyN(bw, r, ffo.dataSize()); err != nil {
                return err
        }
        if err := bw.Flush(); err != nil {
                return err
        }
 
-       if ffh.ForkCount == [2]byte{0, 3} {
-               var resForkHeader FlatFileDataForkHeader
-               if _, err := io.ReadFull(conn, resForkHeader.ForkType[:]); err != nil {
+       if ffo.FlatFileHeader.ForkCount == [2]byte{0, 3} {
+               if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileResForkHeader); err != nil {
                        return err
                }
 
-               if _, err := io.ReadFull(conn, resForkHeader.CompressionType[:]); err != nil {
+               bw = bufio.NewWriterSize(resForkFile, fileCopyBufSize)
+               _, err = io.CopyN(resForkFile, r, ffo.rsrcSize())
+               if err != nil {
                        return err
                }
-
-               if _, err := io.ReadFull(conn, resForkHeader.RSVD[:]); err != nil {
+               if err := bw.Flush(); err != nil {
                        return err
                }
+       }
+       return nil
+}
 
-               if _, err := io.ReadFull(conn, resForkHeader.DataSize[:]); err != nil {
-                       return err
-               }
+func sendFile(w io.Writer, r io.Reader, offset int) (err error) {
+       br := bufio.NewReader(r)
+       if _, err := br.Discard(offset); err != nil {
+               return err
+       }
 
-               bw = bufio.NewWriterSize(resForkFile, fileCopyBufSize)
-               _, err = io.CopyN(resForkFile, conn, int64(binary.BigEndian.Uint32(resForkHeader.DataSize[:])))
+       rSendBuffer := make([]byte, 1024)
+       for {
+               var bytesRead int
+
+               if bytesRead, err = br.Read(rSendBuffer); err == io.EOF {
+                       if _, err := w.Write(rSendBuffer[:bytesRead]); err != nil {
+                               return err
+                       }
+                       return nil
+               }
                if err != nil {
                        return err
                }
-               if err := bw.Flush(); err != nil {
+               // totalSent += int64(bytesRead)
+
+               // fileTransfer.BytesSent += bytesRead
+
+               if _, err := w.Write(rSendBuffer[:bytesRead]); err != nil {
                        return err
                }
        }
-       return nil
+
 }
index 255d914829cc5ca7e00b26b140f5cb422474896b..fb7d39cfa89da66614d2db0797c994dc852ce335 100644 (file)
@@ -115,20 +115,24 @@ func Test_receiveFile(t *testing.T) {
                wantErr         assert.ErrorAssertionFunc
        }{
                {
-                       name: "transfers file",
+                       name: "transfers file when there is no resource fork",
                        args: args{
                                conn: func() io.Reader {
                                        testFile := flattenedFileObject{
-                                               FlatFileHeader:                NewFlatFileHeader(),
-                                               FlatFileInformationForkHeader: FlatFileInformationForkHeader{},
+                                               FlatFileHeader: FlatFileHeader{
+                                                       Format:    [4]byte{0x46, 0x49, 0x4c, 0x50}, // "FILP"
+                                                       Version:   [2]byte{0, 1},
+                                                       RSVD:      [16]byte{},
+                                                       ForkCount: [2]byte{0, 2},
+                                               },
+                                               FlatFileInformationForkHeader: FlatFileForkHeader{},
                                                FlatFileInformationFork:       NewFlatFileInformationFork("testfile.txt", make([]byte, 8), "TEXT", "TEXT"),
-                                               FlatFileDataForkHeader: FlatFileDataForkHeader{
+                                               FlatFileDataForkHeader: FlatFileForkHeader{
                                                        ForkType:        [4]byte{0x4d, 0x41, 0x43, 0x52}, // DATA
                                                        CompressionType: [4]byte{0, 0, 0, 0},
                                                        RSVD:            [4]byte{0, 0, 0, 0},
                                                        DataSize:        [4]byte{0x00, 0x00, 0x00, 0x03},
                                                },
-                                               FileData: nil,
                                        }
                                        fakeFileData := []byte{1, 2, 3}
                                        b := testFile.BinaryMarshal()
@@ -141,12 +145,50 @@ func Test_receiveFile(t *testing.T) {
 
                        wantErr: assert.NoError,
                },
+               // {
+               //      name: "transfers fileWrapper when there is a resource fork",
+               //      args: args{
+               //              conn: func() io.Reader {
+               //                      testFile := flattenedFileObject{
+               //                              FlatFileHeader: FlatFileHeader{
+               //                                      Format:    [4]byte{0x46, 0x49, 0x4c, 0x50}, // "FILP"
+               //                                      Version:   [2]byte{0, 1},
+               //                                      RSVD:      [16]byte{},
+               //                                      ForkCount: [2]byte{0, 3},
+               //                              },
+               //                              FlatFileInformationForkHeader: FlatFileForkHeader{},
+               //                              FlatFileInformationFork:       NewFlatFileInformationFork("testfile.txt", make([]byte, 8), "TEXT", "TEXT"),
+               //                              FlatFileDataForkHeader: FlatFileForkHeader{
+               //                                      ForkType:        [4]byte{0x44, 0x41, 0x54, 0x41}, // DATA
+               //                                      CompressionType: [4]byte{0, 0, 0, 0},
+               //                                      RSVD:            [4]byte{0, 0, 0, 0},
+               //                                      DataSize:        [4]byte{0x00, 0x00, 0x00, 0x03},
+               //                              },
+               //                              FlatFileResForkHeader: FlatFileForkHeader{
+               //                                      ForkType:        [4]byte{0x4d, 0x41, 0x43, 0x52}, // MACR
+               //                                      CompressionType: [4]byte{0, 0, 0, 0},
+               //                                      RSVD:            [4]byte{0, 0, 0, 0},
+               //                                      DataSize:        [4]byte{0x00, 0x00, 0x00, 0x03},
+               //                              },
+               //                      }
+               //                      fakeFileData := []byte{1, 2, 3}
+               //                      b := testFile.BinaryMarshal()
+               //                      b = append(b, fakeFileData...)
+               //                      return bytes.NewReader(b)
+               //              }(),
+               //      },
+               //      wantTargetFile:  []byte{1, 2, 3},
+               //      wantResForkFile: []byte(nil),
+               //
+               //      wantErr: assert.NoError,
+               // },
        }
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
                        targetFile := &bytes.Buffer{}
                        resForkFile := &bytes.Buffer{}
-                       err := receiveFile(tt.args.conn, targetFile, resForkFile)
+                       infoForkFile := &bytes.Buffer{}
+                       err := receiveFile(tt.args.conn, targetFile, resForkFile, infoForkFile)
                        if !tt.wantErr(t, err, fmt.Sprintf("receiveFile(%v, %v, %v)", tt.args.conn, targetFile, resForkFile)) {
                                return
                        }