@markup-carve/carve-lsp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/analyze.d.ts +17 -1
- package/dist/analyze.js +57 -1
- package/dist/analyze.js.map +1 -1
- package/dist/analyze.test.js +95 -0
- package/dist/analyze.test.js.map +1 -1
- package/dist/definition.js +26 -2
- package/dist/definition.js.map +1 -1
- package/dist/definition.test.js +8 -0
- package/dist/definition.test.js.map +1 -1
- package/dist/folding.js +4 -4
- package/dist/folding.js.map +1 -1
- package/dist/hover.js +33 -16
- package/dist/hover.js.map +1 -1
- package/dist/include-directive.d.ts +61 -0
- package/dist/include-directive.js +158 -0
- package/dist/include-directive.js.map +1 -0
- package/dist/include-directive.test.d.ts +1 -0
- package/dist/include-directive.test.js +83 -0
- package/dist/include-directive.test.js.map +1 -0
- package/dist/include-path.d.ts +63 -0
- package/dist/include-path.js +135 -0
- package/dist/include-path.js.map +1 -0
- package/dist/include-path.test.d.ts +1 -0
- package/dist/include-path.test.js +189 -0
- package/dist/include-path.test.js.map +1 -0
- package/dist/include-security-conformance.test.d.ts +1 -0
- package/dist/include-security-conformance.test.js +100 -0
- package/dist/include-security-conformance.test.js.map +1 -0
- package/dist/include-settings.d.ts +50 -0
- package/dist/include-settings.js +135 -0
- package/dist/include-settings.js.map +1 -0
- package/dist/include-settings.test.d.ts +1 -0
- package/dist/include-settings.test.js +191 -0
- package/dist/include-settings.test.js.map +1 -0
- package/dist/includes.d.ts +78 -0
- package/dist/includes.js +158 -0
- package/dist/includes.js.map +1 -0
- package/dist/includes.test.d.ts +1 -0
- package/dist/includes.test.js +202 -0
- package/dist/includes.test.js.map +1 -0
- package/dist/inline-text.d.ts +24 -0
- package/dist/inline-text.js +34 -0
- package/dist/inline-text.js.map +1 -0
- package/dist/inline-text.test.d.ts +1 -0
- package/dist/inline-text.test.js +32 -0
- package/dist/inline-text.test.js.map +1 -0
- package/dist/position-encoding.test.d.ts +1 -0
- package/dist/position-encoding.test.js +143 -0
- package/dist/position-encoding.test.js.map +1 -0
- package/dist/position.d.ts +82 -0
- package/dist/position.js +137 -0
- package/dist/position.js.map +1 -0
- package/dist/position.test.d.ts +1 -0
- package/dist/position.test.js +61 -0
- package/dist/position.test.js.map +1 -0
- package/dist/references.js +7 -0
- package/dist/references.js.map +1 -1
- package/dist/semantic.d.ts +14 -0
- package/dist/semantic.js +113 -27
- package/dist/semantic.js.map +1 -1
- package/dist/semantic.test.d.ts +1 -0
- package/dist/semantic.test.js +93 -0
- package/dist/semantic.test.js.map +1 -0
- package/dist/server.js +50 -23
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single place an include path becomes a file read.
|
|
3
|
+
*
|
|
4
|
+
* PART 9 §19 puts four MUSTs on this function, and they are here rather than
|
|
5
|
+
* spread over the callers on purpose: a guard applied on one path and missed on
|
|
6
|
+
* another is the defect this repository's siblings keep hitting. Nothing else
|
|
7
|
+
* in this package joins, canonicalizes or opens an include target.
|
|
8
|
+
*
|
|
9
|
+
* - MUST resolve include paths only to files under the configured project root
|
|
10
|
+
* AFTER symlink resolution (rejecting `..`-traversal and absolute paths that
|
|
11
|
+
* escape the root)
|
|
12
|
+
* - MUST NOT fetch remote URLs unless an allowlist is explicitly configured
|
|
13
|
+
*
|
|
14
|
+
* The remaining two MUSTs (recursion depth, total expanded byte size) are
|
|
15
|
+
* properties of the include GRAPH rather than of one path, so they live in
|
|
16
|
+
* {@link ./includes.js} where the walk is.
|
|
17
|
+
*/
|
|
18
|
+
import { closeSync, fstatSync, openSync, readSync, realpathSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
/** A scheme with an authority (`https://…`), or a protocol-relative `//host/…`. */
|
|
21
|
+
const REMOTE_RE = /^(?:[A-Za-z][A-Za-z0-9+.-]*:)?\/\//;
|
|
22
|
+
function remoteHost(spec) {
|
|
23
|
+
const withScheme = spec.startsWith('//') ? `https:${spec}` : spec;
|
|
24
|
+
try {
|
|
25
|
+
return new URL(withScheme).hostname;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Filesystem resolver with canonical root containment, for a workspace the
|
|
33
|
+
* client reports as trusted.
|
|
34
|
+
*
|
|
35
|
+
* Canonicalize-then-contain: the candidate is resolved to its real path
|
|
36
|
+
* (symlinks followed) and only then compared against the real root.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately NOT a lexical ban on `..`, which is both too strict and too
|
|
39
|
+
* weak. Too strict: `../shared/glossary.crv` from `chapters/ch1.crv` is a
|
|
40
|
+
* normal book layout whose canonical target is inside the root, and the
|
|
41
|
+
* conformance vector `i10-fs-dotdot-inside-root-allowed` requires it to
|
|
42
|
+
* resolve. Too weak: a symlink inside the root pointing out of it, or an
|
|
43
|
+
* absolute path, escapes without containing `..` at all
|
|
44
|
+
* (`i10-fs-symlink-dir-escape-denied`). Canonical containment subsumes both.
|
|
45
|
+
*/
|
|
46
|
+
export function fileSystemResolver(includeRoot, opts = {}) {
|
|
47
|
+
const rootReal = realpathSync(includeRoot);
|
|
48
|
+
const allowedRemoteHosts = opts.allowedRemoteHosts ?? [];
|
|
49
|
+
const contains = (candidate) => {
|
|
50
|
+
const rel = path.relative(rootReal, candidate);
|
|
51
|
+
if (rel === '')
|
|
52
|
+
return true;
|
|
53
|
+
if (!rel || path.isAbsolute(rel))
|
|
54
|
+
return false;
|
|
55
|
+
// Segment-wise, so a directory legitimately named "..foo" is not read as
|
|
56
|
+
// an escape the way a startsWith('..') prefix test would.
|
|
57
|
+
return rel.split(path.sep)[0] !== '..';
|
|
58
|
+
};
|
|
59
|
+
return (includePath, ctx) => {
|
|
60
|
+
// GUARD 1 (§19: MUST NOT fetch remote URLs unless an allowlist is
|
|
61
|
+
// explicitly configured). Checked before any path join, so a URL can never
|
|
62
|
+
// be mistaken for a relative path and read off disk as `<root>/https:/…`.
|
|
63
|
+
if (REMOTE_RE.test(includePath)) {
|
|
64
|
+
const host = remoteHost(includePath);
|
|
65
|
+
if (!allowedRemoteHosts.includes(host)) {
|
|
66
|
+
return { ok: false, id: includePath, denial: 'remote-not-allowed' };
|
|
67
|
+
}
|
|
68
|
+
// Allowlisted, and still refused: this server has no fetcher. Reported
|
|
69
|
+
// as a denial rather than silently, so the author sees why.
|
|
70
|
+
return { ok: false, id: includePath, denial: 'remote-not-allowed' };
|
|
71
|
+
}
|
|
72
|
+
// GUARD 2 (§19: absolute paths). Denied outright by default; when the host
|
|
73
|
+
// opts in, an absolute path still has to survive GUARD 3.
|
|
74
|
+
if (!opts.allowAbsolute && path.isAbsolute(includePath)) {
|
|
75
|
+
return { ok: false, id: includePath, denial: 'absolute-denied' };
|
|
76
|
+
}
|
|
77
|
+
// The stack carries the canonical path of each ancestor, so a nested
|
|
78
|
+
// relative include resolves against its actual parent directory, not the
|
|
79
|
+
// root.
|
|
80
|
+
const parent = ctx.stack[ctx.stack.length - 1];
|
|
81
|
+
const base = parent ? path.dirname(path.resolve(rootReal, parent)) : rootReal;
|
|
82
|
+
const joined = path.isAbsolute(includePath)
|
|
83
|
+
? includePath
|
|
84
|
+
: path.resolve(base, includePath);
|
|
85
|
+
let real;
|
|
86
|
+
try {
|
|
87
|
+
// Symlink resolution happens HERE, before containment, which is what
|
|
88
|
+
// makes a symlinked escape visible to GUARD 3 at all.
|
|
89
|
+
real = realpathSync(joined);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { ok: false, id: includePath, denial: 'not-found' };
|
|
93
|
+
}
|
|
94
|
+
// GUARD 3 (§19: only files under the configured project root AFTER symlink
|
|
95
|
+
// resolution). Catches `..` that leaves the root, a symlink that points out
|
|
96
|
+
// of it, and an absolute path outside it.
|
|
97
|
+
if (!contains(real)) {
|
|
98
|
+
return { ok: false, id: includePath, denial: 'outside-root' };
|
|
99
|
+
}
|
|
100
|
+
let fd;
|
|
101
|
+
try {
|
|
102
|
+
fd = openSync(real, 'r');
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return { ok: false, id: includePath, denial: 'not-found' };
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const stat = fstatSync(fd);
|
|
109
|
+
// Regular files only. A FIFO or a character device would block the read
|
|
110
|
+
// or stream without end, and neither is a document.
|
|
111
|
+
if (!stat.isFile())
|
|
112
|
+
return { ok: false, id: real, denial: 'not-a-file' };
|
|
113
|
+
const buffer = Buffer.allocUnsafe(stat.size);
|
|
114
|
+
let read = 0;
|
|
115
|
+
while (read < stat.size) {
|
|
116
|
+
const n = readSync(fd, buffer, read, stat.size - read, read);
|
|
117
|
+
if (n <= 0)
|
|
118
|
+
break;
|
|
119
|
+
read += n;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
id: real,
|
|
124
|
+
source: buffer.subarray(0, read).toString('utf8'),
|
|
125
|
+
// Encoded bytes, not JavaScript characters, so the budget counts what
|
|
126
|
+
// the include-bomb actually costs.
|
|
127
|
+
bytes: read,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
closeSync(fd);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=include-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"include-path.js","sourceRoot":"","sources":["../src/include-path.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileSystemResolver } from './include-path.js';
|
|
7
|
+
// PART 9 §19 puts four MUSTs on path resolution. Each has a test here, and
|
|
8
|
+
// each was proven load-bearing by deleting its guard in `include-path.ts` and
|
|
9
|
+
// watching THIS test go red (see the pull request for the transcript).
|
|
10
|
+
//
|
|
11
|
+
// Expectations follow the cross-engine include-conformance vectors
|
|
12
|
+
// `i10-fs-*` in markup-carve/carve, which currently live only on the unmerged
|
|
13
|
+
// spec branch for markup-carve/carve#291. They are cited, not vendored.
|
|
14
|
+
const CTX = { stack: [], depth: 0 };
|
|
15
|
+
function fixture(tree) {
|
|
16
|
+
const dir = realpathSync(mkdtempSync(path.join(tmpdir(), 'carve-lsp-inc-')));
|
|
17
|
+
for (const [name, value] of Object.entries(tree)) {
|
|
18
|
+
const full = path.join(dir, name);
|
|
19
|
+
mkdirSync(path.dirname(full), { recursive: true });
|
|
20
|
+
if (typeof value === 'string')
|
|
21
|
+
writeFileSync(full, value);
|
|
22
|
+
else
|
|
23
|
+
symlinkSync(path.join(dir, value.symlink), full);
|
|
24
|
+
}
|
|
25
|
+
test.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// MUST 1: resolve only under the root AFTER symlink resolution
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
test('a symlinked DIRECTORY escaping the root is denied (i10-fs-symlink-dir-escape-denied)', () => {
|
|
32
|
+
const dir = fixture({
|
|
33
|
+
'root/main.crv': '{{ linkdir/secret.crv }}\n',
|
|
34
|
+
'outside/secret.crv': 'TOP SECRET\n',
|
|
35
|
+
'root/linkdir': { symlink: 'outside' },
|
|
36
|
+
});
|
|
37
|
+
const resolve = fileSystemResolver(path.join(dir, 'root'));
|
|
38
|
+
const result = resolve('linkdir/secret.crv', CTX);
|
|
39
|
+
assert.equal(result.ok, false);
|
|
40
|
+
assert.equal(result.ok === false && result.denial, 'outside-root');
|
|
41
|
+
// The dependency id is the path as written, never the real path behind it.
|
|
42
|
+
assert.equal(result.id, 'linkdir/secret.crv');
|
|
43
|
+
});
|
|
44
|
+
test('a symlinked FILE whose real target escapes the root is denied (i10-fs-symlink-file-and-dotdot-denied)', () => {
|
|
45
|
+
const dir = fixture({
|
|
46
|
+
'root/main.crv': '{{ link.crv }}\n',
|
|
47
|
+
'secret.crv': 'TOP SECRET\n',
|
|
48
|
+
'root/link.crv': { symlink: 'secret.crv' },
|
|
49
|
+
});
|
|
50
|
+
const resolve = fileSystemResolver(path.join(dir, 'root'));
|
|
51
|
+
const result = resolve('link.crv', CTX);
|
|
52
|
+
assert.equal(result.ok, false);
|
|
53
|
+
assert.equal(result.ok === false && result.denial, 'outside-root');
|
|
54
|
+
});
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// MUST 2: reject `..`-traversal that leaves the root
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
test('a ../ escape out of the root is denied (i10-fs-dotdot-escape-denied)', () => {
|
|
59
|
+
const dir = fixture({
|
|
60
|
+
'root/main.crv': '{{ ../secret.crv }}\n',
|
|
61
|
+
'secret.crv': 'TOP SECRET\n',
|
|
62
|
+
});
|
|
63
|
+
const resolve = fileSystemResolver(path.join(dir, 'root'));
|
|
64
|
+
const result = resolve('../secret.crv', CTX);
|
|
65
|
+
assert.equal(result.ok, false);
|
|
66
|
+
assert.equal(result.ok === false && result.denial, 'outside-root');
|
|
67
|
+
});
|
|
68
|
+
test('a ../ path whose canonical target stays INSIDE the root resolves (i10-fs-dotdot-inside-root-allowed)', () => {
|
|
69
|
+
// Control for the test above: the guard is canonical containment, not a
|
|
70
|
+
// lexical ban on `..`. A book layout that reaches a sibling directory is
|
|
71
|
+
// legal and must keep working. No mutation of the containment check breaks
|
|
72
|
+
// this case - deleting the check makes it pass just as well - so it is
|
|
73
|
+
// stated here as the boundary the guard must NOT overreach, not as proof.
|
|
74
|
+
const dir = fixture({
|
|
75
|
+
'chapters/ch1.crv': 'Chapter one.\n',
|
|
76
|
+
'shared/glossary.crv': 'Glossary body.\n',
|
|
77
|
+
});
|
|
78
|
+
const resolve = fileSystemResolver(dir);
|
|
79
|
+
const result = resolve('../shared/glossary.crv', {
|
|
80
|
+
stack: [path.join(dir, 'chapters/ch1.crv')],
|
|
81
|
+
depth: 1,
|
|
82
|
+
});
|
|
83
|
+
assert.equal(result.ok, true);
|
|
84
|
+
assert.equal(result.ok === true && result.source, 'Glossary body.\n');
|
|
85
|
+
});
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// MUST 3: reject absolute paths that escape the root
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
test('an absolute path outside the root is denied even with allowAbsolute (i10-fs-absolute-path-denied)', () => {
|
|
90
|
+
// With absolute paths explicitly enabled, containment is the ONLY thing
|
|
91
|
+
// standing between the directive and the file, which is what makes this the
|
|
92
|
+
// test for the MUST rather than for the convenience default below.
|
|
93
|
+
const dir = fixture({
|
|
94
|
+
'root/main.crv': 'x\n',
|
|
95
|
+
'secret.crv': 'TOP SECRET\n',
|
|
96
|
+
});
|
|
97
|
+
const resolve = fileSystemResolver(path.join(dir, 'root'), { allowAbsolute: true });
|
|
98
|
+
const result = resolve(path.join(dir, 'secret.crv'), CTX);
|
|
99
|
+
assert.equal(result.ok, false);
|
|
100
|
+
assert.equal(result.ok === false && result.denial, 'outside-root');
|
|
101
|
+
});
|
|
102
|
+
test('an absolute path is denied by default, before containment is consulted', () => {
|
|
103
|
+
const dir = fixture({ 'root/main.crv': 'x\n', 'root/inside.crv': 'Inside.\n' });
|
|
104
|
+
const resolve = fileSystemResolver(path.join(dir, 'root'));
|
|
105
|
+
// Absolute AND inside the root: only the default-deny can reject this one,
|
|
106
|
+
// so it isolates that guard from containment.
|
|
107
|
+
const result = resolve(path.join(dir, 'root/inside.crv'), CTX);
|
|
108
|
+
assert.equal(result.ok, false);
|
|
109
|
+
assert.equal(result.ok === false && result.denial, 'absolute-denied');
|
|
110
|
+
});
|
|
111
|
+
test('an absolute path inside the root resolves once allowAbsolute is set', () => {
|
|
112
|
+
const dir = fixture({ 'root/inside.crv': 'Inside.\n' });
|
|
113
|
+
const resolve = fileSystemResolver(path.join(dir, 'root'), { allowAbsolute: true });
|
|
114
|
+
const result = resolve(path.join(dir, 'root/inside.crv'), CTX);
|
|
115
|
+
assert.equal(result.ok, true);
|
|
116
|
+
});
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// MUST 4: never fetch a remote URL without an explicit allowlist
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
test('a remote URL is denied with no allowlist configured, and never touches the disk', () => {
|
|
121
|
+
const dir = fixture({ 'main.crv': 'x\n' });
|
|
122
|
+
const resolve = fileSystemResolver(dir);
|
|
123
|
+
for (const url of [
|
|
124
|
+
'https://example.com/evil.crv',
|
|
125
|
+
'http://example.com/evil.crv',
|
|
126
|
+
'file://localhost/etc/passwd',
|
|
127
|
+
'//example.com/evil.crv',
|
|
128
|
+
]) {
|
|
129
|
+
const result = resolve(url, CTX);
|
|
130
|
+
assert.equal(result.ok, false, url);
|
|
131
|
+
assert.equal(result.ok === false && result.denial, 'remote-not-allowed', url);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
test('a remote URL is denied even when its host IS allowlisted, because nothing fetches', () => {
|
|
135
|
+
const dir = fixture({ 'main.crv': 'x\n' });
|
|
136
|
+
const resolve = fileSystemResolver(dir, { allowedRemoteHosts: ['example.com'] });
|
|
137
|
+
const result = resolve('https://example.com/ok.crv', CTX);
|
|
138
|
+
assert.equal(result.ok, false);
|
|
139
|
+
assert.equal(result.ok === false && result.denial, 'remote-not-allowed');
|
|
140
|
+
});
|
|
141
|
+
test('a URL-shaped path is not read off disk as a relative path', () => {
|
|
142
|
+
// Without the scheme check a URL falls through to path joining, and
|
|
143
|
+
// `<root>/https:/example.com/evil.crv` is a file an attacker can plant.
|
|
144
|
+
const dir = fixture({
|
|
145
|
+
'https:/example.com/evil.crv': 'PLANTED\n',
|
|
146
|
+
});
|
|
147
|
+
const resolve = fileSystemResolver(dir);
|
|
148
|
+
const result = resolve('https://example.com/evil.crv', CTX);
|
|
149
|
+
assert.equal(result.ok, false);
|
|
150
|
+
assert.equal(result.ok === false && result.denial, 'remote-not-allowed');
|
|
151
|
+
});
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Ordinary resolution and non-file targets
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
test('a missing target is reported as not-found, not as a containment denial', () => {
|
|
156
|
+
const dir = fixture({ 'main.crv': 'x\n' });
|
|
157
|
+
const resolve = fileSystemResolver(dir);
|
|
158
|
+
const result = resolve('absent.crv', CTX);
|
|
159
|
+
assert.equal(result.ok, false);
|
|
160
|
+
assert.equal(result.ok === false && result.denial, 'not-found');
|
|
161
|
+
});
|
|
162
|
+
test('a directory is not a document', () => {
|
|
163
|
+
const dir = fixture({ 'sub/child.crv': 'x\n' });
|
|
164
|
+
const resolve = fileSystemResolver(dir);
|
|
165
|
+
const result = resolve('sub', CTX);
|
|
166
|
+
assert.equal(result.ok, false);
|
|
167
|
+
assert.equal(result.ok === false && result.denial, 'not-a-file');
|
|
168
|
+
});
|
|
169
|
+
test('bytes are counted as encoded UTF-8, not as JavaScript characters', () => {
|
|
170
|
+
const dir = fixture({ 'child.crv': 'éé\u{1f600}' });
|
|
171
|
+
const resolve = fileSystemResolver(dir);
|
|
172
|
+
const result = resolve('child.crv', CTX);
|
|
173
|
+
assert.equal(result.ok, true);
|
|
174
|
+
assert.equal(result.ok === true && result.bytes, Buffer.byteLength('éé\u{1f600}', 'utf8'));
|
|
175
|
+
});
|
|
176
|
+
test('a nested relative include resolves against its parent directory, not the root', () => {
|
|
177
|
+
const dir = fixture({
|
|
178
|
+
'chapters/ch1.crv': 'Chapter one.\n',
|
|
179
|
+
'chapters/notes.crv': 'Notes.\n',
|
|
180
|
+
});
|
|
181
|
+
const resolve = fileSystemResolver(dir);
|
|
182
|
+
const result = resolve('notes.crv', {
|
|
183
|
+
stack: [path.join(dir, 'chapters/ch1.crv')],
|
|
184
|
+
depth: 1,
|
|
185
|
+
});
|
|
186
|
+
assert.equal(result.ok, true);
|
|
187
|
+
assert.equal(result.ok === true && result.source, 'Notes.\n');
|
|
188
|
+
});
|
|
189
|
+
//# sourceMappingURL=include-path.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"include-path.test.js","sourceRoot":"","sources":["../src/include-path.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { fileSystemResolver } from './include-path.js';
|
|
8
|
+
import { resolveIncludes } from './includes.js';
|
|
9
|
+
const sourceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const corpusPath = path.join(sourceRoot, 'tests/spec/tests/include-security-conformance/vectors.json');
|
|
11
|
+
const corpus = JSON.parse(readFileSync(corpusPath, 'utf8'));
|
|
12
|
+
function fixture(tree) {
|
|
13
|
+
const dir = realpathSync(mkdtempSync(path.join(tmpdir(), 'carve-lsp-security-')));
|
|
14
|
+
const links = [];
|
|
15
|
+
for (const [name, value] of Object.entries(tree ?? {})) {
|
|
16
|
+
const full = path.join(dir, name);
|
|
17
|
+
mkdirSync(path.dirname(full), { recursive: true });
|
|
18
|
+
if (typeof value === 'string')
|
|
19
|
+
writeFileSync(full, value);
|
|
20
|
+
else
|
|
21
|
+
links.push([full, value.symlink]);
|
|
22
|
+
}
|
|
23
|
+
for (const [full, target] of links)
|
|
24
|
+
symlinkSync(path.join(dir, target), full);
|
|
25
|
+
test.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
function graph(vector) {
|
|
29
|
+
const calls = [];
|
|
30
|
+
let maxVisitedDepth = 0;
|
|
31
|
+
const resolver = (request, context) => {
|
|
32
|
+
calls.push(request);
|
|
33
|
+
maxVisitedDepth = Math.max(maxVisitedDepth, context.depth + 1);
|
|
34
|
+
const source = vector.files?.[request];
|
|
35
|
+
return source === undefined
|
|
36
|
+
? { ok: false, id: request, denial: 'not-found' }
|
|
37
|
+
: { ok: true, id: request, source, bytes: Buffer.byteLength(source, 'utf8') };
|
|
38
|
+
};
|
|
39
|
+
const result = resolveIncludes(vector.entry ?? '', {
|
|
40
|
+
resolver,
|
|
41
|
+
maxDepth: vector.maxDepth,
|
|
42
|
+
maxBytes: vector.maxBytes,
|
|
43
|
+
});
|
|
44
|
+
const warning = result.warnings.find((item) => item.rule === 'include-depth' || item.rule === 'include-budget');
|
|
45
|
+
return {
|
|
46
|
+
resolverCalls: calls,
|
|
47
|
+
maxVisitedDepth,
|
|
48
|
+
chargedBytes: result.bytes,
|
|
49
|
+
status: warning ? 'denied' : 'allowed',
|
|
50
|
+
denial: warning?.rule === 'include-depth' ? 'depth' : warning ? 'budget' : undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function run(vector) {
|
|
54
|
+
if (vector.kind === 'activation') {
|
|
55
|
+
const calls = [];
|
|
56
|
+
const resolver = (request) => {
|
|
57
|
+
calls.push(request);
|
|
58
|
+
return { ok: false, id: request, denial: 'not-found' };
|
|
59
|
+
};
|
|
60
|
+
resolveIncludes(vector.entry ?? '', vector.enabled ? { resolver } : {});
|
|
61
|
+
return { resolverCalls: calls };
|
|
62
|
+
}
|
|
63
|
+
if (vector.kind === 'graph')
|
|
64
|
+
return graph(vector);
|
|
65
|
+
const dir = fixture(vector.tree ?? { 'root/main.crv': '' });
|
|
66
|
+
const root = realpathSync(path.join(dir, vector.root ?? 'root'));
|
|
67
|
+
const resolver = fileSystemResolver(root, {
|
|
68
|
+
allowAbsolute: vector.allowAbsolute,
|
|
69
|
+
allowedRemoteHosts: vector.allowedRemoteHosts,
|
|
70
|
+
});
|
|
71
|
+
const rawRequest = vector.request ?? '';
|
|
72
|
+
const request = rawRequest.replace(/^<ABS:([^>]+)>$/, (_match, rel) => path.join(dir, rel));
|
|
73
|
+
const context = vector.from
|
|
74
|
+
? { stack: [realpathSync(path.join(dir, vector.from))], depth: 0 }
|
|
75
|
+
: { stack: [], depth: 0 };
|
|
76
|
+
const result = resolver(request, context);
|
|
77
|
+
if (vector.kind === 'remote') {
|
|
78
|
+
return {
|
|
79
|
+
status: result.ok ? 'allowed' : vector.allowedRemoteHosts?.length ? 'unsupported' : 'denied',
|
|
80
|
+
denial: result.ok ? undefined : result.denial,
|
|
81
|
+
remoteFetches: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return result.ok
|
|
85
|
+
? { status: 'allowed', canonicalId: result.id.replace(root, '<ROOT>') }
|
|
86
|
+
: { status: 'denied', denial: result.denial };
|
|
87
|
+
}
|
|
88
|
+
test('executes every shared PART 9 section 19 security vector', async (t) => {
|
|
89
|
+
assert.equal(corpus.version, 1);
|
|
90
|
+
assert.equal(corpus.vectors.length, 12);
|
|
91
|
+
for (const vector of corpus.vectors) {
|
|
92
|
+
await t.test(vector.name, () => {
|
|
93
|
+
const actual = run(vector);
|
|
94
|
+
for (const [field, expected] of Object.entries(vector.expected)) {
|
|
95
|
+
assert.deepEqual(actual[field], expected, `${vector.name}: ${field}`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=include-security-conformance.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"include-security-conformance.test.js","sourceRoot":"","sources":["../src/include-security-conformance.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { IncludeOptions } from './includes.js';
|
|
2
|
+
export interface IncludeSettings {
|
|
3
|
+
/**
|
|
4
|
+
* `auto` (the default) means ON ONLY for a workspace the client has reported
|
|
5
|
+
* as trusted. A client that says nothing about trust therefore gets includes
|
|
6
|
+
* OFF: silence is not consent.
|
|
7
|
+
*/
|
|
8
|
+
enabled: 'auto' | 'on' | 'off';
|
|
9
|
+
/** Containment root override. Otherwise the workspace root, then the document's directory. */
|
|
10
|
+
includeRoot?: string;
|
|
11
|
+
/** Allow absolute include paths, still subject to root containment. */
|
|
12
|
+
allowAbsolute?: boolean;
|
|
13
|
+
/** Hosts a remote include may name. Empty means none; this server never fetches. */
|
|
14
|
+
allowedRemoteHosts?: string[];
|
|
15
|
+
maxDepth?: number;
|
|
16
|
+
maxBytes?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare const DEFAULT_INCLUDE_SETTINGS: IncludeSettings;
|
|
19
|
+
/**
|
|
20
|
+
* Read `carve.includes` out of `initializationOptions` or a
|
|
21
|
+
* `workspace/didChangeConfiguration` payload. Anything unrecognized falls back
|
|
22
|
+
* to the default rather than being trusted as given.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readIncludeSettings(raw: unknown): IncludeSettings;
|
|
25
|
+
/** Client-reported workspace trust. Absent means untrusted. */
|
|
26
|
+
export declare function readWorkspaceTrusted(raw: unknown): boolean;
|
|
27
|
+
export declare function fsPath(uri: string): string | undefined;
|
|
28
|
+
export interface IncludeGateInput {
|
|
29
|
+
uri: string;
|
|
30
|
+
settings: IncludeSettings;
|
|
31
|
+
workspaceTrusted: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Every workspace folder the client reported, in the order it reported them.
|
|
34
|
+
* A multi-root session gets one root per folder, not the first folder for
|
|
35
|
+
* everything: rooting a document from the second folder at the first would
|
|
36
|
+
* reject its ordinary relative includes as escapes.
|
|
37
|
+
*/
|
|
38
|
+
workspaceRoots?: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the include options for one document, or undefined to leave its
|
|
42
|
+
* directives literal.
|
|
43
|
+
*
|
|
44
|
+
* The containment root is the explicit override, else the workspace root, else
|
|
45
|
+
* the document's own directory for a file opened outside any workspace. It is
|
|
46
|
+
* NEVER the process working directory: a language server is commonly spawned
|
|
47
|
+
* from the user's home or from `/`, and rooting there would make containment
|
|
48
|
+
* meaningless.
|
|
49
|
+
*/
|
|
50
|
+
export declare function includeOptionsFor(input: IncludeGateInput): IncludeOptions | undefined;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-facing include settings and the decision of whether a document gets
|
|
3
|
+
* include resolution at all (PART 9 §19: "MUST treat includes as opt-in (off
|
|
4
|
+
* for untrusted input)").
|
|
5
|
+
*
|
|
6
|
+
* Kept out of `server.ts` so the opt-in decision is testable on its own. It is
|
|
7
|
+
* the gate in front of a file-read capability, which is not something to leave
|
|
8
|
+
* exercised only by hand.
|
|
9
|
+
*/
|
|
10
|
+
import { realpathSync } from 'node:fs';
|
|
11
|
+
import path, { dirname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { fileSystemResolver } from './include-path.js';
|
|
14
|
+
export const DEFAULT_INCLUDE_SETTINGS = { enabled: 'auto' };
|
|
15
|
+
/**
|
|
16
|
+
* Read `carve.includes` out of `initializationOptions` or a
|
|
17
|
+
* `workspace/didChangeConfiguration` payload. Anything unrecognized falls back
|
|
18
|
+
* to the default rather than being trusted as given.
|
|
19
|
+
*/
|
|
20
|
+
export function readIncludeSettings(raw) {
|
|
21
|
+
const source = raw?.carve
|
|
22
|
+
?.includes;
|
|
23
|
+
if (!source || typeof source !== 'object')
|
|
24
|
+
return DEFAULT_INCLUDE_SETTINGS;
|
|
25
|
+
const enabled = source['enabled'];
|
|
26
|
+
const settings = {
|
|
27
|
+
enabled: enabled === 'on' || enabled === 'off' ? enabled : 'auto',
|
|
28
|
+
};
|
|
29
|
+
if (typeof source['includeRoot'] === 'string')
|
|
30
|
+
settings.includeRoot = source['includeRoot'];
|
|
31
|
+
if (typeof source['allowAbsolute'] === 'boolean')
|
|
32
|
+
settings.allowAbsolute = source['allowAbsolute'];
|
|
33
|
+
if (Array.isArray(source['allowedRemoteHosts'])) {
|
|
34
|
+
settings.allowedRemoteHosts = source['allowedRemoteHosts'].filter((host) => typeof host === 'string');
|
|
35
|
+
}
|
|
36
|
+
if (typeof source['maxDepth'] === 'number')
|
|
37
|
+
settings.maxDepth = source['maxDepth'];
|
|
38
|
+
if (typeof source['maxBytes'] === 'number')
|
|
39
|
+
settings.maxBytes = source['maxBytes'];
|
|
40
|
+
return settings;
|
|
41
|
+
}
|
|
42
|
+
/** Client-reported workspace trust. Absent means untrusted. */
|
|
43
|
+
export function readWorkspaceTrusted(raw) {
|
|
44
|
+
return raw?.workspaceTrusted === true;
|
|
45
|
+
}
|
|
46
|
+
export function fsPath(uri) {
|
|
47
|
+
if (!uri.startsWith('file:'))
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
return fileURLToPath(uri);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** True when `candidate` is `root` or sits underneath it, segment-wise. */
|
|
57
|
+
function contains(root, candidate) {
|
|
58
|
+
const rel = path.relative(root, candidate);
|
|
59
|
+
if (rel === '')
|
|
60
|
+
return true;
|
|
61
|
+
if (!rel || path.isAbsolute(rel))
|
|
62
|
+
return false;
|
|
63
|
+
return rel.split(path.sep)[0] !== '..';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The workspace folder this document belongs to: the deepest one containing
|
|
67
|
+
* it. A document in no folder at all has no workspace root, and falls back to
|
|
68
|
+
* its own directory rather than to somebody else's folder.
|
|
69
|
+
*/
|
|
70
|
+
function workspaceRootFor(documentPath, roots) {
|
|
71
|
+
let best;
|
|
72
|
+
for (const root of roots ?? []) {
|
|
73
|
+
if (!contains(root, documentPath))
|
|
74
|
+
continue;
|
|
75
|
+
if (best === undefined || root.length > best.length)
|
|
76
|
+
best = root;
|
|
77
|
+
}
|
|
78
|
+
return best;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build the include options for one document, or undefined to leave its
|
|
82
|
+
* directives literal.
|
|
83
|
+
*
|
|
84
|
+
* The containment root is the explicit override, else the workspace root, else
|
|
85
|
+
* the document's own directory for a file opened outside any workspace. It is
|
|
86
|
+
* NEVER the process working directory: a language server is commonly spawned
|
|
87
|
+
* from the user's home or from `/`, and rooting there would make containment
|
|
88
|
+
* meaningless.
|
|
89
|
+
*/
|
|
90
|
+
export function includeOptionsFor(input) {
|
|
91
|
+
// GUARD 0 (§19: MUST treat includes as opt-in, off for untrusted input).
|
|
92
|
+
if (input.settings.enabled === 'off')
|
|
93
|
+
return undefined;
|
|
94
|
+
if (input.settings.enabled === 'auto' && !input.workspaceTrusted)
|
|
95
|
+
return undefined;
|
|
96
|
+
const documentPath = fsPath(input.uri);
|
|
97
|
+
// A document with no filesystem identity (untitled:, a remote scheme) has no
|
|
98
|
+
// directory to resolve against and gets no capability.
|
|
99
|
+
if (documentPath === undefined)
|
|
100
|
+
return undefined;
|
|
101
|
+
const configured = input.settings.includeRoot ??
|
|
102
|
+
workspaceRootFor(documentPath, input.workspaceRoots) ??
|
|
103
|
+
dirname(documentPath);
|
|
104
|
+
// Canonicalize the root here as well as inside the resolver, so that the
|
|
105
|
+
// root a caller sees on `includeRoot` is in the same coordinate system as
|
|
106
|
+
// the ids the resolver returns. Without it, a root reached through a symlink
|
|
107
|
+
// (or given relatively) makes every child id look like it sits outside the
|
|
108
|
+
// root, and a diagnostic naming that child would print an absolute path.
|
|
109
|
+
let root;
|
|
110
|
+
try {
|
|
111
|
+
root = realpathSync(configured);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
let resolver;
|
|
117
|
+
try {
|
|
118
|
+
resolver = fileSystemResolver(root, {
|
|
119
|
+
allowAbsolute: input.settings.allowAbsolute ?? false,
|
|
120
|
+
allowedRemoteHosts: input.settings.allowedRemoteHosts ?? [],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// A root that is not a real directory yields no capability at all, rather
|
|
125
|
+
// than a resolver that silently falls back to somewhere wider.
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const options = { resolver, sourcePath: documentPath, includeRoot: root };
|
|
129
|
+
if (input.settings.maxDepth !== undefined)
|
|
130
|
+
options.maxDepth = input.settings.maxDepth;
|
|
131
|
+
if (input.settings.maxBytes !== undefined)
|
|
132
|
+
options.maxBytes = input.settings.maxBytes;
|
|
133
|
+
return options;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=include-settings.js.map
|