feat(linux): close three parity gaps against the Windows implementation - #32

Merged
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps
Jul 29, 2026
Merged

feat(linux): close three parity gaps against the Windows implementation#32
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps

Conversation

@devhardiyanto

Copy link
Copy Markdown
Owner

What

Closes the three real behaviour gaps between linux/phpvm.sh and the Windows implementation: unverified PHP downloads, ext list semantics, and a shallower doctor. Linux/macOS only — Windows is untouched apart from the version bump.

Why

An audit of both implementations found the command surface all but identical. cacert (Windows) and deps (Linux) are the only command-level differences and both are by design. Three differences were not by design, and one of them is a genuine integrity hole.

Splitting phpvm.sh into modules the way windows/src/ is split is queued next — deliberately after this, because a split closes no gap and is safer on top of the behaviour work plus its tests.

How

1. SHA-256 verification of the PHP tarball

The tarball came from php.net and went straight into the build with no check at all, then got cached — so a corrupt or substituted archive would be trusted on every later install too, because a cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli (SHA-512) were already verified in this same file; only PHP itself was not.

The digest now comes from php.net's per-version release JSON and is checked after both the download and the cache path. Mismatch aborts and deletes the file. A missing digest, or a host with no sha256sum/shasum/openssl, degrades to a warning instead of blocking an otherwise valid install — the same fallback Windows takes. PHPVM_SKIP_HASH opts out.

Digest lookup can't reuse the hash_file() trick Composer/wp-cli use: this runs before any PHP exists.

2. ext list gains ON/OFF, ext loaded becomes distinct

Both verbs ran php -m, so one was dead weight and there was no way to see an extension that is available but not enabled. ext list now reports ON/OFF across the union of loaded extensions and the .so files in extension_dir; ext loaded is php -m alone.

ON has to come from php -m rather than a directory listing — extensions compiled into the binary (pdo, mbstring, …) own no .so, and reading the directory alone would drop them.

3. Deeper doctor

  • PATH: only the first php was inspected, so a distro or Homebrew PHP sitting behind phpvm went unreported — the exact situation the check exists for. It now walks PATH and names the second one. (Split via tr, because for d in $PATH does not split on colons in zsh.)
  • extension_dir: checking the directory merely exists passes an ini left pointing at another installed version — precisely what fix-ini repairs. Now compared against PHP_EXTENSION_DIR, mirroring Windows' Test-ExtDirMatch.
  • Which php it reads: it went through PATH, which the check above may have just reported resolves somewhere else. Now goes through the active version's own binary.
  • OpenSSL: the host version is reported upfront, so the OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather than after one. Reported as a note, not a warning, when it cannot be determined.

Changes

  • linux/phpvm.sh_phpvm_php_sha256, _phpvm_sha256_file, _phpvm_verify_tarball; verification wired into phpvm_install after both download and cache paths; phpvm_ext_list rewritten, phpvm_ext_loaded added, ext dispatch de-duplicated; phpvm_doctor checks 2, 3 and a new 6; help text in both phpvm_help and phpvm_ext_help
  • tests/linux/verify.bats — new, 12 tests
  • tests/linux/commands.bats — +18 tests; fake php stub gained FAKE_PHP_INI_EXT_DIR so ini and compiled-in dirs can be driven apart
  • tests/linux/zsh-smoke.zsh — 20 → 40 checks
  • README.md — download verification, ext list semantics, doctor description, PHPVM_SKIP_HASH no longer described as Windows-only
  • Version 1.13.2 → 1.14.0 (5 files + generated windows/phpvm.ps1)

Testing Done

  • bats 82 → 112, all green
  • zsh smoke (zsh 5.9) 20 → 40 checks, all green
  • ShellCheck clean on linux/install.sh + linux/phpvm.sh
  • bash -n and zsh -n clean
  • Windows untouched: drift check OK (15 modules), Pester 168/168 still green
  • Digest parser checked against the live php.net API for 8.3.0, 8.4.1, 7.3.33 and 5.6.40 — picks the .tar.gz digest, never its .bz2/.xz siblings

Two zsh-specific risks are covered by name in the smoke suite, since bats only runs under bash: the PATH scan (colon splitting) and the ext list ON/OFF counters (a while read fed by a here-string, which would have come back as zeroes had it been a pipeline).

An audit of both implementations found the command surface all but
identical - `cacert` (Windows) and `deps` (Linux) are the only differences,
and both are by design. Three gaps were not by design. All three live in
the same file, so they land together.
1. The PHP source tarball was never verified. It was downloaded from
php.net and handed straight to the build, then cached - so a corrupt or
substituted archive would be trusted on every later install too, since a
cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli
(SHA-512) were already verified here; only PHP itself was not. The
digest now comes from php.net's per-version release JSON and is checked
after both the download and the cache path. A mismatch aborts and
deletes the file. A missing digest, or a host with no
sha256sum/shasum/openssl, degrades to a warning rather than blocking an
otherwise valid install - the same fallback Windows takes.
PHPVM_SKIP_HASH opts out.
2. `ext list` and `ext loaded` both ran `php -m`, so there was no way to
see an extension that is available but not enabled, and one of the two
verbs was dead weight. `ext list` now reports ON/OFF over the union of
loaded extensions and the .so files in extension_dir; `ext loaded` is
`php -m` alone. ON has to come from `php -m` rather than a directory
listing, because extensions compiled into the binary own no .so.
3. `doctor` was shallower than its Windows counterpart in three ways.
It only looked at the first `php` on PATH, so a distro or Homebrew PHP
waiting behind phpvm went unreported - the very situation the check
exists for. It checked that extension_dir merely existed, which passes
an ini left pointing at another installed version, the exact case
fix-ini repairs. And it read through PATH, which check 2 may have just
said resolves elsewhere; it now goes through the active version's own
binary. It also reports the host OpenSSL version upfront, so the
OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather
than after one.
bats 82 -> 112, zsh smoke 20 -> 40 checks.
devhardiyanto
macOS ships bash 3.2, whose parser chokes on a `case` nested inside a
command substitution - the scan could not even be sourced there, so
phpvm.sh was broken outright on macOS, not merely degraded.
Split PATH with parameter expansion instead. That drops the `$( ... case
... )` construct, and with it the dependency on tr and awk: the check that
tells you PATH is broken should not need to find coreutils on that same
PATH. It also keeps working under zsh, where `for d in $PATH` does not
split on colons.
The PATH tests can now hand doctor a PATH holding nothing but phpvm, so
they stop depending on whether the runner ships a php of its own - which
is what made them pass locally and fail on ubuntu.
devhardiyanto
Those tests hand doctor a PATH holding nothing but phpvm, and bats runs
its own cleanup with whatever PATH the test left behind - so `rm` went
missing and the job exited 1 with all 112 assertions green.
devhardiyanto
@devhardiyanto
devhardiyanto merged commit 08cd6a1 into mainJul 29, 2026
5 checks passed
@devhardiyanto
devhardiyanto deleted the linux-parity-gaps branch July 30, 2026 02:10
Sign up for freeto 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.

1 participant

