diff --git a/PRMasterServer/Servers/LoginServerMessages.cs b/PRMasterServer/Servers/LoginServerMessages.cs index cf92d7c..340b213 100644 --- a/PRMasterServer/Servers/LoginServerMessages.cs +++ b/PRMasterServer/Servers/LoginServerMessages.cs @@ -13,6 +13,8 @@ internal class LoginServerMessages { private readonly static Random _random = new Random(); + private readonly static Encoding _byteEncoder = Encoding.GetEncoding("ISO-8859-1"); + public static byte[] GenerateServerChallenge(ref LoginSocketState state) { state.ServerChallenge = _random.GetString(10); @@ -164,6 +166,14 @@ public static byte[] NewUser(ref LoginSocketState state, Dictionary 30) { + return DataFunctions.StringToBytes(@"\error\\err\0\fatal\\errmsg\The password is too long, must be 30 characters at most!\id\1\final\"); + } + LoginDatabase.Instance.CreateUser(state.Name, password.ToMD5(), state.Email, "??", ((IPEndPoint)state.Socket.RemoteEndPoint).Address); var clientData = LoginDatabase.Instance.GetData(state.Name); @@ -285,49 +295,49 @@ private static string GenerateResponseValue(ref LoginSocketState state) return value.ToMD5(); } + private static readonly ushort[] SessionCRCTable = new ushort[256] { + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, + 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, + 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, + 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, + 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, + 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, + 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, + 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, + 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, + 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, + 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, + 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, + 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, + 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, + 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, + 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, + 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, + 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, + 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, + 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, + 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, + 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, + 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, + 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, + 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, + 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, + 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 + }; + private static ushort GenerateSession(string name) { - ushort[] crc_table = new ushort[256] { - 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, - 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, - 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, - 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, - 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, - 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, - 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, - 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, - 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, - 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, - 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, - 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, - 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, - 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, - 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, - 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, - 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, - 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, - 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, - 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, - 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, - 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, - 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, - 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, - 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, - 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, - 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, - 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, - 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, - 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, - 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, - 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 - }; - int len = name.Length; int nameIndex = 0; ushort session = 0; while (len-- != 0) { - session = (ushort)(crc_table[((name[nameIndex] ^ session) & 0xff) % 256] ^ (session >> 8)); + session = (ushort)(SessionCRCTable[((name[nameIndex] ^ session) & 0xff) % 256] ^ (session >> 8)); nameIndex++; } @@ -336,12 +346,11 @@ private static ushort GenerateSession(string name) private static string DecryptPassword(string password) { - string decrypted = gsBase64Decode(password, password.Length); - gsEncode(ref decrypted); - return decrypted; + string decrypted = gsBase64Decode(password); + return gsEncode(decrypted); } - public static int gsEncode(ref string password) + public static string gsEncode(string password) { byte[] pass = DataFunctions.StringToBytes(password); @@ -371,8 +380,7 @@ public static int gsEncode(ref string password) pass[i] ^= (byte)(a % 256); } - password = DataFunctions.BytesToString(pass); - return passlen; + return DataFunctions.BytesToString(pass); } private static int gsLame(int num) @@ -400,94 +408,74 @@ private static int gsLame(int num) return a; } - private static string gsBase64Decode(string s, int size) + private static readonly char[] Base64Chars = new char[128] { // supports also the Gamespy base64 + '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', + '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', + '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x3e', '\x00', '\x00', '\x00', '\x3f', + '\x34', '\x35', '\x36', '\x37', '\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', + '\x00', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', + '\x0f', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x3e', '\x00', '\x3f', '\x00', '\x00', + '\x00', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', '\x20', '\x21', '\x22', '\x23', '\x24', '\x25', '\x26', '\x27', '\x28', + '\x29', '\x2a', '\x2b', '\x2c', '\x2d', '\x2e', '\x2f', '\x30', '\x31', '\x32', '\x33', '\x00', '\x00', '\x00', '\x00', '\x00' + }; + + private static string gsBase64Decode(string s) { byte[] data = DataFunctions.StringToBytes(s); - int len; - int xlen; + int datalen = data.Length; + + int bufflen = ((datalen >> 2) * 3) + 1; + byte[] buff = new byte[bufflen]; + int a = 0; int b = 0; int c = 0; - int step; - int limit; - int y = 0; - int z = 0; - - byte[] buff; - byte[] p; - - char[] basechars = new char[128] { // supports also the Gamespy base64 - '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', - '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', - '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x3e', '\x00', '\x00', '\x00', '\x3f', - '\x34', '\x35', '\x36', '\x37', '\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', - '\x00', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', - '\x0f', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x3e', '\x00', '\x3f', '\x00', '\x00', - '\x00', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', '\x20', '\x21', '\x22', '\x23', '\x24', '\x25', '\x26', '\x27', '\x28', - '\x29', '\x2a', '\x2b', '\x2c', '\x2d', '\x2e', '\x2f', '\x30', '\x31', '\x32', '\x33', '\x00', '\x00', '\x00', '\x00', '\x00' - }; - - if (size <= 0) - len = data.Length; - else - len = size; - - xlen = ((len >> 2) * 3) + 1; - buff = new byte[xlen % 256]; - if (buff.Length == 0) return null; - - p = buff; - limit = data.Length + len; - - for (step = 0; ; step++) { - do { - if (z >= limit) { - c = 0; + int buffoffs = 0; + int dataoffs = 0; + for (int step = 0; ; step++) + { + do + { + if (dataoffs >= datalen) + { + c = 0; // reached the end break; } - if (z < data.Length) - c = data[z]; else - c = 0; - z++; - if ((c == '=') || (c == '_')) { - c = 0; - break; + { + c = data[dataoffs++]; + if ((c == '=') || (c == '_')) // support also the Gamespy base64 + { + c = 0; + break; + } } - } while (c != 0 && ((c <= (byte)' ') || (c > 0x7f))); + } while (c != 0 && ((c <= ' ') || (c > 0x7f))); if (c == 0) break; - switch (step & 3) { + switch (step & 3) + { case 0: - a = basechars[c]; + a = Base64Chars[c]; break; case 1: - b = basechars[c]; - p[y++] = (byte)(((a << 2) | (b >> 4)) % 256); + b = Base64Chars[c]; + buff[buffoffs++] = (byte)(((a << 2) | (b >> 4)) % 256); break; case 2: - a = basechars[c]; - p[y++] = (byte)((((b & 15) << 4) | (a >> 2)) % 256); + a = Base64Chars[c]; + buff[buffoffs++] = (byte)((((b & 15) << 4) | (a >> 2)) % 256); break; case 3: - p[y++] = (byte)((((a & 3) << 6) | basechars[c]) % 256); + buff[buffoffs++] = (byte)((((a & 3) << 6) | Base64Chars[c]) % 256); break; default: break; } } - p[y] = 0; - - len = p.Length - buff.Length; - - if (size != 0) - size = len; - - if ((len + 1) != xlen) - if (buff.Length == 0) return null; - return DataFunctions.BytesToString(buff).Substring(0, y); + return _byteEncoder.GetString(buff, 0, buffoffs); } } } diff --git a/PRMasterServer/Servers/ServerListRetrieve.cs b/PRMasterServer/Servers/ServerListRetrieve.cs index 7809b42..48e8b61 100644 --- a/PRMasterServer/Servers/ServerListRetrieve.cs +++ b/PRMasterServer/Servers/ServerListRetrieve.cs @@ -327,21 +327,28 @@ private bool ParseRequest(SocketState state, string message) string gamename = data[1].ToLowerInvariant(); string validate = data[2].Substring(0, 8); - string filter = FixFilter(data[2].Substring(8)); + string filter = data[2].Substring(8); string[] fields = data[3].Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); Log(Category, String.Format("Received client request: {0}:{1}", ((IPEndPoint)state.Socket.RemoteEndPoint).Address, ((IPEndPoint)state.Socket.RemoteEndPoint).Port)); IQueryable servers = _report.Servers.ToList().Select(x => x.Value).Where(x => x.Valid).AsQueryable(); - if (!String.IsNullOrWhiteSpace(filter)) { + + string fixedFilter = FixFilter(filter); + if (!String.IsNullOrWhiteSpace(fixedFilter)) + { try { //Console.WriteLine(filter); - servers = servers.Where(filter); + servers = servers.Where(fixedFilter); //Console.WriteLine(servers.Count()); } catch (Exception e) { LogError(Category, "Error parsing filter"); - LogError(Category, filter); - LogError(Category, e.ToString()); + LogError(Category, String.Format(" Source filter: {0}", filter)); + LogError(Category, String.Format(" Fixed filter: {0}", fixedFilter)); + LogError(Category, String.Format(" {0}", e.ToString())); + + // Return empty server list + servers = (new List()).AsQueryable(); } } @@ -439,210 +446,204 @@ private static string GetField(GameServer server, string fieldName) return value.ToString(); } - private string FixFilter(string filter) + private enum FilterWordTypes { - // escape [ - filter = filter.Replace("[", "[[]"); - - // fix an issue in the BF2 main menu where filter expressions aren't joined properly - // i.e. "numplayers > 0gametype like '%gpm_cq%'" - // becomes "numplayers > 0 && gametype like '%gpm_cq%'" - try { - filter = FixFilterOperators(filter); - } catch (Exception e) { - LogError(Category, e.ToString()); - } - - // fix quotes inside quotes - // i.e. hostname like 'flyin' high' - // becomes hostname like 'flyin_ high' - try { - filter = FixFilterQuotes(filter); - } catch (Exception e) { - LogError(Category, e.ToString()); - } - - // fix consecutive whitespace - filter = Regex.Replace(filter, @"\s+", " ").Trim(); - - return filter; - } - - private static string FixFilterOperators(string filter) + None, + String, + OpenBracket, + CloseBracket, + Comparison, + Logical, + Other, + }; + + private static string FixFilter(string filter) { + // get all the properties that aren't "[NonFilter]" PropertyInfo[] properties = typeof(GameServer).GetProperties(); List filterableProperties = new List(); - - // get all the properties that aren't "[NonFilter]" - foreach (var property in properties) { + foreach (var property in properties) + { if (property.GetCustomAttributes(false).Any(x => x.GetType().Name == "NonFilterAttribute")) continue; filterableProperties.Add(property.Name); } + + // escape [ + filter = filter.Replace("[", "[[]"); - // go through each property, see if they exist in the filter, - // and check to see if what's before the property is a logical operator - // if it is not, then we slap a && before it - foreach (var property in filterableProperties) { - IEnumerable indexes = filter.IndexesOf(property); - foreach (var index in indexes) { - if (index > 0) { - int length = 0; - bool hasLogical = IsLogical(filter, index, out length, true) || IsOperator(filter, index, out length, true) || IsGroup(filter, index, out length, true); - if (!hasLogical) { - filter = filter.Insert(index, " && "); - } - } - } - } - return filter; - } - - private static string FixFilterQuotes(string filter) - { - StringBuilder newFilter = new StringBuilder(filter); - - for (int i = 0; i < filter.Length; i++) { - int length = 0; - bool isOperator = IsOperator(filter, i, out length); - - if (isOperator) { - i += length; - bool isInsideString = false; - for (; i < filter.Length; i++) { - if (filter[i] == '\'' || filter[i] == '"') { - if (isInsideString) { - // check what's after the quote to see if we terminate the string - if (i >= filter.Length - 1) { - // end of string - isInsideString = false; + StringBuilder filterBuilder = new StringBuilder(); + int len = filter.Length; + var prevWordTrueType = FilterWordTypes.None; + var curWordType = FilterWordTypes.None; + int curWordStart = 0; + int endOfString = -1; + for(int i = 0; i < len; i++) + { + FilterWordTypes newWordType; + if (i <= endOfString) { + newWordType = FilterWordTypes.String; + } else { + char ch = filter[i]; + if (ch == '\'' || ch == '"') + { + newWordType = FilterWordTypes.String; + + // Search for the trailing quote + // This is a nightmare, since they forgot to escape filter strings in the BF2 client, so you can easily get something like that: + // hostname like 'flyin' high' + int quotes = filter.Substring(i + 1).Count(x => x == ch); + if (quotes == 0) + endOfString = len - 1; // No trailing quote + else if (quotes == 1) + endOfString = filter.IndexOf(ch, i + 1); + else // quotes > 1 + { + endOfString = i; + bool doPercentCheck = (filter[i + 1] == '%'); + for (int j = 1; j <= quotes; j++) + { + endOfString = filter.IndexOf(ch, endOfString + 1); + if (j == quotes) // Last quote? break; - } - for (int j = i + 1; j < filter.Length; j++) { - // continue along whitespace - if (filter[j] == ' ') { + + if (doPercentCheck) + { + if (endOfString <= (i + 2)) + continue; + if (filter[endOfString - 1] != '%') continue; - } else { - // if it's a logical operator, then we terminate - bool op = IsLogical(filter, j, out length); - if (op) { - isInsideString = false; - j += length; - i = j; - } - break; - } } - if (isInsideString) { - // and if we're still inside the string, replace the quote with a wildcard character - newFilter[i] = '_'; + + string trailStr = filter.Substring(endOfString + 1).TrimStart(); + bool isTerminated = (trailStr.StartsWith(")") + || trailStr.StartsWith("(") + || trailStr.StartsWith("and ", StringComparison.InvariantCultureIgnoreCase) + || trailStr.StartsWith("or ", StringComparison.InvariantCultureIgnoreCase)); + if (isTerminated == false) { + foreach(var property in filterableProperties) + if (trailStr.StartsWith(property)) { + isTerminated = true; + break; + } } - continue; - } else { - isInsideString = true; + + if (isTerminated) + break; } } } + else if (ch <= ' ') + newWordType = FilterWordTypes.None; // Skip whitespaces + else if (ch == '(') + newWordType = FilterWordTypes.OpenBracket; + else if (ch == ')') + newWordType = FilterWordTypes.CloseBracket; + else if (ch == '=' || ch == '!' || ch == '<' || ch == '>') + newWordType = FilterWordTypes.Comparison; + //else if (ch == '&' || ch == '|') // No idea how these C logical operators can get into a BF2 filter, but they were in the original... + // newWordType = FilterWordTypes.Logical; + else + newWordType = FilterWordTypes.Other; } - } - - return newFilter.ToString(); - } - - private static bool IsOperator(string filter, int i, out int length, bool previous = false) - { - bool isOperator = false; - length = 0; - - if (i < filter.Length - 1) { - string op = filter.Substring(i - (i >= 2 ? (previous ? 2 : 0) : 0), 1); - if (op == "=" || op == "<" || op == ">") { - isOperator = true; - length = 1; - } - } - if (!isOperator) { - if (i < filter.Length - 2) { - string op = filter.Substring(i - (i >= 3 ? (previous ? 3 : 0) : 0), 2); - if (op == "==" || op == "!=" || op == "<>" || op == "<=" || op == ">=") { - isOperator = true; - length = 2; + if (newWordType != curWordType || newWordType == FilterWordTypes.OpenBracket || newWordType == FilterWordTypes.CloseBracket) + { + if (curWordType != FilterWordTypes.None) + { + prevWordTrueType = AddFilterWord(filterBuilder, filter, curWordStart, i, curWordType, prevWordTrueType, filterableProperties); } - } - } - if (!isOperator) { - if (i < filter.Length - 4) { - string op = filter.Substring(i - (i >= 5 ? (previous ? 5 : 0) : 0), 4); - if (op.Equals("like", StringComparison.InvariantCultureIgnoreCase)) { - isOperator = true; - length = 4; - } + curWordType = newWordType; + curWordStart = i; } } - if (!isOperator) { - if (i < filter.Length - 8) { - string op = filter.Substring(i - (i >= 9 ? (previous ? 9 : 0) : 0), 8); - if (op.Equals("not like", StringComparison.InvariantCultureIgnoreCase)) { - isOperator = true; - length = 8; - } - } + if (curWordType != FilterWordTypes.None && curWordStart < len) + { + AddFilterWord(filterBuilder, filter, curWordStart, len, curWordType, prevWordTrueType, filterableProperties); } - return isOperator; + return filterBuilder.ToString(); } - private static bool IsLogical(string filter, int i, out int length, bool previous = false) + private static FilterWordTypes AddFilterWord(StringBuilder filterBuilder, string filter, int wordStart, int nextWordStart, FilterWordTypes wordType, FilterWordTypes prevWordType, List filterableProperties) { - bool isLogical = false; - length = 0; - - if (i < filter.Length - 2) { - string op = filter.Substring(i - (i >= 3 ? (previous ? 3 : 0) : 0), 2); - if (op == "&&" || op == "||" || op.Equals("or", StringComparison.InvariantCultureIgnoreCase)) { - isLogical = true; - length = 2; - } - } + string word = filter.Substring(wordStart, nextWordStart - wordStart); - if (!isLogical) { - if (i < filter.Length - 3) { - string op = filter.Substring(i - (i >= 4 ? (previous ? 4 : 0) : 0), 3); - if (op.Equals("and", StringComparison.InvariantCultureIgnoreCase)) { - isLogical = true; - length = 3; - } + if (wordType == FilterWordTypes.Other) + { + // Try to fix properties merged with other stuff + foreach(var property in filterableProperties) + { + int propIndex = word.IndexOf(property); + if (propIndex < 0) + continue; + + if (propIndex > 0) + prevWordType = AddFilterWord(filterBuilder, word.Substring(0, propIndex), FilterWordTypes.Other, prevWordType); + + prevWordType = AddFilterWord(filterBuilder, property, FilterWordTypes.Other, prevWordType); + + int trailIndex = propIndex + property.Length; + if (trailIndex < word.Length) + prevWordType = AddFilterWord(filterBuilder, word.Substring(trailIndex), FilterWordTypes.Other, prevWordType); + + return prevWordType; } } - return isLogical; + return AddFilterWord(filterBuilder, word, wordType, prevWordType); } - private static bool IsGroup(string filter, int i, out int length, bool previous = false) + private static FilterWordTypes AddFilterWord(StringBuilder filterBuilder, string word, FilterWordTypes wordType, FilterWordTypes prevWordType) { - bool isGroup = false; - length = 0; - - if (i < filter.Length - 1) { - string op = filter.Substring(i - (i >= 2 ? (previous ? 2 : 0) : 0), 1); - if (op == "(" || op == ")") { - isGroup = true; - length = 1; + if (wordType == FilterWordTypes.Other) + { + if (word.Equals("and", StringComparison.InvariantCultureIgnoreCase)) + wordType = FilterWordTypes.Logical; + else if (word.Equals("or", StringComparison.InvariantCultureIgnoreCase)) + wordType = FilterWordTypes.Logical; + else if (word.Equals("like", StringComparison.InvariantCultureIgnoreCase)) + wordType = FilterWordTypes.Comparison; + else if (word.Equals("not", StringComparison.InvariantCultureIgnoreCase)) + wordType = FilterWordTypes.Comparison; + } + + // Not the first word or start/end of a group + if (prevWordType != FilterWordTypes.None && prevWordType != FilterWordTypes.OpenBracket && wordType != FilterWordTypes.CloseBracket) + { + filterBuilder.Append(' '); + + // fix an issue in the BF2 main menu where filter expressions aren't joined properly + // i.e. "numplayers > 0gametype like '%gpm_cq%'" + // becomes "numplayers > 0 and gametype like '%gpm_cq%'" + if (wordType == FilterWordTypes.Other) + { + if (prevWordType != FilterWordTypes.Logical && prevWordType != FilterWordTypes.Comparison) + filterBuilder.Append("and "); + } else if (wordType == FilterWordTypes.OpenBracket) { + if (prevWordType == FilterWordTypes.Other || prevWordType == FilterWordTypes.String) + filterBuilder.Append("and "); } - if (!isGroup && previous) { - op = filter.Substring(i - (i >= 1 ? (previous ? 1 : 0) : 0), 1); - if (op == "(" || op == ")") { - isGroup = true; - length = 1; - } + } + + if (wordType == FilterWordTypes.String) + { + char quote = word[0]; + filterBuilder.Append(quote); + if (word.Length > 2) + { + string strContent = word.Substring(1, word.Length - 2); + filterBuilder.Append(strContent.Replace(quote, '_')); // replace quote characters inside the string with a wildcard character } + filterBuilder.Append(quote); } + else + filterBuilder.Append(word); - return isGroup; + return wordType; } private class SocketState : IDisposable