Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  test_dictionary_replacement.js   Sprache: JAVA

 

/**
 * Tests for HTTP Compression Dictionary replacement functionality
 * - Verify that when a dictionary resource is reloaded without Use-As-Dictionary,
 *   the dictionary metadata is properly removed
 * - Test that Available-Dictionary header is no longer sent for matching resources
 *   after dictionary is replaced with non-dictionary content
 *
 * This tests the fix for the race condition in DictionaryOriginReader::OnCacheEntryAvailable
 * where mEntry was not set for existing origins loaded from disk.
 */


"use strict";

// Load cache helpers
Services.scriptloader.loadSubScript("resource://test/head_cache.js", this);

const { NodeHTTPSServer } = ChromeUtils.importESModule(
  "resource://testing-common/NodeServer.sys.mjs"
);

const DICTIONARY_CONTENT = "DICTIONARY_CONTENT_FOR_COMPRESSION_TEST_DATA";
const REPLACEMENT_CONTENT = "REPLACED_CONTENT_WITHOUT_DICTIONARY_HEADER";

let server = null;

add_setup(async function () {
  Services.prefs.setBoolPref("network.http.dictionaries.enable", true);

  server = new NodeHTTPSServer();
  await server.start();

  // Clear any existing cache
  let lci = Services.loadContextInfo.custom(false, {
    partitionKey: `(https,localhost)`,
  });
  evict_cache_entries("all", lci);

  registerCleanupFunction(async () => {
    try {
      await server.stop();
    } catch (e) {
      // Ignore server stop errors during cleanup
    }
  });
});

function makeChan(url, bypassCache = false) {
  let chan = NetUtil.newChannel({
    uri: url,
    loadUsingSystemPrincipal: true,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
  }).QueryInterface(Ci.nsIHttpChannel);

  if (bypassCache) {
    chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
  }

  return chan;
}

function channelOpenPromise(chan, intermittentFail = false) {
  return new Promise(resolve => {
    function finish(req, buffer) {
      resolve([req, buffer]);
    }
    if (intermittentFail) {
      chan.asyncOpen(
        new SimpleChannelListener(finish, null, CL_ALLOW_UNKNOWN_CL)
      );
    } else {
      chan.asyncOpen(new ChannelListener(finish, null, CL_ALLOW_UNKNOWN_CL));
    }
  });
}

function verifyDictionaryStored(url, shouldExist) {
  return new Promise(resolve => {
    let lci = Services.loadContextInfo.custom(false, {
      partitionKey: `(https,localhost)`,
    });
    asyncCheckCacheEntryPresence(url, "disk", shouldExist, lci, resolve);
  });
}

function syncCache() {
  return new Promise(resolve => {
    syncWithCacheIOThread(resolve, true);
  });
}

// Clear in-memory DictionaryCache and purge cache entries from memory.
// This forces dictionary origin entries to be reloaded from disk on next access,
// triggering DictionaryOriginReader::OnCacheEntryAvailable.
async function clearDictionaryCacheAndPurgeMemory() {
  // Clear the DictionaryCache in-memory hashmap
  let testingInterface = Services.cache2.QueryInterface(Ci.nsICacheTesting);
  testingInterface.clearDictionaryCacheMemory();

  // Force GC to release references to cache entries.  Probably not strictly needed
  gc();
}

/**
 * Test that replacing a dictionary resource with non-dictionary content
 * properly removes the dictionary metadata.
 *
 * Steps:
 * 1. Load a resource with Use-As-Dictionary header (creates dictionary entry)
 * 2. Verify Available-Dictionary is sent for matching resources
 * 3. Force-reload the dictionary resource WITHOUT Use-As-Dictionary
 * 4. Verify Available-Dictionary is NO LONGER sent for matching resources
 */

