From 804c53527008e62ffcd296e3f36543c472b96fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Wr=C3=B3blewski?= Date: Thu, 3 Aug 2023 19:48:01 +0300 Subject: [PATCH 1/3] =?UTF-8?q?AG-24484=20Improve=20'prevent-xhr'=20?= =?UTF-8?q?=E2=80=94=20multiple=20requests.=20#347?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit 7de58b6397170e4963d1f1447b681489ef4a2424 Author: Adam Wróblewski Date: Tue Aug 1 20:15:15 2023 +0200 Fix issue with multiple requests in prevent-xhr --- CHANGELOG.md | 9 ++++- src/scriptlets/prevent-xhr.js | 15 ++++++-- tests/scriptlets/prevent-xhr.test.js | 52 ++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d2613699..f7258a97f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [Unreleased] + +### Fixed + +- `prevent-xhr` closure bug on multiple requests + [#347](https://github.com/AdguardTeam/Scriptlets/issues/347) + ## [v1.9.61] - 2023-08-01 ### Added @@ -219,7 +226,7 @@ prevent inline `onerror` and match `link` tag [#276](https://github.com/AdguardT - `metrika-yandex-tag` [#254](https://github.com/AdguardTeam/Scriptlets/issues/254) - `googlesyndication-adsbygoogle` [#252](https://github.com/AdguardTeam/Scriptlets/issues/252) - +[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.58...HEAD [v1.9.58]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.57...v1.9.58 [v1.9.57]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.37...v1.9.57 [v1.9.37]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.7...v1.9.37 diff --git a/src/scriptlets/prevent-xhr.js b/src/scriptlets/prevent-xhr.js index 4d47e9f3e..83ebc6ee0 100644 --- a/src/scriptlets/prevent-xhr.js +++ b/src/scriptlets/prevent-xhr.js @@ -112,6 +112,8 @@ export function preventXHR(source, propsToMatch, customResponseText) { const nativeOpen = window.XMLHttpRequest.prototype.open; const nativeSend = window.XMLHttpRequest.prototype.send; + const nativeGetResponseHeader = window.XMLHttpRequest.prototype.getResponseHeader; + const nativeGetAllResponseHeaders = window.XMLHttpRequest.prototype.getAllResponseHeaders; let xhrData; let modifiedResponse = ''; @@ -128,6 +130,9 @@ export function preventXHR(source, propsToMatch, customResponseText) { hit(source); } else if (matchRequestProps(source, propsToMatch, xhrData)) { thisArg.shouldBePrevented = true; + // Add xhrData to thisArg to keep original values in case of multiple requests + // https://github.com/AdguardTeam/Scriptlets/issues/347 + thisArg.xhrData = xhrData; } // Trap setRequestHeader of target xhr object to mimic request headers later; @@ -194,7 +199,7 @@ export function preventXHR(source, propsToMatch, customResponseText) { readyState: { value: readyState, writable: false }, statusText: { value: statusText, writable: false }, // If the request is blocked, responseURL is an empty string - responseURL: { value: responseURL || xhrData.url, writable: false }, + responseURL: { value: responseURL || thisArg.xhrData.url, writable: false }, responseXML: { value: responseXML, writable: false }, // modified values status: { value: 200, writable: false }, @@ -217,7 +222,7 @@ export function preventXHR(source, propsToMatch, customResponseText) { hit(source); }); - nativeOpen.apply(forgedRequest, [xhrData.method, xhrData.url]); + nativeOpen.apply(forgedRequest, [thisArg.xhrData.method, thisArg.xhrData.url]); // Mimic request headers before sending // setRequestHeader can only be called on open request objects @@ -246,6 +251,9 @@ export function preventXHR(source, propsToMatch, customResponseText) { * @returns {string|null} Header value or null if header is not set. */ const getHeaderWrapper = (target, thisArg, args) => { + if (!thisArg.shouldBePrevented) { + return nativeGetResponseHeader.apply(thisArg, args); + } if (!thisArg.collectedHeaders.length) { return null; } @@ -270,6 +278,9 @@ export function preventXHR(source, propsToMatch, customResponseText) { * @returns {string} All headers as a string. For no headers an empty string is returned. */ const getAllHeadersWrapper = (target, thisArg) => { + if (!thisArg.shouldBePrevented) { + return nativeGetAllResponseHeaders.call(thisArg); + } if (!thisArg.collectedHeaders.length) { return ''; } diff --git a/tests/scriptlets/prevent-xhr.test.js b/tests/scriptlets/prevent-xhr.test.js index 57267d949..dad85d447 100644 --- a/tests/scriptlets/prevent-xhr.test.js +++ b/tests/scriptlets/prevent-xhr.test.js @@ -696,6 +696,7 @@ if (isSupported) { xhr1.onload = () => { assert.strictEqual(xhr1.readyState, 4, 'Response done'); + assert.ok(xhr1.responseURL.includes(URL_TO_PASS.substring(1)), 'Origianl URL mocked'); assert.ok(xhr1.response, 'Response data exists'); assert.strictEqual(window.hit, undefined, 'hit should not fire'); done(); @@ -703,6 +704,7 @@ if (isSupported) { xhr2.onload = () => { assert.strictEqual(xhr2.readyState, 4, 'Response done'); + assert.ok(xhr2.responseURL.includes(URL_TO_BLOCK.substring(1)), 'Origianl URL mocked'); assert.strictEqual(typeof xhr2.responseText, 'string', 'Response text mocked'); assert.strictEqual(window.hit, 'FIRED', 'hit function fired'); clearGlobalProps('hit'); @@ -713,6 +715,56 @@ if (isSupported) { // use timeout to avoid hit collisions setTimeout(() => xhr2.send(), 10); }); + + // https://github.com/AdguardTeam/Scriptlets/issues/347 + test('Works correctly with different parallel XHR requests and blocked request', async (assert) => { + const METHOD = 'GET'; + // advert.js does not exist, it imitate blocked request + const URL_TO_BLOCK = `${FETCH_OBJECTS_PATH}/advert.js`; + const URL_TO_PASS = `${FETCH_OBJECTS_PATH}/test01.json`; + const MATCH_DATA = ['advert.js']; + + runScriptlet(name, MATCH_DATA); + + const done = assert.async(2); + + const xhr1 = new XMLHttpRequest(); + const xhr2 = new XMLHttpRequest(); + + xhr1.open(METHOD, URL_TO_BLOCK); + xhr2.open(METHOD, URL_TO_PASS); + + xhr1.onload = () => { + const responseHeader = xhr1.getResponseHeader('date'); + const responseHeaders = xhr1.getAllResponseHeaders(); + + assert.strictEqual(xhr1.readyState, 4, 'Response done'); + assert.ok(responseHeaders.length === 0, 'Response header is empty'); + assert.strictEqual(responseHeader, null, 'Response header date returns null'); + assert.strictEqual(typeof xhr1.responseText, 'string', 'Response text'); + assert.ok(xhr1.responseText.length === 0, 'Response text is empty'); + assert.ok(xhr1.responseURL.includes(URL_TO_BLOCK.substring(1)), 'Origianl URL mocked'); + assert.strictEqual(window.hit, 'FIRED', 'hit function fired'); + done(); + }; + + xhr2.onload = () => { + const responseHeader = xhr2.getResponseHeader('date'); + const responseHeaders = xhr2.getAllResponseHeaders(); + + assert.strictEqual(xhr2.readyState, 4, 'Response done'); + assert.ok(responseHeaders.length > 0, 'Response header contains data'); + assert.ok(responseHeader.length > 0, 'Response header date is not empty'); + assert.ok(xhr2.responseURL.includes(URL_TO_PASS.substring(1)), 'Origianl URL mocked'); + assert.strictEqual(typeof xhr2.responseText, 'string', 'Response text mocked'); + clearGlobalProps('hit'); + done(); + }; + + xhr1.send(); + // use timeout to avoid hit collisions + setTimeout(() => xhr2.send(), 10); + }); } else { test('unsupported', (assert) => { assert.ok(true, 'Browser does not support it'); From 8d0ddfe9f7a605db9764a86874916364f0ea75e2 Mon Sep 17 00:00:00 2001 From: Atlassian Bamboo Date: Thu, 3 Aug 2023 19:48:11 +0300 Subject: [PATCH 2/3] skipci: Automatic increment build number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a8a88e6c6..baeb833df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adguard/scriptlets", - "version": "1.9.61", + "version": "1.9.62", "description": "AdGuard's JavaScript library of Scriptlets and Redirect resources", "scripts": { "build": "babel-node -x .js,.ts scripts/build.js", From 975ad118e2f115bfcf6624ff37c0b3255b07ded6 Mon Sep 17 00:00:00 2001 From: Atlassian Bamboo Date: Fri, 4 Aug 2023 11:47:35 +0300 Subject: [PATCH 3/3] deploy: update dist v1.9.62 --- dist/build.txt | 2 +- dist/redirects.yml | 2 +- dist/scriptlets.corelibs.json | 4 ++-- dist/scriptlets.js | 30 +++++++++++++++++++++++++----- dist/umd/scriptlets.umd.js | 30 +++++++++++++++++++++++++----- 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/dist/build.txt b/dist/build.txt index 96bce640c..074d65464 100644 --- a/dist/build.txt +++ b/dist/build.txt @@ -1 +1 @@ -version=1.9.61 \ No newline at end of file +version=1.9.62 \ No newline at end of file diff --git a/dist/redirects.yml b/dist/redirects.yml index 7a8655f29..f6727dcfe 100644 --- a/dist/redirects.yml +++ b/dist/redirects.yml @@ -1,6 +1,6 @@ # # AdGuard Scriptlets (Redirects Source) -# Version 1.9.61 +# Version 1.9.62 # - title: 1x1-transparent.gif added: v1.0.4 diff --git a/dist/scriptlets.corelibs.json b/dist/scriptlets.corelibs.json index b066645e9..46c02ee13 100644 --- a/dist/scriptlets.corelibs.json +++ b/dist/scriptlets.corelibs.json @@ -1,5 +1,5 @@ { - "version": "1.9.61", + "version": "1.9.62", "scriptlets": [ { "names": [ @@ -418,7 +418,7 @@ "ubo-no-xhr-if.js", "ubo-no-xhr-if" ], - "scriptlet": "function preventXHR(source,args){function hit(source){if(!0===source.verbose){try{var log=console.log.bind(console),trace=console.trace.bind(console),prefix=source.ruleText||\"\";if(source.domainName){var ruleStartIndex;source.ruleText.includes(\"#%#//\")?ruleStartIndex=source.ruleText.indexOf(\"#%#//\"):source.ruleText.includes(\"##+js\")&&(ruleStartIndex=source.ruleText.indexOf(\"##+js\"));var rulePart=source.ruleText.slice(ruleStartIndex);prefix=\"\".concat(source.domainName).concat(rulePart)}log(\"\".concat(prefix,\" trace start\")),trace&&trace(),log(\"\".concat(prefix,\" trace end\"))}catch(e){}\"function\"==typeof window.__debug&&window.__debug(source)}}function objectToString(obj){return obj&&\"object\"==typeof obj?function(obj){return 0===Object.keys(obj).length&&!obj.prototype}(obj)?\"{}\":Object.entries(obj).map((function(pair){var key=pair[0],value=pair[1],recordValueStr=value;return value instanceof Object&&(recordValueStr=\"{ \".concat(objectToString(value),\" }\")),\"\".concat(key,':\"').concat(recordValueStr,'\"')})).join(\" \"):String(obj)}function getXhrData(method,url,async,user,password){return{method:method,url:url,async:async,user:user,password:password}}function logMessage(source,message){var forced=arguments.length>2&&void 0!==arguments[2]&&arguments[2],convertMessageToString=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],name=source.name,verbose=source.verbose;if(forced||verbose){var nativeConsole=console.log;convertMessageToString?nativeConsole(\"\".concat(name,\": \").concat(message)):nativeConsole(\"\".concat(name,\":\"),message)}}function getNumberFromString(rawString){var num,parsedDelay=parseInt(rawString,10);return num=parsedDelay,(Number.isNaN||window.isNaN)(num)?null:parsedDelay}function nativeIsFinite(num){return(Number.isFinite||window.isFinite)(num)}var updatedArgs=args?[].concat(source).concat(args):[source];try{(function(source,propsToMatch,customResponseText){if(\"undefined\"!=typeof Proxy){var xhrData,nativeOpen=window.XMLHttpRequest.prototype.open,nativeSend=window.XMLHttpRequest.prototype.send,modifiedResponse=\"\",modifiedResponseText=\"\",openHandler={apply:function(target,thisArg,args){if(xhrData=getXhrData.apply(null,args),void 0===propsToMatch?(logMessage(source,\"xhr( \".concat(objectToString(xhrData),\" )\"),!0),hit(source)):function(source,propsToMatch,requestData){if(\"\"===propsToMatch||\"*\"===propsToMatch)return!0;var isMatched,PROPS_DIVIDER,PAIRS_MARKER,isRequestProp,propsObj,data,parsedData=(PROPS_DIVIDER=\" \",PAIRS_MARKER=\":\",isRequestProp=function(prop){return[\"url\",\"method\",\"headers\",\"body\",\"credentials\",\"cache\",\"redirect\",\"referrer\",\"referrerPolicy\",\"integrity\",\"keepalive\",\"signal\",\"mode\"].includes(prop)},propsObj={},propsToMatch.split(PROPS_DIVIDER).forEach((function(prop){var dividerInd=prop.indexOf(PAIRS_MARKER),key=prop.slice(0,dividerInd);if(isRequestProp(key)){var value=prop.slice(dividerInd+1);propsObj[key]=value}else propsObj.url=prop})),propsObj);if(data=parsedData,Object.values(data).every((function(value){return function(input){var isValid,FORWARD_SLASH=\"/\",str=function(str){return str.replace(/[.*+?^${}()|[\\]\\\\]/g,\"\\\\$&\")}(input);input[0]===FORWARD_SLASH&&input[input.length-1]===FORWARD_SLASH&&(str=input.slice(1,-1));try{isValid=new RegExp(str),isValid=!0}catch(e){isValid=!1}return isValid}(value)}))){var matchData=function(data){var matchData={};return Object.keys(data).forEach((function(key){matchData[key]=function(){var input=arguments.length>0&&void 0!==arguments[0]?arguments[0]:\"\",FORWARD_SLASH=\"/\";if(\"\"===input)return new RegExp(\".?\");var regExpStr,flagsStr,delimiterIndex=input.lastIndexOf(FORWARD_SLASH),flagsPart=input.substring(delimiterIndex+1),regExpPart=input.substring(0,delimiterIndex+1),isValidRegExpFlag=function(flag){if(!flag)return!1;try{return new RegExp(\"\",flag),!0}catch(ex){return!1}},flags=(flagsStr=flagsPart,(regExpStr=regExpPart).startsWith(FORWARD_SLASH)&®ExpStr.endsWith(FORWARD_SLASH)&&!regExpStr.endsWith(\"\\\\/\")&&isValidRegExpFlag(flagsStr)?flagsStr:\"\");if(input.startsWith(FORWARD_SLASH)&&input.endsWith(FORWARD_SLASH)||flags)return new RegExp((flags?regExpPart:input).slice(1,-1),flags);var escaped=input.replace(/\\\\'/g,\"'\").replace(/\\\\\"/g,'\"').replace(/[.*+?^${}()|[\\]\\\\]/g,\"\\\\$&\");return new RegExp(escaped)}(data[key])})),matchData}(parsedData);isMatched=Object.keys(matchData).every((function(matchKey){var matchValue=matchData[matchKey],dataValue=requestData[matchKey];return Object.prototype.hasOwnProperty.call(requestData,matchKey)&&\"string\"==typeof dataValue&&(null==matchValue?void 0:matchValue.test(dataValue))}))}else logMessage(source,\"Invalid parameter: \".concat(propsToMatch)),isMatched=!1;return isMatched}(source,propsToMatch,xhrData)&&(thisArg.shouldBePrevented=!0),thisArg.shouldBePrevented){thisArg.collectedHeaders=[];var setRequestHeaderHandler={apply:function(target,thisArg,args){return thisArg.collectedHeaders.push(args),Reflect.apply(target,thisArg,args)}};thisArg.setRequestHeader=new Proxy(thisArg.setRequestHeader,setRequestHeaderHandler)}return Reflect.apply(target,thisArg,args)}},sendHandler={apply:function(target,thisArg,args){if(!thisArg.shouldBePrevented)return Reflect.apply(target,thisArg,args);if(\"blob\"===thisArg.responseType&&(modifiedResponse=new Blob),\"arraybuffer\"===thisArg.responseType&&(modifiedResponse=new ArrayBuffer),customResponseText){var randomText=function(customResponseText){var customResponse=customResponseText;if(\"true\"===customResponse)return Math.random().toString(36).slice(-10);if(customResponse=customResponse.replace(\"length:\",\"\"),!/^\\d+-\\d+$/.test(customResponse))return null;var min,max,rangeMin=getNumberFromString(customResponse.split(\"-\")[0]),rangeMax=getNumberFromString(customResponse.split(\"-\")[1]);if(!nativeIsFinite(rangeMin)||!nativeIsFinite(rangeMax))return null;if(rangeMin>rangeMax){var temp=rangeMin;rangeMin=rangeMax,rangeMax=temp}return rangeMax>5e5?null:function(length){for(var result=\"\",characters=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+=~\",charactersLength=characters.length,i=0;i2&&void 0!==arguments[2]&&arguments[2],convertMessageToString=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],name=source.name,verbose=source.verbose;if(forced||verbose){var nativeConsole=console.log;convertMessageToString?nativeConsole(\"\".concat(name,\": \").concat(message)):nativeConsole(\"\".concat(name,\":\"),message)}}function getNumberFromString(rawString){var num,parsedDelay=parseInt(rawString,10);return num=parsedDelay,(Number.isNaN||window.isNaN)(num)?null:parsedDelay}function nativeIsFinite(num){return(Number.isFinite||window.isFinite)(num)}var updatedArgs=args?[].concat(source).concat(args):[source];try{(function(source,propsToMatch,customResponseText){if(\"undefined\"!=typeof Proxy){var xhrData,nativeOpen=window.XMLHttpRequest.prototype.open,nativeSend=window.XMLHttpRequest.prototype.send,nativeGetResponseHeader=window.XMLHttpRequest.prototype.getResponseHeader,nativeGetAllResponseHeaders=window.XMLHttpRequest.prototype.getAllResponseHeaders,modifiedResponse=\"\",modifiedResponseText=\"\",openHandler={apply:function(target,thisArg,args){if(xhrData=getXhrData.apply(null,args),void 0===propsToMatch?(logMessage(source,\"xhr( \".concat(objectToString(xhrData),\" )\"),!0),hit(source)):function(source,propsToMatch,requestData){if(\"\"===propsToMatch||\"*\"===propsToMatch)return!0;var isMatched,PROPS_DIVIDER,PAIRS_MARKER,isRequestProp,propsObj,data,parsedData=(PROPS_DIVIDER=\" \",PAIRS_MARKER=\":\",isRequestProp=function(prop){return[\"url\",\"method\",\"headers\",\"body\",\"credentials\",\"cache\",\"redirect\",\"referrer\",\"referrerPolicy\",\"integrity\",\"keepalive\",\"signal\",\"mode\"].includes(prop)},propsObj={},propsToMatch.split(PROPS_DIVIDER).forEach((function(prop){var dividerInd=prop.indexOf(PAIRS_MARKER),key=prop.slice(0,dividerInd);if(isRequestProp(key)){var value=prop.slice(dividerInd+1);propsObj[key]=value}else propsObj.url=prop})),propsObj);if(data=parsedData,Object.values(data).every((function(value){return function(input){var isValid,FORWARD_SLASH=\"/\",str=function(str){return str.replace(/[.*+?^${}()|[\\]\\\\]/g,\"\\\\$&\")}(input);input[0]===FORWARD_SLASH&&input[input.length-1]===FORWARD_SLASH&&(str=input.slice(1,-1));try{isValid=new RegExp(str),isValid=!0}catch(e){isValid=!1}return isValid}(value)}))){var matchData=function(data){var matchData={};return Object.keys(data).forEach((function(key){matchData[key]=function(){var input=arguments.length>0&&void 0!==arguments[0]?arguments[0]:\"\",FORWARD_SLASH=\"/\";if(\"\"===input)return new RegExp(\".?\");var regExpStr,flagsStr,delimiterIndex=input.lastIndexOf(FORWARD_SLASH),flagsPart=input.substring(delimiterIndex+1),regExpPart=input.substring(0,delimiterIndex+1),isValidRegExpFlag=function(flag){if(!flag)return!1;try{return new RegExp(\"\",flag),!0}catch(ex){return!1}},flags=(flagsStr=flagsPart,(regExpStr=regExpPart).startsWith(FORWARD_SLASH)&®ExpStr.endsWith(FORWARD_SLASH)&&!regExpStr.endsWith(\"\\\\/\")&&isValidRegExpFlag(flagsStr)?flagsStr:\"\");if(input.startsWith(FORWARD_SLASH)&&input.endsWith(FORWARD_SLASH)||flags)return new RegExp((flags?regExpPart:input).slice(1,-1),flags);var escaped=input.replace(/\\\\'/g,\"'\").replace(/\\\\\"/g,'\"').replace(/[.*+?^${}()|[\\]\\\\]/g,\"\\\\$&\");return new RegExp(escaped)}(data[key])})),matchData}(parsedData);isMatched=Object.keys(matchData).every((function(matchKey){var matchValue=matchData[matchKey],dataValue=requestData[matchKey];return Object.prototype.hasOwnProperty.call(requestData,matchKey)&&\"string\"==typeof dataValue&&(null==matchValue?void 0:matchValue.test(dataValue))}))}else logMessage(source,\"Invalid parameter: \".concat(propsToMatch)),isMatched=!1;return isMatched}(source,propsToMatch,xhrData)&&(thisArg.shouldBePrevented=!0,thisArg.xhrData=xhrData),thisArg.shouldBePrevented){thisArg.collectedHeaders=[];var setRequestHeaderHandler={apply:function(target,thisArg,args){return thisArg.collectedHeaders.push(args),Reflect.apply(target,thisArg,args)}};thisArg.setRequestHeader=new Proxy(thisArg.setRequestHeader,setRequestHeaderHandler)}return Reflect.apply(target,thisArg,args)}},sendHandler={apply:function(target,thisArg,args){if(!thisArg.shouldBePrevented)return Reflect.apply(target,thisArg,args);if(\"blob\"===thisArg.responseType&&(modifiedResponse=new Blob),\"arraybuffer\"===thisArg.responseType&&(modifiedResponse=new ArrayBuffer),customResponseText){var randomText=function(customResponseText){var customResponse=customResponseText;if(\"true\"===customResponse)return Math.random().toString(36).slice(-10);if(customResponse=customResponse.replace(\"length:\",\"\"),!/^\\d+-\\d+$/.test(customResponse))return null;var min,max,rangeMin=getNumberFromString(customResponse.split(\"-\")[0]),rangeMax=getNumberFromString(customResponse.split(\"-\")[1]);if(!nativeIsFinite(rangeMin)||!nativeIsFinite(rangeMax))return null;if(rangeMin>rangeMax){var temp=rangeMin;rangeMin=rangeMax,rangeMax=temp}return rangeMax>5e5?null:function(length){for(var result=\"\",characters=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+=~\",charactersLength=characters.length,i=0;i