Skip to content

Commit 1dd5a86

Browse files
authored
feat: Move to yauzl to fix FD error (#264)
`unzipper` would throw an FD error occasionally. Relevant issue: ZJONSSON/node-unzipper#104 There is a pending PR, but it seemed simpler to simply move to `yauzl`. The error: ``` events.js:174 throw er; // Unhandled 'error' event ^ Error: EBADF: bad file descriptor, read Emitted 'error' event at: at lazyFs.read (internal/fs/streams.js:165:12) at FSReqWrap.wrapper [as oncomplete] (fs.js:467:17) ```
1 parent 768b738 commit 1dd5a86

File tree

8 files changed

+632
-350
lines changed

8 files changed

+632
-350
lines changed

modules/integration-browser/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"@trust/keyto": "^0.3.7",
2323
"@types/got": "^9.6.2",
2424
"@types/stream-to-promise": "^2.2.0",
25-
"@types/unzipper": "^0.10.2",
25+
"@types/yauzl": "^2.9.1",
2626
"@types/yargs": "^15.0.3",
2727
"got": "^10.6.0",
2828
"jasmine-core": "^3.4.0",
@@ -34,7 +34,7 @@
3434
"puppeteer": "^1.14.0",
3535
"stream-to-promise": "^2.2.0",
3636
"tslib": "^1.9.3",
37-
"unzipper": "^0.9.11",
37+
"yauzl": "^2.10.0",
3838
"webpack": "^4.30.0",
3939
"yargs": "^13.2.2"
4040
},

modules/integration-browser/src/build_decrypt_fixtures.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Open } from 'unzipper'
17+
import {
18+
open,
19+
Entry, // eslint-disable-line no-unused-vars
20+
ZipFile // eslint-disable-line no-unused-vars
21+
} from 'yauzl'
1822
import streamToPromise from 'stream-to-promise'
1923
import { writeFileSync } from 'fs'
24+
import { Readable } from 'stream' // eslint-disable-line no-unused-vars
2025

2126
import { DecryptManifestList } from './types' // eslint-disable-line no-unused-vars
2227

