Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions debug-tokenizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
$code = file_get_contents('test-simple-static.php');
$tokens = token_get_all($code);

foreach ($tokens as $i => $token) {
if (is_string($token)) {
printf("%3d: CHAR '%s'\n", $i, $token);
} else {
printf("%3d: %-20s '%s'\n", $i, token_name($token[0]), $token[1]);
}
}
320 changes: 320 additions & 0 deletions encrypted-execution/src/transformer/snip-transform-v2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
<?php
/**
* Token-Aware PHP Transformer with Type Declaration Support
* Copyright (c) 2026 Encrypted Execution
*
* This transformer properly handles PHP type declarations and prevents
* transformation of type names in type hint contexts.
*/

const IGNORE = array(T_STRING, T_INLINE_HTML, T_CONSTANT_ENCAPSED_STRING, T_START_HEREDOC,
T_END_HEREDOC, T_COMMENT, T_ENCAPSED_AND_WHITESPACE, T_CLOSE_TAG, T_OPEN_TAG,
T_OPEN_TAG_WITH_ECHO);

// PHP type names that should NEVER be transformed
// These are used in type declarations and must remain as-is
const TYPE_NAMES = array(
'int', 'float', 'string', 'bool', 'array', 'object',
'callable', 'iterable', 'void', 'mixed', 'never',
'null', 'false', 'true', 'self', 'parent', 'static',
'resource' // Added for completeness
);

const DICTIONARY = "/scrambled.json";
const ENCRYPTED_EXECUTION_PATH = "ENCRYPTED_EXECUTION_PATH";

$GLOBALS["keys_ps_map"] = [];

class String_State
{
public $in_str;
public $curl_depth;
}

class Type_Context
{
public $in_param_list = false;
public $after_colon = false; // After : in return type
public $after_question = false; // After ? in nullable type
public $after_pipe = false; // After | in union type
public $paren_depth = 0;

public function reset() {
$this->in_param_list = false;
$this->after_colon = false;
$this->after_question = false;
$this->after_pipe = false;
}

public function is_in_type_context() {
return $this->in_param_list || $this->after_colon ||
$this->after_question || $this->after_pipe;
}
}

$str_state = new String_State();
$type_context = new Type_Context();

function ee_snip($snip, $is_test, $dictionary_path = null)
{
global $custom_dictionary;
if ($dictionary_path != null) {
$custom_dictionary = $dictionary_path;
}
get_dir(); init_str_count();

global $tokens, $type_context;

init_str_count();
$type_context = new Type_Context();

$tokens = token_get_all($snip);
$snipOut = "";

for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];

// Update type context based on current token
update_type_context($token, $i);

//Keep expected output for php built-in tests
if ($is_test && check_expected($i) && ($tokens[$i + 1][1] === "EXPECT" || $tokens[$i + 1][1] == "EXPECTF")) {
return $snipOut . grab_expected($snip, $tokens[$i + 1][1]);
} else if (check_expected($i)) {
$snipOut .= "--" . $tokens[++$i][1] . "--";
$i++;
}

if (!is_array($token)) {
$snipOut .= get_from_dictionary($token);
} else {
$snipOut .= get_tok_val($token, $i);
}

if ($is_test && $token[0] == "T_DEC" && check_expected($i)) {
$snipOut .= grab_expected($snip);
return $snipOut;
}
}
return $snipOut;
}

function update_type_context($token, $i)
{
global $type_context, $tokens;

if (!is_array($token)) {
// Character tokens
if ($token === '(') {
$type_context->paren_depth++;
// Check if this is a parameter list (after function/fn keyword)
$prev = get_prev_meaningful_token($i);
if ($prev && is_array($prev)) {
$prev_str = strtolower($prev[1]);
if ($prev_str === 'function' || $prev_str === 'fn') {
$type_context->in_param_list = true;
}
}
} else if ($token === ')') {
$type_context->paren_depth--;
if ($type_context->paren_depth === 0) {
$type_context->in_param_list = false;
}
} else if ($token === ':') {
// Could be return type declaration
if ($type_context->paren_depth === 0) {
$type_context->after_colon = true;
}
} else if ($token === '?') {
// Nullable type
if ($type_context->is_in_type_context()) {
$type_context->after_question = true;
}
} else if ($token === '|') {
// Union type
if ($type_context->is_in_type_context()) {
$type_context->after_pipe = true;
}
} else if ($token === ',') {
// Continue in param list context but reset sub-contexts
$type_context->after_question = false;
$type_context->after_pipe = false;
} else if ($token === '{' || $token === ';') {
// End of function signature
$type_context->reset();
}
} else if ($token[0] === T_WHITESPACE) {
// Whitespace doesn't change context
} else {
// After consuming a type name, reset the immediate context flags
if ($type_context->after_question || $type_context->after_pipe) {
$type_context->after_question = false;
$type_context->after_pipe = false;
} else if ($type_context->after_colon && !is_whitespace_or_comment($token)) {
// After return type, reset
$type_context->after_colon = false;
}
}
}

