php 结构体-PHP实现类似于python中Contruct库的功能(一)基本设计思想

介绍

python中有一个Construct库可以用来解析二进制数据。 使用该工具可以非常方便地分析网络数据包、格式化数据文件等。

如果前段时间分析sqlite数据库文件格式时使用这些工具,可以省去很多麻烦。

不过Construct2.9与之前的版本相比有了很大的改变。 原来用Construct写的python代码基本上都需要重写。 在查看Construct源码的过程中,我发现Construct的基本实现思想就是递归增长分析方法。 通过对象构造方法动态定义数据结构,并在parse方法中实现对二进制数据的分析。

我打算用php来分析二进制数据,但是实现思路和python的Construct完全不同。

基本思想

由于Python的Construct使用Python的宾语句型来实现动态层次结构的定义和解析,因此由于Python宾语句型的限制php 结构体,一些结构定义显得冗长。

php 结构体

我的看法是定义一种专门用来描述动态层次结构的小语言,这样可以尽可能照顾人们共同的表达习惯。

这个小项目以C语言的结构定义句型为蓝本,在此基础上减少了循环的条件化和结构定义。

二进制数据有一个共同的结构php 结构体,前几个字节存储前一个数据块的宽度,接下来的就是数据块。 用C语言的结构体定义来表示这种结构体并不方便。 整个数据块的宽度发生变化,在编译时很难确定,只能在解析时确定。 因此,有必要对C语言的结构定义句型进行扩展。

第一步实现C语言结构分析

这一步我们不考虑动态层次结构定义,而是实现这个小语言的核心部分。 至少必须能够理解C语言的结构体定义,并且能够根据这个结构体定义来处理二进制数据。 进行分析。 这一步完成后,就将实现动态结构的定义和分析。

该项目基于上面文件中简要介绍的ADOS脚本语言引擎。

我们先来看看我们的使命

这是一个完全C语言大小的结构体定义文件

块结构体.h

struct student
{
  char name[2];
  int num;
  int age;
  char addr[3];
};
struct teacher
{
  char name[2];
  int num;  
  char addr[3];
};

待解析的二进制数据块

"x41x42x01x00x00x00x02x00x00x00x41x42x43x41x42x01x00x00x00x41x42x43"

期望的分析结果

[value] => Array
        (
            [name] => AB
            [num] => 1
            [age] => 2
            [addr] => ABC
        )
[value] => Array
        (
            [name] => AB
            [num] => 1
            [addr] => ABC
        )

php 结构体

有兴趣的同学可以关注一下两者的关系

下面是一个词法规则文件,只实现了C语言结构体的编译。

<?php
/*!
 * struckwkr的词法规则
 * 45022300@qq.com
 * Version 0.9.0
 *
 * Copyright 2019, Zhu Hui
 * Released under the MIT license
 */
$GLOBALS['structwkr_lexRules'] =	[
		['/^"(.*?)"/','_cons'	,'"'],
		['/^(/','_lp'			,'('],
		['/^)/','_rp'			,')'],
		['/^[/','_lb'			,'['],
		['/^]/','_rb'			,']'],
		['/^{/','_lcb'			,'{'],
		['/^}/','_rcb'			,'}'],
		['/^;/','_semi'			,';'],
		['/^,/','_comma'		,','],
		['/^==/','_bieq'		,'='],
		['/^!=/','_uneq'		,'!'],		
		['/^>=/','_greq'		,'>'],		
		['/^<=/','_leeq'		,'<'],		
		['/^=/','_equa'			,'='],		
		['/^>/','_grea'		,'>'],
		['/^</','_less'		,'<'],	
		['/^+/','_add'			,'+'],
		['/^-/','_sub'			,'-'],
		['/^*/','_mul'			,'*'],
		['/^//','_div'			,'/'],	
		['/^%/','_mod'			,'$'],
		['/^&&/','_and'			,'&'],	
		['/^||/','_or'		,'|'],	
		['/^!/','_not'			,'!'],	
		['/^struct/','_strukey'	,'s'],
		['/^char/','_char'		,'c'],
		['/^int/','_int'		,'i'],
		['/^float/','_float'	,'f'],
		['/^double/','_double'	,'d'],
		['/^[0-9]+([.]{1}[0-9]+){0,1}/','_num',''],
		['/^./','_dot'			,'.'],
		['/^[x{4e00}-x{9fa5}A-Za-z_][x{4e00}-x{9fa5}A-Za-z0-9_]*b/u','_iden',''],
		['/^s*/','_null','']
		];

下面是一个仅实现C语言结构编译的句型规则文件。

