| Age | Commit message (Collapse) | Author |
|
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>
|
|
The reason string blamed shirabe_php_shim::runtime::constant(), which
PlatformRepository stopped reaching once its constant lookups went through
the RuntimeInterface seam. All four pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The link getters and setters and the release date accessors were explicit
errors for every non-empty value, because an immutable value has no entity
to point a handle at. They now cross as materialized values: the descriptor
names the real class and the constructor arguments, and each side builds a
genuine instance of its own.
The semver constraint a link holds is encoded structurally rather than
re-parsed from its string form, so the pretty strings and the conjunctive
flag survive the crossing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
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>
|
|
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>
|
|
|
|
The generated-file templates are heredocs in Composer's
AutoloadGenerator. Writing them as raw string literals keeps the emitted
PHP laid out the way it is written out, matching the blocks in
getAutoloadRealFile and getStaticFile that already use them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The proxy templates are heredocs in Composer's BinaryInstaller. Writing
them as raw string literals keeps the generated PHP and shell code laid
out the way it is emitted, instead of one escaped line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The docblock allows string|iterable<SplFileInfo>, but every call site in
Composer passes a string, so the iterable branch was only a todo!().
Taking &str drops it together with the InvalidArgumentException that the
type now rules out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PHP hands back a positional array, so every caller had to reach into a
PhpMixed list by index and fall back to a default when an element was
missing or the wrong variant -- fallbacks that could never fire, since
the list shape is fixed. LicenseMetadata names the four elements and
makes the accesses total.
This also drops the crate's last use of PhpMixed, and with it the
dependency on shirabe-php-shim.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
getLicenses(), getExceptionByIdentifier(), getIdentifierByName(),
isOsiApprovedByIdentifier(), isDeprecatedByIdentifier() and the
LICENSES_FILE / EXCEPTIONS_FILE constants had no callers.
Unused members are normally kept so the port stays a faithful mirror of
the PHP class, but SpdxLicenses is pure logic over the bundled SPDX data
that holds no Composer state, and plugins reach the real PHP
implementation rather than this port, so nothing can observe the
difference. The struct now records what is left out and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
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>
|
|
PHP mutates the caught exception's protected $code via ReflectionProperty.
The port takes ownership of the exception through anyhow's downcast, writes
ERROR_TRANSPORT_EXCEPTION into its public code field and re-wraps it, so no
reflection equivalent is needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Port the Symfony Filesystem branches this file had left out:
* copy() preserves the origin mtime via touch()
* exists() rejects paths longer than PHP_MAXPATHLEN - 2, so its return
type becomes anyhow::Result<bool>
* doRemove()'s symlink branch keeps the DIRECTORY_SEPARATOR disjunct,
which stops it from throwing on Unix where upstream never does
* symlink() normalizes separators and mirrors instead of linking when
copyOnWindows is set
* mirror() skips entries whose real path is the target directory or was
already created earlier in the same call
PHP_MAXPATHLEN is new in shirabe-php-shim.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
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>
|
|
A single install leaves the installer contract half tested: it never reaches
`update`, never runs over an already-installed tree, and never reaches the
plugin's own `uninstall()` override — the one chaining onto the promise
`LibraryInstaller::uninstall` returns. Nor does it touch the configuration
surface real projects use, `installer-paths` and `installer-name`, which the
plugin reads back through the package proxy.
All three now compare command output as well as the resulting tree against
upstream Composer. Progress bar frames are dropped from that comparison:
Shirabe renders them differently for every install, including projects with
no plugin at all, so they say nothing about the plugin under test.
With those covered the plugin joins the verified list in the README.
|
|
The fixture project pins composer/installers 2.3.0 and requires two packages
of framework-specific types, so the plugin's LibraryInstaller subclass decides
where they land. Upstream Composer and Shirabe install the same project and
the whole resulting tree is compared.
The plugin tarball is fetched by `fixtures/e2e-installers/fetch` into a
git-ignored directory and pinned by a digest over the extracted files, so the
third-party source never enters this repository and the test skips itself
while the directory is absent. The tree is staged and only moved into place
once verified, so an unverified tree is never observable under the name the
test looks for.
|
|
An installer that extends LibraryInstaller reaches for the Composer object
graph in ways the proxy did not answer: the download manager and the config,
the writable repository methods, a React promise as its own return value, and
`new Package(...)` from its supports() path.
* `Composer::getConfig`/`getDownloadManager` are dispatched, and both classes
become generated proxy stubs. Their reads and mutators answer from the Rust
entity; the surfaces needing stubs of their own (ConfigSourceInterface,
DownloaderInterface) stay explicit errors.
* The download manager's futures are driven to completion and handed back as
already-settled React promises, since PHP declares a non-nullable
PromiseInterface there. A promise a plugin returns is drained the same way:
settled yields its value, rejected re-raises, pending is an explicit error.
* Proxy stubs now carry the real class's constructor and ask the Rust side to
allocate the entity; reviving a stub for an existing entity binds the handle
without running it. Classes Rust cannot build name themselves in the error.
* The package proxy covers `Package`'s own setters, `CompletePackage`'s
metadata, `RootPackage`'s root-only state, and `BasePackage::$id`.
Link values and release dates still have no wire image, so the methods
carrying them remain explicit errors.
|
|
The fixture plugin registers an InstallerInterface implementation of its own
and installs a package of a custom type with it, recording every contract
call it receives. A fresh install produces a byte-identical project tree on
both implementations, trace included.
A second test pins the divergence a re-run and a `remove` expose: the two
implementations consult getInstaller at different points, so the trace
order — and its length once `remove` re-creates the Composer instance —
differs. It is written in full and marked ignored rather than trimmed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
A plugin can now hand an InstallerInterface implementation to
InstallationManager::addInstaller across the wire, and a legacy
composer-installer package is loaded as one; both are backed by a
PhpInstallerProxy forwarding the whole installer contract to the entity in
the PHP worker. An installer returning a real promise is an explicit error
until promises can cross the boundary.
InstallationManager takes installers as shared handles instead of boxes, so
the object identity removeInstaller and PluginManager's registeredPlugins
compare against survives registration, and holds them in a RefCell:
Installer::run keeps a shared borrow of the manager for the whole run, and a
plugin activated inside it registers its installer from there. The type
cache keys on the installer itself, like upstream, so re-entrant
registration cannot leave a stale index behind. InstallerInterface::supports
is fallible for the same reason getCapabilities and getCommands are: it
answers over RPC.
Cloning a proxy stub clones the Rust-side entity and rebinds the copy to the
fresh handle. Previously only the classes declaring __clone got a throwing
body, and the rest let two stubs share (and twice release) one handle.
Package entities answer with AnyPackage::dup, which already carries
BasePackage::__clone and the RootAliasPackage override; the others are an
explicit error.
The package proxy covers the whole PackageInterface surface; only the link
maps and the release date still lack a wire image for their value objects.
PluginManager gains a test-only seam for the reported Plugin API version,
and the three PluginInstallerTest cases that need it are ported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The pinned plugin and its real dependency tree (nine packages, fetched by
commit and verified by tree hash into the git-ignored ext/) install under
both implementations, and the list/help renderings of the plugin-provided
normalize command must match upstream byte for byte. The execution
comparison is written but ignored: NormalizeCommand builds a second,
in-process Composer instance, and the worker's proxy stubs reject native
construction of the classes that path instantiates. Stale comments about
the missing worker-side application are updated to the current facts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
A Shirabe-authored CommandProvider fixture plugin (greet) runs under both
implementations: exit codes, the command's own output, help/list rendering,
alias resolution, a validation failure, and a file recording the shared
object graph the command observed (root package, strict application FQCN,
and the exit code of the built-in about command it invoked) must match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
A same-FQCN Composer\Console\Application, hand-written under the new
php/runtime/ tree, hosts CommandProvider commands inside the PHP worker:
PhpCommandProxy overrides run() and forwards the stringified input, so the
real Symfony machinery binds, validates and executes against the live
command object, while help/list render Rust-side from a definition read
back at construction. Reverse \Shirabe\RustCommandStub rows let a plugin
command invoke built-in commands back in the Rust process, keeping every
command on the side whose helper set it was written for.
Composer\EventDispatcher\Event moves from a generated stub to a dual-mode
runtime class: the real BaseCommand::initialize constructs a
PreCommandRunEvent natively in the worker, which a proxy-only constructor
guard rejected. Its PRE_COMMAND_RUN dispatch reaches a new EventDispatcher
stub whose dispatch supports the observably-no-op no-listener case and
fails explicitly otherwise. The stub generator now accepts runtime-provided
classes as stub bases (never as targets) and cross-checks the Application
handoff property table against the real class.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Every Symfony input already ports __toString as a Display impl, but callers
holding a dyn InputInterface could not reach it; forwarding a command run
across the plugin RPC boundary needs the stringified input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Port Application::getPluginCommands: resolve the local composer with
plugins force-enabled, fall back to Factory::createGlobal, and collect
commands from CommandProvider capability adapters. PhpCommandProxy now
mirrors name/description/aliases/hidden over RPC so `list` output
matches upstream; input definitions remain TODO(plugin).
PhpClass::php_class_name returns an owned String because PHP-backed
proxies only know their class at runtime (the override-skip warning
prints get_class). CommandProvider::getCommands hands out shared
Rc<RefCell> handles since the commands are stored in the application.
Factory::createGlobal now propagates createConfig errors instead of
swallowing them, and Application::getComposer only catches the
exception classes upstream catches, so a ParsingException reaches
doRun's GithubActionError path as in Composer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
getPluginCapability was a no-op returning None. Now it runs the real
flow: class_exists in the worker, new $capabilityClass($ctorArgs) with
the plugin's own phandle spliced in as $ctorArgs['plugin'], both
instanceof checks answered by is_a in the child, and a per-interface
Rust adapter over the resulting entity (CommandProvider and the plain
Capability marker; anything else is an explicit error).
getPluginCapabilities now propagates errors instead of swallowing them.
Capable::get_capabilities widens from IndexMap<String, String> to
IndexMap<String, PhpMixed>: upstream casts the return with (array) and
validates only the queried key, so the narrow type both rejected maps
Composer accepts and made the invalidImplementationClassNames data
provider unrepresentable. The old code also returned ' 0 ' (trimmed to
the falsy '0') as a valid class name where upstream throws; the
rewritten validation follows upstream's empty/is_string/trim sequence.
CommandProvider::get_commands returns BaseCommand adapters whose name is
read back over RPC after the PHP constructor ran configure(); executing
one still needs the PHP-side Symfony Application and stays an explicit
error. The is_array / instanceof BaseCommand checks that upstream's
Application::getPluginCommands performs on the raw getCommands value
live in the adapter, because Vec<Box<dyn BaseCommand>> asserts every
element up front.
Ports testCommandProviderCapability (plugin-v8 end to end against the
real worker) and testQueryingWithInvalidCapabilityClassNameThrows (all
eight provider cases); the two tests that pass a PHPUnit mock plugin
into PHP stay ignored — a Rust-native mock has no PHP-side entity to
cross the boundary as $ctorArgs['plugin'].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
PhpMixed::Object hit a todo!(); the capability-name validation message in
PluginManager (var_export of an invalid getCapabilities value) reaches it
with a stdClass. Match the PHP 8.5.8 output byte for byte — the
"(object) array(...)" shape, properties indented one space deeper than
array elements, keys always quoted strings — verified against the real
PHP as the oracle and pinned by a unit test. Other classes would render
as \Class::__set_state(...), which stays out of reach because
PhpMixed::Object carries no class name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Run upstream Composer and Shirabe over pristine copies of a fixture
project at the same path and require composer.lock, the whole vendor tree
(including the plugin-generated GeneratedConfig.php), the plugin's IO
lines and the exit code to match byte for byte.
The plugin under test (phpstan/extension-installer 1.4.3) is downloaded by
fixtures/e2e/fetch — pinned to an upstream commit and hash-verified — into
a git-ignored directory rather than committed; the test skips while it is
absent, like the other real-PHP prerequisites. Its dependencies are
minimal stand-ins resolved from local repositories, so test runs stay
offline. A zip dist would exercise the known lossy-string byte-precision
debt in RemoteFilesystem, so the fixture serves the plugin through a path
dist until that is resolved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Widen the R table beyond Composer/IO with InstallationManager,
RepositoryManager, repository and package entities, interned by pointer
identity through a shared register_entity. Script events answer
getComposer/getIO, Composer hands out its graph getters, repositories list
their packages as per-variant stubs, package getters cover the
extension-installer surface (getRequires only while empty: the Link
snapshot encoding does not exist yet), and getInstallPath resolves its
package argument back through the R table. Everything else stays an
explicit error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Emit the eleven stubs a subscriber plugin's object-graph calls reach
(BasePackage through RootPackage, the installed-repository hierarchy,
RepositoryManager, InstallationManager). The generator learns the member
kinds these classes need: builtin interfaces contribute no closure entries,
public static properties are materialized, public instance properties
forward through __get/__set, __toString forwards like a plain method,
__clone throws (proxy clone semantics are still an open design question),
and a subclass stub may add interfaces to its inherited surface.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
The PHP worker is shared across the test binary, so whichever subscriber
test runs first owns the bare class name and every later install registers
the plugin under a _composer_tmpN rename (as upstream does). The exact-name
lookup made the removal test depend on lock acquisition order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
PHP's `(array)` cast passes an array through unchanged, but the port only
matched the map shape and wrapped a JSON list (e.g. ["MIT"]) as a single
license value, which then failed the is-string check and was silently
dropped from composer.lock / installed.json with a bogus warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Wire removePlugin to EventDispatcher::removeListener now that subscriber
listeners carry the plugin's P-table handle: PHP's $candidate[0] ===
$listener identity check maps to phandle equality on Callable::PhpMethod.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Wires the addPlugin subscriber branch end to end: EventSubscriberInterface
and Capable become fallible and dyn-compatible (their sole implementor is
the PHP plugin proxy, which answers getSubscribedEvents over RPC), listeners
register as Callable::PhpMethod and are invoked with a per-call event handle,
and the R table now drops entries when a child-side stub destructs. The R
table keeps its IndexMap with monotonically increasing handles, so released
handles are never reused and no generation counter is needed.
Upstream has no subscriber-plugin test, so the path is covered by a
Shirabe-owned fixture exercising all three getSubscribedEvents shapes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Replace the hand-written proxy stubs under crates/shirabe-php-rpc/php/stubs
with output of scripts/plugin-stub-generator, a deterministic emitter that
derives every stub from the Composer checkout and the classifier report.
Anything it cannot faithfully proxy (by-ref/variadic parameters, magic
methods, public properties, diverging omitted overrides, stale stub files,
a STUB_FILES entry missing in lib.rs) fails generation instead of degrading
silently, so future Composer releases surface new members as explicit
errors rather than silent gaps.
Regenerating the stubs also normalizes the hand-written inconsistencies
(uniform guarded constructors, import-based type spellings) and fixes real
gaps the review of the generated diff uncovered: the BaseIO authentication
methods now carry the real class's untyped signatures, and the previously
missing ConsoleIO::sanitize is materialized together with its private
static helper. A cargo test runs generate-stubs --check to keep the
committed stubs, the generator and the embedded list from drifting apart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
InstallationManager::execute now takes &self (the mock recorder moved
into a RefCell) and its callers hold only shared borrows: plugin
registration inside a batch re-enters the same manager handle through
Composer::getInstallationManager()->getInstallPath(), which panicked
on the RefCell re-borrow under the &mut shape — the same re-entrancy
the repository side already fixed, unreachable from the ported tests
because they call PluginInstaller::install directly like PHPUnit does.
The worker-side InstalledVersions mirror now matches the full tail of
FilesystemRepository::write: unconditional reload plus the
reflection-based selfDir/installedIsLocalDir restore. The previous
class_exists(false) guard rested on a lazy-load assumption that does
not hold in the worker (its real ClassLoader only knows the Composer
checkout's vendor dir, so a later lazy load would read the checkout's
installed.php, not the project's); the mirror is now skipped only when
the class is not autoloadable at all, i.e. no plugin runtime and hence
no observer code. Boot-time seeding stays TODO(plugin).
Also from the review: registered_plugins entries are removed only
after the deactivate/uninstall loop (PHP unsets last, and a throw must
leave the entry observable); extra.class keeps associative-array
values and fails loudly on non-strings instead of silently dropping
them; the two discarded write() results now propagate (they carry the
reload-push failure); register_package's allow-plugins skip message is
DEBUG like the addPlugin side; the loader-eviction divergence of
REGISTERED_LOADERS and the lossy UTF-8 spots carry searchable
markers; the test-only proxy downcast follows the __ naming rule; the
R-table dispatch clones the entity out instead of holding the table
borrow across the handler; the IO/PartialComposer stubs turn a
plugin-side `new NullIO()` into an explicit error instead of an
ArgumentCountError; and the empty() emulation covers float 0.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Implement the remainder of PluginManager::registerPackage: the plugin
autoload map is built by the ported createLoader/parseAutoloads and
served to the worker over the existing reverse-RPC autoloader, files
entries go through a composerRequire-equivalent glue call, and
already-defined classes take the upstream _composer_tmp rename/eval
path. Instantiation uses the new NewObject/CallPhpMethod lanes backed
by a P table in the worker; PhpPluginProxy adapts the resulting handle
to PluginInterface, with $composer/$io exposed to plugin callbacks via
an R table (unsupported methods stay explicit errors). Hand-written
proxy stubs cover Composer, PartialComposer and the IO hierarchy, and
the stub autoloader is re-prepended after loading the Composer PHP
runtime so its vendor autoloader cannot shadow proxied FQCNs.
FilesystemRepository::write now mirrors InstalledVersions::reload into
a running worker (class_exists-guarded, so an unloaded class keeps its
upstream lazy-load behavior), removing the previously undefined
observation window.
The installer pipeline passes the installed repository as a shared
handle instead of a long-lived `&mut dyn`: plugin registration runs
inside InstallationManager::execute and re-enters the same local
repository through the RepositoryManager, which would panic on the
RefCell re-borrow under the old shape.
PluginInterface lifecycle methods now take an owned ComposerHandle
(plugins retain $composer past the call) and return anyhow::Result
(PHP plugin code may throw); the plugin list uses shared ownership so
the identity comparison of removePlugin survives the dual storage in
registeredPlugins, matching PHP reference semantics.
Ports the activate/upgrade/uninstall tests of PluginInstallerTest,
serialized across the shared worker process whose persistent class
table is exactly what exercises the rename path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Implement the two script execution paths that previously stopped at
todo!(): a Class::method listener is invoked as CallStaticMethod with the
event crossing the boundary as a proxy-stub handle, and a Command-class
listener runs inside a throwaway bare Symfony Application hosted by the
worker via a generated snippet, its BufferedOutput written back through
the dispatcher's IO. makeAutoloader is ported for real (canonical-package
hash, setDevMode, buildPackageMap/parseAutoloads/createLoader), and the
class_exists/is_callable/is_a/defined guards now query the worker, whose
script autoloader resolves classes by asking the Rust-side ClassLoader
over the reverse channel. EventInterface gains as_any (the IOInterface
downcast pattern) so the concrete event type is reachable behind the
trait object. Application::do_run now registers ScriptAliasCommand
entries as typed commands, unblocking the run-script --list/alias tests;
the dev-mode-to-generator test is ported with local mockall mocks. The
remaining ignored tests carry re-verified reasons: the listener methods
live on the PHPUnit test class itself (unloadable in the worker), or the
test needs live import of a user PHP Command class into the Application.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
test_preserve_indentation_after_read and test_overwrites_indentation_by_default
copy to and delete the same fixture path; PHPUnit runs them serially, the
parallel Rust harness let them race and flake.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Replace the name\0arg framing with the plugin wire protocol: tagged
frames with corr_id multiplexing, a MAX_FRAME_LEN bound, a thread-ID
based reentrant SessionLock, and the PluginValue codec (encoder plus
the first recursive decoder, iterative with a 512-level depth cap).
Float formatting is ported from php-src into shirabe-php-src so the
encoder is byte-compatible with serialize() under serialize_precision=-1,
which the spawned worker now pins. The worker gains a standing dispatch
loop, CallRustMethod reentrancy, hand-written Event proxy stubs, and
explicit-error answers for everything not implemented yet. The public
query API (get_php_version and friends) is unchanged and now rides the
new protocol; the codec is verified against real PHP by roundtrip
oracle tests covering floats, non-UTF-8 bytes and deep nesting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
The Package family settles on pure rust-proxy: it is genuinely mutable
(60+ setters, and Composer itself mutates packages in flight), so the
planned snapshot-with-writeback treatment is dropped rather than filed
as an override. The other open questions remain open with their interim
behavior pinned instead of resolved: proxy-side getLoop() access and the
ConsoleIO table/progress-bar members are explicit errors, and the
InstalledVersions state a plugin observes after a Rust-side dump is
undefined, marked TODO(plugin) at the reload site.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Three fixes for the ComposerRepository/FilesystemRepository/PlatformRepository
hazards where inner-composition delegation skipped PHP's late-bound virtual
dispatch:
- ComposerRepository::has_package now builds its packageMap through the
late-bound getPackages() equivalent, so lazy-providers repos surface the
LogicException and available-packages repos load their package list, as in
PHP, instead of silently answering false from the raw array.
- RepositoryInterface::get_repo_name returns anyhow::Result<String>: PHP's
getRepoName() counts through the late-bound initialize(), which is fallible
in file-reading subclasses. FilesystemRepository and PackageRepository now
run that initialization instead of freezing the inner array repository to an
empty state (which also made a later write() truncate installed.json).
Supporting changes keep the initialization chain callable from &self:
JsonFile::read takes &self (indent moved into a RefCell), FilesystemRepository
dev_mode became a Cell, and WritableArrayRepository dev_package_names a
RefCell.
- PlatformRepository::new routes constructor packages through its own
add_package so the override handling and full platform initialization run
as they do via PHP's parent constructor; the inner find_package/add_package
delegations inside add_package (and ComposerRepository::add_package) gained
the same is_initialized guard, since the constructor path would otherwise
freeze the repository.
Same defect class as 7db937af, 97b5211a and 3e367f78.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
fallthroughs
findPackage()/findPackages()/search() delegate their non-lazy, non-provider
fallthrough to the inner ArrayRepository, whose self-initialization froze the
packages array before ComposerRepository::initialize could read the root file,
so a plain v1-style repo (inline "packages" in packages.json) always answered
empty. Guard the delegations with the same is_initialized() check used by
count()/hasPackage().
getProviders() had the inverse defect: PHP reads the raw $this->packages
property, but the port went through count(), whose initialize poisoned the
initialization flag so the root file would never load afterwards. Check the
raw field for non-empty instead, matching PHP's truthiness test.
Same defect class as 97b5211a and the 3e367f78 downloader fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
add/remove
ArrayRepository::addPackage()/removePackage() rely on PHP late binding to run
FilesystemRepository::initialize (reading the file) on first touch. The inner
delegation skipped that: an add on a not-yet-read repository froze the array
to just the added packages (a later write() would truncate installed.json),
and a remove hit the packages-initialized expect(). Guard both with
ensure_initialized(), matching the pattern used by the read paths.
Same defect class as the 3e367f78 downloader fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Add NO_COLOR=1, the COMPOSER/COMPOSER_VENDOR_DIR/COMPOSER_BIN_DIR clears and
the timezone normalization, and make bootstrap() Once-guarded so additional
call sites stay safe. Wiring it into every test binary still needs a libtest
setup hook (ctor crate or fixture-level calls), recorded in the TODO.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
mb_check_encoding
http_get_last_response_headers()/http_clear_last_response_headers() now back
a thread-local store with a recording hook for the (still unported) HTTP
stream layer; with no request recorded they return None, matching PHP.
mb_check_encoding gains the trivial ASCII arm; other encodings still need
the mbstring tables and stay todo!().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
PHP's JsonFile::encode throws a RuntimeException when json_encode fails; the
port swallowed that into an .unwrap() marked TODO(phase-c). Return
anyhow::Result from encode/encode_with_options and propagate at every call
site (print_table and list_repositories become Result-returning to carry it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
PHP's getStyle returns the shared style instance (reference semantics), which
the previous Box-returning signature could not express, leaving a todo!().
Store each style behind Rc<RefCell<...>> and hand out handle clones, per the
shared-ownership policy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|