# SPDX-FileCopyrightText: 2019 Frank Hunleth # SPDX-FileCopyrightText: 2019 Justin Schneck # SPDX-FileCopyrightText: 2021 Connor Rigby # SPDX-FileCopyrightText: 2022 Jon Carstens # # SPDX-License-Identifier: Apache-2.0 # defmodule Nerves.Erlinit do @moduledoc """ Decode and encode erlinit.config files This module is used to decode, merge, and encode multiple erlinit.config files. """ @switches [ boot: :string, ctty: :string, uniqueid_exec: :string, env: :keep, gid: :integer, graceful_shutdown_timeout: :integer, hang_on_exit: :boolean, reboot_on_exit: :boolean, hang_on_fatal: :boolean, limits: :string, mount: :keep, hostname_pattern: :string, pre_run_exec: :string, poweroff_on_exit: :boolean, poweroff_on_fatal: :boolean, reboot_on_fatal: :boolean, release_path: :string, run_on_exit: :string, alternate_exec: :string, print_timing: :boolean, uid: :integer, update_clock: :boolean, verbose: :boolean, warn_unused_tty: :boolean, working_directory: :string, shutdown_report: :string ] @aliases [ b: :boot, c: :ctty, d: :uniqueid_exec, e: :env, h: :hang_on_exit, l: :limits, m: :mount, n: :hostname_pattern, r: :release_path, s: :alternate_exec, t: :print_timing, v: :verbose ] @type option() :: {:boot, Path.t()} | {:ctty, String.t()} | {:uniqueid_exec, String.t()} | {:env, String.t()} | {:gid, non_neg_integer()} | {:graceful_shutdown_timeout, non_neg_integer()} | {:hang_on_exit, boolean()} | {:hang_on_fatal, boolean()} | {:limits, String.t()} | {:mount, String.t()} | {:hostname_pattern, String.t()} | {:pre_run_exec, String.t()} | {:poweroff_on_exit, boolean()} | {:poweroff_on_fatal, boolean()} | {:reboot_on_fatal, boolean()} | {:release_path, Path.t()} | {:run_on_exit, String.t()} | {:alternate_exec, String.t()} | {:print_timing, boolean()} | {:uid, non_neg_integer()} | {:update_clock, boolean()} | {:verbose, boolean()} | {:warn_unused_tty, boolean()} | {:working_directory, Path.t()} | {:shutdown_report, Path.t()} @type t :: [option()] @doc """ Return the path to the erlinit.config file provided by the Nerves System """ @spec system_config_file(Nerves.Package.t()) :: {:ok, Path.t()} | {:error, :no_config} def system_config_file(%Nerves.Package{path: path}) do file = Path.join(path, "rootfs_overlay/etc/erlinit.config") case File.exists?(file) do true -> {:ok, file} false -> {:error, :no_config} end end # TODO: Remove this once this fix has been released in Elixir # https://github.com/elixir-lang/elixir/pull/11804 @dialyzer {:nowarn_function, decode_config: 1} @doc """ Decode the data from the config into a keyword list """ @spec decode_config(String.t()) :: t() def decode_config(config) do argv = config |> String.split("\n") |> Enum.map(&String.trim_leading/1) |> Enum.filter(&String.starts_with?(&1, "-")) |> Enum.map(&trim_trailing_comments/1) |> Enum.map(&String.split(&1, " ", parts: 2)) |> List.flatten() |> Enum.map(&String.trim/1) |> Enum.map(&trim_quoted_string/1) # `allow_nonexistent_atoms: true` allows unknown erlinit options to pass through. {opts, _, _} = OptionParser.parse(argv, switches: @switches, aliases: @aliases, allow_nonexistent_atoms: true ) opts end defp trim_quoted_string(<>) do content_len = byte_size(rest) - 1 <> = rest content end defp trim_quoted_string(s), do: s defp trim_trailing_comments(s) do # Trim everything after a #. This is flawed since quoted '#'s should work, # but I don't think that that exists in anything that erlinit can do... String.split(s, "#", parts: 2) |> hd() end @doc """ Merge keyword options """ @spec merge_opts(t(), t()) :: t() def merge_opts(old, new) do Enum.reduce(new, old, fn {k, nil}, acc -> Keyword.delete(acc, k) {k, v}, acc -> case Keyword.get(@switches, k) do :keep -> [{k, v} | acc] _ -> Keyword.put(acc, k, v) end end) end @doc """ Encode the keyword list options into an erlinit.config file format """ @spec encode_config(t()) :: String.t() def encode_config(config) do config |> Enum.map(&encode_line/1) |> IO.iodata_to_binary() end defp encode_line({k, v}) do Keyword.get(@switches, k) |> encode_kv(k, v) end defp encode_kv(:boolean, _k, false), do: [] defp encode_kv(:boolean, k, true), do: [encode_key(k), "\n"] defp encode_kv(type, k, v) do [encode_key(k), " ", encode_value(type, v), "\n"] end defp encode_value(:string, v) do if String.contains?(v, " ") do ["\"", v, "\""] else v end end defp encode_value(_, v), do: to_string(v) defp encode_key(key), do: "--" <> String.replace(to_string(key), "_", "-") end