add_task(async function test_dictionary_replacement_removes_metadata() {
  // Track Available-Dictionary headers received by server
  let receivedAvailableDictionary = null;

  // Register dictionary endpoint that returns dictionary content
  await server.registerPathHandler(
    "/dict/resource",
    function (request, response) {
      response.writeHead(200, {
        "Content-Type": "application/octet-stream",
        "Use-As-Dictionary": 'match="/matching/*", id="test-dict", type=raw',
        "Cache-Control": "max-age=3600",
      });
      response.end("DICTIONARY_CONTENT_FOR_COMPRESSION_TEST_DATA", "binary");
    }
  );

  // Register matching resource endpoint
  await server.registerPathHandler(
    "/matching/test",
    function (request, response) {
      // Store the Available-Dictionary header value in global for later retrieval
      global.lastAvailableDictionary =
        request.headers["available-dictionary"] || null;
      response.writeHead(200, {
        "Content-Type": "text/plain",
        "Cache-Control": "no-cache",
      });
      response.end("CONTENT_THAT_SHOULD_MATCH_DICTIONARY", "binary");
    }
  );

  dump("**** Step 1: Load dictionary resource with Use-As-Dictionary\n");

  let dictUrl = `https://localhost:${server.port()}/dict/resource`;
  let chan = makeChan(dictUrl);
  let [, data] = await channelOpenPromise(chan);

  Assert.equal(data, DICTIONARY_CONTENT, "Dictionary content should match");

  // Verify dictionary is stored in cache
  await verifyDictionaryStored(dictUrl, true);

  // Sync to ensure everything is written to disk
  await syncCache();

  dump("**** Step 1.5: Clear in-memory caches to force reload from disk\n");

  // Clear in-memory DictionaryCache and purge cache entries from memory.
  // This forces dictionary entries to be reloaded from disk via
  // DictionaryOriginReader::OnCacheEntryAvailable, which is the code path
  // with the bug we're testing.
  await clearDictionaryCacheAndPurgeMemory();

  dump(
    "**** Step 2: Verify Available-Dictionary is sent for matching resource\n"
  );

  let matchingUrl = `https://localhost:${server.port()}/matching/test`;
  chan = makeChan(matchingUrl);
  await channelOpenPromise(chan);

  // Get the Available-Dictionary value from the server
  receivedAvailableDictionary = await server.execute(
    "global.lastAvailableDictionary"
  );

  Assert.notStrictEqual(
    receivedAvailableDictionary,
    null,
    "Available-Dictionary header should be sent for matching resource"
  );
  Assert.ok(
    receivedAvailableDictionary.includes(":"),
    "Available-Dictionary should contain a hash"
  );

  dump(`**** Received Available-Dictionary: ${receivedAvailableDictionary}\n`);

  dump(
    "**** Step 3: Force-reload dictionary resource WITHOUT Use-As-Dictionary\n"
  );

  // Re-register the dictionary endpoint to return content WITHOUT Use-As-Dictionary
  await server.registerPathHandler(
    "/dict/resource",
    function (request, response) {
      response.writeHead(200, {
        "Content-Type": "application/octet-stream",
        "Cache-Control": "max-age=3600",
        // No Use-As-Dictionary header!
      });
      response.end("REPLACED_CONTENT_WITHOUT_DICTIONARY_HEADER", "binary");
    }
  );

  chan = makeChan(dictUrl, true /* bypassCache */);
  [, data] = await channelOpenPromise(chan);

  Assert.equal(data, REPLACEMENT_CONTENT, "Replacement content should match");

  // Sync to ensure cache operations complete
  await syncCache();

  dump("**** Step 4: Verify Available-Dictionary is NOT sent anymore\n");

  // Reset the server's stored value
  await server.execute("global.lastAvailableDictionary = null");

  // Now request the matching resource again
  chan = makeChan(matchingUrl);
  await channelOpenPromise(chan);

  receivedAvailableDictionary = await server.execute(
    "global.lastAvailableDictionary"
  );

  Assert.equal(
    receivedAvailableDictionary,
    null,
    "Available-Dictionary header should NOT be sent after dictionary is replaced"
  );

  dump("**** Test passed: Dictionary metadata was properly removed\n");
});

/**
 * Test the same scenario but with gzip-compressed replacement content.
 * This simulates the real-world case where a server might return
 * compressed content without Use-As-Dictionary.
 */

