@xiaozhi-client/cli 2.0.0 → 2.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/Container.ts +8 -0
- package/src/commands/CommandHandlerFactory.ts +9 -0
- package/src/commands/ConfigCommandHandler.ts +22 -14
- package/src/services/DaemonManager.ts +8 -0
- package/src/services/ServiceManager.ts +8 -0
- package/src/services/TemplateManager.ts +9 -0
- package/src/utils/__tests__/path-utils.config.test.ts +214 -0
- package/src/utils/__tests__/path-utils.executable.test.ts +424 -0
- package/src/utils/__tests__/path-utils.filesystem.test.ts +278 -0
- package/src/utils/__tests__/path-utils.security.test.ts +357 -0
- package/src/utils/__tests__/path-utils.test.ts +0 -1165
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { PathUtils } from "../PathUtils.js";
|
|
6
|
+
|
|
7
|
+
// Mock dependencies
|
|
8
|
+
vi.mock("node:os", () => ({
|
|
9
|
+
tmpdir: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("node:url", () => ({
|
|
13
|
+
fileURLToPath: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock environment variables
|
|
17
|
+
const originalEnv = process.env;
|
|
18
|
+
|
|
19
|
+
describe("PathUtils - 路径安全性", () => {
|
|
20
|
+
let mockTmpdir: any;
|
|
21
|
+
let mockFileURLToPath: any;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
|
|
26
|
+
// Setup mocks
|
|
27
|
+
mockTmpdir = vi.mocked(tmpdir);
|
|
28
|
+
mockFileURLToPath = vi.mocked(fileURLToPath);
|
|
29
|
+
|
|
30
|
+
// Default mock implementations
|
|
31
|
+
mockTmpdir.mockReturnValue("/tmp");
|
|
32
|
+
mockFileURLToPath.mockReturnValue("/test/src/cli/utils/PathUtils.js");
|
|
33
|
+
|
|
34
|
+
// Reset environment variables
|
|
35
|
+
process.env = { ...originalEnv };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
process.env = originalEnv;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("validatePath 验证路径安全性", () => {
|
|
43
|
+
it("应该验证安全的路径", () => {
|
|
44
|
+
const safePaths = [
|
|
45
|
+
"normal/path/file.txt",
|
|
46
|
+
"./relative/path",
|
|
47
|
+
"file.txt",
|
|
48
|
+
"dir/subdir/file.ext",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
for (const safePath of safePaths) {
|
|
52
|
+
const result = PathUtils.validatePath(safePath);
|
|
53
|
+
expect(result).toBe(true);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("应该拒绝包含路径遍历的路径", () => {
|
|
58
|
+
const unsafePaths = [
|
|
59
|
+
"../../../etc/passwd",
|
|
60
|
+
"dir/../../../secret",
|
|
61
|
+
"normal/../../../file",
|
|
62
|
+
"..\\..\\windows\\system32",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const unsafePath of unsafePaths) {
|
|
66
|
+
const result = PathUtils.validatePath(unsafePath);
|
|
67
|
+
expect(result).toBe(false);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("应该处理复杂的路径遍历尝试", () => {
|
|
72
|
+
const complexUnsafePaths = [
|
|
73
|
+
"dir/./../../file",
|
|
74
|
+
"normal/path/../../../../../../etc/passwd",
|
|
75
|
+
"./../config",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
for (const unsafePath of complexUnsafePaths) {
|
|
79
|
+
const result = PathUtils.validatePath(unsafePath);
|
|
80
|
+
expect(result).toBe(false);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("ensurePathWithin 确保路径在指定目录内", () => {
|
|
86
|
+
it("应该返回在基础目录内的安全路径", () => {
|
|
87
|
+
const baseDir = "/safe/base";
|
|
88
|
+
const inputPath = "subdir/file.txt";
|
|
89
|
+
|
|
90
|
+
const result = PathUtils.ensurePathWithin(inputPath, baseDir);
|
|
91
|
+
const expected = path.resolve(baseDir, inputPath);
|
|
92
|
+
|
|
93
|
+
expect(result).toBe(expected);
|
|
94
|
+
expect(result.startsWith(path.resolve(baseDir))).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("应该拒绝超出基础目录的路径", () => {
|
|
98
|
+
const baseDir = "/safe/base";
|
|
99
|
+
const inputPath = "../../../etc/passwd";
|
|
100
|
+
|
|
101
|
+
expect(() => {
|
|
102
|
+
PathUtils.ensurePathWithin(inputPath, baseDir);
|
|
103
|
+
}).toThrow("路径 ../../../etc/passwd 超出了允许的范围");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("应该处理绝对路径", () => {
|
|
107
|
+
const baseDir = "/safe/base";
|
|
108
|
+
const inputPath = "/safe/base/subdir/file.txt";
|
|
109
|
+
|
|
110
|
+
const result = PathUtils.ensurePathWithin(inputPath, baseDir);
|
|
111
|
+
|
|
112
|
+
expect(result).toBe(path.resolve(inputPath));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("应该拒绝指向基础目录外的绝对路径", () => {
|
|
116
|
+
const baseDir = "/safe/base";
|
|
117
|
+
const inputPath = "/dangerous/path/file.txt";
|
|
118
|
+
|
|
119
|
+
expect(() => {
|
|
120
|
+
PathUtils.ensurePathWithin(inputPath, baseDir);
|
|
121
|
+
}).toThrow("路径 /dangerous/path/file.txt 超出了允许的范围");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("createSafePath 创建安全的文件路径", () => {
|
|
126
|
+
it("应该创建安全的路径", () => {
|
|
127
|
+
const segments = ["dir", "subdir", "file.txt"];
|
|
128
|
+
|
|
129
|
+
const result = PathUtils.createSafePath(...segments);
|
|
130
|
+
const expected = path.normalize(path.join(...segments));
|
|
131
|
+
|
|
132
|
+
expect(result).toBe(expected);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("应该拒绝包含危险字符的路径", () => {
|
|
136
|
+
// 测试会导致规范化后包含 ".." 的路径
|
|
137
|
+
const dangerousSegments = [
|
|
138
|
+
["dir", "..", "file.txt"], // 规范化后: dir/../file.txt -> file.txt (不包含 "..")
|
|
139
|
+
["~", "file.txt"], // 规范化后: ~/file.txt (包含 "~")
|
|
140
|
+
["dir", "subdir", "../../../etc/passwd"], // 规范化后: ../etc/passwd (包含 "..")
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// 只有包含 "~" 或规范化后仍包含 ".." 的路径会抛出错误
|
|
144
|
+
expect(() => {
|
|
145
|
+
PathUtils.createSafePath("~", "file.txt");
|
|
146
|
+
}).toThrow();
|
|
147
|
+
|
|
148
|
+
expect(() => {
|
|
149
|
+
PathUtils.createSafePath("dir", "subdir", "../../../etc/passwd");
|
|
150
|
+
}).toThrow();
|
|
151
|
+
|
|
152
|
+
// 这个不会抛出错误,因为规范化后是 "file.txt"
|
|
153
|
+
expect(() => {
|
|
154
|
+
PathUtils.createSafePath("dir", "..", "file.txt");
|
|
155
|
+
}).not.toThrow();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("应该处理空的路径段", () => {
|
|
159
|
+
const result = PathUtils.createSafePath("dir", "", "file.txt");
|
|
160
|
+
const expected = path.normalize(path.join("dir", "", "file.txt"));
|
|
161
|
+
|
|
162
|
+
expect(result).toBe(expected);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("应该处理单个路径段", () => {
|
|
166
|
+
const result = PathUtils.createSafePath("file.txt");
|
|
167
|
+
|
|
168
|
+
expect(result).toBe("file.txt");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("边界条件和特殊情况", () => {
|
|
173
|
+
it("应该处理空字符串路径", () => {
|
|
174
|
+
expect(() => PathUtils.createSafePath("")).not.toThrow();
|
|
175
|
+
expect(PathUtils.validatePath("")).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("应该处理非常长的路径", () => {
|
|
179
|
+
const longPath = "a".repeat(1000);
|
|
180
|
+
|
|
181
|
+
expect(PathUtils.validatePath(longPath)).toBe(true);
|
|
182
|
+
expect(() => PathUtils.createSafePath(longPath)).not.toThrow();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("应该处理包含特殊字符的路径", () => {
|
|
186
|
+
const specialChars = [
|
|
187
|
+
"file name with spaces.txt",
|
|
188
|
+
"file-with-dashes.txt",
|
|
189
|
+
"file_with_underscores.txt",
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
for (const fileName of specialChars) {
|
|
193
|
+
expect(PathUtils.validatePath(fileName)).toBe(true);
|
|
194
|
+
expect(() => PathUtils.createSafePath("dir", fileName)).not.toThrow();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("应该处理 Unicode 字符", () => {
|
|
199
|
+
const unicodePaths = ["文件.txt", "file.txt", "ファイル.txt"];
|
|
200
|
+
|
|
201
|
+
for (const unicodePath of unicodePaths) {
|
|
202
|
+
expect(PathUtils.validatePath(unicodePath)).toBe(true);
|
|
203
|
+
expect(() =>
|
|
204
|
+
PathUtils.createSafePath("dir", unicodePath)
|
|
205
|
+
).not.toThrow();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("应该处理路径分隔符的不同组合", () => {
|
|
210
|
+
const paths = [
|
|
211
|
+
"dir/file.txt",
|
|
212
|
+
"dir\\file.txt",
|
|
213
|
+
"./dir/file.txt",
|
|
214
|
+
".\\dir\\file.txt",
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
for (const testPath of paths) {
|
|
218
|
+
if (!testPath.includes("..")) {
|
|
219
|
+
expect(PathUtils.validatePath(testPath)).toBe(true);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("跨平台兼容性", () => {
|
|
226
|
+
it("应该处理 Windows 风格的路径", () => {
|
|
227
|
+
const windowsPaths = [
|
|
228
|
+
"C:\\Users\\user\\file.txt",
|
|
229
|
+
"D:\\Projects\\app\\config.json",
|
|
230
|
+
"\\\\server\\share\\file.txt",
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
for (const windowsPath of windowsPaths) {
|
|
234
|
+
expect(() =>
|
|
235
|
+
PathUtils.ensurePathWithin("file.txt", windowsPath)
|
|
236
|
+
).not.toThrow();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("应该处理 Unix 风格的路径", () => {
|
|
241
|
+
const unixPaths = [
|
|
242
|
+
"/home/user/file.txt",
|
|
243
|
+
"/var/log/app.log",
|
|
244
|
+
"/tmp/temp-file.txt",
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
for (const unixPath of unixPaths) {
|
|
248
|
+
expect(() =>
|
|
249
|
+
PathUtils.ensurePathWithin("file.txt", unixPath)
|
|
250
|
+
).not.toThrow();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("应该正确处理路径规范化", () => {
|
|
255
|
+
const pathsToNormalize = [
|
|
256
|
+
{ path: "dir/./file.txt", shouldPass: true }, // 规范化后: dir/file.txt
|
|
257
|
+
{ path: "dir//file.txt", shouldPass: true }, // 规范化后: dir/file.txt
|
|
258
|
+
{ path: "dir/subdir/../file.txt", shouldPass: true }, // 规范化后: dir/file.txt (不包含 "..")
|
|
259
|
+
{ path: "../../../etc/passwd", shouldPass: false }, // 规范化后: ../../../etc/passwd (包含 "..")
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
for (const { path: pathToNormalize, shouldPass } of pathsToNormalize) {
|
|
263
|
+
const result = PathUtils.validatePath(pathToNormalize);
|
|
264
|
+
expect(result).toBe(shouldPass);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("错误处理", () => {
|
|
270
|
+
it("应该在 fileURLToPath 失败时处理错误", () => {
|
|
271
|
+
mockFileURLToPath.mockImplementation(() => {
|
|
272
|
+
throw new Error("Invalid URL");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(() => PathUtils.getScriptDir()).toThrow("Invalid URL");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("应该处理无效的环境变量值", () => {
|
|
279
|
+
process.env.XIAOZHI_CONFIG_DIR = null as any;
|
|
280
|
+
|
|
281
|
+
const result = PathUtils.getConfigDir();
|
|
282
|
+
|
|
283
|
+
expect(result).toBe(process.cwd());
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("应该处理 tmpdir 函数失败", () => {
|
|
287
|
+
mockTmpdir.mockImplementation(() => {
|
|
288
|
+
throw new Error("Cannot access temp directory");
|
|
289
|
+
});
|
|
290
|
+
process.env.TMPDIR = undefined;
|
|
291
|
+
process.env.TEMP = undefined;
|
|
292
|
+
|
|
293
|
+
expect(() => PathUtils.getTempDir()).toThrow(
|
|
294
|
+
"Cannot access temp directory"
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("性能和内存测试", () => {
|
|
300
|
+
it("应该高效处理大量路径操作", () => {
|
|
301
|
+
const startTime = Date.now();
|
|
302
|
+
|
|
303
|
+
for (let i = 0; i < 1000; i++) {
|
|
304
|
+
PathUtils.validatePath(`path/to/file${i}.txt`);
|
|
305
|
+
PathUtils.createSafePath("dir", `file${i}.txt`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const endTime = Date.now();
|
|
309
|
+
expect(endTime - startTime).toBeLessThan(100); // 应该在 100ms 内完成
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("应该正确处理重复的路径操作", () => {
|
|
313
|
+
const samePath = "test/path/file.txt";
|
|
314
|
+
|
|
315
|
+
for (let i = 0; i < 100; i++) {
|
|
316
|
+
const result1 = PathUtils.validatePath(samePath);
|
|
317
|
+
const result2 = PathUtils.createSafePath("test", "path", "file.txt");
|
|
318
|
+
|
|
319
|
+
expect(result1).toBe(true);
|
|
320
|
+
expect(result2).toContain("file.txt");
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("集成测试", () => {
|
|
326
|
+
it("应该在完整的工作流程中正确工作", () => {
|
|
327
|
+
// 设置环境
|
|
328
|
+
process.env.XIAOZHI_CONFIG_DIR = "/test/config";
|
|
329
|
+
|
|
330
|
+
// 测试完整的路径解析流程
|
|
331
|
+
const configDir = PathUtils.getConfigDir();
|
|
332
|
+
const workDir = PathUtils.getWorkDir();
|
|
333
|
+
|
|
334
|
+
expect(configDir).toBe("/test/config");
|
|
335
|
+
expect(workDir).toBe(path.join("/test/config", ".xiaozhi"));
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("应该在没有配置的情况下使用合理的默认值", () => {
|
|
339
|
+
// 清除所有环境变量
|
|
340
|
+
process.env.XIAOZHI_CONFIG_DIR = undefined;
|
|
341
|
+
process.env.TMPDIR = undefined;
|
|
342
|
+
process.env.TEMP = undefined;
|
|
343
|
+
process.env.HOME = undefined;
|
|
344
|
+
process.env.USERPROFILE = undefined;
|
|
345
|
+
|
|
346
|
+
mockTmpdir.mockReturnValue("/system/tmp");
|
|
347
|
+
|
|
348
|
+
const configDir = PathUtils.getConfigDir();
|
|
349
|
+
const tempDir = PathUtils.getTempDir();
|
|
350
|
+
const homeDir = PathUtils.getHomeDir();
|
|
351
|
+
|
|
352
|
+
expect(configDir).toBe(process.cwd());
|
|
353
|
+
expect(tempDir).toBe("/system/tmp");
|
|
354
|
+
expect(homeDir).toBe("");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|