diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-25 03:20:08 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-25 03:33:09 +0900 |
| commit | c65cab710bb3d79db20862c9361d4dbbac8ac5d7 (patch) | |
| tree | b4d192237bbb8dbaa80166f1ffa24703e57243f4 | |
| parent | f73e9ab05f8d94937d2437919199d9d9681c8cde (diff) | |
| download | php-shirabe-c65cab710bb3d79db20862c9361d4dbbac8ac5d7.tar.gz php-shirabe-c65cab710bb3d79db20862c9361d4dbbac8ac5d7.tar.zst php-shirabe-c65cab710bb3d79db20862c9361d4dbbac8ac5d7.zip | |
feat(php-shim): model $_ENV/$_SERVER as OsString snapshots
Rework the environment shim around getenv/putenv on the real environment
and $_ENV/$_SERVER as startup snapshots, all over OsString. Migrate every
caller off the old server()/server_argv() helpers and force the snapshots
in main() before any putenv() runs. Document the porting rules in
docs/dev/env-vars-porting.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
20 files changed, 327 insertions, 151 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/color.rs b/crates/shirabe-external-packages/src/symfony/console/color.rs index 3d5a7c4..fb7cc9f 100644 --- a/crates/shirabe-external-packages/src/symfony/console/color.rs +++ b/crates/shirabe-external-packages/src/symfony/console/color.rs @@ -196,7 +196,9 @@ impl Color { let b = color & 255; // see https://github.com/termstandard/colors/ for more information about true color support - if shirabe_php_shim::getenv("COLORTERM").as_deref() != Some("truecolor") { + if shirabe_php_shim::getenv("COLORTERM").as_deref() + != Some(std::ffi::OsStr::new("truecolor")) + { return Self::degrade_hex_color_to_ansi(r, g, b).to_string(); } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/command.rs b/crates/shirabe-external-packages/src/symfony/console/command/command.rs index 840f11a..89f4ae2 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/command.rs @@ -731,7 +731,13 @@ impl Command for CommandData { "%command.name%".to_string(), "%command.full_name%".to_string(), ]; - let php_self = shirabe_php_shim::server("PHP_SELF"); + let php_self = shirabe_php_shim::PHP_SERVER + .lock() + .unwrap() + .php_self() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); let replacements = [ name.clone().unwrap_or_default(), if is_single_command { diff --git a/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs index cc700dc..da59a75 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs @@ -125,7 +125,15 @@ impl CompleteCommand { return; } - let command_name = shirabe_php_shim::basename(&shirabe_php_shim::server_argv()[0]); + let command_name = shirabe_php_shim::basename( + &shirabe_php_shim::PHP_SERVER + .lock() + .unwrap() + .argv() + .next() + .unwrap_or_default() + .to_string_lossy(), + ); shirabe_php_shim::file_put_contents3( &format!( "{}/sf_{}.log", @@ -206,7 +214,9 @@ impl Command for CompleteCommand { ) -> anyhow::Result<()> { let _ = (input, output); self.is_debug.set(shirabe_php_shim::filter_var_boolean( - &shirabe_php_shim::getenv("SYMFONY_COMPLETION_DEBUG").unwrap_or_default(), + &shirabe_php_shim::getenv("SYMFONY_COMPLETION_DEBUG") + .unwrap_or_default() + .to_string_lossy(), )); Ok(()) @@ -268,7 +278,16 @@ impl Command for CompleteCommand { "<info>Input:</> <comment>(\"|\" indicates the cursor position)</>".to_string(), format!(" {}", completion_input.to_string()), "<info>Command:</>".to_string(), - format!(" {}", shirabe_php_shim::server_argv().join(" ")), + format!( + " {}", + shirabe_php_shim::PHP_SERVER + .lock() + .unwrap() + .argv() + .map(|a| a.to_string_lossy().into_owned()) + .collect::<Vec<_>>() + .join(" ") + ), "<info>Messages:</>".to_string(), ]); diff --git a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs index 306bd59..588b868 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs @@ -73,7 +73,14 @@ impl DumpCompletionCommand { } fn guess_shell() -> String { - shirabe_php_shim::basename(&shirabe_php_shim::server_shell().unwrap_or_default()) + shirabe_php_shim::basename( + &shirabe_php_shim::PHP_SERVER + .lock() + .unwrap() + .get("SHELL") + .unwrap_or_default() + .to_string_lossy(), + ) } fn tail_debug_log(&self, command_name: &str, _output: &dyn OutputInterface) { @@ -112,7 +119,13 @@ impl DumpCompletionCommand { impl Command for DumpCompletionCommand { fn configure(&self) -> anyhow::Result<()> { - let full_command = shirabe_php_shim::server_php_self(); + let full_command = shirabe_php_shim::PHP_SERVER + .lock() + .unwrap() + .php_self() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); let command_name = shirabe_php_shim::basename(&full_command); // @realpath($fullCommand) ?: $fullCommand let full_command = match shirabe_php_shim::realpath(&full_command) { @@ -170,7 +183,15 @@ impl Command for DumpCompletionCommand { input: Rc<RefCell<dyn InputInterface>>, output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { - let command_name = shirabe_php_shim::basename(&shirabe_php_shim::server_argv()[0]); + let command_name = shirabe_php_shim::basename( + &shirabe_php_shim::PHP_SERVER + .lock() + .unwrap() + .argv() + .next() + .unwrap_or_default() + .to_string_lossy(), + ); if input.borrow().get_option("debug")?.to_bool() { self.tail_debug_log(&command_name, &*output.borrow()); diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs index 39d694d..c421edf 100644 --- a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs @@ -77,13 +77,17 @@ impl OutputFormatterStyleInterface for OutputFormatterStyle { if self.handles_href_gracefully.is_none() { self.handles_href_gracefully = Some( shirabe_php_shim::getenv("TERMINAL_EMULATOR").as_deref() - != Some("JetBrains-JediTerm") + != Some(std::ffi::OsStr::new("JetBrains-JediTerm")) && (shirabe_php_shim::getenv("KONSOLE_VERSION").is_none_or(|v| v.is_empty()) || shirabe_php_shim::getenv("KONSOLE_VERSION") - .map(|v| v.parse::<i64>().unwrap_or(0)) + .map(|v| v.to_string_lossy().parse::<i64>().unwrap_or(0)) .unwrap_or(0) > 201100) - && !shirabe_php_shim::server_contains_key("IDEA_INITIAL_DIRECTORY"), + && shirabe_php_shim::PHP_SERVER + .lock() + .unwrap() + .get("IDEA_INITIAL_DIRECTORY") + .is_none(), ); } diff --git a/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs index dd6840d..254ec2c 100644 --- a/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs +++ b/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs @@ -103,7 +103,10 @@ impl ConsoleOutput { } else { String::new() }, - shirabe_php_shim::getenv("OSTYPE").unwrap_or_default(), + shirabe_php_shim::getenv("OSTYPE") + .unwrap_or_default() + .to_string_lossy() + .into_owned(), shirabe_php_shim::PHP_OS.to_string(), ]; diff --git a/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs index f517cbe..ac85f3a 100644 --- a/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs +++ b/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs @@ -91,7 +91,9 @@ impl StreamOutput { if !shirabe_php_shim::stream_isatty_resource(stream) && !["MINGW32", "MINGW64"].contains( &shirabe_php_shim::strtoupper( - &shirabe_php_shim::getenv("MSYSTEM").unwrap_or_default(), + &shirabe_php_shim::getenv("MSYSTEM") + .unwrap_or_default() + .to_string_lossy(), ) .as_str(), ) @@ -105,15 +107,19 @@ impl StreamOutput { return true; } - if Some("Hyper".to_string()) == shirabe_php_shim::getenv("TERM_PROGRAM") + if shirabe_php_shim::getenv("TERM_PROGRAM").as_deref() + == Some(std::ffi::OsStr::new("Hyper")) || shirabe_php_shim::getenv("COLORTERM").is_some() || shirabe_php_shim::getenv("ANSICON").is_some() - || Some("ON".to_string()) == shirabe_php_shim::getenv("ConEmuANSI") + || shirabe_php_shim::getenv("ConEmuANSI").as_deref() == Some(std::ffi::OsStr::new("ON")) { return true; } - let term = shirabe_php_shim::getenv("TERM").unwrap_or_default(); + let term = shirabe_php_shim::getenv("TERM") + .unwrap_or_default() + .to_string_lossy() + .into_owned(); if "dumb" == term { return false; } @@ -130,7 +136,10 @@ impl StreamOutput { /// PHP: `(($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')`. fn no_color_first_char() -> String { - let value = shirabe_php_shim::getenv("NO_COLOR").unwrap_or_default(); + let value = shirabe_php_shim::getenv("NO_COLOR") + .unwrap_or_default() + .to_string_lossy() + .into_owned(); value .chars() .next() diff --git a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs index 92e80b2..d7b2ef6 100644 --- a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs +++ b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs @@ -277,7 +277,8 @@ impl SymfonyStyle { let mut progress_bar = self.inner.create_progress_bar(max); if std::path::MAIN_SEPARATOR != '\\' - || shirabe_php_shim::getenv("TERM_PROGRAM").as_deref() == Some("Hyper") + || shirabe_php_shim::getenv("TERM_PROGRAM").as_deref() + == Some(std::ffi::OsStr::new("Hyper")) { progress_bar.set_empty_bar_character("░"); // light shade character ░ progress_bar.set_progress_character(""); diff --git a/crates/shirabe-external-packages/src/symfony/console/terminal.rs b/crates/shirabe-external-packages/src/symfony/console/terminal.rs index c02b87f..cc8f812 100644 --- a/crates/shirabe-external-packages/src/symfony/console/terminal.rs +++ b/crates/shirabe-external-packages/src/symfony/console/terminal.rs @@ -28,7 +28,8 @@ impl Terminal { let width = shirabe_php_shim::getenv("COLUMNS"); if let Some(width) = width { return shirabe_php_shim::intval(&PhpMixed::String(shirabe_php_shim::trim( - &width, None, + &width.to_string_lossy(), + None, ))); } @@ -44,7 +45,8 @@ impl Terminal { let height = shirabe_php_shim::getenv("LINES"); if let Some(height) = height { return shirabe_php_shim::intval(&PhpMixed::String(shirabe_php_shim::trim( - &height, None, + &height.to_string_lossy(), + None, ))); } @@ -85,7 +87,7 @@ impl Terminal { if let Some(ansicon) = &ansicon && shirabe_php_shim::preg_match( "/^(\\d+)x(\\d+)(?: \\((\\d+)x(\\d+)\\))?$/", - &shirabe_php_shim::trim(ansicon, None), + &shirabe_php_shim::trim(&ansicon.to_string_lossy(), None), &mut matches, ) { diff --git a/crates/shirabe-external-packages/src/symfony/process/process.rs b/crates/shirabe-external-packages/src/symfony/process/process.rs index caf8aa1..3550f09 100644 --- a/crates/shirabe-external-packages/src/symfony/process/process.rs +++ b/crates/shirabe-external-packages/src/symfony/process/process.rs @@ -1670,13 +1670,20 @@ impl Process { } fn get_default_env(&self) -> IndexMap<String, PhpMixed> { - let env = php::getenv_all(); - let server = php::php_server(); + let env: IndexMap<String, String> = php::getenv_all() + .map(|(k, v)| { + ( + k.to_string_lossy().into_owned(), + v.to_string_lossy().into_owned(), + ) + }) + .collect(); + let server = php::PHP_SERVER.lock().unwrap(); // non-Windows: array_intersect_key($env, $_SERVER) ?: $env let mut intersect: IndexMap<String, PhpMixed> = IndexMap::new(); for (k, v) in &env { - if server.contains_key(k) { + if server.get(k).is_some() { intersect.insert(k.clone(), PhpMixed::String(v.clone())); } } @@ -1689,7 +1696,17 @@ impl Process { }; // $_ENV + env_map - let mut result = php::php_env(); + let mut result: IndexMap<String, PhpMixed> = php::PHP_ENV + .lock() + .unwrap() + .get_all() + .map(|(k, v)| { + ( + k.to_string_lossy().into_owned(), + PhpMixed::String(v.to_string_lossy().into_owned()), + ) + }) + .collect(); for (k, v) in env_map { result.entry(k).or_insert(v); } diff --git a/crates/shirabe-php-shim/src/env.rs b/crates/shirabe-php-shim/src/env.rs index 62dc666..cf0c659 100644 --- a/crates/shirabe-php-shim/src/env.rs +++ b/crates/shirabe-php-shim/src/env.rs @@ -1,100 +1,90 @@ -use crate::PhpMixed; -use indexmap::IndexMap; +//! Porting rules for PHP environment variables: see `docs/dev/env-vars-porting.md`. -pub fn getenv(_name: &str) -> Option<String> { - std::env::var(_name).ok() +pub fn getenv_all() -> std::env::VarsOs { + std::env::vars_os() } -// TODO(phase-c): only the simple `^(\w+)(=(.+))?$` form is supported. -pub fn putenv(setting: &str) -> bool { - let is_word = - |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_'); - // A setting without `=` deletes the variable, mirroring PHP's putenv('NAME'). - match setting.split_once('=') { - Some((name, value)) => { - if !is_word(name) { - panic!("putenv: unsupported setting format: {setting:?}"); - } - unsafe { - std::env::set_var(name, value); - } - } - None => { - if !is_word(setting) { - panic!("putenv: unsupported setting format: {setting:?}"); - } - unsafe { - std::env::remove_var(setting); - } - } - } - true +pub fn getenv<K: AsRef<std::ffi::OsStr>>(key: K) -> Option<std::ffi::OsString> { + std::env::var_os(key) } -/// PHP superglobal $_SERVER access. In the CLI SAPI $_SERVER is populated from -/// the environment, which is the only source modeled here. -pub fn server_get(name: &str) -> Option<String> { - // TODO: is var_os() better? - std::env::var(name).ok() +pub unsafe fn putenv<K: AsRef<std::ffi::OsStr>, V: AsRef<std::ffi::OsStr>>(key: K, value: V) { + // TODO: validate key and value format to avoid panic? + unsafe { std::env::set_var(key, value) } } -// TODO(php-runtime): modify the real PHP's $_SERVER. -pub fn server_set(_name: &str, _value: String) {} - -// TODO(php-runtime): modify the real PHP's $_SERVER. -pub fn server_unset(_name: &str) {} - -pub fn server_contains_key(name: &str) -> bool { - std::env::var_os(name).is_some() +pub unsafe fn putenv_clear<K: AsRef<std::ffi::OsStr>>(key: K) { + // TODO: validate key and value format to avoid panic? + unsafe { std::env::remove_var(key) } } -/// PHP superglobal $_ENV access. -pub fn env_get(name: &str) -> Option<String> { - // TODO: is var_os() better? - std::env::var(name).ok() +pub struct Superglobal { + vars: indexmap::IndexMap<std::ffi::OsString, std::ffi::OsString>, } -// TODO(php-runtime): modify the real PHP's $_ENV. -pub fn env_set(_name: &str, _value: String) {} +pub struct SuperglobalServer(Superglobal); -// TODO(php-runtime): modify the real PHP's $_ENV. -pub fn env_unset(_name: &str) {} +impl Superglobal { + fn from_env_vars() -> Self { + let vars = std::env::vars_os().collect(); + Self { vars } + } -pub fn env_contains_key(name: &str) -> bool { - std::env::var_os(name).is_some() -} + pub fn get_all(&self) -> impl Iterator<Item = (std::ffi::OsString, std::ffi::OsString)> + '_ { + self.vars.iter().map(|(k, v)| (k.clone(), v.clone())) + } -/// PHP `getenv()` with no argument: all environment variables. -pub fn getenv_all() -> IndexMap<String, String> { - todo!() -} + pub fn get<K: AsRef<std::ffi::OsStr>>(&self, key: K) -> Option<std::ffi::OsString> { + self.vars.get(key.as_ref()).cloned() + } -/// PHP superglobal `$_ENV`. -pub fn php_env() -> IndexMap<String, PhpMixed> { - todo!() -} + pub fn put(&mut self, key: std::ffi::OsString, value: std::ffi::OsString) { + self.vars.insert(key, value); + } -/// PHP superglobal `$_SERVER`. -pub fn php_server() -> IndexMap<String, PhpMixed> { - todo!() + pub fn clear<K: AsRef<std::ffi::OsStr>>(&mut self, key: K) { + self.vars.shift_remove(key.as_ref()); + } } -pub fn server(key: &str) -> String { - if key == "PHP_SELF" { - return server_php_self(); +impl SuperglobalServer { + fn from_env_vars() -> Self { + Self(Superglobal::from_env_vars()) } - server_get(key).unwrap_or_default() -} -pub fn server_argv() -> Vec<String> { - std::env::args().collect() -} + pub fn get_all(&self) -> impl Iterator<Item = (std::ffi::OsString, std::ffi::OsString)> + '_ { + self.0.get_all() + } -pub fn server_php_self() -> String { - // CLI SAPI: $_SERVER['PHP_SELF'] is the path of the executed script, i.e. argv[0]. - std::env::args().next().unwrap_or_default() -} + pub fn get<K: AsRef<std::ffi::OsStr>>(&self, key: K) -> Option<std::ffi::OsString> { + self.0.get(key) + } + + pub fn put(&mut self, key: std::ffi::OsString, value: std::ffi::OsString) { + self.0.put(key, value) + } + + pub fn clear<K: AsRef<std::ffi::OsStr>>(&mut self, key: K) { + self.0.clear(key) + } + + pub fn argv(&self) -> std::env::ArgsOs { + std::env::args_os() + } -pub fn server_shell() -> Option<String> { - todo!() + pub fn php_self(&self) -> Option<std::ffi::OsString> { + self.argv().next() + } } + +/// PHP superglobal $_SERVER. $_SERVER is a snapshot at startup. Modifying it does not affect the +/// real environment variables, while putenv() does. +/// TODO(php-runtime): modify the real PHP's $_SERVER. +pub static PHP_SERVER: std::sync::LazyLock<std::sync::Mutex<SuperglobalServer>> = + std::sync::LazyLock::new(|| std::sync::Mutex::new(SuperglobalServer::from_env_vars())); + +/// PHP superglobal $_ENV. $_ENV is a snapshot at startup. Modifying it does not affect the real +/// environment variables, while putenv() does. +/// TODO(php-runtime): modify the real PHP's $_ENV. +pub static PHP_ENV: std::sync::LazyLock<std::sync::Mutex<Superglobal>> = + std::sync::LazyLock::new(|| std::sync::Mutex::new(Superglobal::from_env_vars())); diff --git a/crates/shirabe/src/autoload/autoload_generator.rs b/crates/shirabe/src/autoload/autoload_generator.rs index 7b02841..ded7cee 100644 --- a/crates/shirabe/src/autoload/autoload_generator.rs +++ b/crates/shirabe/src/autoload/autoload_generator.rs @@ -148,7 +148,12 @@ impl AutoloadGenerator { if self.run_scripts { // set COMPOSER_DEV_MODE in case not set yet so it is available in the dump-autoload event listeners - if shirabe_php_shim::server_get("COMPOSER_DEV_MODE").is_none() { + if shirabe_php_shim::PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEV_MODE") + .is_none() + { Platform::put_env( "COMPOSER_DEV_MODE", if self.dev_mode.unwrap_or(false) { diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs index 9683b89..557ebca 100644 --- a/crates/shirabe/src/command/init_command.rs +++ b/crates/shirabe/src/command/init_command.rs @@ -10,11 +10,11 @@ use shirabe_external_packages::symfony::console::input::ArrayInput; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_php_shim::{ - FILE_IGNORE_NEW_LINES, InvalidArgumentException, PHP_EOL, PhpMixed, array_filter, array_flip, - array_flip_strings, array_intersect_key, array_keys, array_map, basename, empty, explode, file, - file_exists, file_get_contents, file_put_contents, function_exists, get_current_user, implode, - is_dir, is_string, preg_quote, realpath, server_get, sprintf, str_replace, strpos, strtolower, - trim, ucwords, + FILE_IGNORE_NEW_LINES, InvalidArgumentException, PHP_EOL, PHP_SERVER, PhpMixed, array_filter, + array_flip, array_flip_strings, array_intersect_key, array_keys, array_map, basename, empty, + explode, file, file_exists, file_get_contents, file_put_contents, function_exists, + get_current_user, implode, is_dir, is_string, preg_quote, realpath, sprintf, str_replace, + strpos, strtolower, trim, ucwords, }; use shirabe_spdx_licenses::SpdxLicenses; use std::cell::RefCell; @@ -723,7 +723,11 @@ impl Command for InitCommand { .as_string() .map(|s| s.to_string()); if license.is_none() { - let default_license = server_get("COMPOSER_DEFAULT_LICENSE"); + let default_license = PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEFAULT_LICENSE") + .map(|value| value.to_string_lossy().into_owned()); if !empty( &default_license .clone() @@ -1163,7 +1167,21 @@ impl InitCommand { let name = self.sanitize_package_name_component(&name); let mut vendor = name.clone(); - let composer_default_vendor = server_get("COMPOSER_DEFAULT_VENDOR"); + let composer_default_vendor = PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEFAULT_VENDOR") + .map(|value| value.to_string_lossy().into_owned()); + let server_username = PHP_SERVER + .lock() + .unwrap() + .get("USERNAME") + .map(|value| value.to_string_lossy().into_owned()); + let server_user = PHP_SERVER + .lock() + .unwrap() + .get("USER") + .map(|value| value.to_string_lossy().into_owned()); if !empty( &composer_default_vendor .clone() @@ -1174,19 +1192,19 @@ impl InitCommand { } else if git.contains_key("github.user") { vendor = git.get("github.user").cloned().unwrap_or_default(); } else if !empty( - &server_get("USERNAME") + &server_username .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), ) { - vendor = server_get("USERNAME").unwrap_or_default(); + vendor = server_username.unwrap_or_default(); } else if !empty( - &server_get("USER") + &server_user .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), ) { - vendor = server_get("USER").unwrap_or_default(); + vendor = server_user.unwrap_or_default(); } else if !get_current_user().is_empty() { vendor = get_current_user(); } @@ -1200,7 +1218,11 @@ impl InitCommand { let git = self.get_git_config(); let mut author_name: Option<String> = None; - let composer_default_author = server_get("COMPOSER_DEFAULT_AUTHOR"); + let composer_default_author = PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEFAULT_AUTHOR") + .map(|value| value.to_string_lossy().into_owned()); if !empty( &composer_default_author .clone() @@ -1213,7 +1235,11 @@ impl InitCommand { } let mut author_email: Option<String> = None; - let composer_default_email = server_get("COMPOSER_DEFAULT_EMAIL"); + let composer_default_email = PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEFAULT_EMAIL") + .map(|value| value.to_string_lossy().into_owned()); if !empty( &composer_default_email .clone() diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index 251fc67..319e8dd 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -1402,7 +1402,10 @@ impl Application { input.borrow_mut().set_interactive(false); } - let mut shell_verbosity = shirabe_php_shim::getenv("SHELL_VERBOSITY").unwrap_or_default(); + let shell_verbosity = shirabe_php_shim::getenv("SHELL_VERBOSITY") + .unwrap_or_default() + .to_string_lossy() + .into_owned(); let shell_verbosity_int: i64 = shell_verbosity.parse().unwrap_or(0); let mut shell_verbosity: i64 = shell_verbosity_int; match shell_verbosity_int { @@ -1500,10 +1503,16 @@ impl Application { } if shirabe_php_shim::function_exists("putenv") { - shirabe_php_shim::putenv(&format!("SHELL_VERBOSITY={}", shell_verbosity)); + unsafe { shirabe_php_shim::putenv("SHELL_VERBOSITY", shell_verbosity.to_string()) }; } - shirabe_php_shim::env_set("SHELL_VERBOSITY", shell_verbosity.to_string()); - shirabe_php_shim::server_set("SHELL_VERBOSITY", shell_verbosity.to_string()); + shirabe_php_shim::PHP_ENV + .lock() + .unwrap() + .put("SHELL_VERBOSITY".into(), shell_verbosity.to_string().into()); + shirabe_php_shim::PHP_SERVER + .lock() + .unwrap() + .put("SHELL_VERBOSITY".into(), shell_verbosity.to_string().into()); let _ = &mut shell_verbosity; @@ -2285,7 +2294,12 @@ impl ApplicationHandle { { io.write_error(&format!( "<warning>Warning: This development build of Composer is over 60 days old. It is recommended to update it by running \"{} self-update\" to get the latest version.</warning>", - shirabe_php_shim::server_get("PHP_SELF").unwrap_or_default(), + shirabe_php_shim::PHP_SERVER + .lock() + .unwrap() + .get("PHP_SELF") + .unwrap_or_default() + .to_string_lossy(), )); } @@ -2598,8 +2612,8 @@ impl ApplicationHandle { let app = application.borrow(); (app.terminal.get_height(), app.terminal.get_width()) }; - shirabe_php_shim::putenv(&format!("LINES={}", height)); - shirabe_php_shim::putenv(&format!("COLUMNS={}", width)); + unsafe { shirabe_php_shim::putenv("LINES", height.to_string()) }; + unsafe { shirabe_php_shim::putenv("COLUMNS", width.to_string()) }; } let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = match input { diff --git a/crates/shirabe/src/main.rs b/crates/shirabe/src/main.rs index 3e3623d..538d89d 100644 --- a/crates/shirabe/src/main.rs +++ b/crates/shirabe/src/main.rs @@ -1,8 +1,13 @@ //! ref: composer/bin/composer -use shirabe_php_shim::run_shutdown_functions; +use shirabe_php_shim::{PHP_ENV, PHP_SERVER, run_shutdown_functions}; fn main() { + // Take the $_ENV / $_SERVER snapshots before any putenv() mutates the real environment. + // See `docs/dev/env-vars-porting.md` for details. + std::sync::LazyLock::force(&PHP_ENV); + std::sync::LazyLock::force(&PHP_SERVER); + let result = shirabe::run(std::env::args().collect()); run_shutdown_functions(); let mut exit_code = match result { diff --git a/crates/shirabe/src/util/platform.rs b/crates/shirabe/src/util/platform.rs index 72fe5f9..889da6e 100644 --- a/crates/shirabe/src/util/platform.rs +++ b/crates/shirabe/src/util/platform.rs @@ -5,11 +5,11 @@ use std::sync::Mutex; use anyhow::Result; use shirabe_external_packages::composer::pcre::Preg; use shirabe_php_shim::{ - PhpMixed, PhpResource, RuntimeException, defined, env_contains_key, env_get, env_set, - env_unset, file_exists, file_get_contents, fstat, function_exists, getcwd, getenv, in_array, - ini_get, is_array, is_readable, mb_strlen, php_os_family, posix_geteuid, posix_getpwuid, - posix_getuid, posix_isatty, putenv, realpath, server_contains_key, server_get, server_set, - server_unset, stream_isatty, stripos, strlen, strtoupper, substr, usleep, + PHP_ENV, PHP_SERVER, PhpMixed, PhpResource, RuntimeException, defined, file_exists, + file_get_contents, fstat, function_exists, getcwd, getenv, in_array, ini_get, is_array, + is_readable, mb_strlen, php_os_family, posix_geteuid, posix_getpwuid, posix_getuid, + posix_isatty, putenv, putenv_clear, realpath, stream_isatty, stripos, strlen, strtoupper, + substr, usleep, }; use crate::util::ProcessExecutor; @@ -66,28 +66,28 @@ impl Platform { /// /// @return string|false pub fn get_env(name: &str) -> Option<String> { - if server_contains_key(name) { - return Some(server_get(name).unwrap_or_default()); + if let Some(value) = PHP_SERVER.lock().unwrap().get(name) { + return Some(value.to_string_lossy().into_owned()); } - if env_contains_key(name) { - return Some(env_get(name).unwrap_or_default()); + if let Some(value) = PHP_ENV.lock().unwrap().get(name) { + return Some(value.to_string_lossy().into_owned()); } - getenv(name) + getenv(name).map(|value| value.to_string_lossy().into_owned()) } /// putenv() equivalent but updates the runtime global variables too pub fn put_env(name: &str, value: &str) { - putenv(&format!("{}={}", name, value)); - server_set(name, value.to_string()); - env_set(name, value.to_string()); + unsafe { putenv(name, value) }; + PHP_SERVER.lock().unwrap().put(name.into(), value.into()); + PHP_ENV.lock().unwrap().put(name.into(), value.into()); } /// putenv('X') equivalent but updates the runtime global variables too pub fn clear_env(name: &str) { - putenv(name); - server_unset(name); - env_unset(name); + unsafe { putenv_clear(name) }; + PHP_SERVER.lock().unwrap().clear(name); + PHP_ENV.lock().unwrap().clear(name); } /// Parses tildes and environment variables in paths. diff --git a/crates/shirabe/tests/command/init_command_test.rs b/crates/shirabe/tests/command/init_command_test.rs index 1c2fca6..0c38bf4 100644 --- a/crates/shirabe/tests/command/init_command_test.rs +++ b/crates/shirabe/tests/command/init_command_test.rs @@ -4,12 +4,13 @@ use crate::test_case::{RunOptions, get_application_tester, init_temp_composer}; use serial_test::serial; use shirabe::command::init_command::InitCommand; use shirabe::json::JsonFile; -use shirabe_php_shim::{PhpMixed, server_set, server_unset}; +use shirabe_php_shim::{PHP_SERVER, PhpMixed}; use tempfile::TempDir; fn set_up() { - server_set("COMPOSER_DEFAULT_AUTHOR", "John Smith".to_string()); - server_set("COMPOSER_DEFAULT_EMAIL", "john@example.com".to_string()); + let mut server = PHP_SERVER.lock().unwrap(); + server.put("COMPOSER_DEFAULT_AUTHOR".into(), "John Smith".into()); + server.put("COMPOSER_DEFAULT_EMAIL".into(), "john@example.com".into()); } /// const DEFAULT_AUTHORS in PHP. @@ -497,12 +498,15 @@ fn test_run_guess_name_from_dir_sanitizes_dir() { std::fs::create_dir(dir_name).unwrap(); std::env::set_current_dir(dir_name).unwrap(); - server_set("COMPOSER_DEFAULT_VENDOR", ".vendorName".to_string()); + PHP_SERVER + .lock() + .unwrap() + .put("COMPOSER_DEFAULT_VENDOR".into(), ".vendorName".into()); let mut app_tester = get_application_tester(); let result = app_tester.run(non_interactive_input(vec![]), RunOptions::default()); - server_unset("COMPOSER_DEFAULT_VENDOR"); + PHP_SERVER.lock().unwrap().clear("COMPOSER_DEFAULT_VENDOR"); result.unwrap(); assert_eq!(0, app_tester.get_status_code()); diff --git a/crates/shirabe/tests/config_test.rs b/crates/shirabe/tests/config_test.rs index 9c2f80b..df29281 100644 --- a/crates/shirabe/tests/config_test.rs +++ b/crates/shirabe/tests/config_test.rs @@ -1,6 +1,7 @@ //! ref: composer/tests/Composer/Test/ConfigTest.php use indexmap::IndexMap; +use serial_test::serial; use shirabe::advisory::Auditor; use shirabe::config::Config; use shirabe::util::Platform; @@ -509,6 +510,7 @@ fn test_disable_tls_can_be_overridden() { } #[test] +#[serial] fn test_process_timeout() { Platform::put_env("COMPOSER_PROCESS_TIMEOUT", "0"); let config = Config::new(true, None); @@ -520,6 +522,7 @@ fn test_process_timeout() { #[ignore] #[test] +#[serial] fn test_htaccess_protect() { Platform::put_env("COMPOSER_HTACCESS_PROTECT", "0"); let config = Config::new(true, None); @@ -530,6 +533,7 @@ fn test_htaccess_protect() { } #[test] +#[serial] fn test_get_source_of_value() { Platform::clear_env("COMPOSER_PROCESS_TIMEOUT"); @@ -552,6 +556,7 @@ fn test_get_source_of_value() { } #[test] +#[serial] fn test_get_source_of_value_env_variables() { Platform::put_env("COMPOSER_HTACCESS_PROTECT", "0"); let mut config = Config::new(true, None); @@ -562,6 +567,7 @@ fn test_get_source_of_value_env_variables() { } #[test] +#[serial] fn test_audit() { let mut config = Config::new(true, None); let result = config.get("audit"); diff --git a/crates/shirabe/tests/util/ini_helper_test.rs b/crates/shirabe/tests/util/ini_helper_test.rs index f511d76..d1e2071 100644 --- a/crates/shirabe/tests/util/ini_helper_test.rs +++ b/crates/shirabe/tests/util/ini_helper_test.rs @@ -12,7 +12,8 @@ fn set_up() -> TearDown { // XdebugHandler is a unit struct with no name-registration API, so this // step is a no-op here. // Save current state - let env_original = getenv("COMPOSER_ORIGINAL_INIS"); + let env_original = + getenv("COMPOSER_ORIGINAL_INIS").map(|value| value.to_string_lossy().into_owned()); TearDown { env_original } } @@ -20,7 +21,7 @@ fn set_up() -> TearDown { fn tear_down(env_original: &Option<String>) { // Restore original state if let Some(env_original) = env_original { - putenv(&format!("COMPOSER_ORIGINAL_INIS={env_original}")); + unsafe { putenv("COMPOSER_ORIGINAL_INIS", env_original) }; } else { Platform::clear_env("COMPOSER_ORIGINAL_INIS"); } @@ -38,10 +39,7 @@ impl Drop for TearDown { } fn set_env(paths: &[&str]) { - putenv(&format!( - "COMPOSER_ORIGINAL_INIS={}", - paths.join(PATH_SEPARATOR) - )); + unsafe { putenv("COMPOSER_ORIGINAL_INIS", paths.join(PATH_SEPARATOR)) }; } #[test] diff --git a/docs/dev/env-vars-porting.md b/docs/dev/env-vars-porting.md new file mode 100644 index 0000000..fbee870 --- /dev/null +++ b/docs/dev/env-vars-porting.md @@ -0,0 +1,44 @@ +# Environment variable porting + +PHP exposes three distinct ways to read process environment variables: the `$_ENV` and `$_SERVER` +superglobals, and `getenv()`/`putenv()`. They look interchangeable but are not, so Shirabe ports +each to a separate construct in `crates/shirabe-php-shim/src/env.rs`. Choose the porting target by +matching the exact PHP construct used in Composer; do not substitute one for another. + +## The three are not interchangeable + +The three differ in what they observe and when: + +* `getenv()`/`putenv()` read and write the real environment variables. `putenv()` mutates the + live process environment, so a value set with `putenv()` is inherited by child processes spawned + afterwards. +* `$_ENV` and `$_SERVER` are snapshots taken at startup. They are populated once when the + process starts and are not kept in sync afterwards. A later `putenv()` does *not* appear in + `$_ENV`/`$_SERVER`, and assigning to `$_ENV`/`$_SERVER` does *not* change the real environment. +* `$_ENV` and `$_SERVER` are not shared with child processes. Launching an external program via `system()` + and friends passes along the real environment (as mutated by `putenv()`), not the + `$_ENV`/`$_SERVER` snapshot. +* Also, `$_ENV` and `$_SERVER` have their own storage; they are not shared. + +Because of these differences, porting must preserve which of the three Composer actually used at +each call site. + +## Mapping + +| PHP | Shirabe | +| --- | --- | +| `getenv()` | `getenv()`/`getenv_all()` | +| `putenv("K=V")` | `putenv()` | +| `putenv("K")` (unset) | `putenv_clear()` | +| `$_ENV` | `PHP_ENV` | +| `$_SERVER` | `PHP_SERVER` | + +## TODOs + +The current implementation only models the Rust side. Two things remain unimplemented: + +* Propagating `$_ENV`/`$_SERVER` into the real PHP runtime. When Shirabe hands control to PHP + (for the plugin API), the PHP side needs to see the same `$_ENV`/`$_SERVER` snapshot Shirabe + holds. This is marked `TODO(php-runtime)` in `env.rs` and is not yet wired up. +* Reflecting PHP-side mutations of `$_ENV`/`$_SERVER` back into Shirabe. How to handle the case + where PHP code rewrites `$_ENV` or `$_SERVER` is still TBD. |
