Superblock.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.Superblock = void 0;
  4. const path_1 = require("@jsonjoy.com/fs-node-builtins/lib/path");
  5. const Node_1 = require("./Node");
  6. const Link_1 = require("./Link");
  7. const File_1 = require("./File");
  8. const buffer_1 = require("@jsonjoy.com/fs-node-builtins/lib/internal/buffer");
  9. const process_1 = require("./process");
  10. const fs_node_utils_1 = require("@jsonjoy.com/fs-node-utils");
  11. const fs_node_utils_2 = require("@jsonjoy.com/fs-node-utils");
  12. const util_1 = require("./util");
  13. const json_1 = require("./json");
  14. const result_1 = require("./result");
  15. const fanout_1 = require("thingies/lib/fanout");
  16. const FsEvent_1 = require("./watch/FsEvent");
  17. const pathSep = path_1.posix ? path_1.posix.sep : path_1.sep;
  18. const pathRelative = path_1.posix ? path_1.posix.relative : path_1.relative;
  19. const pathJoin = path_1.posix ? path_1.posix.join : path_1.join;
  20. const { O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC, O_APPEND, O_DIRECTORY } = fs_node_utils_1.constants;
  21. /**
  22. * Represents a filesystem superblock, which is the root of a virtual
  23. * filesystem in Linux.
  24. * @see https://lxr.linux.no/linux+v3.11.2/include/linux/fs.h#L1242
  25. */
  26. class Superblock {
  27. static fromJSON(json, cwd, opts) {
  28. const vol = new Superblock(opts);
  29. vol.fromJSON(json, cwd);
  30. return vol;
  31. }
  32. static fromNestedJSON(json, cwd, opts) {
  33. const vol = new Superblock(opts);
  34. vol.fromNestedJSON(json, cwd);
  35. return vol;
  36. }
  37. constructor(opts = {}) {
  38. // I-node number counter.
  39. this.ino = 0;
  40. // A mapping for i-node numbers to i-nodes (`Node`);
  41. this.inodes = {};
  42. // List of released i-node numbers, for reuse.
  43. this.releasedInos = [];
  44. // A mapping for file descriptors to `File`s.
  45. this.fds = {};
  46. // A list of reusable (opened and closed) file descriptors, that should be
  47. // used first before creating a new file descriptor.
  48. this.releasedFds = [];
  49. // Max number of open files.
  50. this.maxFiles = 10000;
  51. // Current number of open files.
  52. this.openFiles = 0;
  53. /** Fan-out of file system change events. Multiple consumers may subscribe. */
  54. this.changes = new fanout_1.FanOut();
  55. this.open = (filename, flagsNum, modeNum, resolveSymlinks = true) => {
  56. const file = this.openFile(filename, flagsNum, modeNum, resolveSymlinks);
  57. if (!file)
  58. throw (0, util_1.createError)("ENOENT" /* ERROR_CODE.ENOENT */, 'open', filename);
  59. return file.fd;
  60. };
  61. this.writeFile = (id, buf, flagsNum, modeNum) => {
  62. const isUserFd = typeof id === 'number';
  63. let fd;
  64. if (isUserFd)
  65. fd = id;
  66. else
  67. fd = this.open((0, util_1.pathToFilename)(id), flagsNum, modeNum);
  68. let offset = 0;
  69. let length = buf.length;
  70. let position = flagsNum & O_APPEND ? undefined : 0;
  71. try {
  72. while (length > 0) {
  73. const written = this.write(fd, buf, offset, length, position);
  74. offset += written;
  75. length -= written;
  76. if (position !== undefined)
  77. position += written;
  78. }
  79. }
  80. finally {
  81. if (!isUserFd)
  82. this.close(fd);
  83. }
  84. };
  85. this.read = (fd, buffer, offset, length, position) => {
  86. if (buffer.byteLength < length) {
  87. throw (0, util_1.createError)("ERR_OUT_OF_RANGE" /* ERROR_CODE.ERR_OUT_OF_RANGE */, 'read', undefined, undefined, RangeError);
  88. }
  89. const file = this.getFileByFdOrThrow(fd);
  90. if (file.node.isSymlink()) {
  91. throw (0, util_1.createError)("EPERM" /* ERROR_CODE.EPERM */, 'read', file.link.getPath());
  92. }
  93. return file.read(buffer, Number(offset), Number(length), position === -1 || typeof position !== 'number' ? undefined : position);
  94. };
  95. this.readv = (fd, buffers, position) => {
  96. const file = this.getFileByFdOrThrow(fd);
  97. let p = position ?? undefined;
  98. if (p === -1)
  99. p = undefined;
  100. let bytesRead = 0;
  101. for (const buffer of buffers) {
  102. const bytes = file.read(buffer, 0, buffer.byteLength, p);
  103. p = undefined;
  104. bytesRead += bytes;
  105. if (bytes < buffer.byteLength)
  106. break;
  107. }
  108. return bytesRead;
  109. };
  110. this.link = (filename1, filename2) => {
  111. let link1;
  112. try {
  113. link1 = this.getLinkOrThrow(filename1, 'link');
  114. }
  115. catch (err) {
  116. if (err.code)
  117. err = (0, util_1.createError)(err.code, 'link', filename1, filename2);
  118. throw err;
  119. }
  120. const dirname2 = (0, path_1.dirname)(filename2);
  121. let dir2;
  122. try {
  123. dir2 = this.getLinkOrThrow(dirname2, 'link');
  124. }
  125. catch (err) {
  126. // Augment error with filename1
  127. if (err.code)
  128. err = (0, util_1.createError)(err.code, 'link', filename1, filename2);
  129. throw err;
  130. }
  131. const name = (0, path_1.basename)(filename2);
  132. if (dir2.getChild(name))
  133. throw (0, util_1.createError)("EEXIST" /* ERROR_CODE.EEXIST */, 'link', filename1, filename2);
  134. const node = link1.getNode();
  135. node.nlink++;
  136. const newLink = dir2.createChild(name, node);
  137. this.emit(new FsEvent_1.FsEvent(0 /* FsEventType.CREATE */, newLink.steps, node, newLink));
  138. };
  139. this.unlink = (filename) => {
  140. const link = this.getLinkOrThrow(filename, 'unlink');
  141. if (link.getNode().isDirectory())
  142. throw (0, util_1.createError)("EPERM" /* ERROR_CODE.EPERM */, 'unlink', filename);
  143. this._emitDeleteRecursive(link);
  144. this.deleteLink(link);
  145. const node = link.getNode();
  146. node.nlink--;
  147. // When all hard links to i-node are deleted, remove the i-node, too.
  148. if (node.nlink <= 0) {
  149. this.deleteNode(node);
  150. }
  151. };
  152. this.symlink = (targetFilename, pathFilename) => {
  153. const pathSteps = (0, util_1.filenameToSteps)(pathFilename);
  154. // Check if directory exists, where we about to create a symlink.
  155. let dirLink;
  156. try {
  157. dirLink = this.getLinkParentAsDirOrThrow(pathSteps);
  158. }
  159. catch (err) {
  160. // Catch error to populate with the correct fields - getLinkParentAsDirOrThrow won't be aware of the second path
  161. if (err.code)
  162. err = (0, util_1.createError)(err.code, 'symlink', targetFilename, pathFilename);
  163. throw err;
  164. }
  165. const name = pathSteps[pathSteps.length - 1];
  166. // Check if new file already exists.
  167. if (dirLink.getChild(name))
  168. throw (0, util_1.createError)("EEXIST" /* ERROR_CODE.EEXIST */, 'symlink', targetFilename, pathFilename);
  169. // Check permissions on the path where we are creating the symlink.
  170. // Note we're not checking permissions on the target path: It is not an error to create a symlink to a
  171. // non-existent or inaccessible target
  172. const node = dirLink.getNode();
  173. if (!node.canExecute() || !node.canWrite())
  174. throw (0, util_1.createError)("EACCES" /* ERROR_CODE.EACCES */, 'symlink', targetFilename, pathFilename);
  175. // Create symlink.
  176. const symlink = dirLink.createChild(name);
  177. symlink.getNode().makeSymlink(targetFilename);
  178. this.emit(new FsEvent_1.FsEvent(0 /* FsEventType.CREATE */, symlink.steps, symlink.getNode(), symlink));
  179. return symlink;
  180. };
  181. this.rename = (oldPathFilename, newPathFilename) => {
  182. let link;
  183. try {
  184. link = this.getResolvedLinkOrThrow(oldPathFilename);
  185. }
  186. catch (err) {
  187. // Augment err with newPathFilename
  188. if (err.code)
  189. err = (0, util_1.createError)(err.code, 'rename', oldPathFilename, newPathFilename);
  190. throw err;
  191. }
  192. // TODO: Check if it is directory, if non-empty, we cannot move it, right?
  193. // Check directory exists for the new location.
  194. let newPathDirLink;
  195. try {
  196. newPathDirLink = this.getLinkParentAsDirOrThrow(newPathFilename);
  197. }
  198. catch (err) {
  199. // Augment error with oldPathFilename
  200. if (err.code)
  201. err = (0, util_1.createError)(err.code, 'rename', oldPathFilename, newPathFilename);
  202. throw err;
  203. }
  204. // TODO: Also treat cases with directories and symbolic links.
  205. // TODO: See: http://man7.org/linux/man-pages/man2/rename.2.html
  206. // Remove hard link from old folder.
  207. const oldLinkParent = link.parent;
  208. if (!oldLinkParent)
  209. throw (0, util_1.createError)("EINVAL" /* ERROR_CODE.EINVAL */, 'rename', oldPathFilename, newPathFilename);
  210. // Check we have access and write permissions in both places
  211. const oldParentNode = oldLinkParent.getNode();
  212. const newPathDirNode = newPathDirLink.getNode();
  213. if (!oldParentNode.canExecute() ||
  214. !oldParentNode.canWrite() ||
  215. !newPathDirNode.canExecute() ||
  216. !newPathDirNode.canWrite()) {
  217. throw (0, util_1.createError)("EACCES" /* ERROR_CODE.EACCES */, 'rename', oldPathFilename, newPathFilename);
  218. }
  219. oldLinkParent.deleteChild(link);
  220. // Rename should overwrite the new path, if that exists.
  221. const name = (0, path_1.basename)(newPathFilename);
  222. const oldSteps = link.steps;
  223. link.name = name;
  224. link.steps = [...newPathDirLink.steps, name];
  225. newPathDirLink.setChild(link.getName(), link);
  226. this.emit(new FsEvent_1.FsEvent(4 /* FsEventType.MOVE */, link.steps, link.getNode(), link, oldSteps));
  227. };
  228. this.mkdir = (filename, modeNum) => {
  229. const steps = (0, util_1.filenameToSteps)(filename);
  230. // This will throw if user tries to create root dir `fs.mkdirSync('/')`.
  231. if (!steps.length)
  232. throw (0, util_1.createError)("EEXIST" /* ERROR_CODE.EEXIST */, 'mkdir', filename);
  233. const dir = this.getLinkParentAsDirOrThrow(filename, 'mkdir');
  234. // Check path already exists.
  235. const name = steps[steps.length - 1];
  236. if (dir.getChild(name))
  237. throw (0, util_1.createError)("EEXIST" /* ERROR_CODE.EEXIST */, 'mkdir', filename);
  238. const node = dir.getNode();
  239. if (!node.canWrite() || !node.canExecute())
  240. throw (0, util_1.createError)("EACCES" /* ERROR_CODE.EACCES */, 'mkdir', filename);
  241. const child = dir.createChild(name, this.createNode(fs_node_utils_1.constants.S_IFDIR | modeNum));
  242. this.emit(new FsEvent_1.FsEvent(0 /* FsEventType.CREATE */, child.steps, child.getNode(), child));
  243. };
  244. /**
  245. * Creates directory tree recursively.
  246. */
  247. this.mkdirp = (filename, modeNum) => {
  248. let created = false;
  249. const steps = (0, util_1.filenameToSteps)(filename);
  250. let curr = null;
  251. let i = steps.length;
  252. // Find the longest subpath of filename that still exists:
  253. for (i = steps.length; i >= 0; i--) {
  254. curr = this.getResolvedLink(steps.slice(0, i));
  255. if (curr)
  256. break;
  257. }
  258. if (!curr) {
  259. curr = this.root;
  260. i = 0;
  261. }
  262. // curr is now the last directory that still exists.
  263. // (If none of them existed, curr is the root.)
  264. // Check access the lazy way:
  265. curr = this.getResolvedLinkOrThrow(path_1.sep + steps.slice(0, i).join(path_1.sep), 'mkdir');
  266. // Start creating directories:
  267. for (i; i < steps.length; i++) {
  268. const node = curr.getNode();
  269. if (node.isDirectory()) {
  270. // Check we have permissions
  271. if (!node.canExecute() || !node.canWrite())
  272. throw (0, util_1.createError)("EACCES" /* ERROR_CODE.EACCES */, 'mkdir', filename);
  273. }
  274. else {
  275. throw (0, util_1.createError)("ENOTDIR" /* ERROR_CODE.ENOTDIR */, 'mkdir', filename);
  276. }
  277. created = true;
  278. curr = curr.createChild(steps[i], this.createNode(fs_node_utils_1.constants.S_IFDIR | modeNum));
  279. this.emit(new FsEvent_1.FsEvent(0 /* FsEventType.CREATE */, curr.steps, curr.getNode(), curr));
  280. }
  281. return created ? filename : undefined;
  282. };
  283. this.rmdir = (filename, recursive = false) => {
  284. const link = this.getLinkAsDirOrThrow(filename, 'rmdir');
  285. if (link.length && !recursive)
  286. throw (0, util_1.createError)("ENOTEMPTY" /* ERROR_CODE.ENOTEMPTY */, 'rmdir', filename);
  287. this._emitDeleteRecursive(link);
  288. this.deleteLink(link);
  289. };
  290. this.rm = (filename, force = false, recursive = false) => {
  291. // "stat" is used to match Node's native error message.
  292. let link;
  293. try {
  294. link = this.getResolvedLinkOrThrow(filename, 'stat');
  295. }
  296. catch (err) {
  297. // Silently ignore missing paths if force option is true
  298. if (err.code === "ENOENT" /* ERROR_CODE.ENOENT */ && force)
  299. return;
  300. else
  301. throw err;
  302. }
  303. if (link.getNode().isDirectory() && !recursive)
  304. throw (0, util_1.createError)("ERR_FS_EISDIR" /* ERROR_CODE.ERR_FS_EISDIR */, 'rm', filename);
  305. if (!link.parent?.getNode().canWrite())
  306. throw (0, util_1.createError)("EACCES" /* ERROR_CODE.EACCES */, 'rm', filename);
  307. this._emitDeleteRecursive(link);
  308. this.deleteLink(link);
  309. };
  310. this.close = (fd) => {
  311. (0, util_1.validateFd)(fd);
  312. const file = this.getFileByFdOrThrow(fd, 'close');
  313. this.closeFile(file);
  314. };
  315. this.ftruncate = (fd, len) => {
  316. const file = this.getFileByFdOrThrow(fd, 'ftruncate');
  317. file.truncate(len);
  318. this.emit(new FsEvent_1.FsEvent(2 /* FsEventType.MODIFY */, file.link.steps, file.node, file.link));
  319. };
  320. this.fchmod = (fd, modeNum) => {
  321. const file = this.getFileByFdOrThrow(fd, 'fchmod');
  322. file.chmod(modeNum);
  323. this.emit(new FsEvent_1.FsEvent(3 /* FsEventType.ATTRIB */, file.link.steps, file.node, file.link));
  324. };
  325. this.chmod = (filename, modeNum) => {
  326. const link = this.getResolvedLinkOrThrow(filename, 'chmod');
  327. link.getNode().chmod(modeNum);
  328. this.emit(new FsEvent_1.FsEvent(3 /* FsEventType.ATTRIB */, link.steps, link.getNode(), link));
  329. };
  330. this.lchmod = (filename, modeNum) => {
  331. const link = this.getLinkOrThrow(filename, 'lchmod');
  332. link.getNode().chmod(modeNum);
  333. this.emit(new FsEvent_1.FsEvent(3 /* FsEventType.ATTRIB */, link.steps, link.getNode(), link));
  334. };
  335. this.fchown = (fd, uid, gid) => {
  336. const file = this.getFileByFdOrThrow(fd, 'fchown');
  337. file.chown(uid, gid);
  338. this.emit(new FsEvent_1.FsEvent(3 /* FsEventType.ATTRIB */, file.link.steps, file.node, file.link));
  339. };
  340. this.chown = (filename, uid, gid) => {
  341. const link = this.getResolvedLinkOrThrow(filename, 'chown');
  342. link.getNode().chown(uid, gid);
  343. this.emit(new FsEvent_1.FsEvent(3 /* FsEventType.ATTRIB */, link.steps, link.getNode(), link));
  344. };
  345. this.lchown = (filename, uid, gid) => {
  346. const link = this.getLinkOrThrow(filename, 'lchown');
  347. link.getNode().chown(uid, gid);
  348. this.emit(new FsEvent_1.FsEvent(3 /* FsEventType.ATTRIB */, link.steps, link.getNode(), link));
  349. };
  350. this.futimes = (fd, atime, mtime) => {
  351. const file = this.getFileByFdOrThrow(fd, 'futimes');
  352. const node = file.node;
  353. node.atime = new Date(atime * 1000);
  354. node.mtime = new Date(mtime * 1000);
  355. this.emit(new FsEvent_1.FsEvent(3 /* FsEventType.ATTRIB */, file.link.steps, file.node, file.link));
  356. };
  357. this.utimes = (filename, atime, mtime, followSymlinks = true) => {
  358. const link = followSymlinks
  359. ? this.getResolvedLinkOrThrow(filename, 'utimes')
  360. : this.getLinkOrThrow(filename, 'lutimes');
  361. const node = link.getNode();
  362. node.atime = new Date(atime * 1000);
  363. node.mtime = new Date(mtime * 1000);
  364. this.emit(new FsEvent_1.FsEvent(3 /* FsEventType.ATTRIB */, link.steps, node, link));
  365. };
  366. this.process = opts.process ?? process_1.default;
  367. const root = this.createLink();
  368. root.setNode(this.createNode(fs_node_utils_1.constants.S_IFDIR | 0o777));
  369. root.setChild('.', root);
  370. root.getNode().nlink++;
  371. root.setChild('..', root);
  372. root.getNode().nlink++;
  373. this.root = root;
  374. }
  375. emit(change) {
  376. this.changes.emit(change);
  377. }
  378. createLink(parent, name, isDirectory = false, mode) {
  379. if (!parent) {
  380. return new Link_1.Link(this, void 0, '');
  381. }
  382. if (!name) {
  383. throw new Error('createLink: name cannot be empty');
  384. }
  385. // If no explicit permission is provided, use defaults based on type
  386. const finalPerm = mode ?? (isDirectory ? 0o777 : 0o666);
  387. // To prevent making a breaking change, `mode` can also just be a permission number
  388. // and the file type is set based on `isDirectory`
  389. const hasFileType = mode && mode & fs_node_utils_1.constants.S_IFMT;
  390. const modeType = hasFileType ? mode & fs_node_utils_1.constants.S_IFMT : isDirectory ? fs_node_utils_1.constants.S_IFDIR : fs_node_utils_1.constants.S_IFREG;
  391. const finalMode = (finalPerm & ~fs_node_utils_1.constants.S_IFMT) | modeType;
  392. return parent.createChild(name, this.createNode(finalMode));
  393. }
  394. deleteLink(link) {
  395. const parent = link.parent;
  396. if (parent) {
  397. parent.deleteChild(link);
  398. return true;
  399. }
  400. return false;
  401. }
  402. _emitDeleteRecursive(link) {
  403. if (link.getNode().isDirectory()) {
  404. for (const [name, child] of link.children.entries()) {
  405. if (child && name !== '.' && name !== '..') {
  406. this._emitDeleteRecursive(child);
  407. }
  408. }
  409. }
  410. this.emit(new FsEvent_1.FsEvent(1 /* FsEventType.DELETE */, link.steps, link.getNode(), link));
  411. }
  412. newInoNumber() {
  413. const releasedFd = this.releasedInos.pop();
  414. if (releasedFd)
  415. return releasedFd;
  416. else {
  417. this.ino = (this.ino + 1) % 0xffffffff;
  418. return this.ino;
  419. }
  420. }
  421. newFdNumber() {
  422. const releasedFd = this.releasedFds.pop();
  423. return typeof releasedFd === 'number' ? releasedFd : Superblock.fd--;
  424. }
  425. createNode(mode) {
  426. const uid = this.process.getuid?.() ?? 0;
  427. const gid = this.process.getgid?.() ?? 0;
  428. const node = new Node_1.Node(this.newInoNumber(), mode, uid, gid);
  429. this.inodes[node.ino] = node;
  430. return node;
  431. }
  432. deleteNode(node) {
  433. node.del();
  434. delete this.inodes[node.ino];
  435. this.releasedInos.push(node.ino);
  436. }
  437. walk(stepsOrFilenameOrLink, resolveSymlinks = false, checkExistence = false, checkAccess = false, funcName) {
  438. let steps;
  439. let filename;
  440. if (stepsOrFilenameOrLink instanceof Link_1.Link) {
  441. steps = stepsOrFilenameOrLink.steps;
  442. filename = pathSep + steps.join(pathSep);
  443. }
  444. else if (typeof stepsOrFilenameOrLink === 'string') {
  445. steps = (0, util_1.filenameToSteps)(stepsOrFilenameOrLink);
  446. filename = stepsOrFilenameOrLink;
  447. }
  448. else {
  449. steps = stepsOrFilenameOrLink;
  450. filename = pathSep + steps.join(pathSep);
  451. }
  452. let curr = this.root;
  453. let i = 0;
  454. const uid = this.process.getuid?.() ?? 0;
  455. const gid = this.process.getgid?.() ?? 0;
  456. while (i < steps.length) {
  457. let node = curr.getNode();
  458. // Check access permissions if current link is a directory
  459. if (node.isDirectory()) {
  460. if (checkAccess && !node.canExecute(uid, gid)) {
  461. return (0, result_1.Err)((0, util_1.createStatError)("EACCES" /* ERROR_CODE.EACCES */, funcName, filename));
  462. }
  463. }
  464. else {
  465. if (i < steps.length - 1) {
  466. return (0, result_1.Err)((0, util_1.createStatError)("ENOTDIR" /* ERROR_CODE.ENOTDIR */, funcName, filename));
  467. }
  468. }
  469. curr = curr.getChild(steps[i]) ?? null;
  470. // Check existence of current link
  471. if (!curr)
  472. if (checkExistence) {
  473. return (0, result_1.Err)((0, util_1.createStatError)("ENOENT" /* ERROR_CODE.ENOENT */, funcName, filename));
  474. }
  475. else {
  476. return (0, result_1.Ok)(null);
  477. }
  478. node = curr?.getNode();
  479. // Resolve symlink if we're resolving all symlinks OR if this is an intermediate path component
  480. // This allows lstat to traverse through symlinks in intermediate directories while not resolving the final component
  481. if (node.isSymlink() && (resolveSymlinks || i < steps.length - 1)) {
  482. const resolvedPath = (0, path_1.isAbsolute)(node.symlink) ? node.symlink : pathJoin((0, path_1.dirname)(curr.getPath()), node.symlink); // Relative to symlink's parent
  483. steps = (0, util_1.filenameToSteps)(resolvedPath).concat(steps.slice(i + 1));
  484. curr = this.root;
  485. i = 0;
  486. continue;
  487. }
  488. // After resolving symlinks, check if it's not a directory and we still have more steps
  489. // This handles the case where we try to traverse through a file
  490. // Only do this check when we're doing filesystem operations (checkExistence = true)
  491. if (checkExistence && !node.isDirectory() && i < steps.length - 1) {
  492. // On Windows, use ENOENT for consistency with Node.js behavior
  493. // On other platforms, use ENOTDIR which is more semantically correct
  494. const errorCode = this.process.platform === 'win32' ? "ENOENT" /* ERROR_CODE.ENOENT */ : "ENOTDIR" /* ERROR_CODE.ENOTDIR */;
  495. return (0, result_1.Err)((0, util_1.createStatError)(errorCode, funcName, filename));
  496. }
  497. i++;
  498. }
  499. return (0, result_1.Ok)(curr);
  500. }
  501. // Returns a `Link` (hard link) referenced by path "split" into steps.
  502. getLink(steps) {
  503. const result = this.walk(steps, false, false, false);
  504. if (result.ok) {
  505. return result.value;
  506. }
  507. throw result.err.toError();
  508. }
  509. // Just link `getLink`, but throws a correct user error, if link to found.
  510. getLinkOrThrow(filename, funcName) {
  511. const result = this.walk(filename, false, true, true, funcName);
  512. if (result.ok) {
  513. return result.value;
  514. }
  515. throw result.err.toError();
  516. }
  517. // Just like `getLink`, but also dereference/resolves symbolic links.
  518. getResolvedLink(filenameOrSteps) {
  519. const result = this.walk(filenameOrSteps, true, false, false);
  520. if (result.ok) {
  521. return result.value;
  522. }
  523. throw result.err.toError();
  524. }
  525. /**
  526. * Just like `getLinkOrThrow`, but also dereference/resolves symbolic links.
  527. */
  528. getResolvedLinkOrThrow(filename, funcName) {
  529. const result = this.walk(filename, true, true, true, funcName);
  530. if (result.ok) {
  531. return result.value;
  532. }
  533. throw result.err.toError();
  534. }
  535. getResolvedLinkResult(filename, funcName) {
  536. const result = this.walk(filename, true, true, true, funcName);
  537. if (result.ok) {
  538. return (0, result_1.Ok)(result.value);
  539. }
  540. return result;
  541. }
  542. resolveSymlinks(link) {
  543. return this.getResolvedLink(link.steps.slice(1));
  544. }
  545. /**
  546. * Just like `getLinkOrThrow`, but also verifies that the link is a directory.
  547. */
  548. getLinkAsDirOrThrow(filename, funcName) {
  549. const link = this.getLinkOrThrow(filename, funcName);
  550. if (!link.getNode().isDirectory())
  551. throw (0, util_1.createError)("ENOTDIR" /* ERROR_CODE.ENOTDIR */, funcName, filename);
  552. return link;
  553. }
  554. // Get the immediate parent directory of the link.
  555. getLinkParent(steps) {
  556. return this.getLink(steps.slice(0, -1));
  557. }
  558. getLinkParentAsDirOrThrow(filenameOrSteps, funcName) {
  559. const steps = (filenameOrSteps instanceof Array ? filenameOrSteps : (0, util_1.filenameToSteps)(filenameOrSteps)).slice(0, -1);
  560. const filename = pathSep + steps.join(pathSep);
  561. const link = this.getLinkOrThrow(filename, funcName);
  562. if (!link.getNode().isDirectory())
  563. throw (0, util_1.createError)("ENOTDIR" /* ERROR_CODE.ENOTDIR */, funcName, filename);
  564. return link;
  565. }
  566. getFileByFd(fd) {
  567. return this.fds[String(fd)];
  568. }
  569. getFileByFdOrThrow(fd, funcName) {
  570. if (!(0, util_1.isFd)(fd))
  571. throw TypeError(fs_node_utils_2.ERRSTR.FD);
  572. const file = this.getFileByFd(fd);
  573. if (!file)
  574. throw (0, util_1.createError)("EBADF" /* ERROR_CODE.EBADF */, funcName);
  575. return file;
  576. }
  577. _toJSON(link = this.root, json = {}, path, asBuffer) {
  578. let isEmpty = true;
  579. let children = link.children;
  580. if (link.getNode().isFile()) {
  581. children = new Map([[link.getName(), link.parent.getChild(link.getName())]]);
  582. link = link.parent;
  583. }
  584. for (const name of children.keys()) {
  585. if (name === '.' || name === '..') {
  586. continue;
  587. }
  588. isEmpty = false;
  589. const child = link.getChild(name);
  590. if (!child) {
  591. throw new Error('_toJSON: unexpected undefined');
  592. }
  593. const node = child.getNode();
  594. if (node.isFile()) {
  595. let filename = child.getPath();
  596. if (path)
  597. filename = pathRelative(path, filename);
  598. json[filename] = asBuffer ? node.getBuffer() : node.getString();
  599. }
  600. else if (node.isDirectory()) {
  601. this._toJSON(child, json, path, asBuffer);
  602. }
  603. }
  604. let dirPath = link.getPath();
  605. if (path)
  606. dirPath = pathRelative(path, dirPath);
  607. if (dirPath && isEmpty) {
  608. json[dirPath] = null;
  609. }
  610. return json;
  611. }
  612. toJSON(paths, json = {}, isRelative = false, asBuffer = false) {
  613. const links = [];
  614. if (paths) {
  615. if (!Array.isArray(paths))
  616. paths = [paths];
  617. for (const path of paths) {
  618. const filename = (0, util_1.pathToFilename)(path);
  619. const link = this.getResolvedLink(filename);
  620. if (!link)
  621. continue;
  622. links.push(link);
  623. }
  624. }
  625. else {
  626. links.push(this.root);
  627. }
  628. if (!links.length)
  629. return json;
  630. for (const link of links)
  631. this._toJSON(link, json, isRelative ? link.getPath() : '', asBuffer);
  632. return json;
  633. }
  634. fromJSON(json, cwd = this.process.cwd()) {
  635. for (let filename in json) {
  636. const data = json[filename];
  637. filename = (0, util_1.resolve)(filename, cwd);
  638. if (typeof data === 'string' || data instanceof buffer_1.Buffer) {
  639. const dir = (0, path_1.dirname)(filename);
  640. this.mkdirp(dir, 511 /* MODE.DIR */);
  641. const buffer = (0, util_1.dataToBuffer)(data);
  642. this.writeFile(filename, buffer, fs_node_utils_2.FLAGS.w, 438 /* MODE.DEFAULT */);
  643. }
  644. else {
  645. this.mkdirp(filename, 511 /* MODE.DIR */);
  646. }
  647. }
  648. }
  649. fromNestedJSON(json, cwd) {
  650. this.fromJSON((0, json_1.flattenJSON)(json), cwd);
  651. }
  652. reset() {
  653. this.ino = 0;
  654. this.inodes = {};
  655. this.releasedInos = [];
  656. this.fds = {};
  657. this.releasedFds = [];
  658. this.openFiles = 0;
  659. this.root = this.createLink();
  660. this.root.setNode(this.createNode(fs_node_utils_1.constants.S_IFDIR | 0o777));
  661. }
  662. // Legacy interface
  663. mountSync(mountpoint, json) {
  664. this.fromJSON(json, mountpoint);
  665. }
  666. openLink(link, flagsNum, resolveSymlinks = true) {
  667. if (this.openFiles >= this.maxFiles) {
  668. // Too many open files.
  669. throw (0, util_1.createError)("EMFILE" /* ERROR_CODE.EMFILE */, 'open', link.getPath());
  670. }
  671. // Resolve symlinks.
  672. //
  673. // @TODO: This should be superfluous. This method is only ever called by openFile(), which does its own symlink resolution
  674. // prior to calling.
  675. let realLink = link;
  676. if (resolveSymlinks)
  677. realLink = this.getResolvedLinkOrThrow(link.getPath(), 'open');
  678. const node = realLink.getNode();
  679. // Check whether node is a directory
  680. if (node.isDirectory()) {
  681. if ((flagsNum & (O_RDONLY | O_RDWR | O_WRONLY)) !== O_RDONLY)
  682. throw (0, util_1.createError)("EISDIR" /* ERROR_CODE.EISDIR */, 'open', link.getPath());
  683. }
  684. else {
  685. if (flagsNum & O_DIRECTORY)
  686. throw (0, util_1.createError)("ENOTDIR" /* ERROR_CODE.ENOTDIR */, 'open', link.getPath());
  687. }
  688. // Check node permissions
  689. // For read access: check if flags are O_RDONLY or O_RDWR (i.e., not only O_WRONLY)
  690. if ((flagsNum & (O_RDONLY | O_RDWR | O_WRONLY)) !== O_WRONLY) {
  691. if (!node.canRead()) {
  692. throw (0, util_1.createError)("EACCES" /* ERROR_CODE.EACCES */, 'open', link.getPath());
  693. }
  694. }
  695. // For write access: check if flags are O_WRONLY or O_RDWR
  696. if (flagsNum & (O_WRONLY | O_RDWR)) {
  697. if (!node.canWrite()) {
  698. throw (0, util_1.createError)("EACCES" /* ERROR_CODE.EACCES */, 'open', link.getPath());
  699. }
  700. }
  701. const file = new File_1.File(link, node, flagsNum, this.newFdNumber());
  702. this.fds[file.fd] = file;
  703. this.openFiles++;
  704. if (flagsNum & O_TRUNC) {
  705. const hadContent = file.node.getSize() > 0;
  706. file.truncate();
  707. if (hadContent)
  708. this.emit(new FsEvent_1.FsEvent(2 /* FsEventType.MODIFY */, file.link.steps, file.node, file.link));
  709. }
  710. return file;
  711. }
  712. openFile(filename, flagsNum, modeNum, resolveSymlinks = true) {
  713. const steps = (0, util_1.filenameToSteps)(filename);
  714. let link;
  715. try {
  716. link = resolveSymlinks ? this.getResolvedLinkOrThrow(filename, 'open') : this.getLinkOrThrow(filename, 'open');
  717. // Check if file already existed when trying to create it exclusively (O_CREAT and O_EXCL flags are set).
  718. // This is an error, see https://pubs.opengroup.org/onlinepubs/009695399/functions/open.html:
  719. // "If O_CREAT and O_EXCL are set, open() shall fail if the file exists."
  720. if (link && flagsNum & O_CREAT && flagsNum & O_EXCL)
  721. throw (0, util_1.createError)("EEXIST" /* ERROR_CODE.EEXIST */, 'open', filename);
  722. }
  723. catch (err) {
  724. // Try creating a new file, if it does not exist and O_CREAT flag is set.
  725. // Note that this will still throw if the ENOENT came from one of the
  726. // intermediate directories instead of the file itself.
  727. if (err.code === "ENOENT" /* ERROR_CODE.ENOENT */ && flagsNum & O_CREAT) {
  728. const dirName = (0, path_1.dirname)(filename);
  729. const dirLink = this.getResolvedLinkOrThrow(dirName);
  730. const dirNode = dirLink.getNode();
  731. // Check that the place we create the new file is actually a directory and that we are allowed to do so:
  732. if (!dirNode.isDirectory())
  733. throw (0, util_1.createError)("ENOTDIR" /* ERROR_CODE.ENOTDIR */, 'open', filename);
  734. if (!dirNode.canExecute() || !dirNode.canWrite())
  735. throw (0, util_1.createError)("EACCES" /* ERROR_CODE.EACCES */, 'open', filename);
  736. // This is a difference to the original implementation, which would simply not create a file unless modeNum was specified.
  737. // However, current Node versions will default to 0o666.
  738. modeNum ?? (modeNum = 0o666);
  739. link = this.createLink(dirLink, steps[steps.length - 1], false, modeNum);
  740. this.emit(new FsEvent_1.FsEvent(0 /* FsEventType.CREATE */, link.steps, link.getNode(), link));
  741. }
  742. else
  743. throw err;
  744. }
  745. if (link)
  746. return this.openLink(link, flagsNum, resolveSymlinks);
  747. throw (0, util_1.createError)("ENOENT" /* ERROR_CODE.ENOENT */, 'open', filename);
  748. }
  749. closeFile(file) {
  750. if (!this.fds[file.fd])
  751. return;
  752. this.openFiles--;
  753. delete this.fds[file.fd];
  754. this.releasedFds.push(file.fd);
  755. }
  756. write(fd, buf, offset, length, position) {
  757. const file = this.getFileByFdOrThrow(fd, 'write');
  758. if (file.node.isSymlink()) {
  759. throw (0, util_1.createError)("EBADF" /* ERROR_CODE.EBADF */, 'write', file.link.getPath());
  760. }
  761. const bytes = file.write(buf, offset, length, position === -1 || typeof position !== 'number' ? undefined : position);
  762. if (bytes > 0)
  763. this.emit(new FsEvent_1.FsEvent(2 /* FsEventType.MODIFY */, file.link.steps, file.node, file.link));
  764. return bytes;
  765. }
  766. }
  767. exports.Superblock = Superblock;
  768. /**
  769. * Global file descriptor counter. UNIX file descriptors start from 0 and go sequentially
  770. * up, so here, in order not to conflict with them, we choose some big number and descrease
  771. * the file descriptor of every new opened file.
  772. * @type {number}
  773. * @todo This should not be static, right?
  774. */
  775. Superblock.fd = 0x7fffffff;
  776. //# sourceMappingURL=Superblock.js.map