defmodule GitGud.Packages.Formats.Tar do
@moduledoc """
Lists a tar archive held in memory: each member's path and either its
bytes or where it links to.
`:erl_tar` can extract to memory, but it drops symlinks on the floor,
and an icon theme is mostly symlinks — `/usr/share/pixmaps/foo.png`
pointing into `hicolor/` is the common case, not the odd one. So
this reads the headers itself, which is 512 bytes of fixed-width
ASCII a member.
Members come back as sub-binaries of the archive, so listing a
200 MB `data.tar` copies nothing.
Understood: ustar, GNU long names and long links (`L`/`K`), and pax
`path`/`linkpath` records — between them, everything `dpkg-deb`,
`cargo-deb` and `tar` write. Paths are normalised to absolute
(`./usr/bin/x` and `usr/bin/x` both become `/usr/bin/x`).
"""
@type entry :: {:file, binary()} | {:symlink, String.t()} | {:hardlink, String.t()}
@doc "Every file and link in `tar`, keyed by absolute path."
@spec entries(binary()) :: {:ok, %{String.t() => entry()}} | :error
def entries(tar) when is_binary(tar), do: read(tar, %{}, %{})
defp read(<<header::binary-size(512), rest::binary>>, pending, acc) do
if header == <<0::size(512)-unit(8)>> do
{:ok, acc}
else
member(header, rest, pending, acc)
end
end
# Archives that end without the two zero blocks are still readable up
# to where they stop.
defp read(_rest, _pending, acc), do: {:ok, acc}
defp member(header, rest, pending, acc) do
<<name::binary-size(100), _mode::binary-size(8), _uid::binary-size(8), _gid::binary-size(8),
size::binary-size(12), _mtime::binary-size(12), _checksum::binary-size(8), type,
linkname::binary-size(100), magic::binary-size(6), _version::binary-size(2),
_uname::binary-size(32), _gname::binary-size(32), _devmajor::binary-size(8),
_devminor::binary-size(8), prefix::binary-size(155), _::binary>> = header
with {:ok, size} <- size(size),
true <- byte_size(rest) >= size || :error do
data = binary_part(rest, 0, size)
skip = min(size + rem(512 - rem(size, 512), 512), byte_size(rest))
rest = binary_part(rest, skip, byte_size(rest) - skip)
case type do
?L ->
read(rest, Map.put(pending, :path, cstring(data)), acc)
?K ->
read(rest, Map.put(pending, :linkpath, cstring(data)), acc)
?x ->
read(rest, Map.merge(pending, pax(data)), acc)
_ ->
path = pending[:path] || joined(magic, prefix, cstring(name))
link = pending[:linkpath] || cstring(linkname)
read(rest, %{}, add(acc, type, normalize(path), data, link))
end
end
end
# Directories, devices and fifos carry nothing an icon lookup wants.
defp add(acc, type, path, data, _link) when type in [?0, 0, ?7],
do: Map.put(acc, path, {:file, data})
defp add(acc, ?2, path, _data, link), do: Map.put(acc, path, {:symlink, link})
defp add(acc, ?1, path, _data, link), do: Map.put(acc, path, {:hardlink, normalize(link)})
defp add(acc, _type, _path, _data, _link), do: acc
# POSIX ustar splits a long path across `prefix` and `name`; GNU tar
# writes "ustar " and uses those bytes for something else.
defp joined("ustar" <> <<0>>, prefix, name) do
case cstring(prefix) do
"" -> name
prefix -> prefix <> "/" <> name
end
end
defp joined(_magic, _prefix, name), do: name
# Octal ASCII, NUL- or space-terminated — or, for members over 8 GB,
# big-endian binary flagged by the high bit of the first byte.
defp size(<<1::1, _::7, _::binary>> = raw) do
<<_::1, n::size(95)>> = raw
{:ok, n}
end
defp size(raw) do
case raw |> cstring() |> String.trim() |> Integer.parse(8) do
{n, ""} -> {:ok, n}
_ when raw == <<0::96>> -> {:ok, 0}
_ -> :error
end
end
# pax records: "<len> <key>=<value>\n", repeated.
defp pax(data) do
data
|> String.split("\n", trim: true)
|> Enum.reduce(%{}, fn record, acc ->
with [_len, kv] <- String.split(record, " ", parts: 2),
[key, value] <- String.split(kv, "=", parts: 2) do
case key do
"path" -> Map.put(acc, :path, value)
"linkpath" -> Map.put(acc, :linkpath, value)
_ -> acc
end
else
_ -> acc
end
end)
end
defp cstring(bytes) do
case :binary.match(bytes, <<0>>) do
{at, _} -> binary_part(bytes, 0, at)
:nomatch -> bytes
end
end
@doc false
def normalize(path) do
"/" <>
(path
|> String.split("/", trim: true)
|> Enum.reject(&(&1 == "."))
|> Enum.join("/"))
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
4.6 KiB · text
History
b868907