<?php
/*!
 * structwkr的语法规则处理器
 * 45022300@qq.com
 * Version 0.9.0
 *
 * Copyright 2019, Zhu Hui
 * Released under the MIT license
 */
namespace Ados;
require_once 'const.php';
require_once __SCRIPTCORE__.'syntax_rule/base_rules_handler.php';
class StructwkrRulesHandler extends BaseRulesHandler{
//语法分析的起始记号,归约到最后得到就是若干个结构体定义的列表
function startToken(){
	return '_structList';
}
//附加类型
function extraType($extraArray){
	if(count($extraArray)>0){
		return $extraArray[0];
	}else{
		return '';
	}
}
//求出放在附加信息中的数组长度
function elementSize($extraArray){
	if(count($extraArray)>0){
		return intval($extraArray[0]);
	}else{
		return 0;
	}
}
//处理源代码中的多条声明语句
function handleStatementList($stack,$coder,$listIndex,$statementIndex){
	
	$t1= $this->topItem($stack,$statementIndex);	
	if($listIndex>0){
		$t2= $this->topItem($stack,$listIndex);
		//将元素名所在项的附加信息数组找出来
		$extraArray=$t2[TokenExtraIndex];
	}else{ //$listIndex=0表示,statementList是上级节点,附加信息数组应该是空数组
		$extraArray=[];
	}
	//将statement中的附加信息添加到statementList的附加信息中去
	$extraArray[]=$t1[TokenExtraIndex];
	return ['#',$extraArray];
}
//处理源代码中的一条声明语句
function handleStatement($stack,$coder,$typeItemIndex,$idenItemIndex){
	
	$t1= $this->topItem($stack,$typeItemIndex);
	$elementType = 	$t1[TokenValueIndex];
	$t2= $this->topItem($stack,$idenItemIndex);
	$elementName = 	$t2[TokenValueIndex];
	
	//将元素名所在项的附加信息数组找出来
	$extraArray=$t2[TokenExtraIndex];
	$valLen =$extraArray[0];
	//附加信息中包含 元素名称,元素类型,数据长度
	return [$elementName,[$elementName,$elementType,$valLen]];
}
//语法规则处理函数名由规则右边部分与规则左边部分拼接而成
//语法规则定义的先后决定了归约时匹配的顺序,要根据实际的语法安排
//如果不熟悉语法,随意调整语法规则的先后次序将有可能导致语法错误
// struct list  {{{
function _structList_0_structList_struct($stack,$coder){
	$coder->pushBlockTail();
	return ['#',[]];
	
	
}
function _structList_0_struct($stack,$coder){	
	$coder->pushBlockTail();
	return ['#',[]];
}
// struct list  }}}
// struct   {{{
function _struct_0_structName_blockStatement_semi($stack,$coder){
	$t1= $this->topItem($stack,3);
	$structName = 	$t1[TokenValueIndex];
	$t2= $this->topItem($stack,2);
	$extraArray=$t2[TokenExtraIndex];
	
	return [$structName,$extraArray];
}
// struct   }}}
// struct name     {{{
function _structName_0_strukey_iden($stack,$coder){  
	$t1= $this->topItem($stack,1);
	$structName = 	$t1[TokenValueIndex];
	$coder->pushBlockHeader($structName);	
	return $this->pass($stack,1);
}
// struct name     }}}
// blockStatement    {{{
function _blockStatement_0_lcb_statementList_rcb($stack,$coder){
	return $this->pass($stack,2);
}
// blockStatement    }}}
// statement list  {{{
function _statementList_0_statementList_statement($stack,$coder){
	
	return $this->handleStatementList($stack,$coder,2,1);
}
function _statementList_0_statement($stack,$coder){
	//此处0表示statementList是上一级节点,要做特殊处理
	return $this->handleStatementList($stack,$coder,0,1);	
}
// statement list  }}}
// statement       {{{
function _statement_0_double_term_semi($stack,$coder){	
	$t1= $this->topItem($stack,2);
	$elementName = 	$t1[TokenValueIndex];
	$parseFuncName = 'parseDouble';	
	$coder->pushBlockBody($parseFuncName,$elementName);	
	
	return $this->handleStatement($stack,$coder,3,2);
}
function _statement_0_float_term_semi($stack,$coder){	
	$t1= $this->topItem($stack,2);
	$elementName = 	$t1[TokenValueIndex];
	$parseFuncName = 'parseFloat';	
	$coder->pushBlockBody($parseFuncName,$elementName);	
	
	return $this->handleStatement($stack,$coder,3,2);
}
function _statement_0_char_term_semi($stack,$coder){
	$t1= $this->topItem($stack,2);
	$elementName = 	$t1[TokenValueIndex];
	$size = $this->elementSize($t1[TokenExtraIndex]);
	$parseFuncName = 'parseFixStr';	
	$coder->pushBlockBody($parseFuncName,$elementName,$size);		
	
	return $this->handleStatement($stack,$coder,3,2);
}
function _statement_0_int_term_semi($stack,$coder){	
	$t1= $this->topItem($stack,2);
	$elementName = 	$t1[TokenValueIndex];
	$parseFuncName = 'parseInt';	
	$coder->pushBlockBody($parseFuncName,$elementName,4);	
	
	return $this->handleStatement($stack,$coder,3,2);
}
// statement        }}}
//   term    {{{
function _term_0_term_array($stack,$coder){
	$t1= $this->topItem($stack,1);
	$valLen = 	$t1[TokenValueIndex];
	//将数据长度放入附加信息 
	$t2= $this->topItem($stack,2);
	return [$t2[TokenValueIndex],[$valLen]];
	
	
}
function _term_0_iden($stack,$coder){
	$t1= $this->topItem($stack,1);
	//未指定数据长度时将长度值设为0 
	$valLen = 	'0';	
	$t2= $this->topItem($stack,2);
	return [$t1[TokenValueIndex],[$valLen]];	
}
//   term     }}}
// array 	  {{{
function _array_0_lb_num_rb($stack,$coder){
	return $this->pass($stack,2);	
}
// array 	   }}}
} // end of class

