Skip to content

Commit

Permalink
Restrict hex.registry add to a single package
Browse files Browse the repository at this point in the history
  • Loading branch information
aj-foster committed Nov 12, 2022
1 parent fe833de commit cd00ed8
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 206 deletions.
175 changes: 66 additions & 109 deletions lib/mix/tasks/hex.registry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,11 @@ defmodule Mix.Tasks.Hex.Registry do
$ mix hex.registry add PUBLIC_DIR PACKAGE
To add a package to an existing registry, supply the public directory of the registry and path to
the new packages. This action also requires the name of the registry and the private key
originally used to generate it:
the new package. This action also requires the name of the registry and the private key originally
used to generate it:
$ mix hex.registry add public --name=acme --private-key=private_key.pem foo-1.0.0.tar
* reading public/name
* reading public/versions
* moving foo-1.0.0.tar -> public/tarballs/foo-1.0.0.tar
* reading public/packages/foo
* copying foo-1.0.0.tar -> public/tarballs/foo-1.0.0.tar
* updating public/packages/foo
* updating public/names
* updating public/versions
Expand All @@ -83,8 +80,8 @@ defmodule Mix.Tasks.Hex.Registry do
{opts, args} = OptionParser.parse!(args, strict: @switches)

case args do
["add", public_dir, package | additional_packages] ->
add(public_dir, [package | additional_packages], opts)
["add", public_dir, package] ->
add(public_dir, package, opts)

["build", public_dir] ->
build(public_dir, opts)
Expand All @@ -109,90 +106,72 @@ defmodule Mix.Tasks.Hex.Registry do

## Add

defp add(public_dir, packages, opts) do
defp add(public_dir, package, opts) do
repo_name = opts[:name] || Mix.raise("missing --name")
private_key_path = opts[:private_key] || Mix.raise("missing --private-key")
private_key = private_key_path |> File.read!() |> decode_private_key()
add(repo_name, public_dir, private_key, packages)
add(repo_name, public_dir, private_key, package)
end

defp add(repo_name, public_dir, private_key, packages) do
defp add(repo_name, public_dir, private_key, package) do
public_key = ensure_public_key(private_key, public_dir)

existing_names =
read_names!(repo_name, public_dir, public_key)
|> Enum.map(fn %{name: name, updated_at: updated_at} -> {name, updated_at} end)
|> Enum.into(%{})

existing_versions =
read_versions!(repo_name, public_dir, public_key)
|> Enum.map(fn %{name: name, versions: versions} ->
{name, %{updated_at: existing_names[name], versions: versions}}
end)
|> Enum.into(%{})

tarball_dir = Path.join(public_dir, "tarballs")
create_directory(tarball_dir)

paths_per_name =
packages
|> Enum.map(fn path -> move_file!(path, tarball_dir) end)
|> Enum.group_by(fn path ->
[name | _rest] = String.split(Path.basename(path), ["-", ".tar"], trim: true)
name
end)
path = copy_file(package, tarball_dir)
[package_name | _rest] = String.split(Path.basename(path), ["-", ".tar"], trim: true)

