Skip to main content

vivacity_core/
binproxy.rs

1//! vendor/bin proxies: byte-for-byte port of
2//! `BinaryInstaller::generateUnixyProxyCode` (docs/reference/BinaryInstaller.php).
3//! Three shapes: PHP target with shebang (anti-shebang stream wrapper for
4//! PHP<8, special phpunit hack), bare PHP target, non-PHP target (sh proxy).
5//! Parity is held by the differential test against the proxies generated by
6//! Composer in the Laravel fixture (tests/fixtures_binproxy.rs).
7
8use crate::error::{Error, Result};
9use crate::pathutil::{find_shortest_path, find_shortest_path_code, normalize_path};
10use std::path::Path;
11
12/// Generates the content of the proxy `link` (vendor/bin/<name>) to the binary
13/// `bin` (absolute, inside vendor/ or outside it with composer/installers),
14/// reading the target's header. Relative paths are those of
15/// `BinaryInstaller::installUnixyProxyBinaries` (findShortestPath from the
16/// proxy file).
17pub 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            // Stream wrapper only if the target does not start directly
51            // with `<?php` (shebang or whitespace in front).
52            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
74/// Composer's regex: `^(#!.*\r?\n)?[\r\n\t ]*<\?php`.
75fn 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    // ProcessExecutor::escape on a simple path = single quotes.
214    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
256/// Installs a package's proxies (laid out in `package_dir`) into vendor/bin (0755).
257pub 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; // binary declared but missing from the dist: Composer skips it too
267        }
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}