Skip to content

Commit 1e73594

Browse files
committed
fix(@angular-devkit/schematics): handle non-existent paths in workspace-root containment
The previous WorkspaceRootHost resolved the workspace root with realpathSync(getSystemPath(root)) in the constructor, which throws ENOENT when the root directory does not exist yet — e.g. during `ng new`, which creates the workspace — crashing the workflow. Extract a resolveRealPath helper that walks up to the first existing ancestor, resolves its real path, and re-appends the remaining non-existent segments. Use it for both the workspace root and the asserted target path, so containment works for not-yet-created files and a not-yet-created root while still rejecting symlink escapes.
1 parent 33e009c commit 1e73594

1 file changed

Lines changed: 31 additions & 20 deletions

File tree

packages/angular_devkit/schematics/tools/workflow/node-workflow.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { Path, getSystemPath, normalize, schema, virtualFs } from '@angular-devkit/core';
1010
import { NodeJsSyncHost } from '@angular-devkit/core/node';
1111
import { realpathSync } from 'node:fs';
12-
import { dirname, isAbsolute, relative, resolve as resolveSystemPath, sep } from 'node:path';
12+
import { basename, dirname, isAbsolute, relative, resolve as resolveSystemPath, sep } from 'node:path';
1313
import { Observable } from 'rxjs';
1414
import { workflow } from '../../src';
1515
import { BuiltinTaskExecutor } from '../../tasks/node';
@@ -31,6 +31,34 @@ export interface NodeWorkflowOptions {
3131
engineHostCreator?: (options: NodeWorkflowOptions) => NodeModulesEngineHost;
3232
}
3333

34+
/**
35+
* Resolves the real path of a system path, walking up to the first existing ancestor if the path or
36+
* its descendants do not exist, and preserving the non-existent trailing segments. This keeps the
37+
* containment check working for not-yet-created files and for a workspace root that does not exist
38+
* yet (e.g. during `ng new`), where `realpathSync` would otherwise throw `ENOENT`.
39+
*/
40+
function resolveRealPath(systemPath: string): string {
41+
let current = resolveSystemPath(systemPath);
42+
const segments: string[] = [];
43+
for (;;) {
44+
try {
45+
const real = realpathSync(current);
46+
47+
return resolveSystemPath(real, ...segments.reverse());
48+
} catch (e) {
49+
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
50+
throw e;
51+
}
52+
const parent = dirname(current);
53+
if (parent === current) {
54+
throw e;
55+
}
56+
segments.push(basename(current));
57+
current = parent;
58+
}
59+
}
60+
}
61+
3462
/**
3563
* A {@link virtualFs.ScopedHost} that additionally rejects any write/delete/rename whose real
3664
* (symlink-resolved) location escapes the workspace root.
@@ -45,28 +73,11 @@ class WorkspaceRootHost<T extends object> extends virtualFs.ScopedHost<T> {
4573

4674
constructor(delegate: virtualFs.Host<T>, root: Path) {
4775
super(delegate, root);
48-
this._systemRoot = realpathSync(getSystemPath(root));
76+
this._systemRoot = resolveRealPath(getSystemPath(root));
4977
}
5078

5179
private _assertWithinRoot(path: Path): void {
52-
// Resolve the real path, walking up to the first existing ancestor for not-yet-created files.
53-
let current = resolveSystemPath(getSystemPath(this._resolve(path)));
54-
let real: string;
55-
for (;;) {
56-
try {
57-
real = realpathSync(current);
58-
break;
59-
} catch (e) {
60-
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
61-
throw e;
62-
}
63-
const parent = dirname(current);
64-
if (parent === current) {
65-
throw e;
66-
}
67-
current = parent;
68-
}
69-
}
80+
const real = resolveRealPath(getSystemPath(this._resolve(path)));
7081

7182
const rel = relative(this._systemRoot, real);
7283
if (rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel)) {

0 commit comments

Comments
 (0)