5.2 KiB · text History ff5c190
defmodule GitGud.Packages.Formats.Rpm.HeaderTest do
  @moduledoc """
  The RPM header parser, against a real package.

  The fixture is the first 9141 bytes of
  `aspell-sl-0.50-4.6.x86_64.rpm` from openSUSE Tumbleweed — the lead,
  the signature header and the main header, with the 5.9 MB compressed
  payload cut off. Truncating it is not a shortcut around a hard case:
  the parser never reads past the main header, and a fixture that
  stops exactly where the parser should proves that.

  Every expected value below was taken from `rpm -qp --qf` on the
  untruncated file, not from this parser, so the assertions are a
  comparison against rpm rather than a recording of our own output.
  """
  use ExUnit.Case, async: true

  alias GitGud.Packages.Formats.Rpm.Header

  @fixture File.read!("test/support/fixtures/rpm/aspell-sl-headers.rpm")

  setup do
    {:ok, header} = Header.parse(@fixture)
    %{header: header}
  end

  describe "scalar tags" do
    test "the identity rpm prints", %{header: header} do
      assert Header.get(header, :name) == "aspell-sl"
      assert Header.get(header, :version) == "0.50"
      assert Header.get(header, :release) == "4.6"
      assert Header.get(header, :arch) == "x86_64"
    end

    test "no epoch is nil, not zero", %{header: header} do
      # rpm prints "(none)" here. Storing a 0 would be a claim the
      # package doesn't make, and epoch 0 and no epoch compare the same
      # way only because the *renderer* defaults it.
      assert Header.get(header, :epoch) == nil
    end

    test "integers are integers", %{header: header} do
      assert Header.get(header, :buildtime) == 1_709_119_039
      assert Header.get(header, :size) == 22_694_857
    end

    test "the free text", %{header: header} do
      assert Header.get(header, :summary) == "Slovenian (slovenski) Dictionary for Aspell"
      assert Header.get(header, :license) == "GPL-2.0-or-later"
      assert Header.get(header, :vendor) == "openSUSE"
      assert Header.get(header, :url) == "http://aspell.net/"
      assert Header.get(header, :sourcerpm) == "aspell-dictionaries-0.50.6-4.6.src.rpm"
    end

    test "the payload size comes out of the signature header", %{header: header} do
      # The one tag that isn't in the main header at all. It is tag
      # 1007 there, which the main header calls BUILDHOST — reading
      # both with one table gives you a hostname where a size belongs,
      # and this is the assertion that catches that.
      assert Header.get(header, :archivesize) == 22_696_668
      assert Header.get(header, :buildhost) == "reproducible"
    end
  end

  describe "array tags" do
    test "provides line up name, flags and version by index", %{header: header} do
      assert Enum.take(Header.list(header, :providename), 3) ==
               ["aspell-sl", "aspell-sl(x86-64)", "locale(aspell:sl)"]

      assert Enum.take(Header.list(header, :provideversion), 3) == ["0.50-4.6", "0.50-4.6", ""]
      assert Enum.take(Header.list(header, :provideflags), 3) == [8, 8, 0]
    end

    test "this package requires nothing but rpmlib", %{header: header} do
      assert Header.list(header, :requirename) == [
               "rpmlib(CompressedFileNames)",
               "rpmlib(FileDigests)",
               "rpmlib(PayloadFilesHavePrefix)",
               "rpmlib(PayloadIsZstd)"
             ]
    end

    test "list/2 wraps a scalar so callers never branch", %{header: header} do
      assert Header.list(header, :name) == ["aspell-sl"]
      assert Header.list(header, :nonexistent) == []
    end
  end

  describe "files" do
    test "are reassembled from dirnames, dirindexes and basenames", %{header: header} do
      files = Header.files(header)

      assert length(files) == 11
      assert "/usr/lib64/aspell-0.60/sl.dat" in files
      assert "/usr/lib64/aspell-0.60/sl.rws" in files
    end
  end

  describe "the header range" do
    test "brackets the main header in the file", %{header: header} do
      # These go into primary.xml as <rpm:header-range/>, and dnf uses
      # them to range-request a package's metadata instead of the
      # package. Wrong values mean a client fetches garbage and can't
      # say why.
      assert header.header_start == 4504
      assert header.header_end == 9141

      # …which is the whole truncated fixture, by construction: the
      # payload starts exactly where the header ends.
      assert header.header_end == byte_size(@fixture)
    end

    test "the start is 8-byte aligned", %{header: header} do
      assert rem(header.header_start, 8) == 0
    end
  end

  describe "the filename" do
    test "is the one rpmbuild writes", %{header: header} do
      assert Header.filename(header) == "aspell-sl-0.50-4.6.x86_64.rpm"
    end
  end

  describe "refusing input" do
    test "something that isn't an RPM at all" do
      assert {:error, {422, message}} = Header.parse("PK\x03\x04 this is a zip")
      assert message =~ "lead magic"
    end

    test "a truncated header rather than a truncated payload" do
      # Cutting inside the main header has to be an error; cutting
      # after it is the fixture above and must not be.
      assert {:error, {422, _}} = Header.parse(binary_part(@fixture, 0, 6000))
    end

    test "an empty body" do
      assert {:error, {422, _}} = Header.parse("")
    end
  end
end