CVE-2026-74983
Overview
Background
- Data Loss Prevention (DLP) / Content Analysis
- the
nsIContentAnalysismechanism that submits downloaded file contents to an external agent to allow or block the download. - Part file
- the temporary
.partfile that a download is written to (download.target.partFilePath) before it is moved to its final destination (download.target.path). - `DownloadIntegration`
- the
toolkit/components/downloads/DownloadIntegration.sys.mjsmodule that builds the content-analysis request for a completed download. - `filePath`
- the field in the content-analysis request that tells the scanner which on-disk file to inspect.
Root Cause Analysis
When a download completes, DownloadIntegration constructs a content-analysis request and populates its filePath from download.target.path, the final destination path. However, at the point the scan is requested the downloaded bytes still live in the .part file at download.target.partFilePath and have not yet been moved to download.target.path, so when a part file is in use the filePath pointed at a path that did not yet hold the download’s contents. This violated the invariant that the file handed to Content Analysis must be the actual downloaded data being evaluated, causing the DLP scan to inspect a missing or wrong file and effectively pass without seeing the real payload.
The fix uses download.target.partFilePath || download.target.path, so the scanner receives the part-file path that actually contains the downloaded bytes whenever a part file is involved, and falls back to the final path only when no part file exists.
The added test hook in head.js now asserts the requested filePath exists on disk, and the browser tests exercise both the part-file and non-part-file cases and compare scanned bytes against the download.
download.target.path instead of the download.target.partFilePath that actually held the downloaded data; the fix restores correct scanning by preferring partFilePath when a part file is present.Attack Path
- Craft a policy-violating download An attacker or insider serves a file whose contents would be blocked by the organization’s DLP policy.
- Trigger a part-file download
The download proceeds through a configuration that writes to a
.partfile, sodownload.target.partFilePathis set and distinct fromdownload.target.path. - DLP scans the wrong path
DownloadIntegrationsubmitsdownload.target.pathasfilePath, but the real bytes are in the.partfile, so the scanner sees no matching content. - Scan passes and file lands
Content Analysis returns allow, the
.partfile is moved to the final path, and the disallowed file is delivered despite the enterprise policy.
Impact Assessment
.part intermediate file, which is the common download path. Impact is rated low because it requires DLP to be deployed and only defeats an organizational policy rather than breaching browser security boundaries.Changed Functions
| Function | Change | Notes |
|---|---|---|
add_tasktoolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js |
modified | |
iftoolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js |
modified | |
registerCleanupFunctiontoolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js |
modified | |
iftoolkit/components/contentanalysis/tests/browser/head.js |
modified |
Files Changed
toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.jstoolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.jstoolkit/components/contentanalysis/tests/browser/head.jstoolkit/components/downloads/DownloadIntegration.sys.mjs
Audit Directions
- Path vs. data timingAudit any code that hands a file path to a security scanner or validator and confirm the path points at where the bytes currently reside, not at a destination the data has not yet been moved to.
- Optional intermediate filesReview uses of
download.target.pathacross the downloads and content-analysis code for other places that ignorepartFilePath, since the.partindirection can silently change which file is examined. - Test coverage of both branchesEnsure security-relevant features have tests exercising both the part-file and no-part-file code paths and asserting the checked file exists, as the new
head.jsfilePathexistence check now enforces.
Patch
diff --git a/toolkit/components/downloads/DownloadIntegration.sys.mjs b/toolkit/components/downloads/DownloadIntegration.sys.mjs
index 038472a1c33..4fc5e7a245c 100644
--- a/toolkit/components/downloads/DownloadIntegration.sys.mjs
+++ b/toolkit/components/downloads/DownloadIntegration.sys.mjs
@@ -622,7 +622,9 @@ export var DownloadIntegration = {
resources,
requestToken,
userActionId,
- filePath: download.target.path,
+ // Use the path to the part-file if a part file is involved.
+ // We haven't moved it yet.
+ filePath: download.target.partFilePath || download.target.path,
sha256Digest: download.saver.getSha256Hash(),
}
),
diff --git a/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
index 0b578d8a5b5..bad2fa0a975 100644
--- a/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
+++ b/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
@@ -12,9 +12,9 @@ add_setup(async function test_setup() {
const DOWNLOAD_URL =
"https://example.com/browser/toolkit/components/contentanalysis/tests/browser/file_to_download.unknownextension";
-async function createTargetFileAndDownload() {
+async function createTargetFileAndDownload(usePartFile) {
// Create a temporary file that will be downloaded to.
- const targetFile = await IOUtils.createUniqueFile(
+ const targetFileName = await IOUtils.createUniqueFile(
PathUtils.tempDir,
"target_download_for_content_analysis.txt",
0o600
@@ -23,13 +23,16 @@ async function createTargetFileAndDownload() {
// successfully downloaded. Note that this theoretically introduces
// a race condition since something else could come in and create
// a temp file with the same name. This seems unlikely.
- await IOUtils.remove(targetFile, { ignoreAbsent: false });
+ await IOUtils.remove(targetFileName, { ignoreAbsent: false });
return await Downloads.createDownload({
source: {
url: DOWNLOAD_URL,
},
- target: targetFile,
+ target: {
+ path: targetFileName,
+ partFilePath: usePartFile ? targetFileName + ".part" : undefined,
+ },
});
}
@@ -90,7 +93,7 @@ function assertContentAnalysisDownloadRequest(request, expectedFilePath) {
ok(!!request.requestToken.length, "request requestToken should not be empty");
}
-add_task(async function test_download_content_analysis_allows() {
+async function check_download_content_analysis_allows(usePartFile) {
mockCA.setupForTest(true);
await SpecialPowers.pushPrefEnv({
set: [
@@ -98,7 +101,7 @@ add_task(async function test_download_content_analysis_allows() {
],
});
- const download = await createTargetFileAndDownload();
+ const download = await createTargetFileAndDownload(usePartFile);
let list = await Downloads.getList(Downloads.PUBLIC);
await list.add(download);
await download.start();
@@ -106,13 +109,33 @@ add_task(async function test_download_content_analysis_allows() {
// Make sure the download succeeded.
ok(download.succeeded, "Download should succeed");
is(mockCA.calls.length, 1, "Content analysis should be called once");
- assertContentAnalysisDownloadRequest(mockCA.calls[0], download.target.path);
- ok(await IOUtils.exists(download.target.path), "Target file should exist");
+ assertContentAnalysisDownloadRequest(
+ mockCA.calls[0],
+ usePartFile ? download.target.partFilePath : download.target.path
+ );
+ ok(
+ await IOUtils.exists(download.target.path),
+ "Final target file should exist"
+ );
+ if (usePartFile) {
+ ok(
+ !(await IOUtils.exists(download.target.partFilePath)),
+ "Part file should not still exist"
+ );
+ }
await IOUtils.remove(download.target.path);
await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_allows() {
+ await check_download_content_analysis_allows(false /* usePartFile */);
});
-add_task(async function test_download_content_analysis_blocks() {
+add_task(async function test_download_content_analysis_allows_partfile() {
+ await check_download_content_analysis_allows(true /* usePartFile */);
+});
+
+async function check_download_content_analysis_blocks(usePartFile) {
mockCA.setupForTest(false);
await SpecialPowers.pushPrefEnv({
set: [
@@ -120,28 +143,45 @@ add_task(async function test_download_content_analysis_blocks() {
],
});
- const download = await createTargetFileAndDownload();
- info(`path is ${download.target.path}`);
+ const download = await createTargetFileAndDownload(usePartFile);
+ if (usePartFile) {
+ info(`part file path is ${download.target.partFilePath}`);
+ }
+ info(`final path is ${download.target.path}`);
await Assert.rejects(
download.start(),
ex => ex instanceof Downloads.Error && ex.becauseBlockedByContentAnalysis,
"Download should have been rejected"
);
- info(`path is ${download.target.path}`);
ok(!download.succeeded, "Download should not succeed");
is(mockCA.calls.length, 1, "Content analysis should be called once");
- info(`path is ${download.target.path}`);
- assertContentAnalysisDownloadRequest(mockCA.calls[0], download.target.path);
- info(`path is ${download.target.path}`);
+ assertContentAnalysisDownloadRequest(
+ mockCA.calls[0],
+ usePartFile ? download.target.partFilePath : download.target.path
+ );
ok(
!(await IOUtils.exists(download.target.path)),
- "Target file should not exist"
+ "Final target file should not exist"
);
+ if (usePartFile) {
+ ok(
+ !(await IOUtils.exists(download.target.partFilePath)),
+ "Part file should not still exist"
+ );
+ }
await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_blocks() {
+ await check_download_content_analysis_blocks(false /* usePartFile */);
});
-add_task(async function test_download_content_analysis_user_cancels() {
+add_task(async function test_download_content_analysis_blocks_partfile() {
+ await check_download_content_analysis_blocks(true /* usePartFile */);
+});
+
+async function check_download_content_analysis_user_cancels(usePartFile) {
// Make the mock CA service wait for event so the test can
// cancel the download before the scan finishes.
mockCA.setupForTest(true, /* waitForEvent */ true);
@@ -156,9 +196,13 @@ add_task(async function test_download_content_analysis_user_cancels() {
once: true,
});
});
- const download = await createTargetFileAndDownload();
+ const download = await createTargetFileAndDownload(usePartFile);
+
+ if (usePartFile) {
+ info(`part file path is ${download.target.partFilePath}`);
+ }
+ info(`final path is ${download.target.path}`);
- info(`path is ${download.target.path}`);
let list = await Downloads.getList(Downloads.PUBLIC);
await list.add(download);
download.start();
@@ -189,12 +233,20 @@ add_task(async function test_download_content_analysis_user_cancels() {
// may or may not exist on disk.
}
await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_user_cancels() {
+ await check_download_content_analysis_user_cancels(false /* usePartFile */);
+});
+
+add_task(async function test_download_content_analysis_user_cancels_partfile() {
+ await check_download_content_analysis_user_cancels(true /* usePartFile */);
});
add_task(async function test_download_content_analysis_pref_defaults_to_off() {
- mockCA.setupForTest(false);
// do not set pref, so content analysis should not be consulted
- const download = await createTargetFileAndDownload();
+ mockCA.setupForTest(false);
+ const download = await createTargetFileAndDownload(false);
let list = await Downloads.getList(Downloads.PUBLIC);
await list.add(download);
await download.start();
@@ -202,6 +254,9 @@ add_task(async function test_download_content_analysis_pref_defaults_to_off() {
// Make sure the download succeeded.
ok(download.succeeded, "Download should succeed");
is(mockCA.calls.length, 0, "Content analysis should not be called");
- ok(await IOUtils.exists(download.target.path), "Target file should exist");
+ ok(
+ await IOUtils.exists(download.target.path),
+ "Final target file should exist"
+ );
await IOUtils.remove(download.target.path);
});
diff --git a/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
index 9ceef3b7088..4e9cd9815b3 100644
--- a/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
+++ b/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
@@ -23,27 +23,55 @@ async function triggerSaveAs({ selector }) {
contextMenu.activateItem(saveLinkCommand);
}
+let tempDir;
+
+// Final file path for download.
let destFilePath;
-add_setup(async () => {
- mockCA = await mockContentAnalysisService(mockCA);
-});
+// path to file that is a copy of the file sent to CA, so we can (async) check
+// that it is the same file.
+let copiedScannedFilePath;
-function setupMockFilePicker() {
- const tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+add_setup(async () => {
+ tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
tempDir.append("test-download-dir");
if (!tempDir.exists()) {
tempDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
}
+ registerCleanupFunction(function () {
+ if (tempDir.exists()) {
+ tempDir.remove(true);
+ }
+ });
+ let oldAnalyzeContentRequestPrivate = mockCA.analyzeContentRequestPrivate;
+ mockCA.analyzeContentRequestPrivate = function (
+ request,
+ autoAcknowledge,
+ callback
+ ) {
+ // Copy the file being scanned now because the original (a .part file) is
+ // renamed or removed once the download completes.
+ let requestFile = new FileUtils.File(request.filePath);
+ requestFile.copyTo(tempDir, "file.copy");
+ let tempDirClone = tempDir.clone();
+ tempDirClone.append("file.copy");
+ copiedScannedFilePath = tempDirClone.path;
+ oldAnalyzeContentRequestPrivate.call(
+ this,
+ request,
+ autoAcknowledge,
+ callback
+ );
+ };
+ mockCA = await mockContentAnalysisService(mockCA);
+});
+
+function setupMockFilePicker() {
let MockFilePicker = SpecialPowers.MockFilePicker;
MockFilePicker.init();
registerCleanupFunction(function () {
MockFilePicker.cleanup();
-
- if (tempDir.exists()) {
- tempDir.remove(true);
- }
});
MockFilePicker.displayDirectory = tempDir;
@@ -59,7 +87,7 @@ function setupMockFilePicker() {
};
}
-function assertContentAnalysisDownloadRequest(request, expectedFilePath) {
+async function assertContentAnalysisDownloadRequest(request) {
is(request.url.spec, DOWNLOAD_URL, "request has correct URL");
is(
request.analysisType,
@@ -76,7 +104,17 @@ function assertContentAnalysisDownloadRequest(request, expectedFilePath) {
Ci.nsIContentAnalysisRequest.eDownload,
"request has correct operationTypeForDisplay"
);
- is(request.filePath, expectedFilePath, "request filePath should match");
+ // Compare file contents, not file names, as the download name will be the
+ // temporary name assigned before content analysis approval.
+ let scannedBytes = await IOUtils.read(copiedScannedFilePath);
+ let expectedBytes = new Uint8Array(
+ await (await fetch(DOWNLOAD_URL)).arrayBuffer()
+ );
+ Assert.deepEqual(
+ scannedBytes,
+ expectedBytes,
+ "scanned file is download file"
+ );
ok(!request.textContent?.length, "request textContent should be empty");
is(
request.userActionRequestsCount,
@@ -109,8 +147,9 @@ add_task(async function test_download_content_analysis_download_save_as() {
);
}, "Wait for the file to be downloaded");
is(mockCA.calls.length, 1, "Content analysis should be called once");
- assertContentAnalysisDownloadRequest(mockCA.calls[0], destFilePath);
+ await assertContentAnalysisDownloadRequest(mockCA.calls[0]);
await IOUtils.remove(destFilePath);
+ await IOUtils.remove(copiedScannedFilePath);
await SpecialPowers.popPrefEnv();
});
diff --git a/toolkit/components/contentanalysis/tests/browser/head.js b/toolkit/components/contentanalysis/tests/browser/head.js
index 2356dde6763..f5da5a14d55 100644
--- a/toolkit/components/contentanalysis/tests/browser/head.js
+++ b/toolkit/components/contentanalysis/tests/browser/head.js
@@ -277,6 +277,12 @@ function makeMockContentAnalysis() {
Services.obs.notifyObservers(request, "dlp-request-made");
}
+ // If we are checking a file, make sure it exists.
+ if (request.filePath) {
+ const nsiFile = new FileUtils.File(request.filePath);
+ ok(nsiFile.exists(), "File to check exists");
+ }
+
// Use setTimeout to simulate an async activity.
setTimeout(async () => {
if (this.waitForEvent) {
Regression Test / PoC
diff --git a/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
index 0b578d8a5b5..bad2fa0a975 100644
--- a/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
+++ b/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
@@ -12,9 +12,9 @@ add_setup(async function test_setup() {
const DOWNLOAD_URL =
"https://example.com/browser/toolkit/components/contentanalysis/tests/browser/file_to_download.unknownextension";
-async function createTargetFileAndDownload() {
+async function createTargetFileAndDownload(usePartFile) {
// Create a temporary file that will be downloaded to.
- const targetFile = await IOUtils.createUniqueFile(
+ const targetFileName = await IOUtils.createUniqueFile(
PathUtils.tempDir,
"target_download_for_content_analysis.txt",
0o600
@@ -23,13 +23,16 @@ async function createTargetFileAndDownload() {
// successfully downloaded. Note that this theoretically introduces
// a race condition since something else could come in and create
// a temp file with the same name. This seems unlikely.
- await IOUtils.remove(targetFile, { ignoreAbsent: false });
+ await IOUtils.remove(targetFileName, { ignoreAbsent: false });
return await Downloads.createDownload({
source: {
url: DOWNLOAD_URL,
},
- target: targetFile,
+ target: {
+ path: targetFileName,
+ partFilePath: usePartFile ? targetFileName + ".part" : undefined,
+ },
});
}
@@ -90,7 +93,7 @@ function assertContentAnalysisDownloadRequest(request, expectedFilePath) {
ok(!!request.requestToken.length, "request requestToken should not be empty");
}
-add_task(async function test_download_content_analysis_allows() {
+async function check_download_content_analysis_allows(usePartFile) {
mockCA.setupForTest(true);
await SpecialPowers.pushPrefEnv({
set: [
@@ -98,7 +101,7 @@ add_task(async function test_download_content_analysis_allows() {
],
});
- const download = await createTargetFileAndDownload();
+ const download = await createTargetFileAndDownload(usePartFile);
let list = await Downloads.getList(Downloads.PUBLIC);
await list.add(download);
await download.start();
@@ -106,13 +109,33 @@ add_task(async function test_download_content_analysis_allows() {
// Make sure the download succeeded.
ok(download.succeeded, "Download should succeed");
is(mockCA.calls.length, 1, "Content analysis should be called once");
- assertContentAnalysisDownloadRequest(mockCA.calls[0], download.target.path);
- ok(await IOUtils.exists(download.target.path), "Target file should exist");
+ assertContentAnalysisDownloadRequest(
+ mockCA.calls[0],
+ usePartFile ? download.target.partFilePath : download.target.path
+ );
+ ok(
+ await IOUtils.exists(download.target.path),
+ "Final target file should exist"
+ );
+ if (usePartFile) {
+ ok(
+ !(await IOUtils.exists(download.target.partFilePath)),
+ "Part file should not still exist"
+ );
+ }
await IOUtils.remove(download.target.path);
await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_allows() {
+ await check_download_content_analysis_allows(false /* usePartFile */);
});
-add_task(async function test_download_content_analysis_blocks() {
+add_task(async function test_download_content_analysis_allows_partfile() {
+ await check_download_content_analysis_allows(true /* usePartFile */);
+});
+
+async function check_download_content_analysis_blocks(usePartFile) {
mockCA.setupForTest(false);
await SpecialPowers.pushPrefEnv({
set: [
@@ -120,28 +143,45 @@ add_task(async function test_download_content_analysis_blocks() {
],
});
- const download = await createTargetFileAndDownload();
- info(`path is ${download.target.path}`);
+ const download = await createTargetFileAndDownload(usePartFile);
+ if (usePartFile) {
+ info(`part file path is ${download.target.partFilePath}`);
+ }
+ info(`final path is ${download.target.path}`);
await Assert.rejects(
download.start(),
ex => ex instanceof Downloads.Error && ex.becauseBlockedByContentAnalysis,
"Download should have been rejected"
);
- info(`path is ${download.target.path}`);
ok(!download.succeeded, "Download should not succeed");
is(mockCA.calls.length, 1, "Content analysis should be called once");
- info(`path is ${download.target.path}`);
- assertContentAnalysisDownloadRequest(mockCA.calls[0], download.target.path);
- info(`path is ${download.target.path}`);
+ assertContentAnalysisDownloadRequest(
+ mockCA.calls[0],
+ usePartFile ? download.target.partFilePath : download.target.path
+ );
ok(
!(await IOUtils.exists(download.target.path)),
- "Target file should not exist"
+ "Final target file should not exist"
);
+ if (usePartFile) {
+ ok(
+ !(await IOUtils.exists(download.target.partFilePath)),
+ "Part file should not still exist"
+ );
+ }
await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_blocks() {
+ await check_download_content_analysis_blocks(false /* usePartFile */);
});
-add_task(async function test_download_content_analysis_user_cancels() {
+add_task(async function test_download_content_analysis_blocks_partfile() {
+ await check_download_content_analysis_blocks(true /* usePartFile */);
+});
+
+async function check_download_content_analysis_user_cancels(usePartFile) {
// Make the mock CA service wait for event so the test can
// cancel the download before the scan finishes.
mockCA.setupForTest(true, /* waitForEvent */ true);
@@ -156,9 +196,13 @@ add_task(async function test_download_content_analysis_user_cancels() {
once: true,
});
});
- const download = await createTargetFileAndDownload();
+ const download = await createTargetFileAndDownload(usePartFile);
+
+ if (usePartFile) {
+ info(`part file path is ${download.target.partFilePath}`);
+ }
+ info(`final path is ${download.target.path}`);
- info(`path is ${download.target.path}`);
let list = await Downloads.getList(Downloads.PUBLIC);
await list.add(download);
download.start();
@@ -189,12 +233,20 @@ add_task(async function test_download_content_analysis_user_cancels() {
// may or may not exist on disk.
}
await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_user_cancels() {
+ await check_download_content_analysis_user_cancels(false /* usePartFile */);
+});
+
+add_task(async function test_download_content_analysis_user_cancels_partfile() {
+ await check_download_content_analysis_user_cancels(true /* usePartFile */);
});
add_task(async function test_download_content_analysis_pref_defaults_to_off() {
- mockCA.setupForTest(false);
// do not set pref, so content analysis should not be consulted
- const download = await createTargetFileAndDownload();
+ mockCA.setupForTest(false);
+ const download = await createTargetFileAndDownload(false);
let list = await Downloads.getList(Downloads.PUBLIC);
await list.add(download);
await download.start();
@@ -202,6 +254,9 @@ add_task(async function test_download_content_analysis_pref_defaults_to_off() {
// Make sure the download succeeded.
ok(download.succeeded, "Download should succeed");
is(mockCA.calls.length, 0, "Content analysis should not be called");
- ok(await IOUtils.exists(download.target.path), "Target file should exist");
+ ok(
+ await IOUtils.exists(download.target.path),
+ "Final target file should exist"
+ );
await IOUtils.remove(download.target.path);
});
diff --git a/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
index 9ceef3b7088..4e9cd9815b3 100644
--- a/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
+++ b/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
@@ -23,27 +23,55 @@ async function triggerSaveAs({ selector }) {
contextMenu.activateItem(saveLinkCommand);
}
+let tempDir;
+
+// Final file path for download.
let destFilePath;
-add_setup(async () => {
- mockCA = await mockContentAnalysisService(mockCA);
-});
+// path to file that is a copy of the file sent to CA, so we can (async) check
+// that it is the same file.
+let copiedScannedFilePath;
-function setupMockFilePicker() {
- const tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+add_setup(async () => {
+ tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
tempDir.append("test-download-dir");
if (!tempDir.exists()) {
tempDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
}
+ registerCleanupFunction(function () {
+ if (tempDir.exists()) {
+ tempDir.remove(true);
+ }
+ });
+ let oldAnalyzeContentRequestPrivate = mockCA.analyzeContentRequestPrivate;
+ mockCA.analyzeContentRequestPrivate = function (
+ request,
+ autoAcknowledge,
+ callback
+ ) {
+ // Copy the file being scanned now because the original (a .part file) is
+ // renamed or removed once the download completes.
+ let requestFile = new FileUtils.File(request.filePath);
+ requestFile.copyTo(tempDir, "file.copy");
+ let tempDirClone = tempDir.clone();
+ tempDirClone.append("file.copy");
+ copiedScannedFilePath = tempDirClone.path;
+ oldAnalyzeContentRequestPrivate.call(
+ this,
+ request,
+ autoAcknowledge,
+ callback
+ );
+ };
+ mockCA = await mockContentAnalysisService(mockCA);
+});
+
+function setupMockFilePicker() {
let MockFilePicker = SpecialPowers.MockFilePicker;
MockFilePicker.init();
registerCleanupFunction(function () {
MockFilePicker.cleanup();
-
- if (tempDir.exists()) {
- tempDir.remove(true);
- }
});
MockFilePicker.displayDirectory = tempDir;
@@ -59,7 +87,7 @@ function setupMockFilePicker() {
};
}
-function assertContentAnalysisDownloadRequest(request, expectedFilePath) {
+async function assertContentAnalysisDownloadRequest(request) {
is(request.url.spec, DOWNLOAD_URL, "request has correct URL");
is(
request.analysisType,
@@ -76,7 +104,17 @@ function assertContentAnalysisDownloadRequest(request, expectedFilePath) {
Ci.nsIContentAnalysisRequest.eDownload,
"request has correct operationTypeForDisplay"
);
- is(request.filePath, expectedFilePath, "request filePath should match");
+ // Compare file contents, not file names, as the download name will be the
+ // temporary name assigned before content analysis approval.
+ let scannedBytes = await IOUtils.read(copiedScannedFilePath);
+ let expectedBytes = new Uint8Array(
+ await (await fetch(DOWNLOAD_URL)).arrayBuffer()
+ );
+ Assert.deepEqual(
+ scannedBytes,
+ expectedBytes,
+ "scanned file is download file"
+ );
ok(!request.textContent?.length, "request textContent should be empty");
is(
request.userActionRequestsCount,
@@ -109,8 +147,9 @@ add_task(async function test_download_content_analysis_download_save_as() {
);
}, "Wait for the file to be downloaded");
is(mockCA.calls.length, 1, "Content analysis should be called once");
- assertContentAnalysisDownloadRequest(mockCA.calls[0], destFilePath);
+ await assertContentAnalysisDownloadRequest(mockCA.calls[0]);
await IOUtils.remove(destFilePath);
+ await IOUtils.remove(copiedScannedFilePath);
... (truncated)