async function parseFields(settings, login) {
    var response = await hostAction(settings, "fetch", {
        storeId: login.store.id,
        file: login.loginPath,
    });
    if (response.status != "ok") {
        throw new Error(JSON.stringify(response)); // TODO handle host error
    }

    var allowEmpty = ["login"];

    // save raw data inside login
    login.raw = response.data.contents;

    // parse lines
    login.fields = helpers.deepCopy(helpers.fieldsPrefix);
    login.settings = {
        autoSubmit: { name: "autosubmit", type: "bool" },
    };
    var lines = login.raw.split(/[\r\n]+/).filter((line) => line.trim().length > 0);
    lines.forEach(function (line) {
        // check for uri-encoded otp without line prefix
        if (line.match(/^otpauth:\/\/.+/i)) {
            line = `otp: ${line}`;
        }

        // split key / value & ignore non-k/v lines
        var parts = line.match(/^(.+?):(.*)$/);
        if (parts === null) {
            return;
        }
        parts = parts
            .slice(1)
            .map((value) => value.trim())
            .filter((value) => value.length);
        if (!parts.length) {
            return;
        }

        // assign to fields
        for (var key in login.fields) {
            if (
                Array.isArray(login.fields[key]) &&
                login.fields[key].includes(parts[0].toLowerCase())
            ) {
                if (parts.length < 2 && !allowEmpty.includes(key)) {
                    return;
                }
                login.fields[key] = parts[1];
                break;
            }
        }

        // assign to settings
        for (var key in login.settings) {
            if (
                typeof login.settings[key].type !== "undefined" &&
                login.settings[key].name === parts[0].toLowerCase()
            ) {
                if (login.settings[key].type === "bool") {
                    login.settings[key] = ["true", "yes"].includes(parts[1].toLowerCase());
                } else {
                    login.settings[key] = parts[1];
                }

                break;
            }
        }
    });

    // clean up unassigned fields
    for (var key in login.fields) {
        if (Array.isArray(login.fields[key])) {
            if (key === "secret" && lines.length) {
                login.fields.secret = lines[0];
            } else if (key === "login") {
                const defaultUsername = helpers.getSetting("username", login, settings);
                login.fields[key] = defaultUsername || login.login.match(/([^\/]+)$/)[1];
            } else {
                delete login.fields[key];
            }
        }
    }
    for (var key in login.settings) {
        if (typeof login.settings[key].type !== "undefined") {
            delete login.settings[key];
        }
    }

    // preprocess otp
    if (helpers.getSetting("enableOTP", login, settings) && login.fields.hasOwnProperty("otp")) {
        if (login.fields.otp.match(/^otpauth:\/\/.+/i)) {
            // attempt to parse otp data as URI
            try {
                // change otpauth:// to http:// to work around a bug in firefox versions
                // between 122 and 132 where the hostname is read as an empty string for
                // urls that use a custom protocol like otpauth:// so the
                // parsing behavior of such urls changes depending on
                // browser version, while if we change it to http:// first
                // then we always get the same result
                let url = new URL(login.fields.otp.toLowerCase().replace("otpauth://", "http://"));
                login.fields.otp = {
                    raw: login.fields.otp,
                    params: {
                        type: url.host === "otp" ? "totp" : url.host,
                        secret: url.searchParams.get("secret").toUpperCase(),
                        algorithm: url.searchParams.get("algorithm") || "sha1",
                        digits: parseInt(url.searchParams.get("digits") || "6"),
                        period: parseInt(url.searchParams.get("period") || "30"),
                    },
                };
            } catch (e) {
                throw new Exception(`Unable to parse URI: ${otp.data}`, e);
            }
        } else {
            // use default params for secret-only otp data
            login.fields.otp = {
                raw: login.fields.otp,
                params: {
                    type: "totp",
                    secret: login.fields.otp.toUpperCase(),
                    algorithm: "sha1",
                    digits: 6,
                    period: 30,
                },
            };
        }
    }
}
Fetch file & parse fields

@since 3.0.0

@param object settings Settings object
@param object login    Login object
@return void