-
Notifications
You must be signed in to change notification settings - Fork 14
/
shapeShifter.js
2084 lines (1906 loc) · 97.2 KB
/
shapeShifter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Node.js init
if (typeof require !== 'undefined') {
var PackerData = require("./packerData");
var StringHelper = require("./stringHelper");
var ContextDescriptor = require("./contextDescriptor_node");
}
/**
* @constructor
* ShapeShifter is the preprocessor used by RegPack.
* It shortens the code, without compression, by running those algorithms on the original code according to settings
* - puts the code in "with(Math)" environment
* - wraps the main code loop into a call to setInterval()
* - hashes method / property names for 2D / GL / Audio contexts
* - renames the variable to optimize compression
*/
function ShapeShifter() {
this.contextDescriptor = new ContextDescriptor();
this.stringHelper = StringHelper.getInstance();
// hashing functions for method and property renaming
this.hashFunctions = [
["w[x]", 0, 2, 0, 0, function(w,x,y) { return w[x]; } ],
["w[x]+w[y]", 0, 2, 0, 20, function(w,x,y) { return w[x]+w[y]; } ],
["w[x]+w.length", 0, 2, 0, 0, function(w,x,y) { return w[x]+w.length; } ],
["w[x]+w[w.length-1]", 0, 2, 0, 0, function(w,x,y) { return w[x]+w[w.length-1]; } ],
["w[x]+[w[y]]", 0, 2, 3, 20, function(w,x,y) { return w[x]+[w[y]]; } ],
["w[0]+w[x]+[w[y]]", 0, 20, 3, 20, function(w,x,y) { return w[0]+w[x]+[w[y]]; } ],
["w[1]+w[x]+[w[y]]", 0, 20, 3, 20, function(w,x,y) { return w[1]+w[x]+[w[y]]; } ],
["w[2]+w[x]+[w[y]]", 0, 20, 3, 20, function(w,x,y) { return w[2]+w[x]+[w[y]]; } ],
["w[0]+[w[x]]+[w[y]]", 3, 20, 3, 20, function(w,x,y) { return w[0]+[w[x]]+[w[y]]; } ],
["w.substr(x,3)", 0, 2, 0, 0, function(w,x,y) { return w.substr(x,3); } ]
];
}
ShapeShifter.prototype = {
/**
* Preparation stage : attempt to rehash the methods from canvas context
* Produces a pair of hashed/not hashed strings for each option
* so each selected "splitter" flag doubles the number of tests.
* Creates a list with all the combinations to feed to the packer, one at a time.
* "with Math()" option is applied on all entries if selected (does not create a pair)
*
* @param input : the string to pack
* @param options : preprocessing options, as follows
* - withMath : true if the option "Pack with(Math)" was selected, false otherwise
* - hash2DContext : (splitter) true if the option "Hash and rename 2D canvas context" was selected, false otherwise
* - hashWebGLContext : (splitter) true if the option "Hash and rename WebGL canvas context" was selected, false otherwise
* - hashAudioContext : (splitter) true if the option "Hash and rename AudioContext" was selected, false otherwise
* - hashAllObjects : (splitter) true if the option "Hash and rename properties for any object" was selected, false otherwise
* - contextVariableName : a string representing the variable holding the context if the "assume context" option was selected, false otherwise
* - contextType : the context type (0=2D, 1=WebGL) if the "assume context" option was selected, irrelevant otherwise
* - reassignVars : true to globally reassign variable names
* - varsNotReassigned : string or array listing all protected variables (whose name will not be modified)
* - wrapInSetInterval : true to wrap the unpacked code in a setInterval() call instead of eval()
* - timeVariableName : if "setInterval" option is set, the variable to use for time (zero on first loop, nonzero after)
* - useES6 : true to add ES6 constructs to the code, false otherwise
* @return an array of PackerData, representing all combinations for splitter flags
*/
preprocess : function(input, options) {
// Transform the list of protected variables into a boolean array[128] (true = protected).
// Same information but easier to access by algorithms.
// Only perform it once on an option set : unit tests reuse the same options for several calls
if (!options.varsNotReassignedRaw) {
options.varsNotReassignedRaw = options.varsNotReassigned;
options.varsNotReassigned = [];
for (var i=0; i<128; ++i) { // replace by Array.fill() once ES6 is supported
options.varsNotReassigned.push(false);
}
for (var i=0; i<options.varsNotReassignedRaw.length; ++i) {
var ascii = options.varsNotReassignedRaw[i].charCodeAt(0);
if (ascii>=0 && ascii<128 && this.isCharAllowedInVariable(ascii)) {
options.varsNotReassigned[ascii] = true;
}
}
}
// #74 , #96 : minification now performed inside the preprocessor, not as a hidden step in the main entry point
var minifiedInput = this.minify(input);
var inputData = new PackerData ('', minifiedInput);
if (options.withMath) {
// call module : Define environment
this.defineEnvironment(inputData);
}
var inputList = [ inputData ];
if (options.wrapInSetInterval) {
// call module : wrap with setInterval
this.refactorToSetInterval(inputData, options);
} else {
// map the bytes of the default interpreter call "eval(_)" to the entire code
var envMapping = [ { inLength : inputData.contents.length, outLength : inputData.contents.length, complete : false},
{ chapter : 4,
rangeIn : [0, inputData.contents.length],
rangeOut : [0, inputData.interpreterCall.length]
} ];
inputData.thermalMapping.push(envMapping);
}
// Hash and rename methods of the 2d canvas context
// - method hashing only
// - method and property
// then store the results in the inputList
if (options.hash2DContext) {
for (var count=inputList.length, i=0; i<count; ++i) {
var newBranches = this.preprocess2DContext(inputList[i], options);
inputList.push(...newBranches); // ES6 syntax : concatenate arrays
}
}
// for WebGL contexts, there are three options
// - hash and replace method names only
// - as above, plus replace the definitions of constants with their values (magic numbers)
// - hash and replace method and property names
if (options.hashWebGLContext) {
for (var count=inputList.length, i=0; i<count; ++i) {
var newBranches = this.preprocessWebGLContext(inputList[i], options);
inputList.push(...newBranches); // ES6 syntax : concatenate arrays
}
}
// for AudioContexts, method hashing only
if (options.hashAudioContext) {
for (var count=inputList.length, i=0; i<count; ++i) {
var newBranches = this.preprocessAudioContext(inputList[i], options.varsNotReassigned);
inputList.push(...newBranches); // ES6 syntax : concatenate arrays
}
}
// #86 : for all objects, using a variable as container for the method or property name
if (options.hashAllObjects) {
for (var count=inputList.length, i=0; i<count; ++i) {
var newBranchData = this.hashPropertyNamesAsVariables(inputList[i], options);
if (newBranchData[0]) { // true if at least one property was assigned to a variable
inputList.push(newBranchData[1]);
}
}
}
inputList[0].name="unhashed";
for (var i=0; i<inputList.length; ++i) {
this.identifyStrings(inputList[i], false);
// call module : quote strings
this.quoteStrings(inputList[i], options);
if (options.reassignVars) {
// call module : reassign variables
this.reassignVariableNames(inputList[i], options);
}
}
return inputList;
},
/**
* Performs a light minification on the input string
*
* Removes C-style comments
* Removes C++ one-line style comments
* Removes spaces, tabs, CR, CR LF
* Replaces ;} with }
*
* Exception : any string defined inside the program is not modified
*
* @param input : the string (program) to minify
* @return the same, minified
*/
minify : function(input) {
var output = "";
var previousCharCode = 0;
var closingSequence = "";
var inRemovedSequence = false, inString = false;
for (let i=0; i<input.length; ++i) {
let currentChar = input[i];
let nextCharCode = i<input.length-1 ? input.charCodeAt(i+1) : 0;
let testForSequenceEnd = true;
if (closingSequence == "") {
// no sequence in progress, detect a beginning
testForSequenceEnd = false;
if (currentChar == "'" || currentChar == '"' || currentChar == '`') {
// #96 : leave string contents untouched, no whitespace removal
closingSequence = currentChar;
inString = true;
}
if (input.substr(i,2) == "//") { // C++ style single-line comment
closingSequence = "\n";
inRemovedSequence = true;
}
if (input.substr(i,2) == "/*") { // C multi-line comment
closingSequence = "*/";
inRemovedSequence = true;
}
if (input.substr(i,4) == "<!--") { // XML multi-line comment
closingSequence = "-->";
inRemovedSequence = true;
}
}
if (!inRemovedSequence) {
let isBlank = "\n\r\t ".indexOf(currentChar) > -1;
// all non-blanks are copied
// #74 : so are all blanks between expressions or keywords
// multiple blanks between expressions are shortened, only the last one is kept
let doCopy = (!isBlank) || (this.isCharAllowedInVariable(previousCharCode) && this.isCharAllowedInVariable(nextCharCode));
//console.log(currentChar.charCodeAt(0)+" ("+currentChar+") "+doCopy);
if (doCopy || inString) { // #96 : no change inside a string
output+=currentChar;
previousCharCode = currentChar.charCodeAt(0);
}
}
if (testForSequenceEnd) {
if (input.substr(i,closingSequence.length) == closingSequence)
{
i+= closingSequence.length-1;
closingSequence = "";
inRemovedSequence = false;
inString = false;
}
}
}
// Remove any semicolon located right before a block ends
output = output.replace(/;}/g, "}");
return output;
},
/**
* Modifies the environment execution of the unpacked code, wrapping it into with(Math).
* Removes all references to Math. in the input code
* @param inputData (in/out) PackerData structure containing the code to refactor and setup
* @return nothing. Result of refactoring is stored in parameter inputData.
*/
defineEnvironment : function(inputData) {
inputData.environment = 'with(Math)';
var envMapping = { chapter : 2, rangeOut : [0, inputData.environment.length] };
inputData.contents = this.stringHelper.matchAndReplaceAll(inputData.contents, false, 'Math.', '', '', '', envMapping, inputData.thermalMapping);
},
/**
* Rewrites the input code so that it can entirely be executed inside
* a setInterval() loop without prior initialization.
*
* Detects the function that is currently called through setInterval()
* and strips it. Wraps the code before that function into a if sequence
* at the beginning of the loop, that will only be run once.
*
* The method makes use of a "time" variable that usually controls
* the flow of execution/rendering, and is increased at each loop.
* It needs to be zero on the first run to trigger the initialization
* sequence, then nonzero on the subsequent runs.
*
* Output (refactored code and log) is stored in parameter inputData.
* Setup for the unpacking routine is also changed in the same object.
*
* @param inputData (in/out) PackerData structure containing the code to refactor and setup
* @param options options set, see below for use details
* @return nothing. Result of refactoring is stored in parameter inputData.
* Options used are :
* - timeVariableName : the variable containing time, or empty string to allocate one
* - varsNotReassigned : boolean array[128], true to avoid altering variable
*
*/
refactorToSetInterval : function(inputData, options) {
var input = inputData.contents;
var output = input; // initialized from input, in case we bail out early
var timeVariableName = options.timeVariableName;
var varsNotReassigned = options.varsNotReassigned;
var details = "----------- Refactoring to run with setInterval() ---------------\n";
var timeVariableProvided = true;
// implementation for #44 : match arrow function syntax (new in ES6)
// regular expression matches pre-ES5 syntax : function(params){...}
var loopMatch = input.match(/setInterval\(function\(([\w\d.=,]*)\){/);
var functionDeclaration = "function(";
if (!loopMatch) { // regular expression matches ES6 syntax : (params)=>{...}
loopMatch = input.match(/setInterval\(\(([\w\d.=,]*)\)=>{/);
functionDeclaration = ")=>";
}
if (!loopMatch) { // regular expression matches ES6 syntax : one_param=>{...}
loopMatch = input.match(/setInterval\(([\w\d.]*)(=>){/);
functionDeclaration = "=>";
}
if (loopMatch) {
var initCode = input.substr(0, loopMatch.index);
// remove any trailing comma or semicolon
if (initCode[initCode.length-1]==';' || initCode[initCode.length-1]==',') {
initCode = input.substr(0, initCode.length-1);
}
details += "First "+loopMatch.index+" bytes moved to conditional sequence.\n";
// parameters of the function passed to setInterval() : extract default values
// The regex matches a variable declaration, without value assignment (no "="),
// at the beginning, end, or between two commas
var paramsCode = loopMatch[1];
var paramsExp = /(^|,)[A-Za-z$_][\w$_]*(,|$)/;
var paramsMatch = paramsExp.exec(paramsCode);
while (paramsMatch && paramsMatch[0] != "") {
// if the variable is between two commas, keep one : ",k," becomes ","
var keptCommas = (paramsMatch[1]+paramsMatch[2]).length>1 ? "," : "";
paramsCode = paramsCode.substr(0, paramsMatch.index)+keptCommas+paramsCode.substr(paramsMatch.index+paramsMatch[0].length);
paramsMatch = paramsExp.exec(paramsCode);
}
// end by a semicolon if there are any initializations that will be added to the main loop code
paramsCode += (paramsCode != "" ? ";" : "");
if (timeVariableName=="") {
timeVariableProvided = false;
timeVariableName = this.allocateNewVariable(inputData, options);
details += "Using variable "+timeVariableName+" for time.\n";
}
// Strip the declaration of the time variable from the init code,
// as it will be defined in the unpacking routine instead.
var timeDefinitionBegin = initCode.length;
var timeDefinitionEnd = timeDefinitionBegin;
var timeDefinitionExp = new RegExp("(^|[^\\w$])"+timeVariableName+"=","g");
var timeDefinitionMatch=timeDefinitionExp.exec(initCode);
if (timeDefinitionMatch) {
timeDefinitionBegin = timeDefinitionMatch.index+timeDefinitionMatch[1].length;
timeDefinitionEnd = timeDefinitionBegin+2;
// Check if we can strip more than "t=" depending on what comes before and after :
// - Brackets means no : the declaration is used as an argument in a function
// - Square brackets means no : used to define an array
// - Both commas before and after : same context, inside function or array
// - a leading = means no : multiple variables are defined at the same time
// - anything other than "0" after is no
// - other configurations are ok to remove up to the separator
var canRemoveInitValue = true;
var leadingChar = timeDefinitionBegin>0 ? initCode[timeDefinitionBegin-1] : "";
var trailingChar = initCode[timeDefinitionEnd];
var furtherChar = timeDefinitionEnd+1<initCode.length ? initCode[timeDefinitionEnd+1] : "";
canRemoveInitValue = canRemoveInitValue && (leadingChar != "="); // multiple variable init
canRemoveInitValue = canRemoveInitValue && (trailingChar == "0"); // multiple variable init, or function call
canRemoveInitValue = canRemoveInitValue && (leadingChar != "("); // used as a function argument
canRemoveInitValue = canRemoveInitValue && (leadingChar != "["); // used as array member
canRemoveInitValue = canRemoveInitValue && (furtherChar != ")"); // used as a function argument
canRemoveInitValue = canRemoveInitValue && (furtherChar != "]"); // used as array member
canRemoveInitValue = canRemoveInitValue && !(leadingChar == "," && furtherChar == ","); // used as a function argument
timeDefinitionEnd+=(canRemoveInitValue?1:0);
timeDefinitionEnd+=(canRemoveInitValue&&furtherChar==";"?1:0);
timeDefinitionEnd+=(canRemoveInitValue&&furtherChar==","&&leadingChar==""?1:0);
timeDefinitionBegin+=(canRemoveInitValue&&furtherChar!=";"&&leadingChar==";"?-1:0);
timeDefinitionBegin+=(canRemoveInitValue&&furtherChar==""&&leadingChar==","?-1:0);
details += "Removed declaration \""+initCode.substr(timeDefinitionBegin, timeDefinitionEnd-timeDefinitionBegin)+"\"\n";
initCode = initCode.substr(0,timeDefinitionBegin)+initCode.substr(timeDefinitionEnd);
}
var inString = 0, bracketDepth = 1, index = loopMatch.index+loopMatch[0].length;
while (bracketDepth>0 && index<input.length)
{
if (inString == 0) {
switch (input.charCodeAt(index)) {
case 34 : // "
case 39 : // '
case 96 : // `
inString = input.charCodeAt(index);
break;
case 123 : // {
++bracketDepth;
break;
case 125 : // }
--bracketDepth;
break;
}
} else if (input.charCodeAt(index) == inString && inString>0) {
inString = 0;
}
++index;
}
var finalCode = input.substr(index);
var delayMatch = finalCode.match(/,([\w\d.=]*)\);?/);
if (delayMatch) {
finalCode = finalCode.substr(delayMatch[0].length);
if (finalCode.length) {
details += "Last "+finalCode.length+" bytes also moved there.\n";
finalCode = (initCode.length > 0 ? ";" : "")+finalCode;
}
details += "Interval of "+delayMatch[1]+ "ms pushed to unpacking routine.\n";
// wrap the initialization code into a conditional sequence :
// - if(!t){/*init code*/} if the variable is used (and set) afterwards
// - if(!t++){/*init code*/} if it is created only for the test
// - #72 : nothing if there is no init code
var wrapperCode = "", wrapperEnd = "";
if (initCode.length + finalCode.length > 0) {
wrapperCode = "if(!"+timeVariableName+(timeVariableProvided?"":"++")+"){";
wrapperEnd = "}";
}
var mainLoopCode = input.substr(loopMatch.index+loopMatch[0].length, index-loopMatch.index-loopMatch[0].length-1);
// Redefine the "offset zero" of our transformed code,
// used to hash methods/properties of contexts provided by shim
inputData.initialDeclarationOffset = wrapperCode.length;
output = wrapperCode + initCode + finalCode + wrapperEnd + paramsCode + mainLoopCode;
inputData.interpreterCall = 'setInterval(_,'+delayMatch[1]+')';
inputData.wrappedInit = timeVariableName+'=0';
// Special case : the assignment of the time variable is done
// as a parameter of setInterval()
// (featured in 2012 - A rose is a rose)
if (delayMatch[1].indexOf(inputData.wrappedInit) != -1) {
// in this case, no need to declare the variable again
inputData.wrappedInit = "";
details += timeVariableName+" initialized as parameter to setInterval, kept as is.\n";
}
var functionDeclarationOffset = loopMatch.index+loopMatch[0].indexOf(functionDeclaration);
// Record the change in the thermal transform
var transform = [ { inLength : input.length, outLength : output.length, complete : true } ];
// "if(!t){" : mapped to "function("
transform.push ( { chapter : 0, rangeIn : [functionDeclarationOffset, functionDeclaration.length], rangeOut : [0, wrapperCode.length] });
var rangeOutBegin = wrapperCode.length;
// code before the main loop, before the time variable declaration, mapped to itself
transform.push ( { chapter : 0, rangeIn : [0, timeDefinitionBegin], rangeOut : [rangeOutBegin, timeDefinitionBegin] });
rangeOutBegin += timeDefinitionBegin;
var initSecondHalfLength = 0;
if (timeDefinitionMatch) {
// code before the main loop, after the time variable declaration, mapped to itself
// may omit the final "," or ";", if any, that is eliminated = mapped to nothing
initSecondHalfLength = initCode.length-timeDefinitionBegin;
transform.push ( { chapter : 0, rangeIn : [timeDefinitionEnd, initSecondHalfLength], rangeOut : [rangeOutBegin, initSecondHalfLength] });
rangeOutBegin += initSecondHalfLength;
}
if (finalCode.length) {
// code after the main loop, mapped to itself
transform.push ( { chapter : 0, rangeIn : [index+delayMatch[0].length, finalCode.length], rangeOut : [rangeOutBegin, finalCode.length] });
rangeOutBegin += finalCode.length;
}
// "}" : mapped to final "}" of the main loop, if any
transform.push ( { chapter : 0, rangeIn : [index-1, 1], rangeOut : [rangeOutBegin, wrapperEnd.length] });
rangeOutBegin += wrapperEnd.length;
// parameters of the loop function, mapped to themselves
var paramsOffset = loopMatch.index+loopMatch[0].indexOf(loopMatch[1]);
transform.push ( { chapter : 0, rangeIn : [paramsOffset, loopMatch[1].length], rangeOut : [rangeOutBegin, paramsCode.length] });
rangeOutBegin += paramsCode.length;
// code in the loop function, mapped to itself
transform.push ( { chapter : 0, rangeIn : [loopMatch.index+loopMatch[0].length, mainLoopCode.length], rangeOut : [rangeOutBegin, mainLoopCode.length] });
// "setInterval(" : map to original declaration
var blockLength = "setInterval(".length;
transform.push ( { chapter : 3, rangeIn : [loopMatch.index, blockLength], rangeOut : [0, blockLength] });
// "_" : mapped to "function("
transform.push ( { chapter : 3, rangeIn : [functionDeclarationOffset, functionDeclaration.length], rangeOut : [blockLength, 1] });
// ",nn)" : map to original declaration
transform.push ( { chapter : 3, rangeIn : [index, delayMatch[0].length], rangeOut : [blockLength+1, delayMatch[1].length+2] });
// time declaration variable "t=0" : map to original declaration if any, to function declaration otherwise
if (timeDefinitionMatch) {
transform.push ( { chapter : 4, rangeIn : [timeDefinitionBegin, timeDefinitionEnd-timeDefinitionBegin], rangeOut : [0, inputData.wrappedInit.length] });
} else {
transform.push ( { chapter : 4, rangeIn : [functionDeclarationOffset, functionDeclaration.length], rangeOut : [0, inputData.wrappedInit.length] });
}
inputData.thermalMapping.push(transform);
} else { // delayMatch === false
details += "Unable to find delay for setInterval, module skipped.\n";
}
} else {
details += "setInterval() loop not found, module skipped.\n";
}
details += "\n";
// output stored in inputData parameter instead of being returned
inputData.contents = output;
inputData.log += details;
},
/**
* Performs an optimal hashing and renaming of the methods/properties of a canvas 2d context.
* Uses a context reference passed from a shim (if provided), plus attempts to
* identify all contexts created within the code.
* Returns an array containing two sub-arrays,
* each in the same format as the compression methods :
* [output length, output string, informations],
* even if the preprocessing actually lenghtened the string.
*
*
* @param inputData (constant) PackerData structure containing setup data and the code to preprocess
* @param options (constant) options set, see below for use details
* Options used are :
* - contextType : type of context provided by shim : 0 for 2D, 1 for GL
* - contextVariableName : the variable holding the context if provided by shim, false otherwise
* - varsNotReassigned : boolean array[128], true to avoid altering variable
* @return an array containing branched (and hashed) PackerData, empty if no 2d context definition is found in the code.
*/
preprocess2DContext : function(inputData, options) {
// Obtain all context definitions (variable name and location in the code)
var objectNames = [], objectOffsets = [], objectDeclarationLengths = [], searchIndex = 0;
var variableName = (options.contextType==0?options.contextVariableName:false);
var varsNotReassigned = options.varsNotReassigned;
// Start with the preset context, if any
if (variableName)
{
objectNames.push(variableName);
objectOffsets.push(inputData.initialDeclarationOffset);
objectDeclarationLengths.push(0);
}
// Then search for additional definitions inside the code. Keep name, declaration offset, and declaration length
var input = inputData.contents;
var declarations = input.match (/([\w\d.]*)=[\w\d.]*\.getContext\(?[`'"]2d[`'"]\)?/gi);
if (declarations) {
for (var declIndex=0; declIndex<declarations.length; ++declIndex)
{
var oneDecl = declarations[declIndex];
objectNames.push(oneDecl.substr(0, oneDecl.indexOf('=')));
var oneOffset = input.indexOf(oneDecl, searchIndex);
objectOffsets.push(oneOffset);
objectDeclarationLengths.push(oneDecl.length);
searchIndex = oneOffset + oneDecl.length;
}
}
if (objectNames.length) {
// obtain the list of properties in a 2D context from the ContextDescriptor
var referenceProperties = this.contextDescriptor.canvas2DContextDescription.properties;
var methodHashedData = PackerData.clone(inputData, " 2D(methods)");
methodHashedData.log += "----------- Hashing methods for 2D context -----------\n";
// output stored in methodHashedData
this.renameObjectMethods(methodHashedData, objectNames, objectOffsets, objectDeclarationLengths, referenceProperties, varsNotReassigned);
var propertyHashedData = PackerData.clone(inputData, " 2D(properties)");
propertyHashedData.log += "----------- Hashing properties for 2D context -----------\n";
// output stored in propertyHashedData
this.hashObjectProperties(propertyHashedData, objectNames, objectOffsets, objectDeclarationLengths, referenceProperties, varsNotReassigned);
return [methodHashedData, propertyHashedData];
}
return [];
},
/**
* Performs an optimal hashing and renaming of the methods of a canvas WebGL context.
* Uses a context reference passed from a shim, or attempts to locate in inside the code.
* Features the call to replaceWebGLconstants() as one of the results.
* @see replaceWebGLconstants
*
* Returns an array containing three sub-arrays,
* [output length, output string, informations],
* - first one has method hashing performed
* - second one has method hashing + GL constants replaced by their value
* - third one has method + property renaming
*
* @param inputData (constant) PackerData structure containing setup data and the code to preprocess
* @param options (constant) options set, see below for use details
* Options used are :
* - contextType : type of context provided by shim : 0 for 2D, 1 for GL
* - contextVariableName : the variable holding the context if provided by shim, false otherwise
* - varsNotReassigned : boolean array[128], true to avoid altering variable
* @return an array containing branched (and hashed) PackerData, empty if no WebGL context definition is found in the code.
*/
preprocessWebGLContext : function(inputData, options) {
// Obtain all context definitions (variable name and location in the code)
var objectNames = [], objectOffsets = [], objectDeclarationLengths = [], searchIndex = 0;
var input = inputData.contents;
var variableName = options.contextType==1?options.contextVariableName:false;
var varsNotReassigned = options.varsNotReassigned;
// Start with the preset context, if any
if (variableName)
{
objectNames.push(variableName);
objectOffsets.push(inputData.initialDeclarationOffset);
objectDeclarationLengths.push(0);
}
// Then search for additional definitions inside the code. Keep name, declaration offset, and declaration length
var declarations = input.match (/([\w\d.]*)\s*=\s*[\w\d.]*\.getContext\(?[`'"](experimental-)*webgl[`'"](,[\w\d\s{}:.,!]*)*\)?(\s*\|\|\s*[\w\d.]*\.getContext\(?[`'"](experimental-)*webgl[`'"](,[\w\d\s{}:.,!]*)*\)?)*/gi);
if (declarations) {
for (var declIndex=0; declIndex<declarations.length; ++declIndex)
{
var oneDecl = declarations[declIndex];
objectNames.push(oneDecl.substr(0, oneDecl.indexOf('=')));
var oneOffset = input.indexOf(oneDecl, searchIndex);
objectOffsets.push(oneOffset);
objectDeclarationLengths.push(oneDecl.length);
searchIndex = oneOffset + oneDecl.length;
}
}
if (objectNames.length) {
// list of properties in a GL context
var referenceProperties = this.contextDescriptor.canvasGLContextDescription.properties;
var methodHashedData = PackerData.clone(inputData, " WebGL(methods)");
methodHashedData.log += "----------- Hashing methods for GL context -----------\n";
// output stored in methodHashedData
this.renameObjectMethods(methodHashedData, objectNames, objectOffsets, objectDeclarationLengths, referenceProperties, varsNotReassigned);
// elaborate on the previous result : replace GL constants with values
var constantHashedData = PackerData.clone(methodHashedData, " WebGL(methods+constants)");
constantHashedData.log += "----------- Replacing constants for GL context -----------\n";
// output stored in constantHashedData
this.replaceWebGLconstants(constantHashedData, objectNames, this.contextDescriptor.canvasGLContextDescription.constants);
var propertyHashedData = PackerData.clone(inputData, " WebGL(properties)");
propertyHashedData.log += "----------- Hashing properties for GL context -----------\n";
// output stored in propertyHashedData
this.hashObjectProperties(propertyHashedData, objectNames, objectOffsets, objectDeclarationLengths, referenceProperties, varsNotReassigned);
return [methodHashedData, constantHashedData, propertyHashedData];
}
return [];
},
/**
* Replaces, in the provided code, all definition of WebGL constants with their numerical values
* Uses a context reference passed from a shim, or attempts to locate in inside the code.
* Returns an array in the same format as the compression methods : [output length, output string, informations],
* even if the replacement actually lenghtened the string.
* Only the constant values in CAPITALS are replaced. Other properties and methods are untouched.
*
* @param inputData (in/out) PackerData structure containing setup data and the code to preprocess
* @param objectNames array containing variable names of context objects (mandatory)
* @param referenceConstants : object containing all constants of the GL context
* @return nothing - output is stored in parameter inputData
*/
replaceWebGLconstants : function (inputData, objectNames, referenceConstants) {
var output = inputData.contents, details=inputData.log;
for (var contextIndex=0; contextIndex<objectNames.length; ++contextIndex) {
var exp = new RegExp("(^|[^\\w$])"+objectNames[contextIndex]+"\\.([0-9A-Z_]*)[^\\w\\d_(]","g");
var constantsInUse=[];
var result=exp.exec(output);
while (result) { // get a set with a unique entry for each method
if (constantsInUse.indexOf(result[2])==-1) {
constantsInUse.push(result[2]);
}
result=exp.exec(output);
}
details += "Replaced constants of "+objectNames[contextIndex]+"\n";
for (var index=0; index<constantsInUse.length; ++index) {
if (constantsInUse[index] in referenceConstants) {
var constant = referenceConstants[constantsInUse[index]];
var exp = new RegExp("(^|[^\\w$])"+objectNames[contextIndex]+"\\."+constantsInUse[index]+"(^|[^\\w$])","g");
// output = output.replace(exp, "$1"+constant+"$2");
output = this.stringHelper.matchAndReplaceAll(output, exp, objectNames[contextIndex]+"."+constantsInUse[index], constant, "", "", 0, inputData.thermalMapping);
// show the renaming in the details
details += constantsInUse[index] + " -> " + constant + "\n";
}
}
}
details+="\n";
// output stored in inputData parameter
inputData.contents = output;
inputData.log = details;
},
/**
* Performs an optimal hashing and renaming of the methods of an AudioContext.
* Unlike 2D contexts, only one audio context is considered.
*
* Returns an array in the same format as the compression methods : [output length, output string, informations],
* even if the preprocessing actually lenghtened the string.
*
* @param inputData (constant) PackerData structure containing the code to refactor and setup
* @param varsNotReassigned boolean array[128], true to keep name of variable
* @return an array containing branched (and hashed) PackerData, empty if no AudioContext definition is found in the code.
*/
preprocessAudioContext : function(inputData, varsNotReassigned) {
// list of properties in an AudioContext
var referenceProperties = this.contextDescriptor.audioContextDescription.properties;
// Initialization of a *AudioContext can be performed in a variety of ways
// and is usually less consistent that a 2D or WebGL graphic context.
// Multiple tests cover the most common cases
var objectOffset = 0;
var replacementOffset = 0;
var objectName = "";
var methodHashedData = PackerData.clone(inputData, " Audio");
var input = methodHashedData.contents;
methodHashedData.log += "----------- Hashing methods for AudioContext --------------------\n";
// direct instanciation of an AudioContext
// var c = new AudioContext()
var result = input.match (/([\w\.]*)\s*=\s*new AudioContext/i);
if (result) {
objectOffset = input.indexOf(result[0]);
objectName = result[1];
// start replacement at the next semicolon
replacementOffset = 1+input.indexOf(";", objectOffset);
// unless the object is used before
replacementOffset = Math.min(replacementOffset, input.indexOf(objectName, objectOffset+result[0].length));
}
// direct instanciation of a webkitAudioContext
// beware, the same code could try to create both (see TestCase audioContext_create1)
// var c = new webkitAudioContext()
var resultWebkit = input.match (/([\w\.]*)\s*=\s*new webkitAudioContext/i);
if (resultWebkit) {
if (resultWebkit[1] == objectName || objectName == "") {
// we take the latter declaration, as to add the renaming loop after both of them
objectOffset = Math.max(objectOffset, input.indexOf(resultWebkit[0]));
// start replacement at the next semicolon
replacementOffset = 1+input.indexOf(";", objectOffset);
// unless the object is used before
replacementOffset = Math.min(replacementOffset, input.indexOf(objectName, objectOffset+resultWebkit[0].length));
} else {
// the webkitAudioContext was created with a different name than the AudioContext
// in this case, we separately rename objects for both of them
// (see TestCase audioContext_create2 and audioContext_create3)
var webkitObjectOffset = input.indexOf(resultWebkit[0]);
var webkitObjectName = resultWebkit[1];
// start replacement at the next semicolon
var webkitReplacementOffset = 1+input.indexOf(";", webkitObjectOffset);
// unless the object is used before
webkitReplacementOffset = Math.min(webkitReplacementOffset, input.indexOf(webkitObjectName, webkitObjectOffset+resultWebkit[0].length));
var secondObjectOffset = replacementOffset>webkitReplacementOffset ? objectOffset : webkitObjectOffset;
var secondReplacementOffset = replacementOffset>webkitReplacementOffset ? replacementOffset : webkitReplacementOffset;
var secondObjectName = replacementOffset>webkitReplacementOffset ? objectName : resultWebkit[1];
objectOffset = replacementOffset>webkitReplacementOffset ? webkitObjectOffset : objectOffset ;
replacementOffset = replacementOffset>webkitReplacementOffset ? webkitReplacementOffset : replacementOffset ;
objectName = replacementOffset>webkitReplacementOffset ? resultWebkit[1] : objectName ;
// perform the replacement for the latter object first, so the offset of the former is not changed
this.renameObjectMethods(methodHashedData, [secondObjectName], [secondReplacementOffset], [0], referenceProperties, varsNotReassigned);
}
}
// direct instanciation of the appropriate context
// var c = new (window.AudioContext||window.webkitAudioContext)()
result = input.match (/([\w\.]*)\s*=\s*new\s*\(*\s*(window\.)*(webkit)*AudioContext\s*\|\|\s*(window\.)*(webkit)*AudioContext/i);
if (result) {
objectOffset = input.indexOf(result[0]);
objectName = result[1];
// start replacement at the next semicolon
replacementOffset = 1+input.indexOf(";", objectOffset);
// unless the object is used before
replacementOffset = Math.min(replacementOffset, input.indexOf(objectName, objectOffset+result[0].length));
}
// direct instanciation of the appropriate context, not factored in
// var c = new AudioContext()||new webkitAudioContext()
// (see TestCase audioContext_create4)
result = input.match (/([\w\.]*)\s*=\s*new\s*\(*\s*(window\.)*(webkit)*AudioContext(\(\))*\s*\|\|\s*new\s*(window\.)*(webkit)*AudioContext(\(\))*/i);
if (result) {
objectOffset = input.indexOf(result[0]);
objectName = result[1];
// start replacement at the next semicolon
replacementOffset = 1+input.indexOf(";", objectOffset);
// unless the object is used before
replacementOffset = Math.min(replacementOffset, input.indexOf(objectName, objectOffset+result[0].length));
}
// instanciation of the appropriate context, through a temporary variable
// contextType = window.AudioContext||window.webkitAudioContext; var c = new contextType;
result = input.match (/([\w\.]*)\s*=\s*\(*\s*(window\.)*(webkit)*AudioContext\s*\|\|\s*(window\.)*(webkit)*AudioContext/i);
if (result) {
var contextTypeName = result[1];
var exp = new RegExp("([\\w\\.]*)\\s*=\\s*new\\s*\\(*\\s*"+contextTypeName);
result = input.match(exp);
if (result) {
objectOffset = input.indexOf(result[0]);
objectName = result[1];
// start replacement at the next semicolon
replacementOffset = 1+input.indexOf(";", objectOffset);
// unless the object is used before
replacementOffset = Math.min(replacementOffset, input.indexOf(objectName, objectOffset+result[0].length));
}
}
if (replacementOffset>0) {
this.renameObjectMethods(methodHashedData, [objectName], [replacementOffset], [0], referenceProperties, varsNotReassigned);
return [methodHashedData];
}
return [];
},
/**
* Implementation of #86
* Identifies method and object properties names used multiple times
* Stores their name as a variable on the first occurrence, then use the variable as a property afterwards
*
* c.fillStyle becomes c[F="fillStyle"] the first time then c[F] afterwards.
*
* The advantage over hashing schemes is that this works with different objects sharing the same property
*
* Variables assigned that way are from the same pool that would be used for reassignment,
* but leaving enough for that module to complete.
*
* Returns an array [boolean, PackerData]
* - the boolean is true if at least one property name was packed that way, false if no change occurred
* - the PackerData derives from the input with the packed properties
*
* @param inputData (constant) PackerData structure containing the code to refactor and setup
* @param options : preprocessing options, as follows
* - reassignVars : true to globally reassign variable names
* - varsNotReassigned : string or array listing all protected variables (whose name will not be modified)
* - wrapInSetInterval : true to wrap the unpacked code in a setInterval() call instead of eval()
* - timeVariableName : if "setInterval" option is set, the variable to use for time (zero on first loop, nonzero after)
* @return an array containing [boolean, PackerData] as described above
*/
hashPropertyNamesAsVariables : function (inputData, options) {
var outputData = PackerData.clone(inputData, " all_properties");
var input = outputData.contents;
// Anything inside strings (save for template literals) is ignored.
// Strings boundaries will be identified again after the preprocessing is done
this.identifyStrings(outputData, true);
var propertiesInUse=[];
var propertiesMatchData=[];
var exp = new RegExp("\\.(\\w*)\\W","g");
var result=exp.exec(input);
while (result) { // get a set with a unique entry for each method
var recordIndex = propertiesInUse.indexOf(result[1]);
if (this.stringHelper.isActualCodeAt(recordIndex, outputData)) {
if (recordIndex == -1) {
propertiesInUse.push(result[1]);
propertiesMatchData.push({name:result[1], count:1, offset:exp.lastIndex});
} else {
++propertiesMatchData[recordIndex].count;
}
}
result=exp.exec(input);
}
for (var index=0; index<propertiesMatchData.length; ++index) {
var rawCost = (1+propertiesMatchData[index].name.length) * propertiesMatchData[index].count;
var hashedCost = propertiesMatchData[index].name.length + 6 + (propertiesMatchData[index].count-1)*3;
propertiesMatchData[index].gain = rawCost - hashedCost;
}
// sort by decreasing gain, offset as tiebreaker to ensure a deterministic result
// even when there are equivalent gains
propertiesMatchData.sort(function(a,b) { return b.gain-a.gain + (b.offset-a.offset)/10000; });
outputData.log += "----------- Storing property names into variables ----------- \n";
var keywordsAndVariables = this.discriminateKeywordsAndVariables(inputData, options);
var keywordChars = keywordsAndVariables[0];
var variableChars = keywordsAndVariables[1];
var availableChars = keywordsAndVariables[2];
var variableCharsOnly = keywordsAndVariables[3];
var availableCharList = "";
var charsNeededForReassignModule = 0;
var formerVariableCharList = "";
for (var i=32; i<128; ++i) {
// Identify as available all characters used in keywords but not variables
if (availableChars[i]) {
availableCharList+=String.fromCharCode(i);
}
// Simply count variables that need to be renamed, if the "reassign variables" module is active
// They shall be deducted from our pool
if (variableCharsOnly[i] && options.reassignVars) {
++charsNeededForReassignModule;
}
}
var availableVariableCount = availableCharList.length - charsNeededForReassignModule;
var processedInput = outputData.contents;
for (var index=0 ; index<propertiesMatchData.length && propertiesMatchData[index].gain>0 && index<availableVariableCount ; ++index) {
// on the first occurrence, declare the variable
var declarationReplacement = '['+availableCharList[index]+'="'+propertiesMatchData[index].name+'"]';
// replace all other occurences without the declaration
var otherReplacement = '['+availableCharList[index]+']';
processedInput = this.stringHelper.matchAndReplaceFirstAndAll(processedInput, false, '.'+propertiesMatchData[index].name, declarationReplacement, otherReplacement, "", "", 0, outputData.thermalMapping);
outputData.log += availableCharList[index] + '="' + propertiesMatchData[index].name+ '" (x'+propertiesMatchData[index].count + ') : gain = '+propertiesMatchData[index].gain+"\n";
}
// log the replacements missed for lack of a token
for ( ; index<propertiesMatchData.length && propertiesMatchData[index].gain>0 ; ++index) {
outputData.log += 'No token left : "' + propertiesMatchData[index].name+ '" (x'+propertiesMatchData[index].count + ') : missed gain = '+propertiesMatchData[index].gain+"\n";
}
outputData.contents = processedInput;
return [index>0, outputData];
},
/**
* Identifies the optimal hashing function (the one returning the shortest result)
* then renames all the methods with their respective hash, and preprends the hashing code.
*
* The hashing loop looks like : for(i in c)c[i[0]+[i[6]]=c[i];
* meaning that later one may call c.fc(...) instead of c.fillRect(...)
* The newly created properties are reference to existing functions.
*
* Replacement is performed at the last object assignment(graphic or audio context),
* or at the beginning for shim context, hence the offset parameter.
*
* If there are several contexts, only one hash is used. It is applied to
* all or only some of the contexts, depending on the computed gain.
* The algorithm will not define different hashes for the multiple
* contexts. The rationale behind this is the assumption that the lesser
* gain from using the same hash for all will be offset by the better
* compression - as the repeated hashing pattern will be picked up by the
* packer.
*
* Returns an array in the same format as the compression methods : [output length, output string, informations],
*
* @param inputData (in/out) PackerData structure containing the code to refactor and setup
* @param objectNames : array containing variable names of context objects, whose methods to rename in the source string
* @param objectOffsets : array, in the same order, of character offset to the beginning of the object declaration. Zero if defined outside (shim)
* @param objectDeclarationLengths : array, in the same order, of lengths of the object declaration, starting at offset. Zero if defined outside (shim)
* @param referenceProperties : an array of strings containing property names for the appropriate context type
* @param varsNotReassigned : boolean array[128], true to keep name of variable
* @return true if the replacement was performed (the gain was >= 0), false otherwise (net loss, replacement cancelled)
*/
renameObjectMethods : function(inputData, objectNames, objectOffsets, objectDeclarationLengths, referenceProperties, varsNotReassigned)
{
var input = inputData.contents;
var details = inputData.log;
var methodsInUseByContext=[];
for (var contextIndex=0; contextIndex<objectNames.length; ++contextIndex)
{
var methodsInUse = [];
var exp = new RegExp("(^|[^\\w$])"+objectNames[contextIndex]+"\\.(\\w*)\\(","g");
var result=exp.exec(input);
while (result) { // get a set with a unique entry for each method
if (methodsInUse.indexOf(result[2])==-1) {
methodsInUse.push(result[2]);
}
--exp.lastIndex; // the final ( can be reused as initial separator in the next expression
result=exp.exec(input);
}
methodsInUseByContext.push(methodsInUse);
}
var bestTotalScore = -999, bestIndex =-1, bestXValue = 0, bestYValue = 0, bestScoreByContext = [];
// For each hashing function, we compute the hashed names of all methods of the context object
// All collisions (such as taking the first letter of scale() and save() end up in s()) are eliminated
// as the order depends on the browser and we cannot assume the browser that will be running the final code
// A score is then assigned to the hashing function :
// - for each context, the algorithm computes the gain obtained by shortening the non-colliding method names to their hash
// (the number of occurrences is irrelevant, we assume that the compression step will amount them to one)
// - the length of the hashing function is subtracted
// Only the hashing function with the best score is kept - and applied
for (var functionIndex=0; functionIndex<this.hashFunctions.length; ++functionIndex) {
var functionDesc = this.hashFunctions[functionIndex];
for (var xValue=functionDesc[1]; xValue<=functionDesc[2] ; ++xValue) {
for (var yValue=functionDesc[3]; yValue<=functionDesc[4] ; ++yValue) {
var reverseLookup = [], forwardLookup = [];
for (var index=0; index<referenceProperties.length; ++index) {
var w = referenceProperties[index];
var hashedName = functionDesc[5].call(null, w, xValue, yValue);
reverseLookup[hashedName] = (reverseLookup[hashedName] ? "<collision>" : w);
}
for (w in reverseLookup) {
forwardLookup[reverseLookup[w]]=w; // keep only the method names with no collisions
}
var allScores = [], totalScore = 0;
for (var contextIndex=0; contextIndex<objectNames.length; ++contextIndex)
{
var score = 0;
var methodsInUse = methodsInUseByContext[contextIndex];
for (var methodIndex=0; methodIndex<methodsInUse.length; ++methodIndex) {
// Fix for issue #11 : in FF, arrays have a method fill(), as 2D contexts do
// typeof() discriminates between "string" (hash match), "undefined" (no match) and "function" (array built-in)
if (typeof(forwardLookup[methodsInUse[methodIndex]])=="string") {
var delta = methodsInUse[methodIndex].length - forwardLookup[methodsInUse[methodIndex]].length;
// Complement for issue #23, when the hash could be longer than the original name
score += Math.max(0, delta);
}
}
score -= 2; // a[hash]=b[hash]=a[i], "[hash]=" is packed for 1, remains "a" total 1+1
allScores.push(score);
score = Math.max(0, score); // if the gain is negative, no replacement will be performed
totalScore += score;
}
// the score for the hash is the gain as computed above,
// minus the length of the hash function itself.
totalScore-=functionDesc[0].replace(/x/g, xValue).replace(/y/g, yValue).length;
if (totalScore>bestTotalScore) {
bestTotalScore = totalScore;
bestScoreByContext = allScores;
bestIndex = functionIndex;
bestXValue = xValue;
bestYValue = yValue;
}
}
}
}
// bail out early if no gain. Keep if just par, to see how compression behaves
if (bestTotalScore < 0) {
details += "Best hash loses "+(-bestTotalScore)+" bytes, skipping.\n";
inputData.log = details;