Skip to content

Commit 94cb8bd

Browse files
feat: add file hashes to asar header (#221)
* feat: add file hashes to asar header * feat: add getRawHeader method to public API * chore: fix lint * chore: update docs * refactor: use integrity instead of hash pairs * feat: add block hashes * fix: ensure executables are extracted with executable permission * fix: ensure symlinks are not deeply resolved when packaging * chore: update test files * chore: remove DS_Store * perf: generate block hashes as we parse the stream * docs: update README with new options * revert * chore: update per feedback
1 parent 6fb376b commit 94cb8bd

17 files changed

+137
-4
lines changed

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,24 @@ Structure of `header` is something like this:
153153
"ls": {
154154
"offset": "0",
155155
"size": 100,
156-
"executable": true
156+
"executable": true,
157+
"integrity": {
158+
"algorithm": "SHA256",
159+
"hash": "...",
160+
"blockSize": 1024,
161+
"blocks": ["...", "..."]
162+
}
157163
},
158164
"cd": {
159165
"offset": "100",
160166
"size": 100,
161-
"executable": true
167+
"executable": true,
168+
"integrity": {
169+
"algorithm": "SHA256",
170+
"hash": "...",
171+
"blockSize": 1024,
172+
"blocks": ["...", "..."]
173+
}
162174
}
163175
}
164176
}
@@ -168,7 +180,13 @@ Structure of `header` is something like this:
168180
"files": {
169181
"hosts": {
170182
"offset": "200",
171-
"size": 32
183+
"size": 32,
184+
"integrity": {
185+
"algorithm": "SHA256",
186+
"hash": "...",
187+
"blockSize": 1024,
188+
"blocks": ["...", "..."]
189+
}
172190
}
173191
}
174192
}
@@ -187,6 +205,12 @@ precisely represent UINT64 in JavaScript `Number`. `size` is a JavaScript
187205
because file size in Node.js is represented as `Number` and it is not safe to
188206
convert `Number` to UINT64.
189207

208+
`integrity` is an object consisting of a few keys:
209+
* A hashing `algorithm`, currently only `SHA256` is supported.
210+
* A hex encoded `hash` value representing the hash of the entire file.
211+
* An array of hex encoded hashes for the `blocks` of the file. i.e. for a blockSize of 4KB this array contains the hash of every block if you split the file into N 4KB blocks.
212+
* A integer value `blockSize` representing the size in bytes of each block in the `blocks` hashes above
213+
190214
[pickle]: https://chromium.googlesource.com/chromium/src/+/master/base/pickle.h
191215
[node-pickle]: https://www.npmjs.org/package/chromium-pickle
192216
[grunt-asar]: https://github.com/bwin/grunt-asar

lib/asar.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ module.exports.statFile = function (archive, filename, followLinks) {
157157
return filesystem.getFile(filename, followLinks)
158158
}
159159

160+
module.exports.getRawHeader = function (archive) {
161+
return disk.readArchiveHeaderSync(archive)
162+
}
163+
160164
module.exports.listPackage = function (archive, options) {
161165
return disk.readFilesystemSync(archive).listFiles(options)
162166
}
@@ -199,6 +203,9 @@ module.exports.extractAll = function (archive, dest) {
199203
// it's a file, extract it
200204
const content = disk.readFileSync(filesystem, filename, file)
201205
fs.writeFileSync(destFilename, content)
206+
if (file.executable) {
207+
fs.chmodSync(destFilename, '755')
208+
}
202209
}
203210
}
204211
}

lib/crawlfs.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,21 @@ module.exports = async function (dir, options) {
2020
const metadata = {}
2121
const crawled = await glob(dir, options)
2222
const results = await Promise.all(crawled.map(async filename => [filename, await determineFileType(filename)]))
23+
const links = []
2324
const filenames = results.map(([filename, type]) => {
2425
if (type) {
2526
metadata[filename] = type
27+
if (type.type === 'link') links.push(filename)
2628
}
2729
return filename
30+
}).filter((filename) => {
31+
// Newer glob can return files inside symlinked directories, to avoid
32+
// those appearing in archives we need to manually exclude theme here
33+
const exactLinkIndex = links.findIndex(link => filename === link)
34+
return links.every((link, index) => {
35+
if (index === exactLinkIndex) return true
36+
return !filename.startsWith(link)
37+
})
2838
})
2939
return [filenames, metadata]
3040
}

lib/disk.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ module.exports.readArchiveHeaderSync = function (archive) {
7676

7777
const headerPickle = pickle.createFromBuffer(headerBuf)
7878
const header = headerPickle.createIterator().readString()
79-
return { header: JSON.parse(header), headerSize: size }
79+
return { headerString: header, header: JSON.parse(header), headerSize: size }
8080
}
8181