@devhardiyanto
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(linux): close three parity gaps against the Windows implementation - #32

Merged
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps
Jul 29, 2026
Merged

feat(linux): close three parity gaps against the Windows implementation#32
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps

Conversation

@devhardiyanto

Copy link
Copy Markdown
Owner

What

Closes the three real behaviour gaps between linux/phpvm.sh and the Windows implementation: unverified PHP downloads, ext list semantics, and a shallower doctor. Linux/macOS only — Windows is untouched apart from the version bump.

Why

An audit of both implementations found the command surface all but identical. cacert (Windows) and deps (Linux) are the only command-level differences and both are by design. Three differences were not by design, and one of them is a genuine integrity hole.

Splitting phpvm.sh into modules the way windows/src/ is split is queued next — deliberately after this, because a split closes no gap and is safer on top of the behaviour work plus its tests.

How

1. SHA-256 verification of the PHP tarball

The tarball came from php.net and went straight into the build with no check at all, then got cached — so a corrupt or substituted archive would be trusted on every later install too, because a cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli (SHA-512) were already verified in this same file; only PHP itself was not.

The digest now comes from php.net's per-version release JSON and is checked after both the download and the cache path. Mismatch aborts and deletes the file. A missing digest, or a host with no sha256sum/shasum/openssl, degrades to a warning instead of blocking an otherwise valid install — the same fallback Windows takes. PHPVM_SKIP_HASH opts out.

Digest lookup can't reuse the hash_file() trick Composer/wp-cli use: this runs before any PHP exists.

2. ext list gains ON/OFF, ext loaded becomes distinct

Both verbs ran php -m, so one was dead weight and there was no way to see an extension that is available but not enabled. ext list now reports ON/OFF across the union of loaded extensions and the .so files in extension_dir; ext loaded is php -m alone.

ON has to come from php -m rather than a directory listing — extensions compiled into the binary (pdo, mbstring, …) own no .so, and reading the directory alone would drop them.

3. Deeper doctor

  • PATH: only the first php was inspected, so a distro or Homebrew PHP sitting behind phpvm went unreported — the exact situation the check exists for. It now walks PATH and names the second one. (Split via tr, because for d in $PATH does not split on colons in zsh.)
  • extension_dir: checking the directory merely exists passes an ini left pointing at another installed version — precisely what fix-ini repairs. Now compared against PHP_EXTENSION_DIR, mirroring Windows' Test-ExtDirMatch.
  • Which php it reads: it went through PATH, which the check above may have just reported resolves somewhere else. Now goes through the active version's own binary.
  • OpenSSL: the host version is reported upfront, so the OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather than after one. Reported as a note, not a warning, when it cannot be determined.

Changes

  • linux/phpvm.sh_phpvm_php_sha256, _phpvm_sha256_file, _phpvm_verify_tarball; verification wired into phpvm_install after both download and cache paths; phpvm_ext_list rewritten, phpvm_ext_loaded added, ext dispatch de-duplicated; phpvm_doctor checks 2, 3 and a new 6; help text in both phpvm_help and phpvm_ext_help
  • tests/linux/verify.bats — new, 12 tests
  • tests/linux/commands.bats — +18 tests; fake php stub gained FAKE_PHP_INI_EXT_DIR so ini and compiled-in dirs can be driven apart
  • tests/linux/zsh-smoke.zsh — 20 → 40 checks
  • README.md — download verification, ext list semantics, doctor description, PHPVM_SKIP_HASH no longer described as Windows-only
  • Version 1.13.2 → 1.14.0 (5 files + generated windows/phpvm.ps1)

Testing Done

  • bats 82 → 112, all green
  • zsh smoke (zsh 5.9) 20 → 40 checks, all green
  • ShellCheck clean on linux/install.sh + linux/phpvm.sh
  • bash -n and zsh -n clean
  • Windows untouched: drift check OK (15 modules), Pester 168/168 still green
  • Digest parser checked against the live php.net API for 8.3.0, 8.4.1, 7.3.33 and 5.6.40 — picks the .tar.gz digest, never its .bz2/.xz siblings

Two zsh-specific risks are covered by name in the smoke suite, since bats only runs under bash: the PATH scan (colon splitting) and the ext list ON/OFF counters (a while read fed by a here-string, which would have come back as zeroes had it been a pipeline).

An audit of both implementations found the command surface all but
identical - `cacert` (Windows) and `deps` (Linux) are the only differences,
and both are by design. Three gaps were not by design. All three live in
the same file, so they land together.
1. The PHP source tarball was never verified. It was downloaded from
php.net and handed straight to the build, then cached - so a corrupt or
substituted archive would be trusted on every later install too, since a
cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli
(SHA-512) were already verified here; only PHP itself was not. The
digest now comes from php.net's per-version release JSON and is checked
after both the download and the cache path. A mismatch aborts and
deletes the file. A missing digest, or a host with no
sha256sum/shasum/openssl, degrades to a warning rather than blocking an
otherwise valid install - the same fallback Windows takes.
PHPVM_SKIP_HASH opts out.
2. `ext list` and `ext loaded` both ran `php -m`, so there was no way to
see an extension that is available but not enabled, and one of the two
verbs was dead weight. `ext list` now reports ON/OFF over the union of
loaded extensions and the .so files in extension_dir; `ext loaded` is
`php -m` alone. ON has to come from `php -m` rather than a directory
listing, because extensions compiled into the binary own no .so.
3. `doctor` was shallower than its Windows counterpart in three ways.
It only looked at the first `php` on PATH, so a distro or Homebrew PHP
waiting behind phpvm went unreported - the very situation the check
exists for. It checked that extension_dir merely existed, which passes
an ini left pointing at another installed version, the exact case
fix-ini repairs. And it read through PATH, which check 2 may have just
said resolves elsewhere; it now goes through the active version's own
binary. It also reports the host OpenSSL version upfront, so the
OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather
than after one.
bats 82 -> 112, zsh smoke 20 -> 40 checks.
devhardiyanto
macOS ships bash 3.2, whose parser chokes on a `case` nested inside a
command substitution - the scan could not even be sourced there, so
phpvm.sh was broken outright on macOS, not merely degraded.
Split PATH with parameter expansion instead. That drops the `$( ... case
... )` construct, and with it the dependency on tr and awk: the check that
tells you PATH is broken should not need to find coreutils on that same
PATH. It also keeps working under zsh, where `for d in $PATH` does not
split on colons.
The PATH tests can now hand doctor a PATH holding nothing but phpvm, so
they stop depending on whether the runner ships a php of its own - which
is what made them pass locally and fail on ubuntu.
devhardiyanto
Those tests hand doctor a PATH holding nothing but phpvm, and bats runs
its own cleanup with whatever PATH the test left behind - so `rm` went
missing and the job exited 1 with all 112 assertions green.
devhardiyanto
@devhardiyanto
devhardiyanto merged commit 08cd6a1 into mainJul 29, 2026
5 checks passed
@devhardiyanto
devhardiyanto deleted the linux-parity-gaps branch July 30, 2026 02:10
Sign up for freeto 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.

1 participant

