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/. */
import { ctypes } from "resource://gre/modules/ctypes.sys.mjs";
import { MailServices } from "resource:///modules/MailServices.sys.mjs";
// See Bug 903946
function avoidUncaughtExceptionInExternalProtocolService() {
try {
Services.prefs.setCharPref(
"helpers.private_mime_types_file",
Services.prefs.getCharPref("helpers.global_mime_types_file")
);
} catch (ex) {}
try {
Services.prefs.setCharPref(
"helpers.private_mailcap_file",
Services.prefs.getCharPref("helpers.global_mailcap_file")
);
} catch (ex) {}
}
avoidUncaughtExceptionInExternalProtocolService();
export var mailTestUtils = {
// Loads a file to a string
// If aCharset is specified, treats the file as being of that charset
loadFileToString(aFile, aCharset) {
var data = "";
var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
Ci.nsIFileInputStream
);
fstream.init(aFile, -1, 0, 0);
if (aCharset) {
var cstream = Cc[
"@mozilla.org/intl/converter-input-stream;1"
].createInstance(Ci.nsIConverterInputStream);
cstream.init(fstream, aCharset, 4096, 0x0000);
const str = {};
while (cstream.readString(4096, str) != 0) {
data += str.value;
}
cstream.close();
} else {
var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
Ci.nsIScriptableInputStream
);
sstream.init(fstream);
let str = sstream.read(4096);
while (str.length > 0) {
data += str;
str = sstream.read(4096);
}
sstream.close();
}
fstream.close();
return data;
},
// Loads a message to a string
// If aCharset is specified, treats the file as being of that charset
loadMessageToString(aFolder, aMsgHdr, aCharset) {
var data = "";
let bytesLeft = aMsgHdr.messageSize;
const stream = aFolder.getLocalMsgStream(aMsgHdr);
if (aCharset) {
const cstream = Cc[
"@mozilla.org/intl/converter-input-stream;1"
].createInstance(Ci.nsIConverterInputStream);
cstream.init(stream, aCharset, 4096, 0x0000);
const str = {};
let bytesToRead = Math.min(bytesLeft, 4096);
while (cstream.readString(bytesToRead, str) != 0) {
data += str.value;
bytesLeft -= bytesToRead;
if (bytesLeft <= 0) {
break;
}
bytesToRead = Math.min(bytesLeft, 4096);
}
cstream.close();
} else {
var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
Ci.nsIScriptableInputStream
);
sstream.init(stream);
let bytesToRead = Math.min(bytesLeft, 4096);
var str = sstream.read(bytesToRead);
bytesLeft -= str.length;
while (str.length > 0) {
data += str;
if (bytesLeft <= 0) {
break;
}
bytesToRead = Math.min(bytesLeft, 4096);
str = sstream.read(bytesToRead);
bytesLeft -= str.length;
}
sstream.close();
}
stream.close();
return data;
},
// Loads a message to a UTF-16 string.
loadMessageToUTF16String(folder, msgHdr, charset) {
const str = this.loadMessageToString(folder, msgHdr, charset);
const arr = new Uint8Array(Array.from(str, x => x.charCodeAt(0)));
return new TextDecoder().decode(arr);
},
// Gets the first message header in a folder.
firstMsgHdr(folder) {
const enumerator = folder.msgDatabase.enumerateMessages();
const first = enumerator[Symbol.iterator]().next();
return first.done ? null : first.value;
},
// Gets message header number N (0 based index) in a folder.
getMsgHdrN(folder, n) {
let i = 0;
for (const next of folder.msgDatabase.enumerateMessages()) {
if (i == n) {
return next;
}
i++;
}
return null;
},
/**
* Returns the file system a particular file is on.
* Currently supported on Windows only.
*
* @param {nsIFile} aFile - The file to get the file system for.
* @returns {string} The file system a particular file is on, or 'null'
* if not on Windows.
*/
get_file_system(aFile) {
if (!("@mozilla.org/windows-registry-key;1" in Cc)) {
dump("get_file_system() is supported on Windows only.\n");
return null;
}
// Win32 type and other constants.
const BOOL = ctypes.int32_t;
const MAX_PATH = 260;
const kernel32 = ctypes.open("kernel32.dll");
try {
// Returns the path of the volume a file is on.
const GetVolumePathName = kernel32.declare(
"GetVolumePathNameW",
ctypes.winapi_abi,
BOOL, // return type: 1 indicates success, 0 failure
ctypes.char16_t.ptr, // in: lpszFileName
ctypes.char16_t.ptr, // out: lpszVolumePathName
ctypes.uint32_t // in: cchBufferLength
);
const filePath = aFile.path;
// The volume path should be at most 1 greater than than the length of the
// path -- add 1 for a trailing backslash if necessary, and 1 for the
// terminating null character. Note that the parentheses around the type are
// necessary for new to apply correctly.
const volumePath = new (ctypes.char16_t.array(filePath.length + 2))();
if (!GetVolumePathName(filePath, volumePath, volumePath.length)) {
throw new Error(
"Unable to get volume path for " +
filePath +
", error " +
ctypes.winLastError
);
}
// Returns information about the file system for the given volume path. We just need
// the file system name.
const GetVolumeInformation = kernel32.declare(
"GetVolumeInformationW",
ctypes.winapi_abi,
BOOL, // return type: 1 indicates success, 0 failure
ctypes.char16_t.ptr, // in, optional: lpRootPathName
ctypes.char16_t.ptr, // out: lpVolumeNameBuffer
ctypes.uint32_t, // in: nVolumeNameSize
ctypes.uint32_t.ptr, // out, optional: lpVolumeSerialNumber
ctypes.uint32_t.ptr, // out, optional: lpMaximumComponentLength
ctypes.uint32_t.ptr, // out, optional: lpFileSystemFlags
ctypes.char16_t.ptr, // out: lpFileSystemNameBuffer
ctypes.uint32_t // in: nFileSystemNameSize
);
// We're only interested in the name of the file system.
const fsName = new (ctypes.char16_t.array(MAX_PATH + 1))();
if (
!GetVolumeInformation(
volumePath,
null,
0,
null,
null,
null,
fsName,
fsName.length
)
) {
throw new Error(
"Unable to get volume information for " +
volumePath.readString() +
", error " +
ctypes.winLastError
);
}
return fsName.readString();
} finally {
kernel32.close();
}
},
/**
* Try marking a region of a file as sparse, so that zeros don't consume
* significant amounts of disk space. This is a platform-dependent routine and
* is not supported on all platforms. The current status of this function is:
* - Windows: Supported, but only on NTFS volumes.
* - Mac: Not supported.
* - Linux: As long as you seek to a position before writing, happens automatically
* on most file systems, so this function is a no-op.
*
* @param {nsIFile} aFile - The file to mark as sparse.
* @param {integer} aRegionStart - The start position of the sparse region,
* in bytes.
* @param {integer} aRegionBytes - The number of bytes to mark as sparse.
* @returns {boolean} Whether the OS and file system supports marking files as
* sparse. If this is true, then the file has been marked as sparse.
* If this isfalse, then the underlying system doesn't support marking files as
* sparse. If an exception is thrown, then the system does support marking
* files as sparse, but an error occurred while doing so.
*
*/
mark_file_region_sparse(aFile, aRegionStart, aRegionBytes) {
const fileSystem = this.get_file_system(aFile);
dump(
"[mark_file_region_sparse()] File system = " +
(fileSystem || "(unknown)") +
", file region = at " +
this.toMiBString(aRegionStart) +
" for " +
this.toMiBString(aRegionBytes) +
"\n"
);
if ("@mozilla.org/windows-registry-key;1" in Cc) {
// On Windows, check whether the drive is NTFS. If it is, proceed.
// If it isn't, then bail out now, because in all probability it is
// FAT32, which doesn't support sparse files.
if (fileSystem != "NTFS") {
return false;
}
// Win32 type and other constants.
const BOOL = ctypes.int32_t;
const HANDLE = ctypes.voidptr_t;
// A BOOLEAN (= BYTE = unsigned char) is distinct from a BOOL.
const BOOLEAN = ctypes.unsigned_char;
const FILE_SET_SPARSE_BUFFER = new ctypes.StructType(
"FILE_SET_SPARSE_BUFFER",
[{ SetSparse: BOOLEAN }]
);
// LARGE_INTEGER is actually a type union. We'll use the int64 representation
const LARGE_INTEGER = ctypes.int64_t;
const FILE_ZERO_DATA_INFORMATION = new ctypes.StructType(
"FILE_ZERO_DATA_INFORMATION",
[{ FileOffset: LARGE_INTEGER }, { BeyondFinalZero: LARGE_INTEGER }]
);
const GENERIC_WRITE = 0x40000000;
const OPEN_ALWAYS = 4;
const FILE_ATTRIBUTE_NORMAL = 0x80;
const INVALID_HANDLE_VALUE = new ctypes.Int64(-1);
const FSCTL_SET_SPARSE = 0x900c4;
const FSCTL_SET_ZERO_DATA = 0x980c8;
const FILE_BEGIN = 0;
const kernel32 = ctypes.open("kernel32.dll");
try {
const CreateFile = kernel32.declare(
"CreateFileW",
ctypes.winapi_abi,
HANDLE, // return type: handle to the file
ctypes.char16_t.ptr, // in: lpFileName
ctypes.uint32_t, // in: dwDesiredAccess
ctypes.uint32_t, // in: dwShareMode
ctypes.voidptr_t, // in, optional: lpSecurityAttributes (note that
// we're cheating here by not declaring a
// SECURITY_ATTRIBUTES structure -- that's because
// we're going to pass in null anyway)
ctypes.uint32_t, // in: dwCreationDisposition
ctypes.uint32_t, // in: dwFlagsAndAttributes
HANDLE // in, optional: hTemplateFile
);
const filePath = aFile.path;
const hFile = CreateFile(
filePath,
GENERIC_WRITE,
0,
null,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
null
);
const hFileInt = ctypes.cast(hFile, ctypes.intptr_t);
if (ctypes.Int64.compare(hFileInt.value, INVALID_HANDLE_VALUE) == 0) {
throw new Error(
"CreateFile failed for " +
filePath +
", error " +
ctypes.winLastError
);
}
try {
const DeviceIoControl = kernel32.declare(
"DeviceIoControl",
ctypes.winapi_abi,
BOOL, // return type: 1 indicates success, 0 failure
HANDLE, // in: hDevice
ctypes.uint32_t, // in: dwIoControlCode
ctypes.voidptr_t, // in, optional: lpInBuffer
ctypes.uint32_t, // in: nInBufferSize
ctypes.voidptr_t, // out, optional: lpOutBuffer
ctypes.uint32_t, // in: nOutBufferSize
ctypes.uint32_t.ptr, // out, optional: lpBytesReturned
ctypes.voidptr_t // inout, optional: lpOverlapped (again, we're
// cheating here by not having this as an
// OVERLAPPED structure
);
// bytesReturned needs to be passed in, even though it's meaningless
const bytesReturned = new ctypes.uint32_t();
const sparseBuffer = new FILE_SET_SPARSE_BUFFER();
sparseBuffer.SetSparse = 1;
// Mark the file as sparse
if (
!DeviceIoControl(
hFile,
FSCTL_SET_SPARSE,
sparseBuffer.address(),
FILE_SET_SPARSE_BUFFER.size,
null,
0,
bytesReturned.address(),
null
)
) {
throw new Error(
"Unable to mark file as sparse, error " + ctypes.winLastError
);
}
const zdInfo = new FILE_ZERO_DATA_INFORMATION();
zdInfo.FileOffset = aRegionStart;
const regionEnd = aRegionStart + aRegionBytes;
zdInfo.BeyondFinalZero = regionEnd;
// Mark the region as a sparse region
if (
!DeviceIoControl(
hFile,
FSCTL_SET_ZERO_DATA,
zdInfo.address(),
FILE_ZERO_DATA_INFORMATION.size,
null,
0,
bytesReturned.address(),
null
)
) {
throw new Error(
"Unable to mark region as zero, error " + ctypes.winLastError
);
}
// Move to past the sparse region and mark it as the end of the file. The
// above DeviceIoControl call is useless unless followed by this.
const SetFilePointerEx = kernel32.declare(
"SetFilePointerEx",
ctypes.winapi_abi,
BOOL, // return type: 1 indicates success, 0 failure
HANDLE, // in: hFile
LARGE_INTEGER, // in: liDistanceToMove
LARGE_INTEGER.ptr, // out, optional: lpNewFilePointer
ctypes.uint32_t // in: dwMoveMethod
);
if (!SetFilePointerEx(hFile, regionEnd, null, FILE_BEGIN)) {
throw new Error(
"Unable to set file pointer to end, error " + ctypes.winLastError
);
}
const SetEndOfFile = kernel32.declare(
"SetEndOfFile",
ctypes.winapi_abi,
BOOL, // return type: 1 indicates success, 0 failure
HANDLE // in: hFile
);
if (!SetEndOfFile(hFile)) {
throw new Error(
"Unable to set end of file, error " + ctypes.winLastError
);
}
return true;
} finally {
const CloseHandle = kernel32.declare(
"CloseHandle",
ctypes.winapi_abi,
BOOL, // return type: 1 indicates success, 0 failure
HANDLE // in: hObject
);
CloseHandle(hFile);
}
} finally {
kernel32.close();
}
} else if ("nsILocalFileMac" in Ci) {
// Macs don't support marking files as sparse.
return false;
} else {
// Assuming Unix here. Unix file systems generally automatically sparsify
// files.
return true;
}
},
/**
* Converts a size in bytes into its mebibytes string representation.
* NB: 1 MiB = 1024 * 1024 = 1048576 B.
*
* @param {integer} aSize - The size in bytes.
* @returns {string} A string representing the size in mebibytes.
*/
toMiBString(aSize) {
return aSize / 1048576 + " MiB";
},
/**
* A variant of do_timeout that accepts an actual function instead of
* requiring you to pass a string to evaluate. If the function throws an
* exception when invoked, we will use do_throw to ensure that the test fails.
*
* @param {integer} aDelayInMS - The number of milliseconds to wait before firing the timer.
* @param {Function} aFunc - The function to invoke when the timer fires.
* @param {object} [aFuncThis] - Optional 'this' pointer to use.
* @param {*[]} aFuncArgs - Optional list of arguments to pass to the function.
*/
_timer: null,
do_timeout_function(aDelayInMS, aFunc, aFuncThis, aFuncArgs) {
this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
const wrappedFunc = function () {
try {
aFunc.apply(aFuncThis, aFuncArgs);
} catch (ex) {
// we want to make sure that if the thing we call throws an exception,
// that this terminates the test.
do_throw(ex);
}
};
this._timer.initWithCallback(
wrappedFunc,
aDelayInMS,
Ci.nsITimer.TYPE_ONE_SHOT
);
},
/**
* Ensure the given nsIMsgFolder's database is up-to-date, calling the provided
* callback once the folder has been loaded. (This may be instantly or
* after a re-parse.)
*
* @param {nsIMsgFolder} aFolder - The nsIMsgFolder whose database you want
* to ensure is up-to-date.
* @param {Function} aCallback - The callback function to invoke once the
* folder has been loaded.
* @param {object} aCallbackThis - The 'this' to use when calling the callback.
* Pass null if your callback does not rely on 'this'.
* @param {*[]} aCallbackArgs - A list of arguments to pass to the callback
* via apply. If you provide [1,2,3], we will effectively call:
* aCallbackThis.aCallback(1,2,3);
* @param {boolean} [aSomeoneElseWillTriggerTheUpdate=false] If this is true,
* we do not trigger the updateFolder call and it is assumed someone else is
* taking care of that.
*/
updateFolderAndNotify(
aFolder,
aCallback,
aCallbackThis,
aCallbackArgs,
aSomeoneElseWillTriggerTheUpdate = false
) {
// register for the folder loaded notification ahead of time... even though
// we may not need it...
const folderListener = {
onFolderEvent(aEventFolder, aEvent) {
if (aEvent == "FolderLoaded" && aFolder.URI == aEventFolder.URI) {
MailServices.mailSession.RemoveFolderListener(this);
aCallback.apply(aCallbackThis, aCallbackArgs);
}
},
};
MailServices.mailSession.AddFolderListener(
folderListener,
Ci.nsIFolderListener.event
);
if (!aSomeoneElseWillTriggerTheUpdate) {
aFolder.updateFolder(null);
}
},
/**
* For when you want to compare elements non-strictly.
*/
non_strict_index_of(aArray, aElem) {
for (const [i, elem] of aArray.entries()) {
if (elem == aElem) {
return i;
}
}
return -1;
},
/**
* Click on a particular cell in a tree. `window` is not defined here in this
* file, so we can't provide it as a default argument. Similarly, we pass in
* `EventUtils` as an argument because importing it here does not work
* because `window` is not defined.
*
* @param {object} EventUtils - The EventUtils object.
* @param {Window} win - The window the tree is in.
* @param {Element} tree - The tree element.
* @param {number} row - The tree row to click on.
* @param {number} column - The tree column to click on.
* @param {object} event - The mouse event to synthesize, e.g. `{ clickCount: 2 }`.
*/
treeClick(EventUtils, win, tree, row, column, event) {
const coords = tree.getCoordsForCellItem(row, tree.columns[column], "cell");
const treeChildren = tree.lastElementChild;
EventUtils.synthesizeMouse(
treeChildren,
coords.x + coords.width / 2,
coords.y + coords.height / 2,
event,
win
);
},
/**
* For waiting until an element exists in a given document. Pass in the
* `MutationObserver` as an argument because importing it here does not work
* because `window` is not defined here.
*
* @param {object} MutationObserver - The MutationObserver object.
* @param {Document} doc - Document that contains the elements.
* @param {string} observedNodeId - Id of the element to observe.
* @param {string} awaitedNodeId - Id of the element that will soon exist.
* @returns {Promise.<undefined>} - A promise fulfilled when the element exists.
*/
awaitElementExistence(MutationObserver, doc, observedNodeId, awaitedNodeId) {
return new Promise(resolve => {
const outerObserver = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type == "childList" && mutation.addedNodes.length) {
const element = doc.getElementById(awaitedNodeId);
if (element) {
observer.disconnect();
resolve();
return;
}
}
}
});
const nodeToObserve = doc.getElementById(observedNodeId);
outerObserver.observe(nodeToObserve, { childList: true });
});
},
};