aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-15 07:37:45 +0900
committernsfisis <nsfisis@gmail.com>2026-08-15 07:37:45 +0900
commit548463bad1f72c97f68b47f54a263a4f4ad87b3a (patch)
tree1285d740611be3ad175f9e57c61d880b500dd818 /crates/shirabe-php-shim
parented8694f89eb7702eb7e617af29f8f444f32b8d3c (diff)
downloadphp-shirabe-548463bad1f72c97f68b47f54a263a4f4ad87b3a.tar.gz
php-shirabe-548463bad1f72c97f68b47f54a263a4f4ad87b3a.tar.zst
php-shirabe-548463bad1f72c97f68b47f54a263a4f4ad87b3a.zip
feat(php-rpc): embed the Composer PHP runtime in the executable
Plugins and scripts need the real `Composer\` classes and the packages Composer depends on, which so far came from a checkout found through SHIRABE_COMPOSER_PHP_DIR or a path next to the workspace. Neither exists for a distributed binary. The build script now archives those PHP sources into a phar the way Compiler.php does and the executable carries it. The worker maps it with Phar::loadPhar and reads a content-addressed sentinel back to tell a bundle it can use from one it cannot; where its PHP cannot open the phar, the bundle is unpacked once into the cache directory and autoloaded from there. SHIRABE_COMPOSER_PHP_DIR still overrides both for development. PHP locates a phar's manifest by the first __HALT_COMPILER(); token in the file, so the executable must hold no other copy of it: phar.rs builds the token at run time, and a linter keeps further literals out of the sources that reach the binary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim')
-rw-r--r--crates/shirabe-php-shim/src/phar.rs39
1 files changed, 36 insertions, 3 deletions
diff --git a/crates/shirabe-php-shim/src/phar.rs b/crates/shirabe-php-shim/src/phar.rs
index dccdfebf..ecd842f3 100644
--- a/crates/shirabe-php-shim/src/phar.rs
+++ b/crates/shirabe-php-shim/src/phar.rs
@@ -286,15 +286,35 @@ fn verify_phar_signature(path: &std::path::Path, bytes: &[u8]) -> anyhow::Result
Ok(())
}
+/// The special token to separate a phar stub and contents.
+///
+/// The Shirabe executable embeds the Composer runtime bundle as a phar archive, so the token must
+/// not appear in the binary. See `docs/dev/composer-runtime-bundle.md`. We use `black_box()` to
+/// prevent the Rust compiler from inlining the function to a constant.
+fn halt_compiler_token() -> [u8; 18] {
+ let mut token = *std::hint::black_box(b"__UNYG_PBZCVYRE();");
+ for byte in &mut token {
+ if byte.is_ascii_uppercase() {
+ *byte = b'A' + (*byte - b'A' + 13) % 26;
+ }
+ }
+ token
+}
+
fn parse_native_phar(path: &std::path::Path) -> anyhow::Result<Vec<PharEntry>> {
let bytes = std::fs::read(path)
.map_err(|e| corruption_error(path, &format!("unable to open phar: {}", e)))?;
- let halt = b"__HALT_COMPILER();";
+ let halt = halt_compiler_token();
let halt_pos = bytes
.windows(halt.len())
.position(|window| window == halt)
- .ok_or_else(|| corruption_error(path, "__HALT_COMPILER(); not found in stub"))?;
+ .ok_or_else(|| {
+ corruption_error(
+ path,
+ &format!("{} not found in stub", String::from_utf8_lossy(&halt)),
+ )
+ })?;
let mut offset = halt_pos + halt.len();
for close_tag in [&b" ?>"[..], &b"\n?>"[..]] {
if bytes[offset..].starts_with(close_tag) {
@@ -846,6 +866,17 @@ mod tests {
use super::*;
use crate::Catch as _;
+ #[test]
+ fn halt_compiler_token_decodes() {
+ assert_eq!(
+ halt_compiler_token(),
+ [
+ b'_', b'_', b'H', b'A', b'L', b'T', b'_', b'C', b'O', b'M', b'P', b'I', b'L', b'E',
+ b'R', b'(', b')', b';',
+ ],
+ );
+ }
+
fn write_file(dir: &std::path::Path, name: &str, content: &[u8]) -> std::path::PathBuf {
let path = dir.join(name);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
@@ -971,7 +1002,9 @@ mod tests {
manifest.extend_from_slice(&0u32.to_le_bytes());
}
- let mut bytes = b"<?php __HALT_COMPILER(); ?>\r\n".to_vec();
+ let mut bytes = b"<?php ".to_vec();
+ bytes.extend_from_slice(&halt_compiler_token());
+ bytes.extend_from_slice(b" ?>\r\n");
bytes.extend_from_slice(&(manifest.len() as u32).to_le_bytes());
bytes.extend_from_slice(&manifest);
bytes.extend_from_slice(&stored.0);