Skip to content

Commit abd843e

Browse files
indutnyindutny-signal
authored andcommitted
lib: use Writable#_final for 'finish' tracking
Instead overriding `emit` method and using `this._realFinish` to detect unexpected end of data - implement `_final` method and track the logic there. The only behavior change from the use of `_final` is that we would no longer emit `finish` when the input data is terminated early. Instead we would emit `error` as before and stop the processing. Note that this is the behavior provided by `stream.Writable`, and it is thus conformant with the specification. Additionally, replace uses of private `_events` property with either straight `emit()` with return value check, or `listenerCount()` in the situations where there might be an overhead from the constructed event arguments. Replace `emit('error', error)` with `destroy(error)` calls as well, as otherwise the behavior is undefined. Fix: #26
1 parent c64ada8 commit abd843e

3 files changed

Lines changed: 101 additions & 75 deletions

File tree

lib/Dicer.js

Lines changed: 38 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -48,44 +48,8 @@ class Dicer extends Writable {
4848
this._part.emit('header', header);
4949
});
5050
this._hparser.on('error', (err) => {
51-
if (this._part && !this._ignoreData) {
52-
this._part.emit('error', err);
53-
this._part.push(null);
54-
}
55-
});
56-
}
57-
58-
emit(ev) {
59-
if (ev !== 'finish' || this._realFinish) {
60-
Writable.prototype.emit.apply(this, arguments);
61-
return;
62-
}
63-
64-
if (this._finished)
65-
return;
66-
67-
process.nextTick(() => {
68-
this.emit('error', new Error('Unexpected end of multipart data'));
69-
70-
if (this._part && !this._ignoreData) {
71-
const type = (this._isPreamble ? 'Preamble' : 'Part');
72-
this._part.emit(
73-
'error',
74-
new Error(`${type} terminated early due to `
75-
+ 'unexpected end of multipart data')
76-
);
77-
this._part.push(null);
78-
process.nextTick(() => {
79-
this._realFinish = true;
80-
this.emit('finish');
81-
this._realFinish = false;
82-
});
83-
return;
84-
}
85-
86-
this._realFinish = true;
87-
this.emit('finish');
88-
this._realFinish = false;
51+
if (this._part && !this._ignoreData)
52+
this._part.destroy(err);
8953
});
9054
}
9155

