@gesslar/toolkit 3.9.0 → 3.12.3
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 +3 -3
- package/src/browser/lib/Data.js +89 -5
- package/src/browser/lib/TypeSpec.js +3 -1
- package/src/lib/CappedDirectoryObject.js +71 -470
- package/src/lib/DirectoryObject.js +94 -146
- package/src/lib/FS.js +221 -70
- package/src/lib/FileObject.js +78 -81
- package/src/lib/TempDirectoryObject.js +92 -141
- package/src/lib/Valid.js +1 -1
- package/src/types/browser/lib/Data.d.ts +46 -2
- package/src/types/browser/lib/Data.d.ts.map +1 -1
- package/src/types/browser/lib/TypeSpec.d.ts.map +1 -1
- package/src/types/lib/CappedDirectoryObject.d.ts +30 -55
- package/src/types/lib/CappedDirectoryObject.d.ts.map +1 -1
- package/src/types/lib/DirectoryObject.d.ts +13 -54
- package/src/types/lib/DirectoryObject.d.ts.map +1 -1
- package/src/types/lib/FS.d.ts +115 -15
- package/src/types/lib/FS.d.ts.map +1 -1
- package/src/types/lib/FileObject.d.ts +6 -3
- package/src/types/lib/FileObject.d.ts.map +1 -1
- package/src/types/lib/TempDirectoryObject.d.ts +15 -62
- package/src/types/lib/TempDirectoryObject.d.ts.map +1 -1
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import path from "node:path"
|
|
12
12
|
|
|
13
|
-
import {Data, Valid} from "../browser/index.js"
|
|
14
13
|
import DirectoryObject from "./DirectoryObject.js"
|
|
15
14
|
import FileObject from "./FileObject.js"
|
|
16
15
|
import FS from "./FS.js"
|
|
17
|
-
import
|
|
16
|
+
import Valid from "./Valid.js"
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* CappedDirectoryObject extends DirectoryObject with constraints that ensure
|
|
@@ -26,7 +25,10 @@ import Sass from "./Sass.js"
|
|
|
26
25
|
* @augments DirectoryObject
|
|
27
26
|
*/
|
|
28
27
|
export default class CappedDirectoryObject extends DirectoryObject {
|
|
28
|
+
#real
|
|
29
29
|
#cap
|
|
30
|
+
#cappedParentPath
|
|
31
|
+
#cappedParent
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Constructs a CappedDirectoryObject instance.
|
|
@@ -35,9 +37,8 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
35
37
|
* (virtual root). With a parent, the path is resolved relative to the parent's
|
|
36
38
|
* cap using virtual path semantics (absolute paths treated as cap-relative).
|
|
37
39
|
*
|
|
38
|
-
* @param {string} [
|
|
40
|
+
* @param {string} [directory="."] - Directory path (becomes cap if no parent, else relative to parent's cap, defaults to current directory)
|
|
39
41
|
* @param {CappedDirectoryObject?} [parent] - Optional parent capped directory
|
|
40
|
-
* @param {boolean} [temporary=false] - Whether this is a temporary directory
|
|
41
42
|
* @throws {Sass} If parent is provided but not a CappedDirectoryObject
|
|
42
43
|
* @throws {Sass} If the resulting path would escape the cap
|
|
43
44
|
* @example
|
|
@@ -60,60 +61,35 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
60
61
|
* const config = new CappedDirectoryObject("/etc/config", cache)
|
|
61
62
|
* // path: /home/user/.cache/etc/config, cap: /home/user/.cache
|
|
62
63
|
*/
|
|
63
|
-
constructor(
|
|
64
|
-
Valid.type(
|
|
64
|
+
constructor(directory, source=null) {
|
|
65
|
+
Valid.type(source, "Null|CappedDirectoryObject")
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
if(parent !== null && !Data.isType(parent, "CappedDirectoryObject")) {
|
|
68
|
-
throw Sass.new(`Parent must be null or a CappedDirectoryObject instance, got ${Data.typeOf(parent)}`)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let cap
|
|
72
|
-
let resolvedPath
|
|
67
|
+
directory ||= "."
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
cap = path.resolve(dirPath)
|
|
77
|
-
resolvedPath = cap
|
|
78
|
-
} else {
|
|
79
|
-
// With parent: inherit cap and resolve dirPath relative to it
|
|
80
|
-
cap = parent.#cap
|
|
69
|
+
const baseLocalPath = source?.path ?? "/"
|
|
70
|
+
const baseRealPath = source?.real.path ?? directory
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const capResolved = path.resolve(cap)
|
|
72
|
+
if(source && directory.startsWith("/"))
|
|
73
|
+
directory = directory.slice(1)
|
|
85
74
|
|
|
86
|
-
|
|
75
|
+
// Find out what directory means to the basePath
|
|
76
|
+
const realResolved = FS.resolvePath(baseRealPath, directory)
|
|
77
|
+
const localResolved = source
|
|
78
|
+
? FS.resolvePath(baseLocalPath, directory)
|
|
79
|
+
: path.parse(path.resolve("")).root
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
if(path.isAbsolute(dirPath)) {
|
|
90
|
-
const relative = dirPath.replace(/^[/\\]+/, "")
|
|
91
|
-
targetPath = relative ? path.join(capResolved, relative) : capResolved
|
|
92
|
-
} else {
|
|
93
|
-
// Relative path - resolve from parent directory
|
|
94
|
-
targetPath = FS.resolvePath(parentPath, dirPath)
|
|
95
|
-
}
|
|
81
|
+
super(localResolved)
|
|
96
82
|
|
|
97
|
-
|
|
98
|
-
|
|
83
|
+
this.#real = new DirectoryObject(realResolved)
|
|
84
|
+
this.#cap = source?.cap ?? this
|
|
99
85
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
86
|
+
if(source) {
|
|
87
|
+
this.#cappedParent = source
|
|
88
|
+
this.#cappedParentPath = source.path
|
|
89
|
+
} else {
|
|
90
|
+
this.#cappedParent = null
|
|
91
|
+
this.#cappedParentPath = null
|
|
107
92
|
}
|
|
108
|
-
|
|
109
|
-
// Call parent constructor with the path
|
|
110
|
-
super(resolvedPath, temporary)
|
|
111
|
-
|
|
112
|
-
// Store the cap AFTER calling super()
|
|
113
|
-
this.#cap = cap
|
|
114
|
-
|
|
115
|
-
// Validate that this path is within the cap
|
|
116
|
-
this.#validateCapPath()
|
|
117
93
|
}
|
|
118
94
|
|
|
119
95
|
/**
|
|
@@ -133,95 +109,36 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
133
109
|
}
|
|
134
110
|
|
|
135
111
|
/**
|
|
136
|
-
*
|
|
112
|
+
* Indicates whether this directory is capped (constrained to a specific tree).
|
|
113
|
+
* Always returns true for CappedDirectoryObject instances.
|
|
137
114
|
*
|
|
138
|
-
* @
|
|
139
|
-
* @
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const cap = this.#cap
|
|
143
|
-
const resolved = path.resolve(this.#realPath)
|
|
144
|
-
const capResolved = path.resolve(cap)
|
|
145
|
-
|
|
146
|
-
// Check if the resolved path starts with the cap directory
|
|
147
|
-
if(!resolved.startsWith(capResolved)) {
|
|
148
|
-
throw Sass.new(
|
|
149
|
-
`Path '${this.#realPath}' is not within the cap directory '${cap}'`
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Re-caps this directory to itself, making it the new root of the capped tree.
|
|
156
|
-
* This is a protected method intended for use by subclasses like TempDirectoryObject.
|
|
157
|
-
*
|
|
158
|
-
* @protected
|
|
159
|
-
*/
|
|
160
|
-
_recapToSelf() {
|
|
161
|
-
this.#cap = this.#realPath
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Returns the cap path for this directory.
|
|
166
|
-
*
|
|
167
|
-
* @returns {string} The cap directory path
|
|
168
|
-
*/
|
|
169
|
-
get cap() {
|
|
170
|
-
return this.#cap
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Returns whether this directory is capped.
|
|
115
|
+
* @returns {boolean} True for all CappedDirectoryObject instances
|
|
116
|
+
* @example
|
|
117
|
+
* const capped = new TempDirectoryObject("myapp")
|
|
118
|
+
* console.log(capped.isCapped) // true
|
|
175
119
|
*
|
|
176
|
-
*
|
|
120
|
+
* const regular = new DirectoryObject("/tmp")
|
|
121
|
+
* console.log(regular.isCapped) // false
|
|
177
122
|
*/
|
|
178
|
-
get
|
|
123
|
+
get isCapped() {
|
|
179
124
|
return true
|
|
180
125
|
}
|
|
181
126
|
|
|
182
127
|
/**
|
|
183
|
-
* Returns the
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* @returns {string} The actual filesystem path
|
|
187
|
-
*/
|
|
188
|
-
get realPath() {
|
|
189
|
-
return super.path
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Private alias for realPath (for use in private methods).
|
|
194
|
-
*
|
|
195
|
-
* @private
|
|
196
|
-
* @returns {string} The actual filesystem path
|
|
197
|
-
*/
|
|
198
|
-
get #realPath() {
|
|
199
|
-
return this.realPath
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Returns the virtual path relative to the cap.
|
|
204
|
-
* This is the default path representation in the capped environment.
|
|
205
|
-
* Use `.real.path` to access the actual filesystem path.
|
|
128
|
+
* Returns the cap (root) of the capped directory tree.
|
|
129
|
+
* For root CappedDirectoryObject instances, returns itself.
|
|
130
|
+
* For children, returns the inherited cap from the parent chain.
|
|
206
131
|
*
|
|
207
|
-
* @returns {
|
|
132
|
+
* @returns {CappedDirectoryObject} The cap directory object (root of the capped tree)
|
|
208
133
|
* @example
|
|
209
134
|
* const temp = new TempDirectoryObject("myapp")
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
135
|
+
* console.log(temp.cap === temp) // true (root is its own cap)
|
|
136
|
+
*
|
|
137
|
+
* const subdir = temp.getDirectory("data")
|
|
138
|
+
* console.log(subdir.cap === temp) // true (child inherits parent's cap)
|
|
213
139
|
*/
|
|
214
|
-
get
|
|
215
|
-
|
|
216
|
-
const relative = path.relative(capResolved, this.#realPath)
|
|
217
|
-
|
|
218
|
-
// If at cap root or empty, return "/"
|
|
219
|
-
if(!relative || relative === ".") {
|
|
220
|
-
return "/"
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Return with leading slash to indicate it's cap-relative
|
|
224
|
-
return "/" + relative.split(path.sep).join("/")
|
|
140
|
+
get cap() {
|
|
141
|
+
return this.#cap
|
|
225
142
|
}
|
|
226
143
|
|
|
227
144
|
/**
|
|
@@ -243,7 +160,7 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
243
160
|
* subdir.real.parent // Can traverse outside the cap
|
|
244
161
|
*/
|
|
245
162
|
get real() {
|
|
246
|
-
return
|
|
163
|
+
return this.#real
|
|
247
164
|
}
|
|
248
165
|
|
|
249
166
|
/**
|
|
@@ -261,61 +178,31 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
261
178
|
* console.log(capped.parent) // null (at cap root)
|
|
262
179
|
*/
|
|
263
180
|
get parent() {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// If we're at the cap, return null (cap is the "root")
|
|
267
|
-
if(this.#realPath === capResolved) {
|
|
268
|
-
return null
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Compute parent's real path
|
|
272
|
-
const parentPath = path.dirname(this.#realPath)
|
|
273
|
-
const isRoot = parentPath === this.#realPath
|
|
274
|
-
|
|
275
|
-
if(isRoot) {
|
|
276
|
-
return null
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Compute relative path from current to parent (just "..")
|
|
280
|
-
// Then use getDirectory to create the parent, which preserves the class type
|
|
281
|
-
return this.getDirectory("..")
|
|
181
|
+
return this.#cappedParent
|
|
282
182
|
}
|
|
283
183
|
|
|
284
184
|
/**
|
|
285
|
-
* Returns the
|
|
185
|
+
* Returns the path of the parent directory.
|
|
186
|
+
* Returns null if this directory is at the cap root (no parent).
|
|
187
|
+
*
|
|
188
|
+
* @returns {string|null} The parent directory path, or null if at cap root
|
|
189
|
+
* @example
|
|
190
|
+
* const temp = new TempDirectoryObject("myapp")
|
|
191
|
+
* console.log(temp.parentPath) // null (at cap root)
|
|
286
192
|
*
|
|
287
|
-
*
|
|
193
|
+
* const subdir = temp.getDirectory("data")
|
|
194
|
+
* console.log(subdir.parentPath) // "/data" or similar (parent's virtual path)
|
|
288
195
|
*/
|
|
289
|
-
get
|
|
290
|
-
return
|
|
196
|
+
get parentPath() {
|
|
197
|
+
return this.#cappedParentPath
|
|
291
198
|
}
|
|
292
199
|
|
|
293
200
|
/**
|
|
294
|
-
* Returns JSON representation
|
|
201
|
+
* Returns a JSON representation of the DirectoryObject.
|
|
295
202
|
*
|
|
296
|
-
* @returns {object} JSON representation
|
|
203
|
+
* @returns {object} JSON representation of the DirectoryObject
|
|
297
204
|
*/
|
|
298
205
|
toJSON() {
|
|
299
|
-
const capResolved = path.resolve(this.#cap)
|
|
300
|
-
let parentPath
|
|
301
|
-
|
|
302
|
-
if(this.#realPath === capResolved) {
|
|
303
|
-
// At cap root, no parent
|
|
304
|
-
parentPath = null
|
|
305
|
-
} else {
|
|
306
|
-
// Compute parent's virtual path
|
|
307
|
-
const parentReal = path.dirname(this.#realPath)
|
|
308
|
-
const relative = path.relative(capResolved, parentReal)
|
|
309
|
-
|
|
310
|
-
// If parent is cap root or empty, return "/"
|
|
311
|
-
if(!relative || relative === ".") {
|
|
312
|
-
parentPath = "/"
|
|
313
|
-
} else {
|
|
314
|
-
// Return parent's virtual path with leading slash
|
|
315
|
-
parentPath = "/" + relative.split(path.sep).join("/")
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
206
|
return {
|
|
320
207
|
supplied: this.supplied,
|
|
321
208
|
path: this.path,
|
|
@@ -325,260 +212,12 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
325
212
|
extension: this.extension,
|
|
326
213
|
isFile: this.isFile,
|
|
327
214
|
isDirectory: this.isDirectory,
|
|
328
|
-
parent:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Generator that walks up the directory tree, stopping at the cap.
|
|
336
|
-
* Yields parent directories from current up to (and including) the cap root.
|
|
337
|
-
*
|
|
338
|
-
* @returns {Generator<DirectoryObject>} Generator yielding parent DirectoryObject instances
|
|
339
|
-
* @example
|
|
340
|
-
* const capped = new TempDirectoryObject("myapp")
|
|
341
|
-
* const deep = capped.getDirectory("data").getDirectory("files")
|
|
342
|
-
* for(const parent of deep.walkUp) {
|
|
343
|
-
* console.log(parent.path)
|
|
344
|
-
* // .../myapp-ABC123/data/files
|
|
345
|
-
* // .../myapp-ABC123/data
|
|
346
|
-
* // .../myapp-ABC123 (stops at cap)
|
|
347
|
-
* }
|
|
348
|
-
*/
|
|
349
|
-
*#walkUpCapped() {
|
|
350
|
-
const capResolved = path.resolve(this.#cap)
|
|
351
|
-
|
|
352
|
-
// Build trail from real path
|
|
353
|
-
const trail = this.#realPath.split(path.sep).filter(Boolean)
|
|
354
|
-
const curr = [...trail]
|
|
355
|
-
|
|
356
|
-
while(curr.length > 0) {
|
|
357
|
-
const joined = path.sep + curr.join(path.sep)
|
|
358
|
-
|
|
359
|
-
// Don't yield anything beyond the cap
|
|
360
|
-
if(!joined.startsWith(capResolved)) {
|
|
361
|
-
break
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Yield plain DirectoryObject with real path
|
|
365
|
-
yield new DirectoryObject(joined, this.temporary)
|
|
366
|
-
|
|
367
|
-
// Stop after yielding the cap
|
|
368
|
-
if(joined === capResolved) {
|
|
369
|
-
break
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
curr.pop()
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Returns a generator that walks up to the cap.
|
|
378
|
-
*
|
|
379
|
-
* @returns {Generator<DirectoryObject>} Generator yielding parent directories
|
|
380
|
-
*/
|
|
381
|
-
get walkUp() {
|
|
382
|
-
return this.#walkUpCapped()
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Creates a new CappedDirectoryObject by extending this directory's path.
|
|
387
|
-
*
|
|
388
|
-
* All paths are coerced to remain within the cap directory tree:
|
|
389
|
-
* - Absolute paths (e.g., "/foo") are treated as relative to the cap
|
|
390
|
-
* - Parent traversal ("..") is allowed but clamped at the cap boundary
|
|
391
|
-
* - The cap acts as the virtual root directory
|
|
392
|
-
*
|
|
393
|
-
* @param {string} newPath - The path to resolve (can be absolute or contain ..)
|
|
394
|
-
* @returns {CappedDirectoryObject} A new CappedDirectoryObject with the coerced path
|
|
395
|
-
* @example
|
|
396
|
-
* const capped = new TempDirectoryObject("myapp")
|
|
397
|
-
* const subDir = capped.getDirectory("data")
|
|
398
|
-
* console.log(subDir.path) // "/tmp/myapp-ABC123/data"
|
|
399
|
-
*
|
|
400
|
-
* @example
|
|
401
|
-
* // Absolute paths are relative to cap
|
|
402
|
-
* const abs = capped.getDirectory("/foo/bar")
|
|
403
|
-
* console.log(abs.path) // "/tmp/myapp-ABC123/foo/bar"
|
|
404
|
-
*
|
|
405
|
-
* @example
|
|
406
|
-
* // Excessive .. traversal clamps to cap
|
|
407
|
-
* const up = capped.getDirectory("../../../etc/passwd")
|
|
408
|
-
* console.log(up.path) // "/tmp/myapp-ABC123" (clamped to cap)
|
|
409
|
-
*/
|
|
410
|
-
getDirectory(newPath) {
|
|
411
|
-
Valid.type(newPath, "String")
|
|
412
|
-
|
|
413
|
-
// Fast path: if it's a simple name (no separators, not absolute, no ..)
|
|
414
|
-
// use the subclass constructor directly to preserve type
|
|
415
|
-
const isSimpleName = !path.isAbsolute(newPath) &&
|
|
416
|
-
!newPath.includes("/") &&
|
|
417
|
-
!newPath.includes("\\") &&
|
|
418
|
-
!newPath.includes("..")
|
|
419
|
-
|
|
420
|
-
if(isSimpleName) {
|
|
421
|
-
// Both CappedDirectoryObject and subclasses use same signature now
|
|
422
|
-
return new this.constructor(newPath, this, this.temporary)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Complex path - handle coercion
|
|
426
|
-
const capResolved = path.resolve(this.#cap)
|
|
427
|
-
let targetPath
|
|
428
|
-
|
|
429
|
-
// If absolute, treat as relative to cap (virtual root)
|
|
430
|
-
if(path.isAbsolute(newPath)) {
|
|
431
|
-
// Strip leading slashes to make relative
|
|
432
|
-
const relative = newPath.replace(/^[/\\]+/, "")
|
|
433
|
-
|
|
434
|
-
// Join with cap (unless empty, which means cap root)
|
|
435
|
-
targetPath = relative ? path.join(capResolved, relative) : capResolved
|
|
436
|
-
} else {
|
|
437
|
-
// Relative path - resolve from current directory
|
|
438
|
-
targetPath = FS.resolvePath(this.#realPath, newPath)
|
|
215
|
+
parent: this.parent,
|
|
216
|
+
parentPath: this.parentPath,
|
|
217
|
+
capped: this.isCapped,
|
|
218
|
+
cap: this.cap,
|
|
219
|
+
real: this.real
|
|
439
220
|
}
|
|
440
|
-
|
|
441
|
-
// Resolve to absolute path (handles .. and .)
|
|
442
|
-
const resolved = path.resolve(targetPath)
|
|
443
|
-
|
|
444
|
-
// Coerce: if path escaped cap, clamp to cap boundary
|
|
445
|
-
const coerced = resolved.startsWith(capResolved)
|
|
446
|
-
? resolved
|
|
447
|
-
: capResolved
|
|
448
|
-
|
|
449
|
-
// Compute path relative to cap for reconstruction
|
|
450
|
-
const relativeToCap = path.relative(capResolved, coerced)
|
|
451
|
-
|
|
452
|
-
// If we're at the cap root, return cap root directory
|
|
453
|
-
if(!relativeToCap || relativeToCap === ".") {
|
|
454
|
-
return this.#createCappedAtRoot()
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Build directory by traversing segments from cap
|
|
458
|
-
return this.#buildDirectoryFromRelativePath(relativeToCap)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Creates a CappedDirectoryObject at the cap root.
|
|
463
|
-
* Can be overridden by subclasses that have different root semantics.
|
|
464
|
-
*
|
|
465
|
-
* @private
|
|
466
|
-
* @returns {CappedDirectoryObject} Directory object at cap root
|
|
467
|
-
*/
|
|
468
|
-
#createCappedAtRoot() {
|
|
469
|
-
// Create a base CappedDirectoryObject at the cap path
|
|
470
|
-
// This works for direct usage of CappedDirectoryObject
|
|
471
|
-
// Subclasses may need to override if they have special semantics
|
|
472
|
-
return new CappedDirectoryObject(this.#cap, null, this.temporary)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Builds a directory by traversing path segments from cap.
|
|
477
|
-
*
|
|
478
|
-
* @private
|
|
479
|
-
* @param {string} relativePath - Path relative to cap
|
|
480
|
-
* @returns {CappedDirectoryObject} The directory at the final path
|
|
481
|
-
*/
|
|
482
|
-
#buildDirectoryFromRelativePath(relativePath) {
|
|
483
|
-
const segments = relativePath.split(path.sep).filter(Boolean)
|
|
484
|
-
|
|
485
|
-
// Start at cap root
|
|
486
|
-
let current = this.#createCappedAtRoot()
|
|
487
|
-
|
|
488
|
-
// Traverse each segment, using constructor to preserve class type
|
|
489
|
-
for(const segment of segments) {
|
|
490
|
-
// Use simple name constructor to preserve subclass type
|
|
491
|
-
// Works for both CappedDirectoryObject and TempDirectoryObject
|
|
492
|
-
current = new this.constructor(segment, current, this.temporary)
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return current
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Creates a new FileObject by extending this directory's path.
|
|
500
|
-
*
|
|
501
|
-
* All paths are coerced to remain within the cap directory tree:
|
|
502
|
-
* - Absolute paths (e.g., "/config.json") are treated as relative to the cap
|
|
503
|
-
* - Parent traversal ("..") is allowed but clamped at the cap boundary
|
|
504
|
-
* - The cap acts as the virtual root directory
|
|
505
|
-
*
|
|
506
|
-
* @param {string} filename - The filename to resolve (can be absolute or contain ..)
|
|
507
|
-
* @returns {FileObject} A new FileObject with the coerced path
|
|
508
|
-
* @example
|
|
509
|
-
* const capped = new TempDirectoryObject("myapp")
|
|
510
|
-
* const file = capped.getFile("config.json")
|
|
511
|
-
* console.log(file.path) // "/tmp/myapp-ABC123/config.json"
|
|
512
|
-
*
|
|
513
|
-
* @example
|
|
514
|
-
* // Absolute paths are relative to cap
|
|
515
|
-
* const abs = capped.getFile("/data/config.json")
|
|
516
|
-
* console.log(abs.path) // "/tmp/myapp-ABC123/data/config.json"
|
|
517
|
-
*
|
|
518
|
-
* @example
|
|
519
|
-
* // Excessive .. traversal clamps to cap
|
|
520
|
-
* const up = capped.getFile("../../../etc/passwd")
|
|
521
|
-
* console.log(up.path) // "/tmp/myapp-ABC123/passwd" (clamped to cap)
|
|
522
|
-
*/
|
|
523
|
-
getFile(filename) {
|
|
524
|
-
Valid.type(filename, "String")
|
|
525
|
-
|
|
526
|
-
// Fast path: if it's a simple filename (no separators, not absolute, no ..)
|
|
527
|
-
// use this as the parent directly
|
|
528
|
-
const isSimpleName = !path.isAbsolute(filename) &&
|
|
529
|
-
!filename.includes("/") &&
|
|
530
|
-
!filename.includes("\\") &&
|
|
531
|
-
!filename.includes("..")
|
|
532
|
-
|
|
533
|
-
if(isSimpleName) {
|
|
534
|
-
// Simple filename - create directly with this as parent
|
|
535
|
-
return new FileObject(filename, this)
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Complex path - handle coercion
|
|
539
|
-
const capResolved = path.resolve(this.#cap)
|
|
540
|
-
let targetPath
|
|
541
|
-
|
|
542
|
-
// If absolute, treat as relative to cap (virtual root)
|
|
543
|
-
if(path.isAbsolute(filename)) {
|
|
544
|
-
// Strip leading slashes to make relative
|
|
545
|
-
const relative = filename.replace(/^[/\\]+/, "")
|
|
546
|
-
|
|
547
|
-
// Join with cap
|
|
548
|
-
targetPath = path.join(capResolved, relative)
|
|
549
|
-
} else {
|
|
550
|
-
// Relative path - resolve from current directory
|
|
551
|
-
targetPath = FS.resolvePath(this.#realPath, filename)
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Resolve to absolute path (handles .. and .)
|
|
555
|
-
const resolved = path.resolve(targetPath)
|
|
556
|
-
|
|
557
|
-
// Coerce: if path escaped cap, clamp to cap boundary
|
|
558
|
-
const coerced = resolved.startsWith(capResolved)
|
|
559
|
-
? resolved
|
|
560
|
-
: capResolved
|
|
561
|
-
|
|
562
|
-
// Extract directory and filename parts
|
|
563
|
-
let fileDir = path.dirname(coerced)
|
|
564
|
-
let fileBasename = path.basename(coerced)
|
|
565
|
-
|
|
566
|
-
// Special case: if coerced is exactly the cap (file tried to escape),
|
|
567
|
-
// the file should be placed at the cap root with just the filename
|
|
568
|
-
if(coerced === capResolved) {
|
|
569
|
-
// Extract just the filename from the original path
|
|
570
|
-
fileBasename = path.basename(resolved)
|
|
571
|
-
fileDir = capResolved
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Get or create the parent directory
|
|
575
|
-
const relativeToCap = path.relative(capResolved, fileDir)
|
|
576
|
-
const parentDir = !relativeToCap || relativeToCap === "."
|
|
577
|
-
? this.#createCappedAtRoot()
|
|
578
|
-
: this.#buildDirectoryFromRelativePath(relativeToCap)
|
|
579
|
-
|
|
580
|
-
// Create FileObject with parent directory
|
|
581
|
-
return new FileObject(fileBasename, parentDir)
|
|
582
221
|
}
|
|
583
222
|
|
|
584
223
|
/**
|
|
@@ -596,20 +235,14 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
596
235
|
* @param {string} [pat=""] - Optional glob pattern
|
|
597
236
|
* @returns {Promise<{files: Array<FileObject>, directories: Array}>} Directory contents
|
|
598
237
|
*/
|
|
599
|
-
async read(
|
|
600
|
-
const {files, directories} = await this.real.read(
|
|
601
|
-
|
|
602
|
-
// Convert plain DirectoryObjects to CappedDirectoryObjects with same cap
|
|
603
|
-
const cappedDirectories = directories.map(dir => {
|
|
604
|
-
const name = dir.name
|
|
605
|
-
|
|
606
|
-
return new this.constructor(name, this)
|
|
607
|
-
})
|
|
238
|
+
async read(...arg) {
|
|
239
|
+
const {files, directories} = await this.real.read(...arg)
|
|
608
240
|
|
|
609
|
-
//
|
|
610
|
-
const
|
|
241
|
+
// we need to re-cast
|
|
242
|
+
const recastDirs = directories.map(e => this.getDirectory(e.name))
|
|
243
|
+
const recastFiles = files.map(f => new FileObject(f.name, this))
|
|
611
244
|
|
|
612
|
-
return {files:
|
|
245
|
+
return {files: recastFiles, directories: recastDirs}
|
|
613
246
|
}
|
|
614
247
|
|
|
615
248
|
/**
|
|
@@ -640,36 +273,4 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
640
273
|
async delete() {
|
|
641
274
|
return await this.real.delete()
|
|
642
275
|
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Override remove to preserve temporary flag check.
|
|
646
|
-
*
|
|
647
|
-
* @returns {Promise<void>}
|
|
648
|
-
*/
|
|
649
|
-
async remove() {
|
|
650
|
-
if(!this.temporary)
|
|
651
|
-
throw Sass.new("This is not a temporary directory.")
|
|
652
|
-
|
|
653
|
-
const {files, directories} = await this.read()
|
|
654
|
-
|
|
655
|
-
// Remove subdirectories recursively
|
|
656
|
-
for(const dir of directories)
|
|
657
|
-
await dir.remove()
|
|
658
|
-
|
|
659
|
-
// Remove files
|
|
660
|
-
for(const file of files)
|
|
661
|
-
await file.delete()
|
|
662
|
-
|
|
663
|
-
// Delete the now-empty directory
|
|
664
|
-
await this.delete()
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Returns a string representation of the CappedDirectoryObject.
|
|
669
|
-
*
|
|
670
|
-
* @returns {string} string representation of the CappedDirectoryObject
|
|
671
|
-
*/
|
|
672
|
-
toString() {
|
|
673
|
-
return `[CappedDirectoryObject: ${this.path} (real: ${this.#realPath})]`
|
|
674
|
-
}
|
|
675
276
|
}
|