pi-better-background-tasks 0.6.0 → 0.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-background-tasks",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Pi extension for durable background shell tasks, watchers, logs, and status inspection.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -552,50 +552,18 @@ function contains(root: string, target: string): boolean {
552
552
  return target.startsWith(root.endsWith(sep) ? root : `${root}${sep}`);
553
553
  }
554
554
 
555
- /** Per-device answer to "does this volume fold case?" (APFS/HFS+ default). */
556
- const caseFoldingByDevice = new Map<number, boolean>();
557
-
558
- /**
559
- * Whether the volume holding `path` (its nearest existing ancestor) ignores
560
- * case: an existing component whose case-swapped spelling names the same
561
- * inode proves it. macOS only; false whenever it cannot be shown.
562
- */
563
- function caseInsensitiveVolume(path: string): boolean {
564
- if (process.platform !== "darwin") return false;
565
- let current = path;
566
- while (!existsSync(current) && dirname(current) !== current) current = dirname(current);
567
- let device: number;
568
- try { device = statSync(current).dev; } catch { return false; }
569
- const known = caseFoldingByDevice.get(device);
570
- if (known !== undefined) return known;
571
- for (let probe = current; dirname(probe) !== probe; probe = dirname(probe)) {
572
- const name = basename(probe);
573
- const swapped = [...name].map((c) => c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase()).join("");
574
- if (swapped === name) continue;
575
- try {
576
- const original = statSync(probe);
577
- if (original.dev !== device) break;
578
- let folded = false;
579
- try {
580
- const other = statSync(join(dirname(probe), swapped));
581
- folded = other.dev === original.dev && other.ino === original.ino;
582
- } catch { /* The swapped name is absent: case-sensitive. */ }
583
- caseFoldingByDevice.set(device, folded);
584
- return folded;
585
- } catch { return false; }
586
- }
587
- return false;
588
- }
589
-
590
555
  /**
591
- * Spell `path` the way the policy spells it when they differ only in case on a
592
- * case-insensitive volume. `canonicalizePath` fixes the case of every existing
593
- * component, but a missing tail (`~/.NPMRC` while `~/.npmrc` is absent) keeps
594
- * the caller's case, and the kernel still treats it as the protected name. The
595
- * longest policy path that matches case-insensitively wins, so the pre-check
596
- * decides as the kernel does (#354).
556
+ * Spell `path` the way the policy spells it when they differ only in case, on
557
+ * macOS. `canonicalizePath` fixes the case of every existing component on a
558
+ * case-insensitive volume, but a missing tail (`~/.NPMRC` while `~/.npmrc` is
559
+ * absent) keeps the caller's case, and the kernel still treats it as the
560
+ * protected name. Seatbelt matches paths case-insensitively on every volume,
561
+ * case-sensitive APFS included, so this folds on darwin without probing the
562
+ * volume: the pre-check decides as the kernel does (#354, #358). The longest
563
+ * policy path that matches case-insensitively wins. Other platforms never fold.
597
564
  */
598
- function alignCaseToPolicy(path: string, policy: CompiledSandboxWritePolicy): string {
565
+ function alignCaseToPolicy(path: string, policy: CompiledSandboxWritePolicy, seams: SandboxSeams): string {
566
+ if (currentPlatform(seams) !== "darwin") return path;
599
567
  const broad = policy.broad;
600
568
  const candidates = [policy.writableRoot, ...policy.denyWrite, ...(policy.credentialPaths ?? []),
601
569
  ...(policy.runtimeWrite ?? []), ...(policy.compatibilityWrite ?? []),
@@ -608,8 +576,7 @@ function alignCaseToPolicy(path: string, policy: CompiledSandboxWritePolicy): st
608
576
  if (folded.length !== candidate.length || !contains(folded, lower)) continue;
609
577
  if (!best || candidate.length > best.length) best = candidate;
610
578
  }
611
- // Already spelled as the policy spells it: nothing to probe.
612
- if (!best || path.startsWith(best) || !caseInsensitiveVolume(path)) return path;
579
+ if (!best || path.startsWith(best)) return path;
613
580
  return `${best}${path.slice(best.length)}`;
614
581
  }
615
582
 
@@ -627,7 +594,7 @@ export function evaluateReadAccess(
627
594
  policy: CompiledSandboxWritePolicy,
628
595
  seams: SandboxSeams = {},
629
596
  ): ReadAccessDecision {
630
- const path = alignCaseToPolicy(canonicalizePath(target, seams), policy);
597
+ const path = alignCaseToPolicy(canonicalizePath(target, seams), policy, seams);
631
598
  const permissions = policy.permissions;
632
599
  if (!permissions) return { allowed: true, path };
633
600
  if (policy.broad) {
@@ -654,7 +621,7 @@ export function evaluateWriteAccess(
654
621
  policy: CompiledSandboxWritePolicy,
655
622
  seams: SandboxSeams = {},
656
623
  ): WriteAccessDecision {
657
- const path = alignCaseToPolicy(canonicalizePath(target, seams), policy);
624
+ const path = alignCaseToPolicy(canonicalizePath(target, seams), policy, seams);
658
625
  if (policy.broad) return evaluateBroadWrite(path, policy, policy.broad, seams);
659
626
  if (!policy.permissions && !contains(policy.writableRoot, path)) {
660
627
  return { allowed: false, path, reason: "outside-writable-root" };
@@ -1582,7 +1549,7 @@ function buildLinuxBroadCommand(
1582
1549
  fileArgs: [...fileArgs, ...(!permissions.network ? ["--unshare-net"] : []),
1583
1550
  "--", args.execPath, ...args.execArgs],
1584
1551
  ...(created.length && { notices: [
1585
- `Sandbox: created an empty read-only placeholder where a protected symlink points at nothing, and left it in place: ${created.join(", ")}`,
1552
+ `Sandbox: created an empty read-only placeholder where a protected symlink points at nothing, and left it in place (remove it before creating the real target): ${created.join(", ")}`,
1586
1553
  ] }),
1587
1554
  };
1588
1555
  }