From 981e52bd62f2bec9c061f5513831cf0258ed462b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Wed, 1 Jul 2026 16:19:40 -0600 Subject: [PATCH 1/4] Replace regex with Integer.parse in cooldown duration parsing Regex literals are compiled into beam files, and the compiled PCRE pattern format is not portable across OTP versions. An archive built on OTP 28.1+ embeds patterns that load via :re.import/1, which does not exist on OTP <= 28.0, crashing with UndefinedFunctionError. --- lib/hex/cooldown.ex | 11 ++++++----- test/hex/cooldown_test.exs | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/hex/cooldown.ex b/lib/hex/cooldown.ex index ece91156..53b93d8c 100644 --- a/lib/hex/cooldown.ex +++ b/lib/hex/cooldown.ex @@ -70,15 +70,16 @@ defmodule Hex.Cooldown do @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 + # The leading-digit guard rejects the signs Integer.parse/1 accepts ("+5d", "-5d") + def duration_to_seconds(<> = string) when digit in ?0..?9 do + case Integer.parse(string) do + {n, unit} when unit in ["d", "w", "mo"] -> {:ok, n * unit_seconds(unit)} _ -> :error end end + def duration_to_seconds(string) when is_binary(string), do: :error + defp unit_seconds("d"), do: 86_400 defp unit_seconds("w"), do: 86_400 * 7 defp unit_seconds("mo"), do: 86_400 * 30 diff --git a/test/hex/cooldown_test.exs b/test/hex/cooldown_test.exs index e61059b6..dd9ad15d 100644 --- a/test/hex/cooldown_test.exs +++ b/test/hex/cooldown_test.exs @@ -37,6 +37,21 @@ defmodule Hex.CooldownTest do assert {:ok, 14 * 86_400} == Cooldown.duration_to_seconds("2w") assert {:ok, 30 * 86_400} == Cooldown.duration_to_seconds("1mo") end + + test "rejects malformed durations" do + assert :error == Cooldown.duration_to_seconds("") + assert :error == Cooldown.duration_to_seconds("5") + assert :error == Cooldown.duration_to_seconds("00") + assert :error == Cooldown.duration_to_seconds("d") + assert :error == Cooldown.duration_to_seconds("5m") + assert :error == Cooldown.duration_to_seconds("5x") + assert :error == Cooldown.duration_to_seconds("5dd") + assert :error == Cooldown.duration_to_seconds("+5d") + assert :error == Cooldown.duration_to_seconds("-5d") + assert :error == Cooldown.duration_to_seconds("1_0d") + assert :error == Cooldown.duration_to_seconds(" 5d") + assert :error == Cooldown.duration_to_seconds("5d ") + end end describe "build_cutoff/0" do From f5d0a8efe0e3712f9d35069a1f8ea367e276d1dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Wed, 1 Jul 2026 16:28:15 -0600 Subject: [PATCH 2/4] Replace .DS_Store exclusion regex with Path.basename check Keeps the compiled archive free of embedded regex patterns, which are not portable across OTP versions. User-supplied :exclude_patterns regexes are unaffected; they are built in the user's project at runtime and never embedded in Hex's beams. --- lib/mix/tasks/hex.build.ex | 5 +++-- test/mix/tasks/hex.build_test.exs | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/hex.build.ex b/lib/mix/tasks/hex.build.ex index 2c91011b..453e93d8 100644 --- a/lib/mix/tasks/hex.build.ex +++ b/lib/mix/tasks/hex.build.ex @@ -251,13 +251,14 @@ defmodule Mix.Tasks.Hex.Build do @doc false def package(package, config) do files = package[:files] || Hex.Package.default_files() - exclude_patterns = (package[:exclude_patterns] || []) ++ [~r/\W\.DS_Store$/] + exclude_patterns = package[:exclude_patterns] || [] files = files |> expand_paths(File.cwd!()) |> Enum.reject(fn path -> - Enum.any?(exclude_patterns, &(path =~ &1)) + Path.basename(path) == ".DS_Store" or + Enum.any?(exclude_patterns, &(path =~ &1)) end) package diff --git a/test/mix/tasks/hex.build_test.exs b/test/mix/tasks/hex.build_test.exs index a4e2938d..8d24f254 100644 --- a/test/mix/tasks/hex.build_test.exs +++ b/test/mix/tasks/hex.build_test.exs @@ -149,6 +149,7 @@ defmodule Mix.Tasks.Hex.BuildTest do File.write!("myfile.txt", "hello") File.write!("executable.sh", "world") File.write!("dir/dir2/test.txt", "and") + File.write!("dir/.DS_Store", "junk") File.chmod!("myfile.txt", 0o100644) File.chmod!("executable.sh", 0o100755) File.chmod!("dir/dir2/test.txt", 0o100644) @@ -170,6 +171,7 @@ defmodule Mix.Tasks.Hex.BuildTest do assert File.read!("unzip/myfile.txt") == "hello" assert File.read!("unzip/dir/.dotfile") == "" assert File.read!("unzip/dir/dir2/test.txt") == "and" + refute File.exists?("unzip/dir/.DS_Store") assert File.stat!("unzip/myfile.txt").mode == 0o100644 assert File.stat!("unzip/executable.sh").mode == 0o100755 end) From 812d90c0f38255b3ee12c09f55854b1fd8930b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Wed, 1 Jul 2026 16:32:11 -0600 Subject: [PATCH 3/4] Add lint test rejecting regexes in shipped code --- test/hex/no_regex_test.exs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/hex/no_regex_test.exs diff --git a/test/hex/no_regex_test.exs b/test/hex/no_regex_test.exs new file mode 100644 index 00000000..d8d86772 --- /dev/null +++ b/test/hex/no_regex_test.exs @@ -0,0 +1,23 @@ +defmodule Hex.NoRegexTest do + use ExUnit.Case, async: true + + # Compiled regexes are embedded into beam files, and the compiled PCRE + # pattern format is not portable across OTP versions (for example, + # :re.import/1 does not exist before OTP 28.1). The Hex archive is + # precompiled once and installed on many OTP versions, so code under + # lib/ must not use regexes; parse with pattern matching instead. + test "lib/ contains no regexes" do + paths = Path.wildcard("lib/**/*.ex") + assert paths != [] + + offenders = + Enum.filter(paths, fn path -> + content = File.read!(path) + String.contains?(content, ["~r", "~R", "Regex.", ":re."]) + end) + + assert offenders == [], + "Regex usage found in shipped code: #{inspect(offenders)}. " <> + "Regexes must not appear under lib/, see comment in #{__ENV__.file}" + end +end From 7a21131b1e9283c319e9f9554d96275b02ac0541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Wed, 1 Jul 2026 16:36:29 -0600 Subject: [PATCH 4/4] Update CHANGELOG for regex removal --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5457010..d179d5ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v2.5.1-dev +### Bug fixes + +* Remove regexes from compiled code. Compiled regex patterns are not portable across OTP versions, so the precompiled archive could crash with `:re.import/1 is undefined` when built and run on different OTP versions + ## v2.5.0 (2026-06-28) ### Enhancements