So I have an ancient custom search engine XML that was broken by remote API changes. In trying to fix it, I inadvertently had it deleted from search.json.mozlz4, and have to figure out how to add it back.

This is basically a rewrite of this post by Frederick Zhang. That post is excellent, but there have been some changes since 2018. The following is tested on the latest stable Firefox (138.0.1).

First download this Python script. Use it to decompress search.json.mozlz4 (located in the Firefox profile directory): python3 mozlz4a.py -d search.json.mozlz4 search.json

Optionally, format it with python3 -m json.tool.

Add a new object to the "engines" array. My example for Startpage with custom params:

        {
            "id": "38c37483-6e61-4f86-bfaf-2b99ed7d8464",
            "_name": "Startpage (Unfiltered)",
            "_loadPath": "[profile]/searchplugins/startpage-unfiltered.xml",
            "_iconMapObj": {
                "16": "data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2jkj+9YtD/vWLQ/71i0P+9otD/vaLRP72i0T+9YtE/vWLRP72i0T+9otD/vaNRP72jUT+9otF/vaLRf73kkv+9Yc///WJP//1iT//9Yk///rAmf/94Mz/+sCa//aRTv/1iUH/9ok///aJP//2i0H/9otB//aJQv/2iUL/9otC//aNRP/2jUT/9o1E//aNRP/6wpv////////////96dr/95dQ//aNRP/2kET/9pBG//aQRv/2kEb/9pBG//aRR//3lEz/95BH//mueP/7xJ3/959g//efYf/4p23//vDm//3p2//3kEr/95FJ//aRSf/niFH/95FK//aRSv/2mE//95hS/vq4iP/////////////////81bj/95xZ//q4iP//////+bF+//eZT//njFT/PSqi/2xGjv/2mVD/951V/vedVv783cX///////vQrf/++PP///////748//+8uj///////m3gf/olFr/PSuj/w8Pt/9sSJD/951V//eeWf73oVv++8ul///////5sXf/+KRi//vRsf////////////3r3v/olF//Piyk/w8Pt/9sSJH/+J5Z//ieWv/3oV/++KZf/vihXP/97N7//vn0//zTs//6wJP/+bBy//q6iP/onW//Piyl/w8Pt/8fGbH/m2iB/+icY//4pGD/96hl/viqZf74pmD/+Kxr//3iy/////////n1//ivbP/onGj/Pi2m/w8Pt/8uJKz/fFeQ/x8Zsf8+Lqb/6J9r//ivbP74rm3++Klm//mpZv/5q2f/+bR9//m0e//poW7/Pi6n/w8Pt/9sTZj/+Ktp//ira/+rd4P/Dw+3/4xijv/5snH++LN1/vmvbf/5r23/+a5t//mvb//4r2//TTuk/w8Pt/8fGrL/6ah1//ivcP/4r3P/q3yI/w8Pt/+MZpP/+bN5/vm4ev75t3X/+bV1//m1df/5t3X/+Ld3/8qUhP98XZn/Hxqz/+mse//5t3f/2p+B/x8as/8PD7f/u4qK//m7fv76u4D++bl7//m3fP/5uXz/+bl8//m5fP/5t3z/+bl//x8as/9NPKf/fWCb/x8as/8PD7f/bVOh//q5f//6v4X++sGI/vm9g//5voX/+b6F//m9hf/6vYX/+r6F//nCh/+bepr/Hxu0/w8Pt/8PD7f/fWOh//q+hf/6wof/+saN/vrGjf75xIv/+ceL//nEi//5xIv/+sSL//rHi//6x43/+ceN/+m7kP+7lpj/6ruQ//rHkP/6x43/+seQ//rLlf76ypT++seR//rJkf/6yZH/+seR//rJkf/6yZH/+8mR//vJlP/7yZT/+smU//rJlP/6yZT/+8yV//rJlf/6zpn+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
            },
            "_metaData": {
                "loadPathHash": "<SEE BELOW>",
                "alias": "sp",
                "hideOneOffButton": false,
                "order": 8
            },
            "_urls": [
                {
                    "params": [
                        {
                            "name": "query",
                            "value": "{searchTerms}"
                        },
                        {
                            "name": "cat",
                            "value": "web"
                        },
                        {
                            "name": "lui",
                            "value": "english"
                        },
                        {
                            "name": "prfe",
                            "value": "c774daad5dd23ae2db70252aa321b9354508577d11ee4a79664d62c5d56672dd946c593d0daa5e9f3e7d1ebafaf773669d191e7b802605a743232b31a2254feeccf8ccc0f306bf45f8fa73f3"
                        }
                    ],
                    "rels": [],
                    "template": "https://www.startpage.com/sp/search",
                    "method": "POST"
                }
            ],
            "_orderHint": null,
            "_telemetryId": null,
            "_filePath": "<ABSOLUTE PATH TO PROFILE DIRECTORY>/searchplugins/startpage-unfiltered.xml",
            "_definedAliases": [],
            "_updateInterval": null,
            "_updateURL": null
        }

I assume the "id" is just some UUID. I ended up reusing an existing one that I no longer needed. I’m not sure whether it would be “validated” in any way.

The "loadPathHash" is calculated using the following function:

 function getVerificationHash(name, profileDir = PathUtils.profileDir) {
   let disclaimer =
     "By modifying this file, I agree that I am doing so " +
     "only within $appName itself, using official, user-driven search " +
     "engine selection processes, and in a way which does not circumvent " +
     "user consent. I acknowledge that any attempt to change this file " +
     "from outside of $appName is a malicious act, and will be responded " +
     "to accordingly.";

  let salt =
    PathUtils.filename(profileDir) +
    name +
    disclaimer.replace(/\$appName/g, Services.appinfo.name);

  let data = new TextEncoder().encode(salt);
  let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
    Ci.nsICryptoHash
  );
  hasher.init(hasher.SHA256);
  hasher.update(data, data.length);

  return hasher.finish(true);
}

This code is taken from the Firefox source code here. It is to be pasted into the Browser Console, then called with getVerificationHash("<VALUE IN _loadPath>", "<ABSOLUTE PATH TO PROFILE DIRECTORY>"). The second argument is required if you are running on a different profile.

The "order" is just whichever unused number that comes next.

Save this, then compress it again with: python3 mozlz4a.py search.json search.json.mozlz4.

Finally, create a backup of the original search.json.mozlz4, and replace it with the new one.