Skip to content

Commit 2f7ed7a

Browse files
committed
Support a per-source integrity hash
`.src()` takes an optional 4th `hash` argument. It's forwarded to the downloader, which verifies the download before extraction.
1 parent c87f6b5 commit 2f7ed7a

4 files changed

Lines changed: 65 additions & 9 deletions

File tree

index.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import osFilterObject from '@xhmikosr/os-filter-obj';
1717
* @property {string} url - The URL of the file.
1818
* @property {string} [os] - The operating system the file is for.
1919
* @property {string} [arch] - The architecture the file is for.
20+
* @property {string} [hash] - Expected hash as `"<algorithm>:<hex>"`, verified after download.
2021
*/
2122

2223
export default class BinWrapper {
@@ -39,15 +40,21 @@ export default class BinWrapper {
3940
* @param {string} [src] - The source URL of the file.
4041
* @param {string} [os] - The operating system the file is for.
4142
* @param {string} [arch] - The architecture the file is for.
43+
* @param {string} [hash] - Expected hash as `"<algorithm>:<hex>"`, verified after download.
4244
* @returns {SourceFile[]|undefined|this} - Returns the source files if no arguments are provided, otherwise returns `this`.
4345
*/
44-
src(src, os, arch) {
46+
src(src, os, arch, hash) {
4547
if (arguments.length === 0) {
4648
return this._src;
4749
}
4850

4951
this._src ||= [];
50-
this._src.push({url: src, os, arch});
52+
this._src.push({
53+
url: src,
54+
os,
55+
arch,
56+
hash,
57+
});
5158

5259
return this;
5360
}
@@ -109,13 +116,22 @@ export default class BinWrapper {
109116
return path.join(this.dest(), this.use());
110117
}
111118

119+
/**
120+
* Filter the configured sources down to the ones matching the current OS and arch
121+
*
122+
* @returns {SourceFile[]}
123+
*/
124+
#resolveSources() {
125+
return osFilterObject(this.src() || []);
126+
}
127+
112128
/**
113129
* Get the source URLs matching the current OS and arch
114130
*
115131
* @returns {string[]}
116132
*/
117133
resolvedUrls() {
118-
return osFilterObject(this.src() || []).map(file => file.url);
134+
return this.#resolveSources().map(file => file.url);
119135
}
120136

121137
/**
@@ -177,15 +193,16 @@ export default class BinWrapper {
177193
* @api private
178194
*/
179195
async download() {
180-
const urls = this.resolvedUrls();
196+
const sources = this.#resolveSources();
181197

182-
if (urls.length === 0) {
198+
if (sources.length === 0) {
183199
throw new Error('No binary found matching your system. It\'s probably not supported.');
184200
}
185201

186-
const results = await Promise.all(urls.map(url =>
187-
downloader(url, this.dest(), {
202+
const results = await Promise.all(sources.map(source =>
203+
downloader(source.url, this.dest(), {
188204
extract: true,
205+
hash: source.hash,
189206
decompress: {
190207
...this.options.decompress,
191208
strip: this.options.strip,
@@ -197,7 +214,7 @@ export default class BinWrapper {
197214
return item.map(file => file.path);
198215
}
199216

200-
const parsedUrl = new URL(urls[index]);
217+
const parsedUrl = new URL(sources[index].url);
201218

202219
return path.parse(parsedUrl.pathname).base;
203220
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
},
5858
"xo": {
5959
"rules": {
60+
"require-unicode-regexp": "off",
6061
"unicorn/prevent-abbreviations": "off"
6162
}
6263
}

readme.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ Strip a number of leading paths from file names on extraction.
7171
Extra options forwarded to [`@xhmikosr/decompress`](https://github.com/XhmikosR/decompress) e.g. `plugins`.
7272
The `strip` key is ignored here; use the top-level `strip` option instead.
7373

74-
### .src(url, [os], [arch])
74+
### .src(url, [os], [arch], [hash])
7575

7676
Adds a source to download.
7777

@@ -93,6 +93,17 @@ Type: `string`
9393

9494
Tie the source to a specific arch.
9595

96+
#### hash
97+
98+
Type: `string`
99+
100+
Expected `"<algorithm>:<hex>"` digest, verified against the download before extraction (see
101+
[`@xhmikosr/downloader`](https://github.com/XhmikosR/download#optionshash)).
102+
103+
```js
104+
bin.src(`${base}/linux/x64/gifsicle`, 'linux', 'x64', 'sha256:9f86d0...');
105+
```
106+
96107
### .dest(destination)
97108

98109
#### destination

test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {createHash} from 'node:crypto';
12
import fs, {promises as fsP} from 'node:fs';
23
import path from 'node:path';
34
import process from 'node:process';
@@ -92,6 +93,32 @@ test('resolvedUrls returns empty when no source is set', t => {
9293
t.deepEqual(new BinWrapper().resolvedUrls(), []);
9394
});
9495

96+
test('download a source matching its hash', async t => {
97+
const temporaryDir = temporaryDirectory();
98+
const archive = await fsP.readFile(fixture(`gifsicle-${process.platform}.tar.gz`));
99+
const hash = `sha256:${createHash('sha256').update(archive).digest('hex')}`;
100+
const bin = new BinWrapper({skipCheck: true})
101+
.src('http://foo.com/gifsicle.tar.gz', undefined, undefined, hash)
102+
.dest(temporaryDir)
103+
.use(binary);
104+
105+
await bin.run();
106+
t.true(await pathExists(bin.path()));
107+
await removeDir(temporaryDir);
108+
});
109+
110+
test('throw when a source does not match its hash', async t => {
111+
const temporaryDir = temporaryDirectory();
112+
const wrongHash = `sha256:${'0'.repeat(64)}`;
113+
const bin = new BinWrapper({skipCheck: true})
114+
.src('http://foo.com/gifsicle.tar.gz', undefined, undefined, wrongHash)
115+
.dest(temporaryDir)
116+
.use(binary);
117+
118+
await t.throwsAsync(bin.run(), {message: /Hash mismatch/});
119+
await removeDir(temporaryDir);
120+
});
121+
95122
test('verify that a binary is working', async t => {
96123
const temporaryDir = temporaryDirectory();
97124
const bin = new BinWrapper()

0 commit comments

Comments
 (0)