diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 7f234a21371d08c1f00b9dc1227467691837b346..241bf645e94eaeb87193624ceb54a063c77a76c9 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -14,6 +14,10 @@ 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.6] - 2023-01-01 (par [1000i100]) +### Corrections +- dictionary-parser génère désormais correctement les variantes désaccentuées et majuscule/minuscule, sans couplage entre identifiant secret et mot de passe. + ## [Version 3.5.5] - 2022-12-31 (par [1000i100]) ### Corrections - Dans dictionary et dictionary-parser, les différents modes autour des majuscules sont désormais strictement progressif : 0 tel quel, 1 TOUT MAJUSCULE + tout minuscule + Première Lettre En Majuscule, 2 comme 1 mais en passant en minuscule chaque caractère individuellement. @@ -148,8 +152,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.4...main +[Non-publié/Non-Stabilisé]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.5.6...main +[Version 3.5.6]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.5.5...v3.5.6 +[Version 3.5.5]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.5.4...v3.5.5 [Version 3.5.4]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.5.3...v3.5.4 [Version 3.5.3]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.5.2...v3.5.3 [Version 3.5.2]: https://git.duniter.org/libs/g1lib.js/-/compare/v3.5.1...v3.5.2 diff --git a/npm/package.json b/npm/package.json index 90d303c14dd2e67684b63489a6790bcf5fcfb33d..d9c026445f9ca13be174472fa902820d189a7ee9 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "g1lib", - "version": "3.5.5", + "version": "3.5.6", "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 index 11155e58229b9db7b5024e2b7d8a513d96e6796b..bd62c08e6c792babffb1e43fe65fffd4646cc8a0 100644 --- a/src/dictionary-escaper.mjs +++ b/src/dictionary-escaper.mjs @@ -1,19 +1,19 @@ const specialMap = { - '(': String.fromCharCode(0x2001), - ')': String.fromCharCode(0x2002), - '|': String.fromCharCode(0x2003), - '{': String.fromCharCode(0x2004), - '}': String.fromCharCode(0x2005), - ',': String.fromCharCode(0x2006), - '[': String.fromCharCode(0x2007), - ']': String.fromCharCode(0x2008), - '-': String.fromCharCode(0x2009), - '<': String.fromCharCode(0x200a), - '>': String.fromCharCode(0x200b), - ':': String.fromCharCode(0x200c), - '=': String.fromCharCode(0x200d), - '@': String.fromCharCode(0x200e), - '\\': String.fromCharCode(0x200f), + '(': String.fromCharCode(0x2300), + ')': String.fromCharCode(0x2301), + '|': String.fromCharCode(0x2302), + '{': String.fromCharCode(0x2303), + '}': String.fromCharCode(0x2304), + ',': String.fromCharCode(0x2305), + '[': String.fromCharCode(0x2306), + ']': String.fromCharCode(0x2307), + '-': String.fromCharCode(0x2308), + '<': String.fromCharCode(0x2309), + '>': String.fromCharCode(0x230a), + ':': String.fromCharCode(0x230b), + '=': String.fromCharCode(0x230c), + '@': String.fromCharCode(0x230d), + '\\': String.fromCharCode(0x230e), }; const revertSpecial = swapKeyValue(specialMap); function swapKeyValue(object) { diff --git a/src/dictionary-parser.mjs b/src/dictionary-parser.mjs index a3d2da8a58bb6a353052df150bdd40988dc12099..277f56d76e989513230ff0f3b5bae4d5526636ce 100644 --- a/src/dictionary-parser.mjs +++ b/src/dictionary-parser.mjs @@ -11,10 +11,13 @@ export function parse(dictionaryString,options={}) { if(typeof options.leetSpeak === 'undefined') options.leetSpeak = 0; const escapeAllString = options.escapeAll===1?escapeAll(dictionaryString):options.escapeAll===2?`${dictionaryString}\n${escapeAll(dictionaryString)}`:dictionaryString; const escapedString = escape2utfSpecial(escapeAllString); - const unbracketed = bracketsHandler(escapedString); - const allLines = unbracketed.split('\n'); + const refConverted = refConverter(escapedString); + const unbracketed = bracketsHandler(refConverted); + const quantized = qtyHandler(unbracketed); + const allLines = quantized.split('\n'); const parsedLines = parseAllLines(allLines); - const monoLined = `(${parsedLines.join('|')})` + const enhancedLines = parsedLines.map(l=>addVariants(l,options)); + const monoLined = `(${enhancedLines.join('|')})` .replace(/§void§\|/g,'') .replace(/\|§void§/g,'') .replace(/§void§/g,''); @@ -27,10 +30,23 @@ export function parse(dictionaryString,options={}) { const combined = combineUnspecified2IdSecPwd(flatParts); return parseEnd(combined,allLines,options); } -function parseEnd(str,allLines,options) { - let theString = syncRefHandler(str,allLines) - if(theString.match(/=[^>]+>/)) throw new Error(`Unable to parse : ${utfSpecial2escaped(flattenIt(theString))}`); - theString = qtyHandler(theString); +function refConverter(str) { + str = str.replace(/(^|\n)([^:\n]+)::/g,(full,before,capture)=>`${before}§!§${capture}::`); + str = str.replace(/<([^<=>]+)>/g,(full,capture)=>`<§!§${capture}>`); + str = str.replace(/=([^<=>]+)>/g,(full,capture)=>`=§!§${capture}>`); + let ref = 0x2460; + while (1){ + try{ + let refName = str.match(/§!§([^<:=>]+)(::|>)/)[1]; + str = str.replace(new RegExp(`§!§${refName}(::|>)`,'g'),(full, end)=>`${String.fromCharCode(ref)}${end}`); + ref++; + } catch (e) { + break; + } + } + return str; +} +function addVariants(theString,options) { if(options.accent===1) theString = zeroAccent(theString); if(options.accent===2) theString = optionalAccent(theString); if(options.lowerCase===1) theString = allCase(theString); @@ -39,6 +55,11 @@ function parseEnd(str,allLines,options) { if(options.leetSpeak===2) theString = allLeetSpeak(theString); if(options.leetSpeak===3) theString = everyLeetSpeak(theString); //TODO: bigLeet Variants + return theString; +} +function parseEnd(str,allLines,options) { + let theString = syncRefHandler(str,allLines) + if(theString.match(/=[^>]+>/)) throw new Error(`Unable to parse : ${utfSpecial2escaped(flattenIt(theString))}`); return utfSpecial2escaped(flattenIt(theString)); } function zeroVariant(str,func){ @@ -197,16 +218,28 @@ function qtyHandlerReplaceCallback(all, chr, qty) { } function qtyHandler(theString) { // Handle {qty} and {min,max} - theString = theString.replace(/([^)]){([^}]+)}/g, qtyHandlerReplaceCallback); - theString = theString.replace(/^(.*)\(([^)]*)\){([^}]+)}(.*)$/, (all, before, choices, qty, after) => before + qtyHandlerReplaceCallback('', `(${choices})`, qty) + after); // eslint-disable-line max-params + theString = theString.replace(/([^>)]){([^}]+)}/g, qtyHandlerReplaceCallback); while(1){ const qtyFound = rightParser(theString,'{','}'); if(!qtyFound.before || !qtyFound.inside) break; - const qtyPerimeter = rightParser(qtyFound.before,'(',')'); - theString = qtyPerimeter.before + qtyHandlerReplaceCallback('',`(${qtyPerimeter.inside})`,qtyFound.inside)+qtyFound.after; + const qtyPerimeter = selectPerimeter(qtyFound.before); + theString = qtyPerimeter.before + qtyHandlerReplaceCallback('',qtyPerimeter.inside,qtyFound.inside)+qtyFound.after; } return theString; } +function selectPerimeter(str){ + let candidates = [ + rightParser(str,'(',')'), + rightParser(str,'<','>'), + rightParser(str,'=','>'), + ]; + candidates[0].inside = `(${candidates[0].inside})`; + candidates[1].inside = `<${candidates[1].inside}>`; + candidates[2].inside = `=${candidates[2].inside}>`; + candidates = candidates.filter(c=>c.after.length===0).filter(c=>c.inside.length>2); + if(candidates.length===1) return candidates[0]; + return candidates[0].inside.length<candidates[1].inside.length?candidates[0]:candidates[1]; +} function rightParser(fullString,openLeftChr,closeRightChr) { const chrList = fullString.split(''); const res = { diff --git a/src/dictionary-tree.test.mjs b/src/dictionary-tree.test.mjs index b8bdfecf7a4051b333652da2b1efa7657e3d3d15..5cf98e3ca5b33a1ed9618bd73c8f168d557c0d84 100644 --- a/src/dictionary-tree.test.mjs +++ b/src/dictionary-tree.test.mjs @@ -68,9 +68,9 @@ test('getAlternative 28 or more throw', t => t.throws(() => app.getAlternative(2 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(0x2001)}bc@@e`); + t.is(app.getRawAlternative(0,tree), `a${String.fromCharCode(0x2300)}bc@@e`); const treeWhereRawIsUseful = app.buildTreeStruct('a\\@\\@b(c|d)@@e'); t.is(app.getAlternative(0,treeWhereRawIsUseful), 'a@@bc@@e'); - const escAro = String.fromCharCode(0x200e); + const escAro = String.fromCharCode(0x230d); t.is(app.getRawAlternative(0,treeWhereRawIsUseful), `a${escAro+escAro}bc@@e`); }); diff --git a/src/dictionary.mjs b/src/dictionary.mjs index 0411a592bbfb2f03f2d7a8093715879ec195ca5e..8e6601ed165053f019fb5b7b9ae6b6508d64bbc0 100644 --- a/src/dictionary.mjs +++ b/src/dictionary.mjs @@ -62,6 +62,9 @@ export class Dictionary { this.serialize = ()=>serialize(this.tree); } } +function isAuto(evaluable){ + return evaluable === -1 || typeof evaluable === 'undefined' || evaluable === "auto" || evaluable === '-1'; +} function adjustVariant(self){ const durationGoal = 3_600; if (self.estimateDuration() >= durationGoal) return; @@ -74,24 +77,24 @@ function adjustVariant(self){ while (self.estimateDuration() < durationGoal && self.length < self.config.cacheMax){ //console.log(`${parseInt(self.estimateDuration())}s, alt:${self.length} \\:${self.config.escapeAll} à :${self.config.accent} M:${self.config.lowerCase} 3:${self.config.leetSpeak}`, serialize(self.tree)); lastLess1hConfig = JSON.parse(JSON.stringify(self.config)); - if(self.config.escapeAll !== 2 && (typeof self.originalConfig.escapeAll === 'undefined' || self.originalConfig.escapeAll === "auto")) { + if(self.config.escapeAll !== 2 && isAuto(self.originalConfig.escapeAll)) { self.config.escapeAll = 2; rebuildTree(self); continue; } - if(self.config.accent !== 2 && (typeof self.originalConfig.accent === 'undefined' || self.originalConfig.accent === "auto")) { + if(self.config.accent !== 2 && isAuto(self.originalConfig.accent)) { if(!self.config.accent) self.config.accent = 1; else self.config.accent++; rebuildTree(self); continue; } - if(self.config.lowerCase !== 2 && (typeof self.originalConfig.lowerCase === 'undefined' || self.originalConfig.lowerCase === "auto")) { + if(self.config.lowerCase !== 2 && isAuto(self.originalConfig.lowerCase)) { if(!self.config.lowerCase) self.config.lowerCase = 1; else self.config.lowerCase++; rebuildTree(self); continue; } - if(self.config.leetSpeak !== 3 && (typeof self.originalConfig.leetSpeak === 'undefined' || self.originalConfig.leetSpeak === "auto")) { + if(self.config.leetSpeak !== 3 && isAuto(self.originalConfig.leetSpeak)) { if(!self.config.leetSpeak) self.config.leetSpeak = 1; else self.config.leetSpeak++; rebuildTree(self); diff --git a/src/dictionary.test.mjs b/src/dictionary.test.mjs index d25df880589791d01bb6fbee12d226bc7b4df290..5be56784951adccd5f8486e10d919d70d3aa70e7 100644 --- a/src/dictionary.test.mjs +++ b/src/dictionary.test.mjs @@ -34,8 +34,8 @@ test('_\\@\\@_@@_@\\@_ can be ambiguous with get, dryGet or not with rawGet, raw t.is(dico.dryGet(0), '_@@)@@'); t.is(dico.dryGet(1), '@@@@@@'); t.is(dico.get(1), '@@@@@@'); - const escAro = String.fromCharCode(0x200e); - t.is(dico.rawGet(0), `_@@${String.fromCharCode(0x2002)}@${escAro}`); + const escAro = String.fromCharCode(0x230d); + t.is(dico.rawGet(0), `_@@${String.fromCharCode(0x2301)}@${escAro}`); t.is(dico.rawDryGet(1), `${escAro+escAro}@@${escAro+escAro}`); t.is(dico.splitGet(0)[0], '_'); t.is(dico.splitGet(0)[1], ')@@'); @@ -116,11 +116,11 @@ test('duplicate match §duplicate§ pattern', t => { }); test('dictionary called with speed option try to activate variante accent, caps and leetSpeak option to reach 1h of compute estimated time', t => { const dictionaryString = 'Ǧ3Ǧo'; - const dico = new app.Dictionary(dictionaryString, {speed:300}); - t.is(dico.length,526257); - t.is(dico.duplicateTotal,194481); - t.is(dico.duplicateRatio.toPrecision(2),'0.37'); - t.is(dico.uniqueRatio.toPrecision(2),'0.63'); + const dico = new app.Dictionary(dictionaryString, {speed:30}); + t.is(dico.length,2025); + t.is(dico.duplicateTotal,729); + t.is(dico.duplicateRatio.toPrecision(2),'0.36'); + t.is(dico.uniqueRatio.toPrecision(2),'0.64'); }); test('auto activate leetSpeak for tiny case', t => { const dictionaryString = 'a'; @@ -131,7 +131,7 @@ test('auto activate leetSpeak for tiny case', t => { test('dico should not crash', t=>{ const v8CrashStringButFirefoxWork = '(((Ǧ|ǧ)|(G|g))(1|i|l|I)((Ǧ|ǧ)|(G|g))(1|i|l|I)@@((Ǧ|ǧ)|(G|g))(1|i|l|I)((Ǧ|ǧ)|(G|g))(1|i|l|I)|(((Ǧ|ǧ)|(G|g))(1|i|l|I)((Ǧ|ǧ)|(G|g))(1|i|l|I)@@((Ǧ|ǧ)|(G|g))(1|i|l|I)((Ǧ|ǧ)|(G|g))(1|i|l|I)|(ǧ|g)(1|i|l|I)(ǧ|g)(1|i|l|I)@@(ǧ|g)(1|i|l|I)(ǧ|g)(1|i|l|I)))'; const dico = new app.Dictionary(v8CrashStringButFirefoxWork,{speed:30}) - t.is(dico.length,73986); + t.is(dico.length,73988); t.is(dico.duplicateTotal,8448); t.is(dico.duplicateRatio.toFixed(3),'0.114'); t.is(dico.uniqueRatio.toFixed(3),'0.886'); @@ -186,13 +186,18 @@ test('escapeAll:2 -> everything is escaped', t => { }); test('escapeAll:auto -> try 2 if too slow 0 if previous fail 1', t => { const dico = new app.Dictionary('[0-1]{3}@@=autre>\nautre::ça',{escapeAll:'auto',speed:30}); - t.is(dico.length, 154); + t.is(dico.length, 537); }); test('escapeAll:undefined -> like auto', t => { const dico = new app.Dictionary('[0-1]{3}@@=autre>\nautre::ça',{speed:30}); - t.is(dico.length, 154); + t.is(dico.length, 537); }); test('invalid string -> autofallback to escapeAll:1', t => { const dico = new app.Dictionary('[0-1]{z}@@=autre>\nautre::ça'); t.is(dico.length, 4); }); +test(`"aa" with default settings & a speed should generate alt including "Aa@@AA"`, t => { + const dico = new app.Dictionary('aa',{speed:30, leetSpeak:0}); + for(let i=0;i<dico.length;i++) dico.splitGet(i); + t.is(dico.cache['Aa@@AA'][0], 2); +});