From f77cd98ff4fffbe3b802f014ec4dcd324d47896f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Tue, 19 May 2026 00:08:24 +0200 Subject: [PATCH 1/3] Add release-age cooldown to dependency resolution A configurable delay between when a package version is published and when it becomes eligible for selection during dependency resolution, to mitigate supply-chain attacks where a freshly compromised release is pulled into projects before it can be detected and retired. Configuration: * New `cooldown` config key (env `HEX_COOLDOWN`, project `:hex` block, global `hex.config`); accepts "d", "w", "mo". * New `cooldown_exclude_repos` config key (env `HEX_COOLDOWN_EXCLUDE_REPOS` comma-separated, project / global as list); skips cooldown for the named repos so an organization can consume hotfixes to its own repository without delay. Implementation: * `Hex.Cooldown` parses durations, computes cutoffs, and renders pre-flight errors and solver failure notes. * `Hex.Registry.Server` stores per-release `published_at` (Unix seconds extracted from the registry Timestamp) under a new ETS slot; `@ets_version` bumps to 5 so stale caches missing the slot are invalidated on upgrade. `Hex.Dev.copy_package` and the test fixture helper grow alongside. * `Hex.Registry.Cooldown` wraps the registry behaviour, filtering versions on `versions/2` against the active cutoff and respecting the per-repo exclude list and a per-resolution bypass set. * `Hex.RemoteConverger` builds the cutoff and bypass set at the start of resolution. The bypass set has two sources: - packages whose lock survived prepare_locked (mix deps.get against an intact lockfile bypasses cooldown entirely); - packages in old_lock whose locked version is known-unsafe (retired or carrying a security advisory) so re-resolving to escape a known-unsafe lock is never blocked. A pre-flight check walks both top-level requirements and nested hex deps under non-Hex (path / git) parents, raising a formatted cooldown error when every matching version of a direct dep is in cooldown. Solver failures get a generic cooldown hint appended when cooldown is non-zero. Releases without `published_at` (legacy registry data, repos that have not rebuilt their index, or self-hosted clones) are treated as eligible. Empty env values (`HEX_COOLDOWN=`, `HEX_COOLDOWN_EXCLUDE_REPOS=`) fall through to the next source instead of clobbering project / global config. Re-vendor hex_core to pick up the Timestamp `published_at` field on `Release`. --- lib/hex/cooldown.ex | 214 ++++++++++++ lib/hex/dev.ex | 3 +- lib/hex/registry/cooldown.ex | 46 +++ lib/hex/registry/server.ex | 26 +- lib/hex/remote_converger.ex | 115 ++++++- lib/hex/state.ex | 19 +- lib/mix/tasks/hex.config.ex | 12 + src/mix_hex_api.erl | 2 +- src/mix_hex_api_auth.erl | 2 +- src/mix_hex_api_key.erl | 2 +- src/mix_hex_api_oauth.erl | 2 +- src/mix_hex_api_organization.erl | 2 +- src/mix_hex_api_organization_member.erl | 2 +- src/mix_hex_api_package.erl | 2 +- src/mix_hex_api_package_owner.erl | 2 +- src/mix_hex_api_release.erl | 2 +- src/mix_hex_api_short_url.erl | 2 +- src/mix_hex_api_user.erl | 2 +- src/mix_hex_core.erl | 2 +- src/mix_hex_core.hrl | 4 +- src/mix_hex_erl_tar.erl | 2 +- src/mix_hex_erl_tar.hrl | 2 +- src/mix_hex_http.erl | 2 +- src/mix_hex_http_httpc.erl | 2 +- src/mix_hex_licenses.erl | 2 +- src/mix_hex_pb_names.erl | 2 +- src/mix_hex_pb_package.erl | 348 ++++++++++++++------ src/mix_hex_pb_signed.erl | 2 +- src/mix_hex_pb_versions.erl | 2 +- src/mix_hex_registry.erl | 2 +- src/mix_hex_repo.erl | 2 +- src/mix_hex_safe_binary_to_term.erl | 2 +- src/mix_hex_tarball.erl | 2 +- src/mix_safe_erl_term.xrl | 2 +- test/fixtures/registries/20200917.ets | Bin 223411 -> 223306 bytes test/fixtures/registries/20210915.ets | Bin 224485 -> 224380 bytes test/fixtures/registries/20210926.ets | Bin 40558 -> 40453 bytes test/hex/cooldown_test.exs | 259 +++++++++++++++ test/hex/mix_task_test.exs | 109 ++++++ test/hex/registry/cooldown_test.exs | 125 +++++++ test/hex/remote_converger_cooldown_test.exs | 161 +++++++++ test/mix/tasks/hex.config_test.exs | 19 ++ test/support/case.ex | 22 +- 43 files changed, 1389 insertions(+), 143 deletions(-) create mode 100644 lib/hex/cooldown.ex create mode 100644 lib/hex/registry/cooldown.ex create mode 100644 test/hex/cooldown_test.exs create mode 100644 test/hex/registry/cooldown_test.exs create mode 100644 test/hex/remote_converger_cooldown_test.exs diff --git a/lib/hex/cooldown.ex b/lib/hex/cooldown.ex new file mode 100644 index 00000000..260e3f27 --- /dev/null +++ b/lib/hex/cooldown.ex @@ -0,0 +1,214 @@ +defmodule Hex.Cooldown do + @moduledoc false + + @type duration :: String.t() + @type cutoff :: {:cutoff, integer(), non_neg_integer()} | :disabled + + @doc """ + Parses a duration string into a normalized form for `Hex.State`. + + Returns `{:ok, duration}` for valid input or `:error` for invalid input. + + Accepted forms: `"0"`, `"d"`, `"w"`, `"mo"`. + """ + @spec parse_config(String.t() | nil) :: {:ok, duration()} | :error + def parse_config(nil), do: {:ok, "0d"} + def parse_config(""), do: {:ok, "0d"} + + def parse_config(string) when is_binary(string) do + case duration_to_seconds(string) do + {:ok, _} -> {:ok, string} + :error -> :error + end + end + + def parse_config(_), do: :error + + @doc """ + Parses the `cooldown_exclude_repos` config value into a list of repo + name strings. + + Accepts either a list of strings (project / global config) or a + comma-separated string (env var). Empty entries are dropped, whitespace + is trimmed. Repo names should match `Hex.Repo` keys, e.g. `"hexpm"`, + `"hexpm:myorg"`, or a custom repo name. + """ + @spec parse_exclude_repos([String.t()] | String.t() | nil) :: {:ok, [String.t()]} | :error + def parse_exclude_repos(nil), do: {:ok, []} + def parse_exclude_repos([]), do: {:ok, []} + + def parse_exclude_repos(list) when is_list(list) do + if Enum.all?(list, &is_binary/1) do + {:ok, list |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == ""))} + else + :error + end + end + + def parse_exclude_repos(string) when is_binary(string) do + {:ok, + string + |> String.split(",") + |> Enum.map(&String.trim/1) + |> Enum.reject(&(&1 == ""))} + end + + def parse_exclude_repos(_), do: :error + + @doc """ + Returns true if cooldown should be skipped for the given repo. + """ + @spec repo_excluded?(String.t() | nil) :: boolean() + def repo_excluded?(repo) do + name = repo || "hexpm" + name in Hex.State.fetch!(:cooldown_exclude_repos) + end + + @doc """ + Converts a duration string into a number of seconds. + """ + @spec duration_to_seconds(String.t()) :: {:ok, non_neg_integer()} | :error + def duration_to_seconds("0"), do: {:ok, 0} + + def duration_to_seconds(string) when is_binary(string) do + with [_, digits, unit] <- Regex.run(~r/\A(\d+)(d|w|mo)\z/, string), + {n, ""} <- Integer.parse(digits) do + {:ok, n * unit_seconds(unit)} + else + _ -> :error + end + end + + defp unit_seconds("d"), do: 86_400 + defp unit_seconds("w"), do: 86_400 * 7 + defp unit_seconds("mo"), do: 86_400 * 30 + + @doc """ + Builds a resolution cutoff from the local cooldown configuration. + + Returns `:disabled` when the effective duration is zero. + """ + @spec build_cutoff() :: cutoff() + def build_cutoff() do + case duration_to_seconds(Hex.State.fetch!(:cooldown)) do + {:ok, 0} -> + :disabled + + {:ok, seconds} -> + now = System.system_time(:second) + {:cutoff, now - seconds, seconds} + + :error -> + :disabled + end + end + + @doc """ + Returns true if the release is eligible under the cutoff. + + A release with no `published_at` (legacy registry data) is treated as + eligible; cooldown only applies to releases whose publish time is known. + """ + @spec eligible?(integer() | nil, cutoff()) :: boolean() + def eligible?(_published_at, :disabled), do: true + def eligible?(nil, _cutoff), do: true + + def eligible?(published_at, {:cutoff, cutoff_seconds, _}) when is_integer(published_at) do + published_at <= cutoff_seconds + end + + @doc """ + Returns the date a release would become eligible under the cutoff. + """ + @spec eligible_on(integer(), cutoff()) :: Date.t() + def eligible_on(published_at, {:cutoff, _cutoff_seconds, window_seconds}) + when is_integer(published_at) do + published_at + |> Kernel.+(window_seconds) + |> DateTime.from_unix!() + |> DateTime.to_date() + end + + @doc """ + Returns the effective duration string for the configured cutoff. + """ + @spec describe_duration(cutoff()) :: String.t() + def describe_duration(:disabled), do: "0" + + def describe_duration({:cutoff, _, window_seconds}) do + cond do + rem(window_seconds, 86_400 * 7) == 0 -> "#{div(window_seconds, 86_400 * 7)} weeks" + true -> "#{div(window_seconds, 86_400)} days" + end + end + + @doc """ + Describes the configuration source that contributed the local cooldown. + Used in error messages. + """ + @spec describe_source() :: String.t() + def describe_source() do + case Hex.State.fetch_source!(:cooldown) do + {:env, var} -> "#{var}" + {:project_config, key} -> "mix.exs (#{key})" + {:global_config, key} -> "~/.hex/hex.config (#{key})" + :default -> "default" + :computed -> "runtime" + end + end + + @doc """ + Builds the pre-flight error message for a direct dependency whose entire + matching version set has been filtered by cooldown. + """ + @spec preflight_error(String.t(), String.t(), [{String.t(), integer()}], cutoff()) :: String.t() + def preflight_error(package, requirement, filtered, cutoff) do + today = Date.utc_today() + + lines = + Enum.map(filtered, fn {version, published_at} -> + published_date = published_at |> DateTime.from_unix!() |> DateTime.to_date() + days_ago = Date.diff(today, published_date) + eligible_date = eligible_on(published_at, cutoff) + + " #{version} published #{published_date} (#{days_ago} days ago), eligible #{eligible_date}" + end) + + earliest_eligible = + filtered + |> Enum.map(fn {_version, published_at} -> eligible_on(published_at, cutoff) end) + |> Enum.min(Date) + + duration = describe_duration(cutoff) + source = describe_source() + + """ + All versions of "#{package}" matching "#{requirement}" are in cooldown: + + #{Enum.join(lines, "\n")} + + Effective cooldown is #{duration} (#{source}). + + To proceed: + * Wait until #{earliest_eligible} and re-run + * Bypass for this run: HEX_COOLDOWN=0 mix deps.get + """ + end + + @doc """ + Note appended to solver failures when cooldown is active and a transitive + dependency may have been filtered out. + """ + @spec solver_failure_note(cutoff()) :: String.t() | nil + def solver_failure_note(:disabled), do: nil + + def solver_failure_note(cutoff) do + duration = describe_duration(cutoff) + + """ + + Note: cooldown is set to #{duration}. If a dependency was filtered because + it was too recently published, re-run with HEX_COOLDOWN=0 to bypass. + """ + end +end diff --git a/lib/hex/dev.ex b/lib/hex/dev.ex index efdada6a..d6304558 100644 --- a/lib/hex/dev.ex +++ b/lib/hex/dev.ex @@ -5,7 +5,7 @@ if Mix.env() == :dev do @ets_name __MODULE__ @registry_filename "cache.ets" @repo "hexpm" - @ets_version 4 + @ets_version 5 def extract_registry(packages, new_path) do {:ok, original_ets} = :ets.file2tab(String.to_charlist(ets_path())) @@ -42,6 +42,7 @@ if Mix.env() == :dev do copy(original_ets, new_ets, {:outer_checksum, @repo, package, version}) copy(original_ets, new_ets, {:retired, @repo, package, version}) copy(original_ets, new_ets, {:advisories, @repo, package, version}) + copy(original_ets, new_ets, {:published_at, @repo, package, version}) copy(original_ets, new_ets, {:timestamp, @repo, package, version}) [{_, dep_tuples}] = :ets.lookup(original_ets, {:deps, @repo, package, version}) diff --git a/lib/hex/registry/cooldown.ex b/lib/hex/registry/cooldown.ex new file mode 100644 index 00000000..d2669ec7 --- /dev/null +++ b/lib/hex/registry/cooldown.ex @@ -0,0 +1,46 @@ +defmodule Hex.Registry.Cooldown do + @moduledoc false + + @behaviour Hex.Solver.Registry + + alias Hex.Registry.Server + + @impl true + defdelegate prefetch(packages), to: Server + + @impl true + defdelegate dependencies(repo, package, version), to: Server + + @impl true + def versions(repo, package) do + case Server.versions(repo, package) do + {:ok, versions} -> + cutoff = Hex.State.fetch!(:cooldown_cutoff) + bypass = Hex.State.fetch!(:cooldown_bypass_packages) + + cond do + cutoff == :disabled -> + {:ok, versions} + + Hex.Cooldown.repo_excluded?(repo) -> + {:ok, versions} + + MapSet.member?(bypass, package) -> + {:ok, versions} + + true -> + {:ok, filter(versions, repo, package, cutoff)} + end + + :error -> + :error + end + end + + defp filter(versions, repo, package, cutoff) do + Enum.filter(versions, fn version -> + published_at = Server.published_at(repo, package, to_string(version)) + Hex.Cooldown.eligible?(published_at, cutoff) + end) + end +end diff --git a/lib/hex/registry/server.ex b/lib/hex/registry/server.ex index 6c50b40c..c99601f2 100644 --- a/lib/hex/registry/server.ex +++ b/lib/hex/registry/server.ex @@ -7,7 +7,7 @@ defmodule Hex.Registry.Server do @name __MODULE__ @filename "cache.ets" @timeout 60_000 - @ets_version 4 + @ets_version 5 @public_keys_html "https://hex.pm/docs/public_keys" def start_link(opts \\ []) do @@ -58,6 +58,10 @@ defmodule Hex.Registry.Server do GenServer.call(@name, {:advisories, repo, package, version}, @timeout) end + def published_at(repo, package, version) do + GenServer.call(@name, {:published_at, repo, package, version}, @timeout) + end + def last_update() do GenServer.call(@name, :last_update, @timeout) end @@ -216,6 +220,12 @@ defmodule Hex.Registry.Server do end) end + def handle_call({:published_at, repo, package, version}, from, state) do + maybe_wait({repo, package}, from, state, fn -> + lookup(state.ets, {:published_at, repo || "hexpm", package, version}) + end) + end + def handle_call(:last_update, _from, state) do time = lookup(state.ets, :last_update) {:reply, time, state} @@ -313,6 +323,7 @@ defmodule Hex.Registry.Server do # {{:outer_checksum, ^repo, _package, _version}, _} -> true # {{:retired, ^repo, _package, _version}, _} -> true # {{:advisories, ^repo, _package, _version}, _} -> true + # {{:published_at, ^repo, _package, _version}, _} -> true # {{:registry_etag, ^repo, _package}, _} -> true # {{:timestamp, ^repo, _package}, _} -> true # {{:timestamp, ^repo, _package, _version}, _} -> true @@ -327,6 +338,7 @@ defmodule Hex.Registry.Server do {{{:outer_checksum, :"$1", :"$2", :"$3"}, :_}, [{:"=:=", {:const, repo}, :"$1"}], [true]}, {{{:retired, :"$1", :"$2", :"$3"}, :_}, [{:"=:=", {:const, repo}, :"$1"}], [true]}, {{{:advisories, :"$1", :"$2", :"$3"}, :_}, [{:"=:=", {:const, repo}, :"$1"}], [true]}, + {{{:published_at, :"$1", :"$2", :"$3"}, :_}, [{:"=:=", {:const, repo}, :"$1"}], [true]}, {{{:registry_etag, :"$1", :"$2"}, :_}, [{:"=:=", {:const, repo}, :"$1"}], [true]}, {{{:timestamp, :"$1", :"$2"}, :_}, [{:"=:=", {:const, repo}, :"$1"}], [true]}, {{{:timestamp, :"$1", :"$2", :"$3"}, :_}, [{:"=:=", {:const, repo}, :"$1"}], [true]}, @@ -388,6 +400,14 @@ defmodule Hex.Registry.Server do :ets.insert(tid, {{:outer_checksum, repo, package, version}, release[:outer_checksum]}) :ets.insert(tid, {{:retired, repo, package, version}, release[:retired]}) + # The registry encodes published_at as a {seconds, nanos} Timestamp + # map. Cooldown only needs second granularity, so store the integer + # to keep the consumers simple. + :ets.insert( + tid, + {{:published_at, repo, package, version}, timestamp_seconds(release[:published_at])} + ) + release_advisories = (release[:advisory_indexes] || []) |> Enum.map(&Enum.at(pkg_advisories, &1)) @@ -537,6 +557,7 @@ defmodule Hex.Registry.Server do :ets.delete(tid, {:checksum, repo, package, version}) :ets.delete(tid, {:retired, repo, package, version}) :ets.delete(tid, {:advisories, repo, package, version}) + :ets.delete(tid, {:published_at, repo, package, version}) :ets.delete(tid, {:deps, repo, package, version}) end) end @@ -547,4 +568,7 @@ defmodule Hex.Registry.Server do [] -> nil end end + + defp timestamp_seconds(nil), do: nil + defp timestamp_seconds(%{seconds: seconds}), do: seconds end diff --git a/lib/hex/remote_converger.ex b/lib/hex/remote_converger.ex index 96c3cb57..494b3f12 100644 --- a/lib/hex/remote_converger.ex +++ b/lib/hex/remote_converger.ex @@ -67,17 +67,20 @@ defmodule Hex.RemoteConverger do defp run_solver(lock, old_lock, requests, locked, overridden) do start_time = System.monotonic_time(:millisecond) dependencies = Enum.map(requests, &request_to_dependency/1) + setup_cooldown(old_lock, locked) locked = Enum.map(locked, &request_to_locked/1) overridden = Enum.map(overridden, &Atom.to_string/1) verify_otp_app_names(dependencies) + cooldown_preflight(requests) + level = Logger.level() Logger.configure(level: if(Hex.State.fetch!(:debug_solver), do: :debug, else: :info)) solution = try do Hex.Solver.run( - Registry, + Hex.Registry.Cooldown, dependencies, locked, overridden, @@ -98,10 +101,120 @@ defmodule Hex.RemoteConverger do {:error, message} -> Hex.Shell.info(message) + maybe_print_cooldown_hint() Mix.raise("Hex dependency resolution failed") end end + defp setup_cooldown(old_lock, locked) do + cutoff = Hex.Cooldown.build_cutoff() + Hex.State.put(:cooldown_cutoff, cutoff) + + bypass = build_cooldown_bypass(old_lock, locked, cutoff) + Hex.State.put(:cooldown_bypass_packages, bypass) + end + + @doc false + def build_cooldown_bypass(_old_lock, _locked, :disabled), do: MapSet.new() + + def build_cooldown_bypass(old_lock, locked, _cutoff) do + # The bypass set has two sources: + # + # 1. Packages in `locked` (the post-`prepare_locked/3` set): the lockfile + # entry survived requirement checking and dep unlocking, so this + # package is being installed from the lock — no re-resolution, no + # cooldown. Matches the design's "cooldown does not apply when + # proceeding from the lockfile" promise. + # + # 2. Packages in `old_lock` whose locked version is known-unsafe (retired + # or carrying a security advisory): the user re-resolving is trying + # to escape that version; cooldown must not block the escape. Walks + # `old_lock` rather than `locked` because `mix deps.update foo` to + # escape an unsafe foo removes foo from `locked`. + lock_satisfied = for %{name: name} <- locked, into: MapSet.new(), do: name + + unsafe = + for %{repo: repo, name: name, version: version} <- Hex.Mix.from_lock(old_lock), + locked_version_unsafe?(repo || "hexpm", name, to_string(version)), + into: MapSet.new(), + do: name + + MapSet.union(lock_satisfied, unsafe) + end + + defp locked_version_unsafe?(repo, name, version) do + Registry.retired(repo, name, version) != nil or + Registry.advisories(repo, name, version) not in [nil, []] + end + + defp cooldown_preflight(requests) do + cutoff = Hex.State.fetch!(:cooldown_cutoff) + bypass = Hex.State.fetch!(:cooldown_bypass_packages) + + if cutoff != :disabled do + walk_preflight(requests, cutoff, bypass) + end + end + + defp walk_preflight(requests, cutoff, bypass) do + # Both Hex and non-Hex requests carry :requirement (nil for non-Hex + # parents). check_request_cooldown is a no-op for nil-requirement + # requests, so the walk uniformly visits every node and recurses into + # nested deps under non-Hex (path / git) parents. + Enum.each(requests, fn request -> + cond do + MapSet.member?(bypass, request.name) -> :ok + Hex.Cooldown.repo_excluded?(request[:repo]) -> :ok + true -> check_request_cooldown(request, cutoff) + end + + walk_preflight(Map.get(request, :dependencies, []), cutoff, bypass) + end) + end + + defp check_request_cooldown(%{requirement: nil}, _cutoff), do: :ok + + defp check_request_cooldown(%{repo: repo, name: name, requirement: requirement}, cutoff) do + with {:ok, parsed} <- Version.parse_requirement(requirement), + {:ok, versions} <- Registry.versions(repo, name) do + matching = + versions + |> Enum.filter(&Version.match?(&1, parsed)) + |> Enum.map(&to_string/1) + + filtered = + Enum.flat_map(matching, fn version -> + case Registry.published_at(repo, name, version) do + nil -> + [] + + published_at -> + if Hex.Cooldown.eligible?(published_at, cutoff) do + [] + else + [{version, published_at}] + end + end + end) + + if matching != [] and length(filtered) == length(matching) do + Mix.raise(Hex.Cooldown.preflight_error(name, requirement, filtered, cutoff)) + end + else + _ -> :ok + end + end + + defp check_request_cooldown(_, _), do: :ok + + defp maybe_print_cooldown_hint() do + cutoff = Hex.State.fetch!(:cooldown_cutoff) + + if note = Hex.Cooldown.solver_failure_note(cutoff) do + Hex.Shell.info(note) + end + end + defp request_to_dependency(request) do constraint = if request.requirement do diff --git a/lib/hex/state.ex b/lib/hex/state.ex index 2e4546d3..e273b47a 100644 --- a/lib/hex/state.ex +++ b/lib/hex/state.ex @@ -128,6 +128,20 @@ defmodule Hex.State do env: ["CI"], default: false, fun: {__MODULE__, :to_truthy_boolean} + }, + cooldown: %{ + env: ["HEX_COOLDOWN"], + config: [:cooldown], + default: "0d", + skip_env_if_empty: true, + fun: {Hex.Cooldown, :parse_config} + }, + cooldown_exclude_repos: %{ + env: ["HEX_COOLDOWN_EXCLUDE_REPOS"], + config: [:cooldown_exclude_repos], + default: [], + skip_env_if_empty: true, + fun: {Hex.Cooldown, :parse_exclude_repos} } } @@ -234,7 +248,7 @@ defmodule Hex.State do env = System.get_env() result = - load_env(spec[:env], env) || + load_env(spec[:env], env, spec[:skip_env_if_empty]) || load_project_config(project_config, spec[:config]) || load_global_config(global_config, spec[:config]) @@ -279,9 +293,10 @@ defmodule Hex.State do |> Path.join("hex.config") end - defp load_env(keys, env) do + defp load_env(keys, env, skip_if_empty) do Enum.find_value(keys || [], fn key -> case Map.fetch(env, key) do + {:ok, ""} when skip_if_empty == true -> nil {:ok, value} -> {{:env, key}, value} :error -> nil end diff --git a/lib/mix/tasks/hex.config.ex b/lib/mix/tasks/hex.config.ex index cd57d972..164a2432 100644 --- a/lib/mix/tasks/hex.config.ex +++ b/lib/mix/tasks/hex.config.ex @@ -63,6 +63,18 @@ defmodule Mix.Tasks.Hex.Config do * `no_short_urls` - If set to true Hex will not shorten any links. Can be overridden by setting the environment variable `HEX_NO_SHORT_URLS` (Default: `false`) + * `cooldown` - Minimum age a package release must have to be considered + during dependency resolution, e.g. `"7d"`, `"2w"`, `"1mo"`. Releases + published more recently are filtered out of the candidate set when + resolving dependencies. Does not apply to installs from an existing + lockfile. Can be overridden by setting the environment variable + `HEX_COOLDOWN` (Default: `"0d"` — no cooldown) + * `cooldown_exclude_repos` - List of repository names for which + `cooldown` does not apply, e.g. `["hexpm:myorg"]`. Useful when an + organization publishes hotfixes to its own repository and wants to + consume them without cooldown delay. Can be overridden by setting + the environment variable `HEX_COOLDOWN_EXCLUDE_REPOS` to a + comma-separated list (Default: `[]`) Hex responds to these additional environment variables: diff --git a/src/mix_hex_api.erl b/src/mix_hex_api.erl index 1bc95746..1aa1e22d 100644 --- a/src/mix_hex_api.erl +++ b/src/mix_hex_api.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API diff --git a/src/mix_hex_api_auth.erl b/src/mix_hex_api_auth.erl index 8979fd38..695449c3 100644 --- a/src/mix_hex_api_auth.erl +++ b/src/mix_hex_api_auth.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API - Authentication. diff --git a/src/mix_hex_api_key.erl b/src/mix_hex_api_key.erl index d35b6e88..0d922146 100644 --- a/src/mix_hex_api_key.erl +++ b/src/mix_hex_api_key.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API - Keys. diff --git a/src/mix_hex_api_oauth.erl b/src/mix_hex_api_oauth.erl index d8ac5bb6..bb610ee1 100644 --- a/src/mix_hex_api_oauth.erl +++ b/src/mix_hex_api_oauth.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API - OAuth. diff --git a/src/mix_hex_api_organization.erl b/src/mix_hex_api_organization.erl index a9fdb034..4be6b8da 100644 --- a/src/mix_hex_api_organization.erl +++ b/src/mix_hex_api_organization.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API - Organizations. diff --git a/src/mix_hex_api_organization_member.erl b/src/mix_hex_api_organization_member.erl index ab8fabe6..e26aefa3 100644 --- a/src/mix_hex_api_organization_member.erl +++ b/src/mix_hex_api_organization_member.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API - Organization Members. diff --git a/src/mix_hex_api_package.erl b/src/mix_hex_api_package.erl index 465f6717..5ec346e9 100644 --- a/src/mix_hex_api_package.erl +++ b/src/mix_hex_api_package.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API - Packages. diff --git a/src/mix_hex_api_package_owner.erl b/src/mix_hex_api_package_owner.erl index b53f30d5..a548b8ba 100644 --- a/src/mix_hex_api_package_owner.erl +++ b/src/mix_hex_api_package_owner.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API - Package Owners. diff --git a/src/mix_hex_api_release.erl b/src/mix_hex_api_release.erl index 94c19765..6cf82c6a 100644 --- a/src/mix_hex_api_release.erl +++ b/src/mix_hex_api_release.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API - Releases. diff --git a/src/mix_hex_api_short_url.erl b/src/mix_hex_api_short_url.erl index ef8904ec..6aecd612 100644 --- a/src/mix_hex_api_short_url.erl +++ b/src/mix_hex_api_short_url.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API - Short URLs. diff --git a/src/mix_hex_api_user.erl b/src/mix_hex_api_user.erl index 7ffa75e8..eaeb5a0b 100644 --- a/src/mix_hex_api_user.erl +++ b/src/mix_hex_api_user.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex HTTP API - Users. diff --git a/src/mix_hex_core.erl b/src/mix_hex_core.erl index b8b7908b..3572552d 100644 --- a/src/mix_hex_core.erl +++ b/src/mix_hex_core.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% `hex_core' entrypoint module. diff --git a/src/mix_hex_core.hrl b/src/mix_hex_core.hrl index 70d96530..1e4a8ee5 100644 --- a/src/mix_hex_core.hrl +++ b/src/mix_hex_core.hrl @@ -1,3 +1,3 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually --define(HEX_CORE_VERSION, "0.16.0"). +-define(HEX_CORE_VERSION, "0.16.1"). diff --git a/src/mix_hex_erl_tar.erl b/src/mix_hex_erl_tar.erl index e0dc46b6..c92addb9 100644 --- a/src/mix_hex_erl_tar.erl +++ b/src/mix_hex_erl_tar.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% This file is a copy of erl_tar.erl from OTP with the following modifications: %% 1. Module renamed from erl_tar to mix_hex_erl_tar diff --git a/src/mix_hex_erl_tar.hrl b/src/mix_hex_erl_tar.hrl index 05fbc8f8..3a58dfa1 100644 --- a/src/mix_hex_erl_tar.hrl +++ b/src/mix_hex_erl_tar.hrl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% This file is a copy of erl_tar.hrl from OTP with the following modifications: %% 1. Added chunk_size field to #read_opts{} for streaming extraction to disk diff --git a/src/mix_hex_http.erl b/src/mix_hex_http.erl index 30aad081..66b0449a 100644 --- a/src/mix_hex_http.erl +++ b/src/mix_hex_http.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% HTTP contract. diff --git a/src/mix_hex_http_httpc.erl b/src/mix_hex_http_httpc.erl index 1cdc24e5..2dd831c1 100644 --- a/src/mix_hex_http_httpc.erl +++ b/src/mix_hex_http_httpc.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% httpc-based implementation of {@link mix_hex_http} contract. diff --git a/src/mix_hex_licenses.erl b/src/mix_hex_licenses.erl index 63e46db2..fb7dd544 100644 --- a/src/mix_hex_licenses.erl +++ b/src/mix_hex_licenses.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Hex Licenses. diff --git a/src/mix_hex_pb_names.erl b/src/mix_hex_pb_names.erl index 5d0fd296..f77cbf71 100644 --- a/src/mix_hex_pb_names.erl +++ b/src/mix_hex_pb_names.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% -*- coding: utf-8 -*- %% % this file is @generated diff --git a/src/mix_hex_pb_package.erl b/src/mix_hex_pb_package.erl index 6f5628a8..52d7db64 100644 --- a/src/mix_hex_pb_package.erl +++ b/src/mix_hex_pb_package.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% -*- coding: utf-8 -*- %% % this file is @generated @@ -71,7 +71,8 @@ dependencies => ['Dependency'()], % = 3, repeated retired => 'RetirementStatus'(), % = 4, optional outer_checksum => iodata(), % = 5, optional - advisory_indexes => [non_neg_integer()] % = 6, repeated, 32 bits + advisory_indexes => [non_neg_integer()], % = 6, repeated, 32 bits + published_at => 'Timestamp'() % = 7, optional }. -type 'RetirementStatus'() :: @@ -96,20 +97,19 @@ repository => unicode:chardata() % = 5, optional }. --export_type(['Package'/0, 'Release'/0, 'RetirementStatus'/0, 'SecurityAdvisory'/0, 'Dependency'/0]). --type '$msg_name'() :: 'Package' | 'Release' | 'RetirementStatus' | 'SecurityAdvisory' | 'Dependency'. --type '$msg'() :: 'Package'() | 'Release'() | 'RetirementStatus'() | 'SecurityAdvisory'() | 'Dependency'(). +-type 'Timestamp'() :: + #{seconds => integer(), % = 1, required, 64 bits + nanos => integer() % = 2, required, 32 bits + }. + +-export_type(['Package'/0, 'Release'/0, 'RetirementStatus'/0, 'SecurityAdvisory'/0, 'Dependency'/0, 'Timestamp'/0]). +-type '$msg_name'() :: 'Package' | 'Release' | 'RetirementStatus' | 'SecurityAdvisory' | 'Dependency' | 'Timestamp'. +-type '$msg'() :: 'Package'() | 'Release'() | 'RetirementStatus'() | 'SecurityAdvisory'() | 'Dependency'() | 'Timestamp'(). -export_type(['$msg_name'/0, '$msg'/0]). --if(?OTP_RELEASE >= 24). --dialyzer({no_underspecs, encode_msg/2}). --endif. -spec encode_msg('$msg'(), '$msg_name'()) -> binary(). encode_msg(Msg, MsgName) when is_atom(MsgName) -> encode_msg(Msg, MsgName, []). --if(?OTP_RELEASE >= 24). --dialyzer({no_underspecs, encode_msg/3}). --endif. -spec encode_msg('$msg'(), '$msg_name'(), list()) -> binary(). encode_msg(Msg, MsgName, Opts) -> verify_msg(Msg, MsgName, Opts), @@ -119,7 +119,8 @@ encode_msg(Msg, MsgName, Opts) -> 'Release' -> encode_msg_Release(id(Msg, TrUserData), TrUserData); 'RetirementStatus' -> encode_msg_RetirementStatus(id(Msg, TrUserData), TrUserData); 'SecurityAdvisory' -> encode_msg_SecurityAdvisory(id(Msg, TrUserData), TrUserData); - 'Dependency' -> encode_msg_Dependency(id(Msg, TrUserData), TrUserData) + 'Dependency' -> encode_msg_Dependency(id(Msg, TrUserData), TrUserData); + 'Timestamp' -> encode_msg_Timestamp(id(Msg, TrUserData), TrUserData) end. @@ -168,13 +169,17 @@ encode_msg_Release(#{version := F1, inner_checksum := F2} = M, Bin, TrUserData) #{outer_checksum := F5} -> begin TrF5 = id(F5, TrUserData), e_type_bytes(TrF5, <>, TrUserData) end; _ -> B4 end, + B6 = case M of + #{advisory_indexes := F6} -> + TrF6 = id(F6, TrUserData), + if TrF6 == [] -> B5; + true -> e_field_Release_advisory_indexes(TrF6, B5, TrUserData) + end; + _ -> B5 + end, case M of - #{advisory_indexes := F6} -> - TrF6 = id(F6, TrUserData), - if TrF6 == [] -> B5; - true -> e_field_Release_advisory_indexes(TrF6, B5, TrUserData) - end; - _ -> B5 + #{published_at := F7} -> begin TrF7 = id(F7, TrUserData), e_mfield_Release_published_at(TrF7, <>, TrUserData) end; + _ -> B6 end. encode_msg_RetirementStatus(Msg, TrUserData) -> encode_msg_RetirementStatus(Msg, <<>>, TrUserData). @@ -223,6 +228,13 @@ encode_msg_Dependency(#{package := F1, requirement := F2} = M, Bin, TrUserData) _ -> B4 end. +encode_msg_Timestamp(Msg, TrUserData) -> encode_msg_Timestamp(Msg, <<>>, TrUserData). + + +encode_msg_Timestamp(#{seconds := F1, nanos := F2}, Bin, TrUserData) -> + B1 = begin TrF1 = id(F1, TrUserData), e_type_int64(TrF1, <>, TrUserData) end, + begin TrF2 = id(F2, TrUserData), e_type_int32(TrF2, <>, TrUserData) end. + e_mfield_Package_releases(Msg, Bin, TrUserData) -> SubBin = encode_msg_Release(Msg, <<>>, TrUserData), Bin2 = e_varint(byte_size(SubBin), Bin), @@ -267,6 +279,11 @@ e_field_Release_advisory_indexes([Elem | Rest], Bin, TrUserData) -> e_field_Release_advisory_indexes(Rest, Bin3, TrUserData); e_field_Release_advisory_indexes([], Bin, _TrUserData) -> Bin. +e_mfield_Release_published_at(Msg, Bin, TrUserData) -> + SubBin = encode_msg_Timestamp(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + e_enum_RetirementReason('RETIRED_OTHER', Bin, _TrUserData) -> <>; e_enum_RetirementReason('RETIRED_INVALID', Bin, _TrUserData) -> <>; e_enum_RetirementReason('RETIRED_SECURITY', Bin, _TrUserData) -> <>; @@ -407,7 +424,8 @@ decode_msg_2_doit('Package', Bin, TrUserData) -> id(decode_msg_Package(Bin, TrUs decode_msg_2_doit('Release', Bin, TrUserData) -> id(decode_msg_Release(Bin, TrUserData), TrUserData); decode_msg_2_doit('RetirementStatus', Bin, TrUserData) -> id(decode_msg_RetirementStatus(Bin, TrUserData), TrUserData); decode_msg_2_doit('SecurityAdvisory', Bin, TrUserData) -> id(decode_msg_SecurityAdvisory(Bin, TrUserData), TrUserData); -decode_msg_2_doit('Dependency', Bin, TrUserData) -> id(decode_msg_Dependency(Bin, TrUserData), TrUserData). +decode_msg_2_doit('Dependency', Bin, TrUserData) -> id(decode_msg_Dependency(Bin, TrUserData), TrUserData); +decode_msg_2_doit('Timestamp', Bin, TrUserData) -> id(decode_msg_Timestamp(Bin, TrUserData), TrUserData). @@ -490,16 +508,18 @@ skip_32_Package(<<_:32, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, TrUser skip_64_Package(<<_:64, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, TrUserData) -> dfp_read_field_def_Package(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, TrUserData). -decode_msg_Release(Bin, TrUserData) -> dfp_read_field_def_Release(Bin, 0, 0, 0, id('$undef', TrUserData), id('$undef', TrUserData), id([], TrUserData), id('$undef', TrUserData), id('$undef', TrUserData), id([], TrUserData), TrUserData). - -dfp_read_field_def_Release(<<10, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> d_field_Release_version(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -dfp_read_field_def_Release(<<18, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> d_field_Release_inner_checksum(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -dfp_read_field_def_Release(<<26, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> d_field_Release_dependencies(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -dfp_read_field_def_Release(<<34, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> d_field_Release_retired(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -dfp_read_field_def_Release(<<42, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> d_field_Release_outer_checksum(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -dfp_read_field_def_Release(<<50, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> d_pfield_Release_advisory_indexes(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -dfp_read_field_def_Release(<<48, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> d_field_Release_advisory_indexes(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -dfp_read_field_def_Release(<<>>, 0, 0, _, F@_1, F@_2, R1, F@_4, F@_5, R2, TrUserData) -> +decode_msg_Release(Bin, TrUserData) -> + dfp_read_field_def_Release(Bin, 0, 0, 0, id('$undef', TrUserData), id('$undef', TrUserData), id([], TrUserData), id('$undef', TrUserData), id('$undef', TrUserData), id([], TrUserData), id('$undef', TrUserData), TrUserData). + +dfp_read_field_def_Release(<<10, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> d_field_Release_version(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +dfp_read_field_def_Release(<<18, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> d_field_Release_inner_checksum(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +dfp_read_field_def_Release(<<26, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> d_field_Release_dependencies(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +dfp_read_field_def_Release(<<34, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> d_field_Release_retired(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +dfp_read_field_def_Release(<<42, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> d_field_Release_outer_checksum(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +dfp_read_field_def_Release(<<50, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> d_pfield_Release_advisory_indexes(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +dfp_read_field_def_Release(<<48, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> d_field_Release_advisory_indexes(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +dfp_read_field_def_Release(<<58, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> d_field_Release_published_at(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +dfp_read_field_def_Release(<<>>, 0, 0, _, F@_1, F@_2, R1, F@_4, F@_5, R2, F@_7, TrUserData) -> S1 = #{version => F@_1, inner_checksum => F@_2, advisory_indexes => lists_reverse(R2, TrUserData)}, S2 = if R1 == '$undef' -> S1; true -> S1#{dependencies => lists_reverse(R1, TrUserData)} @@ -507,32 +527,36 @@ dfp_read_field_def_Release(<<>>, 0, 0, _, F@_1, F@_2, R1, F@_4, F@_5, R2, TrUser S3 = if F@_4 == '$undef' -> S2; true -> S2#{retired => F@_4} end, - if F@_5 == '$undef' -> S3; - true -> S3#{outer_checksum => F@_5} + S4 = if F@_5 == '$undef' -> S3; + true -> S3#{outer_checksum => F@_5} + end, + if F@_7 == '$undef' -> S4; + true -> S4#{published_at => F@_7} end; -dfp_read_field_def_Release(Other, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> dg_read_field_def_Release(Other, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData). +dfp_read_field_def_Release(Other, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> dg_read_field_def_Release(Other, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData). -dg_read_field_def_Release(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) when N < 32 - 7 -> dg_read_field_def_Release(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -dg_read_field_def_Release(<<0:1, X:7, Rest/binary>>, N, Acc, _, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> +dg_read_field_def_Release(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) when N < 32 - 7 -> dg_read_field_def_Release(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +dg_read_field_def_Release(<<0:1, X:7, Rest/binary>>, N, Acc, _, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> Key = X bsl N + Acc, case Key of - 10 -> d_field_Release_version(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); - 18 -> d_field_Release_inner_checksum(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); - 26 -> d_field_Release_dependencies(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); - 34 -> d_field_Release_retired(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); - 42 -> d_field_Release_outer_checksum(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); - 50 -> d_pfield_Release_advisory_indexes(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); - 48 -> d_field_Release_advisory_indexes(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); + 10 -> d_field_Release_version(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 18 -> d_field_Release_inner_checksum(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 26 -> d_field_Release_dependencies(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 34 -> d_field_Release_retired(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 42 -> d_field_Release_outer_checksum(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 50 -> d_pfield_Release_advisory_indexes(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 48 -> d_field_Release_advisory_indexes(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 58 -> d_field_Release_published_at(Rest, 0, 0, 0, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); _ -> case Key band 7 of - 0 -> skip_varint_Release(Rest, 0, 0, Key bsr 3, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); - 1 -> skip_64_Release(Rest, 0, 0, Key bsr 3, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); - 2 -> skip_length_delimited_Release(Rest, 0, 0, Key bsr 3, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); - 3 -> skip_group_Release(Rest, 0, 0, Key bsr 3, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); - 5 -> skip_32_Release(Rest, 0, 0, Key bsr 3, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) + 0 -> skip_varint_Release(Rest, 0, 0, Key bsr 3, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 1 -> skip_64_Release(Rest, 0, 0, Key bsr 3, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 2 -> skip_length_delimited_Release(Rest, 0, 0, Key bsr 3, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 3 -> skip_group_Release(Rest, 0, 0, Key bsr 3, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); + 5 -> skip_32_Release(Rest, 0, 0, Key bsr 3, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) end end; -dg_read_field_def_Release(<<>>, 0, 0, _, F@_1, F@_2, R1, F@_4, F@_5, R2, TrUserData) -> +dg_read_field_def_Release(<<>>, 0, 0, _, F@_1, F@_2, R1, F@_4, F@_5, R2, F@_7, TrUserData) -> S1 = #{version => F@_1, inner_checksum => F@_2, advisory_indexes => lists_reverse(R2, TrUserData)}, S2 = if R1 == '$undef' -> S1; true -> S1#{dependencies => lists_reverse(R1, TrUserData)} @@ -540,27 +564,31 @@ dg_read_field_def_Release(<<>>, 0, 0, _, F@_1, F@_2, R1, F@_4, F@_5, R2, TrUserD S3 = if F@_4 == '$undef' -> S2; true -> S2#{retired => F@_4} end, - if F@_5 == '$undef' -> S3; - true -> S3#{outer_checksum => F@_5} + S4 = if F@_5 == '$undef' -> S3; + true -> S3#{outer_checksum => F@_5} + end, + if F@_7 == '$undef' -> S4; + true -> S4#{published_at => F@_7} end. -d_field_Release_version(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) when N < 57 -> d_field_Release_version(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -d_field_Release_version(<<0:1, X:7, Rest/binary>>, N, Acc, F, _, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> +d_field_Release_version(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) when N < 57 -> d_field_Release_version(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +d_field_Release_version(<<0:1, X:7, Rest/binary>>, N, Acc, F, _, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> {NewFValue, RestF} = begin Len = X bsl N + Acc, <> = Rest, Bytes2 = binary:copy(Bytes), {id(Bytes2, TrUserData), Rest2} end, - dfp_read_field_def_Release(RestF, 0, 0, F, NewFValue, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData). + dfp_read_field_def_Release(RestF, 0, 0, F, NewFValue, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData). -d_field_Release_inner_checksum(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) when N < 57 -> d_field_Release_inner_checksum(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -d_field_Release_inner_checksum(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, _, F@_3, F@_4, F@_5, F@_6, TrUserData) -> +d_field_Release_inner_checksum(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) when N < 57 -> + d_field_Release_inner_checksum(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +d_field_Release_inner_checksum(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, _, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> {NewFValue, RestF} = begin Len = X bsl N + Acc, <> = Rest, Bytes2 = binary:copy(Bytes), {id(Bytes2, TrUserData), Rest2} end, - dfp_read_field_def_Release(RestF, 0, 0, F, F@_1, NewFValue, F@_3, F@_4, F@_5, F@_6, TrUserData). + dfp_read_field_def_Release(RestF, 0, 0, F, F@_1, NewFValue, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData). -d_field_Release_dependencies(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) when N < 57 -> d_field_Release_dependencies(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -d_field_Release_dependencies(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, Prev, F@_4, F@_5, F@_6, TrUserData) -> +d_field_Release_dependencies(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) when N < 57 -> d_field_Release_dependencies(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +d_field_Release_dependencies(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, Prev, F@_4, F@_5, F@_6, F@_7, TrUserData) -> {NewFValue, RestF} = begin Len = X bsl N + Acc, <> = Rest, {id(decode_msg_Dependency(Bs, TrUserData), TrUserData), Rest2} end, - dfp_read_field_def_Release(RestF, 0, 0, F, F@_1, F@_2, cons(NewFValue, Prev, TrUserData), F@_4, F@_5, F@_6, TrUserData). + dfp_read_field_def_Release(RestF, 0, 0, F, F@_1, F@_2, cons(NewFValue, Prev, TrUserData), F@_4, F@_5, F@_6, F@_7, TrUserData). -d_field_Release_retired(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) when N < 57 -> d_field_Release_retired(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -d_field_Release_retired(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, Prev, F@_5, F@_6, TrUserData) -> +d_field_Release_retired(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) when N < 57 -> d_field_Release_retired(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +d_field_Release_retired(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, Prev, F@_5, F@_6, F@_7, TrUserData) -> {NewFValue, RestF} = begin Len = X bsl N + Acc, <> = Rest, {id(decode_msg_RetirementStatus(Bs, TrUserData), TrUserData), Rest2} end, dfp_read_field_def_Release(RestF, 0, @@ -574,24 +602,28 @@ d_field_Release_retired(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, end, F@_5, F@_6, + F@_7, TrUserData). -d_field_Release_outer_checksum(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) when N < 57 -> d_field_Release_outer_checksum(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -d_field_Release_outer_checksum(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, _, F@_6, TrUserData) -> +d_field_Release_outer_checksum(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) when N < 57 -> + d_field_Release_outer_checksum(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +d_field_Release_outer_checksum(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, _, F@_6, F@_7, TrUserData) -> {NewFValue, RestF} = begin Len = X bsl N + Acc, <> = Rest, Bytes2 = binary:copy(Bytes), {id(Bytes2, TrUserData), Rest2} end, - dfp_read_field_def_Release(RestF, 0, 0, F, F@_1, F@_2, F@_3, F@_4, NewFValue, F@_6, TrUserData). + dfp_read_field_def_Release(RestF, 0, 0, F, F@_1, F@_2, F@_3, F@_4, NewFValue, F@_6, F@_7, TrUserData). -d_field_Release_advisory_indexes(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) when N < 57 -> d_field_Release_advisory_indexes(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -d_field_Release_advisory_indexes(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, Prev, TrUserData) -> +d_field_Release_advisory_indexes(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) when N < 57 -> + d_field_Release_advisory_indexes(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +d_field_Release_advisory_indexes(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, Prev, F@_7, TrUserData) -> {NewFValue, RestF} = {id((X bsl N + Acc) band 4294967295, TrUserData), Rest}, - dfp_read_field_def_Release(RestF, 0, 0, F, F@_1, F@_2, F@_3, F@_4, F@_5, cons(NewFValue, Prev, TrUserData), TrUserData). + dfp_read_field_def_Release(RestF, 0, 0, F, F@_1, F@_2, F@_3, F@_4, F@_5, cons(NewFValue, Prev, TrUserData), F@_7, TrUserData). -d_pfield_Release_advisory_indexes(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) when N < 57 -> d_pfield_Release_advisory_indexes(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -d_pfield_Release_advisory_indexes(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, E, TrUserData) -> +d_pfield_Release_advisory_indexes(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) when N < 57 -> + d_pfield_Release_advisory_indexes(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +d_pfield_Release_advisory_indexes(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, E, F@_7, TrUserData) -> Len = X bsl N + Acc, <> = Rest, NewSeq = d_packed_field_Release_advisory_indexes(PackedBytes, 0, 0, F, E, TrUserData), - dfp_read_field_def_Release(Rest2, 0, 0, F, F@_1, F@_2, F@_3, F@_4, F@_5, NewSeq, TrUserData). + dfp_read_field_def_Release(Rest2, 0, 0, F, F@_1, F@_2, F@_3, F@_4, F@_5, NewSeq, F@_7, TrUserData). d_packed_field_Release_advisory_indexes(<<1:1, X:7, Rest/binary>>, N, Acc, F, AccSeq, TrUserData) when N < 57 -> d_packed_field_Release_advisory_indexes(Rest, N + 7, X bsl N + Acc, F, AccSeq, TrUserData); d_packed_field_Release_advisory_indexes(<<0:1, X:7, Rest/binary>>, N, Acc, F, AccSeq, TrUserData) -> @@ -599,22 +631,41 @@ d_packed_field_Release_advisory_indexes(<<0:1, X:7, Rest/binary>>, N, Acc, F, Ac d_packed_field_Release_advisory_indexes(RestF, 0, 0, F, [NewFValue | AccSeq], TrUserData); d_packed_field_Release_advisory_indexes(<<>>, 0, 0, _, AccSeq, _) -> AccSeq. -skip_varint_Release(<<1:1, _:7, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> skip_varint_Release(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -skip_varint_Release(<<0:1, _:7, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> dfp_read_field_def_Release(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData). +d_field_Release_published_at(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) when N < 57 -> d_field_Release_published_at(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +d_field_Release_published_at(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, Prev, TrUserData) -> + {NewFValue, RestF} = begin Len = X bsl N + Acc, <> = Rest, {id(decode_msg_Timestamp(Bs, TrUserData), TrUserData), Rest2} end, + dfp_read_field_def_Release(RestF, + 0, + 0, + F, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + if Prev == '$undef' -> NewFValue; + true -> merge_msg_Timestamp(Prev, NewFValue, TrUserData) + end, + TrUserData). + +skip_varint_Release(<<1:1, _:7, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> skip_varint_Release(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +skip_varint_Release(<<0:1, _:7, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> dfp_read_field_def_Release(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData). -skip_length_delimited_Release(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) when N < 57 -> skip_length_delimited_Release(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData); -skip_length_delimited_Release(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> +skip_length_delimited_Release(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) when N < 57 -> + skip_length_delimited_Release(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData); +skip_length_delimited_Release(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> Length = X bsl N + Acc, <<_:Length/binary, Rest2/binary>> = Rest, - dfp_read_field_def_Release(Rest2, 0, 0, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData). + dfp_read_field_def_Release(Rest2, 0, 0, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData). -skip_group_Release(Bin, _, Z2, FNum, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> +skip_group_Release(Bin, _, Z2, FNum, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> {_, Rest} = read_group(Bin, FNum), - dfp_read_field_def_Release(Rest, 0, Z2, FNum, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData). + dfp_read_field_def_Release(Rest, 0, Z2, FNum, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData). -skip_32_Release(<<_:32, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> dfp_read_field_def_Release(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData). +skip_32_Release(<<_:32, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> dfp_read_field_def_Release(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData). -skip_64_Release(<<_:64, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData) -> dfp_read_field_def_Release(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, TrUserData). +skip_64_Release(<<_:64, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> dfp_read_field_def_Release(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData). decode_msg_RetirementStatus(Bin, TrUserData) -> dfp_read_field_def_RetirementStatus(Bin, 0, 0, 0, id('$undef', TrUserData), id('$undef', TrUserData), TrUserData). @@ -866,6 +917,57 @@ skip_32_Dependency(<<_:32, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_ skip_64_Dependency(<<_:64, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) -> dfp_read_field_def_Dependency(Rest, Z1, Z2, F, F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData). +decode_msg_Timestamp(Bin, TrUserData) -> dfp_read_field_def_Timestamp(Bin, 0, 0, 0, id('$undef', TrUserData), id('$undef', TrUserData), TrUserData). + +dfp_read_field_def_Timestamp(<<8, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, TrUserData) -> d_field_Timestamp_seconds(Rest, Z1, Z2, F, F@_1, F@_2, TrUserData); +dfp_read_field_def_Timestamp(<<16, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, TrUserData) -> d_field_Timestamp_nanos(Rest, Z1, Z2, F, F@_1, F@_2, TrUserData); +dfp_read_field_def_Timestamp(<<>>, 0, 0, _, F@_1, F@_2, _) -> #{seconds => F@_1, nanos => F@_2}; +dfp_read_field_def_Timestamp(Other, Z1, Z2, F, F@_1, F@_2, TrUserData) -> dg_read_field_def_Timestamp(Other, Z1, Z2, F, F@_1, F@_2, TrUserData). + +dg_read_field_def_Timestamp(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, TrUserData) when N < 32 - 7 -> dg_read_field_def_Timestamp(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, TrUserData); +dg_read_field_def_Timestamp(<<0:1, X:7, Rest/binary>>, N, Acc, _, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 8 -> d_field_Timestamp_seconds(Rest, 0, 0, 0, F@_1, F@_2, TrUserData); + 16 -> d_field_Timestamp_nanos(Rest, 0, 0, 0, F@_1, F@_2, TrUserData); + _ -> + case Key band 7 of + 0 -> skip_varint_Timestamp(Rest, 0, 0, Key bsr 3, F@_1, F@_2, TrUserData); + 1 -> skip_64_Timestamp(Rest, 0, 0, Key bsr 3, F@_1, F@_2, TrUserData); + 2 -> skip_length_delimited_Timestamp(Rest, 0, 0, Key bsr 3, F@_1, F@_2, TrUserData); + 3 -> skip_group_Timestamp(Rest, 0, 0, Key bsr 3, F@_1, F@_2, TrUserData); + 5 -> skip_32_Timestamp(Rest, 0, 0, Key bsr 3, F@_1, F@_2, TrUserData) + end + end; +dg_read_field_def_Timestamp(<<>>, 0, 0, _, F@_1, F@_2, _) -> #{seconds => F@_1, nanos => F@_2}. + +d_field_Timestamp_seconds(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, TrUserData) when N < 57 -> d_field_Timestamp_seconds(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, TrUserData); +d_field_Timestamp_seconds(<<0:1, X:7, Rest/binary>>, N, Acc, F, _, F@_2, TrUserData) -> + {NewFValue, RestF} = {begin <> = <<(X bsl N + Acc):64/unsigned-native>>, id(Res, TrUserData) end, Rest}, + dfp_read_field_def_Timestamp(RestF, 0, 0, F, NewFValue, F@_2, TrUserData). + +d_field_Timestamp_nanos(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, TrUserData) when N < 57 -> d_field_Timestamp_nanos(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, TrUserData); +d_field_Timestamp_nanos(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, _, TrUserData) -> + {NewFValue, RestF} = {begin <> = <<(X bsl N + Acc):32/unsigned-native>>, id(Res, TrUserData) end, Rest}, + dfp_read_field_def_Timestamp(RestF, 0, 0, F, F@_1, NewFValue, TrUserData). + +skip_varint_Timestamp(<<1:1, _:7, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, TrUserData) -> skip_varint_Timestamp(Rest, Z1, Z2, F, F@_1, F@_2, TrUserData); +skip_varint_Timestamp(<<0:1, _:7, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, TrUserData) -> dfp_read_field_def_Timestamp(Rest, Z1, Z2, F, F@_1, F@_2, TrUserData). + +skip_length_delimited_Timestamp(<<1:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, TrUserData) when N < 57 -> skip_length_delimited_Timestamp(Rest, N + 7, X bsl N + Acc, F, F@_1, F@_2, TrUserData); +skip_length_delimited_Timestamp(<<0:1, X:7, Rest/binary>>, N, Acc, F, F@_1, F@_2, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_Timestamp(Rest2, 0, 0, F, F@_1, F@_2, TrUserData). + +skip_group_Timestamp(Bin, _, Z2, FNum, F@_1, F@_2, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_Timestamp(Rest, 0, Z2, FNum, F@_1, F@_2, TrUserData). + +skip_32_Timestamp(<<_:32, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, TrUserData) -> dfp_read_field_def_Timestamp(Rest, Z1, Z2, F, F@_1, F@_2, TrUserData). + +skip_64_Timestamp(<<_:64, Rest/binary>>, Z1, Z2, F, F@_1, F@_2, TrUserData) -> dfp_read_field_def_Timestamp(Rest, Z1, Z2, F, F@_1, F@_2, TrUserData). + d_enum_RetirementReason(0) -> 'RETIRED_OTHER'; d_enum_RetirementReason(1) -> 'RETIRED_INVALID'; d_enum_RetirementReason(2) -> 'RETIRED_SECURITY'; @@ -947,7 +1049,8 @@ merge_msgs(Prev, New, MsgName, Opts) -> 'Release' -> merge_msg_Release(Prev, New, TrUserData); 'RetirementStatus' -> merge_msg_RetirementStatus(Prev, New, TrUserData); 'SecurityAdvisory' -> merge_msg_SecurityAdvisory(Prev, New, TrUserData); - 'Dependency' -> merge_msg_Dependency(Prev, New, TrUserData) + 'Dependency' -> merge_msg_Dependency(Prev, New, TrUserData); + 'Timestamp' -> merge_msg_Timestamp(Prev, New, TrUserData) end. -compile({nowarn_unused_function,merge_msg_Package/3}). @@ -986,11 +1089,17 @@ merge_msg_Release(#{} = PMsg, #{version := NFversion, inner_checksum := NFinner_ {#{outer_checksum := PFouter_checksum}, _} -> S3#{outer_checksum => PFouter_checksum}; _ -> S3 end, + S5 = case {PMsg, NMsg} of + {#{advisory_indexes := PFadvisory_indexes}, #{advisory_indexes := NFadvisory_indexes}} -> S4#{advisory_indexes => 'erlang_++'(PFadvisory_indexes, NFadvisory_indexes, TrUserData)}; + {_, #{advisory_indexes := NFadvisory_indexes}} -> S4#{advisory_indexes => NFadvisory_indexes}; + {#{advisory_indexes := PFadvisory_indexes}, _} -> S4#{advisory_indexes => PFadvisory_indexes}; + {_, _} -> S4 + end, case {PMsg, NMsg} of - {#{advisory_indexes := PFadvisory_indexes}, #{advisory_indexes := NFadvisory_indexes}} -> S4#{advisory_indexes => 'erlang_++'(PFadvisory_indexes, NFadvisory_indexes, TrUserData)}; - {_, #{advisory_indexes := NFadvisory_indexes}} -> S4#{advisory_indexes => NFadvisory_indexes}; - {#{advisory_indexes := PFadvisory_indexes}, _} -> S4#{advisory_indexes => PFadvisory_indexes}; - {_, _} -> S4 + {#{published_at := PFpublished_at}, #{published_at := NFpublished_at}} -> S5#{published_at => merge_msg_Timestamp(PFpublished_at, NFpublished_at, TrUserData)}; + {_, #{published_at := NFpublished_at}} -> S5#{published_at => NFpublished_at}; + {#{published_at := PFpublished_at}, _} -> S5#{published_at => PFpublished_at}; + {_, _} -> S5 end. -compile({nowarn_unused_function,merge_msg_RetirementStatus/3}). @@ -1035,6 +1144,9 @@ merge_msg_Dependency(#{} = PMsg, #{package := NFpackage, requirement := NFrequir _ -> S3 end. +-compile({nowarn_unused_function,merge_msg_Timestamp/3}). +merge_msg_Timestamp(#{}, #{seconds := NFseconds, nanos := NFnanos}, _) -> #{seconds => NFseconds, nanos => NFnanos}. + verify_msg(Msg, MsgName) when is_atom(MsgName) -> verify_msg(Msg, MsgName, []). @@ -1046,12 +1158,12 @@ verify_msg(Msg, MsgName, Opts) -> 'RetirementStatus' -> v_msg_RetirementStatus(Msg, [MsgName], TrUserData); 'SecurityAdvisory' -> v_msg_SecurityAdvisory(Msg, [MsgName], TrUserData); 'Dependency' -> v_msg_Dependency(Msg, [MsgName], TrUserData); + 'Timestamp' -> v_msg_Timestamp(Msg, [MsgName], TrUserData); _ -> mk_type_error(not_a_known_message, Msg, []) end. -compile({nowarn_unused_function,v_msg_Package/3}). --dialyzer({nowarn_function,v_msg_Package/3}). v_msg_Package(#{name := F2, repository := F3} = M, Path, TrUserData) -> case M of #{releases := F1} -> @@ -1085,11 +1197,9 @@ v_msg_Package(M, Path, _TrUserData) when is_map(M) -> mk_type_error({missing_fie v_msg_Package(X, Path, _TrUserData) -> mk_type_error({expected_msg, 'Package'}, X, Path). -compile({nowarn_unused_function,v_submsg_Release/3}). --dialyzer({nowarn_function,v_submsg_Release/3}). v_submsg_Release(Msg, Path, TrUserData) -> v_msg_Release(Msg, Path, TrUserData). -compile({nowarn_unused_function,v_msg_Release/3}). --dialyzer({nowarn_function,v_msg_Release/3}). v_msg_Release(#{version := F1, inner_checksum := F2} = M, Path, TrUserData) -> v_type_string(F1, [version | Path], TrUserData), v_type_bytes(F2, [inner_checksum | Path], TrUserData), @@ -1119,12 +1229,17 @@ v_msg_Release(#{version := F1, inner_checksum := F2} = M, Path, TrUserData) -> end; _ -> ok end, + case M of + #{published_at := F7} -> v_submsg_Timestamp(F7, [published_at | Path], TrUserData); + _ -> ok + end, lists:foreach(fun (version) -> ok; (inner_checksum) -> ok; (dependencies) -> ok; (retired) -> ok; (outer_checksum) -> ok; (advisory_indexes) -> ok; + (published_at) -> ok; (OtherKey) -> mk_type_error({extraneous_key, OtherKey}, M, Path) end, maps:keys(M)), @@ -1133,11 +1248,9 @@ v_msg_Release(M, Path, _TrUserData) when is_map(M) -> mk_type_error({missing_fie v_msg_Release(X, Path, _TrUserData) -> mk_type_error({expected_msg, 'Release'}, X, Path). -compile({nowarn_unused_function,v_submsg_RetirementStatus/3}). --dialyzer({nowarn_function,v_submsg_RetirementStatus/3}). v_submsg_RetirementStatus(Msg, Path, TrUserData) -> v_msg_RetirementStatus(Msg, Path, TrUserData). -compile({nowarn_unused_function,v_msg_RetirementStatus/3}). --dialyzer({nowarn_function,v_msg_RetirementStatus/3}). v_msg_RetirementStatus(#{reason := F1} = M, Path, TrUserData) -> v_enum_RetirementReason(F1, [reason | Path], TrUserData), case M of @@ -1154,11 +1267,9 @@ v_msg_RetirementStatus(M, Path, _TrUserData) when is_map(M) -> mk_type_error({mi v_msg_RetirementStatus(X, Path, _TrUserData) -> mk_type_error({expected_msg, 'RetirementStatus'}, X, Path). -compile({nowarn_unused_function,v_submsg_SecurityAdvisory/3}). --dialyzer({nowarn_function,v_submsg_SecurityAdvisory/3}). v_submsg_SecurityAdvisory(Msg, Path, TrUserData) -> v_msg_SecurityAdvisory(Msg, Path, TrUserData). -compile({nowarn_unused_function,v_msg_SecurityAdvisory/3}). --dialyzer({nowarn_function,v_msg_SecurityAdvisory/3}). v_msg_SecurityAdvisory(#{id := F1, summary := F2, html_url := F3, api_url := F6} = M, Path, TrUserData) -> v_type_string(F1, [id | Path], TrUserData), v_type_string(F2, [summary | Path], TrUserData), @@ -1186,11 +1297,9 @@ v_msg_SecurityAdvisory(M, Path, _TrUserData) when is_map(M) -> mk_type_error({mi v_msg_SecurityAdvisory(X, Path, _TrUserData) -> mk_type_error({expected_msg, 'SecurityAdvisory'}, X, Path). -compile({nowarn_unused_function,v_submsg_Dependency/3}). --dialyzer({nowarn_function,v_submsg_Dependency/3}). v_submsg_Dependency(Msg, Path, TrUserData) -> v_msg_Dependency(Msg, Path, TrUserData). -compile({nowarn_unused_function,v_msg_Dependency/3}). --dialyzer({nowarn_function,v_msg_Dependency/3}). v_msg_Dependency(#{package := F1, requirement := F2} = M, Path, TrUserData) -> v_type_string(F1, [package | Path], TrUserData), v_type_string(F2, [requirement | Path], TrUserData), @@ -1218,8 +1327,23 @@ v_msg_Dependency(#{package := F1, requirement := F2} = M, Path, TrUserData) -> v_msg_Dependency(M, Path, _TrUserData) when is_map(M) -> mk_type_error({missing_fields, [package, requirement] -- maps:keys(M), 'Dependency'}, M, Path); v_msg_Dependency(X, Path, _TrUserData) -> mk_type_error({expected_msg, 'Dependency'}, X, Path). +-compile({nowarn_unused_function,v_submsg_Timestamp/3}). +v_submsg_Timestamp(Msg, Path, TrUserData) -> v_msg_Timestamp(Msg, Path, TrUserData). + +-compile({nowarn_unused_function,v_msg_Timestamp/3}). +v_msg_Timestamp(#{seconds := F1, nanos := F2} = M, Path, TrUserData) -> + v_type_int64(F1, [seconds | Path], TrUserData), + v_type_int32(F2, [nanos | Path], TrUserData), + lists:foreach(fun (seconds) -> ok; + (nanos) -> ok; + (OtherKey) -> mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_Timestamp(M, Path, _TrUserData) when is_map(M) -> mk_type_error({missing_fields, [seconds, nanos] -- maps:keys(M), 'Timestamp'}, M, Path); +v_msg_Timestamp(X, Path, _TrUserData) -> mk_type_error({expected_msg, 'Timestamp'}, X, Path). + -compile({nowarn_unused_function,v_enum_RetirementReason/3}). --dialyzer({nowarn_function,v_enum_RetirementReason/3}). v_enum_RetirementReason('RETIRED_OTHER', _Path, _TrUserData) -> ok; v_enum_RetirementReason('RETIRED_INVALID', _Path, _TrUserData) -> ok; v_enum_RetirementReason('RETIRED_SECURITY', _Path, _TrUserData) -> ok; @@ -1229,7 +1353,6 @@ v_enum_RetirementReason(V, _Path, _TrUserData) when -2147483648 =< V, V =< 21474 v_enum_RetirementReason(X, Path, _TrUserData) -> mk_type_error({invalid_enum, 'RetirementReason'}, X, Path). -compile({nowarn_unused_function,v_enum_AdvisorySeverity/3}). --dialyzer({nowarn_function,v_enum_AdvisorySeverity/3}). v_enum_AdvisorySeverity('SEVERITY_NONE', _Path, _TrUserData) -> ok; v_enum_AdvisorySeverity('SEVERITY_LOW', _Path, _TrUserData) -> ok; v_enum_AdvisorySeverity('SEVERITY_MEDIUM', _Path, _TrUserData) -> ok; @@ -1238,14 +1361,22 @@ v_enum_AdvisorySeverity('SEVERITY_CRITICAL', _Path, _TrUserData) -> ok; v_enum_AdvisorySeverity(V, _Path, _TrUserData) when -2147483648 =< V, V =< 2147483647, is_integer(V) -> ok; v_enum_AdvisorySeverity(X, Path, _TrUserData) -> mk_type_error({invalid_enum, 'AdvisorySeverity'}, X, Path). +-compile({nowarn_unused_function,v_type_int32/3}). +v_type_int32(N, _Path, _TrUserData) when is_integer(N), -2147483648 =< N, N =< 2147483647 -> ok; +v_type_int32(N, Path, _TrUserData) when is_integer(N) -> mk_type_error({value_out_of_range, int32, signed, 32}, N, Path); +v_type_int32(X, Path, _TrUserData) -> mk_type_error({bad_integer, int32, signed, 32}, X, Path). + +-compile({nowarn_unused_function,v_type_int64/3}). +v_type_int64(N, _Path, _TrUserData) when is_integer(N), -9223372036854775808 =< N, N =< 9223372036854775807 -> ok; +v_type_int64(N, Path, _TrUserData) when is_integer(N) -> mk_type_error({value_out_of_range, int64, signed, 64}, N, Path); +v_type_int64(X, Path, _TrUserData) -> mk_type_error({bad_integer, int64, signed, 64}, X, Path). + -compile({nowarn_unused_function,v_type_uint32/3}). --dialyzer({nowarn_function,v_type_uint32/3}). v_type_uint32(N, _Path, _TrUserData) when is_integer(N), 0 =< N, N =< 4294967295 -> ok; v_type_uint32(N, Path, _TrUserData) when is_integer(N) -> mk_type_error({value_out_of_range, uint32, unsigned, 32}, N, Path); v_type_uint32(X, Path, _TrUserData) -> mk_type_error({bad_integer, uint32, unsigned, 32}, X, Path). -compile({nowarn_unused_function,v_type_bool/3}). --dialyzer({nowarn_function,v_type_bool/3}). v_type_bool(false, _Path, _TrUserData) -> ok; v_type_bool(true, _Path, _TrUserData) -> ok; v_type_bool(0, _Path, _TrUserData) -> ok; @@ -1253,7 +1384,6 @@ v_type_bool(1, _Path, _TrUserData) -> ok; v_type_bool(X, Path, _TrUserData) -> mk_type_error(bad_boolean_value, X, Path). -compile({nowarn_unused_function,v_type_float/3}). --dialyzer({nowarn_function,v_type_float/3}). v_type_float(N, _Path, _TrUserData) when is_float(N) -> ok; v_type_float(N, _Path, _TrUserData) when is_integer(N) -> ok; v_type_float(infinity, _Path, _TrUserData) -> ok; @@ -1262,7 +1392,6 @@ v_type_float(nan, _Path, _TrUserData) -> ok; v_type_float(X, Path, _TrUserData) -> mk_type_error(bad_float_value, X, Path). -compile({nowarn_unused_function,v_type_string/3}). --dialyzer({nowarn_function,v_type_string/3}). v_type_string(S, Path, _TrUserData) when is_list(S); is_binary(S) -> try unicode:characters_to_binary(S) of B when is_binary(B) -> ok; @@ -1273,7 +1402,6 @@ v_type_string(S, Path, _TrUserData) when is_list(S); is_binary(S) -> v_type_string(X, Path, _TrUserData) -> mk_type_error(bad_unicode_string, X, Path). -compile({nowarn_unused_function,v_type_bytes/3}). --dialyzer({nowarn_function,v_type_bytes/3}). v_type_bytes(B, _Path, _TrUserData) when is_binary(B) -> ok; v_type_bytes(B, _Path, _TrUserData) when is_list(B) -> ok; v_type_bytes(X, Path, _TrUserData) -> mk_type_error(bad_binary_value, X, Path). @@ -1286,9 +1414,8 @@ mk_type_error(Error, ValueSeen, Path) -> -compile({nowarn_unused_function,prettify_path/1}). --dialyzer({nowarn_function,prettify_path/1}). prettify_path([]) -> top_level; -prettify_path(PathR) -> lists:append(lists:join(".", lists:map(fun atom_to_list/1, lists:reverse(PathR)))). +prettify_path(PathR) -> string:join(lists:map(fun atom_to_list/1, lists:reverse(PathR)), "."). -compile({nowarn_unused_function,id/2}). @@ -1329,7 +1456,8 @@ get_msg_defs() -> #{name => dependencies, fnum => 3, rnum => 4, type => {msg, 'Dependency'}, occurrence => repeated, opts => []}, #{name => retired, fnum => 4, rnum => 5, type => {msg, 'RetirementStatus'}, occurrence => optional, opts => []}, #{name => outer_checksum, fnum => 5, rnum => 6, type => bytes, occurrence => optional, opts => []}, - #{name => advisory_indexes, fnum => 6, rnum => 7, type => uint32, occurrence => repeated, opts => []}]}, + #{name => advisory_indexes, fnum => 6, rnum => 7, type => uint32, occurrence => repeated, opts => []}, + #{name => published_at, fnum => 7, rnum => 8, type => {msg, 'Timestamp'}, occurrence => optional, opts => []}]}, {{msg, 'RetirementStatus'}, [#{name => reason, fnum => 1, rnum => 2, type => {enum, 'RetirementReason'}, occurrence => required, opts => []}, #{name => message, fnum => 2, rnum => 3, type => string, occurrence => optional, opts => []}]}, {{msg, 'SecurityAdvisory'}, [#{name => id, fnum => 1, rnum => 2, type => string, occurrence => required, opts => []}, @@ -1343,16 +1471,17 @@ get_msg_defs() -> #{name => requirement, fnum => 2, rnum => 3, type => string, occurrence => required, opts => []}, #{name => optional, fnum => 3, rnum => 4, type => bool, occurrence => optional, opts => []}, #{name => app, fnum => 4, rnum => 5, type => string, occurrence => optional, opts => []}, - #{name => repository, fnum => 5, rnum => 6, type => string, occurrence => optional, opts => []}]}]. + #{name => repository, fnum => 5, rnum => 6, type => string, occurrence => optional, opts => []}]}, + {{msg, 'Timestamp'}, [#{name => seconds, fnum => 1, rnum => 2, type => int64, occurrence => required, opts => []}, #{name => nanos, fnum => 2, rnum => 3, type => int32, occurrence => required, opts => []}]}]. -get_msg_names() -> ['Package', 'Release', 'RetirementStatus', 'SecurityAdvisory', 'Dependency']. +get_msg_names() -> ['Package', 'Release', 'RetirementStatus', 'SecurityAdvisory', 'Dependency', 'Timestamp']. get_group_names() -> []. -get_msg_or_group_names() -> ['Package', 'Release', 'RetirementStatus', 'SecurityAdvisory', 'Dependency']. +get_msg_or_group_names() -> ['Package', 'Release', 'RetirementStatus', 'SecurityAdvisory', 'Dependency', 'Timestamp']. get_enum_names() -> ['RetirementReason', 'AdvisorySeverity']. @@ -1383,7 +1512,8 @@ find_msg_def('Release') -> #{name => dependencies, fnum => 3, rnum => 4, type => {msg, 'Dependency'}, occurrence => repeated, opts => []}, #{name => retired, fnum => 4, rnum => 5, type => {msg, 'RetirementStatus'}, occurrence => optional, opts => []}, #{name => outer_checksum, fnum => 5, rnum => 6, type => bytes, occurrence => optional, opts => []}, - #{name => advisory_indexes, fnum => 6, rnum => 7, type => uint32, occurrence => repeated, opts => []}]; + #{name => advisory_indexes, fnum => 6, rnum => 7, type => uint32, occurrence => repeated, opts => []}, + #{name => published_at, fnum => 7, rnum => 8, type => {msg, 'Timestamp'}, occurrence => optional, opts => []}]; find_msg_def('RetirementStatus') -> [#{name => reason, fnum => 1, rnum => 2, type => {enum, 'RetirementReason'}, occurrence => required, opts => []}, #{name => message, fnum => 2, rnum => 3, type => string, occurrence => optional, opts => []}]; find_msg_def('SecurityAdvisory') -> [#{name => id, fnum => 1, rnum => 2, type => string, occurrence => required, opts => []}, @@ -1398,6 +1528,7 @@ find_msg_def('Dependency') -> #{name => optional, fnum => 3, rnum => 4, type => bool, occurrence => optional, opts => []}, #{name => app, fnum => 4, rnum => 5, type => string, occurrence => optional, opts => []}, #{name => repository, fnum => 5, rnum => 6, type => string, occurrence => optional, opts => []}]; +find_msg_def('Timestamp') -> [#{name => seconds, fnum => 1, rnum => 2, type => int64, occurrence => required, opts => []}, #{name => nanos, fnum => 2, rnum => 3, type => int32, occurrence => required, opts => []}]; find_msg_def(_) -> error. @@ -1489,6 +1620,7 @@ fqbin_to_msg_name(<<"Release">>) -> 'Release'; fqbin_to_msg_name(<<"RetirementStatus">>) -> 'RetirementStatus'; fqbin_to_msg_name(<<"SecurityAdvisory">>) -> 'SecurityAdvisory'; fqbin_to_msg_name(<<"Dependency">>) -> 'Dependency'; +fqbin_to_msg_name(<<"Timestamp">>) -> 'Timestamp'; fqbin_to_msg_name(E) -> error({gpb_error, {badmsg, E}}). @@ -1497,6 +1629,7 @@ msg_name_to_fqbin('Release') -> <<"Release">>; msg_name_to_fqbin('RetirementStatus') -> <<"RetirementStatus">>; msg_name_to_fqbin('SecurityAdvisory') -> <<"SecurityAdvisory">>; msg_name_to_fqbin('Dependency') -> <<"Dependency">>; +msg_name_to_fqbin('Timestamp') -> <<"Timestamp">>; msg_name_to_fqbin(E) -> error({gpb_error, {badmsg, E}}). @@ -1537,7 +1670,7 @@ get_all_source_basenames() -> ["mix_hex_pb_package.proto"]. get_all_proto_names() -> ["mix_hex_pb_package"]. -get_msg_containment("mix_hex_pb_package") -> ['Dependency', 'Package', 'Release', 'RetirementStatus', 'SecurityAdvisory']; +get_msg_containment("mix_hex_pb_package") -> ['Dependency', 'Package', 'Release', 'RetirementStatus', 'SecurityAdvisory', 'Timestamp']; get_msg_containment(P) -> error({gpb_error, {badproto, P}}). @@ -1557,6 +1690,7 @@ get_enum_containment("mix_hex_pb_package") -> ['AdvisorySeverity', 'RetirementRe get_enum_containment(P) -> error({gpb_error, {badproto, P}}). +get_proto_by_msg_name_as_fqbin(<<"Timestamp">>) -> "mix_hex_pb_package"; get_proto_by_msg_name_as_fqbin(<<"RetirementStatus">>) -> "mix_hex_pb_package"; get_proto_by_msg_name_as_fqbin(<<"Release">>) -> "mix_hex_pb_package"; get_proto_by_msg_name_as_fqbin(<<"Package">>) -> "mix_hex_pb_package"; diff --git a/src/mix_hex_pb_signed.erl b/src/mix_hex_pb_signed.erl index d39d05af..8cac998f 100644 --- a/src/mix_hex_pb_signed.erl +++ b/src/mix_hex_pb_signed.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% -*- coding: utf-8 -*- %% % this file is @generated diff --git a/src/mix_hex_pb_versions.erl b/src/mix_hex_pb_versions.erl index d9e0d2fe..77019f5b 100644 --- a/src/mix_hex_pb_versions.erl +++ b/src/mix_hex_pb_versions.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% -*- coding: utf-8 -*- %% % this file is @generated diff --git a/src/mix_hex_registry.erl b/src/mix_hex_registry.erl index 54c03f1d..b0ce74b5 100644 --- a/src/mix_hex_registry.erl +++ b/src/mix_hex_registry.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Functions for encoding and decoding Hex registries. diff --git a/src/mix_hex_repo.erl b/src/mix_hex_repo.erl index 0ecfd2e1..221764da 100644 --- a/src/mix_hex_repo.erl +++ b/src/mix_hex_repo.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Repo API. diff --git a/src/mix_hex_safe_binary_to_term.erl b/src/mix_hex_safe_binary_to_term.erl index fb2aca38..bea8ca08 100644 --- a/src/mix_hex_safe_binary_to_term.erl +++ b/src/mix_hex_safe_binary_to_term.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @hidden %% Safe deserialization of Erlang terms from binary. diff --git a/src/mix_hex_tarball.erl b/src/mix_hex_tarball.erl index 89f99a52..dda16942 100644 --- a/src/mix_hex_tarball.erl +++ b/src/mix_hex_tarball.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %% @doc %% Functions for creating and unpacking Hex tarballs. diff --git a/src/mix_safe_erl_term.xrl b/src/mix_safe_erl_term.xrl index 768e9fc5..05bcdb83 100644 --- a/src/mix_safe_erl_term.xrl +++ b/src/mix_safe_erl_term.xrl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.16.0 (0e332e5), do not edit manually +%% Vendored from hex_core v0.16.1 (f0f6762), do not edit manually %%% Author : Robert Virding %%% Purpose : Token definitions for Erlang. diff --git a/test/fixtures/registries/20200917.ets b/test/fixtures/registries/20200917.ets index cec6171763b8b511351294f1ff8e58f002a7c563..d68b062a48d43ea2d94d7e9a7dc00b24615c7746 100644 GIT binary patch delta 23787 zcmZ8pcU)D+*3FzlxpYvffP$!WxLhtB1$$TQ8Vg{Fy`|eV(KL;sW7zhfF>36Jiir)| zGnOctZY63oc8o@2OyalpoO{uH|GXd1z&U4Tc3FGvwWsv7$FT|zccJMH1A4Z1d+8-n zJlDORs~$M|_{Nt+pSN4g&0Hu3&He5)dC$|5MY3ETpF{tCsl`y22yuoSf<=n#<{2b& z1EQ!rQPiZWG|hp=`0CN}+XJz*`6tau-ib+*{$9c4wM)2D?f?-@lXq!u^ns5SLDSuI zvkac^stE^58ZF`|V83>WCY;s6siL=Vp^9@ z<(|{LDK1gOlCLI`sn;}-O+R^KSE`N(3(XvezjAQX>dAuPPI6>mARQVZ63LvajgYzT z$5YoDB3AB8ccic!EtxLz_RMs_6XMruCMsMh+P-iCt$jsjIp$5zVv{h*C7)th-)L4k zo23WQ?S0tydwn$@T3Vnv%dpTOD)G~@<))|cR4-2?$|0e{X@|dVri3W%zE~}NnnlZ> zqG#}7;}1(k4LPK?u`_ydLbB|1t~n)i<4?E!_nNY-b|5Umox-N!Ll14!qRBcyWXeAr zqsz>rg-K9FzLuaTB_z==ef4blYv)+CI10|uT;#yQR9d6MN?u>1Ws+{uc2VnHT9n#6 znPQHmZ7sDln&G9#(m)?QO_sEFl$*i=WclDY3cDvn3{oC5-P-By7Hk=10rcU}LP2TAW-tvLS8%OhnCl;z31?@hunQbSGIS z6NYZqQsw-gqGi|Hnes`!AS!Aq%qDAs`7M3a&=IoZ0!wN0e|HI)y(o&t_-J0#uDRwy zXRd1z()U;+%Giz#IpnLw)1`y3A(sMdL7%1A*lmYIZ8>K~0DXN{w4$HeiEw#utFN4s zZ=xH0b*s!hm`XX3S{?cDmmHk!X|GUTv@dx#&>U-e_V3ZI@w-E`x18S`^y+IvM~^CW zlvUvg^vggkhn7adH#Tn-@wERFyqv9%%pb$ox=a$e@==Rex$>)bD67AgN&|itfwF99 zoq3=5$Xw^A)X%CX${u%v#oLC@<(zS(eV=K)sClezrOLL#0oISBZzOLWjkfQj7v&Zp zTFmZTS9V%pE?xiGF2PTapl3HVU)e7;P?n!cqoGgOdwRjOHaB*M7rN1=#PoEpIn zmqFgQa83vqvggh&94jdIC>;Bke}rM%m8rp0u|rtokhDO<$Rm1+`*K2VHch&(xsul^ zYczj=BE><1(gYbl*nyK*;Enen2dCIKOF;tkKSIJo`5~$=9_G521-9^S;ccV`m zYf1c=cyxE~vPkNRvfQE;2g9 zy*N?C$&IaCsOpZWtB#iDoX`U0%7m6|uPMbdvDxLjHFtTeVK7yW#t&l(qvRz-K|Nnj zqSpJx7+TOqKdT`usCQg()&c}I41)i7zaw(UVF*mMz)!fyq7-L&sZk*1w$QU_^#y+^8yFuScDG!v!jRg}Jza z?kLZ#iu%t>oMdQ|Kr)Yq`)zL;jGai4mb%L-KLKjNC<0*boi2} zF#M$IDUM#>p+(A)_GYRW0#8bOtc7s6@S?#z?C!MLLvzrQhS73=5j*dR58XM72j4w~ zBT|7lr%SL3qZKm|M!k+{DSX=WnkOv$^5fxbrg|zCRB~RVi4@wO1LHBA!j(S$U5l0j zdt2pAZKBLFS>$3(8H|;NU&6}2Jz%B-M@6b`NieIFuC2#<9YWyt`!0(p?WHW0N+~}Z z`~RIoAg#>Tt@!06`OB1KWGk(akr(J!FEIn2;6^8hAO#$i;oN=?L4;iyh`gHM zt&OD7xq23-V@KM21J1f~US z@BrLl-B{$7pqWv!{6~KZiPWQ2g63ThamR+vzaGPx)>F3a9YkL(1i)$YhBkpR7ix8A zv!8H7sxwokNYQZKXC~?XL4;h{uPcq3BhqP3BCJOp(Ko~anX}U`9k89q-dWUoxIHr_ z7HKu*(1JjkxB)n)R|)oIPA}b^O5#L_+-w5Y$wk6AY=UvTRt#(M?v3p1R;Uto8dVL3 zbM3)jR-7z1iD4IVpmHh~@$|CozXiR=Rb&i8p8Gou-sd&4AkXv^o z(9S-3Gns`%KU=_Mc1}cCNRJVXWnSZ0S#=&!xi|=xG_{FvlxqeD$;!+1IAO|^A;G*S zaa8w!_O>R@8d1H@K+m$kD`Kla%2r?CLSxPdS9xxyvfm_%3fF>Y{HuZ=;w(#s22kZm z1Qk3{B*{)~a%I>Eg*_}r-fnUWxu!awx%Sap$*Z}sG&NK-rc3qoOc^{X$OwA5Z*-CM z3e#xCYT--wj)+j?TRC-4GSysz%nh${q+9p2Kw9JiyUSaKxcoN!L=LgtO3gw2Kpve(M6Mh-N)3nKoJ6;UeOO*iZC@HeS&9m2}FC0ZMW zYlg<*4_-*DuChmVv@HGbnc;E|&#R+SP_+S7?$pxcz^aCHsV4pk&?iP_Hp}6FL$jM| z6N?jccSOo~a`_8Ei+?!r03W6T!kP_YBq9dV3O2H4i$Lo@%ZtVDgJUyQmo9I=|OV`;}_<~i3~Y<6<&_a*{>baqly6E+(p=0 zl7_Iwz>uOgAxZeZ4FhM0Drdddf{JoAZ~6Xf(RB5QXe1x@FyLk8K`d(Jw^rG`eprsgeC2hFo?^(q zp%x`e|LITiiO8f2{){*lM1=Pk;MI0K9N+qt1)*J^i1GhSWP1N2Nf9@6uq( zT_@7zu_Z5-=2CvZlczRhVkJ zESNg@A$oEdCLb>ivJ{ zc5WFBKZ|&*rO?0-WVJ()DOAu}H__w<0Ia^pvgv>QRV1*|`~KLAhvN{O`hQl7Dtc)N z^3>;{?7NAw>Yj-Ui!!e!Ix36of%tOoSs_&AuGN+DZwnnw7WZV~rc~snv$va5UM50j zqoP#Vu}2V77q+r&&sLOqU2pR`0%WhAL6pA{hq$$St)SrQ4g$+BehW&rEp=CNj=QS6F8uH3Vq0zOM_*@s0O_oaFG( z!2g%YC@&jFz4bBxf)dz1vRXFnoQ8ixsLhtSkq+du6`t^pk9d_fgdiwyj@P~9#r=Vl;i<=~ zdhdP>^ zBSGw35j3Wc2%w-RNMW#-2nGN(sd!JWsbg{4A5Weh4>xLs-`}VQIpM5E-6D(my zK6tl^)_Q$ao2#M0Un*ec9AMV_6i*a8c^{FQc(!Um!6E`Pa!K|O{(LaeG6m5A_O z=Z3J7@YCOAj14nNE8CE#HxSg1b1WRekA{rK0Vzj__z^z=Jb=gm_77br3?_sAECWR5 zeDm{QBv6NL2G9$8+f?e~lI);Z)lG+hPM+1!O`@lP52u9Jr2GirL*F&<0=#By;rqzRd6cc`(q;eB)bg$J7=gtM&tzvYmz7CMsUEV z<2y7L`m#QpWz86zxT4w2tAWmLO#wtm3gv<~m_B-qzXF(d!Z>*7{Og^VHdT1oA|n^X zP}5;rmV9#&$AeT_QV654lI_$l^}u#>0U$r!?Z&{0lba`nm18-JCLqU}3#Lhx1gcG-*L%Iz-vpOdKFPT0(1D-c+ZJGIFDZ$R2#7mG(&Qj}53F8*T|C{W1t=#3q5k${lssJ5FhwVKU~jf~Pc%v*?+~pIP0vF7&dYX|YwmG?8lozr zg8Pvw!UR;JG``}f4`3_C@|wVbVULdF&5(J>+c-@oz8SQ6)PYG%jRjGPdiR4IZ5-3> znbh|LY~CTxz~8d{;OOcnDFzT_!dCEju0D2v61)l?xf*10FL)S;DrPTYIcYgDoHDb& zlUd>wd;rw)2JnYkT*~TT`Ya0s@!i#iVLhG7?mPsikhVsZbH?YRl5?YFZ}KVh5`z^e zqRVX&$STVLidF6ghiD*UXoczGii`P{iecxH?>soM|SwmM2&=2w^aV!MleZ9Rh{wLetmHmaKbYb<%#DP zI0#SrvWDQzNRhc)9O%P1F3K2rFnqwiFmW(D5>Fob7m)TN#uX#+vD~bX^*3eHls!OJ zro1(ZLa0%o`zvahE>A=S>~#Q+b zxj)^;{qJ@M6|YEAFg`J|W}jrl-NGw5^DJ(Z>4FU`++@XWSEstvca;~W(_MFd-)NX| zt@but)p8e38o2)+9j*%i%6tYze6GTDJcyiB^eoKe9jE!pB1aRI)xbU>*;#05tk#y6 z%|qQ&{t@~opY=;;PLpm}^Z+J{EtK<2+@qmifC?Vd-huqRu$iZ7$I8~fHI@gnqGUxQ zv<6mk9LA1OP$ZxL>X~?&`V3fXUQ`g1HwxASP_w~Ut}^MjP(n+6vq4nIH^$nkvAlDz zj&B+P9woQH(VNd`zlj0l6N6P;djbrHd?zEu)mI=1Sl{R)(Ei@q5CkBLJa{e6_``&^ z#Bb`8DJ%+W`KpFcwU3qZuHgl<)&u=Mn3W*QC+?wu2z1Xtl~Pq8;LsQG9Hso}d?bG5 zAU1UW05Jh%lSP*MJCMl&|5R{x-zTuOy?eB}WtP7{ajeA1@@?2p$2Dp-a` zB`78iID%_Mh0Qm|pa>dx2)~8uHx0{O9^aU5O$A=&;>Tw>&W0%x`S(vjil)#rz*X)O zsOfUwca&{@9&TgJCU#MOu(m%omW#G$XF0xu#2EuiShN7o_noIDQtca9!8&J{dyjgk z>T_=jkRt07M>`ZOecJG7fxRBSZyeM5w6EOpdZNV&r+;<#|^@F-f}0Z8;H+BMVDgK`6Xs_ zn(X+jpNDRe?e0DQkUJdExD?jS30AcORFx#am!SW`Oa{8Au5x3?2(i*eqxh;~;BL5h zQ+Dc;O}{R}@*N5R1gu^#B5Rcx$jOgxT5-A_7NpzY7*R*gf2Fa}9pi8%4+a~|4Z7kc zDtAMq;sVuiC?0TVCdila`c70(ChC?IbP~OUoaAkhUcWoio4<*1M%qwa(tj*C&Ou*V z5NW4G`q9OIaj4~BHeTr2A>}t-0*o>F&Fa>LK`Hpkib&O4aAkW*kcUdL>CzQ&xR)>1 z18bB)zs=L~WaUyXn%#+gH47o2zzWYSnhYBTk&;D=NwlH1AE8J2@c=jZ){}5KW|mPC zP;e5;EBAxUhM=}yf$}%{%-fvG)8&~&R2w_cm*ObN21l>rByZRW!>w(bjjCF<@(6iEk+lFf?9}LG^VXqpJAB0oYTdX z+?w+Wm>>#U3=o2rs7dbsJ&GwfF1qhzzzn{t4K%Rp2+|h=FZyVQ<|Y@F0YL=7?^Ra+ z1Q~>yW$GC+HH8_dSB+p@l7~&X5rQ``AjjFY1~Mys^CX)#_jHV0iYLW2ZK2bnq$ zXoJbK9~=!YOf~ps{=yfIj`9!Sp?-^z6f51~@`BT<4JS#L^)a+!AZWLLQb2v6AIPMS z7Z^qp1@;EoDjKQz$PJ~a3IqJ;>ozb&xJx4Ujgki7&0w6}Xv!G4i9MuvV7*1Pkk1<4 z(VEDDtqix#v~VWQPr;u(f7a5Fx%T_SOMl-7pnzfUv&vw-W6lDMuzP|78T|y|e^vmJ zFh`r#=kUSX2J1uFJu+n7Dd;X0TIGoWCOY{s@&lMFdLs+G*288N`gswm>{%=7Dv&$X z1tllwJ$Zk66YfOO6M+?=KbJ_O8tY?aCVDxQ8lH|6o=WS#;EiEOlGp_e5662AW#q*; z@H1w*=C9wUJAUZUb5jK7`x>iY%HP8-x zX9)5!2zl<#%Q5||Ty{jrOVevoJx822ClWikIcYTrB@SBbMs%s4UQett^4O;1PSWSW zd-TS4+7O!Vr^gXW%!X(nyD=6Dmp!iY|FJMf=SgQy!#edLqFr@87DE&Kk--st>Bdl; zLFHu&y>!H3M)TbUryQopvm)TMSgY10)2B0){Xp@M2W+Zs_d$o z1WE+|fB>IKOVII<^H(EXY{Exg?E^<>`zH|K>?qyMsB?0nbrW?B!}3vugUIZ74&ZdP z7C@^`;Njq=2GFacL^p1go8^a~df?I-6mn5>F$5}J4o=i-3_`JWRJ?3Ce1#Nmn`u#N z@rV-h@Bkz;CgfeDPwl$o`!nwp2W4wQ`tw>w$#xcR!f!KQNBOV9D(qB=O-AJPDt4%0 zavf>j;0CFLl}lBlF)7C+n&|Ua#W3nP1dfxE3&!lZmgm*ZIKAa(`cSu)h%O^9!0zph z4Jv61Z}}!qixR~Uwlttmi}9&<`;1ULkSq!Fq~AjDR?ZkIx(^9bGLsC-3(VTh=tZHx3O|9QSU5X!25cD0)`eEPJ6}{9s?tH5Fzm z3>Q5V1Tdx8l_gbEUsn47m4Jzc=i$v@-}gu3!x9Pk$Acmi41~X+wnA}K_Mz)#WpT+Xzg^=rm`7UtXlfZmcc3>gvXM& z2~-0P$HG912DJouK@w}iIS$w+Rt2ku7n;bEgOcRrNCuW^v|t#{7u=rFs+6k_#v`$2 z$@5>EFUR^k|s)7nv z*t`~fjCgw++KaND0h!tfbJ|=8@Ei6a*2)x`^3={AT0clB9OUKqIG!j z>?r!>9m>#)au>jk+4B0ks97NxA-^+NCnB6&Tk1xOoWTAmEzyJI6m|4N79f6B?{UFz z*yB12M02BDs~f`LI7c4c2x_|!(Zcy~Ebke4^hVI<#uq({)-O=juMn#sf%(X9%1mVMD@G*VS|qni(nWNjDgfM(77 z)K=%xADP%{L%iZ6YiGF0r^%`ya-pf8iV(U{4h%S;$t47-x~i_A{@pd+a4St?xlQ?x zIYuyo@WDiV=HPhsDg;W`5*(U6o%G<+i~jdD$WoSeRMQZP!5x)+iakdsEZyKqSdn1# zE|i|nSMfw4g}~+uYpM2U40Wpodr|U|C4=B@@PM@9>)7tszQM~?{GZbSM_bV}MV@rx z^(0dIGrMBtW;5)?bk^bwNx_S&6$1Zm60PJb`dJ$0jTk;Ndw{U@w5NU!Fw5+w($)mx zq+V?dM3L7QJmlsN@J74lvVqY`yW3HZlJ)PJX?AzS9#nDhNQquE(Yg9p3y~|gB1=MQ zlZ5OAj2lO2A$rrL?k{GE%D5P#+U}N*YWsW-JxhLlIGRHQY5fp6?B6U$jc8>@c>V|` zc4m*wQOX#m3>#AJ9uZRtjj3QN0!SiCI7sp_K*Rfe#4EHjoW&lk-~42^p3eZbzamh{ zIUk?2ZI8KZDEh>L_B98Q^G|t%v>bk!8dqxOvWx4rEMZsLmW}vcGcjA_O;=p7t*Y!v z_yHfwq!}&T4r^2TmJT~bj1o}Yb9BPzpe*$*t|7XrPAq-A7U^_oI1FI>4)F^6ree-V zgZiThj)*CW^$fet)H9sU(NT_qNCId8g$^y9qBSP(83-Wt$}DtiGU^PY)OK`KAwd@Q zPLTs2Po}^KqZ2@rc4^L(e*zCjZ-e`HzA`_Z-O?%>cZ;RdeYMzmA2?8c8|^SayE}Oo zVm%wCBj=9&ON#)YQF4l$dfxQ=InXp*&Cs=E{cYCy!~s#worX>nx)6@^_$vVTp-L}_ zQWOQ5v)+hRDCx9z7u>|Ay$e-x1=-;1DA~MyJiz!g7tsQrWRhmb2pg>-7k1n9+2%YU zJ1jQzz;TDM-Ksoky9T9lV_MK3o5MU&!e!mOgMNjDLJNe{ebe4owud7oKpW98W+q&5BL| zk}7wKs9e*X$gwtpG#l^rU3@qS6F5+a83%M^1vHj3Z~zs(K%@-py;))U#s~QoEp;Q( zL0jlRIa2{A6(hPn7hrSPMfhF49DGhrDvqP(Dv^%&?hyd)Ooc1{q&gHA?E~QvG#;Q_ zsS_uKBLINYwbIRdP!vG~Vx>Xj@$CSAP$MJ)4P}|o_t@fCJb?>CtHzpbn{P!?@Nw*f z5|qWdA|KclBQ)X|s=0*DsOCl`%3HOT8?v2AFGIu^%)+kR$ygMUX*?_fnX77&K|N5> zZPDFeN=|gPnJlxZyQYFR`F<=6=+rbgA|s8hhmeF&vbd>w9?moN71SZu-$AHb@J*&H z{ce|ha?(OK%Ajp$IceTd6z$U@(@}IWzRIMUb0v{$J)lCg>? z{+=-LLlxL}WF$wL8>PQx^r>0*oI`TFUq<;d1Jaij1qNR5&yv7)4seoA{D7 z#mddBa&FyN7J|vYYAf1BmEv;299tWC#Xx{YD{E|iTn#+JH64BWGv3U!o})P-JpuiV zX6bb79YiYb3CpUVE%Ng1hV(2LO~6}I(okue$#*gOy{ctg@(>8N>X_#6$q$g?+mTi0 z^P(;7-2@8A9(m}}`!M86o!y`?mHIkh?E{k<(U?DAWT+kG#V?)cqgvp}RPmF9^jqYu zrvlg9PtM??FBmp5Oi{DNT@Z2VT!QU_MT>$%xKX2TMF(yf2GIET0oxc%!|EIViBkPY zP2HE=Pift0dpD6;yc`^wO^WWy0~gSba6$FwFR)Aje%>sEuO)OsylZWc0?M3j6^t;>P4aP+Rssq$3bx$DJYrl%wHs z%$&zioqmwLFDtiIIUM%Ad$`CCBNHgtU-wY?-R};*&)z{(iP)2(b*!EZ`^+_wEg<8G zu(MXq^hGg@26aIS{d*1XpMTv-dsi^GXKxL?(jHNQB`x&ibK}vu^yq!qp}qFwnGL$% z9RC6dOct%HsTES!^Kexz7D}_>=L72^ZsdKSz00z%?ks;a$d?Z<2GYC|(UA#zXf+$V z7mo$vsP}H9!Q0fI4U|+g8?h*C5T;GmPl2Zo_|5QXZsp6ez%=q~rMt=R%M6vN5y+Tv z;1LS0w#3`7Y}S$TD2uGXG=+gY;CAC9uTW+c5k1NM0`0}xM14^Q>=pq3p0AHoilQeGq7 z0)1{AP3^Cz(#ADt*j%hErgEn1CoVxKe)a{J+(2!i(rK}hn*yHTAn)|Sk-B~+*2!E4 zqx@2IqeDBCh)QBR0A(b8d0WZBz zijmau3#}H7i$Mp;uJ>l`yc2Z`MG3>z185Id*==Nm;!Rn8%|0E+LD-Y_r=VE;aiDmL zWMBb%KP|E;oydV81%($aAIyCIL6cn8obhNPRlNy(wT5Cf!Gx?vLX3ShNG&Ks0X zo-TS5L*OyT1;$~=9JHyG?p$mW3_HYdl_<@*6Bek*l!d-9 z9R+dXeKbQ8I{O}8d*Dy5G5ckyZl_@*auUvp^{4W|z>yr-lnfMfNjACqYM?{ISluGI zHJS71fehRKjv=F6KOL{w_7RNeT?`-0U!{$%E*|>Y8}06>sfx-ld<0rjt~=&NZKi^m zF8ET+;OOBmuLppM8-eOSqaoV&S93?m$Z;&xPDa1gjLpinH?$qG-%Pd^c))=ekIPSg zF(G`RrNFv_;a}rh&{Akf_2XdihY)9fo+FkbU3i0R3eSL&_MBFO1_Ro2SOv-mdEV%TT2ojf>B!ibt~7bFW6$$F3TFqqR`@8Yjnt@=(VcG$bDCSC2KCzW2^1BXFbX<=!!ue|gY z+O`8wu>!(5QC@R|DgYBREeX+TGnb0q&Tcdo!!vx1fyh)RJPZXlvOy9&DwcAGAnxv} z4YaG+9Ix7347b$X=xHMiBs@I=EK?Pxby8b#Xrztc8QcsD>WVon)@FhmwT+d?Hb21j zc{u7~48rW`S*qSorP}ovOxraq)O!e4#uzj0O-3?>R*Gg4LS@vTNcfk%xP!O=jRdp- z=yWvphr15U&{9qV5SedWO_Mv*I372W%eFevMAr8x0F(>>gi$+;!>rbF`L7@!Ua8gO z8ZBPBy#eZ4jpO*x%)3|}Iy6R~wNGuZc%9?X2R}FhZH3U9CTePif1@OqlN!6xy4CQ5 zziXiV897cRGe1aaIy&W;%n38mRjciKk<6C zD%6)Iwm@tOc1F!uT?6{k-&gS#7r!j}@DTQx`Lp4N*r7owzK+OSa~jq)bQ>F$O#zeO zg#B(`%>5!*ChNZ-)`fv>!T0klVU4BONenS&&>KyW^388SzZePsRtf{|vY)U*_q_uW zhSpzdlU1b^kfI`8irg#Xm6ubUA7T4vqYTXr_+|dGRBF$S6h0(*J{DZxKWXw(fQeR( zM6ZUMDSL}Vl}*y8Mp>VR{mFXdU?V-=1&Dltahx%#Bh`W~J5co-lByB=fA-^^V1$LG$ zn%kZ6IVd=+geD7wTU^DPXkA+zpsJI5wuJ@7Ujb^KgIaY_8HajrnrFq3)~0rN5~hLG zaFGM4->gQ{sJTtF3mqbZ4w};ev4R7aEcqco{<&|MK_pi$#jqF}9O3yu4riWeH|dv7 zTCeA2$L26D4Ljj)RZ(sX3!}N_BICE$HIn$~cMx{i8IZbJmVj1jSC}|A#@@vb2IOBm z7LM|LXOyj}*wp3m4V50l@qV=USZ;YOS$ecCv^(9r&v@=>oG}pDqYhG(U7OX%&=Ej( z=tvj3YYSK*O(u-0D=G`!4nwmd_m||Lb^}G@3#Tsm93f5(PY>^fWmIpx8s8yv2f!VO zmSu@&%W7k8z!kA4E$)rKoZbr%nLUZcm@!mP8~>&<0bI!mdLv{x4)DbyhwHw?1&Gb*a?OWl$QjOz)D-{ zqrZtp1AR3CAqAB-s7~EZ|6}mmBoEKza2!jM`k=P6S0dT@*e*pSD$Uu6@wI8rU10EI zP4(+^xF2HLsWT{CScJefo9u@A0EsQ6}o7x%5b<>+6>ECapMHe?0Zb*jHR$^*-QUHcKb^vf<(orT1P-np{ z;2Pxm6rZbdEsL1J{40TXlKeX*R*X_=5u47?k<#vqRy>lPMw=hQg7!856A63bDowpC-5(~7pOVyV1NedUFb-5U1Yh${TO8>R zR(&J=nF<4{=nb)>_aP?$3kHNq;4QM9$>SRwGLsY~?E@%w82;=(b839<(a(hleqxwR zS`TUqvuA9)j&f)Qwj44QTF?y-Vm}VbU4pnwL? z!aMCrw^tTkdq{lFa_wNYO>l?0AS^&r#NE_IDX)uH<5>|a>?d#)q@ItG^H(~ zXF&rj>Opus4T(WA?%4q~t@B$rCk5PCMu|CW7o)W|WE31)rIWAT#-Ue-PjEx{N*)dK zd19c=g@58VS(r*ckHfRtoz@y6X71~zv#!I3_U0fSB#g(fcXfa3_+3D8Tf%UZx7tK0 z97X%q!x2E?#d2?~dTlSSjUgEa2WB-nH??WN5uhD>T0Q2d8_?4VgnVY+V3}NJ@%;n< zk>I(gybp-3??Np+z8P;cra{?|&v_K4pZ161Zqq2lDK*Hs`G3yTZ3>c$O3Ks6W8YON z2Bd@A0eA>{-OpMDE$fPyQRj{eMSle2i{Hq^zFFokt;TiuvfuF+s!KQS8q$90SRg!g zGr`+#z)2O*>dGeZw#I!$fIE9JIrPVGst)EaW0cvaQ}8>y?L*S&)*pDD5+p*(Vsk|2 zcd^sR=gIW)bo|8Zk$e(qWSOS#DJNAGgC7N55HC~y`&und^=0P2g*#OHxW$0>erWt5 zeh*S-Rm=ZV7aMDr%{tQG!N`=W7s0rH_%>U09r)wPfH5;L_vYWPr=eD7w| zT{<*HR2Hq?QZmBN)wqZujp0^2*K3H;6uLt+{C+iSQD7!7^dg}aP&pq6MNN4{gF z6T6|KLWhJ)+7v}GszL3rJ86{B3~Q`t>L$;5)x^-CqH0~}&I1(?JJk>t%{olZ{Tpa4 zLceDidOP{zdaLz^s%K-Sb&J7XAT`lUzRf_j06)baaN|yi4s$xfrq4CfwwMshGva6+jAf@nEM>LMl$b=^k+Uv;D2|b1zgN+%?$u6XMI& z+<5u)N{*U9qE8p%OF3fFtVIYI)k#*4MvS!HLzH55Bq}-sUM$OnRdC~xK763?l0xJG z~R4Qo&wz@1}CWS3z~J5LGLQ|J-*F}mJHdCApu&I&yFIWPfgO~BFzE0RU_ zoo$kR_C>I!CzCp#gV!*0-MkWqc4EAhemf*uKNlqM9DV{#zm4D;>4N1cK6I-S=2;GA zb!YHlraqey`k;Wfg1|t)2+&wQlv_VaF$lW_GWwtUVhQJG3{6$vk3&lq!GW*$R(RYb z?|zT8^GM5QO<69@c?b62%Nf~Uu{jIAz{#>ClsXbhN@$E|$PDc>qms9g;Iq6?B-$%} z2*;ZvUR-{xKz;&UIf9w?UAeY+dwLxnr^eTiJm;^fGR1EqsWt!9YEfBBKd;NmF8M-D3(@E z9EJd!SckFU2&9!JaWKrCT5E0;l&)>tN~l{gLi_1x^0CSceR@EKEJrk4K%1iV)R6kr zfIbYv3r2Ig<4S}Cjty6aX|brc)aoiigcnWSjr@CJd?MwYLJ#PTe?&6{B4n>Z)%JnN z*-)S+$z*Co>t4eN@N;khlS+feC>O4Auqa7KMn~55L$G`#bwS8wu zX9$7L^+THk?FTP)d4Z~cPVch?S6Bylp-k#F$4KBx0|aR?2E2L9G3<4m=G8bbtBj!J zE1{uGo~i3S41L=4-Z&o?f6~8)@kpim*08N(($T*9F4W4+P+O0+{r_M)ih5Jg<$S(w z%+1b{0{0U!GhMA)h(WACL}3Q5$ZGdTrB2$g9gE`dMSl#zF7Z__Q~mHhzO0ANI3l*e z>@sEhR!sX?$gMs6VSY}EbnLM8`ItO;o-mN0S;nTR>ndXC+95y@HIB(rP>woCd(WG_ za?^big+L4o*T|svzD5vJH!pp^K~-_>tJA6nSI)=3!BTBJN2sA%^zLUOp8>En1w{)_ zzK~g>Wc!gOIy+C)Eo=O`2-6q?8PlYJK8VMLp2~rG&%poTn-HE1()sQc+X#R2_wX(? zhj8f>P5||-8bhk?&=pl^wHcbd(2Y2IfHzm&@{+{TlT50t&_*a|Ocj0LjcP;yEk8H8 zBn7fS`!J!pNCYAsC%HHR3*qV4cRIp76go<53hU>Qh2yhel$eOiF7AWh)aGy-6Jv6_ zSok`UqnXicp^vumOtGWhxe*1$=X>2#yxUl|%BYe3vP$4{?A6$enpR2zx~k~16Snn3X{BfhCR zWol>a&GW0j%#c1eQfeusj31M%x4{N-kdRZNTxh^$BcmIG*%WdahiPL*W!LdA^{OE} z{*xlR`e$-cYBfHx8pKg<7=~*&_7D6O9}mfECWZV8l4g{r1%u1DS%I6`vd>$pmyL@A zuHnBKFNl}L^+31y8g$F9R@!y2kthsDz+5rk?=E}YQZbM{R57j38(W`XC<7$eaE06D zxDXfvE>e(Tl{P_eSUKVY=VCe4LD?LY^F->2QFtofq80rm{`>b|C|$n?&Dn@aa&BH! zUt8lB4NrrLtfK*Tlum<7IP7CGjS7Hh4DthY-wg>xwwtx{)?ZdHA44sqY+Dg94!9olF!2aQi7u9K(Yt2X-eHx+|h0<#o#;ff^ z465V6>=&aCb)B9sef|_%R_(qnjSWAekWpCk0&kr~34t7sD5i(LmFZa982*sZU{LN2 z>0@^+7v)a8j6JPxLBu%O6`1_-3os6KSya1^L0fNMjDVZ{H_|}x98~c3+blqNndrOk z@d>t3Z9gYNPD?(kFlKF=oN&h9s+kI>r&=h=(O7D}5gvm%JL6Iw3^B4@7&CLwIK9y6 z&HqOmYj=wj?$2ZR4mV(Zdc~PF$^R|>#xBR=x|IPJBQKh$n1M5|d}1M1u6SO)`N$gs zs)pb)^O+GdQ>i2n&qeG!5lgAgmOON&=-9>a26M$~R?H6zveV9hn z;x1IWHduY2Qf!b@Eb@DAgeB5%oe)aVX+b0ehZPK=RT?ms6-b~)fCHv0tnC(Chw-`@h=@|W3-8P_!F zdz16jU?URXz7BsHz#N%ZhVBA)g*MCuRJZC@Z}m_BB9r!}A~X(eVVDxtbOU0w$IUb) zsMw#Qr#oQ(hSA)EVj@N{>=Tf#G$9BXvU*~JN57ILW)ySpjuRGjz6P7*c$kQr{I>rJ zTT$FWZBJbVJV?jiccjYKaALR%(A_=&M-E>sl)$_UCA9HoYU=H_e#)5U>TWl`ncr$Q;*d_r1 z7I$H&UPxE&Y02C;6UFqws&Rh}-^c+4kL=)-EBF6qHTtZT_=V;?JFDo2_xkEF3Ui$| z>yN}x)!?MuNdR$&szh_;JcG6(6+rc!uae&qf4x@q4GMJk9{m0Y-(cVpi2Y^dkkWkF z`p;w6Xbt6=Z+5W~z(Tu&@GJkBRB)usAYBQjX;%vVR1GjUiN&V#MJRMI1b=`$Z?kDd z{vm8F;o#y=1wtQ674y-Tv0tMGd4r2FCD|ub4^~2~8u)>&+Dc6UI@!pw<>$Da3K2Jz zeoWL=acwb%PPPRjv9aSi0RLiec$LURE5a-%bnmak5fB05vP{JEjpvbAmGbv#9XuY8 z&jpOXp8lM^?+}fc=KqsT^iaWU=FM7Ipt^UHuhCG!z=kvxFHb26P|vD4)mU;`f_U}G zG_*mL>?^1~z7w@hg8aD5!}!(X(i3q_Oa9HBc8=O9wf`vtA+7fr#ekK-$LTv0Jv-Lbv_Ex~M z#j0S5DOT0!TEcR0i<99s_Glh%^p^i%CW2M$-zOUOVB|b#9UbW-93dw@xvc^c|Bi_) zxkyo53AJB^W5G>ranx*rXt+AhP4I}Vsy?G=Ynh1WQTx|Rag>U>*z_w>b6UPlVo`b@ z*21k;*lj-_+&F%ZToFY zsLCAVXYcq|-?hXnSsS(;)QK2UYeMt7Lt;{dyRE2B8WvT3h1_bxk|$+g@k(=As)1}& zzZjc|b5tnKnviPOZN|uV-i_u4LLS&9B;+!8qwT#n79=zWY&y3Ui&Lb;y0hpgz4atj zo(*v`F7sM7KTBplO@Q}>{fL{Pj?a#xIug&R{=ZeUsh?rsR^;*J$PF;(}peRo(UD)n@_iF=EXIaEg~2<<~XP;@ucpL~Dz%$Ncf~@hPKv!IeLL@Pp@vw`<9D%+I_$$O4&HBiQB(`$Mvn^#bo&z!)qEd< z;!jucy{anHZeFT+sO&5GE;1EZadn8ID=d(BQXO zI#8jsrBoEPiWt?eTEF*mKKDlY`^PJidq3lxXS|>H^M1|`r5-y^dAJKrcj(`>mD_8t ziO-g}hq=DiWtWfBYvQ|FVN23ih~7)ioT@SWwbx$LCb*C4H*{eCv3>fF8aHz61g8-L zlHMFQVyK*PJ6>++9z)YtYON^mZ!J-Jv z=B1q-Xxcq3k}mfUbtx}XbCS1P2gpZ9)2ZJ!5larowJcgXNQ;)cRwc=tIgavXKqM`@ zCag3gK+o2b;w%&&fzNm@5pHz*tca%K<(ebC94hL`vScs0FzPw|wO5#B+m4ZxSENNy z=v@(|Idq~DFRhL&h;rU}@QhZl$p8n;#? z$=CHj^1h{6X@3W;sXVbYURoW3=uo;|hw|L?s>}ZnSu}8&7A-!I?cAfNs8~42?EJUn z{qc#?@5WTwqgkv&Vw`!XynjE1vR30+PgfODRN7A8A#>|Qs*<2@C)hB>g{tcSqrKz=!$Ld)6;-xQj6pmEbM?_$k zQt9k2Y}eIaGpYVQ{IKa!GQ}5M8O$m9gs4x8gY-!IH(liLDRpRUFy3UyaM;Cb zqu4HL%TZQ0xqNhU+I1NBP7e;yum+>&MN)Qs&6QpxX#b;kp5ZO}oH4HFP9PdLgwbpmMbhgjB-DlW=qWysIow10tc zp=U0dJ9!`15~w&Ddtdjc7ApNBst5;aw*j`i`lh%j|9(H3%EE=God3Xu=Dx&(7K90J z`eY3DIpUdJyq_D z4x-YDc-Ml?v9jCkXgbqHPor8^tjwcKErU$^*?ejy$+zFN%8*H(^g3aU-wYP1NFT$66MUoV&wG2=h)^}j|*Z~ ziJ*chS`~S+trIOtfH@BMJynjW5l#J1id?$X3h(pyZ!L^hfrh%m8n=uViSp92Xt^vW zg*@GKGwzitLnxXY%HRN42-=<`5^2}JT7n!tDM+-X+HY%#6fzQ)bE2~dq~ac;7VOW3 zzWfv0Hf}c-ym)f3iX(+?x+k?Ng=@rTt0#NCJLkJDV`n{NeB z&TTD)dcLojC@2!P4yR3_-Kkh#wkoRm3%;Iq4u9|)s12dSUCVyW#R;UvAS{**UMi=wvYHE&uFh85f7nWQHm*q-X6`BBk9 z%}hhKu;0DxCp-Grrarat>+JV%Q=;L89cC`wPlY^-2h-O%c`MkIMw{|E! z%F)xQdUFvesu_9VMjQP;b>FCUrcJ3@wj4AzQVf!xd4*Gv6iq1c3|`GYORrBIYhmx# z_SZb=YNVbax1hqW6S{pyL*iOfTw*Ea{)4(n4)&+aGK0RTGZ;o2Ls+P7-c%dU$QQYkZd6 zb<`~TjCGbL$wc8Ng$1i?rZtW9bUFGnBUV${6VZWVQySgQMaV)(mF90v^r1U~-LgML zEi$_cH;1G+>rlF%sb$lPUGT^eksMMHv6pzuPZ|Zj^01cwL{n~cEtTdq6)wwPN9aL1 zrBNraxoq=UTcW95Yi+sMBrji2qU<$dJin3g4#*7-G`y?sCY=W+%lb_MIa*s)D4{PW zV=0#%)H3CP!YI0OL0I-|-LEwhRNM>hdP=|>IfNv{CD1}&cyx$+RrzhRAh~+2MYg@} zK(0S(JLrG$dY!@_k%Zdxd|M?6b0_Tgf^hu9!C5|k(ls1wzk@G~-p?83NJQZ<%}d6_M#zWvljJ+q0Uz$fOPA(Bv?vFg8xoIHv!*|? zSc4KRToXE{RQc`Pi}b!5Vj}Mvzp=0`zbvw4@pcoP+#*6bTgTJEZDOk&nPjHP{e+un zPm79xH11vXp!#8Yy;m`b?))ZPuv(!@tu8XBhb#5!DPCnID%%63bLXP~V;#$fA|&sa zfsOd61O1qyzj_4uMlUxy*9VEaC?}f3jk8>K5Z?GWL9U#ZsgMnobpg&;y`?Gdr?ATP zNm!4)R{FIeEEvHyS{Ao*6bZC`wD41VA{*_Elw-@HWZP+8GH_u409rCV{aj3@1Dj#v zEgx%ca?%ku8WjPv*Y9faqL5~^(j)2K6s+*--eNc#Mxrd)=SW*O!R06R#uMihXjQ1R zhvuYi^eQ`_!gp3)1(@(E6TmbTc_Kny-5o0zcMX@`Gg4?wq8_aR$)PctuXGQ0p*ddo z&M4wWX+!<$!`FH=h#gmvCt|! zsK;-J>)yF|WBHr@laZR)6v;OmW~`D+mVt*5I{Q9K;A&0>80 zom@Sd{)iOA*xJohtBUT%i7sC4!w-K&a7?kvhk1dN-%LvpX3BZ4#UYVK7P{+cvROkw z3&un-5wKg88Ac8WqVgXRg-a|BP6-BX{k?|>mmwa`a@PQVnHPl^k!Sdce8>F(xjfWj z)Z5uVzGOe{^G6r4fo|QpyVjvEfm2 zeC_&_y;Y>jvOz(6Tzct_f)@Xcn7{1=vPaKEpww4zNp36oA1-Cv!}-;8lf3I1Di4n| zQ=emal?S0BL(UuGMIBre8VsXS&Qz{#MSyxcD*F^yZtxsHEM{0Z<+O$^7d6(Mq~|xG z>~9Iw-d9TjzIOmHu=GF@01A+=&xmA56@kTdHo0b6otM;(~8 zf*n!y5XBNFiDYRRjD_apZ5^z0w!|C3jDiXg$(;u#%2uNlaOJRG zi+0}9>eI+Jh%Me_2nvh4dXi%>!p-ciNbv=|A9Hj`rpeXu(vPzAw`A=zR(XDOx-5uw zpxf6CSVE7gYHAN^%Eh~V3fpOp(q+->bo&O1scl~e0oPh7WD6X=&c=WWkZbtDyN3hq z*eq7lO|+#JVpd{(J(jw8iUWIQKNrKrD;%`55#HshXMH(zzY5UO^#>JcuwrAY$vAT) zjh!meIxz`(>GV(!y9`6Wj9GjCd*xuN!ttf{zz1SobtqJ1qCzY_Fhi%$)BNe%h8j^csRA}CQx)0^|#;&P&Q_vnr_ufKy)D)kZVPoUX{V zoZN!5O!QNZR)^js7|A#ChWt9+B1^{pNcqo^x5{P)&?m?63?(x(Ke^zH3rB5UOE1QA zW{wjP_A<*s{^)O_^j?S|d};D+qa)325~&C;rxDz_?QWiH}`qRB15iW{R7UbvQ z?T{{NQ~5RFMI*YR8kpMxFUz4q7B_UFvc`BA#{APrxhsGiXI#h25qo^U6}Lr2c#?xI zM~ls}c3!vC?pygytI8Ftf{YTi34Q>e zw$jN0JW8Q2oZj12ca$+}0;sqaV)-9~0y$A7%8P4)$lMPLbD@)V`W5c!;{)p*g|rsd z6Yt2Y*vH99&R!cpC9^fgHm-oRaj%x7e4v9our5HpxadLK+(ZpotEr#dW^$sRssJiJ z8m`5fEOC~hpmoAk6^o5_lMl|XrN!%DF-NY7&ho`XGxe;FU&CqG@}pnfyZ9e`M$`QX zYh~U!J9fXI_+xN{QUPB)yd4>Zaj1M_qw)Im_FP0T6n1H{|8!LqSrH0dY2AHT*M$0^ zRChL(hOXm70AuM?qazCQuuX^!@5}{zQT73RGv_8gTYA@#0)D}-DsniT`~}bZ$4~fd zr)FA^xS)!4Dn10uL|N-7O)fiW99uV-oN82X50~OPL$3SMiQb>0)!B23L%D_)Dr&EUtOhxT~6IHnqzFG?V zoV_ucox+3Z3WZ1fr1w4tIpW_yD*qM)RoP4=YQ}A~p26dD^+XyTh(v&LDP4Y79LZ@g zSMAwtcHrD|Koir(X_M%_uO3gwUe`yf0LXQ)36+9M_OiiIJl5MHhqhG{nbfZkcepwx zgZ$i)OLpA}QzVDU7Ykjnf|570{|!VB#VHh6kV%?s4K%9B-zTGfnBF)P1+&UN^xhS% zwcPrdyDay#(A}0O7*DnUX~tOk*;Xb36ptd;cdTSBEw@U))pAk%#vgT${XO?f`gSPgWTa3tBLf(!a-BcJ#L{ z&qnCl;D)HWE<&qLt=ePNQ04;3W@`cT@&%GmwJst^E{>eZpiY&9CVC5tEB=6k*P{|2 z?Hfj7akMy2cL3BanS>`O9jsN8F}rO3xqFw27e?iJa&UrC-pmcsGT{S7$TMIRJJ45g zs8IfXKSc&lb|cz^t6XdbBmka?`L+mJ^c&Xu%o9|H1B+l?ma2Lxt#j6zBQbTPDtExk zeE5}FW)`~8&|?~NEAsX5WO-?CxbmBhZ=#CjjQM#-q|U@n$-DmzF<2Lil+sGkgUKlSh=hquVL6=KRroS*`!=KXpTTFeS!WDzVA= z9{{F~?VLvLoZcT+ME{3lnpzValX)-Jd&3q4&jUA6^0%*rwP-seLp|T-ldz1d?ezpY zv!BHJO+Ue=?Cs{`K+iaGn{;x`exux z;s?Yvc27Fd6e0V{nbz{qZ$1MWBW?) zBF;Yj3dn=|2LsaId!LK_}w2UW^de6*JbEb`1K7{}P~H{d8h4 zbgC&i&Z_KU=b`iM0Lh!h%DA7M;o6Cm;Vz=&HzO@{;6sEr8(~z7;@gP&DB0Ul`b|{D zmwv+EzC)vd#+^hl6LK*`t{dcn2$V#99N`sr6Y%~F_hiV+Xli&ACX>5CLt5_w^8)0i z{1zZMqOKSuqKs{AqFt^43LHM=#4AJo-+r(CQ*)6mR%VbFbN1p60xhs!v_Yqi`dnKN z1e6Rmz(Ia~)!^Cq27%P&F=`feh_YkI96?P{>84$)F53+8pckQ{FU_2UwCGWazcqi3 z3Ihe4{PucaC5Y2L&H#)#E8QrnC!U?@-Rg5$vF6;Fsz*5& zl)FYSVeih|hX<7;i%{{Q0Xm9e@EqG(i8(gUFWL-L`&jIgQE&5u`q1h>v~XH~8?HL} z4RJ#gAqsj;{~b{29rvn*ZS`*SX$&I8_>aV7(O=%boLYDnq*X$TP>`)Dbf6O!CL;#h z!eEHXm*IAlBm8CQu5|ijA~s)TfW$cTOM2_sl|g}puI$ko0=I%%?-efxJdLzrTJOu+ zJG6Qp;7-{A54mJ8Iuq!($P#apoaFZ^?Psq++6TquZjDP)b_(Dapjukyr<(Il@BjG= z(2J=C9!v|}&sC*6<-5XPmUlvFme?Ax*v}Q&isREoClmGb(iO8#b-Zj3PV+Z`W(w(O zrQ3~#tpxb6g`O$@aa1_oLssn=Z{UFoO|ZS-8x?m-U-X34AhjmS>OrW1xN~#qU0f8M znlyUvbzBRm(tYW-W*Yeb<~kAx_!VcQs>xn05qcDT-tR+>rqz&+b2H$cyH=%8NOklG zde%g?;hgC8!uTXS*F>GK1B{>9o5?-PCLh{d3H78mo$a9of}}|y0CyB_U=he`oQSMw z_sHfwBH0D|k)H+8JhL(E3}H8uZdJnrsCRH>k6P9gZ_|4*-Uht{YvDSiYw)}gu9i4% z8=~|2KM<8M#xs^$^^^7npcgH8f+rqZmP&K_ag*|-X6Bv^=a3w_u~AFHT~{^ICz7cT zzyzal1d19o`2uJu)pGlFFS0an9UXJR3W9%1lB4__4XEA3gkN3M^ceX|n_1xGElw7S zUW%tb)B-U%KcN~`Im;W+KuZORs^6HY4*;8%ISQ!*5Ukoa$#`vkP z`x*WW@X~?TzmLk1vh7t9^=`|o(wGcc4ImrrTNH)3i3akMWx!^gBIKk;{N4ssKH)Ez zU`e1^_yOSo{Gf*Y1X!=ny%A1iyGDH1CVde{vGd`dPX3`w_gA4`0!$+3xEGdQ&P)LG$I>7Hx{LVIeC# zWx&E$OsCPtK}R!M4j$CN-idOi*1HiTigw{4*lcL=Rn1YLH!vJI!~34*pjn1eUsrG$ z{ojr>%zgtpv~(sL9h;UrR2zW)>5Ml9+0{?=;B#&vRsYizwhrt?NBZM!72#-h0ANN( zFq`rQ;^|FmVDWdu(J?4!rSD?y(?ZqH!(kg~r^FlF%qE0OS882byw05@iW;Lh=3%P2 zB5xi1FA3o$|2m59RbLQ9AcyGX8t^bD-c^LFjYDA=k~AKVh29%Qu{;2k75(NcGNt=t z_vHmz4KO&q+RfR`r8#V;Ia4BCavprmpwfr8#bqsz77_O1+htwi+w zY$PBeH=U_)B;MS%oxuk8(J~Lc`kzzin?+hAuF}A{j@&-Uk!p_;yO=^vkmi!tW%p+u z+{s1zB3cE50zVKbAUSNb?8(9Ils!y5l#hOnl?$GHAS3@`F2y9vy+RcsB=2=ajAj~R z?nPTyG%6fhX6FT%AEbMAp$RB)!0d$?Y=?HsQq`PsaLSHil3=0J#dh>J~LW(C?C+edxnKt z{U-K^vpk@wiJqD9aI4oQ(abK|dMbDym1~~Keb0<4B1KcWgZ#!=84fdABJ6ybNYTx4 z7FqUdLs~N)ivc%ImSqVMY*P+th^Na(<7^|mAvFV~ zduCDTBt()5*k3--k=8E6WiJjv|DpG%+l;=@4<|G?`U_+YxFmyr*g0Yz+Mel)kfshU zM(9Kal+;aCdpaO4x!%EorRHio$?=I6MtM_^ZJI9;t!Qooygawf|5|0DIGq8dy`5=- zqKAp9wsy4Sy2m3%gei!jjIS^hyT)l0DQ$5ER-TDqS$q{O)-lGaN$*A`S(Y10N8SWC z^@XlaQc%-TVHsH;pmf;Z>zSNJBe1*fCqvGeWY;R|GYw zK+mC%LK-4**;lw87r`>{bx!qZR60hBs_ap+oAn+iG}-jN;)l>fafE}KS#jV^@9n`N zu&m&m4mf~H^rg+gVrlBPu%k2cU^r(VYpGE9xYIR%SVF}Xab>n)wHQR}`hgt^SfPEQ zpa2V&JjuEO)}Gr^|G=JMK3HSCoYG|+%`$vZFi-&W@-^e)#`l~QO7O>u+ocj$U5F3432sx((k9(uvx72 zofGv1(PnU2r_X{{F*{Z>v|-?@3~n49O%Hi47!9KeX3~}7{|ELLA4w-`cRj}7Tpq3! z`9e-U5k>uTfHIuh!VOi@oYewBpVb|zO#I+DuUuQ$7-JYJ-2=SwVkq8oIroTo+4i@^ zV!XU*r9(qdDy^-K2UGwIK@DMN84_8Ghv;jh1_xCtgSh>@vXNfVitS+_i5U&_29OQd z`aK~lykyE$tPzV>(lT+mUC4{_o2PgpL#X9WRYR~khkuBt3;$>?N^s%ZMbxAvokb_k zFIF%7MGS_Ud$7p?0ZV&v%`z-($Xx_-Zrx>abPEkA5y>04n_17Bp^s_}+ zt8FFlNU$oyG|k^AcF;t0*;95}dnAzaShHkUZr;D3X@8+)TICMI`tnB<07_ki7-dv< zD%P*eu(yY&TfotVJXHl>m{Qx()Q2dg5gzNy7G>b8pgIGoWo9m}K5e)O>Jm&wD=xfM zn_ImX{%56{`(9<_+SJdgXQ611nW#Ek<8ePB(}Y0Uw&>^Z3Y@JBRN$rJY|h=qp@Z$bTsrT{mIWXpd{ zG^-uf`S3_99Y2WcLcSG=HTIS%fob5`|qjY%_FQH`|FOoyEQ?9`e&-ECtH_M0#>mRM%dstWo;(D4x*PXQ)bRyE7$H z7R5@$aD&N4+(7h(LbLHiY&b*_7b?0v)M5gx*#NVC0GiN9(&rQL#0Z_SRMZ(gqX*em z1)!+S6r`$<4$4HU(lC5V8RT;Z5e1FM=to*jX%2L!=B2`IPw(HgeMZ*-uja$y8MQv3 z;rz+!TPWCjMoIVCk*cZ35;7-QFh=-*#bfz}b;lhVzBT)kd!O+L!S;v+o(o0t)u8d3gqsp6Kl#5T$-pG=* zYAJ)3Wgj@mbyeEE>eCK_OWM>#G)u>&(6>*f8p}j8<*7Hh{8Pm;>&D`&y6QR8^b~A+ zt9f-Ov=bx=!^a>fGqpzfm+{(Y#VEOOO}TQgk`17Q3$Pc55PE?LG8Pf*%-(F-bT`(WWrOX4^!lpz zk<57UYCRA&A`ozD(G0JTyc)bqG+_5UkN#3wuwfI{@~Bp(p7aqI&KneQ5;)* z!@1|~CD(0sliN)Z+=PZuULrmhK$;7IdlGA`Evi|5bTvP8GWnGw!NK`pxF%}gso}1As06G{f7*Blq2=KfqtTD`4xXEpfyF)o~&9W)KAp#Q^6gg!m1=9-EO`hF*}xNoj7{+7;A9Y@72^S(@?|%t!WdYb zt!Ijg+=*7N*P1ba&6b&Wm1--MYMq23vaktp-+9l(NNt3uL{<23%!p=k5eifX>gIy& zKRXJ^A1Y3Irxd%54w{UE+8r$crl-x6zYCb$_bC>V^|#O`_;D{a9nt=Nc9Q%(!Hwl= zN~rBr6}EnLH&`?ukWx1Fv6}XI`!QyWcR>4nD1|IQMT_p72a^o6XcM5>j%T2~A16Yy zpYQ2N@)%62R}T~bxFEGS2GdoXbADf3Ox1RWmH~oXoBf?C54U@ z%(?_60AAp$#js+9=A1%=%G|EOnG$L#zd@tRm^SgJ9dB!0De#N}ST00GqNW@bP=&5d zgBPhmnzHW5#6F+F_768&&myIGI$?Y~SuR}SB>S~Oy5}NhUei+h@Xr$med~XaOwCuB zX`301-hulye|w84l}0y&BdGQ(wT{rlc~y<<4Cz-Q{vWM~@-!M11KRmk%JjjyBG#ml zzc&yGYFhB_9Nfpw(4zK0pgclQW^=t3M-ipEj*SIp$88bvt_`BL>JKq=^;2yYk9fq= zv3!8A^cwE6WL2bM)*|<4NlWV*KOY`}=d;n#u5#%>s0tsfH~7zyiCF-H}4LjaXS8``$ zuN;vT$^dyJKAMRex!7!y>Fo{FD^IR){SO<_#s_Q!3iu2P{70pxc$)rDuwubX9p`9I zZIR!PCLGdQ@YI`?n*WW*=g}6*e+Krrw*>)wY&Zyci00@!uCqZjBo_A7oc>d8eBvnX ztLY>a0qM#Sa1&|_rS2&#my!xXYG9K4|EVKqZH$BMCK=*^{}8FPDjD{2xKV;UP-`Tu z{uS$9{9XdZe}}|Wr2{A4MalF}qG%`=-%uJki#%}yWy%yIlN1kzZat@m8+G_i__4Za z!f7lZ8om&zrD+>z)jWX9#npvpVX$tpiwCR_jNfmL0!WDmxvz^Z^;_!oe$W$pu35Hm@bPWDXN1Q(vPE|_R$2<+dy$6$Kd<*F^{2~u>KGD7Rq(`b#n-H zEXgz|0M4OOh*KmUjJXFVqw_Coo=I0SGbc-2GUt<`dkB-IN-QSSZb0SNs2ea)E{BGItbtpj%V7}ST{)PS8xD{pkhx90L@N;dZSh#-m#*=_RVFq4tcnZQJx}@>;s@w zGF&`o_?F7Bx&^AMg4X`>a#0#?Ak#YwD1uHV~%s4!D1?ktk8a^AkLwiVl{svAsgbNut z{EIK}F$ln-Xy7BU6J4as*Tbo_IkY~OpVRmmqD5CjoP24VuN-%}9+xZq?Ip4=9p-UQ zg|}MO1sVjZwCubyBvUA5xmScERnY3xDh%r!vP0xDca82TTA8gt0;xAsT1?wo)*|#z zxLPvABk}Rb@k_mU?(|isNDirb&k%fXwdhS1`Km_i{tbn>^Na#8Q37M4B$_u~%i<7A zMcZIu%OcV!wH8Xo3v=1S<}UKUzwCZ#w6PvMiY0G%JHpY9*8_@F z14bF&2v*?n_`s-T_{h4)sZ_jI5y7fOB|UeVJ(aZ%*6QuV^j=KBG-(} z0~f|qD0CnW+so5tN*GShj=_>x{6hKhFd&u?vEF}_8Hth0_s2_cZ5EH7>x^{w?>{iU zXPL-2d*8qov|ZB;BX%h=q5uYD)ABh|;0mp3VIOVPo`%hZCx=96R2svCLsQ^2%;lS} zz~Gp|aZiXd;ti1SW+VHTrMuJjeZ>qKaEyE5Nm?)xLZWQ3I+r{4Y#B~;G9JFB;ugkq z)oRK4OQLpumE1-JV9Kke$Ivzxd|wr`mr^kRGL%lu6IM7mgwYraxIIY`O2z>5FrQuq zeS5qRJDmHE_J-jm_gG7)1`6C)!4{z-q^2Td6GW(pMe8Fe390mzSI( zzwBU=H(%CKt-oVmX^#!L1Ei=J+67;dijE!6k}(d0n5s9{io+HfXKadGUb{G&RU=IC z@ChyOBpaJ+-3;C!xRAt3$-;N$&+fbGT;# zcL8mJFUoF(6;M?Yj{-JptT%W?zWwMQ%_3Fu84jbVJU`(?H>WUuOu!?*7tAO-7BN48 zn+92wd`VQQVO7!8p`F%(Ud#e%HbG$jD__b%#{Qv9&}NZH;U?LyLwAB65JfYBm)T*HhwdMPE{$VP)s#(Ce zZ0v%foZTM+ZEZb7MYC^t2{frc_ke$c_GfJ6!To7^G8NX(x>}%o z*3~M3VbT+uC8$aq#usgj!~FA&c7P_&!FWo}E)1)T#B?9V5||VBr<1+#N=k_t=Bwx0 zN5nBSr6ipfL7=K8XXG7FPP=VG^$;$|A(I1`c9sESxJQvF`+$B*42BrpIYpnQ29~I5 zKNt>ItFrfoXoDjA`wcuc$EZ%N6vna8(*Y1qRWO2W2BQe9nvB{Rexc?xfcLy{We|C? zQ-o4g_fg!}*cGZN6Ho5%X(g)WatD&KV@5U7NrlrPuEt!i(FlyHgL~UxRWQ3&5;cj& zWtmbta2^%~CrG2dsOhlysdQi{o?bC!gF0isfWHT;evy3M&8ESv{}!wz@JOgJ{O2)8 zj9{u#mGiW>z5ZWau95^*R?<2%tJ;8OSh1`2@dNtC*nTx1qaYxl1QOEk-%p$SnA{bn5MsiiFy1`U(Oi@78`0u2sf-0cLcSF zl4nkc&<1jkD_*{Lp~POT1!I!XjoNh9-Q?9v5o~RAz*~2tnVI@fqaGPjh|6+l+HS^- z((KzYvQw8yTuU1znxW(Ml1tjg7_-wDH+CXf2d0PMNsjF>wHb0Ts)7-a=v2y?hXSe2 zO6_Aqwm;K~wO5#I)Lzs+&u@h)%u+r9n=>E+p%deI@#?q<==}_r9Xtttvo}AupdZI6|ZYu7J;K152PYV16w?t@DE@L+4&&!th(DThOn)55-)qOAaK&CB? zM<~vC3-v9_0pfaSJcVOdG5JW>&p}ClkNyFUn@U+lpz4Z+GU^Wa635P;3Z(mXRt=r7*!WCon&Dut#4(90I{cT+L(a)ekUxM8F zX8#5wal6jZs>`Cobrjz%hc|LVV?wBC9L8^Iw>c3zOHnsRbpj2q2~11(^%!Dv&vE0l`iv1rC^kf~!_Q%xW# zdMzyn?=YaPHpFNVC%?(dup2$hHt5f**;I85$EwFJl#~p86US9W)}zKrR(T$z4@Ryt zd0;qd4>VuV!oEOqYiB5~=VO~}+B66H=mLVUT}bFBSjT0^MCAUT*prPx+&evheG*w+ z8xn`aefd`-ai>Y|ov$!;em-V!2Ren*ur+utL+#tD8V?$&5!?Xz!I$U)8-fH^Zi!(u z%PgAkm>CrZqI24GYbE0HqhIl0+;4_tq-Q1F_u3GBJw5M+-$PGKEz)GXxC*OLrnSJENsn7?Le5v(RX~Eo{^t}p+ z-Xvh*L4)xnh}sir-eb(dL*T21+gxc1z^DpZ-B;rgxQvEu-NXr0kwiHEqy|5aFQqSJ zXxBMS4qa#rl*M{qFCBs8EZ`=RS{2be-dc?WjRLSDIg45lXhY1j?C7MeZ&EEZGH zgeSyN=+u7Nh}w2UI2=0?V^9?%FAnr|3J^rwT&=e(+wCAG8kGX%jPhA@H$`o{aS+?e z%v{U$c!l=PqO;P|aS!ozDsB%q#5lElHz87e8WT4rnlv2om1UV1O`xj*K4NkH3t~?1 zKk?7g-3Z06Xe-upmbb+6vFewou4*!K%<rTIKL=HJH153nBT$;EYk&;K3Fp*ce zlXvp4Cr|#P@vvR1#|)3{7!1~tPD@0k4%XW^=0>ha26wna#Ho7DZ3kWpNG75XRF`ak ze88$oqXek#gpPuuFE9*=n>7F=aS6x#J_{|5n)HB5e6AZ!!Y>T@i8^j&b^u08_)L-C z>gg_q3U;a0k$%~Y`ypu5r$(&Df|+7U?=YOX?icV&)W&8ypMr(Ixe@OE^mjLfmu10J z4#Hk^c_SQ$TlMnz1KZfLB60tng&kFM=Gk8f&<@RF5pc~UAbnR8s7k2HNj_WtJuaFo zvDiFi*xm#1iskWI3wswSS_b?_splXUe0Lute+23&FUo0%>b8Qs!J#UZwg6}Qz#ba1 z+}EBp94Yn?Rux4wZJe*=DwF?P0`qb&1_}^#syB%=5cA3g*L;kZ0B>ZahhHNC@o{2u z*v_UjslUz#a=0s{p<)SOPLy>7SGJvR;b%;Xl^@31Ds|9eZv^^>2b8A55T}2Oj}s1~ ziIV}2muS9@OU~*$SuSpuLtp;_A4R7?R+))7vLH;&T;4niJ1cRs(SAyaa^)<%C~rR& z|6$#9MH9IN!!K1Iy~80<-BD0DS`FOwb81SzUICC$gDRfRFmKjas4AY%CZXHk z;WoywXtJTu!68ge^md*e%9k_R;25xhOx1pNmJ_;aJY~%#x^Y~KLZP$TL>NcHO+b03 z3VHdmjw^Nd1-+tfRsCmCMXrm9mJtt(fQ*Q_rEWchwl_ zy%_7GBwLG485JZ1G?3w)!j2lMQg=!_4fX~9q5PlmHLf8s!NlF7pr6HHJ{ihN)qX*6 zLkHY!oV~?e3x_Ev@@#|QH1HSbq?j*hI|nS+t)no2{9mf{3Q zbQL$aQv~>Fm8FzLXELBWhA1=wZB>7Ivo#(Y0*@s5@s5~19X0?l89Y{>^KfE2T_#(7 zV#tgOtAWkibG-8SBjnlp)puyj*e-)H+NN zo$RiE#LQ!yqValdMpVO*VCbYfKM5>K$hCl#d9YeJSD(u5(>+*pWv8$Vx8h! z!>&VDu3B86wxTg0iLw}qXv+vy)mmh$#Rm=OX&8bWC# z^SVL8riPq%dZDUj9xePfq6;q1^F(mSOTXj$XjEp)6(dowh4rs+wJP)_TJlC*ZFLQC z)z)F5uh${v^T|-j_l)d?Ibug@J{S(GcB@Z6t<*q(`K^(tz4cMo>`0u>xhOs^*su!j5wrqQ`wK8y^mYW6)xZ0nmk- zIB1_?swwX!LGwCEYhvss#cr>e?j!jlxdR^$aEDK0NJ19oDye_9WZb_ zS4zzc{mrivkd@R7_9vX!pn0c~^Jt*Pt#IMIk3(FZcsKxxxX$FWP9B6|rLO)um~#-HAQ5Sp z@z{7Gh%-d)9vy*P)k!8z%P`nl zacW@cNO<-da1YFZlm-F2sM_?G4Uh?-DS)Fgvk^^(@55T1e*kb;a2qUVz1p}F=G5zp zTNMX_Qnj%lgK>gcLwW4&SPu7e-Axa)4`-uq=|n9<^!C&w9=(KrpCCaT9MO`BtD>KP z)_*1+DnTwDSU0u5mfn~M<*2hCGY=yMs{x&CpW(IHF&|(hYWY%=~*@BC2xJq zFu+RVw_~@tl;sIAu=w(;Mo>jx&_*ivlb;??XXP2qAV~g9@Q$z7L1>>ib~A19wiVD zbvI2`2U;Z4zqRpq#b0l!4Z~N3y}3CCGx>~&DvuDZ@Oh4rzG|mlosSv%ZY1Y!3vO$+ z#43S;oINUn6?pWx4a@~6R2jP6w7R%3+$@bg2Dn4T0TE|i)YJ%Q5ge_dN2!Egz|J|+ zk9F}>Fv3`DNfH$wM&W=tA=Mf&W-Aza`B6r)ZnQL<-xYsXYDhX$%Z1uqbs!nt4A)s@ z9j6BJ%d;yFL;V;JynJ&xPJY5(kMRWKo#*> z`!rYCt}_fPQtt>L>&}hDD0%E>ctCg*58>OG!oW^k)Et8koW;Qfh#rw#MalH_5sD)n zwFnSxcVNYdgJ9akMRII|5@GXa0L-=tQNj>dqS_HWCR1Egmaf-Q!y<<^!4gKsz;sYV zsotlVk0v%)c|#%AG|m=HAdP`C)sdFsAXw!+lyej@s`LpEIUhR812abNZ|N*UUx&2N znK@vKEKWRFLEEVFD{0qqSUwM4cv`T%H_l)$3!>bpypCwi;=@2$_f2DO4h8DmZc3zu z-ke|f#JW{s@R1(A@Dax7SRK2o^;1kDbiQv_CL?%MsDg}8BLoTrV;b!OmNboit_hRJ z!4L5^Q3;DNv*lkanTY1Rl1PQAuqW;+v8olAB^3*Xb99R6MEn2H4m{Qqi&DT=5c$x` z>%e{rhZm1`#4!^{22d16IUrIDrLSu1X%x@}8c6Qje)tzq8{h29G%Pq`0<@ads694A zQ6POrAno05gsg5f)ZFNMkm7X96KobY?cU7A>TuIe9`!Y9Fg{|67RCcPsr<9OomkHR zFpW#$SmVqZS^AYTZR)ChN%t^XR~U~fE#l({1DG^f0${?kwWS_9=CI}3#~JiVPmBU7 z6>rE5@Tc2aW8st%Zf#?fhTk+Y8;bb!CStfT%zt|$h8RBYjl)pK_lcvbL0Ti}_BfN4 z)j?q3^esT_bD_ z#Q%y12~Y@yKJI|)r~`a4L3ZV9jGg#4)?3@Ka<<-q(DpV{_z>|s0Qq@({vB#Bba9uq zbfO!nuyqBKM=wHNW6Fb9TOOTmKi11T83s}PS25Ed1-rH|rZ0DTyzeWNdW24V1};WP zm7BS-N!i-2m~yiN4lkOt9QMCFm?dY<)SwamV&CPqz8d6X)eM1Y})*8&^p9X-iPqrQxDMTg(ErA2ZLG1 zX>@4v4S8*TB+PC?o|aTN4N#i9qHGxUuIR4#xZUK)wAj{83J?Nd== zoEgzMmO$Z~Xv62sTPp4un&xY;T-za64lHL@a50Q9apgm0-EbowszaAGhITqar%{R% zu*v*VbK(P9Sx5E&r%thqQ9c^jOyzrnw0W-=mb+E!iW`mL9 zGQS6e`CqnKzVX}-CoJ_=?5~{rvC)$porKf`okh~}!#I-tqd_3@`|80JL&4yw9;2qN zmZ=ou}K5pm3!2jj(e{fwXR*&aG&t#>V0N9$^DV z4(Ky#P@l1V`wbtcem6v$;Mkwf>hJg3Yo>wtpN=E@M?e(fJ?F^DMc1D`Uh-YCYf$6! Kzg!zS^#1^!>|x3P diff --git a/test/fixtures/registries/20210915.ets b/test/fixtures/registries/20210915.ets index 53dad456878119921182880c5cabd29d1de42dcc..3de0114e92107fe0c49d65b0dbe73959b1cf6e90 100644 GIT binary patch delta 25324 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 delta 25335 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 delta 4348 zcmYjVc~nz(8YMt5BrFMnCPY990^yO6J%oTsWOD)8D!7ASC?J9&N-HYhR<&9rx7OX_ zvD2}g9;?&N)M-5|&OKtzf>Xi=pqEJ*T~q?Cc2Ba#&Z9}Z7z4GIbh?uuxy zZ}B&D);6?nXzT0>TeHf(e#4p;3~w>v*@Q`wIq-!h<3@-9vo^R;=95D6csi;!Ij}KQ z3&pM^td%K)O)VH)V#k|wX~$lN#Dt6ab{M965bAZ}=e=@luF>JcEdr)5l}cKm8gL<| zN{=h4@u*s%!{=%}dklshdb*y6JAGl8C+Lu`u;AN#4?cHi zfGqYq~eN31=A53mDY~S3j_+lf?u*M{3Qrsoo9%ML2onrE*V$qRH$i) z6}w!j!szC7yp?XkeXUl~jXsqXRr0dQQS~(?zU@uHU6VHYJJvV1x2+7th~I(h_B32U z98Pz8D0G1r^G2b8pqoNXMNFawzcjNf1w{2yztv8B5GQbCuT0dUsn#N&0MqkG%$-3) z5QfujyLkMja2akALdRns&5_}!eL8fOTM*}P5xAUK-IpULkd0vym(%U|SCU4OjTw$i zj?7yXu}G{+prn=9ewcRzd1(s#rrU|6Jp!*ZEA8nSTHIS{#~bs^_S9KSj&Y^sGUO-6?ANyPCIQ zxJKaExbBu=?(T>{NET8n#{wu-Vs5&IDnFk?hyn_+^U-nRo^4SmW!Qps<#vP*D6q`O z(8-2Ug)h3(k<#CY-yX7~I!VB`{aLuwE`@&8YrGjm1^8)WI{c(djiH8gAGKyd#q*iC4Y;f_H^&$nT)TS0kRu);i>5Bb-`HEgIRWw(VoSf0LT zmcc(YnM1b9?Sj&pz#)0b+Qc7_6-IBY*0Y-ftHt{D+V!ZY^r9$M!0@a`C099?h9*Ve zld4zPO(9mQ<{h5{H@l^n?9Zj@S}u_}YlJ(~<2C53^E7`(c}OB5W6tu8%5)rNjYIJ$c5 z7^ML*_qpgH6;Bw^>()ya!Y56|dxXq=hf;}f%_wfY@DRch>6>*~#6Nzk#EP5#TpE7j zmqKN*xdkH!b8xmN42R=&xR#gC4v7*${7S8>oHd&d`t22rG6eiDBb>b*f3K$MWjb)u ztEItee29@3sst1~lTLRluwH8680&QtC8RO@F!6AW7lQM8ouP#Ca^kB)tjR7^O*7!> ztgKKfHAUyP;>PZ9d{UE)p;8U8R1y;bg55wC9VKa7ozDUg*~qq14DwPxQ?7CP|FMfv z1qWwSfd+5xbz-p5Kyv58f4l-Qx)l+Y4*vOaVh1}4lMshMVanKiL}Bu>2NV8Wk(TLe zAj^PPkGarj)gzR;{$piipg4+3$dqIfI*Zl#;|!O?0&hw&zMMmPQoEn4hc3{Gtrk)Z z;%j*c8w?X(-Yya_gT%KDRt}-x?|%(1)mflekVed7#+g~-5V5Z4XXoLa5(UrYs8NpY zQXK;8cXIAOlOagjAeo+l&W-c&Qkh{w!Q`;i=iuAo3z8)wVxDkQFAF&JU-O4^=xcKX z&H|=zrkOVUe3Vp~P`B);4EgIs4P#_qQl8H65N0x3jJ6gmNX^Cfl?il<1$ilvJkih4 z$sc*LCF7N8xh0Ybj?7r79jB*I3+u9}gZMv{N>HulHsxkR9G~}_IvH-Q)k*9ympK_K zSxZ^|U294I)GmCyNYAl6Xco277*7gJ1H3zp1A#|J7$00hR7=zKMbfy?-Ux}s@x>Z4 z8)kg8p%XWfBkFz=2*`tEHllj>jT*F;ieS*&`YDSB%MgogQCovIVlFm`{ix@*!O` zG6o^07_Rm}PKwEV4lBfGE43J$x*4x8GZHUK@$MEGsg)52!lf8|N=AxhXZ8>AZRtFQ z*7^&ab8Lj>LS%1>#J$B)ywW$O2smG!i`Sb(boSbnczv&z3Mt1QqQua=je0)OTz*&! zd+jRpxTF|eo)sXs`l4Qrz70%}^YC?r8^uKeAE`OUNUUv&!j}OW-WbF-X?X%`Wh%VA zT1_RkQ^0cUQ|gGV?MOFUEU5J1h|NHy&cpp}VHok~ICC*JyW`8j?PX*pACkGnJI2wdM1PEQ zqS}QqT8;;Ap$f;-MHQ8kRGI2FW18BHC-e7INDKxSmH_0!h;C$JJD@ zRaC4B@+7&K6RF2@n|x?)jDXT>z;R+U^f8dVwuTj$Z6={ix=wAdVRdpRU9#c(hAe0gTtqRcOB}HSGo=jatvw3v zFK*0K;O(QyoY~uZ@~~?rvzbnunJsYD>Cwp1wwgIGH%5sANvQ(8Q_$b3qc&y3xu$>{ zp-cUua5xF}`HC$h5CYlVmMMUMFc0g;afVkUf8jso%{j$@KG}LRkTKot6j~0M$s0`I~YmUiMWb9 zNdFTW+&gR`Xs|tq07p6)U6o1f8jD@K^{j>DVa#}g9GaU()Hw5D@b4X9MXBdT!Nxa( zS;eBv_wFJ1Q4BK)s;q3MkF(q?JmvwdJ&b#2;>pT3dW8em+)hL(6_P5hGxwvS;F+2_ z7C0Ftje%=STdxe=nXKlV)$VU&1n5FopD<==?(3Ckoh#mW*BFm`+u3HCM8q0L=w=qw zvn86tGad;OWE_v?^`a*KHK0XQu?g};tob&)L1M+KOZH@RA56P zkE#iFo4H4D9%IYA4P|}>nhY+P&1e?+fK8~H)?c=kmK{&PIe!+LoaibqZiVS#J2A+g zJ49RTx<|(rt~h+&Ofo?$C0RtsJJ`%+m75rM6!8U4{4pXj#oxB7w!N{ob7g(2pI@WR rPRf6qsHdMY{!IUc&`%U==sxA)|Gn_o+qZ+JM)p0jTzl0cX=(XCAW~Kl diff --git a/test/hex/cooldown_test.exs b/test/hex/cooldown_test.exs new file mode 100644 index 00000000..0b37a89f --- /dev/null +++ b/test/hex/cooldown_test.exs @@ -0,0 +1,259 @@ +defmodule Hex.CooldownTest do + use HexTest.Case + alias Hex.Cooldown + + describe "parse_config/1" do + test "accepts integer + unit forms" do + assert {:ok, "0d"} == Cooldown.parse_config("0d") + assert {:ok, "7d"} == Cooldown.parse_config("7d") + assert {:ok, "14d"} == Cooldown.parse_config("14d") + assert {:ok, "2w"} == Cooldown.parse_config("2w") + assert {:ok, "1mo"} == Cooldown.parse_config("1mo") + assert {:ok, "0"} == Cooldown.parse_config("0") + end + + test "treats nil and empty as default" do + assert {:ok, "0d"} == Cooldown.parse_config(nil) + assert {:ok, "0d"} == Cooldown.parse_config("") + end + + test "rejects malformed durations" do + assert :error == Cooldown.parse_config("7day") + assert :error == Cooldown.parse_config("7 days") + assert :error == Cooldown.parse_config("1m") + assert :error == Cooldown.parse_config("1 month") + assert :error == Cooldown.parse_config("d7") + assert :error == Cooldown.parse_config("seven") + assert :error == Cooldown.parse_config("-1d") + end + end + + describe "duration_to_seconds/1" do + test "converts canonical units to seconds" do + assert {:ok, 0} == Cooldown.duration_to_seconds("0") + assert {:ok, 0} == Cooldown.duration_to_seconds("0d") + assert {:ok, 86_400} == Cooldown.duration_to_seconds("1d") + assert {:ok, 7 * 86_400} == Cooldown.duration_to_seconds("7d") + assert {:ok, 14 * 86_400} == Cooldown.duration_to_seconds("2w") + assert {:ok, 30 * 86_400} == Cooldown.duration_to_seconds("1mo") + end + end + + describe "build_cutoff/0" do + test "returns :disabled when cooldown is zero" do + Hex.State.put(:cooldown, "0d") + assert :disabled == Cooldown.build_cutoff() + end + + test "returns a cutoff for non-zero durations" do + Hex.State.put(:cooldown, "7d") + assert {:cutoff, _, seconds} = Cooldown.build_cutoff() + assert seconds == 7 * 86_400 + end + + test "weeks and months are honored" do + Hex.State.put(:cooldown, "2w") + assert {:cutoff, _, seconds} = Cooldown.build_cutoff() + assert seconds == 14 * 86_400 + + Hex.State.put(:cooldown, "1mo") + assert {:cutoff, _, seconds} = Cooldown.build_cutoff() + assert seconds == 30 * 86_400 + end + end + + describe "eligible?/2" do + test ":disabled cutoff makes everything eligible" do + assert Cooldown.eligible?(0, :disabled) + assert Cooldown.eligible?(nil, :disabled) + end + + test "nil published_at is treated as eligible" do + now = System.system_time(:second) + cutoff = {:cutoff, now - 7 * 86_400, 7 * 86_400} + assert Cooldown.eligible?(nil, cutoff) + end + + test "publish time older than cutoff is eligible" do + now = System.system_time(:second) + cutoff = {:cutoff, now - 7 * 86_400, 7 * 86_400} + old_publish = now - 10 * 86_400 + assert Cooldown.eligible?(old_publish, cutoff) + end + + test "publish time newer than cutoff is not eligible" do + now = System.system_time(:second) + cutoff = {:cutoff, now - 7 * 86_400, 7 * 86_400} + fresh_publish = now - 86_400 + refute Cooldown.eligible?(fresh_publish, cutoff) + end + end + + describe "describe_source/0" do + test "reflects HEX_COOLDOWN env var" do + Hex.State.put(:cooldown, "7d") + # Hex.State sources are set during init; force via direct setter + assert is_binary(Cooldown.describe_source()) + end + end + + describe "HEX_COOLDOWN= env handling" do + test "empty HEX_COOLDOWN= falls through to next source, not env" do + original = System.get_env("HEX_COOLDOWN") + + try do + System.put_env("HEX_COOLDOWN", "") + Hex.State.refresh() + + # Without a project or global config contribution, fallthrough lands at default. + # The key assertion is that the source is NOT :env — if fallthrough were broken + # the source would be {:env, "HEX_COOLDOWN"} with value "0d". + assert :default == Hex.State.fetch_source!(:cooldown) + assert "0d" == Hex.State.fetch!(:cooldown) + after + if original do + System.put_env("HEX_COOLDOWN", original) + else + System.delete_env("HEX_COOLDOWN") + end + + Hex.State.refresh() + HexTest.Case.reset_state() + end + end + + test "HEX_COOLDOWN=0 explicitly disables (env wins)" do + original = System.get_env("HEX_COOLDOWN") + + try do + System.put_env("HEX_COOLDOWN", "0") + Hex.State.refresh() + + assert {:env, "HEX_COOLDOWN"} == Hex.State.fetch_source!(:cooldown) + assert "0" == Hex.State.fetch!(:cooldown) + after + if original do + System.put_env("HEX_COOLDOWN", original) + else + System.delete_env("HEX_COOLDOWN") + end + + Hex.State.refresh() + HexTest.Case.reset_state() + end + end + + test "empty HEX_COOLDOWN_EXCLUDE_REPOS= falls through to next source, not env" do + # Symmetric to the HEX_COOLDOWN= fall-through. Without + # skip_env_if_empty an empty env would silently shadow a project's + # cooldown_exclude_repos with []. + original = System.get_env("HEX_COOLDOWN_EXCLUDE_REPOS") + + try do + System.put_env("HEX_COOLDOWN_EXCLUDE_REPOS", "") + Hex.State.refresh() + + assert :default == Hex.State.fetch_source!(:cooldown_exclude_repos) + assert [] == Hex.State.fetch!(:cooldown_exclude_repos) + after + if original do + System.put_env("HEX_COOLDOWN_EXCLUDE_REPOS", original) + else + System.delete_env("HEX_COOLDOWN_EXCLUDE_REPOS") + end + + Hex.State.refresh() + HexTest.Case.reset_state() + end + end + end + + describe "parse_exclude_repos/1" do + test "accepts a list of strings" do + assert {:ok, ["a", "b"]} == Cooldown.parse_exclude_repos(["a", "b"]) + assert {:ok, []} == Cooldown.parse_exclude_repos([]) + end + + test "accepts a comma-separated string (env var form)" do + assert {:ok, ["hexpm:myorg", "private"]} == + Cooldown.parse_exclude_repos("hexpm:myorg,private") + + assert {:ok, ["hexpm:myorg"]} == Cooldown.parse_exclude_repos("hexpm:myorg") + end + + test "trims whitespace and drops empty entries" do + assert {:ok, ["a", "b"]} == Cooldown.parse_exclude_repos("a, b ,") + assert {:ok, ["a"]} == Cooldown.parse_exclude_repos([" a ", "", " "]) + end + + test "rejects non-string list elements" do + assert :error == Cooldown.parse_exclude_repos([:atom]) + end + + test "accepts nil and empty string as empty list" do + assert {:ok, []} == Cooldown.parse_exclude_repos(nil) + assert {:ok, []} == Cooldown.parse_exclude_repos("") + end + end + + describe "repo_excluded?/1" do + test "true when the repo name appears in the exclude list" do + Hex.State.put(:cooldown_exclude_repos, ["hexpm:myorg"]) + assert Cooldown.repo_excluded?("hexpm:myorg") + end + + test "false when the repo is not in the list" do + Hex.State.put(:cooldown_exclude_repos, ["hexpm:other"]) + refute Cooldown.repo_excluded?("hexpm:myorg") + end + + test "nil repo is treated as hexpm" do + Hex.State.put(:cooldown_exclude_repos, ["hexpm"]) + assert Cooldown.repo_excluded?(nil) + end + + test "false on empty list" do + Hex.State.put(:cooldown_exclude_repos, []) + refute Cooldown.repo_excluded?("hexpm:myorg") + refute Cooldown.repo_excluded?(nil) + end + end + + describe "preflight_error/4" do + test "includes package, requirement, source, and per-version eligibility lines" do + Hex.State.put(:cooldown, "7d") + cutoff = Cooldown.build_cutoff() + now = System.system_time(:second) + published_at = now - 3 * 86_400 + + message = + Cooldown.preflight_error("phoenix", "~> 1.7", [{"1.7.14", published_at}], cutoff) + + assert message =~ "phoenix" + assert message =~ "~> 1.7" + assert message =~ "1.7.14" + assert message =~ "HEX_COOLDOWN=0" + assert message =~ ~r/eligible \d{4}-/ + end + + test "Wait until shows earliest eligible date across the filtered set" do + Hex.State.put(:cooldown, "7d") + cutoff = Cooldown.build_cutoff() + now = System.system_time(:second) + # Two filtered versions; the one published earlier becomes eligible earlier. + older = now - 5 * 86_400 + newer = now - 1 * 86_400 + + message = + Cooldown.preflight_error( + "phoenix", + "~> 1.7", + [{"1.7.14", newer}, {"1.7.13", older}], + cutoff + ) + + expected_earliest = Cooldown.eligible_on(older, cutoff) + assert message =~ "Wait until #{expected_earliest} and re-run" + end + end +end diff --git a/test/hex/mix_task_test.exs b/test/hex/mix_task_test.exs index 451d54a7..86723baa 100644 --- a/test/hex/mix_task_test.exs +++ b/test/hex/mix_task_test.exs @@ -1128,4 +1128,113 @@ defmodule Hex.MixTaskTest do EctoOverride.Fixture.MixProject ]) end + + describe "cooldown" do + # All registry packages in the integration suite were just published by + # setup_hexpm.exs, so any non-zero cooldown filters them out. The unit + # tests cover env / project / global precedence and the empty-env + # fallthrough; this layer covers the end-to-end resolution behavior. + + test "non-zero cooldown raises a pre-flight error when all candidates are filtered" do + Mix.Project.push(Simple) + + in_tmp(fn -> + Hex.State.put(:cache_home, File.cwd!()) + Hex.State.put(:cooldown, "1d") + + message = + assert_raise Mix.Error, fn -> + Mix.Task.run("deps.get") + end + + assert message.message =~ ~r/cooldown/i + assert message.message =~ "ecto" + end) + after + Hex.State.put(:cooldown, "0d") + + purge([ + Ecto.NoConflict.MixProject, + Postgrex.NoConflict.MixProject, + Ex_doc.NoConflict.MixProject + ]) + end + + test "cooldown 0d (default) resolves normally" do + Mix.Project.push(Simple) + + in_tmp(fn -> + Hex.State.put(:cache_home, File.cwd!()) + Hex.State.put(:cooldown, "0d") + + Mix.Task.run("deps.get") + + assert_received {:mix_shell, :info, ["* Getting ecto (Hex package)"]} + end) + after + purge([ + Ecto.NoConflict.MixProject, + Postgrex.NoConflict.MixProject, + Ex_doc.NoConflict.MixProject + ]) + end + + defmodule TiredPin do + def project do + [ + app: :tired_app, + version: "0.1.0", + consolidate_protocols: false, + deps: [{:tired, "0.1.0"}] + ] + end + end + + defmodule TiredRange do + def project do + [ + app: :tired_app, + version: "0.1.0", + consolidate_protocols: false, + deps: [{:tired, "~> 0.1"}] + ] + end + end + + test "retired locked version escapes cooldown via bypass on deps.update" do + # setup_hexpm.exs publishes the `tired` package with versions 0.1.0 + # (retired) and 0.2.0. Both were published in the test session, so a + # 1-day cooldown filters BOTH from the candidate set. Without the + # unsafe-locked-version bypass, `mix deps.update tired` would + # pre-flight error because every matching version is in cooldown. + # The bypass detects the locked 0.1.0 is retired and lets the + # resolver see the full set, so it picks 0.2.0. + Mix.Project.push(TiredPin) + + in_tmp(fn -> + Hex.State.put(:cache_home, File.cwd!()) + + # Lock at the retired 0.1.0 — cooldown off so this resolves cleanly. + Mix.Task.run("deps.get") + assert %{tired: {:hex, :tired, "0.1.0", _, _, _, _, _}} = Mix.Dep.Lock.read() + + purge([Tired.NoConflict.MixProject]) + Mix.State.clear_cache() + Mix.Project.pop() + Mix.Project.push(TiredRange) + Mix.Task.clear() + + # Now turn on cooldown that would filter every version of tired — + # only the retirement bypass can let the upgrade succeed. + Hex.State.put(:cooldown, "1d") + + Mix.Task.run("deps.update", ["tired"]) + + assert %{tired: {:hex, :tired, "0.2.0", _, _, _, _, _}} = Mix.Dep.Lock.read() + end) + after + Hex.State.put(:cooldown, "0d") + purge([Tired.NoConflict.MixProject]) + end + end end diff --git a/test/hex/registry/cooldown_test.exs b/test/hex/registry/cooldown_test.exs new file mode 100644 index 00000000..ca34f32e --- /dev/null +++ b/test/hex/registry/cooldown_test.exs @@ -0,0 +1,125 @@ +defmodule Hex.Registry.CooldownTest do + use HexTest.Case + alias Hex.Registry.Cooldown + alias Hex.Registry.Server + + setup do + now = System.system_time(:second) + + registry = [ + {:hexpm, :foo, "1.0.0", []}, + {:hexpm, :foo, "1.1.0", []}, + {:hexpm, :foo, "1.2.0", []} + ] + + publish_times = %{ + {:hexpm, :foo, "1.0.0"} => now - 30 * 86_400, + {:hexpm, :foo, "1.1.0"} => now - 14 * 86_400, + {:hexpm, :foo, "1.2.0"} => now - 2 * 86_400 + } + + path = tmp_path("cache.ets") + File.rm(path) + create_test_registry(path, registry, [], publish_times) + + Hex.State.put(:offline, true) + Server.open(registry_path: path) + Server.prefetch([{"hexpm", "foo"}]) + + %{now: now} + end + + test "passes through versions when cooldown disabled" do + Hex.State.put(:cooldown_cutoff, :disabled) + Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + + {:ok, versions} = Cooldown.versions("hexpm", "foo") + assert Enum.map(versions, &to_string/1) == ["1.0.0", "1.1.0", "1.2.0"] + end + + test "filters versions within cooldown window" do + Hex.State.put(:cooldown, "7d") + cutoff = Hex.Cooldown.build_cutoff() + Hex.State.put(:cooldown_cutoff, cutoff) + Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + + {:ok, versions} = Cooldown.versions("hexpm", "foo") + # 1.2.0 published 2 days ago should be filtered + assert Enum.map(versions, &to_string/1) == ["1.0.0", "1.1.0"] + end + + test "bypass set short-circuits filtering for a package" do + Hex.State.put(:cooldown, "60d") + cutoff = Hex.Cooldown.build_cutoff() + Hex.State.put(:cooldown_cutoff, cutoff) + Hex.State.put(:cooldown_bypass_packages, MapSet.new(["foo"])) + + {:ok, versions} = Cooldown.versions("hexpm", "foo") + assert Enum.map(versions, &to_string/1) == ["1.0.0", "1.1.0", "1.2.0"] + end + + test "cooldown_exclude_repos short-circuits filtering for repos in the list" do + Hex.State.put(:cooldown, "60d") + cutoff = Hex.Cooldown.build_cutoff() + Hex.State.put(:cooldown_cutoff, cutoff) + Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_exclude_repos, ["hexpm"]) + + {:ok, versions} = Cooldown.versions("hexpm", "foo") + assert Enum.map(versions, &to_string/1) == ["1.0.0", "1.1.0", "1.2.0"] + after + Hex.State.put(:cooldown_exclude_repos, []) + end + + test "cooldown_exclude_repos does not affect non-excluded repos" do + Hex.State.put(:cooldown, "60d") + cutoff = Hex.Cooldown.build_cutoff() + Hex.State.put(:cooldown_cutoff, cutoff) + Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_exclude_repos, ["hexpm:other"]) + + # hexpm is not in the exclude list — cooldown still applies, filtering + # everything in the fixture (all versions are <60d old). + {:ok, versions} = Cooldown.versions("hexpm", "foo") + assert versions == [] + after + Hex.State.put(:cooldown_exclude_repos, []) + end + + test "nil published_at is treated as eligible" do + now = System.system_time(:second) + + registry = [ + {:hexpm, :legacy, "1.0.0", []}, + {:hexpm, :legacy, "1.1.0", []} + ] + + publish_times = %{ + {:hexpm, :legacy, "1.1.0"} => now - 1 * 86_400 + } + + path = tmp_path("cache_legacy.ets") + File.rm(path) + create_test_registry(path, registry, [], publish_times) + + Server.close() + Hex.State.put(:offline, true) + Server.open(registry_path: path) + Server.prefetch([{"hexpm", "legacy"}]) + + Hex.State.put(:cooldown, "30d") + cutoff = Hex.Cooldown.build_cutoff() + Hex.State.put(:cooldown_cutoff, cutoff) + Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + + {:ok, versions} = Cooldown.versions("hexpm", "legacy") + # 1.0.0 has nil published_at -> eligible; 1.1.0 is too fresh -> filtered + assert Enum.map(versions, &to_string/1) == ["1.0.0"] + end + + test "delegates prefetch and dependencies to Server" do + Code.ensure_loaded!(Cooldown) + assert function_exported?(Cooldown, :prefetch, 1) + assert function_exported?(Cooldown, :dependencies, 3) + end +end diff --git a/test/hex/remote_converger_cooldown_test.exs b/test/hex/remote_converger_cooldown_test.exs new file mode 100644 index 00000000..58258d89 --- /dev/null +++ b/test/hex/remote_converger_cooldown_test.exs @@ -0,0 +1,161 @@ +defmodule Hex.RemoteConvergerCooldownTest do + # Non-integration tests for the cooldown helpers in Hex.RemoteConverger. + # The full integration scenarios live under HexTest.IntegrationCase. + use HexTest.Case + + alias Hex.RemoteConverger + alias Hex.Registry.Server + + setup do + registry = [ + {:hexpm, :good, "1.0.0", []}, + {:hexpm, :retired_dep, "1.0.0", []}, + {:hexpm, :retired_dep, "2.0.0", []}, + {:hexpm, :advised_dep, "1.0.0", []}, + {:hexpm, :advised_dep, "2.0.0", []} + ] + + retired = %{ + # The currently-locked version of retired_dep is retired. + {:hexpm, :retired_dep, "1.0.0"} => %{reason: :RETIRED_SECURITY, message: "CVE"} + } + + # Per-release advisories. Stored under {:advisories, repo, pkg, vsn} as + # a list; non-empty means the version is known-unsafe. + advisories = [ + {{"hexpm", "advised_dep", "1.0.0"}, + [%{id: "GHSA-test-aaaa-bbbb", summary: "test", severity: :SEVERITY_HIGH}]} + ] + + path = tmp_path("cache_bypass.ets") + File.rm(path) + create_test_registry(path, registry, advisories, %{}, retired) + + Server.close() + Hex.State.put(:offline, true) + Server.open(registry_path: path) + Server.prefetch([{"hexpm", "good"}, {"hexpm", "retired_dep"}, {"hexpm", "advised_dep"}]) + + :ok + end + + defp lock_tuple(name, version, repo \\ "hexpm") do + {:hex, String.to_atom(name), version, "checksum", [:mix], [], repo, "outer_checksum"} + end + + defp locked_request(name, version, repo \\ "hexpm") do + %{repo: repo, name: name, app: name, version: version} + end + + @cutoff {:cutoff, System.system_time(:second) - 7 * 86_400, 7 * 86_400} + + describe "build_cooldown_bypass/3 — disabled" do + test "returns empty set when cutoff is :disabled" do + old_lock = %{retired_dep: lock_tuple("retired_dep", "1.0.0")} + assert MapSet.new() == RemoteConverger.build_cooldown_bypass(old_lock, [], :disabled) + end + end + + describe "build_cooldown_bypass/3 — unsafe-locked-version bypass" do + test "includes packages whose locked version is retired" do + old_lock = %{ + retired_dep: lock_tuple("retired_dep", "1.0.0"), + good: lock_tuple("good", "1.0.0") + } + + bypass = RemoteConverger.build_cooldown_bypass(old_lock, [], @cutoff) + + assert MapSet.member?(bypass, "retired_dep") + refute MapSet.member?(bypass, "good") + end + + test "regression: unsafe-bypass walks old_lock so updates can escape retirement" do + # Guards against the bug where the bypass was built from the post- + # prepare_locked value, which removes the package being `mix deps.update`d + # — exactly the package that needs bypass. + old_lock = %{retired_dep: lock_tuple("retired_dep", "1.0.0")} + # locked is empty because deps.update unlocked the package + bypass = RemoteConverger.build_cooldown_bypass(old_lock, [], @cutoff) + + assert MapSet.member?(bypass, "retired_dep") + end + + test "includes packages whose locked version carries a security advisory" do + old_lock = %{advised_dep: lock_tuple("advised_dep", "1.0.0")} + bypass = RemoteConverger.build_cooldown_bypass(old_lock, [], @cutoff) + assert MapSet.member?(bypass, "advised_dep") + end + + test "locked versions with empty advisory list are not bypassed via unsafe-set" do + old_lock = %{advised_dep: lock_tuple("advised_dep", "2.0.0")} + assert MapSet.new() == RemoteConverger.build_cooldown_bypass(old_lock, [], @cutoff) + end + end + + describe "build_cooldown_bypass/3 — lock-satisfied bypass (spec C)" do + test "packages in `locked` (lockfile survived prepare_locked) are bypassed" do + # mix deps.get against an intact lockfile: the dep made it through + # prepare_locked, so it is being installed from the lock. Cooldown + # must not interfere — matches the design's promise that + # \"mix deps.get against an existing lockfile: cooldown does not apply\". + old_lock = %{good: lock_tuple("good", "1.0.0")} + locked = [locked_request("good", "1.0.0")] + + bypass = RemoteConverger.build_cooldown_bypass(old_lock, locked, @cutoff) + + assert MapSet.member?(bypass, "good") + end + + test "packages in old_lock but not in locked (being updated) are not bypassed by lock-satisfied" do + # mix deps.update foo: foo is in old_lock but prepare_locked removed it. + # If foo's locked version is not unsafe, foo should NOT bypass cooldown. + old_lock = %{good: lock_tuple("good", "1.0.0")} + locked = [] + + assert MapSet.new() == RemoteConverger.build_cooldown_bypass(old_lock, locked, @cutoff) + end + + test "new top-level deps (not in old_lock) are not bypassed" do + # User added a new dep to mix.exs: no lock entry, no bypass. + # Cooldown filtering applies normally to fresh additions. + old_lock = %{} + locked = [] + assert MapSet.new() == RemoteConverger.build_cooldown_bypass(old_lock, locked, @cutoff) + end + end + + describe "end-to-end advisory bypass" do + # Wires the bypass set into the wrapper to confirm that an + # advisory-flagged locked version actually unblocks the upgrade path + # — not only that build_cooldown_bypass produces the right MapSet, + # but that the wrapper consumes it and stops filtering. + test "advisory-only locked version unblocks the upgrade through the wrapper" do + # advised_dep 1.0.0 has an advisory; 2.0.0 is clean but assumed young. + old_lock = %{advised_dep: lock_tuple("advised_dep", "1.0.0")} + locked = [] + + bypass = RemoteConverger.build_cooldown_bypass(old_lock, locked, @cutoff) + Hex.State.put(:cooldown_cutoff, @cutoff) + Hex.State.put(:cooldown_bypass_packages, bypass) + + # Without bypass the wrapper would filter every version (no published_at + # in the fixture means eligible, but if we'd populated young + # published_at the bypass is what saves us). Either way, the wrapper + # must return the full version list because the package is bypassed. + {:ok, versions} = Hex.Registry.Cooldown.versions("hexpm", "advised_dep") + assert Enum.map(versions, &to_string/1) == ["1.0.0", "2.0.0"] + end + + test "retired locked version unblocks the upgrade through the wrapper" do + old_lock = %{retired_dep: lock_tuple("retired_dep", "1.0.0")} + locked = [] + + bypass = RemoteConverger.build_cooldown_bypass(old_lock, locked, @cutoff) + Hex.State.put(:cooldown_cutoff, @cutoff) + Hex.State.put(:cooldown_bypass_packages, bypass) + + {:ok, versions} = Hex.Registry.Cooldown.versions("hexpm", "retired_dep") + assert Enum.map(versions, &to_string/1) == ["1.0.0", "2.0.0"] + end + end +end diff --git a/test/mix/tasks/hex.config_test.exs b/test/mix/tasks/hex.config_test.exs index f74e9e19..d291271c 100644 --- a/test/mix/tasks/hex.config_test.exs +++ b/test/mix/tasks/hex.config_test.exs @@ -25,11 +25,30 @@ defmodule Mix.Tasks.Hex.ConfigTest do assert_received {:mix_shell, :info, ["trusted_mirror_url: nil (default)"]} assert_received {:mix_shell, :info, ["config_home:" <> _]} assert_received {:mix_shell, :info, ["no_short_urls: false (default)"]} + assert_received {:mix_shell, :info, ["cooldown: \"0d\" (default)"]} end) after purge([ReleaseCustomApiUrl.MixProject]) end + test "cooldown config key" do + in_tmp(fn -> + System.put_env("HEX_HOME", File.cwd!()) + Hex.State.refresh() + + Mix.Tasks.Hex.Config.run(["cooldown"]) + assert_received {:mix_shell, :info, ["\"0d\""]} + + System.put_env("HEX_COOLDOWN", "7d") + Hex.State.refresh() + Mix.Tasks.Hex.Config.run(["cooldown"]) + assert_received {:mix_shell, :info, ["\"7d\""]} + + System.delete_env("HEX_COOLDOWN") + Hex.State.refresh() + end) + end + test "config key" do in_tmp(fn -> System.put_env("HEX_HOME", File.cwd!()) diff --git a/test/support/case.ex b/test/support/case.ex index 4c13912a..9f39d55a 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -119,7 +119,7 @@ defmodule HexTest.Case do @ets_table :hex_index - def create_test_registry(path, registry, advisories \\ []) do + def create_test_registry(path, registry, advisories \\ [], publish_times \\ %{}, retired \\ %{}) do registry = Enum.sort(registry) versions = @@ -143,10 +143,10 @@ defmodule HexTest.Case do {{Atom.to_string(outer_repo), Atom.to_string(name), vsn}, deps} end) - create_registry(path, versions, deps, advisories) + create_registry(path, versions, deps, advisories, publish_times, retired) end - defp create_registry(path, versions, deps, advisories) do + defp create_registry(path, versions, deps, advisories, publish_times, retired) do tid = :ets.new(@ets_table, []) versions = @@ -164,7 +164,21 @@ defmodule HexTest.Case do {{:advisories, repo, pkg, vsn}, adv} end) - :ets.insert(tid, versions ++ deps ++ advisories ++ [{:version, 4}]) + published_at = + Enum.map(publish_times, fn {{repo, pkg, vsn}, ts} -> + {{:published_at, Atom.to_string(repo), Atom.to_string(pkg), vsn}, ts} + end) + + retired = + Enum.map(retired, fn {{repo, pkg, vsn}, retirement} -> + {{:retired, Atom.to_string(repo), Atom.to_string(pkg), vsn}, retirement} + end) + + :ets.insert( + tid, + versions ++ deps ++ advisories ++ published_at ++ retired ++ [{:version, 5}] + ) + File.mkdir_p!(Path.dirname(path)) :ok = :ets.tab2file(tid, String.to_charlist(path)) :ets.delete(tid) From 6d1e52b5fc69e39352077a691959a95ba04dda4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Wed, 20 May 2026 21:35:55 +0200 Subject: [PATCH 2/3] Treat cooldown as a filter, not a hard error Versions currently in the lockfile are exempt from cooldown filtering, so re-resolution can fall back to them when no newer eligible candidate exists instead of aborting the whole run. The direct-dep pre-flight raise is removed; both direct and transitive cooldown dead ends now produce the solver's own "no matching versions" message, followed by a summary of every version filtered during the solve. The summary prints on successful resolves too, so users can see which updates cooldown is holding back. --- lib/hex/cooldown.ex | 110 ++++++-------------- lib/hex/registry/cooldown.ex | 39 +++++-- lib/hex/remote_converger.ex | 89 ++++------------ test/hex/cooldown_test.exs | 91 ++++++++++------ test/hex/mix_task_test.exs | 25 +++-- test/hex/registry/cooldown_test.exs | 83 +++++++++++++++ test/hex/remote_converger_cooldown_test.exs | 41 ++++++++ 7 files changed, 292 insertions(+), 186 deletions(-) diff --git a/lib/hex/cooldown.ex b/lib/hex/cooldown.ex index 260e3f27..42bed05e 100644 --- a/lib/hex/cooldown.ex +++ b/lib/hex/cooldown.ex @@ -130,85 +130,43 @@ defmodule Hex.Cooldown do end @doc """ - Returns the effective duration string for the configured cutoff. - """ - @spec describe_duration(cutoff()) :: String.t() - def describe_duration(:disabled), do: "0" + Formats the post-solver summary of versions skipped by cooldown. - def describe_duration({:cutoff, _, window_seconds}) do - cond do - rem(window_seconds, 86_400 * 7) == 0 -> "#{div(window_seconds, 86_400 * 7)} weeks" - true -> "#{div(window_seconds, 86_400)} days" - end - end + Takes a list of `{repo, package, version, published_at}` tuples accumulated + during the solve. Returns `nil` when nothing eligible to report — empty + input, disabled cutoff, or every entry missing a `published_at`. - @doc """ - Describes the configuration source that contributed the local cooldown. - Used in error messages. + Entries are deduplicated and sorted by package then version. Repo is used + for keying only; it is not rendered. """ - @spec describe_source() :: String.t() - def describe_source() do - case Hex.State.fetch_source!(:cooldown) do - {:env, var} -> "#{var}" - {:project_config, key} -> "mix.exs (#{key})" - {:global_config, key} -> "~/.hex/hex.config (#{key})" - :default -> "default" - :computed -> "runtime" + @spec format_summary([{String.t(), String.t(), String.t(), integer() | nil}], cutoff()) :: + String.t() | nil + def format_summary(_entries, :disabled), do: nil + def format_summary([], _cutoff), do: nil + + def format_summary(entries, cutoff) do + entries = + entries + |> Enum.reject(fn {_repo, _pkg, _vsn, published_at} -> is_nil(published_at) end) + |> Enum.uniq() + |> Enum.sort_by(fn {repo, pkg, vsn, _} -> {repo, pkg, vsn} end) + + case entries do + [] -> + nil + + entries -> + today = Date.utc_today() + + lines = + Enum.map(entries, fn {_repo, pkg, vsn, published_at} -> + published_date = published_at |> DateTime.from_unix!() |> DateTime.to_date() + days_ago = Date.diff(today, published_date) + eligible_date = eligible_on(published_at, cutoff) + " #{pkg} #{vsn} — published #{days_ago} days ago, eligible #{eligible_date}" + end) + + "\nVersions filtered by cooldown:\n" <> Enum.join(lines, "\n") <> "\n" end end - - @doc """ - Builds the pre-flight error message for a direct dependency whose entire - matching version set has been filtered by cooldown. - """ - @spec preflight_error(String.t(), String.t(), [{String.t(), integer()}], cutoff()) :: String.t() - def preflight_error(package, requirement, filtered, cutoff) do - today = Date.utc_today() - - lines = - Enum.map(filtered, fn {version, published_at} -> - published_date = published_at |> DateTime.from_unix!() |> DateTime.to_date() - days_ago = Date.diff(today, published_date) - eligible_date = eligible_on(published_at, cutoff) - - " #{version} published #{published_date} (#{days_ago} days ago), eligible #{eligible_date}" - end) - - earliest_eligible = - filtered - |> Enum.map(fn {_version, published_at} -> eligible_on(published_at, cutoff) end) - |> Enum.min(Date) - - duration = describe_duration(cutoff) - source = describe_source() - - """ - All versions of "#{package}" matching "#{requirement}" are in cooldown: - - #{Enum.join(lines, "\n")} - - Effective cooldown is #{duration} (#{source}). - - To proceed: - * Wait until #{earliest_eligible} and re-run - * Bypass for this run: HEX_COOLDOWN=0 mix deps.get - """ - end - - @doc """ - Note appended to solver failures when cooldown is active and a transitive - dependency may have been filtered out. - """ - @spec solver_failure_note(cutoff()) :: String.t() | nil - def solver_failure_note(:disabled), do: nil - - def solver_failure_note(cutoff) do - duration = describe_duration(cutoff) - - """ - - Note: cooldown is set to #{duration}. If a dependency was filtered because - it was too recently published, re-run with HEX_COOLDOWN=0 to bypass. - """ - end end diff --git a/lib/hex/registry/cooldown.ex b/lib/hex/registry/cooldown.ex index d2669ec7..53c2410b 100644 --- a/lib/hex/registry/cooldown.ex +++ b/lib/hex/registry/cooldown.ex @@ -29,7 +29,10 @@ defmodule Hex.Registry.Cooldown do {:ok, versions} true -> - {:ok, filter(versions, repo, package, cutoff)} + locked = + Map.get(Hex.State.fetch!(:cooldown_locked_versions), {repo || "hexpm", package}, []) + + {:ok, filter(versions, repo, package, cutoff, locked)} end :error -> @@ -37,10 +40,34 @@ defmodule Hex.Registry.Cooldown do end end - defp filter(versions, repo, package, cutoff) do - Enum.filter(versions, fn version -> - published_at = Server.published_at(repo, package, to_string(version)) - Hex.Cooldown.eligible?(published_at, cutoff) - end) + defp filter(versions, repo, package, cutoff, locked) do + {eligible, filtered_out} = + Enum.split_with(versions, fn version -> + version_str = to_string(version) + + if version_str in locked do + true + else + published_at = Server.published_at(repo, package, version_str) + Hex.Cooldown.eligible?(published_at, cutoff) + end + end) + + record_filtered(repo, package, filtered_out) + eligible + end + + defp record_filtered(_repo, _package, []), do: :ok + + defp record_filtered(repo, package, versions) do + repo_key = repo || "hexpm" + + entries = + Enum.map(versions, fn version -> + version_str = to_string(version) + {repo_key, package, version_str, Server.published_at(repo, package, version_str)} + end) + + Hex.State.update!(:cooldown_filtered_versions, &(entries ++ &1)) end end diff --git a/lib/hex/remote_converger.ex b/lib/hex/remote_converger.ex index 494b3f12..8c09176c 100644 --- a/lib/hex/remote_converger.ex +++ b/lib/hex/remote_converger.ex @@ -72,8 +72,6 @@ defmodule Hex.RemoteConverger do overridden = Enum.map(overridden, &Atom.to_string/1) verify_otp_app_names(dependencies) - cooldown_preflight(requests) - level = Logger.level() Logger.configure(level: if(Hex.State.fetch!(:debug_solver), do: :debug, else: :info)) @@ -97,11 +95,12 @@ defmodule Hex.RemoteConverger do case solution do {:ok, resolved} -> resolved = normalize_resolved(resolved) + print_cooldown_summary() solver_success(resolved, requests, lock, old_lock) {:error, message} -> Hex.Shell.info(message) - maybe_print_cooldown_hint() + print_cooldown_summary() Mix.raise("Hex dependency resolution failed") end end @@ -112,6 +111,22 @@ defmodule Hex.RemoteConverger do bypass = build_cooldown_bypass(old_lock, locked, cutoff) Hex.State.put(:cooldown_bypass_packages, bypass) + + Hex.State.put(:cooldown_locked_versions, build_cooldown_locked_versions(old_lock)) + Hex.State.put(:cooldown_filtered_versions, []) + end + + @doc false + def build_cooldown_locked_versions(old_lock) do + # Maps {repo, package} to the list of versions currently in the lockfile + # for that package. The cooldown filter treats these as exempt: a version + # the user is already running is trusted, even if cooldown would otherwise + # filter it. This lets re-resolution fall back to the locked version when + # no newer eligible candidate exists, instead of failing. + for %{repo: repo, name: name, version: version} <- Hex.Mix.from_lock(old_lock), + into: %{} do + {{repo || "hexpm", name}, [to_string(version)]} + end end @doc false @@ -147,71 +162,13 @@ defmodule Hex.RemoteConverger do Registry.advisories(repo, name, version) not in [nil, []] end - defp cooldown_preflight(requests) do - cutoff = Hex.State.fetch!(:cooldown_cutoff) - bypass = Hex.State.fetch!(:cooldown_bypass_packages) - - if cutoff != :disabled do - walk_preflight(requests, cutoff, bypass) - end - end - - defp walk_preflight(requests, cutoff, bypass) do - # Both Hex and non-Hex requests carry :requirement (nil for non-Hex - # parents). check_request_cooldown is a no-op for nil-requirement - # requests, so the walk uniformly visits every node and recurses into - # nested deps under non-Hex (path / git) parents. - Enum.each(requests, fn request -> - cond do - MapSet.member?(bypass, request.name) -> :ok - Hex.Cooldown.repo_excluded?(request[:repo]) -> :ok - true -> check_request_cooldown(request, cutoff) - end - - walk_preflight(Map.get(request, :dependencies, []), cutoff, bypass) - end) - end - - defp check_request_cooldown(%{requirement: nil}, _cutoff), do: :ok - - defp check_request_cooldown(%{repo: repo, name: name, requirement: requirement}, cutoff) do - with {:ok, parsed} <- Version.parse_requirement(requirement), - {:ok, versions} <- Registry.versions(repo, name) do - matching = - versions - |> Enum.filter(&Version.match?(&1, parsed)) - |> Enum.map(&to_string/1) - - filtered = - Enum.flat_map(matching, fn version -> - case Registry.published_at(repo, name, version) do - nil -> - [] - - published_at -> - if Hex.Cooldown.eligible?(published_at, cutoff) do - [] - else - [{version, published_at}] - end - end - end) - - if matching != [] and length(filtered) == length(matching) do - Mix.raise(Hex.Cooldown.preflight_error(name, requirement, filtered, cutoff)) - end - else - _ -> :ok - end - end - - defp check_request_cooldown(_, _), do: :ok - - defp maybe_print_cooldown_hint() do + @doc false + def print_cooldown_summary() do cutoff = Hex.State.fetch!(:cooldown_cutoff) + entries = Hex.State.fetch!(:cooldown_filtered_versions) - if note = Hex.Cooldown.solver_failure_note(cutoff) do - Hex.Shell.info(note) + if summary = Hex.Cooldown.format_summary(entries, cutoff) do + Hex.Shell.info(summary) end end diff --git a/test/hex/cooldown_test.exs b/test/hex/cooldown_test.exs index 0b37a89f..0b48ff34 100644 --- a/test/hex/cooldown_test.exs +++ b/test/hex/cooldown_test.exs @@ -89,13 +89,6 @@ defmodule Hex.CooldownTest do end end - describe "describe_source/0" do - test "reflects HEX_COOLDOWN env var" do - Hex.State.put(:cooldown, "7d") - # Hex.State sources are set during init; force via direct setter - assert is_binary(Cooldown.describe_source()) - end - end describe "HEX_COOLDOWN= env handling" do test "empty HEX_COOLDOWN= falls through to next source, not env" do @@ -219,41 +212,77 @@ defmodule Hex.CooldownTest do end end - describe "preflight_error/4" do - test "includes package, requirement, source, and per-version eligibility lines" do + describe "format_summary/2" do + test "returns nil for an empty list" do + Hex.State.put(:cooldown, "7d") + cutoff = Cooldown.build_cutoff() + assert Cooldown.format_summary([], cutoff) == nil + end + + test "returns nil when cutoff is :disabled" do + now = System.system_time(:second) + assert Cooldown.format_summary([{"hexpm", "foo", "1.0.0", now}], :disabled) == nil + end + + test "renders a single entry with days ago and eligible date" do Hex.State.put(:cooldown, "7d") cutoff = Cooldown.build_cutoff() now = System.system_time(:second) published_at = now - 3 * 86_400 - message = - Cooldown.preflight_error("phoenix", "~> 1.7", [{"1.7.14", published_at}], cutoff) + summary = Cooldown.format_summary([{"hexpm", "castore", "1.0.19", published_at}], cutoff) + + assert summary =~ "Versions filtered by cooldown:" + assert summary =~ "castore 1.0.19" + assert summary =~ "3 days ago" + assert summary =~ "eligible #{Cooldown.eligible_on(published_at, cutoff)}" + end + + test "sorts by package then version and dedupes identical entries" do + Hex.State.put(:cooldown, "7d") + cutoff = Cooldown.build_cutoff() + now = System.system_time(:second) + young = now - 1 * 86_400 + + entries = [ + {"hexpm", "plug", "1.16.5", young}, + {"hexpm", "castore", "1.0.19", young}, + {"hexpm", "castore", "1.0.19", young}, + {"hexpm", "castore", "1.0.18", young} + ] - assert message =~ "phoenix" - assert message =~ "~> 1.7" - assert message =~ "1.7.14" - assert message =~ "HEX_COOLDOWN=0" - assert message =~ ~r/eligible \d{4}-/ + summary = Cooldown.format_summary(entries, cutoff) + + # Ordering: castore 1.0.18, castore 1.0.19, plug 1.16.5; dedup keeps one castore 1.0.19. + [_header, line1, line2, line3 | _] = String.split(summary, "\n", trim: true) + assert line1 =~ "castore 1.0.18" + assert line2 =~ "castore 1.0.19" + assert line3 =~ "plug 1.16.5" + + occurrences = summary |> String.split("castore 1.0.19") |> length() + assert occurrences == 2 end - test "Wait until shows earliest eligible date across the filtered set" do + test "omits entries with nil published_at" do Hex.State.put(:cooldown, "7d") cutoff = Cooldown.build_cutoff() now = System.system_time(:second) - # Two filtered versions; the one published earlier becomes eligible earlier. - older = now - 5 * 86_400 - newer = now - 1 * 86_400 - - message = - Cooldown.preflight_error( - "phoenix", - "~> 1.7", - [{"1.7.14", newer}, {"1.7.13", older}], - cutoff - ) - - expected_earliest = Cooldown.eligible_on(older, cutoff) - assert message =~ "Wait until #{expected_earliest} and re-run" + + entries = [ + {"hexpm", "legacy", "1.0.0", nil}, + {"hexpm", "castore", "1.0.19", now - 1 * 86_400} + ] + + summary = Cooldown.format_summary(entries, cutoff) + + assert summary =~ "castore 1.0.19" + refute summary =~ "legacy" + end + + test "returns nil when every entry has nil published_at" do + Hex.State.put(:cooldown, "7d") + cutoff = Cooldown.build_cutoff() + assert Cooldown.format_summary([{"hexpm", "legacy", "1.0.0", nil}], cutoff) == nil end end end diff --git a/test/hex/mix_task_test.exs b/test/hex/mix_task_test.exs index 86723baa..4cefc4ca 100644 --- a/test/hex/mix_task_test.exs +++ b/test/hex/mix_task_test.exs @@ -1,6 +1,15 @@ defmodule Hex.MixTaskTest do use HexTest.IntegrationCase + defp drain_shell_info_matching(substring) do + receive do + {:mix_shell, :info, [text]} -> + if text =~ substring, do: text, else: drain_shell_info_matching(substring) + after + 0 -> nil + end + end + defmodule Simple do def project do [ @@ -1135,20 +1144,22 @@ defmodule Hex.MixTaskTest do # tests cover env / project / global precedence and the empty-env # fallthrough; this layer covers the end-to-end resolution behavior. - test "non-zero cooldown raises a pre-flight error when all candidates are filtered" do + test "non-zero cooldown lets the solver fail and prints a filtered-versions summary" do Mix.Project.push(Simple) in_tmp(fn -> Hex.State.put(:cache_home, File.cwd!()) Hex.State.put(:cooldown, "1d") - message = - assert_raise Mix.Error, fn -> - Mix.Task.run("deps.get") - end + assert_raise Mix.Error, "Hex dependency resolution failed", fn -> + Mix.Task.run("deps.get") + end - assert message.message =~ ~r/cooldown/i - assert message.message =~ "ecto" + # Solver prints its own "no matching versions" message; Hex appends a + # post-solver summary naming the ecto versions filtered by cooldown. + summary = drain_shell_info_matching("Versions filtered by cooldown:") + assert summary + assert summary =~ "ecto" end) after Hex.State.put(:cooldown, "0d") diff --git a/test/hex/registry/cooldown_test.exs b/test/hex/registry/cooldown_test.exs index ca34f32e..aea9cc44 100644 --- a/test/hex/registry/cooldown_test.exs +++ b/test/hex/registry/cooldown_test.exs @@ -32,6 +32,8 @@ defmodule Hex.Registry.CooldownTest do test "passes through versions when cooldown disabled" do Hex.State.put(:cooldown_cutoff, :disabled) Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) {:ok, versions} = Cooldown.versions("hexpm", "foo") assert Enum.map(versions, &to_string/1) == ["1.0.0", "1.1.0", "1.2.0"] @@ -42,6 +44,8 @@ defmodule Hex.Registry.CooldownTest do cutoff = Hex.Cooldown.build_cutoff() Hex.State.put(:cooldown_cutoff, cutoff) Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) {:ok, versions} = Cooldown.versions("hexpm", "foo") # 1.2.0 published 2 days ago should be filtered @@ -63,6 +67,8 @@ defmodule Hex.Registry.CooldownTest do cutoff = Hex.Cooldown.build_cutoff() Hex.State.put(:cooldown_cutoff, cutoff) Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) Hex.State.put(:cooldown_exclude_repos, ["hexpm"]) {:ok, versions} = Cooldown.versions("hexpm", "foo") @@ -76,6 +82,8 @@ defmodule Hex.Registry.CooldownTest do cutoff = Hex.Cooldown.build_cutoff() Hex.State.put(:cooldown_cutoff, cutoff) Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) Hex.State.put(:cooldown_exclude_repos, ["hexpm:other"]) # hexpm is not in the exclude list — cooldown still applies, filtering @@ -111,6 +119,8 @@ defmodule Hex.Registry.CooldownTest do cutoff = Hex.Cooldown.build_cutoff() Hex.State.put(:cooldown_cutoff, cutoff) Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) {:ok, versions} = Cooldown.versions("hexpm", "legacy") # 1.0.0 has nil published_at -> eligible; 1.1.0 is too fresh -> filtered @@ -122,4 +132,77 @@ defmodule Hex.Registry.CooldownTest do assert function_exported?(Cooldown, :prefetch, 1) assert function_exported?(Cooldown, :dependencies, 3) end + + test "version present in :cooldown_locked_versions survives the filter" do + # An in-cooldown version that is currently in the lockfile must remain + # a solver candidate so re-resolution can fall back to it when no newer + # eligible version exists. + Hex.State.put(:cooldown, "7d") + cutoff = Hex.Cooldown.build_cutoff() + Hex.State.put(:cooldown_cutoff, cutoff) + Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) + Hex.State.put(:cooldown_locked_versions, %{{"hexpm", "foo"} => ["1.2.0"]}) + + {:ok, versions} = Cooldown.versions("hexpm", "foo") + assert Enum.map(versions, &to_string/1) == ["1.0.0", "1.1.0", "1.2.0"] + end + + test "locked-version exemption only applies to the matching repo+package+version" do + Hex.State.put(:cooldown, "7d") + cutoff = Hex.Cooldown.build_cutoff() + Hex.State.put(:cooldown_cutoff, cutoff) + Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) + # Stale entry naming a different version — does not save 1.2.0. + Hex.State.put(:cooldown_locked_versions, %{{"hexpm", "foo"} => ["1.1.0"]}) + + {:ok, versions} = Cooldown.versions("hexpm", "foo") + assert Enum.map(versions, &to_string/1) == ["1.0.0", "1.1.0"] + end + + describe "filtered-versions recording" do + test "records each filtered version with its published_at", %{now: now} do + Hex.State.put(:cooldown, "7d") + cutoff = Hex.Cooldown.build_cutoff() + Hex.State.put(:cooldown_cutoff, cutoff) + Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) + Hex.State.put(:cooldown_filtered_versions, []) + + {:ok, _} = Cooldown.versions("hexpm", "foo") + + filtered = Hex.State.fetch!(:cooldown_filtered_versions) + assert [{"hexpm", "foo", "1.2.0", published_at}] = filtered + assert_in_delta published_at, now - 2 * 86_400, 2 + end + + test "does not record versions that survive via the locked exemption" do + Hex.State.put(:cooldown, "7d") + cutoff = Hex.Cooldown.build_cutoff() + Hex.State.put(:cooldown_cutoff, cutoff) + Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_locked_versions, %{{"hexpm", "foo"} => ["1.2.0"]}) + Hex.State.put(:cooldown_filtered_versions, []) + + {:ok, _} = Cooldown.versions("hexpm", "foo") + + assert Hex.State.fetch!(:cooldown_filtered_versions) == [] + end + + test "records nothing when cooldown is disabled" do + Hex.State.put(:cooldown_cutoff, :disabled) + Hex.State.put(:cooldown_bypass_packages, MapSet.new()) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) + Hex.State.put(:cooldown_filtered_versions, []) + + {:ok, _} = Cooldown.versions("hexpm", "foo") + + assert Hex.State.fetch!(:cooldown_filtered_versions) == [] + end + end end diff --git a/test/hex/remote_converger_cooldown_test.exs b/test/hex/remote_converger_cooldown_test.exs index 58258d89..9cac26d4 100644 --- a/test/hex/remote_converger_cooldown_test.exs +++ b/test/hex/remote_converger_cooldown_test.exs @@ -124,6 +124,43 @@ defmodule Hex.RemoteConvergerCooldownTest do end end + describe "print_cooldown_summary/0" do + test "prints the summary when filtered versions were recorded" do + now = System.system_time(:second) + Hex.State.put(:cooldown, "7d") + Hex.State.put(:cooldown_cutoff, Hex.Cooldown.build_cutoff()) + + Hex.State.put(:cooldown_filtered_versions, [ + {"hexpm", "castore", "1.0.19", now - 6 * 86_400} + ]) + + RemoteConverger.print_cooldown_summary() + + assert_received {:mix_shell, :info, [output]} + assert output =~ "Versions filtered by cooldown:" + assert output =~ "castore 1.0.19" + end + + test "prints nothing when nothing was filtered" do + Hex.State.put(:cooldown, "7d") + Hex.State.put(:cooldown_cutoff, Hex.Cooldown.build_cutoff()) + Hex.State.put(:cooldown_filtered_versions, []) + + RemoteConverger.print_cooldown_summary() + + refute_received {:mix_shell, :info, _} + end + + test "prints nothing when cooldown is disabled" do + Hex.State.put(:cooldown_cutoff, :disabled) + Hex.State.put(:cooldown_filtered_versions, []) + + RemoteConverger.print_cooldown_summary() + + refute_received {:mix_shell, :info, _} + end + end + describe "end-to-end advisory bypass" do # Wires the bypass set into the wrapper to confirm that an # advisory-flagged locked version actually unblocks the upgrade path @@ -137,6 +174,8 @@ defmodule Hex.RemoteConvergerCooldownTest do bypass = RemoteConverger.build_cooldown_bypass(old_lock, locked, @cutoff) Hex.State.put(:cooldown_cutoff, @cutoff) Hex.State.put(:cooldown_bypass_packages, bypass) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) # Without bypass the wrapper would filter every version (no published_at # in the fixture means eligible, but if we'd populated young @@ -153,6 +192,8 @@ defmodule Hex.RemoteConvergerCooldownTest do bypass = RemoteConverger.build_cooldown_bypass(old_lock, locked, @cutoff) Hex.State.put(:cooldown_cutoff, @cutoff) Hex.State.put(:cooldown_bypass_packages, bypass) + Hex.State.put(:cooldown_locked_versions, %{}) + Hex.State.put(:cooldown_filtered_versions, []) {:ok, versions} = Hex.Registry.Cooldown.versions("hexpm", "retired_dep") assert Enum.map(versions, &to_string/1) == ["1.0.0", "2.0.0"] From 6bb470eb24404f59baf9ebf5a4718be5bd935e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Wed, 20 May 2026 21:45:02 +0200 Subject: [PATCH 3/3] Format cooldown_test.exs --- test/hex/cooldown_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/hex/cooldown_test.exs b/test/hex/cooldown_test.exs index 0b48ff34..204b06f6 100644 --- a/test/hex/cooldown_test.exs +++ b/test/hex/cooldown_test.exs @@ -89,7 +89,6 @@ defmodule Hex.CooldownTest do end end - describe "HEX_COOLDOWN= env handling" do test "empty HEX_COOLDOWN= falls through to next source, not env" do original = System.get_env("HEX_COOLDOWN")