defmodule Nerves.Package.Utils.Squashfs do @moduledoc false use GenServer require Logger @file_types ["c", "b", "l", "d", "-"] @device_types ["c", "b"] @posix [r: 4, w: 2, x: 1, s: 1, t: 1] @sticky ["s", "t", "S", "T"] @spec start_link(String.t()) :: GenServer.on_start() def start_link(rootfs) do params = unsquashfs(rootfs) dir = Path.dirname(rootfs) |> Path.join("squashfs") case Nerves.Port.cmd("unsquashfs", [rootfs, "-d", dir]) do {_result, 0} -> GenServer.start_link(__MODULE__, [rootfs, dir, params]) {error, _} -> {:error, error} end end @spec stop(GenServer.server()) :: :ok def stop(pid) do GenServer.call(pid, :stop) GenServer.stop(pid) end @spec pseudofile(GenServer.server()) :: String.t() def pseudofile(pid) do GenServer.call(pid, {:pseudofile}) end @spec pseudofile_fragment(GenServer.server(), String.t()) :: String.t() def pseudofile_fragment(pid, fragment) do GenServer.call(pid, {:pseudofile_fragment, fragment}) end @spec fragment(GenServer.server(), String.t(), Path.t(), Keyword.t()) :: Path.t() def fragment(pid, fragment, path, opts \\ []) do GenServer.call(pid, {:fragment, fragment, path, opts}) end @spec files(GenServer.server()) :: [Path.t()] def files(pid) do GenServer.call(pid, {:files}) end # def merge(pid, file_systems, pseudofiles, path) do # GenServer.call(pid, {:mergefs, file_systems, pseudofiles, path}) # end defp unsquashfs(rootfs) do case Nerves.Port.cmd("unsquashfs", ["-n", "-ll", rootfs]) do {result, 0} -> String.split(result, "\n") |> parse {error, _} -> raise "Error parsing Rootfs: #{inspect(error)}" end end @impl GenServer def init([rootfs, stage, params]) do {:ok, %{ rootfs: rootfs, params: params, stage: stage }} end @impl GenServer def handle_call(:stop, _from, s) do _ = File.rm_rf!(s.stage) {:reply, :ok, s} end def handle_call({:files}, _from, s) do files = Enum.reduce(s.params, [], fn {"d", _, _, _, _}, acc -> acc {_, file, _, _, _}, acc -> [file | acc] end) {:reply, files, s} end def handle_call({:pseudofile}, _from, s) do {:reply, params_to_pseudofile(s.params), s} end def handle_call({:pseudofile_fragment, fragment}, _from, s) do fragment = Enum.filter(s.params, fn {_, file, _, _, _} -> file in fragment end) {:reply, params_to_pseudofile(fragment), s} end def handle_call({:fragment, fragment, path, opts}, _from, s) do pseudo_fragment = fragment |> Enum.map(&path_to_paths/1) |> List.flatten() |> Enum.uniq() pseudo_fragment = Enum.filter(s.params, fn {_, file, _, _, _} -> file in pseudo_fragment end) fragment = Enum.filter(s.params, fn {_, file, _, _, _} -> file in fragment end) pseudofile = params_to_pseudofile(pseudo_fragment) tmp_dir = Path.dirname(path) |> Path.join("tmp") File.mkdir_p!(tmp_dir) pseudofile_name = opts[:name] || "pseudofile" pseudofile_path = Path.dirname(path) |> Path.join(pseudofile_name) File.write!(pseudofile_path, pseudofile) Enum.each(fragment, fn {_, file, _, _, _} -> src = Path.join(s.stage, file) dest = Path.join(tmp_dir, file) Path.dirname(dest) |> File.mkdir_p!() File.cp!(src, dest) end) IO.puts(path) _ = Nerves.Port.cmd("mksquashfs", [ tmp_dir, path, "-pf", pseudofile_path, "-noappend", "-no-recovery", "-no-progress" ]) _ = File.rm_rf!(tmp_dir) # File.rm!(pseudofile_path) {:reply, {:ok, path}, s} end # def handle_call({:mergefs, file_systems, pseudofiles, path}, from, s) do # # stage_path = # s.stage # |> Path.dirname # # unionfs = Path.join(stage_path, "union") # Enum.each(fs, fn() -> # Nerves.Port.cmd("unsquashfs", ["-d", s.stage, "-f", fs]) # end) # # pseudofile = Enum.reduce(pseudofiles, "", fn(file, acc) -> # File.read!(file) <> acc <> "\n" # end) # pseudofile <> "\n" <> params_to_pseudofile(s.params) # # pseudofile_path = Path.join(stage_path, "pseudofile") # File.write!(pseudofile_path, pseudofile) # # Nerves.Port.cmd("mksquashfs", [s.stage, path, "-pf", pseudofile_path, "-noappend", "-no-recovery", "-no-progress"]) # # #File.rm!(pseudofile_path) # # {:reply, {:ok, path}, s} # end defp params_to_pseudofile(fragment) do Enum.map(fragment, fn {type, file, {major, minor}, {p0, p1, p2, p3}, {o, g}} when type in @device_types -> "#{file} #{type} #{p0}#{p1}#{p2}#{p3} #{o} #{g} #{major} #{minor}" {_type, file, _attr, {p0, p1, p2, p3}, {o, g}} -> file = if file == "", do: "/", else: file "#{file} m #{p0}#{p1}#{p2}#{p3} #{o} #{g}" end) |> Enum.reverse() |> Enum.join("\n") end defp parse(_, _ \\ []) defp parse([], collect), do: collect defp parse([line | tail], collect) do collect = case parse_line(line) do nil -> collect value -> [value | collect] end parse(tail, collect) end defp parse_line(""), do: nil defp parse_line(<>) when type in @file_types do permissions = parse_permissions(permissions) [own, tail] = String.split(tail, " ", parts: 2) own = parse_own(own) tail = String.trim(tail) {attr, tail} = if type in @device_types do [major, tail] = String.split(tail, ",", parts: 2) tail = String.trim(tail) [minor, tail] = String.split(tail, " ", parts: 2) {{major, minor}, tail} else [_, tail] = String.split(tail, " ", parts: 2) {nil, tail} end <<_modified::binary-size(16), tail::binary>> = tail <<"squashfs-root", file::binary>> = String.trim(tail) file = if type == "l" do [file, _] = String.split(file, "->") String.trim(file) else file end {type, file, attr, permissions, own} end defp parse_line(_), do: nil defp parse_permissions(<>) do sticky = 0 sticky = sticky + sticky_to_int(owner, 4) + sticky_to_int(group, 2) + sticky_to_int(other, 1) {sticky, posix_to_int(owner), posix_to_int(group), posix_to_int(other)} end defp parse_own(own) do [owner, group] = String.split(own, "/") {owner, group} end defp sticky_to_int(<<_::binary-size(1), _::binary-size(1), bit::binary-size(1)>>, weight) when bit in @sticky, do: weight defp sticky_to_int(_, _), do: 0 defp posix_to_int(<>) do Enum.reduce([r, w, x], 0, fn p, a -> Keyword.get(@posix, String.to_atom(p), 0) + a end) end defp path_to_paths(path) do path |> Path.split() |> Enum.reduce(["/"], fn p, acc -> [h | _t] = acc [Path.join(h, p) | acc] end) |> Enum.uniq() end end