以下是基本数据类型、整数、字符串的解析函数(注意,用于实验,尚未涵盖所有基本数据类型)

<?php
//用于解析基本数据类型的内置函数
namespace Ados;
//解析一个字节,得到无符号整数值
function parseByte($context,$size = 0){
	$pos=$context['pos'];
	$data = substr($context['data'], $pos);
	if(strlen($data)>0){
		$raw = substr($data, 0,1);
		$value = unpack("C1",$raw)[1];
		return ['value'=>$value,'size'=>1,'error'=>0,'msg'=>'ok'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'not enough for parseByte'];
	}
	
}
//解析一个有符号整数
function parseInt($context,$size = 4){
	if(!($size == 2 or $size == 4 or $size == 8 )){
		return ['value'=>False,'size'=>0,'error'=>2,'msg'=>'not a valid size'];
	}
	if($size == 2) $format = "s1";
	if($size == 4) $format = "l1";
	if($size == 8) $format = "q1";
	$pos=$context['pos'];
	$data = substr($context['data'], $pos);
	if(strlen($data)>=$size){
		$raw = substr($data, 0,$size);
		$value = unpack($format,$raw)[1];
		return ['value'=>$value,'size'=>$size,'error'=>0,'msg'=>'ok'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'no data for parseInt'];
	}
	
}
//解析一个大端无符号整数
function parseBUInt($context,$size = 4){
	if(!($size == 2 or $size == 4 or $size == 8 )){
		return ['value'=>False,'size'=>0,'error'=>2,'msg'=>'not a valid size'];
	}
	if($size == 2) $format = "n1";
	if($size == 4) $format = "N1";
	if($size == 8) $format = "J1";
	$pos=$context['pos'];
	$data = substr($context['data'], $pos);
	if(strlen($data)>=$size){
		$raw = substr($data, 0,$size);
		$value = unpack($format,$raw)[1];
		return ['value'=>$value,'size'=>$size,'error'=>0,'msg'=>'ok'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'no data for parseBUInt'];
	}
	
}
//解析一个小端无符号整数
function parseLUInt($context,$size = 4){
	if(!($size == 2 or $size == 4 or $size == 8 )){
		return ['value'=>False,'size'=>0,'error'=>2,'msg'=>'not a valid size'];
	}
	if($size == 2) $format = "v1";
	if($size == 4) $format = "NL";
	if($size == 8) $format = "P1";
	$pos=$context['pos'];
	$data = substr($context['data'], $pos);
	if(strlen($data)>=$size){
		$raw = substr($data, 0,$size);
		$value = unpack($format,$raw)[1];
		return ['value'=>$value,'size'=>$size,'error'=>0,'msg'=>'ok'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'no data for parseLUInt'];
	}
	
}
//解析一个null结束的字符串
function parseString($context,$size=0){
	$pos=$context['pos'];
	$data = substr($context['data'], $pos);
	$p=0;
	$raw = substr($data, $p,1);
	$byte = unpack("C1",$raw)[1];	
	$result ='';
	while($byte){
		$result.=$raw;
		$p++;
		if($p>=strlen($data)){
			return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'not find null end for parseString'];
		}
		$raw = substr($data, $p,1);
		$byte = unpack("C1",$raw)[1];		
	}
	return ['value'=>$result,'size'=>$p,'error'=>0,'msg'=>'ok'];
}
//解析一个定长字符串
function parseFixStr($context,$size=0){
	$pos=$context['pos'];
	$data = substr($context['data'], $pos);
	//var_dump($data);	
	if(strlen($data)>=$size){
		$result ='';
		for($i=0;$i<$size;$i++){
			$raw = substr($data, $i,1);
			$value = unpack("a1",$raw)[1];
			$result.=$value;			
		}
		return ['value'=>$result,'size'=>$size,'error'=>0,'msg'=>'ok']; 
	}
	return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'not enough for parseFixedString'];
}

