aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/downloader
AgeCommit message (Collapse)Author
2026-08-24refactor(silencer): stop guarding work that stays inside Rustnsfisis
Silencer only lowers the PHP error_reporting() level and re-throws whatever the guarded work raises. A region that never reaches the PHP runtime has no level to lower and emits no diagnostic on failure, so wrapping it is indistinguishable from running it unguarded. The pair kept in Application::hint_common_errors brackets a getComposer() call, which loads installed plugins and dispatches PluginEvents::INIT. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23fix(fs): carry file_get_contents results as bytesnsfisis
file_get_contents() and file_get_contents_with_max_length() return Vec<u8> instead of a from_utf8_lossy'd String. Call sites whose consumer takes a &str still convert lossily and are marked TODO(bytes). file_get_contents_with_max_length() now reads at most the requested number of bytes instead of the whole file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22feat(path-downloader): implement safe_junctions as always truensfisis
Composer requires Windows 7 or later with proc_open available before it will use junctions, because a PHP bug (bugs.php.net #77552) makes junction detection fragile and can lose the target content when a package is removed. Junctions are created and removed here without going through PHP, so that bug does not apply and the check always passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22feat(cli): tell the user to run `shirabe`, not `composer`nsfisis
Messages that instruct the user to run a command named the Composer binary. Shirabe ships as its own binary, so the hints now name it. File names (composer.json, composer.lock), the "composer" repository type and prose about Composer itself are untouched. The installer and functional integration fixtures live in the Composer submodule and cannot be edited, so their expected output is normalized on load.
2026-08-18refactor(zip): return a ZipEntryStat struct from stat_indexnsfisis
The stat array was modelled as IndexMap<String, PhpMixed>, forcing callers through untyped lookups with an unwrap_or(0) fallback PHP has no counterpart for. Only `size` and `comp_size` are kept: statIndex has a single call site in Composer and it reads nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18fix: propagate ported exceptions instead of flattening themnsfisis
`ProcessExecutor::execute_args` existed only to turn `execute`'s `anyhow::Result` into an exit code of 1, so every one of its ~87 call sites silently took the "command failed" branch on an error PHP would have thrown. It is gone; callers use `execute` and propagate with `?`. Where the enclosing function had no `Result` to propagate into, its signature grew one, up to and including `Git::get_version`, `Svn::binary_version`, `GitHub`/`GitLab`/`Bitbucket::authorize_oauth`, `InitCommand::get_git_config` and `DiagnoseCommand::check_git`. The VCS drivers had the same problem in the other direction: their `get_contents` returned `Result<Response, Box<TransportException>>`, a type too narrow for the PHP method, which lets any Throwable out of the `catch (TransportException $e)` block. Every non-transport error was therefore rewritten into a `TransportException` with code 0, which the callers switch on. They now return `anyhow::Result<Result<Response, Box<TransportException>>>`: the outer `Result` carries what PHP does not catch, the inner one the exception the drivers handle. That signature also restores `GitLabDriver::getContents`: the 400/401 `TransportException`s it raises to force authentication are thrown inside its own `try` block and handled by its own `catch`, but the port returned them straight to the caller, so the authentication flow behind them never ran. `impl_php_exception!` gains `From<Box<$ty>> for anyhow::Error` so a caught exception can be re-propagated with `?`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18fix(process-executor): model commands as a CommandLine enumnsfisis
Commands were carried as `PhpMixed`, whose `String`/`List` variants do not tell a shell command line apart from an argv list at the type level. `Perforce::execute_command` and `Git::run_command` therefore funnelled string commands through `execute_args`, spawning `p4 set` or `git command` as a single argument instead of running it through a shell as PHP does; their tests were written against that shape. Introduce `CommandLine::{Shell, Args}` and use it for every `ProcessExecutor` entry point, the mock expectation queue and the `Git::run_command` callables. The unreachable "Invalid command type" branches disappear with it, and the affected tests go back to the string expectations the PHP suite uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18refactor(function-exists): drop checks for always-present capabilitiesnsfisis
function_exists() returns false in PHP when a function is blocked by disable_functions, when its extension is not compiled in, or when the PHP version predates it. None of those apply to a native binary.
2026-08-18refactor(preg): make preg_match_all yield matches per occurrencensfisis
PHP's PREG_PATTERN_ORDER is column-oriented, but 7 of the 10 call sites read it row-wise, rebuilding each occurrence by indexing every column at the same offset. Return an iterator of PregMatches instead, which is also what the set-order and offset-capture variants were carrying, so the three functions collapse into one and PregMatchesAll, PregMatchesAllWithOffsets, CaptureKey and preg_match_map! all go away. The offset-capture call sites are served by the new PregMatches get_offset/name_offset accessors. The search stays eager: regex::Captures borrows only the subject, so the matches outlive the pattern resolved for the call, and PHP's preg_match_all is eager too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18refactor(preg): add preg_is_match for existence-only call sitesnsfisis
The capture groups were discarded at 162 of the preg_match call sites, which only tested the Option. They now call preg_is_match, which lets the regex engine skip capture tracking. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18refactor(preg): drop the offset argument from preg_matchnsfisis
Every call site but one passed offset 0. The remaining one, the UTF-8 chunking loop in Application, slices the subject instead: its pattern has no anchor or lookaround, so matching a suffix is equivalent to starting the search at that offset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18refactor(preg): drop the Vec-returning preg_match_allnsfisis
The two preg_match_all variants took the same arguments and differed only in what they returned: a Vec of columns, or the named-and-numbered PregMatchesAll. The latter is the one all but two call sites already used, so preg_match_all2 takes over the plain PHP name and the Vec variant goes away. Its remaining readers only ever wanted group 0's column, which they now take through CaptureKey::ByIndex(0); in the formatter this replaces the array_shift that popped that column off the PREG_PATTERN_ORDER array. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18refactor(pcre): inline Preg into its call sites and drop the cratensfisis
Preg had shed everything it owned: after the last few rounds its methods were one-line forwards to the shim's preg_*(), differing only in a default argument or a wrapper the caller unwrapped anyway. The 460 call sites now name the shim function, and shirabe-pcre is gone from the workspace along with its LICENSE entry. The forwards expand as they read: isMatch becomes preg_match2(.., 0).is_some() (is_none() where PHP negates it), isMatch3 and match3 drop the .is_some(), matchAll counts through preg_match_all2(..).occurrence_count(), and replace4/replace5 spell out the limit and count arguments preg_replace2 takes. Callbacks are the one place the shapes differ: preg_replace_callback carries an error out of the callback, so the fourteen infallible closures wrap their result in Ok() and expect() it back. Config::process() is the fifteenth, and it drops the `error` cell it captured to smuggle a failure past a closure that could only return a String. The `?` in the closure now carries it, which is what the PHP does -- a throw from the callback leaves preg_replace_callback at the failing match rather than running the remaining replacements and reporting the last error. The module doc that explained why composer/pcre's exceptions and *StrictGroups() variants have no counterpart moves to the shim's preg module, where the functions it describes live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18refactor(preg): split PregMatches reads into get() and name()nsfisis
PregMatches keyed both forms of a capture group through CaptureKey, so every read built one: a usize wrapped in an enum, or worse, a String allocated to name a group that regex::Captures can look up from a &str. It now mirrors regex::Captures instead -- get() takes the group number, name() the group name -- and the enum drops out of the type entirely. That is 285 call sites across 59 files, and the named ones carry most of the win: `matches.get(&CaptureKey::ByName("host".to_string()))` reads as `matches.name("host")`. ProcessExecutor loses a `user_key` binding that existed only to build the key once. CaptureKey stays as the key type of PregMatchesAll and PregMatchesAllWithOffsets, where numbered and named entries share one IndexMap and a key type is the point. Five files still name it. Also retargets the two preg_match_all comments that described the occurrence count through `matches[&CaptureKey::ByIndex(0)].len()`, an Index impl these types no longer carry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18refactor(pcre): hand back the match instead of copying it outnsfisis
Preg::match4 and Preg::replace_callback gave callers a PregMatchedGroups: an IndexMap rebuilt from the match with an owned String per group, plus a second String for a named group's name key. That is the copy PregMatches shed when it started wrapping regex::Captures, reinstated one layer up -- and nearly every regex call in the tree goes through Preg rather than the shim's preg_* directly, so almost nothing saw the borrow. PregMatchedGroups existed only to drop the null (unmatched) groups the old PregMatches held as Option<String> values. PregMatches::get reports a non-participating group as None on its own, so the two read alike and the type collapses into it. Call sites still reach groups through get(&CaptureKey::ByIndex(N)); what changes is that the value arrives as a &str borrowed from the subject, which the signatures now carry as a lifetime. Three places needed the borrow reckoned with rather than a mechanical rewrite: PhpFileCleaner::clean and Problem::get_messages read their groups out before mutating what the match borrows, and Git::get_authentication_failure names the lifetime of its url argument, which the result borrows instead of self. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17refactor(pcre): return the Preg $matches instead of filling an out-paramnsfisis
`Composer\Pcre\Preg` fills `$matches` through a by-ref parameter, and the port mirrored that with a `&mut` (or `Option<&mut>`) out-param plus a bool or count return. Callers had to declare an empty map one line ahead of the call, and the type never said the map is only meaningful when the call matched. Return the matches instead: - match3/match4/is_match3/is_match4 -> Option<PregMatchedGroups> - is_match_named -> Option<PregNamedGroups> - match_all2/is_match_all -> PregMatchesAll - is_match_all_with_offsets3 -> PregMatchesAllWithOffsets Nothing is lost: the bool is `Option::is_some()`, and the occurrence count is the length of any one column of a PREG_PATTERN_ORDER map, now spelled `PregMatchesAll::occurrence_count()`. is_match() still answers the bool question directly for callers that want no groups. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17refactor(preg): wrap the preg_* $matches maps in newtypesnsfisis
The five IndexMap shapes that the preg_* functions and Preg fill in are now distinct types generated by preg_match_map!, so a matches map no longer interchanges with any other map of the same key and value type. Index<usize> is kept alongside Index<&Q> because call sites such as config_command and event_dispatcher reach for a group by its position in the map rather than by its capture key.
2026-08-17refactor(preg): replace Preg::split*() with shim preg_split*()nsfisis
preg_split2()'s limit was always -1 and its flags were always either 0 or PREG_SPLIT_DELIM_CAPTURE alone, so both arguments are gone: the shim now exposes preg_split() and preg_split_delim_capture() over a shared preg_split_impl(). That leaves Preg::split()/split4() as bare pass-throughs, so callers use the shim functions directly and the wrappers are dropped along with the now-unreferenced PREG_SPLIT_* constants. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17refactor(preg): drop Option wrapper from matches arg of match_all*()nsfisis
Every caller of Preg::match_all3()/is_match_all3() passed Some(&mut _), so the argument is now a plain &mut. Preg::match_all() keeps the no-captures form with a local throwaway map, and the arity suffixes are renumbered accordingly (match_all2(), is_match_all()). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17fix(pcre): preserve unmatched groups in Preg::match_all*()nsfisis
PHP's Preg::matchAll() and matchAllWithOffsets() always set PREG_UNMATCHED_AS_NULL, so a non-participating group is `null` and its offset is -1. The Rust wrappers collapsed those to "" and 0, so callers could not tell a group that did not participate from one that matched an empty string at offset 0, and the offset value matched no PHP mode at all. Hand the shim's representation through unchanged and let each caller mirror what the PHP original does with it: `isset()` and `(string)` casts stay lenient, while `assert(is_string(...))` and the *StrictGroups() variants become `expect()`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16refactor(php-shim): give parse_url a typed UrlComponents resultnsfisis
parse_url now returns Option<UrlComponents> instead of a PhpMixed array, and the component-selecting overload with the PHP_URL_* constants is gone: callers read the field they want. Two call sites change behaviour as a result, both towards PHP: * CurlDownloader::handle_redirect tested scheme and host with is_null(), so an unparsable Location header (PhpMixed::Bool(false)) counted as an absolute URL. PHP's truthiness test sends it to the relative-path branch. * Url::get_origin appended a literal port 0, which PHP treats as falsy and leaves off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16refactor: narrow pub(crate) items to privatensfisis
Porting mapped every PHP `protected` member onto `pub(crate)`, which is wider than nearly all of them need. Each item demoted here is reached only from the module that defines it, so the crate-wide visibility conveyed nothing. Every `pub(crate)` that survives has at least one reader in another module of the same crate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15feat(cli): report Shirabe's own identity instead of Composer'snsfisis
The binary called itself Composer everywhere: the application name, the logo, --version, about, and every warning that talks about the running program. Prompts to file a bug also pointed at Composer's issue tracker. Add SHIRABE_VERSION and SHIRABE_RELEASE_DATE next to the Composer version constants and report those, naming the Composer version this port tracks alongside them. Composer::VERSION and getVersion() are untouched, so the composer platform package, composer-runtime-api and the HTTP User-Agent keep the value plugins and package repositories expect. build.rs stamps the release date with the UTC date of the HEAD commit, the way Composer's Compiler fills in @release_date@ when building the phar. It now also fails the build when git cannot be read, instead of letting COMPOSER_DEV_WARNING_TIME fall back to the tagged-release value and suppress the outdated-build warning forever. Messages about the Composer ecosystem keep their wording. Two of them are pinned by upstream installer fixtures (Rule's "cannot be modified by Composer" and SolverProblemsException's "you can run Composer with") and stay as they are so those fixtures can keep being used verbatim. The e2e list comparison against upstream Composer now skips the banner, which cannot match by design, and compares everything below it as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15feat(php-shim): drop the modelled PHP version constantsnsfisis
The shim reported a fixed PHP 8.1.0 through PHP_VERSION, PHP_VERSION_ID, the major/minor/release triple and the PHP_WINDOWS_VERSION_* trio. Their uses split in two. Some guarded branches PHP only needs on runtimes this port cannot be: proc_get_status reports the exit status on every call, so Symfony's pre-8.3 exit-code cache has nothing to work around; hash_raw and hash_file always offer xxh3, so the sha1 fallback is unreachable; and http_get_last_response_headers is always available, so the pre-8.4 $http_response_header branch is gone. safeJunctions reads the host Windows version rather than PHP state, and joins the Windows work on hold. The rest ask about the PHP the user actually runs, and now reach the worker through a new php-rpc PhpVersion payload: the startup banner and the 7.2.5 warning, self-update's min-php filter, the ext-* recommendation in VersionSelector, the stream User-Agent, and whether PhpFileParser scans for enums. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11chore(php-shim): drop the substring predicate portsnsfisis
str_contains(), str_starts_with() and str_ends_with() were thin wrappers over the str methods of the same semantics. Call sites now use contains()/starts_with()/ends_with() directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09feat(path-downloader): restrict a mirrored path package to its archivable filesnsfisis
PathDownloader builds an ArchivableFilesFinder and hands it to Filesystem::mirror as the iterator that selects what to copy, so a path package installed by mirroring drops what .gitattributes marks export-ignore, what .gitignore excludes and the VCS directories. The port dropped the argument, and mirrored the source tree whole. Every method mirror() calls on the entries it walks is derived from the pathname, so the iterator is modeled as a list of pathnames rather than as a Traversable of SplFileInfo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09feat(symfony-filesystem): finish the Filesystem port and trim its APInsfisis
doRemove() renames a directory to a random hidden name before emptying it, and undoes that rename when the final rmdir fails, so a concurrent process cannot recreate the path mid-removal. It also walks one level at a time through FilesystemIterator instead of flattening the whole tree, and lets an inner rmdir failure pass, both as upstream does. copy() keeps the mode fopen($targetFile, 'w') would have left rather than the origin's, symlink() and mirror() call readlink() and getLinkTarget() where upstream does, and a directory iterator that cannot be opened propagates its UnexpectedValueException instead of being swallowed or flattened into an IOException. Error message text is out of scope per docs/known-incompatibilities.md, so the three TODO(phase-c) markers that only tracked wording are gone, along with linkException()'s Windows-only branch. So are the arguments no caller varies -- symlink()'s copyOnWindows, mirror()'s iterator and options, copy()'s overwriteNewerFiles -- which removes the last TODO(phase-c) in the file. New shim functions: readlink, filesystem_iterator, stream_is_local, strrev and SplFileInfo::getLinkTarget. base64_encode takes bytes so random_bytes() can feed it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(symfony-finder): extract symfony/finder into the ↵nsfisis
shirabe-symfony-finder crate Move `Symfony\Component\Finder` out of shirabe-external-packages and into its own crate, so the path is `shirabe_symfony_finder::Finder` instead of `shirabe_external_packages::symfony::finder::Finder`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(symfony-filesystem): extract symfony/filesystem into the ↵nsfisis
shirabe-symfony-filesystem crate Move `Symfony\Component\Filesystem` out of shirabe-external-packages and into its own crate, so the path is `shirabe_symfony_filesystem::Filesystem` instead of `shirabe_external_packages::symfony::filesystem::Filesystem`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(symfony-process): extract symfony/process into the ↵nsfisis
shirabe-symfony-process crate Move `Symfony\Component\Process` out of shirabe-external-packages and into its own crate, so the path is `shirabe_symfony_process::Process` instead of `shirabe_external_packages::symfony::process::Process`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(pcre): extract composer/pcre into the shirabe-pcre cratensfisis
Move `Composer\Pcre` out of shirabe-external-packages and into its own crate, so the path is `shirabe_pcre::preg::Preg` instead of `shirabe_external_packages::composer::pcre::preg::Preg`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(path): build paths with Path::joinnsfisis
Concatenating a directory, MAIN_SEPARATOR and a component reimplements what Path::join already does. Joining directly also drops the separator from the intermediate values it used to be baked into, such as ClassLoader's $pathEnd. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(php-shim): use std::path::MAIN_SEPARATOR over a shim constantnsfisis
The shim's DIRECTORY_SEPARATOR was hardcoded to "/", so every ported `'\\' === DIRECTORY_SEPARATOR` check compared against a constant that does not track the target platform. std::path::MAIN_SEPARATOR and MAIN_SEPARATOR_STR carry the same meaning as PHP's constant and resolve per platform, so the Windows branches are selected on Windows targets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09fix(exception): carry PHP's $previous through the ported throw sitesnsfisis
Composer hands the exception it caught to the one it throws in its place, so `getPrevious()` reaches the cause and Application's renderer prints the whole chain. Every ported site dropped it, because the flat exception structs had nowhere to put one. `AnyThrowable::into_previous` turns the caught error into that argument, and the 13 sites now pass it. `getCode()` came along for the ride at the four sites that derive the new exception's code from the caught one (PharArchiver, ArrayLoader x2), and ComposerRepository's message now names the caught exception's class instead of the literal "Exception". GitHubDriver::attemptCloneFallback took the previous exception's message and appended it to its own, which no `\RuntimeException('Fallback to git driver disabled')` in Composer ever says; it now chains it instead. Git::syncMirror restores what PHP's `finally` does to an exception in flight: the `git remote set-url` that scrubs credentials back out of the URL runs in a `finally`, and when it fails PHP propagates *its* exception over the one already leaving, chaining the displaced one as previous. The port discarded the finally's result, so a failure to scrub the URL was reported as a successful mirror sync. `AnyThrowable::set_previous` models the engine-level chaining. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08feat(php-shim): give ported exceptions PHP's class hierarchynsfisis
Ported exceptions were flat structs reached with `downcast_ref`, so Composer's `catch (\RuntimeException $e)` only matched the exact leaf type and `get_class($e)` had nothing to report. Each exception now embeds an instance of the class it extends and travels inside an `AnyThrowable`; `Catch::catch`/`catch_mut` walk that chain, and `PhpClass::php_class_name` yields the PHP FQCN. Dropping the `std::error::Error` impls from the exception types leaves `AnyThrowable` as the only route into an `anyhow::Error`, so the walk cannot be bypassed. A `no_exception_downcast` linter catches the `downcast::<X>()` calls that would now silently answer `None`. Three sites change behavior as a result: the `TransportException` exit-code override reaches `MaxFileSizeExceededException`, the `catch (\LogicException)` in findSimilar() reaches its subclasses, and rendered exception titles carry the real class name rather than a guess. `get_class_err()` is no longer a `todo!()`, which re-enables FilesystemRepositoryTest::testCorruptedRepositoryFile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08refactor(semver): replace Constraint's OP_*/STR_OP_* with CmpOpnsfisis
PHP's version_compare takes its operator as a string, so the shim's port did too, and Constraint carried two families of operator constants plus translation tables to convert between the string form and its own int codes. Five copies of those tables had accumulated across Constraint, CompilingMatcher and the plugin value bridge. version_compare now takes a CmpOp, which makes an invalid operator unrepresentable and removes the tables' reason to exist. Constraint stores a CmpOp and keeps only the string parsing its constructor needs; getOperator, compile and CompilingMatcher::match speak CmpOp as well. PHP's OP_* numbering stays observable: a plugin reads the raw integer off the Constraint object over RPC, so get_operator_constant and its new inverse hold that 0..5 mapping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07refactor: merge split inherent impl blocks into one per typensfisis
Enable clippy::multiple_inherent_impl and fix the 21 sites it reports. Types whose inherent methods were spread across two or three impl blocks now keep them in a single block; only the impl headers move, no method bodies change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07chore: use shirabe_php_shim::impl_php_classnsfisis
2026-08-06fix(download-manager): name the downloader class in its LogicExceptionnsfisis
getDownloaderForPackage reports get_class($downloader) when the resolved downloader's installation source does not match. Rust has no runtime class name, so the message was built from a shim stub that panicked instead — the error could never be returned. DownloaderInterface now requires PhpClass, the trait already used for the same purpose on Command, and each downloader states the name PHP reports. That leaves get_class_obj without callers, so it is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06refactor(php-shim): take &str and return String from pathinfonsfisis
pathinfo only ever receives a string and only ever returns one for the single-component options it supports, so the PhpMixed wrapping forced every call site to pack and unpack the value again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06refactor: replace literal-list in_array_strict with matches!nsfisis
Call sites whose haystack was an inline array of literals (or a local built solely to feed one) had to wrap both sides in PhpMixed just to compare, allocating a String per element on every call. matches! does the same test against the underlying &str/i64/Option directly, so the PhpMixed round trip and its .to_string()/.clone()/.iter().map() conversions are gone. Sites whose haystack is a runtime value or a named constant array are left on in_array_strict: inlining a named constant would duplicate its contents at the call site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06refactor(php-shim): split in_array into strict and loose variantsnsfisis
2026-08-06chore: drop @param/@return tags that only restate Rust typesnsfisis
The ported docblocks copied @param and @return straight from the PHP source. When such a tag carries nothing but a type and an argument name, the Rust signature already states it, so the line is noise. Tags whose text adds prose beyond the type are kept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06refactor(php-shim): take filesystem paths as impl AsRef<Path>nsfisis
The shim's filesystem entry points took `&str` even though each one resolves to a local path through `std::fs` or a syscall, so callers holding a `PathBuf` had to stringify it at the call site. They now take `impl AsRef<Path>`, the form `file_exists`, `is_dir`, `unlink` and the rest of the already-converted set use. `Phar`, `PharData` and `ZipArchive` keep their archive path as a `PathBuf`. `PharData::compress` names the compressed sibling by appending the suffix to the file name rather than formatting the path into a `String`. Arguments PHP resolves through a stream wrapper (`fopen`, `file_put_contents`, `include`) still take `&str`, as do the byte-string operations (`dirname`, `basename`, `pathinfo`) and archive-internal entry names, which are `/`-joined logical names rather than OS paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(io): propagate ask/select errors instead of panickingnsfisis
IOInterface::ask/select now return anyhow::Result<PhpMixed>, and ConsoleIO::ask_question forwards QuestionHelper errors (validator failures, MissingInputException) instead of collapsing them with .expect(). In PHP these exceptions propagate from QuestionHelper through ConsoleIO to the caller, so callers such as UpdateCommand's interactive package selection must be able to observe them; the MissingInputException is wrapped with its concrete type preserved so Application's ExceptionInterface downcast keeps working. All call sites now propagate with `?` (Perforce::query_p4_user becomes Result-returning: PHP declares it void but exceptions still escape), and the previously ignored test_interactive_mode_throws_if_no_package_entered passes. ask_confirmation/ask_and_hide_answer still collapse errors; extending propagation to them is left as TODO(phase-c) pending a decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(downloader): restore late binding in getLocalChanges/update pathsnsfisis
PHP's FileDownloader::getLocalChanges and ::update call $this->download() / $this->install() / $this->remove() / $this->getInstallOperationAppendix(), which late-bind to the concrete downloader class. The Rust port embeds the parent as `inner`, so delegating these methods to FileDownloader pinned the calls to FileDownloader's own implementations: `status` built the compare tree without extracting the archive (flagging every file of dist-installed packages as changed), and `update` re-installed the raw dist file instead of extracting it. Thread the concrete downloader in as `this: &dyn DownloaderInterface` via shared helpers (base_get_local_changes / base_update) and pass `self` from each delegating downloader. 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-25refactor: replace redundant clones with movesnsfisis
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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-20fix(process-executor): un-ignore 11 tests by implementing execute_async mock ↵nsfisis
support ProcessExecutor::execute_async's mock branch was an unimplemented todo!(), blocking every test whose code path calls it (feature-branch git diffing, system-unzip/7z fallback extraction). Implement it by: - Adding Process::__mock (mirroring the existing ZipArchive::__mock pattern) so execute_async can resolve with a fabricated, already- terminated Process instead of spawning a real subprocess. - Extracting the sync mock's expectation-matching logic into a shared ProcessExecutor::mock_match, wrapping the mock state in a RefCell so it works from execute_async's &self/&mut self receivers. - Setting error_output/capture_output from the mock branch, matching PHP's ProcessExecutorMock::executeAsync sharing doExecute with the sync path; execute_async now takes &mut self for this (safe, since the borrow only needs to live through the synchronous setup, not across the .await). - Turning a strict-mode expectation mismatch from a panic!() into a shirabe_php_shim::RuntimeException Err, mirroring PHPUnit's AssertionFailedError extending \RuntimeException: PHP call sites that catch (\RuntimeException $e) around a mocked git/hg/svn call (e.g. Git::get_mirror_default_branch, GitDriver::supports) treat a mismatch as an ordinary recoverable failure, and now so does the port. The RefMut is dropped before firing an expectation's optional callback so a re-entrant callback doesn't panic on double-borrow. Also fixes two real bugs found while porting test_private_repository_ no_interaction: GitHub::authorize_oauth and GitLab::authorize_oauth checked their domains config via PhpMixed::as_array(), which only matches the Array (map) variant, but github-domains/gitlab-domains default to PhpMixed::List, so the check always returned false and OAuth token lookup was silently skipped. Use the in_array shim instead, matching PHP's in_array() semantics. Also fix Git::run_command's "capture credentials from git remote -v" call, which used the panic- swallowing execute_args wrapper instead of a fallible execute(), so a mock mismatch there couldn't reach get_mirror_default_branch's catch. Un-ignores: - zip_downloader_test::test_system_unzip_only_{good,failed} - zip_downloader_test::test_non_windows_fallback_{good,failed} - event_dispatcher_test::test_dispatcher_outputs_error_on_failed_command - root_package_loader_test::test_feature_branch_pretty_version - version_guesser_test::test_guess_version_reads_and_respects_non_feature_branches_configuration_for_arbitrary_naming{,_regex} - version_guesser_test::test_remote_branches_are_selected - github_driver_test::test_private_repository_no_interaction (also adds the missing #[serial], since it seeds the shared Git::VERSION static that vcs_repository_test::test_load_versions depends on for real) - init_command_test::test_get_git_config, made deterministic by pointing HOME at a throwaway dir with its own .gitconfig instead of depending on the host's global git config Deduplicates the GitVersionGuard/RestoreEnv test-drop-guard idioms into tests/common/test_case.rs instead of reimplementing them per file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>