8282
module.exports.readFilesystemSync = function (archive) {

lib/filesystem.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const os = require('os')
55
const path = require('path')
66
const { promisify } = require('util')
77
const stream = require('stream')
8+
const getFileIntegrity = require('./integrity')
89

910
const UINT32_MAX = 2 ** 32 - 1
1011

@@ -57,6 +58,7 @@ class Filesystem {
5758
if (shouldUnpack || dirNode.unpacked) {
5859
node.size = file.stat.size
5960
node.unpacked = true
61+
node.integrity = await getFileIntegrity(p)
6062
return Promise.resolve()
6163
}
6264

@@ -86,6 +88,7 @@ class Filesystem {
8688

8789
node.size = size
8890
node.offset = this.offset.toString()
91+
node.integrity = await getFileIntegrity(p)
8992
if (process.platform !== 'win32' && (file.stat.mode & 0o100)) {
9093
node.executable = true
9194
}

lib/index.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,29 @@ export type InputMetadata = {
4444
}
4545
};
4646

47+
export type DirectoryRecord = {
48+
files: Record<string, DirectoryRecord | FileRecord>;
49+
};
50+
51+
export type FileRecord = {
52+
offset: string;
53+
size: number;
54+
executable?: boolean;
55+
integrity: {
56+
hash: string;
57+
algorithm: 'SHA256';
58+
blocks: string[];
59+
blockSize: number;
60+
};
61+
}
62+
63+
export type ArchiveHeader = {
64+
// The JSON parsed header string
65+
header: DirectoryRecord;
66+
headerString: string;
67+
headerSize: number;
68+
}
69+
4770
export function createPackage(src: string, dest: string): Promise<void>;
4871
export function createPackageWithOptions(
4972
src: string,
@@ -59,6 +82,7 @@ export function createPackageFromFiles(
5982
): Promise<void>;
6083

6184
export function statFile(archive: string, filename: string, followLinks?: boolean): Metadata;
85+
export function getRawHeader(archive: string): ArchiveHeader;
6286
export function listPackage(archive: string, options?: ListOptions): string[];
6387
export function extractFile(archive: string, filename: string): Buffer;
6488
export function extractAll(archive: string, dest: string): void;

lib/integrity.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const crypto = require('crypto')
2+
const fs = require('fs')
3+
const stream = require('stream')
4+
const { promisify } = require('util')
5+
6+
const ALGORITHM = 'SHA256'
7+
// 4MB default block size
8+
const BLOCK_SIZE = 4 * 1024 * 1024
9+
10+
const pipeline = promisify(stream.pipeline)
11+
12+
function hashBlock (block) {
13+
return crypto.createHash(ALGORITHM).update(block).digest('hex')
14+
}
15+
16+
async function getFileIntegrity (path) {
17+
const fileHash = crypto.createHash(ALGORITHM)
18+
19+
const blocks = []
20+
let currentBlockSize = 0
21+
let currentBlock = []
22+
23+
await pipeline(
24+
fs.createReadStream(path),
25+
new stream.PassThrough({
26+
decodeStrings: false,
27+
transform (_chunk, encoding, callback) {
28+
fileHash.update(_chunk)
29+
30+
function handleChunk (chunk) {
31+
const diffToSlice = Math.min(BLOCK_SIZE - currentBlockSize, chunk.byteLength)
32+
currentBlockSize += diffToSlice
33+
currentBlock.push(chunk.slice(0, diffToSlice))
34+
if (currentBlockSize === BLOCK_SIZE) {
35+
blocks.push(hashBlock(Buffer.concat(currentBlock)))
36+
currentBlock = []
37+
currentBlockSize = 0
38+
}
39+
if (diffToSlice < chunk.byteLength) {
40+
handleChunk(chunk.slice(diffToSlice))
41+
}
42+
}
43+
handleChunk(_chunk)
44+
callback()
45+
},
46+
flush (callback) {
47+
blocks.push(hashBlock(Buffer.concat(currentBlock)))
48+
currentBlock = []
49+
callback()
50+
}
51+
})
52+
)
53+
54+
return {
55+
algorithm: ALGORITHM,
56+
hash: fileHash.digest('hex'),
57+
blockSize: BLOCK_SIZE,
58+
blocks: blocks
59+
}
60+
}
61+
62+
module.exports = getFileIntegrity
1.21 KB
Binary file not shown.
1.21 KB
Binary file not shown.
412 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)