@devhardiyanto
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(linux): close three parity gaps against the Windows implementation - #32

Merged
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps
Jul 29, 2026
Merged

feat(linux): close three parity gaps against the Windows implementation#32
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps

Conversation

@devhardiyanto

Copy link
Copy Markdown
Owner

What

Closes the three real behaviour gaps between linux/phpvm.sh and the Windows implementation: unverified PHP downloads, ext list semantics, and a shallower doctor. Linux/macOS only — Windows is untouched apart from the version bump.

Why

An audit of both implementations found the command surface all but identical. cacert (Windows) and deps (Linux) are the only command-level differences and both are by design. Three differences were not by design, and one of them is a genuine integrity hole.

Splitting phpvm.sh into modules the way windows/src/ is split is queued next — deliberately after this, because a split closes no gap and is safer on top of the behaviour work plus its tests.

How

1. SHA-256 verification of the PHP tarball

The tarball came from php.net and went straight into the build with no check at all, then got cached — so a corrupt or substituted archive would be trusted on every later install too, because a cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli (SHA-512) were already verified in this same file; only PHP itself was not.

The digest now comes from php.net's per-version release JSON and is checked after both the download and the cache path. Mismatch aborts and deletes the file. A missing digest, or a host with no sha256sum/shasum/openssl, degrades to a warning instead of blocking an otherwise valid install — the same fallback Windows takes. PHPVM_SKIP_HASH opts out.

Digest lookup can't reuse the hash_file() trick Composer/wp-cli use: this runs before any PHP exists.

2. ext list gains ON/OFF, ext loaded becomes distinct

Both verbs ran php -m, so one was dead weight and there was no way to see an extension that is available but not enabled. ext list now reports ON/OFF across the union of loaded extensions and the .so files in extension_dir; ext loaded is php -m alone.

ON has to come from php -m rather than a directory listing — extensions compiled into the binary (pdo, mbstring, …) own no .so, and reading the directory alone would drop them.

3. Deeper doctor

  • PATH: only the first php was inspected, so a distro or Homebrew PHP sitting behind phpvm went unreported — the exact situation the check exists for. It now walks PATH and names the second one. (Split via tr, because for d in $PATH does not split on colons in zsh.)
  • extension_dir: checking the directory merely exists passes an ini left pointing at another installed version — precisely what fix-ini repairs. Now compared against PHP_EXTENSION_DIR, mirroring Windows' Test-ExtDirMatch.
  • Which php it reads: it went through PATH, which the check above may have just reported resolves somewhere else. Now goes through the active version's own binary.
  • OpenSSL: the host version is reported upfront, so the OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather than after one. Reported as a note, not a warning, when it cannot be determined.

Changes

  • linux/phpvm.sh_phpvm_php_sha256, _phpvm_sha256_file, _phpvm_verify_tarball; verification wired into phpvm_install after both download and cache paths; phpvm_ext_list rewritten, phpvm_ext_loaded added, ext dispatch de-duplicated; phpvm_doctor checks 2, 3 and a new 6; help text in both phpvm_help and phpvm_ext_help
  • tests/linux/verify.bats — new, 12 tests
  • tests/linux/commands.bats — +18 tests; fake php stub gained FAKE_PHP_INI_EXT_DIR so ini and compiled-in dirs can be driven apart
  • tests/linux/zsh-smoke.zsh — 20 → 40 checks
  • README.md — download verification, ext list semantics, doctor description, PHPVM_SKIP_HASH no longer described as Windows-only
  • Version 1.13.2 → 1.14.0 (5 files + generated windows/phpvm.ps1)

Testing Done

  • bats 82 → 112, all green
  • zsh smoke (zsh 5.9) 20 → 40 checks, all green
  • ShellCheck clean on linux/install.sh + linux/phpvm.sh
  • bash -n and zsh -n clean
  • Windows untouched: drift check OK (15 modules), Pester 168/168 still green
  • Digest parser checked against the live php.net API for 8.3.0, 8.4.1, 7.3.33 and 5.6.40 — picks the .tar.gz digest, never its .bz2/.xz siblings

Two zsh-specific risks are covered by name in the smoke suite, since bats only runs under bash: the PATH scan (colon splitting) and the ext list ON/OFF counters (a while read fed by a here-string, which would have come back as zeroes had it been a pipeline).

An audit of both implementations found the command surface all but
identical - `cacert` (Windows) and `deps` (Linux) are the only differences,
and both are by design. Three gaps were not by design. All three live in
the same file, so they land together.
1. The PHP source tarball was never verified. It was downloaded from
php.net and handed straight to the build, then cached - so a corrupt or
substituted archive would be trusted on every later install too, since a
cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli
(SHA-512) were already verified here; only PHP itself was not. The
digest now comes from php.net's per-version release JSON and is checked
after both the download and the cache path. A mismatch aborts and
deletes the file. A missing digest, or a host with no
sha256sum/shasum/openssl, degrades to a warning rather than blocking an
otherwise valid install - the same fallback Windows takes.
PHPVM_SKIP_HASH opts out.
2. `ext list` and `ext loaded` both ran `php -m`, so there was no way to
see an extension that is available but not enabled, and one of the two
verbs was dead weight. `ext list` now reports ON/OFF over the union of
loaded extensions and the .so files in extension_dir; `ext loaded` is
`php -m` alone. ON has to come from `php -m` rather than a directory
listing, because extensions compiled into the binary own no .so.
3. `doctor` was shallower than its Windows counterpart in three ways.
It only looked at the first `php` on PATH, so a distro or Homebrew PHP
waiting behind phpvm went unreported - the very situation the check
exists for. It checked that extension_dir merely existed, which passes
an ini left pointing at another installed version, the exact case
fix-ini repairs. And it read through PATH, which check 2 may have just
said resolves elsewhere; it now goes through the active version's own
binary. It also reports the host OpenSSL version upfront, so the
OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather
than after one.
bats 82 -> 112, zsh smoke 20 -> 40 checks.
devhardiyanto
macOS ships bash 3.2, whose parser chokes on a `case` nested inside a
command substitution - the scan could not even be sourced there, so
phpvm.sh was broken outright on macOS, not merely degraded.
Split PATH with parameter expansion instead. That drops the `$( ... case
... )` construct, and with it the dependency on tr and awk: the check that
tells you PATH is broken should not need to find coreutils on that same
PATH. It also keeps working under zsh, where `for d in $PATH` does not
split on colons.
The PATH tests can now hand doctor a PATH holding nothing but phpvm, so
they stop depending on whether the runner ships a php of its own - which
is what made them pass locally and fail on ubuntu.
devhardiyanto
Those tests hand doctor a PATH holding nothing but phpvm, and bats runs
its own cleanup with whatever PATH the test left behind - so `rm` went
missing and the job exited 1 with all 112 assertions green.
devhardiyanto
@devhardiyanto
devhardiyanto merged commit 08cd6a1 into mainJul 29, 2026
5 checks passed
@devhardiyanto
devhardiyanto deleted the linux-parity-gaps branch July 30, 2026 02:10
Sign up for freeto 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.

1 participant

