diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 278c318ef1b2f09cd89a43944617fd23938aa4e1..1990aedb4ac0b590adc550595fef211462f2d62e 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -14,6 +14,11 @@ et ce projet adhère au [versionnage sémantique](https://semver.org/spec/v2.0.0 ## [Non-publié/Non-Stabilisé] (par [1000i100]) +## [Version 3.5.2] - 2022-12-16 (par [1000i100]) +### Corrections +- Dictionary échappe correctement les caractères spéciaux +- Dictionary splitGet permet d'avoir séparément idSec et pwd sans ambiguïté liée aux caractères échappés. + ## [Version 3.5.1] - 2022-12-09 (par [1000i100]) ### Corrections - Dictionary applique systématiquement les variantes passées en option. @@ -125,8 +130,10 @@ et ce projet adhère au [versionnage sémantique](https://semver.org/spec/v2.0.0 - intégration des librairies de crypto nécessaires - calcul de la clef publique correspondant à chaque combinaison de secrets saisie, et comparaison à la clef publique de référence. -[Non-publié/Non-Stabilisé]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.5.0...main +[Non-publié/Non-Stabilisé]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.5.2...main +[Version 3.5.2]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.5.1...v3.5.2 +[Version 3.5.1]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.5.0...v3.5.1 [Version 3.5.0]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.4.2...v3.5.0 [Version 3.4.2]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.4.1...v3.4.2 [Version 3.4.1]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.4.0...v3.4.1 diff --git a/npm/package.json b/npm/package.json index 24424ff00161b726409793584818aaa8ee9f6452..a15559e0717f660e3d54c1532b1b1e9fbe56d19b 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "g1lib", - "version": "3.5.1", + "version": "3.5.2", "description": "An ubiquitous static javascript toolbox lib for Ǧ1 / Duniter ecosystem with reliability in mind.", "main": "nodejs/all.mjs", "browser": "browser/all.mjs", diff --git a/src/dictionary-escaper.mjs b/src/dictionary-escaper.mjs new file mode 100644 index 0000000000000000000000000000000000000000..233d9445b0a350a5eed882d09f6243196a376bda --- /dev/null +++ b/src/dictionary-escaper.mjs @@ -0,0 +1,34 @@ +const specialMap = { + '(': String.fromCharCode(0x01), + ')': String.fromCharCode(0x02), + '|': String.fromCharCode(0x03), + '{': String.fromCharCode(0x04), + '}': String.fromCharCode(0x05), + ',': String.fromCharCode(0x06), + '[': String.fromCharCode(0x07), + ']': String.fromCharCode(0x08), + '-': String.fromCharCode(0x09), + '<': String.fromCharCode(0x0a), + '>': String.fromCharCode(0x0b), + ':': String.fromCharCode(0x0c), + '=': String.fromCharCode(0x0d), + '@': String.fromCharCode(0x0e) +}; +const revertSpecial = swapKeyValue(specialMap); +function swapKeyValue(object) { + const result = {}; + for (const key in object) { + result[object[key]] = key; + } + + return result; +} +export function escape2utfSpecial(str) { + return str.replace(/\\(.)/g, (a, chr) => specialMap[chr] ? specialMap[chr] : chr); +} +export function utfSpecial2unEscaped(str) { + return str.split('').map(chr => revertSpecial[chr] ? revertSpecial[chr] : chr).join(''); +} +export function utfSpecial2escaped(str) { + return str.split('').map(chr => revertSpecial[chr] ? `\\${revertSpecial[chr]}` : chr).join(''); +} diff --git a/src/dictionary-escaper.test.mjs b/src/dictionary-escaper.test.mjs new file mode 100644 index 0000000000000000000000000000000000000000..cf308d501581d0b7f8c554fd776bf2b10e9c3638 --- /dev/null +++ b/src/dictionary-escaper.test.mjs @@ -0,0 +1,17 @@ +import test from "ava"; +import * as app from "./dictionary-escaper.mjs"; +import {escape2utfSpecial} from "./dictionary-escaper.mjs"; + +test('unescape special characters & re-escape them generate identical string', t => { + const cases = [ + 'plop:\\:ici', + '[\\]*]', + ] + cases.forEach(c=>t.is(app.utfSpecial2escaped(app.escape2utfSpecial(c)), c)); +}); +test('unescape special characters & reconvert without escaping remove escaping', t => { + t.is(app.utfSpecial2unEscaped(app.escape2utfSpecial('plop:\\:ici')), 'plop::ici'); +}); +test('unescape usual characters & re-escape them generate usual string', t => { + t.is(app.utfSpecial2escaped(app.escape2utfSpecial('\\a')), 'a'); +}); diff --git a/src/dictionary-parser.mjs b/src/dictionary-parser.mjs index 7e04e64c51c9d587f34d38a937b0864f96384369..538f73bc67174335751534e7651f0ba8cd460c17 100644 --- a/src/dictionary-parser.mjs +++ b/src/dictionary-parser.mjs @@ -1,5 +1,6 @@ import latinize from '../node_modules/latinize-to-ascii/latinize.mjs'; import * as tree from './dictionary-tree.mjs'; +import {escape2utfSpecial, utfSpecial2escaped} from "./dictionary-escaper.mjs"; export function parse(dictionaryString,idSecPwd=true,accent=0,lowerCase=0,leet=0) { resetCache(); @@ -11,7 +12,7 @@ export function parse(dictionaryString,idSecPwd=true,accent=0,lowerCase=0,leet=0 .replace(/§void§\|/g,'') .replace(/\|§void§/g,'') .replace(/§void§/g,''); - if(monoLined.includes('§infiniteRecursion§')) throw new Error(`Unable to parse : ${utfSpecial2unEscaped(flattenIt(monoLined))}`); + if(monoLined.includes('§infiniteRecursion§')) throw new Error(`Unable to parse : ${utfSpecial2escaped(flattenIt(monoLined))}`); if(!idSecPwd) return parseEnd(monoLined,allLines,accent,lowerCase,leet); const parts = tree.splitAround('@@',tree.buildTreeStruct(monoLined)); const flatParts = []; @@ -22,7 +23,7 @@ export function parse(dictionaryString,idSecPwd=true,accent=0,lowerCase=0,leet=0 } function parseEnd(str,allLines,accent,lowerCase,leet) { let theString = syncRefHandler(str,allLines) - if(theString.match(/=[^>]+>/)) throw new Error(`Unable to parse : ${utfSpecial2unEscaped(flattenIt(theString))}`); + if(theString.match(/=[^>]+>/)) throw new Error(`Unable to parse : ${utfSpecial2escaped(flattenIt(theString))}`); theString = qtyHandler(theString); if(accent===1) theString = zeroAccent(theString); if(accent===2) theString = optionalAccent(theString); @@ -34,7 +35,7 @@ function parseEnd(str,allLines,accent,lowerCase,leet) { if(leet===2) theString = allLeetSpeak(theString); if(leet===3) theString = everyLeetSpeak(theString); //TODO: bigLeet Variants - return utfSpecial2unEscaped(flattenIt(theString)); + return utfSpecial2escaped(flattenIt(theString)); } function zeroVariant(str,func){ const zero = func(str); @@ -164,37 +165,6 @@ function cache(key, func, ...args) { /* Compute and cache */ if (typeof cache.cached[key] === 'undefined') cache.cached[key] = func(...args); /* Answer from cache */ return cache.cached[key]; } -const specialMap = { - '(': String.fromCharCode(0x01), - ')': String.fromCharCode(0x02), - '|': String.fromCharCode(0x03), - '{': String.fromCharCode(0x04), - '}': String.fromCharCode(0x05), - ',': String.fromCharCode(0x06), - '[': String.fromCharCode(0x07), - ']': String.fromCharCode(0x08), - '-': String.fromCharCode(0x09), - '<': String.fromCharCode(0x0a), - '>': String.fromCharCode(0x0b), - ':': String.fromCharCode(0x0c), - '=': String.fromCharCode(0x0d), - '@': String.fromCharCode(0x0e) -}; -const revertSpecial = swapKeyValue(specialMap); -function swapKeyValue(object) { - const result = {}; - for (const key in object) { - result[object[key]] = key; - } - - return result; -} -function escape2utfSpecial(str) { - return str.replace(/\\(.)/g, (a, chr) => specialMap[chr] ? specialMap[chr] : chr); -} -function utfSpecial2unEscaped(str) { - return str.split('').map(chr => revertSpecial[chr] ? revertSpecial[chr] : chr).join(''); -} function bracketsHandler(theString) { // Handle [] const lower = 'abcdefghijklmnopqrstuvwxyz'; diff --git a/src/dictionary-parser.test.mjs b/src/dictionary-parser.test.mjs index b96bc42643743d560d94604109999a44c553fa83..6e8fffe0e7558bdff2f98caa96e922c9ad8d27b0 100644 --- a/src/dictionary-parser.test.mjs +++ b/src/dictionary-parser.test.mjs @@ -44,15 +44,12 @@ test('parse handle nested (s|t(ri|ng)){qty}', t => { }); test('parse handle plop:\\:', t => { - t.is(app.parse('plop:\\:ici',false), 'plop::ici'); - t.is(app.parse('plop\\::ici',false), 'plop::ici'); + t.is(app.parse('plop:\\:ici',false), 'plop:\\:ici'); + t.is(app.parse('plop\\::ici',false), 'plop\\::ici'); t.is(app.parse('plop::ici',false), ''); }); test('parse handle [\\]*]', t => { - t.is(app.parse('[\\]*]',false), '(]|*)'); -}); -test('parse handle escaping common chr \\a', t => { - t.is(app.parse('\\a',false), 'a'); + t.is(app.parse('[\\]*]',false), '(\\]|*)'); }); test('parse handle =ref>', t => { diff --git a/src/dictionary-tree.mjs b/src/dictionary-tree.mjs index b219137a5ed8c9f986be338bc66836aabdf8558e..b9da3c96b6668b664f9be66c5f94ab32b283f4df 100644 --- a/src/dictionary-tree.mjs +++ b/src/dictionary-tree.mjs @@ -1,8 +1,10 @@ +import {escape2utfSpecial, utfSpecial2escaped, utfSpecial2unEscaped} from "./dictionary-escaper.mjs"; + export function buildTreeStruct(monoLineString) { - const stringAsArray = monoLineString.split(''); + const stringAsArray = escape2utfSpecial(monoLineString).split(''); const rawTree = leftParser(stringAsArray); const outOfScope = stringAsArray.length; - if (outOfScope) throw new Error(`fail to build tree from : "${monoLineString}" parsed: ${JSON.stringify(rawTree)} unparsed/failed: ${stringAsArray.join('')}`); + if (outOfScope) throw new Error(`fail to build tree from : "${utfSpecial2escaped(monoLineString)}" parsed: ${JSON.stringify(rawTree)} unparsed/failed: ${utfSpecial2escaped(stringAsArray.join(''))}`); let lastTree, tree = rawTree; do { lastTree = JSON.stringify(tree); @@ -119,7 +121,11 @@ function trivialDedup(tree) { } export function splitAround(pattern,treeStruct){ function recSplitter(treeStruct){ - if (isString(treeStruct)) return treeStruct.str.includes(pattern) ? {matching:treeStruct.str} : {notMatching:treeStruct.str}; + if (isString(treeStruct)) { + if(!treeStruct.str.includes(pattern)) return {notMatching:treeStruct.str}; + if(treeStruct.str.split(pattern).length>2) throw new Error('Error: @@ can only appear once in expression (idSec at left, password à right)'); + return {matching:treeStruct.str}; + } if (isStep(treeStruct)) { let isMatch = 0; let isAlt = false; @@ -162,7 +168,7 @@ export function splitAround(pattern,treeStruct){ return res; } export function serialize(treeStruct) { - if (isString(treeStruct)) return treeStruct.str; + if (isString(treeStruct)) return utfSpecial2escaped(treeStruct.str); if (isStep(treeStruct)) return treeStruct.step.map(serialize).join(''); if (isAlt(treeStruct)) return `(${treeStruct.alt.map(serialize).join('|')})`; throw new Error(`Error: how to serialize ${JSON.stringify(treeStruct)} RAW: ${treeStruct}`); @@ -206,6 +212,10 @@ export function altCount(treeStruct) { } export function getAlternative(altIndex, tree) { + const refAltIndex = {index: altIndex}; + return utfSpecial2unEscaped(_getAlternative(refAltIndex, tree)); +} +export function getRawAlternative(altIndex, tree) { const refAltIndex = {index: altIndex}; return _getAlternative(refAltIndex, tree); } diff --git a/src/dictionary-tree.test.mjs b/src/dictionary-tree.test.mjs index c71dc7e03810b6f223a7f82e705e00af90234e6f..48e0cdcf731379f8440ccf1783716ef6e107faed 100644 --- a/src/dictionary-tree.test.mjs +++ b/src/dictionary-tree.test.mjs @@ -13,11 +13,12 @@ test('(b|b) trivial dedup', t => t.is(buildTreeThenSerialize('(|b||b|)'), '(|b)' test('a(b|c) mix fix and alt', t => t.is(buildTreeThenSerialize('a(b|c)'), 'a(b|c)')); test('a(b) flat merge when no alt', t => t.is(buildTreeThenSerialize('a(b)'), 'ab')); test('(a(b|c)|(d|e)) flat merge when unneeded depth', t => t.is(buildTreeThenSerialize('(a(b|c)|(d|e))'), '(a(b|c)|d|e)')); -test('build complexe tree with (|) pattern', t => t.is(buildTreeThenSerialize('(a(|b@@@@c|d|)|(e|f)|g|h@@@@i)'), '(a(|b@@@@c|d)|e|f|g|h@@@@i)')); +test('build complexe tree with (|) pattern', t => t.is(buildTreeThenSerialize('(a(|b@@c|d|)|(e|f)|g|h@@i)'), '(a(|b@@c|d)|e|f|g|h@@i)')); test('serialize incorrect tree throw', t => t.throws(() => app.serialize({plop: ['a']}))); -test('splitAround incorrect tree throw', t => t.throws(() => app.splitAround('@@',{plop: ['a']}))); +test('splitAround throw when to many split', t => t.throws(() => app.splitAround('@@',app.buildTreeStruct('z@@a(b|c@@d)')))); +test('splitAround throw with @@@@', t => t.throws(() => app.splitAround('@@',app.buildTreeStruct('@@@@')))); test('splitAround return notMatching case', t => t.deepEqual(app.splitAround('@',app.buildTreeStruct('a(b|c)')),{notMatching:'a(b|c)'})); test('splitAround return matching case', t => t.deepEqual(app.splitAround('@',app.buildTreeStruct('a@b')),{matching:'a@b'})); test('splitAround return both matching case and not matching one', t => t.deepEqual(app.splitAround('@',app.buildTreeStruct('a@b|c')),{matching:'a@b',notMatching:'c'})); @@ -25,7 +26,7 @@ test('splitAround return both matching case and not matching one', t => t.deepEq test('mono altCount', t => t.is(app.altCount(app.buildTreeStruct('ipsum')), 1)); test('simple altCount', t => t.is(app.altCount(app.buildTreeStruct('(lore|ipsu)m')), 2)); test('multi altCount', t => t.is(app.altCount(app.buildTreeStruct('(a|b|c)(d|e|f)g(h|i|j|k)')), 36)); -test('multi level tree altCount', t => t.is(app.altCount(app.buildTreeStruct('a(b(c|d)|e(f|g|h)ij(k|l)|@@@@m)')), 9)); +test('multi level tree altCount', t => t.is(app.altCount(app.buildTreeStruct('a(b(c|d)|e(f|g|h)ij(k|l)|@@m)')), 9)); const exampleTree = () => app.buildTreeStruct('a(b(c|d)|e(f|g(h|i)|j)kl(m|n(o|p)|q(r|s)|t)|(u|v)w)'); // console.log(JSON.stringify(exampleTree())); @@ -61,3 +62,12 @@ test('getAlternative 20', t => t.is(app.getAlternative(20, exampleTree()), 'aejk test('getAlternative 26', t => t.is(app.getAlternative(26, exampleTree()), 'auw')); test('getAlternative 27', t => t.is(app.getAlternative(27, exampleTree()), 'avw')); test('getAlternative 28 or more throw', t => t.throws(() => app.getAlternative(28, exampleTree()))); + +test('escaped special characters are reconverted with getAlternative but not with getRawAlternative', t => { + const tree = app.buildTreeStruct('a\\(b(c|d)@@e'); + t.is(app.getAlternative(0,tree), 'a(bc@@e'); + t.is(app.getRawAlternative(0,tree), `a${String.fromCharCode(0x01)}bc@@e`); + const treeWhereRawIsUseful = app.buildTreeStruct('a\\@\\@b(c|d)@@e'); + t.is(app.getAlternative(0,treeWhereRawIsUseful), 'a@@bc@@e'); + t.is(app.getRawAlternative(0,treeWhereRawIsUseful), `a${String.fromCharCode(0x0e)+String.fromCharCode(0x0e)}bc@@e`); +}); diff --git a/src/dictionary.mjs b/src/dictionary.mjs index 6c7be142ea7f8ca16dea33135dff7f5496a47d46..1aa138be80eb92840d2dbe88140c7b1cc489162e 100644 --- a/src/dictionary.mjs +++ b/src/dictionary.mjs @@ -1,5 +1,6 @@ -import {buildTreeStruct, getAlternative, serialize} from "./dictionary-tree.mjs"; +import {buildTreeStruct, getRawAlternative, serialize} from "./dictionary-tree.mjs"; import {parse} from "./dictionary-parser.mjs"; +import {utfSpecial2unEscaped} from "./dictionary-escaper.mjs"; export class Dictionary { constructor(dictionaryString, options= {}) { @@ -17,11 +18,13 @@ export class Dictionary { this.tried = 0; this.cache = []; this.duplicatedCount = ()=> Object.keys(this.cache).length? this.tried - Object.keys(this.cache).length : 0; - this.dryGet = index => getAlternative(index,this.tree); - this.get = index => { + this.dryGet = index => utfSpecial2unEscaped(this.rawDryGet(index)); + this.rawDryGet = index => getRawAlternative(index,this.tree); + this.get = index => utfSpecial2unEscaped(this.rawGet(index)); + this.rawGet = index => { if(typeof this.startTime === "undefined") this.startTime = Date.now(); this.tried++; - const alt = getAlternative(index,this.tree); + const alt = getRawAlternative(index,this.tree); if(this.config.cache && this.length < this.config.cacheMax) { if(typeof this.cache[alt] === "undefined") this.cache[alt] = []; this.cache[alt].push(index); @@ -29,6 +32,18 @@ export class Dictionary { } return alt; } + function _split(str){ + const parts = str.split('@@'); + const idSec = utfSpecial2unEscaped(parts[0]); + const pwd = utfSpecial2unEscaped(parts[parts.length - 1]); + const res = [idSec,pwd]; + res.idSec = idSec; + res.pwd = pwd; + res.pass = pwd; + return res; + } + this.splitGet = index => _split(this.rawGet(index)); + this.splitDryGet = index => _split(this.rawDryGet(index)); this.timeSpent = ()=>(Date.now()-this.startTime)/1000; this.duplicatedFound = ()=>{ const duplicated = []; @@ -37,6 +52,7 @@ export class Dictionary { return sortedDuplicate; } this.dryRunDedup = ()=>dryRunDedup(this); + this.serialize = ()=>serialize(this.tree); } } function adjustVariant(self){ diff --git a/src/dictionary.test.mjs b/src/dictionary.test.mjs index d87278834cf0582937bce11a46546682bcca1e7b..027da3867300d6e5b4feca1caf2c9c4af3480fdf 100644 --- a/src/dictionary.test.mjs +++ b/src/dictionary.test.mjs @@ -1,6 +1,9 @@ import test from 'ava'; import * as app from './dictionary.mjs'; +function sleep(ms){ + return new Promise((resolve)=>setTimeout(resolve,ms)); +} test('get dictionary length', t => { const dictionaryString = '(a|b|c)d(e|f|g)'; const dico = new app.Dictionary(dictionaryString,{idSecPwd:false}); @@ -11,14 +14,52 @@ test('get dictionary iteration', t => { const dico = new app.Dictionary(dictionaryString); t.is(dico.get(5), "ade@@bdg"); }); -test('get past iteration count in this dictionary', t => { - const dictionaryString = '(a|b|c)d(e|f|g)'; - const dico = new app.Dictionary(dictionaryString); +test('get iteration are tracked', t => { + const dico = new app.Dictionary('(a|b|c)d(e|f|g)'); t.is(dico.tried, 0); dico.get(1); dico.get(2); t.is(dico.tried, 2); }); +test('dryGet iteration are not tracked', t => { + const dico = new app.Dictionary('(a|b|c)d(e|f|g)'); + t.is(dico.tried, 0); + dico.dryGet(1); + dico.dryGet(2); + t.is(dico.tried, 0); +}); +test('_\\@\\@_@@_@\\@_ can be ambiguous with get, dryGet or not with rawGet, rawDryGet, splitGet', t => { + const dico = new app.Dictionary('(\\@\\@|_@@\\)@\\@)', {cache:false}); + t.is(dico.length, 2); + t.is(dico.dryGet(0), '_@@)@@'); + t.is(dico.dryGet(1), '@@@@@@'); + t.is(dico.get(1), '@@@@@@'); + const escAro = String.fromCharCode(0x0e); + t.is(dico.rawGet(0), `_@@${String.fromCharCode(0x02)}@${escAro}`); + t.is(dico.rawDryGet(1), `${escAro+escAro}@@${escAro+escAro}`); + t.is(dico.splitGet(0)[0], '_'); + t.is(dico.splitGet(0)[1], ')@@'); + t.is(dico.splitGet(0).idSec, '_'); + t.is(dico.splitGet(0).pwd, ')@@'); + t.is(dico.splitGet(0).pass, ')@@'); + t.is(dico.splitDryGet(0)[0], '_'); + t.is(dico.splitDryGet(0)[1], ')@@'); +}); + +test('get is time tracked', async t => { + const dico = new app.Dictionary('(a|b|c)d(e|f|g)'); + dico.get(1); + await sleep(5); + dico.get(2); + t.true(dico.timeSpent()>=0.005); +}); +test('estimateDuration && estimateRemaining', t => { + const dico = new app.Dictionary('(a|b|c)d(e|f|g)'); + dico.get(1); + dico.get(2); + t.is(dico.estimateDuration(), 81); + t.is(dico.estimateRemaining(), 79); +}); test('get duplicated found count (from dictionary)', t => { const dictionaryString = '(a|b|c)d(e|f|g)'; const dico = new app.Dictionary(dictionaryString); @@ -107,3 +148,10 @@ test("huge alt number apply variants if set", t => { t.is((new app.Dictionary(`[0-9]{${2+x}}@@[A-Z]`, {lowerCase:0})).length,2_600 * 10**x); t.is((new app.Dictionary(`[0-9]{${2+x}}@@[A-Z]`, {lowerCase:1})).length,5_200 * 10**x); }); +test('escaped special characters still escaped when re-serialized', t => { + const dictionaryString = '(a|b|c\\)c)d(e|f|g)'; + const dico = new app.Dictionary(dictionaryString, {idSecPwd:false}); + const serialized = dico.serialize(); + t.is(serialized, dictionaryString); + t.is(dico.get(6), 'c)cde'); +});