Skip to content

test(file-safety): pin null-byte contract for cleanup helpers#53

Merged
mambax7 merged 5 commits into
XOOPS:masterfrom
mambax7:feature/FileSafetyTest
May 13, 2026
Merged

test(file-safety): pin null-byte contract for cleanup helpers#53
mambax7 merged 5 commits into
XOOPS:masterfrom
mambax7:feature/FileSafetyTest

Conversation

@mambax7
Copy link
Copy Markdown
Contributor

@mambax7 mambax7 commented May 13, 2026

Covers the three helpers in htdocs/include/file_safety.php -
xoops_safe_basename(), xoops_chmod_quietly(), xoops_remove_file_quietly() -
against the null-byte case that motivated the defensive shape: basename()
does not throw on "\0" in PHP 8.2-8.4 but chmod()/unlink() do, and the
helpers must collapse both into a single E_USER_WARNING without propagating
ValueError. Also pins the backslash-normalisation and XOOPS_ROOT_PATH
stripping branches of xoops_file_label().

Summary by CodeRabbit

  • Tests
    • Added unit tests to verify file-safety behaviors: null-byte paths collapse to a placeholder, Windows backslashes are normalized, ordinary forward-slash paths pass through unchanged, quiet permission change returns false and emits a single warning for invalid paths, quiet file removal does not throw and is a no-op for missing files, and file labels strip the root-prefix while preserving install-relative paths.

Review Change Stack

Review Change Stack

  Covers the three helpers in htdocs/include/file_safety.php -
  xoops_safe_basename(), xoops_chmod_quietly(), xoops_remove_file_quietly() -
  against the null-byte case that motivated the defensive shape: basename()
  does not throw on "\0" in PHP 8.2-8.4 but chmod()/unlink() do, and the
  helpers must collapse both into a single E_USER_WARNING without propagating
  ValueError. Also pins the backslash-normalisation and XOOPS_ROOT_PATH
  stripping branches of xoops_file_label().
Copilot AI review requested due to automatic review settings May 13, 2026 02:15
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Warning

Rate limit exceeded

@mambax7 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 40 minutes and 47 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 48d12415-28b1-4818-99b9-5dbdbdc86473

📥 Commits

Reviewing files that changed from the base of the PR and between 6415a00 and 01d6aee.

📒 Files selected for processing (1)
  • tests/unit/htdocs/include/FileSafetyTest.php

Walkthrough

Adds PHPUnit test coverage for four file-safety helpers in htdocs/include/file_safety.php: validating null-byte handling, backslash normalization, controlled warning emission without exception propagation, silence for missing paths, and XOOPS_ROOT_PATH prefix stripping across seven test methods.

Changes

File Safety Test Suite

Layer / File(s) Summary
Test class and loader
tests/unit/htdocs/include/FileSafetyTest.php
Adds FileSafetyTest class, strict typing, and includes htdocs/include/file_safety.php for the suite.
xoops_safe_basename() assertions
tests/unit/htdocs/include/FileSafetyTest.php
Asserts null-byte paths collapse to invalid-path, backslashes are normalized before basename extraction, and ordinary forward-slash paths pass through.
xoops_chmod_quietly() warning capture
tests/unit/htdocs/include/FileSafetyTest.php
Installs an error handler and asserts xoops_chmod_quietly() returns false for null-byte input and emits exactly one E_USER_WARNING containing invalid-path without throwing.
xoops_remove_file_quietly() null-byte tolerance
tests/unit/htdocs/include/FileSafetyTest.php
Verifies xoops_remove_file_quietly() does not throw for null-byte paths.
xoops_remove_file_quietly() no-op for missing path
tests/unit/htdocs/include/FileSafetyTest.php
Installs an error handler and asserts removing a definitively missing path produces no E_USER_WARNING.
xoops_file_label() root-path stripping
tests/unit/htdocs/include/FileSafetyTest.php
Asserts xoops_file_label() strips XOOPS_ROOT_PATH for forward- and backslash-separated inputs and leaves outside-root labels unchanged.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • XOOPS/XoopsCore27#52: Related changes touching the same htdocs/include/file_safety.php helpers; that PR integrates these helpers into CP/SystemMaintenance cleanup logic.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding tests that validate the null-byte contract for file-safety cleanup helpers, which is the core focus of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 18.12%. Comparing base (b563cbc) to head (01d6aee).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master      #53      +/-   ##
============================================
+ Coverage     18.04%   18.12%   +0.07%     
- Complexity     7845     7854       +9     
============================================
  Files           665      666       +1     
  Lines         43083    43142      +59     
============================================
+ Hits           7775     7820      +45     
- Misses        35308    35322      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds PHPUnit coverage to lock in the “best-effort / non-propagating” behavior of htdocs/include/file_safety.php helpers, especially around null-byte paths that can trigger ValueError in PHP 8+ filesystem functions. This helps ensure cleanup/atomic-write code paths don’t crash and emit consistent project-standard warnings.

Changes:

  • Introduces unit tests for xoops_safe_basename(), xoops_chmod_quietly(), and xoops_remove_file_quietly() focusing on null-byte handling.
  • Adds tests for xoops_safe_basename() backslash normalization and for xoops_file_label() root-prefix stripping + basename fallback.
  • Verifies warning emission behavior via a local error handler (without @ suppression).