versions =
Enum.map(paths_per_name, fn {name, paths} ->
existing_releases = read_package(repo_name, public_dir, public_key, name)
existing_package_releases = read_package(repo_name, public_dir, public_key, package_name)
new_package_release = build_release(repo_name, package)

releases =
paths
|> Enum.map(&build_release(repo_name, &1))
|> Enum.concat(existing_releases)
|> Enum.sort(&(Hex.Version.compare(&1.version, &2.version) == :lt))
|> Enum.uniq_by(& &1.version)
package_releases =
[new_package_release | existing_package_releases]
|> Enum.sort(&(Hex.Version.compare(&1.version, &2.version) == :lt))
|> Enum.uniq_by(& &1.version)

updated_at =
paths
|> Enum.map(&File.stat!(&1).mtime)
|> Enum.sort()
|> Enum.at(-1)
package =
:mix_hex_registry.build_package(
%{repository: repo_name, name: package_name, releases: package_releases},
private_key
)

previous_updated_at = get_in(existing_names, [name, :updated_at, :seconds])
updated_at = %{seconds: max_updated_at(previous_updated_at, updated_at), nanos: 0}
write_file("#{public_dir}/packages/#{package_name}", package)

package =
:mix_hex_registry.build_package(
%{repository: repo_name, name: name, releases: releases},
private_key
)
existing_names =
read_names(repo_name, public_dir, public_key)
|> Enum.map(fn %{name: name, updated_at: updated_at} -> {name, updated_at} end)
|> Enum.into(%{})

write_file("#{public_dir}/packages/#{name}", package)
versions = Enum.map(releases, & &1.version)
{name, %{updated_at: updated_at, versions: versions}}
previous_updated_at = get_in(existing_names, [package_name, :updated_at, :seconds])
new_version_updated_at = File.stat!(path).mtime
updated_at = %{seconds: max_updated_at(previous_updated_at, new_version_updated_at), nanos: 0}
package_versions = Enum.map(package_releases, & &1.version)

repo_versions =
read_versions(repo_name, public_dir, public_key)
|> Enum.map(fn %{name: name, versions: versions} ->
{name, %{updated_at: existing_names[name], versions: versions}}
end)
|> Enum.into(%{})
|> Map.put(package_name, %{updated_at: updated_at, versions: package_versions})

versions = Map.merge(existing_versions, versions)

names =
for {name, %{updated_at: updated_at}} <- versions do
repo_names =
Enum.map(repo_versions, fn {name, %{updated_at: updated_at}} ->
%{name: name, updated_at: updated_at}
end
end)

payload = %{repository: repo_name, packages: names}
names = :mix_hex_registry.build_names(payload, private_key)
write_file("#{public_dir}/names", names)
payload = %{repository: repo_name, packages: repo_names}
repo_names = :mix_hex_registry.build_names(payload, private_key)
write_file("#{public_dir}/names", repo_names)

versions =
for {name, %{versions: versions}} <- versions do
repo_versions =
Enum.map(repo_versions, fn {name, %{versions: versions}} ->
%{name: name, versions: versions}
end
end)

payload = %{repository: repo_name, packages: versions}
versions = :mix_hex_registry.build_versions(payload, private_key)
write_file("#{public_dir}/versions", versions)
payload = %{repository: repo_name, packages: repo_versions}
repo_versions = :mix_hex_registry.build_versions(payload, private_key)
write_file("#{public_dir}/versions", repo_versions)
end

## Build
Expand Down Expand Up @@ -332,46 +311,44 @@ defmodule Mix.Tasks.Hex.Registry do
encoded_public_key
end

defp read_names!(repo_name, public_dir, public_key) do
defp read_names(repo_name, public_dir, public_key) do
path = Path.join(public_dir, "names")
payload = read_file!(path)
payload = File.read!(path)
repo_name_or_no_verify = repo_name || :no_verify

case :mix_hex_registry.unpack_names(payload, repo_name_or_no_verify, public_key) do
{:ok, names} ->
names

_ ->
Mix.raise("""
Invalid package name manifest at #{path}
Is the repository name #{repo_name} correct?
""")
{:error, reason} ->
Mix.raise(
"Invalid package name manifest at #{path}: #{inspect(reason)}." <>
"\n\nIs the repository name #{repo_name} correct?"
)
end
end

defp read_versions!(repo_name, public_dir, public_key) do
defp read_versions(repo_name, public_dir, public_key) do
path = Path.join(public_dir, "versions")
payload = read_file!(path)
payload = File.read!(path)
repo_name_or_no_verify = repo_name || :no_verify

case :mix_hex_registry.unpack_versions(payload, repo_name_or_no_verify, public_key) do
{:ok, versions} ->
versions

_ ->
Mix.raise("""
Invalid package version manifest at #{path}
Is the repository name #{repo_name} correct?
""")
{:error, reason} ->
Mix.raise(
"Invalid package version manifest at #{path}: #{inspect(reason)}." <>
"\n\nIs the repository name #{repo_name} correct?"
)
end
end

defp read_package(repo_name, public_dir, public_key, package_name) do
path = Path.join([public_dir, "packages", package_name])

case read_file(path) do
case File.read(path) do
{:ok, payload} ->
case :mix_hex_registry.unpack_package(payload, repo_name, package_name, public_key) do
{:ok, package} -> package
Expand All @@ -392,26 +369,6 @@ defmodule Mix.Tasks.Hex.Registry do
end
end

defp read_file!(path) do
if File.exists?(path) do
Hex.Shell.info(["* reading ", path])
else
Mix.raise("Error reading file #{path}")
end

File.read!(path)
end

defp read_file(path) do
if File.exists?(path) do
Hex.Shell.info(["* reading ", path])
else
Hex.Shell.info(["* skipping ", path])
end

File.read(path)
end

defp write_file(path, data) do
if File.exists?(path) do
Hex.Shell.info(["* updating ", path])
Expand All @@ -423,11 +380,11 @@ defmodule Mix.Tasks.Hex.Registry do
File.write!(path, data)
end

defp move_file!(path, destination_dir) do
defp copy_file(path, destination_dir) do
file = Path.basename(path)
destination_file = Path.join(destination_dir, file)
Hex.Shell.info(["* moving ", path, " -> ", destination_file])
File.rename!(path, destination_file)
Hex.Shell.info(["* copying ", path, " -> ", destination_file])
File.cp!(path, destination_file)
destination_file
end

Expand Down
99 changes: 2 additions & 97 deletions test/mix/tasks/hex.registry_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,9 @@ defmodule Mix.Tasks.Hex.RegistryTest do
~w(add public --name acme --private-key private_key.pem subdir/foo-0.10.0.tar)
)