add_task(async function test_dictionary_replacement_with_compressed_content() {
  dump("**** Clear cache and start fresh\n");
  let lci = Services.loadContextInfo.custom(false, {
    partitionKey: `(https,localhost)`,
  });
  evict_cache_entries("all", lci);

  // Also clear in-memory DictionaryCache to start fresh
  let testingInterface = Services.cache2.QueryInterface(Ci.nsICacheTesting);
  testingInterface.clearDictionaryCacheMemory();

  await syncCache();

  let receivedAvailableDictionary = null;

  // Register dictionary endpoint
  await server.registerPathHandler(
    "/dict/compressed",
    function (request, response) {
      response.writeHead(200, {
        "Content-Type": "application/octet-stream",
        "Use-As-Dictionary":
          'match="/compressed-match/*", id="compressed-dict", type=raw',
        "Cache-Control": "max-age=3600",
      });
      response.end("DICTIONARY_FOR_COMPRESSED_TEST", "binary");
    }
  );

  // Register matching resource endpoint
  await server.registerPathHandler(
    "/compressed-match/test",
    function (request, response) {
      global.lastCompressedAvailDict =
        request.headers["available-dictionary"] || null;
      response.writeHead(200, {
        "Content-Type": "text/plain",
        "Cache-Control": "no-cache",
      });
      response.end("MATCHING_CONTENT", "binary");
    }
  );

  dump("**** Step 1: Load dictionary resource with Use-As-Dictionary\n");

  let dictUrl = `https://localhost:${server.port()}/dict/compressed`;
  let chan = makeChan(dictUrl);
  let [, data] = await channelOpenPromise(chan);

  Assert.equal(
    data,
    "DICTIONARY_FOR_COMPRESSED_TEST",
    "Dictionary content should match"
  );
  await verifyDictionaryStored(dictUrl, true);
  await syncCache();

  dump("**** Step 1.5: Clear in-memory caches to force reload from disk\n");
  await clearDictionaryCacheAndPurgeMemory();

  dump("**** Step 2: Verify Available-Dictionary is sent\n");

  let matchingUrl = `https://localhost:${server.port()}/compressed-match/test`;
  chan = makeChan(matchingUrl);
  await channelOpenPromise(chan);

  receivedAvailableDictionary = await server.execute(
    "global.lastCompressedAvailDict"
  );
  Assert.notStrictEqual(
    receivedAvailableDictionary,
    null,
    "Available-Dictionary should be sent initially"
  );

  dump(
    "**** Step 3: Force-reload with gzip-compressed content (no Use-As-Dictionary)\n"
  );

  // Re-register to return gzip-compressed content without Use-As-Dictionary
  await server.registerPathHandler(
    "/dict/compressed",
    function (request, response) {
      // Gzip-compressed version of "GZIP_COMPRESSED_REPLACEMENT"
      // Using Node.js zlib in the handler
      const zlib = require("zlib");
      const compressed = zlib.gzipSync("GZIP_COMPRESSED_REPLACEMENT");

      response.writeHead(200, {
        "Content-Type": "application/octet-stream",
        "Content-Encoding": "gzip",
        "Cache-Control": "max-age=3600",
        // No Use-As-Dictionary header!
      });
      response.end(compressed);
    }
  );

  chan = makeChan(dictUrl, true /* bypassCache */);
  [, data] = await channelOpenPromise(chan);

  // Content should be decompressed by the channel
  Assert.equal(
    data,
    "GZIP_COMPRESSED_REPLACEMENT",
    "Decompressed replacement content should match"
  );

  dump("**** Step 4: Verify Available-Dictionary is NOT sent anymore\n");

  await server.execute("global.lastCompressedAvailDict = null");
  chan = makeChan(matchingUrl);
  await channelOpenPromise(chan);

  receivedAvailableDictionary = await server.execute(
    "global.lastCompressedAvailDict"
  );

  Assert.equal(
    receivedAvailableDictionary,
    null,
    "Available-Dictionary should NOT be sent after dictionary replaced with compressed content"
  );

  dump(
    "**** Test passed: Dictionary metadata removed even with compressed replacement\n"
  );
});

/**
 * Test that multiple sequential replacements work correctly.
 * Dictionary -> Non-dictionary -> Dictionary -> Non-dictionary
 */