@devhardiyanto
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(linux): close three parity gaps against the Windows implementation - #32

Merged
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps
Jul 29, 2026
Merged

feat(linux): close three parity gaps against the Windows implementation#32
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps

Conversation

@devhardiyanto

Copy link
Copy Markdown
Owner

What

Closes the three real behaviour gaps between linux/phpvm.sh and the Windows implementation: unverified PHP downloads, ext list semantics, and a shallower doctor. Linux/macOS only — Windows is untouched apart from the version bump.

Why

An audit of both implementations found the command surface all but identical. cacert (Windows) and deps (Linux) are the only command-level differences and both are by design. Three differences were not by design, and one of them is a genuine integrity hole.

Splitting phpvm.sh into modules the way windows/src/ is split is queued next — deliberately after this, because a split closes no gap and is safer on top of the behaviour work plus its tests.

How

1. SHA-256 verification of the PHP tarball

The tarball came from php.net and went straight into the build with no check at all, then got cached — so a corrupt or substituted archive would be trusted on every later install too, because a cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli (SHA-512) were already verified in this same file; only PHP itself was not.

The digest now comes from php.net's per-version release JSON and is checked after both the download and the cache path. Mismatch aborts and deletes the file. A missing digest, or a host with no sha256sum/shasum/openssl, degrades to a warning instead of blocking an otherwise valid install — the same fallback Windows takes. PHPVM_SKIP_HASH opts out.

Digest lookup can't reuse the hash_file() trick Composer/wp-cli use: this runs before any PHP exists.

2. ext list gains ON/OFF, ext loaded becomes distinct

Both verbs ran php -m, so one was dead weight and there was no way to see an extension that is available but not enabled. ext list now reports ON/OFF across the union of loaded extensions and the .so files in extension_dir; ext loaded is php -m alone.

ON has to come from php -m rather than a directory listing — extensions compiled into the binary (pdo, mbstring, …) own no .so, and reading the directory alone would drop them.

3. Deeper doctor

  • PATH: only the first php was inspected, so a distro or Homebrew PHP sitting behind phpvm went unreported — the exact situation the check exists for. It now walks PATH and names the second one. (Split via tr, because for d in $PATH does not split on colons in zsh.)
  • extension_dir: checking the directory merely exists passes an ini left pointing at another installed version — precisely what fix-ini repairs. Now compared against PHP_EXTENSION_DIR, mirroring Windows' Test-ExtDirMatch.
  • Which php it reads: it went through PATH, which the check above may have just reported resolves somewhere else. Now goes through the active version's own binary.
  • OpenSSL: the host version is reported upfront, so the OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather than after one. Reported as a note, not a warning, when it cannot be determined.

Changes

  • linux/phpvm.sh_phpvm_php_sha256, _phpvm_sha256_file, _phpvm_verify_tarball; verification wired into phpvm_install after both download and cache paths; phpvm_ext_list rewritten, phpvm_ext_loaded added, ext dispatch de-duplicated; phpvm_doctor checks 2, 3 and a new 6; help text in both phpvm_help and phpvm_ext_help
  • tests/linux/verify.bats — new, 12 tests
  • tests/linux/commands.bats — +18 tests; fake php stub gained FAKE_PHP_INI_EXT_DIR so ini and compiled-in dirs can be driven apart
  • tests/linux/zsh-smoke.zsh — 20 → 40 checks
  • README.md — download verification, ext list semantics, doctor description, PHPVM_SKIP_HASH no longer described as Windows-only
  • Version 1.13.2 → 1.14.0 (5 files + generated windows/phpvm.ps1)

Testing Done

  • bats 82 → 112, all green
  • zsh smoke (zsh 5.9) 20 → 40 checks, all green
  • ShellCheck clean on linux/install.sh + linux/phpvm.sh
  • bash -n and zsh -n clean
  • Windows untouched: drift check OK (15 modules), Pester 168/168 still green
  • Digest parser checked against the live php.net API for 8.3.0, 8.4.1, 7.3.33 and 5.6.40 — picks the .tar.gz digest, never its .bz2/.xz siblings

Two zsh-specific risks are covered by name in the smoke suite, since bats only runs under bash: the PATH scan (colon splitting) and the ext list ON/OFF counters (a while read fed by a here-string, which would have come back as zeroes had it been a pipeline).

An audit of both implementations found the command surface all but
identical - `cacert` (Windows) and `deps` (Linux) are the only differences,
and both are by design. Three gaps were not by design. All three live in
the same file, so they land together.
1. The PHP source tarball was never verified. It was downloaded from
php.net and handed straight to the build, then cached - so a corrupt or
substituted archive would be trusted on every later install too, since a
cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli
(SHA-512) were already verified here; only PHP itself was not. The
digest now comes from php.net's per-version release JSON and is checked
after both the download and the cache path. A mismatch aborts and
deletes the file. A missing digest, or a host with no
sha256sum/shasum/openssl, degrades to a warning rather than blocking an
otherwise valid install - the same fallback Windows takes.
PHPVM_SKIP_HASH opts out.
2. `ext list` and `ext loaded` both ran `php -m`, so there was no way to
see an extension that is available but not enabled, and one of the two
verbs was dead weight. `ext list` now reports ON/OFF over the union of
loaded extensions and the .so files in extension_dir; `ext loaded` is
`php -m` alone. ON has to come from `php -m` rather than a directory
listing, because extensions compiled into the binary own no .so.
3. `doctor` was shallower than its Windows counterpart in three ways.
It only looked at the first `php` on PATH, so a distro or Homebrew PHP
waiting behind phpvm went unreported - the very situation the check
exists for. It checked that extension_dir merely existed, which passes
an ini left pointing at another installed version, the exact case
fix-ini repairs. And it read through PATH, which check 2 may have just
said resolves elsewhere; it now goes through the active version's own
binary. It also reports the host OpenSSL version upfront, so the
OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather
than after one.
bats 82 -> 112, zsh smoke 20 -> 40 checks.
devhardiyanto
macOS ships bash 3.2, whose parser chokes on a `case` nested inside a
command substitution - the scan could not even be sourced there, so
phpvm.sh was broken outright on macOS, not merely degraded.
Split PATH with parameter expansion instead. That drops the `$( ... case
... )` construct, and with it the dependency on tr and awk: the check that
tells you PATH is broken should not need to find coreutils on that same
PATH. It also keeps working under zsh, where `for d in $PATH` does not
split on colons.
The PATH tests can now hand doctor a PATH holding nothing but phpvm, so
they stop depending on whether the runner ships a php of its own - which
is what made them pass locally and fail on ubuntu.
devhardiyanto
Those tests hand doctor a PATH holding nothing but phpvm, and bats runs
its own cleanup with whatever PATH the test left behind - so `rm` went
missing and the job exited 1 with all 112 assertions green.
devhardiyanto
@devhardiyanto
devhardiyanto merged commit 08cd6a1 into mainJul 29, 2026
5 checks passed
@devhardiyanto
devhardiyanto deleted the linux-parity-gaps branch July 30, 2026 02:10
Sign up for freeto 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.

1 participant

