Skip to content

Commit dd7b983

Browse files
M09Icclaude
andcommitted
fix: cross-platform path handling in mock filesystem and download
Adds RemoteBase, RemoteDir, RemoteJoin helpers to fileutils for handling implant paths (which may use Windows backslashes) on any host OS. Replaces all filepath.Base/Dir/Join calls on remote paths with these cross-platform helpers across download, explorer, mock filesystem, and e2e tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 48c3bfc commit dd7b983

5 files changed

Lines changed: 53 additions & 43 deletions

File tree

client/command/explorer/file.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import (
66
"github.com/chainreactors/IoM-go/proto/client/clientpb"
77
"github.com/chainreactors/IoM-go/proto/implant/implantpb"
88
"github.com/chainreactors/malice-network/client/core"
9+
"github.com/chainreactors/malice-network/helper/utils/fileutils"
910
"github.com/chainreactors/tui"
1011
tea "github.com/charmbracelet/bubbletea"
1112
"github.com/spf13/cobra"
12-
"path/filepath"
13-
"strconv"
13+
"strconv"
1414
"strings"
1515
"time"
1616
)
@@ -87,7 +87,7 @@ func fileExplorerCmd(cmd *cobra.Command, con *core.Console) {
8787
return
8888
}
8989
fileModel = fileModel.SetHeaderView(func(m *tui.TreeModel) string {
90-
return fmt.Sprintf("Current Path: %s%s\n", root.Name, filepath.Join(m.Selected...))
90+
return fmt.Sprintf("Current Path: %s%s\n", root.Name, fileutils.RemoteJoin(m.Selected...))
9191
})
9292
// Register custom action for 'enter' key
9393
fileModel = fileModel.SetKeyBinding("enter", func(m *tui.TreeModel) (tea.Model, tea.Cmd) {
@@ -150,10 +150,10 @@ func fileEnterFunc(cmd *cobra.Command, m *tui.TreeModel, con *core.Console) (tea
150150
if selectedNode.Info[0] == "false" {
151151
return m, nil
152152
}
153-
path := filepath.Join(m.Selected...)
153+
path := fileutils.RemoteJoin(m.Selected...)
154154
task, err := con.Rpc.Ls(session.Clone(consts.CalleeExplorer).Context(), &implantpb.Request{
155155
Name: consts.ModuleLs,
156-
Input: filepath.Join(m.Root.Name, path, selectedNode.Name),
156+
Input: fileutils.RemoteJoin(m.Root.Name, path, selectedNode.Name),
157157
})
158158
session.Console(task, string(*con.App.Shell().Line()))
159159
if err != nil {

client/command/file/download.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import (
66
"github.com/chainreactors/IoM-go/proto/implant/implantpb"
77
"github.com/chainreactors/IoM-go/proto/services/clientrpc"
88
"github.com/chainreactors/malice-network/client/core"
9+
"github.com/chainreactors/malice-network/helper/utils/fileutils"
910
"github.com/spf13/cobra"
10-
"path"
11-
"strings"
1211
)
1312

1413
func DownloadCmd(cmd *cobra.Command, con *core.Console) error {
@@ -24,16 +23,9 @@ func DownloadCmd(cmd *cobra.Command, con *core.Console) error {
2423
return nil
2524
}
2625

27-
// remotePath extracts the basename from a remote path that may use
28-
// either forward slashes (Unix) or backslashes (Windows).
29-
func remotePath(p string) string {
30-
// Normalise Windows backslashes so path.Base works on all platforms.
31-
return path.Base(strings.ReplaceAll(p, `\`, "/"))
32-
}
33-
3426
func Download(rpc clientrpc.MaliceRPCClient, session *client.Session, path string, is_dir bool) (*clientpb.Task, error) {
3527
task, err := rpc.Download(session.Context(), &implantpb.DownloadRequest{
36-
Name: remotePath(path),
28+
Name: fileutils.RemoteBase(path),
3729
Path: path,
3830
Dir: is_dir,
3931
})

helper/utils/fileutils/path.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fileutils
22

33
import (
4+
"path"
45
"regexp"
56
"strings"
67
)
@@ -81,6 +82,29 @@ func CheckLinuxPath(path string) bool {
8182
}
8283
return true
8384
}
85+
// toForwardSlash normalises a remote path (which may use Windows backslashes)
86+
// to forward slashes so that the standard path package works on any OS.
87+
func toForwardSlash(p string) string {
88+
return strings.ReplaceAll(p, `\`, "/")
89+
}
90+
91+
// RemoteBase extracts the last element of a remote path that may use either
92+
// forward or backward slashes, working correctly on any host OS.
93+
func RemoteBase(p string) string {
94+
return path.Base(toForwardSlash(p))
95+
}
96+
97+
// RemoteDir returns the parent directory of a remote path.
98+
func RemoteDir(p string) string {
99+
return path.Dir(toForwardSlash(p))
100+
}
101+
102+
// RemoteJoin joins remote path elements using forward slashes,
103+
// suitable for building implant-side paths that work across OSes.
104+
func RemoteJoin(elem ...string) string {
105+
return path.Join(elem...)
106+
}
107+
84108
func CheckFullPath(path string) bool {
85109
checkWindowsPath := CheckWindowsPath(path)
86110
checkLinuxPath := CheckLinuxPath(path)

server/mock_implant_common_rpc_e2e_test.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package main
55
import (
66
"context"
77
"os"
8-
"path/filepath"
8+
stdpath "path"
99
"strings"
1010
"testing"
1111
"time"
@@ -14,6 +14,7 @@ import (
1414
"github.com/chainreactors/IoM-go/proto/client/clientpb"
1515
implantpb "github.com/chainreactors/IoM-go/proto/implant/implantpb"
1616
"github.com/chainreactors/IoM-go/proto/services/clientrpc"
17+
"github.com/chainreactors/malice-network/helper/utils/fileutils"
1718
"github.com/chainreactors/malice-network/helper/utils/output"
1819
"github.com/chainreactors/malice-network/server/internal/core"
1920
"github.com/chainreactors/malice-network/server/testsupport"
@@ -115,17 +116,19 @@ func requireAddon(t *testing.T, addons []*implantpb.Addon, want string) {
115116
t.Fatalf("addon list does not contain %q", want)
116117
}
117118

118-
func normalizePath(path string) string {
119-
path = strings.TrimSpace(path)
120-
if path == "" {
119+
func normalizePath(p string) string {
120+
p = strings.TrimSpace(p)
121+
if p == "" {
121122
return ""
122123
}
123-
path = strings.ReplaceAll(path, "/", `\`)
124-
path = filepath.Clean(path)
125-
if len(path) == 2 && strings.HasSuffix(path, ":") {
126-
path += `\`
124+
// Normalise to forward slashes for path.Clean, then back to backslashes.
125+
p = strings.ReplaceAll(p, `\`, "/")
126+
p = stdpath.Clean(p)
127+
p = strings.ReplaceAll(p, "/", `\`)
128+
if len(p) == 2 && strings.HasSuffix(p, ":") {
129+
p += `\`
127130
}
128-
return strings.ToLower(path)
131+
return strings.ToLower(p)
129132
}
130133

131134
func requireFileInfo(t *testing.T, files []*implantpb.FileInfo, want string) {
@@ -1022,7 +1025,7 @@ func TestMockImplantFilesystemMutationRPCsE2E(t *testing.T) {
10221025
func TestMockImplantFileTransferRPCsE2E(t *testing.T) {
10231026
f := newMockRPCFixture(t)
10241027

1025-
uploadedPath := filepath.Join(f.lib.WorkDir, "uploaded.txt")
1028+
uploadedPath := fileutils.RemoteJoin(f.lib.WorkDir, "uploaded.txt")
10261029
uploadBody := []byte("uploaded from mock implant e2e")
10271030

10281031
uploadBefore := len(f.mock.RequestsByName(consts.ModuleUpload))
@@ -1083,7 +1086,7 @@ func TestMockImplantFileTransferRPCsE2E(t *testing.T) {
10831086
downloadBefore := len(f.mock.RequestsByName(consts.ModuleDownload))
10841087
downloadTask, err := f.rpc.Download(f.session, &implantpb.DownloadRequest{
10851088
Path: f.lib.NotesPath,
1086-
Name: filepath.Base(f.lib.NotesPath),
1089+
Name: fileutils.RemoteBase(f.lib.NotesPath),
10871090
})
10881091
if err != nil {
10891092
t.Fatalf("Download failed: %v", err)

server/testsupport/mock_scenarios.go

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"encoding/hex"
99
"fmt"
1010
stdpath "path"
11+
12+
"github.com/chainreactors/malice-network/helper/utils/fileutils"
1113
"sort"
1214
"strings"
1315
"sync"
@@ -1117,10 +1119,10 @@ func (s *MockScenarioLibrary) ensureDir(path string) {
11171119
return
11181120
}
11191121
s.dirEntries[path] = map[string]*implantpb.FileInfo{}
1120-
parent := s.normPath(winDir(path))
1122+
parent := s.normPath(fileutils.RemoteDir(path))
11211123
if parent != "" && parent != path {
11221124
s.ensureDir(parent)
1123-
name := winBase(path)
1125+
name := fileutils.RemoteBase(path)
11241126
s.dirEntries[parent][name] = &implantpb.FileInfo{
11251127
Name: name,
11261128
IsDir: true,
@@ -1133,9 +1135,9 @@ func (s *MockScenarioLibrary) ensureDir(path string) {
11331135

11341136
func (s *MockScenarioLibrary) setFile(path string, data []byte) {
11351137
path = s.normPath(path)
1136-
parent := s.normPath(winDir(path))
1138+
parent := s.normPath(fileutils.RemoteDir(path))
11371139
s.ensureDir(parent)
1138-
name := winBase(path)
1140+
name := fileutils.RemoteBase(path)
11391141
s.dirEntries[parent][name] = &implantpb.FileInfo{
11401142
Name: name,
11411143
IsDir: false,
@@ -1148,8 +1150,8 @@ func (s *MockScenarioLibrary) setFile(path string, data []byte) {
11481150

11491151
func (s *MockScenarioLibrary) removePath(path string) {
11501152
path = s.normPath(path)
1151-
parent := s.normPath(winDir(path))
1152-
name := winBase(path)
1153+
parent := s.normPath(fileutils.RemoteDir(path))
1154+
name := fileutils.RemoteBase(path)
11531155
delete(s.fileContents, path)
11541156
delete(s.dirEntries, path)
11551157
if entries, ok := s.dirEntries[parent]; ok {
@@ -1281,17 +1283,6 @@ func (s *MockScenarioLibrary) normPath(p string) string {
12811283
return strings.ToLower(p)
12821284
}
12831285

1284-
// winDir returns the parent directory of a Windows-style backslash path,
1285-
// working correctly on any OS by normalising to forward slashes first.
1286-
func winDir(p string) string {
1287-
return strings.ReplaceAll(stdpath.Dir(strings.ReplaceAll(p, `\`, "/")), "/", `\`)
1288-
}
1289-
1290-
// winBase returns the last element of a Windows-style backslash path.
1291-
func winBase(p string) string {
1292-
return stdpath.Base(strings.ReplaceAll(p, `\`, "/"))
1293-
}
1294-
12951286
func (s *MockScenarioLibrary) execOutputLocked(request *implantpb.ExecRequest) ([]MockExecChunk, []byte) {
12961287
if request == nil {
12971288
return nil, nil

0 commit comments

Comments
 (0)