3.5 KiB · text History ff5c190
defmodule GitGud.RpmFixtures do
  @moduledoc """
  A synthetic but structurally real `.rpm`.

  Real packages are tens of megabytes and their interesting part —
  the header — is a few kilobytes of it. `GitGud.Packages.Formats.Rpm`
  never reads past the header, so a fixture that carries a correct lead,
  a correct signature header and a correct main header over a payload
  of nonsense exercises everything the adapter does.

  The parser's own tests use a real openSUSE package instead, truncated
  to its headers. That is the right fixture for asserting *what RPM
  looks like*; this is the right one for asserting what the adapter
  does with it, and the two are deliberately separate.
  """

  # {tag number, type} for each field this builder knows how to write.
  # Types are rpm's: 4 INT32, 6 STRING, 8 STRING_ARRAY, 9 I18NSTRING.
  @tags %{
    name: {1000, 6},
    version: {1001, 6},
    release: {1002, 6},
    epoch: {1003, 4},
    summary: {1004, 9},
    description: {1005, 9},
    buildtime: {1006, 4},
    buildhost: {1007, 6},
    size: {1009, 4},
    license: {1014, 6},
    arch: {1022, 6},
    providename: {1047, 8},
    requireflags: {1048, 4},
    requirename: {1049, 8},
    requireversion: {1050, 8},
    dirindexes: {1116, 4},
    basenames: {1117, 8},
    dirnames: {1118, 8}
  }

  @doc """
  Build an `.rpm`. `overrides` replaces any of the default fields;
  pass `%{name: "widget"}` for a second package, `%{epoch: 2}` to
  exercise epochs.
  """
  def build_rpm(overrides \\ %{}) do
    fields = Map.merge(defaults(), overrides)

    # lead(96) + signature header — which lands on a multiple of 8 here
    # by construction, so the alignment the parser does is exercised by
    # the real fixture rather than this one — then the main header,
    # then a payload nothing reads.
    <<0xED, 0xAB, 0xEE, 0xDB>> <>
      :binary.copy(<<0>>, 92) <>
      header(%{}) <> header(fields) <> "not a real cpio payload"
  end

  defp defaults do
    %{
      name: "koco",
      version: "0.1.2",
      release: "1",
      arch: "x86_64",
      summary: "A Kodi Controller",
      description: "A Kodi Controller, in Rust.",
      license: "GPLv3",
      buildhost: "runner",
      buildtime: 1_757_000_000,
      size: 4096,
      providename: ["koco", "koco(x86-64)"],
      # Two requirements, one of them rpmlib — which the index must
      # drop, because createrepo does.
      requirename: ["libc.so.6()(64bit)", "rpmlib(FileDigests)"],
      requireflags: [0, 0x1000008],
      requireversion: ["", "4.6.0-1"],
      # One file that belongs in primary.xml and one that doesn't.
      dirnames: ["/usr/bin/", "/usr/share/doc/koco/"],
      dirindexes: [0, 1],
      basenames: ["koco", "README.md"]
    }
  end

  defp header(fields) do
    {index, store} =
      Enum.reduce(fields, {[], <<>>}, fn {key, value}, {index, store} ->
        {tag, type} = Map.fetch!(@tags, key)
        {bytes, count} = encode(type, value)
        {[<<tag::32, type::32, byte_size(store)::32, count::32>> | index], store <> bytes}
      end)

    index = IO.iodata_to_binary(Enum.reverse(index))

    <<0x8E, 0xAD, 0xE8, 0x01, 0::32, div(byte_size(index), 16)::32, byte_size(store)::32>> <>
      index <> store
  end

  defp encode(type, value) when type in [6, 9], do: {value <> <<0>>, 1}
  defp encode(8, values), do: {Enum.map_join(values, &(&1 <> <<0>>)), length(values)}

  defp encode(4, values) when is_list(values),
    do: {for(v <- values, into: <<>>, do: <<v::32>>), length(values)}

  defp encode(4, value), do: {<<value::32>>, 1}
end