UNPKG

@2003scape/rsc-client

Version:
1,165 lines (934 loc) 32.5 kB
const C_0 = '0'.charCodeAt(0); const C_9 = '9'.charCodeAt(0); const C_A = 'a'.charCodeAt(0); const C_ASTERISK = '*'.charCodeAt(0); const C_BACKSLASH = '\\'.charCodeAt(0); const C_BIG_A = 'A'.charCodeAt(0); const C_BIG_Z = 'Z'.charCodeAt(0); const C_COMMA = ','.charCodeAt(0); const C_DOT = '.'.charCodeAt(0); const C_J = 'j'.charCodeAt(0); const C_Q = 'q'.charCodeAt(0); const C_SINGLE_QUOTE = "'".charCodeAt(0); const C_SLASH = '/'.charCodeAt(0); const C_SPACE = ' '.charCodeAt(0); const C_V = 'v'.charCodeAt(0); const C_X = 'x'.charCodeAt(0); const C_Z = 'z'.charCodeAt(0); function toCharArray(s) { const a = new Uint16Array(s.length); for (let i = 0; i < s.length; i += 1) { a[i] = s.charCodeAt(i); } return a; } function fromCharArray(a) { return Array.from(a) .map((c) => String.fromCharCode(c)) .join(''); } class WordFilter { static loadFilters(fragments, bad, host, tld) { WordFilter.loadBad(bad); WordFilter.loadHost(host); WordFilter.loadFragments(fragments); WordFilter.loadTLD(tld); } static loadTLD(buffer) { const tldCount = buffer.getUnsignedInt(); WordFilter.tldList = []; WordFilter.tldType = new Int32Array(tldCount); for (let i = 0; i < tldCount; i++) { WordFilter.tldType[i] = buffer.getUnsignedByte(); const currentTLD = new Uint16Array(buffer.getUnsignedByte()); for (let j = 0; j < currentTLD.length; j++) { currentTLD[j] = buffer.getUnsignedByte(); } WordFilter.tldList.push(currentTLD); } } static loadBad(buffer) { const wordCount = buffer.getUnsignedInt(); WordFilter.badList = []; WordFilter.badList.length = wordCount; WordFilter.badList.fill(null); WordFilter.badCharIds = []; WordFilter.badCharIds.length = wordCount; WordFilter.badCharIds.fill(null); WordFilter.readBuffer( buffer, WordFilter.badList, WordFilter.badCharIds ); } static loadHost(buffer) { const wordCount = buffer.getUnsignedInt(); WordFilter.hostList = []; WordFilter.hostList.length = wordCount; WordFilter.hostList.fill(null); WordFilter.hostCharIds = []; WordFilter.hostCharIds.length = wordCount; WordFilter.hostCharIds.fill(null); WordFilter.readBuffer( buffer, WordFilter.hostList, WordFilter.hostCharIds ); } static loadFragments(buffer) { WordFilter.hashFragments = new Int32Array(buffer.getUnsignedInt()); for (let i = 0; i < WordFilter.hashFragments.length; i++) { WordFilter.hashFragments[i] = buffer.getUnsignedShort(); } } static readBuffer(buffer, wordList, charIDs) { for (let i = 0; i < wordList.length; i++) { const currentWord = new Uint16Array(buffer.getUnsignedByte()); for (let j = 0; j < currentWord.length; j++) { currentWord[j] = buffer.getUnsignedByte(); } wordList[i] = currentWord; const ids = []; ids.length = buffer.getUnsignedInt(); for (let j = 0; j < ids.length; j++) { ids[j] = [ buffer.getUnsignedByte() & 0xff, buffer.getUnsignedByte() & 0xff ]; } if (ids.length > 0) { charIDs[i] = ids; } } } static filter(input) { const inputChars = toCharArray(input.toLowerCase()); WordFilter.applyDotSlashFilter(inputChars); WordFilter.applyBadwordFilter(inputChars); WordFilter.applyHostFilter(inputChars); WordFilter.filterDigits(inputChars); for (let i = 0; i < WordFilter.ignoreList.length; i++) { for ( let j = -1; (j = input.indexOf(WordFilter.ignoreList[i], j + 1)) !== -1; ) { const ignoreWordChars = toCharArray(WordFilter.ignoreList[i]); for (let k = 0; k < ignoreWordChars.length; k++) { inputChars[k + j] = ignoreWordChars[k]; } } } if (WordFilter.forceLowerCase) { WordFilter.stripLowerCase(toCharArray(input), inputChars); WordFilter.toLowerCase(inputChars); } return fromCharArray(inputChars); } static stripLowerCase(input, output) { for (let i = 0; i < input.length; i++) { if (output[i] !== C_ASTERISK && WordFilter.isUpperCase(input[i])) { output[i] = input[i]; } } } static toLowerCase(input) { let isUpperCase = true; for (let i = 0; i < input.length; i++) { const currentChar = input[i]; if (WordFilter.isLetter(currentChar)) { if (isUpperCase) { if (WordFilter.isLowerCase(currentChar)) { isUpperCase = false; } } else if (WordFilter.isUpperCase(currentChar)) { input[i] = currentChar + 97 - 65; } } else { isUpperCase = true; } } } static applyBadwordFilter(input) { for (let i = 0; i < 2; i++) { for (let j = WordFilter.badList.length - 1; j >= 0; j--) { WordFilter.applyWordFilter( input, WordFilter.badList[j], WordFilter.badCharIds[j] ); } } } static applyHostFilter(input) { for (let i = WordFilter.hostList.length - 1; i >= 0; i--) { WordFilter.applyWordFilter( input, WordFilter.hostList[i], WordFilter.hostCharIds[i] ); } } static applyDotSlashFilter(input) { const input1 = input.slice(); const dot = toCharArray('dot'); WordFilter.applyWordFilter(input1, dot, null); const input2 = input.slice(); const slash = toCharArray('slash'); WordFilter.applyWordFilter(input2, slash, null); for (let i = 0; i < WordFilter.tldList.length; i++) { WordFilter.applyTLDFilter( input, input1, input2, WordFilter.tldList[i], WordFilter.tldType[i] ); } } static applyTLDFilter(input, input1, input2, tld, type) { if (tld.length > input.length) { return; } for (let i = 0; i <= input.length - tld.length; i++) { let inputCharCount = i; let l = 0; while (inputCharCount < input.length) { let i1 = 0; const current = input[inputCharCount]; let next = 0; if (inputCharCount + 1 < input.length) { next = input[inputCharCount + 1]; } if ( l < tld.length && (i1 = WordFilter.compareLettersNumbers( tld[l], current, next )) > 0 ) { inputCharCount += i1; l++; continue; } if (l === 0) { break; } if ( (i1 = WordFilter.compareLettersNumbers( tld[l - 1], current, next )) > 0 ) { inputCharCount += i1; continue; } if (l >= tld.length || !WordFilter.isSpecial(current)) { break; } inputCharCount++; } if (l >= tld.length) { let flag = false; const startMatch = WordFilter.getAsteriskCount( input, input1, i ); const endMatch = WordFilter.getAsteriskCount2( input, input2, inputCharCount - 1 ); if (WordFilter.DEBUGTLD) { console.log( `Potential tld: ${tld} at char ${i} ` + `(type="${type}, startmatch="${startMatch}, ` + `endmatch=${endMatch})` ); } if (type === 1 && startMatch > 0 && endMatch > 0) { flag = true; } if ( type === 2 && ((startMatch > 2 && endMatch > 0) || (startMatch > 0 && endMatch > 2)) ) { flag = true; } if (type === 3 && startMatch > 0 && endMatch > 2) { flag = true; } if (flag) { if (WordFilter.DEBUGTLD) { console.log(`Filtered tld: ${tld} at char ${i}`); } let l1 = i; let i2 = inputCharCount - 1; if (startMatch > 2) { if (startMatch === 4) { let flag1 = false; for (let k2 = l1 - 1; k2 >= 0; k2--) { if (flag1) { if (input1[k2] !== C_ASTERISK) { break; } l1 = k2; } else if (input1[k2] === C_ASTERISK) { l1 = k2; flag1 = true; } } } let flag2 = false; for (let l2 = l1 - 1; l2 >= 0; l2--) { if (flag2) { if (WordFilter.isSpecial(input[l2])) { break; } l1 = l2; } else if (!WordFilter.isSpecial(input[l2])) { flag2 = true; l1 = l2; } } } if (endMatch > 2) { if (endMatch === 4) { let flag3 = false; for (let i3 = i2 + 1; i3 < input.length; i3++) { if (flag3) { if (input2[i3] !== C_ASTERISK) { break; } i2 = i3; } else if (input2[i3] === C_ASTERISK) { i2 = i3; flag3 = true; } } } let flag4 = false; for (let j3 = i2 + 1; j3 < input.length; j3++) { if (flag4) { if (WordFilter.isSpecial(input[j3])) { break; } i2 = j3; } else if (!WordFilter.isSpecial(input[j3])) { flag4 = true; i2 = j3; } } } for (let j2 = l1; j2 <= i2; j2++) { input[j2] = C_ASTERISK; } } } } } static getAsteriskCount(input, input1, len) { if (len === 0) { return 2; } for (let j = len - 1; j >= 0; j--) { if (!WordFilter.isSpecial(input[j])) { break; } if (input[j] === C_COMMA || input[j] === C_DOT) { return 3; } } let filtered = 0; for (let l = len - 1; l >= 0; l--) { if (!WordFilter.isSpecial(input1[l])) { break; } if (input1[l] === C_ASTERISK) { filtered++; } } if (filtered >= 3) { return 4; } return WordFilter.isSpecial(input[len - 1]) ? 1 : 0; } static getAsteriskCount2(input, input1, len) { if (len + 1 === input.length) { return 2; } for (let j = len + 1; j < input.length; j++) { if (!WordFilter.isSpecial(input[j])) { break; } if (input[j] === C_BACKSLASH || input[j] === C_SLASH) { return 3; } } let filtered = 0; for (let l = len + 1; l < input.length; l++) { if (!WordFilter.isSpecial(input1[l])) { break; } if (input1[l] === C_ASTERISK) { filtered++; } } if (filtered >= 5) { return 4; } return WordFilter.isSpecial(input[len + 1]) ? 1 : 0; } static applyWordFilter(input, wordList, charIDs) { if (wordList.length > input.length) { return; } for ( let charIndex = 0; charIndex <= input.length - wordList.length; charIndex++ ) { let inputCharCount = charIndex; let k = 0; let specialChar = false; while (inputCharCount < input.length) { let l = 0; const inputChar = input[inputCharCount]; let nextChar = 0; if (inputCharCount + 1 < input.length) { nextChar = input[inputCharCount + 1]; } if ( k < wordList.length && (l = WordFilter.compareLettersSymbols( wordList[k], inputChar, nextChar )) > 0 ) { inputCharCount += l; k++; continue; } if (k === 0) { break; } if ( (l = WordFilter.compareLettersSymbols( wordList[k - 1], inputChar, nextChar )) > 0 ) { inputCharCount += l; continue; } if ( k >= wordList.length || !WordFilter.isNotLowerCase(inputChar) ) { break; } if ( WordFilter.isSpecial(inputChar) && inputChar !== C_SINGLE_QUOTE ) { specialChar = true; } inputCharCount++; } if (k >= wordList.length) { let filter = true; if (WordFilter.DEBUGTLD) { console.log( `Potential word: ${wordList} at char ${charIndex}` ); } if (!specialChar) { let prevChar = C_SPACE; if (charIndex - 1 >= 0) { prevChar = input[charIndex - 1]; } let curChar = C_SPACE; if (inputCharCount < input.length) { curChar = input[inputCharCount]; } const previousID = WordFilter.getCharId(prevChar); const currentID = WordFilter.getCharId(curChar); if ( charIDs && WordFilter.compareCharIds( charIDs, previousID, currentID ) ) { filter = false; } } else { let flag2 = false; let flag3 = false; if ( charIndex - 1 < 0 || (WordFilter.isSpecial(input[charIndex - 1]) && input[charIndex - 1] !== C_SINGLE_QUOTE) ) { flag2 = true; } if ( inputCharCount >= input.length || (WordFilter.isSpecial(input[inputCharCount]) && input[inputCharCount] !== C_SINGLE_QUOTE) ) { flag3 = true; } if (!flag2 || !flag3) { let flag4 = false; let j1 = charIndex - 2; if (flag2) { j1 = charIndex; } for (; !flag4 && j1 < inputCharCount; j1++) { if ( j1 >= 0 && (!WordFilter.isSpecial(input[j1]) || input[j1] === C_SINGLE_QUOTE) ) { const ac2 = new Uint16Array(3); let k1; for (k1 = 0; k1 < 3; k1++) { if ( j1 + k1 >= input.length || (WordFilter.isSpecial(input[j1 + k1]) && input[j1 + k1] !== C_SINGLE_QUOTE) ) { break; } ac2[k1] = input[j1 + k1]; } let flag5 = true; if (k1 === 0) { flag5 = false; } if ( k1 < 3 && j1 - 1 >= 0 && (!WordFilter.isSpecial(input[j1 - 1]) || input[j1 - 1] === C_SINGLE_QUOTE) ) { flag5 = false; } if ( flag5 && !WordFilter.containsFragmentHashes(ac2) ) { flag4 = true; } } } if (!flag4) { filter = false; } } } if (filter) { if (WordFilter.DEBUGWORD) { console.log( `Filtered word: ${wordList} at char ${charIndex}` ); } for (let i1 = charIndex; i1 < inputCharCount; i1++) { input[i1] = C_ASTERISK; } } } } } static compareCharIds(charIdData, prevCharId, curCharId) { let first = 0; if ( charIdData[first][0] === prevCharId && charIdData[first][1] === curCharId ) { return true; } let last = charIdData.length - 1; if ( charIdData[last][0] === prevCharId && charIdData[last][1] === curCharId ) { return true; } while (first !== last && first + 1 !== last) { const middle = ((first + last) / 2) | 0; if ( charIdData[middle][0] === prevCharId && charIdData[middle][1] === curCharId ) { return true; } if ( prevCharId < charIdData[middle][0] || (prevCharId === charIdData[middle][0] && curCharId < charIdData[middle][1]) ) { last = middle; } else { first = middle; } } return false; } static compareLettersNumbers(filterChar, currentChar, nextChar) { filterChar = String.fromCharCode(filterChar); currentChar = String.fromCharCode(currentChar); nextChar = String.fromCharCode(nextChar); if (filterChar === currentChar) { return 1; } if (filterChar === 'e' && currentChar === '3') { return 1; } if ( filterChar === 't' && (currentChar === '7' || currentChar === '+') ) { return 1; } if ( filterChar === 'a' && (currentChar === '4' || currentChar === '@') ) { return 1; } if (filterChar === 'o' && currentChar === '0') { return 1; } if (filterChar === 'i' && currentChar === '1') { return 1; } if (filterChar === 's' && currentChar === '5') { return 1; } if (filterChar === 'f' && currentChar === 'p' && nextChar === 'h') { return 2; } return filterChar === 'g' && currentChar === '9' ? 1 : 0; } static compareLettersSymbols(filterChar, currentChar, nextChar) { filterChar = String.fromCharCode(filterChar); currentChar = String.fromCharCode(currentChar); nextChar = String.fromCharCode(nextChar); if (filterChar === '*') { return 0; } if (filterChar === currentChar) { return 1; } if (filterChar >= 'a' && filterChar <= 'z') { if (filterChar === 'e') { return currentChar === '3' ? 1 : 0; } if (filterChar === 't') { return currentChar === '7' ? 1 : 0; } if (filterChar === 'a') { return currentChar === '4' || currentChar === '@' ? 1 : 0; } if (filterChar === 'o') { if (currentChar === '0' || currentChar === '*') { return 1; } return currentChar === '(' && nextChar === ')' ? 2 : 0; } if (filterChar === 'i') { return currentChar === 'y' || currentChar === 'l' || currentChar === 'j' || currentChar === 'l' || currentChar === '!' || currentChar === ':' || currentChar === ';' ? 1 : 0; } if (filterChar === 'n') { return 0; } if (filterChar === 's') { return currentChar === '5' || currentChar === 'z' || currentChar === '$' ? 1 : 0; } if (filterChar === 'r') { return 0; } if (filterChar === 'h') { return 0; } if (filterChar === 'l') { return currentChar === '1' ? 1 : 0; } if (filterChar === 'd') { return 0; } if (filterChar === 'c') { return currentChar === '(' ? 1 : 0; } if (filterChar === 'u') { return currentChar === 'v' ? 1 : 0; } if (filterChar === 'm') { return 0; } if (filterChar === 'f') { return currentChar === 'p' && nextChar === 'h' ? 2 : 0; } if (filterChar === 'p') { return 0; } if (filterChar === 'g') { return currentChar === '9' || currentChar === '6' ? 1 : 0; } if (filterChar === 'w') { return currentChar === 'v' && nextChar === 'v' ? 2 : 0; } if (filterChar === 'y') { return 0; } if (filterChar === 'b') { return currentChar === '1' && nextChar === '3' ? 2 : 0; } if (filterChar === 'v') { return 0; } if (filterChar === 'k') { return 0; } if (filterChar === 'x') { return currentChar === ')' && nextChar === '(' ? 2 : 0; } if (filterChar === 'j') { return 0; } if (filterChar === 'q') { return 0; } if (filterChar === 'z') { return 0; } } if (filterChar >= '0' && filterChar <= '9') { if (filterChar === '0') { if (currentChar === 'o' || currentChar === 'O') { return 1; } return currentChar === '(' && nextChar === ')' ? 2 : 0; } if (filterChar === '1') { return currentChar !== 'l' ? 0 : 1; } if (filterChar === '2') { return 0; } if (filterChar === '3') { return 0; } if (filterChar === '4') { return 0; } if (filterChar === '5') { return 0; } if (filterChar === '6') { return 0; } if (filterChar === '7') { return 0; } if (filterChar === '8') { return 0; } if (filterChar === '9') { return 0; } } if (filterChar === '-') { return 0; } if (filterChar === ',') { return currentChar === '.' ? 1 : 0; } if (filterChar === '.') { return currentChar === ',' ? 1 : 0; } if (filterChar === '(') { return 0; } if (filterChar === ')') { return 0; } if (filterChar === '!') { return currentChar === 'i' ? 1 : 0; } if (filterChar === "'") { return 0; } if (WordFilter.DEBUGWORD) { console.log(`Letter=${filterChar} not matched`); } return 0; } static getCharId(c) { if (c >= C_A && c <= C_Z) { return c - 97 + 1; } if (c === C_SINGLE_QUOTE) { return 28; } if (c >= C_0 && c <= C_9) { return c - 48 + 29; } return 27; } static filterDigits(input) { let digitIndex = 0; let fromIndex = 0; let k = 0; let l = 0; while ((digitIndex = WordFilter.indexOfDigit(input, fromIndex)) != -1) { let flag = false; for (let i = fromIndex; i >= 0 && i < digitIndex && !flag; i++) { if ( !WordFilter.isSpecial(input[i]) && !WordFilter.isNotLowerCase(input[i]) ) { flag = true; } } if (flag) { k = 0; } if (k === 0) { l = digitIndex; } fromIndex = WordFilter.indexOfNonDigit(input, digitIndex); let j1 = 0; for (let i = digitIndex; i < fromIndex; i++) { j1 = j1 * 10 + input[i] - 48; } if (j1 > 255 || fromIndex - digitIndex > 8) { k = 0; } else { k++; } if (k === 4) { for (let i = l; i < fromIndex; i++) { input[i] = C_ASTERISK; } k = 0; } } } static indexOfDigit(input, fromIndex) { for (let i = fromIndex; i < input.length && i >= 0; i++) { if (input[i] >= C_0 && input[i] <= C_9) { return i; } } return -1; } static indexOfNonDigit(input, fromIndex) { for (let i = fromIndex; i < input.length && i >= 0; i++) { if (input[i] < C_0 || input[i] > C_9) { return i; } } return input.length; } static isSpecial(c) { return !WordFilter.isLetter(c) && !WordFilter.isDigit(c); } static isNotLowerCase(c) { if (c < C_A || c > C_Z) { return true; } return c === C_V || c === C_X || c === C_J || c === C_Q || c === C_Z; } static isLetter(c) { return (c >= C_A && c <= C_Z) || (c >= C_BIG_A && c <= C_BIG_Z); } static isDigit(c) { return c >= C_0 && c <= C_9; } static isLowerCase(c) { return c >= C_A && c <= C_Z; } static isUpperCase(c) { return c >= C_BIG_A && c <= C_BIG_Z; } static containsFragmentHashes(input) { let notNum = true; for (let i = 0; i < input.length; i++) { if (!WordFilter.isDigit(input[i]) && input[i] !== 0) { notNum = false; } } if (notNum) { return true; } const inputHash = WordFilter.wordToHash(input); let first = 0; let last = WordFilter.hashFragments.length - 1; if ( inputHash === WordFilter.hashFragments[first] || inputHash === WordFilter.hashFragments[last] ) { return true; } while (first != last && first + 1 != last) { const middle = ((first + last) / 2) | 0; if (inputHash === WordFilter.hashFragments[middle]) { return true; } if (inputHash < WordFilter.hashFragments[middle]) { last = middle; } else { first = middle; } } return false; } static wordToHash(word) { if (word.length > 6) { return 0; } let hash = 0; for (let i = 0; i < word.length; i++) { const currentChar = word[word.length - i - 1]; if (currentChar >= C_A && currentChar <= C_Z) { hash = (hash * 38 + currentChar - 97 + 1) | 0; } else if (currentChar === C_SINGLE_QUOTE) { hash = (hash * 38 + 27) | 0; } else if (currentChar >= C_0 && currentChar <= C_9) { hash = (hash * 38 + currentChar - 48 + 28) | 0; } else if (currentChar !== 0) { if (WordFilter.DEBUGWORD) { console.log(`wordToHash failed on ${fromCharArray(word)}`); } return 0; } } return hash; } } WordFilter.DEBUGTLD = false; WordFilter.DEBUGWORD = false; WordFilter.forceLowerCase = true; WordFilter.ignoreList = ['cook', "cook's", 'cooks', 'seeks', 'sheet']; module.exports = WordFilter;