Skip to content

Commit

Permalink
fix(decode-stream): Fix issue where some whitespace characters were lost
Browse files Browse the repository at this point in the history
Merge branch 'nahne-nahne/stream-whitespace'
  • Loading branch information
andris9 committed Nov 29, 2024
2 parents 5e18930 + df2b0b8 commit 2231159
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 20 deletions.
31 changes: 11 additions & 20 deletions lib/libqp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(/\=[^\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();
}
}
Expand Down
160 changes: 160 additions & 0 deletions test/libqp-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

0 comments on commit 2231159

Please sign in to comment.