Skip to content

test: cover Utils.safe_filename and .safe_filename? - #23773

Merged
MikeMcQuaid merged 1 commit into
Homebrew:mainfrom
rawsun007:test/safe-filename-specs
Sep 4, 2026
Merged

test: cover Utils.safe_filename and .safe_filename?#23773
MikeMcQuaid merged 1 commit into
Homebrew:mainfrom
rawsun007:test/safe-filename-specs

Conversation

@rawsun007

Copy link
Copy Markdown
Contributor

  • Have you followed our Contributing guidelines?
  • Have you checked for other open Pull Requests for the same change?
  • Have you explained what your changes do? Performance claims (e.g. "this is faster") must include brew benchmark results.
  • Have you explained why you'd like these changes included, not just what they do?
  • For bug fixes, have you given step-by-step brew commands to reproduce the bug?
  • Have you written new tests (excluding integration tests)? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) locally?

  • I did not use AI/LLM to create this PR, or I disclosed the tool/model below and reviewed its output; I did not attribute commits to AI and will answer maintainer questions and review comments myself without AI/LLM.

Tests only, no behaviour change.

Utils.safe_filename and Utils.safe_filename? had no coverage at all, and both sit on paths where the input is not fully ours: Cleanup#cleanup_cask passes File.basename of a cask URL through safe_filename, and VCSDownloadStrategy / CurlGitHubPackagesDownloadStrategy build cache paths out of the result. A sanitiser with no tests is one refactor away from quietly widening.

Why I bothered: reading it, SAFE_FILENAME_REGEX looked like it might be missing relative-path handling, since File.basename("http://example.com/../") really does return "..". It is not a hole - every caller that builds a path prefixes the value ("#{name}--", "#{Digest::SHA256.hexdigest(url)}--"), and the one caller that does not only uses it for a start_with? comparison, so a bare ".." can never be a whole path component. That reasoning is exactly what was not written down anywhere, so the specs now pin it: safe_filename?("..") is true, safe_filename?("../etc") is false.

The rest is the current contract: control characters and path separators are stripped, everything else survives, and the output of safe_filename always satisfies safe_filename?.

The unticked box is the bug-fix one - there is no bug here to reproduce, and I did not change utils.rb.

Verification: brew lgtm passes locally (style, typechecking, brew tests --changed), and brew tests --only=utils is green. To check the specs actually bite, I replaced basename.gsub(SAFE_FILENAME_REGEX, "") with basename and both new safe_filename examples failed, then restored it.

AI/LLM disclosure: I used Claude Code (Claude Opus 5) to audit the helper and draft these specs, and reviewed the result; no commit is attributed to AI. I will answer review questions myself.

Neither had any coverage, and both gate attacker-influenced basenames: the
cask cleanup path feeds them File.basename of a URL, and the download
strategies build cache paths from the result.

Pins what the regex actually strips (control characters and path separators)
and, for the case that reads like a gap, that a bare ".." is left alone
while "../etc" is rejected.
@MikeMcQuaid

Copy link
Copy Markdown
Member

Thanks!

@MikeMcQuaid
MikeMcQuaid added this pull request to the merge queue Sep 4, 2026
Merged via the queue into Homebrew:main with commit 6084aaf Sep 4, 2026
58 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants