@remcp/runtime 0.2.153 → 0.2.155
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/CHANGELOG.md +5 -0
- package/package.json +1 -1
- package/src/extended/browser-actions.mjs +311 -0
- package/src/extended/browser-endpoint.mjs +469 -0
- package/src/extended/browser-read.mjs +288 -0
- package/src/extended/browser-session.mjs +413 -0
- package/src/extended/browser.mjs +4 -1440
- package/src/extended/desktop-computer-actions.mjs +346 -0
- package/src/extended/desktop-computer-snapshot.mjs +245 -0
- package/src/extended/desktop-core.mjs +449 -0
- package/src/extended/desktop-linux-displays.mjs +366 -0
- package/src/extended/desktop-linux-input.mjs +485 -0
- package/src/extended/desktop-linux-screen.mjs +105 -0
- package/src/extended/desktop-linux-ui.mjs +407 -0
- package/src/extended/desktop-linux-window-actions.mjs +237 -0
- package/src/extended/desktop-linux-windows.mjs +321 -0
- package/src/extended/desktop-linux.mjs +6 -1791
- package/src/extended/desktop-ui-actions.mjs +402 -0
- package/src/extended/desktop-vision.mjs +209 -0
- package/src/extended/desktop.mjs +6 -1583
- package/src/tools/files-archive-actions.mjs +166 -0
- package/src/tools/files-archive-safety.mjs +395 -0
- package/src/tools/files-bulk-operations.mjs +415 -0
- package/src/tools/files-read-traversal.mjs +347 -0
- package/src/tools/files-screenshots.mjs +167 -0
- package/src/tools/files-secure-paths.mjs +408 -0
- package/src/tools/files-write-edit.mjs +336 -0
- package/src/tools/files.mjs +8 -2111
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { constants, createReadStream } from 'node:fs';
|
|
6
|
+
import { access, chmod, chown, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, stat, unlink } from 'node:fs/promises';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import { liveConfig, runtimeConfig } from '../config.mjs';
|
|
9
|
+
import { documentKind, readDocxText, readPdfText } from '../documents.mjs';
|
|
10
|
+
import { diffStats, unifiedDiff } from '../diff.mjs';
|
|
11
|
+
import { describeFilesystemFailure } from '../permissions.mjs';
|
|
12
|
+
import { capturePortalScreenshot, isWaylandSession } from '../screenshot-portal.mjs';
|
|
13
|
+
import { applyHunks, parseUnifiedDiff } from '../patch.mjs';
|
|
14
|
+
import { countEvent, recordEvent } from '../telemetry.mjs';
|
|
15
|
+
import { clampInteger, decodeText, displayPath, fail, fitsOutput, globToRegExp, image, isInsideRoot, looksBinary, multi, pageLines, resolveSafePath, splitLines, text, throwIfCancelled } from '../util.mjs';
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export const MAX_INLINE_FILE_BYTES = 20 * 1024 * 1024;
|
|
20
|
+
// An image travels base64-encoded, which costs a third more bytes. The agent's stdio transport holds
|
|
21
|
+
// 24 MiB and the relay's WebSocket frames hold 32 MiB, so 8 MiB of image is ~11 MiB on the wire with
|
|
22
|
+
// room to spare; anything larger goes through read_binary in chunks instead. Binary chunks are 4 MiB
|
|
23
|
+
// for the same reason: four times fewer round trips for the same file.
|
|
24
|
+
export const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
25
|
+
export const MAX_BINARY_CHUNK_BYTES = 1024 * 1024;
|
|
26
|
+
export const IMAGE_TYPES = new Map([
|
|
27
|
+
['.png', 'image/png'], ['.jpg', 'image/jpeg'], ['.jpeg', 'image/jpeg'], ['.gif', 'image/gif'],
|
|
28
|
+
['.webp', 'image/webp'], ['.bmp', 'image/bmp'], ['.svg', 'image/svg+xml'], ['.avif', 'image/avif'],
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export function detectEol(content) {
|
|
32
|
+
const crlf = (content.match(/\r\n/g) || []).length;
|
|
33
|
+
const lf = (content.match(/(?<!\r)\n/g) || []).length;
|
|
34
|
+
return crlf > lf ? '\r\n' : '\n';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Only regular files can be read: a FIFO blocks until a writer appears, a device node can be
|
|
38
|
+
// endless, and both would hang or flood a tool call instead of returning an answer.
|
|
39
|
+
export function assertRegularFile(info, absolute) {
|
|
40
|
+
if (info.isDirectory()) fail(`${displayPath(absolute)} is a directory, not a file`);
|
|
41
|
+
if (!info.isFile()) fail(`${displayPath(absolute)} is not a regular file`);
|
|
42
|
+
return info;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const NOFOLLOW = constants.O_NOFOLLOW || 0;
|
|
46
|
+
export const NONBLOCK = constants.O_NONBLOCK || 0;
|
|
47
|
+
export const READ_NOFOLLOW = constants.O_RDONLY | NOFOLLOW;
|
|
48
|
+
export const READ_NOFOLLOW_NONBLOCK = READ_NOFOLLOW | NONBLOCK;
|
|
49
|
+
export const READWRITE_NOFOLLOW = constants.O_RDWR | NOFOLLOW;
|
|
50
|
+
export const WRITE_CREATE_NOFOLLOW = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW | NONBLOCK;
|
|
51
|
+
export const WRITE_TRUNCATE_NOFOLLOW = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | NOFOLLOW | NONBLOCK;
|
|
52
|
+
export const DIRECTORY_READ_NOFOLLOW = constants.O_RDONLY | (constants.O_DIRECTORY || 0) | NOFOLLOW | NONBLOCK;
|
|
53
|
+
|
|
54
|
+
export async function openDirectoryFromFilesystemRoot(absolute) {
|
|
55
|
+
const root = path.parse(absolute).root;
|
|
56
|
+
const parts = absolute.slice(root.length).split(path.sep).filter(Boolean);
|
|
57
|
+
const directories = [];
|
|
58
|
+
let current = root;
|
|
59
|
+
const closeDirectories = async () => {
|
|
60
|
+
for (const handle of directories.reverse()) await handle.close().catch(() => {});
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
const rootHandle = await open(root, DIRECTORY_READ_NOFOLLOW);
|
|
64
|
+
directories.push(rootHandle);
|
|
65
|
+
current = `/proc/self/fd/${rootHandle.fd}`;
|
|
66
|
+
for (const part of parts) {
|
|
67
|
+
const candidate = path.join(current, part);
|
|
68
|
+
const next = await open(candidate, DIRECTORY_READ_NOFOLLOW);
|
|
69
|
+
try {
|
|
70
|
+
const info = await next.stat();
|
|
71
|
+
if (!info.isDirectory()) fail('Filesystem path component is not a directory');
|
|
72
|
+
directories.push(next);
|
|
73
|
+
current = `/proc/self/fd/${next.fd}`;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await next.close().catch(() => {});
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const handle = directories.pop();
|
|
80
|
+
return {
|
|
81
|
+
handle,
|
|
82
|
+
anchor: current,
|
|
83
|
+
close: async () => {
|
|
84
|
+
try { await handle.close(); }
|
|
85
|
+
finally { await closeDirectories(); }
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
await closeDirectories();
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const secureRootPromises = new Map();
|
|
95
|
+
|
|
96
|
+
export async function secureAllowedRoot(root) {
|
|
97
|
+
if (!secureRootPromises.has(root)) {
|
|
98
|
+
secureRootPromises.set(root, openDirectoryFromFilesystemRoot(root));
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return await secureRootPromises.get(root);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
secureRootPromises.delete(root);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function cloneDirectory(directory) {
|
|
109
|
+
const handle = await open(`${directory.anchor}/.`, DIRECTORY_READ_NOFOLLOW);
|
|
110
|
+
return {
|
|
111
|
+
handle,
|
|
112
|
+
anchor: `/proc/self/fd/${handle.fd}`,
|
|
113
|
+
close: () => handle.close(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function openConfinedDirectoryPath(absolute, create) {
|
|
118
|
+
if (!runtimeConfig.allowedRoots.length || absolute.startsWith('/proc/')) return null;
|
|
119
|
+
const root = runtimeConfig.allowedRoots.find(candidate => isInsideRoot(absolute, candidate));
|
|
120
|
+
if (!root) return null;
|
|
121
|
+
const relative = path.relative(root, absolute);
|
|
122
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return null;
|
|
123
|
+
const rootDirectory = await secureAllowedRoot(root);
|
|
124
|
+
const directories = [];
|
|
125
|
+
let current = rootDirectory.anchor;
|
|
126
|
+
const closeDirectories = async () => {
|
|
127
|
+
for (const handle of directories.reverse()) await handle.close().catch(() => {});
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
for (const part of relative.split(path.sep).filter(Boolean)) {
|
|
131
|
+
const candidate = path.join(current, part);
|
|
132
|
+
if (create) {
|
|
133
|
+
await mkdir(candidate, { mode:0o700 }).catch(error => {
|
|
134
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const handle = await open(candidate, DIRECTORY_READ_NOFOLLOW);
|
|
138
|
+
const info = await handle.stat();
|
|
139
|
+
if (!info.isDirectory()) {
|
|
140
|
+
await handle.close().catch(() => {});
|
|
141
|
+
fail('Filesystem path component is not a directory');
|
|
142
|
+
}
|
|
143
|
+
directories.push(handle);
|
|
144
|
+
current = `/proc/self/fd/${handle.fd}`;
|
|
145
|
+
}
|
|
146
|
+
if (!directories.length) return cloneDirectory(rootDirectory);
|
|
147
|
+
const handle = directories.pop();
|
|
148
|
+
return {
|
|
149
|
+
handle,
|
|
150
|
+
anchor: current,
|
|
151
|
+
close: async () => {
|
|
152
|
+
try { await handle.close(); }
|
|
153
|
+
finally { await closeDirectories(); }
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await closeDirectories();
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function openDescriptorDirectoryPath(absolute, create) {
|
|
163
|
+
const match = absolute.match(/^(\/proc\/(?:self|\d+)\/fd\/\d+)(?:\/(.*))?$/);
|
|
164
|
+
if (!match) return null;
|
|
165
|
+
// The proc-fd entry itself is a symlink. Make it an intermediate component so
|
|
166
|
+
// O_NOFOLLOW protects the referenced directory entry rather than rejecting procfs.
|
|
167
|
+
const base = await open(`${match[1]}/.`, DIRECTORY_READ_NOFOLLOW);
|
|
168
|
+
const directories = [base];
|
|
169
|
+
let current = `/proc/self/fd/${base.fd}`;
|
|
170
|
+
const closeDirectories = async () => {
|
|
171
|
+
for (const handle of directories.reverse()) await handle.close().catch(() => {});
|
|
172
|
+
};
|
|
173
|
+
try {
|
|
174
|
+
for (const part of (match[2] || '').split(path.sep).filter(Boolean)) {
|
|
175
|
+
const candidate = path.join(current, part);
|
|
176
|
+
if (create) {
|
|
177
|
+
await mkdir(candidate, { mode:0o700 }).catch(error => {
|
|
178
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const handle = await open(candidate, DIRECTORY_READ_NOFOLLOW);
|
|
182
|
+
const info = await handle.stat();
|
|
183
|
+
if (!info.isDirectory()) {
|
|
184
|
+
await handle.close().catch(() => {});
|
|
185
|
+
fail('Filesystem path component is not a directory');
|
|
186
|
+
}
|
|
187
|
+
directories.push(handle);
|
|
188
|
+
current = `/proc/self/fd/${handle.fd}`;
|
|
189
|
+
}
|
|
190
|
+
const handle = directories.pop();
|
|
191
|
+
return {
|
|
192
|
+
handle,
|
|
193
|
+
anchor: current,
|
|
194
|
+
close: async () => {
|
|
195
|
+
try { await handle.close(); }
|
|
196
|
+
finally { await closeDirectories(); }
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
await closeDirectories();
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function openDirectoryPath(absolute, { create = false, allowOutside = false } = {}) {
|
|
206
|
+
const descriptorBound = process.platform === 'linux' && Boolean(constants.O_NOFOLLOW) && Boolean(constants.O_DIRECTORY);
|
|
207
|
+
if (!descriptorBound) {
|
|
208
|
+
if (create) await mkdir(absolute, { recursive:true, mode:0o700 });
|
|
209
|
+
const info = await lstat(absolute);
|
|
210
|
+
if (info.isSymbolicLink() || !info.isDirectory()) fail('Filesystem path component is not a safe directory');
|
|
211
|
+
const handle = {
|
|
212
|
+
stat: () => stat(absolute),
|
|
213
|
+
chmod: mode => chmod(absolute, mode),
|
|
214
|
+
close: async () => {},
|
|
215
|
+
};
|
|
216
|
+
return { handle, anchor:absolute, descriptorBound:false, close: handle.close };
|
|
217
|
+
}
|
|
218
|
+
const confinedPath = await openConfinedDirectoryPath(absolute, create);
|
|
219
|
+
if (confinedPath) return confinedPath;
|
|
220
|
+
const descriptorPath = await openDescriptorDirectoryPath(absolute, create);
|
|
221
|
+
if (descriptorPath) return descriptorPath;
|
|
222
|
+
if (runtimeConfig.allowedRoots.length && !allowOutside) fail('Path is outside the directories this device allows');
|
|
223
|
+
const root = path.parse(absolute).root;
|
|
224
|
+
const parts = absolute.slice(root.length).split(path.sep).filter(Boolean);
|
|
225
|
+
if (!parts.length) {
|
|
226
|
+
const handle = await open(root, DIRECTORY_READ_NOFOLLOW);
|
|
227
|
+
return { handle, anchor: `/proc/self/fd/${handle.fd}`, close: () => handle.close() };
|
|
228
|
+
}
|
|
229
|
+
const directories = [];
|
|
230
|
+
let current = root;
|
|
231
|
+
const closeDirectories = async () => {
|
|
232
|
+
for (const handle of directories.reverse()) await handle.close().catch(() => {});
|
|
233
|
+
};
|
|
234
|
+
try {
|
|
235
|
+
for (const part of parts) {
|
|
236
|
+
const candidate = path.join(current, part);
|
|
237
|
+
if (create) {
|
|
238
|
+
await mkdir(candidate, { mode:0o700 }).catch(error => {
|
|
239
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const handle = await open(candidate, DIRECTORY_READ_NOFOLLOW);
|
|
243
|
+
const info = await handle.stat();
|
|
244
|
+
if (!info.isDirectory()) {
|
|
245
|
+
await handle.close().catch(() => {});
|
|
246
|
+
fail('Filesystem path component is not a directory');
|
|
247
|
+
}
|
|
248
|
+
directories.push(handle);
|
|
249
|
+
current = `/proc/self/fd/${handle.fd}`;
|
|
250
|
+
}
|
|
251
|
+
const handle = directories.pop();
|
|
252
|
+
return {
|
|
253
|
+
handle,
|
|
254
|
+
anchor: current,
|
|
255
|
+
close: async () => {
|
|
256
|
+
try { await handle.close(); }
|
|
257
|
+
finally { await closeDirectories(); }
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
} catch (error) {
|
|
261
|
+
await closeDirectories();
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function openEntryAtPath(absolute, flags) {
|
|
267
|
+
const descriptorFile = /^\/proc\/(?:self|\d+)\/fd\/\d+$/.test(absolute);
|
|
268
|
+
if (process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY && !descriptorFile) {
|
|
269
|
+
const parent = await openDirectoryPath(path.dirname(absolute));
|
|
270
|
+
try {
|
|
271
|
+
const handle = await open(path.join(parent.anchor, path.basename(absolute)), flags);
|
|
272
|
+
return {
|
|
273
|
+
handle,
|
|
274
|
+
close: async () => {
|
|
275
|
+
try { await handle.close(); }
|
|
276
|
+
finally { await parent.close().catch(() => {}); }
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
} catch (error) {
|
|
280
|
+
await parent.close().catch(() => {});
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const handle = await open(absolute, flags);
|
|
285
|
+
return { handle, close: () => handle.close() };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// A hard link inside an allowed root is the one escape path-based confinement cannot see: the very
|
|
289
|
+
// same inode is reachable from outside the root, so a read or write here is visible there (round 2
|
|
290
|
+
// audit R2-14). The allowlist is a guardrail, not an inode boundary, so this warns instead of
|
|
291
|
+
// refusing — refusing would break legitimate layouts (backups, dotfile managers, deduplicated
|
|
292
|
+
// stores). One warning per path keeps a hot loop from flooding the log.
|
|
293
|
+
export const hardlinkWarned = new Set();
|
|
294
|
+
|
|
295
|
+
export function warnOnHardlink(label, info) {
|
|
296
|
+
if (!Number.isFinite(info?.nlink) || info.nlink <= 1 || hardlinkWarned.has(label)) return;
|
|
297
|
+
if (hardlinkWarned.size > 1000) hardlinkWarned.clear();
|
|
298
|
+
hardlinkWarned.add(label);
|
|
299
|
+
countEvent('hardlinkWarnings');
|
|
300
|
+
console.warn(`ReMCP runtime: ${label} has ${info.nlink} hard links, so the same file is reachable from outside the allowed roots`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function openRegularFileAtPath(absolute, flags, label = absolute) {
|
|
304
|
+
const opened = await openEntryAtPath(absolute, flags);
|
|
305
|
+
try {
|
|
306
|
+
const info = assertRegularFile(await opened.handle.stat(), label);
|
|
307
|
+
warnOnHardlink(label, info);
|
|
308
|
+
return { handle: opened.handle, info, close: opened.close };
|
|
309
|
+
} catch (error) {
|
|
310
|
+
await opened.close().catch(() => {});
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function openRegularFile(absolute, flags = READ_NOFOLLOW) {
|
|
316
|
+
return openRegularFileAtPath(absolute, flags, absolute);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function openWritableParent(absolute, options = {}) {
|
|
320
|
+
if (process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY) {
|
|
321
|
+
return openDirectoryPath(absolute, { create:true, ...options });
|
|
322
|
+
}
|
|
323
|
+
await mkdir(absolute, { recursive:true });
|
|
324
|
+
const info = await lstat(absolute);
|
|
325
|
+
if (info.isSymbolicLink() || !info.isDirectory()) fail('Filesystem path component is not a directory');
|
|
326
|
+
return { anchor:absolute, close:async() => {} };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function writeResolvedFile(absolute, content, { append = false } = {}) {
|
|
330
|
+
const parent = await openWritableParent(path.dirname(absolute));
|
|
331
|
+
let handle;
|
|
332
|
+
try {
|
|
333
|
+
const flags = append
|
|
334
|
+
? constants.O_WRONLY | constants.O_CREAT | (constants.O_APPEND || 0) | NOFOLLOW | NONBLOCK
|
|
335
|
+
: WRITE_TRUNCATE_NOFOLLOW;
|
|
336
|
+
handle = await open(path.join(parent.anchor, path.basename(absolute)), flags, 0o600);
|
|
337
|
+
await handle.writeFile(content);
|
|
338
|
+
} finally {
|
|
339
|
+
if (handle) await handle.close().catch(() => {});
|
|
340
|
+
await parent.close().catch(() => {});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function withWritableParent(absolute, callback) {
|
|
345
|
+
const parent = await openWritableParent(path.dirname(absolute));
|
|
346
|
+
try {
|
|
347
|
+
return await callback(path.join(parent.anchor, path.basename(absolute)));
|
|
348
|
+
} finally {
|
|
349
|
+
await parent.close().catch(() => {});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function openExistingParent(absolute) {
|
|
354
|
+
if (process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY) {
|
|
355
|
+
return openDirectoryPath(absolute);
|
|
356
|
+
}
|
|
357
|
+
const info = await lstat(absolute);
|
|
358
|
+
if (info.isSymbolicLink() || !info.isDirectory()) fail('Filesystem path component is not a directory');
|
|
359
|
+
return { anchor:absolute, close:async() => {} };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function withExistingParent(absolute, callback) {
|
|
363
|
+
const parent = await openExistingParent(path.dirname(absolute));
|
|
364
|
+
try {
|
|
365
|
+
return await callback(path.join(parent.anchor, path.basename(absolute)));
|
|
366
|
+
} finally {
|
|
367
|
+
await parent.close().catch(() => {});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export async function ensureDirectoryPath(absolute, options = {}) {
|
|
372
|
+
if (process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY) {
|
|
373
|
+
const directory = await openDirectoryPath(absolute, { create:true, ...options });
|
|
374
|
+
await directory.close();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
await mkdir(absolute, { recursive:true });
|
|
378
|
+
const info = await lstat(absolute);
|
|
379
|
+
if (info.isSymbolicLink() || !info.isDirectory()) fail('Filesystem path component is not a directory');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function readRegularBuffer(absolute, maxBytes = Infinity) {
|
|
383
|
+
const opened = await openRegularFile(absolute);
|
|
384
|
+
const { handle, info } = opened;
|
|
385
|
+
try {
|
|
386
|
+
if (info.size > maxBytes) fail(`File is too large to read inline (${info.size} bytes)`);
|
|
387
|
+
return { info, buffer: await handle.readFile() };
|
|
388
|
+
} finally {
|
|
389
|
+
await opened.close();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function overwriteOpenFile(handle, content) {
|
|
394
|
+
const buffer = Buffer.from(content, 'utf8');
|
|
395
|
+
await handle.truncate(0);
|
|
396
|
+
let offset = 0;
|
|
397
|
+
while (offset < buffer.length) {
|
|
398
|
+
const { bytesWritten } = await handle.write(buffer, offset, buffer.length - offset, offset);
|
|
399
|
+
if (!bytesWritten) throw new Error('Could not finish writing the open file');
|
|
400
|
+
offset += bytesWritten;
|
|
401
|
+
}
|
|
402
|
+
await handle.truncate(buffer.length);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Traversal helper for every multi-file tool. A symbolic link inside an allowed root can point
|
|
406
|
+
// anywhere, so links are never followed and each collected path is resolved through
|
|
407
|
+
// resolveSafePath again before a tool reads or writes it. `stat` follows links, which is exactly
|
|
408
|
+
// how a symlinked directory inside a root used to expose files outside it.
|