| Age | Commit message (Collapse) | Author |
|
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>
|
|
PregMatches was an IndexMap of owned Strings copied out of the match, so
every preg_match2/preg_replace_callback call allocated a String per
capture group (twice over for a named group) whether or not the caller
read it. It now wraps the regex::Captures itself, held alongside the
pattern it came from so groups stay reachable by both their named and
their numbered form, and hands out &str borrowed from the subject. The
subject's lifetime becomes a parameter of the type.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
`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>
|
|
PHP fills `$matches` through a by-ref parameter, which the port mirrored
with a `&mut` out-param plus a bool or count return. Every caller then
had to declare an empty binding one line ahead of the call, and nothing
in the type said the binding is only meaningful when the call succeeded.
Return the matches instead: preg_match() and preg_match2() hand back an
Option, and the three preg_match_all* functions hand back the collection
they used to fill.
The occurrence count the two map-shaped preg_match_all* functions used
to return is the length of any one of the map's columns, so it is not
lost -- Preg::match_all() and friends derive it via occurrence_count().
preg_replace2() keeps its `count: Option<&mut usize>`: that one is not
derivable from the replaced string, and callers that do not want it pay
nothing for passing None.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
preg_match_all() and preg_match_all_set_order() were the last preg_*
functions handing back a bare Vec<String>, where a group that did not
participate is indistinguishable from one that captured "". Hand back
Option<String> as the map-shaped functions already do; php_match_row(),
the last of the truncate-then-pad helpers, goes with them.
preg_split_delim_capture() keeps its Vec<String>. preg_split() accepts
no PREG_UNMATCHED_AS_NULL, so there is no null form to move it to, and
its result interleaves split segments -- which can never be absent --
with the captured delimiters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The shim carried two reporting modes for the preg_* $matches maps: PHP's
default (trailing unmatched groups dropped, interior ones ""), and the
PREG_UNMATCHED_AS_NULL form, picked by calling a *_unmatched_as_null()
variant. The regex crate hands out Option<Match>, which maps onto the
null form directly, and no caller distinguished a dropped group from a
null one -- preg_match() and preg_match_all2() already reported nulls
unconditionally. Keep only the null form; the shim API no longer mirrors
PHP's flag set, which is intended.
Preg::is_match_with_indexed_captures() modelled PHP's "unset" as a
truncated Vec<String>, and now returns Vec<Option<String>>. That is what
Composer actually does: Preg::isMatch() always sets
PREG_UNMATCHED_AS_NULL, and its callers test groups with `!== null`.
preg_match_all(), preg_match_all_set_order() and
preg_split_delim_capture() still hand back Vec<String> and keep the ""
form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The possessive quantifiers in Symfony's tag-matching patterns only
suppress backtracking: the open-tag class excludes backslash, so giving
a character back can never let the \\. alternative or the closing >
match, and the same holds for the close-tag class and >. Removing them
is the documented port for performance-only possessive quantifiers, not
an approximation waiting on a PCRE engine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
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.
|
|
preg_match2() and preg_match_all_offset_capture() each become a pair over
a shared private impl, and their flags arguments are gone: no caller passed
anything but 0 or PREG_UNMATCHED_AS_NULL. Preg::match5()/is_match5() lose
their own flags argument for the same reason -- both call sites passed 0,
and the value had nowhere left to go -- so they are renumbered to
match4()/is_match4(). PREG_UNMATCHED_AS_NULL itself is now unreferenced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The alternation that stands in for the original conditional subpattern
reports the branch it did not take as an empty string, so `dvar` was
always present and won over `pvar`, leaving %VAR% unexpanded. `\w+`
cannot capture an empty string, so an empty branch means it did not
participate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Each branch of the flag is now its own function. The PREG_UNMATCHED_AS_NULL
variant needs neither the trailing-group truncation nor the empty-string
fallback, so it loses the break condition and the last_participating scan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Preg::match5() rejected PREG_OFFSET_CAPTURE and check_set_order() rejected
PREG_SET_ORDER, mirroring the PHP where those flags would change the type
of $matches. The Rust ports of both take a typed `matches` out-param
instead, so neither flag can reach them; check_set_order() already had no
caller. The three constants are unreferenced once the guards are gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The PHP splits with PREG_SPLIT_NO_EMPTY, but the port dropped that flag,
so a leading separator in NO_PROXY kept an empty first entry. That made
"empty($hostNames) || '*' === $hostNames[0]" false for values such as
" *", turning "bypass the proxy for every host" into "use the proxy
for every host".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
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>
|
|
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>
|
|
match_all5() was identical to match_all3(), and replace_impl() only
forwarded its arguments to preg_replace2(). Call the destinations
directly.
|
|
The two functions ran the same search and differed only in how they
reported a non-participating group: one as ("", 0) in a bespoke struct,
the other as (null, -1) in a capture-key map. Neither shape covered both
callers, because PHP reaches preg_match_all() with different flags from
each: Preg::matchAllWithOffsets() always ORs in PREG_UNMATCHED_AS_NULL,
while OutputFormatter::formatAndWrap() passes PREG_OFFSET_CAPTURE alone.
Keep the capture-key map, which also exposes named groups, and take the
flags that decide between null and "" for an unmatched group. The offset
is -1 either way, so the ("", 0) approximation is gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
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>
|
|
The sole caller, Preg::match_all_with_offsets5(), always passed flags=0
and offset=0, so PREG_UNMATCHED_AS_NULL is now unconditional and the
subject is scanned from the beginning. That method is only reached from
Preg::match_all_with_offsets(), so it is no longer public either.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Every caller reached preg_match_all2() through Preg::match_all5(), which
always passed flags = PREG_UNMATCHED_AS_NULL and offset = 0. Inline those
constants and make match_all5() private, since the two public wrappers are
its only callers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Callers had to build a temporary Vec<&str> at every call site to satisfy
the &[&str] parameter. Taking IntoIterator and yielding the matched items
lets them pass owned or borrowed strings directly.
The flags variant and PREG_GREP_INVERT go away with it: no caller passes
flags, and Composer's Preg::grep() has no such parameter either.
|
|
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
|
|
preg_replace_callback2 had one caller, Preg::replace_callback6, which
was itself reached only from Preg::replace_callback with the default
limit, count and flags. Fold the two shim functions into one and drop
those parameters along with replace_callback6.
The surviving callback takes callback2's IndexMap of matches: it can be
keyed by group name, and it omits trailing non-participating groups the
way PHP does, which is what the callbacks in Process and ProgressBar
test for.
|
|
The stub token search compared every 18-byte window of the file case
insensitively. The token starts with a byte that ASCII case folding
leaves alone, so memchr can pick out the candidate offsets and leave
only those to the case-insensitive compare.
Over the 35 MB executable, finding the token in the embedded bundle
stub drops from 2.3 ms to 0.1 ms, and a scan that finds nothing drops
from 24 ms to 3.6 ms.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Running the ignored tests shows several reasons naming a blocker the
test never reaches. The plugin hooks in all_functional_test do run, and
what stops both cases is the worker's Composer\InstalledVersions;
class_loader_test stops at include_file, not class_exists;
auth_helper_test's wrapper is ported and the blocker is trigger_error;
composerRequire is ported as __shirabe_composer_require.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
plugin_installer_test.rs carried its own copy of php_runtime_available,
lock_php_worker and load_composer_php_runtime, and the other ten files
in the binary imported them from there. The bodies matched
tests/common/php_worker.rs, so include that instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
AutoloadGenerator.php builds the target-dir pattern by quoting the path
with a <dirsep> marker in place of the separators, then replacing the
quoted marker with [\\/]. The port kept that shape, but preg_quote()
here deliberately leaves < and > alone so the regex crate does not read
\< as a word boundary, so the replacement never matched and the pattern
came out as the literal {^Main<dirsep>Foo<dirsep>}.
A root package's target-dir was therefore never stripped from files,
classmap or exclude-from-classmap, and dumping one failed outright.
Split on the separators and quote each segment instead, so the marker
never passes through preg_quote() at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
This is the type Filesystem::directory_size() actually iterates, and it
swallowed a failed stat as 0 the same way. PHP's iterator yields
\SplFileInfo there, so raise the same \RuntimeException it would.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
\SplFileInfo::getSize() throws a \RuntimeException when stat fails, and
neither Symfony's subclass nor Composer's Filesystem::directorySize()
catches it. Returning 0 turned an unreadable file into an empty one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
These sites have no PHP counterpart to mirror, so they read std::env
directly. Going through the shim's getenv() keeps every environment
read in one place and lets a lint forbid the direct form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
CaBundle::getEnvVariable() prefers $_SERVER and falls back to getenv()
only under the CLI SAPI. The port read the live process environment for
both SSL_CERT_FILE and SSL_CERT_DIR, skipping the snapshot entirely.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
ProxyManager::initProxyData() reads $_SERVER[$name], a startup snapshot,
while the port read the live process environment. A putenv() issued
after startup changed the proxy the port picked but not the one PHP
would pick.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Factory::useXdg() enumerates array_keys($_SERVER). The port enumerated
the live process environment instead, so a COMPOSER_HOME resolved
against keys the PHP side never sees.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The signature took usize, so PHP's negative $start and $length, which
count from the end of the string, could not be expressed. Take i64 and
an optional length, and apply PHP's clamping rules.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
serialize(), var_export() and the string cast each hand-rolled
`format!("{}", f)`, which never emits PHP's exponential spelling and
carries the wrong precision for the string cast. PHP writes 1.0E+20
where Rust writes 1e20, and (string) 1/3 is 0.33333333333333, not the
shortest round-trip form.
Delegate all three to smart_str_append_double, which shirabe-php-src
already provides and shirabe-php-rpc's codec already used: precision -1
for serialize() and var_export(), 14 for the string cast.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Rust's shortest float formatting rounds a tie away from zero where
zend_dtoa mode 0 rounds to even, so digits taken straight from `{:e}`
diverged from PHP on values such as -166050639803968.125.
Take only the digit count from `{:e}` and re-format at that fixed
precision, which rounds the exact decimal value half to even. The same
routine supplies the fixed-precision mode `php_gcvt` previously refused
with an assert, so both modes now share one path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The class was shadowed by a guard, so a plugin doing `new Filesystem()`
got an explicit error. It is classified rust-proxy and plugin-constructible
and the Rust port is complete, so listing it as a stub target and answering
its public surface from the entity is all it takes.
The constructor rejects a caller-supplied ProcessExecutor: that class has no
proxy stub, so the argument could only be a second instance the Rust side
never sees. findShortestPath re-checks its arguments at the boundary because
the port panics where PHP throws, and a panic would take the process down
instead of reaching the plugin's catch block.
phpstan/extension-installer matches upstream Composer byte for byte again.
|
|
The worker is a long-lived child holding the environment it was handed
at spawn, so `@putenv`, the bin dir the event dispatcher prepends to
PATH, and COMPOSER_DEV_MODE never reached the PHP code running in it.
The shim now journals every write to the three storages PHP exposes, and
the outermost rpc_call replays the entries the worker has not seen yet
through __shirabe_sync_env. Replaying the writes rather than pushing a
whole snapshot keeps the worker's own $_SERVER entries intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The worker's autoloader fell through to the real Composer source for every
Rust-owned FQCN without a proxy stub, so plugin code doing `new Filesystem()`
or subclassing `LibraryInstaller` silently ran on a second instance the Rust
side never sees. An unimplemented part of the plugin API has to fail with an
explicit error naming it, not quietly work on a disconnected copy.
The stub generator now emits a guard class for each of those FQCNs: the real
declaration, hierarchy and constants, with every constructor and method
raising an explicit error. References satisfied by the declaration alone
(`instanceof`, `X::class`, `Link::TYPE_REQUIRE`) keep working. Two FQCNs stay
resolvable to the real class, each listed with the worker-side mechanism that
makes a natively constructed instance correct.
The error had nowhere to go: `Installer::run` dropped the `Result` of both
`dispatch_script` calls, so an exception from a listener ended in exit 0.
Both propagate now, the way the exception does upstream.
Three real-plugin E2E comparisons stop at a guard and are ignored, each
naming the class it needs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
json_decode built a serde_json::Value first and then converted it. Drive
serde_json's Deserializer with a DeserializeSeed instead, so the value is
built in one pass without the intermediate representation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The assoc flag was always a literal at every call site, so the boolean
carried no information the function name could not. json_decode_assoc
and json_decode_obj make the resulting PhpMixed shape visible at the
call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Config::get() resolves cache-files-maxsize by matching the stored value
against a size regex. The port read that value with as_string(), which
yields None for anything that is not a string, where PHP casts with
(string).
create-project feeds Config::all() back through Config::merge(), so the
second read finds the byte count the first read produced rather than the
original "300MiB". as_string() then yields an empty string, the regex
fails, and the resulting RuntimeException is swallowed by Config::get(),
which maps Err to null. FileDownloader turns that null into a zero max
size via .as_int().unwrap_or(0), and Cache::gc() deletes every file in
the download cache because the total always exceeds zero.
Apply the same cast on the path branch, the other site where Config.php
writes (string).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
fstat and lstat now return Option<FileStat> instead of a PhpMixed array, so
Platform::is_tty and Filesystem::is_junction read `mode` as a field. The
array carried each of the 13 values twice — once under its numeric index and
once under its name — which no caller relied on, and building it spelled the
field list out four times.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
posix_getpwuid now returns Option<PasswdEntry> instead of a PhpMixed array,
so Platform reads the field it wants rather than digging through the map.
posix_getuid and posix_geteuid return u32, matching the uid PasswdEntry is
looked up by.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
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>
|
|
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
SymfonyStyle modelled PHP's string|array message parameters, its array of
listing elements and its table headers/rows as PhpMixed, then normalised
and stringified them at every entry point. Take &[String] for the message
and listing parameters, Vec<Cell>/Vec<Row> for table (matching the
horizontal_table signature) and Vec<String> for the choice options, so the
is_array/is_iterable normalisation and the php_string helper both go away.
PhpMixed stays where PHP is genuinely mixed: the question answers returned
by ask/ask_hidden/choice, their validators, choice's string|int|null
default, and the progress_iterate elements.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
doWrite, doOverwrite and sanitize accept PHP's string|list<string>, which
this port modelled as PhpMixed. Every call site inside ConsoleIO passes a
single string, so take &str and return String instead, dropping the
(array) casts and the to_string_list helper.
Auditor is the only caller that passed a list: it builds table rows, whose
cells must stay separate, so it now sanitizes each cell. select() likewise
sanitizes each choice while projecting them into the keyed form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The listeners of five tests are methods of
`Composer\Test\EventDispatcher\EventDispatcherTest`, which the worker
could not load because the class extends `PHPUnit\Framework\TestCase` and
phpunit is not part of the Composer runtime bundle. An empty stand-in for
that base class is enough: a parent class only has to exist at declaration
time, and method bodies and type declarations resolve lazily. The upstream
file is then required into the worker unchanged, so the listener bodies and
their `__DIR__` stay what Composer ships. The two assertions those bodies
call are the only PHPUnit members the stand-in has to implement.
`remove_listener` now compares a handle for an object the worker really
holds instead of a hand-written one, and `create_composer_instance` wires
the collaborators the autoloader-rebuild path reaches for, as the PHP
helper does.
Two tests stay ignored for a different reason: `Platform::put_env` writes
only the Shirabe process environment, while the listeners read `getenv()`
inside the long-lived worker.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|