php 结构体

下面是模板替换的工作函数

<?php
//用于进行模板替换的工作函数
namespace Ados;
defined('__STRUCT_PARSE_TEMP__') or define('__STRUCT_PARSE_TEMP__', './');
define('_blockParsTempFile_',__STRUCT_PARSE_TEMP__.'parseFunc.template.php');
//取出两个记号串之间的内容
function strBetweenToke($src,$toke1,$toke2){
	$p1 = strpos($src,$toke1);
	$p2 = strpos($src,$toke2);
	return substr($src,$p1+strlen($toke1),$p2-$p1-strlen($toke1));
}
//取得块分析函数的header
function blockHeaderStr(){
	$src = file_get_contents(_blockParsTempFile_);
	return strBetweenToke($src,'/*blockHeader{{*/','/*blockHeader}}*/');
}
//取得块分析函数的body
function blockBodyStr(){
	$src = file_get_contents(_blockParsTempFile_);
	return strBetweenToke($src,'/*blockBody{{*/','/*blockBody}}*/');
}
//取得块分析函数的tail
function blockTailStr(){
	$src = file_get_contents(_blockParsTempFile_);
	return strBetweenToke($src,'/*blockTail{{*/','/*blockTail}}*/');
}
define('_blockHeaderStr_',blockHeaderStr());
define('_blockBodyStr_',blockBodyStr());
define('_blockTailStr_',blockTailStr());
function makeBlockHeader($blockName){
	return str_replace('parseBlock_Temp', 'parse'.$blockName, _blockHeaderStr_);
}
function makeblockBody($parseFuncName,$filedName='',$filedSize=0){
	$tmp = str_replace('parseByte', $parseFuncName, _blockBodyStr_);
	$tmp = str_replace('$filedName', $filedName, $tmp);
	$tmp = str_replace('$filedSize', $filedSize, $tmp);
	return $tmp;
}
function makeblockTail(){
	return _blockTailStr_ ;
}
/*
echo blockHeaderStr();
echo blockBodyStr();
echo blockTailStr();
echo makeBlockHeader('Test1');
echo makeblockBody('parseInt');
echo makeblockBody('parseStr');
echo makeblockTail();
*/

准备就绪后,就可以实现句子模式引导的编码器来生成最终的解析函数。

下一步是一个简单的编码器,它生成可用于在解析结构定义时解析二进制数据的脚本代码。

<?php
/*!
 * structwkr编码器,
 *
 * 45022300@qq.com
 * Version 0.9.0
 *
 * Copyright 2019, Zhu Hui
 * Released under the MIT license
 */
namespace Ados;
require_once __SCRIPTCORE__.'coder/base_coder.php';
require_once __STRUCT_PARSE_TEMP__.'templateReplaceFuncs.php';
class StructwkrCoder extends BaseCoder{
	public function __construct($engine)
	{
		if($engine){ 
			$this->engine = $engine;
		}else{ 
			exit('the engine is not valid in StructwkrCoder construct.');
		}
	}
	//编译得到的最终结果
	public function codeLines(){
		if(count($this->codeLines)<1){
			return '';
		}
		$script='';		
		for ($i=0;$i< count($this->codeLines);$i+=1) {
			$script.=$this->codeLines[$i];
		}	
		return $script;
	}	
	//输出编译后的结果
	public function printCodeLines(){
		echo $this->codeLines();	
	}
	//添加一个块解析函数头
	public function pushBlockHeader($structName){
		$structName=ucfirst($structName);
		$content = makeBlockHeader($structName);
		array_push($this->codeLines, $content);
		$lineIndex=$this->lineIndex;
		$this->lineIndex+=1;
		return $lineIndex;		
	}
	//添加一个块解析函数体
	public function pushBlockBody($parseFuncName,$filedName='',$filedSize=0){
		$content = makeblockBody($parseFuncName,$filedName,$filedSize);
		array_push($this->codeLines, $content);
		$lineIndex=$this->lineIndex;
		$this->lineIndex+=1;
		return $lineIndex;		
	}
	//添加一个块解析函数尾
	public function pushBlockTail(){
		$content = makeblockTail();
		array_push($this->codeLines, $content);
		$lineIndex=$this->lineIndex;
		$this->lineIndex+=1;
		return $lineIndex;		
	}
}

