UNPKG

23.2 kBJavaScriptView Raw
1/*!
2 * config.js - configuration parsing for bcoin
3 * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License).
4 * https://github.com/bcoin-org/bcoin
5 */
6
7'use strict';
8
9const assert = require('bsert');
10const Path = require('path');
11const os = require('os');
12const fs = require('./fs');
13const HOME = os.homedir ? os.homedir() : '/';
14
15/**
16 * Config Parser
17 */
18
19class Config {
20 /**
21 * Create a config.
22 * @constructor
23 * @param {String} module - Module name (e.g. `bcoin`).
24 * @param {Object?} options
25 */
26
27 constructor(module, options) {
28 assert(typeof module === 'string');
29 assert(module.length > 0);
30
31 this.module = module;
32 this.prefix = Path.join(HOME, `.${module}`);
33 this.suffix = null;
34 this.fallback = null;
35 this.alias = Object.create(null);
36
37 this.options = Object.create(null);
38 this.data = Object.create(null);
39 this.env = Object.create(null);
40 this.args = Object.create(null);
41 this.argv = [];
42 this.pass = [];
43 this.query = Object.create(null);
44 this.hash = Object.create(null);
45
46 if (options)
47 this.init(options);
48 }
49
50 /**
51 * Initialize options.
52 * @private
53 * @param {Object} options
54 */
55
56 init(options) {
57 assert(options && typeof options === 'object');
58
59 if (options.suffix != null) {
60 assert(typeof options.suffix === 'string');
61 this.suffix = options.suffix;
62 }
63
64 if (options.fallback != null) {
65 assert(typeof options.fallback === 'string');
66 this.fallback = options.fallback;
67 }
68
69 if (options.alias) {
70 assert(typeof options.alias === 'object');
71 for (const key of Object.keys(options.alias)) {
72 const value = options.alias[key];
73 assert(typeof value === 'string');
74 this.alias[key] = value;
75 }
76 }
77 }
78
79 /**
80 * Inject options.
81 * @param {Object} options
82 */
83
84 inject(options) {
85 for (const key of Object.keys(options)) {
86 const value = options[key];
87
88 switch (key) {
89 case 'hash':
90 case 'query':
91 case 'env':
92 case 'argv':
93 case 'config':
94 continue;
95 }
96
97 this.set(key, value);
98 }
99 }
100
101 /**
102 * Load options from hash, query, env, or args.
103 * @param {Object} options
104 */
105
106 load(options) {
107 if (options.hash)
108 this.parseHash(options.hash);
109
110 if (options.query)
111 this.parseQuery(options.query);
112
113 if (options.env)
114 this.parseEnv(options.env);
115
116 if (options.argv)
117 this.parseArg(options.argv);
118
119 this.prefix = this.getPrefix();
120 }
121
122 /**
123 * Open a config file.
124 * @param {String} file - e.g. `bcoin.conf`.
125 * @throws on IO error
126 */
127
128 open(file) {
129 if (fs.unsupported)
130 return;
131
132 const path = this.getFile(file);
133
134 let text;
135 try {
136 text = fs.readFileSync(path, 'utf8');
137 } catch (e) {
138 if (e.code === 'ENOENT')
139 return;
140 throw e;
141 }
142
143 this.parseConfig(text);
144
145 this.prefix = this.getPrefix();
146 }
147
148 /**
149 * Create a child config. Filter by plugin name.
150 * @param {String} name
151 * @returns {Config}
152 */
153
154 filter(name) {
155 assert(typeof name === 'string');
156
157 const child = new Config(this.module);
158
159 child.prefix = this.prefix;
160 child.suffix = this.suffix;
161 child.fallback = this.fallback;
162 child.argv = this.argv;
163 child.pass = this.pass;
164
165 _filter(name, this.env, child.env);
166 _filter(name, this.args, child.args);
167 _filter(name, this.query, child.query);
168 _filter(name, this.hash, child.hash);
169
170 return child;
171 }
172
173 /**
174 * Set default option.
175 * @param {String} key
176 * @param {Object} value
177 */
178
179 set(key, value) {
180 assert(typeof key === 'string', 'Key must be a string.');
181
182 if (value == null)
183 return;
184
185 key = key.replace(/-/g, '');
186 key = key.toLowerCase();
187
188 this.options[key] = value;
189 }
190
191 /**
192 * Test whether a config option is present.
193 * @param {String} key
194 * @returns {Boolean}
195 */
196
197 has(key) {
198 if (typeof key === 'number') {
199 assert(key >= 0, 'Index must be positive.');
200 if (key >= this.argv.length)
201 return false;
202 return true;
203 }
204
205 assert(typeof key === 'string', 'Key must be a string.');
206
207 key = key.replace(/-/g, '');
208 key = key.toLowerCase();
209
210 if (this.hash[key] != null)
211 return true;
212
213 if (this.query[key] != null)
214 return true;
215
216 if (this.args[key] != null)
217 return true;
218
219 if (this.env[key] != null)
220 return true;
221
222 if (this.data[key] != null)
223 return true;
224
225 if (this.options[key] != null)
226 return true;
227
228 return false;
229 }
230
231 /**
232 * Get a config option.
233 * @param {String} key
234 * @param {Object?} fallback
235 * @returns {Object|null}
236 */
237
238 get(key, fallback) {
239 if (fallback === undefined)
240 fallback = null;
241
242 if (Array.isArray(key)) {
243 const keys = key;
244 for (const key of keys) {
245 const value = this.get(key);
246 if (value !== null)
247 return value;
248 }
249 return fallback;
250 }
251
252 if (typeof key === 'number') {
253 assert(key >= 0, 'Index must be positive.');
254
255 if (key >= this.argv.length)
256 return fallback;
257
258 if (this.argv[key] != null)
259 return this.argv[key];
260
261 return fallback;
262 }
263
264 assert(typeof key === 'string', 'Key must be a string.');
265
266 key = key.replace(/-/g, '');
267 key = key.toLowerCase();
268
269 if (this.hash[key] != null)
270 return this.hash[key];
271
272 if (this.query[key] != null)
273 return this.query[key];
274
275 if (this.args[key] != null)
276 return this.args[key];
277
278 if (this.env[key] != null)
279 return this.env[key];
280
281 if (this.data[key] != null)
282 return this.data[key];
283
284 if (this.options[key] != null)
285 return this.options[key];
286
287 return fallback;
288 }
289
290 /**
291 * Get a value's type.
292 * @param {String} key
293 * @returns {String}
294 */
295
296 typeOf(key) {
297 const value = this.get(key);
298
299 if (value === null)
300 return 'null';
301
302 return typeof value;
303 }
304
305 /**
306 * Get a config option (as a string).
307 * @param {String} key
308 * @param {Object?} fallback
309 * @returns {String|null}
310 */
311
312 str(key, fallback) {
313 const value = this.get(key);
314
315 if (fallback === undefined)
316 fallback = null;
317
318 if (value === null)
319 return fallback;
320
321 if (typeof value !== 'string')
322 throw new Error(`${fmt(key)} must be a string.`);
323
324 return value;
325 }
326
327 /**
328 * Get a config option (as an integer).
329 * @param {String} key
330 * @param {Object?} fallback
331 * @returns {Number|null}
332 */
333
334 int(key, fallback) {
335 const value = this.get(key);
336
337 if (fallback === undefined)
338 fallback = null;
339
340 if (value === null)
341 return fallback;
342
343 if (typeof value !== 'string') {
344 if (typeof value !== 'number')
345 throw new Error(`${fmt(key)} must be an int.`);
346
347 if (!Number.isSafeInteger(value))
348 throw new Error(`${fmt(key)} must be an int.`);
349
350 return value;
351 }
352
353 if (!/^\-?\d+$/.test(value))
354 throw new Error(`${fmt(key)} must be an int.`);
355
356 const num = parseInt(value, 10);
357
358 if (!Number.isSafeInteger(num))
359 throw new Error(`${fmt(key)} must be an int.`);
360
361 return num;
362 }
363
364 /**
365 * Get a config option (as a unsigned integer).
366 * @param {String} key
367 * @param {Object?} fallback
368 * @returns {Number|null}
369 */
370
371 uint(key, fallback) {
372 const value = this.int(key);
373
374 if (fallback === undefined)
375 fallback = null;
376
377 if (value === null)
378 return fallback;
379
380 if (value < 0)
381 throw new Error(`${fmt(key)} must be a uint.`);
382
383 return value;
384 }
385
386 /**
387 * Get a config option (as a float).
388 * @param {String} key
389 * @param {Object?} fallback
390 * @returns {Number|null}
391 */
392
393 float(key, fallback) {
394 const value = this.get(key);
395
396 if (fallback === undefined)
397 fallback = null;
398
399 if (value === null)
400 return fallback;
401
402 if (typeof value !== 'string') {
403 if (typeof value !== 'number')
404 throw new Error(`${fmt(key)} must be a float.`);
405
406 if (!isFinite(value))
407 throw new Error(`${fmt(key)} must be a float.`);
408
409 return value;
410 }
411
412 if (!/^\-?\d*(?:\.\d*)?$/.test(value))
413 throw new Error(`${fmt(key)} must be a float.`);
414
415 if (!/\d/.test(value))
416 throw new Error(`${fmt(key)} must be a float.`);
417
418 const num = parseFloat(value);
419
420 if (!isFinite(num))
421 throw new Error(`${fmt(key)} must be a float.`);
422
423 return num;
424 }
425
426 /**
427 * Get a config option (as a positive float).
428 * @param {String} key
429 * @param {Object?} fallback
430 * @returns {Number|null}
431 */
432
433 ufloat(key, fallback) {
434 const value = this.float(key);
435
436 if (fallback === undefined)
437 fallback = null;
438
439 if (value === null)
440 return fallback;
441
442 if (value < 0)
443 throw new Error(`${fmt(key)} must be a positive float.`);
444
445 return value;
446 }
447
448 /**
449 * Get a value (as a fixed number).
450 * @param {String} key
451 * @param {Number?} exp
452 * @param {Object?} fallback
453 * @returns {Number|null}
454 */
455
456 fixed(key, exp, fallback) {
457 const value = this.float(key);
458
459 if (fallback === undefined)
460 fallback = null;
461
462 if (value === null)
463 return fallback;
464
465 try {
466 return fromFloat(value, exp || 0);
467 } catch (e) {
468 throw new Error(`${fmt(key)} must be a fixed number.`);
469 }
470 }
471
472 /**
473 * Get a value (as a positive fixed number).
474 * @param {String} key
475 * @param {Number?} exp
476 * @param {Object?} fallback
477 * @returns {Number|null}
478 */
479
480 ufixed(key, exp, fallback) {
481 const value = this.fixed(key, exp);
482
483 if (fallback === undefined)
484 fallback = null;
485
486 if (value === null)
487 return fallback;
488
489 if (value < 0)
490 throw new Error(`${fmt(key)} must be a positive fixed number.`);
491
492 return value;
493 }
494
495 /**
496 * Get a config option (as a boolean).
497 * @param {String} key
498 * @param {Object?} fallback
499 * @returns {Boolean|null}
500 */
501
502 bool(key, fallback) {
503 const value = this.get(key);
504
505 if (fallback === undefined)
506 fallback = null;
507
508 if (value === null)
509 return fallback;
510
511 // Bitcoin Core compat.
512 if (typeof value === 'number') {
513 if (value === 1)
514 return true;
515
516 if (value === 0)
517 return false;
518 }
519
520 if (typeof value !== 'string') {
521 if (typeof value !== 'boolean')
522 throw new Error(`${fmt(key)} must be a boolean.`);
523 return value;
524 }
525
526 if (value === 'true' || value === '1')
527 return true;
528
529 if (value === 'false' || value === '0')
530 return false;
531
532 throw new Error(`${fmt(key)} must be a boolean.`);
533 }
534
535 /**
536 * Get a config option (as a buffer).
537 * @param {String} key
538 * @param {Object?} fallback
539 * @returns {Buffer|null}
540 */
541
542 buf(key, fallback, enc) {
543 const value = this.get(key);
544
545 if (!enc)
546 enc = 'hex';
547
548 if (fallback === undefined)
549 fallback = null;
550
551 if (value === null)
552 return fallback;
553
554 if (typeof value !== 'string') {
555 if (!Buffer.isBuffer(value))
556 throw new Error(`${fmt(key)} must be a buffer.`);
557 return value;
558 }
559
560 const data = Buffer.from(value, enc);
561
562 if (data.length !== Buffer.byteLength(value, enc))
563 throw new Error(`${fmt(key)} must be a ${enc} string.`);
564
565 return data;
566 }
567
568 /**
569 * Get a config option (as an array of strings).
570 * @param {String} key
571 * @param {Object?} fallback
572 * @returns {String[]|null}
573 */
574
575 array(key, fallback) {
576 const value = this.get(key);
577
578 if (fallback === undefined)
579 fallback = null;
580
581 if (value === null)
582 return fallback;
583
584 if (typeof value !== 'string') {
585 if (!Array.isArray(value))
586 throw new Error(`${fmt(key)} must be an array.`);
587 return value;
588 }
589
590 const parts = value.trim().split(/\s*,\s*/);
591 const result = [];
592
593 for (const part of parts) {
594 if (part.length === 0)
595 continue;
596
597 result.push(part);
598 }
599
600 return result;
601 }
602
603 /**
604 * Get a config option (as an object).
605 * @param {String} key
606 * @param {Object?} fallback
607 * @returns {Object|null}
608 */
609
610 obj(key, fallback) {
611 const value = this.get(key);
612
613 if (fallback === undefined)
614 fallback = null;
615
616 if (value === null)
617 return fallback;
618
619 if (typeof value !== 'object' || Array.isArray(value))
620 throw new Error(`${fmt(key)} must be an object.`);
621
622 return value;
623 }
624
625 /**
626 * Get a config option (as a function).
627 * @param {String} key
628 * @param {Object?} fallback
629 * @returns {Function|null}
630 */
631
632 func(key, fallback) {
633 const value = this.get(key);
634
635 if (fallback === undefined)
636 fallback = null;
637
638 if (value === null)
639 return fallback;
640
641 if (typeof value !== 'function')
642 throw new Error(`${fmt(key)} must be a function.`);
643
644 return value;
645 }
646
647 /**
648 * Get a config option (as a string).
649 * @param {String} key
650 * @param {Object?} fallback
651 * @returns {String|null}
652 */
653
654 path(key, fallback) {
655 let value = this.str(key);
656
657 if (fallback === undefined)
658 fallback = null;
659
660 if (value === null)
661 return fallback;
662
663 if (value.length === 0)
664 return fallback;
665
666 switch (value[0]) {
667 case '~': // home dir
668 value = Path.join(HOME, value.substring(1));
669 break;
670 case '@': // prefix
671 value = Path.join(this.prefix, value.substring(1));
672 break;
673 default: // cwd
674 break;
675 }
676
677 return Path.normalize(value);
678 }
679
680 /**
681 * Get a config option (in MB).
682 * @param {String} key
683 * @param {Object?} fallback
684 * @returns {Number|null}
685 */
686
687 mb(key, fallback) {
688 const value = this.uint(key);
689
690 if (fallback === undefined)
691 fallback = null;
692
693 if (value === null)
694 return fallback;
695
696 return value * 1024 * 1024;
697 }
698
699 /**
700 * Grab suffix from config data.
701 * @returns {String}
702 */
703
704 getSuffix() {
705 if (!this.suffix)
706 throw new Error('No suffix presented.');
707
708 const suffix = this.str(this.suffix, this.fallback);
709
710 assert(isAlpha(suffix), 'Bad suffix.');
711
712 return suffix;
713 }
714
715 /**
716 * Grab prefix from config data.
717 * @private
718 * @returns {String}
719 */
720
721 getPrefix() {
722 let prefix = this.str('prefix');
723
724 if (prefix) {
725 if (prefix[0] === '~')
726 prefix = Path.join(HOME, prefix.substring(1));
727 } else {
728 prefix = Path.join(HOME, `.${this.module}`);
729 }
730
731 if (this.suffix) {
732 const suffix = this.str(this.suffix);
733
734 if (suffix) {
735 assert(isAlpha(suffix), 'Bad suffix.');
736 if (this.fallback && suffix !== this.fallback)
737 prefix = Path.join(prefix, suffix);
738 }
739 }
740
741 return Path.normalize(prefix);
742 }
743
744 /**
745 * Grab config filename from config data.
746 * @private
747 * @param {String} file
748 * @returns {String}
749 */
750
751 getFile(file) {
752 const name = this.str('config');
753
754 if (name)
755 return name;
756
757 return Path.join(this.prefix, file);
758 }
759
760 /**
761 * Create a file path using `prefix`.
762 * @param {String} file
763 * @returns {String}
764 */
765
766 location(file) {
767 return Path.join(this.prefix, file);
768 }
769
770 /**
771 * Parse config text.
772 * @private
773 * @param {String} text
774 */
775
776 parseConfig(text) {
777 assert(typeof text === 'string', 'Config must be text.');
778
779 if (text.charCodeAt(0) === 0xfeff)
780 text = text.substring(1);
781
782 text = text.replace(/\r\n/g, '\n');
783 text = text.replace(/\r/g, '\n');
784 text = text.replace(/\\\n/g, '');
785
786 let num = 0;
787
788 for (const chunk of text.split('\n')) {
789 const line = chunk.trim();
790
791 num += 1;
792
793 if (line.length === 0)
794 continue;
795
796 if (line[0] === '#')
797 continue;
798
799 const index = line.indexOf(':');
800
801 if (index === -1)
802 throw new Error(`Expected ':' on line ${num}: "${line}".`);
803
804 let key = line.substring(0, index).trim();
805
806 key = key.replace(/\-/g, '');
807
808 if (!isLowerKey(key))
809 throw new Error(`Invalid option on line ${num}: ${key}.`);
810
811 const value = line.substring(index + 1).trim();
812
813 if (value.length === 0)
814 continue;
815
816 const alias = this.alias[key];
817
818 if (alias)
819 key = alias;
820
821 this.data[key] = value;
822 }
823 }
824
825 /**
826 * Parse arguments.
827 * @private
828 * @param {Array?} argv
829 */
830
831 parseArg(argv) {
832 if (!argv || typeof argv !== 'object')
833 argv = process.argv;
834
835 assert(Array.isArray(argv));
836
837 let last = null;
838 let pass = false;
839
840 for (let i = 2; i < argv.length; i++) {
841 const arg = argv[i];
842
843 assert(typeof arg === 'string');
844
845 if (arg === '--') {
846 pass = true;
847 continue;
848 }
849
850 if (pass) {
851 this.pass.push(arg);
852 continue;
853 }
854
855 if (arg.length === 0) {
856 last = null;
857 continue;
858 }
859
860 if (arg.indexOf('--') === 0) {
861 const index = arg.indexOf('=');
862
863 let key = null;
864 let value = null;
865 let empty = false;
866
867 if (index !== -1) {
868 // e.g. --opt=val
869 key = arg.substring(2, index);
870 value = arg.substring(index + 1);
871 last = null;
872 empty = false;
873 } else {
874 // e.g. --opt
875 key = arg.substring(2);
876 value = 'true';
877 last = null;
878 empty = true;
879 }
880
881 key = key.replace(/\-/g, '');
882
883 if (!isLowerKey(key))
884 throw new Error(`Invalid argument: --${key}.`);
885
886 if (value.length === 0)
887 continue;
888
889 // Do not allow one-letter aliases.
890 if (key.length > 1) {
891 const alias = this.alias[key];
892 if (alias)
893 key = alias;
894 }
895
896 this.args[key] = value;
897
898 if (empty)
899 last = key;
900
901 continue;
902 }
903
904 if (arg[0] === '-') {
905 // e.g. -abc
906 last = null;
907
908 for (let j = 1; j < arg.length; j++) {
909 let key = arg[j];
910
911 if ((key < 'a' || key > 'z')
912 && (key < 'A' || key > 'Z')
913 && (key < '0' || key > '9')
914 && key !== '?') {
915 throw new Error(`Invalid argument: -${key}.`);
916 }
917
918 const alias = this.alias[key];
919
920 if (alias)
921 key = alias;
922
923 this.args[key] = 'true';
924
925 last = key;
926 }
927
928 continue;
929 }
930
931 // e.g. foo
932 const value = arg;
933
934 if (value.length === 0) {
935 last = null;
936 continue;
937 }
938
939 if (last) {
940 this.args[last] = value;
941 last = null;
942 } else {
943 this.argv.push(value);
944 }
945 }
946 }
947
948 /**
949 * Parse environment variables.
950 * @private
951 * @param {Object?} env
952 * @returns {Object}
953 */
954
955 parseEnv(env) {
956 let prefix = this.module;
957
958 prefix = prefix.toUpperCase();
959 prefix = prefix.replace(/-/g, '_');
960 prefix += '_';
961
962 if (!env || typeof env !== 'object')
963 env = process.env;
964
965 assert(env && typeof env === 'object');
966
967 for (let key of Object.keys(env)) {
968 const value = env[key];
969
970 assert(typeof value === 'string');
971
972 if (key.indexOf(prefix) !== 0)
973 continue;
974
975 key = key.substring(prefix.length);
976 key = key.replace(/_/g, '');
977
978 if (!isUpperKey(key))
979 continue;
980
981 if (value.length === 0)
982 continue;
983
984 key = key.toLowerCase();
985
986 // Do not allow one-letter aliases.
987 if (key.length > 1) {
988 const alias = this.alias[key];
989 if (alias)
990 key = alias;
991 }
992
993 this.env[key] = value;
994 }
995 }
996
997 /**
998 * Parse uri querystring variables.
999 * @private
1000 * @param {String} query
1001 */
1002
1003 parseQuery(query) {
1004 if (typeof query !== 'string') {
1005 if (!global.location)
1006 return {};
1007
1008 query = global.location.search;
1009
1010 if (typeof query !== 'string')
1011 return {};
1012 }
1013
1014 return this.parseForm(query, '?', this.query);
1015 }
1016
1017 /**
1018 * Parse uri hash variables.
1019 * @private
1020 * @param {String} hash
1021 */
1022
1023 parseHash(hash) {
1024 if (typeof hash !== 'string') {
1025 if (!global.location)
1026 return {};
1027
1028 hash = global.location.hash;
1029
1030 if (typeof hash !== 'string')
1031 return {};
1032 }
1033
1034 return this.parseForm(hash, '#', this.hash);
1035 }
1036
1037 /**
1038 * Parse form-urlencoded variables.
1039 * @private
1040 * @param {String} query
1041 * @param {String} ch
1042 * @param {Object} map
1043 */
1044
1045 parseForm(query, ch, map) {
1046 assert(typeof query === 'string');
1047
1048 if (query.length === 0)
1049 return;
1050
1051 if (query[0] === ch)
1052 query = query.substring(1);
1053
1054 for (const pair of query.split('&')) {
1055 const index = pair.indexOf('=');
1056
1057 let key, value;
1058 if (index !== -1) {
1059 key = pair.substring(0, index);
1060 value = pair.substring(index + 1);
1061 } else {
1062 key = pair;
1063 value = 'true';
1064 }
1065
1066 key = unescape(key);
1067 key = key.replace(/\-/g, '');
1068
1069 if (!isLowerKey(key))
1070 continue;
1071
1072 value = unescape(value);
1073
1074 if (value.length === 0)
1075 continue;
1076
1077 const alias = this.alias[key];
1078
1079 if (alias)
1080 key = alias;
1081
1082 map[key] = value;
1083 }
1084 }
1085}
1086
1087/*
1088 * Helpers
1089 */
1090
1091function fmt(key) {
1092 if (Array.isArray(key))
1093 key = key[0];
1094
1095 if (typeof key === 'number')
1096 return `Argument #${key}`;
1097
1098 return key;
1099}
1100
1101function unescape(str) {
1102 try {
1103 str = decodeURIComponent(str);
1104 str = str.replace(/\+/g, ' ');
1105 } catch (e) {
1106 ;
1107 }
1108 str = str.replace(/\0/g, '');
1109 return str;
1110}
1111
1112function isAlpha(str) {
1113 return /^[a-z0-9_\-]+$/i.test(str);
1114}
1115
1116function isKey(key) {
1117 return /^[a-zA-Z0-9]+$/.test(key);
1118}
1119
1120function isLowerKey(key) {
1121 if (!isKey(key))
1122 return false;
1123
1124 return !/[A-Z]/.test(key);
1125}
1126
1127function isUpperKey(key) {
1128 if (!isKey(key))
1129 return false;
1130
1131 return !/[a-z]/.test(key);
1132}
1133
1134function _filter(name, a, b) {
1135 for (const key of Object.keys(a)) {
1136 if (key.length > name.length && key.indexOf(name) === 0) {
1137 const sub = key.substring(name.length);
1138 b[sub] = a[key];
1139 }
1140 }
1141}
1142
1143function fromFloat(num, exp) {
1144 assert(typeof num === 'number' && isFinite(num));
1145 assert(Number.isSafeInteger(exp));
1146
1147 let str = num.toFixed(exp);
1148 let sign = 1;
1149
1150 if (str.length > 0 && str[0] === '-') {
1151 str = str.substring(1);
1152 sign = -1;
1153 }
1154
1155 let hi = str;
1156 let lo = '0';
1157
1158 const index = str.indexOf('.');
1159
1160 if (index !== -1) {
1161 hi = str.substring(0, index);
1162 lo = str.substring(index + 1);
1163 }
1164
1165 hi = hi.replace(/^0+/, '');
1166 lo = lo.replace(/0+$/, '');
1167
1168 assert(hi.length <= 16 - exp,
1169 'Fixed number string exceeds 2^53-1.');
1170
1171 assert(lo.length <= exp,
1172 'Too many decimal places in fixed number string.');
1173
1174 if (hi.length === 0)
1175 hi = '0';
1176
1177 while (lo.length < exp)
1178 lo += '0';
1179
1180 if (lo.length === 0)
1181 lo = '0';
1182
1183 assert(/^\d+$/.test(hi) && /^\d+$/.test(lo),
1184 'Non-numeric characters in fixed number string.');
1185
1186 hi = parseInt(hi, 10);
1187 lo = parseInt(lo, 10);
1188
1189 const mult = Math.pow(10, exp);
1190 const maxLo = Number.MAX_SAFE_INTEGER % mult;
1191 const maxHi = (Number.MAX_SAFE_INTEGER - maxLo) / mult;
1192
1193 assert(hi < maxHi || (hi === maxHi && lo <= maxLo),
1194 'Fixed number string exceeds 2^53-1.');
1195
1196 return sign * (hi * mult + lo);
1197}
1198
1199/*
1200 * Expose
1201 */
1202
1203module.exports = Config;