assert_received {:mix_shell, :info, ["* reading public/names"]}
assert_received {:mix_shell, :info, ["* reading public/versions"]}

assert_received {:mix_shell, :info,
["* moving subdir/foo-0.10.0.tar -> public/tarballs/foo-0.10.0.tar"]}
["* copying subdir/foo-0.10.0.tar -> public/tarballs/foo-0.10.0.tar"]}

assert_received {:mix_shell, :info, ["* skipping public/packages/foo"]}
assert_received {:mix_shell, :info, ["* creating public/packages/foo"]}
assert_received {:mix_shell, :info, ["* updating public/names"]}
assert_received {:mix_shell, :info, ["* updating public/versions"]}
Expand Down Expand Up @@ -89,13 +85,9 @@ defmodule Mix.Tasks.Hex.RegistryTest do
~w(add public --name acme --private-key private_key.pem subdir/foo-0.9.0.tar)
)

assert_received {:mix_shell, :info, ["* reading public/names"]}
assert_received {:mix_shell, :info, ["* reading public/versions"]}

assert_received {:mix_shell, :info,
["* moving subdir/foo-0.9.0.tar -> public/tarballs/foo-0.9.0.tar"]}
["* copying subdir/foo-0.9.0.tar -> public/tarballs/foo-0.9.0.tar"]}

assert_received {:mix_shell, :info, ["* reading public/packages/foo"]}
assert_received {:mix_shell, :info, ["* updating public/packages/foo"]}
assert_received {:mix_shell, :info, ["* updating public/names"]}
assert_received {:mix_shell, :info, ["* updating public/versions"]}
Expand All @@ -121,93 +113,6 @@ defmodule Mix.Tasks.Hex.RegistryTest do
assert versions == [%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]}]
end)
end

test "adds multiple packages" do
in_tmp(fn ->
bypass = setup_bypass()

{:ok, %{tarball: tarball}} =
:mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, [])

File.mkdir_p!("public/tarballs")
File.write!("public/tarballs/foo-0.10.0.tar", tarball)
0 = Mix.shell().cmd("openssl genrsa -out private_key.pem")

Mix.Task.run(
"hex.registry",
~w(build public --name acme --private-key private_key.pem)
)

flush()

{:ok, %{tarball: bar_tarball}} =
:mix_hex_tarball.create(%{name: "bar", version: "0.1.0"}, [])

{:ok, %{tarball: foo_tarball}} =
:mix_hex_tarball.create(%{name: "foo", version: "0.9.0"}, [])

File.mkdir!("subdir")
File.write!("subdir/bar-0.1.0.tar", bar_tarball)
File.write!("subdir/foo-0.9.0.tar", foo_tarball)

Mix.Task.reenable("hex.registry")

Mix.Task.run(
"hex.registry",
~w(add public --name acme --private-key private_key.pem subdir/foo-0.9.0.tar subdir/bar-0.1.0.tar)
)

assert_received {:mix_shell, :info, ["* reading public/names"]}
assert_received {:mix_shell, :info, ["* reading public/versions"]}

assert_received {:mix_shell, :info,
["* moving subdir/foo-0.9.0.tar -> public/tarballs/foo-0.9.0.tar"]}

assert_received {:mix_shell, :info,
["* moving subdir/bar-0.1.0.tar -> public/tarballs/bar-0.1.0.tar"]}

assert_received {:mix_shell, :info, ["* reading public/packages/foo"]}
assert_received {:mix_shell, :info, ["* updating public/packages/foo"]}
assert_received {:mix_shell, :info, ["* skipping public/packages/bar"]}
assert_received {:mix_shell, :info, ["* creating public/packages/bar"]}
assert_received {:mix_shell, :info, ["* updating public/names"]}
assert_received {:mix_shell, :info, ["* updating public/versions"]}
refute_received _

config = %{
:mix_hex_core.default_config()
| repo_url: "http://localhost:#{bypass.port}",
repo_verify: false,
repo_verify_origin: false
}

assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config)

assert [
%{name: "bar", updated_at: %{seconds: bar_updated_at}},
%{name: "foo", updated_at: %{seconds: foo_updated_at}}
] = names

assert bar_updated_at ==
"public/tarballs/bar-0.1.0.tar"
|> File.stat!()
|> Map.fetch!(:mtime)
|> Mix.Tasks.Hex.Registry.to_unix()

assert foo_updated_at ==
"public/tarballs/foo-0.9.0.tar"
|> File.stat!()
|> Map.fetch!(:mtime)
|> Mix.Tasks.Hex.Registry.to_unix()

assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config)

assert versions == [
%{name: "bar", retired: [], versions: ["0.1.0"]},
%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]}
]
end)
end
end

describe "build" do
Expand Down

0 comments on commit cd00ed8

Please sign in to comment.