add_task(async function test_dictionary_multiple_replacements() {
  dump("**** Clear cache and start fresh\n");
  let lci = Services.loadContextInfo.custom(false, {
    partitionKey: `(https,localhost)`,
  });
  evict_cache_entries("all", lci);

  // Also clear in-memory DictionaryCache to start fresh
  let testingInterface = Services.cache2.QueryInterface(Ci.nsICacheTesting);
  testingInterface.clearDictionaryCacheMemory();

  await syncCache();

  let receivedAvailableDictionary = null;

  // Register matching resource endpoint
  await server.registerPathHandler(
    "/multi-match/test",
    function (request, response) {
      global.lastMultiAvailDict =
        request.headers["available-dictionary"] || null;
      response.writeHead(200, {
        "Content-Type": "text/plain",
        "Cache-Control": "no-cache",
      });
      response.end("MULTI_MATCHING_CONTENT", "binary");
    }
  );

  let dictUrl = `https://localhost:${server.port()}/dict/multi`;
  let matchingUrl = `https://localhost:${server.port()}/multi-match/test`;

  // === First: Load as dictionary ===
  dump("**** Load as dictionary (first time)\n");
  await server.registerPathHandler("/dict/multi", function (request, response) {
    response.writeHead(200, {
      "Content-Type": "application/octet-stream",
      "Use-As-Dictionary":
        'match="/multi-match/*", id="multi-dict-1", type=raw',
      "Cache-Control": "max-age=3600",
    });
    response.end("DICTIONARY_CONTENT_V1", "binary");
  });

  let chan = makeChan(dictUrl);
  await channelOpenPromise(chan);
  await syncCache();

  // Clear in-memory caches to force reload from disk
  await clearDictionaryCacheAndPurgeMemory();

  chan = makeChan(matchingUrl);
  await channelOpenPromise(chan);
  receivedAvailableDictionary = await server.execute(
    "global.lastMultiAvailDict"
  );
  Assert.notStrictEqual(
    receivedAvailableDictionary,
    null,
    "Available-Dictionary should be sent (first dictionary)"
  );

  // === Second: Replace with non-dictionary ===
  dump("**** Replace with non-dictionary\n");
  await server.registerPathHandler("/dict/multi", function (request, response) {
    response.writeHead(200, {
      "Content-Type": "application/octet-stream",
      "Cache-Control": "max-age=3600",
    });
    response.end("NON_DICTIONARY_CONTENT", "binary");
  });

  chan = makeChan(dictUrl, true);
  await channelOpenPromise(chan);
  await syncCache();
  await new Promise(resolve => do_timeout(200, resolve));

  await server.execute("global.lastMultiAvailDict = null");
  chan = makeChan(matchingUrl);
  await channelOpenPromise(chan);
  receivedAvailableDictionary = await server.execute(
    "global.lastMultiAvailDict"
  );
  Assert.equal(
    receivedAvailableDictionary,
    null,
    "Available-Dictionary should NOT be sent (after first replacement)"
  );

  // === Third: Load as dictionary again ===
  dump("**** Load as dictionary (second time)\n");
  await server.registerPathHandler("/dict/multi", function (request, response) {
    response.writeHead(200, {
      "Content-Type": "application/octet-stream",
      "Use-As-Dictionary":
        'match="/multi-match/*", id="multi-dict-2", type=raw',
      "Cache-Control": "max-age=3600",
    });
    response.end("DICTIONARY_CONTENT_V2", "binary");
  });

  chan = makeChan(dictUrl, true);
  await channelOpenPromise(chan);
  await syncCache();

  // Clear in-memory caches to force reload from disk
  await clearDictionaryCacheAndPurgeMemory();

  chan = makeChan(matchingUrl);
  await channelOpenPromise(chan);
  receivedAvailableDictionary = await server.execute(
    "global.lastMultiAvailDict"
  );
  Assert.notStrictEqual(
    receivedAvailableDictionary,
    null,
    "Available-Dictionary should be sent (second dictionary)"
  );

  // === Fourth: Replace with non-dictionary again ===
  dump("**** Replace with non-dictionary again\n");
  await server.registerPathHandler("/dict/multi", function (request, response) {
    response.writeHead(200, {
      "Content-Type": "application/octet-stream",
      "Cache-Control": "max-age=3600",
    });
    response.end("NON_DICTIONARY_CONTENT_V2", "binary");
  });

  chan = makeChan(dictUrl, true);
  await channelOpenPromise(chan);
  await syncCache();
  await new Promise(resolve => do_timeout(200, resolve));

  await server.execute("global.lastMultiAvailDict = null");
  chan = makeChan(matchingUrl);
  await channelOpenPromise(chan);
  receivedAvailableDictionary = await server.execute(
    "global.lastMultiAvailDict"
  );
  Assert.equal(
    receivedAvailableDictionary,
    null,
    "Available-Dictionary should NOT be sent (after second replacement)"
  );

  dump("**** Test passed: Multiple replacements work correctly\n");
});

/**
 * Test that hash mismatch during dictionary load causes the request to fail
 * and the corrupted dictionary entry to be removed.
 *
 * Steps:
 * 1. Load a resource with Use-As-Dictionary header (creates dictionary entry)
 * 2. Verify Available-Dictionary is sent for matching resources
 * 3. Corrupt the hash using the testing API
 * 4. Clear memory cache to force reload from disk
 * 5. Request a matching resource - dictionary prefetch should fail
 * 6. Verify the dictionary entry was removed (Available-Dictionary no longer sent)
 */

