aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
AgeCommit message (Collapse)Author
2026-08-02fix(symfony-finder): make Glob::to_regex regex-crate compatiblensfisis
Glob::toRegex emits PCRE-only constructs — the (?=[^\.]) look-ahead for the strict-leading-dot rule, the possessive [^/]++ in /**/ segments, and, via BaseExcludeFilter, the (?=$|/) dir-boundary look-ahead — which the regex crate cannot compile, so `archive` and every ArchivableFilesFinder path panicked. Rewrite the port to tokenize the glob (mirroring the PHP loop's dispatch) and resolve every no-dot constraint by recursive union expansion. The dir boundary must take part in that expansion (a trailing `*` matching zero characters drops the constraint onto the boundary itself), so BaseExcludeFilter now uses the new Glob::to_regex_dir_boundary instead of string surgery. Equivalence was verified against PHP 8.5.8 (vendored Glob.php + preg_match) over 66,176 glob x flag x subject combinations with zero divergence. Un-ignores the five archiver tests blocked on this and updates GitExcludeFilterTest's expected pattern text, an explicitly authorized exception to the no-test-modification rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02refactor(git-exclude-filter): pass the line parser as a method referencensfisis
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01feat(symfony-console): implement interactive question/style helpersnsfisis
Resolve the remaining todo!()s in SymfonyStyle, OutputStyle, QuestionHelper and SymfonyQuestionHelper: * Wire up the virtual dispatch PHP performs for the protected writePrompt()/writeError() overrides, following the codebase's established inheritance idiom (Command, ArchiveDownloader): the base class becomes a trait (QuestionHelperInterface, named after the QuestionInterface precedent) whose provided methods ask/do_ask/ validate_attempts carry the template logic and late-bind the write_prompt/write_error hooks through Self, with inner()/inner_mut() reaching the base-class state. SymfonyQuestionHelper overrides the hooks as plain trait-impl methods, mirroring PHP's protected-method overriding, so SymfonyStyle-driven questions now render the Symfony Style Guide prompt. * Type definition_list input as an enum (string|array|TableSeparator) because PhpMixed intentionally cannot carry objects; the InvalidArgumentException branch (a LogicException) becomes unrepresentable. horizontal_table now takes typed Cells/Rows. * Propagate the MissingInputException thrown inside autocomplete() through a Result instead of aborting. * Implement as_console_output_interface via Ref::filter_map on ConsoleOutput, the interface's only implementor. * Port progressIterate eagerly, following ProgressBar::iterate. * Map __FILE__ to current_exe(): a native binary never runs from a phar, so the hiddeninput.exe relocation branch correctly never fires. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01feat(php-shim): implement Phar/PharData and the zlib/bzip2 functionsnsfisis
Adopt the tar, flate2, and bzip2 crates to fill in the phar.rs and compress.rs todos: PharData tar/zip reading, building, and whole-archive compression, plus a native .phar reader that follows the php.net file-format manual and verifies hash-based signatures. Callers now propagate the constructor/extract errors PHP throws, and fwrite accepts byte strings so gzread no longer needs lossy UTF-8. The native .phar writing API stays todo!() (no call sites; Composer's Compiler is not ported) and OPENSSL phar signatures are accepted unverified (TODO(phase-c)). This unblocks Tar::getComposerJson and the tar/phar/gzip downloaders; tar_test (7), artifact_repository_test (2), and phar_archiver_test zip (1) are un-ignored. The archive command itself still panics because ArchiveManager::archive always generates glob excludes whose look-ahead regexes the regex crate cannot compile; converting those patterns to regex-compatible ones is a separate, still-undecided work item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26feat(diagnose-command): report the real curl versionnsfisis
The line was a "TODO: curl_version()" placeholder. Extend the diagnose payload with curl_version() and the CURL_* constants getCurlVersion() consults, so the libz/brotli/zstd/ssl/HTTP details come from the PHP runtime instead of being guessed. curl_version() is only reachable while the extension is loaded, mirroring the ioncube_loader_* entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26fix(diagnose-command): give the audit BufferIO normal verbositynsfisis
PHP's BufferIO defaults to StreamOutput::VERBOSITY_NORMAL; passing 0 sits below VERBOSITY_QUIET, so every write was dropped and "Audit found some issues:" was followed by an empty advisory table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26feat(diagnose-command): query the real PHP runtime in one RPC callnsfisis
diagnose used to read hardcoded shim stubs, so it described a fictional runtime: OPENSSL_VERSION_NUMBER was always 0 and tripped the TLSv1.1/1.2 check, PHP_BINARY and OPENSSL_VERSION_TEXT were empty, and the extension, function and ini probes answered from a fixed table. The PHP worker gained a `diagnose` entry that returns every fact the command needs as one PHP array, cached in a OnceLock so the several call sites share a single round trip. Reading it back needed array support in the serialize() parser, which in turn lets get_loaded_extensions and get_all_ini_files return real lists instead of comma-joined strings. Also fixes the openssl_version message, which dropped strstr()'s before_needle argument during the port, and check_connectivity's allow_url_fopen test, which did not follow PHP string truthiness. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25fix(package-discovery): honor ignored platform reqs in shadowed-repo lookupnsfisis
The ALLOW_SHADOWED_REPOSITORIES probe that decides whether to raise the repository-priority error hardcoded ignoreNothing(), while PHP reuses $platformRequirementFilter. With --ignore-platform-req the probe could fail to find the lower-priority package and suppress the error PHP would raise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25fix(require-command): end input borrow before asking for confirmationnsfisis
The `fixed` option was read as a temporary inside the argument list of update_requirements_after_resolution(), so the Ref lived until the end of the enclosing let statement — i.e. across the whole call. When the resolved version looks like a feature branch, that call asks for confirmation, and ConsoleIO takes the same input RefCell mutably, panicking with "RefCell already borrowed". Hoisting the read into its own statement ends the borrow before the call. The expected output of the un-ignored test transcribed PHP's string concatenation operator (`[y,n]? '.'`, used to keep the trailing space visible) as a literal `.`; it now matches RequireCommandTest.php. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25fix(package-discovery): pass IO so platform requirement warnings surfacensfisis
findBestVersionForPackage is the only findBestCandidate() caller that PHP hands $this->getIO() to; the port passed None, so VersionSelector silently skipped every "Cannot use <pkg> as it requires <ext> which is missing from your platform" warning. require/update/create-project therefore dropped a candidate without telling the user why. Un-ignores require_command_test::test_require, whose first data-provider case asserts exactly that warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25feat(console): register plugin commands via Application::addnsfisis
get_plugin_commands now yields shared command handles so the discovered commands can go through the same add() path as built-in ones, dropping the placeholder that discarded them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25refactor(json): embed Composer schemas instead of copying them to targetnsfisis
build.rs guessed target/<profile> from OUT_DIR to place res/*.json next to the executable (twice, since test binaries live in deps/), and JsonFile resolved them through current_exe(). That made the binary undistributable on its own. The schemas are now include_str!'d and referenced through a shirabe:///res/ URI that SchemaRetriever resolves, keeping the $ref indirection PHP uses for the phar case. The res/ path segment is required so composer-lock-schema.json's relative "./composer-schema.json" reference still resolves.
2026-07-25perf(advisory): share AnySecurityAdvisory via Rcnsfisis
PHP's SecurityAdvisoryPoolFilter stores advisory *object references* in $securityRemovedVersions, and PoolOptimizer::applyRemovalsToPool hands that array to the new Pool by copy-on-write. Porting AnySecurityAdvisory as a value type turned both of those into deep copies. Measured on `require laravel/framework` (offline, warm cache): 113 distinct advisories were duplicated into 314,309 copies of ~1.08 KiB, retaining 331.9 MiB in the filter loop and another 331.9 MiB when apply_removals_to_pool cloned the whole map. 3.54s -> 2.62s (-26%), peak RSS 975 MB -> 343 MB, which matches the upper bound measured by ablation. Composer runs the same workload in 1.49s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25refactor: replace redundant clones with movesnsfisis
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25feat(tracing): init tracing subscriber from $SHIRABE_TRACINGnsfisis
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25refactor(operation): replace OperationInterface with AnyOperation enumnsfisis
Operations are only ever constructed by the dependency resolver, so a plugin has no way to inject an implementation of its own and the set is closed. Modelling it as an enum, like AnyPackage, removes the OperationInterface trait together with its two parallel downcast mechanisms (as_any() + downcast_ref, and as_*_operation()) and the get_package() default method that panicked on UpdateOperation. The PHP idiom `$op instanceof UpdateOperation ? getTargetPackage() : getPackage()`, written out at six call sites, becomes AnyOperation::get_target_package(). InstallationManager's three blocks that matched on the type string and then recovered the type with expect() collapse into exhaustive matches. SolverOperation keeps only its TYPE constant; the shared getOperationType()/__toString() implementations move to AnyOperation, which also drops the five Self::TYPE.to_string() allocations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25chore: remove unused importsnsfisis
2026-07-24fix(event-dispatcher): invoke Closure listeners instead of always failing ↵nsfisis
is_callable RequireCommand registers an inline listener on InstallerEvents::PRE_OPERATIONS_EXEC to track dependency_resolution_completed, mirroring PHP's `function () use (&$dependencyResolutionCompleted) { ... }`. This is Composer's own code, not a Plugin subscriber, but it went through the shared non-string-callable path, which checked is_callable() against a hardcoded PhpMixed::Null and always failed, breaking every `require` that reaches the install step. Callable::Closure now carries the actual Rc<dyn Fn> instead of being a data-less placeholder, and is invoked directly (Closures are always callable in PHP). The ArrayCallable path used by future Plugin subscribers is untouched. Un-ignoring the two require_command_test cases that cited this bug reveals two separate, pre-existing issues (a missing ext-requirement warning message, and a RefCell re-entrancy panic in ConsoleIO::ask_question); their #[ignore] reasons are updated to describe the real current blocker instead of the now-fixed one.
2026-07-24test(installer-test): revert to loop-based fixture iterationnsfisis
4a5a9556 split each installer/installer-slow fixture into its own #[test] fn (with #[ignore] on known failures) so they could be triaged one at a time, and said explicitly that it should be reverted to a single loop-based test per group once triage was done. Only github-issues-7665.test still fails, and its cause is now understood (see the previous commit): an upstream Composer defect, not a porting bug. Restore the three loop-based tests, skipping that one fixture inline with a TODO(phase-d) comment instead of a per-fixture #[ignore].
2026-07-24docs(installer-test): record confirmed cause for github-issues-7665 ignorensfisis
The ignore reason for slow_github_issues_7665 said "unknown reason, needs further investigation." Investigation since then traced the mismatch to an upstream Composer defect: Problem::getPrettyString's RULE_LEARNED tie-break comparator (getSortableString() <=> getSortableString()) is not transitive, and the values it compares are literal ids that shift with the platform package count, which Installer::createPlatformRepo() derives from the real ambient PHP runtime and which Composer's own test suite never mocks. This fixture is brittle to whichever extensions are installed on the machine generating or running it. Nothing to fix on the Rust side; this is an upstream Composer bug (composer/composer#12111 introduced the comparator).
2026-07-24fix(solver-problems): compare RULE_LEARNED sort keys like PHP's <=>nsfisis
Problem::getPrettyString sorts same-priority reasons by getSortableString(), whose RULE_LEARNED key is a '-'-joined literal id string (e.g. "-95"). PHP's <=> compares two numeric strings numerically, but the port used plain String::cmp (byte-wise), which reverses relative order for same-length negative-number keys. Added shirabe_php_shim::loosely_compare to approximate PHP's <=> for this pattern (numeric compare when both sides parse as numbers, else byte compare) and switched the sort comparator to use it. The diagnosis this replaces (from the commit being amended) blamed Pool package-id assignment order diverging from PHP under COMPOSER_POOL_OPTIMIZER=0. That was disproven this session: direct instrumentation of both PHP and the Rust port confirmed identical relative package-id order, including on the ~335-package github-issues-7665 fixture (ids matched up to a constant +2 offset from a platform-mock package count difference). The sort comparator was the actual bug, not package loading order. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24fix(solver-problems): preserve first-occurrence order in extension hintnsfisis
create_extension_hint's --ignore-platform-req suggestion list sorted missing extensions alphabetically before deduping. PHP's array_unique removes duplicates while preserving first-occurrence order instead, which for this call site matches the order problems were reported in (root-require-not-found problems before SAT-conflict problems). Replaced the sort+dedup with the existing order-preserving shirabe_php_shim::array_unique, which was already ported for exactly this PHP semantic but wasn't used here.
2026-07-24fix(installer-test): catch exceptions thrown during composer constructionnsfisis
PHPUnit's expectException/expectExceptionMessage wrap the whole rest of InstallerTest::doTestIntegration, so a fixture's expected exception can legitimately come from FactoryMock::create() itself (root package construction), not just the later install/update run. The Rust port only checked for that at one point, after the run, and unconditionally unwrapped Factory::__create_mock's result — so a fixture like install-self-from-root.test (root package requiring itself, which throws during root package construction) panicked on that unwrap instead of being caught and compared against the expected message. Capture the construction Result and, when an exception was expected, run the same normalize/contains/assert check the later block already uses before returning early, matching PHPUnit's behavior of running no further test-method code once the expected exception has fired.
2026-07-24fix(array-dumper): dump source/dist mirrors as a JSON arraynsfisis
ArrayDumper::dump built the mirrors field as PhpMixed::Array keyed by stringified index ("0", "1", ...), which this codebase's PhpMixed JSON serialization renders as an object. PHP just assigns the plain, sequentially-keyed mirrors array directly, which json_encode renders as a JSON array. Use PhpMixed::List instead, matching the actual shape written to composer.lock.
2026-07-24fix(package): rewrite dist-reference SHA regex without look-aroundnsfisis
Package::set_source_dist_references and LockTransaction's dist-url mirroring both used {(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i, but the regex crate has no look-around support at all and panics compiling it. Per docs/dev/regex-porting.md, rewrote the boundary assertions into capturing groups and switched to Preg::replace_callback, which re-emits the captured delimiters around the replaced reference.
2026-07-24fix(composer-repository): initialize wrapper before delegating loadPackagesnsfisis
For a simple, non-lazy composer repository (no providers-url/ metadata-url/providers-lazy-url), load_packages() delegates to self.inner.load_packages() (PHP: parent::loadPackages()). PHP's ArrayRepository::loadPackages() calls $this->getPackages(), which virtual-dispatches through ComposerRepository::getPackages() down to ComposerRepository::initialize() (the real HTTP/file fetch). Composition doesn't get that dispatch for free: self.inner is a bare ArrayRepository, so self.inner.load_packages() ends up calling ArrayRepository::initialize() — a no-op stub that just sets an empty package list — instead of ComposerRepository::initialize(). The repository's packages.json fetched and parsed successfully, but the result was silently discarded, so the repo always looked empty. get_packages() already carries the identical guard with the same diagnosis in its own comment; load_packages() was just missing it. Documented the same latent gap in find_package/find_packages/count/ has_package, which call into ArrayRepository the same unguarded way but aren't known to be exercised by any test yet.
2026-07-24fix(installer-test): port InstallerTest::setUp's chdir(__DIR__)nsfisis
PHP's InstallerTest::setUp() does chdir(__DIR__) before every test so relative "type": "path" repository URLs in fixtures resolve against composer/tests/Composer/Test; tearDown() restores the previous cwd. This was never ported, only the env-var clears were, so any fixture declaring a relative path repo failed at PathRepository::initialize() with "the `url` supplied for the path (...) repository does not exist" — not a missing PathRepository feature, just an unset cwd. Turned TearDown into a real guard that captures and chdirs on construction and restores on Drop. Since set_current_dir is process-wide state and tests run concurrently by default, added #[serial] to test_installer (the only call site not already inside a #[serial] macro-generated fn) so no two TearDown-holding tests can race on cwd; serial_test's plain #[serial] shares one unnamed lock across the whole binary, so this is sufficient.
2026-07-24fix(pool-builder): use plain getPackages() on locked repositorynsfisis
PoolBuilder::build_pool() and warn_about_non_matching_update_allow_list() called get_canonical_packages() on the locked repository during a partial update, but PHP's PoolBuilder uses the plain getPackages(). get_canonical_packages() unwraps AliasPackage down to its base package, discarding the alias's own version identity. Locker::get_locked_repository() wraps a lock entry with extra.branch-alias in a single CompleteAliasPackage rather than adding a separate base object, so canonicalizing it silently dropped the locked branch-alias version (e.g. 2.2.x-dev), leaving only the raw dev-master version behind. For a package excluded from the partial update's allow-list, that meant its locked branch-alias version could no longer satisfy the root's constraint, producing a spurious solver conflict instead of keeping the package pinned exactly as locked. Fixed the same bug in AuditCommand's --locked package listing, which had the identical get_canonical_packages()/getPackages() mismatch against AuditCommand.php.
2026-07-24fix(dependency-resolver): query real PHP for ext-* version/loaded checksnsfisis
Two shim gaps made the extension-related branches of solver-problem messages wrong: - phpversion($ext) with a non-empty extension can't be known statically (it's shirabe-php-shim's todo!()); Problem::get_missing_package_reason now calls shirabe_php_rpc::phpversion, the same RPC bridge platform::runtime::Runtime::get_extension_version already uses. - extension_loaded's hardcoded allowlist was missing "pcre", a mandatory always-compiled-in PHP extension, so ext-pcre was misreported as "missing from your system" instead of "disabled by your platform config" whenever a platform override disabled it. Also fix XdebugHandler::getAllIniFiles() always returning `[""]` (a php-runtime stub): create_extension_hint()'s early-return guard (`paths[0] empty && len==1`) fired unconditionally, silently dropping the entire "To enable extensions..." hint from every solver-problem message that mentions missing extensions. shirabe-external-packages can't depend on shirabe-php-rpc (shirabe-php-rpc already depends on shirabe-external-packages), so IniHelper::get_all() queries a new get_all_ini_files RPC command directly instead of going through the stub. This exposed that XdebugHandler is never constructed with a name because bin/composer's restart-without-Xdebug bootstrap was never ported to main.rs, making COMPOSER_ORIGINAL_INIS-driven behavior unreachable; documented with a TODO(phase-c) and updated ini_helper_test.rs's ignore reasons (and ignored test_with_no_ini, which only passed before by coincidence with the old stub's constant output) to match.
2026-07-24fix(installer-test): classify EXPECT-LOCK by PHP truthiness, not string matchnsfisis
PHP's readTestFile splits sections with a regex that leaves a section's own trailing newline attached when it is the file's last section (verified against real PHP); for install-without-lock.test and update-without-lock.test, --EXPECT-LOCK-- is that last section, so its raw content is "false\n", not "false". PHP's `$expectLock === 'false'` check therefore also misses, but PHP falls through to JsonFile::parseJson("false\n"), which json_decodes to boolean false anyway, and every downstream check in doTestIntegration branches on PHP truthiness of $expectLock, so the outcome is unaffected either way. The Rust port only mirrored the literal string comparison, so the same "false\n" produced ExpectLock::Json(Value::Bool(false)) instead of ExpectLock::Never, and do_test_integration's Json arm unconditionally read composer.lock, panicking because config.lock=false means no lock file is ever written. Parse EXPECT-LOCK as JSON first and classify by PHP truthiness of the result, matching what doTestIntegration actually checks instead of the raw string.
2026-07-24fix(php-shim): keep literal '>' outside tags in strip_tagsnsfisis
The b'>' match arm in strip_tags's state machine never pushed the character to the output buffer when encountered outside a tag (state 0), unlike every other special-character arm (!, ?, -, and the catch-all) which does push in that state. Every literal '>' not part of an HTML-like tag was silently dropped. This corrupted "=>" into "=" in Composer's operation trace strings (e.g. "Upgrading foo/bar (1.0.0 => 1.1.0)"), which go through strip_tags to remove the <info>/<comment> markup before comparison, un-ignoring 82 integration tests that were asserting on that exact arrow.
2026-07-24fix(locker): return stability-flags as int, not stringnsfisis
Locker::get_stability_flags returned IndexMap<String, String>, converting each value via PhpMixed::as_string(), which only matches the String variant. composer.lock's "stability-flags" values are always JSON integers (BasePackage::STABILITIES), so every flag silently decoded to "" and installer.rs's downstream .parse::<i64>() defaulted it to 0 (stable). This made any locked package pinned via a non-stable stability-flags entry look "unacceptable" during `composer install`, silently dropping it from the solver's pool instead of fixing/requiring it — turning a real dependency conflict into a spurious "lock file needs changes" result. Return i64 directly, matching RootPackageInterface::get_stability_flags and set_lock_data's existing convention for this same PHP array shape.
2026-07-24fix(installer-test): match Locker content-hash encoding to lock hashnsfisis
do_test_integration built the Locker's composer.json content string with serde_json::to_string, which doesn't escape slashes, while the fixture's auto-computed lock "hash" field used JsonFile::encode_with_options with slashes escaped (mirroring PHP's json_encode default). Since virtually every package name contains a "/", this made Locker::isFresh() spuriously report every such lock as stale, unignoring 12 fixtures that depended on the lock being recognized as fresh during `install`.
2026-07-24test(installer): split integration tests into one #[test] per fixturensfisis
Replace the three loop-based test functions (each iterating all fixtures with a shared SKIP_INSTALLER_* skip list) with macro-generated per-fixture test functions, so a single failing or panicking fixture no longer hides the pass/fail status of the rest. Known-failing fixtures keep their TODO(phase-d) reasons via #[ignore] on their own test instead of a shared skip list. All generated tests are #[serial] since they mutate global env vars (COMPOSER_POOL_OPTIMIZER, COMPOSER_FUND) that would otherwise race across parallel test threads. This is an interim structure for triaging the ~131 ignored fixtures one at a time; it should be reverted to a single loop-based test per group (matching Composer's directory-scanned fixtures) once they are resolved.
2026-07-24refactor(filesystem-repository): drop eval() shim, defer installed.php ↵nsfisis
reload to PHP runtime Rust has no PHP interpreter, so eval() can never be ported faithfully. safely_load_installed_versions()'s job of priming Composer\InstalledVersions before plugins run only matters within a single shared PHP process, which the RPC-based plugin architecture does not have; the PHP runtime process can call InstalledVersions::reload() itself instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23feat(process-executor): stub plugin-facing execute_async_php pathnsfisis
A plugin can reach ProcessExecutor::executeAsync() through the rust-proxy stub, which resolves with a real Symfony Process instance that can't be reconstructed on the Rust side (its state is tied to whichever process calls proc_open(), and it refuses serialization). Add execute_async_php() as a todo!() stub, documented as a dual- instantiation split in plugin-class-classification.md: Rust-internal callers keep using execute_async(), while the plugin path must forward spawning to the PHP child once the RPC channel exists.
2026-07-20perf(composer-repository): avoid redundant clones when checking security ↵nsfisis
advisories get_security_advisories cloned each package's full metadata blob (hundreds of versions for popular packages) just to peek at its "security-advisories" field. Same pattern already fixed in load_async_packages by 2862d2da; move ownership through pattern matches and IndexMap::shift_remove instead. Measured with laravel/laravel create-project: response processing in the security-advisories metadata branch drops ~80% (236ms -> 48ms), and the whole run_security_advisory_filter phase drops from ~420-500ms to ~250-270ms. End-to-end, shirabe now matches or edges out upstream Composer (6.57s vs 6.85s in this run) instead of trailing by ~1.2s.
2026-07-20perf(composer-repository): avoid redundant clones when loading async package ↵nsfisis
metadata load_async_packages cloned each response's version metadata up to three times (spec_list/response, response_arr, versions_mixed, then every per-version field) before building the versions Vec. Move ownership through pattern matches and IndexMap::shift_remove instead. Measured with laravel/laravel create-project: per-batch response processing time in load_async_packages drops ~39% (1.30s -> 0.80s cumulative), narrowing the E2E gap vs upstream Composer from 1.37s to 1.14s on average.
2026-07-20fix(json-file): forward detected indent into write's encode optionsnsfisis
JsonFile::write_with_options() used the caller-supplied JsonEncodeOptions verbatim, ignoring self.indent (set by read()'s detect_indenting), unlike PHP's write() which always passes $this->indent to encode() regardless of the $options argument. Un-ignore test_preserve_indentation_after_read.
2026-07-20fix(search-command): avoid usize overflow panic when truncating descriptionnsfisis
The negated remaining-width subtraction was cast straight to usize, overflowing whenever it went negative (e.g. an abandoned-package warning ate the available width) and panicking on slice indexing. Route through the shim's substr(), which mirrors PHP's negative-length semantics and clamps instead of overflowing.
2026-07-20fix(pool-builder): fix three build_pool bugs found by un-ignoring ↵nsfisis
test_pool_builder build_pool never populated skipped_load for locked packages skipped due to the update allow list (nor their replace targets), so load_package's skipped_load-driven transitive-unlock branch never fired. load_packages_marked_for_loading never recorded loaded packages into loaded_per_repo, so repositories had no way to dedupe already-loaded versions when a constraint got re-expanded, producing duplicate pool entries. unlock_package looked up a locked package's removal index via the position in a Vec snapshot of self.packages.values() instead of its actual IndexMap key, so after earlier removals the wrong entry (or none) got removed, leaving stale locked packages in the pool. Fixing all three lets test_pool_builder run un-ignored.
2026-07-20fix(no-proxy-pattern): stop chr() corrupting IP bytes as lossy UTF-8nsfisis
shirabe_php_shim::chr() returned a Rust String, which lossily re-encodes bytes >= 0x80 as UTF-8 replacement characters. ip_get_mask, ip_get_network, and ip_map_to_6 relied on chr() to build raw in_addr and netmask byte arrays, corrupting IPv4-in-IPv6 mappings and CIDR netmasks. Build the Vec<u8> byte arrays directly instead of round-tripping through String, and un-ignore test_ip_address and test_ip_range now that the underlying bug is fixed. chr()'s only other caller (http_downloader.rs, an ASCII ESC byte in a regex pattern) didn't need the indirection either, so remove the shim function entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20test(proxy-manager): un-ignore tests by serializing env-var accessnsfisis
The ignored tests raced on the process-wide HTTP_PROXY/etc env vars and the ProxyManager::INSTANCE mutex, causing spurious PoisonError panics when run in parallel. Adding #[serial_test::serial], the same annotation test_instantiation already used, fixes the race with no other changes needed.
2026-07-20fix(php-shim): use PhpMixed::to_bool() for PHP truthy castsnsfisis
Several call sites coerced PhpMixed to bool via `.as_bool()` (which only matches a literal Bool variant) where the corresponding PHP code does a plain `(bool)` cast or truthy check (isset()/array_key_exists() + implicit bool conversion). This silently dropped truthy non-bool values (e.g. String("true"), String("1"), Int(1)) to their unwrap_or default instead of PHP's actual truthy result. Switched these sites to PhpMixed::to_bool(), which implements PHP's full truthy-cast rules.
2026-07-20test(ignore): document root causes for unannotated #[ignore] testsnsfisis
74 tests carried a bare #[ignore] with no explanation. Re-ran each: 25 now pass and had the attribute removed; the remaining 49 got a concise reason (todo!() stubs, regex-crate PCRE gaps, PhpMixed type mismatches, config bool-coercion bugs, missing skipped_load wiring in PoolBuilder, etc.) so future work can find and fix them by grep. No production code or test logic/assertions were changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20fix(advisory): treat int-keyed ignore entries as plain IDsnsfisis
parseIgnoreWithApply distinguishes ['CVE-123' => 'reason'] from [0 => 'CVE-123'] by checking is_int($key) in PHP. AuditConfig only matched on the value's type, so a canonical-int-keyed string value was mistaken for an id => reason pair. Expose canonical_int_key from the php-shim to replicate PHP's key-canonicalization rule and unignore test_mixed_formats.
2026-07-20fix(diagnose-command): stop holding a Config borrow across http callsnsfisis
execute() held &config.borrow() across check_http/check_composer_repo/ check_composer_audit, which reach HttpDownloader -> CurlDownloader:: download; that method does self.config.borrow_mut() on the same Config RefCell, panicking with "RefCell already borrowed" once the phpinfo panic that previously masked this was fixed. Switch those three helpers to take the Rc<RefCell<Config>> handle (matching check_version's existing pattern) and borrow only where a field is actually read, so no borrow spans the downstream network call. Un-ignore the now-passing run_diagnose smoke test and test_cmd_fail; test_cmd_success stays ignored, now for two separate reasons: it needs real network access (as the PHP original does), and shirabe_php_shim::OPENSSL_VERSION_NUMBER is a hardcoded stub (0) that always trips check_platform's TLSv1.1/1.2 support check regardless of the real linked OpenSSL, forcing a non-zero exit code.
2026-07-20feat(php-rpc): implement phpinfo() capture over RPCnsfisis
DiagnoseCommand::check_platform needed phpinfo() output but shirabe-php-shim's ob_start()/phpinfo()/ob_get_clean() are unmodeled todo!()s. Add a phpinfo dispatch entry to the PHP worker (capturing output the same way extension_info already does) and a get_phpinfo() wrapper, then switch check_platform to call it directly. This unblocks the phpinfo-related panic in diagnose; the ignored tests now hit a separate RefCell double-borrow bug in check_http, so their ignore reasons are updated to point at that instead.
2026-07-20test(fund-command): match PHP tester options for deterministic displaynsfisis
capture_stderr_separately routed the tester through ConsoleOutput, which detects color/hyperlink support from the real process STDOUT before the stream is swapped to the memory buffer. When STDOUT is a real tty, decoration leaks into the captured output. The PHP original never passes this option and get_error_output() was unused here, so drop it to match and keep the assertion deterministic.
2026-07-20test(show-command): ignore test_self over UTC-only date() timezone gapnsfisis
The shim date() renders in UTC only (no timezone database) while PHP's date() uses the system default timezone, so get_relative_time misses the "today" match and prints "this week" whenever the local date differs from the UTC date (daily 00:00-09:00 JST on this machine). Verified by running the test with and without TZ=UTC at 07:40 JST. Mark the gap with a TODO(phase-c) in the shim; fixing it needs a timezone database (a new crate), which is a user decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>