rule = $rule;
}
static function getList(string $path) {
$ret = [];
$dir = new DirectoryIterator($path);
foreach ($dir as $fileinfo) {
if($fileinfo->getExtension() === 'lst') {
$fn = $fileinfo->getFilename();
$fo = $fileinfo->openFile('r');
$short = trim($fo->fgets(),"# \r\n");
$long = trim($fo->fgets(),"# \r\n");
$ret[substr($fn,0,-4)] = ['short'=>$short, 'long'=>$long];
}
}
uasort($ret, function($a,$b) { return $a['short']<=>$b['short']; });
return $ret;
}
}
class Rule {
public $title = '';
public static $symbol_table = []; // This is the symbol table populated in by variables set in msgtrue/msgfalse as they trigger
static function make (string $line, array $varstrs) {
@[$rulestr,$msgstr] = preg_split('@"[^"]*"(*SKIP)(*F)|\s+@', $line);
@[$msgtrue,$msgfalse] = preg_split('@"[^"]*"(*SKIP)(*F)|:@', $msgstr);
if(!preg_match('@"[^"]*"(*SKIP)(*F)|(==|!=|=|<|>|~=)@', $rulestr, $match)) {
throw new \Exception("Syntax error on rule: $rulestr");
}
$op = $match[1];
[$left,$right] = preg_split('@"[^"]*"(*SKIP)(*F)|(==|!=|=|<|>|~=)@', $rulestr);
$vars = [];
foreach($varstrs as $var) {
if(strstr($var, '=') === false) {
throw new \Exception("Syntax error on variable assign $var");
}
[$name,$val] = explode('=', $var, 2);
$vars['{'.$name.'}'] = $val;
}
if($left == 'count') {
return new CountRule($op, $right, $msgtrue, $msgfalse, $vars);
}
if($left[0] == '[') {
return new AttrValueRule($op, $left, $right, $msgtrue, $msgfalse, $vars);
}
return new SettingRule($op, $left, $right, $msgtrue, $msgfalse, $vars);
}
static function valStr($val, $node=null, $raw_binary = false) {
if($val === true) $ret = "Yes";
else if($val === false) $ret = "No";
else if($val === "") $ret = "_-blank-_";
else if($node && $node instanceof CFPropertyList\CFData) {
if($raw_binary) $ret = $val;
else $ret = bin2hex((string)$val);
} else $ret = $val;
return $ret;
}
static function valCast($val, $node=null, $raw_binary=false) {
if($node instanceof CFPropertyList\CFString) {
return (string)$val;
} else if($node instanceof CFPropertyList\CFNumber) {
return (int)$val;
} else if($node instanceof CFPropertyList\CFData) {
if(!$raw_binary) return bin2hex($val);
else return $val;
}
return $val;
}
static function setVars(?string $str):?string {
if(empty($str)) return $str;
$tmp = trim($str, '"');
if(empty($tmp)) return $str;
if($tmp[0] == '$') {
if(preg_match("@^(\\\$\w+)=(.*(?:'[^']*'(*SKIP)(*F)|;))(.*)\$@", $tmp, $matches)) {
static::$symbol_table['{'.$matches[1].'}'] = trim(trim($matches[2],';'),"'");
$str = '"'.$matches[3].'"';
}
}
return $str;
}
static function repVars(?string $str, array $vars):?string {
if(empty($str)) return $str;
$str = strtr($str, $vars + static::$symbol_table);
// Allow for vars to contain vars, so do it again
$str = strtr($str, $vars + static::$symbol_table);
return $str;
}
}
class CountRule extends Rule {
private $op;
private $right;
private $msgtrue;
private $msgfalse;
private $vars;
function __construct(string $op, string $right, ?string $msgtrue, ?string $msgfalse, array $vars) {
$this->op = $op;
$this->right = $right;
$this->msgtrue = $msgtrue;
$this->msgfalse = $msgfalse;
$this->vars = $vars;
$this->title = '';
}
public function exec($arg, $unused_node, $unused_check_missing=true):array {
$count = count($arg);
$vars = $this->vars + [ '{$count}' => $count ];
$msgtrue = Rule::repVars((string)$this->msgtrue, $vars);
$msgfalse = Rule::repVars((string)$this->msgfalse, $vars);
switch($this->op) {
case '==': $ret = ($count==$this->right) ? [$msgtrue]:[$msgfalse]; break;
case '!=': $ret = ($count!=$this->right) ? [$msgtrue]:[$msgfalse]; break;
case '<': $ret = ($count<$this->right) ? [$msgtrue]:[$msgfalse]; break;
case '>': $ret = ($count>$this->right) ? [$msgtrue]:[$msgfalse]; break;
default:
throw new \Exception("Invalid operator in count expression");
}
return [Rule::setVars($ret[0])];
}
}
class AttrValueRule extends Rule {
private $op;
private $left;
private $right;
private $msgtrue;
private $msgfalse;
private $vars = [];
function __construct(string $op, string $left, string $right, ?string $msgtrue, ?string $msgfalse, array $vars) {
$this->op = $op;
$this->left = substr($left,1,-1);
$this->msgtrue = $msgtrue;
$this->msgfalse = $msgfalse;
$this->vars = $vars;
$this->title = '';
if(strtolower($right)=='yes') $this->right = true;
else if(strtolower($right)=='no') $this->right = false;
else $this->right = trim($right,'"');
}
public function exec($arg, $node, $unused_check_missing=true):array {
$vars = $this->vars;
$ret = [];
$lop = null;
// Special case, * matches all remaining attributes that haven't matched a previous rule
if($this->op == '==' && $this->right === '*') {
foreach($arg as $key=>$v) {
if(!is_array($v)) continue;
foreach($v as $kk=>$vv) $vars = array_merge($vars, [ '{$'.$kk.'}' => Rule::valStr($vv) ]);
$msgtrue = Rule::repVars(Rule::setVars((string)$this->msgtrue), $vars);
$ret[$key+1] = $msgtrue;
}
return $ret;
}
$lookfor = [$this->right];
if(preg_match('@"[^"]*"(*SKIP)(*F)|\|@', $this->right)) {
$lookfor = preg_split('@"[^"]*"(*SKIP)(*F)|\|@', $this->right);
$lop = '|';
}
if(preg_match('@"[^"]*"(*SKIP)(*F)|&@', $this->right)) {
if($lop) {
throw new \Exception("Mixing | and & in a single expression is currently not supported");
}
$lookfor = preg_split('@"[^"]*"(*SKIP)(*F)|&@', $this->right);
$lop = '&';
}
$found = false;
$found_count = 0;
$fvs = [];
foreach($arg as $key=>$v) {
if(!is_array($v)) continue;
foreach($lookfor as $look) {
if(is_string($look)) $look = Rule::repVars($look, $vars);
if($this->op == '~=') {
if(array_key_exists($this->left, $v) && preg_match('@'.$look.'@', $v[$this->left])) { $found_count++; $fvs[$key] = $v; }
} else {
if(!$lop || $lop=='|') {
if(array_key_exists($this->left, $v) && $v[$this->left] === $look) { $found_count++; $fvs[$key] = $v; }
} else if($lop=='&') {
if(array_key_exists($this->left, $v) && $v[$this->left] === $look) { $found_count++; $fvs[$key] = $v; }
}
}
}
}
if($lop=='&' && $found_count >= count($lookfor)) $found = true;
else if($lop!='&' && $found_count) $found = true;
if(!$found) {
$ret = [Rule::setVars($this->msgfalse)];
} else {
foreach($fvs as $fkey=>$fv) {
foreach($fv as $kk=>$vv) $vars = array_merge($vars, [ '{$'.$kk.'}' => Rule::valStr($vv) ]);
if($this->op == '!=') {
$msgfalse = Rule::repVars((string)$this->msgfalse, $vars);
$ret[$fkey+1] = Rule::setVars($msgfalse); // return with index to remove match from list
} else {
$msgtrue = Rule::repVars((string)$this->msgtrue, $vars);
$ret[$fkey+1] = Rule::setVars($msgtrue); // return with index to remove match from list
}
}
}
return $ret;
}
}
class SettingRule extends Rule {
private $op = "";
private $left = "";
private $right = "";
private $msgtrue = "";
private $msgfalse = "";
private $vars = [];
function __construct(string $op, string $left, string $right, ?string $msgtrue, ?string $msgfalse, array $vars) {
$this->op = $op;
$this->left = $left;
if($op != '=' && $op != '~=') $this->title = $left;
$this->msgtrue = $msgtrue;
$this->msgfalse = $msgfalse;
$this->vars = $vars;
if(strtolower($right)=='yes') $this->right = true;
else if(strtolower($right)=='no') $this->right = false;
else $this->right = trim($right,'"');
}
public function exec($arg, $node, $check_missing=true):array {
$vars = $this->vars;
$ret = [];
$msgtrue = $msgfalse = "";
foreach($arg as $key=>$val) {
if($this->op == '=' || $this->op == '~=') {
if($this->left === $key) {
// Populate local symbol table
$vars = $this->vars + [ '{$setting}' => $key, '{$value}' => Rule::valStr($val, $node->{$key}), '{@value}' => Rule::valStr($val, $node->{$key}, true) ];
if(!empty($this->msgtrue)) {
$msgtrue = Rule::repVars($this->msgtrue, $vars);
}
if(!empty($this->msgfalse)) {
$msgfalse = Rule::repVars($this->msgfalse, $vars);
}
if(is_string($this->right)) $right = Rule::repVars($this->right, $vars);
else $right = $this->right;
$cmp = Rule::valCast($val, $node->{$key});
// Apply condition
if(($this->op == '=' && $right == $cmp) || ($this->op == '~=' && preg_match('@'.$right.'@', $cmp))) {
$ret[$key] = empty($msgtrue) ? "\n$key = ".Rule::valStr($val, $node->{$key})."" : Rule::setVars($msgtrue);
} else {
if(empty($msgfalse)) {
$ret[$key] = "-$key = ".Rule::valStr($val, $node->{$key})." but should normally be ".Rule::valStr($right, $node->{$key})."";
} else {
$ret[$key] = Rule::setVars($msgfalse);
}
}
}
} else if($this->left === $key) {
if(!is_array($val)) {
return ["!$key is missing"];
}
//
// Special case, regex across all remaining attributes
if($this->op == '==' && $this->right[0] === '~') {
foreach($val as $k=>$v) {
$vars = $this->vars + [ '{$setting}' => $key, '{$value}' => Rule::valStr($v, $node->{$k}), '{@value}' => Rule::valStr($v, $node->{$k}, true) ];
$msgtrue = Rule::repVars($this->msgtrue, $vars);
if(preg_match('@'.substr($this->right,1).'@', $v)) {
$ret[":$key:$k"] = Rule::setvars($msgtrue);
}
}
continue;
}
// Special case, * matches all remaining attributes that haven't matched a previous rule
if($this->op == '==' && $this->right === '*') {
foreach($val as $k=>$v) {
$vars = $this->vars + [ '{$setting}' => $key, '{$value}' => Rule::valStr($v, $node->{$k}), '{@value}' => Rule::valStr($v, $node->{$k}, true) ];
$msgtrue = Rule::repVars($this->msgtrue, $vars);
$ret[":$key:$k"] = Rule::setvars($msgtrue);
}
return $ret;
}
$found = false;
$found_count = 0;
$fv = [];
$lop = null;
$lookfor = [$this->right];
if(preg_match('@"[^"]*"(*SKIP)(*F)|\|@', $this->right)) {
$lookfor = preg_split('@"[^"]*"(*SKIP)(*F)|\|@', $this->right);
$lop = '|';
}
if(preg_match('@"[^"]*"(*SKIP)(*F)|&@', $this->right)) {
if($lop) {
throw new \Exception("Mixing | and & in a single expression is currently not supported");
}
$lookfor = preg_split('@"[^"]*"(*SKIP)(*F)|&@', $this->right);
$lop = '&';
}
$found = false;
$found_count = $fkey = 0;
$fv = '';
foreach($val as $k=>$v) {
$cmp = Rule::valCast($v, $node->{$k});
foreach($lookfor as $look) {
if(is_string($look)) $look = Rule::repVars($look, $vars);
if(!$lop || $lop=='|') {
if($cmp === $look) { $found_count++; $fkey = $k; $fv = $cmp; }
} else if($lop=='&') {
if($cmp === $look) { $found_count++; $fkey = $k; $fv = $cmp; }
}
}
}
if($lop=='&' && $found_count >= count($lookfor)) $found = true;
else if((!$lop || $lop=='|') && $found_count) $found = true;
$vars = $this->vars + [ '{$setting}' => $key, '{$value}' => $fv ];
$msgtrue = Rule::repVars($this->msgtrue, $vars);
$msgfalse = Rule::repVars($this->msgfalse, $vars);
if($this->op == '!=') {
if(!$found) $ret = [Rule::setVars($msgtrue)];
else $ret[":$key:$fkey"] = Rule::setVars($msgfalse); // return with full encoded index to remove match from list
} else {
if($found) $ret[":$key:$fkey"] = Rule::setVars($msgtrue); // return with full encoded index to remove match from list
else $ret = [Rule::setVars($msgfalse)];
}
}
}
if(empty($this->msgtrue) && $check_missing && ($this->op == '=' || $this->op == '~=') && empty($ret)) {
$right = Rule::valStr($this->right);
$vars = $this->vars + [ '{$setting}' => $this->left, '{$value}' => $right ];
// Overriding user-supplied msgfalse in this missing setting case
$msgfalse = "-{$this->left} is missing. Normally set to {$right}";
$ret = [$msgfalse];
}
return $ret;
}
}