@lone-wolf-akela/dsh-sandboxie-redirect 0.3.0 → 0.3.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/CHANGELOG.md +23 -0
- package/README.md +15 -6
- package/README.zh.md +15 -6
- package/lib/cli.mjs +8 -3
- package/lib/run.mjs +2 -2
- package/lib/sbie-locate.mjs +464 -0
- package/lib/sbie.mjs +48 -15
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
Notable changes per release. The format is loose; the version numbers are what
|
|
4
4
|
the DSH compatibility gate reads.
|
|
5
5
|
|
|
6
|
+
## 0.3.1 — 2026-10-12
|
|
7
|
+
|
|
8
|
+
**Sandboxie installation lookup**
|
|
9
|
+
|
|
10
|
+
- The engine is no longer looked for in a fixed four-path list. The new
|
|
11
|
+
`lib/sbie-locate.mjs` reads Sandboxie's own registry entries first — the
|
|
12
|
+
`SbieSvc` service's `ImagePath` above all, which every installation able to
|
|
13
|
+
create a box must have — then the driver service and the installer's uninstall
|
|
14
|
+
entry, then the conventional folders and `PATH`, and only as a last resort
|
|
15
|
+
walks well-known parent folders and fixed-drive roots for a directory named
|
|
16
|
+
after Sandboxie. An installation on another drive, in a per-user folder or in
|
|
17
|
+
a portable directory is now found instead of reported as "not installed".
|
|
18
|
+
- `DSH_SBIE_DIR` still wins when set, and the "not installed" error now names it
|
|
19
|
+
as well as `SbieDll.dll`, so the remaining unusual cases are actionable.
|
|
20
|
+
- `--manage list` prints the installation folder it resolved and how it found it
|
|
21
|
+
(registry / conventional folder / `PATH` / disk scan).
|
|
22
|
+
- The registry is read through `koffi`/`advapi32` when available — exact wide
|
|
23
|
+
characters, no subprocess — and through `reg.exe` when it is not.
|
|
24
|
+
- New `test/sbie-locate.mjs` (25 checks, no Sandboxie and no registry needed)
|
|
25
|
+
covers the registry value shapes, the candidate ordering, and the staged
|
|
26
|
+
resolution, including that the disk scan runs only when nothing cheap answers.
|
|
27
|
+
It is part of `npm test`.
|
|
28
|
+
|
|
6
29
|
## 0.3.0 — 2026-10-07
|
|
7
30
|
|
|
8
31
|
The distribution changed shape, so this is a breaking release for the earlier
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# dsh-sandboxie-redirect
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@lone-wolf-akela/dsh-sandboxie-redirect)
|
|
4
|
+
[](https://github.com/lone-wolf-akela/dsh-sandboxie-redirect/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](#installation)
|
|
7
|
+
|
|
3
8
|
> **中文版**:[README.zh.md](README.zh.md)
|
|
4
9
|
|
|
5
10
|
A [DSH](https://github.com/deepseek-ai/deepseek-harness) bundle that adds a fourth permission preset, **Copy-on-write (写时复制)**: shell commands run inside a Sandboxie box dedicated to the workspace, so writes inside the workspace land on the real disk as usual, while writes, edits and deletions anywhere else keep succeeding — but only inside that box's copy. The three shipped presets (read-only, workspace-write, danger-full-access) behave exactly as before.
|
|
@@ -35,6 +40,8 @@ A [DSH](https://github.com/deepseek-ai/deepseek-harness) bundle that adds a four
|
|
|
35
40
|
|
|
36
41
|
**Prerequisite:** [Sandboxie-Plus](https://sandboxie-plus.com/) ([source](https://github.com/sandboxie-plus/Sandboxie)). The installer must have created its service; every box this plugin uses is created and managed by Sandboxie.
|
|
37
42
|
|
|
43
|
+
**Any installation folder works — there is nothing to configure.** The plugin does not assume Sandboxie lives in one fixed directory: it reads Sandboxie's own registry entries first (the `SbieSvc` service's `ImagePath` is the authority — an installation that can create a box always has it, followed by the driver service and the installer's uninstall entry), then the conventional folders and `PATH`, and only as a last resort sweeps well-known parent folders and each fixed drive's root for a folder named after Sandboxie that holds `SbieDll.dll`. Another drive, a per-user folder and a portable directory all work. Only when every one of those comes up empty does it report "not installed", and that message names `DSH_SBIE_DIR` (see [Environment](#environment)) as the way to point at it directly.
|
|
44
|
+
|
|
38
45
|
The package is a DSH *bundle*: installing it also adds its configuration layer, so no profile file needs hand-editing.
|
|
39
46
|
|
|
40
47
|
```powershell
|
|
@@ -50,7 +57,7 @@ Other supported sources, for a local checkout or an offline machine:
|
|
|
50
57
|
|
|
51
58
|
```powershell
|
|
52
59
|
dsh plugin --profile <profile> add C:\path\to\dsh-sandboxie-redirect # a checkout (linked)
|
|
53
|
-
dsh plugin --profile <profile> add .\lone-wolf-akela-dsh-sandboxie-redirect-0.3.
|
|
60
|
+
dsh plugin --profile <profile> add .\lone-wolf-akela-dsh-sandboxie-redirect-0.3.1.tgz
|
|
54
61
|
```
|
|
55
62
|
|
|
56
63
|
> **Which command for which profile?** The CLI refuses to touch a profile that a running desktop app owns (`profile "desktop" is managed exclusively by the Electron application`). For that profile, install from inside the app: **Settings → Plugins → add a bundle**, and give the package name above. For any other profile, the CLI works normally.
|
|
@@ -118,6 +125,7 @@ All optional; each exists as an escape hatch rather than a configuration surface
|
|
|
118
125
|
|
|
119
126
|
| Variable | Effect |
|
|
120
127
|
|---|---|
|
|
128
|
+
| `DSH_SBIE_DIR` | The Sandboxie installation folder (the one holding `SbieDll.dll`). Normally unnecessary: the automatic lookup reads Sandboxie's registry entries, the conventional folders and `PATH` first. Use it when Sandboxie lives somewhere unusual and that lookup misses it. |
|
|
121
129
|
| `DSH_SBIE_NODE` | The `node.exe` that runs the launcher. Set it if the plugin cannot find one (the desktop app's own runtime, then `$DSH_HOME/dsh-runtimes`, are searched automatically). |
|
|
122
130
|
| `DSH_SBIE_ROOT` | Overrides the sandbox root. Normally unnecessary: Sandboxie's configured `FileRootPath` is followed. Default `%SystemDrive%\Sandbox\%USERNAME%`. |
|
|
123
131
|
| `DSH_SBIE_LOG_DIR` | Where diagnostics land. Default `$DSH_HOME/state/dsh-sandboxie-redirect`. |
|
|
@@ -137,15 +145,16 @@ Diagnostics: `provider.log`, `host.log`, `tool.log` under the log directory abov
|
|
|
137
145
|
|
|
138
146
|
## Development
|
|
139
147
|
|
|
140
|
-
See [AGENTS.md](AGENTS.md) for the architecture and the hard-won constraints. The short version:
|
|
148
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and the development loop, and [AGENTS.md](AGENTS.md) for the architecture and the hard-won constraints. The short version:
|
|
141
149
|
|
|
142
150
|
```powershell
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
node test\run-tests.mjs # acceptance: real boxes, real redirection (outside any box, medium IL)
|
|
151
|
+
npm test # unit + host projection + bundle-layer validation (no DSH, no Sandboxie)
|
|
152
|
+
npm run test:acceptance # acceptance: real boxes, real redirection (outside any box, medium IL)
|
|
153
|
+
npm run pack:check # what the published tarball would contain
|
|
147
154
|
```
|
|
148
155
|
|
|
156
|
+
Security issues: see [SECURITY.md](SECURITY.md).
|
|
157
|
+
|
|
149
158
|
## License
|
|
150
159
|
|
|
151
160
|
MIT — see [LICENSE](LICENSE).
|
package/README.zh.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# dsh-sandboxie-redirect
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@lone-wolf-akela/dsh-sandboxie-redirect)
|
|
4
|
+
[](https://github.com/lone-wolf-akela/dsh-sandboxie-redirect/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](#安装)
|
|
7
|
+
|
|
3
8
|
> **English**: [README.md](README.md)
|
|
4
9
|
|
|
5
10
|
给 [DSH](https://github.com/deepseek-ai/deepseek-harness) 增加第四种权限预设 **「写时复制」** 的 bundle:shell 命令跑在本工作区专属的 Sandboxie 沙盒里,工作区内的写入照常落到真实磁盘;工作区外的写入、修改、删除不会失败,但只落进该沙盒的副本。原有三个预设(只读 / 仅工作区 / 完全权限)行为不变。
|
|
@@ -34,6 +39,8 @@
|
|
|
34
39
|
|
|
35
40
|
**前置条件**:[Sandboxie-Plus](https://sandboxie-plus.com/)([源码](https://github.com/sandboxie-plus/Sandboxie))。安装后它的服务须在运行;本插件的所有沙盒都由 Sandboxie 创建与管理。
|
|
36
41
|
|
|
42
|
+
**装在哪儿都行,不需要配置。** 插件不假设 Sandboxie 在某个固定目录:它先读 Sandboxie 自己的注册表项(`SbieSvc` 服务的 `ImagePath` 最权威——能建沙盒的安装必然有它,其次是驱动服务与卸载项),再看 `%ProgramFiles%\Sandboxie-Plus` 等常规位置与 `PATH`,最后才按名字扫一遍常见父目录与各固定磁盘根目录,找含 `SbieDll.dll` 的文件夹。装在别的盘、用户目录、便携目录都可以。只有全部落空时才会报 "not installed",此时用 `DSH_SBIE_DIR` 直接指定(见[环境变量](#环境变量))。
|
|
43
|
+
|
|
37
44
|
本包是 DSH 的 **bundle**(自带配置层),装进去就自动挂载,**不需要手改 profile 文件**。
|
|
38
45
|
|
|
39
46
|
```powershell
|
|
@@ -49,7 +56,7 @@ dsh --profile <profile>
|
|
|
49
56
|
|
|
50
57
|
```powershell
|
|
51
58
|
dsh plugin --profile <profile> add C:\path\to\dsh-sandboxie-redirect # 本地 checkout(link 方式)
|
|
52
|
-
dsh plugin --profile <profile> add .\lone-wolf-akela-dsh-sandboxie-redirect-0.3.
|
|
59
|
+
dsh plugin --profile <profile> add .\lone-wolf-akela-dsh-sandboxie-redirect-0.3.1.tgz
|
|
53
60
|
```
|
|
54
61
|
|
|
55
62
|
> **哪个 profile 用哪条路?** 正在运行的桌面应用独占的 profile(`desktop`)会被 CLI 拒绝操作(`profile "desktop" is managed exclusively by the Electron application`)。这种 profile 请在**应用内**装:**设置 → 插件 → 添加 bundle**,填上面的包名。其它 profile 用 CLI 即可。
|
|
@@ -117,6 +124,7 @@ node bin\dsh-sbie-run.mjs --manage delete-box <沙盒名> # 删除指定沙
|
|
|
117
124
|
|
|
118
125
|
| 变量 | 作用 |
|
|
119
126
|
|---|---|
|
|
127
|
+
| `DSH_SBIE_DIR` | Sandboxie 安装目录(含 `SbieDll.dll` 的那个文件夹)。通常不需要:上面的自动查找会先从注册表、常规位置与 `PATH` 里找。装在非常规目录且自动查找没命中时用它。 |
|
|
120
128
|
| `DSH_SBIE_NODE` | 跑启动器的 `node.exe`。插件会自动去找(桌面应用自带的运行时,其次 `$DSH_HOME/dsh-runtimes`),找不到时用它指定。 |
|
|
121
129
|
| `DSH_SBIE_ROOT` | 覆盖沙盒根。通常不需要:Sandboxie 配置的 `FileRootPath` 会被自动跟随。默认 `%SystemDrive%\Sandbox\%USERNAME%`。 |
|
|
122
130
|
| `DSH_SBIE_LOG_DIR` | 诊断日志目录。默认 `$DSH_HOME/state/dsh-sandboxie-redirect`。 |
|
|
@@ -136,15 +144,16 @@ node bin\dsh-sbie-run.mjs --manage delete-box <沙盒名> # 删除指定沙
|
|
|
136
144
|
|
|
137
145
|
## 开发
|
|
138
146
|
|
|
139
|
-
|
|
147
|
+
环境准备与开发回路见 [CONTRIBUTING.md](CONTRIBUTING.md),架构与踩过的坑见 [AGENTS.md](AGENTS.md)。常用命令:
|
|
140
148
|
|
|
141
149
|
```powershell
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
node test\run-tests.mjs # 验收:真沙盒、真重定向(须在盒外、Medium IL)
|
|
150
|
+
npm test # 单元 + 标题栏投影 + bundle 层校验(不需要 DSH、不需要 Sandboxie)
|
|
151
|
+
npm run test:acceptance # 验收:真沙盒、真重定向(须在盒外、Medium IL)
|
|
152
|
+
npm run pack:check # 看发布的 tarball 里会有什么
|
|
146
153
|
```
|
|
147
154
|
|
|
155
|
+
安全问题见 [SECURITY.md](SECURITY.md)。
|
|
156
|
+
|
|
148
157
|
## 许可
|
|
149
158
|
|
|
150
159
|
MIT —— 见 [LICENSE](LICENSE)。
|
package/lib/cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { boxNameCandidates, canonicalWorkspace, listManagedBoxes, managedBoxesFor, removeBox, rootFor } from "./boxes.mjs";
|
|
11
|
-
import { boxExists, reloadConfig, sbieDir, sbieIni } from "./sbie.mjs";
|
|
11
|
+
import { boxExists, reloadConfig, sbieDir, sbieDirSource, sbieIni, sbieNotInstalledError } from "./sbie.mjs";
|
|
12
12
|
|
|
13
13
|
function print(line) {
|
|
14
14
|
process.stdout.write(`${line}\n`);
|
|
@@ -62,10 +62,15 @@ function directorySize(dir) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function list() {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const installDir = sbieDir();
|
|
66
|
+
if (installDir === undefined) {
|
|
67
|
+
print(sbieNotInstalledError().message);
|
|
67
68
|
return 1;
|
|
68
69
|
}
|
|
70
|
+
// Reported because "which Sandboxie is this plugin driving?" is the first
|
|
71
|
+
// question when a machine has more than one installation, and the answer is
|
|
72
|
+
// no longer a single fixed path.
|
|
73
|
+
print(`Sandboxie: ${installDir} (found via ${sbieDirSource()})`);
|
|
69
74
|
const boxes = listManagedBoxes();
|
|
70
75
|
if (boxes.length === 0) {
|
|
71
76
|
print("No managed boxes (Dsh_* / Dshr_*) exist yet.");
|
package/lib/run.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import path from "node:path";
|
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import { boxNameFor, canonicalWorkspace, ensureBox } from "./boxes.mjs";
|
|
32
32
|
import { Frame, createFrameDecoder, encodeFrame, encodeJsonFrame } from "./protocol.mjs";
|
|
33
|
-
import { runSandboxed, killProcess, sbieDir } from "./sbie.mjs";
|
|
33
|
+
import { runSandboxed, killProcess, sbieDir, sbieNotInstalledError } from "./sbie.mjs";
|
|
34
34
|
|
|
35
35
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
36
36
|
const relayScript = path.join(here, "inner.mjs");
|
|
@@ -193,7 +193,7 @@ export async function run(argv, io = {}) {
|
|
|
193
193
|
const box = options.box ?? process.env.DSH_SBIE_BOX ?? boxNameFor(workspace, mode);
|
|
194
194
|
const log = options.verbose ? (message) => process.stderr.write(`dsh-sbie-run: ${message}\n`) : () => {};
|
|
195
195
|
|
|
196
|
-
if (sbieDir() === undefined) throw
|
|
196
|
+
if (sbieDir() === undefined) throw sbieNotInstalledError();
|
|
197
197
|
|
|
198
198
|
const ensured = ensureBox({ box, workspace, mode, logger: log });
|
|
199
199
|
const env = { ...process.env, DSH_SANDBOX_ROOT: ensured.root, DSH_SBIE_BOX: ensured.box };
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find where Sandboxie-Plus is installed.
|
|
3
|
+
*
|
|
4
|
+
* The engine is reached through `SbieDll.dll` and `SbieIni.exe`, both of which
|
|
5
|
+
* live in the installation folder — and that folder is NOT always the default
|
|
6
|
+
* `%ProgramFiles%\Sandboxie-Plus`. A user may install to another drive, to a
|
|
7
|
+
* per-user folder, or into a portable directory. Probing a fixed list of four
|
|
8
|
+
* paths therefore reports "Sandboxie-Plus is not installed" on machines where
|
|
9
|
+
* it plainly is.
|
|
10
|
+
*
|
|
11
|
+
* The authoritative source is Sandboxie's own service registration:
|
|
12
|
+
* `SbieDll_RunSandboxed` is only usable while `SbieSvc` runs, and the service
|
|
13
|
+
* key's `ImagePath` names the real installation folder. The driver service and
|
|
14
|
+
* the installer's uninstall entry name it too. Those are read first, then the
|
|
15
|
+
* conventional folders, then `PATH`, and only if all of that fails is the disk
|
|
16
|
+
* walked — a bounded, one-level scan of well-known parent folders and of each
|
|
17
|
+
* fixed drive's root, looking for a directory whose name names Sandboxie.
|
|
18
|
+
*
|
|
19
|
+
* Everything here is dependency-light on purpose: the module is imported by the
|
|
20
|
+
* launcher (a plain Node process) and is unit-testable with no Sandboxie, no
|
|
21
|
+
* registry, and no Windows at all, because every side effect is injectable.
|
|
22
|
+
* `koffi` is loaded lazily and only when the registry is actually consulted.
|
|
23
|
+
*/
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { loadKoffi } from "./koffi.mjs";
|
|
28
|
+
|
|
29
|
+
/** The DLL whose presence in a folder is what makes it an installation folder. */
|
|
30
|
+
export const SBIE_DLL = "SbieDll.dll";
|
|
31
|
+
|
|
32
|
+
/** Windows' predefined `HKEY_LOCAL_MACHINE`. */
|
|
33
|
+
const HKEY_LOCAL_MACHINE = 0x80000002;
|
|
34
|
+
/** `RegGetValueW` restrictions: accept `REG_SZ` and `REG_EXPAND_SZ` (expanded for us). */
|
|
35
|
+
const RRF_RT_REG_SZ = 0x00000002;
|
|
36
|
+
const RRF_RT_REG_EXPAND_SZ = 0x00000004;
|
|
37
|
+
/** The largest registry value this module bothers to hold (a path never comes close). */
|
|
38
|
+
const REGISTRY_VALUE_BYTES = 4096;
|
|
39
|
+
/** `GetDriveTypeW` answer for a fixed disk. */
|
|
40
|
+
const DRIVE_FIXED = 3;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Registry locations that name the installation folder, most authoritative
|
|
44
|
+
* first. `kind: "file"` means the value names a file inside the folder (so the
|
|
45
|
+
* folder is its directory); `kind: "dir"` means the value IS the folder.
|
|
46
|
+
*
|
|
47
|
+
* `HKLM\SYSTEM\CurrentControlSet\Services\SbieSvc` is the load-bearing one: a
|
|
48
|
+
* Sandboxie that cannot register that service cannot start a boxed process at
|
|
49
|
+
* all, so the folder it names is the folder this plugin must use whenever the
|
|
50
|
+
* plugin can work.
|
|
51
|
+
*/
|
|
52
|
+
export const SBIE_REGISTRY_SOURCES = [
|
|
53
|
+
{
|
|
54
|
+
key: "HKLM\\SYSTEM\\CurrentControlSet\\Services\\SbieSvc",
|
|
55
|
+
values: [{ name: "ImagePath", kind: "file" }]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: "HKLM\\SYSTEM\\CurrentControlSet\\Services\\SbieDrv",
|
|
59
|
+
values: [{ name: "ImagePath", kind: "file" }]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "HKLM\\SOFTWARE\\Sandboxie-Plus",
|
|
63
|
+
values: [
|
|
64
|
+
{ name: "InstallDir", kind: "dir" },
|
|
65
|
+
{ name: "InstallLocation", kind: "dir" },
|
|
66
|
+
{ name: "SbieHome", kind: "dir" },
|
|
67
|
+
{ name: "HomeDir", kind: "dir" }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "HKLM\\SOFTWARE\\WOW6432Node\\Sandboxie-Plus",
|
|
72
|
+
values: [
|
|
73
|
+
{ name: "InstallDir", kind: "dir" },
|
|
74
|
+
{ name: "InstallLocation", kind: "dir" },
|
|
75
|
+
{ name: "SbieHome", kind: "dir" },
|
|
76
|
+
{ name: "HomeDir", kind: "dir" }
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
// Sandboxie-Plus ships an Inno Setup installer; both registry views are read
|
|
81
|
+
// because the installer's bitness decides which one it wrote.
|
|
82
|
+
key: "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Sandboxie-Plus_is1",
|
|
83
|
+
values: [
|
|
84
|
+
{ name: "InstallLocation", kind: "dir" },
|
|
85
|
+
{ name: "Inno Setup: App Path", kind: "dir" },
|
|
86
|
+
{ name: "DisplayIcon", kind: "file" }
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Sandboxie-Plus_is1",
|
|
91
|
+
values: [
|
|
92
|
+
{ name: "InstallLocation", kind: "dir" },
|
|
93
|
+
{ name: "Inno Setup: App Path", kind: "dir" },
|
|
94
|
+
{ name: "DisplayIcon", kind: "file" }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/** Environment lookup that is case-insensitive, like Windows' own. */
|
|
100
|
+
function envLookup(env, name) {
|
|
101
|
+
if (env === undefined || env === null) return undefined;
|
|
102
|
+
const wanted = name.toLowerCase();
|
|
103
|
+
for (const key of Object.keys(env)) {
|
|
104
|
+
if (key.toLowerCase() !== wanted) continue;
|
|
105
|
+
const value = env[key];
|
|
106
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Replace `%NAME%` references with the environment's values. Unknown names are
|
|
113
|
+
* left alone: a candidate that keeps an unresolved placeholder simply fails the
|
|
114
|
+
* `SbieDll.dll` check and costs nothing, whereas guessing a value could point
|
|
115
|
+
* at the wrong folder.
|
|
116
|
+
*/
|
|
117
|
+
export function expandEnvRefs(value, env = process.env) {
|
|
118
|
+
return String(value).replace(/%([^%]+)%/g, (whole, name) => envLookup(env, name) ?? whole);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Turn one registry value into the folder it names.
|
|
123
|
+
*
|
|
124
|
+
* Handles the shapes these keys actually hold: surrounding quotes, the
|
|
125
|
+
* `\??\`/`\\?\` prefixes the driver service uses, `%ProgramFiles%`-style
|
|
126
|
+
* references (already expanded on the `RegGetValueW` path, expanded here on the
|
|
127
|
+
* `reg.exe` one), and trailing separators. Returns `undefined` when the value
|
|
128
|
+
* cannot name a folder.
|
|
129
|
+
*
|
|
130
|
+
* @param raw - the registry value.
|
|
131
|
+
* @param kind - `"file"` (a file inside the folder) or `"dir"` (the folder).
|
|
132
|
+
* @param env - environment used to expand `%NAME%` references.
|
|
133
|
+
*/
|
|
134
|
+
export function directoryFromRegistryValue(raw, kind = "dir", env = process.env) {
|
|
135
|
+
if (typeof raw !== "string") return undefined;
|
|
136
|
+
let text = expandEnvRefs(raw, env).trim();
|
|
137
|
+
if (text.length === 0) return undefined;
|
|
138
|
+
const quoted = /^"(.*)"$/s.exec(text);
|
|
139
|
+
if (quoted !== null) text = quoted[1].trim();
|
|
140
|
+
// `\??\C:\…` (driver ImagePath), `\\?\C:\…` (long-path form), `\SystemRoot\…`.
|
|
141
|
+
text = text.replace(/^(?:\\\?\?\\|\\\\\?\\|\\SystemRoot\\)/i, "");
|
|
142
|
+
if (text.length === 0) return undefined;
|
|
143
|
+
const trimmed = text.replace(/[\\/]+$/, "");
|
|
144
|
+
if (trimmed.length === 0) return undefined;
|
|
145
|
+
if (kind !== "file") return trimmed;
|
|
146
|
+
// A file value must carry a directory; a bare name would resolve to ".".
|
|
147
|
+
if (!/[\\/]/.test(trimmed)) return undefined;
|
|
148
|
+
const dir = path.dirname(trimmed).replace(/[\\/]+$/, "");
|
|
149
|
+
return dir.length === 0 || dir === "." ? undefined : dir;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The value of one `reg query` line, or `undefined`.
|
|
154
|
+
*
|
|
155
|
+
* The format is fixed: `<value>\t<type>\t<data>`, with two or more spaces in
|
|
156
|
+
* place of tabs. A value name may itself contain spaces (`Inno Setup: App
|
|
157
|
+
* Path`), so the name/type split is found by the `REG_*` token rather than by
|
|
158
|
+
* splitting on the first run of whitespace.
|
|
159
|
+
*
|
|
160
|
+
* @param stdout - `reg query <key> /v <name>` output.
|
|
161
|
+
* @param name - the value name that was asked for.
|
|
162
|
+
*/
|
|
163
|
+
export function parseRegQueryValue(stdout, name) {
|
|
164
|
+
const wanted = String(name).toLowerCase();
|
|
165
|
+
for (const line of String(stdout ?? "").split(/\r?\n/)) {
|
|
166
|
+
const match = /^\s{2,}(\S.*?)\s{2,}(REG_[A-Z_]+)\s{2,}(.*)$/.exec(line);
|
|
167
|
+
if (match === null) continue;
|
|
168
|
+
if (match[1].trim().toLowerCase() !== wanted) continue;
|
|
169
|
+
return match[3].trim();
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Read the registry through koffi/advapi32 (wide characters, no subprocess). */
|
|
175
|
+
function readRegistryValuesNative() {
|
|
176
|
+
const koffi = loadKoffi();
|
|
177
|
+
const advapi = koffi.load("advapi32.dll");
|
|
178
|
+
const regGetValue = advapi.func("__stdcall", "RegGetValueW", "int32", [
|
|
179
|
+
"uintptr", // HKEY
|
|
180
|
+
"str16", // LPCWSTR lpSubKey
|
|
181
|
+
"str16", // LPCWSTR lpValue
|
|
182
|
+
"uint32", // DWORD dwFlags
|
|
183
|
+
"void *", // LPDWORD pdwType
|
|
184
|
+
"void *", // PVOID pvData
|
|
185
|
+
koffi.inout(koffi.pointer("uint32")) // LPDWORD pcbData
|
|
186
|
+
]);
|
|
187
|
+
const found = [];
|
|
188
|
+
for (const source of SBIE_REGISTRY_SOURCES) {
|
|
189
|
+
const subkey = source.key.replace(/^HKLM\\/i, "");
|
|
190
|
+
for (const entry of source.values) {
|
|
191
|
+
const buffer = Buffer.alloc(REGISTRY_VALUE_BYTES);
|
|
192
|
+
const size = [buffer.length];
|
|
193
|
+
const status = regGetValue(HKEY_LOCAL_MACHINE, subkey, entry.name, RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ, null, buffer, size);
|
|
194
|
+
if (status !== 0) continue; // ERROR_FILE_NOT_FOUND / no such key: simply not this source
|
|
195
|
+
const value = buffer.toString("utf16le").replace(/\0[\s\S]*$/s, "").trim();
|
|
196
|
+
if (value.length > 0) found.push({ value, kind: entry.kind, source: source.key });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return found;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Read the registry through `reg.exe`; used only when koffi cannot be used at all.
|
|
204
|
+
*
|
|
205
|
+
* One spawn per KEY rather than per value: `reg query <key>` prints every value
|
|
206
|
+
* of the key, and the parser picks the ones that interest us. Spawning costs
|
|
207
|
+
* tens of milliseconds each, so this halves the fallback's latency.
|
|
208
|
+
*/
|
|
209
|
+
function readRegistryValuesRegExe() {
|
|
210
|
+
const found = [];
|
|
211
|
+
for (const source of SBIE_REGISTRY_SOURCES) {
|
|
212
|
+
let result;
|
|
213
|
+
try {
|
|
214
|
+
result = spawnSync("reg.exe", ["query", source.key], {
|
|
215
|
+
encoding: "utf8",
|
|
216
|
+
windowsHide: true,
|
|
217
|
+
timeout: 5000
|
|
218
|
+
});
|
|
219
|
+
} catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (result?.status !== 0) continue;
|
|
223
|
+
for (const entry of source.values) {
|
|
224
|
+
const value = parseRegQueryValue(result.stdout, entry.name);
|
|
225
|
+
if (value !== undefined && value.length > 0) found.push({ value, kind: entry.kind, source: source.key });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return found;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Every registry value that names an installation folder, in source order.
|
|
233
|
+
*
|
|
234
|
+
* `koffi` is tried first because it decodes the wide-character values exactly
|
|
235
|
+
* and spawns nothing; `reg.exe` is the fallback, since a machine where the
|
|
236
|
+
* plugin works always has it even if koffi cannot be located.
|
|
237
|
+
*/
|
|
238
|
+
export function readSbieRegistryValues() {
|
|
239
|
+
try {
|
|
240
|
+
return readRegistryValuesNative();
|
|
241
|
+
} catch {
|
|
242
|
+
/* no koffi, or advapi32 unavailable: reg.exe still answers */
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
return readRegistryValuesRegExe();
|
|
246
|
+
} catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Folders from registry values, in order, with `undefined`s dropped. */
|
|
252
|
+
export function registryCandidateDirs(values, env = process.env) {
|
|
253
|
+
const dirs = [];
|
|
254
|
+
for (const entry of values ?? []) {
|
|
255
|
+
const dir = directoryFromRegistryValue(entry?.value, entry?.kind, env);
|
|
256
|
+
if (dir !== undefined) dirs.push(dir);
|
|
257
|
+
}
|
|
258
|
+
return dirs;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The conventional installation folders. `%ProgramFiles%` and friends cover a
|
|
263
|
+
* relocated Windows as well as a relocated Sandboxie; the literal last entry is
|
|
264
|
+
* the historical default, kept so a machine with no environment at all still
|
|
265
|
+
* gets the old behaviour.
|
|
266
|
+
*/
|
|
267
|
+
export function defaultSbieDirs(env = process.env) {
|
|
268
|
+
const dirs = [];
|
|
269
|
+
const under = (base) => {
|
|
270
|
+
if (typeof base !== "string" || base.length === 0) return undefined;
|
|
271
|
+
return path.join(base, "Sandboxie-Plus");
|
|
272
|
+
};
|
|
273
|
+
for (const name of ["ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"]) {
|
|
274
|
+
const dir = under(envLookup(env, name));
|
|
275
|
+
if (dir !== undefined) dirs.push(dir);
|
|
276
|
+
}
|
|
277
|
+
const localAppData = envLookup(env, "LOCALAPPDATA");
|
|
278
|
+
if (localAppData !== undefined) dirs.push(path.join(localAppData, "Programs", "Sandboxie-Plus"));
|
|
279
|
+
const systemDrive = envLookup(env, "SystemDrive");
|
|
280
|
+
if (systemDrive !== undefined) dirs.push(path.join(systemDrive, "Sandboxie-Plus"));
|
|
281
|
+
dirs.push("C:\\Program Files\\Sandboxie-Plus");
|
|
282
|
+
return dirs;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Directories from `PATH` that are plausibly an installation folder. Only names
|
|
287
|
+
* that mention Sandboxie are considered, so a long `PATH` does not turn into a
|
|
288
|
+
* long list of candidates.
|
|
289
|
+
*/
|
|
290
|
+
export function pathCandidateDirs(env = process.env) {
|
|
291
|
+
const raw = envLookup(env, "PATH");
|
|
292
|
+
if (raw === undefined) return [];
|
|
293
|
+
const dirs = [];
|
|
294
|
+
for (const entry of raw.split(";")) {
|
|
295
|
+
const dir = entry.trim().replace(/^"(.*)"$/s, "$1").trim();
|
|
296
|
+
if (dir.length === 0) continue;
|
|
297
|
+
if (!/sbie|sandboxie/i.test(dir)) continue;
|
|
298
|
+
dirs.push(dir.replace(/[\\/]+$/, ""));
|
|
299
|
+
}
|
|
300
|
+
return dirs;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Parent folders whose immediate children may be an installation folder. */
|
|
304
|
+
export function scanParentDirs(env = process.env) {
|
|
305
|
+
const dirs = [];
|
|
306
|
+
for (const name of ["ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"]) {
|
|
307
|
+
const base = envLookup(env, name);
|
|
308
|
+
if (base !== undefined) dirs.push(base);
|
|
309
|
+
}
|
|
310
|
+
const localAppData = envLookup(env, "LOCALAPPDATA");
|
|
311
|
+
if (localAppData !== undefined) dirs.push(path.join(localAppData, "Programs"));
|
|
312
|
+
const systemDrive = envLookup(env, "SystemDrive");
|
|
313
|
+
if (systemDrive !== undefined) {
|
|
314
|
+
dirs.push(`${systemDrive}\\`);
|
|
315
|
+
dirs.push(path.join(systemDrive, "Programs"));
|
|
316
|
+
dirs.push(path.join(systemDrive, "Apps"));
|
|
317
|
+
}
|
|
318
|
+
return dirs;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Roots of the machine's fixed drives, so an installation on `D:` is found.
|
|
323
|
+
*
|
|
324
|
+
* Only `DRIVE_FIXED` disks are returned: probing a disconnected network drive,
|
|
325
|
+
* an empty optical drive or a card reader can block for seconds, and Sandboxie
|
|
326
|
+
* on such a volume is not a case worth that cost.
|
|
327
|
+
*/
|
|
328
|
+
export function fixedDriveRoots() {
|
|
329
|
+
try {
|
|
330
|
+
const koffi = loadKoffi();
|
|
331
|
+
const kernel32 = koffi.load("kernel32.dll");
|
|
332
|
+
const getLogicalDrives = kernel32.func("GetLogicalDrives", "uint32", []);
|
|
333
|
+
const getDriveType = kernel32.func("GetDriveTypeW", "uint32", ["str16"]);
|
|
334
|
+
const mask = getLogicalDrives();
|
|
335
|
+
const roots = [];
|
|
336
|
+
for (let index = 0; index < 26; index += 1) {
|
|
337
|
+
if ((mask & (1 << index)) === 0) continue;
|
|
338
|
+
const root = `${String.fromCharCode(65 + index)}:\\`;
|
|
339
|
+
if (getDriveType(root) === DRIVE_FIXED) roots.push(root);
|
|
340
|
+
}
|
|
341
|
+
return roots;
|
|
342
|
+
} catch {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Walk the immediate children of `parents` (and of `drives`' roots) for a
|
|
349
|
+
* directory whose NAME names Sandboxie.
|
|
350
|
+
*
|
|
351
|
+
* One level only, and by name only: this is the last resort after the
|
|
352
|
+
* authoritative sources, so it must be cheap and must never wander into a
|
|
353
|
+
* user's tree. A directory that matches but holds no `SbieDll.dll` is simply
|
|
354
|
+
* filtered out by the caller's existence check. Junctions and symlinks count as
|
|
355
|
+
* directories, so a linked installation is not missed by the name sweep.
|
|
356
|
+
*/
|
|
357
|
+
export function scanSbieDirs(options = {}) {
|
|
358
|
+
const readdir = options.readdir ?? fs.readdirSync;
|
|
359
|
+
const parents = [...(options.parents ?? scanParentDirs(options.env)), ...(options.drives ?? [])];
|
|
360
|
+
const seen = new Set();
|
|
361
|
+
const found = [];
|
|
362
|
+
for (const parent of parents) {
|
|
363
|
+
if (typeof parent !== "string" || parent.length === 0) continue;
|
|
364
|
+
let entries;
|
|
365
|
+
try {
|
|
366
|
+
entries = readdir(parent, { withFileTypes: true });
|
|
367
|
+
} catch {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
for (const entry of entries) {
|
|
371
|
+
const isDirectory = entry?.isDirectory?.() === true || entry?.isSymbolicLink?.() === true;
|
|
372
|
+
if (!isDirectory) continue;
|
|
373
|
+
const name = String(entry.name ?? "");
|
|
374
|
+
if (!/sandboxie/i.test(name)) continue;
|
|
375
|
+
const dir = path.join(parent, name);
|
|
376
|
+
const key = dir.toLowerCase();
|
|
377
|
+
if (seen.has(key)) continue;
|
|
378
|
+
seen.add(key);
|
|
379
|
+
found.push(dir);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return found;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** De-duplicate candidates case-insensitively, keeping the first occurrence's spelling. */
|
|
386
|
+
export function uniqueDirs(dirs) {
|
|
387
|
+
const seen = new Set();
|
|
388
|
+
const unique = [];
|
|
389
|
+
for (const dir of dirs ?? []) {
|
|
390
|
+
if (typeof dir !== "string" || dir.length === 0) continue;
|
|
391
|
+
const key = dir.replace(/[\\/]+$/, "").toLowerCase();
|
|
392
|
+
if (key.length === 0 || seen.has(key)) continue;
|
|
393
|
+
seen.add(key);
|
|
394
|
+
unique.push(dir);
|
|
395
|
+
}
|
|
396
|
+
return unique;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Every cheap candidate, in the order they are trusted: the explicit override,
|
|
401
|
+
* then what Sandboxie's own registry entries say, then the conventional
|
|
402
|
+
* folders, then `PATH`. The disk scan is deliberately NOT part of this list —
|
|
403
|
+
* it is expensive and is only reached when nothing here holds `SbieDll.dll`.
|
|
404
|
+
*
|
|
405
|
+
* This is the same list `resolveSbieDir` walks (minus the scan), so the
|
|
406
|
+
* "not installed" message can be built from it and cannot claim to have looked
|
|
407
|
+
* somewhere the resolver never looks.
|
|
408
|
+
*/
|
|
409
|
+
export function sbieDirCandidates(options = {}) {
|
|
410
|
+
const env = options.env ?? process.env;
|
|
411
|
+
const override = typeof env?.DSH_SBIE_DIR === "string" && env.DSH_SBIE_DIR.length > 0 ? [env.DSH_SBIE_DIR] : [];
|
|
412
|
+
const registryValues = options.registryValues ?? readSbieRegistryValues();
|
|
413
|
+
return uniqueDirs([
|
|
414
|
+
...override,
|
|
415
|
+
...registryCandidateDirs(registryValues, env),
|
|
416
|
+
...defaultSbieDirs(env),
|
|
417
|
+
...pathCandidateDirs(env)
|
|
418
|
+
]);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The installation folder, with the reason it was chosen.
|
|
423
|
+
*
|
|
424
|
+
* @param options.env - environment (default `process.env`).
|
|
425
|
+
* @param options.exists - existence probe (default `fs.existsSync`).
|
|
426
|
+
* @param options.registryValues - registry values to trust, when the caller has
|
|
427
|
+
* them; the real registry is read only when this is omitted.
|
|
428
|
+
* @param options.readdir / options.parents / options.drives - the disk scan's
|
|
429
|
+
* inputs, injectable so the whole resolver is testable off-Windows.
|
|
430
|
+
* @param options.scan - `false` disables the last-resort scan.
|
|
431
|
+
* @returns `{dir, source}` or `undefined`.
|
|
432
|
+
*/
|
|
433
|
+
export function resolveSbieDir(options = {}) {
|
|
434
|
+
const env = options.env ?? process.env;
|
|
435
|
+
const exists = options.exists ?? fs.existsSync;
|
|
436
|
+
const hasDll = (dir) => {
|
|
437
|
+
try {
|
|
438
|
+
return exists(path.join(dir, SBIE_DLL)) === true;
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const override = typeof env?.DSH_SBIE_DIR === "string" && env.DSH_SBIE_DIR.length > 0 ? env.DSH_SBIE_DIR : undefined;
|
|
445
|
+
if (override !== undefined && hasDll(override)) return { dir: override, source: "DSH_SBIE_DIR" };
|
|
446
|
+
|
|
447
|
+
const registryValues = options.registryValues ?? readSbieRegistryValues();
|
|
448
|
+
for (const dir of registryCandidateDirs(registryValues, env)) {
|
|
449
|
+
if (hasDll(dir)) return { dir, source: "registry" };
|
|
450
|
+
}
|
|
451
|
+
for (const dir of defaultSbieDirs(env)) {
|
|
452
|
+
if (hasDll(dir)) return { dir, source: "default-folder" };
|
|
453
|
+
}
|
|
454
|
+
for (const dir of pathCandidateDirs(env)) {
|
|
455
|
+
if (hasDll(dir)) return { dir, source: "PATH" };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (options.scan === false) return undefined;
|
|
459
|
+
const drives = options.drives ?? fixedDriveRoots();
|
|
460
|
+
for (const dir of scanSbieDirs({ ...options, env, drives })) {
|
|
461
|
+
if (hasDll(dir)) return { dir, source: "disk-scan" };
|
|
462
|
+
}
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
package/lib/sbie.mjs
CHANGED
|
@@ -16,29 +16,62 @@
|
|
|
16
16
|
* Writing that file directly would need an administrator token.
|
|
17
17
|
*/
|
|
18
18
|
import { spawnSync } from "node:child_process";
|
|
19
|
-
import fs from "node:fs";
|
|
20
19
|
import path from "node:path";
|
|
21
20
|
import { loadKoffi } from "./koffi.mjs";
|
|
21
|
+
import { SBIE_DLL, resolveSbieDir, sbieDirCandidates } from "./sbie-locate.mjs";
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The resolver behind `sbieDir()`: `lib/sbie-locate.mjs` reads Sandboxie's own
|
|
25
|
+
* registry entries (the `SbieSvc` service's `ImagePath` above all), then the
|
|
26
|
+
* conventional folders, then `PATH`, and finally walks well-known parent
|
|
27
|
+
* folders and fixed-drive roots for a directory named after Sandboxie. A fixed
|
|
28
|
+
* four-path list used to live here, which reported "not installed" on every
|
|
29
|
+
* machine whose Sandboxie is not in `%ProgramFiles%\Sandboxie-Plus`.
|
|
30
|
+
*
|
|
31
|
+
* The result is memoized per process because `sbieExe()` resolves for every
|
|
32
|
+
* call; the launcher is a fresh process per command, so a Sandboxie installed
|
|
33
|
+
* while the host runs is picked up on the next command either way.
|
|
34
|
+
*/
|
|
35
|
+
let resolved;
|
|
30
36
|
|
|
31
|
-
/** The Sandboxie installation directory, or undefined when
|
|
37
|
+
/** The Sandboxie installation directory, or undefined when it cannot be found. */
|
|
32
38
|
export function sbieDir() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
if (resolved === undefined) resolved = resolveSbieDir() ?? null;
|
|
40
|
+
return resolved === null ? undefined : resolved.dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** How the installation directory was found (`"registry"`, `"disk-scan"`, …). `undefined` when it was not. */
|
|
44
|
+
export function sbieDirSource() {
|
|
45
|
+
sbieDir();
|
|
46
|
+
return resolved === null ? undefined : resolved.source;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Forget the memoized lookup. For tests and for diagnostics. */
|
|
50
|
+
export function resetSbieDirCache() {
|
|
51
|
+
resolved = undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The one "not installed" error, naming what can be done about it.
|
|
56
|
+
*
|
|
57
|
+
* A fixed candidate list used to be the whole story, so the message could just
|
|
58
|
+
* say "not found". Now the search is broad, and the remaining case — a truly
|
|
59
|
+
* unusual installation, or one the machine's own registry does not describe —
|
|
60
|
+
* needs the escape hatch named in the message itself, because the user who hits
|
|
61
|
+
* it has no other way to learn it exists.
|
|
62
|
+
*/
|
|
63
|
+
export function sbieNotInstalledError() {
|
|
64
|
+
const tried = sbieDirCandidates();
|
|
65
|
+
const shown = tried.slice(0, 6).join(", ");
|
|
66
|
+
const more = tried.length > 6 ? `, … (${tried.length - 6} more)` : "";
|
|
67
|
+
return new Error(
|
|
68
|
+
`Sandboxie-Plus is not installed (${SBIE_DLL} not found). Searched the service and uninstall registry entries, ${shown}${more}, and the fixed drives' Sandboxie folders. Set DSH_SBIE_DIR to the folder containing ${SBIE_DLL} if Sandboxie lives somewhere unusual.`
|
|
69
|
+
);
|
|
37
70
|
}
|
|
38
71
|
|
|
39
72
|
export function sbieExe(name) {
|
|
40
73
|
const dir = sbieDir();
|
|
41
|
-
if (dir === undefined) throw
|
|
74
|
+
if (dir === undefined) throw sbieNotInstalledError();
|
|
42
75
|
return path.join(dir, name);
|
|
43
76
|
}
|
|
44
77
|
|
|
@@ -63,7 +96,7 @@ let api;
|
|
|
63
96
|
export function sbieApi() {
|
|
64
97
|
if (api !== undefined) return api;
|
|
65
98
|
const dir = sbieDir();
|
|
66
|
-
if (dir === undefined) throw
|
|
99
|
+
if (dir === undefined) throw sbieNotInstalledError();
|
|
67
100
|
const koffi = loadKoffi();
|
|
68
101
|
const kernel32 = koffi.load("kernel32.dll");
|
|
69
102
|
const dll = koffi.load(path.join(dir, "SbieDll.dll"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lone-wolf-akela/dsh-sandboxie-redirect",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "DSH bundle adding a copy-on-write permission preset: shell commands run inside a per-workspace Sandboxie box, so writes outside the workspace land in the box's copy while the real disk is untouched.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,9 +76,10 @@
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
|
-
"test": "node test/unit.mjs && node test/host-projection.mjs && node test/validate-patch.mjs",
|
|
79
|
+
"test": "node test/unit.mjs && node test/sbie-locate.mjs && node test/host-projection.mjs && node test/validate-patch.mjs",
|
|
80
80
|
"test:acceptance": "node test/run-tests.mjs",
|
|
81
81
|
"test:soak": "node test/soak.mjs 30",
|
|
82
|
+
"test:locate": "node test/sbie-locate.mjs",
|
|
82
83
|
"validate:patch": "node test/validate-patch.mjs",
|
|
83
84
|
"pack:check": "npm pack --dry-run"
|
|
84
85
|
},
|