use crate::error::{Error, Result};
use crate::pathutil::{find_shortest_path, find_shortest_path_code, normalize_path};
use std::path::Path;
pub fn proxy_content(vendor_dir: &Path, link: &Path, bin: &Path) -> Result<String> {
let mut head = [0u8; 500];
let n = {
use std::io::Read as _;
let mut f = std::fs::File::open(bin).map_err(Error::io(bin))?;
f.read(&mut head).map_err(Error::io(bin))?
};
let head = String::from_utf8_lossy(&head[..n]);
let link_s = link.to_string_lossy();
let bin_s = bin.to_string_lossy();
let vendor_s = vendor_dir.to_string_lossy();
let bin_path = find_shortest_path(&link_s, &bin_s, false);
let bin_exported = find_shortest_path_code(&link_s, &bin_s, false, true);
let autoload_exported =
find_shortest_path_code(&link_s, &format!("{vendor_s}/autoload.php"), false, true);
match php_header(&head) {
Some(PhpHeader { shebang }) => {
let proxy_code = shebang
.clone()
.unwrap_or_else(|| "#!/usr/bin/env php".to_owned());
let is_phpunit = normalize_path(&bin_s)
== normalize_path(&format!("{vendor_s}/phpunit/phpunit/phpunit"));
let mut globals = String::from("$GLOBALS['_composer_bin_dir'] = __DIR__;\n");
globals.push_str(&format!(
"$GLOBALS['_composer_autoload_path'] = {autoload_exported};\n"
));
if is_phpunit {
globals.push_str(&format!(
"$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath({bin_exported}));\n"
));
}
let needs_stream = shebang.is_some() || !head.starts_with("<?php");
let (stream_hint, stream_code) = if needs_stream {
(
" using a stream wrapper to prevent the shebang from being output on PHP<8\n *"
.to_owned(),
stream_proxy_code(&bin_exported, is_phpunit),
)
} else {
(String::new(), String::new())
};
Ok(format!(
"{proxy_code}\n<?php\n\n/**\n * Proxy PHP file generated by Composer\n *\n * This file includes the referenced bin path ({bin_path})\n *{stream_hint}\n * @generated\n */\n\nnamespace Composer;\n\n{globals}\n{stream_code}\nreturn include {bin_exported};\n"
))
}
None => Ok(sh_proxy(&bin_path)),
}
}
struct PhpHeader {
shebang: Option<String>,
}
fn php_header(head: &str) -> Option<PhpHeader> {
let (shebang, rest) = if head.starts_with("#!") {
let end = head.find('\n')?;
(
Some(head[..end].trim_end_matches('\r').to_owned()),
&head[end + 1..],
)
} else {
(None, head)
};
let trimmed = rest.trim_start_matches(['\r', '\n', '\t', ' ']);
trimmed
.starts_with("<?php")
.then_some(PhpHeader { shebang })
}
fn stream_proxy_code(bin_exported: &str, is_phpunit: bool) -> String {
let hack1 = if is_phpunit {
"'phpvfscomposer://'."
} else {
""
};
let hack2 = if is_phpunit {
"\n $data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);\n $data = str_replace('__FILE__', var_export($this->realpath, true), $data);"
} else {
""
};
format!(
r#"if (PHP_VERSION_ID < 80000) {{
if (!class_exists('Composer\BinProxyWrapper')) {{
/**
* @internal
*/
final class BinProxyWrapper
{{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = {hack1}$this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}}
public function stream_read($count)
{{
$data = fread($this->handle, $count);
if ($this->position === 0) {{
$data = preg_replace('{{^#!.*\r?\n}}', '', $data);
}}{hack2}
$this->position += strlen($data);
return $data;
}}
public function stream_cast($castAs)
{{
return $this->handle;
}}
public function stream_close()
{{
fclose($this->handle);
}}
public function stream_lock($operation)
{{
return $operation ? flock($this->handle, $operation) : true;
}}
public function stream_seek($offset, $whence)
{{
if (0 === fseek($this->handle, $offset, $whence)) {{
$this->position = ftell($this->handle);
return true;
}}
return false;
}}
public function stream_tell()
{{
return $this->position;
}}
public function stream_eof()
{{
return feof($this->handle);
}}
public function stream_stat()
{{
return array();
}}
public function stream_set_option($option, $arg1, $arg2)
{{
return true;
}}
public function url_stat($path, $flags)
{{
$path = substr($path, 17);
if (file_exists($path)) {{
return stat($path);
}}
return false;
}}
}}
}}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {{
return include("phpvfscomposer://" . {bin_exported});
}}
}}
"#
)
}
fn sh_proxy(bin_path: &str) -> String {
let dir = bin_path.rsplit_once('/').map(|(d, _)| d).unwrap_or(".");
let file = bin_path
.rsplit_once('/')
.map(|(_, f)| f)
.unwrap_or(bin_path);
format!(
r#"#!/usr/bin/env sh
# Support bash to support `source` with fallback on $0 if this does not run with bash
# https://stackoverflow.com/a/35006505/6512
selfArg="$BASH_SOURCE"
if [ -z "$selfArg" ]; then
selfArg="$0"
fi
self=$(realpath "$selfArg" 2> /dev/null)
if [ -z "$self" ]; then
self="$selfArg"
fi
dir=$(cd "${{self%[/\\]*}}" > /dev/null; cd '{dir}' && pwd)
if [ -d /proc/cygdrive ]; then
case $(which php) in
$(readlink -n /proc/cygdrive)/*)
# We are in Cygwin using Windows php, so the path must be translated
dir=$(cygpath -m "$dir");
;;
esac
fi
export COMPOSER_RUNTIME_BIN_DIR="$(cd "${{self%[/\\]*}}" > /dev/null; pwd)"
# If bash is sourcing this file, we have to source the target as well
bashSource="$BASH_SOURCE"
if [ -n "$bashSource" ]; then
if [ "$bashSource" != "$0" ]; then
source "${{dir}}/{file}" "$@"
return
fi
fi
exec "${{dir}}/{file}" "$@"
"#
)
}
pub fn install_binaries(vendor_dir: &Path, package_dir: &Path, bins: &[&str]) -> Result<()> {
let bin_dir = vendor_dir.join("bin");
std::fs::create_dir_all(&bin_dir).map_err(Error::io(&bin_dir))?;
for bin in bins {
let bin = bin.trim_start_matches("./");
let target = package_dir.join(bin);
let link_name = bin.rsplit_once('/').map(|(_, f)| f).unwrap_or(bin);
let link = bin_dir.join(link_name);
if !target.exists() {
continue; }
let content = proxy_content(vendor_dir, &link, &target)?;
std::fs::write(&link, content).map_err(Error::io(&link))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(&link, std::fs::Permissions::from_mode(0o755))
.map_err(Error::io(&link))?;
}
}
Ok(())
}