@devhardiyanto
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(linux): close three parity gaps against the Windows implementation - #32

Merged
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps
Jul 29, 2026
Merged

feat(linux): close three parity gaps against the Windows implementation#32
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps

Conversation

@devhardiyanto

Copy link
Copy Markdown
Owner

What

Closes the three real behaviour gaps between linux/phpvm.sh and the Windows implementation: unverified PHP downloads, ext list semantics, and a shallower doctor. Linux/macOS only — Windows is untouched apart from the version bump.

Why

An audit of both implementations found the command surface all but identical. cacert (Windows) and deps (Linux) are the only command-level differences and both are by design. Three differences were not by design, and one of them is a genuine integrity hole.

Splitting phpvm.sh into modules the way windows/src/ is split is queued next — deliberately after this, because a split closes no gap and is safer on top of the behaviour work plus its tests.

How

1. SHA-256 verification of the PHP tarball

The tarball came from php.net and went straight into the build with no check at all, then got cached — so a corrupt or substituted archive would be trusted on every later install too, because a cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli (SHA-512) were already verified in this same file; only PHP itself was not.

The digest now comes from php.net's per-version release JSON and is checked after both the download and the cache path. Mismatch aborts and deletes the file. A missing digest, or a host with no sha256sum/shasum/openssl, degrades to a warning instead of blocking an otherwise valid install — the same fallback Windows takes. PHPVM_SKIP_HASH opts out.

Digest lookup can't reuse the hash_file() trick Composer/wp-cli use: this runs before any PHP exists.

2. ext list gains ON/OFF, ext loaded becomes distinct

Both verbs ran php -m, so one was dead weight and there was no way to see an extension that is available but not enabled. ext list now reports ON/OFF across the union of loaded extensions and the .so files in extension_dir; ext loaded is php -m alone.

ON has to come from php -m rather than a directory listing — extensions compiled into the binary (pdo, mbstring, …) own no .so, and reading the directory alone would drop them.

3. Deeper doctor

  • PATH: only the first php was inspected, so a distro or Homebrew PHP sitting behind phpvm went unreported — the exact situation the check exists for. It now walks PATH and names the second one. (Split via tr, because for d in $PATH does not split on colons in zsh.)
  • extension_dir: checking the directory merely exists passes an ini left pointing at another installed version — precisely what fix-ini repairs. Now compared against PHP_EXTENSION_DIR, mirroring Windows' Test-ExtDirMatch.
  • Which php it reads: it went through PATH, which the check above may have just reported resolves somewhere else. Now goes through the active version's own binary.
  • OpenSSL: the host version is reported upfront, so the OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather than after one. Reported as a note, not a warning, when it cannot be determined.

Changes

  • linux/phpvm.sh_phpvm_php_sha256, _phpvm_sha256_file, _phpvm_verify_tarball; verification wired into phpvm_install after both download and cache paths; phpvm_ext_list rewritten, phpvm_ext_loaded added, ext dispatch de-duplicated; phpvm_doctor checks 2, 3 and a new 6; help text in both phpvm_help and phpvm_ext_help
  • tests/linux/verify.bats — new, 12 tests
  • tests/linux/commands.bats — +18 tests; fake php stub gained FAKE_PHP_INI_EXT_DIR so ini and compiled-in dirs can be driven apart
  • tests/linux/zsh-smoke.zsh — 20 → 40 checks
  • README.md — download verification, ext list semantics, doctor description, PHPVM_SKIP_HASH no longer described as Windows-only
  • Version 1.13.2 → 1.14.0 (5 files + generated windows/phpvm.ps1)

Testing Done

  • bats 82 → 112, all green
  • zsh smoke (zsh 5.9) 20 → 40 checks, all green
  • ShellCheck clean on linux/install.sh + linux/phpvm.sh
  • bash -n and zsh -n clean
  • Windows untouched: drift check OK (15 modules), Pester 168/168 still green
  • Digest parser checked against the live php.net API for 8.3.0, 8.4.1, 7.3.33 and 5.6.40 — picks the .tar.gz digest, never its .bz2/.xz siblings

Two zsh-specific risks are covered by name in the smoke suite, since bats only runs under bash: the PATH scan (colon splitting) and the ext list ON/OFF counters (a while read fed by a here-string, which would have come back as zeroes had it been a pipeline).

An audit of both implementations found the command surface all but
identical - `cacert` (Windows) and `deps` (Linux) are the only differences,
and both are by design. Three gaps were not by design. All three live in
the same file, so they land together.
1. The PHP source tarball was never verified. It was downloaded from
php.net and handed straight to the build, then cached - so a corrupt or
substituted archive would be trusted on every later install too, since a
cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli
(SHA-512) were already verified here; only PHP itself was not. The
digest now comes from php.net's per-version release JSON and is checked
after both the download and the cache path. A mismatch aborts and
deletes the file. A missing digest, or a host with no
sha256sum/shasum/openssl, degrades to a warning rather than blocking an
otherwise valid install - the same fallback Windows takes.
PHPVM_SKIP_HASH opts out.
2. `ext list` and `ext loaded` both ran `php -m`, so there was no way to
see an extension that is available but not enabled, and one of the two
verbs was dead weight. `ext list` now reports ON/OFF over the union of
loaded extensions and the .so files in extension_dir; `ext loaded` is
`php -m` alone. ON has to come from `php -m` rather than a directory
listing, because extensions compiled into the binary own no .so.
3. `doctor` was shallower than its Windows counterpart in three ways.
It only looked at the first `php` on PATH, so a distro or Homebrew PHP
waiting behind phpvm went unreported - the very situation the check
exists for. It checked that extension_dir merely existed, which passes
an ini left pointing at another installed version, the exact case
fix-ini repairs. And it read through PATH, which check 2 may have just
said resolves elsewhere; it now goes through the active version's own
binary. It also reports the host OpenSSL version upfront, so the
OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather
than after one.
bats 82 -> 112, zsh smoke 20 -> 40 checks.
devhardiyanto
macOS ships bash 3.2, whose parser chokes on a `case` nested inside a
command substitution - the scan could not even be sourced there, so
phpvm.sh was broken outright on macOS, not merely degraded.
Split PATH with parameter expansion instead. That drops the `$( ... case
... )` construct, and with it the dependency on tr and awk: the check that
tells you PATH is broken should not need to find coreutils on that same
PATH. It also keeps working under zsh, where `for d in $PATH` does not
split on colons.
The PATH tests can now hand doctor a PATH holding nothing but phpvm, so
they stop depending on whether the runner ships a php of its own - which
is what made them pass locally and fail on ubuntu.
devhardiyanto
Those tests hand doctor a PATH holding nothing but phpvm, and bats runs
its own cleanup with whatever PATH the test left behind - so `rm` went
missing and the job exited 1 with all 112 assertions green.
devhardiyanto
@devhardiyanto
devhardiyanto merged commit 08cd6a1 into mainJul 29, 2026
5 checks passed
@devhardiyanto
devhardiyanto deleted the linux-parity-gaps branch July 30, 2026 02:10
Sign up for freeto 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.

1 participant

