@farm.js/search 0.1.0-beta.0 → 0.1.0-beta.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+E1F;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKhF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGnE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQpE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAoB3E"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAgF1F;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKhF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGnE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQpE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAoB3E"}
package/dist/build.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, readdir, rm } from "node:fs/promises";
1
+ import { lstat, mkdir, readFile, readdir, realpath, rm } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import * as pagefind from "pagefind";
4
4
  export async function writeSearchIndex(input) {
@@ -6,6 +6,7 @@ export async function writeSearchIndex(input) {
6
6
  const basePath = normalizeBasePath(input.basePath);
7
7
  const bundlePath = withBasePath(`/${input.options.output}`, basePath);
8
8
  const outputPath = resolveOutputPath(publicDir, bundlePath);
9
+ await assertOutputPathInside(publicDir, outputPath);
9
10
  const htmlFiles = await collectHtmlFiles(publicDir, outputPath);
10
11
  const routes = htmlFiles
11
12
  .map((file) => ({
@@ -163,11 +164,44 @@ async function collectHtmlFiles(root, ignoredDirectory) {
163
164
  function resolveOutputPath(publicDir, bundlePath) {
164
165
  const outputPath = path.resolve(publicDir, `.${bundlePath}`);
165
166
  const relative = path.relative(publicDir, outputPath);
166
- if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
167
+ if (!relative ||
168
+ relative === ".." ||
169
+ relative.startsWith(`..${path.sep}`) ||
170
+ path.isAbsolute(relative)) {
167
171
  throw new Error("[farm:search] Search output must stay inside the public output directory");
168
172
  }
169
173
  return outputPath;
170
174
  }
175
+ async function assertOutputPathInside(publicDir, outputPath) {
176
+ const realPublicDir = await realpath(publicDir);
177
+ let existingAncestor = outputPath;
178
+ while (true) {
179
+ try {
180
+ existingAncestor = await realpath(existingAncestor);
181
+ break;
182
+ }
183
+ catch (error) {
184
+ if (error.code !== "ENOENT")
185
+ throw error;
186
+ try {
187
+ await lstat(existingAncestor);
188
+ throw new Error("[farm:search] Search output must stay inside the public output directory, including through symlinks");
189
+ }
190
+ catch (statError) {
191
+ if (statError.code !== "ENOENT")
192
+ throw statError;
193
+ }
194
+ const parent = path.dirname(existingAncestor);
195
+ if (parent === existingAncestor)
196
+ throw error;
197
+ existingAncestor = parent;
198
+ }
199
+ }
200
+ const relative = path.relative(realPublicDir, existingAncestor);
201
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
202
+ throw new Error("[farm:search] Search output must stay inside the public output directory, including through symlinks");
203
+ }
204
+ }
171
205
  function pagefindError(action, errors) {
172
206
  const details = errors.length ? `:\n${errors.map((error) => ` - ${error}`).join("\n")}` : "";
173
207
  return new Error(`[farm:search] Pagefind ${action}${details}`);
package/dist/config.js CHANGED
@@ -57,11 +57,30 @@ function normalizeOutput(value) {
57
57
  value.includes("\\") ||
58
58
  value.includes("?") ||
59
59
  value.includes("#") ||
60
- segments.some((segment) => segment === "." || segment === ".." || !segment)) {
60
+ segments.some((segment) => !isStableOutputSegment(segment))) {
61
61
  throw new TypeError("Search output must stay inside the public output directory");
62
62
  }
63
63
  return normalized;
64
64
  }
65
+ function isStableOutputSegment(segment) {
66
+ if (!segment)
67
+ return false;
68
+ let decoded = segment;
69
+ try {
70
+ decoded = decodeURIComponent(segment);
71
+ }
72
+ catch {
73
+ // Malformed escapes remain literal in browser pathnames.
74
+ }
75
+ return (decoded !== "." &&
76
+ decoded !== ".." &&
77
+ !decoded.includes("/") &&
78
+ !decoded.includes("\\") &&
79
+ !Array.from(decoded).some((character) => {
80
+ const code = character.charCodeAt(0);
81
+ return code <= 31 || (code >= 127 && code <= 159);
82
+ }));
83
+ }
65
84
  function normalizeLanguage(value) {
66
85
  const normalized = normalizeOptionalString(value, "language")?.toLowerCase();
67
86
  if (normalized && !/^[a-z]{2}$/.test(normalized)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/search",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.1",
4
4
  "description": "Build-time static page search for Farm.js applications",
5
5
  "keywords": [
6
6
  "farm.js",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "pagefind": "^1.5.2",
40
- "@farm.js/core": "0.1.0-beta.92"
40
+ "@farm.js/core": "0.1.0-beta.93"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^20.10.5",