w3wallets 0.1.1 → 0.1.3
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 +4 -6
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +21 -12
- package/dist/index.mjs +21 -12
- package/package.json +4 -4
- package/src/scripts/download.js +297 -70
package/README.md
CHANGED
|
@@ -17,13 +17,11 @@ Only the `Backpack` wallet is supported at this point.
|
|
|
17
17
|
|
|
18
18
|
#### 1. Download Backpack
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
```sh
|
|
21
|
+
npx w3wallets backpack
|
|
22
|
+
```
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
npx w3wallets
|
|
26
|
-
``` -->
|
|
24
|
+
The unzipped files should be stored in the `wallets/backpack` directory. Add them to `.gitignore`.
|
|
27
25
|
|
|
28
26
|
#### 2. Wrap your fixture `withWallets`
|
|
29
27
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as playwright_test from 'playwright/test';
|
|
2
2
|
import { Page, test, BrowserContext } from '@playwright/test';
|
|
3
3
|
|
|
4
|
-
type BackPackNetwork = "Eclipse";
|
|
4
|
+
type BackPackNetwork = "Eclipse" | "Ethereum";
|
|
5
5
|
|
|
6
6
|
declare class Backpack {
|
|
7
7
|
private page;
|
|
@@ -18,7 +18,7 @@ declare class Backpack {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
type Config = {
|
|
21
|
-
backpack
|
|
21
|
+
backpack?: boolean;
|
|
22
22
|
};
|
|
23
23
|
declare function withWallets(test: typeof test, config: Config): playwright_test.TestType<playwright_test.PlaywrightTestArgs & playwright_test.PlaywrightTestOptions & {
|
|
24
24
|
context: BrowserContext;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as playwright_test from 'playwright/test';
|
|
2
2
|
import { Page, test, BrowserContext } from '@playwright/test';
|
|
3
3
|
|
|
4
|
-
type BackPackNetwork = "Eclipse";
|
|
4
|
+
type BackPackNetwork = "Eclipse" | "Ethereum";
|
|
5
5
|
|
|
6
6
|
declare class Backpack {
|
|
7
7
|
private page;
|
|
@@ -18,7 +18,7 @@ declare class Backpack {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
type Config = {
|
|
21
|
-
backpack
|
|
21
|
+
backpack?: boolean;
|
|
22
22
|
};
|
|
23
23
|
declare function withWallets(test: typeof test, config: Config): playwright_test.TestType<playwright_test.PlaywrightTestArgs & playwright_test.PlaywrightTestOptions & {
|
|
24
24
|
context: BrowserContext;
|
package/dist/index.js
CHANGED
|
@@ -91,35 +91,42 @@ var Backpack = class {
|
|
|
91
91
|
|
|
92
92
|
// src/withWallets.ts
|
|
93
93
|
function withWallets(test, config) {
|
|
94
|
-
const
|
|
94
|
+
const backpackPath = import_path.default.join(process.cwd(), "wallets", "backpack");
|
|
95
|
+
const metamaskPath = import_path.default.join(process.cwd(), "wallets", "metamask");
|
|
95
96
|
return test.extend({
|
|
96
97
|
backpack: async ({ context, extensionId }, use) => {
|
|
97
98
|
const page = context.pages()[0];
|
|
98
99
|
if (!page) throw Error("No pages in context");
|
|
99
|
-
const
|
|
100
|
+
const backpack = new Backpack(page, extensionId);
|
|
100
101
|
await page.goto(
|
|
101
102
|
`chrome-extension://${extensionId}/options.html?onboarding=true`
|
|
102
103
|
);
|
|
103
|
-
await use(
|
|
104
|
+
await use(backpack);
|
|
104
105
|
},
|
|
106
|
+
// Browser context fixture
|
|
105
107
|
context: async ({}, use, testInfo) => {
|
|
106
108
|
const userDataDir = import_path.default.join(
|
|
107
109
|
process.cwd(),
|
|
108
110
|
".w3wallets",
|
|
109
111
|
testInfo.testId
|
|
110
112
|
);
|
|
111
|
-
if (import_fs.default.existsSync(userDataDir))
|
|
113
|
+
if (import_fs.default.existsSync(userDataDir)) {
|
|
112
114
|
import_fs.default.rmSync(userDataDir, { recursive: true });
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
}
|
|
116
|
+
const extensionPaths = [];
|
|
117
|
+
if (config.backpack) {
|
|
118
|
+
if (!import_fs.default.existsSync(import_path.default.join(backpackPath, "manifest.json"))) {
|
|
119
|
+
throw Error(
|
|
120
|
+
"Cannot find Backpack. Please download it via `npx w3wallets`"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
extensionPaths.push(backpackPath);
|
|
124
|
+
}
|
|
118
125
|
const context = await import_test3.chromium.launchPersistentContext(userDataDir, {
|
|
119
126
|
headless: false,
|
|
120
127
|
args: [
|
|
121
|
-
`--disable-extensions-except=${
|
|
122
|
-
`--load-extension=${
|
|
128
|
+
`--disable-extensions-except=${extensionPaths.join(",")}`,
|
|
129
|
+
`--load-extension=${extensionPaths.join(",")}`
|
|
123
130
|
]
|
|
124
131
|
});
|
|
125
132
|
await use(context);
|
|
@@ -127,7 +134,9 @@ function withWallets(test, config) {
|
|
|
127
134
|
},
|
|
128
135
|
extensionId: async ({ context }, use) => {
|
|
129
136
|
let [background] = context.serviceWorkers();
|
|
130
|
-
if (!background)
|
|
137
|
+
if (!background) {
|
|
138
|
+
background = await context.waitForEvent("serviceworker");
|
|
139
|
+
}
|
|
131
140
|
const extensionId = background.url().split("/")[2];
|
|
132
141
|
if (!extensionId) throw Error("No extension id");
|
|
133
142
|
await use(extensionId);
|
package/dist/index.mjs
CHANGED
|
@@ -55,35 +55,42 @@ var Backpack = class {
|
|
|
55
55
|
|
|
56
56
|
// src/withWallets.ts
|
|
57
57
|
function withWallets(test, config) {
|
|
58
|
-
const
|
|
58
|
+
const backpackPath = path.join(process.cwd(), "wallets", "backpack");
|
|
59
|
+
const metamaskPath = path.join(process.cwd(), "wallets", "metamask");
|
|
59
60
|
return test.extend({
|
|
60
61
|
backpack: async ({ context, extensionId }, use) => {
|
|
61
62
|
const page = context.pages()[0];
|
|
62
63
|
if (!page) throw Error("No pages in context");
|
|
63
|
-
const
|
|
64
|
+
const backpack = new Backpack(page, extensionId);
|
|
64
65
|
await page.goto(
|
|
65
66
|
`chrome-extension://${extensionId}/options.html?onboarding=true`
|
|
66
67
|
);
|
|
67
|
-
await use(
|
|
68
|
+
await use(backpack);
|
|
68
69
|
},
|
|
70
|
+
// Browser context fixture
|
|
69
71
|
context: async ({}, use, testInfo) => {
|
|
70
72
|
const userDataDir = path.join(
|
|
71
73
|
process.cwd(),
|
|
72
74
|
".w3wallets",
|
|
73
75
|
testInfo.testId
|
|
74
76
|
);
|
|
75
|
-
if (fs.existsSync(userDataDir))
|
|
77
|
+
if (fs.existsSync(userDataDir)) {
|
|
76
78
|
fs.rmSync(userDataDir, { recursive: true });
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
}
|
|
80
|
+
const extensionPaths = [];
|
|
81
|
+
if (config.backpack) {
|
|
82
|
+
if (!fs.existsSync(path.join(backpackPath, "manifest.json"))) {
|
|
83
|
+
throw Error(
|
|
84
|
+
"Cannot find Backpack. Please download it via `npx w3wallets`"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
extensionPaths.push(backpackPath);
|
|
88
|
+
}
|
|
82
89
|
const context = await chromium.launchPersistentContext(userDataDir, {
|
|
83
90
|
headless: false,
|
|
84
91
|
args: [
|
|
85
|
-
`--disable-extensions-except=${
|
|
86
|
-
`--load-extension=${
|
|
92
|
+
`--disable-extensions-except=${extensionPaths.join(",")}`,
|
|
93
|
+
`--load-extension=${extensionPaths.join(",")}`
|
|
87
94
|
]
|
|
88
95
|
});
|
|
89
96
|
await use(context);
|
|
@@ -91,7 +98,9 @@ function withWallets(test, config) {
|
|
|
91
98
|
},
|
|
92
99
|
extensionId: async ({ context }, use) => {
|
|
93
100
|
let [background] = context.serviceWorkers();
|
|
94
|
-
if (!background)
|
|
101
|
+
if (!background) {
|
|
102
|
+
background = await context.waitForEvent("serviceworker");
|
|
103
|
+
}
|
|
95
104
|
const extensionId = background.url().split("/")[2];
|
|
96
105
|
if (!extensionId) throw Error("No extension id");
|
|
97
106
|
await use(extensionId);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w3wallets",
|
|
3
3
|
"description": "browser wallets for playwright",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"homepage": "https://github.com/Maksandre/w3wallets",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"local-release": "changeset version && changeset publish"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"follow-redirects": "^1.15.9",
|
|
37
|
+
"unzip-crx": "^0.2.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@arethetypeswrong/cli": "^0.17.2",
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@playwright/test": "^1.49.1"
|
|
51
51
|
}
|
|
52
|
-
}
|
|
52
|
+
}
|
package/src/scripts/download.js
CHANGED
|
@@ -1,79 +1,306 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* Downloads and extracts Chrome extensions by alias ("backpack" and "metamask")
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx w3wallets backpack
|
|
9
|
+
* npx w3wallets metamask
|
|
10
|
+
* npx w3wallets backpack metamask
|
|
11
|
+
*/
|
|
12
|
+
|
|
3
13
|
const fs = require("fs");
|
|
14
|
+
const https = require("https");
|
|
4
15
|
const path = require("path");
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
16
|
+
const url = require("url");
|
|
17
|
+
const zlib = require("zlib");
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------
|
|
20
|
+
// 1. Known aliases -> extension IDs
|
|
21
|
+
// ---------------------------------------------------------------------
|
|
22
|
+
const ALIASES = {
|
|
23
|
+
backpack: "aflkmfhebedbjioipglgcbcmnbpgliof",
|
|
24
|
+
metamask: "nkbihfbeogaeaoehlefnkodbefgpgknn",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------
|
|
28
|
+
// 2. Read aliases from CLI
|
|
29
|
+
// ---------------------------------------------------------------------
|
|
30
|
+
const inputAliases = process.argv.slice(2);
|
|
31
|
+
|
|
32
|
+
if (!inputAliases.length) {
|
|
33
|
+
console.error("Usage: npx w3wallets <aliases...>");
|
|
34
|
+
console.error("Available aliases:", Object.keys(ALIASES).join(", "));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const alias of inputAliases) {
|
|
39
|
+
if (!ALIASES[alias]) {
|
|
40
|
+
console.error(
|
|
41
|
+
`Unknown alias "${alias}". Must be one of: ${Object.keys(ALIASES).join(", ")}`,
|
|
42
|
+
);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------
|
|
48
|
+
// 3. Main: download and extract each requested alias
|
|
49
|
+
// ---------------------------------------------------------------------
|
|
50
|
+
(async function main() {
|
|
51
|
+
for (const alias of inputAliases) {
|
|
52
|
+
const extensionId = ALIASES[alias];
|
|
53
|
+
console.log(`\n=== Processing alias: "${alias}" (ID: ${extensionId}) ===`);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// 1) Download CRX
|
|
57
|
+
const crxBuffer = await downloadCrx(extensionId);
|
|
58
|
+
console.log(`Got CRX data for "${alias}"! ${crxBuffer.length} bytes`);
|
|
59
|
+
|
|
60
|
+
// 2) Save raw CRX to disk
|
|
61
|
+
const outDir = path.join("wallets", alias);
|
|
62
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
const debugPath = path.join(outDir, `debug-${alias}.crx`);
|
|
65
|
+
fs.writeFileSync(debugPath, crxBuffer);
|
|
66
|
+
console.log(`Saved ${debugPath}`);
|
|
67
|
+
|
|
68
|
+
// 3) Extract CRX into "wallets/<alias>"
|
|
69
|
+
extractCrxToFolder(crxBuffer, outDir);
|
|
70
|
+
console.log(`Extraction complete! See folder: ${outDir}`);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(`Failed to process "${alias}":`, err.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------
|
|
79
|
+
// downloadCrx: Build CRX URL and fetch it
|
|
80
|
+
// ---------------------------------------------------------------------
|
|
81
|
+
async function downloadCrx(extensionId) {
|
|
82
|
+
const downloadUrl =
|
|
83
|
+
"https://clients2.google.com/service/update2/crx" +
|
|
84
|
+
"?response=redirect" +
|
|
85
|
+
"&prod=chrome" +
|
|
86
|
+
"&prodversion=9999" +
|
|
87
|
+
"&acceptformat=crx2,crx3" +
|
|
88
|
+
`&x=id%3D${extensionId}%26uc`;
|
|
89
|
+
|
|
90
|
+
console.log("Requesting:", downloadUrl);
|
|
91
|
+
|
|
92
|
+
const crxBuffer = await fetchUrl(downloadUrl);
|
|
93
|
+
return crxBuffer;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------
|
|
97
|
+
// fetchUrl: minimal GET + redirect handling
|
|
98
|
+
// ---------------------------------------------------------------------
|
|
99
|
+
function fetchUrl(
|
|
100
|
+
targetUrl,
|
|
101
|
+
options = {},
|
|
102
|
+
redirectCount = 0,
|
|
103
|
+
maxRedirects = 10,
|
|
104
|
+
) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
if (redirectCount > maxRedirects) {
|
|
107
|
+
return reject(new Error("Too many redirects"));
|
|
39
108
|
}
|
|
40
109
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (files.length === 1) {
|
|
59
|
-
const singleDirPath = path.join(outputDir, files[0]);
|
|
60
|
-
if (
|
|
61
|
-
fs.lstatSync(singleDirPath).isDirectory() &&
|
|
62
|
-
fs.existsSync(path.join(singleDirPath, "manifest.json"))
|
|
63
|
-
) {
|
|
64
|
-
// Move all files from the directory to the outputDir
|
|
65
|
-
const nestedFiles = fs.readdirSync(singleDirPath);
|
|
66
|
-
nestedFiles.forEach((file) => {
|
|
67
|
-
const srcPath = path.join(singleDirPath, file);
|
|
68
|
-
const destPath = path.join(outputDir, file);
|
|
69
|
-
fs.renameSync(srcPath, destPath);
|
|
70
|
-
});
|
|
71
|
-
} else {
|
|
72
|
-
throw Error("Cannot find the manifest.json file");
|
|
73
|
-
}
|
|
110
|
+
const req = https.get(targetUrl, options, (res) => {
|
|
111
|
+
const { statusCode, headers } = res;
|
|
112
|
+
|
|
113
|
+
// Follow redirects
|
|
114
|
+
if ([301, 302, 303, 307, 308].includes(statusCode) && headers.location) {
|
|
115
|
+
const newUrl = url.resolve(targetUrl, headers.location);
|
|
116
|
+
res.resume(); // discard body
|
|
117
|
+
return resolve(
|
|
118
|
+
fetchUrl(newUrl, options, redirectCount + 1, maxRedirects),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (statusCode !== 200) {
|
|
123
|
+
res.resume();
|
|
124
|
+
return reject(
|
|
125
|
+
new Error(`Request failed with status code ${statusCode}`),
|
|
126
|
+
);
|
|
74
127
|
}
|
|
128
|
+
|
|
129
|
+
const dataChunks = [];
|
|
130
|
+
res.on("data", (chunk) => dataChunks.push(chunk));
|
|
131
|
+
res.on("end", () => resolve(Buffer.concat(dataChunks)));
|
|
75
132
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.error(`Error downloading the file: ${err.message}`);
|
|
133
|
+
|
|
134
|
+
req.on("error", (err) => reject(err));
|
|
79
135
|
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------
|
|
139
|
+
// extractCrxToFolder
|
|
140
|
+
// 1) Checks "Cr24" magic
|
|
141
|
+
// 2) Reads version (2 or 3/4) to find the ZIP start
|
|
142
|
+
// 3) Uses parseZipCentralDirectory() to extract files properly
|
|
143
|
+
// ---------------------------------------------------------------------
|
|
144
|
+
function extractCrxToFolder(crxBuffer, outFolder) {
|
|
145
|
+
if (crxBuffer.toString("utf8", 0, 4) !== "Cr24") {
|
|
146
|
+
throw new Error("Not a valid CRX file (missing Cr24 magic).");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const version = crxBuffer.readUInt32LE(4);
|
|
150
|
+
let zipStartOffset = 0;
|
|
151
|
+
if (version === 2) {
|
|
152
|
+
const pkLen = crxBuffer.readUInt32LE(8);
|
|
153
|
+
const sigLen = crxBuffer.readUInt32LE(12);
|
|
154
|
+
zipStartOffset = 16 + pkLen + sigLen;
|
|
155
|
+
} else if (version === 3 || version === 4) {
|
|
156
|
+
const headerSize = crxBuffer.readUInt32LE(8);
|
|
157
|
+
zipStartOffset = 12 + headerSize;
|
|
158
|
+
} else {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Unsupported CRX version (${version}). Only v2, v3, or v4 are supported.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (zipStartOffset >= crxBuffer.length) {
|
|
165
|
+
throw new Error("Malformed CRX: header size exceeds file length.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const zipBuffer = crxBuffer.slice(zipStartOffset);
|
|
169
|
+
|
|
170
|
+
// Parse that ZIP via the central directory approach
|
|
171
|
+
parseZipCentralDirectory(zipBuffer, outFolder);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------
|
|
175
|
+
// parseZipCentralDirectory(buffer, outFolder)
|
|
176
|
+
// 1) Finds End of Central Directory (EOCD) record (0x06054b50).
|
|
177
|
+
// 2) Reads central directory for file metadata
|
|
178
|
+
// 3) For each file, decompress into outFolder
|
|
179
|
+
// ---------------------------------------------------------------------
|
|
180
|
+
function parseZipCentralDirectory(zipBuffer, outFolder) {
|
|
181
|
+
const eocdSig = 0x06054b50;
|
|
182
|
+
let eocdPos = -1;
|
|
183
|
+
const minPos = Math.max(0, zipBuffer.length - 65557);
|
|
184
|
+
for (let i = zipBuffer.length - 4; i >= minPos; i--) {
|
|
185
|
+
if (zipBuffer.readUInt32LE(i) === eocdSig) {
|
|
186
|
+
eocdPos = i;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (eocdPos < 0) {
|
|
191
|
+
throw new Error("Could not find End of Central Directory (EOCD) in ZIP.");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const totalCD = zipBuffer.readUInt16LE(eocdPos + 10);
|
|
195
|
+
const cdSize = zipBuffer.readUInt32LE(eocdPos + 12);
|
|
196
|
+
const cdOffset = zipBuffer.readUInt32LE(eocdPos + 16);
|
|
197
|
+
|
|
198
|
+
if (cdOffset + cdSize > zipBuffer.length) {
|
|
199
|
+
throw new Error("Central directory offset/size out of range.");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let ptr = cdOffset;
|
|
203
|
+
const files = [];
|
|
204
|
+
for (let i = 0; i < totalCD; i++) {
|
|
205
|
+
const sig = zipBuffer.readUInt32LE(ptr);
|
|
206
|
+
if (sig !== 0x02014b50) {
|
|
207
|
+
throw new Error(`Central directory signature mismatch at ${ptr}`);
|
|
208
|
+
}
|
|
209
|
+
ptr += 4;
|
|
210
|
+
|
|
211
|
+
/* const verMade = */ zipBuffer.readUInt16LE(ptr);
|
|
212
|
+
ptr += 2;
|
|
213
|
+
const verNeed = zipBuffer.readUInt16LE(ptr);
|
|
214
|
+
ptr += 2;
|
|
215
|
+
const flags = zipBuffer.readUInt16LE(ptr);
|
|
216
|
+
ptr += 2;
|
|
217
|
+
const method = zipBuffer.readUInt16LE(ptr);
|
|
218
|
+
ptr += 2;
|
|
219
|
+
/* const modTime = */ zipBuffer.readUInt16LE(ptr);
|
|
220
|
+
ptr += 2;
|
|
221
|
+
/* const modDate = */ zipBuffer.readUInt16LE(ptr);
|
|
222
|
+
ptr += 2;
|
|
223
|
+
const crc32 = zipBuffer.readUInt32LE(ptr);
|
|
224
|
+
ptr += 4;
|
|
225
|
+
const compSize = zipBuffer.readUInt32LE(ptr);
|
|
226
|
+
ptr += 4;
|
|
227
|
+
const unCompSize = zipBuffer.readUInt32LE(ptr);
|
|
228
|
+
ptr += 4;
|
|
229
|
+
const fLen = zipBuffer.readUInt16LE(ptr);
|
|
230
|
+
ptr += 2;
|
|
231
|
+
const xLen = zipBuffer.readUInt16LE(ptr);
|
|
232
|
+
ptr += 2;
|
|
233
|
+
const cLen = zipBuffer.readUInt16LE(ptr);
|
|
234
|
+
ptr += 2;
|
|
235
|
+
/* const diskNo = */ zipBuffer.readUInt16LE(ptr);
|
|
236
|
+
ptr += 2;
|
|
237
|
+
/* const intAttr = */ zipBuffer.readUInt16LE(ptr);
|
|
238
|
+
ptr += 2;
|
|
239
|
+
/* const extAttr = */ zipBuffer.readUInt32LE(ptr);
|
|
240
|
+
ptr += 4;
|
|
241
|
+
const localHeaderOffset = zipBuffer.readUInt32LE(ptr);
|
|
242
|
+
ptr += 4;
|
|
243
|
+
|
|
244
|
+
const filename = zipBuffer.toString("utf8", ptr, ptr + fLen);
|
|
245
|
+
ptr += fLen + xLen + cLen; // skip the extra + comment
|
|
246
|
+
|
|
247
|
+
files.push({
|
|
248
|
+
filename,
|
|
249
|
+
method,
|
|
250
|
+
compSize,
|
|
251
|
+
unCompSize,
|
|
252
|
+
flags,
|
|
253
|
+
localHeaderOffset,
|
|
254
|
+
crc32,
|
|
255
|
+
verNeed,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fs.mkdirSync(outFolder, { recursive: true });
|
|
260
|
+
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
const { filename, method, compSize, localHeaderOffset } = file;
|
|
263
|
+
|
|
264
|
+
if (filename.endsWith("/")) {
|
|
265
|
+
fs.mkdirSync(path.join(outFolder, filename), { recursive: true });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let lhPtr = localHeaderOffset;
|
|
270
|
+
const localSig = zipBuffer.readUInt32LE(lhPtr);
|
|
271
|
+
if (localSig !== 0x04034b50) {
|
|
272
|
+
throw new Error(`Local file header mismatch at ${lhPtr} for ${filename}`);
|
|
273
|
+
}
|
|
274
|
+
lhPtr += 4;
|
|
275
|
+
|
|
276
|
+
lhPtr += 2; // version needed
|
|
277
|
+
lhPtr += 2; // flags
|
|
278
|
+
lhPtr += 2; // method
|
|
279
|
+
lhPtr += 2; // mod time
|
|
280
|
+
lhPtr += 2; // mod date
|
|
281
|
+
lhPtr += 4; // crc32
|
|
282
|
+
lhPtr += 4; // comp size
|
|
283
|
+
lhPtr += 4; // uncomp size
|
|
284
|
+
const lhFNameLen = zipBuffer.readUInt16LE(lhPtr);
|
|
285
|
+
lhPtr += 2;
|
|
286
|
+
const lhXLen = zipBuffer.readUInt16LE(lhPtr);
|
|
287
|
+
lhPtr += 2;
|
|
288
|
+
|
|
289
|
+
lhPtr += lhFNameLen + lhXLen;
|
|
290
|
+
const fileData = zipBuffer.slice(lhPtr, lhPtr + compSize);
|
|
291
|
+
|
|
292
|
+
const outPath = path.join(outFolder, filename);
|
|
293
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
294
|
+
|
|
295
|
+
if (method === 0) {
|
|
296
|
+
fs.writeFileSync(outPath, fileData);
|
|
297
|
+
} else if (method === 8) {
|
|
298
|
+
const unzipped = zlib.inflateRawSync(fileData);
|
|
299
|
+
fs.writeFileSync(outPath, unzipped);
|
|
300
|
+
} else {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Unsupported compression method (${method}) for file ${filename}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|