Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This file is used to test the mime parser implemented in JS, mostly by means
// of creating custom emitters and verifying that the methods on that emitter
// are called in the correct order. This also tests that the various
// HeaderParser methods are run correctly.
const { MimeParser } = ChromeUtils.importESModule(
);
// Utility method to compare objects
function compare_objects(real, expected) {
// real is a Map; convert it into an object for uneval purposes
if (typeof real == "object") {
var newreal = {};
for (const [k, v] of real) {
newreal[k] = v;
}
real = newreal;
}
var a = uneval(real),
b = uneval(expected);
// Very long strings don't get printed out fully (unless they're wrong)
if ((a.length > 100 || b.length > 100) && a == b) {
Assert.ok(a == b);
} else {
Assert.equal(a, b);
}
}
// Returns and deletes object[field] if present, or undefined if not.
function extract_field(object, field) {
if (field in object) {
var result = object[field];
delete object[field];
return result;
}
return undefined;
}
// A file cache for read_file.
var file_cache = {};
/**
* Read a file into a string (all line endings become CRLF).
*/
async function read_file(file, start, end) {
if (!(file in file_cache)) {
var realFile = do_get_file("../../../data/" + file);
file_cache[file] = (await IOUtils.readUTF8(realFile.path)).split(
/\r\n|[\r\n]/
);
}
var contents = file_cache[file];
if (start !== undefined) {
contents = contents.slice(start - 1, end - 1);
}
return contents.join("\r\n");
}
/**
* Helper for body tests.
*
* Some extra options are listed too:
* _split: The contents of the file will be passed in packets split by this
* regex. Be sure to include the split delimiter in a group so that they
* are included in the output packets!
* _eol: The CRLFs in the input file will be replaced with the given line
* ending instead.
*
* @param test The name of test
* @param file The name of the file to read (relative to mailnews/data)
* @param opts Options for the mime parser, as well as a few extras detailed
* above.
* @param partspec An array of [partnum, line start, line end] detailing the
* expected parts in the body. It will be expected that the
* accumulated body part data for partnum would be the contents
* of the file from [line start, line end) [1-based lines]
*/
async function make_body_test(test, file, opts, partspec) {
const results = [];
for (const p of partspec) {
results.push([p[0], await read_file(file, p[1], p[2])]);
}
const msgcontents = await read_file(file);
return [test, msgcontents, opts, results];
}
async function make_bodydecode_test(test, file, opts, expected) {
const msgcontents = await read_file(file);
return [test, msgcontents, opts, expected];
}
// This is the expected part specifier for the multipart-complex1 test file,
// specified here because it is used in several cases.
var mpart_complex1 = [
["1", 8, 10],
["2", 14, 16],
["3.1", 22, 24],
["4", 29, 31],
["5", 33, 35],
];
// Format of tests:
// entry[0] = name of the test
// entry[1] = message (a string or an array of packets)
// entry[2] = options for the MIME parser
// entry[3] = A checker result:
// either a {partnum: header object} (to check headers)
// or a [[partnum body], [partnum body], ...] (to check bodies)
// (the partnums refer to the expected part numbers of the MIME test)
// For body tests, unless you're testing decoding, use make_body_test.
// For decoding tests, use make_bodydecode_test
var parser_tests = [
// Body tests from data
// (Note: line numbers are 1-based. Also, to capture trailing EOF, add 2 to
// the last line number of the file).
make_body_test("Basic body", "basic1", {}, [["", 3, 5]]),
make_body_test("Basic multipart", "multipart1", {}, [["1", 10, 12]]),
make_body_test("Basic multipart", "multipart2", {}, [["1", 8, 11]]),
make_body_test("Complex multipart", "multipart-complex1", {}, mpart_complex1),
make_body_test("Truncated multipart", "multipart-complex2", {}, [
["1.1.1.1", 20, 24],
["2", 26, 56],
["3", 59, 61],
]),
make_body_test("No LF multipart", "multipartmalt-detach", {}, [
["1", 20, 21],
["2.1", 27, 38],
["2.2", 42, 43],
["2.3", 47, 48],
]),
make_body_test("Raw body", "multipart1", { bodyformat: "raw" }, [
["", 4, 14],
]),
make_bodydecode_test(
"Base64 decode 1",
"base64-1",
{ bodyformat: "decode" },
[
[
"",
"\r\nHello, world! (Again...)\r\n\r\nLet's see how well base64 text" +
" is handled. Yay, lots of spaces! There" +
"'s even a CRLF at the end and one at the beginning, but the output" +
" shouldn't have it.\r\n",
],
]
),
make_bodydecode_test(
"Base64 decode 2",
"base64-2",
{ bodyformat: "decode" },
[
[
"",
"<html><body>This is base64 encoded HTML text, and the tags shouldn" +
"'t be stripped.\r\n<b>Bold text is bold!</b></body></html>\r\n",
],
]
),
make_body_test("Base64 nodecode", "base64-1", {}, [["", 4, 9]]),
make_bodydecode_test(
"QP decode",
{ pruneat: "1", bodyformat: "decode" },
[
[
"1",
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r' +
'\n<HTML><HEAD>\r\n<META HTTP-EQUIV="Content-Type" CONTENT="text/h' +
'tml; charset=us-ascii">\r\n\r\n\r\n<META content="MSHTML 6.00.600' +
'0.16735" name=GENERATOR></HEAD>\r\n<BODY> bbb\r\n</BODY></HTML>',
],
]
),
// Comprehensive tests from the torture test
make_body_test("Torture regular body", "mime-torture", {}, [
["1", 17, 21],
["2$.1", 58, 75],
["2$.2.1", 83, 97],
["2$.3", 102, 130],
["3$", 155, 7742],
["4", 7747, 8213],
["5", 8218, 8242],
["6$.1.1", 8284, 8301],
["6$.1.2", 8306, 8733],
["6$.2.1", 8742, 9095],
["6$.2.2", 9100, 9354],
["6$.2.3", 9357, 11794],
["6$.2.4", 11797, 12155],
["6$.3", 12161, 12809],
["7$.1", 12844, 12845],
["7$.2", 12852, 13286],
["7$.3", 13288, 13297],
["8$.1", 13331, 13358],
["8$.2", 13364, 13734],
["9$", 13757, 20179],
["10", 20184, 21200],
["11$.1", 21223, 22031],
["11$.2", 22036, 22586],
["12$.1", 22607, 23469],
["12$.2", 23474, 23774],
["12$.3$.1", 23787, 23795],
["12$.3$.2.1", 23803, 23820],
["12$.3$.2.2", 23825, 24633],
["12$.3$.3", 24640, 24836],
["12$.3$.4$", 24848, 25872],
]),
make_body_test("Torture pruneat", "mime-torture", { pruneat: "4" }, [
["4", 7747, 8213],
]),
];
function test_parser(message, opts, results) {
var checkingHeaders = !(results instanceof Array);
var calls = 0,
dataCalls = 0;
var fusingParts = extract_field(opts, "_nofuseparts") === undefined;
var emitter = {
stack: [],
startMessage: function emitter_startMsg() {
Assert.equal(this.stack.length, 0, "no stack at start");
calls++;
this.partData = "";
},
endMessage: function emitter_endMsg() {
Assert.equal(this.stack.length, 0, "no stack at end");
calls++;
},
startPart: function emitter_startPart(partNum, headers) {
this.stack.push(partNum);
if (checkingHeaders) {
Assert.ok(partNum in results);
compare_objects(headers, results[partNum]);
if (fusingParts) {
Assert.equal(this.partData, "");
}
}
},
deliverPartData: function emitter_partData(partNum, data) {
Assert.equal(this.stack[this.stack.length - 1], partNum);
try {
if (!checkingHeaders) {
if (fusingParts) {
this.partData += data;
} else {
Assert.equal(partNum, results[dataCalls][0]);
compare_objects(data, results[dataCalls][1]);
}
}
} finally {
if (!fusingParts) {
dataCalls++;
}
}
},
endPart: function emitter_endPart(partNum) {
if (this.partData != "") {
Assert.equal(partNum, results[dataCalls][0]);
compare_objects(this.partData, results[dataCalls][1]);
dataCalls++;
this.partData = "";
}
Assert.equal(this.stack.pop(), partNum);
},
};
opts.onerror = function (e) {
throw e;
};
MimeParser.parseSync(message, emitter, opts);
Assert.equal(calls, 2);
if (!checkingHeaders) {
Assert.equal(dataCalls, results.length);
}
}
// Format of tests:
// entry[0] = header
// entry[1] = flags
// entry[2] = result to match
var header_tests = [
// Parameter passing
["multipart/related", MimeParser.HEADER_PARAMETER, ["multipart/related", {}]],
["a ; b=v", MimeParser.HEADER_PARAMETER, ["a", { b: "v" }]],
["a ; b='v'", MimeParser.HEADER_PARAMETER, ["a", { b: "'v'" }]],
['a; b = "v"', MimeParser.HEADER_PARAMETER, ["a", { b: "v" }]],
["a;b=1;b=2", MimeParser.HEADER_PARAMETER, ["a", { b: "1" }]],
["a;b=2;b=1", MimeParser.HEADER_PARAMETER, ["a", { b: "2" }]],
['a;b="a;b"', MimeParser.HEADER_PARAMETER, ["a", { b: "a;b" }]],
['a;b="\\\\"', MimeParser.HEADER_PARAMETER, ["a", { b: "\\" }]],
['a;b="a\\b\\c"', MimeParser.HEADER_PARAMETER, ["a", { b: "abc" }]],
["a;b=1;c=2", MimeParser.HEADER_PARAMETER, ["a", { b: "1", c: "2" }]],
['a;b="a\\', MimeParser.HEADER_PARAMETER, ["a", { b: "a" }]],
["a;b", MimeParser.HEADER_PARAMETER, ["a", {}]],
['a;b=";";c=d', MimeParser.HEADER_PARAMETER, ["a", { b: ";", c: "d" }]],
];
function test_header(headerValue, flags, expected) {
const result = MimeParser.parseHeaderField(headerValue, flags);
Assert.equal(result.preSemi, expected[0]);
compare_objects(result, expected[1]);
}
add_task(async function testit() {
for (let test of parser_tests) {
test = await test;
dump("Testing message " + test[0]);
if (test[1] instanceof Array) {
dump(" using " + test[1].length + " packets");
}
dump("\n");
test_parser(test[1], test[2], test[3]);
}
for (const test of header_tests) {
dump("Testing value ->" + test[0] + "<- with flags " + test[1] + "\n");
test_header(test[0], test[1], test[2]);
}
});