From 1ed3f83e482659fb60b21c20fb1bae05ed576d1a Mon Sep 17 00:00:00 2001 From: Nahne Steinauer Date: Thu, 21 Nov 2024 16:05:51 +0000 Subject: [PATCH 1/2] Fix whitespace in stream decoder --- lib/libqp.js | 2 +- test/libqp-test.js | 160 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/lib/libqp.js b/lib/libqp.js index 0a2ef47..937543e 100644 --- a/lib/libqp.js +++ b/lib/libqp.js @@ -285,7 +285,7 @@ class Decoder extends Transform { qp = this._curLine + chunk; this._curLine = ''; - qp = qp.replace(/\=[^\n]?$/, lastLine => { + qp = qp.replace(/[\t ]*(?:=[^\n]?)?$/, lastLine => { this._curLine = lastLine; return ''; }); diff --git a/test/libqp-test.js b/test/libqp-test.js index 8c9af73..0b616a8 100644 --- a/test/libqp-test.js +++ b/test/libqp-test.js @@ -39,3 +39,163 @@ test('Encoding tests', async t => { assert.strictEqual(encoded, 'tere j=C3=B5geva'); }); }); + +test('Decoding tests', async t => { + // Example taken from RFC2045 section 6.7 + const encoded = + "Now's the time =\r\n" + + "for all folk to come=\r\n" + + " to the aid of their country." + const expectedDecoded = + "Now's the time for all folk to come to the aid of their country." + + await t.test('simple string', async () => { + const decoded = libqp.decode(encoded).toString(); + assert.strictEqual(decoded, expectedDecoded); + }); + + await t.test('stream', async () => { + const decoder = new libqp.Decoder(); + + const decoded = await new Promise((resolve, reject) => { + const chunks = []; + decoder.on('readable', () => { + let chunk; + + while ((chunk = decoder.read()) !== null) { + chunks.push(chunk); + } + }); + decoder.on('end', () => { + resolve(Buffer.concat(chunks).toString()); + }); + decoder.on('Error', err => { + reject(err); + }); + + decoder.end(Buffer.from(encoded)); + }); + + assert.strictEqual(decoded, expectedDecoded); + }); + + await t.test('stream, multiple chunks', async () => { + const encodedChunk1Length = 3; + const encodedChunk1 = encoded.substring(0, encodedChunk1Length) + const encodedChunk2 = encoded.substring(encodedChunk1Length) + + const decoder = new libqp.Decoder(); + + const decoded = await new Promise((resolve, reject) => { + const chunks = []; + decoder.on('readable', () => { + let chunk; + + while ((chunk = decoder.read()) !== null) { + chunks.push(chunk); + } + }); + decoder.on('end', () => { + resolve(Buffer.concat(chunks).toString()); + }); + decoder.on('Error', err => { + reject(err); + }); + + decoder.write(Buffer.from(encodedChunk1)); + decoder.end(Buffer.from(encodedChunk2)); + }); + + assert.strictEqual(decoded, expectedDecoded); + }); + + await t.test('stream, space at end of chunk', async () => { + const encodedChunk1Length = encoded.indexOf(' ') + 1; + const encodedChunk1 = encoded.substring(0, encodedChunk1Length) + const encodedChunk2 = encoded.substring(encodedChunk1Length) + + const decoder = new libqp.Decoder(); + + const decoded = await new Promise((resolve, reject) => { + const chunks = []; + decoder.on('readable', () => { + let chunk; + + while ((chunk = decoder.read()) !== null) { + chunks.push(chunk); + } + }); + decoder.on('end', () => { + resolve(Buffer.concat(chunks).toString()); + }); + decoder.on('Error', err => { + reject(err); + }); + + decoder.write(Buffer.from(encodedChunk1)); + decoder.end(Buffer.from(encodedChunk2)); + }); + + assert.strictEqual(decoded, expectedDecoded); + }); + + await t.test('stream, soft line break equals sign at end of chunk', async () => { + const encodedChunk1Length = encoded.indexOf('=') + 1; + const encodedChunk1 = encoded.substring(0, encodedChunk1Length) + const encodedChunk2 = encoded.substring(encodedChunk1Length) + + const decoder = new libqp.Decoder(); + + const decoded = await new Promise((resolve, reject) => { + const chunks = []; + decoder.on('readable', () => { + let chunk; + + while ((chunk = decoder.read()) !== null) { + chunks.push(chunk); + } + }); + decoder.on('end', () => { + resolve(Buffer.concat(chunks).toString()); + }); + decoder.on('Error', err => { + reject(err); + }); + + decoder.write(Buffer.from(encodedChunk1)); + decoder.end(Buffer.from(encodedChunk2)); + }); + + assert.strictEqual(decoded, expectedDecoded); + }); + + await t.test('stream, CR at end of chunk', async () => { + const encodedChunk1Length = encoded.indexOf('\r') + 1; + const encodedChunk1 = encoded.substring(0, encodedChunk1Length) + const encodedChunk2 = encoded.substring(encodedChunk1Length) + + const decoder = new libqp.Decoder(); + + const decoded = await new Promise((resolve, reject) => { + const chunks = []; + decoder.on('readable', () => { + let chunk; + + while ((chunk = decoder.read()) !== null) { + chunks.push(chunk); + } + }); + decoder.on('end', () => { + resolve(Buffer.concat(chunks).toString()); + }); + decoder.on('Error', err => { + reject(err); + }); + + decoder.write(Buffer.from(encodedChunk1)); + decoder.end(Buffer.from(encodedChunk2)); + }); + + assert.strictEqual(decoded, expectedDecoded); + }); +}); From df2b0b8d9f13c9e4c9ffa034c0e5ffa126f7d095 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Fri, 29 Nov 2024 11:10:29 +0200 Subject: [PATCH 2/2] Process the entire input as a single value --- lib/libqp.js | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/lib/libqp.js b/lib/libqp.js index 937543e..ec821b1 100644 --- a/lib/libqp.js +++ b/lib/libqp.js @@ -255,6 +255,7 @@ class Encoder extends Transform { /** * Creates a transform stream for decoding Quoted-Printable encoded strings + * The input is not actually processed as a stream but concatted and processed as a single input * * @constructor * @param {Object} options Stream options @@ -270,42 +271,32 @@ class Decoder extends Transform { this.inputBytes = 0; this.outputBytes = 0; + + this.qpChunks = []; } _transform(chunk, encoding, done) { - let qp, buf; - - chunk = chunk.toString('ascii'); - if (!chunk || !chunk.length) { return done(); } - this.inputBytes += chunk.length; - - qp = this._curLine + chunk; - this._curLine = ''; - qp = qp.replace(/[\t ]*(?:=[^\n]?)?$/, lastLine => { - this._curLine = lastLine; - return ''; - }); - - if (qp) { - buf = decode(qp); - this.outputBytes += buf.length; - this.push(buf); + if (typeof chunk === 'string') { + chunk = Buffer.from(chunk, encoding); } + this.qpChunks.push(chunk); + this.inputBytes += chunk.length; + done(); } _flush(done) { - let buf; - if (this._curLine) { - buf = decode(this._curLine); + if (this.inputBytes) { + let buf = decode(Buffer.concat(this.qpChunks, this.inputBytes).toString()); this.outputBytes += buf.length; this.push(buf); } + done(); } }