Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ project adheres to [Semantic Versioning](https://semver.org/).
- Playwright setup note in the Quick start section of `README.md`.

### Fixed
- Bootstrap and provenance now refuse a `deps` directory that is a symlink.
Checkout and later reads followed the link, so a planted `deps` link was
accepted as the component root.
- Saved-case GUI paths with malformed percent-encoding now return HTTP 400.
`decodeURIComponent` used to throw, and the request became HTTP 500.
- The workbench comparison panel now says the comparison is unavailable when
Expand Down
2 changes: 2 additions & 0 deletions bin/aas.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { dirname, isAbsolute, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { isDeepStrictEqual } from "node:util";
import {
assertDependencyDirectory,
assertFullStackNodeVersion,
inspectDependencyDirectory,
loadComponentLock,
Expand Down Expand Up @@ -567,6 +568,7 @@ export function resolveComponentProvenance(
depsDir = DEFAULT_PATHS.deps,
lockPath = DEFAULT_PATHS.lock,
) {
assertDependencyDirectory(depsDir);
const components = loadComponentLock(lockPath);
return components.map((component) => {
const target = join(depsDir, component.name);
Expand Down
18 changes: 18 additions & 0 deletions scripts/bootstrap.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ export function assertFullStackNodeVersion({ version = process.versions.node } =
return parsed;
}

/**
* The dependency root must be a real directory. A symlink is followed by
* checkout and provenance reads, so bootstrap and demo would use another tree.
*/
export function assertDependencyDirectory(depsDir) {
let stat;
try {
stat = lstatSync(depsDir);
} catch (error) {
if (error?.code === "ENOENT") return;
throw error;
}
if (stat.isSymbolicLink() || !stat.isDirectory()) {
throw new Error("Dependency directory must be a regular directory.");
}
}

function normalizeRemote(value) {
return value.trim().replace(/\.git$/, "").replace(/\/$/, "").toLowerCase();
}
Expand Down Expand Up @@ -200,6 +217,7 @@ function runNpm(target, args) {
export function prepareDependencies({ root: projectRoot = root, deps = join(projectRoot, "deps"), components = loadComponentLock(join(projectRoot, "stack-lock.json")), nodeVersion } = {}) {
assertFullStackNodeVersion(nodeVersion === undefined ? {} : { version: nodeVersion });
mkdirSync(deps, { recursive: true });
assertDependencyDirectory(deps);
const prepared = [];
for (const component of components) {
const target = join(deps, component.name);
Expand Down
19 changes: 19 additions & 0 deletions test/stack.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,25 @@ test("pre-existing dependency symlinks are rejected", (t) => {
);
});

test("a symlinked dependency directory is not used as the checkout root", (t) => {
const outside = tempRoot();
const marker = join(outside, "keep.txt");
writeFileSync(marker, "keep");
const project = tempRoot();
const link = join(project, "deps");
if (!linkOrSkip(t, outside, link)) return;
assert.throws(
() => prepareDependencies({ root: project, deps: link, components: [] }),
/regular directory/,
);
assert.throws(
() => resolveComponentProvenance(link, LOCK),
/regular directory/,
);
assert.equal(readFileSync(marker, "utf8"), "keep");
assert.equal(existsSync(join(outside, ".git")), false);
});

test("missing or non-Git pre-existing directories fail closed", () => {
const component = loadComponentLock(LOCK)[1];
const target = join(tempRoot(), component.name);
Expand Down
Loading