@@ -97,9 +61,7 @@ class Dicer extends Writable {
9761
if (this._headerFirst && this._isPreamble) {
9862
if (!this._part) {
9963
this._part = new PartStream(this._partOpts);
100-
if (this._events.preamble)
101-
this.emit('preamble', this._part);
102-
else
64+
if (!this.emit('preamble', this._part))
10365
ignore(this);
10466
}
10567
const r = this._hparser.push(data);
@@ -123,6 +85,34 @@ class Dicer extends Writable {
12385
cb();
12486
}
12587

88+
_final(cb) {
89+
if (this._finished) {
90+
if (this._parts !== 0) {
91+
this._pause = true;
92+
this._cb = cb;
93+
return;
94+
}
95+
96+
cb();
97+
return;
98+
}
99+
100+
if (this._part && !this._ignoreData) {
101+
const type = (this._isPreamble ? 'Preamble' : 'Part');
102+
this._part.destroy(
103+
new Error(`${type} terminated early due to `
104+
+ 'unexpected end of multipart data')
105+
);
106+
ignore(this);
107+
}
108+
109+
// Node <= 12 compatibility, otherwise `part`'s 'error'/'end' have no chance
110+
// to emit.
111+
process.nextTick(() => {
112+
cb(new Error('Unexpected end of multipart data'));
113+
});
114+
}
115+
126116
reset() {
127117
this._part = undefined;
128118
this._bparser = undefined;
@@ -154,16 +144,14 @@ function onInfo(isMatch, data, start, end) {
154144
}
155145
}
156146
if (this._dashes === 2) {
157-
if ((start + i) < end && this._events.trailer)
147+
if ((start + i) < end && this.listenerCount('trailer'))
158148
this.emit('trailer', data.slice(start + i, end));
159149
this.reset();
160150
this._finished = true;
161151
// No more parts will be added
162-
if (this._parts === 0) {
163-
this._realFinish = true;
164-
this.emit('finish');
165-
this._realFinish = false;
166-
}
152+
if (this._parts === 0)
153+
unpause(this);
154+
167155
}
168156
if (this._dashes)
169157
return;
@@ -176,9 +164,7 @@ function onInfo(isMatch, data, start, end) {
176164
unpause(this);
177165
};
178166
ev = this._isPreamble ? 'preamble' : 'part';
179-
if (this._events[ev])
180-
this.emit(ev, this._part);
181-
else
167+
if (!this.emit(ev, this._part))
182168
ignore(this);
183169
if (!this._isPreamble)
184170
this._inHeader = true;
@@ -205,15 +191,8 @@ function onInfo(isMatch, data, start, end) {
205191
} else {
206192
++this._parts;
207193
this._part.on('end', () => {
208-
if (--this._parts === 0) {
209-
if (this._finished) {
210-
this._realFinish = true;
211-
this.emit('finish');
212-
this._realFinish = false;
213-
} else {
214-
unpause(this);
215-
}
216-
}
194+
if (--this._parts === 0)
195+
unpause(this);
217196
});
218197
}
219198
this._part.push(null);

test/test-multipart.js

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ function next() {
8484
header: undefined
8585
};
8686

87+
const onPreambleEnd = () => {
88+
if (preamble.body)
89+
preamble.body = Buffer.concat(preamble.body, preamble.bodylen);
90+
if (preamble.body || preamble.header)
91+
state.preamble = preamble;
92+
};
93+
8794
p.on('header', (h) => {
8895
preamble.header = h;
8996
if (v.setBoundary)
@@ -100,12 +107,8 @@ function next() {
100107
preamble.bodylen += data.length;
101108
}).on('error', (err) => {
102109
preamble.error = err;
103-
}).on('end', () => {
104-
if (preamble.body)
105-
preamble.body = Buffer.concat(preamble.body, preamble.bodylen);
106-
if (preamble.body || preamble.header)
107-
state.preamble = preamble;
108-
});
110+
onPreambleEnd();
111+
}).on('end', onPreambleEnd);
109112
});
110113
dicer.on('part', (p) => {
111114
const part = {
@@ -115,6 +118,12 @@ function next() {
115118
header: undefined
116119
};
117120

121+
const onPartEnd = () => {
122+
if (part.body)
123+
part.body = Buffer.concat(part.body, part.bodylen);
124+
state.parts.push(part);
125+
};
126+
118127
p.on('header', (h) => {
119128
part.header = h;
120129
}).on('data', (data) => {
@@ -126,15 +135,23 @@ function next() {
126135
}).on('error', (err) => {
127136
part.error = err;
128137
++partErrors;
129-
}).on('end', () => {
130-
if (part.body)
131-
part.body = Buffer.concat(part.body, part.bodylen);
132-
state.parts.push(part);
133-
});
134-
}).on('error', (err) => {
135-
error = err;
136-
}).on('finish', () => {
137-
assert(finishes++ === 0, makeMsg(v.what, 'finish emitted multiple times'));
138+
onPartEnd();
139+
}).on('end', onPartEnd);
140+
}).on('error', onFinish).on('finish', onFinish);
141+
142+
function onFinish(err) {
143+
if (err) {
144+
assert(
145+
error === undefined,
146+
makeMsg(v.what, 'error emitted multiple times')
147+
);
148+
error = err;
149+
}
150+
151+
// Node <= 12 emits both 'error' and 'end', while Node > 14 emits only
152+
// 'error'.
153+
if (finishes++ > 0)
154+
return;
138155

139156
if (v.dicerError) {
140157
assert(error !== undefined, makeMsg(v.what, 'Expected error'));
@@ -242,7 +259,7 @@ function next() {
242259
}
243260
++t;
244261
next();
245-
});
262+
}
246263

247264
fs.createReadStream(fixtureBase + '/original').pipe(dicer);
248265
}

test/test-pipeline.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
5+
const { Readable, pipeline } = require('stream');
6+
const Dicer = require('..');
7+
8+
const r = new Readable({ read() {} });
9+
const d = new Dicer({ boundary: 'a' });
10+
11+
let isFinished = false;
12+
13+
d.on('part', async (part) => {
14+
part.resume();
15+
});
16+
17+
r.push('--a\r\nA: 1\r\nB: 1\r\n\r\n123\r\n--a\r\n\r\n456\r\n--a--\r\n');
18+
setImmediate(() => {
19+
r.push(null);
20+
});
21+
22+
pipeline(r, d, (error) => {
23+
assert(isFinished === false, 'Double-invocation of pipeline callback');
24+
assert(error === undefined, 'Unexpected pipeline error');
25+
isFinished = true;
26+
});
27+
28+
process.on('exit', () => {
29+
assert(isFinished === true, 'Should finish before exiting');
30+
});

0 commit comments

Comments
 (0)