Skip to content
File

Blob: src/workerd/io/worker-fs.c++

63.9 KB
1#include "worker-fs.h"
2 
3#include <workerd/io/io-context.h>
4#include <workerd/io/tracer.h>
5#include <workerd/util/uuid.h>
6#include <workerd/util/weak-refs.h>
7 
8#include <algorithm>
9 
10namespace workerd {
11 
12#define DEFINE_DEFAULTS_FOR_ROOTS(name, path) \
13 const jsg::Url FsMap::kDefault##name##Path = path##_url;
14KNOWN_VFS_ROOTS(DEFINE_DEFAULTS_FOR_ROOTS)
15#undef DEFINE_DEFAULTS_FOR_ROOTS
16 
17// Helper function to get the current working directory path.
18// Returns the Cwd if available, otherwise returns an empty path (root).
19kj::Maybe<kj::PathPtr> getCurrentWorkingDirectory() {
20 KJ_IF_SOME(ioContext, IoContext::tryCurrent()) {
21 return ioContext.getTmpDirStoreScope().getCwd();
22 }
23 if (TmpDirStoreScope::hasCurrent()) {
24 return TmpDirStoreScope::current().getCwd();
25 }
26 return kj::none;
27}
28 
29// Helper function to set the current working directory path.
30// Returns false if no context is available.
31bool setCurrentWorkingDirectory(kj::Path newCwd) {
32 KJ_IF_SOME(ioContext, IoContext::tryCurrent()) {
33 ioContext.getTmpDirStoreScope().setCwd(kj::mv(newCwd));
34 return true;
35 } else if (TmpDirStoreScope::hasCurrent()) {
36 TmpDirStoreScope::current().setCwd(kj::mv(newCwd));
37 return true;
38 }
39 return false;
40}
41 
42namespace {
43// Maximum distinct symlinks that can be traversed during a single resolution.
44static constexpr size_t kMaxSymlinkDepth = 256;
45 
46// The SymbolicLinkRecursionGuardScope is used on-stack to guard against
47// circular symbolic links. As soon as a cycle is detected, it throws.
48// Since resolution is always synchronous, we can use thread-local to
49// track the current scope, allowing multiple scopes to be in the stack
50// without needed to pass the guard around or do any other bookkeeping.
51thread_local SymbolicLinkRecursionGuardScope* symbolicLinkGuard = nullptr;
52 
53// Thread-local storage to track the current temp directory storage scope
54// on the stack.
55static thread_local TmpDirStoreScope* tmpDirStorageScope = nullptr;
56 
57// The TmpDirectory is a special directory implementation that uses the
58// current TmpDirStoreScope to actually store the directory contents. The
59// current TmpDirStoreScope can either be set on the stack or via the current
60// IoContext. What this means is that every IoContext has it's own temporary
61// directory that is deleted when the IoContext is destructed. This allows
62// allows for top-level evaluations running outside of the IoContext to have
63// their own temporary directory space should we decide that temp files at
64// the global scope are useful.
65class TmpDirectory final: public Directory {
66 public:
67 kj::Maybe<kj::OneOf<FsError, Stat>> stat(jsg::Lock& js, kj::PathPtr ptr) override {
68 KJ_IF_SOME(dir, tryGetDirectory()) {
69 return kj::Maybe<kj::OneOf<FsError, Stat>>(dir->stat(js, ptr));
70 }
71 if (ptr.size() == 0) {
72 return kj::Maybe<kj::OneOf<FsError, Stat>>(Stat{
73 .type = FsType::DIRECTORY,
74 .size = 0,
75 .lastModified = kj::UNIX_EPOCH,
76 .writable = true,
77 });
78 }
79 return kj::none;
80 }
81 
82 size_t count(jsg::Lock& js, kj::Maybe<FsType> typeFilter = kj::none) override {
83 KJ_IF_SOME(dir, tryGetDirectory()) {
84 return dir->count(js, typeFilter);
85 }
86 return 0;
87 }
88 
89 Entry* begin() override {
90 KJ_IF_SOME(dir, tryGetDirectory()) {
91 return dir->begin();
92 }
93 return nullptr;
94 }
95 
96 Entry* end() override {
97 KJ_IF_SOME(dir, tryGetDirectory()) {
98 return dir->end();
99 }
100 return nullptr;
101 }
102 
103 const Entry* begin() const override {
104 KJ_IF_SOME(dir, tryGetDirectory()) {
105 return dir->begin();
106 }
107 return nullptr;
108 }
109 
110 const Entry* end() const override {
111 KJ_IF_SOME(dir, tryGetDirectory()) {
112 return dir->end();
113 }
114 return nullptr;
115 }
116 
117 kj::Maybe<FsNodeWithError> tryOpen(
118 jsg::Lock& js, kj::PathPtr path, OpenOptions options = {}) override {
119 KJ_IF_SOME(dir, tryGetDirectory()) {
120 return dir->tryOpen(js, path, kj::mv(options));
121 }
122 return kj::none;
123 }
124 
125 kj::Maybe<FsError> add(jsg::Lock& js, kj::StringPtr name, Item entry) override {
126 KJ_IF_SOME(dir, tryGetDirectory()) {
127 return dir->add(js, name, kj::mv(entry));
128 }
129 return FsError::NOT_PERMITTED;
130 }
131 
132 kj::OneOf<FsError, bool> remove(
133 jsg::Lock& js, kj::PathPtr path, RemoveOptions options = {}) override {
134 KJ_IF_SOME(dir, tryGetDirectory()) {
135 return dir->remove(js, path, kj::mv(options));
136 }
137 return false;
138 }
139 
140 kj::StringPtr jsgGetMemoryName() const override {
141 return "TmpDirectory"_kj;
142 }
143 
144 size_t jsgGetMemorySelfSize() const override {
145 return sizeof(TmpDirectory);
146 }
147 
148 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {
149 // The memory contents of this directory are not tracked. This is
150 // because they are entirely dependent on the current storage scope
151 // or IoContext. However, it is not likely that jsgGetMemoryInfo
152 // will be called when either of these are current.
153 }
154 
155 kj::StringPtr getUniqueId(jsg::Lock&) const override {
156 KJ_IF_SOME(id, maybeUniqueId) {
157 return id;
158 }
159 // Generating a UUID requires randomness, which requires an IoContext.
160 auto& ioContext = JSG_REQUIRE_NONNULL(
161 IoContext::tryCurrent(), Error, "Cannot generate a unique ID outside of a request");
162 maybeUniqueId = workerd::randomUUID(ioContext.getEntropySource());
163 return KJ_ASSERT_NONNULL(maybeUniqueId);
164 }
165 
166 private:
167 mutable kj::Maybe<kj::String> maybeUniqueId;
168 
169 kj::Maybe<kj::Rc<Directory>> tryGetDirectory() const {
170 KJ_IF_SOME(ioContext, IoContext::tryCurrent()) {
171 return ioContext.getTmpDirStoreScope().getDirectory();
172 }
173 if (TmpDirStoreScope::hasCurrent()) {
174 return TmpDirStoreScope::current().getDirectory();
175 }
176 return kj::none;
177 }
178};
179 
180// LazyDirectory is a directory that is lazily loaded on first access.
181// It is used, for example, by bundle-fs to load the bundle directory
182// from the worker configuration lazily when the directory is first
183// accessed in order to avoid the cost of loading the entire bundle if
184// the worker never actually accesses it.
185class LazyDirectory final: public Directory {
186 public:
187 LazyDirectory(kj::Function<kj::Rc<Directory>()> func): lazyDir(kj::mv(func)) {}
188 
189 kj::Maybe<kj::OneOf<FsError, Stat>> stat(jsg::Lock& js, kj::PathPtr ptr) override {
190 return getDirectory()->stat(js, ptr);
191 }
192 
193 size_t count(jsg::Lock& js, kj::Maybe<FsType> typeFilter = kj::none) override {
194 return getDirectory()->count(js, typeFilter);
195 }
196 
197 Entry* begin() override {
198 return getDirectory()->begin();
199 }
200 
201 Entry* end() override {
202 return getDirectory()->end();
203 }
204 
205 const Entry* begin() const override {
206 return getDirectory()->begin();
207 }
208 
209 const Entry* end() const override {
210 return getDirectory()->end();
211 }
212 
213 kj::Maybe<FsNodeWithError> tryOpen(
214 jsg::Lock& js, kj::PathPtr path, OpenOptions options = {}) override {
215 return getDirectory()->tryOpen(js, path, kj::mv(options));
216 }
217 
218 kj::Maybe<FsError> add(jsg::Lock& js, kj::StringPtr name, Item item) override {
219 return getDirectory()->add(js, name, kj::mv(item));
220 }
221 
222 kj::OneOf<FsError, bool> remove(
223 jsg::Lock& js, kj::PathPtr path, RemoveOptions options = {}) override {
224 return getDirectory()->remove(js, path, kj::mv(options));
225 }
226 
227 kj::StringPtr jsgGetMemoryName() const override {
228 return "LazyDirectory"_kj;
229 }
230 
231 size_t jsgGetMemorySelfSize() const override {
232 return sizeof(LazyDirectory);
233 }
234 
235 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {
236 // We will only track the contents of this directory if it has
237 // be lazily loaded.
238 KJ_SWITCH_ONEOF(lazyDir) {
239 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
240 dir->jsgGetMemoryInfo(tracker);
241 return;
242 }
243 KJ_CASE_ONEOF(func, kj::Function<kj::Rc<Directory>()>) {
244 // We don't know the contents of this directory If it has not, then the contents are yet.
245 // Do not track.
246 return;
247 }
248 }
249 }
250 
251 kj::StringPtr getUniqueId(jsg::Lock& js) const override {
252 return getDirectory()->getUniqueId(js);
253 }
254 
255 private:
256 mutable kj::OneOf<kj::Rc<Directory>, kj::Function<kj::Rc<Directory>()>> lazyDir;
257 
258 kj::Rc<Directory> getDirectory() const {
259 KJ_SWITCH_ONEOF(lazyDir) {
260 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
261 return dir.addRef();
262 }
263 KJ_CASE_ONEOF(func, kj::Function<kj::Rc<Directory>()>) {
264 auto dir = func();
265 auto ret = dir.addRef();
266 lazyDir = kj::mv(dir);
267 return kj::mv(ret);
268 }
269 }
270 KJ_UNREACHABLE;
271 }
272};
273 
274// Validates that the given path does not contain any path separators and
275// can be parsed as a single path element. Throws if the checks fail.
276bool validatePathWithNoSeparators(kj::StringPtr path) {
277 try {
278 auto parsed = kj::Path::parse(path);
279 return parsed.size() == 1;
280 } catch (kj::Exception& e) {
281 return false;
282 }
283}
284 
285// The primary implementation of the Directory interface.
286template <bool Writable = false>
287class DirectoryBase final: public Directory {
288 public:
289 DirectoryBase() = default;
290 
291 DirectoryBase(kj::HashMap<kj::String, Item> entries): entries(kj::mv(entries)) {
292 // When this constructor is used, we assume that the directory is read-only.
293 KJ_DASSERT(!Writable);
294 }
295 
296 kj::Maybe<kj::OneOf<FsError, Stat>> stat(jsg::Lock& js, kj::PathPtr ptr) override {
297 // When the path ptr size is 0, then we're looking for the stat of this directory.
298 if (ptr.size() == 0) {
299 return kj::Maybe<kj::OneOf<FsError, Stat>>(Stat{
300 .type = FsType::DIRECTORY,
301 .size = 0,
302 .lastModified = kj::UNIX_EPOCH,
303 .writable = Writable,
304 });
305 }
306 
307 // Otherwise, we need to look up the entry...
308 KJ_IF_SOME(found, entries.find(ptr[0])) {
309 KJ_SWITCH_ONEOF(found) {
310 KJ_CASE_ONEOF(file, kj::Rc<File>) {
311 // We found a file. If the remaining path is empty, yay! Return the stat.
312 if (ptr.size() == 1) {
313 return kj::Maybe<kj::OneOf<FsError, Stat>>(file->stat(js));
314 }
315 // Otherwise we'll fall through to return kj::none
316 }
317 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
318 // We found a directory. We can just ask it for the stat. If the path
319 // ends up being empty, then that directory will return it's own stat.
320 return dir->stat(js, ptr.slice(1, ptr.size()));
321 }
322 KJ_CASE_ONEOF(link, kj::Rc<SymbolicLink>) {
323 // We found a symbolic link. We will resolve it and, if it resolves
324 // to something, we will ask it for the stat. Otherwise we return
325 // kj::none.
326 SymbolicLinkRecursionGuardScope guardScope;
327 KJ_IF_SOME(err, guardScope.checkSeen(link.get())) {
328 return kj::Maybe<kj::OneOf<FsError, Stat>>(err);
329 }
330 KJ_IF_SOME(resolved, link->resolve(js)) {
331 KJ_SWITCH_ONEOF(resolved) {
332 KJ_CASE_ONEOF(file, kj::Rc<File>) {
333 return kj::Maybe<kj::OneOf<FsError, Stat>>(file->stat(js));
334 }
335 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
336 return dir->stat(js, ptr.slice(1, ptr.size()));
337 }
338 KJ_CASE_ONEOF(err, FsError) {
339 return kj::Maybe<kj::OneOf<FsError, Stat>>(err);
340 }
341 }
342 KJ_UNREACHABLE;
343 }
344 }
345 }
346 }
347 return kj::none;
348 }
349 
350 size_t count(jsg::Lock& js, kj::Maybe<FsType> typeFilter = kj::none) override {
351 KJ_IF_SOME(type, typeFilter) {
352 return std::count_if(begin(), end(), [type](const auto& entry) {
353 KJ_SWITCH_ONEOF(entry.value) {
354 KJ_CASE_ONEOF(file, kj::Rc<File>) {
355 return type == FsType::FILE;
356 }
357 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
358 return type == FsType::DIRECTORY;
359 }
360 KJ_CASE_ONEOF(link, kj::Rc<SymbolicLink>) {
361 return type == FsType::SYMLINK;
362 }
363 }
364 KJ_UNREACHABLE;
365 });
366 }
367 return entries.size();
368 }
369 
370 Entry* begin() override {
371 return entries.begin();
372 }
373 
374 Entry* end() override {
375 return entries.end();
376 }
377 
378 const Entry* begin() const override {
379 return entries.begin();
380 }
381 
382 const Entry* end() const override {
383 return entries.end();
384 }
385 
386 kj::Maybe<FsNodeWithError> tryOpen(
387 jsg::Lock& js, kj::PathPtr path, OpenOptions opts = {}) override {
388 if (path.size() == 0) {
389 // An empty path ends up just returning this directory.
390 return kj::Maybe<FsNodeWithError>(kj::Rc<Directory>(addRefToThis()));
391 }
392 
393 KJ_IF_SOME(found, entries.find(path[0])) {
394 if (path.size() == 1) {
395 // We found the entry, return it.
396 KJ_SWITCH_ONEOF(found) {
397 KJ_CASE_ONEOF(file, kj::Rc<File>) {
398 return kj::Maybe<FsNodeWithError>(file.addRef());
399 }
400 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
401 return kj::Maybe<FsNodeWithError>(dir.addRef());
402 }
403 KJ_CASE_ONEOF(link, kj::Rc<SymbolicLink>) {
404 if (!opts.followLinks) {
405 // If we're not following links, when we just return the link itself
406 // here.
407 return kj::Maybe<FsNodeWithError>(link.addRef());
408 }
409 // Resolve the symbolic link and return the target, guarding against
410 // recursion while doing so.
411 SymbolicLinkRecursionGuardScope guardScope;
412 KJ_IF_SOME(err, guardScope.checkSeen(link.get())) {
413 return kj::Maybe<FsNodeWithError>(err);
414 }
415 return link->resolve(js);
416 }
417 }
418 KJ_UNREACHABLE;
419 }
420 
421 // There's more than one component in the path, we need to keep looking.
422 path = path.slice(1, path.size());
423 KJ_SWITCH_ONEOF(found) {
424 KJ_CASE_ONEOF(file, kj::Rc<File>) {
425 // We found a file, but we were looking for a directory.
426 return kj::none;
427 }
428 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
429 // We found a directory, continue searching.
430 return dir->tryOpen(js, path, kj::mv(opts));
431 }
432 KJ_CASE_ONEOF(link, kj::Rc<SymbolicLink>) {
433 // If the symbolic link resolves to a directory, then we can continue
434 // searching, otherwise we return nothing.
435 // Unless we're being asked to not follow links, then just return kj::none.
436 if (!opts.followLinks) {
437 return kj::none;
438 }
439 SymbolicLinkRecursionGuardScope guardScope;
440 KJ_IF_SOME(err, guardScope.checkSeen(link.get())) {
441 return kj::Maybe<FsNodeWithError>(err);
442 }
443 KJ_IF_SOME(resolved, link->resolve(js)) {
444 KJ_SWITCH_ONEOF(resolved) {
445 KJ_CASE_ONEOF(file, kj::Rc<File>) {
446 return kj::none;
447 }
448 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
449 return dir->tryOpen(js, path, kj::mv(opts));
450 }
451 KJ_CASE_ONEOF(err, FsError) {
452 return kj::Maybe<FsNodeWithError>(err);
453 }
454 }
455 } else {
456 // The symbolic link does not resolve to anything.
457 return kj::none;
458 }
459 }
460 }
461 }
462 
463 // If we haven't found anything, we can try to create a new file or directory
464 // if the directory is writable and the createAs parameter is set.
465 if constexpr (Writable) {
466 KJ_IF_SOME(type, opts.createAs) {
467 return tryCreate(js, path, type);
468 }
469 } else {
470 if (opts.createAs != kj::none) {
471 return kj::Maybe<FsNodeWithError>(FsError::READ_ONLY);
472 }
473 }
474 
475 return kj::none;
476 }
477 
478 kj::Maybe<FsError> add(jsg::Lock& js, kj::StringPtr name, Item fileOrDirectory) override {
479 if constexpr (Writable) {
480 if (!validatePathWithNoSeparators(name)) {
481 return FsError::INVALID_PATH;
482 }
483 if (entries.find(name) != kj::none) {
484 return FsError::ALREADY_EXISTS;
485 }
486 auto ret = ([&]() -> Item {
487 KJ_SWITCH_ONEOF(fileOrDirectory) {
488 KJ_CASE_ONEOF(file, kj::Rc<File>) {
489 return file.addRef();
490 }
491 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
492 return dir.addRef();
493 }
494 KJ_CASE_ONEOF(link, kj::Rc<SymbolicLink>) {
495 return link.addRef();
496 }
497 }
498 KJ_UNREACHABLE;
499 })();
500 entries.insert(kj::str(name), kj::mv(ret));
501 return kj::none;
502 } else {
503 return FsError::NOT_PERMITTED;
504 }
505 }
506 
507 // Tries to remove the file or directory at the given path. If the node does
508 // not exist, false will be returned. If the node is a directory and the recursive
509 // option is not set, an exception will be thrown if the directory is not empty.
510 // If the directory is read only, an exception will be thrown.
511 // If the node is a file, it will be removed regardless of the recursive option
512 // if the directory is not read only.
513 // The path must be relative to the current directory.
514 kj::OneOf<FsError, bool> remove(
515 jsg::Lock& js, kj::PathPtr path, RemoveOptions opts = {}) override {
516 if constexpr (Writable) {
517 if (path.size() == 0) return false;
518 KJ_IF_SOME(found, entries.find(path[0])) {
519 KJ_SWITCH_ONEOF(found) {
520 KJ_CASE_ONEOF(file, kj::Rc<File>) {
521 if (path.size() != 1) return FsError::NOT_DIRECTORY;
522 return entries.erase(path[0]);
523 }
524 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
525 if (path.size() == 1) {
526 if (dir->count(js) > 0 && !opts.recursive) {
527 return FsError::NOT_EMPTY;
528 }
529 return entries.erase(path[0]);
530 }
531 return dir->remove(js, path.slice(1, path.size()), kj::mv(opts));
532 }
533 KJ_CASE_ONEOF(link, kj::Rc<SymbolicLink>) {
534 // If we found a symbolic link, we can remove it if our path
535 // is exactly the symbolic link. If the path is longer, then
536 // we are trying to remove the target of the symbolic link
537 // which we do not allow.
538 if (path.size() != 1) return FsError::NOT_DIRECTORY;
539 return entries.erase(path[0]);
540 }
541 }
542 }
543 
544 return false;
545 } else {
546 return FsError::READ_ONLY;
547 }
548 }
549 
550 kj::StringPtr jsgGetMemoryName() const override {
551 return "Directory"_kj;
552 }
553 
554 size_t jsgGetMemorySelfSize() const override {
555 return sizeof(DirectoryBase);
556 }
557 
558 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {
559 for (auto& entry: entries) {
560 KJ_SWITCH_ONEOF(entry.value) {
561 KJ_CASE_ONEOF(file, kj::Rc<File>) {
562 tracker.trackField("file", *file.get());
563 break;
564 }
565 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
566 tracker.trackField("directory", *dir.get());
567 break;
568 }
569 KJ_CASE_ONEOF(link, kj::Rc<SymbolicLink>) {
570 // There's no need to track the symbolic link itself.
571 }
572 }
573 }
574 }
575 
576 kj::StringPtr getUniqueId(jsg::Lock&) const override {
577 KJ_IF_SOME(id, maybeUniqueId) {
578 return id;
579 }
580 // Generating a UUID requires randomness, which requires an IoContext.
581 auto& ioContext = JSG_REQUIRE_NONNULL(
582 IoContext::tryCurrent(), Error, "Cannot generate a unique ID outside of a request");
583 maybeUniqueId = workerd::randomUUID(ioContext.getEntropySource());
584 return KJ_ASSERT_NONNULL(maybeUniqueId);
585 }
586 
587 void countTowardsIsolateLimit(jsg::Lock& js) const override {
588 // We only count writable directories towards the isolate limit.
589 // Why? Because read-only directories are controlled by the runtime
590 // and not users and we don't want to count them against the isolate.
591 if constexpr (Writable) {
592 if (maybeMemoryAdjustment == kj::none) {
593 maybeMemoryAdjustment = js.getExternalMemoryAdjustment(sizeof(DirectoryBase));
594 }
595 }
596 }
597 
598 private:
599 kj::HashMap<kj::String, Item> entries;
600 mutable kj::Maybe<kj::String> maybeUniqueId;
601 mutable kj::Maybe<jsg::ExternalMemoryAdjustment> maybeMemoryAdjustment;
602 
603 // Called by tryOpen to create a new file or directory at the given path.
604 kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>> tryCreate(
605 jsg::Lock& js, kj::PathPtr path, FsType createAs) {
606 KJ_DASSERT(Writable);
607 KJ_DASSERT(path.size() > 0);
608 
609 // If the path size is one, then we are creating the file or directory
610 // in *this* directory.
611 if (path.size() == 1) {
612 switch (createAs) {
613 case FsType::FILE: {
614 auto file = File::newWritable(js);
615 auto ret = file.addRef();
616 entries.insert(kj::str(path[0]), kj::mv(file));
617 return kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>>(kj::mv(ret));
618 }
619 case FsType::DIRECTORY: {
620 auto dir = Directory::newWritable(js);
621 auto ret = dir.addRef();
622 entries.insert(kj::str(path[0]), kj::mv(dir));
623 return kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>>(kj::mv(ret));
624 }
625 case FsType::SYMLINK: {
626 return kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>>(
627 FsError::NOT_PERMITTED);
628 }
629 }
630 }
631 
632 // Otherwise we need to recursively create a directory and ask it to create the file.
633 auto dir = Directory::newWritable(js);
634 KJ_IF_SOME(ret,
635 dir->tryOpen(js, path.slice(1, path.size()),
636 OpenOptions{
637 .createAs = createAs,
638 })) {
639 // We will only create the new subdirectory in this directory if the
640 // child target was successfully created/opened.
641 entries.insert(kj::str(path[0]), kj::mv(dir));
642 KJ_SWITCH_ONEOF(ret) {
643 KJ_CASE_ONEOF(file, kj::Rc<File>) {
644 return kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>>(kj::mv(file));
645 }
646 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
647 return kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>>(kj::mv(dir));
648 }
649 KJ_CASE_ONEOF(link, kj::Rc<SymbolicLink>) {
650 KJ_UNREACHABLE;
651 }
652 KJ_CASE_ONEOF(err, FsError) {
653 return kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>>(err);
654 }
655 }
656 }
657 return kj::none;
658 }
659};
660 
661// The implementation of the File interface.
662class FileImpl final: public File {
663 public:
664 // Constructor used to create a read-only file.
665 FileImpl(kj::ArrayPtr<const kj::byte> data): ownedOrView(data), lastModified(kj::UNIX_EPOCH) {}
666 FileImpl(kj::Array<kj::byte>&& owned) = delete;
667 
668 // Constructor used to create a writable file.
669 FileImpl(jsg::Lock& js, kj::Array<kj::byte> owned)
670 : ownedOrView(Owned(js, kj::mv(owned))),
671 lastModified(kj::UNIX_EPOCH) {}
672 
673 kj::Maybe<FsError> setLastModified(jsg::Lock& js, kj::Date date = kj::UNIX_EPOCH) override {
674 if (isWritable()) {
675 lastModified = date;
676 }
677 return kj::none;
678 }
679 
680 Stat stat(jsg::Lock& js) override {
681 return Stat{
682 .type = FsType::FILE,
683 .size = static_cast<uint32_t>(readableView().size()),
684 .lastModified = lastModified,
685 .writable = isWritable(),
686 };
687 }
688 
689 uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
690 auto data = readableView();
691 if (offset >= data.size() || buffer.size() == 0) return 0;
692 auto src = data.slice(offset);
693 KJ_DASSERT(src.size() > 0);
694 if (buffer.size() > src.size()) {
695 buffer.first(src.size()).copyFrom(src);
696 return src.size();
697 }
698 buffer.copyFrom(src.first(buffer.size()));
699 return buffer.size();
700 }
701 
702 // Writes data to the file at the given offset.
703 kj::OneOf<FsError, uint32_t> write(
704 jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
705 auto maxSize = Worker::Isolate::from(js).getLimitEnforcer().getBlobSizeLimit();
706 if (buffer.size() > maxSize) {
707 return FsError::FILE_SIZE_LIMIT_EXCEEDED;
708 }
709 
710 size_t end = offset + buffer.size();
711 if (end > maxSize || end < offset /* overflow check */) {
712 return FsError::FILE_SIZE_LIMIT_EXCEEDED;
713 }
714 if (!isWritable()) {
715 return FsError::READ_ONLY;
716 }
717 
718 if (end > writableView().size()) {
719 KJ_IF_SOME(err, resize(js, end)) {
720 return err;
721 }
722 }
723 writableView().slice(offset, end).copyFrom(buffer);
724 return static_cast<uint32_t>(buffer.size());
725 }
726 
727 kj::Maybe<FsError> resize(jsg::Lock& js, uint32_t size) override {
728 if (!isWritable()) {
729 return FsError::READ_ONLY;
730 }
731 auto& owned = ownedOrView.get<Owned>();
732 if (size == owned.data.size()) return kj::none; // Nothing to do.
733 
734 auto maxSize = Worker::Isolate::from(js).getLimitEnforcer().getBlobSizeLimit();
735 if (size > maxSize) {
736 return FsError::FILE_SIZE_LIMIT_EXCEEDED;
737 }
738 
739 auto newData = kj::heapArray<kj::byte>(size);
740 
741 if (size > owned.data.size()) {
742 // To grow the file, we need to allocate a new array, copy the old data over,
743 // and replace the original.
744 newData.first(owned.data.size()).copyFrom(owned.data);
745 newData.slice(owned.data.size()).fill(0);
746 } else {
747 newData.asPtr().copyFrom(owned.data.first(size));
748 }
749 owned.adjustment.setNow(js, newData.size());
750 owned.data = kj::mv(newData);
751 return kj::none;
752 }
753 
754 kj::Maybe<FsError> fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
755 if (!isWritable()) {
756 return FsError::READ_ONLY;
757 }
758 auto view = writableView();
759 int actualOffset = offset.orDefault(0);
760 if (actualOffset >= view.size() || view.size() == 0) return kj::none;
761 view.slice(actualOffset).fill(value);
762 return kj::none;
763 }
764 
765 kj::StringPtr jsgGetMemoryName() const override {
766 return "File"_kj;
767 }
768 
769 size_t jsgGetMemorySelfSize() const override {
770 return sizeof(FileImpl);
771 }
772 
773 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {
774 // We only track the memory if we own the data.
775 KJ_SWITCH_ONEOF(ownedOrView) {
776 KJ_CASE_ONEOF(owned, Owned) {
777 tracker.trackField("owned", owned.data);
778 return;
779 }
780 KJ_CASE_ONEOF(view, kj::ArrayPtr<const kj::byte>) {
781 return;
782 }
783 }
784 }
785 
786 kj::OneOf<FsError, kj::Rc<File>> clone(jsg::Lock& js) override {
787 auto maxSize = Worker::Isolate::from(js).getLimitEnforcer().getBlobSizeLimit();
788 KJ_SWITCH_ONEOF(ownedOrView) {
789 KJ_CASE_ONEOF(owned, Owned) {
790 if (owned.data.size() > maxSize) [[unlikely]] {
791 return FsError::FILE_SIZE_LIMIT_EXCEEDED;
792 }
793 kj::Rc<File> file = kj::rc<FileImpl>(js, kj::heapArray<kj::byte>(owned.data));
794 return kj::mv(file);
795 }
796 KJ_CASE_ONEOF(view, kj::ArrayPtr<const kj::byte>) {
797 if (view.size() > maxSize) [[unlikely]] {
798 return FsError::FILE_SIZE_LIMIT_EXCEEDED;
799 }
800 kj::Rc<File> file = kj::rc<FileImpl>(js, kj::heapArray<kj::byte>(view));
801 return kj::mv(file);
802 }
803 }
804 KJ_UNREACHABLE;
805 }
806 
807 kj::Maybe<FsError> replace(jsg::Lock& js, kj::Rc<File> file) override {
808 if (!isWritable()) {
809 return FsError::READ_ONLY;
810 }
811 
812 auto stat = file->stat(js);
813 auto buffer = kj::heapArray<kj::byte>(stat.size);
814 file->read(js, 0, buffer.asPtr());
815 auto& owned = ownedOrView.get<Owned>();
816 owned.adjustment.setNow(js, buffer.size());
817 owned.data = kj::mv(buffer);
818 lastModified = stat.lastModified;
819 return kj::none;
820 }
821 
822 kj::StringPtr getUniqueId(jsg::Lock&) const override {
823 KJ_IF_SOME(id, maybeUniqueId) {
824 return id;
825 }
826 // Generating a UUID requires randomness, which requires an IoContext.
827 auto& ioContext = JSG_REQUIRE_NONNULL(
828 IoContext::tryCurrent(), Error, "Cannot generate a unique ID outside of a request");
829 maybeUniqueId = workerd::randomUUID(ioContext.getEntropySource());
830 return KJ_ASSERT_NONNULL(maybeUniqueId);
831 }
832 
833 void countTowardsIsolateLimit(jsg::Lock& js) const override {
834 // We only count writable files towards the isolate limit. Read-only files are
835 // controlled by the runtime. We don't want to count them against the isolate.
836 if (isWritable()) {
837 if (maybeMemoryAdjustment == kj::none) {
838 maybeMemoryAdjustment = js.getExternalMemoryAdjustment(sizeof(FileImpl));
839 }
840 }
841 }
842 
843 private:
844 struct Owned {
845 kj::Array<kj::byte> data;
846 jsg::ExternalMemoryAdjustment adjustment;
847 Owned(jsg::Lock& js, kj::Array<kj::byte>&& data)
848 : data(kj::mv(data)),
849 adjustment(js.getExternalMemoryAdjustment(this->data.size())) {}
850 };
851 kj::OneOf<Owned, kj::ArrayPtr<const kj::byte>> ownedOrView;
852 kj::Date lastModified;
853 mutable kj::Maybe<kj::String> maybeUniqueId;
854 mutable kj::Maybe<jsg::ExternalMemoryAdjustment> maybeMemoryAdjustment;
855 
856 bool isWritable() const {
857 // Our file is only writable if it owns the actual data buffer.
858 return ownedOrView.is<Owned>();
859 }
860 
861 kj::ArrayPtr<kj::byte> writableView() {
862 return KJ_REQUIRE_NONNULL(ownedOrView.tryGet<Owned>()).data.asPtr();
863 }
864 
865 jsg::ExternalMemoryAdjustment& getAdjustment() {
866 return KJ_REQUIRE_NONNULL(ownedOrView.tryGet<Owned>()).adjustment;
867 }
868 
869 kj::ArrayPtr<const kj::byte> readableView() const {
870 KJ_SWITCH_ONEOF(ownedOrView) {
871 KJ_CASE_ONEOF(view, kj::ArrayPtr<const kj::byte>) {
872 return view;
873 }
874 KJ_CASE_ONEOF(owned, Owned) {
875 return owned.data.asPtr().asConst();
876 }
877 }
878 KJ_UNREACHABLE;
879 }
880};
881 
882using ReadableDirectory = DirectoryBase<false>;
883using WritableDirectory = DirectoryBase<true>;
884 
885class VirtualFileSystemImpl;
886 
887class FdHandle final {
888 public:
889 FdHandle(kj::Rc<WeakRef<VirtualFileSystemImpl>> weakFs, int fd): weakFs(kj::mv(weakFs)), fd(fd) {}
890 
891 ~FdHandle() noexcept(false);
892 
893 private:
894 kj::Rc<WeakRef<VirtualFileSystemImpl>> weakFs;
895 int fd;
896};
897 
898class VirtualFileSystemImpl final: public VirtualFileSystem {
899 public:
900 VirtualFileSystemImpl(
901 kj::Own<FsMap> fsMap, kj::Rc<Directory>&& root, kj::Own<VirtualFileSystem::Observer> observer)
902 : fsMap(kj::mv(fsMap)),
903 root(kj::mv(root)),
904 observer(kj::mv(observer)),
905 weakThis(
906 kj::rc<WeakRef<VirtualFileSystemImpl>>(kj::Badge<VirtualFileSystemImpl>(), *this)) {}
907 
908 kj::Rc<OpenedFile> getStdio(jsg::Lock& js, Stdio stdio) const override;
909 
910 ~VirtualFileSystemImpl() noexcept(false) override {
911 weakThis->invalidate();
912 }
913 
914 kj::Rc<Directory> getRoot(jsg::Lock& js) const override {
915 return root.addRef();
916 }
917 
918 const jsg::Url& getBundleRoot() const override {
919 return fsMap->getBundleRoot();
920 }
921 
922 const jsg::Url& getTmpRoot() const override {
923 return fsMap->getTempRoot();
924 }
925 
926 const jsg::Url& getDevRoot() const override {
927 return fsMap->getDevRoot();
928 }
929 
930 kj::OneOf<FsError, kj::Rc<OpenedFile>> openFd(
931 jsg::Lock& js, const jsg::Url& url, OpenOptions opts = {}) const override {
932 
933 kj::Path root{};
934 auto str = kj::str(url.getPathname().slice(1));
935 
936 // We will impose an absolute max number of total file descriptors to
937 // max int... in practice, the production system should condemn the
938 // worker far before this limit is reached. Note that this is not *opened*
939 // file descriptors, this is total file descriptors opened. There is no
940 // way to reset this counter.
941 static constexpr int kMax = kj::maxValue;
942 if (nextFd == kMax) {
943 observer->onMaxFds(openedFiles.size());
944 return FsError::TOO_MANY_OPEN_FILES;
945 }
946 
947 auto rootDir = getRoot(js);
948 auto path = root.eval(str);
949 
950 if (opts.exclusive && opts.write) {
951 // If the exclusive flag is set with the witable flag, then we fail
952 // if the file already exists.
953 KJ_IF_SOME(maybeStat, rootDir->stat(js, path)) {
954 KJ_SWITCH_ONEOF(maybeStat) {
955 KJ_CASE_ONEOF(stat, Stat) {
956 return FsError::ALREADY_EXISTS;
957 }
958 KJ_CASE_ONEOF(err, FsError) {
959 return err;
960 }
961 }
962 KJ_UNREACHABLE;
963 }
964 }
965 
966 KJ_IF_SOME(node,
967 rootDir->tryOpen(js, path,
968 Directory::OpenOptions{
969 .createAs = FsType::FILE,
970 .followLinks = opts.followLinks,
971 })) {
972 KJ_SWITCH_ONEOF(node) {
973 KJ_CASE_ONEOF(file, kj::Rc<File>) {
974 if (opts.write) {
975 auto stat = file->stat(js);
976 if (!stat.writable) return FsError::NOT_PERMITTED;
977 }
978 KJ_DASSERT(openedFiles.find(nextFd) == kj::none);
979 KJ_DEFER(observer->onOpen(openedFiles.size(), nextFd));
980 auto fd = nextFd++;
981 auto opened = kj::rc<OpenedFile>(fd, opts.read, opts.write, opts.append, kj::mv(file));
982 openedFiles.insert(fd, opened.addRef());
983 return kj::mv(opened);
984 }
985 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
986 if (opts.write) {
987 // Similar to Node.js, we do not allow opening fd's for
988 // directories for writing.
989 return FsError::NOT_PERMITTED_ON_DIRECTORY;
990 }
991 KJ_DASSERT(openedFiles.find(nextFd) == kj::none);
992 KJ_DEFER(observer->onOpen(openedFiles.size(), nextFd));
993 auto fd = nextFd++;
994 auto opened = kj::rc<OpenedFile>(fd, opts.read, opts.write, opts.append, kj::mv(dir));
995 openedFiles.insert(fd, opened.addRef());
996 return kj::mv(opened);
997 }
998 KJ_CASE_ONEOF(link, kj::Rc<SymbolicLink>) {
999 // Symlinks are never directly writable so the write flag
1000 // makes no sense.
1001 if (opts.write) {
1002 return FsError::NOT_PERMITTED;
1003 }
1004 KJ_DASSERT(openedFiles.find(nextFd) == kj::none);
1005 KJ_DEFER(observer->onOpen(openedFiles.size(), nextFd));
1006 auto fd = nextFd++;
1007 auto opened = kj::rc<OpenedFile>(fd, opts.read, opts.write, opts.append, kj::mv(link));
1008 openedFiles.insert(fd, opened.addRef());
1009 return kj::mv(opened);
1010 }
1011 KJ_CASE_ONEOF(err, FsError) {
1012 // If we got an error, we just return it.
1013 return err;
1014 }
1015 }
1016 KJ_UNREACHABLE;
1017 }
1018 
1019 // The file does not exist, and apparently was not created. Likely the
1020 // directory is not writable or does not exist.
1021 return FsError::FAILED;
1022 }
1023 
1024 void closeFd(jsg::Lock& js, int fd) const override {
1025 // We do not allow closing the stdio file descriptors.
1026 static constexpr int kMaxFd = static_cast<int>(Stdio::ERR);
1027 if (fd <= kMaxFd) return;
1028 closeFdWithoutExplicitLock(fd);
1029 }
1030 
1031 kj::Maybe<kj::Rc<OpenedFile>> tryGetFd(jsg::Lock& js, int fd) const override {
1032 static constexpr int kMaxFd = static_cast<int>(Stdio::ERR);
1033 if (fd <= kMaxFd) {
1034 return getStdio(js, static_cast<Stdio>(fd));
1035 }
1036 KJ_IF_SOME(opened, openedFiles.find(fd)) {
1037 return opened.addRef();
1038 }
1039 return kj::none;
1040 }
1041 
1042 kj::Own<void> wrapFd(jsg::Lock& js, int fd) const override {
1043 return kj::heap<FdHandle>(weakThis.addRef(), fd);
1044 }
1045 
1046 // While held, holds a lock on the given locator url. While locked,
1047 // certain operations on the identified node are not allowed.
1048 struct Lock {
1049 // Because we can't guarantee that the the lock will certainly be destroyed
1050 // before the VFS, we hold a weak reference to the VFS. If the VFS is destroyed
1051 // first, the lock essentially becomes a no-op.
1052 kj::Rc<WeakRef<VirtualFileSystemImpl>> vfs;
1053 const jsg::Url& url;
1054 
1055 void lock(const jsg::Url& locator) const {
1056 vfs->runIfAlive([&locator](auto& vfs) {
1057 KJ_IF_SOME(locked, vfs.locks.find(locator)) {
1058 locked++;
1059 } else {
1060 vfs.locks.insert(locator.clone(), 1);
1061 }
1062 });
1063 }
1064 
1065 void unlock(const jsg::Url& locator) const {
1066 vfs->runIfAlive([&locator](auto& vfs) {
1067 KJ_IF_SOME(locked, vfs.locks.find(locator)) {
1068 if (locked == 1) {
1069 vfs.locks.erase(locator);
1070 } else {
1071 KJ_ASSERT(locked > 0);
1072 --locked;
1073 }
1074 }
1075 });
1076 }
1077 
1078 Lock(kj::Rc<WeakRef<VirtualFileSystemImpl>> vfs, const jsg::Url& url)
1079 : vfs(kj::mv(vfs)),
1080 url(url) {
1081 lock(url);
1082 
1083 // This is fun... per the webfs spec, we need to prevent directories from
1084 // being removed if any locks are held on any files descendent from it,
1085 // so when we grab a lock, we also need to lock the parent path.
1086 auto maybeParent = url.getParent();
1087 while (maybeParent != kj::none) {
1088 auto& parent = KJ_ASSERT_NONNULL(maybeParent);
1089 lock(parent);
1090 maybeParent = parent.getParent();
1091 }
1092 }
1093 ~Lock() noexcept(false) {
1094 unlock(url);
1095 auto maybeParent = url.getParent();
1096 while (maybeParent != kj::none) {
1097 auto& parent = KJ_ASSERT_NONNULL(maybeParent);
1098 unlock(parent);
1099 maybeParent = parent.getParent();
1100 }
1101 }
1102 };
1103 
1104 kj::Own<void> lock(jsg::Lock& js, const jsg::Url& locator) const override {
1105 return kj::heap<Lock>(weakThis.addRef(), locator);
1106 }
1107 bool isLocked(jsg::Lock& js, const jsg::Url& locator) const override {
1108 KJ_IF_SOME(locked, locks.find(locator)) {
1109 return locked > 0;
1110 }
1111 return false;
1112 }
1113 
1114 private:
1115 // All operations on the VFS are expected to be performed while holding the
1116 // isolate lock even tho the VFS itself does not depend directly on the
1117 // lock. This is to ensure that the VFS operations are thread-safe without
1118 // incurring additional locking overhead.
1119 kj::Own<FsMap> fsMap;
1120 mutable kj::Rc<Directory> root;
1121 kj::Own<VirtualFileSystem::Observer> observer;
1122 mutable kj::Rc<WeakRef<VirtualFileSystemImpl>> weakThis;
1123 friend class FdHandle;
1124 
1125 // The next file descriptor to be used for the next file opened.
1126 mutable int nextFd = static_cast<int>(Stdio::ERR) + 1;
1127 
1128 mutable kj::HashMap<int, kj::Rc<OpenedFile>> openedFiles;
1129 mutable kj::HashMap<jsg::Url, size_t> locks;
1130 
1131 void closeFdWithoutExplicitLock(int fd) const {
1132 openedFiles.erase(fd);
1133 observer->onClose(openedFiles.size(), nextFd);
1134 }
1135};
1136 
1137FdHandle::~FdHandle() noexcept(false) {
1138 weakFs->runIfAlive([&](VirtualFileSystemImpl& vfs) { vfs.closeFdWithoutExplicitLock(fd); });
1139}
1140 
1141} // namespace
1142 
1143kj::OneOf<FsError, jsg::JsString> File::readAllText(jsg::Lock& js) {
1144 auto info = stat(js);
1145 KJ_DASSERT(info.type == FsType::FILE);
1146 if (info.size == 0) return js.str();
1147 
1148 KJ_STACK_ARRAY(char, data, info.size, 4096, 4096);
1149 auto size = read(js, 0, data.asBytes());
1150 if (size != info.size) {
1151 return FsError::FAILED;
1152 }
1153 return js.str(data);
1154}
1155 
1156kj::OneOf<FsError, jsg::BufferSource> File::readAllBytes(jsg::Lock& js) {
1157 auto info = stat(js);
1158 KJ_DASSERT(info.type == FsType::FILE);
1159 auto backing = jsg::BackingStore::alloc<v8::Uint8Array>(js, info.size);
1160 if (info.size > 0) {
1161 KJ_ASSERT(read(js, 0, backing) == info.size);
1162 }
1163 return jsg::BufferSource(js, kj::mv(backing));
1164}
1165 
1166void Directory::Builder::add(
1167 kj::StringPtr name, kj::OneOf<kj::Rc<File>, kj::Rc<Directory>> fileOrDirectory) {
1168 KJ_REQUIRE(validatePathWithNoSeparators(name));
1169 KJ_REQUIRE(entries.find(name) == kj::none, "file or directory already exists: \"", name, "\"");
1170 entries.insert(kj::str(name), kj::mv(fileOrDirectory));
1171}
1172 
1173void Directory::Builder::add(kj::StringPtr name, kj::Own<Directory::Builder> builder) {
1174 KJ_REQUIRE(validatePathWithNoSeparators(name));
1175 KJ_REQUIRE(entries.find(name) == kj::none, "file or directory already exists: \"", name, "\"");
1176 entries.insert(kj::str(name), kj::mv(builder));
1177}
1178 
1179void Directory::Builder::addPath(
1180 kj::PathPtr path, kj::OneOf<kj::Rc<File>, kj::Rc<Directory>> fileOrDirectory) {
1181 KJ_ASSERT(path.size() > 0);
1182 
1183 if (path.size() == 1) {
1184 return add(path[0], kj::mv(fileOrDirectory));
1185 }
1186 
1187 // We have multiple path segments. We need to either find or create the
1188 // directory at the first segment and then add the rest of the path to
1189 // it.
1190 auto& entry = entries.findOrCreate(
1191 path[0], [&] { return Entry{kj::str(path[0]), kj::heap<Directory::Builder>()}; });
1192 
1193 KJ_SWITCH_ONEOF(entry) {
1194 KJ_CASE_ONEOF(file, kj::Rc<File>) {
1195 // The current entry is a file but we are trying to add a directory.
1196 // This is an error.
1197 KJ_FAIL_ASSERT("Path already exists and is a file: ", path[0]);
1198 }
1199 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
1200 // The current entry is a directory that is already built.
1201 // This is an error.
1202 KJ_FAIL_ASSERT("Path already exists and is a directory: ", path[0]);
1203 }
1204 KJ_CASE_ONEOF(builder, kj::Own<Directory::Builder>) {
1205 // The current entry is a directory builder. We need to add the
1206 // rest of the path to it.
1207 builder->addPath(path.slice(1, path.size()), kj::mv(fileOrDirectory));
1208 }
1209 }
1210}
1211 
1212kj::Rc<Directory> Directory::Builder::finish() {
1213 auto map = kj::mv(entries);
1214 kj::HashMap<kj::String, Directory::Item> ret;
1215 
1216 auto addEntry = [&](kj::String name, kj::OneOf<kj::Rc<File>, kj::Rc<Directory>>&& entry) {
1217 ret.upsert(
1218 kj::str(name), kj::mv(entry), [&](auto& current, auto&& node) { return kj::mv(node); });
1219 };
1220 
1221 for (auto& entry: map) {
1222 KJ_SWITCH_ONEOF(entry.value) {
1223 KJ_CASE_ONEOF(file, kj::Rc<File>) {
1224 addEntry(kj::mv(entry.key), kj::mv(file));
1225 }
1226 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
1227 addEntry(kj::mv(entry.key), kj::mv(dir));
1228 }
1229 KJ_CASE_ONEOF(builder, kj::Own<Directory::Builder>) {
1230 addEntry(kj::mv(entry.key), builder->finish());
1231 }
1232 }
1233 }
1234 return kj::rc<ReadableDirectory>(kj::mv(ret));
1235}
1236 
1237kj::Rc<Directory> Directory::newWritable() {
1238 return kj::rc<WritableDirectory>();
1239}
1240 
1241kj::Rc<Directory> Directory::newEmptyReadonly() {
1242 Directory::Builder builder;
1243 return builder.finish();
1244}
1245 
1246kj::Rc<Directory> Directory::newWritable(jsg::Lock& js) {
1247 auto dir = kj::rc<WritableDirectory>();
1248 dir->countTowardsIsolateLimit(js);
1249 return kj::mv(dir);
1250}
1251 
1252kj::Rc<File> File::newWritable(jsg::Lock& js, kj::Maybe<uint32_t> size) {
1253 // We will cap the maximum size of the file.
1254 auto maxSize = Worker::Isolate::from(js).getLimitEnforcer().getBlobSizeLimit();
1255 auto actualSize = kj::min(size.orDefault(0), maxSize);
1256 auto data = kj::heapArray<kj::byte>(actualSize);
1257 if (actualSize > 0) data.asPtr().fill(0);
1258 auto file = kj::rc<FileImpl>(js, kj::mv(data));
1259 file->countTowardsIsolateLimit(js);
1260 return kj::mv(file);
1261}
1262 
1263kj::Rc<File> File::newReadable(kj::ArrayPtr<const kj::byte> data) {
1264 return kj::rc<FileImpl>(data);
1265}
1266 
1267kj::Own<VirtualFileSystem> newVirtualFileSystem(
1268 kj::Own<FsMap> fsMap, kj::Rc<Directory>&& root, kj::Own<VirtualFileSystem::Observer> observer) {
1269 return kj::heap<VirtualFileSystemImpl>(kj::mv(fsMap), kj::mv(root), kj::mv(observer));
1270}
1271 
1272kj::Own<VirtualFileSystem> newWorkerFileSystem(kj::Own<FsMap> fsMap,
1273 kj::Rc<Directory> bundleDirectory,
1274 kj::Own<VirtualFileSystem::Observer> observer) {
1275 // Our root directory is a read-only directory
1276 Directory::Builder builder;
1277 builder.addPath(fsMap->getBundlePath(), kj::mv(bundleDirectory));
1278 builder.addPath(fsMap->getTempPath(), getTmpDirectoryImpl());
1279 builder.addPath(fsMap->getDevPath(), getDevDirectory());
1280 return newVirtualFileSystem(kj::mv(fsMap), builder.finish(), kj::mv(observer));
1281}
1282 
1283kj::Rc<Directory> getTmpDirectoryImpl() {
1284 return kj::rc<TmpDirectory>();
1285}
1286 
1287bool TmpDirStoreScope::hasCurrent() {
1288 return tmpDirStorageScope != nullptr;
1289}
1290 
1291TmpDirStoreScope& TmpDirStoreScope::current() {
1292 KJ_ASSERT(hasCurrent(), "no current TmpDirStoreScope");
1293 return *tmpDirStorageScope;
1294}
1295 
1296TmpDirStoreScope::TmpDirStoreScope(kj::Maybe<kj::Badge<TmpDirStoreScope>> guard)
1297 : dir(Directory::newWritable()),
1298 // we use the /bundle cwd for the isolate vfs
1299 // and the /tmp cwd for the iocontext vfs
1300 cwd({"bundle"}) {
1301 if (guard == kj::none) {
1302 kj::requireOnStack(this, "must be created on the stack");
1303 onStack = true;
1304 KJ_ASSERT(!hasCurrent(), "TmpDirStoreScope already exists on this thread");
1305 tmpDirStorageScope = this;
1306 }
1307}
1308 
1309TmpDirStoreScope::~TmpDirStoreScope() noexcept(false) {
1310 if (onStack) {
1311 KJ_ASSERT(tmpDirStorageScope == this, "this TmpDirStoreScope not on the stack");
1312 tmpDirStorageScope = nullptr;
1313 }
1314}
1315 
1316kj::Own<TmpDirStoreScope> TmpDirStoreScope::create() {
1317 // Creating the instance with the badge will ensure that
1318 // it is not set as current in the stack.
1319 return kj::heap<TmpDirStoreScope>(kj::Badge<TmpDirStoreScope>());
1320}
1321 
1322Stat SymbolicLink::stat(jsg::Lock& js) {
1323 return Stat{
1324 .type = FsType::SYMLINK,
1325 .size = 0,
1326 .lastModified = kj::UNIX_EPOCH,
1327 .writable = false,
1328 };
1329}
1330 
1331jsg::Url SymbolicLink::getTargetUrl() const {
1332 auto path = getTargetPath().toString(false);
1333 return KJ_ASSERT_NONNULL(jsg::Url::tryParse(path, "file:///"_kj));
1334}
1335 
1336kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>> SymbolicLink::resolve(
1337 jsg::Lock& js) {
1338 KJ_IF_SOME(ret, root->tryOpen(js, getTargetPath())) {
1339 KJ_SWITCH_ONEOF(ret) {
1340 KJ_CASE_ONEOF(file, kj::Rc<File>) {
1341 return kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>>(kj::mv(file));
1342 }
1343 KJ_CASE_ONEOF(dir, kj::Rc<Directory>) {
1344 return kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>>(kj::mv(dir));
1345 }
1346 KJ_CASE_ONEOF(link, kj::Rc<SymbolicLink>) {
1347 // The resolve(...) method here follows all symbolic links in the path,
1348 // so when it encounters a symlink as the path is being processed, it
1349 // will attempt to resolve it into it's target or return kj::none. If
1350 // you want the symlink itself, then use tryOpen(...) on the directory
1351 KJ_UNREACHABLE;
1352 }
1353 KJ_CASE_ONEOF(err, FsError) {
1354 return kj::Maybe<kj::OneOf<FsError, kj::Rc<File>, kj::Rc<Directory>>>(err);
1355 }
1356 }
1357 KJ_UNREACHABLE;
1358 }
1359 return kj::none;
1360}
1361 
1362kj::StringPtr SymbolicLink::getUniqueId(jsg::Lock&) const {
1363 KJ_IF_SOME(id, maybeUniqueId) {
1364 return id;
1365 }
1366 // Generating a UUID requires randomness, which requires an IoContext.
1367 auto& ioContext = JSG_REQUIRE_NONNULL(
1368 IoContext::tryCurrent(), Error, "Cannot generate a unique ID outside of a request");
1369 maybeUniqueId = workerd::randomUUID(ioContext.getEntropySource());
1370 return KJ_ASSERT_NONNULL(maybeUniqueId);
1371}
1372 
1373void SymbolicLink::countTowardsIsolateLimit(jsg::Lock& js) const {
1374 if (maybeMemoryAdjustment == kj::none) {
1375 maybeMemoryAdjustment = js.getExternalMemoryAdjustment(sizeof(SymbolicLink));
1376 }
1377}
1378 
1379kj::Rc<Directory> getLazyDirectoryImpl(kj::Function<kj::Rc<Directory>()> func) {
1380 return kj::rc<LazyDirectory>(kj::mv(func));
1381}
1382 
1383const VirtualFileSystem& VirtualFileSystem::current(jsg::Lock& js) {
1384 // The VFS is stored in an embedder data slot in the v8::Context associated with
1385 // the current jsg::Lock. The actual instance is kept alive using a kj::Own held
1386 // by the Worker::Script.
1387 return KJ_ASSERT_NONNULL(jsg::getAlignedPointerFromEmbedderData<VirtualFileSystem>(
1388 js.v8Context(), jsg::ContextPointerSlot::VIRTUAL_FILE_SYSTEM));
1389}
1390 
1391kj::Maybe<FsNodeWithError> VirtualFileSystem::resolve(
1392 jsg::Lock& js, const jsg::Url& url, ResolveOptions options) const {
1393 if (url.getProtocol() != "file:"_kj) {
1394 // We only accept file URLs.
1395 return kj::none;
1396 }
1397 // We want to strip the leading slash from the path.
1398 auto path = kj::str(url.getPathname().slice(1));
1399 kj::Path root{};
1400 return getRoot(js)->tryOpen(js, root.eval(path),
1401 Directory::OpenOptions{
1402 .followLinks = options.followLinks,
1403 });
1404}
1405 
1406kj::Maybe<kj::OneOf<FsError, Stat>> VirtualFileSystem::resolveStat(
1407 jsg::Lock& js, const jsg::Url& url) const {
1408 if (url.getProtocol() != "file:"_kj) {
1409 // We only accept file URLs.
1410 return kj::none;
1411 }
1412 // We want to strip the leading slash from the path.
1413 auto path = kj::str(url.getPathname().slice(1));
1414 kj::Path root{};
1415 return getRoot(js)->stat(js, root.eval(path));
1416}
1417 
1418kj::Rc<SymbolicLink> VirtualFileSystem::newSymbolicLink(jsg::Lock& js, const jsg::Url& url) const {
1419 KJ_REQUIRE(url.getProtocol() == "file:"_kj);
1420 auto path = kj::str(url.getPathname().slice(1));
1421 kj::Path root{};
1422 return kj::rc<SymbolicLink>(getRoot(js), root.eval(path));
1423}
1424 
1425SymbolicLinkRecursionGuardScope::SymbolicLinkRecursionGuardScope() {
1426 if (symbolicLinkGuard == nullptr) {
1427 symbolicLinkGuard = this;
1428 }
1429}
1430SymbolicLinkRecursionGuardScope::~SymbolicLinkRecursionGuardScope() noexcept(false) {
1431 if (symbolicLinkGuard == this) {
1432 symbolicLinkGuard = nullptr;
1433 }
1434}
1435 
1436kj::Maybe<FsError> SymbolicLinkRecursionGuardScope::checkSeen(SymbolicLink* link) {
1437 if (symbolicLinkGuard == nullptr) {
1438 return kj::none;
1439 }
1440 auto& guard = *symbolicLinkGuard;
1441 if (guard.linksSeen.find(link) != kj::none) {
1442 return FsError::SYMLINK_DEPTH_EXCEEDED;
1443 }
1444 guard.linksSeen.insert(link);
1445 if (guard.linksSeen.size() > kMaxSymlinkDepth) {
1446 return FsError::SYMLINK_DEPTH_EXCEEDED;
1447 }
1448 return kj::none;
1449}
1450 
1451namespace {
1452// Implementations of special "device" files equivalent to special devices typically
1453// found on posix systems.
1454 
1455// /dev/null is a special file that discards all data written to it and returns
1456// EOF on reads.
1457class DevNullFile final: public File {
1458 public:
1459 DevNullFile() = default;
1460 
1461 Stat stat(jsg::Lock& js) override {
1462 return Stat{
1463 .type = FsType::FILE,
1464 .size = 0,
1465 .lastModified = kj::UNIX_EPOCH,
1466 .writable = true,
1467 .device = true,
1468 };
1469 }
1470 
1471 kj::OneOf<FsError, kj::Rc<File>> clone(jsg::Lock&) override {
1472 kj::Rc<File> ref = addRefToThis();
1473 return kj::mv(ref);
1474 }
1475 
1476 kj::Maybe<FsError> replace(jsg::Lock& js, kj::Rc<File> file) override {
1477 return kj::none;
1478 }
1479 
1480 kj::Maybe<FsError> setLastModified(jsg::Lock& js, kj::Date date = kj::UNIX_EPOCH) override {
1481 return kj::none;
1482 }
1483 
1484 kj::Maybe<FsError> fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
1485 return kj::none;
1486 }
1487 
1488 kj::Maybe<FsError> resize(jsg::Lock& js, uint32_t size) override {
1489 return kj::none;
1490 }
1491 
1492 kj::StringPtr jsgGetMemoryName() const override {
1493 return "/dev/null"_kj;
1494 }
1495 
1496 size_t jsgGetMemorySelfSize() const override {
1497 return sizeof(DevNullFile);
1498 }
1499 
1500 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {
1501 // No-op.
1502 }
1503 
1504 uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
1505 return 0;
1506 }
1507 
1508 kj::OneOf<FsError, uint32_t> write(
1509 jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
1510 return static_cast<uint32_t>(buffer.size());
1511 }
1512 
1513 kj::StringPtr getUniqueId(jsg::Lock&) const override {
1514 KJ_IF_SOME(id, maybeUniqueId) {
1515 return id;
1516 }
1517 // Generating a UUID requires randomness, which requires an IoContext.
1518 auto& ioContext = JSG_REQUIRE_NONNULL(
1519 IoContext::tryCurrent(), Error, "Cannot generate a unique ID outside of a request");
1520 maybeUniqueId = workerd::randomUUID(ioContext.getEntropySource());
1521 return KJ_ASSERT_NONNULL(maybeUniqueId);
1522 }
1523 
1524 private:
1525 mutable kj::Maybe<kj::String> maybeUniqueId;
1526};
1527 
1528// /dev/zero is a special file that returns zeroes when read from and
1529// ignores writes.
1530class DevZeroFile final: public File {
1531 public:
1532 DevZeroFile() = default;
1533 
1534 Stat stat(jsg::Lock& js) override {
1535 return Stat{
1536 .type = FsType::FILE,
1537 .size = 0,
1538 .lastModified = kj::UNIX_EPOCH,
1539 .writable = true,
1540 .device = true,
1541 };
1542 }
1543 
1544 kj::OneOf<FsError, kj::Rc<File>> clone(jsg::Lock&) override {
1545 kj::Rc<File> ref = addRefToThis();
1546 return kj::mv(ref);
1547 }
1548 
1549 kj::Maybe<FsError> replace(jsg::Lock& js, kj::Rc<File> file) override {
1550 return kj::none;
1551 }
1552 
1553 kj::Maybe<FsError> setLastModified(jsg::Lock& js, kj::Date date = kj::UNIX_EPOCH) override {
1554 return kj::none;
1555 }
1556 
1557 kj::Maybe<FsError> fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
1558 return kj::none;
1559 }
1560 
1561 kj::Maybe<FsError> resize(jsg::Lock& js, uint32_t size) override {
1562 return kj::none;
1563 }
1564 
1565 kj::StringPtr jsgGetMemoryName() const override {
1566 return "/dev/zero"_kj;
1567 }
1568 
1569 size_t jsgGetMemorySelfSize() const override {
1570 return sizeof(DevZeroFile);
1571 }
1572 
1573 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {
1574 // No-op.
1575 }
1576 
1577 uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
1578 buffer.fill(0);
1579 return buffer.size();
1580 }
1581 
1582 kj::OneOf<FsError, uint32_t> write(
1583 jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
1584 return static_cast<uint32_t>(buffer.size());
1585 }
1586 
1587 kj::StringPtr getUniqueId(jsg::Lock&) const override {
1588 KJ_IF_SOME(id, maybeUniqueId) {
1589 return id;
1590 }
1591 // Generating a UUID requires randomness, which requires an IoContext.
1592 auto& ioContext = JSG_REQUIRE_NONNULL(
1593 IoContext::tryCurrent(), Error, "Cannot generate a unique ID outside of a request");
1594 maybeUniqueId = workerd::randomUUID(ioContext.getEntropySource());
1595 return KJ_ASSERT_NONNULL(maybeUniqueId);
1596 }
1597 
1598 private:
1599 mutable kj::Maybe<kj::String> maybeUniqueId;
1600};
1601 
1602// /dev/full is a special file that returns zeroes when read from and
1603// returns an error when written to.
1604class DevFullFile final: public File {
1605 public:
1606 DevFullFile() = default;
1607 
1608 Stat stat(jsg::Lock& js) override {
1609 return Stat{
1610 .type = FsType::FILE,
1611 .size = 0,
1612 .lastModified = kj::UNIX_EPOCH,
1613 .writable = true,
1614 .device = true,
1615 };
1616 }
1617 
1618 kj::OneOf<FsError, kj::Rc<File>> clone(jsg::Lock&) override {
1619 kj::Rc<File> ref = addRefToThis();
1620 return kj::mv(ref);
1621 }
1622 
1623 kj::Maybe<FsError> replace(jsg::Lock& js, kj::Rc<File> file) override {
1624 return FsError::NOT_PERMITTED;
1625 }
1626 
1627 kj::Maybe<FsError> setLastModified(jsg::Lock& js, kj::Date date = kj::UNIX_EPOCH) override {
1628 return kj::none;
1629 }
1630 
1631 kj::Maybe<FsError> fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
1632 return FsError::NOT_PERMITTED;
1633 }
1634 
1635 kj::Maybe<FsError> resize(jsg::Lock& js, uint32_t size) override {
1636 return FsError::NOT_PERMITTED;
1637 }
1638 
1639 kj::StringPtr jsgGetMemoryName() const override {
1640 return "/dev/full"_kj;
1641 }
1642 
1643 size_t jsgGetMemorySelfSize() const override {
1644 return sizeof(DevFullFile);
1645 }
1646 
1647 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {
1648 // No-op.
1649 }
1650 
1651 uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
1652 buffer.fill(0);
1653 return buffer.size();
1654 }
1655 
1656 kj::OneOf<FsError, uint32_t> write(
1657 jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
1658 return FsError::NOT_PERMITTED;
1659 }
1660 
1661 kj::StringPtr getUniqueId(jsg::Lock&) const override {
1662 KJ_IF_SOME(id, maybeUniqueId) {
1663 return id;
1664 }
1665 // Generating a UUID requires randomness, which requires an IoContext.
1666 auto& ioContext = JSG_REQUIRE_NONNULL(
1667 IoContext::tryCurrent(), Error, "Cannot generate a unique ID outside of a request");
1668 maybeUniqueId = workerd::randomUUID(ioContext.getEntropySource());
1669 return KJ_ASSERT_NONNULL(maybeUniqueId);
1670 }
1671 
1672 private:
1673 mutable kj::Maybe<kj::String> maybeUniqueId;
1674};
1675 
1676class DevRandomFile final: public File {
1677 public:
1678 DevRandomFile() = default;
1679 
1680 Stat stat(jsg::Lock& js) override {
1681 return Stat{
1682 .type = FsType::FILE,
1683 .size = 0,
1684 .lastModified = kj::UNIX_EPOCH,
1685 .writable = true,
1686 .device = true,
1687 };
1688 }
1689 
1690 kj::OneOf<FsError, kj::Rc<File>> clone(jsg::Lock&) override {
1691 kj::Rc<File> ref = addRefToThis();
1692 return kj::mv(ref);
1693 }
1694 
1695 kj::Maybe<FsError> replace(jsg::Lock& js, kj::Rc<File> file) override {
1696 return FsError::NOT_PERMITTED;
1697 }
1698 
1699 kj::Maybe<FsError> setLastModified(jsg::Lock& js, kj::Date date = kj::UNIX_EPOCH) override {
1700 return FsError::NOT_PERMITTED;
1701 }
1702 
1703 kj::Maybe<FsError> fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
1704 return FsError::NOT_PERMITTED;
1705 }
1706 
1707 kj::Maybe<FsError> resize(jsg::Lock& js, uint32_t size) override {
1708 return FsError::NOT_PERMITTED;
1709 }
1710 
1711 kj::StringPtr jsgGetMemoryName() const override {
1712 return "/dev/random"_kj;
1713 }
1714 
1715 size_t jsgGetMemorySelfSize() const override {
1716 return sizeof(DevRandomFile);
1717 }
1718 
1719 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {
1720 // No-op.
1721 }
1722 
1723 kj::OneOf<FsError, uint32_t> write(
1724 jsg::Lock& js, uint32_t offset, kj::ArrayPtr<const kj::byte> buffer) override {
1725 return FsError::NOT_PERMITTED;
1726 }
1727 
1728 uint32_t read(jsg::Lock& js, uint32_t offset, kj::ArrayPtr<kj::byte> buffer) const override {
1729 // We can only generate random bytes when we have an active IoContext.
1730 // If there is no IoContext, this will return 0 bytes.
1731 KJ_IF_SOME(ioContext, IoContext::tryCurrent()) {
1732 if (isPredictableModeForTest()) {
1733 buffer.fill(9);
1734 } else {
1735 ioContext.getEntropySource().generate(buffer);
1736 }
1737 return buffer.size();
1738 }
1739 return 0;
1740 }
1741 
1742 kj::StringPtr getUniqueId(jsg::Lock&) const override {
1743 KJ_IF_SOME(id, maybeUniqueId) {
1744 return id;
1745 }
1746 // Generating a UUID requires randomness, which requires an IoContext.
1747 auto& ioContext = JSG_REQUIRE_NONNULL(
1748 IoContext::tryCurrent(), Error, "Cannot generate a unique ID outside of a request");
1749 maybeUniqueId = workerd::randomUUID(ioContext.getEntropySource());
1750 return KJ_ASSERT_NONNULL(maybeUniqueId);
1751 }
1752 
1753 private:
1754 mutable kj::Maybe<kj::String> maybeUniqueId;
1755};
1756 
1757// Write stdio via console.log. Somewhat convoluted, but this then supports:
1758// - inspector reporting
1759// - structured logging
1760// - stdio output otherwise
1761void writeStdio(jsg::Lock& js, VirtualFileSystem::Stdio type, kj::ArrayPtr<const kj::byte> bytes) {
1762 auto chars = bytes.asChars();
1763 size_t endPos = chars.size();
1764 if (endPos > 0 && chars[endPos - 1] == '\n') endPos--;
1765 
1766 KJ_IF_SOME(console, js.global().get(js, "console"_kj).tryCast<jsg::JsObject>()) {
1767 auto method = console.get(js, "log"_kj);
1768 if (method.isFunction()) {
1769 v8::Local<v8::Value> methodVal(method);
1770 auto methodFunc = jsg::JsFunction(methodVal.As<v8::Function>());
1771 
1772 kj::String outputStr;
1773 auto isolate = &Worker::Isolate::from(js);
1774 auto prefix = type == VirtualFileSystem::Stdio::OUT ? isolate->getStdoutPrefix()
1775 : isolate->getStderrPrefix();
1776 if (endPos == 0) {
1777 methodFunc.call(js, console, js.str(prefix));
1778 } else if (prefix.size() > 0) {
1779 methodFunc.call(js, console, js.str(kj::str(prefix, " "_kj, chars.first(endPos))));
1780 } else {
1781 methodFunc.call(js, console, js.str(chars.first(endPos)));
1782 }
1783 return;
1784 }
1785 }
1786 KJ_LOG(WARNING, "No console.log implementation available for stdio logging");
1787}
1788 
1789// An StdioFile is a special file implementation used to represent stdin,
1790// stdout, and stderr outputs. Writes are always forwarded to the underlying
1791// logging mechanisms. Reads always return EOF (0-byte reads).
1792class StdioFile final: public File {
1793 public:
1794 StdioFile(VirtualFileSystem::Stdio type)
1795 : type(type),
1796 // TODO(sometime): Investigate if we can refactor out the weakref here?
1797 weakThis(kj::rc<WeakRef<StdioFile>>(kj::Badge<StdioFile>(), *this)) {}
1798 
1799 ~StdioFile() noexcept(false) override {
1800 weakThis->invalidate();
1801 }
1802 
1803 Stat stat(jsg::Lock& js) override {
1804 return Stat{
1805 .type = FsType::FILE,
1806 .size = 0,
1807 .lastModified = kj::UNIX_EPOCH,
1808 .writable = true,
1809 .device = false,
1810 };
1811 }
1812 
1813 kj::OneOf<FsError, kj::Rc<File>> clone(jsg::Lock&) override {
1814 kj::Rc<File> ref = addRefToThis();
1815 return kj::mv(ref);
1816 }
1817 
1818 kj::Maybe<FsError> replace(jsg::Lock& js, kj::Rc<File> file) override {
1819 return FsError::NOT_PERMITTED;
1820 }
1821 
1822 kj::Maybe<FsError> setLastModified(jsg::Lock& js, kj::Date date = kj::UNIX_EPOCH) override {
1823 return FsError::NOT_PERMITTED;
1824 }
1825 
1826 kj::Maybe<FsError> fill(jsg::Lock& js, kj::byte value, kj::Maybe<uint32_t> offset) override {
1827 return FsError::NOT_PERMITTED;
1828 }
1829 
1830 kj::Maybe<FsError> resize(jsg::Lock& js, uint32_t size) override {
1831 return FsError::NOT_PERMITTED;
1832 }
1833 
1834 kj::StringPtr jsgGetMemoryName() const override {
1835 return "stdio"_kj;
1836 }
1837 
1838 size_t jsgGetMemorySelfSize() const override {
1839 return sizeof(StdioFile);
1840 }
1841 
1842 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {}
1843 
1844 kj::OneOf<FsError, uint32_t> write(
1845 jsg::Lock& js, uint32_t, kj::ArrayPtr<const kj::byte> buffer) override {
1846 if (buffer.size() > MAX_WRITE_SIZE) {
1847 buffer = buffer.first(MAX_WRITE_SIZE);
1848 }
1849 
1850 if (buffer.size() == 0) return static_cast<uint32_t>(0);
1851 
1852 // We ignore the offset here. All writes are assumed to be appends.
1853 if (type != VirtualFileSystem::Stdio::IN) {
1854 size_t pos = 0;
1855 
1856 // Newline-based buffering
1857 while (pos < buffer.size()) {
1858 size_t newlinePos = pos;
1859 while (newlinePos < buffer.size() && buffer[newlinePos] != '\n') {
1860 newlinePos++;
1861 }
1862 
1863 if (newlinePos < buffer.size()) {
1864 auto lineData = buffer.slice(pos, newlinePos + 1);
1865 
1866 if (!lineBuffer.empty()) {
1867 // We have buffered data - append the line data to it
1868 lineBuffer.addAll(lineData);
1869 writeStdio(js, type, lineBuffer.asPtr());
1870 lineBuffer.clear();
1871 } else {
1872 // No buffered data - log this line directly
1873 writeStdio(js, type, lineData);
1874 }
1875 
1876 pos = newlinePos + 1;
1877 } else {
1878 // No newlines -> append to line buffer
1879 auto remaining = buffer.slice(pos);
1880 auto totalSize = lineBuffer.size() + remaining.size();
1881 
1882 if (totalSize <= MAX_LINE_BUFFER_SIZE) {
1883 lineBuffer.addAll(remaining);
1884 } else {
1885 // New data alone exceeds limit, replace entire line buffer
1886 if (remaining.size() >= MAX_LINE_BUFFER_SIZE) {
1887 lineBuffer.clear();
1888 lineBuffer.addAll(remaining.slice(remaining.size() - MAX_LINE_BUFFER_SIZE));
1889 } else {
1890 // Combined size exceeds limit, remove oldest data
1891 auto toRemove = totalSize - MAX_LINE_BUFFER_SIZE;
1892 kj::Vector<kj::byte> newBuffer(MAX_LINE_BUFFER_SIZE);
1893 newBuffer.addAll(lineBuffer.slice(toRemove, lineBuffer.size()));
1894 newBuffer.addAll(remaining);
1895 lineBuffer = kj::mv(newBuffer);
1896 }
1897 }
1898 
1899 // Schedule a microtask to flush the lineBuffer
1900 // this way synchronous writes join the line, but async writes will be on a new line
1901 scheduleFlushMicrotask(js);
1902 
1903 break;
1904 }
1905 }
1906 }
1907 return static_cast<uint32_t>(buffer.size());
1908 }
1909 
1910 uint32_t read(jsg::Lock&, uint32_t, kj::ArrayPtr<kj::byte>) const override {
1911 return 0; // EOF
1912 }
1913 
1914 kj::StringPtr getUniqueId(jsg::Lock&) const override {
1915 KJ_IF_SOME(id, maybeUniqueId) {
1916 return id;
1917 }
1918 // Generating a UUID requires randomness, which requires an IoContext.
1919 auto& ioContext = JSG_REQUIRE_NONNULL(
1920 IoContext::tryCurrent(), Error, "Cannot generate a unique ID outside of a request");
1921 maybeUniqueId = workerd::randomUUID(ioContext.getEntropySource());
1922 return KJ_ASSERT_NONNULL(maybeUniqueId);
1923 }
1924 
1925 private:
1926 VirtualFileSystem::Stdio type;
1927 mutable kj::Maybe<kj::String> maybeUniqueId;
1928 
1929 static constexpr size_t MAX_LINE_BUFFER_SIZE = 4096;
1930 static constexpr size_t MAX_WRITE_SIZE = 16 * 1024;
1931 mutable kj::Vector<kj::byte> lineBuffer;
1932 mutable bool microtaskScheduled = false;
1933 
1934 kj::Rc<WeakRef<StdioFile>> weakThis;
1935 
1936 void scheduleFlushMicrotask(jsg::Lock& js) {
1937 if (microtaskScheduled) return;
1938 microtaskScheduled = true;
1939 
1940 // Create ephemeral callback with weak reference for safety
1941 auto callback = js.wrapSimpleFunction(js.v8Context(),
1942 [weakThis = weakThis->addRef()](jsg::Lock& js, const v8::FunctionCallbackInfo<v8::Value>&) {
1943 weakThis->runIfAlive([&](StdioFile& self) {
1944 self.microtaskScheduled = false;
1945 
1946 if (!self.lineBuffer.empty()) {
1947 if (IoContext::hasCurrent()) {
1948 writeStdio(js, self.type, self.lineBuffer.asPtr());
1949 }
1950 self.lineBuffer.clear();
1951 }
1952 });
1953 });
1954 js.v8Isolate->EnqueueMicrotask(callback);
1955 }
1956};
1957 
1958} // namespace
1959 
1960kj::Rc<File> getDevNull() {
1961 return kj::rc<DevNullFile>();
1962}
1963 
1964kj::Rc<File> getDevZero() {
1965 return kj::rc<DevZeroFile>();
1966}
1967 
1968kj::Rc<File> getDevFull() {
1969 return kj::rc<DevFullFile>();
1970}
1971 
1972kj::Rc<File> getDevRandom() {
1973 return kj::rc<DevRandomFile>();
1974}
1975 
1976kj::Rc<Directory> getDevDirectory() {
1977 Directory::Builder builder;
1978 builder.add("null", getDevNull());
1979 builder.add("zero", getDevZero());
1980 builder.add("full", getDevFull());
1981 builder.add("random", getDevRandom());
1982 return builder.finish();
1983}
1984 
1985kj::Rc<VirtualFileSystem::OpenedFile> VirtualFileSystemImpl::getStdio(
1986 jsg::Lock& js, Stdio stdio) const {
1987 int n = static_cast<int>(stdio);
1988 KJ_IF_SOME(existing, openedFiles.find(n)) {
1989 return existing.addRef();
1990 }
1991 auto stdioFile = kj::rc<StdioFile>(stdio);
1992 kj::Rc<workerd::File> file = kj::mv(stdioFile);
1993 auto opened = kj::rc<VirtualFileSystem::OpenedFile>(n, true, true, true, kj::mv(file));
1994 openedFiles.insert(n, opened.addRef());
1995 return kj::mv(opened);
1996}
1997 
1998} // namespace workerd