defmodule UBootEnv.Config do
@moduledoc """
Utilities for reading the U-Boot's `fw_env.config` file.
"""
alias UBootEnv.Location
defstruct [:locations]
@type t() :: %__MODULE__{locations: [Location.t()]}
@doc """
Create a UBootEnv.Config from a file (`/etc/fw_env.config` by default)
This file should be formatted as described in `from_string/1`.
"""
@spec from_file(Path.t()) :: {:ok, t()} | {:error, atom()}
def from_file(config_file) do
with {:ok, config} <- File.read(config_file) do
from_string(config)
end
end
@doc """
Raising version of `from_file/1`
"""
@spec from_file!(Path.t()) :: UBootEnv.Config.t()
def from_file!(config) do
case from_file(config) do
{:ok, result} -> result
{:error, reason} -> raise reason
end
end
@doc """
Create a UBootEnv.Config from the contents of an `fw_env.config` file
Only one or two U-Boot environment locations are supported. Each location
row has the following format:
```
<Device name> <Device offset> <Env. size> [Flash sector size] [Number of sectors]
```
"""
@spec from_string(String.t()) :: {:ok, t()} | {:error, atom()}
def from_string(config) do
config
|> parse_file()
|> Enum.flat_map(&parse_line/1)
|> locations_to_config()
end
@doc """
Raising version of `from_string/1`
"""
@spec from_string!(String.t()) :: UBootEnv.Config.t()
def from_string!(config) do
case from_string(config) do
{:ok, result} -> result
{:error, reason} -> raise reason
end
end
@doc """
Return the environment block size
"""
@spec size(t()) :: pos_integer()
def size(config) do
first(config).size
end
@doc """
Return the first location
"""
@spec first(t()) :: Location.t()
def first(config) do
hd(config.locations)
end
@doc """
Return the second location
This raises for nonredundant environments.
"""
@spec second(t()) :: Location.t()
def second(config) do
[_first, second] = config.locations
second
end
@doc """
Return whether this is a redundant environment
"""
@spec format(t()) :: :redundant | :nonredundant
def format(config) do
case length(config.locations) do
1 -> :nonredundant
2 -> :redundant
end
end
defp parse_file(config) do
for line <- String.split(config, "\n", trim: true),
line != "",
!String.starts_with?(line, "#"),
do: line
end
defp parse_line(line) do
case line |> String.split() |> Enum.map(&String.trim/1) do
[dev_name, dev_offset, env_size | _] ->
[
%UBootEnv.Location{
path: dev_name,
offset: parse_int(dev_offset),
size: parse_int(env_size)
}
]
_other ->
[]
end
end
defp locations_to_config(locations) do
case length(locations) do
count when count == 1 or count == 2 ->
{:ok, %__MODULE__{locations: locations}}
_other ->
{:error, :parse_error}
end
end
@doc """
Parse an integer
Examples:
```elixir
iex> UBootEnv.Config.parse_int("0x12")
18
iex> UBootEnv.Config.parse_int("1234")
1234
```
"""
@spec parse_int(String.t()) :: integer()
def parse_int(<<"0x", hex_int::binary>>), do: String.to_integer(hex_int, 16)
def parse_int(decimal_int), do: String.to_integer(decimal_int)
end