@devhardiyanto
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(linux): close three parity gaps against the Windows implementation - #32

Merged
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps
Jul 29, 2026
Merged

feat(linux): close three parity gaps against the Windows implementation#32
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps

Conversation

@devhardiyanto

Copy link
Copy Markdown
Owner

What

Closes the three real behaviour gaps between linux/phpvm.sh and the Windows implementation: unverified PHP downloads, ext list semantics, and a shallower doctor. Linux/macOS only — Windows is untouched apart from the version bump.

Why

An audit of both implementations found the command surface all but identical. cacert (Windows) and deps (Linux) are the only command-level differences and both are by design. Three differences were not by design, and one of them is a genuine integrity hole.

Splitting phpvm.sh into modules the way windows/src/ is split is queued next — deliberately after this, because a split closes no gap and is safer on top of the behaviour work plus its tests.

How

1. SHA-256 verification of the PHP tarball

The tarball came from php.net and went straight into the build with no check at all, then got cached — so a corrupt or substituted archive would be trusted on every later install too, because a cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli (SHA-512) were already verified in this same file; only PHP itself was not.

The digest now comes from php.net's per-version release JSON and is checked after both the download and the cache path. Mismatch aborts and deletes the file. A missing digest, or a host with no sha256sum/shasum/openssl, degrades to a warning instead of blocking an otherwise valid install — the same fallback Windows takes. PHPVM_SKIP_HASH opts out.

Digest lookup can't reuse the hash_file() trick Composer/wp-cli use: this runs before any PHP exists.

2. ext list gains ON/OFF, ext loaded becomes distinct

Both verbs ran php -m, so one was dead weight and there was no way to see an extension that is available but not enabled. ext list now reports ON/OFF across the union of loaded extensions and the .so files in extension_dir; ext loaded is php -m alone.

ON has to come from php -m rather than a directory listing — extensions compiled into the binary (pdo, mbstring, …) own no .so, and reading the directory alone would drop them.

3. Deeper doctor

  • PATH: only the first php was inspected, so a distro or Homebrew PHP sitting behind phpvm went unreported — the exact situation the check exists for. It now walks PATH and names the second one. (Split via tr, because for d in $PATH does not split on colons in zsh.)
  • extension_dir: checking the directory merely exists passes an ini left pointing at another installed version — precisely what fix-ini repairs. Now compared against PHP_EXTENSION_DIR, mirroring Windows' Test-ExtDirMatch.
  • Which php it reads: it went through PATH, which the check above may have just reported resolves somewhere else. Now goes through the active version's own binary.
  • OpenSSL: the host version is reported upfront, so the OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather than after one. Reported as a note, not a warning, when it cannot be determined.

Changes

  • linux/phpvm.sh_phpvm_php_sha256, _phpvm_sha256_file, _phpvm_verify_tarball; verification wired into phpvm_install after both download and cache paths; phpvm_ext_list rewritten, phpvm_ext_loaded added, ext dispatch de-duplicated; phpvm_doctor checks 2, 3 and a new 6; help text in both phpvm_help and phpvm_ext_help
  • tests/linux/verify.bats — new, 12 tests
  • tests/linux/commands.bats — +18 tests; fake php stub gained FAKE_PHP_INI_EXT_DIR so ini and compiled-in dirs can be driven apart
  • tests/linux/zsh-smoke.zsh — 20 → 40 checks
  • README.md — download verification, ext list semantics, doctor description, PHPVM_SKIP_HASH no longer described as Windows-only
  • Version 1.13.2 → 1.14.0 (5 files + generated windows/phpvm.ps1)

Testing Done

  • bats 82 → 112, all green
  • zsh smoke (zsh 5.9) 20 → 40 checks, all green
  • ShellCheck clean on linux/install.sh + linux/phpvm.sh
  • bash -n and zsh -n clean
  • Windows untouched: drift check OK (15 modules), Pester 168/168 still green
  • Digest parser checked against the live php.net API for 8.3.0, 8.4.1, 7.3.33 and 5.6.40 — picks the .tar.gz digest, never its .bz2/.xz siblings

Two zsh-specific risks are covered by name in the smoke suite, since bats only runs under bash: the PATH scan (colon splitting) and the ext list ON/OFF counters (a while read fed by a here-string, which would have come back as zeroes had it been a pipeline).

An audit of both implementations found the command surface all but
identical - `cacert` (Windows) and `deps` (Linux) are the only differences,
and both are by design. Three gaps were not by design. All three live in
the same file, so they land together.
1. The PHP source tarball was never verified. It was downloaded from
php.net and handed straight to the build, then cached - so a corrupt or
substituted archive would be trusted on every later install too, since a
cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli
(SHA-512) were already verified here; only PHP itself was not. The
digest now comes from php.net's per-version release JSON and is checked
after both the download and the cache path. A mismatch aborts and
deletes the file. A missing digest, or a host with no
sha256sum/shasum/openssl, degrades to a warning rather than blocking an
otherwise valid install - the same fallback Windows takes.
PHPVM_SKIP_HASH opts out.
2. `ext list` and `ext loaded` both ran `php -m`, so there was no way to
see an extension that is available but not enabled, and one of the two
verbs was dead weight. `ext list` now reports ON/OFF over the union of
loaded extensions and the .so files in extension_dir; `ext loaded` is
`php -m` alone. ON has to come from `php -m` rather than a directory
listing, because extensions compiled into the binary own no .so.
3. `doctor` was shallower than its Windows counterpart in three ways.
It only looked at the first `php` on PATH, so a distro or Homebrew PHP
waiting behind phpvm went unreported - the very situation the check
exists for. It checked that extension_dir merely existed, which passes
an ini left pointing at another installed version, the exact case
fix-ini repairs. And it read through PATH, which check 2 may have just
said resolves elsewhere; it now goes through the active version's own
binary. It also reports the host OpenSSL version upfront, so the
OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather
than after one.
bats 82 -> 112, zsh smoke 20 -> 40 checks.
devhardiyanto
macOS ships bash 3.2, whose parser chokes on a `case` nested inside a
command substitution - the scan could not even be sourced there, so
phpvm.sh was broken outright on macOS, not merely degraded.
Split PATH with parameter expansion instead. That drops the `$( ... case
... )` construct, and with it the dependency on tr and awk: the check that
tells you PATH is broken should not need to find coreutils on that same
PATH. It also keeps working under zsh, where `for d in $PATH` does not
split on colons.
The PATH tests can now hand doctor a PATH holding nothing but phpvm, so
they stop depending on whether the runner ships a php of its own - which
is what made them pass locally and fail on ubuntu.
devhardiyanto
Those tests hand doctor a PATH holding nothing but phpvm, and bats runs
its own cleanup with whatever PATH the test left behind - so `rm` went
missing and the job exited 1 with all 112 assertions green.
devhardiyanto
@devhardiyanto
devhardiyanto merged commit 08cd6a1 into mainJul 29, 2026
5 checks passed
@devhardiyanto
devhardiyanto deleted the linux-parity-gaps branch July 30, 2026 02:10
Sign up for freeto 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.

1 participant