Comment on lines +54 to +70
set_error_handler(static function (int $level, string $msg) use (&$captured): bool {
if (E_USER_WARNING === $level) {
$captured = $msg;
return true;
}
return false;
});

try {
$result = xoops_chmod_quietly("bad\0path", 0644, 'test');
} finally {
restore_error_handler();
}

$this->assertFalse($result, 'chmod on a null-byte path must report failure');
$this->assertNotNull($captured, 'helper must still emit one E_USER_WARNING on failure');
$this->assertStringContainsString('invalid-path', (string) $captured);
Comment on lines +75 to +78
// The pre-check file_exists() / is_link() does not throw on a
// "\0"-bearing path in PHP 8.2-8.4 (it returns false), so the
// helper returns early without ever reaching unlink(). The
// contract being tested is just "no exception escapes".
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/htdocs/include/FileSafetyTest.php`:
- Around line 54-71: The test currently only records the last E_USER_WARNING in
$captured, so update the error handler used around
xoops_chmod_quietly("bad\0path", 0644, 'test') to count warnings and capture the
message: add a counter (e.g. $warningCount = 0) in the scope, increment it
inside the set_error_handler callback whenever E_USER_WARNING is seen and still
set $captured to the first (or last) message as desired; after
restore_error_handler(), assert both that $result is false and that
$warningCount === 1 (exactly one warning emitted) in addition to the existing
assertNotNull/assertStringContainsString checks referencing xoops_chmod_quietly
and the handler variables.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dea9560c-e30a-4e6b-86dd-0acc5e5579e6

📥 Commits

Reviewing files that changed from the base of the PR and between c224880 and 7f88ca6.

📒 Files selected for processing (1)
  • tests/unit/htdocs/include/FileSafetyTest.php

Comment thread tests/unit/htdocs/include/FileSafetyTest.php Outdated
… helper

  Address PR XOOPS#53 review notes from Copilot and CodeRabbit:

  - testXoopsChmodQuietlyDoesNotPropagateOnNullBytePath() now asserts
    exactly one E_USER_WARNING and explicitly rejects any non-E_USER_WARNING
    leak, pinning the "collapse both into a single warning" contract.
  - Reword the testXoopsRemoveFileQuietlyDoesNotPropagateOnNullBytePath()
    comment: the pre-check may return false OR throw depending on PHP/
    runtime; the helper's catch(\Throwable) exists for forward-compat and
    userland error handlers. The contract is just "no exception escapes"
@mambax7 mambax7 requested a review from Copilot May 13, 2026 02:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment on lines +112 to +120
// xoops_file_label() keeps install-relative context (unlike
// xoops_safe_basename()) for atomic-write call sites that
// benefit from knowing WHICH file failed. Spot-check the
// strip-prefix branch and the basename fallback.
$absUnderRoot = rtrim(XOOPS_ROOT_PATH, '/\\') . '/uploads/foo.png';
$this->assertSame('uploads/foo.png', xoops_file_label($absUnderRoot));

$absOutside = sys_get_temp_dir() . '/foo.png';
$this->assertSame('foo.png', xoops_file_label($absOutside));
Comment on lines +9 to +17
/**
* Coverage for the three side-effect-free file helpers in
* htdocs/include/file_safety.php. The helpers are documented as
* best-effort / non-propagating: on any failure they emit a single
* E_USER_WARNING (or return early) and continue. These tests pin that
* contract specifically against null-byte payloads, which trigger
* ValueError in the underlying filesystem calls on PHP 8+ — the case
* that motivated the explicit catch(\Throwable) wrappers and the
* xoops_safe_basename() defensive shape.
…lock

  Address PR XOOPS#53 second round of Copilot review notes:

  - testXoopsFileLabelStripsXoopsRootPrefix() now asserts that a
    backslash-separated path under XOOPS_ROOT_PATH collapses to a
    forward-slash relative label, exercising the str_replace('\\\\', '/')
    branch in xoops_file_label() that was previously untested. The outside-
    root branch is intentionally left to forward slashes only because it
    falls through to basename(), which is platform-dependent on '\\'.
  - Reword class docblock: the suite covers four helpers, not three
    (xoops_file_label() was added but the count was not).
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment on lines +85 to +86
xoops_remove_file_quietly("bad\0path");
$this->assertTrue(true, 'xoops_remove_file_quietly returned without throwing');
});

try {
xoops_remove_file_quietly(sys_get_temp_dir() . '/xoops_definitely_missing_' . uniqid() . '.tmp');
…uffix

  Address PR XOOPS#53 third-round Copilot polish notes:

  - testXoopsRemoveFileQuietlyDoesNotPropagateOnNullBytePath() replaces
    the assertTrue(true, ...) placeholder with expectNotToPerformAssertions(),
    PHPUnit's idiomatic form for the "no exception escapes" contract that
    also avoids the always-true assertion some linters flag.
  - testXoopsRemoveFileQuietlyIsNoOpForMissingPath() switches uniqid() to
    bin2hex(random_bytes(16)) for the temp suffix. PHP 8.2+ is the
    baseline; this makes parallel-runner collision concerns disappear.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread tests/unit/htdocs/include/FileSafetyTest.php Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@sonarqubecloud
Copy link
Copy Markdown

@mambax7 mambax7 merged commit 6a9fc1a into XOOPS:master May 13, 2026
8 of 9 checks passed
@mambax7 mambax7 deleted the feature/FileSafetyTest branch May 15, 2026 12:54
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