@@ -30,23 +35,22 @@ import { DecryptManifestList } from './types' // eslint-disable-line no-unused-v
3035
export async function buildDecryptFixtures (fixtures: string, vectorFile: string, testName?: string, slice?: string) {
3136
const [start = 0, end = 9999] = (slice || '').split(':').map(n => parseInt(n, 10))
3237

33-
const centralDirectory = await Open.file(vectorFile)
34-
const filesMap = new Map(centralDirectory.files.map(file => [file.path, file]))
38+
const filesMap = await centralDirectory(vectorFile)
3539

3640
const readUriOnce = (() => {
3741
const cache = new Map()
3842
return async (uri: string) => {
3943
const has = cache.get(uri)
4044
if (has) return has
41-
const fileInfo = filesMap.get(testUri2Path(uri))
45+
const fileInfo = filesMap.get(uri)
4246
if (!fileInfo) throw new Error(`${uri} does not exist`)
43-
const buffer = await fileInfo.buffer()
47+
const buffer = await streamToPromise(await fileInfo.stream())
4448
cache.set(uri, buffer)
4549
return buffer
4650
}
4751
})()
4852

49-
const manifestBuffer = await readUriOnce('manifest.json')
53+
const manifestBuffer = await readUriOnce('file://manifest.json')
5054
const { keys: keysFile, tests }: DecryptManifestList = JSON.parse(manifestBuffer.toString('utf8'))
5155
const keysBuffer = await readUriOnce(keysFile)
5256
const { keys } = JSON.parse(keysBuffer.toString('utf8'))
@@ -68,12 +72,12 @@ export async function buildDecryptFixtures (fixtures: string, vectorFile: string
6872
testNames.push(name)
6973

7074
const { plaintext: plaintextFile, ciphertext, 'master-keys': masterKeys } = testInfo
71-
const plainTextInfo = filesMap.get(testUri2Path(plaintextFile))
72-
const cipherInfo = filesMap.get(testUri2Path(ciphertext))
75+
const plainTextInfo = filesMap.get(plaintextFile)
76+
const cipherInfo = filesMap.get(ciphertext)
7377
if (!cipherInfo || !plainTextInfo) throw new Error(`no file for ${name}: ${ciphertext} | ${plaintextFile}`)
7478

75-
const cipherText = await streamToPromise(<NodeJS.ReadableStream>cipherInfo.stream())
76-
const plainText = await readUriOnce(plainTextInfo.path)
79+
const cipherText = await streamToPromise(await cipherInfo.stream())
80+
const plainText = await readUriOnce(`file://${plainTextInfo.fileName}`)
7781
const keysInfo = masterKeys.map(keyInfo => {
7882
const key = keys[keyInfo.key]
7983
if (!key) throw new Error(`no key for ${name}`)
@@ -83,7 +87,7 @@ export async function buildDecryptFixtures (fixtures: string, vectorFile: string
8387
const test = JSON.stringify({
8488
name,
8589
keysInfo,
86-
cipherFile: cipherInfo.path,
90+
cipherFile: cipherInfo.fileName,
8791
cipherText: cipherText.toString('base64'),
8892
plainText: plainText.toString('base64')
8993
})
@@ -94,6 +98,38 @@ export async function buildDecryptFixtures (fixtures: string, vectorFile: string
9498
writeFileSync(`${fixtures}/decrypt_tests.json`, JSON.stringify(testNames))
9599
}
96100

97-
function testUri2Path (uri: string) {
98-
return uri.replace('file://', '')
101+
interface StreamEntry extends Entry {
102+
stream: () => Promise<Readable>
103+
}
104+
105+
function centralDirectory (vectorFile: string): Promise<Map<string, StreamEntry>> {
106+
const filesMap = new Map<string, StreamEntry>()
107+
return new Promise((resolve, reject) => {
108+
open(vectorFile, { lazyEntries: true, autoClose: false }, (err, zipfile) => {
109+
if (err || !zipfile) return reject(err)
110+
111+
zipfile
112+
.on('entry', (entry: StreamEntry) => {
113+
entry.stream = curryStream(zipfile, entry)
114+
filesMap.set(`file://${entry.fileName}`, entry)
115+
zipfile.readEntry()
116+
})
117+
.on('end', () => {
118+
resolve(filesMap)
119+
})
120+
.on('error', (err) => reject(err))
121+
.readEntry()
122+
})
123+
})
124+
}
125+
126+
function curryStream (zipfile: ZipFile, entry: Entry) {
127+
return function stream (): Promise<Readable> {
128+
return new Promise((resolve, reject) => {
129+
zipfile.openReadStream(entry, (err, readStream) => {
130+
if (err || !readStream) return reject(err)
131+
resolve(readStream)
132+
})
133+
})
134+
}
99135
}

modules/integration-node/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
"license": "Apache-2.0",
1616
"dependencies": {
1717
"@aws-crypto/client-node": "file:../client-node",
18-
"@types/got": "^9.6.2",
19-
"@types/unzipper": "^0.10.2",
18+
"@types/got": "^9.6.9",
19+
"@types/yauzl": "^2.9.1",
2020
"@types/yargs": "^15.0.3",
21+
"@types/stream-to-promise": "^2.2.0",
2122
"got": "^10.6.0",
2223
"tslib": "^1.9.3",
23-
"unzipper": "^0.9.11",
24+
"yauzl": "^2.10.0",
25+
"stream-to-promise": "^2.2.0",
2426
"yargs": "^13.2.2"
2527
},
2628
"sideEffects": false,

modules/integration-node/src/get_decrypt_test_iterator.ts

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,52 +14,54 @@
1414
*/
1515

1616
import {
17-
Open,
18-
File // eslint-disable-line no-unused-vars
19-
} from 'unzipper'
17+
open,
18+
Entry, // eslint-disable-line no-unused-vars
19+
ZipFile // eslint-disable-line no-unused-vars
20+
} from 'yauzl'
2021
import {
2122
DecryptManifestList, // eslint-disable-line no-unused-vars
2223
KeyList, // eslint-disable-line no-unused-vars
2324
KeyInfoTuple // eslint-disable-line no-unused-vars
2425
} from './types'
2526
import { Readable } from 'stream' // eslint-disable-line no-unused-vars
27+
import streamToPromise from 'stream-to-promise'
2628

2729
export async function getDecryptTestVectorIterator (vectorFile: string) {
28-
const centralDirectory = await Open.file(vectorFile)
29-
// @ts-ignore
30-
const filesMap = new Map(centralDirectory.files.map(file => [file.path, file]))
30+
const filesMap = await centralDirectory(vectorFile)
3131

3232
return _getDecryptTestVectorIterator(filesMap)
3333
}
3434

3535
/* Just a simple more testable function */
36-
export async function _getDecryptTestVectorIterator (filesMap: Map<string, File>) {
36+
export async function _getDecryptTestVectorIterator (filesMap: Map<string, StreamEntry>) {
3737
const readUriOnce = (() => {
3838
const cache: Map<string, Buffer> = new Map()
39-
return async (uri: string) => {
39+
return async (uri: string): Promise<Buffer> => {
4040
const has = cache.get(uri)
4141
if (has) return has
42-
const fileInfo = filesMap.get(testUri2Path(uri))
42+
const fileInfo = filesMap.get(uri)
4343
if (!fileInfo) throw new Error(`${uri} does not exist`)
44-
const buffer = await fileInfo.buffer()
44+
const stream = await fileInfo.stream()
45+
46+
const buffer = await streamToPromise(stream)
4547
cache.set(uri, buffer)
4648
return buffer
4749
}
4850
})()
4951

50-
const manifestBuffer = await readUriOnce('manifest.json')
52+
const manifestBuffer = await readUriOnce('file://manifest.json')
5153
const { keys: keysFile, tests }: DecryptManifestList = JSON.parse(manifestBuffer.toString('utf8'))
5254
const keysBuffer = await readUriOnce(keysFile)
5355
const { keys }: KeyList = JSON.parse(keysBuffer.toString('utf8'))
5456

5557
return (function * nextTest (): IterableIterator<TestVectorInfo> {
5658
for (const [name, testInfo] of Object.entries(tests)) {
5759
const { plaintext: plaintextFile, ciphertext, 'master-keys': masterKeys } = testInfo
58-
const plainTextInfo = filesMap.get(testUri2Path(plaintextFile))
59-
const cipherInfo = filesMap.get(testUri2Path(ciphertext))
60-
if (!cipherInfo || !plainTextInfo) throw new Error(`no file for ${name}: ${testUri2Path(ciphertext)} | ${testUri2Path(plaintextFile)}`)
61-
const cipherStream = cipherInfo.stream()
62-
const plainTextStream = plainTextInfo.stream()
60+
const plainTextInfo = filesMap.get(plaintextFile)
61+
const cipherInfo = filesMap.get(ciphertext)
62+
if (!cipherInfo || !plainTextInfo) throw new Error(`no file for ${name}: ${ciphertext} | ${plaintextFile}`)
63+
const cipherStream = cipherInfo.stream
64+
const plainTextStream = plainTextInfo.stream
6365
const keysInfo = <KeyInfoTuple[]>masterKeys.map(keyInfo => {
6466
const key = keys[keyInfo.key]
6567
if (!key) throw new Error(`no key for ${name}`)
@@ -76,13 +78,45 @@ export async function _getDecryptTestVectorIterator (filesMap: Map<string, File>
7678
})()
7779
}
7880

79-
function testUri2Path (uri: string) {
80-
return uri.replace('file://', '')
81-
}
82-
8381
export interface TestVectorInfo {
8482
name: string,
8583
keysInfo: KeyInfoTuple[],
86-
cipherStream: Readable
87-
plainTextStream: Readable
84+
cipherStream: () => Promise<Readable>
85+
plainTextStream: () => Promise<Readable>
86+
}
87+
88+
interface StreamEntry extends Entry {
89+
stream: () => Promise<Readable>
90+
}
91+
92+
function centralDirectory (vectorFile: string): Promise<Map<string, StreamEntry>> {
93+
const filesMap = new Map<string, StreamEntry>()
94+
return new Promise((resolve, reject) => {
95+
open(vectorFile, { lazyEntries: true, autoClose: false }, (err, zipfile) => {
96+
if (err || !zipfile) return reject(err)
97+
98+
zipfile
99+
.on('entry', (entry: StreamEntry) => {
100+
entry.stream = curryStream(zipfile, entry)
101+
filesMap.set('file://' + entry.fileName, entry)
102+
zipfile.readEntry()
103+
})
104+
.on('end', () => {
105+
resolve(filesMap)
106+
})
107+
.on('error', (err) => reject(err))
108+
.readEntry()
109+
})
110+
})
111+
}
112+
113+
function curryStream (zipfile: ZipFile, entry: Entry) {
114+
return function stream (): Promise<Readable> {
115+
return new Promise((resolve, reject) => {
116+
zipfile.openReadStream(entry, (err, readStream) => {
117+
if (err || !readStream) return reject(err)
118+
resolve(readStream)
119+
})
120+
})
121+
}
88122
}

modules/integration-node/src/integration_tests.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { decryptMaterialsManagerNode, encryptMaterialsManagerNode } from './decr
2525
import { decrypt, encrypt, needs } from '@aws-crypto/client-node'
2626
import { URL } from 'url'
2727
import got from 'got'
28+
import streamToPromise from 'stream-to-promise'
2829

2930
const notSupportedDecryptMessages = [
3031
'Not supported at this time.'
@@ -39,18 +40,17 @@ const notSupportedEncryptMessages = [
3940
export async function testDecryptVector ({ name, keysInfo, plainTextStream, cipherStream }: TestVectorInfo): Promise<TestVectorResults> {
4041
try {
4142
const cmm = decryptMaterialsManagerNode(keysInfo)
42-
const knowGood: Buffer[] = []
43-
plainTextStream.on('data', (chunk: Buffer) => knowGood.push(chunk))
44-
const { plaintext } = await decrypt(cmm, cipherStream)
45-
const result = Buffer.concat(knowGood).equals(plaintext)
43+
const knowGood = await streamToPromise(await plainTextStream())
44+
const { plaintext } = await decrypt(cmm, await cipherStream())
45+
const result = knowGood.equals(plaintext)
4646
return { result, name }
4747
} catch (err) {
4848
return { result: false, name, err }
4949
}
5050
}
5151

5252
// This is only viable for small streams, if we start get get larger streams, an stream equality should get written
53-
export async function testEncryptVector ({ name, keysInfo, encryptOp, plainTextData }: EncryptTestVectorInfo, decryptOracle: URL): Promise<TestVectorResults> {
53+
export async function testEncryptVector ({ name, keysInfo, encryptOp, plainTextData }: EncryptTestVectorInfo, decryptOracle: string): Promise<TestVectorResults> {
5454
try {
5555
const cmm = encryptMaterialsManagerNode(keysInfo)
5656
const { result: encryptResult } = await encrypt(cmm, plainTextData, encryptOp)
@@ -61,7 +61,7 @@ export async function testEncryptVector ({ name, keysInfo, encryptOp, plainTextD
6161
'Accept': 'application/octet-stream'
6262
},
6363
body: encryptResult,
64-
encoding: null
64+
responseType: 'buffer'
6565
})
6666
needs(decryptResponse.statusCode === 200, 'decrypt failure')
6767
const { body } = decryptResponse
@@ -97,7 +97,7 @@ export async function integrationDecryptTestVectors (vectorFile: string, tolerat
9797
}
9898

9999
export async function integrationEncryptTestVectors (manifestFile: string, keyFile: string, decryptOracle: string, tolerateFailures: number = 0, testName?: string, concurrency: number = 1) {
100-
const decryptOracleUrl = new URL(decryptOracle)
100+
const decryptOracleUrl = new URL(decryptOracle).toString()
101101
const tests = await getEncryptTestVectorIterator(manifestFile, keyFile)
102102

103103
return parallelTests(concurrency, tolerateFailures, runTest, tests)

modules/integration-node/test/get_decrypt_test_iterator.test.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
_getDecryptTestVectorIterator
2222
} from '../src/index'
2323
import { DecryptManifestList, KeyList } from '../src/types' // eslint-disable-line no-unused-vars
24+
import { PassThrough } from 'stream'
2425

2526
const keyList: KeyList = {
2627
'manifest': {
@@ -63,29 +64,33 @@ const manifest:DecryptManifestList = {
6364

6465
const filesMap = new Map([
6566
[
66-
'manifest.json', {
67-
async buffer () {
68-
return Buffer.from(JSON.stringify(manifest))
67+
'file://manifest.json', {
68+
async stream () {
69+
const stream = new PassThrough()
70+
setImmediate(() => stream.end(Buffer.from(JSON.stringify(manifest))))
71+
return stream
6972
}
7073
} as any
7174
],
7275
[
73-
'keys.json', {
74-
async buffer () {
75-
return Buffer.from(JSON.stringify(keyList))
76+
'file://keys.json', {
77+
async stream () {
78+
const stream = new PassThrough()
79+
setImmediate(() => stream.end(Buffer.from(JSON.stringify(keyList))))
80+
return stream
7681
}
7782
} as any
7883
],
7984
[
80-
'ciphertexts/460bd892-c137-4178-8201-4ab5ee5d3041', {
81-
stream () {
85+
'file://ciphertexts/460bd892-c137-4178-8201-4ab5ee5d3041', {
86+
async stream () {
8287
return {} as any
8388
}
8489
} as any
8590
],
8691
[
87-
'plaintexts/small', {
88-
stream () {
92+
'file://plaintexts/small', {
93+
async stream () {
8994
return {} as any
9095
}
9196
} as any

0 commit comments

Comments
 (0)