@devhardiyanto
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(linux): close three parity gaps against the Windows implementation - #32

Merged
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps
Jul 29, 2026
Merged

feat(linux): close three parity gaps against the Windows implementation#32
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps

Conversation

@devhardiyanto

Copy link
Copy Markdown
Owner

What

Closes the three real behaviour gaps between linux/phpvm.sh and the Windows implementation: unverified PHP downloads, ext list semantics, and a shallower doctor. Linux/macOS only — Windows is untouched apart from the version bump.

Why

An audit of both implementations found the command surface all but identical. cacert (Windows) and deps (Linux) are the only command-level differences and both are by design. Three differences were not by design, and one of them is a genuine integrity hole.

Splitting phpvm.sh into modules the way windows/src/ is split is queued next — deliberately after this, because a split closes no gap and is safer on top of the behaviour work plus its tests.

How

1. SHA-256 verification of the PHP tarball

The tarball came from php.net and went straight into the build with no check at all, then got cached — so a corrupt or substituted archive would be trusted on every later install too, because a cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli (SHA-512) were already verified in this same file; only PHP itself was not.

The digest now comes from php.net's per-version release JSON and is checked after both the download and the cache path. Mismatch aborts and deletes the file. A missing digest, or a host with no sha256sum/shasum/openssl, degrades to a warning instead of blocking an otherwise valid install — the same fallback Windows takes. PHPVM_SKIP_HASH opts out.

Digest lookup can't reuse the hash_file() trick Composer/wp-cli use: this runs before any PHP exists.

2. ext list gains ON/OFF, ext loaded becomes distinct

Both verbs ran php -m, so one was dead weight and there was no way to see an extension that is available but not enabled. ext list now reports ON/OFF across the union of loaded extensions and the .so files in extension_dir; ext loaded is php -m alone.

ON has to come from php -m rather than a directory listing — extensions compiled into the binary (pdo, mbstring, …) own no .so, and reading the directory alone would drop them.

3. Deeper doctor

  • PATH: only the first php was inspected, so a distro or Homebrew PHP sitting behind phpvm went unreported — the exact situation the check exists for. It now walks PATH and names the second one. (Split via tr, because for d in $PATH does not split on colons in zsh.)
  • extension_dir: checking the directory merely exists passes an ini left pointing at another installed version — precisely what fix-ini repairs. Now compared against PHP_EXTENSION_DIR, mirroring Windows' Test-ExtDirMatch.
  • Which php it reads: it went through PATH, which the check above may have just reported resolves somewhere else. Now goes through the active version's own binary.
  • OpenSSL: the host version is reported upfront, so the OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather than after one. Reported as a note, not a warning, when it cannot be determined.

Changes

  • linux/phpvm.sh_phpvm_php_sha256, _phpvm_sha256_file, _phpvm_verify_tarball; verification wired into phpvm_install after both download and cache paths; phpvm_ext_list rewritten, phpvm_ext_loaded added, ext dispatch de-duplicated; phpvm_doctor checks 2, 3 and a new 6; help text in both phpvm_help and phpvm_ext_help
  • tests/linux/verify.bats — new, 12 tests
  • tests/linux/commands.bats — +18 tests; fake php stub gained FAKE_PHP_INI_EXT_DIR so ini and compiled-in dirs can be driven apart
  • tests/linux/zsh-smoke.zsh — 20 → 40 checks
  • README.md — download verification, ext list semantics, doctor description, PHPVM_SKIP_HASH no longer described as Windows-only
  • Version 1.13.2 → 1.14.0 (5 files + generated windows/phpvm.ps1)

Testing Done

  • bats 82 → 112, all green
  • zsh smoke (zsh 5.9) 20 → 40 checks, all green
  • ShellCheck clean on linux/install.sh + linux/phpvm.sh
  • bash -n and zsh -n clean
  • Windows untouched: drift check OK (15 modules), Pester 168/168 still green
  • Digest parser checked against the live php.net API for 8.3.0, 8.4.1, 7.3.33 and 5.6.40 — picks the .tar.gz digest, never its .bz2/.xz siblings

Two zsh-specific risks are covered by name in the smoke suite, since bats only runs under bash: the PATH scan (colon splitting) and the ext list ON/OFF counters (a while read fed by a here-string, which would have come back as zeroes had it been a pipeline).

An audit of both implementations found the command surface all but
identical - `cacert` (Windows) and `deps` (Linux) are the only differences,
and both are by design. Three gaps were not by design. All three live in
the same file, so they land together.
1. The PHP source tarball was never verified. It was downloaded from
php.net and handed straight to the build, then cached - so a corrupt or
substituted archive would be trusted on every later install too, since a
cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli
(SHA-512) were already verified here; only PHP itself was not. The
digest now comes from php.net's per-version release JSON and is checked
after both the download and the cache path. A mismatch aborts and
deletes the file. A missing digest, or a host with no
sha256sum/shasum/openssl, degrades to a warning rather than blocking an
otherwise valid install - the same fallback Windows takes.
PHPVM_SKIP_HASH opts out.
2. `ext list` and `ext loaded` both ran `php -m`, so there was no way to
see an extension that is available but not enabled, and one of the two
verbs was dead weight. `ext list` now reports ON/OFF over the union of
loaded extensions and the .so files in extension_dir; `ext loaded` is
`php -m` alone. ON has to come from `php -m` rather than a directory
listing, because extensions compiled into the binary own no .so.
3. `doctor` was shallower than its Windows counterpart in three ways.
It only looked at the first `php` on PATH, so a distro or Homebrew PHP
waiting behind phpvm went unreported - the very situation the check
exists for. It checked that extension_dir merely existed, which passes
an ini left pointing at another installed version, the exact case
fix-ini repairs. And it read through PATH, which check 2 may have just
said resolves elsewhere; it now goes through the active version's own
binary. It also reports the host OpenSSL version upfront, so the
OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather
than after one.
bats 82 -> 112, zsh smoke 20 -> 40 checks.
devhardiyanto
macOS ships bash 3.2, whose parser chokes on a `case` nested inside a
command substitution - the scan could not even be sourced there, so
phpvm.sh was broken outright on macOS, not merely degraded.
Split PATH with parameter expansion instead. That drops the `$( ... case
... )` construct, and with it the dependency on tr and awk: the check that
tells you PATH is broken should not need to find coreutils on that same
PATH. It also keeps working under zsh, where `for d in $PATH` does not
split on colons.
The PATH tests can now hand doctor a PATH holding nothing but phpvm, so
they stop depending on whether the runner ships a php of its own - which
is what made them pass locally and fail on ubuntu.
devhardiyanto
Those tests hand doctor a PATH holding nothing but phpvm, and bats runs
its own cleanup with whatever PATH the test left behind - so `rm` went
missing and the job exited 1 with all 112 assertions green.
devhardiyanto
@devhardiyanto
devhardiyanto merged commit 08cd6a1 into mainJul 29, 2026
5 checks passed
@devhardiyanto
devhardiyanto deleted the linux-parity-gaps branch July 30, 2026 02:10
Sign up for freeto 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.

1 participant

