Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions lib/hex/cooldown.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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(<<digit, _::binary>> = 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
Expand Down
5 changes: 3 additions & 2 deletions lib/mix/tasks/hex.build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions test/hex/cooldown_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions test/hex/no_regex_test.exs
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions test/mix/tasks/hex.build_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Loading