add_task(async function test_dictionary_hash_mismatch() {
  dump("**** Clear cache and start fresh\n");
  let lci = Services.loadContextInfo.custom(false, {
    partitionKey: `(https,localhost)`,
  });
  evict_cache_entries("all", lci);

  let testingInterface = Services.cache2.QueryInterface(Ci.nsICacheTesting);
  testingInterface.clearDictionaryCacheMemory();

  await syncCache();

  let receivedAvailableDictionary = null;

  // Register dictionary endpoint
  await server.registerPathHandler(
    "/dict/hash-test",
    function (request, response) {
      response.writeHead(200, {
        "Content-Type": "application/octet-stream",
        "Use-As-Dictionary":
          'match="/hash-match/*", id="hash-test-dict", type=raw',
        "Cache-Control": "max-age=3600",
      });
      response.end("DICTIONARY_FOR_HASH_TEST", "binary");
    }
  );

  // Register matching resource endpoint
  await server.registerPathHandler(
    "/hash-match/test",
    function (request, response) {
      global.lastHashTestAvailDict =
        request.headers["available-dictionary"] || null;
      response.writeHead(200, {
        "Content-Type": "text/plain",
        "Cache-Control": "no-cache",
      });
      response.end("HASH_MATCHING_CONTENT", "binary");
    }
  );

  dump("**** Step 1: Load dictionary resource with Use-As-Dictionary\n");

  let dictUrl = `https://localhost:${server.port()}/dict/hash-test`;
  let chan = makeChan(dictUrl);
  let [, data] = await channelOpenPromise(chan);

  Assert.equal(
    data,
    "DICTIONARY_FOR_HASH_TEST",
    "Dictionary content should match"
  );
  await verifyDictionaryStored(dictUrl, true);
  await syncCache();

  dump("**** Step 2: Verify Available-Dictionary is sent\n");

  let matchingUrl = `https://localhost:${server.port()}/hash-match/test`;
  chan = makeChan(matchingUrl);
  await channelOpenPromise(chan);

  receivedAvailableDictionary = await server.execute(
    "global.lastHashTestAvailDict"
  );
  Assert.notStrictEqual(
    receivedAvailableDictionary,
    null,
    "Available-Dictionary should be sent initially"
  );

  dump("**** Step 3: Corrupt the dictionary hash\n");

  testingInterface.corruptDictionaryHash(dictUrl);

  dump("**** Step 4: Clear dictionary data to force reload from disk\n");

  // Clear dictionary data while keeping the corrupted hash.
  // When next prefetch happens, data will be reloaded and compared
  // against the corrupted hash, causing a mismatch.
  testingInterface.clearDictionaryDataForTesting(dictUrl);

  dump(
    "**** Step 5: Request matching resource - should fail due to hash mismatch\n"
  );

  await server.execute("global.lastHashTestAvailDict = null");

  // The request for the matching resource will try to prefetch the dictionary,
  // which will fail due to hash mismatch. The channel should be cancelled.
  chan = makeChan(matchingUrl);
  try {
    await channelOpenPromise(chan, true); // intermittent failure
  } catch (e) {
    dump(`**** Request failed with: ${e}\n`);
  }

  // Note: The request may or may not fail depending on timing. The important
  // thing is that the dictionary entry should be removed.

  dump("**** Step 6: Verify dictionary entry was removed\n");

  // Wait a bit for the removal to complete
  await syncCache();

  await server.execute("global.lastHashTestAvailDict = null");
  chan = makeChan(matchingUrl);
  await channelOpenPromise(chan);

  receivedAvailableDictionary = await server.execute(
    "global.lastHashTestAvailDict"
  );
  Assert.equal(
    receivedAvailableDictionary,
    null,
    "Available-Dictionary should NOT be sent after dictionary was removed due to hash mismatch"
  );

  dump("**** Test passed: Hash mismatch properly handled\n");
});

// Cleanup
add_task(async function cleanup() {
  let lci = Services.loadContextInfo.custom(false, {
    partitionKey: `(https,localhost)`,
  });
  evict_cache_entries("all", lci);
  dump("**** All dictionary replacement tests completed\n");
});

Messung V0.5 in Prozent
C=89 H=100 G=94

¤ Dauer der Verarbeitung: 0.14 Sekunden  (vorverarbeitet am  2026-09-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1126438
#Domains=1867298