aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-16 14:46:30 +0900
committernsfisis <nsfisis@gmail.com>2026-05-16 14:46:30 +0900
commit78eb6205ccee4b8b0094b6cba7a852bb1b3a9162 (patch)
tree31a6a94d28dd73f0344753845fba857054703ab9 /crates
parent2173438176ceef9d4121787f590cabb7a0ab4e37 (diff)
downloadphp-shirabe-78eb6205ccee4b8b0094b6cba7a852bb1b3a9162.tar.gz
php-shirabe-78eb6205ccee4b8b0094b6cba7a852bb1b3a9162.tar.zst
php-shirabe-78eb6205ccee4b8b0094b6cba7a852bb1b3a9162.zip
feat(port): port Platform.php
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-php-shim/src/lib.rs94
-rw-r--r--crates/shirabe/src/util/platform.rs419
2 files changed, 513 insertions, 0 deletions
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs
index cefe46a..c3547a8 100644
--- a/crates/shirabe-php-shim/src/lib.rs
+++ b/crates/shirabe-php-shim/src/lib.rs
@@ -853,6 +853,100 @@ pub fn abs(value: i64) -> i64 {
todo!()
}
+pub fn str_contains(haystack: &str, needle: &str) -> bool {
+ todo!()
+}
+
+pub fn usleep(microseconds: u64) {
+ todo!()
+}
+
+pub fn mb_strlen(s: &str, encoding: &str) -> i64 {
+ todo!()
+}
+
+pub fn strlen(s: &str) -> i64 {
+ todo!()
+}
+
+pub fn substr(s: &str, offset: i64, length: Option<i64>) -> String {
+ todo!()
+}
+
+pub fn strtoupper(s: &str) -> String {
+ todo!()
+}
+
+pub fn stream_isatty(stream: PhpMixed) -> bool {
+ todo!()
+}
+
+pub fn posix_getuid() -> i64 {
+ todo!()
+}
+
+pub fn posix_geteuid() -> i64 {
+ todo!()
+}
+
+pub fn posix_getpwuid(uid: i64) -> PhpMixed {
+ todo!()
+}
+
+pub fn posix_isatty(stream: PhpMixed) -> bool {
+ todo!()
+}
+
+pub fn fstat(stream: PhpMixed) -> PhpMixed {
+ todo!()
+}
+
+pub fn getenv(name: &str) -> Option<String> {
+ todo!()
+}
+
+pub fn putenv(setting: &str) -> bool {
+ todo!()
+}
+
+/// PHP superglobal $_SERVER access
+pub fn server_get(name: &str) -> Option<String> {
+ todo!()
+}
+
+pub fn server_set(name: &str, value: String) {
+ todo!()
+}
+
+pub fn server_unset(name: &str) {
+ todo!()
+}
+
+pub fn server_contains_key(name: &str) -> bool {
+ todo!()
+}
+
+pub fn server_argv() -> Vec<String> {
+ todo!()
+}
+
+/// PHP superglobal $_ENV access
+pub fn env_get(name: &str) -> Option<String> {
+ todo!()
+}
+
+pub fn env_set(name: &str, value: String) {
+ todo!()
+}
+
+pub fn env_unset(name: &str) {
+ todo!()
+}
+
+pub fn env_contains_key(name: &str) -> bool {
+ todo!()
+}
+
impl Phar {
pub const SHA512: i64 = 16;
diff --git a/crates/shirabe/src/util/platform.rs b/crates/shirabe/src/util/platform.rs
index 7f83d7d..e8f5dad 100644
--- a/crates/shirabe/src/util/platform.rs
+++ b/crates/shirabe/src/util/platform.rs
@@ -1 +1,420 @@
//! ref: composer/src/Composer/Util/Platform.php
+
+use std::sync::Mutex;
+
+use anyhow::Result;
+use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_php_shim::{
+ defined, env_contains_key, env_get, env_set, env_unset, file_exists, file_get_contents,
+ fopen, fstat, function_exists, getcwd, getenv, in_array, ini_get, is_array, is_readable,
+ mb_strlen, posix_geteuid, posix_getpwuid, posix_getuid, posix_isatty, putenv, realpath,
+ server_argv, server_contains_key, server_get, server_set, server_unset, stream_isatty,
+ stripos, strlen, strtoupper, substr, usleep, PhpMixed, RuntimeException,
+};
+
+use crate::util::process_executor::ProcessExecutor;
+use crate::util::silencer::Silencer;
+
+/// Platform helper for uniform platform-specific tests.
+pub struct Platform;
+
+static IS_VIRTUAL_BOX_GUEST: Mutex<Option<bool>> = Mutex::new(None);
+static IS_WINDOWS_SUBSYSTEM_FOR_LINUX: Mutex<Option<bool>> = Mutex::new(None);
+static IS_DOCKER: Mutex<Option<bool>> = Mutex::new(None);
+
+impl Platform {
+ /// getcwd() equivalent which always returns a string
+ ///
+ /// @throws \RuntimeException
+ pub fn get_cwd(allow_empty: bool) -> Result<String> {
+ let mut cwd = getcwd();
+
+ // fallback to realpath('') just in case this works but odds are it would break as well if we are in a case where getcwd fails
+ if cwd.is_none() {
+ cwd = realpath("");
+ }
+
+ // crappy state, assume '' and hopefully relative paths allow things to continue
+ if cwd.is_none() {
+ if allow_empty {
+ return Ok(String::new());
+ }
+
+ return Err(RuntimeException {
+ message: "Could not determine the current working directory".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ Ok(cwd.unwrap())
+ }
+
+ /// Infallible realpath version that falls back on the given $path if realpath is not working
+ pub fn realpath(path: &str) -> String {
+ let real_path = realpath(path);
+ if real_path.is_none() {
+ return path.to_string();
+ }
+
+ real_path.unwrap()
+ }
+
+ /// getenv() equivalent but reads from the runtime global variables first
+ ///
+ /// @param non-empty-string $name
+ ///
+ /// @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 env_contains_key(name) {
+ return Some(env_get(name).unwrap_or_default());
+ }
+
+ getenv(name)
+ }
+
+ /// 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());
+ }
+
+ /// putenv('X') equivalent but updates the runtime global variables too
+ pub fn clear_env(name: &str) {
+ putenv(name);
+ server_unset(name);
+ env_unset(name);
+ }
+
+ /// Parses tildes and environment variables in paths.
+ pub fn expand_path(path: &str) -> String {
+ if Preg::is_match(r"#^~[\\/]#", path) {
+ return format!("{}{}", Self::get_user_directory().unwrap(), substr(path, 1, None));
+ }
+
+ Preg::replace_callback(
+ r"#^(\$|(?P<percent>%))(?P<var>\w++)(?(percent)%)(?P<path>.*)#",
+ |matches| -> String {
+ // Treat HOME as an alias for USERPROFILE on Windows for legacy reasons
+ if Platform::is_windows()
+ && matches.get("var").map(|s| s.as_str()).unwrap_or("") == "HOME"
+ {
+ if Platform::get_env("HOME").is_some() {
+ return format!(
+ "{}{}",
+ Platform::get_env("HOME").unwrap_or_default(),
+ matches.get("path").map(|s| s.as_str()).unwrap_or(""),
+ );
+ }
+
+ return format!(
+ "{}{}",
+ Platform::get_env("USERPROFILE").unwrap_or_default(),
+ matches.get("path").map(|s| s.as_str()).unwrap_or(""),
+ );
+ }
+
+ format!(
+ "{}{}",
+ Platform::get_env(matches.get("var").map(|s| s.as_str()).unwrap_or(""))
+ .unwrap_or_default(),
+ matches.get("path").map(|s| s.as_str()).unwrap_or(""),
+ )
+ },
+ path,
+ )
+ }
+
+ /// @throws \RuntimeException If the user home could not reliably be determined
+ /// @return string The formal user home as detected from environment parameters
+ pub fn get_user_directory() -> Result<String> {
+ if let Some(home) = Self::get_env("HOME") {
+ return Ok(home);
+ }
+
+ if Self::is_windows() {
+ if let Some(home) = Self::get_env("USERPROFILE") {
+ return Ok(home);
+ }
+ }
+
+ if function_exists("posix_getuid") && function_exists("posix_getpwuid") {
+ let info = posix_getpwuid(posix_getuid());
+
+ if is_array(&info) {
+ if let Some(arr) = info.as_array() {
+ if let Some(dir) = arr.get("dir") {
+ if let Some(s) = dir.as_string() {
+ return Ok(s.to_string());
+ }
+ }
+ }
+ }
+ }
+
+ Err(RuntimeException {
+ message: "Could not determine user directory".to_string(),
+ code: 0,
+ }
+ .into())
+ }
+
+ /// @return bool Whether the host machine is running on the Windows Subsystem for Linux (WSL)
+ pub fn is_windows_subsystem_for_linux() -> bool {
+ let mut cached = IS_WINDOWS_SUBSYSTEM_FOR_LINUX.lock().unwrap();
+ if cached.is_none() {
+ *cached = Some(false);
+
+ // while WSL will be hosted within windows, WSL itself cannot be windows based itself.
+ if Self::is_windows() {
+ *cached = Some(false);
+ return false;
+ }
+
+ // TODO(phase-b): Silencer::call returns Result; PHP returns the value or false on error
+ let file_contents = Silencer::call(|| Ok(file_get_contents("/proc/version")))
+ .ok()
+ .flatten()
+ .unwrap_or_default();
+ if !(ini_get("open_basedir").map(|s| !s.is_empty()).unwrap_or(false))
+ && is_readable("/proc/version")
+ && stripos(&file_contents, "microsoft").is_some()
+ && !Self::is_docker()
+ // Docker and Podman running inside WSL should not be seen as WSL
+ {
+ *cached = Some(true);
+ return true;
+ }
+ }
+
+ cached.unwrap()
+ }
+
+ /// @return bool Whether the host machine is running a Windows OS
+ pub fn is_windows() -> bool {
+ defined("PHP_WINDOWS_VERSION_BUILD")
+ }
+
+ pub fn is_docker() -> bool {
+ let mut cached = IS_DOCKER.lock().unwrap();
+ if let Some(v) = *cached {
+ return v;
+ }
+
+ // cannot check so assume no
+ if ini_get("open_basedir").map(|s| !s.is_empty()).unwrap_or(false) {
+ *cached = Some(false);
+ return false;
+ }
+
+ // .dockerenv and .containerenv are present in some cases but not reliably
+ if file_exists("/.dockerenv")
+ || file_exists("/run/.containerenv")
+ || file_exists("/var/run/.containerenv")
+ {
+ *cached = Some(true);
+ return true;
+ }
+
+ // see https://www.baeldung.com/linux/is-process-running-inside-container
+ let cgroups = vec![
+ "/proc/self/mountinfo", // cgroup v2
+ "/proc/1/cgroup", // cgroup v1
+ ];
+ for cgroup in cgroups {
+ if !is_readable(cgroup) {
+ continue;
+ }
+ // suppress errors as some environments have these files as readable but system restrictions prevent the read from succeeding
+ // see https://github.com/composer/composer/issues/12095
+ let data = match Silencer::call(|| Ok(file_get_contents(cgroup))) {
+ Ok(d) => d,
+ Err(_) => break,
+ };
+ let data = match data {
+ Some(d) => d,
+ None => continue,
+ };
+ // detect default mount points created by Docker/containerd
+ if shirabe_php_shim::str_contains(&data, "/var/lib/docker/")
+ || shirabe_php_shim::str_contains(&data, "/io.containerd.snapshotter")
+ {
+ *cached = Some(true);
+ return true;
+ }
+ }
+
+ *cached = Some(false);
+ false
+ }
+
+ /// @return int return a guaranteed binary length of the string, regardless of silly mbstring configs
+ pub fn strlen(str: &str) -> i64 {
+ // TODO(phase-b): function-local static; collapse to a Mutex<Option<bool>> in Phase B
+ static USE_MB_STRING: Mutex<Option<bool>> = Mutex::new(None);
+ let mut use_mb_string = USE_MB_STRING.lock().unwrap();
+ if use_mb_string.is_none() {
+ *use_mb_string = Some(
+ function_exists("mb_strlen")
+ && ini_get("mbstring.func_overload")
+ .map(|s| !s.is_empty())
+ .unwrap_or(false),
+ );
+ }
+
+ if use_mb_string.unwrap() {
+ return mb_strlen(str, "8bit");
+ }
+
+ strlen(str)
+ }
+
+ /// @param ?resource $fd Open file descriptor or null to default to STDOUT
+ pub fn is_tty(fd: Option<PhpMixed>) -> bool {
+ let fd = match fd {
+ Some(f) => f,
+ None => {
+ if defined("STDOUT") {
+ // TODO(phase-b): map STDOUT to the runtime stdout resource
+ todo!("STDOUT constant")
+ } else {
+ let fd = fopen("php://stdout", "w");
+ if matches!(fd, PhpMixed::Bool(false)) {
+ return false;
+ }
+ fd
+ }
+ }
+ };
+
+ // detect msysgit/mingw and assume this is a tty because detection
+ // does not work correctly, see https://github.com/composer/composer/issues/9690
+ if in_array(
+ PhpMixed::String(strtoupper(
+ &Self::get_env("MSYSTEM").unwrap_or_default(),
+ )),
+ &PhpMixed::List(vec![
+ Box::new(PhpMixed::String("MINGW32".to_string())),
+ Box::new(PhpMixed::String("MINGW64".to_string())),
+ ]),
+ true,
+ ) {
+ return true;
+ }
+
+ // modern cross-platform function, includes the fstat
+ // fallback so if it is present we trust it
+ if function_exists("stream_isatty") {
+ return stream_isatty(fd);
+ }
+
+ // only trusting this if it is positive, otherwise prefer fstat fallback
+ if function_exists("posix_isatty") && posix_isatty(fd.clone()) {
+ return true;
+ }
+
+ // TODO(phase-b): Silencer::call wraps the fstat call (`@fstat($fd)`)
+ let stat = Silencer::call(|| Ok(fstat(fd)));
+ let stat = match stat {
+ Ok(s) => s,
+ Err(_) => return false,
+ };
+ if matches!(stat, PhpMixed::Bool(false)) {
+ return false;
+ }
+
+ // Check if formatted mode is S_IFCHR
+ if let Some(arr) = stat.as_array() {
+ if let Some(mode) = arr.get("mode").and_then(|v| v.as_int()) {
+ return 0o020000 == (mode & 0o170000);
+ }
+ }
+
+ false
+ }
+
+ /// @return bool Whether the current command is for bash completion
+ pub fn is_input_completion_process() -> bool {
+ // PHP: $_SERVER['argv'][1] ?? null
+ let argv = server_argv();
+ argv.get(1).map(|s| s.as_str()) == Some("_complete")
+ }
+
+ pub fn workaround_filesystem_issues() {
+ if Self::is_virtual_box_guest() {
+ usleep(200_000);
+ }
+ }
+
+ /// Attempts detection of VirtualBox guest VMs
+ ///
+ /// This works based on the process' user being "vagrant", the COMPOSER_RUNTIME_ENV env var being set to "virtualbox", or lsmod showing the virtualbox guest additions are loaded
+ fn is_virtual_box_guest() -> bool {
+ let mut cached = IS_VIRTUAL_BOX_GUEST.lock().unwrap();
+ if cached.is_none() {
+ *cached = Some(false);
+ if Self::is_windows() {
+ return cached.unwrap();
+ }
+
+ if function_exists("posix_getpwuid") && function_exists("posix_geteuid") {
+ let process_user = posix_getpwuid(posix_geteuid());
+ if is_array(&process_user) {
+ if let Some(arr) = process_user.as_array() {
+ if arr
+ .get("name")
+ .and_then(|v| v.as_string())
+ .map(|s| s == "vagrant")
+ .unwrap_or(false)
+ {
+ *cached = Some(true);
+ return true;
+ }
+ }
+ }
+ }
+
+ if Self::get_env("COMPOSER_RUNTIME_ENV").as_deref() == Some("virtualbox") {
+ *cached = Some(true);
+ return true;
+ }
+
+ if defined("PHP_OS_FAMILY")
+ // TODO(phase-b): PHP_OS_FAMILY constant comparison
+ && true
+ {
+ let process = ProcessExecutor::new();
+ // TODO(phase-b): inner Result for catch(\Exception); use anyhow::Result<Result<_, _>>
+ let mut output = String::new();
+ let result: Result<()> = (|| {
+ if process.execute(&["lsmod"], &mut output)? == 0
+ && shirabe_php_shim::str_contains(&output, "vboxguest")
+ {
+ *cached = Some(true);
+ return Ok(());
+ }
+ Ok(())
+ })();
+ if result.is_ok() && cached.unwrap_or(false) {
+ return true;
+ }
+ // noop on error
+ }
+ }
+
+ cached.unwrap_or(false)
+ }
+
+ /// @return 'NUL'|'/dev/null'
+ pub fn get_dev_null() -> String {
+ if Self::is_windows() {
+ return "NUL".to_string();
+ }
+
+ "/dev/null".to_string()
+ }
+}