function get_prev_meaningful_token($current_index)
{
global $tokens;

for ($i = $current_index - 1; $i >= 0; $i--) {
$token = $tokens[$i];
if (is_array($token) && ($token[0] === T_WHITESPACE || $token[0] === T_COMMENT)) {
continue;
}
return $token;
}
return null;
}

function is_whitespace_or_comment($token)
{
return is_array($token) && ($token[0] === T_WHITESPACE || $token[0] === T_COMMENT);
}

function get_tok_val($token, $i)
{
global $keys_ps_map, $str_state, $type_context;
$char_token_pattern = "/(\()?[A-Za-z0-9 \n]+(\))?/";

$tok_name = token_name($token[0]);
$tok_str = strtolower($token[1]);
$tok_len = strlen($tok_str) - 1;

if ($tok_name == T_DOLLAR_OPEN_CURLY_BRACES) {
$str_state->curl_depth++;
}

if (!preg_match($char_token_pattern, $tok_str)) {
if (in_array($token[0], IGNORE)) {
return $token[1];
} else {
return transform_char_token($tok_str);
}
}

//account for syntax of casting
if ($tok_str[0] == "(" && $tok_str[$tok_len] == ")" && strpos($tok_name, "_CAST")) {
$tok_str = trim($tok_str, "( )");
if (isset ($keys_ps_map[$tok_str])) {
return get_from_dictionary("(") . get_from_dictionary($tok_str) . get_from_dictionary(")");
}
}

// Check if we're in a type declaration context and this is a type name
if ($type_context->is_in_type_context() && in_array($tok_str, TYPE_NAMES)) {
// Don't transform type names in type declarations
return $token[1];
}

if (check_ignore_cases($token[0], $tok_str, $i)) {
return $token[1];
} else {
return get_from_dictionary($tok_str);
}
}

function transform_char_token($tok_str)
{
for ($i = 0; $i < strlen($tok_str); $i++) {
$tok_str[$i] = get_from_dictionary($tok_str[$i]);
}
return $tok_str;
}

function check_ignore_cases($tok_val, $tok_str, $i)
{
global $tokens, $keys_ps_map;
$double_colon_tag = "T_DOUBLE_COLON";

return ((in_array($tok_val, IGNORE) || !isset ($keys_ps_map[$tok_str])) ||
($tok_val === T_CLASS && token_name($tokens[--$i][0]) === $double_colon_tag));
}

function get_from_dictionary($token_key)
{
global $keys_ps_map;

if (is_special_case($token_key)) {
return $token_key;
}

if (isset($keys_ps_map[$token_key])) {
return $keys_ps_map[$token_key];
} else {
return $token_key;
}
}

function is_special_case($token_key)
{
global $str_state;

if ($str_state->in_str == false && $token_key != "\"") {
return false;
}

if ($token_key == "\"") {
stateFlip();
} else if ($token_key == "{") {
$str_state->curl_depth++;
} else if ($token_key == "}") {
$str_state->curl_depth--;
}

if ($token_key == "-" && $str_state->curl_depth == 0 ) {
return true;
}

return false;
}

function stateFlip()
{
global $str_state;
$str_state->in_str = !$str_state->in_str;
}

function init_str_count() {
global $str_state;
$str_state->in_str = false;
$str_state->curl_depth = 0;
}

function get_dir()
{
global $custom_dictionary;
global $keys_ps_map;

if ($custom_dictionary) {
$keys_ps_map = json_decode(file_get_contents( $custom_dictionary), TRUE)
or exit ("Error: path given for dictionary could not be found.");
return;
}

$parent = getenv(ENCRYPTED_EXECUTION_PATH);
if ($parent == "") {
$parent = ".";
echo "Encryption dictionary not found. Looking for scrambled.json in current directory.";
}
$keys_ps_map = json_decode(file_get_contents($parent . DICTIONARY), TRUE)
or exit ("Error: no encryption dictionary found.");
}

//the following tests are used for .phpt files when testing expected output.
function grab_expected($snip, $tag)
{
$expectedTag = "--".$tag."--";
$pos = strpos($snip, $expectedTag);
return substr($snip, $pos);
}

function check_expected($i)
{
global $tokens;
return ($tokens[$i][0] == T_DEC) && ($tokens[++$i][0] == T_STRING) && ($tokens[++$i][0] == T_DEC);
}
Loading