@devhardiyanto
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(linux): close three parity gaps against the Windows implementation - #32

Merged
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps
Jul 29, 2026
Merged

feat(linux): close three parity gaps against the Windows implementation#32
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps

Conversation

@devhardiyanto

Copy link
Copy Markdown
Owner

What

Closes the three real behaviour gaps between linux/phpvm.sh and the Windows implementation: unverified PHP downloads, ext list semantics, and a shallower doctor. Linux/macOS only — Windows is untouched apart from the version bump.

Why

An audit of both implementations found the command surface all but identical. cacert (Windows) and deps (Linux) are the only command-level differences and both are by design. Three differences were not by design, and one of them is a genuine integrity hole.

Splitting phpvm.sh into modules the way windows/src/ is split is queued next — deliberately after this, because a split closes no gap and is safer on top of the behaviour work plus its tests.

How

1. SHA-256 verification of the PHP tarball

The tarball came from php.net and went straight into the build with no check at all, then got cached — so a corrupt or substituted archive would be trusted on every later install too, because a cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli (SHA-512) were already verified in this same file; only PHP itself was not.

The digest now comes from php.net's per-version release JSON and is checked after both the download and the cache path. Mismatch aborts and deletes the file. A missing digest, or a host with no sha256sum/shasum/openssl, degrades to a warning instead of blocking an otherwise valid install — the same fallback Windows takes. PHPVM_SKIP_HASH opts out.

Digest lookup can't reuse the hash_file() trick Composer/wp-cli use: this runs before any PHP exists.

2. ext list gains ON/OFF, ext loaded becomes distinct

Both verbs ran php -m, so one was dead weight and there was no way to see an extension that is available but not enabled. ext list now reports ON/OFF across the union of loaded extensions and the .so files in extension_dir; ext loaded is php -m alone.

ON has to come from php -m rather than a directory listing — extensions compiled into the binary (pdo, mbstring, …) own no .so, and reading the directory alone would drop them.

3. Deeper doctor

  • PATH: only the first php was inspected, so a distro or Homebrew PHP sitting behind phpvm went unreported — the exact situation the check exists for. It now walks PATH and names the second one. (Split via tr, because for d in $PATH does not split on colons in zsh.)
  • extension_dir: checking the directory merely exists passes an ini left pointing at another installed version — precisely what fix-ini repairs. Now compared against PHP_EXTENSION_DIR, mirroring Windows' Test-ExtDirMatch.
  • Which php it reads: it went through PATH, which the check above may have just reported resolves somewhere else. Now goes through the active version's own binary.
  • OpenSSL: the host version is reported upfront, so the OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather than after one. Reported as a note, not a warning, when it cannot be determined.

Changes

  • linux/phpvm.sh_phpvm_php_sha256, _phpvm_sha256_file, _phpvm_verify_tarball; verification wired into phpvm_install after both download and cache paths; phpvm_ext_list rewritten, phpvm_ext_loaded added, ext dispatch de-duplicated; phpvm_doctor checks 2, 3 and a new 6; help text in both phpvm_help and phpvm_ext_help
  • tests/linux/verify.bats — new, 12 tests
  • tests/linux/commands.bats — +18 tests; fake php stub gained FAKE_PHP_INI_EXT_DIR so ini and compiled-in dirs can be driven apart
  • tests/linux/zsh-smoke.zsh — 20 → 40 checks
  • README.md — download verification, ext list semantics, doctor description, PHPVM_SKIP_HASH no longer described as Windows-only
  • Version 1.13.2 → 1.14.0 (5 files + generated windows/phpvm.ps1)

Testing Done

  • bats 82 → 112, all green
  • zsh smoke (zsh 5.9) 20 → 40 checks, all green
  • ShellCheck clean on linux/install.sh + linux/phpvm.sh
  • bash -n and zsh -n clean
  • Windows untouched: drift check OK (15 modules), Pester 168/168 still green
  • Digest parser checked against the live php.net API for 8.3.0, 8.4.1, 7.3.33 and 5.6.40 — picks the .tar.gz digest, never its .bz2/.xz siblings

Two zsh-specific risks are covered by name in the smoke suite, since bats only runs under bash: the PATH scan (colon splitting) and the ext list ON/OFF counters (a while read fed by a here-string, which would have come back as zeroes had it been a pipeline).

An audit of both implementations found the command surface all but
identical - `cacert` (Windows) and `deps` (Linux) are the only differences,
and both are by design. Three gaps were not by design. All three live in
the same file, so they land together.
1. The PHP source tarball was never verified. It was downloaded from
php.net and handed straight to the build, then cached - so a corrupt or
substituted archive would be trusted on every later install too, since a
cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli
(SHA-512) were already verified here; only PHP itself was not. The
digest now comes from php.net's per-version release JSON and is checked
after both the download and the cache path. A mismatch aborts and
deletes the file. A missing digest, or a host with no
sha256sum/shasum/openssl, degrades to a warning rather than blocking an
otherwise valid install - the same fallback Windows takes.
PHPVM_SKIP_HASH opts out.
2. `ext list` and `ext loaded` both ran `php -m`, so there was no way to
see an extension that is available but not enabled, and one of the two
verbs was dead weight. `ext list` now reports ON/OFF over the union of
loaded extensions and the .so files in extension_dir; `ext loaded` is
`php -m` alone. ON has to come from `php -m` rather than a directory
listing, because extensions compiled into the binary own no .so.
3. `doctor` was shallower than its Windows counterpart in three ways.
It only looked at the first `php` on PATH, so a distro or Homebrew PHP
waiting behind phpvm went unreported - the very situation the check
exists for. It checked that extension_dir merely existed, which passes
an ini left pointing at another installed version, the exact case
fix-ini repairs. And it read through PATH, which check 2 may have just
said resolves elsewhere; it now goes through the active version's own
binary. It also reports the host OpenSSL version upfront, so the
OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather
than after one.
bats 82 -> 112, zsh smoke 20 -> 40 checks.
devhardiyanto
macOS ships bash 3.2, whose parser chokes on a `case` nested inside a
command substitution - the scan could not even be sourced there, so
phpvm.sh was broken outright on macOS, not merely degraded.
Split PATH with parameter expansion instead. That drops the `$( ... case
... )` construct, and with it the dependency on tr and awk: the check that
tells you PATH is broken should not need to find coreutils on that same
PATH. It also keeps working under zsh, where `for d in $PATH` does not
split on colons.
The PATH tests can now hand doctor a PATH holding nothing but phpvm, so
they stop depending on whether the runner ships a php of its own - which
is what made them pass locally and fail on ubuntu.
devhardiyanto
Those tests hand doctor a PATH holding nothing but phpvm, and bats runs
its own cleanup with whatever PATH the test left behind - so `rm` went
missing and the job exited 1 with all 112 assertions green.
devhardiyanto
@devhardiyanto
devhardiyanto merged commit 08cd6a1 into mainJul 29, 2026
5 checks passed
@devhardiyanto
devhardiyanto deleted the linux-parity-gaps branch July 30, 2026 02:10
Sign up for freeto 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.

1 participant

@devhardiyanto