2.4 KiB · text History b868907
defmodule GitGud.Packages.Formats.Rpm.Cpio do
  @moduledoc """
  Lists an RPM payload: a cpio archive in the "new ASCII" format, the
  one `rpmbuild` has written since forever.

  Each member is a 110-byte header of hex fields, the NUL-terminated
  name, then the data, each padded to four bytes. A symlink's data is
  its target. The archive ends at a member named `TRAILER!!!`.

  Members come back as sub-binaries of the payload, and paths are
  normalised the way `GitGud.Packages.Formats.Tar` normalises them, so
  the two can be searched by the same code.

  Not understood: rpm's "stripped" `07070X` format, which it uses only
  for packages holding a file over 4 GB and which names members by
  index into the header rather than by path. Such a payload lists as
  empty.
  """

  alias GitGud.Packages.Formats.Tar

  @trailer "TRAILER!!!"

  @doc "Every file and symlink in `cpio`, keyed by absolute path."
  @spec entries(binary()) :: {:ok, %{String.t() => Tar.entry()}} | :error
  def entries(cpio) when is_binary(cpio), do: read(cpio, 0, %{})

  defp read(cpio, at, acc) do
    case cpio do
      <<_::binary-size(^at), "07070", c, fields::binary-size(104), _::binary>>
      when c in [?1, ?2] ->
        with {:ok, [_ino, mode, _uid, _gid, _nlink, _mtime, size, _, _, _, _, namesize, _check]} <-
               hex_fields(fields),
             name_at = at + 110,
             data_at = align4(name_at + namesize),
             true <- byte_size(cpio) >= data_at + size || :error do
          name = binary_part(cpio, name_at, max(namesize - 1, 0))
          data = binary_part(cpio, data_at, size)
          next = align4(data_at + size)

          if name == @trailer do
            {:ok, acc}
          else
            read(cpio, next, add(acc, mode, Tar.normalize(name), data))
          end
        end

      _ ->
        {:ok, acc}
    end
  end

  # S_IFMT: 0o100000 a regular file, 0o120000 a symlink.
  defp add(acc, mode, path, data) do
    case Bitwise.band(mode, 0o170000) do
      0o100000 -> Map.put(acc, path, {:file, data})
      0o120000 -> Map.put(acc, path, {:symlink, data})
      _ -> acc
    end
  end

  # Thirteen eight-digit hex numbers.
  defp hex_fields(fields) do
    parsed =
      for <<field::binary-size(8) <- fields>> do
        case Integer.parse(field, 16) do
          {n, ""} -> n
          _ -> nil
        end
      end

    if Enum.any?(parsed, &is_nil/1), do: :error, else: {:ok, parsed}
  end

  defp align4(n), do: n + rem(4 - rem(n, 4), 4)
end