Чтение сжатого текстового файла LZ4 (mozlz4) в WebExtensions (JavaScript, Firefox)

Я портирую расширение SDK для Firefox на WebExtensions. Раньше я мог получить доступ к поисковым системам браузера, но теперь не могу, поэтому полезный пользователь посоветовал мне попробовать прочитать файл search.json.mozlz4, в котором есть все установленные движки. Однако этот файл представляет собой json со сжатием LZ4 и в собственном формате Mozilla LZ4 с пользовательским магическим числом "mozLz40\0".

Раньше можно было использовать это для чтения текстового файла, который использует сжатие LZ4, включая файл mozlz4:

let bytes = OS.File.read(path, { compression: "lz4" });
let content = new TextDecoder().decode(bytes);

(хотя я не смог найти документацию по полю "компрессия", все работает)

Теперь, используя WebExtensions, лучшее, что я мог придумать, чтобы прочитать файл, это

var reader = new FileReader();
reader.readAsText(file);
reader.onload = function(ev) {
    let content = ev.target.result;
};

Это никак не влияет на сжатие. Эта библиотека обрабатывает LZ4 , но она предназначена для node.js, поэтому я не могу это использовать. [править: он тоже работает автономно]. Однако, даже если я удаляю пользовательскую обработку магических чисел, я не могу заставить ее распаковать файл, в то время как этот код Python, для сравнения, работает как ожидалось:

import lz4
file_obj = open("search.json.mozlz4", "rb")
if file_obj.read(8) != b"mozLz40\0":
    raise InvalidHeader("Invalid magic number")
print(lz4.block.decompress(file_obj.read()))

Как я могу сделать это в JS?

1 ответ

Решение

После долгих проб и ошибок я наконец смог прочитать и расшифровать файл search.json.mozlz4 в WebExtension. Вы можете использовать библиотеку node-lz4, хотя вам понадобится только одна функция - uncompress (с псевдонимом как decodeBlock для внешнего доступа) - поэтому я переименовал его в decodeLz4Block и включил его сюда с небольшими изменениями:

// This method's code was taken from node-lz4 by Pierre Curto. MIT license.
// CHANGES: Added ; to all lines. Reformated one-liners. Removed n = eIdx. Fixed eIdx skipping end bytes if sIdx != 0.
function decodeLz4Block(input, output, sIdx, eIdx)
{
    sIdx = sIdx || 0;
    eIdx = eIdx || input.length;

    // Process each sequence in the incoming data
    for (var i = sIdx, j = 0; i < eIdx;)
    {
        var token = input[i++];

        // Literals
        var literals_length = (token >> 4);
        if (literals_length > 0) {
            // length of literals
            var l = literals_length + 240;
            while (l === 255) {
                l = input[i++];
                literals_length += l;
            }

            // Copy the literals
            var end = i + literals_length;
            while (i < end) {
                output[j++] = input[i++];
            }

            // End of buffer?
            if (i === eIdx) {
                return j;
            }
        }

        // Match copy
        // 2 bytes offset (little endian)
        var offset = input[i++] | (input[i++] << 8);

        // 0 is an invalid offset value
        if (offset === 0 || offset > j) {
            return -(i-2);
        }

        // length of match copy
        var match_length = (token & 0xf);
        var l = match_length + 240;
        while (l === 255) {
            l = input[i++];
            match_length += l;
        }

        // Copy the match
        var pos = j - offset; // position of the match copy in the current output
        var end = j + match_length + 4; // minmatch = 4
        while (j < end) {
            output[j++] = output[pos++];
        }
    }

    return j;
}

Затем объявите эту функцию, которая получает объект File (не путь) и обратные вызовы для успеха / ошибки:

function readMozlz4File(file, onRead, onError)
{
    let reader = new FileReader();

    reader.onload = function() {
        let input = new Uint8Array(reader.result);
        let output;
        let uncompressedSize = input.length*3;  // size estimate for uncompressed data!

        // Decode whole file.
        do {
            output = new Uint8Array(uncompressedSize);
            uncompressedSize = decodeLz4Block(input, output, 8+4);  // skip 8 byte magic number + 4 byte data size field
            // if there's more data than our output estimate, create a bigger output array and retry (at most one retry)
        } while (uncompressedSize > output.length);

        output = output.slice(0, uncompressedSize); // remove excess bytes

        let decodedText = new TextDecoder().decode(output);
        onRead(decodedText);
    };

    if (onError) {
        reader.onerror = onError;
    }

    reader.readAsArrayBuffer(file); // read as bytes
};

Затем вы можете добавить кнопку HTML на страницу настроек надстройки, которая позволяет пользователю выполнять поиск и выбирать search.json.mozlz4 (в WebExtensions вы не можете просто открыть любой файл в файловой системе без вмешательства пользователя):

<input name="selectMozlz4FileButton" type="file" accept=".json.mozlz4">

Чтобы ответить пользователю, выбирающему файл, используйте что-то вроде этого, которое вызывает метод, который мы ранее объявили (здесь я не использую функцию обратного вызова с ошибкой, но вы можете):

let button = document.getElementsByName("selectMozlz4FileButton")[0];
button.onchange = function onButtonPress(ev) {
    let file = ev.target.files[0];
    readMozlz4File(file, function(text){
        console.log(text);
    });
};

Я надеюсь, что это помогает кому-то. Я уверен, что потратил много времени на разработку этой простой вещи.:)

Другие вопросы по тегам