public static SavedVariablesDictionary parse(string sFileName) { SavedVariablesDictionary savedVariables = new SavedVariablesDictionary(); // Note -- file errors are deliberately allowed to bubble to the caller. // Note 2: This function reads in the entire file. There are undoubtedly better ways to do this. SavedVariablesFileStream stream = new SavedVariablesFileStream(sFileName); for (LuaToken variable = getNextToken(stream); variable != null; variable = getNextToken(stream)) { if (stream.getNextCharacter(false) != '=') { throw new InvalidDataException("Expected 'variable_name = value' pairs in saved variables file."); } savedVariables.Add(((LuaKeywordToken)variable).getKeyword(), getNextToken(stream).getInterpretedValue()); } /* * // Sanity check tests * // First, Save -> Reload should yield equivalent databases * // Second, Save File 1 -> Load File 1 -> Save File 2 should yield identical files * * string sExportTest = "c:\\sv\\original\\" + sFileName.Substring(sFileName.LastIndexOf('\\')); * string sReexportTest = "c:\\sv\\reexport\\" + sFileName.Substring(sFileName.LastIndexOf('\\')); * * if (sFileName != sExportTest) * { * // Test export functionality * export(sExportTest, savedVariables); * // And reimport test file * SavedVariablesDictionary reimport = parse(sExportTest); * * if (!savedVariables.Equivalent(reimport)) * { * throw new InvalidDataException("Export file does not match import file."); * } * * // And test export the reimport * export(sReexportTest, reimport); * } */ stream = null; return(savedVariables); }
private static LuaToken getNextToken(SavedVariablesFileStream stream) { // Note: // The general rule on the true/false parameter of getNextCharacter/peekNextCharacter. // That boolean indicates whether the function should parse out comments & white-spaces // from the next possible character. While it's useful to ignore those characters // when talking about separators between keywords, those characters should not // be ignored when the next keyword is being processed. Imagine the following // example: // var1 = 0xc0 // ffee = 72 // In this case, if we were processing the numer '0xc0' and asked for the next // character ignoring white spaces, 'ffee' would be appended letting var1 = 0xc0ffee // when in actuality ffee is the name of the next keyword / variable. char?cCharacter = stream.getNextCharacter(false); if (cCharacter == null) { return(null); } if ((cCharacter >= '0' && cCharacter <= '9') || (cCharacter == '-' || cCharacter == '+')) { char?cNextCharacter = stream.peekNextCharacter(true); // Numeric token if (cCharacter == '0' && cNextCharacter != null && Char.ToUpper((char)cNextCharacter) == 'X') { // Hexadecimal character long lValue = 0; // Consume 'X' stream.getNextCharacter(true); while (true) { cNextCharacter = stream.peekNextCharacter(true); if (cNextCharacter != null && Char.ToUpper((char)cNextCharacter) >= 'A' && Char.ToUpper((char)cNextCharacter) <= 'F') { lValue = lValue * 16 + (10 + (char)cNextCharacter - 'A'); } else if (cNextCharacter >= '0' && (char)cNextCharacter <= '9') { lValue = lValue * 16 + (char)cNextCharacter - '0'; } else { break; } // Consume the character we just read in stream.getNextCharacter(true); } return(new LuaIntegerToken(lValue)); } else { // Numeric token -- scan for the whole string then choose the number format String sValue = null; bool bIsInteger = true; sValue += cCharacter; while (true) { cNextCharacter = stream.peekNextCharacter(true); if (cNextCharacter == 'e' || cNextCharacter == 'E' || cNextCharacter == '.' || cNextCharacter == '-') { bIsInteger = false; } else if (!(cNextCharacter >= '0' && cNextCharacter <= '9')) { break; } sValue += cNextCharacter; // Consume the character we just read in stream.getNextCharacter(true); } if (bIsInteger) { return(new LuaIntegerToken(Int64.Parse(sValue))); } else { return(new LuaDoubleToken(Double.Parse(sValue, System.Globalization.CultureInfo.InvariantCulture))); } } } else if (cCharacter == '"' || cCharacter == '\'') { string sValue = ""; while (true) { char?cNextCharacter = stream.getNextCharacter(true); // String terminates when it ends with the same character // it began with (i.e. ' is needed to close ', and " for ") if (cNextCharacter == cCharacter) { break; } else if (cNextCharacter == null) { throw new InvalidDataException("Unterminated lua string."); } else if (cNextCharacter == '\\') { // Escape the next character... Read it in whatever it is. cNextCharacter = stream.getNextCharacter(true); switch (cNextCharacter) { case '0': cNextCharacter = '\0'; break; case 'a': cNextCharacter = '\a'; break; case 'b': cNextCharacter = '\b'; break; case 'f': cNextCharacter = '\f'; break; case 'n': cNextCharacter = '\n'; break; case 'r': cNextCharacter = '\r'; break; case 't': cNextCharacter = '\t'; break; case 'v': cNextCharacter = '\v'; break; case '\'': case '\"': // These types pass through break; default: // If it's an escape sequence we don't understand // just reproduce it. Sometimes it's not properly formatted // strings (e.g. !Swatter had some paths that used the '\' from // the path separator without escape codes) sValue += '\\'; break; } } sValue += cNextCharacter; } return(new LuaStringToken(sValue)); } else if (cCharacter == '[') { LuaToken key = getNextToken(stream); if (key == null) { throw new InvalidDataException("Expected a field identifier after ["); } else if (stream.getNextCharacter(false) != ']') { throw new InvalidDataException("Expected a ] after field key"); } else if (stream.getNextCharacter(false) != '=') { throw new InvalidDataException("Expected a value after field key"); } else { LuaToken value = getNextToken(stream); if (value == null) { throw new InvalidDataException("Expected value after [" + key + "] = "); } return(new LuaFieldToken(key, value.getInterpretedValue())); } } else if (cCharacter == '{') { SavedVariablesDictionary items = new SavedVariablesDictionary(); long lAnonymousKey = 1; while (true) { char?cNextCharacter = stream.peekNextCharacter(false); if (cNextCharacter == null) { throw new InvalidDataException("Unterminated lua table (no '}' after '{' in file)."); } else if (cNextCharacter == '}') { // Consume the close brackets stream.getNextCharacter(false); break; } else if (cNextCharacter == ',' || cNextCharacter == ';') { stream.getNextCharacter(false); } else { LuaToken nextToken = getNextToken(stream); if (nextToken == null) { throw new InvalidDataException("Unterminated lua table (no '}' after '{' in file)."); } // Case 1: [key] = value pair if (nextToken.getTokenType() == LuaTokenTypes.LUA_FIELD) { items.Add(((LuaFieldToken)nextToken).getKeyValueKey(), ((LuaFieldToken)nextToken).getKeyValueValue()); } else if (stream.peekNextCharacter(false) == '=') // Case 2: key = value pair { // Consume the '=' stream.getNextCharacter(false); switch (nextToken.getTokenType()) { case LuaTokenTypes.LUA_INTEGER: case LuaTokenTypes.LUA_KEYWORD: case LuaTokenTypes.LUA_STRING: items.Add((IComparable)nextToken.getValue(), getNextToken(stream).getInterpretedValue()); break; default: throw new InvalidDataException("Don't know how to hash by " + nextToken.getTokenType() + " token type."); } } else // Case 3: value { items.Add(lAnonymousKey++, nextToken.getInterpretedValue()); } } } return(new LuaTableToken(items)); } else if ((cCharacter >= 'a' && cCharacter <= 'z') || (cCharacter >= 'A' && cCharacter <= 'Z') || cCharacter == '_') { string sKeyword = null; sKeyword += cCharacter; while (true) { char?cNextCharacter = stream.peekNextCharacter(true); if ((cNextCharacter >= 'a' && cNextCharacter <= 'z') || (cNextCharacter >= 'A' && cNextCharacter <= 'Z') || (cNextCharacter >= '0' && cNextCharacter <= '9') || cNextCharacter == '_') { sKeyword += stream.getNextCharacter(true); } else { return(new LuaKeywordToken(sKeyword)); } } } throw new InvalidDataException("Can't parse string " + stream.getErrorContext()); }