@intincrab/updiff 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ajay Chauhan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,311 @@
1
- # Temporary Holding Version
1
+ # updiff
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **See what a macOS app update actually changed: in the app, not in the changelog.**
4
+
5
+ ![macOS](https://img.shields.io/badge/platform-macOS-lightgrey)
6
+ ![Node 20+](https://img.shields.io/badge/node-%E2%89%A520-brightgreen)
7
+ ![License: MIT](https://img.shields.io/badge/license-MIT-blue)
8
+ [![CI](https://github.com/intincrab/updiff/actions/workflows/ci.yml/badge.svg)](https://github.com/intincrab/updiff/actions/workflows/ci.yml)
9
+
10
+ <p align="center">
11
+ <img src="docs/demo.svg" width="860" alt="A terminal runs updiff snapshot; the app Lumen then updates to 4.3.0, and updiff check reports 4 high findings (a new camera prompt, the camera entitlement, a background agent that starts at login, a login item) and 3 notable ones (an Electron upgrade, a new analytics package and a new domain).">
12
+ </p>
13
+
14
+ Release notes say "bug fixes and improvements". The update itself might ask for your
15
+ camera, register something that starts at login, ship a new helper that runs as root,
16
+ bundle a new analytics package, or start talking to a new service. updiff takes a
17
+ snapshot of each app and, after every update, shows you exactly what changed inside it.
18
+
19
+ - **Read-only.** It never runs the app, never changes it and never touches the network.
20
+ - **Local.** Snapshots and reports stay in `~/.updiff`. Nothing is uploaded.
21
+ - **Fast.** The first snapshot of 28 apps took 11 s on an Apple Silicon Mac; later checks
22
+ skip unchanged apps and take about 0.1 s.
23
+
24
+ ## What it looks like
25
+
26
+ A real update, Google Chrome 154 → 156, checked with `--deep`:
27
+
28
+ ```text
29
+ $ updiff check --deep
30
+ Google Chrome 154.0.8037.58 (8037.58) → 156.0.8078.80 (8078.80)
31
+ com.google.Chrome
32
+ 0 high · 2 notable · 6 info
33
+
34
+ NOTABLE
35
+ + New domain chromestorage.goog
36
+ chromestorage.goog
37
+ at Contents/Frameworks/Google Chrome Framework.framework/Versions/156.0.8078.80/Google Chrome Framework
38
+ + New domain perplexity.ai
39
+ frontend-cdn.perplexity.ai, suggest.perplexity.ai, www.perplexity.ai
40
+ at Contents/Frameworks/Google Chrome Framework.framework/Versions/156.0.8078.80/Google Chrome Framework
41
+
42
+ INFO
43
+ ~ Google Chrome Framework.framework 154.0.8037.58 → 156.0.8078.80
44
+ + New host under chromium.org
45
+ g-issues.chromium.org
46
+ + New host under google.com
47
+ groups.google.com
48
+ + New host under googleapis.com
49
+ federatedcompute-pa.googleapis.com
50
+ …
51
+
52
+ Report: ~/.updiff/reports/com.google.Chrome/…__154.0.8037.58__to__156.0.8078.80.md
53
+ ```
54
+
55
+ A quiet update looks like that. A worrying one looks like this. This is the "update" the
56
+ test suite builds, trimmed:
57
+
58
+ ```text
59
+ Fixture 1.0.0 (100) → 2.0.0 (200)
60
+ com.example.updiff-fixture
61
+ 6 high · 6 notable · 2 info
62
+
63
+ HIGH
64
+ - App dropped the hardened runtime
65
+ - Helper app Fixture Helper.app dropped the hardened runtime
66
+ + Can now ask for access to Camera
67
+ NSCameraUsageDescription: “Fixture can now see you.”
68
+ + App can now use the camera
69
+ com.apple.security.device.camera = true
70
+ + New background agent: com.example.updiff-fixture.agent.plist
71
+ label com.example.updiff-fixture.agent · runs Contents/MacOS/Fixture · starts at login · kept alive
72
+ + New login item: FixtureLauncher.app
73
+
74
+ NOTABLE
75
+ + New native Node module: fastnative.node
76
+ + Opens fixture-auth:// links
77
+ ~ Electron 33.2.0 → 35.1.0
78
+ + 1 new bundled npm package
79
+ ai-plugin@0.3.0
80
+ + New domain newvendor.io
81
+ native.newvendor.io, telemetry.newvendor.io
82
+ at …/fastnative.node, Contents/Resources/app.asar:dist/ai.js
83
+ …
84
+ ```
85
+
86
+ `+` added, `-` removed, `~` changed. In a terminal, high findings are red, notable
87
+ findings yellow and info cyan.
88
+
89
+ ## Install
90
+
91
+ With Homebrew:
92
+
93
+ ```bash
94
+ brew install intincrab/tap/updiff
95
+ ```
96
+
97
+ With npm, or without installing at all (macOS, Node.js 20 or newer). npm doesn't allow
98
+ the plain name `updiff` (it is too close to the existing `diff` package), so the package
99
+ lives under the `@intincrab` scope; the command is still `updiff`:
100
+
101
+ ```bash
102
+ npm install -g @intincrab/updiff
103
+ ```
104
+
105
+ ```bash
106
+ npx @intincrab/updiff scan Slack
107
+ ```
108
+
109
+ From source:
110
+
111
+ ```bash
112
+ git clone https://github.com/intincrab/updiff.git
113
+ cd updiff
114
+ npm install && npm run build && npm link
115
+ ```
116
+
117
+ ## Quick start
118
+
119
+ Take a baseline of every app, then check after updates:
120
+
121
+ ```bash
122
+ updiff snapshot # once, before the next round of updates
123
+ updiff check # after updating: shows what changed in each updated app
124
+ ```
125
+
126
+ Or let it check by itself:
127
+
128
+ ```bash
129
+ updiff agent install # runs `updiff check` whenever your app folders change
130
+ ```
131
+
132
+ The agent waits until an update has finished writing, then posts a notification titled
133
+ "Updiff: Google Chrome 156.0.8078.80" that says "0 high, 2 notable changes". The full
134
+ report is saved in `~/.updiff/reports/`.
135
+
136
+ ## Commands
137
+
138
+ | Command | What it does |
139
+ |---|---|
140
+ | `updiff scan <app>` | Summarise one app: signing, sandbox, entitlements, privacy prompts, nested code, background items, Electron and hostnames. Stores nothing. |
141
+ | `updiff snapshot [app…]` | Store a snapshot of each app, or of every app when none are named. Unchanged apps are skipped. |
142
+ | `updiff check` | Snapshot every app that changed since its last snapshot and print what's different. Reports are saved as Markdown. |
143
+ | `updiff diff <app>` | If the installed app changed since its newest snapshot, compare the two; otherwise compare its two newest snapshots. |
144
+ | `updiff diff <a> <b>` | Compare any two things: `.app` bundles, snapshot `.json` files or stored apps. Good for a downloaded beta against the installed version. |
145
+ | `updiff list [app]` | List stored apps, or one app's snapshots. |
146
+ | `updiff agent install \| uninstall \| status` | Manage the LaunchAgent that runs `check` after updates. |
147
+
148
+ `<app>` can be a path (`/Applications/Slack.app`), a name from your Applications folders
149
+ (`Slack`, `"Google Chrome"`) or a stored bundle id (`com.tinyspeck.slackmacgap`).
150
+
151
+ ## Options
152
+
153
+ | Option | Applies to | Effect |
154
+ |---|---|---|
155
+ | `--format text\|md\|json` | all | Output format. JSON is for scripts. |
156
+ | `--min-severity high\|notable\|info` | `diff`, `check` | Hide less important findings. |
157
+ | `--out <file>` | `scan`, `diff` | Write the result to a file. |
158
+ | `--deep` | scanning | Also search framework binaries for hostnames. Needed for apps like Chrome that keep their code in a framework. |
159
+ | `--full` | text output | Don't shorten long details. |
160
+ | `--force` | `snapshot` | Store a snapshot even if nothing changed. |
161
+ | `--apps-dir <dir>` | all | Where to look for apps. Repeatable; the default is `/Applications` and `~/Applications`. |
162
+ | `--home <dir>` | all | Data folder. Default `~/.updiff`, or `$UPDIFF_HOME`. |
163
+ | `--keep <n>` | `snapshot`, `check` | Snapshots kept per app (default 10). |
164
+ | `--notify` | `check` | Post a notification for each updated app. |
165
+ | `--settle <seconds>` | `check` | Wait until no app has changed for this long before scanning. |
166
+ | `--quiet`, `--no-color` | all | Print less; plain text. `NO_COLOR` is respected too. |
167
+ | `--interval <seconds>` | `agent install` | Fallback timer between checks (default 21600, i.e. 6 hours). |
168
+ | `--dry-run` | `agent install` | Print the LaunchAgent without installing it. |
169
+ | `--no-load` | `agent install`, `agent uninstall` | Write or remove the file but don't call `launchctl`. |
170
+
171
+ Exit codes: `0` success, `2` usage error (unknown app, bad option), `1` unexpected error.
172
+
173
+ ## What it checks
174
+
175
+ | | High | Notable | Info |
176
+ |---|---|---|---|
177
+ | **Signing** | different developer team; no longer developer-signed; hardened runtime removed | signing certificate changed | hardened runtime added |
178
+ | **Entitlements**, for every executable in the bundle | camera, microphone, location, contacts, calendars, photos, Apple Events, USB, Bluetooth, library validation off, debugging, Endpoint Security, network extensions, sandbox exceptions; sandbox removed | any other new entitlement; new values in app groups, keychain groups or associated domains | removals |
179
+ | **Privacy prompts** (`NS…UsageDescription`) | a new prompt | | reworded or removed prompts |
180
+ | **Background** | new LaunchAgent or LaunchDaemon, login item, privileged helper, system extension | changed background item | removals |
181
+ | **Updater** (Sparkle) | update feed moved; update signing key changed | feed added or removed | |
182
+ | **Network** | App Transport Security allows plain HTTP | new ATS exception domains | |
183
+ | **Code** | | new helper apps, XPC services, app extensions, plug-ins, executables, native Node modules, frameworks | framework versions; removals |
184
+ | **Electron** | | Electron version; new bundled npm packages | package version changes and removals |
185
+ | **Hostnames** | | new domains | new hosts under domains already present; domains no longer mentioned |
186
+ | **URL schemes** | | new `scheme://` handlers | removals |
187
+ | **Identity** | the two sides are different apps | Dock icon shown or hidden | version, minimum macOS, size |
188
+
189
+ Code inside a bundle is matched between versions by its path and, if that fails, by its
190
+ bundle id. So a renamed helper isn't reported as removed plus added, and Chrome's
191
+ `Versions/154…` → `Versions/156…` framework folders count as the same thing.
192
+
193
+ ## About hostnames
194
+
195
+ updiff finds hostnames in URLs (`https://`, `wss://`, …) and in quoted strings like
196
+ `"events.vendor.io"`. It looks in executables, native modules, Electron `app.asar`
197
+ archives and text resources, and reports them grouped by domain.
198
+
199
+ > **A hostname in the code is not proof that the app connects to it.** It shows what the
200
+ > code mentions: API endpoints, links, sometimes dead code. Treat a new domain as a lead
201
+ > and confirm it with a network monitor such as Little Snitch or LuLu.
202
+
203
+ To keep reports readable, it ignores:
204
+ - source maps, READMEs, licences and the `package.json` files inside `node_modules`
205
+ - XML namespaces and reserved names (`www.w3.org`, `example.com`, `localhost`, `*.test`)
206
+ - bare strings whose ending reads like a file type (`Foo.app`, `README.md`, `build.sh`)
207
+ - any file with more than 1,000 bare hostnames, which is a bundled list rather than code
208
+ (Discord's main bundle carries one with about 6,100 entries). These are named in a
209
+ warning.
210
+
211
+ Framework binaries are only searched with `--deep`. Electron apps keep their own code in
212
+ `app.asar`, which is always searched.
213
+
214
+ ## Where things are stored
215
+
216
+ ```text
217
+ ~/.updiff/
218
+ ├── snapshots/<bundle id>/<time>__<version>__<build>.json one JSON file per snapshot
219
+ ├── reports/<bundle id>/<time>__<from>__to__<to>.md one report per detected update
220
+ └── agent.log output of background checks
221
+ ```
222
+
223
+ A snapshot records a fingerprint of the app's code signature seal
224
+ (`_CodeSignature/CodeResources`), so an unchanged app is recognised without a rescan.
225
+ Delete the folder at any time to start over.
226
+
227
+ ## Privacy and safety
228
+
229
+ - updiff reads files and runs Apple's own tools:
230
+ - `plutil` to read plists
231
+ - `codesign` to read signatures and entitlements
232
+ - `osascript` for notifications
233
+ - `launchctl` for the agent
234
+ - It never runs app code.
235
+ - It never makes network requests, and nothing it records leaves your Mac.
236
+ - The agent is a LaunchAgent in your user account (`dev.updiff.check`). It runs with your
237
+ permissions, at low priority. `updiff agent install --dry-run` shows exactly what it
238
+ installs; `updiff agent uninstall` removes it.
239
+
240
+ ## Limitations
241
+
242
+ - **Static only.** Plugins, models or config that an app downloads after launch aren't in
243
+ the bundle, so updiff can't see them.
244
+ - **Hostnames are pattern-matched.** Hosts assembled at runtime (`"api." + region + ".x.com"`)
245
+ are missed, and a mentioned host is not necessarily contacted.
246
+ - **The first update needs a baseline.** Run `updiff snapshot` before updating. You can
247
+ also diff a downloaded copy of the old version against the installed one.
248
+ - **Not covered yet:** Mac App Store receipts, Safari web extension manifests, managed
249
+ browser policies and installer `.pkg` scripts.
250
+
251
+ ## The ledger
252
+
253
+ [`ledger/`](ledger) runs updiff every day on new releases of about 30 popular Mac apps
254
+ (Slack, Zoom, Cursor, VS Code, Postman, Chrome, Discord, …), found through their
255
+ Homebrew casks. It publishes what changed in each release as a static site with an Atom
256
+ feed. Reports are plain JSON in [`ledger/data`](ledger/data). See
257
+ [ledger/README.md](ledger/README.md) to run it or turn on the daily GitHub Action.
258
+
259
+ ## How it works
260
+
261
+ Every scan, a few steps:
262
+ 1. Read `Info.plist` with `plutil`.
263
+ 2. Walk the bundle without following symlinks, and skip framework versions that
264
+ `Versions/Current` doesn't point to.
265
+ 3. Classify nested code by type and location.
266
+ 4. Read each item's signature and entitlements with `codesign`.
267
+ 5. Read `app.asar` with a built-in parser, without extracting it.
268
+ 6. Search text and the arm64 slice of each binary for hostnames.
269
+
270
+ The diff compares two snapshots and assigns each change a severity. The full design and
271
+ the reasoning behind the rules are in [docs/DESIGN.md](docs/DESIGN.md).
272
+
273
+ ## Development
274
+
275
+ ```bash
276
+ npm install
277
+ npm test # unit + integration tests
278
+ npm run typecheck
279
+ npm run build # compiles to dist/
280
+ npm start -- scan Ghostty # run from source (Node 22.18+ runs the TypeScript directly)
281
+
282
+ # re-record the README demo: a real run against a generated demo app, rendered to docs/demo.svg
283
+ node scripts/demo/record.ts && node scripts/demo/render-svg.ts
284
+ ```
285
+
286
+ The integration tests build two versions of a small Electron-style app with clang,
287
+ `@electron/asar` and `codesign`. They plant permission, background, package and network
288
+ changes, then check that each one is reported at the right severity, both through the
289
+ library and through the commands. Without a C compiler, the assertions about hostnames
290
+ inside binaries are skipped.
291
+
292
+ CI runs all of this on a macOS runner for every push, then installs the packed tarball
293
+ and runs it on Node 20, 22 and 24.
294
+
295
+ ### Releasing
296
+
297
+ Bump the version in `package.json` and `src/version.ts` (a test checks they match),
298
+ commit, and push a matching tag:
299
+
300
+ ```bash
301
+ git tag v0.2.0 && git push origin v0.2.0
302
+ ```
303
+
304
+ The [release workflow](.github/workflows/release.yml) tests and packs updiff, creates a
305
+ GitHub Release with the tarball, and publishes it to npm with provenance. The
306
+ [Homebrew tap](https://github.com/intincrab/homebrew-tap) picks up the new release within
307
+ a few hours, or straight away if you run its workflow by hand.
308
+
309
+ ## License
310
+
311
+ [MIT](LICENSE)
package/dist/agent.js ADDED
@@ -0,0 +1,63 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import { homedir, userInfo } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { run } from "./exec.js";
6
+ import { toPlistXml } from "./plist.js";
7
+ export const AGENT_LABEL = 'dev.updiff.check';
8
+ export function launchAgentsDir() {
9
+ return process.env.UPDIFF_LAUNCH_AGENTS_DIR || join(homedir(), 'Library', 'LaunchAgents');
10
+ }
11
+ export function agentPlist(o) {
12
+ const args = [o.nodePath, o.cliPath, 'check', '--notify', '--quiet', '--settle', '90', '--home', o.home];
13
+ for (const d of o.appsDirs)
14
+ args.push('--apps-dir', d);
15
+ return toPlistXml({
16
+ Label: AGENT_LABEL,
17
+ ProgramArguments: args,
18
+ // Runs after anything changes in the app folders, and on a timer as a fallback.
19
+ WatchPaths: o.watchPaths,
20
+ StartInterval: o.intervalSeconds,
21
+ // An update writes many files; wait before running again.
22
+ ThrottleInterval: 120,
23
+ RunAtLoad: false,
24
+ ProcessType: 'Background',
25
+ LowPriorityIO: true,
26
+ Nice: 10,
27
+ StandardOutPath: join(o.home, 'agent.log'),
28
+ StandardErrorPath: join(o.home, 'agent.log'),
29
+ });
30
+ }
31
+ function domain() {
32
+ return `gui/${userInfo().uid}`;
33
+ }
34
+ export async function installAgent(o, load) {
35
+ const dir = launchAgentsDir();
36
+ await mkdir(dir, { recursive: true });
37
+ await mkdir(o.home, { recursive: true });
38
+ const path = join(dir, `${AGENT_LABEL}.plist`);
39
+ if (load && existsSync(path))
40
+ await run('/bin/launchctl', ['bootout', `${domain()}/${AGENT_LABEL}`]);
41
+ await writeFile(path, agentPlist(o));
42
+ if (!load)
43
+ return { path, loaded: false };
44
+ const res = await run('/bin/launchctl', ['bootstrap', domain(), path]);
45
+ return { path, loaded: res.code === 0, message: res.code === 0 ? undefined : res.stderr.trim() };
46
+ }
47
+ export async function uninstallAgent(unload) {
48
+ const path = join(launchAgentsDir(), `${AGENT_LABEL}.plist`);
49
+ const existed = existsSync(path);
50
+ if (unload)
51
+ await run('/bin/launchctl', ['bootout', `${domain()}/${AGENT_LABEL}`]);
52
+ await rm(path, { force: true });
53
+ return { path, existed };
54
+ }
55
+ export async function agentStatus() {
56
+ const path = join(launchAgentsDir(), `${AGENT_LABEL}.plist`);
57
+ const res = await run('/bin/launchctl', ['print', `${domain()}/${AGENT_LABEL}`]);
58
+ return { path, installed: existsSync(path), loaded: res.code === 0 };
59
+ }
60
+ export async function notify(title, message) {
61
+ const q = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
62
+ await run('/usr/bin/osascript', ['-e', `display notification ${q(message)} with title ${q(title)}`]);
63
+ }
package/dist/asar.js ADDED
@@ -0,0 +1,91 @@
1
+ import { closeSync, existsSync, openSync, readSync, readFileSync } from 'node:fs';
2
+ /**
3
+ * Reads Electron `.asar` archives without extracting them.
4
+ *
5
+ * Layout: an 8-byte pickle that holds the header size, then a pickle whose payload is
6
+ * a length-prefixed JSON header, then the file data. A file's `offset` counts from the
7
+ * start of the data. Files marked `unpacked` live in `<archive>.unpacked/`.
8
+ */
9
+ export class AsarArchive {
10
+ path;
11
+ header;
12
+ dataOffset;
13
+ fd;
14
+ constructor(path, fd, header, dataOffset) {
15
+ this.path = path;
16
+ this.fd = fd;
17
+ this.header = header;
18
+ this.dataOffset = dataOffset;
19
+ }
20
+ static open(path) {
21
+ const fd = openSync(path, 'r');
22
+ try {
23
+ const sizeBuf = Buffer.alloc(8);
24
+ if (readSync(fd, sizeBuf, 0, 8, 0) !== 8)
25
+ throw new Error('File is too small to be an asar archive');
26
+ const headerPickleSize = sizeBuf.readUInt32LE(4);
27
+ if (headerPickleSize < 8 || headerPickleSize > 512 * 1024 * 1024)
28
+ throw new Error('Invalid asar header size');
29
+ const headerBuf = Buffer.alloc(headerPickleSize);
30
+ if (readSync(fd, headerBuf, 0, headerPickleSize, 8) !== headerPickleSize)
31
+ throw new Error('Truncated asar header');
32
+ const jsonLength = headerBuf.readUInt32LE(4);
33
+ if (jsonLength > headerPickleSize - 8)
34
+ throw new Error('Invalid asar header length');
35
+ const header = JSON.parse(headerBuf.toString('utf8', 8, 8 + jsonLength));
36
+ if (!header.files)
37
+ throw new Error('asar header has no files');
38
+ return new AsarArchive(path, fd, header, 8 + headerPickleSize);
39
+ }
40
+ catch (err) {
41
+ closeSync(fd);
42
+ throw err;
43
+ }
44
+ }
45
+ *files() {
46
+ const stack = [['', this.header]];
47
+ while (stack.length) {
48
+ const [prefix, node] = stack.pop();
49
+ for (const [name, child] of Object.entries(node.files ?? {})) {
50
+ const path = prefix ? `${prefix}/${name}` : name;
51
+ if (child.files)
52
+ stack.push([path, child]);
53
+ else if (child.link === undefined) {
54
+ yield {
55
+ path,
56
+ size: child.size ?? 0,
57
+ offset: Number(child.offset ?? 0),
58
+ unpacked: child.unpacked === true,
59
+ executable: child.executable === true,
60
+ };
61
+ }
62
+ }
63
+ }
64
+ }
65
+ /** Reads a file's bytes, or the first `limit` bytes. */
66
+ read(file, limit = Infinity) {
67
+ const length = Math.min(file.size, limit);
68
+ if (file.unpacked) {
69
+ const unpackedPath = `${this.path}.unpacked/${file.path}`;
70
+ if (!existsSync(unpackedPath))
71
+ return Buffer.alloc(0);
72
+ const buf = readFileSync(unpackedPath);
73
+ return length < buf.length ? buf.subarray(0, length) : buf;
74
+ }
75
+ const buf = Buffer.alloc(length);
76
+ let read = 0;
77
+ while (read < length) {
78
+ const n = readSync(this.fd, buf, read, length - read, this.dataOffset + file.offset + read);
79
+ if (n === 0)
80
+ break;
81
+ read += n;
82
+ }
83
+ return read < length ? buf.subarray(0, read) : buf;
84
+ }
85
+ close() {
86
+ if (this.fd >= 0) {
87
+ closeSync(this.fd);
88
+ this.fd = -1;
89
+ }
90
+ }
91
+ }