🏡 index : quine-global-labs/mirror-cockpit-project-cockpit.git

/*
 * Copyright (C) 2022 SUSE LLC
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

import { ConfigFile } from './config-client.js';

/* Parse an dotenv-style config file
 * and monitor it for changes
 */
export class ConfigFileSUSE extends ConfigFile {
    /* parse lines of the config file
     * if a line has a valid key=value format, use the key in _internal structure
     * and also store original line, line index, value and optional line suffix / comment
     * if value was quoted it will be stripped of quotes in `value` and `quoted` flag will
     * be used when writing the file to keep original formatting
     * e.g. for line 'someKey="foo" # comment'
     * outputObject._internal["someKey"] = {
     *      index: 0,
     *      value: "foo",
     *      quoted: true,
     *      origLine: 'someKey="foo" # comment',
     *      suffix: "# comment"
     * }
     * skipNotify: Don't notify about changes, e.g.to avoid multiple updates when writing a file
     */
    _parseText(rawContent, skipNotify = false) {
        this._dataAvailableResolve();

        // clear settings if file is empty/missing
        if (!rawContent) {
            this._originalSettings = null;
            this.settings = null;
            if (!skipNotify)
                this.dispatchEvent("kdumpConfigChanged", this.settings);
            return;
        }

        // if nothing changed, don't bother parsing the content
        if (rawContent === this._rawContent)
            return;

        this._rawContent = rawContent;

        // this is the format expected by the UI
        this.settings = {
            _internal: {},
            targets: {},
            compression: { enabled: false, allowed: true },
            nfs_supports_directory: false,
        };

        this._lines = rawContent.split(/\r?\n/);
        this._lines.forEach((line, index) => {
            const trimmed = line.trim();
            // if the line is empty or only a comment, skip
            if (trimmed.indexOf("#") === 0 || trimmed.length === 0)
                return;

            // parse KEY=value or KEY="value" line
            let parts = trimmed.match(/^([A-Z_]+)\s*=\s*(.*)$/);
            if (parts === null) {
                console.warn("Malformed kdump config line:", trimmed, "in", this.filename);
                return;
            }
            const key = parts[1];
            let value = parts[2];

            // value might be quoted
            let quoted = false;
            if (value.startsWith('"')) {
                quoted = true;
                parts = value.match(/^"([^"]*)"\s*(.*)$/);
                // malformed line, no ending quote?
                if (parts === null) {
                    console.warn("Incorrectly quoted value in kdump config line:", line, "in", this.filename);
                    return;
                }
            } else {
                // not quoted should be simple value but grab everything and quote on write
                parts = value.match(/^([^#]+?)\s*(#.*)?$/);
                if (parts === null)
                    parts = ["", ""];
            }
            value = parts[1];
            const suffix = (parts[2] || "").trim();

            this.settings._internal[key] = {
                index,
                value,
                origLine: line,
                quoted,
                suffix
            };
        });

        // make sure we copy the original keys so we overwrite the correct lines when saving
        this._originalSettings = { };
        Object.keys(this.settings._internal).forEach((key) => {
            this._originalSettings[key] = { ...this.settings._internal[key] };
        });

        this._extractSettings();

        if (!skipNotify)
            this.dispatchEvent("kdumpConfigChanged", this.settings);
    }

    /* extract settings managed by cockpit from _internal into platform independent model
     */
    _extractSettings() {
        // generate target(s) from KDUMP_SAVEDIR
        if ("KDUMP_SAVEDIR" in this.settings._internal && this.settings._internal.KDUMP_SAVEDIR.value) {
            let savedir = this.settings._internal.KDUMP_SAVEDIR.value;
            // handle legacy "file" without prefix
            if (savedir.startsWith("/"))
                savedir = "file://" + savedir;
            // server includes "username:password@" and can be empty for file://
            const parts = savedir.match(/^(.*):\/\/([^/]*)(\/.*)$/);
            // malformed KDUMP_SAVEDIR
            if (parts === null) {
                console.warn("Malformed KDUMP_SAVEDIR entry:", savedir, "in", this.filename);
                return;
            }
            const [, scheme, server, path] = parts;
            if (scheme === "file") {
                this.settings.targets.local = {
                    type: "local",
                    path,
                };
            } else if (scheme === "nfs") {
                this.settings.targets.nfs = {
                    type: scheme,
                    // on read full path is used as export
                    export: path,
                    server,
                };
            } else {
                this.settings.targets[scheme] = {
                    type: scheme,
                    path,
                    server,
                };
                // sshkey is used by ssh and sftp/scp
                if ("KDUMP_SSH_IDENTITY" in this.settings._internal) {
                    this.settings.targets[scheme].sshkey =
                        this.settings._internal.KDUMP_SSH_IDENTITY.value;
                }
            }
        }

        // default to local if no target configured
        if (Object.keys(this.settings.targets).length === 0)
            this.settings.targets.local = { type: "local" };

        this.settings.compression.enabled = (
            !("KDUMP_DUMPFORMAT" in this.settings._internal) ||
            // TODO: what about other compression formats (lzo, snappy)?
            this.settings._internal.KDUMP_DUMPFORMAT.value === "compressed"
        );
    }

    /* update single _internal setting to given value
     * make sure setting exists if value is not empty
     * don't delete existing settings
     */
    _updateSetting(settings, key, value) {
        if (key in settings._internal) {
            settings._internal[key].value = value;
        } else {
            if (value)
                settings._internal[key] = { value };
        }
    }

    /* transform settings from model back to _internal format
     * this.settings = current state from file
     * settings = in-memory state from UI
     */
    _persistSettings(settings) {
        // target
        if (Object.keys(settings.targets).length > 0) {
            const target = Object.values(settings.targets)[0];

            if ("sshkey" in target)
                this._updateSetting(settings, "KDUMP_SSH_IDENTITY", target.sshkey);

            let savedir;
            // default for empty path (except nfs, see below)
            let path = target.path || "/var/crash";
            if (path && !path.startsWith("/"))
                path = "/" + path;
            if (target.type === "local") {
                savedir = "file://" + path;
            } else if (target.type === "nfs") {
                // override empty path default as nfs path is merged into export on read
                if (!target.path)
                    path = "";
                let exprt = target.export;
                if (!exprt.startsWith("/"))
                    exprt = "/" + exprt;
                savedir = "nfs://" + target.server + exprt + path;
            } else {
                savedir = target.type + "://" + target.server + path;
            }
            this._updateSetting(settings, "KDUMP_SAVEDIR", savedir);
        }
        // compression
        if (this.settings.compression.enabled !== settings.compression.enabled) {
            if (settings.compression.enabled) {
                this._updateSetting(settings, "KDUMP_DUMPFORMAT", "compressed");
            } else {
                this._updateSetting(settings, "KDUMP_DUMPFORMAT", "ELF");
            }
        }
        return settings;
    }

    /* generate the config file from raw text and settings
     */
    generateConfig(settings) {
        settings = this._persistSettings(settings);

        const lines = this._lines.slice(0);

        // we take the lines from our last read operation and modify them with the new settings
        Object.keys(settings._internal).forEach((key) => {
            const entry = settings._internal[key];

            let value = entry.value !== undefined ? entry.value : "";
            // quote what was quoted before + empty values + multi-word values
            if (entry.quoted || value === "" || value.includes(" "))
                value = '"' + value + '"';
            let line = key + "=" + value;
            if (entry.suffix)
                line = line + " " + entry.suffix;
            // this might be a new entry
            if (!(key in this._originalSettings)) {
                lines.push(line);
                return;
            }
            // otherwise edit the old line
            const origEntry = this._originalSettings[key];
            lines[origEntry.index] = line;
        });

        // make sure file ends with a newline
        if (lines[lines.length - 1] !== "")
            lines.push("");
        return lines.join("\n");
    }
}