defmodule GitGud.Packages.DesktopIconTest do
@moduledoc """
A .deb or .rpm that installs an application gets the name and icon
its desktop entry gives it, the way an APK gets its launcher icon.
The packages are shaped like koco's: `assets/koco.desktop` with
`Icon=koco`, and `logo.png` installed as
`/usr/share/icons/hicolor/256x256/apps/koco.png`.
"""
use GitGud.DataCase, async: false
import GitGud.ForgeFixtures
import GitGud.RpmFixtures
alias GitGud.Packages
alias GitGud.Packages.About
alias GitGud.Packages.Apt.Native
alias GitGud.Packages.Formats.Deb
alias GitGud.Packages.Formats.Desktop
alias GitGud.Packages.Formats.Rpm
setup do
{_user, repo} = repository_fixture()
%{repo: repo}
end
@desktop """
[Desktop Entry]
Type=Application
Exec=koco
Icon=koco
Name=KoCo
Name[de]=KoKo
[Desktop Action quit]
Name=Quit
Icon=application-exit
"""
# Enough PNG for anything that reads one: the signature and an IHDR
# carrying the width, then a marker so tests can tell icons apart.
defp png(width, tag),
do: <<0x89, "PNG\r\n", 0x1A, "\n", 13::32, "IHDR", width::32, width::32>> <> tag
defp koco_files do
[
{"./usr/bin/koco", "\x7FELF"},
{"./usr/share/applications/koco.desktop", @desktop},
{"./usr/share/icons/hicolor/32x32/apps/koco.png", png(32, "small")},
{"./usr/share/icons/hicolor/256x256/apps/koco.png", png(256, "large")}
]
end
# ── .deb ────────────────────────────────────────────────────────────
defp deb(files, compression \\ :gzip) do
control =
"Package: neiam-koco\nVersion: 0.1.2\nArchitecture: amd64\nDescription: A Kodi Controller\n"
data = tar(files)
data_member =
case compression do
:gzip -> {"data.tar.gz", :zlib.gzip(data)}
:xz -> {"data.tar.xz", elem(Native.xz_compress(data), 1)}
end
ar([
{"debian-binary", "2.0\n"},
{"control.tar.gz", :zlib.gzip(tar([{"./control", control}]))},
data_member
])
end
# ustar, written by hand: `:erl_tar.create/3` can't make a symlink
# out of an in-memory member, and symlinks are half of an icon theme.
defp tar(members) do
Enum.map_join(members, fn
{path, {:symlink, target}} -> tar_header(path, ?2, 0, target)
{path, data} -> tar_header(path, ?0, byte_size(data), "") <> pad512(data)
end) <> :binary.copy(<<0>>, 1024)
end
defp tar_header(path, type, size, link) do
octal = fn n, width ->
String.pad_leading(Integer.to_string(n, 8), width - 1, "0") <> <<0>>
end
header =
field(path, 100) <>
octal.(0o644, 8) <>
octal.(0, 8) <>
octal.(0, 8) <>
octal.(size, 12) <>
octal.(0, 12) <>
" " <>
<<type>> <>
field(link, 100) <> "ustar" <> <<0>> <> "00" <> :binary.copy(<<0>>, 32 + 32 + 8 + 8 + 155)
# The checksum is the byte sum with its own field read as spaces,
# which is how the header was built; `:erl_tar` checks it.
sum = for <<byte <- header>>, reduce: 0, do: (acc -> acc + byte)
<<before::binary-size(148), _::binary-size(8), rest::binary>> = header
pad512(before <> octal.(sum, 7) <> " " <> rest)
end
defp field(text, width), do: text <> :binary.copy(<<0>>, width - byte_size(text))
defp pad512(bytes), do: bytes <> :binary.copy(<<0>>, rem(512 - rem(byte_size(bytes), 512), 512))
defp ar(members) do
body =
Enum.map_join(members, fn {name, data} ->
header =
String.pad_trailing(name, 16) <>
String.pad_trailing("0", 12) <>
String.pad_trailing("0", 6) <>
String.pad_trailing("0", 6) <>
String.pad_trailing("100644", 8) <>
String.pad_trailing(Integer.to_string(byte_size(data)), 10) <> <<0x60, 0x0A>>
header <> data <> if(rem(byte_size(data), 2) == 1, do: "\n", else: "")
end)
"!<arch>\n" <> body
end
defp publish_deb(repo, body) do
ctx = %{
method: "POST",
path: ["upload"],
conn: Plug.Test.conn(:post, "/upload"),
principal: :anonymous
}
{:ok, _type, _reply} = Deb.store(repo, ctx, body)
[package] = Packages.list_packages(repo, "deb")
package
end
describe "a .deb" do
test "takes its name and its largest icon from the desktop entry", %{repo: repo} do
package = publish_deb(repo, deb(koco_files()))
about = About.for_package(package)
assert about.title == "KoCo"
assert about.icon?
assert {:ok, "image/png", bytes, _sha} = Packages.icon(package)
assert String.ends_with?(bytes, "large")
end
test "follows a symlinked pixmap", %{repo: repo} do
files = [
{"./usr/share/applications/koco.desktop", @desktop},
{"./opt/koco/logo.png", png(128, "logo")},
{"./usr/share/pixmaps/koco.png", {:symlink, "../../../opt/koco/logo.png"}}
]
package = publish_deb(repo, deb(files, :xz))
assert {:ok, "image/png", bytes, _sha} = Packages.icon(package)
assert String.ends_with?(bytes, "logo")
end
test "a package with no desktop entry is left as it was", %{repo: repo} do
package = publish_deb(repo, deb([{"./usr/lib/libthing.so", "lib"}]))
about = About.for_package(package)
assert about.title == "neiam-koco"
refute about.icon?
end
end
# ── .rpm ────────────────────────────────────────────────────────────
defp rpm(compressor, payload) do
files = for {"." <> path, _} <- koco_files(), do: path
build_rpm(%{
name: "koco",
dirnames: files |> Enum.map(&(Path.dirname(&1) <> "/")) |> Enum.uniq(),
dirindexes:
Enum.map(files, fn path ->
dirs = files |> Enum.map(&(Path.dirname(&1) <> "/")) |> Enum.uniq()
Enum.find_index(dirs, &(&1 == Path.dirname(path) <> "/"))
end),
basenames: Enum.map(files, &Path.basename/1),
payloadcompressor: compressor,
payload: payload
})
end
describe "an .rpm" do
test "reads its payload for the desktop entry and icon", %{repo: repo} do
body = rpm("gzip", :zlib.gzip(cpio(koco_files())))
assert {:ok, _, _} = Rpm.store(repo, %{method: "POST", path: ["upload"]}, body)
[package] = Packages.list_packages(repo, "rpm")
assert About.for_package(package).title == "KoCo"
assert {:ok, "image/png", bytes, _sha} = Packages.icon(package)
assert String.ends_with?(bytes, "large")
end
test "an unreadable payload costs the icon, not the publish", %{repo: repo} do
body = rpm("xz", "not xz at all")
assert {:ok, _, _} = Rpm.store(repo, %{method: "POST", path: ["upload"]}, body)
[package] = Packages.list_packages(repo, "rpm")
refute About.for_package(package).icon?
end
end
# ── the parts ───────────────────────────────────────────────────────
test "an entry named for the package wins over another app's" do
entries = %{
"/usr/share/applications/zzz-helper.desktop" =>
{:file, "[Desktop Entry]\nType=Application\nName=Helper\nIcon=helper\n"},
"/usr/share/applications/org.neiam.Koco.desktop" =>
{:file, "[Desktop Entry]\nType=Application\nName=KoCo\nIcon=koco\n"},
"/usr/share/icons/hicolor/48x48/apps/helper.png" => {:file, png(48, "helper")},
"/usr/share/icons/hicolor/48x48/apps/koco.png" => {:file, png(48, "koco")}
}
assert {:ok, %{label: "KoCo", icon: icon}} = Desktop.find(entries, "neiam-koco")
assert String.ends_with?(icon, "koco")
end
test "an icon too large to be sensible loses to a sensible one" do
entries = %{
"/usr/share/applications/koco.desktop" => {:file, "[Desktop Entry]\nIcon=koco\n"},
"/usr/share/icons/hicolor/1024x1024/apps/koco.png" => {:file, png(1024, "huge")},
"/usr/share/icons/hicolor/128x128/apps/koco.png" => {:file, png(128, "right")}
}
assert {:ok, %{icon: icon}} = Desktop.find(entries, "koco")
assert String.ends_with?(icon, "right")
end
test "decompression stops at its ceiling rather than filling memory" do
{:ok, xz} = Native.xz_compress(:binary.copy("a", 4096))
assert {:error, _} = Native.xz_decompress_capped(xz, 1024)
assert {:ok, bytes} = Native.xz_decompress_capped(xz, 4096)
assert byte_size(bytes) == 4096
end
# ── backfill ────────────────────────────────────────────────────────
# As it would have been stored before the adapters read desktop
# entries: the artifact, and none of the metadata.
defp publish_bare(repo, format, name, version, body) do
{:ok, package, _v, _f} =
Packages.publish_file(repo, format, name, version, %{
filename: "#{name}-#{version}.#{format}",
body: body,
metadata: %{}
})
package
end
describe "backfill_icons/1" do
test "fills in a .deb and an .rpm published before icons", %{repo: repo} do
deb = publish_bare(repo, "deb", "neiam-koco", "0.1.2", deb(koco_files()))
rpm =
publish_bare(repo, "rpm", "koco", "0.1.2-1", rpm("gzip", :zlib.gzip(cpio(koco_files()))))
refute About.for_package(deb).icon?
assert {2, 2} = Desktop.backfill_icons()
for package <- [deb, rpm] do
assert %{title: "KoCo", icon?: true} = About.for_package(package)
assert {:ok, "image/png", bytes, _} = Packages.icon(package)
assert String.ends_with?(bytes, "large")
end
end
test "a dry run writes nothing, and a package with no entry isn't counted", %{repo: repo} do
deb = publish_bare(repo, "deb", "neiam-koco", "0.1.2", deb(koco_files()))
_lib = publish_bare(repo, "deb", "libthing", "1.0", deb([{"./usr/lib/libthing.so", "lib"}]))
parent = self()
assert {2, 0} = Desktop.backfill_icons(dry_run: true, on_file: &send(parent, {:report, &1}))
refute About.for_package(deb).icon?
assert_received {:report, %{package: "neiam-koco", label: "KoCo", icon: {_, "png"}}}
assert_received {:report, %{package: "libthing", icon: :none, written?: false}}
assert {2, 1} = Desktop.backfill_icons()
end
test "only the newest file of a package, unless asked for all", %{repo: repo} do
_old = publish_bare(repo, "deb", "neiam-koco", "0.1.1", deb(koco_files()))
package = publish_bare(repo, "deb", "neiam-koco", "0.1.2", deb(koco_files()))
assert {1, 1} = Desktop.backfill_icons()
assert About.for_package(package).icon?
assert {2, 2} = Desktop.backfill_icons(all: true)
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
10.7 KiB · text
History
69f26e0