$names) { $file = $root . '/' . $relative; if (!is_file($file) || !is_readable($file)) { throw new RuntimeException('Missing readable source: ' . $file); } $source = file_get_contents($file); $functions = self::extract($source, $names); foreach ($functions as $name => $definition) { if (function_exists($name)) { throw new RuntimeException('Function already defined: ' . $name); } // Preserve the source bytes and weak scalar coercion of the legacy code. eval($definition['code']); $manifest[$relative]['functions'][$name] = [ 'line' => $definition['line'], 'sha256' => hash('sha256', $definition['code']), ]; } $manifest[$relative]['sha256'] = hash('sha256', $source); } return $manifest; } public static function extract(string $source, array $names): array { $tokens = token_get_all($source); $found = []; $depth = 0; for ($i = 0, $count = count($tokens); $i < $count; $i++) { $token = $tokens[$i]; if ($depth === 0 && is_array($token) && $token[0] === T_FUNCTION) { $j = $i + 1; while (isset($tokens[$j]) && is_array($tokens[$j]) && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT, T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG, T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG], true)) { $j++; } $name = is_array($tokens[$j] ?? null) && $tokens[$j][0] === T_STRING ? $tokens[$j][1] : null; if ($name !== null && in_array($name, $names, true)) { if (isset($found[$name])) { throw new RuntimeException('Duplicate function: ' . $name); } $code = ''; $bodyDepth = 0; $started = false; for ($j = $i; $j < $count; $j++) { $part = $tokens[$j]; $code .= is_array($part) ? $part[1] : $part; if (is_array($part) && in_array($part[0], [T_INCLUDE, T_INCLUDE_ONCE, T_REQUIRE, T_REQUIRE_ONCE, T_EVAL, T_EXIT], true)) { throw new RuntimeException('Unsafe startup/exit operation in ' . $name); } if ($part === '{' || (is_array($part) && in_array($part[0], [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true))) { $bodyDepth++; $started = true; } elseif ($part === '}') { $bodyDepth--; if ($started && $bodyDepth === 0) { $found[$name] = ['code' => $code, 'line' => $token[2]]; $i = $j; break; } } } if (!isset($found[$name])) { throw new RuntimeException('Incomplete function: ' . $name); } continue; } } if ($token === '{' || (is_array($token) && in_array($token[0], [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true))) { $depth++; } elseif ($token === '}') { $depth--; } } $missing = array_diff($names, array_keys($found)); if ($missing) { throw new RuntimeException('Missing top-level function(s): ' . implode(', ', $missing)); } return $found; } }