vivacity_core/
binproxy.rs1use crate::error::{Error, Result};
9use crate::pathutil::{find_shortest_path, find_shortest_path_code, normalize_path};
10use std::path::Path;
11
12pub fn proxy_content(vendor_dir: &Path, link: &Path, bin: &Path) -> Result<String> {
18 let mut head = [0u8; 500];
19 let n = {
20 use std::io::Read as _;
21 let mut f = std::fs::File::open(bin).map_err(Error::io(bin))?;
22 f.read(&mut head).map_err(Error::io(bin))?
23 };
24 let head = String::from_utf8_lossy(&head[..n]);
25
26 let link_s = link.to_string_lossy();
27 let bin_s = bin.to_string_lossy();
28 let vendor_s = vendor_dir.to_string_lossy();
29 let bin_path = find_shortest_path(&link_s, &bin_s, false);
30 let bin_exported = find_shortest_path_code(&link_s, &bin_s, false, true);
31 let autoload_exported =
32 find_shortest_path_code(&link_s, &format!("{vendor_s}/autoload.php"), false, true);
33
34 match php_header(&head) {
35 Some(PhpHeader { shebang }) => {
36 let proxy_code = shebang
37 .clone()
38 .unwrap_or_else(|| "#!/usr/bin/env php".to_owned());
39 let is_phpunit = normalize_path(&bin_s)
40 == normalize_path(&format!("{vendor_s}/phpunit/phpunit/phpunit"));
41 let mut globals = String::from("$GLOBALS['_composer_bin_dir'] = __DIR__;\n");
42 globals.push_str(&format!(
43 "$GLOBALS['_composer_autoload_path'] = {autoload_exported};\n"
44 ));
45 if is_phpunit {
46 globals.push_str(&format!(
47 "$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath({bin_exported}));\n"
48 ));
49 }
50 let needs_stream = shebang.is_some() || !head.starts_with("<?php");
53 let (stream_hint, stream_code) = if needs_stream {
54 (
55 " using a stream wrapper to prevent the shebang from being output on PHP<8\n *"
56 .to_owned(),
57 stream_proxy_code(&bin_exported, is_phpunit),
58 )
59 } else {
60 (String::new(), String::new())
61 };
62 Ok(format!(
63 "{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"
64 ))
65 }
66 None => Ok(sh_proxy(&bin_path)),
67 }
68}
69
70struct PhpHeader {
71 shebang: Option<String>,
72}
73
74fn php_header(head: &str) -> Option<PhpHeader> {
76 let (shebang, rest) = if head.starts_with("#!") {
77 let end = head.find('\n')?;
78 (
79 Some(head[..end].trim_end_matches('\r').to_owned()),
80 &head[end + 1..],
81 )
82 } else {
83 (None, head)
84 };
85 let trimmed = rest.trim_start_matches(['\r', '\n', '\t', ' ']);
86 trimmed
87 .starts_with("<?php")
88 .then_some(PhpHeader { shebang })
89}
90
91fn stream_proxy_code(bin_exported: &str, is_phpunit: bool) -> String {
92 let hack1 = if is_phpunit {
93 "'phpvfscomposer://'."
94 } else {
95 ""
96 };
97 let hack2 = if is_phpunit {
98 "\n $data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);\n $data = str_replace('__FILE__', var_export($this->realpath, true), $data);"
99 } else {
100 ""
101 };
102 format!(
103 r#"if (PHP_VERSION_ID < 80000) {{
104 if (!class_exists('Composer\BinProxyWrapper')) {{
105 /**
106 * @internal
107 */
108 final class BinProxyWrapper
109 {{
110 private $handle;
111 private $position;
112 private $realpath;
113
114 public function stream_open($path, $mode, $options, &$opened_path)
115 {{
116 // get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
117 $opened_path = substr($path, 17);
118 $this->realpath = realpath($opened_path) ?: $opened_path;
119 $opened_path = {hack1}$this->realpath;
120 $this->handle = fopen($this->realpath, $mode);
121 $this->position = 0;
122
123 return (bool) $this->handle;
124 }}
125
126 public function stream_read($count)
127 {{
128 $data = fread($this->handle, $count);
129
130 if ($this->position === 0) {{
131 $data = preg_replace('{{^#!.*\r?\n}}', '', $data);
132 }}{hack2}
133
134 $this->position += strlen($data);
135
136 return $data;
137 }}
138
139 public function stream_cast($castAs)
140 {{
141 return $this->handle;
142 }}
143
144 public function stream_close()
145 {{
146 fclose($this->handle);
147 }}
148
149 public function stream_lock($operation)
150 {{
151 return $operation ? flock($this->handle, $operation) : true;
152 }}
153
154 public function stream_seek($offset, $whence)
155 {{
156 if (0 === fseek($this->handle, $offset, $whence)) {{
157 $this->position = ftell($this->handle);
158 return true;
159 }}
160
161 return false;
162 }}
163
164 public function stream_tell()
165 {{
166 return $this->position;
167 }}
168
169 public function stream_eof()
170 {{
171 return feof($this->handle);
172 }}
173
174 public function stream_stat()
175 {{
176 return array();
177 }}
178
179 public function stream_set_option($option, $arg1, $arg2)
180 {{
181 return true;
182 }}
183
184 public function url_stat($path, $flags)
185 {{
186 $path = substr($path, 17);
187 if (file_exists($path)) {{
188 return stat($path);
189 }}
190
191 return false;
192 }}
193 }}
194 }}
195
196 if (
197 (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
198 || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
199 ) {{
200 return include("phpvfscomposer://" . {bin_exported});
201 }}
202}}
203"#
204 )
205}
206
207fn sh_proxy(bin_path: &str) -> String {
208 let dir = bin_path.rsplit_once('/').map(|(d, _)| d).unwrap_or(".");
209 let file = bin_path
210 .rsplit_once('/')
211 .map(|(_, f)| f)
212 .unwrap_or(bin_path);
213 format!(
215 r#"#!/usr/bin/env sh
216
217# Support bash to support `source` with fallback on $0 if this does not run with bash
218# https://stackoverflow.com/a/35006505/6512
219selfArg="$BASH_SOURCE"
220if [ -z "$selfArg" ]; then
221 selfArg="$0"
222fi
223
224self=$(realpath "$selfArg" 2> /dev/null)
225if [ -z "$self" ]; then
226 self="$selfArg"
227fi
228
229dir=$(cd "${{self%[/\\]*}}" > /dev/null; cd '{dir}' && pwd)
230
231if [ -d /proc/cygdrive ]; then
232 case $(which php) in
233 $(readlink -n /proc/cygdrive)/*)
234 # We are in Cygwin using Windows php, so the path must be translated
235 dir=$(cygpath -m "$dir");
236 ;;
237 esac
238fi
239
240export COMPOSER_RUNTIME_BIN_DIR="$(cd "${{self%[/\\]*}}" > /dev/null; pwd)"
241
242# If bash is sourcing this file, we have to source the target as well
243bashSource="$BASH_SOURCE"
244if [ -n "$bashSource" ]; then
245 if [ "$bashSource" != "$0" ]; then
246 source "${{dir}}/{file}" "$@"
247 return
248 fi
249fi
250
251exec "${{dir}}/{file}" "$@"
252"#
253 )
254}
255
256pub fn install_binaries(vendor_dir: &Path, package_dir: &Path, bins: &[&str]) -> Result<()> {
258 let bin_dir = vendor_dir.join("bin");
259 std::fs::create_dir_all(&bin_dir).map_err(Error::io(&bin_dir))?;
260 for bin in bins {
261 let bin = bin.trim_start_matches("./");
262 let target = package_dir.join(bin);
263 let link_name = bin.rsplit_once('/').map(|(_, f)| f).unwrap_or(bin);
264 let link = bin_dir.join(link_name);
265 if !target.exists() {
266 continue; }
268 let content = proxy_content(vendor_dir, &link, &target)?;
269 std::fs::write(&link, content).map_err(Error::io(&link))?;
270 #[cfg(unix)]
271 {
272 use std::os::unix::fs::PermissionsExt as _;
273 std::fs::set_permissions(&link, std::fs::Permissions::from_mode(0o755))
274 .map_err(Error::io(&link))?;
275 }
276 }
277 Ok(())
278}