package mobius import ( "bytes" "encoding/binary" "fmt" "os" "path/filepath" "strings" "github.com/jhalter/mobius/hotline" ) // ResolveUserPath processes paths for the special `~` directory. func ResolveUserPath(cc *hotline.ClientConn, requestedPath string) (string, error) { if !strings.HasPrefix(requestedPath, "~") { return requestedPath, nil } userFolder := filepath.Join("~", cc.Account.Login) actualUserFolder := filepath.Join(cc.FileRoot(), userFolder) if stat, err := os.Stat(actualUserFolder); err == nil && stat.IsDir() { return strings.Replace(requestedPath, "~", userFolder, 1), nil } return "", fmt.Errorf("user folder does not exist") } // updateTransactionPath updates the FieldFilePath in a transaction in-place. func updateTransactionPath(t *hotline.Transaction, newPath string) { for i, field := range t.Fields { if field.Type == hotline.FieldFilePath { if encodedPath, err := txtEncoder.String(newPath); err == nil { if fpBytes, err := encodeFilePath(encodedPath); err == nil { t.Fields[i].Data = fpBytes } } return } } } // encodeFilePath encodes a string path into hotline.FilePath binary format. func encodeFilePath(path string) ([]byte, error) { components := strings.Split(path, string(filepath.Separator)) var fp hotline.FilePath binary.BigEndian.PutUint16(fp.ItemCount[:], uint16(len(components))) var buffer bytes.Buffer if err := binary.Write(&buffer, binary.BigEndian, fp.ItemCount); err != nil { return nil, err } for _, component := range components { if component == "" || len(component) > 255 { return nil, fmt.Errorf("invalid file path component") } encodedComponent, err := txtEncoder.String(component) if err != nil { return nil, err } buffer.Write([]byte{0x00, 0x00, byte(len(encodedComponent))}) buffer.Write([]byte(encodedComponent)) } return buffer.Bytes(), nil } // handleFileTransaction handles various file operations that require resolving `~` func handleFileTransaction(cc *hotline.ClientConn, t *hotline.Transaction, handler func(*hotline.ClientConn, *hotline.Transaction) []hotline.Transaction, errMsg string) []hotline.Transaction { requestedPath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFilePath).Data, nil) if err != nil { return nil } sliceLen := min(len(cc.FileRoot()) + 1, len(requestedPath)) resolvedPath, err := ResolveUserPath(cc, requestedPath[sliceLen:]) if err != nil { return cc.NewErrReply(t, errMsg) } updateTransactionPath(t, resolvedPath) return handler(cc, t) } // File operation handlers func HandleUploadFileWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { return handleFileTransaction(cc, t, HandleUploadFile, "Cannot upload to non-existent user folder.") } func HandleUploadFolderWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { return handleFileTransaction(cc, t, HandleUploadFolder, "Cannot upload to non-existent user folder.") } func HandleDeleteFileWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { return handleFileTransaction(cc, t, HandleDeleteFile, "Cannot delete non-existent file.") } func HandleDownloadFileWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { return handleFileTransaction(cc, t, HandleDownloadFile, "Cannot download non-existent user file.") } func HandleDownloadFolderWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { return handleFileTransaction(cc, t, HandleDownloadFolder, "Cannot download non-existent user folder.") } // HandleGetFileNameListWithUserFolders modifies the file listing behavior for `~` func HandleGetFileNameListWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { requestedPath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFilePath).Data, nil) if err != nil { return nil } if requestedPath == cc.FileRoot() { fileNames, err := hotline.GetFileNameList(cc.FileRoot(), cc.Server.Config.IgnoreFiles) if err != nil { return nil } userFolder := filepath.Join(cc.FileRoot(), "~", cc.Account.Login) if stat, err := os.Stat(userFolder); err != nil || !stat.IsDir() { filteredFiles := make([]hotline.Field, 0, len(fileNames)) for _, file := range fileNames { if !strings.Contains(string(file.Data), "~") { filteredFiles = append(filteredFiles, file) } } return []hotline.Transaction{cc.NewReply(t, filteredFiles...)} } return []hotline.Transaction{cc.NewReply(t, fileNames...)} } if strings.HasPrefix(requestedPath, filepath.Join(cc.FileRoot(), "~")) { resolvedPath, err := ResolveUserPath(cc, requestedPath[len(cc.FileRoot())+1:]) if err != nil { return nil } updateTransactionPath(t, resolvedPath) return HandleGetFileNameList(cc, t) } return HandleGetFileNameList(cc, t) }