自动生成解析脚本文件

编译C语言结构,最终得到一系列可用于解析二进制数据的函数。 例如,在上述结构体定义文件中定义了两个结构体。

php 结构体

struct student
{
  char name[2];
  int num;
  int age;
  char addr[3];
};
struct teacher
{
  char name[2];
  int num;  
  char addr[3];
};

然后就会生成这两个结构体对应的解析函数parseStudent和parseTeacher。

以下是手动生成的测试脚本

<?php
namespace Ados;
//加载常量定义文件
require_once 'const.php';
require_once __STRUCT_PARSE_TEMP__.'templateBuidinFuncs.php';
$context['pos']=0;
$context['data']="x41x42x01x00x00x00x02x00x00x00x41x42x43x41x42x01x00x00x00x41x42x43";
$expRes = parseStudent($context);
$context['pos']+=$expRes['size'];
print_r($expRes);
$expRes = parseTeacher($context);
$context['pos']+=$expRes['size'];
print_r($expRes);
function parseStudent($context,$size=0){
	$valueArray=[];
	$totalSize = 0;
	$expRes = parseFixStr($context,2);
	if($expRes['error']==0){
		$filed = 'name';
		if($filed){
			$valueArray[$filed]=$expRes['value'];
		}else{
			$valueArray[]=$expRes['value'];
		}		
		$context['pos']+=$expRes['size'];
		$totalSize+= $expRes['size'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
	}
	$expRes = parseInt($context,4);
	if($expRes['error']==0){
		$filed = 'num';
		if($filed){
			$valueArray[$filed]=$expRes['value'];
		}else{
			$valueArray[]=$expRes['value'];
		}		
		$context['pos']+=$expRes['size'];
		$totalSize+= $expRes['size'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
	}
	$expRes = parseInt($context,4);
	if($expRes['error']==0){
		$filed = 'age';
		if($filed){
			$valueArray[$filed]=$expRes['value'];
		}else{
			$valueArray[]=$expRes['value'];
		}		
		$context['pos']+=$expRes['size'];
		$totalSize+= $expRes['size'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
	}
	$expRes = parseFixStr($context,3);
	if($expRes['error']==0){
		$filed = 'addr';
		if($filed){
			$valueArray[$filed]=$expRes['value'];
		}else{
			$valueArray[]=$expRes['value'];
		}		
		$context['pos']+=$expRes['size'];
		$totalSize+= $expRes['size'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
	}
	return ['value'=>$valueArray,'size'=>$totalSize,'error'=>0,'msg'=>'ok'];
}
function parseTeacher($context,$size=0){
	$valueArray=[];
	$totalSize = 0;
	$expRes = parseFixStr($context,2);
	if($expRes['error']==0){
		$filed = 'name';
		if($filed){
			$valueArray[$filed]=$expRes['value'];
		}else{
			$valueArray[]=$expRes['value'];
		}		
		$context['pos']+=$expRes['size'];
		$totalSize+= $expRes['size'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
	}
	$expRes = parseInt($context,4);
	if($expRes['error']==0){
		$filed = 'num';
		if($filed){
			$valueArray[$filed]=$expRes['value'];
		}else{
			$valueArray[]=$expRes['value'];
		}		
		$context['pos']+=$expRes['size'];
		$totalSize+= $expRes['size'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
	}
	$expRes = parseFixStr($context,3);
	if($expRes['error']==0){
		$filed = 'addr';
		if($filed){
			$valueArray[$filed]=$expRes['value'];
		}else{
			$valueArray[]=$expRes['value'];
		}		
		$context['pos']+=$expRes['size'];
		$totalSize+= $expRes['size'];
	}else{
		return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
	}
	return ['value'=>$valueArray,'size'=>$totalSize,'error'=>0,'msg'=>'ok'];
}

运行此测试脚本并得到以下结果:


Array
(
    [value] => Array
        (
            [name] => AB
            [num] => 1
            [age] => 2
            [addr] => ABC
        )
    [size] => 13
    [error] => 0
    [msg] => ok
)
Array
(
    [value] => Array
        (
            [name] => AB
            [num] => 1
            [addr] => ABC
        )
    [size] => 9
    [error] => 0
    [msg] => ok
)

好的! 任务的第一步已经完成。 接下来考虑用条件判断来实现结构分析。