/*
* Process Hacker -
* struct definition file parser
*
* Copyright (C) 2008 wj32
*
* This file is part of Process Hacker.
*
* Process Hacker is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Process Hacker is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Process Hacker. If not, see .
*/
using System;
using System.Collections.Generic;
using System.Text;
using ProcessHacker.Common;
using ProcessHacker.Native;
namespace ProcessHacker.Structs
{
public class ParserException : Exception
{
public ParserException(string fileName, int line, string message) :
base((new System.IO.FileInfo(fileName)).Name + ": " + line.ToString() + ": " + message) { }
}
public class StructParser
{
private Dictionary _structs;
private string _fileName = "";
private int _lineNumber = 1;
private Dictionary _typeDefs = new Dictionary();
private Dictionary _defines = new Dictionary();
private bool _eatResult = false;
public Dictionary Structs
{
get { return _structs; }
}
public StructParser(Dictionary structs)
{
_structs = structs;
// Core types
foreach (string s in Enum.GetNames(typeof(FieldType)))
if (s != "Pointer")
_typeDefs.Add(s.ToLowerInvariant(), (FieldType)Enum.Parse(typeof(FieldType), s));
// Core defines
if (OSVersion.Architecture == OSArch.I386)
_defines.Add("_X86_", null);
else if (OSVersion.Architecture == OSArch.Amd64)
_defines.Add("_AMD64_", null);
}
private FieldType GetType(string typeName)
{
if (_typeDefs.ContainsKey(typeName))
return _typeDefs[typeName];
else
throw new ParserException(_fileName, _lineNumber, "Unknown identifier '" + typeName + "' (type name)");
}
private bool IsTypePointer(FieldType type)
{
return (type & FieldType.Pointer) != 0;
}
private string Preprocess(string text)
{
List lines = new List(text.Split('\n'));
Stack includeStack = new Stack();
bool include = true;
for (int i = 0; i < lines.Count; i++)
{
string line = lines[i].Trim(' ', '\t', '\r');
if (line.StartsWith("#if"))
{
string conditionText = line.Remove(0, "#if".Length).Trim(' ', '\t', '\r');
includeStack.Push(include);
include = _defines.ContainsKey(conditionText);
}
else if (line.StartsWith("#elseif"))
{
if (!include)
{
string conditionText = line.Remove(0, "#elseif".Length).Trim(' ', '\t', '\r');
include = _defines.ContainsKey(conditionText);
}
else
{
include = false;
}
}
else if (line.StartsWith("#else"))
{
include = !include;
}
else if (line.StartsWith("#define"))
{
string conditionText = line.Remove(0, "#define".Length).Trim(' ', '\t', '\r');
if (!_defines.ContainsKey(conditionText))
{
_defines.Add(conditionText, null);
}
}
else if (line.StartsWith("#endif"))
{
include = includeStack.Pop();
}
if (!include || line.StartsWith("#"))
{
lines[i] = "";
}
}
return string.Join("\n", lines.ToArray());
}
public void Parse(string fileName)
{
List defs = new List();
int i = 0;
string text = System.IO.File.ReadAllText(fileName);
_lineNumber = 1;
_fileName = fileName;
text = Preprocess(text);
while (true)
{
if (EatWhitespace(text, ref i)) break;
string modeName = EatId(text, ref i);
if (modeName == "")
throw new ParserException(_fileName, _lineNumber, "Expected keyword");
if (modeName == "typedef")
{
this.ParseTypeDef(text, ref i);
}
else if (modeName == "struct")
{
this.ParseStructDef(text, ref i);
}
else if (modeName == "include")
{
_eatResult = EatWhitespace(text, ref i);
string includeFile = EatQuotedString(text, ref i);
if (_eatResult || includeFile == "")
throw new ParserException(_fileName, _lineNumber, "String expected (file name)");
_eatResult = EatWhitespace(text, ref i);
string endSemicolon = EatSymbol(text, ref i);
if (_eatResult || endSemicolon != ";")
throw new ParserException(_fileName, _lineNumber, "Expected ';'");
System.IO.FileInfo info = new System.IO.FileInfo(_fileName);
// if the filename contains ':', use the absolute path. otherwise, append it to the
// current filename's directory
string oldFileName = _fileName;
int oldLine = _lineNumber;
try
{
if (includeFile.Contains(":"))
this.Parse(includeFile);
else
this.Parse(info.DirectoryName + "\\" + includeFile);
}
catch (System.IO.FileNotFoundException)
{
throw new ParserException(_fileName, _lineNumber, "Could not find the file '" + includeFile + "'");
}
_fileName = oldFileName;
_lineNumber = oldLine;
}
else
{
throw new ParserException(_fileName, _lineNumber, "Expected keyword");
}
}
}
private void ParseTypeDef(string text, ref int i)
{
_eatResult = EatWhitespace(text, ref i);
string existingType = EatId(text, ref i);
if (_eatResult || existingType == "")
throw new ParserException(_fileName, _lineNumber, "Expected identifier (type name)");
if (!_typeDefs.ContainsKey(existingType))
throw new ParserException(_fileName, _lineNumber, "Unknown identifier '" + existingType + "' (type name)");
// check for asterisk (pointer)
_eatResult = EatWhitespace(text, ref i);
string asterisk = EatSymbol(text, ref i);
if (asterisk != "*" && asterisk.Length > 0)
throw new ParserException(_fileName, _lineNumber, "Unexpected '" + asterisk + "'");
_eatResult = EatWhitespace(text, ref i);
string newType = EatId(text, ref i);
if (_eatResult || existingType == "")
throw new ParserException(_fileName, _lineNumber, "Expected identifier (new type name)");
if (_typeDefs.ContainsKey(newType))
throw new ParserException(_fileName, _lineNumber, "Type name '" + newType + "' already used");
if (this.IsTypePointer(this.GetType(existingType)) && asterisk == "*")
throw new ParserException(_fileName, _lineNumber, "Invalid '*'; type '" + existingType + "' is already a pointer");
_typeDefs.Add(newType, this.GetType(existingType) | (asterisk == "*" ? FieldType.Pointer : 0));
_eatResult = EatWhitespace(text, ref i);
string endSemicolon = EatSymbol(text, ref i);
if (_eatResult || endSemicolon != ";")
throw new ParserException(_fileName, _lineNumber, "Expected ';'");
}
private void ParseStructDef(string text, ref int i)
{
StructDef def = new StructDef();
_eatResult = EatWhitespace(text, ref i);
string structName = EatId(text, ref i);
if (_eatResult || structName == "")
throw new ParserException(_fileName, _lineNumber, "Expected identifier (struct name)");
if (_structs.ContainsKey(structName))
throw new ParserException(_fileName, _lineNumber, "Struct name '" + structName + "' already used");
// add it first so that structs can be self-referential
_structs.Add(structName, null);
// {
_eatResult = EatWhitespace(text, ref i);
string openingBrace = EatSymbol(text, ref i);
if (_eatResult || openingBrace != "{")
throw new ParserException(_fileName, _lineNumber, "Expected '{'");
while (true)
{
// }
_eatResult = EatWhitespace(text, ref i);
string endBrace = EatSymbol(text, ref i);
if (_eatResult)
throw new ParserException(_fileName, _lineNumber, "Expected type name or '}'");
if (endBrace == "}")
break;
if (endBrace.Length > 0)
throw new ParserException(_fileName, _lineNumber, "Unexpected '" + endBrace + "'");
// TYPE
_eatResult = EatWhitespace(text, ref i);
string typeName = EatId(text, ref i);
if (_eatResult || typeName == "")
throw new ParserException(_fileName, _lineNumber, "Expected type name");
FieldType type;
if (_typeDefs.ContainsKey(typeName))
{
type = this.GetType(typeName);
}
else
{
type = FieldType.Struct;
if (!_structs.ContainsKey(typeName))
throw new ParserException(_fileName, _lineNumber, "Unknown identifier '" + typeName + "' (type or struct name)");
}
// type, without the pointer or array flag
FieldType justType = type;
// TYPE*
// optional asterisk (pointer)
_eatResult = EatWhitespace(text, ref i);
if (EatSymbol(text, ref i) == "*")
{
if (this.IsTypePointer(type))
throw new ParserException(_fileName, _lineNumber, "Invalid '*'; type '" + typeName + "' is already a pointer");
type |= FieldType.Pointer;
}
// TYPE* FIELDNAME
_eatResult = EatWhitespace(text, ref i);
string fieldName = EatId(text, ref i);
if (_eatResult || fieldName == "")
throw new ParserException(_fileName, _lineNumber, "Expected identifier (struct field name)");
if (def.ContainsField(fieldName))
throw new ParserException(_fileName, _lineNumber, "Field name '" + fieldName + "' already used");
_eatResult = EatWhitespace(text, ref i);
string leftSqBracket = EatSymbol(text, ref i);
int varLength = 0;
if (leftSqBracket == "[")
{
_eatResult = EatWhitespace(text, ref i);
string fieldRefName = EatId(text, ref i);
string fieldSizeSpec = EatNumber(text, ref i);
if (fieldRefName != "")
{
if (!def.ContainsField(fieldRefName))
throw new ParserException(_fileName, _lineNumber, "Unknown identifier '" + fieldRefName + "' (field name)");
def.GetField(fieldRefName).SetsVarOn = fieldName;
// const add/multiply
int iSave = i;
_eatResult = EatWhitespace(text, ref i);
string plusOrMulOrDivSign = EatSymbol(text, ref i);
if (plusOrMulOrDivSign == "+")
{
def.GetField(fieldRefName).SetsVarOnAdd = EatParseInt(text, ref i);
}
else if (plusOrMulOrDivSign == "*")
{
def.GetField(fieldRefName).SetsVarOnMultiply = EatParseFloat(text, ref i);
int iSave2 = i;
_eatResult = EatWhitespace(text, ref i);
string plusSign = EatSymbol(text, ref i);
if (plusSign == "+")
def.GetField(fieldRefName).SetsVarOnAdd = EatParseInt(text, ref i);
else if (plusSign == "-")
def.GetField(fieldRefName).SetsVarOnAdd = -EatParseInt(text, ref i);
else
i = iSave2;
}
else if (plusOrMulOrDivSign == "/")
{
// here we just set SetsVarOnMultiply to 1 / value
def.GetField(fieldRefName).SetsVarOnMultiply = 1 / EatParseFloat(text, ref i);
int iSave2 = i;
_eatResult = EatWhitespace(text, ref i);
string plusSign = EatSymbol(text, ref i);
if (plusSign == "+")
def.GetField(fieldRefName).SetsVarOnAdd = EatParseInt(text, ref i);
else if (plusSign == "-")
def.GetField(fieldRefName).SetsVarOnAdd = -EatParseInt(text, ref i);
else
i = iSave2;
}
else
{
// that didn't work; restore the index
i = iSave;
}
}
else if (fieldSizeSpec != "")
{
try
{
varLength = (int)BaseConverter.ToNumberParse(fieldSizeSpec);
varLength = (int)BaseConverter.ToNumberParse(fieldSizeSpec);
}
catch
{
throw new ParserException(_fileName, _lineNumber, "Could not parse number '" + fieldSizeSpec + "'");
}
}
else
{
throw new ParserException(_fileName, _lineNumber, "Number or identifier expected (size specifier)");
}
// if it's not a string, it's an array
if (justType != FieldType.StringASCII && justType != FieldType.StringUTF16)
type |= FieldType.Array;
_eatResult = EatWhitespace(text, ref i);
string rightSqBracket = EatSymbol(text, ref i);
if (_eatResult || rightSqBracket != "]")
throw new ParserException(_fileName, _lineNumber, "Expected ']'");
// fix up the semicolon
_eatResult = EatWhitespace(text, ref i);
leftSqBracket = EatSymbol(text, ref i);
}
// TYPE* FIELDNAME;
string endSemicolon = leftSqBracket;
if (_eatResult || endSemicolon != ";")
throw new ParserException(_fileName, _lineNumber, "Expected ';'");
StructField field = new StructField(fieldName, type);
if (field.Type == FieldType.Struct)
field.StructName = typeName;
field.VarArrayLength = varLength;
field.VarLength = varLength;
def.AddField(field);
}
_structs[structName] = def;
}
private float EatParseFloat(string text, ref int i)
{
_eatResult = EatWhitespace(text, ref i);
string number = EatNumber(text, ref i);
if (_eatResult || number == "")
throw new ParserException(_fileName, _lineNumber, "Expected floating-point number");
try
{
return float.Parse(number);
}
catch
{
throw new ParserException(_fileName, _lineNumber, "Could not parse number '" + number + "'");
}
}
private int EatParseInt(string text, ref int i)
{
_eatResult = EatWhitespace(text, ref i);
string number = EatNumber(text, ref i);
if (_eatResult || number == "")
throw new ParserException(_fileName, _lineNumber, "Expected integer");
try
{
return (int)BaseConverter.ToNumberParse(number);
}
catch
{
throw new ParserException(_fileName, _lineNumber, "Could not parse number '" + number + "'");
}
}
// my idea of a tokenizer follows...
private bool EatWhitespace(string text, ref int i) // and comments
{
bool ranOut = true;
bool preComment = false; // '/'
bool inComment = false; // '*'
bool prePostComment = false; // '*'
while (i < text.Length)
{
if (inComment && text[i] == '*')
{
prePostComment = true;
i++;
continue;
}
else if (prePostComment && text[i] == '/')
{
prePostComment = false;
inComment = false;
i++;
continue;
}
else if (!inComment && text[i] == '/')
{
preComment = true;
i++;
continue;
}
else if (preComment)
{
if (text[i] == '*')
{
preComment = false;
inComment = true;
i++;
continue;
}
else
{
// it's a mistake, revert!
i -= 1;
break;
}
}
else
{
preComment = false;
prePostComment = false;
}
if (text[i] == '\n')
_lineNumber++;
if (!(text[i] == '\r' || text[i] == '\n' || text[i] == ' ' || text[i] == '\t') && !inComment)
{
ranOut = false;
break;
}
i++;
}
return ranOut;
}
private string EatQuotedString(string text, ref int i)
{
StringBuilder sb = new StringBuilder();
bool inEscape = false;
if (text[i] == '"')
{
i++;
}
else
return "";
while (i < text.Length)
{
if (text[i] == '\\')
{
inEscape = true;
i++;
continue;
}
else if (inEscape)
{
if (text[i] == '\\')
sb.Append('\\');
else if (text[i] == '"')
sb.Append('"');
else if (text[i] == '\'')
sb.Append('\'');
else if (text[i] == 'r')
sb.Append('\r');
else if (text[i] == 'n')
sb.Append('\n');
else if (text[i] == 't')
sb.Append('\t');
else
throw new ParserException(_fileName, _lineNumber, "Unrecognized escape sequence '\\" + text[i] + "'");
i++;
inEscape = false;
continue;
}
else if (text[i] == '"')
{
i++;
break;
}
sb.Append(text[i]);
i++;
}
return sb.ToString();
}
private string EatId(string text, ref int i)
{
StringBuilder sb = new StringBuilder();
while (i < text.Length)
{
// identifiers can't start with a number-
if (sb.Length == 0)
{
if (!(char.IsLetter(text[i]) || text[i] == '_'))
break;
}
else
{
if (!(char.IsLetterOrDigit(text[i]) || text[i] == '_'))
break;
}
sb.Append(text[i]);
i++;
}
return sb.ToString();
}
private string EatNumber(string text, ref int i)
{
StringBuilder sb = new StringBuilder();
while (i < text.Length)
{
// allow hex numbers and floating-point numbers
if (sb.Length == 1 && sb[0] == '0')
{
if (!char.IsDigit(text[i]) && char.ToLower(text[i]) != 'x' && text[i] != '.')
break;
}
else if (sb.Length >= 2 && sb[0] == '0' && char.ToLower(sb[1]) == 'x')
{
if (!(char.IsDigit(text[i]) ||
char.ToLower(text[i]) == 'a' ||
char.ToLower(text[i]) == 'b' ||
char.ToLower(text[i]) == 'c' ||
char.ToLower(text[i]) == 'd' ||
char.ToLower(text[i]) == 'e' ||
char.ToLower(text[i]) == 'f'))
break;
}
else
{
if (!char.IsDigit(text[i]))
break;
}
sb.Append(text[i]);
i++;
}
return sb.ToString();
}
private string EatSymbol(string text, ref int i)
{
StringBuilder sb = new StringBuilder();
while (i < text.Length && sb.Length < 1) // we need a proper parser to solve this
{
char c = text[i];
if (c < ' ' || c > '~') // check if its an ASCII character
break;
if (char.IsLetterOrDigit(c) || c == '_') // check if its eligible to be an identifier
break;
sb.Append(c);
i++;
}
return sb.ToString();
}
}
}