Compare commits
3040 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4290ea7539 | ||
|
|
e2b0558c9c | ||
|
|
bb941dd2b6 | ||
|
|
1d39e75dac | ||
|
|
396c6c8e93 | ||
|
|
b435acc6b4 | ||
|
|
f161086705 | ||
|
|
c56dd23358 | ||
|
|
be42a3b9c8 | ||
|
|
8e19b7e402 | ||
|
|
f6a4631130 | ||
|
|
c7d20d8601 | ||
|
|
536ee84034 | ||
|
|
08b2b7b3dd | ||
|
|
ed21930260 | ||
|
|
51600c4a4f | ||
|
|
4b43f89eb8 | ||
|
|
d24ba1afce | ||
|
|
f77ddf4890 | ||
|
|
1343a107cb | ||
|
|
88a2dbab9d | ||
|
|
df545e62b9 | ||
|
|
4a53231a71 | ||
|
|
5a01c71098 | ||
|
|
c7898130e7 | ||
|
|
d172282755 | ||
|
|
d771ea2cc0 | ||
|
|
e5ee170e8d | ||
|
|
0e2e118719 | ||
|
|
9e59868b84 | ||
|
|
81c18e2f69 | ||
|
|
49b855c761 | ||
|
|
de784f6e52 | ||
|
|
bf705e7d43 | ||
|
|
002ed911b1 | ||
|
|
fa219d96f1 | ||
|
|
19c8a8a310 | ||
|
|
86746296be | ||
|
|
9ab5108664 | ||
|
|
4707ddb861 | ||
|
|
eaf185242a | ||
|
|
a4edfdd770 | ||
|
|
3ca7c792df | ||
|
|
79de8f1199 | ||
|
|
eccaf723f5 | ||
|
|
740d350306 | ||
|
|
c51e21a242 | ||
|
|
179eb9a379 | ||
|
|
961bbab682 | ||
|
|
3104e6073f | ||
|
|
6f95a2e17d | ||
|
|
d6f087efad | ||
|
|
3c32e3f64d | ||
|
|
766100aad0 | ||
|
|
89feb7f9e9 | ||
|
|
275b594f7c | ||
|
|
2329a0a0e5 | ||
|
|
e112da3c12 | ||
|
|
9672a8f870 | ||
|
|
6717ae1bc3 | ||
|
|
bd4c59cf34 | ||
|
|
0e50d32ec5 | ||
|
|
9f87fec52d | ||
|
|
080357a4f5 | ||
|
|
a0a369a7ef | ||
|
|
0d2af8ce00 | ||
|
|
08e5c13da4 | ||
|
|
64fc664479 | ||
|
|
8105e3eb07 | ||
|
|
0a0d95bcbd | ||
|
|
9a8a279b82 | ||
|
|
5a2eeed066 | ||
|
|
0fd6190b46 | ||
|
|
f0468d391d | ||
|
|
ddbe914132 | ||
|
|
4a70dff3f2 | ||
|
|
33c2207427 | ||
|
|
e18441d430 | ||
|
|
43f19c0494 | ||
|
|
7d05017648 | ||
|
|
7413e911c2 | ||
|
|
0b9258b8fb | ||
|
|
65f47c0343 | ||
|
|
9e7803e802 | ||
|
|
d358d82cbc | ||
|
|
85db3be5d0 | ||
|
|
50a7319b69 | ||
|
|
cc38f3a53f | ||
|
|
7a28e0534b | ||
|
|
31808e83d5 | ||
|
|
3f62b14d83 | ||
|
|
91178a8e6f | ||
|
|
7f0544e1c6 | ||
|
|
92ea854ea7 | ||
|
|
894560f6ce | ||
|
|
dbcfb06d3e | ||
|
|
86d579b1ec | ||
|
|
d7c7ac4aaf | ||
|
|
78d558c858 | ||
|
|
9d43334429 | ||
|
|
4bd609a5ee | ||
|
|
f30668aeff | ||
|
|
0c0f330ae2 | ||
|
|
5105aad91d | ||
|
|
61deeb8165 | ||
|
|
d735496641 | ||
|
|
c929819189 | ||
|
|
da202d54f1 | ||
|
|
20b46bf1a0 | ||
|
|
3034d63f90 | ||
|
|
8a5c60ac74 | ||
|
|
5318bab19f | ||
|
|
72ca1aeec7 | ||
|
|
89b5c7dee3 | ||
|
|
53d6f0c746 | ||
|
|
a50e868cb9 | ||
|
|
bdcd5fa8b1 | ||
|
|
bf2e12d141 | ||
|
|
61c00ae6da | ||
|
|
5383da81f8 | ||
|
|
6b780bc507 | ||
|
|
98bb5f117d | ||
|
|
772f7866bf | ||
|
|
b47c1c0547 | ||
|
|
3da6a51ef7 | ||
|
|
ea29704a73 | ||
|
|
4de8d338aa | ||
|
|
6a3a370bb1 | ||
|
|
fba4a0a0af | ||
|
|
ad51a17482 | ||
|
|
9b6619c45a | ||
|
|
50d5798c1f | ||
|
|
ad20c91f5b | ||
|
|
2cf778c253 | ||
|
|
37a92460f0 | ||
|
|
b090482bd0 | ||
|
|
7ca4b3a13d | ||
|
|
1baef27e9b | ||
|
|
cf6912f640 | ||
|
|
9803553b61 | ||
|
|
8f9a01ef33 | ||
|
|
ebeb4f7d6a | ||
|
|
0bb5472e50 | ||
|
|
3471731aaf | ||
|
|
d8aec9c43f | ||
|
|
9727e2607c | ||
|
|
055ae50e8e | ||
|
|
a581353434 | ||
|
|
67452c1e0f | ||
|
|
e294caeeb4 | ||
|
|
6822bd5584 | ||
|
|
ba326e2d92 | ||
|
|
b088f75bf3 | ||
|
|
4560648c7f | ||
|
|
81c360bdad | ||
|
|
ed5751f7ec | ||
|
|
d5a0b87194 | ||
|
|
ec4f1f5605 | ||
|
|
5f97ef18e1 | ||
|
|
49002e15cf | ||
|
|
8de0ded56c | ||
|
|
339574ec54 | ||
|
|
1fdd2ecf20 | ||
|
|
9641ae07f9 | ||
|
|
864afa85aa | ||
|
|
e0ba5d2812 | ||
|
|
9e5f94f5dc | ||
|
|
9088561acf | ||
|
|
09bb086853 | ||
|
|
534032f4fe | ||
|
|
ba09f5f1d5 | ||
|
|
97f35095ca | ||
|
|
5a1b121041 | ||
|
|
4080df0bbd | ||
|
|
e0774ed940 | ||
|
|
85513daea4 | ||
|
|
dd9e8c018d | ||
|
|
d591d2be1b | ||
|
|
32b748afd3 | ||
|
|
4675a7dabe | ||
|
|
a249132512 | ||
|
|
093346859d | ||
|
|
991af0f798 | ||
|
|
535838146c | ||
|
|
f646065c36 | ||
|
|
2eb8810bf6 | ||
|
|
d8830e2b3b | ||
|
|
7b9ff751d2 | ||
|
|
c68cc5a096 | ||
|
|
39f97a2af3 | ||
|
|
42adf5b103 | ||
|
|
167cc398ce | ||
|
|
33314ac658 | ||
|
|
e1352065d8 | ||
|
|
eea8bcd250 | ||
|
|
7d9fd4b26f | ||
|
|
80286f1e6f | ||
|
|
c00824a89a | ||
|
|
f9184d3938 | ||
|
|
088da9de01 | ||
|
|
6aa98b4a66 | ||
|
|
b0fa1f8d1b | ||
|
|
ff63ec97b7 | ||
|
|
9cf82cc1a1 | ||
|
|
78d40eab60 | ||
|
|
2b370b9616 | ||
|
|
38a0adaf33 | ||
|
|
8c4a55b2b1 | ||
|
|
a1f9208221 | ||
|
|
32ff3cd55e | ||
|
|
290f824e18 | ||
|
|
7f4f90e4f3 | ||
|
|
607acbd24e | ||
|
|
f7bbb33084 | ||
|
|
d9dacf6b8f | ||
|
|
19bc8169ce | ||
|
|
eef8d55d86 | ||
|
|
4deaa0de20 | ||
|
|
d195a51e2e | ||
|
|
47ba05850b | ||
|
|
64224dacb5 | ||
|
|
76bf4f44db | ||
|
|
c39ae52f7f | ||
|
|
3f91df2381 | ||
|
|
a19261d5f7 | ||
|
|
f759f4c43b | ||
|
|
81b3e9abd4 | ||
|
|
15d7d5507e | ||
|
|
cbe1d39e06 | ||
|
|
0c6454e10c | ||
|
|
cd7dafdf53 | ||
|
|
2b162d494f | ||
|
|
c1b849bdb2 | ||
|
|
1ef4afc82d | ||
|
|
813a543baf | ||
|
|
b2ae8c186e | ||
|
|
2979f2ebb4 | ||
|
|
86dc10bc06 | ||
|
|
1eccfa729b | ||
|
|
976c8b2d56 | ||
|
|
98fb680a92 | ||
|
|
d71dad8568 | ||
|
|
8df8028f43 | ||
|
|
c97baa6fdf | ||
|
|
448cfc8012 | ||
|
|
9b2868a178 | ||
|
|
7ba58cd4ce | ||
|
|
bb41c36578 | ||
|
|
293f304841 | ||
|
|
324a23585e | ||
|
|
fe69ec5881 | ||
|
|
595c90a464 | ||
|
|
042afe4157 | ||
|
|
cc2c6d20fd | ||
|
|
abbb8496cc | ||
|
|
4ca5157508 | ||
|
|
eeb39a0b2b | ||
|
|
0cf7bcaddc | ||
|
|
4e8f0c34f3 | ||
|
|
de3a5ff634 | ||
|
|
693b7d86dd | ||
|
|
fa34e2fafc | ||
|
|
1542cadde8 | ||
|
|
64399ad60e | ||
|
|
96c93b95f2 | ||
|
|
9df82113c4 | ||
|
|
55ef47f645 | ||
|
|
5e53fe113d | ||
|
|
5c49f87727 | ||
|
|
e0c4957da4 | ||
|
|
9512444eee | ||
|
|
dd26c09003 | ||
|
|
a30f5f1b50 | ||
|
|
b885e8423d | ||
|
|
2092e5b20c | ||
|
|
621331b03e | ||
|
|
2a1294f1c0 | ||
|
|
fb968da64f | ||
|
|
314a14a45b | ||
|
|
dc8121e7fc | ||
|
|
554a3b071e | ||
|
|
9fc91b3fc6 | ||
|
|
6dd651a4df | ||
|
|
c57556f49f | ||
|
|
18e5b9f6ec | ||
|
|
411f78d27a | ||
|
|
7ed7fca3ad | ||
|
|
5d3353189b | ||
|
|
f92238bf04 | ||
|
|
2b1ceef6a3 | ||
|
|
f4e73eef54 | ||
|
|
d751117219 | ||
|
|
a55075fdb6 | ||
|
|
288cd8f063 | ||
|
|
a7443778b8 | ||
|
|
e9b182d33b | ||
|
|
0c01c6a7c3 | ||
|
|
06f1982acf | ||
|
|
761173bce7 | ||
|
|
bfec582aee | ||
|
|
3a55b34214 | ||
|
|
95d5fd1789 | ||
|
|
159b80e853 | ||
|
|
f24bd92e34 | ||
|
|
e4f4d9c1c5 | ||
|
|
7ecf8dfdaa | ||
|
|
1bc3105002 | ||
|
|
66d445c5ef | ||
|
|
b2045e5bd8 | ||
|
|
64a4bfeadf | ||
|
|
46762a2af7 | ||
|
|
bd9e0142bc | ||
|
|
da922a45cf | ||
|
|
9538320928 | ||
|
|
b156fc9929 | ||
|
|
dda5814c30 | ||
|
|
7ca472279c | ||
|
|
670721f1f6 | ||
|
|
3c5eea5dfb | ||
|
|
497e2a9d45 | ||
|
|
e4a59c6990 | ||
|
|
85a3a18570 | ||
|
|
a150137c4b | ||
|
|
c6ece4292b | ||
|
|
c475a24854 | ||
|
|
1089c6bfb8 | ||
|
|
4085a4c219 | ||
|
|
a2665ba213 | ||
|
|
0a94c4a7c9 | ||
|
|
64355c9dde | ||
|
|
bd8906f46b | ||
|
|
26d8fa51b0 | ||
|
|
ad91099d5e | ||
|
|
361a3b2929 | ||
|
|
b9ed1fde62 | ||
|
|
a0ade26505 | ||
|
|
575eade84d | ||
|
|
2cf3642493 | ||
|
|
7c8be12d52 | ||
|
|
e849569b10 | ||
|
|
fa559fdbed | ||
|
|
74d631c71e | ||
|
|
305a02c8a2 | ||
|
|
5a3b7bc4c0 | ||
|
|
c83e1ac4fb | ||
|
|
857576dbe4 | ||
|
|
e00a636fc0 | ||
|
|
6f4cbd8688 | ||
|
|
de5b03f854 | ||
|
|
457d28fc55 | ||
|
|
e0bdde89c4 | ||
|
|
68db8e93b7 | ||
|
|
9069d8e3ec | ||
|
|
e146636250 | ||
|
|
c0af2a9eb5 | ||
|
|
ae5648ede1 | ||
|
|
197d6735d2 | ||
|
|
75b52bef6d | ||
|
|
848660f8e1 | ||
|
|
0e7b1f7c78 | ||
|
|
bbb211d819 | ||
|
|
9ca0152aac | ||
|
|
4b72484ca4 | ||
|
|
e469f00fc7 | ||
|
|
75e3f35932 | ||
|
|
9a8804ab94 | ||
|
|
f5b5227e53 | ||
|
|
14472d3b40 | ||
|
|
9f30dd0e7a | ||
|
|
49d69a293b | ||
|
|
23de98f892 | ||
|
|
8852712f0d | ||
|
|
b62def3d3e | ||
|
|
464c92e7bf | ||
|
|
95044ab767 | ||
|
|
a809321b63 | ||
|
|
b6f0f9b200 | ||
|
|
a42c7de112 | ||
|
|
b85036b78f | ||
|
|
3b75e13287 | ||
|
|
98ef5d0110 | ||
|
|
2c21d72a6a | ||
|
|
42efc0187c | ||
|
|
d5f007deea | ||
|
|
566b423065 | ||
|
|
1b4760c447 | ||
|
|
3025391821 | ||
|
|
4e62aa6e1d | ||
|
|
fcecd029c7 | ||
|
|
f4988328be | ||
|
|
7710c5ef49 | ||
|
|
3057a20791 | ||
|
|
b2981d3cc7 | ||
|
|
2c32a64bfe | ||
|
|
d3357d2acd | ||
|
|
91125f00e7 | ||
|
|
4c6b4cd4e7 | ||
|
|
c4574ff042 | ||
|
|
7babef6f93 | ||
|
|
2b2435210b | ||
|
|
4a428e802e | ||
|
|
5b15e75479 | ||
|
|
a40aed2b47 | ||
|
|
5cef2b13a3 | ||
|
|
c2936a6f2c | ||
|
|
06b7f0129f | ||
|
|
e44e6fed35 | ||
|
|
a74e2e585e | ||
|
|
d602897221 | ||
|
|
54a63b37bd | ||
|
|
1b8c229df2 | ||
|
|
aa7bbc1a62 | ||
|
|
6ccbc892c7 | ||
|
|
7b3980a5e2 | ||
|
|
7906f9d560 | ||
|
|
2ae923b73e | ||
|
|
15bbed093c | ||
|
|
23faeadea2 | ||
|
|
50c0d4bcd6 | ||
|
|
7f97afd3e0 | ||
|
|
c3994aa8e3 | ||
|
|
8166f981ae | ||
|
|
1cfbc2babb | ||
|
|
eb912547ac | ||
|
|
bcf62ec73d | ||
|
|
004ef7f5dc | ||
|
|
9ab990ff59 | ||
|
|
fb8e1be9f0 | ||
|
|
324560aeae | ||
|
|
95c6406d05 | ||
|
|
51b9fe644b | ||
|
|
fed10f3ee6 | ||
|
|
7cc3e5804b | ||
|
|
dcf0bbb02b | ||
|
|
f8b613b4e7 | ||
|
|
e48fc4c8d8 | ||
|
|
1b53b8d23f | ||
|
|
355566736a | ||
|
|
b99a76f9a6 | ||
|
|
912bbe54aa | ||
|
|
26a16b6945 | ||
|
|
3aed784ce2 | ||
|
|
feaf3dd119 | ||
|
|
3187ff6322 | ||
|
|
13f94a23f8 | ||
|
|
592e660deb | ||
|
|
3cdbdf4223 | ||
|
|
8cd875d06a | ||
|
|
bf36fe8143 | ||
|
|
d3ee16f38b | ||
|
|
41c5403700 | ||
|
|
e1d3af26da | ||
|
|
5a7cb9e047 | ||
|
|
a41068e633 | ||
|
|
bce04609a5 | ||
|
|
d4da81fa91 | ||
|
|
d3e31890ae | ||
|
|
9d014aeaa5 | ||
|
|
156de53dc1 | ||
|
|
9f2527d736 | ||
|
|
7ecc235b31 | ||
|
|
51ed3b2fc1 | ||
|
|
306e29da76 | ||
|
|
9cc84b1c62 | ||
|
|
f02b3f22ad | ||
|
|
e9b8163626 | ||
|
|
20390ae08e | ||
|
|
fcdcce7ee4 | ||
|
|
3d017d5f84 | ||
|
|
5455061e15 | ||
|
|
caa373c1be | ||
|
|
f4995dbc30 | ||
|
|
f14c423c64 | ||
|
|
6f63de81b1 | ||
|
|
75991321de | ||
|
|
b560d74b8e | ||
|
|
90133e3f90 | ||
|
|
672cb5dea4 | ||
|
|
7eb9e08ad2 | ||
|
|
2007b26d57 | ||
|
|
e804555278 | ||
|
|
6b75ba8903 | ||
|
|
54e559c5a6 | ||
|
|
cc7cc7f8fd | ||
|
|
6a92f922cb | ||
|
|
71c8c8aac0 | ||
|
|
304266020d | ||
|
|
02f45e545d | ||
|
|
3deb121043 | ||
|
|
bab0a7ffed | ||
|
|
77a43b4f78 | ||
|
|
1fcc365d9e | ||
|
|
b92c1e7da0 | ||
|
|
0c6c45b152 | ||
|
|
802c576c60 | ||
|
|
3822c6c520 | ||
|
|
47c85a105d | ||
|
|
d3adfc7c3e | ||
|
|
270a333179 | ||
|
|
895a929925 | ||
|
|
e484d426fe | ||
|
|
4c6dcf182d | ||
|
|
4bb1e335ea | ||
|
|
ae9054dc37 | ||
|
|
f61bdf06c6 | ||
|
|
0f16fa0ce1 | ||
|
|
84ce8e6f30 | ||
|
|
9eff9e5018 | ||
|
|
857d3b6d17 | ||
|
|
54cfb17145 | ||
|
|
99ba6b487c | ||
|
|
a1647b02cb | ||
|
|
b15322467b | ||
|
|
9229fe9ac7 | ||
|
|
488c458720 | ||
|
|
784b2631a8 | ||
|
|
3eb1af0278 | ||
|
|
7e5eb01a5e | ||
|
|
1fce8e5112 | ||
|
|
748379f0b0 | ||
|
|
da74d9b22d | ||
|
|
738e6feda0 | ||
|
|
95f6c4c5d5 | ||
|
|
1fde2fba29 | ||
|
|
a50d4d63eb | ||
|
|
dbc90090cf | ||
|
|
e65869a10b | ||
|
|
d58e162bfa | ||
|
|
7877a0d7d0 | ||
|
|
d635cca210 | ||
|
|
0e82e4b7c6 | ||
|
|
5f88ae44ec | ||
|
|
7a5225c961 | ||
|
|
77c2b7923d | ||
|
|
a80b1568ac | ||
|
|
a616e46b50 | ||
|
|
752ce789c2 | ||
|
|
660069865a | ||
|
|
9169254fff | ||
|
|
e12b00485e | ||
|
|
51a07a27db | ||
|
|
c8c9e572a6 | ||
|
|
7fb4824cd6 | ||
|
|
5e763b86dc | ||
|
|
b090ce0428 | ||
|
|
3dddb516c9 | ||
|
|
fb3c36af85 | ||
|
|
a477f47aec | ||
|
|
965570d250 | ||
|
|
ab37c1e562 | ||
|
|
40fda00db4 | ||
|
|
d5374454f4 | ||
|
|
edd22372d8 | ||
|
|
4c5577ddeb | ||
|
|
534ab23d9b | ||
|
|
fe9f4fa7a9 | ||
|
|
a8b1050d75 | ||
|
|
e80d3d59bc | ||
|
|
1efbd9dfd1 | ||
|
|
6c30c9c6b8 | ||
|
|
4528a1bdb6 | ||
|
|
c23a8de5a0 | ||
|
|
0cd4d41cdc | ||
|
|
2b275f0897 | ||
|
|
63913f4d45 | ||
|
|
b6c95492f1 | ||
|
|
ae03e34489 | ||
|
|
7eb887ca51 | ||
|
|
74d0166682 | ||
|
|
18159ce806 | ||
|
|
86d59783fa | ||
|
|
fb818c3149 | ||
|
|
37398188f9 | ||
|
|
d1a0da3f8d | ||
|
|
57c8c6dbc6 | ||
|
|
bbebba6481 | ||
|
|
f0a49fefa4 | ||
|
|
48f3bf9ea7 | ||
|
|
b7a7eebc78 | ||
|
|
2e76e44a5a | ||
|
|
718ce9ea11 | ||
|
|
a79be1e3ef | ||
|
|
0a709acafe | ||
|
|
29efd77eaf | ||
|
|
2d6ac8023e | ||
|
|
2f42f515b0 | ||
|
|
f304f0832b | ||
|
|
801f3a578d | ||
|
|
0a353eeb37 | ||
|
|
2e2490ce47 | ||
|
|
361a35b44c | ||
|
|
e61d64f618 | ||
|
|
42ad9a49f3 | ||
|
|
7538be68c5 | ||
|
|
21ac516248 | ||
|
|
7e7195c291 | ||
|
|
1fbc7e03f0 | ||
|
|
7b54d9e873 | ||
|
|
8ab2256b88 | ||
|
|
6e8c759dde | ||
|
|
a429902d41 | ||
|
|
f6c5a04088 | ||
|
|
edc23a860d | ||
|
|
67195f65e7 | ||
|
|
4619a511d4 | ||
|
|
cbf990fab9 | ||
|
|
8f182e4a9b | ||
|
|
7a2c9ddbc4 | ||
|
|
28e2a391b8 | ||
|
|
f86817bc86 | ||
|
|
cc28c6af1a | ||
|
|
d21a6c8598 | ||
|
|
7b2d572902 | ||
|
|
0cfb67bd06 | ||
|
|
f606d4b25f | ||
|
|
2191d20fb5 | ||
|
|
d13218caaf | ||
|
|
00f7914c5c | ||
|
|
a09f8451fd | ||
|
|
670c370b90 | ||
|
|
b2e400f474 | ||
|
|
66d3b6e9ed | ||
|
|
8c283c7b19 | ||
|
|
db331e94ee | ||
|
|
2e284d3842 | ||
|
|
a81b2064d4 | ||
|
|
a23e47d8f9 | ||
|
|
e3033173d7 | ||
|
|
1fbf6c94cd | ||
|
|
bf9066ae2e | ||
|
|
33a473c09b | ||
|
|
409e795ce9 | ||
|
|
6ca60cb013 | ||
|
|
ef52195167 | ||
|
|
ed930d2364 | ||
|
|
1b870e559e | ||
|
|
d3fb8bd6e9 | ||
|
|
1adc2ff930 | ||
|
|
fc40580dce | ||
|
|
4652608297 | ||
|
|
0f0efefbf7 | ||
|
|
ab0493f53a | ||
|
|
a1892d4411 | ||
|
|
036f80d734 | ||
|
|
3ca4c2fd45 | ||
|
|
016825d4de | ||
|
|
c35557aea5 | ||
|
|
b5959c6f01 | ||
|
|
2000938040 | ||
|
|
c795271502 | ||
|
|
1d4c810554 | ||
|
|
ccd3839a9b | ||
|
|
fda77cf37a | ||
|
|
dc8c185c39 | ||
|
|
99efcb521d | ||
|
|
d2bd397e8c | ||
|
|
944810614a | ||
|
|
21e5641196 | ||
|
|
077b11e33b | ||
|
|
8d4ad6ae75 | ||
|
|
ab47b3557f | ||
|
|
c5e5ed2b4d | ||
|
|
dd2cfe8bac | ||
|
|
8a17abae45 | ||
|
|
dfedfa9f4c | ||
|
|
56f92afbf4 | ||
|
|
52709f0ecb | ||
|
|
df140b5098 | ||
|
|
6b95ba7dd6 | ||
|
|
61295a968e | ||
|
|
0717407bea | ||
|
|
c46660a0d9 | ||
|
|
4c6200f233 | ||
|
|
3983e1a8e2 | ||
|
|
a6b774da81 | ||
|
|
de8ed6962d | ||
|
|
98edef389c | ||
|
|
1b16c26699 | ||
|
|
866c9a94ae | ||
|
|
6677317e27 | ||
|
|
5c563c2bdf | ||
|
|
8647b50cca | ||
|
|
21dc5d9de0 | ||
|
|
e258e5a783 | ||
|
|
75a28b558e | ||
|
|
6efa2ddfa4 | ||
|
|
b88fbcc960 | ||
|
|
7ccf8cbacd | ||
|
|
a1f6960d4e | ||
|
|
2784978eb5 | ||
|
|
b4133a0afd | ||
|
|
d59ebdd0dc | ||
|
|
3b5225a94f | ||
|
|
acce7ca717 | ||
|
|
d29dbe78d3 | ||
|
|
4fdea84534 | ||
|
|
0f14d23599 | ||
|
|
877c83eae7 | ||
|
|
27b7e3c4d6 | ||
|
|
77b7d7ee0b | ||
|
|
6edf731d46 | ||
|
|
6fd00e2598 | ||
|
|
862cfdf0ae | ||
|
|
943d01c208 | ||
|
|
ef59634c15 | ||
|
|
674c8c7c87 | ||
|
|
953861ab30 | ||
|
|
0026d77b7b | ||
|
|
ee77e0a1e4 | ||
|
|
f1dcc339b3 | ||
|
|
1f316a2f32 | ||
|
|
a5f9a8726b | ||
|
|
64111b8b7b | ||
|
|
762cf3ca41 | ||
|
|
70ff3a52ca | ||
|
|
e4077fb63b | ||
|
|
65ffd8d65d | ||
|
|
16dfb3fac6 | ||
|
|
ef5ff5bec6 | ||
|
|
e1c73a64ad | ||
|
|
81e1abd682 | ||
|
|
9068b6c17a | ||
|
|
5992ba12f1 | ||
|
|
bf833a39fc | ||
|
|
fccc3584d8 | ||
|
|
963e2839ce | ||
|
|
e85acee509 | ||
|
|
4e9567f539 | ||
|
|
a9c00a5856 | ||
|
|
c4e7c41a6e | ||
|
|
210186e754 | ||
|
|
b4c14219b6 | ||
|
|
299a3c48a1 | ||
|
|
cd0aee7626 | ||
|
|
bc82f590d4 | ||
|
|
bebb216df2 | ||
|
|
6331c20306 | ||
|
|
4dc4c7edd2 | ||
|
|
d2ff03cea4 | ||
|
|
61d587a4a5 | ||
|
|
85da5f83b5 | ||
|
|
6c0c53d00f | ||
|
|
196bce04cc | ||
|
|
ba82941cef | ||
|
|
63449872da | ||
|
|
14ee3f0f4b | ||
|
|
774828823c | ||
|
|
9e73e2555b | ||
|
|
dc68b7d9bf | ||
|
|
7a38504015 | ||
|
|
26fa5f3d1d | ||
|
|
112d52cb99 | ||
|
|
47f1f4c549 | ||
|
|
4b9d5a7685 | ||
|
|
fc7c6312e6 | ||
|
|
8b5b17b8e0 | ||
|
|
c7377da37f | ||
|
|
7cfb5e0b2a | ||
|
|
62dfa8f9dd | ||
|
|
7582abfa27 | ||
|
|
3f89a94df2 | ||
|
|
c5e880628a | ||
|
|
abd9024097 | ||
|
|
314b925ec9 | ||
|
|
d700d1d459 | ||
|
|
1a2207a045 | ||
|
|
2b85336c71 | ||
|
|
4b95ab2477 | ||
|
|
a0729304d1 | ||
|
|
dbdcb460e5 | ||
|
|
7588e50f9f | ||
|
|
40a4872f70 | ||
|
|
3b265322e0 | ||
|
|
0da30813da | ||
|
|
e973ea6f08 | ||
|
|
a942fe92fd | ||
|
|
9829b419b9 | ||
|
|
19194a7eb0 | ||
|
|
c13997bdb1 | ||
|
|
35c2d437fa | ||
|
|
a54522f796 | ||
|
|
6c971876de | ||
|
|
d8be3dfa27 | ||
|
|
2b57f94ccd | ||
|
|
a3944fe1d1 | ||
|
|
cfc8a506f7 | ||
|
|
afcf46561a | ||
|
|
59b97bfccb | ||
|
|
20db43c0a8 | ||
|
|
dc7760247b | ||
|
|
56a8e02bb8 | ||
|
|
e532248701 | ||
|
|
73100896d4 | ||
|
|
a95be3b4c5 | ||
|
|
75c4e196fa | ||
|
|
0359a2eccf | ||
|
|
b118932451 | ||
|
|
fd77152343 | ||
|
|
5afecdb11a | ||
|
|
1916137c3c | ||
|
|
c3b89efd9a | ||
|
|
0f3cd72dd3 | ||
|
|
ed59f6a67a | ||
|
|
592745f380 | ||
|
|
10b0d91bdc | ||
|
|
dea2b3a2c0 | ||
|
|
df151eabf9 | ||
|
|
22a873c73e | ||
|
|
c6da0c623d | ||
|
|
44b566789f | ||
|
|
940f78a8a7 | ||
|
|
06c912be72 | ||
|
|
91639ee836 | ||
|
|
b78533bb02 | ||
|
|
b5e3a6c5a3 | ||
|
|
a045c4ffec | ||
|
|
856ba1cf50 | ||
|
|
6510773789 | ||
|
|
7b280b3369 | ||
|
|
1618e112e1 | ||
|
|
c08a7dc6ce | ||
|
|
57731706b3 | ||
|
|
e9e926d054 | ||
|
|
5fa3d691cf | ||
|
|
d022dbc406 | ||
|
|
f96607c81b | ||
|
|
24cd4033c4 | ||
|
|
0bf7852e83 | ||
|
|
aaff2e687d | ||
|
|
77042abc23 | ||
|
|
ac18b74e66 | ||
|
|
76ba630d59 | ||
|
|
362755b22f | ||
|
|
cc21fe18ca | ||
|
|
4255d8d543 | ||
|
|
7353315baa | ||
|
|
6c63302d62 | ||
|
|
efda16b039 | ||
|
|
cb98672549 | ||
|
|
721061d054 | ||
|
|
d2da11c79a | ||
|
|
6ed9d4554a | ||
|
|
2dcd345349 | ||
|
|
81924b4a7e | ||
|
|
5a0c3a3b47 | ||
|
|
7b599fe7f7 | ||
|
|
4faaa07188 | ||
|
|
fa228b2571 | ||
|
|
9a216dc1bf | ||
|
|
0e0bafdeb6 | ||
|
|
5f0f037c46 | ||
|
|
c0e84dcd50 | ||
|
|
6b01cf0c1a | ||
|
|
30f9286ce9 | ||
|
|
e9ff8707ed | ||
|
|
12082ba3cc | ||
|
|
6d3ca3f56c | ||
|
|
15b426ebb6 | ||
|
|
bf1e708a4c | ||
|
|
73a39fa3ce | ||
|
|
4262e6f6f7 | ||
|
|
1bc88728f0 | ||
|
|
7bb0649b27 | ||
|
|
e46a7448d9 | ||
|
|
d102e1b15c | ||
|
|
0a0217cb66 | ||
|
|
3faa9d3a1e | ||
|
|
d902c7e30c | ||
|
|
25fe7bba67 | ||
|
|
ca340cdd7b | ||
|
|
266fbab8fd | ||
|
|
e57fd2a408 | ||
|
|
3328ae8afd | ||
|
|
1caecf70a2 | ||
|
|
6e347742d9 | ||
|
|
2e2dcfa416 | ||
|
|
4bb10a34e1 | ||
|
|
1632e23c78 | ||
|
|
11fca557af | ||
|
|
1cf14c6f01 | ||
|
|
1bba10e7c2 | ||
|
|
8f3096af0a | ||
|
|
6fdf37e2f2 | ||
|
|
a4d6e1f92c | ||
|
|
4a356476b1 | ||
|
|
c2171a08f2 | ||
|
|
84aad167d5 | ||
|
|
493b1eee1e | ||
|
|
6c5b260ece | ||
|
|
7394bb45d4 | ||
|
|
5d3302f6d7 | ||
|
|
92dada8a80 | ||
|
|
c6569b7c47 | ||
|
|
ea56efae47 | ||
|
|
7419009745 | ||
|
|
e58744c63a | ||
|
|
4c7fe147bc | ||
|
|
7605d48f0b | ||
|
|
d6f8e0a520 | ||
|
|
ab283fc801 | ||
|
|
d9d6b7aa37 | ||
|
|
b0c9a1850d | ||
|
|
764bd4b9ae | ||
|
|
a24fdf4250 | ||
|
|
13e3dba784 | ||
|
|
ccea5cdf88 | ||
|
|
51e2b97595 | ||
|
|
61501a7cbc | ||
|
|
bca4c34b3a | ||
|
|
2fab1028c5 | ||
|
|
5d3c66f615 | ||
|
|
cab83cae35 | ||
|
|
bd16378e23 | ||
|
|
65e431bd9e | ||
|
|
b9f2bddffc | ||
|
|
80e8b2339d | ||
|
|
7eda2fd4a6 | ||
|
|
36aafb9246 | ||
|
|
acc1df2031 | ||
|
|
c5766c89f6 | ||
|
|
73f5069971 | ||
|
|
819e4a574c | ||
|
|
8c0e23e196 | ||
|
|
05160227a3 | ||
|
|
5cfc1abf41 | ||
|
|
046a0917e7 | ||
|
|
9cbf217d42 | ||
|
|
bf949c0320 | ||
|
|
9e679f411c | ||
|
|
dd6eae52ef | ||
|
|
3e8c5d945c | ||
|
|
cdde7166cf | ||
|
|
251bd86ce5 | ||
|
|
dee2dc3777 | ||
|
|
533047a3a2 | ||
|
|
659325c85a | ||
|
|
7a2517fd61 | ||
|
|
0b2cb7e68c | ||
|
|
4e747b3697 | ||
|
|
84f0750525 | ||
|
|
fa21768931 | ||
|
|
934efcc5a0 | ||
|
|
91f1be8c70 | ||
|
|
56d1a016da | ||
|
|
d6159cc154 | ||
|
|
e9d7a8363c | ||
|
|
4e512a9a7b | ||
|
|
c1394e299a | ||
|
|
17c349973d | ||
|
|
f2bd838596 | ||
|
|
d5b72548fc | ||
|
|
2c822314df | ||
|
|
1b3d55f051 | ||
|
|
ff45f61b68 | ||
|
|
771a013c9f | ||
|
|
b354f61502 | ||
|
|
2e201c747a | ||
|
|
c1d2970f1e | ||
|
|
2efd075803 | ||
|
|
4b4d3c6845 | ||
|
|
760eff49b5 | ||
|
|
ba12ad8e7c | ||
|
|
249af2ded1 | ||
|
|
7403666a11 | ||
|
|
1cc8b150f3 | ||
|
|
faa25ee662 | ||
|
|
aa981ccd4a | ||
|
|
04378c1f91 | ||
|
|
53179a158e | ||
|
|
7ecde9fd2a | ||
|
|
320f79fe0d | ||
|
|
157346b055 | ||
|
|
904c08c436 | ||
|
|
fa238545a0 | ||
|
|
3f85c32c7c | ||
|
|
55809b2e87 | ||
|
|
5c32c1bdaa | ||
|
|
f84c11f63d | ||
|
|
2bcfb693b7 | ||
|
|
897d2bef6e | ||
|
|
0a3d219ad5 | ||
|
|
ba0dfe91c3 | ||
|
|
bf4e62b4b7 | ||
|
|
657f8940fa | ||
|
|
74d629c491 | ||
|
|
130e9d8192 | ||
|
|
0fb3743b6d | ||
|
|
a19aea1516 | ||
|
|
137f8d9471 | ||
|
|
1f57f1f000 | ||
|
|
468071ee98 | ||
|
|
23403f55a5 | ||
|
|
c25cf44d92 | ||
|
|
e5644b5712 | ||
|
|
9321b79d81 | ||
|
|
e97d535ff8 | ||
|
|
7589361e58 | ||
|
|
f15ef81693 | ||
|
|
d5f4968664 | ||
|
|
74e9edbccf | ||
|
|
51229d85cb | ||
|
|
b979b051cb | ||
|
|
17cf154bc2 | ||
|
|
c7f6954b97 | ||
|
|
9ccc1613cf | ||
|
|
9730ce1f6a | ||
|
|
55a7981547 | ||
|
|
2d99c365dd | ||
|
|
59d8be511a | ||
|
|
8349ffc001 | ||
|
|
c1368c4ecb | ||
|
|
c6935e040d | ||
|
|
f79c3ae91a | ||
|
|
bc27cd2772 | ||
|
|
2b02c44ca4 | ||
|
|
59fe8d1c4b | ||
|
|
9a5d62c4c3 | ||
|
|
9fa82150ee | ||
|
|
d7561ec208 | ||
|
|
743b834f6d | ||
|
|
0658836f87 | ||
|
|
a4e20c7059 | ||
|
|
d77f8ba747 | ||
|
|
78d35ecec3 | ||
|
|
c79bf5caaf | ||
|
|
66cbc6908a | ||
|
|
c04f409d23 | ||
|
|
1a9833132d | ||
|
|
9c3ddca269 | ||
|
|
72889d1c20 | ||
|
|
6c5433b226 | ||
|
|
31a7f83b82 | ||
|
|
39143fa6a1 | ||
|
|
a116a2a423 | ||
|
|
61d4c0ea63 | ||
|
|
44c7a1e92d | ||
|
|
09fd333997 | ||
|
|
ebe2a29543 | ||
|
|
1e83e0e935 | ||
|
|
fe9eb08648 | ||
|
|
2255c5b360 | ||
|
|
c046cf5695 | ||
|
|
3086c25079 | ||
|
|
3700780d14 | ||
|
|
5b134d7e9e | ||
|
|
58b1fb8de5 | ||
|
|
c0bd6645ce | ||
|
|
c6b79cd1c6 | ||
|
|
cb023089bb | ||
|
|
5a507aa1ba | ||
|
|
d23b88e2b8 | ||
|
|
3ac2ed20d2 | ||
|
|
d5ffbbb14c | ||
|
|
b92501ee35 | ||
|
|
570206af77 | ||
|
|
f6ae89587c | ||
|
|
fa30f597ad | ||
|
|
bdb8c02d5a | ||
|
|
5efd125d9b | ||
|
|
01508a2459 | ||
|
|
ed8bd34915 | ||
|
|
5c72791279 | ||
|
|
142f91425c | ||
|
|
d6344760ec | ||
|
|
64c009f266 | ||
|
|
a73decc792 | ||
|
|
f332ca4617 | ||
|
|
937791d33d | ||
|
|
a63a130723 | ||
|
|
0f1175bf15 | ||
|
|
e4db23f857 | ||
|
|
32e7dd030e | ||
|
|
e33950961e | ||
|
|
5d65cb419f | ||
|
|
536053d5f9 | ||
|
|
04ef095b88 | ||
|
|
66277cd71f | ||
|
|
58f01d0464 | ||
|
|
9ba9c56361 | ||
|
|
11902e3220 | ||
|
|
c3f79c4b2c | ||
|
|
576905e8b8 | ||
|
|
77a3b91afe | ||
|
|
40b58aa144 | ||
|
|
d5bcdc8eed | ||
|
|
8e57354307 | ||
|
|
126debf44e | ||
|
|
674649ca7f | ||
|
|
1a9a070c3b | ||
|
|
32bee35f85 | ||
|
|
a68ce5a5af | ||
|
|
026e9ca9c3 | ||
|
|
f1ce67d79b | ||
|
|
312be4772c | ||
|
|
be97a0062e | ||
|
|
00f0101723 | ||
|
|
f450240094 | ||
|
|
a7b8378736 | ||
|
|
98a70c2dd2 | ||
|
|
d502dd9857 | ||
|
|
1ec7033d46 | ||
|
|
28ec56a27f | ||
|
|
bf2afcd2ef | ||
|
|
8f710461da | ||
|
|
a07b8f693b | ||
|
|
a141873db8 | ||
|
|
08b91fd7ff | ||
|
|
cd7156dc55 | ||
|
|
c2cf535f88 | ||
|
|
ced9ab68fa | ||
|
|
cdb197a9c3 | ||
|
|
c8eacb9942 | ||
|
|
1c8e37cb64 | ||
|
|
1b0ced9f9b | ||
|
|
7b245b084a | ||
|
|
b00f64518f | ||
|
|
c3434e894d | ||
|
|
dd66f728b3 | ||
|
|
e40142b8c5 | ||
|
|
1dd1b839b8 | ||
|
|
d90d845f27 | ||
|
|
8c9ad81039 | ||
|
|
cef7a7b27d | ||
|
|
3e715ef21a | ||
|
|
86b43b4ffa | ||
|
|
3893c22275 | ||
|
|
65d883496b | ||
|
|
406da9fa2c | ||
|
|
16b79e32f6 | ||
|
|
e93aa42697 | ||
|
|
69e59916e2 | ||
|
|
475282984b | ||
|
|
2f89130f41 | ||
|
|
7c8a185a3d | ||
|
|
e9dd7eceb8 | ||
|
|
1dfb231033 | ||
|
|
4f0fa2a299 | ||
|
|
c14098a27c | ||
|
|
653af6a300 | ||
|
|
893b84d042 | ||
|
|
0dc2322269 | ||
|
|
5c8aac5572 | ||
|
|
157dacb3a5 | ||
|
|
890f645eeb | ||
|
|
2785459257 | ||
|
|
037590f831 | ||
|
|
85496684d8 | ||
|
|
4200ed4eb9 | ||
|
|
6b16f11d3b | ||
|
|
683bd3e5db | ||
|
|
6a10e94bfd | ||
|
|
25086386c6 | ||
|
|
d99ee32cc4 | ||
|
|
f1d318f229 | ||
|
|
a7fc455e05 | ||
|
|
c02c4a72e4 | ||
|
|
f97ce18ff9 | ||
|
|
dfd9afc2c4 | ||
|
|
eb5663a1ed | ||
|
|
418a7962a5 | ||
|
|
2ffce23c67 | ||
|
|
b828b50ccc | ||
|
|
a6aa40db97 | ||
|
|
45ede4beaf | ||
|
|
98a95c8bbf | ||
|
|
74bb8d92dc | ||
|
|
6cccc2c786 | ||
|
|
99a0a05947 | ||
|
|
94700dab89 | ||
|
|
c9d29c89bb | ||
|
|
7d4e554571 | ||
|
|
2858a74cbf | ||
|
|
28e599a835 | ||
|
|
1fb1d9cbb7 | ||
|
|
2f097e5dfc | ||
|
|
b71e3241be | ||
|
|
4b018bf421 | ||
|
|
f751de896f | ||
|
|
65aeae9c1e | ||
|
|
80943b0c26 | ||
|
|
a9657ac5c7 | ||
|
|
6fa2e49f3a | ||
|
|
50f0f70805 | ||
|
|
fc95d82c49 | ||
|
|
bb6c1c54ff | ||
|
|
c4414bd910 | ||
|
|
68733c74cc | ||
|
|
bc949b47d9 | ||
|
|
85ffe48743 | ||
|
|
42c911838d | ||
|
|
8917eabfd1 | ||
|
|
fc91469807 | ||
|
|
1735d9c091 | ||
|
|
00d754d466 | ||
|
|
906727f133 | ||
|
|
191d7f11f7 | ||
|
|
54fdc05e3a | ||
|
|
2267569c8d | ||
|
|
2f53ee3974 | ||
|
|
a3b846638f | ||
|
|
cc3033266c | ||
|
|
23b168515c | ||
|
|
049690fea2 | ||
|
|
d3de91de85 | ||
|
|
64eae37788 | ||
|
|
8c71b0b8df | ||
|
|
2bf1ac6b9c | ||
|
|
7197a434c2 | ||
|
|
5349115b94 | ||
|
|
4274e8f3a2 | ||
|
|
7610e159a3 | ||
|
|
9ec94434bb | ||
|
|
1ab444bda2 | ||
|
|
3990ba774f | ||
|
|
1fea9a25a5 | ||
|
|
3f57711c39 | ||
|
|
dc46018757 | ||
|
|
1464e5d5e4 | ||
|
|
95f7ed0de4 | ||
|
|
6e7240026a | ||
|
|
8bae7bf809 | ||
|
|
b78bb2d3d6 | ||
|
|
f9a6402825 | ||
|
|
8ec5f3cb18 | ||
|
|
c330394ff2 | ||
|
|
36e66ad5b4 | ||
|
|
a5a89efc06 | ||
|
|
1078c37043 | ||
|
|
535c7188a8 | ||
|
|
d6f9e216a6 | ||
|
|
7d6a879a67 | ||
|
|
668eac1f9e | ||
|
|
ff99436ce6 | ||
|
|
ec577fc075 | ||
|
|
cc9d51b7be | ||
|
|
cf2b54e8c0 | ||
|
|
a895d1d82a | ||
|
|
19423cc437 | ||
|
|
3ea12a2e1b | ||
|
|
11da4188ee | ||
|
|
5b68bad185 | ||
|
|
477e4a7421 | ||
|
|
9a982f05ac | ||
|
|
6959e2cf01 | ||
|
|
f5fe79326a | ||
|
|
f77633cee9 | ||
|
|
c858970573 | ||
|
|
fb3eceaee0 | ||
|
|
8e37fec8f8 | ||
|
|
ccaabfaee8 | ||
|
|
e712af33b7 | ||
|
|
c431fb30c5 | ||
|
|
1c04848480 | ||
|
|
ca1a0797fb | ||
|
|
7b497181fd | ||
|
|
92767b1078 | ||
|
|
7c66dacc40 | ||
|
|
632277f9bd | ||
|
|
3c23ae03f5 | ||
|
|
8117926ca3 | ||
|
|
31e9d27f1a | ||
|
|
0c067d60d8 | ||
|
|
1171d6b165 | ||
|
|
18022a2a48 | ||
|
|
1400b5cca4 | ||
|
|
98b5f9cc29 | ||
|
|
e766cb009e | ||
|
|
993e276858 | ||
|
|
a762fb4df4 | ||
|
|
00781fa459 | ||
|
|
c6e7367a4e | ||
|
|
3749bb1222 | ||
|
|
f3c83b2009 | ||
|
|
d3583c31bc | ||
|
|
2f638d8c0c | ||
|
|
74e43ff65a | ||
|
|
4f0b160ed3 | ||
|
|
709b8696fc | ||
|
|
e080c5d72e | ||
|
|
9cc177a9ad | ||
|
|
9ad4e2525e | ||
|
|
60b5fe0e76 | ||
|
|
9273f97d88 | ||
|
|
e2b7ac68ef | ||
|
|
98a6baf55a | ||
|
|
b5677387f5 | ||
|
|
cd3eaa4762 | ||
|
|
9733bf65de | ||
|
|
bc433f0234 | ||
|
|
ad7283ee6f | ||
|
|
5b941358a9 | ||
|
|
f19e898c57 | ||
|
|
590ffa184d | ||
|
|
46de51512f | ||
|
|
a13f2f26e4 | ||
|
|
c9c26f6f9f | ||
|
|
787c29e42b | ||
|
|
1c0b83d833 | ||
|
|
5c767d09b0 | ||
|
|
75dba51f56 | ||
|
|
78a1827af8 | ||
|
|
9e3733b33b | ||
|
|
4ef65589e8 | ||
|
|
cf9e670309 | ||
|
|
b09f98fbb4 | ||
|
|
e43e010163 | ||
|
|
2a5cee0bd3 | ||
|
|
c962bb79f5 | ||
|
|
add745551b | ||
|
|
7b8213e1f6 | ||
|
|
2e23a33dfc | ||
|
|
bca296ee37 | ||
|
|
2dbd647868 | ||
|
|
2991e7d1fe | ||
|
|
f6f12fc193 | ||
|
|
1fac8c1cea | ||
|
|
590462e2e4 | ||
|
|
578a61d331 | ||
|
|
ed542582f9 | ||
|
|
2574a63975 | ||
|
|
291c55befd | ||
|
|
23763363ba | ||
|
|
b7a978505f | ||
|
|
7db1f39473 | ||
|
|
6017578964 | ||
|
|
1dbcd2ac84 | ||
|
|
671ae6558f | ||
|
|
d2174725a9 | ||
|
|
f630c499d5 | ||
|
|
e8f91316ff | ||
|
|
a7cdb095d2 | ||
|
|
cfc29ef821 | ||
|
|
83c6775038 | ||
|
|
ae1b12c120 | ||
|
|
c423de545f | ||
|
|
84011371b7 | ||
|
|
f831ec6b7e | ||
|
|
05bfa9158d | ||
|
|
649016bc85 | ||
|
|
7492b874cf | ||
|
|
9ea21af61f | ||
|
|
dd18e52993 | ||
|
|
7712ee7f35 | ||
|
|
a4a13666e6 | ||
|
|
07ef4da892 | ||
|
|
e9ca4dc9ca | ||
|
|
57bb8fbc45 | ||
|
|
9175624210 | ||
|
|
289a417dfb | ||
|
|
8379a9b275 | ||
|
|
5b1fad118f | ||
|
|
5e8985810e | ||
|
|
d2568e2a29 | ||
|
|
6dfc21ef06 | ||
|
|
1f19f2f58c | ||
|
|
1728cc7a85 | ||
|
|
fa2fc2ba33 | ||
|
|
9a33498fed | ||
|
|
5001adf221 | ||
|
|
cd4e70b24b | ||
|
|
a3b873fd96 | ||
|
|
97bd03799e | ||
|
|
ffaaaae2b4 | ||
|
|
ff88d30d2f | ||
|
|
88e3c2ccb2 | ||
|
|
6155634d3b | ||
|
|
5029356514 | ||
|
|
e57d5a7e75 | ||
|
|
2bbe54cdcd | ||
|
|
0e2423c390 | ||
|
|
8fadad5891 | ||
|
|
32455cd20f | ||
|
|
1e0e7f16a7 | ||
|
|
95884d77cf | ||
|
|
b69373f5e7 | ||
|
|
61e85474d3 | ||
|
|
3a9bdc58af | ||
|
|
59c1c45d78 | ||
|
|
b5f6cedd30 | ||
|
|
c879af6860 | ||
|
|
22fe5a6ae7 | ||
|
|
57714c86a6 | ||
|
|
70cd375049 | ||
|
|
e27e1dd42f | ||
|
|
8ad18bc7db | ||
|
|
5893ac1a37 | ||
|
|
83c3ab97f9 | ||
|
|
9b6be140fa | ||
|
|
a6a60392c2 | ||
|
|
ccfa0b991e | ||
|
|
73b0e68993 | ||
|
|
31a4eef001 | ||
|
|
d6e2c9a6b9 | ||
|
|
e069f5db13 | ||
|
|
96b59cf0df | ||
|
|
c1e1d4b7e3 | ||
|
|
32d869231e | ||
|
|
6f95f01dda | ||
|
|
61a1c44f26 | ||
|
|
e6c7899569 | ||
|
|
a74a14145e | ||
|
|
04022b22be | ||
|
|
4f3010691c | ||
|
|
672b477751 | ||
|
|
19360391a6 | ||
|
|
447be8af34 | ||
|
|
0989550e5c | ||
|
|
4d9b48b4d8 | ||
|
|
979652387d | ||
|
|
de84fbdd1c | ||
|
|
170e564319 | ||
|
|
530836876f | ||
|
|
1abc46058c | ||
|
|
892a3716ed | ||
|
|
5d982a9c8d | ||
|
|
6206224f1e | ||
|
|
766310e2c7 | ||
|
|
02a397d2ae | ||
|
|
4563c86acd | ||
|
|
0a59f8068e | ||
|
|
24548e3a48 | ||
|
|
f4784d49e7 | ||
|
|
14d5069c6e | ||
|
|
9fdd55c5c6 | ||
|
|
5bc523aeff | ||
|
|
3ae2e2e2c8 | ||
|
|
83e49da7f6 | ||
|
|
fe6df8778f | ||
|
|
69073c9d99 | ||
|
|
d5a0adea0c | ||
|
|
1bcb8e433d | ||
|
|
fa05cf1d78 | ||
|
|
63dff0d34d | ||
|
|
e228b197f9 | ||
|
|
4bbeb6caa3 | ||
|
|
139d25dff9 | ||
|
|
6984258404 | ||
|
|
ba66fd6546 | ||
|
|
47bbefd81f | ||
|
|
50f796049c | ||
|
|
618da545b1 | ||
|
|
21236f1938 | ||
|
|
4169a15066 | ||
|
|
6b10f61e11 | ||
|
|
83f119f7e4 | ||
|
|
041c899a35 | ||
|
|
5412fc01b3 | ||
|
|
76926d9252 | ||
|
|
3270961574 | ||
|
|
9a1ef71aec | ||
|
|
ba878925ad | ||
|
|
8d6b71bfaa | ||
|
|
b6845aa03c | ||
|
|
4a673bd92a | ||
|
|
fdffabfdd4 | ||
|
|
ba8591293b | ||
|
|
9b3aae10cf | ||
|
|
5c85c4df63 | ||
|
|
8ece2603fb | ||
|
|
1b54584820 | ||
|
|
3ce3866000 | ||
|
|
becc258b6c | ||
|
|
16c9e7119d | ||
|
|
f5888fea9c | ||
|
|
b5162c7549 | ||
|
|
1baea1da3d | ||
|
|
40899a6fe4 | ||
|
|
66c533431d | ||
|
|
74ae77f17a | ||
|
|
99eb1cced5 | ||
|
|
c880ecf3c4 | ||
|
|
c54c34d88e | ||
|
|
60b3c597a7 | ||
|
|
372ab32539 | ||
|
|
e14745a973 | ||
|
|
afffe584cf | ||
|
|
cf532f1e30 | ||
|
|
46425ba552 | ||
|
|
9f65fac4e6 | ||
|
|
af98feff51 | ||
|
|
339c741a2c | ||
|
|
d1bde23f00 | ||
|
|
be544faf0f | ||
|
|
eff77fd3bb | ||
|
|
3df57ba3dd | ||
|
|
4bae662357 | ||
|
|
357c90546e | ||
|
|
54769a90ac | ||
|
|
0550aedd54 | ||
|
|
5947ed21fc | ||
|
|
0a0949246f | ||
|
|
09c6e181fb | ||
|
|
02cf394bcd | ||
|
|
46afbf9888 | ||
|
|
7cf19d22a8 | ||
|
|
46d708360f | ||
|
|
7ec91e2366 | ||
|
|
a74ee47bf0 | ||
|
|
7b68b92498 | ||
|
|
274f3acd45 | ||
|
|
53dff8b30f | ||
|
|
9892ee273e | ||
|
|
7dccecb336 | ||
|
|
aa09da0403 | ||
|
|
98d7f1481c | ||
|
|
7d8bdbcf7e | ||
|
|
db009d3689 | ||
|
|
d7bc529a95 | ||
|
|
3f035294a6 | ||
|
|
36282e362f | ||
|
|
223353cf4d | ||
|
|
ded32da632 | ||
|
|
d6fc21cc34 | ||
|
|
b86e960456 | ||
|
|
7747bfe0f2 | ||
|
|
fabea8cc61 | ||
|
|
de4cd2eebc | ||
|
|
e16ce1d9c2 | ||
|
|
345ad741b3 | ||
|
|
e53108c493 | ||
|
|
6129378854 | ||
|
|
11a1416dcc | ||
|
|
9025538544 | ||
|
|
46929e1844 | ||
|
|
bf023cad48 | ||
|
|
f649236bad | ||
|
|
54b1454c0a | ||
|
|
a41b1c2f5e | ||
|
|
0327d7cb7a | ||
|
|
621d7c3683 | ||
|
|
ae7c3fca31 | ||
|
|
1b3295ff59 | ||
|
|
e40e7a0e4e | ||
|
|
cf5fd7cbf2 | ||
|
|
ec37a676a8 | ||
|
|
f33193e122 | ||
|
|
7c40204e4f | ||
|
|
2b2ffb3346 | ||
|
|
39b7e4ff9e | ||
|
|
a1109c43f6 | ||
|
|
887ea0cf06 | ||
|
|
3e0525ee9e | ||
|
|
e6eafc2843 | ||
|
|
3e8dea73d2 | ||
|
|
ed7baf57f0 | ||
|
|
3bb6a40f82 | ||
|
|
6b76b7004a | ||
|
|
bbf19ee944 | ||
|
|
f6c8c9e76c | ||
|
|
9dba1232b7 | ||
|
|
bf14c8983f | ||
|
|
3ab95384df | ||
|
|
953a581a94 | ||
|
|
3bfddd708c | ||
|
|
13a54ec318 | ||
|
|
2781640a2a | ||
|
|
7989f119d3 | ||
|
|
2e0aa7ae87 | ||
|
|
667dfd820e | ||
|
|
3e3c526a62 | ||
|
|
c01ce90e06 | ||
|
|
d68c8cb845 | ||
|
|
0b5ee7c79f | ||
|
|
23cbe1c426 | ||
|
|
de727bcddc | ||
|
|
c9d9730726 | ||
|
|
a380aed878 | ||
|
|
4dafa50799 | ||
|
|
f5a7db03cd | ||
|
|
d4ae241758 | ||
|
|
88947b9d42 | ||
|
|
3c68ad1302 | ||
|
|
e2b3389da6 | ||
|
|
938385c18b | ||
|
|
5d01b06877 | ||
|
|
f007c093eb | ||
|
|
53e69835ff | ||
|
|
939208903a | ||
|
|
7526f4d7b1 | ||
|
|
616b38c6fb | ||
|
|
a302df8f91 | ||
|
|
093512a55a | ||
|
|
007224c92e | ||
|
|
738ee33959 | ||
|
|
d720a6b250 | ||
|
|
5ce3cc17bb | ||
|
|
5c35205315 | ||
|
|
10751934e4 | ||
|
|
d658f91106 | ||
|
|
ee408f7add | ||
|
|
1294d764e2 | ||
|
|
eab1be0e2c | ||
|
|
15dd9d4c93 | ||
|
|
103ecff6a7 | ||
|
|
0182cdda69 | ||
|
|
fae96af17d | ||
|
|
57c1a03c4f | ||
|
|
cb8fe42c66 | ||
|
|
7f4b2574b0 | ||
|
|
fad163e0eb | ||
|
|
7ad3992bd1 | ||
|
|
e7b5c0e37c | ||
|
|
cc35127459 | ||
|
|
1c0ecd29c2 | ||
|
|
1f0fddd0e9 | ||
|
|
18c6b9bc09 | ||
|
|
2233b9a094 | ||
|
|
e0f000b913 | ||
|
|
73864e0809 | ||
|
|
cd8a85975c | ||
|
|
2f94ec20b0 | ||
|
|
09d9deae43 | ||
|
|
209fc07eac | ||
|
|
ae70cb89ed | ||
|
|
4c3324aea1 | ||
|
|
ac2fcee90f | ||
|
|
94e00115fe | ||
|
|
29255d2338 | ||
|
|
bda36e508a | ||
|
|
d2ea1273da | ||
|
|
39278cfce7 | ||
|
|
46cc48cfb9 | ||
|
|
eb4009949d | ||
|
|
57c48a4bd2 | ||
|
|
45011de494 | ||
|
|
5e51ed0a5f | ||
|
|
875802ef2a | ||
|
|
bbc255ef83 | ||
|
|
fc155ec3fc | ||
|
|
3a0c8a199a | ||
|
|
9c729c4490 | ||
|
|
19bdbd66e5 | ||
|
|
ea090f79ee | ||
|
|
1be6c54be2 | ||
|
|
a4eeb226b1 | ||
|
|
d136717636 | ||
|
|
ad0a2e6f58 | ||
|
|
0212bfb46e | ||
|
|
5e0d661542 | ||
|
|
4c5f529ef4 | ||
|
|
b765534b8b | ||
|
|
7c672c5ee9 | ||
|
|
090bf3f8ec | ||
|
|
9f4ef9cdad | ||
|
|
26fa66ef64 | ||
|
|
b69e4567c0 | ||
|
|
26b19350f2 | ||
|
|
2018b7e247 | ||
|
|
cbff4161a1 | ||
|
|
130bdfb7f2 | ||
|
|
d0c43f5aa9 | ||
|
|
f864a5f31e | ||
|
|
ff585584f6 | ||
|
|
2e0af64ac3 | ||
|
|
8a029e5147 | ||
|
|
4251089687 | ||
|
|
dbcd670ca8 | ||
|
|
cecae671d8 | ||
|
|
b4e23ac454 | ||
|
|
e7209ca085 | ||
|
|
022ef71d2c | ||
|
|
0fad891a3a | ||
|
|
47f608b502 | ||
|
|
4ab745f730 | ||
|
|
1f89ac11d2 | ||
|
|
1a5dae76c2 | ||
|
|
032c7f529a | ||
|
|
707818abcc | ||
|
|
58e8b4c618 | ||
|
|
9c0c2867dd | ||
|
|
4308c717c3 | ||
|
|
30bc8dfbe9 | ||
|
|
342b67581b | ||
|
|
75da5b650c | ||
|
|
5b6a53be3e | ||
|
|
5b5105c864 | ||
|
|
9d09146f68 | ||
|
|
1b19d20d0c | ||
|
|
0eacab5ddc | ||
|
|
0d7874bac1 | ||
|
|
5cddfafbd0 | ||
|
|
5ce133c47e | ||
|
|
53a579028c | ||
|
|
570a84b67a | ||
|
|
a68bfd7223 | ||
|
|
fd7176a445 | ||
|
|
0a06472639 | ||
|
|
3f3a7cd4f6 | ||
|
|
99415359d0 | ||
|
|
54cb2d268b | ||
|
|
0e40daecb6 | ||
|
|
21a822b082 | ||
|
|
a770d09687 | ||
|
|
27b81c4e11 | ||
|
|
8826c80e07 | ||
|
|
02e3ce7fc1 | ||
|
|
673e6aede5 | ||
|
|
5809dea0fc | ||
|
|
165ca9ebc1 | ||
|
|
154b9386f7 | ||
|
|
62dd7c3dbc | ||
|
|
82d098fc1a | ||
|
|
c69ac7f0f2 | ||
|
|
5578f4577d | ||
|
|
28bd8a32e3 | ||
|
|
24fd35e6af | ||
|
|
c4493d15b6 | ||
|
|
f9ef9739b0 | ||
|
|
d50b9103c2 | ||
|
|
196a611c9b | ||
|
|
7c057ad254 | ||
|
|
cced384e0a | ||
|
|
cce7abecd5 | ||
|
|
8013b3b5e9 | ||
|
|
64b979e25e | ||
|
|
5e7b004925 | ||
|
|
1e791d7f1b | ||
|
|
a255d1912c | ||
|
|
3d1266f815 | ||
|
|
ede78c540f | ||
|
|
c0f003b450 | ||
|
|
14190fc533 | ||
|
|
9f34009244 | ||
|
|
bc2fefd1b2 | ||
|
|
940b56ba5f | ||
|
|
8f2a1f5b2c | ||
|
|
20d0ae5304 | ||
|
|
2ba37af109 | ||
|
|
728f8e65d6 | ||
|
|
d78730edc0 | ||
|
|
b0d2593968 | ||
|
|
38a033f1a9 | ||
|
|
846ad1796a | ||
|
|
355a6d6b76 | ||
|
|
e91e993fb5 | ||
|
|
e71794d362 | ||
|
|
6090842372 | ||
|
|
0a3bd6456c | ||
|
|
ab6576d739 | ||
|
|
d5615b90bb | ||
|
|
cf1349ccb2 | ||
|
|
36190f1967 | ||
|
|
3ecbe22d99 | ||
|
|
b045dc37f5 | ||
|
|
015d0f065f | ||
|
|
d7cc6c7363 | ||
|
|
a5bc1c6f9e | ||
|
|
c2212f9ab3 | ||
|
|
3fb5bf14a6 | ||
|
|
f32b7d5de5 | ||
|
|
7a58567659 | ||
|
|
4be0a436f2 | ||
|
|
1e71fc91a1 | ||
|
|
0ab96dd4ca | ||
|
|
1509b2b96c | ||
|
|
6fd929160d | ||
|
|
5cdd062ed9 | ||
|
|
4430ea55c4 | ||
|
|
4b6cebc068 | ||
|
|
0259ed8314 | ||
|
|
a06303c2fd | ||
|
|
115b064081 | ||
|
|
3a2580fbc2 | ||
|
|
1197859865 | ||
|
|
293a95e938 | ||
|
|
9a3464a5ec | ||
|
|
55dddd3ef9 | ||
|
|
23956480b7 | ||
|
|
6dbaf6a36c | ||
|
|
1d8c7dcb97 | ||
|
|
41c8a5aff0 | ||
|
|
597fba2fd0 | ||
|
|
81605b2222 | ||
|
|
9e17825b53 | ||
|
|
c689cf7f13 | ||
|
|
0f0674daf6 | ||
|
|
d9b7fe2bb9 | ||
|
|
ace8121d0e | ||
|
|
f7be8d720b | ||
|
|
0a5038e533 | ||
|
|
5feba30956 | ||
|
|
23a228bbd9 | ||
|
|
598813ff88 | ||
|
|
118f0130d8 | ||
|
|
e77d3a4b7d | ||
|
|
bf0bd620f1 | ||
|
|
62edd76d7e | ||
|
|
cef6585cab | ||
|
|
481241b88d | ||
|
|
93c0c7cc10 | ||
|
|
01d9536bbd | ||
|
|
24a7c75926 | ||
|
|
c2eea7a9f7 | ||
|
|
e2812cadb5 | ||
|
|
cde958af16 | ||
|
|
bf70589b3c | ||
|
|
78d1114869 | ||
|
|
ab83caa77b | ||
|
|
42dd03bb84 | ||
|
|
cb09949fb9 | ||
|
|
0c6eac3b21 | ||
|
|
5e771c521c | ||
|
|
b8afbf7458 | ||
|
|
be59efbd6b | ||
|
|
99ccd06f23 | ||
|
|
f4d75f88a9 | ||
|
|
9112bd4936 | ||
|
|
3e513efd59 | ||
|
|
4100a22c7f | ||
|
|
71078d9332 | ||
|
|
72ba579e1e | ||
|
|
5fd2512a9b | ||
|
|
3a1a6a94d2 | ||
|
|
928178716a | ||
|
|
08419a20c0 | ||
|
|
60506ee2d1 | ||
|
|
6e411c9dd9 | ||
|
|
86db43e6dd | ||
|
|
252ac0bdaa | ||
|
|
4d8b1721bc | ||
|
|
fd390bc61b | ||
|
|
813a151524 | ||
|
|
c06502cd76 | ||
|
|
974ce1fd12 | ||
|
|
d2dc50fe8e | ||
|
|
ec50105e34 | ||
|
|
86ebed132d | ||
|
|
47ccafcbb2 | ||
|
|
9f901188af | ||
|
|
ccdd2af8be | ||
|
|
a387db6109 | ||
|
|
b88a35cd14 | ||
|
|
bcafaebf77 | ||
|
|
798f013219 | ||
|
|
61e6423d95 | ||
|
|
c32fec6b53 | ||
|
|
4c0d944992 | ||
|
|
57ee3f305d | ||
|
|
61ab9a904f | ||
|
|
820bd2f867 | ||
|
|
44c2b71e6c | ||
|
|
b806be3f49 | ||
|
|
2750284eea | ||
|
|
5366f1a2eb | ||
|
|
32625dc0b0 | ||
|
|
dfc8f517f2 | ||
|
|
b459c15d74 | ||
|
|
8a02b35d7d | ||
|
|
d4441823aa | ||
|
|
13e9a4f0da | ||
|
|
33471a33d6 | ||
|
|
a6fa0628f2 | ||
|
|
8e5aa2c393 | ||
|
|
1118ff598d | ||
|
|
09e93b4639 | ||
|
|
d8405e5f81 | ||
|
|
0295d0c9b4 | ||
|
|
d16bbe1e7e | ||
|
|
8e1e1d56ca | ||
|
|
4814922e67 | ||
|
|
81d1007bb7 | ||
|
|
63bb19d48d | ||
|
|
610d46a1a4 | ||
|
|
f7acef4642 | ||
|
|
fd5b6c5243 | ||
|
|
e92ed13864 | ||
|
|
a8917e4713 | ||
|
|
04036e001e | ||
|
|
725b0d42f8 | ||
|
|
071c1bdea6 | ||
|
|
7386c145ef | ||
|
|
25ca8d85a6 | ||
|
|
c60ed2c403 | ||
|
|
7d41d4d030 | ||
|
|
6d77fe6eb3 | ||
|
|
40d3c8b071 | ||
|
|
02ec4a3bfd | ||
|
|
7a4ebbf47e | ||
|
|
2e7ce477d7 | ||
|
|
c1a22ef639 | ||
|
|
0a7b78b7ee | ||
|
|
e1cb62848c | ||
|
|
acf5c733c2 | ||
|
|
7c25e29515 | ||
|
|
7c72871c02 | ||
|
|
ddb77c6ab3 | ||
|
|
8502fd246d | ||
|
|
30c6917914 | ||
|
|
33464b3388 | ||
|
|
2205637ad6 | ||
|
|
149198ef1c | ||
|
|
2c40353180 | ||
|
|
a62a6c2aa4 | ||
|
|
4be06f6779 | ||
|
|
03f4740968 | ||
|
|
ea6d80edfb | ||
|
|
68e8a221ff | ||
|
|
cce84c3782 | ||
|
|
c1878ca28b | ||
|
|
e9b7a43b9a | ||
|
|
3921b4f445 | ||
|
|
dfd4cca43f | ||
|
|
69c6c3e790 | ||
|
|
4541d75f49 | ||
|
|
9eda670026 | ||
|
|
d3c13b118d | ||
|
|
8e2345cf9e | ||
|
|
d240d65c5f | ||
|
|
d3473a7462 | ||
|
|
6bfe4ee238 | ||
|
|
e61b7d598e | ||
|
|
eb81b9217e | ||
|
|
4d9bfcad20 | ||
|
|
2387452a56 | ||
|
|
a4772941a7 | ||
|
|
6318f78e29 | ||
|
|
5e6f3cc5b4 | ||
|
|
04f1fa06ad | ||
|
|
8d660e53b2 | ||
|
|
f3864b00fe | ||
|
|
51cc94bf2a | ||
|
|
f63d1354ba | ||
|
|
80362cfa84 | ||
|
|
447a6d7524 | ||
|
|
f022440b4a | ||
|
|
4f5e0c007d | ||
|
|
b83f6591bb | ||
|
|
77a9481cf9 | ||
|
|
a8f029309d | ||
|
|
b0df8b7dca | ||
|
|
2cc05717e6 | ||
|
|
b96394131f | ||
|
|
875c1019b2 | ||
|
|
d1a0a39efa | ||
|
|
fdfbf7ddf8 | ||
|
|
414f8b5ba9 | ||
|
|
03a1c566fc | ||
|
|
0fc1c37e65 | ||
|
|
6b4efb420e | ||
|
|
9ed2b26933 | ||
|
|
70665534b8 | ||
|
|
7244d4d343 | ||
|
|
980c1e8681 | ||
|
|
e1378860d6 | ||
|
|
462f619f43 | ||
|
|
b9571db9f1 | ||
|
|
734962ac22 | ||
|
|
35103bf155 | ||
|
|
4c4d7b5d26 | ||
|
|
daad633195 | ||
|
|
d5cfe9f262 | ||
|
|
a2c46b3f66 | ||
|
|
6a1d11b9b5 | ||
|
|
0630c094e0 | ||
|
|
ace71f20b3 | ||
|
|
c0e02451a1 | ||
|
|
2d12a16771 | ||
|
|
9108b3923b | ||
|
|
726e117656 | ||
|
|
ab524fff15 | ||
|
|
55eae9910f | ||
|
|
d3138a7fdf | ||
|
|
05e65a74ce | ||
|
|
6d138f345f | ||
|
|
3c165dd7e8 | ||
|
|
04561d29b5 | ||
|
|
e5e6c1a2dd | ||
|
|
882efea314 | ||
|
|
89d979d92e | ||
|
|
383aab5f85 | ||
|
|
6659174f88 | ||
|
|
3ca29b8744 | ||
|
|
726bf3345e | ||
|
|
b2d61482d5 | ||
|
|
88d8e9a7f9 | ||
|
|
0b0ddd3140 | ||
|
|
3d4f74945c | ||
|
|
4387038351 | ||
|
|
49f444dfe9 | ||
|
|
061533bb57 | ||
|
|
6e2fb67d76 | ||
|
|
518b336431 | ||
|
|
b125f82784 | ||
|
|
30349dbcb9 | ||
|
|
02f6537973 | ||
|
|
502f126986 | ||
|
|
7c7d1823ca | ||
|
|
ce6d38860d | ||
|
|
60f5093c6c | ||
|
|
665f91ec37 | ||
|
|
0805a011b9 | ||
|
|
3e3322e1f0 | ||
|
|
252b1b65c4 | ||
|
|
e8b4536ec2 | ||
|
|
61d40b5a0b | ||
|
|
d175aa958c | ||
|
|
ac3c220789 | ||
|
|
add65e121a | ||
|
|
de2e757691 | ||
|
|
eb34ab4f6a | ||
|
|
08e4232166 | ||
|
|
adf9772928 | ||
|
|
562171ec86 | ||
|
|
e9e162319f | ||
|
|
17c9ffe107 | ||
|
|
1831c84a29 | ||
|
|
7e27449204 | ||
|
|
282476d530 | ||
|
|
fce0728d5d | ||
|
|
0e9ac90607 | ||
|
|
5383f56b26 | ||
|
|
a02484c6cd | ||
|
|
be365f66ef | ||
|
|
011dc09d5e | ||
|
|
fc4d6d2d2e | ||
|
|
9d73127cae | ||
|
|
223743e3b5 | ||
|
|
44ed372f21 | ||
|
|
4d1f970105 | ||
|
|
b28a891a40 | ||
|
|
834ff95702 | ||
|
|
3e93580aa4 | ||
|
|
ef61735f64 | ||
|
|
a2780ca056 | ||
|
|
d025c8bd9a | ||
|
|
7a3ca027bb | ||
|
|
3c021919dd | ||
|
|
2106e8ddb0 | ||
|
|
acf38e47ba | ||
|
|
4122d4207d | ||
|
|
d550ae7d93 | ||
|
|
815a542cc1 | ||
|
|
3472484601 | ||
|
|
826a8c8a74 | ||
|
|
c66703f0ca | ||
|
|
874e7d8d54 | ||
|
|
4e2b85b8c8 | ||
|
|
5314a456cb | ||
|
|
ba2a5b195c | ||
|
|
3546ee30a2 | ||
|
|
f8115671ee | ||
|
|
494279edd8 | ||
|
|
bd6673afed | ||
|
|
210daf7324 | ||
|
|
794e0effba | ||
|
|
d60d595254 | ||
|
|
06708949a1 | ||
|
|
da901e20d9 | ||
|
|
c28999ec6f | ||
|
|
cd15a8c406 | ||
|
|
be2080259e | ||
|
|
9872578d51 | ||
|
|
3014696fcd | ||
|
|
6b70f77dcd | ||
|
|
7f6d8bffe3 | ||
|
|
458307f5ed | ||
|
|
294aa826f1 | ||
|
|
6810f38808 | ||
|
|
087cc6b8fd | ||
|
|
10c8101476 | ||
|
|
22028b074a | ||
|
|
3d086beed2 | ||
|
|
777d814e70 | ||
|
|
15c26a95c5 | ||
|
|
f87d3bd1cb | ||
|
|
b9ea1e8c71 | ||
|
|
123a0ccd70 | ||
|
|
4c737475d4 | ||
|
|
4e0d97f2c1 | ||
|
|
85906cafbb | ||
|
|
a8dc691033 | ||
|
|
4d7988b78e | ||
|
|
841e760b04 | ||
|
|
db232f4ff2 | ||
|
|
70f705afbc | ||
|
|
912d63067c | ||
|
|
880df212d5 | ||
|
|
b06acd99ec | ||
|
|
a59de80d18 | ||
|
|
1e8dee9935 | ||
|
|
928f1c3e4b | ||
|
|
014e70a7b1 | ||
|
|
5148b16246 | ||
|
|
b4ae4c5a00 | ||
|
|
70346bce35 | ||
|
|
3539e065fa | ||
|
|
8ffc58b340 | ||
|
|
503e733c81 | ||
|
|
7eabaf0de6 | ||
|
|
a8ad10757c | ||
|
|
111546ad1a | ||
|
|
a8fbd5164e | ||
|
|
8dde732514 | ||
|
|
f1659af5e4 | ||
|
|
e68fb51f44 | ||
|
|
95453131c8 | ||
|
|
c60d5c8e85 | ||
|
|
b31f32a7e7 | ||
|
|
f0b3bd0ede | ||
|
|
de5243ec67 | ||
|
|
c6de3eb2ae | ||
|
|
d7b6f29c81 | ||
|
|
5bebd71a44 | ||
|
|
eb769c7fb4 | ||
|
|
5bc5c0df90 | ||
|
|
cfc3684a16 | ||
|
|
0590020130 | ||
|
|
2a91af152d | ||
|
|
d8120d4e13 | ||
|
|
0ac211ce77 | ||
|
|
32c0d6f253 | ||
|
|
de762847e9 | ||
|
|
6248e32148 | ||
|
|
52f88ee32d | ||
|
|
f8d1cf2f60 | ||
|
|
e129425d8d | ||
|
|
ae20d82e1d | ||
|
|
463b2ce040 | ||
|
|
412b47abba | ||
|
|
ed216eee73 | ||
|
|
9dd5234962 | ||
|
|
018532016b | ||
|
|
7dfecc38f6 | ||
|
|
aa5afadcce | ||
|
|
d23a584b9e | ||
|
|
572f035877 | ||
|
|
b94eb6adb0 | ||
|
|
45fccb94e1 | ||
|
|
2628f17fae | ||
|
|
69fb6e77fc | ||
|
|
6992858e67 | ||
|
|
59917cca45 | ||
|
|
2f0b959aa4 | ||
|
|
4f2749e0dc | ||
|
|
c9deaae744 | ||
|
|
950a12360e | ||
|
|
686ca284fe | ||
|
|
b21643f485 | ||
|
|
4c28627459 | ||
|
|
6a6fe0f56d | ||
|
|
48831225ac | ||
|
|
4e8a79d8f1 | ||
|
|
570a4c7fca | ||
|
|
094d352e5f | ||
|
|
44b90be7d6 | ||
|
|
e7980a8886 | ||
|
|
1c9c0a48be | ||
|
|
c541eebe3e | ||
|
|
c51e6efe74 | ||
|
|
6c9ce15b26 | ||
|
|
8e1bd36b4c | ||
|
|
d3e3e6e6fc | ||
|
|
f1794a2dfe | ||
|
|
1efccff730 | ||
|
|
0031345383 | ||
|
|
46fa7475cf | ||
|
|
afc7c40975 | ||
|
|
dc99797f7b | ||
|
|
4624266a5c | ||
|
|
05bfd99318 | ||
|
|
db3faf16b0 | ||
|
|
9fc451ece8 | ||
|
|
9e1079027b | ||
|
|
9774a4bd26 | ||
|
|
ce9e864757 | ||
|
|
737ea19c9e | ||
|
|
82b5e97a2b | ||
|
|
c43f829854 | ||
|
|
f43a868607 | ||
|
|
9f2d1453ed | ||
|
|
082d939f7d | ||
|
|
8d628cf0ed | ||
|
|
19553dcfed | ||
|
|
a7938526aa | ||
|
|
863551ee1d | ||
|
|
772c6bbba5 | ||
|
|
148dcbb0c5 | ||
|
|
82abdb50b1 | ||
|
|
b8dbb11136 | ||
|
|
b14cb99587 | ||
|
|
1d32a5939c | ||
|
|
ae1cd8ba3e | ||
|
|
1fb6bffe1c | ||
|
|
59864e3781 | ||
|
|
e2c7d8c678 | ||
|
|
62f82c5d12 | ||
|
|
ec70d8a3a2 | ||
|
|
1b4471a946 | ||
|
|
43472394c7 | ||
|
|
a4e9025b8e | ||
|
|
6b9e93e310 | ||
|
|
06b385563c | ||
|
|
d90a23bfd5 | ||
|
|
91cdd50ba7 | ||
|
|
4bc4db8232 | ||
|
|
863675e636 | ||
|
|
1cdcaebb4d | ||
|
|
cdb30d86b0 | ||
|
|
138e3c4239 | ||
|
|
66b82598e3 | ||
|
|
780eecf35b | ||
|
|
8548d39318 | ||
|
|
6f26ff0a89 | ||
|
|
2ffd2c6b7a | ||
|
|
0e95ad8ed6 | ||
|
|
3a8b362dfd | ||
|
|
82b94fad5d | ||
|
|
42cfed5fa8 | ||
|
|
e4452b906e | ||
|
|
36abaeb6fb | ||
|
|
507c951f28 | ||
|
|
8a67103808 | ||
|
|
b3a10d4f9e | ||
|
|
d4c4e2599d | ||
|
|
67ead1c250 | ||
|
|
e4d98eba6b | ||
|
|
9c6ceaa58a | ||
|
|
7f168d49a6 | ||
|
|
897dc0fb97 | ||
|
|
a08419a4ce | ||
|
|
710223d7de | ||
|
|
4c40c6fa74 | ||
|
|
fe6d86622b | ||
|
|
d1ad2d0845 | ||
|
|
29cb273d08 | ||
|
|
566517d20f | ||
|
|
ac1c93d29b | ||
|
|
944842d4eb | ||
|
|
d56ff0825a | ||
|
|
d7d50337f9 | ||
|
|
0de89f3145 | ||
|
|
aa362ba18b | ||
|
|
b272d3b32f | ||
|
|
e4e32a9c56 | ||
|
|
4bae718f40 | ||
|
|
5eb3979504 | ||
|
|
e03ff22ba8 | ||
|
|
368f508b17 | ||
|
|
a1cf20bd67 | ||
|
|
d2ff2ec9c6 | ||
|
|
1c24c05647 | ||
|
|
c70f14419a | ||
|
|
80386f1c86 | ||
|
|
4f70a79638 | ||
|
|
080886a2d1 | ||
|
|
5f9112b565 | ||
|
|
2007cfd3a0 | ||
|
|
e49974beaa | ||
|
|
e863c8c549 | ||
|
|
e4b4e8afca | ||
|
|
4d4ebf7279 | ||
|
|
892632a814 | ||
|
|
2ba6a3fd8d | ||
|
|
09bea6e231 | ||
|
|
99f4091c1a | ||
|
|
6964bbc232 | ||
|
|
367d30e421 | ||
|
|
30a66f1441 | ||
|
|
666c447e36 | ||
|
|
a43dff558a | ||
|
|
f39006ea1d | ||
|
|
2355ef9d31 | ||
|
|
13b0ab73d0 | ||
|
|
3766982734 | ||
|
|
54a864e18c | ||
|
|
9a1f4e40dd | ||
|
|
5a83ef4f48 | ||
|
|
b3fa1eaae2 | ||
|
|
6d95cb01c8 | ||
|
|
a7eefa88fa | ||
|
|
a7d4e37851 | ||
|
|
57e71f0e1a | ||
|
|
30be3cab53 | ||
|
|
0e6626bf39 | ||
|
|
bb0eaf4597 | ||
|
|
8ada3bae0c | ||
|
|
db72cad610 | ||
|
|
455b42aa85 | ||
|
|
2d95092479 | ||
|
|
0f87c70cee | ||
|
|
4312d39624 | ||
|
|
cf500b7296 | ||
|
|
7a0180213d | ||
|
|
160e4a0479 | ||
|
|
bed66298d1 | ||
|
|
44643c151a | ||
|
|
bc1bd2427d | ||
|
|
6726994ad5 | ||
|
|
5abc5279f5 | ||
|
|
1135ca5fb4 | ||
|
|
be08a62f52 | ||
|
|
f473807459 | ||
|
|
3a197379d5 | ||
|
|
a9f8dac656 | ||
|
|
3dc57c4a4a | ||
|
|
4c9a61f5a7 | ||
|
|
ba4740a245 | ||
|
|
86c37bab3f | ||
|
|
dd5038c15b | ||
|
|
550ab403f6 | ||
|
|
bac2e8c014 | ||
|
|
d148cae814 | ||
|
|
f22211ce8c | ||
|
|
f9c24f2528 | ||
|
|
6f6786d79e | ||
|
|
a5ea7f7d58 | ||
|
|
d637ac7633 | ||
|
|
c1ad2386ef | ||
|
|
b0b6de116d | ||
|
|
1b161f997b | ||
|
|
b99af58636 | ||
|
|
c46928aefe | ||
|
|
d4edbb3c3e | ||
|
|
1b765605ca | ||
|
|
8286dc26ad | ||
|
|
19a438c15b | ||
|
|
12898a1a8e | ||
|
|
4c27a46d35 | ||
|
|
541f2a2988 | ||
|
|
d0277dde3f | ||
|
|
d184e40116 | ||
|
|
596db07647 | ||
|
|
c233c5c67e | ||
|
|
db788b57e7 | ||
|
|
c0983654d8 | ||
|
|
30bf95f2c1 | ||
|
|
da178107f9 | ||
|
|
4dda1d9e49 | ||
|
|
e11aec64cd | ||
|
|
71575e49d7 | ||
|
|
393d070b05 | ||
|
|
d7e4c28cd0 | ||
|
|
ccf2348cd6 | ||
|
|
cde3eb2c39 | ||
|
|
f29d8eeda8 | ||
|
|
daee7ac761 | ||
|
|
313d1a580e | ||
|
|
824d8e929a | ||
|
|
862d2adf1d | ||
|
|
906ece0c02 | ||
|
|
0bcf57e89c | ||
|
|
c529a406cd | ||
|
|
05d81eb734 | ||
|
|
ca9bab5d4c | ||
|
|
e35ef8f39b | ||
|
|
370cae3c8d | ||
|
|
5731f5c75c | ||
|
|
34d03973f9 | ||
|
|
84204c1d12 | ||
|
|
6ee76e3bf0 | ||
|
|
f497dc3170 | ||
|
|
749ffdd5d1 | ||
|
|
e709582062 | ||
|
|
713f10e63a | ||
|
|
9d60ef5f72 | ||
|
|
5a52e5e9b3 | ||
|
|
d99cf9d499 | ||
|
|
be0e12589d | ||
|
|
a6732ba815 | ||
|
|
466d872d30 | ||
|
|
ad982746dd | ||
|
|
8c2cc5b6d2 | ||
|
|
3cf7238106 | ||
|
|
f8d08cc5db | ||
|
|
97f86af6b9 | ||
|
|
c2087f6d5f | ||
|
|
98af95a6ce | ||
|
|
39101610d5 | ||
|
|
b77239fc15 | ||
|
|
31a3af1f84 | ||
|
|
0e5944e9c6 | ||
|
|
99e9df0211 | ||
|
|
c1bdca8df3 | ||
|
|
15b5cf7c20 | ||
|
|
f55102716e | ||
|
|
17b95f1609 | ||
|
|
2575a762e1 | ||
|
|
f5cf7bb1f2 | ||
|
|
d3e8616e90 | ||
|
|
87dc325932 | ||
|
|
eb91dd7a7d | ||
|
|
89c4f54ce8 | ||
|
|
b3cb800f35 | ||
|
|
4a0f62b636 | ||
|
|
bcaefe39aa | ||
|
|
54bb17c109 | ||
|
|
dc74a389d8 | ||
|
|
784b3b58ca | ||
|
|
34b7c47533 | ||
|
|
d6a80b4865 | ||
|
|
1d20fe13f3 | ||
|
|
7586c3ef8a | ||
|
|
b232069631 | ||
|
|
11d2a96ecb | ||
|
|
586e5a3db8 | ||
|
|
d94a431537 | ||
|
|
9b2f44efb1 | ||
|
|
12e4fe4120 | ||
|
|
476d9c8034 | ||
|
|
85809efcc0 | ||
|
|
722edcc274 | ||
|
|
919c3fd999 | ||
|
|
6c711ec211 | ||
|
|
7528065d4d | ||
|
|
79e2ca6284 | ||
|
|
d22d22d24c | ||
|
|
1a9c9a5ddf | ||
|
|
0bced35931 | ||
|
|
4dcd60adc0 | ||
|
|
d51e8ec378 | ||
|
|
59cdd259ac | ||
|
|
2be2c83f67 | ||
|
|
a271eaabd0 | ||
|
|
e95f92cdeb | ||
|
|
cb86cb1882 | ||
|
|
1d130c88a8 | ||
|
|
c50f5769db | ||
|
|
6df9b649a1 | ||
|
|
6f5018d45e | ||
|
|
f51ee76c72 | ||
|
|
a750b0f08a | ||
|
|
0e850b2a85 | ||
|
|
d326cad611 | ||
|
|
2ddb9b8f7c | ||
|
|
6237db9ba6 | ||
|
|
0cff7bdd04 | ||
|
|
413e80ed3e | ||
|
|
5d52f4a760 | ||
|
|
0855dc617f | ||
|
|
04587a8a47 | ||
|
|
392b96dc34 | ||
|
|
219469f24f | ||
|
|
84cc3d16ed | ||
|
|
e34d521dcc | ||
|
|
2c02900edf | ||
|
|
f5442c307a | ||
|
|
802879ced2 | ||
|
|
0c9e8fe050 | ||
|
|
8f41571e47 | ||
|
|
6d14368e2f | ||
|
|
f90ad48906 | ||
|
|
e95dac82c2 | ||
|
|
144601ffd4 | ||
|
|
cbcc2aa731 | ||
|
|
f9354c8cd1 | ||
|
|
aff0416405 | ||
|
|
6d9a14feed | ||
|
|
3c2e5c143a | ||
|
|
cbf1d2feb0 | ||
|
|
65bb7f32e9 | ||
|
|
3b8c365f7e | ||
|
|
43747accd5 | ||
|
|
50f9bbeac3 | ||
|
|
3cffd9334f | ||
|
|
f72749304d | ||
|
|
689d08416b | ||
|
|
28182713bf | ||
|
|
8190c7aac7 | ||
|
|
6268f091b8 | ||
|
|
cbfde7ddb1 | ||
|
|
35d6c7d79e | ||
|
|
f1a2cf7efc | ||
|
|
ec4eb22a72 | ||
|
|
97c218cb4c | ||
|
|
07021b8dd5 | ||
|
|
aa9bf08157 | ||
|
|
90dd7037c8 | ||
|
|
6084759907 | ||
|
|
0a3d87dfe3 | ||
|
|
65a3897f87 | ||
|
|
df20196201 | ||
|
|
e9d60f73f4 | ||
|
|
8e9717906f | ||
|
|
82183bf204 | ||
|
|
b2406b0465 | ||
|
|
1bdef97dc7 | ||
|
|
ff9c68db56 | ||
|
|
1cedc94652 | ||
|
|
02bda3250b | ||
|
|
f6f60e8203 | ||
|
|
55beb433f3 | ||
|
|
2fff18cb61 | ||
|
|
6d8bf3a08a | ||
|
|
13ee79a06b | ||
|
|
b4208ede3b | ||
|
|
a96cb3f99f | ||
|
|
1f39a922d1 | ||
|
|
145fba5949 | ||
|
|
7a4f418e75 | ||
|
|
8fa8e34027 | ||
|
|
5225874498 | ||
|
|
8cb86c364d | ||
|
|
6d98014ad2 | ||
|
|
1cf83c2485 | ||
|
|
f638bd4ded | ||
|
|
7b169fb798 | ||
|
|
406cbb0f41 | ||
|
|
802493fec4 | ||
|
|
aeb08caf77 | ||
|
|
6d8daac5ca | ||
|
|
82186c8592 | ||
|
|
f099a352b3 | ||
|
|
01f0625d6a | ||
|
|
38ff7ec89f | ||
|
|
7163a0802d | ||
|
|
8d0fcf37c5 | ||
|
|
3da5a8bb34 | ||
|
|
fad33b583b | ||
|
|
8f450501cc | ||
|
|
b3ae0e577a | ||
|
|
aedac94e40 | ||
|
|
08c5dbce09 | ||
|
|
482d658de7 | ||
|
|
39e34081fc | ||
|
|
5797786a75 | ||
|
|
f6977ea264 | ||
|
|
de8c99eceb | ||
|
|
cdc15c0f20 | ||
|
|
18408901be | ||
|
|
982c915931 | ||
|
|
a339eacd45 | ||
|
|
8fc0e012e3 | ||
|
|
b7fb9635e5 | ||
|
|
f988a958bb | ||
|
|
c80cb57b07 | ||
|
|
e764ddf0b8 | ||
|
|
a210f951da | ||
|
|
367532cf8d | ||
|
|
d3e6200385 | ||
|
|
af504891e4 | ||
|
|
dec28e16d4 | ||
|
|
bd3777f9de | ||
|
|
e4ee0fc397 | ||
|
|
9690ccd8a0 | ||
|
|
0804647441 | ||
|
|
76da287cca | ||
|
|
8739dd97d6 | ||
|
|
a7874cc351 | ||
|
|
de21f14fd8 | ||
|
|
b90cca77a9 | ||
|
|
1eadc08098 | ||
|
|
9df26b8c84 | ||
|
|
525cb0689f | ||
|
|
7796c473ae | ||
|
|
e0c9aba25e | ||
|
|
2cb33bfec4 | ||
|
|
2aad7647ef | ||
|
|
6430129b2a | ||
|
|
8445165491 | ||
|
|
c5698fcd65 | ||
|
|
786e50c3c3 | ||
|
|
7d03be3a77 | ||
|
|
4496d7762b | ||
|
|
8b12caad78 | ||
|
|
e1492c3bb1 | ||
|
|
3cc66e9db9 | ||
|
|
37389a62c1 | ||
|
|
aef65620da | ||
|
|
cd094d1475 | ||
|
|
99bef09e0e | ||
|
|
c97e77c765 | ||
|
|
8ece8ebec2 | ||
|
|
e44a22e143 | ||
|
|
c2496fe63e | ||
|
|
7bf8467637 | ||
|
|
27677adbe8 | ||
|
|
6fa06a4f8b | ||
|
|
a3be4d2945 | ||
|
|
3dc5b5c31a | ||
|
|
342e11f83e | ||
|
|
b6d78b4001 | ||
|
|
2aaa6db538 | ||
|
|
fc909d8199 | ||
|
|
262136393b | ||
|
|
1640859542 | ||
|
|
f0b48acaf9 | ||
|
|
bb8c305fc8 | ||
|
|
3950dba2c5 | ||
|
|
903ea45228 | ||
|
|
b116b8ba1e | ||
|
|
908043fb7f | ||
|
|
2d7e3f180e | ||
|
|
c813d17595 | ||
|
|
7d16265c4e | ||
|
|
4fb4764d3f | ||
|
|
2385f1cbf8 | ||
|
|
4d39c3bbd2 | ||
|
|
23ddb87c9f | ||
|
|
db662a7662 | ||
|
|
eb5809fd17 | ||
|
|
f6ab82faaf | ||
|
|
0d8584f499 | ||
|
|
c289e1beef | ||
|
|
db491cbb09 | ||
|
|
3d80d66925 | ||
|
|
ce208b69fe | ||
|
|
a44418c6a1 | ||
|
|
5ac84491c1 | ||
|
|
58769eb06e | ||
|
|
59ae9c6437 | ||
|
|
81d698c091 | ||
|
|
bce0895392 | ||
|
|
39f3383709 | ||
|
|
4f8fc8d65e | ||
|
|
a2cc7a84db | ||
|
|
0397ba857f | ||
|
|
90dd2b5ac2 | ||
|
|
3597aacce9 | ||
|
|
61bc5436a2 | ||
|
|
37428fbe3b | ||
|
|
444fb4555b | ||
|
|
294f890a92 | ||
|
|
578502187d | ||
|
|
786082a9d0 | ||
|
|
b6eb851a13 | ||
|
|
93c41f044c | ||
|
|
a7f61397f7 | ||
|
|
0f5f20247a | ||
|
|
e909eea82a | ||
|
|
ef4b977bef | ||
|
|
fbe1e2c2cc | ||
|
|
9b86ed7c45 | ||
|
|
c0c83c5cdc | ||
|
|
638093d40e | ||
|
|
8707287349 | ||
|
|
37218c1e81 | ||
|
|
b98cab7d62 | ||
|
|
656cf32753 | ||
|
|
9e2aac0696 | ||
|
|
8c48d73b03 | ||
|
|
b48a55bd74 | ||
|
|
a1b116d2f5 | ||
|
|
aad1bc898e | ||
|
|
4add484d2a | ||
|
|
8eed2232ee | ||
|
|
6a01e40394 | ||
|
|
757176727d | ||
|
|
d9fdae9175 | ||
|
|
4a84cf3ac7 | ||
|
|
8f5f3e56cb | ||
|
|
e03e51c450 | ||
|
|
f61e33aa71 | ||
|
|
4bc3fc274b | ||
|
|
e3cdf6361e | ||
|
|
674f879fb8 | ||
|
|
b195fc69dd | ||
|
|
2049e4e1d5 | ||
|
|
d09ab4a153 | ||
|
|
328c0ade22 | ||
|
|
b312e17904 | ||
|
|
2cd3e9cacd | ||
|
|
ce72acdd61 | ||
|
|
b3d92b04cb | ||
|
|
8e74acbf3e | ||
|
|
a5703cb4f1 | ||
|
|
76cc7f1169 | ||
|
|
b8cb7e9ba8 | ||
|
|
2019ae43d7 | ||
|
|
bb5b92571e | ||
|
|
3ad5f889a0 | ||
|
|
3ff10bfeae | ||
|
|
c8a2a8b003 | ||
|
|
6acb3a7ca5 | ||
|
|
ef38897a01 | ||
|
|
33db0e666a | ||
|
|
cf32372a57 | ||
|
|
b98cf9538d | ||
|
|
768fef502d | ||
|
|
d080c5dd14 | ||
|
|
4fafa39e54 | ||
|
|
bb7487c476 | ||
|
|
308195279c | ||
|
|
1a707eab86 | ||
|
|
e2efc3e8e8 | ||
|
|
fa6d5e60ce | ||
|
|
e95b7075b9 | ||
|
|
dfbc1beccd | ||
|
|
0a7a0ac681 | ||
|
|
981d4be44d | ||
|
|
80bdf7410b | ||
|
|
6f45d33c8a | ||
|
|
cd22985f11 | ||
|
|
66c0425080 | ||
|
|
fd7fd9ca35 | ||
|
|
7b3efa746e | ||
|
|
90ddc2bfa7 | ||
|
|
8e3425ed6d | ||
|
|
c1bb42fe65 | ||
|
|
b99f73919f | ||
|
|
978bf75765 | ||
|
|
14d591caa1 | ||
|
|
432d5b43a1 | ||
|
|
3412372d1e | ||
|
|
ca6d472e5d | ||
|
|
c2e130f369 | ||
|
|
6501454424 | ||
|
|
0019a4e1db | ||
|
|
f8874fc586 | ||
|
|
4ae875601a | ||
|
|
e8880f068f | ||
|
|
45c1c23e09 | ||
|
|
e10d4bf45c | ||
|
|
369b640408 | ||
|
|
d2d30bf668 | ||
|
|
dcb59edb79 | ||
|
|
84d31c1d59 | ||
|
|
58d41f4458 | ||
|
|
360effb839 | ||
|
|
3e428c044a | ||
|
|
d923c99975 | ||
|
|
2b538061e9 | ||
|
|
11451ac6b9 | ||
|
|
21a8d03201 | ||
|
|
0c9db5afe9 | ||
|
|
1031429550 | ||
|
|
4a7ea469d4 | ||
|
|
833c1cd98f | ||
|
|
662922be6f | ||
|
|
370ae323f6 | ||
|
|
514eef50de | ||
|
|
ec1fd7b923 | ||
|
|
737ce99398 | ||
|
|
4f1a897e18 | ||
|
|
588a8b2a3a | ||
|
|
f281a32a4e | ||
|
|
d09e6089ca | ||
|
|
7b6062a4a2 | ||
|
|
7975fadfe9 | ||
|
|
ad571e6019 | ||
|
|
55cac17456 | ||
|
|
846e84d3a4 | ||
|
|
14d924f6c7 | ||
|
|
282f02f4d5 | ||
|
|
d36cede0c7 | ||
|
|
a262d70b88 | ||
|
|
9fa7edffbf | ||
|
|
24a47445f6 | ||
|
|
77b098c5fe | ||
|
|
e4b688a2c3 | ||
|
|
6d219ade2d | ||
|
|
b48c16b4db | ||
|
|
0a1ca18de5 | ||
|
|
9a0b784153 | ||
|
|
a446ec31c7 | ||
|
|
9323737d1d | ||
|
|
d288acdd4e | ||
|
|
765aded208 | ||
|
|
1f9fd92b01 | ||
|
|
2be642e4c9 | ||
|
|
934356dea8 | ||
|
|
a037bf6f9d | ||
|
|
c005c86c27 | ||
|
|
68278267e1 | ||
|
|
da2d5674a5 | ||
|
|
5bb8eb22ec | ||
|
|
0d86a7e427 | ||
|
|
9f4aa0a123 | ||
|
|
da838e266e | ||
|
|
fd160e87e8 | ||
|
|
0dd4304902 | ||
|
|
a0b94bba4e | ||
|
|
74c2a2b5cb | ||
|
|
b691c30677 | ||
|
|
01acefe4cf | ||
|
|
1a2c5a95c7 | ||
|
|
eb8725a0db | ||
|
|
57e1061063 | ||
|
|
91f4681a3c | ||
|
|
822a4fab86 | ||
|
|
44a164ed28 | ||
|
|
ff2521aa9f | ||
|
|
9094e8bde9 | ||
|
|
573a292e16 | ||
|
|
04b7f2fa8c | ||
|
|
1f09c03d48 | ||
|
|
aafde8986d | ||
|
|
03e3ab5599 | ||
|
|
8148c1a8a8 | ||
|
|
ce61bcc078 | ||
|
|
49e2b05a11 | ||
|
|
c76322c40d | ||
|
|
39c68ff26e | ||
|
|
481f2a4717 | ||
|
|
c01c076561 | ||
|
|
2391e08ac1 | ||
|
|
a7a2fe243a | ||
|
|
b5c655dd70 | ||
|
|
d84a61d108 | ||
|
|
13abbd2c5d | ||
|
|
d22eac9f35 | ||
|
|
d71aa8d7e2 | ||
|
|
4db6199fd9 | ||
|
|
dd9cbbac77 | ||
|
|
069d4956aa | ||
|
|
61fee3122a | ||
|
|
5a8255a9f4 | ||
|
|
5a2a8b4c8e | ||
|
|
1079080f5c | ||
|
|
1e914c4cf2 | ||
|
|
53226c1050 | ||
|
|
7a4e0301d2 | ||
|
|
42826e542d | ||
|
|
42d1c9403c | ||
|
|
ab43635583 | ||
|
|
b8ecd83bfd | ||
|
|
0db0ced1ab | ||
|
|
3d20833d42 | ||
|
|
7d09ba5669 | ||
|
|
310ff30278 | ||
|
|
821bc9405c | ||
|
|
d54d66cffc | ||
|
|
d2b4c40357 | ||
|
|
f48af97ddc | ||
|
|
f1264d6310 | ||
|
|
4dc5a1499a | ||
|
|
dd51b675b0 | ||
|
|
7679705369 | ||
|
|
58a8af20a6 | ||
|
|
75a58f465c | ||
|
|
220053c044 | ||
|
|
613cbaa556 | ||
|
|
0d0a634255 | ||
|
|
105090db60 | ||
|
|
3e9c75f735 | ||
|
|
d42075072b | ||
|
|
6a099f0813 | ||
|
|
9af5e40071 | ||
|
|
4bf2a29070 | ||
|
|
069d0e48c1 | ||
|
|
c8cb2692dd | ||
|
|
74a22bcf9c | ||
|
|
8b44927cb6 | ||
|
|
781ff956e3 | ||
|
|
7522e5de33 | ||
|
|
3209c94622 | ||
|
|
dd4a7f9fac | ||
|
|
6f8a5ea1be | ||
|
|
69837837b0 | ||
|
|
03d8bf2836 | ||
|
|
eca2c142f3 | ||
|
|
715f7bbbc2 | ||
|
|
291ebd5c12 | ||
|
|
ba04cac7ac | ||
|
|
acb8c0b5af | ||
|
|
0c14bacea7 | ||
|
|
4cabb849f3 | ||
|
|
445a85798b | ||
|
|
55775f48e9 | ||
|
|
b491b9d77d | ||
|
|
237f792fb4 | ||
|
|
4bd923dc06 | ||
|
|
439654ed7f | ||
|
|
87e956fe7d | ||
|
|
02b9dbdee9 | ||
|
|
69797e58cb | ||
|
|
d1961ca3fa | ||
|
|
2e9b1e079c | ||
|
|
3dfaaf4c25 | ||
|
|
fcc39a0397 | ||
|
|
0602f457ce | ||
|
|
d00b0f4c0e | ||
|
|
5e68959c03 | ||
|
|
ad4451a757 | ||
|
|
4d8127a7d9 | ||
|
|
cd2c8078c8 | ||
|
|
ffc4b0a0a8 | ||
|
|
ee360521bb | ||
|
|
04b0b8c723 | ||
|
|
b3ac8d0835 | ||
|
|
1a88a0164c | ||
|
|
8b77ad7748 | ||
|
|
8d1f668fc5 | ||
|
|
68fbbb64db | ||
|
|
eee92aa1aa | ||
|
|
74eb4cca86 | ||
|
|
49f5c94a75 | ||
|
|
8bba4b2973 | ||
|
|
d94e8c8187 | ||
|
|
2307325af8 | ||
|
|
4e57b4be88 | ||
|
|
21335e7d05 | ||
|
|
abdd70c6fa | ||
|
|
78768e00d4 | ||
|
|
3eb44708e5 | ||
|
|
c077b22410 | ||
|
|
9787ab04cd | ||
|
|
79d3c90026 | ||
|
|
c2068b343b | ||
|
|
9e63e40dab | ||
|
|
6424839731 | ||
|
|
8c757d1e03 | ||
|
|
dd9ba4d250 | ||
|
|
ebb632e888 | ||
|
|
4db2335107 | ||
|
|
26a2fb6662 | ||
|
|
4c1521a98e | ||
|
|
df8abb099c | ||
|
|
e74c86842b | ||
|
|
ad6e30f3d4 | ||
|
|
13439e100e | ||
|
|
5ac469b174 | ||
|
|
8ac5b48493 | ||
|
|
1a827ef44f | ||
|
|
0f0e346a02 | ||
|
|
b6eb4e84e4 | ||
|
|
2a6db47aeb | ||
|
|
f7d425189a | ||
|
|
6ca78ce8cb | ||
|
|
4b7c927ca4 | ||
|
|
19b7957523 | ||
|
|
1b1a3c261d | ||
|
|
9d674ce5a7 | ||
|
|
f22e9ceec6 | ||
|
|
0d2cb02f97 | ||
|
|
a4de937eb6 | ||
|
|
7d8d80ca2c | ||
|
|
b4e130234c | ||
|
|
53cc413eae | ||
|
|
0449c46b38 | ||
|
|
f8450183c1 | ||
|
|
eb1f8b4bc6 | ||
|
|
088864fd9c | ||
|
|
cd0c86e0d6 | ||
|
|
a56f92cdee | ||
|
|
4c285bce57 | ||
|
|
ede75530d0 | ||
|
|
3ae225ac59 | ||
|
|
fd07b89028 | ||
|
|
0198f05112 | ||
|
|
729307336e | ||
|
|
f4f9b5c91c | ||
|
|
f355fe3447 | ||
|
|
321718d43a | ||
|
|
8d3836cb16 | ||
|
|
83fa50bf13 | ||
|
|
3efe9309ef | ||
|
|
456b85ed03 | ||
|
|
6bc5f87cb7 | ||
|
|
a010bba047 | ||
|
|
9829491c4c | ||
|
|
e6fb0be1d0 | ||
|
|
d02124550b | ||
|
|
6aa9d2b492 | ||
|
|
19366e3624 | ||
|
|
18f6ab451d | ||
|
|
b19f8f65a4 | ||
|
|
97255bbb33 | ||
|
|
214d6c4405 | ||
|
|
fa87fc8325 | ||
|
|
48f3a3b18f | ||
|
|
25e0585742 | ||
|
|
431f1d4be0 | ||
|
|
cc840c050b | ||
|
|
e19d3d0d06 | ||
|
|
749a587eb2 | ||
|
|
5047619e54 | ||
|
|
63a23e4dcc | ||
|
|
3466f4e452 | ||
|
|
b36e3adf7c | ||
|
|
c6a292bccb | ||
|
|
f51d924ec9 | ||
|
|
96d5930f05 | ||
|
|
7c9ebafd00 | ||
|
|
f6c4b5073c | ||
|
|
f73452e01f | ||
|
|
55f7cac526 | ||
|
|
31e758ca45 | ||
|
|
f81ca3ba60 | ||
|
|
20bb104006 | ||
|
|
1f771a9e01 | ||
|
|
223e2e0b73 | ||
|
|
fbcb626fd0 | ||
|
|
8f85e57fc5 | ||
|
|
7557f43f77 | ||
|
|
a6c0db47ab | ||
|
|
8675d1a666 | ||
|
|
1f634ed341 | ||
|
|
50c916216c | ||
|
|
f100356078 | ||
|
|
a2f4e34866 | ||
|
|
7c1ac4392e | ||
|
|
20452fe3cb | ||
|
|
91eb692d99 | ||
|
|
9abaadf1b6 | ||
|
|
2480dca473 | ||
|
|
f101eefc56 | ||
|
|
fa8736b1a4 | ||
|
|
db7b52d84c | ||
|
|
2027b643b1 | ||
|
|
a4d7065e42 | ||
|
|
2ff7b5902c | ||
|
|
e04f66f599 | ||
|
|
27ee7f8592 | ||
|
|
f95f01b895 | ||
|
|
866b36729c | ||
|
|
471009bc17 | ||
|
|
c90f30a7a1 | ||
|
|
ffb65d8ea1 | ||
|
|
a5d58071cd | ||
|
|
fc1b8917a8 | ||
|
|
ec00413a4b | ||
|
|
33f2c89716 | ||
|
|
45d2fbc5fc | ||
|
|
d00b9e0928 | ||
|
|
58a7f7b739 | ||
|
|
dcf6564d0a | ||
|
|
6db9521419 | ||
|
|
336810cbf4 | ||
|
|
9d5e533e32 | ||
|
|
b45fc630ae | ||
|
|
9a471d2a26 | ||
|
|
0a0240e520 | ||
|
|
0977e82170 | ||
|
|
0a0316a000 | ||
|
|
7e8f78acaa | ||
|
|
615cb561e4 | ||
|
|
c96a1df8e1 | ||
|
|
5efc3f15f2 | ||
|
|
5fb50a1759 | ||
|
|
e50758f0a6 | ||
|
|
3d672802a3 | ||
|
|
c238862efe | ||
|
|
7e7e5e7466 | ||
|
|
b0a8ca56bc | ||
|
|
fbec0a1c7d | ||
|
|
d5b5443a84 | ||
|
|
b8b98358d0 | ||
|
|
fca4ed7013 | ||
|
|
59877b5138 | ||
|
|
6784a1c027 | ||
|
|
39ab600887 | ||
|
|
49ea532cdc | ||
|
|
247e9bfbde | ||
|
|
5944568565 | ||
|
|
1c87707a76 | ||
|
|
b4188db671 | ||
|
|
dc642be1f5 | ||
|
|
6cdc7d3966 | ||
|
|
281d558111 | ||
|
|
fa89713f19 |
17
.cspell.json
Normal file
17
.cspell.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2",
|
||||||
|
"language": "en,en-gb",
|
||||||
|
"words": [],
|
||||||
|
"dictionaries": [
|
||||||
|
"npm",
|
||||||
|
"softwareTerms",
|
||||||
|
"node",
|
||||||
|
"html",
|
||||||
|
"css",
|
||||||
|
"bash",
|
||||||
|
"en-gb",
|
||||||
|
"misc"
|
||||||
|
],
|
||||||
|
"ignorePaths": ["package.json", "package-lock.json", "node_modules"]
|
||||||
|
}
|
||||||
|
|
||||||
41
.devcontainer/devcontainer.json
Normal file
41
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
|
||||||
|
{
|
||||||
|
"name": "CyberChef",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/javascript-node:24-trixie",
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/github-cli": "latest"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
"forwardPorts": [8080],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
"postCreateCommand": {
|
||||||
|
"npm": "bash -c \"sudo chown node node_modules && npm install\""
|
||||||
|
},
|
||||||
|
|
||||||
|
"containerEnv": {
|
||||||
|
"DISPLAY": ":99"
|
||||||
|
},
|
||||||
|
|
||||||
|
"mounts": [
|
||||||
|
"source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"GitHub.vscode-github-actions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
||||||
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
||||||
@ -12,3 +12,7 @@ indent_size = 4
|
|||||||
[{package.json,.travis.yml,nightwatch.json}]
|
[{package.json,.travis.yml,nightwatch.json}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
[.github/**.yml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
src/core/vendor/**
|
|
||||||
111
.eslintrc.json
111
.eslintrc.json
@ -1,111 +0,0 @@
|
|||||||
{
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 9,
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"impliedStrict": true
|
|
||||||
},
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"rules": {
|
|
||||||
// enable additional rules
|
|
||||||
"no-eval": "error",
|
|
||||||
"no-implied-eval": "error",
|
|
||||||
"dot-notation": "error",
|
|
||||||
"eqeqeq": ["error", "smart"],
|
|
||||||
"no-caller": "error",
|
|
||||||
"no-extra-bind": "error",
|
|
||||||
"no-unused-expressions": "error",
|
|
||||||
"no-useless-call": "error",
|
|
||||||
"no-useless-return": "error",
|
|
||||||
"radix": "warn",
|
|
||||||
|
|
||||||
// modify rules from base configurations
|
|
||||||
"no-unused-vars": ["error", {
|
|
||||||
"args": "none",
|
|
||||||
"vars": "all"
|
|
||||||
}],
|
|
||||||
"no-empty": ["error", {
|
|
||||||
"allowEmptyCatch": true
|
|
||||||
}],
|
|
||||||
|
|
||||||
// disable rules from base configurations
|
|
||||||
"no-control-regex": "off",
|
|
||||||
|
|
||||||
// stylistic conventions
|
|
||||||
"brace-style": ["error", "1tbs"],
|
|
||||||
"block-spacing": "error",
|
|
||||||
"array-bracket-spacing": "error",
|
|
||||||
"comma-spacing": "error",
|
|
||||||
"comma-style": "error",
|
|
||||||
"computed-property-spacing": "error",
|
|
||||||
"no-trailing-spaces": "warn",
|
|
||||||
"eol-last": "error",
|
|
||||||
"func-call-spacing": "error",
|
|
||||||
"key-spacing": ["warn", {
|
|
||||||
"mode": "minimum"
|
|
||||||
}],
|
|
||||||
"indent": ["error", 4, {
|
|
||||||
"ignoreComments": true,
|
|
||||||
"ArrayExpression": "first",
|
|
||||||
"SwitchCase": 1
|
|
||||||
}],
|
|
||||||
"linebreak-style": ["error", "unix"],
|
|
||||||
"quotes": ["error", "double", {
|
|
||||||
"avoidEscape": true
|
|
||||||
}],
|
|
||||||
"camelcase": ["error", {
|
|
||||||
"properties": "always"
|
|
||||||
}],
|
|
||||||
"semi": ["error", "always"],
|
|
||||||
"unicode-bom": "error",
|
|
||||||
"require-jsdoc": ["error", {
|
|
||||||
"require": {
|
|
||||||
"FunctionDeclaration": true,
|
|
||||||
"MethodDefinition": true,
|
|
||||||
"ClassDeclaration": true,
|
|
||||||
"ArrowFunctionExpression": true
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"keyword-spacing": ["error", {
|
|
||||||
"before": true,
|
|
||||||
"after": true
|
|
||||||
}],
|
|
||||||
"no-multiple-empty-lines": ["warn", {
|
|
||||||
"max": 2,
|
|
||||||
"maxEOF": 1,
|
|
||||||
"maxBOF": 0
|
|
||||||
}],
|
|
||||||
"no-whitespace-before-property": "error",
|
|
||||||
"operator-linebreak": ["error", "after"],
|
|
||||||
"space-in-parens": "error",
|
|
||||||
"no-var": "error",
|
|
||||||
"prefer-const": "error"
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": "tests/**/*",
|
|
||||||
"rules": {
|
|
||||||
"no-unused-expressions": "off",
|
|
||||||
"no-console": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"globals": {
|
|
||||||
"$": false,
|
|
||||||
"jQuery": false,
|
|
||||||
"log": false,
|
|
||||||
|
|
||||||
"COMPILE_TIME": false,
|
|
||||||
"COMPILE_MSG": false,
|
|
||||||
"PKG_VERSION": false,
|
|
||||||
"ENVIRONMENT_IS_WORKER": false,
|
|
||||||
"ENVIRONMENT_IS_NODE": false,
|
|
||||||
"ENVIRONMENT_IS_WEB": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
14
.github/ISSUE_TEMPLATE.md
vendored
14
.github/ISSUE_TEMPLATE.md
vendored
@ -1,14 +0,0 @@
|
|||||||
<!-- Prefix the title above with one of the following: -->
|
|
||||||
<!-- Bug report: -->
|
|
||||||
<!-- Operation request: -->
|
|
||||||
<!-- Feature request: -->
|
|
||||||
<!-- Misc: -->
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
|
|
||||||
|
|
||||||
### Example
|
|
||||||
<!-- If describing a bug, tell us what happens instead of the expected behavior -->
|
|
||||||
<!-- Include a link that triggers the bug if possible -->
|
|
||||||
<!-- If you are requesting a new operation, include example input and output -->
|
|
||||||
|
|
||||||
33
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: 'Bug report: <Insert title here>'
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behaviour or a link to the recipe / input used to cause the bug:
|
||||||
|
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behaviour**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (if relevant, please complete the following information):**
|
||||||
|
- OS: [e.g. Windows]
|
||||||
|
- Browser: [e.g. chrome 72, firefox 60]
|
||||||
|
- CyberChef version: [e.g. 9.7.14]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for the project
|
||||||
|
title: 'Feature request: <Insert title here>'
|
||||||
|
labels: feature
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
20
.github/ISSUE_TEMPLATE/operation-request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/operation-request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Operation request
|
||||||
|
about: Suggest a new operation
|
||||||
|
title: 'Operation request: <Insert title here>'
|
||||||
|
labels: operation
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your operation request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of the new operation you would like.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Example input and output**
|
||||||
|
Provide an example input to the operation, along with the output that you would expect.
|
||||||
97
.github/dependabot.yml
vendored
Normal file
97
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# See the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
#
|
||||||
|
# Check for minor/patch versions only on a weekly basis - we are likely to be able to
|
||||||
|
# merge these routinely. Major versions we'll check for and update manually.
|
||||||
|
#
|
||||||
|
- package-ecosystem: 'npm'
|
||||||
|
directory: '/'
|
||||||
|
versioning-strategy: increase
|
||||||
|
schedule:
|
||||||
|
interval: 'weekly'
|
||||||
|
day: 'friday'
|
||||||
|
time: '03:00'
|
||||||
|
timezone: Europe/London
|
||||||
|
cooldown:
|
||||||
|
default-days: 2
|
||||||
|
commit-message:
|
||||||
|
prefix: 'chore (deps): '
|
||||||
|
ignore:
|
||||||
|
# we'll do any major version updates manually
|
||||||
|
- dependency-name: '*'
|
||||||
|
update-types: ['version-update:semver-major']
|
||||||
|
# packages we can't currently update
|
||||||
|
# see issue #2214 for rationale for each of these
|
||||||
|
- dependency-name: '@xmldom/xmldom'
|
||||||
|
versions: [ '>=0.9.0' ]
|
||||||
|
- dependency-name: 'bootstrap'
|
||||||
|
versions: [ '>=5.0.0' ]
|
||||||
|
- dependency-name: 'cbor'
|
||||||
|
versions: [ '>=10.0.0' ]
|
||||||
|
- dependency-name: 'eslint'
|
||||||
|
versions: [ '>=10.0.0' ]
|
||||||
|
- dependency-name: 'eslint-plugin-jsdoc'
|
||||||
|
versions: [ '>=51.0.0' ]
|
||||||
|
- dependency-name: 'fernet'
|
||||||
|
versions: [ '>=0.4.0' ]
|
||||||
|
- dependency-name: 'geodesy'
|
||||||
|
versions: [ '>=2.0.0' ]
|
||||||
|
- dependency-name: 'jimp'
|
||||||
|
versions: [ '1.6.1' ]
|
||||||
|
- dependency-name: 'jq-web'
|
||||||
|
versions: [ '>=0.6.0' ]
|
||||||
|
groups:
|
||||||
|
#
|
||||||
|
# Grouping so patch version updates are batched together in a single PR
|
||||||
|
# and similarly with minor version updates
|
||||||
|
#
|
||||||
|
patch-updates:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- '*'
|
||||||
|
update-types:
|
||||||
|
- 'patch'
|
||||||
|
minor-updates:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- '*'
|
||||||
|
update-types:
|
||||||
|
- 'minor'
|
||||||
|
|
||||||
|
# Versioning on Github Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
# Workflow files stored in the default location of `.github/workflows`; no need to
|
||||||
|
# specify `/.github/workflows` for `directory`
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: 'weekly'
|
||||||
|
day: 'friday'
|
||||||
|
time: '03:00'
|
||||||
|
timezone: Europe/London
|
||||||
|
cooldown:
|
||||||
|
default-days: 4
|
||||||
|
commit-message:
|
||||||
|
prefix: 'chore (deps): '
|
||||||
|
groups:
|
||||||
|
actions-dependencies:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: 'weekly'
|
||||||
|
day: 'friday'
|
||||||
|
time: '03:00'
|
||||||
|
timezone: Europe/London
|
||||||
|
cooldown:
|
||||||
|
default-days: 4
|
||||||
|
commit-message:
|
||||||
|
prefix: 'chore (deps): '
|
||||||
|
groups:
|
||||||
|
docker-dependencies:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
15
.github/pull_request_template.md
vendored
Normal file
15
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
**Description**
|
||||||
|
Provide a description of the pull request and the changes that it makes.
|
||||||
|
|
||||||
|
**Existing Issue**
|
||||||
|
If this pull request relates to an existing issue in the repository, please link it here.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If the pull request changes any visual aspects of CyberChef, please include screenshots.
|
||||||
|
|
||||||
|
**AI disclosure**
|
||||||
|
If you have used any AI tools while creating this code, **you must declare your usage along with the name of the tools that you used**.
|
||||||
|
Regardless of AI tool usage, you are responsible for any code that you submit, and we expect you to have checked the code and have enough of an understanding of it to answer any questions we might have.
|
||||||
|
|
||||||
|
**Test Coverage**
|
||||||
|
Please ensure you have added test coverage for your changes.
|
||||||
62
.github/workflows/cla-close-stale.yml
vendored
Normal file
62
.github/workflows/cla-close-stale.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
name: Close Stale Unsigned CLA PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Runs daily at 01:30 UTC.
|
||||||
|
- cron: '30 1 * * *'
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
# Configurable intervals (days).
|
||||||
|
# DAYS_BEFORE_WARNING = grace period before the warning comment.
|
||||||
|
# DAYS_BEFORE_CLOSURE = further period after the warning before closing.
|
||||||
|
env:
|
||||||
|
DAYS_BEFORE_WARNING: 7
|
||||||
|
DAYS_BEFORE_CLOSURE: 21
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Close stale unsigned-CLA PRs
|
||||||
|
uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 #v11.0.0
|
||||||
|
with:
|
||||||
|
# ---- Guards: only act on PRs carrying the CLA label ----
|
||||||
|
only-labels: 'awaiting cla'
|
||||||
|
|
||||||
|
# Never touch issues — PRs only.
|
||||||
|
days-before-issue-stale: -1
|
||||||
|
days-before-issue-close: -1
|
||||||
|
|
||||||
|
# ---- Timing ----
|
||||||
|
# DAYS_BEFORE_WARNING: days of inactivity before the warning comment.
|
||||||
|
days-before-pr-stale: ${{ env.DAYS_BEFORE_WARNING }}
|
||||||
|
# DAYS_BEFORE_CLOSURE: days after being marked stale before closing.
|
||||||
|
days-before-pr-close: ${{ env.DAYS_BEFORE_CLOSURE }}
|
||||||
|
|
||||||
|
# ---- Warning comment (posted once when marked stale) ----
|
||||||
|
stale-pr-message: >
|
||||||
|
As we are unable to accept contributions unless the CLA has
|
||||||
|
been signed, this PR will be automatically closed if the CLA
|
||||||
|
is not signed within ${{ env.DAYS_BEFORE_CLOSURE }} days.
|
||||||
|
|
||||||
|
# ---- Close comment ----
|
||||||
|
close-pr-message: >
|
||||||
|
This PR has been automatically closed as the CLA remains
|
||||||
|
unsigned. We will be happy to have it reopened if the CLA
|
||||||
|
is signed subsequently.
|
||||||
|
|
||||||
|
# A dedicated marker label so we can track stale state without
|
||||||
|
# interfering with the "awaiting cla" label.
|
||||||
|
stale-pr-label: 'cla-stale'
|
||||||
|
|
||||||
|
# If the PR is updated after being marked stale, remove the marker
|
||||||
|
# so the warning-then-close cycle restarts cleanly.
|
||||||
|
remove-pr-stale-when-updated: true
|
||||||
|
|
||||||
|
# Process enough PRs per run for busy repos.
|
||||||
|
operations-per-run: 200
|
||||||
87
.github/workflows/cla-label.yml
vendored
Normal file
87
.github/workflows/cla-label.yml
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
name: CLA Label Sync
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-label:
|
||||||
|
# Only run for PRs (issue_comment fires for issues too)
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'pull_request_target' ||
|
||||||
|
(github.event.issue.pull_request != null)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Sync "awaiting cla" label
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0
|
||||||
|
env:
|
||||||
|
AWAITING_LABEL: 'awaiting cla'
|
||||||
|
# Bot login that posts the CLA comment. Common values:
|
||||||
|
# 'github-actions[bot]', 'CLAassistant', 'cla-assistant[bot]'
|
||||||
|
CLA_BOT_LOGINS: 'CLAassistant'
|
||||||
|
# Regex (case-insensitive) that matches an UNSIGNED CLA comment
|
||||||
|
NOT_SIGNED_REGEX: 'cla-assistant.io/pull/badge/not_signed'
|
||||||
|
# Regex (case-insensitive) that matches a SIGNED CLA comment
|
||||||
|
SIGNED_REGEX: 'cla-assistant.io/pull/badge/signed'
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const awaitingLabel = process.env.AWAITING_LABEL;
|
||||||
|
const botLogins = process.env.CLA_BOT_LOGINS.split(',').map(s => s.trim().toLowerCase());
|
||||||
|
const notSigned = new RegExp(process.env.NOT_SIGNED_REGEX, 'i');
|
||||||
|
const signed = new RegExp(process.env.SIGNED_REGEX, 'i');
|
||||||
|
|
||||||
|
// Resolve PR number for either trigger
|
||||||
|
const prNumber = context.eventName === 'pull_request_target'
|
||||||
|
? context.payload.pull_request.number
|
||||||
|
: context.payload.issue.number;
|
||||||
|
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
|
||||||
|
// Pull the full comment history to find the latest CLA bot comment
|
||||||
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||||
|
owner, repo, issue_number: prNumber, per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const claComments = comments.filter(c =>
|
||||||
|
botLogins.includes((c.user?.login || '').toLowerCase()) &&
|
||||||
|
(notSigned.test(c.body) || signed.test(c.body))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (claComments.length === 0) {
|
||||||
|
core.info('No CLA Assistant comment found yet; nothing to do.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latest = claComments[claComments.length - 1];
|
||||||
|
const isSigned = signed.test(latest.body) && !notSigned.test(latest.body);
|
||||||
|
|
||||||
|
core.info(`Latest CLA comment (id ${latest.id}) => signed=${isSigned}`);
|
||||||
|
|
||||||
|
// Current labels
|
||||||
|
const { data: issue } = await github.rest.issues.get({
|
||||||
|
owner, repo, issue_number: prNumber,
|
||||||
|
});
|
||||||
|
const hasLabel = issue.labels.some(l =>
|
||||||
|
(typeof l === 'string' ? l : l.name) === awaitingLabel
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSigned && hasLabel) {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner, repo, issue_number: prNumber, name: awaitingLabel,
|
||||||
|
}).catch(e => core.warning(`removeLabel failed: ${e.message}`));
|
||||||
|
core.info(`Removed "${awaitingLabel}".`);
|
||||||
|
} else if (!isSigned && !hasLabel) {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner, repo, issue_number: prNumber, labels: [awaitingLabel],
|
||||||
|
});
|
||||||
|
core.info(`Added "${awaitingLabel}".`);
|
||||||
|
} else {
|
||||||
|
core.info('Label already in the correct state.');
|
||||||
|
}
|
||||||
72
.github/workflows/master.yml
vendored
Normal file
72
.github/workflows/master.yml
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
name: "Master Build, Test & Deploy"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pages: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- name: Set node version
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run setheapsize
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npx grunt lint
|
||||||
|
|
||||||
|
- name: Unit Tests
|
||||||
|
run: |
|
||||||
|
npm test
|
||||||
|
npm run testnodeconsumer
|
||||||
|
|
||||||
|
- name: Production Build
|
||||||
|
if: success()
|
||||||
|
run: npx grunt prod --msg=""
|
||||||
|
|
||||||
|
- name: Generate sitemap
|
||||||
|
run: npx grunt exec:sitemap
|
||||||
|
|
||||||
|
- name: Setup Chrome
|
||||||
|
id: setup-chrome
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
npx @puppeteer/browsers install chrome@148
|
||||||
|
echo chromedir=$(dirname $(find `pwd`/chrome/* -name chrome -print -quit)) >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: UI Tests
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
export PATH=${{ steps.setup-chrome.outputs.chromedir }}:$PATH
|
||||||
|
sudo apt-get install xvfb
|
||||||
|
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
|
||||||
|
|
||||||
|
- name: Prepare for GitHub Pages
|
||||||
|
if: success()
|
||||||
|
run: npx grunt copy:ghPages
|
||||||
|
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
if: success() && github.ref == 'refs/heads/master'
|
||||||
|
uses: crazy-max/ghaction-github-pages@1d6ee9b181a81033a16bd707a1401afa978daab4 # v5.0.0
|
||||||
|
with:
|
||||||
|
target_branch: gh-pages
|
||||||
|
build_dir: ./build/prod
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
74
.github/workflows/pull_requests.yml
vendored
Normal file
74
.github/workflows/pull_requests.yml
vendored
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
name: "Pull Requests"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
types: [synchronize, opened, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- name: Set node version
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run setheapsize
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npx grunt lint
|
||||||
|
|
||||||
|
- name: Unit Tests
|
||||||
|
run: |
|
||||||
|
npm test
|
||||||
|
npm run testnodeconsumer
|
||||||
|
|
||||||
|
- name: Production Build
|
||||||
|
if: success()
|
||||||
|
run: npx grunt prod
|
||||||
|
|
||||||
|
- name: Upload Build Artefact
|
||||||
|
if: success()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: zipped-build
|
||||||
|
path: build/prod/*.zip
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
- name: Setup Chrome
|
||||||
|
id: setup-chrome
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
npx @puppeteer/browsers install chrome@148
|
||||||
|
echo chromedir=$(dirname $(find `pwd`/chrome/* -name chrome -print -quit)) >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: UI Tests
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
export PATH=${{ steps.setup-chrome.outputs.chromedir }}:$PATH
|
||||||
|
sudo apt-get install xvfb
|
||||||
|
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: success()
|
||||||
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||||
|
|
||||||
|
- name: Production Image Build
|
||||||
|
if: success()
|
||||||
|
id: build-image
|
||||||
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
128
.github/workflows/releases.yml
vendored
Normal file
128
.github/workflows/releases.yml
vendored
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
name: "Releases"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
REGISTRY_USER: ${{ github.actor }}
|
||||||
|
REGISTRY_PASSWORD: ${{ github.token }}
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- name: Set node version
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run setheapsize
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npx grunt lint
|
||||||
|
|
||||||
|
- name: Unit Tests
|
||||||
|
run: |
|
||||||
|
npm test
|
||||||
|
npm run testnodeconsumer
|
||||||
|
|
||||||
|
- name: Production Build
|
||||||
|
run: npx grunt prod
|
||||||
|
|
||||||
|
- name: Setup Chrome
|
||||||
|
id: setup-chrome
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
npx @puppeteer/browsers install chrome@148
|
||||||
|
echo chromedir=$(dirname $(find `pwd`/chrome/* -name chrome -print -quit)) >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: UI Tests
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
export PATH=${{ steps.setup-chrome.outputs.chromedir }}:$PATH
|
||||||
|
sudo apt-get install xvfb
|
||||||
|
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||||
|
|
||||||
|
- name: Image Metadata
|
||||||
|
id: image-metadata
|
||||||
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ env.REGISTRY_USER }}
|
||||||
|
password: ${{ env.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Publish to GHCR
|
||||||
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.image-metadata.outputs.tags }}
|
||||||
|
labels: ${{ steps.image-metadata.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
|
||||||
|
- name: Upload Release Assets
|
||||||
|
id: upload-release-assets
|
||||||
|
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
file: build/prod/*.zip
|
||||||
|
tag: ${{ github.ref }}
|
||||||
|
overwrite: true
|
||||||
|
file_glob: true
|
||||||
|
body: "See the [CHANGELOG](https://github.com/gchq/CyberChef/blob/master/CHANGELOG.md) and [commit messages](https://github.com/gchq/CyberChef/commits/master) for details."
|
||||||
|
|
||||||
|
npm-publish:
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
needs: main
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- name: Set node version
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Create machine generated files
|
||||||
|
run: npm run node
|
||||||
|
|
||||||
|
- name: Publish to NPM
|
||||||
|
run: npm publish
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@ -2,12 +2,17 @@ node_modules
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
travis.log
|
travis.log
|
||||||
build
|
build
|
||||||
docs/*
|
|
||||||
!docs/*.conf.json
|
|
||||||
!docs/*.ico
|
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
|
.*.swp
|
||||||
src/core/config/modules/*
|
src/core/config/modules/*
|
||||||
src/core/config/OperationConfig.json
|
src/core/config/OperationConfig.json
|
||||||
src/core/operations/index.mjs
|
src/core/operations/index.mjs
|
||||||
|
src/core/lib/HTMLEntities.mjs
|
||||||
|
src/node/config/OperationConfig.json
|
||||||
|
src/node/index.mjs
|
||||||
|
tests/operations/index.mjs
|
||||||
|
**/*.DS_Store
|
||||||
tests/browser/output/*
|
tests/browser/output/*
|
||||||
|
.node-version
|
||||||
|
chrome
|
||||||
|
|||||||
@ -3,6 +3,5 @@ npm-debug.log
|
|||||||
travis.log
|
travis.log
|
||||||
build/*
|
build/*
|
||||||
!build/node
|
!build/node
|
||||||
docs
|
|
||||||
.vscode
|
.vscode
|
||||||
.github
|
.github
|
||||||
|
|||||||
53
.travis.yml
53
.travis.yml
@ -1,53 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- node
|
|
||||||
addons:
|
|
||||||
chrome: stable
|
|
||||||
install: npm install
|
|
||||||
before_script:
|
|
||||||
- npm install -g grunt
|
|
||||||
- export NODE_OPTIONS=--max_old_space_size=2048
|
|
||||||
script:
|
|
||||||
- grunt lint
|
|
||||||
- grunt test
|
|
||||||
- grunt docs
|
|
||||||
- grunt node
|
|
||||||
- grunt prod --msg="$COMPILE_MSG"
|
|
||||||
- xvfb-run --server-args="-screen 0 1200x800x24" grunt testui
|
|
||||||
before_deploy:
|
|
||||||
- grunt exec:sitemap
|
|
||||||
- grunt copy:ghPages
|
|
||||||
deploy:
|
|
||||||
- provider: pages
|
|
||||||
skip_cleanup: true
|
|
||||||
github_token: $GITHUB_TOKEN
|
|
||||||
local_dir: build/prod/
|
|
||||||
target_branch: gh-pages
|
|
||||||
on:
|
|
||||||
repo: gchq/CyberChef
|
|
||||||
branch: master
|
|
||||||
- provider: releases
|
|
||||||
skip_cleanup: true
|
|
||||||
api_key:
|
|
||||||
secure: "HV1WSKv4l/0Y2bKKs1iBJocBcmLj08PCRUeEM/jTwA4jqJ8EiLHWiXtER/D5sEg2iibRVKd2OQjfrmS6bo4AiwdeVgAKmv0FtS2Jw+391N8Nd5AkEANHa5Om/IpHLTL2YRAjpJTsDpY72bMUTJIwjQA3TFJkgrpOw6KYfohOcgbxLpZ4XuNJRU3VL4Hsxdv5V9aOVmfFOmMOVPQlakXy7NgtW5POp1f2WJwgcZxylkR1CjwaqMyXmSoVl46pyH3tr5+dptsQoKSGdi6sIHGA60oDotFPcm+0ifa47wZw+vapuuDi4tdNxhrHGaDMG8xiE0WFDHwQUDlk2/+W7j9SEX0H3Em7us371JXRp56EDwEcDa34VpVkC6i8HGcHK55hnxVbMZXGf3qhOFD8wY7qMbjMRvIpucrMHBi86OfkDfv0vDj2LyvIl5APj/AX50BrE0tfH1MZbH26Jkx4NdlkcxQ14GumarmUqfmVvbX/fsoA6oUuAAE9ZgRRi3KHO4wci6KUcRfdm+XOeUkaBFsL86G3EEYIvrtBTuaypdz+Cx7nd1iPZyWMx5Y1gXnVzha4nBdV4+7l9JIsFggD8QVpw2uHXQiS1KXFjOeqA3DBD8tjMB7q26Fl2fD3jkOo4BTbQ2NrRIZUu/iL+fOmMPsyMt2qulB0yaSBCfkbEq8xrUA="
|
|
||||||
file:
|
|
||||||
- build/prod/cyberchef.htm
|
|
||||||
- build/node/CyberChef.js
|
|
||||||
on:
|
|
||||||
repo: gchq/CyberChef
|
|
||||||
tags: true
|
|
||||||
- provider: npm
|
|
||||||
skip_cleanup: true
|
|
||||||
email: "n1474335@gmail.com"
|
|
||||||
api_key:
|
|
||||||
secure: "UnDQL3Kh+GK2toL0TK3FObO0ujVssU3Eg4BBuYdjwLB81GhiGE5/DTh7THdZPOpbLo6wQeOwfZDuMeKC1OU+0Uf4NsdYFu1aq6xMO20qBQ4qUfgsyiK4Qgywj9gk0p1+OFZdGAZ/j1CNRAaF71XQIY6iV84c+SO4WoizXYrNT0Jh4sr2DA4/97G2xmJtPi0qOzYrJ09R56ZUozmqeik5G0pMRIuJRbpjS/7bZXV+N7WV0ombZc9RkUaetbabEVOLQ+Xx5YAIVq+VuEeMe9VBSnxY/FfCLmy1wJsjGzpLCyBI9nbrG4nw8Wgc2m8NfK9rcpIvBTGner9r2j60NVDkZ8kLZPrqXhq6AZMwa+oz6K5UQCqRo2RRQzSGwXxg67HY5Tcq+oNmjd+DqpPg4LZ3eGlluyP5XfG+hpSr9Ya4d8q8SrUWLxkoLHI6ZKMtoKFbTCSSQPiluW5hsZxjz3yDkkjsJw64M/EM8UyJrgaXqDklQu+7rBGKLfsK6os7RDiqjBWpQ7gwpo8HvY0O8yqEAabPz+QGkanpjcCOZCXFbSkzWxYy37RMAPu88iINVZVlZE4l+WJenCpZY95ueyy0mG9cyMSzVRPyX6A+/n4H6VMFPFjpGDLTD588ACEjY1lmHfS/eXwXJcgqPPD2gW0XdRdUheU/ssqlfCfGWQMTDXs="
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
branch: master
|
|
||||||
notifications:
|
|
||||||
webhooks:
|
|
||||||
urls:
|
|
||||||
- https://webhooks.gitter.im/e/83c143a6822e218d5b34
|
|
||||||
on_success: change
|
|
||||||
on_failure: always
|
|
||||||
on_start: never
|
|
||||||
75
AGENTS.md
Normal file
75
AGENTS.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# CyberChef Agent Development Guide
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
CyberChef is a client-side web app and Node.js package for encoding, decoding, encryption, compression, parsing, and data analysis operations. Users build recipes from operations and run them against browser-local input.
|
||||||
|
|
||||||
|
Core principles for changes:
|
||||||
|
|
||||||
|
- Keep operations and features client-side, avoiding external services whenever possible. CyberChef is used on airgapped networks.
|
||||||
|
- Keep latency low. Keep large libraries in separate modules so they are downloaded only by users who invoke the relevant operations.
|
||||||
|
- Prefer Vanilla JS over jQuery or other frameworks.
|
||||||
|
- Avoid new external package dependencies unless absolutely necessary. Reuse platform APIs and existing project utilities first.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
CyberChef expects Node.js `>=24 <25`.
|
||||||
|
|
||||||
|
- Install: `npm install`
|
||||||
|
- Development server: `npm start`
|
||||||
|
- Production build: `npm run build`
|
||||||
|
- Build Node package artifacts: `npm run node`
|
||||||
|
- Lint: `npm run lint`
|
||||||
|
- Spell/grammar lint for `src`: `npm run lint:grammar`
|
||||||
|
- Full non-UI test suite: `npm test`
|
||||||
|
- UI tests: `npm run testui`
|
||||||
|
- UI tests against the dev server: `npm run testuidev`
|
||||||
|
- Node REPL: `npm run repl`
|
||||||
|
|
||||||
|
## New operations
|
||||||
|
|
||||||
|
Use the existing generator for new operations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run newop
|
||||||
|
```
|
||||||
|
|
||||||
|
This wraps `node src/core/config/scripts/newOperation.mjs`. Run it from the repository root. Afterwards:
|
||||||
|
|
||||||
|
- Implement the operation in `src/core/operations/<Operation>.mjs`.
|
||||||
|
- Add or verify its category entry in `src/core/config/Categories.json`.
|
||||||
|
- Implement the tests in `tests/operations/tests/<Operation>.mjs`.
|
||||||
|
|
||||||
|
## Coding conventions
|
||||||
|
|
||||||
|
* Indentation: Each block should consist of 4 spaces
|
||||||
|
* Object/namespace identifiers: CamelCase
|
||||||
|
* Function/variable names: camelCase
|
||||||
|
* Constants: UNDERSCORE_UPPER_CASE
|
||||||
|
* Source code encoding: UTF-8 (without BOM)
|
||||||
|
* All source files must end with a newline
|
||||||
|
* Line endings: UNIX style (\n)
|
||||||
|
|
||||||
|
## Operation argument validation and errors
|
||||||
|
|
||||||
|
Use the existing operation recipe validation framework. Declare constraints in operation `ingList` entries via operation `args` so `Ingredient` and `Operation.validateIngredients()` can enforce them before `run()` executes. Do not use checks inside `run()` when a constraint belongs in argument metadata.
|
||||||
|
|
||||||
|
For invalid arguments and failures caused by user input, throw `OperationError` from `src/core/errors/OperationError.mjs`. Recipe execution treats `OperationError` as expected output, and is not appropriate for programmer bugs or unexpected internal exceptions.
|
||||||
|
|
||||||
|
## Testing instructions
|
||||||
|
|
||||||
|
Bug fixes require regression tests that must fail before the fix and pass after it. New features require tests including edge cases and invalid input. Use both NodeJS tests and UI tests when a change affects both the operation/API behaviour and browser interaction.
|
||||||
|
|
||||||
|
- Operation tests live under `tests/operations/tests/` and are run by `npm test` through `tests/operations/index.mjs`.
|
||||||
|
- Node API tests live under `tests/node/tests/` and are run by `npm test` through `tests/node/index.mjs`.
|
||||||
|
- UI tests live under `tests/browser/` and are run with `npm run testui` or `npm run testuidev`.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Avoid prototype pollution. Utilise `Object.create(null)`, `Map`, and `Object.prototype.hasOwnProperty.call(...)`.
|
||||||
|
- Avoid XSS. Escape strings before rendering and prefer text APIs over `innerHTML`.
|
||||||
|
- Avoid resource exhaustion. Bound input sizes, counts, recursion depth and loop iterations.
|
||||||
|
|
||||||
|
## Issue and pull request policy
|
||||||
|
|
||||||
|
Do not open GitHub issues or pull requests on behalf of users. If a user asks to create an issue or pull request, stage a root-level file named `AI_MARKER`.
|
||||||
1369
CHANGELOG.md
1369
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -6,10 +6,13 @@ There are lots of opportunities to contribute to CyberChef. If you want ideas, t
|
|||||||
|
|
||||||
Before your contributions can be accepted, you must:
|
Before your contributions can be accepted, you must:
|
||||||
|
|
||||||
- Sign the [GCHQ Contributor Licence Agreement](https://github.com/gchq/Gaffer/wiki/GCHQ-OSS-Contributor-License-Agreement-V1.0)
|
- Fork the CyberChef repo
|
||||||
|
- Create a new branch within your fork for the changes
|
||||||
- Push your changes to your fork.
|
- Push your changes to your fork.
|
||||||
|
- Sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef)
|
||||||
- Submit a pull request.
|
- Submit a pull request.
|
||||||
|
|
||||||
|
Please note that we will ***reject*** pull requests from the master branch of your fork owing to the mess it makes of our own working repositories and the extra work entailed.
|
||||||
|
|
||||||
## Coding conventions
|
## Coding conventions
|
||||||
|
|
||||||
@ -22,15 +25,15 @@ Before your contributions can be accepted, you must:
|
|||||||
* Line endings: UNIX style (\n)
|
* Line endings: UNIX style (\n)
|
||||||
|
|
||||||
|
|
||||||
## Design Principals
|
## Design Principles
|
||||||
|
|
||||||
1. If at all possible, all operations and features should be client-side and not rely on connections to an external server. This increases the utility of CyberChef on closed networks and in virtual machines that are not connected to the Internet. Calls to external APIs may be accepted if there is no other option, but not for critical components.
|
1. If at all possible, all operations and features should be client-side and not rely on connections to an external server. This increases the utility of CyberChef on closed networks and in virtual machines that are not connected to the Internet. Calls to external APIs may be accepted if there is no other option, but not for critical components.
|
||||||
2. Latency should be kept to a minimum to enhance the user experience. This means that all operation code should sit on the client, rather than being loaded dynamically from a server.
|
2. Latency should be kept to a minimum to enhance the user experience. This means that operation code should sit on the client and be executed there. However, as a trade-off between latency and bandwidth, operation code with large dependencies can be loaded in discrete modules in order to reduce the size of the initial download. The downloading of additional modules must remain entirely transparent so that the user is not inconvenienced.
|
||||||
3. Use Vanilla JS if at all possible to reduce the number of libraries required and relied upon. Frameworks like jQuery, although included, should not be used unless absolutely necessary.
|
3. Large libraries should be kept in separate modules so that they are not downloaded by everyone who uses the app, just those who specifically require the relevant operations.
|
||||||
4. Minimise the use of large libraries, especially for niche operations that won't be used very often - these will be downloaded by everyone using the app, whether they use that operation or not (due to principal 2).
|
4. Use Vanilla JS if at all possible to reduce the number of libraries required and relied upon. Frameworks like jQuery, although included, should not be used unless absolutely necessary.
|
||||||
|
|
||||||
|
|
||||||
With these principals in mind, any changes or additions to CyberChef should keep it:
|
With these principles in mind, any changes or additions to CyberChef should keep it:
|
||||||
|
|
||||||
- Standalone
|
- Standalone
|
||||||
- Efficient
|
- Efficient
|
||||||
34
Dockerfile
Normal file
34
Dockerfile
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#####################################
|
||||||
|
# Build the app to a static website #
|
||||||
|
#####################################
|
||||||
|
# Modifier --platform=$BUILDPLATFORM limits the platform to "BUILDPLATFORM" during buildx multi-platform builds
|
||||||
|
# This is because npm "chromedriver" package is not compatiable with all platforms
|
||||||
|
# For more info see: https://docs.docker.com/build/building/multi-platform/#cross-compilation
|
||||||
|
FROM --platform=$BUILDPLATFORM node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
COPY package-lock.json .
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
# --ignore-scripts prevents postinstall script (which runs grunt) as it depends on files other than package.json
|
||||||
|
RUN npm ci --ignore-scripts
|
||||||
|
|
||||||
|
# Copy files needed for postinstall and build
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# npm postinstall runs grunt, which depends on files other than package.json
|
||||||
|
RUN npm run postinstall
|
||||||
|
|
||||||
|
# Build the app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
#########################################
|
||||||
|
# Package static build files into nginx #
|
||||||
|
#########################################
|
||||||
|
FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:44e36330f74d4f3a1d4e222acca9e23b401fb87811a7597024502bb759c4dd49 AS cyberchef
|
||||||
|
|
||||||
|
LABEL maintainer="GCHQ <oss@gchq.gov.uk>"
|
||||||
|
|
||||||
|
COPY --from=builder /app/build/prod /usr/share/nginx/html/
|
||||||
506
Gruntfile.js
506
Gruntfile.js
@ -1,12 +1,15 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const crypto = require("node:crypto");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||||
const NodeExternals = require("webpack-node-externals");
|
|
||||||
const Inliner = require("web-resource-inliner");
|
|
||||||
const glob = require("glob");
|
const glob = require("glob");
|
||||||
const path = require("path");
|
|
||||||
|
const nodeFlags = "--no-warnings --no-deprecation";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grunt configuration for building the app in various formats.
|
* Grunt configuration for building the app in various formats.
|
||||||
@ -25,101 +28,152 @@ module.exports = function (grunt) {
|
|||||||
"A persistent task which creates a development build whenever source files are modified.",
|
"A persistent task which creates a development build whenever source files are modified.",
|
||||||
["clean:dev", "clean:config", "exec:generateConfig", "concurrent:dev"]);
|
["clean:dev", "clean:config", "exec:generateConfig", "concurrent:dev"]);
|
||||||
|
|
||||||
|
grunt.registerTask("prod",
|
||||||
|
"Creates a production-ready build. Use the --msg flag to add a compile message.",
|
||||||
|
[
|
||||||
|
"eslint", "clean:prod", "clean:config", "exec:generateConfig", "findModules", "webpack:web",
|
||||||
|
"copy:standalone", "zip:standalone", "clean:standalone", "calcDownloadHash", "chmod"
|
||||||
|
]);
|
||||||
|
|
||||||
grunt.registerTask("node",
|
grunt.registerTask("node",
|
||||||
"Compiles CyberChef into a single NodeJS module.",
|
"Compiles CyberChef into a single NodeJS module.",
|
||||||
["clean:node", "clean:config", "exec:generateConfig", "webpack:node", "chmod:build"]);
|
[
|
||||||
|
"clean:node", "clean:config", "clean:nodeConfig", "exec:generateConfig", "exec:generateNodeIndex"
|
||||||
|
]);
|
||||||
|
|
||||||
grunt.registerTask("test",
|
grunt.registerTask("configTests",
|
||||||
"A task which runs all the operation tests in the tests directory.",
|
"A task which configures config files in preparation for tests to be run. Use `npm test` to run tests.",
|
||||||
["exec:generateConfig", "exec:opTests"]);
|
[
|
||||||
|
"clean:config", "clean:nodeConfig", "exec:generateConfig", "exec:generateNodeIndex"
|
||||||
|
]);
|
||||||
|
|
||||||
grunt.registerTask("testui",
|
grunt.registerTask("testui",
|
||||||
"A task which runs all the UI tests in the tests directory. The prod task must already have been run.",
|
"A task which runs all the UI tests in the tests directory. The prod task must already have been run.",
|
||||||
["connect:prod", "exec:browserTests"]);
|
["connect:prod", "exec:browserTests"]);
|
||||||
|
|
||||||
grunt.registerTask("docs",
|
grunt.registerTask("testnodeconsumer",
|
||||||
"Compiles documentation in the /docs directory.",
|
"A task which checks whether consuming CJS and ESM apps work with the CyberChef build",
|
||||||
["clean:docs", "jsdoc", "chmod:docs"]);
|
["exec:setupNodeConsumers", "exec:testCJSNodeConsumer", "exec:testESMNodeConsumer", "exec:teardownNodeConsumers"]);
|
||||||
|
|
||||||
grunt.registerTask("prod",
|
|
||||||
"Creates a production-ready build. Use the --msg flag to add a compile message.",
|
|
||||||
["eslint", "clean:prod", "clean:config", "exec:generateConfig", "webpack:web", "inline", "chmod"]);
|
|
||||||
|
|
||||||
grunt.registerTask("default",
|
grunt.registerTask("default",
|
||||||
"Lints the code base",
|
"Lints the code base",
|
||||||
["eslint", "exec:repoSize"]);
|
["eslint", "exec:repoSize"]);
|
||||||
|
|
||||||
grunt.registerTask("inline",
|
|
||||||
"Compiles a production build of CyberChef into a single, portable web page.",
|
|
||||||
["exec:generateConfig", "webpack:webInline", "runInliner", "clean:inlineScripts"]);
|
|
||||||
|
|
||||||
|
|
||||||
grunt.registerTask("runInliner", runInliner);
|
|
||||||
grunt.registerTask("doc", "docs");
|
|
||||||
grunt.registerTask("tests", "test");
|
|
||||||
grunt.registerTask("lint", "eslint");
|
grunt.registerTask("lint", "eslint");
|
||||||
|
|
||||||
|
grunt.registerTask("findModules",
|
||||||
|
"Finds all generated modules and updates the entry point list for Webpack",
|
||||||
|
function (arg1, arg2) {
|
||||||
|
const moduleEntryPoints = listEntryModules();
|
||||||
|
|
||||||
|
grunt.log.writeln(`Found ${Object.keys(moduleEntryPoints).length} modules.`);
|
||||||
|
|
||||||
|
grunt.config.set("webpack.web.entry",
|
||||||
|
Object.assign({
|
||||||
|
main: "./src/web/index.js"
|
||||||
|
}, moduleEntryPoints));
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.registerTask("calcDownloadHash", "Compute download hash", function () {
|
||||||
|
const done = this.async();
|
||||||
|
|
||||||
|
const hash = crypto.createHash("sha256");
|
||||||
|
|
||||||
|
// Use online algorithm to calculate hash, prevents reading the entire 75+ MB zip file into memory
|
||||||
|
fs.createReadStream(`build/prod/${downloadZipFilename}`)
|
||||||
|
.on("data", (chunk) => hash.update(chunk))
|
||||||
|
.on("end", () => {
|
||||||
|
const digest = hash.digest("hex");
|
||||||
|
fs.writeFileSync("build/prod/sha256digest.txt", `${digest}\n`, { encoding: "utf8" });
|
||||||
|
|
||||||
|
const index = fs.readFileSync("build/prod/index.html", { encoding: "utf8" });
|
||||||
|
fs.writeFileSync("build/prod/index.html", index.replace(/DOWNLOAD_HASH_PLACEHOLDER/g, digest), { encoding: "utf8" });
|
||||||
|
|
||||||
|
done(true);
|
||||||
|
})
|
||||||
|
.on("error", (err) => done(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Load tasks provided by each plugin
|
// Load tasks provided by each plugin
|
||||||
grunt.loadNpmTasks("grunt-eslint");
|
grunt.loadNpmTasks("grunt-eslint");
|
||||||
grunt.loadNpmTasks("grunt-webpack");
|
grunt.loadNpmTasks("grunt-webpack");
|
||||||
grunt.loadNpmTasks("grunt-jsdoc");
|
|
||||||
grunt.loadNpmTasks("grunt-contrib-clean");
|
grunt.loadNpmTasks("grunt-contrib-clean");
|
||||||
grunt.loadNpmTasks("grunt-contrib-copy");
|
grunt.loadNpmTasks("grunt-contrib-copy");
|
||||||
grunt.loadNpmTasks("grunt-contrib-watch");
|
grunt.loadNpmTasks("grunt-contrib-watch");
|
||||||
grunt.loadNpmTasks("grunt-chmod");
|
grunt.loadNpmTasks("grunt-chmod");
|
||||||
grunt.loadNpmTasks("grunt-exec");
|
grunt.loadNpmTasks("grunt-exec");
|
||||||
grunt.loadNpmTasks("grunt-accessibility");
|
|
||||||
grunt.loadNpmTasks("grunt-concurrent");
|
grunt.loadNpmTasks("grunt-concurrent");
|
||||||
grunt.loadNpmTasks("grunt-contrib-connect");
|
grunt.loadNpmTasks("grunt-contrib-connect");
|
||||||
|
grunt.loadNpmTasks("grunt-zip");
|
||||||
|
|
||||||
|
|
||||||
// Project configuration
|
// Project configuration
|
||||||
const compileTime = grunt.template.today("UTC:dd/mm/yyyy HH:MM:ss") + " UTC",
|
const compileYear = grunt.template.today("UTC:yyyy"),
|
||||||
|
compileTime = grunt.template.today("UTC:dd/mm/yyyy HH:MM:ss") + " UTC",
|
||||||
pkg = grunt.file.readJSON("package.json"),
|
pkg = grunt.file.readJSON("package.json"),
|
||||||
|
version = process.env.GITHUB_SHA || `v${pkg.version}`,
|
||||||
|
downloadZipFilename = `CyberChef_${version}.zip`,
|
||||||
webpackConfig = require("./webpack.config.js"),
|
webpackConfig = require("./webpack.config.js"),
|
||||||
BUILD_CONSTANTS = {
|
BUILD_CONSTANTS = {
|
||||||
|
COMPILE_YEAR: JSON.stringify(compileYear),
|
||||||
COMPILE_TIME: JSON.stringify(compileTime),
|
COMPILE_TIME: JSON.stringify(compileTime),
|
||||||
COMPILE_MSG: JSON.stringify(grunt.option("compile-msg") || grunt.option("msg") || ""),
|
COMPILE_MSG: JSON.stringify(grunt.option("compile-msg") || grunt.option("msg") || ""),
|
||||||
PKG_VERSION: JSON.stringify(pkg.version),
|
PKG_VERSION: JSON.stringify(pkg.version),
|
||||||
ENVIRONMENT_IS_WORKER: function() {
|
|
||||||
return typeof importScripts === "function";
|
|
||||||
},
|
|
||||||
ENVIRONMENT_IS_NODE: function() {
|
|
||||||
return typeof process === "object" && typeof require === "function";
|
|
||||||
},
|
|
||||||
ENVIRONMENT_IS_WEB: function() {
|
|
||||||
return typeof window === "object";
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
moduleEntryPoints = listEntryModules();
|
moduleEntryPoints = listEntryModules(),
|
||||||
|
nodeConsumerTestPath = "~/tmp-cyberchef",
|
||||||
|
/**
|
||||||
|
* Configuration for Webpack production build. Defined as a function so that it
|
||||||
|
* can be recalculated when new modules are generated.
|
||||||
|
*/
|
||||||
|
webpackProdConf = () => {
|
||||||
|
return {
|
||||||
|
mode: "production",
|
||||||
|
target: "web",
|
||||||
|
entry: Object.assign({
|
||||||
|
main: "./src/web/index.js"
|
||||||
|
}, moduleEntryPoints),
|
||||||
|
output: {
|
||||||
|
path: __dirname + "/build/prod",
|
||||||
|
filename: chunkData => {
|
||||||
|
return chunkData.chunk.name === "main" ? "assets/[name].js" : "[name].js";
|
||||||
|
},
|
||||||
|
globalObject: "this"
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"./config/modules/OpModules.mjs": "./config/modules/Default.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin(BUILD_CONSTANTS),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: "index.html",
|
||||||
|
template: "./src/web/html/index.html",
|
||||||
|
chunks: ["main"],
|
||||||
|
compileYear: compileYear,
|
||||||
|
compileTime: compileTime,
|
||||||
|
version: version,
|
||||||
|
latestReleaseVersion: pkg.version,
|
||||||
|
downloadZipFilename: downloadZipFilename,
|
||||||
|
minify: {
|
||||||
|
removeComments: true,
|
||||||
|
collapseWhitespace: true,
|
||||||
|
minifyJS: true,
|
||||||
|
minifyCSS: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: "static",
|
||||||
|
reportFilename: "BundleAnalyzerReport.html",
|
||||||
|
openAnalyzer: false,
|
||||||
|
excludeAssets: /.*Worker.js/
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Compiles a production build of CyberChef into a single, portable web page.
|
|
||||||
*/
|
|
||||||
function runInliner() {
|
|
||||||
const done = this.async();
|
|
||||||
Inliner.html({
|
|
||||||
relativeTo: "build/prod/",
|
|
||||||
fileContent: grunt.file.read("build/prod/cyberchef.htm"),
|
|
||||||
images: true,
|
|
||||||
svgs: true,
|
|
||||||
scripts: true,
|
|
||||||
links: true,
|
|
||||||
strict: true
|
|
||||||
}, function(error, result) {
|
|
||||||
if (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
done(error);
|
|
||||||
} else {
|
|
||||||
done(new Error(error));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
grunt.file.write("build/prod/cyberchef.htm", result);
|
|
||||||
done(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates an entry list for all the modules.
|
* Generates an entry list for all the modules.
|
||||||
@ -130,192 +184,102 @@ module.exports = function (grunt) {
|
|||||||
glob.sync("./src/core/config/modules/*.mjs").forEach(file => {
|
glob.sync("./src/core/config/modules/*.mjs").forEach(file => {
|
||||||
const basename = path.basename(file);
|
const basename = path.basename(file);
|
||||||
if (basename !== "Default.mjs" && basename !== "OpModules.mjs")
|
if (basename !== "Default.mjs" && basename !== "OpModules.mjs")
|
||||||
entryModules[basename.split(".mjs")[0]] = path.resolve(file);
|
entryModules["modules/" + basename.split(".mjs")[0]] = path.resolve(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
return entryModules;
|
return entryModules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects the correct delimiter to use to chain shell commands together
|
||||||
|
* based on the current OS.
|
||||||
|
*
|
||||||
|
* @param {string[]} cmds
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function chainCommands(cmds) {
|
||||||
|
const win = process.platform === "win32";
|
||||||
|
if (!win) {
|
||||||
|
return cmds.join(";");
|
||||||
|
}
|
||||||
|
return cmds
|
||||||
|
// && means that subsequent commands will not be executed if the
|
||||||
|
// previous one fails. & would coninue on a fail
|
||||||
|
.join("&&")
|
||||||
|
// Windows does not support \n properly
|
||||||
|
.replace(/\n/g, "\\n");
|
||||||
|
}
|
||||||
|
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
clean: {
|
clean: {
|
||||||
dev: ["build/dev/*"],
|
dev: ["build/dev/*"],
|
||||||
prod: ["build/prod/*"],
|
prod: ["build/prod/*"],
|
||||||
node: ["build/node/*"],
|
node: ["build/node/*"],
|
||||||
config: ["src/core/config/OperationConfig.json", "src/core/config/modules/*", "src/code/operations/index.mjs"],
|
config: ["src/core/config/OperationConfig.json", "src/core/config/modules/*", "src/code/operations/index.mjs"],
|
||||||
docs: ["docs/*", "!docs/*.conf.json", "!docs/*.ico", "!docs/*.png"],
|
nodeConfig: ["src/node/index.mjs", "src/node/config/OperationConfig.json"],
|
||||||
inlineScripts: ["build/prod/scripts.js"],
|
standalone: ["build/prod/CyberChef*.html"]
|
||||||
},
|
},
|
||||||
eslint: {
|
eslint: {
|
||||||
options: {
|
|
||||||
configFile: "./.eslintrc.json"
|
|
||||||
},
|
|
||||||
configs: ["*.{js,mjs}"],
|
configs: ["*.{js,mjs}"],
|
||||||
core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*", "!src/core/operations/legacy/**/*"],
|
core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*", "!src/core/operations/legacy/**/*"],
|
||||||
web: ["src/web/**/*.{js,mjs}"],
|
web: ["src/web/**/*.{js,mjs}", "!src/web/static/**/*"],
|
||||||
node: ["src/node/**/*.{js,mjs}"],
|
node: ["src/node/**/*.{js,mjs}"],
|
||||||
tests: ["tests/**/*.{js,mjs}"],
|
tests: ["tests/**/*.{js,mjs}"],
|
||||||
},
|
},
|
||||||
jsdoc: {
|
|
||||||
options: {
|
|
||||||
destination: "docs",
|
|
||||||
template: "node_modules/ink-docstrap/template",
|
|
||||||
recurse: true,
|
|
||||||
readme: "./README.md",
|
|
||||||
configure: "docs/jsdoc.conf.json"
|
|
||||||
},
|
|
||||||
all: {
|
|
||||||
src: [
|
|
||||||
"src/**/*.js",
|
|
||||||
"src/**/*.mjs",
|
|
||||||
"!src/core/vendor/**/*"
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
accessibility: {
|
|
||||||
options: {
|
|
||||||
accessibilityLevel: "WCAG2A",
|
|
||||||
verbose: false,
|
|
||||||
ignore: [
|
|
||||||
"WCAG2A.Principle1.Guideline1_3.1_3_1.H42.2"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
src: ["build/**/*.html"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
webpack: {
|
webpack: {
|
||||||
options: webpackConfig,
|
options: webpackConfig,
|
||||||
web: () => {
|
myConfig: webpackConfig,
|
||||||
return {
|
web: webpackProdConf(),
|
||||||
mode: "production",
|
},
|
||||||
target: "web",
|
"webpack-dev-server": {
|
||||||
entry: Object.assign({
|
options: webpackConfig,
|
||||||
main: "./src/web/index.js",
|
start: {
|
||||||
sitemap: "./src/web/static/sitemap.js"
|
mode: "development",
|
||||||
}, moduleEntryPoints),
|
|
||||||
output: {
|
|
||||||
path: __dirname + "/build/prod"
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"./config/modules/OpModules": "./config/modules/Default"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.DefinePlugin(BUILD_CONSTANTS),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: "index.html",
|
|
||||||
template: "./src/web/html/index.html",
|
|
||||||
chunks: ["main"],
|
|
||||||
compileTime: compileTime,
|
|
||||||
version: pkg.version,
|
|
||||||
minify: {
|
|
||||||
removeComments: true,
|
|
||||||
collapseWhitespace: true,
|
|
||||||
minifyJS: true,
|
|
||||||
minifyCSS: true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new BundleAnalyzerPlugin({
|
|
||||||
analyzerMode: "static",
|
|
||||||
reportFilename: "BundleAnalyzerReport.html",
|
|
||||||
openAnalyzer: false
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
webInline: {
|
|
||||||
mode: "production",
|
|
||||||
target: "web",
|
target: "web",
|
||||||
entry: "./src/web/index.js",
|
entry: Object.assign({
|
||||||
output: {
|
main: "./src/web/index.js"
|
||||||
filename: "scripts.js",
|
}, moduleEntryPoints),
|
||||||
path: __dirname + "/build/prod"
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"./config/modules/OpModules.mjs": "./config/modules/Default.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
port: grunt.option("port") || 8080,
|
||||||
|
client: {
|
||||||
|
logging: "error",
|
||||||
|
overlay: true
|
||||||
|
},
|
||||||
|
hot: "only"
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin(Object.assign({}, BUILD_CONSTANTS, {
|
new webpack.DefinePlugin(BUILD_CONSTANTS),
|
||||||
INLINE: "true"
|
|
||||||
})),
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: "cyberchef.htm",
|
filename: "index.html",
|
||||||
template: "./src/web/html/index.html",
|
template: "./src/web/html/index.html",
|
||||||
|
chunks: ["main"],
|
||||||
|
compileYear: compileYear,
|
||||||
compileTime: compileTime,
|
compileTime: compileTime,
|
||||||
version: pkg.version + "s",
|
version: pkg.version,
|
||||||
inline: true,
|
})
|
||||||
minify: {
|
|
||||||
removeComments: true,
|
|
||||||
collapseWhitespace: true,
|
|
||||||
minifyJS: true,
|
|
||||||
minifyCSS: true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
mode: "production",
|
|
||||||
target: "node",
|
|
||||||
entry: "./src/node/index.mjs",
|
|
||||||
externals: [NodeExternals()],
|
|
||||||
output: {
|
|
||||||
filename: "CyberChef.js",
|
|
||||||
path: __dirname + "/build/node",
|
|
||||||
library: "CyberChef",
|
|
||||||
libraryTarget: "commonjs2"
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.DefinePlugin(BUILD_CONSTANTS)
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"webpack-dev-server": {
|
zip: {
|
||||||
options: {
|
standalone: {
|
||||||
webpack: webpackConfig,
|
cwd: "build/prod/",
|
||||||
host: "0.0.0.0",
|
src: [
|
||||||
disableHostCheck: true,
|
"build/prod/**/*",
|
||||||
overlay: true,
|
"!build/prod/index.html",
|
||||||
inline: false,
|
"!build/prod/BundleAnalyzerReport.html",
|
||||||
clientLogLevel: "error",
|
],
|
||||||
stats: {
|
dest: `build/prod/${downloadZipFilename}`
|
||||||
children: false,
|
|
||||||
chunks: false,
|
|
||||||
modules: false,
|
|
||||||
entrypoints: false,
|
|
||||||
warningsFilter: [
|
|
||||||
/source-map/,
|
|
||||||
/dependency is an expression/,
|
|
||||||
/export 'default'/
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
start: {
|
|
||||||
webpack: {
|
|
||||||
mode: "development",
|
|
||||||
target: "web",
|
|
||||||
entry: Object.assign({
|
|
||||||
main: "./src/web/index.js"
|
|
||||||
}, moduleEntryPoints),
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"./config/modules/OpModules": "./config/modules/Default"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.DefinePlugin(BUILD_CONSTANTS),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: "index.html",
|
|
||||||
template: "./src/web/html/index.html",
|
|
||||||
chunks: ["main"],
|
|
||||||
compileTime: compileTime,
|
|
||||||
version: pkg.version,
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
connect: {
|
connect: {
|
||||||
prod: {
|
prod: {
|
||||||
options: {
|
options: {
|
||||||
port: 8000,
|
port: grunt.option("port") || 8000,
|
||||||
base: "build/prod/"
|
base: "build/prod/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -324,10 +288,16 @@ module.exports = function (grunt) {
|
|||||||
ghPages: {
|
ghPages: {
|
||||||
options: {
|
options: {
|
||||||
process: function (content, srcpath) {
|
process: function (content, srcpath) {
|
||||||
// Add Google Analytics code to index.html
|
|
||||||
if (srcpath.indexOf("index.html") >= 0) {
|
if (srcpath.indexOf("index.html") >= 0) {
|
||||||
|
// Add Google Analytics code to index.html
|
||||||
content = content.replace("</body></html>",
|
content = content.replace("</body></html>",
|
||||||
grunt.file.read("src/web/static/ga.html") + "</body></html>");
|
grunt.file.read("src/web/static/ga.html") + "</body></html>");
|
||||||
|
|
||||||
|
// Add Structured Data for SEO
|
||||||
|
content = content.replace("</head>",
|
||||||
|
"<script type='application/ld+json'>" +
|
||||||
|
JSON.stringify(JSON.parse(grunt.file.read("src/web/static/structuredData.json"))) +
|
||||||
|
"</script></head>");
|
||||||
return grunt.template.process(content, srcpath);
|
return grunt.template.process(content, srcpath);
|
||||||
} else {
|
} else {
|
||||||
return content;
|
return content;
|
||||||
@ -337,14 +307,31 @@ module.exports = function (grunt) {
|
|||||||
},
|
},
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
src: "build/prod/index.html",
|
src: ["build/prod/index.html"],
|
||||||
dest: "build/prod/index.html"
|
dest: "build/prod/index.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
standalone: {
|
||||||
|
options: {
|
||||||
|
process: function (content, srcpath) {
|
||||||
|
if (srcpath.indexOf("index.html") >= 0) {
|
||||||
|
// Replace download link with version number
|
||||||
|
content = content.replace(/<a [^>]+>Download CyberChef.+?<\/a>/,
|
||||||
|
`<span>Version ${pkg.version}</span>`);
|
||||||
|
|
||||||
|
return grunt.template.process(content, srcpath);
|
||||||
|
} else {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
noProcess: ["**", "!**/*.html"]
|
||||||
|
},
|
||||||
|
files: [
|
||||||
{
|
{
|
||||||
expand: true,
|
src: ["build/prod/index.html"],
|
||||||
src: "docs/**",
|
dest: `build/prod/CyberChef_v${pkg.version}.html`
|
||||||
dest: "build/prod/"
|
}
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -354,18 +341,12 @@ module.exports = function (grunt) {
|
|||||||
mode: "755",
|
mode: "755",
|
||||||
},
|
},
|
||||||
src: ["build/**/*", "build/"]
|
src: ["build/**/*", "build/"]
|
||||||
},
|
|
||||||
docs: {
|
|
||||||
options: {
|
|
||||||
mode: "755",
|
|
||||||
},
|
|
||||||
src: ["docs/**/*", "docs/"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
config: {
|
config: {
|
||||||
files: ["src/core/operations/**/*", "!src/core/operations/index.mjs"],
|
files: ["src/core/operations/**/*", "!src/core/operations/index.mjs"],
|
||||||
tasks: ["exec:generateConfig"]
|
tasks: ["exec:generateNodeIndex", "exec:generateConfig"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
concurrent: {
|
concurrent: {
|
||||||
@ -376,35 +357,80 @@ module.exports = function (grunt) {
|
|||||||
},
|
},
|
||||||
exec: {
|
exec: {
|
||||||
repoSize: {
|
repoSize: {
|
||||||
command: [
|
command: chainCommands([
|
||||||
"git ls-files | wc -l | xargs printf '\n%b\ttracked files\n'",
|
"git ls-files | wc -l | xargs printf '\n%b\ttracked files\n'",
|
||||||
"du -hs | egrep -o '^[^\t]*' | xargs printf '%b\trepository size\n'"
|
"du -hs | egrep -o '^[^\t]*' | xargs printf '%b\trepository size\n'"
|
||||||
].join(";"),
|
]),
|
||||||
stderr: false
|
stderr: false
|
||||||
},
|
},
|
||||||
cleanGit: {
|
cleanGit: {
|
||||||
command: "git gc --prune=now --aggressive"
|
command: "git gc --prune=now --aggressive"
|
||||||
},
|
},
|
||||||
sitemap: {
|
sitemap: {
|
||||||
command: "node build/prod/sitemap.js > build/prod/sitemap.xml"
|
command: `node ${nodeFlags} src/web/static/sitemap.mjs > build/prod/sitemap.xml`,
|
||||||
|
sync: true
|
||||||
},
|
},
|
||||||
generateConfig: {
|
generateConfig: {
|
||||||
command: [
|
command: chainCommands([
|
||||||
"echo '\n--- Regenerating config files. ---'",
|
"echo '\n--- Regenerating config files. ---'",
|
||||||
"mkdir -p src/core/config/modules",
|
"echo [] > src/core/config/OperationConfig.json",
|
||||||
"echo 'export default {};\n' > src/core/config/modules/OpModules.mjs",
|
`node ${nodeFlags} src/core/config/scripts/generateHTMLEntities.mjs`,
|
||||||
"echo '[]\n' > src/core/config/OperationConfig.json",
|
`node ${nodeFlags} src/core/config/scripts/generateOpsIndex.mjs`,
|
||||||
"node --experimental-modules --no-warnings --no-deprecation src/core/config/scripts/generateOpsIndex.mjs",
|
`node ${nodeFlags} src/core/config/scripts/generateConfig.mjs`,
|
||||||
"node --experimental-modules --no-warnings --no-deprecation src/core/config/scripts/generateConfig.mjs",
|
|
||||||
"echo '--- Config scripts finished. ---\n'"
|
"echo '--- Config scripts finished. ---\n'"
|
||||||
].join(";")
|
]),
|
||||||
|
sync: true
|
||||||
},
|
},
|
||||||
opTests: {
|
generateNodeIndex: {
|
||||||
command: "node --experimental-modules --no-warnings --no-deprecation tests/operations/index.mjs"
|
command: chainCommands([
|
||||||
|
"echo '\n--- Regenerating node index ---'",
|
||||||
|
`node ${nodeFlags} src/node/config/scripts/generateNodeIndex.mjs`,
|
||||||
|
"echo '--- Node index generated. ---\n'"
|
||||||
|
]),
|
||||||
|
sync: true
|
||||||
},
|
},
|
||||||
browserTests: {
|
browserTests: {
|
||||||
command: "./node_modules/.bin/nightwatch --env prod,inline"
|
command: "./node_modules/.bin/nightwatch --env prod"
|
||||||
}
|
},
|
||||||
|
setupNodeConsumers: {
|
||||||
|
command: chainCommands([
|
||||||
|
"echo '\n--- Testing node consumers ---'",
|
||||||
|
"npm link",
|
||||||
|
`mkdir ${nodeConsumerTestPath}`,
|
||||||
|
`cp tests/node/consumers/* ${nodeConsumerTestPath}`,
|
||||||
|
`cd ${nodeConsumerTestPath}`,
|
||||||
|
"npm link cyberchef"
|
||||||
|
]),
|
||||||
|
sync: true
|
||||||
|
},
|
||||||
|
teardownNodeConsumers: {
|
||||||
|
command: chainCommands([
|
||||||
|
`rm -rf ${nodeConsumerTestPath}`,
|
||||||
|
"echo '\n--- Node consumer tests complete ---'"
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
testCJSNodeConsumer: {
|
||||||
|
command: chainCommands([
|
||||||
|
`cd ${nodeConsumerTestPath}`,
|
||||||
|
`node ${nodeFlags} cjs-consumer.js`,
|
||||||
|
]),
|
||||||
|
stdout: false,
|
||||||
|
},
|
||||||
|
testESMNodeConsumer: {
|
||||||
|
command: chainCommands([
|
||||||
|
`cd ${nodeConsumerTestPath}`,
|
||||||
|
`node ${nodeFlags} esm-consumer.mjs`,
|
||||||
|
]),
|
||||||
|
stdout: false,
|
||||||
|
},
|
||||||
|
fixCryptoApiImports: {
|
||||||
|
command: `node ${nodeFlags} src/core/config/scripts/fixCryptoApiImports.mjs`,
|
||||||
|
stdout: false
|
||||||
|
},
|
||||||
|
fixSnackbarMarkup: {
|
||||||
|
command: `node ${nodeFlags} src/core/config/scripts/fixSnackBarMarkup.mjs`,
|
||||||
|
stdout: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
82
README.md
82
README.md
@ -1,27 +1,52 @@
|
|||||||
# CyberChef
|
# CyberChef
|
||||||
|
|
||||||
[](https://travis-ci.org/gchq/CyberChef)
|
[](https://github.com/gchq/CyberChef/actions?query=workflow%3A%22Master+Build%2C+Test+%26+Deploy%22)
|
||||||
[](https://david-dm.org/gchq/CyberChef)
|
|
||||||
[](https://www.npmjs.com/package/cyberchef)
|
[](https://www.npmjs.com/package/cyberchef)
|
||||||

|
|
||||||
[](https://github.com/gchq/CyberChef/blob/master/LICENSE)
|
[](https://github.com/gchq/CyberChef/blob/master/LICENSE)
|
||||||
[](https://gitter.im/gchq/CyberChef?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
[](https://gitter.im/gchq/CyberChef?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
|
|
||||||
|
|
||||||
#### *The Cyber Swiss Army Knife*
|
#### *The Cyber Swiss Army Knife*
|
||||||
|
|
||||||
CyberChef is a simple, intuitive web app for carrying out all manner of "cyber" operations within a web browser. These operations include simple encoding like XOR or Base64, more complex encryption like AES, DES and Blowfish, creating binary and hexdumps, compression and decompression of data, calculating hashes and checksums, IPv6 and X.509 parsing, changing character encodings, and much more.
|
CyberChef is a simple, intuitive web app for carrying out all manner of "cyber" operations within a web browser. These operations include simple encoding like XOR and Base64, more complex encryption like AES, DES and Blowfish, creating binary and hexdumps, compression and decompression of data, calculating hashes and checksums, IPv6 and X.509 parsing, changing character encodings, and much more.
|
||||||
|
|
||||||
The tool is designed to enable both technical and non-technical analysts to manipulate data in complex ways without having to deal with complex tools or algorithms. It was conceived, designed, built and incrementally improved by an analyst in their 10% innovation time over several years.
|
The tool is designed to enable both technical and non-technical analysts to manipulate data in complex ways without having to deal with complex tools or algorithms. It was conceived, designed, built and incrementally improved by an analyst in their 10% innovation time over several years.
|
||||||
|
|
||||||
## Live demo
|
## Official website
|
||||||
|
|
||||||
CyberChef is still under active development. As a result, it shouldn't be considered a finished product. There is still testing and bug fixing to do, new features to be added and additional documentation to write. Please contribute!
|
[CyberChef's official website can be found here][1] - have fun!
|
||||||
|
|
||||||
Cryptographic operations in CyberChef should not be relied upon to provide security in any situation. No guarantee is offered for their correctness.
|
## Running Locally with Docker
|
||||||
|
|
||||||
[A live demo can be found here][1] - have fun!
|
**Prerequisites**
|
||||||
|
|
||||||
|
- [Docker](https://www.docker.com/products/docker-desktop/)
|
||||||
|
- Docker Desktop must be open and running on your machine
|
||||||
|
|
||||||
|
|
||||||
|
#### Option 1: Build the Docker Image Yourself
|
||||||
|
|
||||||
|
1. Build the docker image
|
||||||
|
```bash
|
||||||
|
docker build --tag cyberchef --ulimit nofile=10000 .
|
||||||
|
```
|
||||||
|
2. Run the docker container
|
||||||
|
```bash
|
||||||
|
docker run -it -p 8080:8080 cyberchef
|
||||||
|
```
|
||||||
|
3. Navigate to `http://localhost:8080` in your browser
|
||||||
|
|
||||||
|
#### Option 2: Use the pre-built Docker Image
|
||||||
|
|
||||||
|
If you prefer to skip the build process, you can use the pre-built image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -it -p 8080:8080 ghcr.io/gchq/cyberchef:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Just like before, navigate to `http://localhost:8080` in your browser.
|
||||||
|
|
||||||
|
This image is built and published through our [GitHub Workflows](.github/workflows/releases.yml)
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
@ -43,6 +68,7 @@ You can use as many operations as you like in simple or complex ways. Some examp
|
|||||||
- [Carry out different operations on data of different types][8]
|
- [Carry out different operations on data of different types][8]
|
||||||
- [Use parts of the input as arguments to operations][9]
|
- [Use parts of the input as arguments to operations][9]
|
||||||
- [Perform AES decryption, extracting the IV from the beginning of the cipher stream][10]
|
- [Perform AES decryption, extracting the IV from the beginning of the cipher stream][10]
|
||||||
|
- [A simpler way to perform the same AES Decryption][13]
|
||||||
- [Automagically detect several layers of nested encoding][12]
|
- [Automagically detect several layers of nested encoding][12]
|
||||||
|
|
||||||
|
|
||||||
@ -50,12 +76,12 @@ You can use as many operations as you like in simple or complex ways. Some examp
|
|||||||
|
|
||||||
- Drag and drop
|
- Drag and drop
|
||||||
- Operations can be dragged in and out of the recipe list, or reorganised.
|
- Operations can be dragged in and out of the recipe list, or reorganised.
|
||||||
- Files up to 500MB can be dragged over the input box to load them directly into the browser.
|
- Files up to 2GB can be dragged over the input box to load them directly into the browser.
|
||||||
- Auto Bake
|
- Auto Bake
|
||||||
- Whenever you modify the input or the recipe, CyberChef will automatically "bake" for you and produce the output immediately.
|
- Whenever you modify the input or the recipe, CyberChef will automatically "bake" for you and produce the output immediately.
|
||||||
- This can be turned off and operated manually if it is affecting performance (if the input is very large, for instance).
|
- This can be turned off and operated manually if it is affecting performance (if the input is very large, for instance).
|
||||||
- Automated encoding detection
|
- Automated encoding detection
|
||||||
- CyberChef uses [a number of techniques](https://github.com/gchq/CyberChef/wiki/Automatic-detection-of-encoded-data-using-CyberChef-Magic) to attempt to automatically detect which encodings your data is under. If it finds a suitable operation which can make sense of your data, it displays the 'magic' icon in the Output field which you can click to decode your data.
|
- CyberChef uses [a number of techniques](https://github.com/gchq/CyberChef/wiki/Automatic-detection-of-encoded-data-using-CyberChef-Magic) to attempt to automatically detect which encodings your data is under. If it finds a suitable operation that make sense of your data, it displays the 'magic' icon in the Output field which you can click to decode your data.
|
||||||
- Breakpoints
|
- Breakpoints
|
||||||
- You can set breakpoints on any operation in your recipe to pause execution before running it.
|
- You can set breakpoints on any operation in your recipe to pause execution before running it.
|
||||||
- You can also step through the recipe one operation at a time to see what the data looks like at each stage.
|
- You can also step through the recipe one operation at a time to see what the data looks like at each stage.
|
||||||
@ -67,26 +93,43 @@ You can use as many operations as you like in simple or complex ways. Some examp
|
|||||||
- Highlighting
|
- Highlighting
|
||||||
- When you highlight text in the input or output, the offset and length values will be displayed and, if possible, the corresponding data will be highlighted in the output or input respectively (example: [highlight the word 'question' in the input to see where it appears in the output][11]).
|
- When you highlight text in the input or output, the offset and length values will be displayed and, if possible, the corresponding data will be highlighted in the output or input respectively (example: [highlight the word 'question' in the input to see where it appears in the output][11]).
|
||||||
- Save to file and load from file
|
- Save to file and load from file
|
||||||
- You can save the output to a file at any time or load a file by dragging and dropping it into the input field. Files up to around 500MB are supported (depending on your browser), however some operations may take a very long time to run over this much data.
|
- You can save the output to a file at any time or load a file by dragging and dropping it into the input field. Files up to around 2GB are supported (depending on your browser), however, some operations may take a very long time to run over this much data.
|
||||||
- CyberChef is entirely client-side
|
- CyberChef is entirely client-side
|
||||||
- It should be noted that none of your recipe configuration or input (either text or files) is ever sent to the CyberChef web server - all processing is carried out within your browser, on your own computer.
|
- It should be noted that none of your recipe configuration or input (either text or files) is ever sent to the CyberChef web server - all processing is carried out within your browser, on your own computer.
|
||||||
- Due to this feature, CyberChef can be compiled into a single HTML file. You can download this file and drop it into a virtual machine, share it with other people, or use it independently on your local machine.
|
- Due to this feature, CyberChef can be downloaded and run locally. You can use the link in the top left corner of the app to download a full copy of CyberChef and drop it into a virtual machine, share it with other people, or host it in a closed network.
|
||||||
|
|
||||||
|
|
||||||
|
## Deep linking
|
||||||
|
|
||||||
|
By manipulating CyberChef's URL hash, you can change the initial settings with which the page opens.
|
||||||
|
The format is `https://gchq.github.io/CyberChef/#recipe=Operation()&input=...`
|
||||||
|
|
||||||
|
Supported arguments are `recipe`, `input` (encoded in Base64), and `theme`.
|
||||||
|
|
||||||
|
|
||||||
## Browser support
|
## Browser support
|
||||||
|
|
||||||
CyberChef is built to support
|
CyberChef is built to support
|
||||||
|
|
||||||
- Google Chrome 40+
|
- Google Chrome 50+
|
||||||
- Mozilla Firefox 35+
|
- Mozilla Firefox 38+
|
||||||
- Microsoft Edge 14+
|
|
||||||
|
|
||||||
|
## Node.js support
|
||||||
|
|
||||||
|
CyberChef is built to fully support Node.js `v24`. For more information, see the ["Node API" wiki page](https://github.com/gchq/CyberChef/wiki/Node-API)
|
||||||
|
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Please see the [CyberChef security policy](./SECURITY.md).
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributing a new operation to CyberChef is super easy! There is a quickstart script which will walk you through the process. If you can write basic JavaScript, you can write a CyberChef operation.
|
Contributing a new operation to CyberChef is super easy! The quickstart script will walk you through the process. If you can write basic JavaScript, you can write a CyberChef operation.
|
||||||
|
|
||||||
An installation walkthrough, how-to guides for adding new operations and themes, descriptions of the repository structure, available data types and coding conventions can all be found in the project [wiki pages](https://github.com/gchq/CyberChef/wiki).
|
An installation walkthrough, how-to guides for adding new operations and themes, descriptions of the repository structure, available data types and coding conventions can all be found in the ["Contributing" wiki page](https://github.com/gchq/CyberChef/wiki/Contributing).
|
||||||
|
|
||||||
- Push your changes to your fork.
|
- Push your changes to your fork.
|
||||||
- Submit a pull request. If you are doing this for the first time, you will be prompted to sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef) via the CLA assistant on the pull request. This will also ask whether you are happy for GCHQ to contact you about a token of thanks for your contribution, or about job opportunities at GCHQ.
|
- Submit a pull request. If you are doing this for the first time, you will be prompted to sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef) via the CLA assistant on the pull request. This will also ask whether you are happy for GCHQ to contact you about a token of thanks for your contribution, or about job opportunities at GCHQ.
|
||||||
@ -94,7 +137,7 @@ An installation walkthrough, how-to guides for adding new operations and themes,
|
|||||||
|
|
||||||
## Licencing
|
## Licencing
|
||||||
|
|
||||||
CyberChef is released under the [Apache 2.0 Licence](https://www.apache.org/licenses/LICENSE-2.0) and is covered by [Crown Copyright](https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/copyright-and-re-use/crown-copyright/).
|
CyberChef is released under the [Apache 2.0 Licence](https://www.apache.org/licenses/LICENSE-2.0) and is covered by [Crown Copyright](https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/).
|
||||||
|
|
||||||
|
|
||||||
[1]: https://gchq.github.io/CyberChef
|
[1]: https://gchq.github.io/CyberChef
|
||||||
@ -106,6 +149,7 @@ CyberChef is released under the [Apache 2.0 Licence](https://www.apache.org/lice
|
|||||||
[7]: https://gchq.github.io/CyberChef/#recipe=Fork('%5C%5Cn','%5C%5Cn',false)From_UNIX_Timestamp('Seconds%20(s)')&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA
|
[7]: https://gchq.github.io/CyberChef/#recipe=Fork('%5C%5Cn','%5C%5Cn',false)From_UNIX_Timestamp('Seconds%20(s)')&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA
|
||||||
[8]: https://gchq.github.io/CyberChef/#recipe=Fork('%5C%5Cn','%5C%5Cn',false)Conditional_Jump('1',false,'base64',10)To_Hex('Space')Return()Label('base64')To_Base64('A-Za-z0-9%2B/%3D')&input=U29tZSBkYXRhIHdpdGggYSAxIGluIGl0ClNvbWUgZGF0YSB3aXRoIGEgMiBpbiBpdA
|
[8]: https://gchq.github.io/CyberChef/#recipe=Fork('%5C%5Cn','%5C%5Cn',false)Conditional_Jump('1',false,'base64',10)To_Hex('Space')Return()Label('base64')To_Base64('A-Za-z0-9%2B/%3D')&input=U29tZSBkYXRhIHdpdGggYSAxIGluIGl0ClNvbWUgZGF0YSB3aXRoIGEgMiBpbiBpdA
|
||||||
[9]: https://gchq.github.io/CyberChef/#recipe=Register('key%3D(%5B%5C%5Cda-f%5D*)',true,false)Find_/_Replace(%7B'option':'Regex','string':'.*data%3D(.*)'%7D,'$1',true,false,true)RC4(%7B'option':'Hex','string':'$R0'%7D,'Hex','Latin1')&input=aHR0cDovL21hbHdhcmV6LmJpei9iZWFjb24ucGhwP2tleT0wZTkzMmE1YyZkYXRhPThkYjdkNWViZTM4NjYzYTU0ZWNiYjMzNGUzZGIxMQ
|
[9]: https://gchq.github.io/CyberChef/#recipe=Register('key%3D(%5B%5C%5Cda-f%5D*)',true,false)Find_/_Replace(%7B'option':'Regex','string':'.*data%3D(.*)'%7D,'$1',true,false,true)RC4(%7B'option':'Hex','string':'$R0'%7D,'Hex','Latin1')&input=aHR0cDovL21hbHdhcmV6LmJpei9iZWFjb24ucGhwP2tleT0wZTkzMmE1YyZkYXRhPThkYjdkNWViZTM4NjYzYTU0ZWNiYjMzNGUzZGIxMQ
|
||||||
[10]: https://gchq.github.io/CyberChef/#recipe=Register('(.%7B32%7D)',true,false)Drop_bytes(0,32,false)AES_Decrypt(%7B'option':'Hex','string':'1748e7179bd56570d51fa4ba287cc3e5'%7D,%7B'option':'Hex','string':'$R0'%7D,'CTR','Hex','Raw',%7B'option':'Hex','string':''%7D)&input=NTFlMjAxZDQ2MzY5OGVmNWY3MTdmNzFmNWI0NzEyYWYyMGJlNjc0YjNiZmY1M2QzODU0NjM5NmVlNjFkYWFjNDkwOGUzMTljYTNmY2Y3MDg5YmZiNmIzOGVhOTllNzgxZDI2ZTU3N2JhOWRkNmYzMTFhMzk0MjBiODk3OGU5MzAxNGIwNDJkNDQ3MjZjYWVkZjU0MzZlYWY2NTI0MjljMGRmOTRiNTIxNjc2YzdjMmNlODEyMDk3YzI3NzI3M2M3YzcyY2Q4OWFlYzhkOWZiNGEyNzU4NmNjZjZhYTBhZWUyMjRjMzRiYTNiZmRmN2FlYjFkZGQ0Nzc2MjJiOTFlNzJjOWU3MDlhYjYwZjhkYWY3MzFlYzBjYzg1Y2UwZjc0NmZmMTU1NGE1YTNlYzI5MWNhNDBmOWU2MjlhODcyNTkyZDk4OGZkZDgzNDUzNGFiYTc5YzFhZDE2NzY3NjlhN2MwMTBiZjA0NzM5ZWNkYjY1ZDk1MzAyMzcxZDYyOWQ5ZTM3ZTdiNGEzNjFkYTQ2OGYxZWQ1MzU4OTIyZDJlYTc1MmRkMTFjMzY2ZjMwMTdiMTRhYTAxMWQyYWYwM2M0NGY5NTU3OTA5OGExNWUzY2Y5YjQ0ODZmOGZmZTljMjM5ZjM0ZGU3MTUxZjZjYTY1MDBmZTRiODUwYzNmMWMwMmU4MDFjYWYzYTI0NDY0NjE0ZTQyODAxNjE1YjhmZmFhMDdhYzgyNTE0OTNmZmRhN2RlNWRkZjMzNjg4ODBjMmI5NWIwMzBmNDFmOGYxNTA2NmFkZDA3MWE2NmNmNjBlNWY0NmYzYTIzMGQzOTdiNjUyOTYzYTIxYTUzZg
|
[10]: https://gchq.github.io/CyberChef/#recipe=Register('(.%7B32%7D)',true,false,false)Drop_bytes(0,32,false)AES_Decrypt(%7B'option':'Hex','string':'1748e7179bd56570d51fa4ba287cc3e5'%7D,%7B'option':'Hex','string':'$R0'%7D,16,'CTR','Hex','Raw',%7B'option':'Hex','string':''%7D,%7B'option':'Hex','string':''%7D,'Off')&input=NTFlMjAxZDQ2MzY5OGVmNWY3MTdmNzFmNWI0NzEyYWYyMGJlNjc0YjNiZmY1M2QzODU0NjM5NmVlNjFkYWFjNDkwOGUzMTljYTNmY2Y3MDg5YmZiNmIzOGVhOTllNzgxZDI2ZTU3N2JhOWRkNmYzMTFhMzk0MjBiODk3OGU5MzAxNGIwNDJkNDQ3MjZjYWVkZjU0MzZlYWY2NTI0MjljMGRmOTRiNTIxNjc2YzdjMmNlODEyMDk3YzI3NzI3M2M3YzcyY2Q4OWFlYzhkOWZiNGEyNzU4NmNjZjZhYTBhZWUyMjRjMzRiYTNiZmRmN2FlYjFkZGQ0Nzc2MjJiOTFlNzJjOWU3MDlhYjYwZjhkYWY3MzFlYzBjYzg1Y2UwZjc0NmZmMTU1NGE1YTNlYzI5MWNhNDBmOWU2MjlhODcyNTkyZDk4OGZkZDgzNDUzNGFiYTc5YzFhZDE2NzY3NjlhN2MwMTBiZjA0NzM5ZWNkYjY1ZDk1MzAyMzcxZDYyOWQ5ZTM3ZTdiNGEzNjFkYTQ2OGYxZWQ1MzU4OTIyZDJlYTc1MmRkMTFjMzY2ZjMwMTdiMTRhYTAxMWQyYWYwM2M0NGY5NTU3OTA5OGExNWUzY2Y5YjQ0ODZmOGZmZTljMjM5ZjM0ZGU3MTUxZjZjYTY1MDBmZTRiODUwYzNmMWMwMmU4MDFjYWYzYTI0NDY0NjE0ZTQyODAxNjE1YjhmZmFhMDdhYzgyNTE0OTNmZmRhN2RlNWRkZjMzNjg4ODBjMmI5NWIwMzBmNDFmOGYxNTA2NmFkZDA3MWE2NmNmNjBlNWY0NmYzYTIzMGQzOTdiNjUyOTYzYTIxYTUzZg
|
||||||
[11]: https://gchq.github.io/CyberChef/#recipe=XOR(%7B'option':'Hex','string':'3a'%7D,'Standard',false)To_Hexdump(16,false,false)&input=VGhlIGFuc3dlciB0byB0aGUgdWx0aW1hdGUgcXVlc3Rpb24gb2YgbGlmZSwgdGhlIFVuaXZlcnNlLCBhbmQgZXZlcnl0aGluZyBpcyA0Mi4
|
[11]: https://gchq.github.io/CyberChef/#recipe=XOR(%7B'option':'Hex','string':'3a'%7D,'Standard',false)To_Hexdump(16,false,false)&input=VGhlIGFuc3dlciB0byB0aGUgdWx0aW1hdGUgcXVlc3Rpb24gb2YgbGlmZSwgdGhlIFVuaXZlcnNlLCBhbmQgZXZlcnl0aGluZyBpcyA0Mi4
|
||||||
[12]: https://gchq.github.io/CyberChef/#recipe=Magic(3,false,false)&input=V1VhZ3dzaWFlNm1QOGdOdENDTFVGcENwQ0IyNlJtQkRvREQ4UGFjZEFtekF6QlZqa0syUXN0RlhhS2hwQzZpVVM3UkhxWHJKdEZpc29SU2dvSjR3aGptMWFybTg2NHFhTnE0UmNmVW1MSHJjc0FhWmM1VFhDWWlmTmRnUzgzZ0RlZWpHWDQ2Z2FpTXl1QlY2RXNrSHQxc2NnSjg4eDJ0TlNvdFFEd2JHWTFtbUNvYjJBUkdGdkNLWU5xaU45aXBNcTFaVTFtZ2tkYk51R2NiNzZhUnRZV2hDR1VjOGc5M1VKdWRoYjhodHNoZVpud1RwZ3FoeDgzU1ZKU1pYTVhVakpUMnptcEM3dVhXdHVtcW9rYmRTaTg4WXRrV0RBYzFUb291aDJvSDRENGRkbU5LSldVRHBNd21uZ1VtSzE0eHdtb21jY1BRRTloTTE3MkFQblNxd3hkS1ExNzJSa2NBc3lzbm1qNWdHdFJtVk5OaDJzMzU5d3I2bVMyUVJQ
|
[12]: https://gchq.github.io/CyberChef/#recipe=Magic(3,false,false)&input=V1VhZ3dzaWFlNm1QOGdOdENDTFVGcENwQ0IyNlJtQkRvREQ4UGFjZEFtekF6QlZqa0syUXN0RlhhS2hwQzZpVVM3UkhxWHJKdEZpc29SU2dvSjR3aGptMWFybTg2NHFhTnE0UmNmVW1MSHJjc0FhWmM1VFhDWWlmTmRnUzgzZ0RlZWpHWDQ2Z2FpTXl1QlY2RXNrSHQxc2NnSjg4eDJ0TlNvdFFEd2JHWTFtbUNvYjJBUkdGdkNLWU5xaU45aXBNcTFaVTFtZ2tkYk51R2NiNzZhUnRZV2hDR1VjOGc5M1VKdWRoYjhodHNoZVpud1RwZ3FoeDgzU1ZKU1pYTVhVakpUMnptcEM3dVhXdHVtcW9rYmRTaTg4WXRrV0RBYzFUb291aDJvSDRENGRkbU5LSldVRHBNd21uZ1VtSzE0eHdtb21jY1BRRTloTTE3MkFQblNxd3hkS1ExNzJSa2NBc3lzbm1qNWdHdFJtVk5OaDJzMzU5d3I2bVMyUVJQ
|
||||||
|
[13]: https://gchq.github.io/CyberChef/#recipe=AES_Decrypt(%7B'option':'Hex','string':'1748e7179bd56570d51fa4ba287cc3e5'%7D,%7B'option':'Hex','string':'$R0'%7D,16,'CTR','Hex','Raw',%7B'option':'Hex','string':''%7D,%7B'option':'Hex','string':''%7D,'From%20start')&input=NTFlMjAxZDQ2MzY5OGVmNWY3MTdmNzFmNWI0NzEyYWYyMGJlNjc0YjNiZmY1M2QzODU0NjM5NmVlNjFkYWFjNDkwOGUzMTljYTNmY2Y3MDg5YmZiNmIzOGVhOTllNzgxZDI2ZTU3N2JhOWRkNmYzMTFhMzk0MjBiODk3OGU5MzAxNGIwNDJkNDQ3MjZjYWVkZjU0MzZlYWY2NTI0MjljMGRmOTRiNTIxNjc2YzdjMmNlODEyMDk3YzI3NzI3M2M3YzcyY2Q4OWFlYzhkOWZiNGEyNzU4NmNjZjZhYTBhZWUyMjRjMzRiYTNiZmRmN2FlYjFkZGQ0Nzc2MjJiOTFlNzJjOWU3MDlhYjYwZjhkYWY3MzFlYzBjYzg1Y2UwZjc0NmZmMTU1NGE1YTNlYzI5MWNhNDBmOWU2MjlhODcyNTkyZDk4OGZkZDgzNDUzNGFiYTc5YzFhZDE2NzY3NjlhN2MwMTBiZjA0NzM5ZWNkYjY1ZDk1MzAyMzcxZDYyOWQ5ZTM3ZTdiNGEzNjFkYTQ2OGYxZWQ1MzU4OTIyZDJlYTc1MmRkMTFjMzY2ZjMwMTdiMTRhYTAxMWQyYWYwM2M0NGY5NTU3OTA5OGExNWUzY2Y5YjQ0ODZmOGZmZTljMjM5ZjM0ZGU3MTUxZjZjYTY1MDBmZTRiODUwYzNmMWMwMmU4MDFjYWYzYTI0NDY0NjE0ZTQyODAxNjE1YjhmZmFhMDdhYzgyNTE0OTNmZmRhN2RlNWRkZjMzNjg4ODBjMmI5NWIwMzBmNDFmOGYxNTA2NmFkZDA3MWE2NmNmNjBlNWY0NmYzYTIzMGQzOTdiNjUyOTYzYTIxYTUzZg
|
||||||
|
|||||||
18
SECURITY.md
Normal file
18
SECURITY.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
CyberChef is supported on a best endeavours basis.
|
||||||
|
Patches will be applied to the latest version rather than retroactively to older versions.
|
||||||
|
To ensure you are using the most secure version of CyberChef, please make sure you have the [latest release](https://github.com/gchq/CyberChef/releases/latest). [The official website](https://gchq.github.io/CyberChef/) is always up to date.
|
||||||
|
|
||||||
|
No guarantee is offered for the correctness or security of CyberChef. In paticular, the security of cryptographic operations should not be relied upon.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a vulnerability in CyberChef, please do not publicly disclose it, and do not create a GitHub issue.
|
||||||
|
|
||||||
|
Instead, send an email as soon as possible to [CyberChefSecurity@gchq.gov.uk](mailto:CyberChefSecurity@gchq.gov.uk).
|
||||||
|
The report will be acknowledged and actioned urgently by the CyberChef maintainers.
|
||||||
|
|
||||||
|
If you do not receive a timely acknowledgement, please notify [oss@gchq.gov.uk](mailto:oss@gchq.gov.uk) and [CyberChef@gchq.gov.uk](mailto:CyberChef@gchq.gov.uk) of your vulnerability report.
|
||||||
@ -4,20 +4,21 @@ module.exports = function(api) {
|
|||||||
return {
|
return {
|
||||||
"presets": [
|
"presets": [
|
||||||
["@babel/preset-env", {
|
["@babel/preset-env", {
|
||||||
"targets": {
|
|
||||||
"chrome": 40,
|
|
||||||
"firefox": 35,
|
|
||||||
"edge": 14,
|
|
||||||
"node": "6.5"
|
|
||||||
},
|
|
||||||
"modules": false,
|
"modules": false,
|
||||||
"useBuiltIns": "entry"
|
"useBuiltIns": "entry",
|
||||||
|
"corejs": 3
|
||||||
}]
|
}]
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
["babel-plugin-transform-builtin-extend", {
|
"@babel/plugin-syntax-import-assertions",
|
||||||
"globals": ["Error"]
|
[
|
||||||
}]
|
"@babel/plugin-transform-runtime", {
|
||||||
]
|
"regenerator": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"generatorOpts": {
|
||||||
|
"importAttributesKeyword": "with"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
BIN
docs/favicon.ico
BIN
docs/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"tags": {
|
|
||||||
"allowUnknownTags": true
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"plugins/markdown",
|
|
||||||
"node_modules/jsdoc-babel"
|
|
||||||
],
|
|
||||||
"templates": {
|
|
||||||
"systemName": "CyberChef",
|
|
||||||
"footer": "",
|
|
||||||
"copyright": "© Crown Copyright 2017",
|
|
||||||
"navType": "inline",
|
|
||||||
"theme": "cerulean",
|
|
||||||
"linenums": true,
|
|
||||||
"collapseSymbols": false,
|
|
||||||
"inverseNav": true,
|
|
||||||
"outputSourceFiles": true,
|
|
||||||
"outputSourcePath": true,
|
|
||||||
"dateFormat": "ddd MMM Do YYYY",
|
|
||||||
"sort": false,
|
|
||||||
"logoFile": "cyberchef-32x32.png",
|
|
||||||
"cleverLinks": false,
|
|
||||||
"monospaceLinks": false,
|
|
||||||
"protocol": "html://",
|
|
||||||
"methodHeadingReturns": false
|
|
||||||
},
|
|
||||||
"markdown": {
|
|
||||||
"parser": "gfm",
|
|
||||||
"hardwrap": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
129
eslint.config.mjs
Executable file
129
eslint.config.mjs
Executable file
@ -0,0 +1,129 @@
|
|||||||
|
import babelParser from "@babel/eslint-parser";
|
||||||
|
import jsdoc from "eslint-plugin-jsdoc";
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
parser: babelParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
ecmaFeatures: {
|
||||||
|
impliedStrict: true
|
||||||
|
},
|
||||||
|
sourceType: "module",
|
||||||
|
allowImportExportEverywhere: true
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
...globals.es6,
|
||||||
|
"$": false,
|
||||||
|
"jQuery": false,
|
||||||
|
"log": false,
|
||||||
|
"app": false,
|
||||||
|
|
||||||
|
"COMPILE_TIME": false,
|
||||||
|
"COMPILE_MSG": false,
|
||||||
|
"PKG_VERSION": false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ignores: ["src/core/vendor/**"],
|
||||||
|
plugins: {
|
||||||
|
jsdoc
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// enable additional rules
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-implied-eval": "error",
|
||||||
|
"dot-notation": "error",
|
||||||
|
"eqeqeq": ["error", "smart"],
|
||||||
|
"no-caller": "error",
|
||||||
|
"no-extra-bind": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"no-useless-call": "error",
|
||||||
|
"no-useless-return": "error",
|
||||||
|
"radix": "warn",
|
||||||
|
|
||||||
|
// modify rules from base configurations
|
||||||
|
"no-unused-vars": ["error", {
|
||||||
|
"args": "none",
|
||||||
|
"vars": "all",
|
||||||
|
"caughtErrors": "none"
|
||||||
|
}],
|
||||||
|
"no-empty": ["error", {
|
||||||
|
"allowEmptyCatch": true
|
||||||
|
}],
|
||||||
|
|
||||||
|
// disable rules from base configurations
|
||||||
|
"no-control-regex": "off",
|
||||||
|
"require-atomic-updates": "off",
|
||||||
|
"no-async-promise-executor": "off",
|
||||||
|
|
||||||
|
// stylistic conventions
|
||||||
|
"brace-style": ["error", "1tbs"],
|
||||||
|
"space-before-blocks": ["error", "always"],
|
||||||
|
"block-spacing": "error",
|
||||||
|
"array-bracket-spacing": "error",
|
||||||
|
"comma-spacing": "error",
|
||||||
|
"spaced-comment": ["error", "always", { "exceptions": ["/"] }],
|
||||||
|
"comma-style": "error",
|
||||||
|
"computed-property-spacing": "error",
|
||||||
|
"no-trailing-spaces": "warn",
|
||||||
|
"eol-last": "error",
|
||||||
|
"func-call-spacing": "error",
|
||||||
|
"key-spacing": ["warn", {
|
||||||
|
"mode": "minimum"
|
||||||
|
}],
|
||||||
|
"indent": ["error", 4, {
|
||||||
|
"ignoreComments": true,
|
||||||
|
"ArrayExpression": "first",
|
||||||
|
"SwitchCase": 1
|
||||||
|
}],
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
|
"quotes": ["error", "double", {
|
||||||
|
"avoidEscape": true,
|
||||||
|
"allowTemplateLiterals": true
|
||||||
|
}],
|
||||||
|
"camelcase": ["error", {
|
||||||
|
"properties": "always"
|
||||||
|
}],
|
||||||
|
"semi": ["error", "always"],
|
||||||
|
"unicode-bom": "error",
|
||||||
|
"jsdoc/require-jsdoc": ["error", {
|
||||||
|
"require": {
|
||||||
|
"FunctionDeclaration": true,
|
||||||
|
"MethodDefinition": true,
|
||||||
|
"ClassDeclaration": true,
|
||||||
|
"ArrowFunctionExpression": false
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"keyword-spacing": ["error", {
|
||||||
|
"before": true,
|
||||||
|
"after": true
|
||||||
|
}],
|
||||||
|
"no-multiple-empty-lines": ["warn", {
|
||||||
|
"max": 2,
|
||||||
|
"maxEOF": 1,
|
||||||
|
"maxBOF": 0
|
||||||
|
}],
|
||||||
|
"no-whitespace-before-property": "error",
|
||||||
|
"operator-linebreak": ["error", "after"],
|
||||||
|
"space-in-parens": "error",
|
||||||
|
"no-var": "error",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"no-console": "error"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// File-pattern specific overrides
|
||||||
|
{
|
||||||
|
files: ["tests/**/*"],
|
||||||
|
rules: {
|
||||||
|
"no-unused-expressions": "off",
|
||||||
|
"no-console": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"src_folders": ["tests/browser"],
|
"src_folders": ["tests/browser"],
|
||||||
|
"exclude": ["tests/browser/browserUtils.js"],
|
||||||
"output_folder": "tests/browser/output",
|
"output_folder": "tests/browser/output",
|
||||||
|
|
||||||
"test_settings": {
|
"test_settings": {
|
||||||
@ -10,11 +11,12 @@
|
|||||||
"start_process": true,
|
"start_process": true,
|
||||||
"server_path": "./node_modules/.bin/chromedriver",
|
"server_path": "./node_modules/.bin/chromedriver",
|
||||||
"port": 9515,
|
"port": 9515,
|
||||||
"log_path": false
|
"log_path": "tests/browser/output"
|
||||||
},
|
},
|
||||||
"desiredCapabilities": {
|
"desiredCapabilities": {
|
||||||
"browserName": "chrome"
|
"browserName": "chrome"
|
||||||
}
|
},
|
||||||
|
"enable_fail_fast": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"dev": {
|
"dev": {
|
||||||
@ -23,10 +25,6 @@
|
|||||||
|
|
||||||
"prod": {
|
"prod": {
|
||||||
"launch_url": "http://localhost:8000/index.html"
|
"launch_url": "http://localhost:8000/index.html"
|
||||||
},
|
|
||||||
|
|
||||||
"inline": {
|
|
||||||
"launch_url": "http://localhost:8000/cyberchef.htm"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
29524
package-lock.json
generated
29524
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
269
package.json
269
package.json
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "cyberchef",
|
"name": "cyberchef",
|
||||||
"version": "8.21.0",
|
"version": "11.3.0",
|
||||||
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
|
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
|
||||||
"author": "n1474335 <n1474335@gmail.com>",
|
"author": "GCHQ <CyberChef@gchq.gov.uk>",
|
||||||
"homepage": "https://gchq.github.io/CyberChef",
|
"homepage": "https://gchq.github.io/CyberChef",
|
||||||
"copyright": "Crown copyright 2016",
|
"copyright": "Crown copyright 2016",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@ -27,118 +27,201 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/gchq/CyberChef/"
|
"url": "https://github.com/gchq/CyberChef/"
|
||||||
},
|
},
|
||||||
"main": "build/node/CyberChef.js",
|
"main": "src/node/wrapper.js",
|
||||||
|
"exports": {
|
||||||
|
"import": "./src/node/index.mjs",
|
||||||
|
"require": "./src/node/wrapper.js"
|
||||||
|
},
|
||||||
"bugs": "https://github.com/gchq/CyberChef/issues",
|
"bugs": "https://github.com/gchq/CyberChef/issues",
|
||||||
|
"browserslist": [
|
||||||
|
"Chrome >= 50",
|
||||||
|
"Firefox >= 38",
|
||||||
|
"node >= 24"
|
||||||
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.2.2",
|
"@babel/eslint-parser": "^7.29.7",
|
||||||
"@babel/preset-env": "^7.2.3",
|
"@babel/plugin-syntax-import-assertions": "^7.28.6",
|
||||||
"autoprefixer": "^9.4.3",
|
"@babel/plugin-transform-runtime": "^7.29.7",
|
||||||
"babel-loader": "^8.0.4",
|
"@babel/preset-env": "^7.29.7",
|
||||||
"bootstrap": "^4.2.1",
|
"@babel/runtime": "^7.29.7",
|
||||||
"chromedriver": "^2.45.0",
|
"@codemirror/commands": "^6.10.4",
|
||||||
"colors": "^1.3.3",
|
"@codemirror/language": "^6.12.4",
|
||||||
"css-loader": "^2.1.0",
|
"@codemirror/search": "^6.7.1",
|
||||||
"eslint": "^5.11.1",
|
"@codemirror/state": "^6.7.1",
|
||||||
"exports-loader": "^0.7.0",
|
"@codemirror/view": "^6.43.7",
|
||||||
"extract-text-webpack-plugin": "^4.0.0-alpha0",
|
"@puppeteer/browsers": "3.0.6",
|
||||||
"file-loader": "^3.0.1",
|
"autoprefixer": "^10.5.4",
|
||||||
"grunt": "^1.0.3",
|
"babel-loader": "^10.1.1",
|
||||||
"grunt-accessibility": "~6.0.0",
|
"base64-loader": "^1.0.0",
|
||||||
|
"chromedriver": "^150.0.4",
|
||||||
|
"cli-progress": "^3.12.0",
|
||||||
|
"colors": "^1.4.0",
|
||||||
|
"compression-webpack-plugin": "^12.0.0",
|
||||||
|
"copy-webpack-plugin": "^14.0.0",
|
||||||
|
"core-js": "^3.49.0",
|
||||||
|
"cspell": "^10.0.1",
|
||||||
|
"css-loader": "^7.1.4",
|
||||||
|
"eslint": "^9.39.5",
|
||||||
|
"eslint-plugin-jsdoc": "^50.8.0",
|
||||||
|
"glob": "^13.0.6",
|
||||||
|
"globals": "^17.9.0",
|
||||||
|
"grunt": "^1.6.3",
|
||||||
"grunt-chmod": "~1.1.1",
|
"grunt-chmod": "~1.1.1",
|
||||||
"grunt-concurrent": "^2.3.1",
|
"grunt-concurrent": "^3.0.0",
|
||||||
"grunt-contrib-clean": "~2.0.0",
|
"grunt-contrib-clean": "~2.0.1",
|
||||||
"grunt-contrib-connect": "^2.0.0",
|
"grunt-contrib-connect": "^5.0.1",
|
||||||
"grunt-contrib-copy": "~1.0.0",
|
"grunt-contrib-copy": "~1.0.0",
|
||||||
"grunt-contrib-watch": "^1.1.0",
|
"grunt-contrib-watch": "^1.1.0",
|
||||||
"grunt-eslint": "^21.0.0",
|
"grunt-eslint": "^26.0.0",
|
||||||
"grunt-exec": "~3.0.0",
|
"grunt-exec": "~3.0.0",
|
||||||
"grunt-jsdoc": "^2.3.0",
|
"grunt-webpack": "^8.0.0",
|
||||||
"grunt-webpack": "^3.1.3",
|
"grunt-zip": "^1.0.0",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^5.6.8",
|
||||||
"imports-loader": "^0.8.0",
|
"imports-loader": "^5.0.0",
|
||||||
"ink-docstrap": "^1.3.2",
|
"mini-css-extract-plugin": "2.10.2",
|
||||||
"jsdoc-babel": "^0.5.0",
|
"modify-source-webpack-plugin": "^4.1.0",
|
||||||
"nightwatch": "^1.0.18",
|
"nightwatch": "^3.16.0",
|
||||||
"node-sass": "^4.11.0",
|
"postcss": "^8.5.25",
|
||||||
"postcss-css-variables": "^0.11.0",
|
"postcss-css-variables": "^0.19.0",
|
||||||
"postcss-import": "^12.0.1",
|
"postcss-import": "^16.1.1",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^8.2.1",
|
||||||
"prompt": "^1.0.0",
|
"prompt": "^1.3.0",
|
||||||
"sass-loader": "^7.1.0",
|
"sitemap": "^9.0.1",
|
||||||
"sitemap": "^2.1.0",
|
"terser": "^5.49.0",
|
||||||
"style-loader": "^0.23.1",
|
"webpack": "^5.109.2",
|
||||||
"url-loader": "^1.1.2",
|
"webpack-bundle-analyzer": "^5.3.1",
|
||||||
"web-resource-inliner": "^4.2.1",
|
"webpack-dev-server": "^5.2.6",
|
||||||
"webpack": "^4.28.3",
|
"webpack-node-externals": "^3.0.0",
|
||||||
"webpack-bundle-analyzer": "^3.0.3",
|
"worker-loader": "^3.0.8"
|
||||||
"webpack-dev-server": "^3.1.14",
|
|
||||||
"webpack-node-externals": "^1.7.2",
|
|
||||||
"worker-loader": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"arrive": "^2.4.1",
|
"@alexaltea/capstone-js": "^3.0.5",
|
||||||
"babel-plugin-transform-builtin-extend": "1.1.2",
|
"@astronautlabs/amf": "^0.0.6",
|
||||||
"babel-polyfill": "^6.26.0",
|
"@blu3r4y/lzma": "^2.3.3",
|
||||||
"bcryptjs": "^2.4.3",
|
"@noble/hashes": "2.2.0",
|
||||||
"bignumber.js": "^8.0.1",
|
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
|
||||||
"bootstrap-colorpicker": "^2.5.3",
|
"@xmldom/xmldom": "^0.8.13",
|
||||||
"bootstrap-material-design": "^4.1.1",
|
"argon2-browser": "^1.18.0",
|
||||||
"bson": "^4.0.1",
|
"arrive": "^2.5.3",
|
||||||
|
"assert": "^2.1.0",
|
||||||
|
"avsc": "^5.7.9",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"bignumber.js": "^11.1.5",
|
||||||
|
"blakejs": "^1.2.1",
|
||||||
|
"bootstrap": "4.6.2",
|
||||||
|
"bootstrap-colorpicker": "^3.4.0",
|
||||||
|
"bootstrap-material-design": "^4.1.3",
|
||||||
|
"browserify-zlib": "^0.2.0",
|
||||||
|
"bson": "^7.3.1",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"cbor": "10.0.12",
|
||||||
"chi-squared": "^1.1.0",
|
"chi-squared": "^1.1.0",
|
||||||
"crypto-api": "^0.8.3",
|
"codepage": "^1.15.0",
|
||||||
"crypto-js": "^3.1.9-1",
|
"crypto-api": "^0.8.5",
|
||||||
|
"crypto-browserify": "^3.12.1",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"ctph.js": "0.0.5",
|
"ctph.js": "0.0.5",
|
||||||
"diff": "^3.5.0",
|
"d3": "7.9.0",
|
||||||
"es6-promisify": "^6.0.1",
|
"d3-hexbin": "^0.2.2",
|
||||||
"escodegen": "^1.11.0",
|
"diff": "^9.0.0",
|
||||||
"esmangle": "^1.0.1",
|
"dompurify": "^3.4.13",
|
||||||
|
"es6-promisify": "^7.0.0",
|
||||||
|
"escodegen": "^2.1.0",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
|
"events": "^3.3.0",
|
||||||
"exif-parser": "^0.1.12",
|
"exif-parser": "^0.1.12",
|
||||||
"file-saver": "^2.0.0",
|
"fernet": "^0.3.3",
|
||||||
"highlight.js": "^9.13.1",
|
"file-saver": "^2.0.5",
|
||||||
"jimp": "^0.6.0",
|
"flat": "^6.0.1",
|
||||||
"jquery": "^3.3.1",
|
"geodesy": "1.1.3",
|
||||||
"js-crc": "^0.2.0",
|
"handlebars": "^4.7.9",
|
||||||
"js-sha3": "^0.8.0",
|
"highlight.js": "^11.11.1",
|
||||||
"jsesc": "^2.5.2",
|
"ieee754": "^1.2.1",
|
||||||
"jsonpath": "^1.0.0",
|
"jimp": "1.6.0",
|
||||||
"jsonwebtoken": "^8.4.0",
|
"jq-web": "^0.5.1",
|
||||||
"jsqr": "^1.1.1",
|
"jquery": "3.7.1",
|
||||||
"jsrsasign": "8.0.12",
|
"js-ascon": "^1.3.0",
|
||||||
"kbpgp": "^2.0.82",
|
"js-sha3": "^0.12.0",
|
||||||
"lodash": "^4.17.11",
|
"js-yaml": "^5.2.3",
|
||||||
"loglevel": "^1.6.1",
|
"jsesc": "^3.1.0",
|
||||||
|
"json5": "^2.2.3",
|
||||||
|
"jsonata": "^2.2.2",
|
||||||
|
"jsonpath-plus": "^10.4.0",
|
||||||
|
"jsonwebtoken": "9.0.3",
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
|
"jsrsasign": "^11.1.3",
|
||||||
|
"kbpgp": "^2.1.17",
|
||||||
|
"libbzip2-wasm": "0.0.4",
|
||||||
|
"libyara-wasm": "^1.2.1",
|
||||||
|
"lodash": "^4.18.1",
|
||||||
|
"loglevel": "^1.9.2",
|
||||||
"loglevel-message-prefix": "^3.0.0",
|
"loglevel-message-prefix": "^3.0.0",
|
||||||
"moment": "^2.23.0",
|
"lz-string": "^1.5.0",
|
||||||
"moment-timezone": "^0.5.23",
|
"lz4js": "^0.2.0",
|
||||||
"ngeohash": "^0.6.3",
|
"markdown-it": "^14.3.0",
|
||||||
"node-forge": "^0.7.6",
|
"moment": "^2.30.1",
|
||||||
|
"moment-timezone": "^0.6.3",
|
||||||
|
"ngeohash": "^0.6.4",
|
||||||
|
"node-forge": "^1.4.0",
|
||||||
"node-md6": "^0.1.0",
|
"node-md6": "^0.1.0",
|
||||||
"notepack.io": "^2.2.0",
|
"nodom": "^2.4.0",
|
||||||
|
"notepack.io": "^3.0.1",
|
||||||
|
"ntlm": "^0.1.3",
|
||||||
"nwmatcher": "^1.4.4",
|
"nwmatcher": "^1.4.4",
|
||||||
"otp": "^0.1.3",
|
"otpauth": "^9.5.0",
|
||||||
"popper.js": "^1.14.6",
|
"path": "^0.12.7",
|
||||||
|
"popper.js": "^1.16.1",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"protobufjs": "^8.7.1",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
"qr-image": "^3.2.0",
|
"qr-image": "^3.2.0",
|
||||||
"scryptsy": "^2.0.0",
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rison": "^0.1.1",
|
||||||
|
"scryptsy": "^2.1.0",
|
||||||
"snackbarjs": "^1.1.0",
|
"snackbarjs": "^1.1.0",
|
||||||
"sortablejs": "^1.8.0-rc1",
|
"sortablejs": "^1.15.7",
|
||||||
"split.js": "^1.5.10",
|
"split.js": "^1.6.5",
|
||||||
"ssdeep.js": "0.0.2",
|
"sql-formatter": "^15.8.2",
|
||||||
"ua-parser-js": "^0.7.19",
|
"ssdeep.js": "0.0.3",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
|
"tesseract.js": "^7.0.0",
|
||||||
|
"ua-parser-js": "^2.0.10",
|
||||||
|
"unorm": "^1.6.0",
|
||||||
|
"url": "^0.11.4",
|
||||||
"utf8": "^3.0.0",
|
"utf8": "^3.0.0",
|
||||||
|
"uuid": "^14.0.1",
|
||||||
"vkbeautify": "^0.99.3",
|
"vkbeautify": "^0.99.3",
|
||||||
"xmldom": "^0.1.27",
|
"xpath": "0.0.34",
|
||||||
"xpath": "0.0.27",
|
"xregexp": "^5.1.2",
|
||||||
"xregexp": "^4.2.0",
|
|
||||||
"zlibjs": "^0.3.1"
|
"zlibjs": "^0.3.1"
|
||||||
},
|
},
|
||||||
|
"allowScripts": {
|
||||||
|
"@nightwatch/nightwatch-inspector@1.0.1": true,
|
||||||
|
"chromedriver@150.0.3": true,
|
||||||
|
"core-js": false,
|
||||||
|
"core-js-pure": false,
|
||||||
|
"fsevents": false,
|
||||||
|
"tesseract.js": false
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "grunt dev",
|
"start": "npx grunt dev",
|
||||||
"build": "grunt prod",
|
"build": "npx grunt prod",
|
||||||
"test": "grunt test",
|
"node": "npx grunt node",
|
||||||
"testui": "grunt testui",
|
"repl": "node --no-warnings src/node/repl.mjs",
|
||||||
"docs": "grunt docs",
|
"test": "npx grunt configTests && node --no-warnings --no-deprecation --openssl-legacy-provider tests/node/index.mjs && node --no-warnings --no-deprecation --openssl-legacy-provider --trace-uncaught tests/operations/index.mjs",
|
||||||
"lint": "grunt lint",
|
"testnodeconsumer": "npx grunt testnodeconsumer",
|
||||||
"newop": "node --experimental-modules src/core/config/scripts/newOperation.mjs"
|
"testui": "npx grunt testui",
|
||||||
|
"testuidev": "npx nightwatch --env=dev",
|
||||||
|
"lint": "npx grunt lint",
|
||||||
|
"lint:grammar": "cspell ./src",
|
||||||
|
"postinstall": "npx grunt exec:fixCryptoApiImports && npx grunt exec:fixSnackbarMarkup",
|
||||||
|
"newop": "node src/core/config/scripts/newOperation.mjs",
|
||||||
|
"minor": "node src/core/config/scripts/newMinorVersion.mjs && npm version minor --git-tag-version=false && echo \"Updated to version v$(npm pkg get version | xargs), please create a pull request and once merged use 'npm run tag'\"",
|
||||||
|
"tag": "git tag -s \"v$(npm pkg get version | xargs)\" -m \"$(npm pkg get version | xargs)\" && echo \"Created v$(npm pkg get version | xargs), now check and push the tag\"",
|
||||||
|
"getheapsize": "node -e 'console.log(`node heap limit = ${require(\"v8\").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'",
|
||||||
|
"setheapsize": "export NODE_OPTIONS=--max_old_space_size=2048"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24 <25"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
require("postcss-import"),
|
require("postcss-import"),
|
||||||
require("autoprefixer")({
|
require("autoprefixer"),
|
||||||
browsers: [
|
|
||||||
"Chrome >= 40",
|
|
||||||
"Firefox >= 35",
|
|
||||||
"Edge >= 14"
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
require("postcss-css-variables")({
|
require("postcss-css-variables")({
|
||||||
preserve: true
|
preserve: true
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -4,9 +4,10 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Dish from "./Dish";
|
import Dish from "./Dish.mjs";
|
||||||
import Recipe from "./Recipe";
|
import Recipe from "./Recipe.mjs";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
import { isWorkerEnvironment } from "./Utils.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main controller for CyberChef.
|
* The main controller for CyberChef.
|
||||||
@ -26,10 +27,8 @@ class Chef {
|
|||||||
*
|
*
|
||||||
* @param {string|ArrayBuffer} input - The input data as a string or ArrayBuffer
|
* @param {string|ArrayBuffer} input - The input data as a string or ArrayBuffer
|
||||||
* @param {Object[]} recipeConfig - The recipe configuration object
|
* @param {Object[]} recipeConfig - The recipe configuration object
|
||||||
* @param {Object} options - The options object storing various user choices
|
* @param {Object} [options={}] - The options object storing various user choices
|
||||||
* @param {boolean} options.attempHighlight - Whether or not to attempt highlighting
|
* @param {string} [options.returnType] - What type to return the result as
|
||||||
* @param {number} progress - The position in the recipe to start from
|
|
||||||
* @param {number} [step] - Whether to only execute one operation in the recipe
|
|
||||||
*
|
*
|
||||||
* @returns {Object} response
|
* @returns {Object} response
|
||||||
* @returns {string} response.result - The output of the recipe
|
* @returns {string} response.result - The output of the recipe
|
||||||
@ -38,77 +37,53 @@ class Chef {
|
|||||||
* @returns {number} response.duration - The number of ms it took to execute the recipe
|
* @returns {number} response.duration - The number of ms it took to execute the recipe
|
||||||
* @returns {number} response.error - The error object thrown by a failed operation (false if no error)
|
* @returns {number} response.error - The error object thrown by a failed operation (false if no error)
|
||||||
*/
|
*/
|
||||||
async bake(input, recipeConfig, options, progress, step) {
|
async bake(input, recipeConfig, options={}) {
|
||||||
log.debug("Chef baking");
|
log.debug("Chef baking");
|
||||||
const startTime = new Date().getTime(),
|
const startTime = Date.now(),
|
||||||
recipe = new Recipe(recipeConfig),
|
recipe = new Recipe(recipeConfig),
|
||||||
containsFc = recipe.containsFlowControl(),
|
containsFc = recipe.containsFlowControl();
|
||||||
notUTF8 = options && options.hasOwnProperty("treatAsUtf8") && !options.treatAsUtf8;
|
let error = false,
|
||||||
let error = false;
|
|
||||||
|
|
||||||
if (containsFc && ENVIRONMENT_IS_WORKER()) self.setOption("attemptHighlight", false);
|
|
||||||
|
|
||||||
// Clean up progress
|
|
||||||
if (progress >= recipeConfig.length) {
|
|
||||||
progress = 0;
|
progress = 0;
|
||||||
}
|
|
||||||
|
|
||||||
if (step) {
|
if (containsFc && isWorkerEnvironment()) self.setOption("attemptHighlight", false);
|
||||||
// Unset breakpoint on this step
|
|
||||||
recipe.setBreakpoint(progress, false);
|
|
||||||
// Set breakpoint on next step
|
|
||||||
recipe.setBreakpoint(progress + 1, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the previously run operation presented a different value to its
|
// Load data
|
||||||
// normal output, we need to recalculate it.
|
const type = input instanceof ArrayBuffer ? Dish.ARRAY_BUFFER : Dish.STRING;
|
||||||
if (recipe.lastOpPresented(progress)) {
|
this.dish.set(input, type);
|
||||||
progress = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If stepping with flow control, we have to start from the beginning
|
|
||||||
// but still want to skip all previous breakpoints
|
|
||||||
if (progress > 0 && containsFc) {
|
|
||||||
recipe.removeBreaksUpTo(progress);
|
|
||||||
progress = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If starting from scratch, load data
|
|
||||||
if (progress === 0) {
|
|
||||||
const type = input instanceof ArrayBuffer ? Dish.ARRAY_BUFFER : Dish.STRING;
|
|
||||||
this.dish.set(input, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
progress = await recipe.execute(this.dish, progress);
|
progress = await recipe.execute(this.dish, progress);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
|
|
||||||
|
let displayStr;
|
||||||
|
if ("displayStr" in err) {
|
||||||
|
displayStr = err.displayStr;
|
||||||
|
} else {
|
||||||
|
displayStr = err.toString();
|
||||||
|
}
|
||||||
error = {
|
error = {
|
||||||
displayStr: err.displayStr,
|
displayStr: displayStr,
|
||||||
};
|
};
|
||||||
progress = err.progress;
|
progress = err.progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Depending on the size of the output, we may send it back as a string or an ArrayBuffer.
|
|
||||||
// This can prevent unnecessary casting as an ArrayBuffer can be easily downloaded as a file.
|
|
||||||
// The threshold is specified in KiB.
|
|
||||||
const threshold = (options.ioDisplayThreshold || 1024) * 1024;
|
|
||||||
const returnType = this.dish.size > threshold ? Dish.ARRAY_BUFFER : Dish.STRING;
|
|
||||||
|
|
||||||
// Create a raw version of the dish, unpresented
|
// Create a raw version of the dish, unpresented
|
||||||
const rawDish = this.dish.clone();
|
const rawDish = this.dish.clone();
|
||||||
|
|
||||||
// Present the raw result
|
// Present the raw result
|
||||||
await recipe.present(this.dish);
|
await recipe.present(this.dish);
|
||||||
|
|
||||||
|
const returnType =
|
||||||
|
this.dish.type === Dish.HTML ? Dish.HTML :
|
||||||
|
options?.returnType ? options.returnType : Dish.ARRAY_BUFFER;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dish: rawDish,
|
dish: rawDish,
|
||||||
result: this.dish.type === Dish.HTML ?
|
result: await this.dish.get(returnType),
|
||||||
await this.dish.get(Dish.HTML, notUTF8) :
|
|
||||||
await this.dish.get(returnType, notUTF8),
|
|
||||||
type: Dish.enumLookup(this.dish.type),
|
type: Dish.enumLookup(this.dish.type),
|
||||||
progress: progress,
|
progress: progress,
|
||||||
duration: new Date().getTime() - startTime,
|
duration: Date.now() - startTime,
|
||||||
error: error
|
error: error
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -134,7 +109,7 @@ class Chef {
|
|||||||
silentBake(recipeConfig) {
|
silentBake(recipeConfig) {
|
||||||
log.debug("Running silent bake");
|
log.debug("Running silent bake");
|
||||||
|
|
||||||
const startTime = new Date().getTime(),
|
const startTime = Date.now(),
|
||||||
recipe = new Recipe(recipeConfig),
|
recipe = new Recipe(recipeConfig),
|
||||||
dish = new Dish();
|
dish = new Dish();
|
||||||
|
|
||||||
@ -143,7 +118,7 @@ class Chef {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Suppress all errors
|
// Suppress all errors
|
||||||
}
|
}
|
||||||
return new Date().getTime() - startTime;
|
return Date.now() - startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -157,12 +132,14 @@ class Chef {
|
|||||||
* @param {number} pos.end - The end offset.
|
* @param {number} pos.end - The end offset.
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
calculateHighlights(recipeConfig, direction, pos) {
|
async calculateHighlights(recipeConfig, direction, pos) {
|
||||||
const recipe = new Recipe(recipeConfig);
|
const recipe = new Recipe(recipeConfig);
|
||||||
const highlights = recipe.generateHighlightList();
|
const highlights = await recipe.generateHighlightList();
|
||||||
|
|
||||||
if (!highlights) return false;
|
if (!highlights) return false;
|
||||||
|
|
||||||
|
if (direction === "reverse") highlights.reverse();
|
||||||
|
|
||||||
for (let i = 0; i < highlights.length; i++) {
|
for (let i = 0; i < highlights.length; i++) {
|
||||||
// Remove multiple highlights before processing again
|
// Remove multiple highlights before processing again
|
||||||
pos = [pos[0]];
|
pos = [pos[0]];
|
||||||
@ -170,7 +147,12 @@ class Chef {
|
|||||||
const func = direction === "forward" ? highlights[i].f : highlights[i].b;
|
const func = direction === "forward" ? highlights[i].f : highlights[i].b;
|
||||||
|
|
||||||
if (typeof func == "function") {
|
if (typeof func == "function") {
|
||||||
pos = func(pos, highlights[i].args);
|
try {
|
||||||
|
pos = func(pos, highlights[i].args);
|
||||||
|
} catch (err) {
|
||||||
|
// Throw away highlighting errors
|
||||||
|
pos = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,6 +175,18 @@ class Chef {
|
|||||||
return await newDish.get(type);
|
return await newDish.get(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the title of a dish and returns it
|
||||||
|
*
|
||||||
|
* @param {Dish} dish
|
||||||
|
* @param {number} [maxLength=100]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
async getDishTitle(dish, maxLength=100) {
|
||||||
|
const newDish = new Dish(dish);
|
||||||
|
return await newDish.getTitle(maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Chef;
|
export default Chef;
|
||||||
|
|||||||
@ -6,26 +6,19 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "babel-polyfill";
|
import Chef from "./Chef.mjs";
|
||||||
import Chef from "./Chef";
|
import OperationConfig from "./config/OperationConfig.json" with { type: "json" };
|
||||||
import OperationConfig from "./config/OperationConfig.json";
|
import OpModules from "./config/modules/OpModules.mjs";
|
||||||
import OpModules from "./config/modules/OpModules";
|
|
||||||
|
|
||||||
// Add ">" to the start of all log messages in the Chef Worker
|
|
||||||
import loglevelMessagePrefix from "loglevel-message-prefix";
|
import loglevelMessagePrefix from "loglevel-message-prefix";
|
||||||
|
|
||||||
loglevelMessagePrefix(log, {
|
|
||||||
prefixes: [],
|
|
||||||
staticPrefixes: [">"],
|
|
||||||
prefixFormat: "%p"
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Set up Chef instance
|
// Set up Chef instance
|
||||||
self.chef = new Chef();
|
self.chef = new Chef();
|
||||||
|
|
||||||
self.OpModules = OpModules;
|
self.OpModules = OpModules;
|
||||||
self.OperationConfig = OperationConfig;
|
self.OperationConfig = OperationConfig;
|
||||||
|
self.inputNum = -1;
|
||||||
|
|
||||||
|
|
||||||
// Tell the app that the worker has loaded and is ready to operate
|
// Tell the app that the worker has loaded and is ready to operate
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
@ -36,6 +29,9 @@ self.postMessage({
|
|||||||
/**
|
/**
|
||||||
* Respond to message from parent thread.
|
* Respond to message from parent thread.
|
||||||
*
|
*
|
||||||
|
* inputNum is optional and only used for baking multiple inputs.
|
||||||
|
* Defaults to -1 when one isn't sent with the bake message.
|
||||||
|
*
|
||||||
* Messages should have the following format:
|
* Messages should have the following format:
|
||||||
* {
|
* {
|
||||||
* action: "bake" | "silentBake",
|
* action: "bake" | "silentBake",
|
||||||
@ -44,14 +40,15 @@ self.postMessage({
|
|||||||
* recipeConfig: {[Object]},
|
* recipeConfig: {[Object]},
|
||||||
* options: {Object},
|
* options: {Object},
|
||||||
* progress: {number},
|
* progress: {number},
|
||||||
* step: {boolean}
|
* step: {boolean},
|
||||||
* } | undefined
|
* [inputNum=-1]: {number}
|
||||||
|
* }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
self.addEventListener("message", function(e) {
|
self.addEventListener("message", function(e) {
|
||||||
// Handle message
|
// Handle message
|
||||||
const r = e.data;
|
const r = e.data;
|
||||||
log.debug("ChefWorker receiving command '" + r.action + "'");
|
log.debug(`Receiving command '${r.action}'`);
|
||||||
|
|
||||||
switch (r.action) {
|
switch (r.action) {
|
||||||
case "bake":
|
case "bake":
|
||||||
@ -63,6 +60,9 @@ self.addEventListener("message", function(e) {
|
|||||||
case "getDishAs":
|
case "getDishAs":
|
||||||
getDishAs(r.data);
|
getDishAs(r.data);
|
||||||
break;
|
break;
|
||||||
|
case "getDishTitle":
|
||||||
|
getDishTitle(r.data);
|
||||||
|
break;
|
||||||
case "docURL":
|
case "docURL":
|
||||||
// Used to set the URL of the current document so that scripts can be
|
// Used to set the URL of the current document so that scripts can be
|
||||||
// imported into an inline worker.
|
// imported into an inline worker.
|
||||||
@ -78,6 +78,12 @@ self.addEventListener("message", function(e) {
|
|||||||
case "setLogLevel":
|
case "setLogLevel":
|
||||||
log.setLevel(r.data, false);
|
log.setLevel(r.data, false);
|
||||||
break;
|
break;
|
||||||
|
case "setLogPrefix":
|
||||||
|
loglevelMessagePrefix(log, {
|
||||||
|
prefixes: [],
|
||||||
|
staticPrefixes: [r.data]
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -92,30 +98,38 @@ self.addEventListener("message", function(e) {
|
|||||||
async function bake(data) {
|
async function bake(data) {
|
||||||
// Ensure the relevant modules are loaded
|
// Ensure the relevant modules are loaded
|
||||||
self.loadRequiredModules(data.recipeConfig);
|
self.loadRequiredModules(data.recipeConfig);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
self.inputNum = data.inputNum === undefined ? -1 : data.inputNum;
|
||||||
const response = await self.chef.bake(
|
const response = await self.chef.bake(
|
||||||
data.input, // The user's input
|
data.input, // The user's input
|
||||||
data.recipeConfig, // The configuration of the recipe
|
data.recipeConfig, // The configuration of the recipe
|
||||||
data.options, // Options set by the user
|
data.options // Options set by the user
|
||||||
data.progress, // The current position in the recipe
|
|
||||||
data.step // Whether or not to take one step or execute the whole recipe
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const transferable = (response.dish.value instanceof ArrayBuffer) ?
|
||||||
|
[response.dish.value] :
|
||||||
|
undefined;
|
||||||
|
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
action: "bakeComplete",
|
action: "bakeComplete",
|
||||||
data: Object.assign(response, {
|
data: Object.assign(response, {
|
||||||
id: data.id
|
id: data.id,
|
||||||
|
inputNum: data.inputNum,
|
||||||
|
bakeId: data.bakeId
|
||||||
})
|
})
|
||||||
});
|
}, transferable);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
action: "bakeError",
|
action: "bakeError",
|
||||||
data: Object.assign(err, {
|
data: {
|
||||||
id: data.id
|
error: err.message || err,
|
||||||
})
|
id: data.id,
|
||||||
|
inputNum: data.inputNum
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
self.inputNum = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -137,13 +151,33 @@ function silentBake(data) {
|
|||||||
*/
|
*/
|
||||||
async function getDishAs(data) {
|
async function getDishAs(data) {
|
||||||
const value = await self.chef.getDishAs(data.dish, data.type);
|
const value = await self.chef.getDishAs(data.dish, data.type);
|
||||||
|
const transferable = (data.type === "ArrayBuffer") ? [value] : undefined;
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
action: "dishReturned",
|
action: "dishReturned",
|
||||||
data: {
|
data: {
|
||||||
value: value,
|
value: value,
|
||||||
id: data.id
|
id: data.id
|
||||||
}
|
}
|
||||||
|
}, transferable);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the dish title
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {Dish} data.dish
|
||||||
|
* @param {number} data.maxLength
|
||||||
|
* @param {number} data.id
|
||||||
|
*/
|
||||||
|
async function getDishTitle(data) {
|
||||||
|
const title = await self.chef.getDishTitle(data.dish, data.maxLength);
|
||||||
|
self.postMessage({
|
||||||
|
action: "dishReturned",
|
||||||
|
data: {
|
||||||
|
value: title,
|
||||||
|
id: data.id
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,12 +187,12 @@ async function getDishAs(data) {
|
|||||||
*
|
*
|
||||||
* @param {Object[]} recipeConfig
|
* @param {Object[]} recipeConfig
|
||||||
* @param {string} direction
|
* @param {string} direction
|
||||||
* @param {Object} pos - The position object for the highlight.
|
* @param {Object[]} pos - The position object for the highlight.
|
||||||
* @param {number} pos.start - The start offset.
|
* @param {number} pos.start - The start offset.
|
||||||
* @param {number} pos.end - The end offset.
|
* @param {number} pos.end - The end offset.
|
||||||
*/
|
*/
|
||||||
function calculateHighlights(recipeConfig, direction, pos) {
|
async function calculateHighlights(recipeConfig, direction, pos) {
|
||||||
pos = self.chef.calculateHighlights(recipeConfig, direction, pos);
|
pos = await self.chef.calculateHighlights(recipeConfig, direction, pos);
|
||||||
|
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
action: "highlightsCalculated",
|
action: "highlightsCalculated",
|
||||||
@ -176,10 +210,10 @@ self.loadRequiredModules = function(recipeConfig) {
|
|||||||
recipeConfig.forEach(op => {
|
recipeConfig.forEach(op => {
|
||||||
const module = self.OperationConfig[op.op].module;
|
const module = self.OperationConfig[op.op].module;
|
||||||
|
|
||||||
if (!OpModules.hasOwnProperty(module)) {
|
if (!(module in OpModules)) {
|
||||||
log.info(`Loading ${module} module`);
|
log.info(`Loading ${module} module`);
|
||||||
self.sendStatusMessage(`Loading ${module} module`);
|
self.sendStatusMessage(`Loading ${module} module`);
|
||||||
self.importScripts(`${self.docURL}/${module}.js`);
|
self.importScripts(`${self.docURL}/modules/${module}.js`); // lgtm [js/client-side-unvalidated-url-redirection]
|
||||||
self.sendStatusMessage("");
|
self.sendStatusMessage("");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -194,7 +228,28 @@ self.loadRequiredModules = function(recipeConfig) {
|
|||||||
self.sendStatusMessage = function(msg) {
|
self.sendStatusMessage = function(msg) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
action: "statusMessage",
|
action: "statusMessage",
|
||||||
data: msg
|
data: {
|
||||||
|
message: msg,
|
||||||
|
inputNum: self.inputNum
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send progress update to the app.
|
||||||
|
*
|
||||||
|
* @param {number} progress
|
||||||
|
* @param {number} total
|
||||||
|
*/
|
||||||
|
self.sendProgressMessage = function(progress, total) {
|
||||||
|
self.postMessage({
|
||||||
|
action: "progressMessage",
|
||||||
|
data: {
|
||||||
|
progress: progress,
|
||||||
|
total: total,
|
||||||
|
inputNum: self.inputNum
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,22 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Utils from "./Utils";
|
import Utils, { isNodeEnvironment } from "./Utils.mjs";
|
||||||
import DishError from "./errors/DishError";
|
import DishError from "./errors/DishError.mjs";
|
||||||
import BigNumber from "bignumber.js";
|
import BigNumber from "bignumber.js";
|
||||||
|
import { detectFileType } from "./lib/FileType.mjs";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
||||||
|
import DishByteArray from "./dishTypes/DishByteArray.mjs";
|
||||||
|
import DishBigNumber from "./dishTypes/DishBigNumber.mjs";
|
||||||
|
import DishFile from "./dishTypes/DishFile.mjs";
|
||||||
|
import DishHTML from "./dishTypes/DishHTML.mjs";
|
||||||
|
import DishJSON from "./dishTypes/DishJSON.mjs";
|
||||||
|
import DishListFile from "./dishTypes/DishListFile.mjs";
|
||||||
|
import DishNumber from "./dishTypes/DishNumber.mjs";
|
||||||
|
import DishString from "./dishTypes/DishString.mjs";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data being operated on by each operation.
|
* The data being operated on by each operation.
|
||||||
*/
|
*/
|
||||||
@ -18,16 +29,27 @@ class Dish {
|
|||||||
/**
|
/**
|
||||||
* Dish constructor
|
* Dish constructor
|
||||||
*
|
*
|
||||||
* @param {Dish} [dish=null] - A dish to clone
|
* @param {Dish || *} [dishOrInput=null] - A dish to clone OR an object
|
||||||
|
* literal to make into a dish
|
||||||
|
* @param {Enum} [type=null] (optional) - A type to accompany object
|
||||||
|
* literal input
|
||||||
*/
|
*/
|
||||||
constructor(dish=null) {
|
constructor(dishOrInput=null, type = null) {
|
||||||
this.value = [];
|
this.value = new ArrayBuffer(0);
|
||||||
this.type = Dish.BYTE_ARRAY;
|
this.type = Dish.ARRAY_BUFFER;
|
||||||
|
|
||||||
if (dish &&
|
// Case: dishOrInput is dish object
|
||||||
dish.hasOwnProperty("value") &&
|
if (dishOrInput &&
|
||||||
dish.hasOwnProperty("type")) {
|
Object.prototype.hasOwnProperty.call(dishOrInput, "value") &&
|
||||||
this.set(dish.value, dish.type);
|
Object.prototype.hasOwnProperty.call(dishOrInput, "type")) {
|
||||||
|
this.set(dishOrInput.value, dishOrInput.type);
|
||||||
|
// input and type defined separately
|
||||||
|
} else if (dishOrInput && type !== null) {
|
||||||
|
this.set(dishOrInput, type);
|
||||||
|
// No type declared, so infer it.
|
||||||
|
} else if (dishOrInput) {
|
||||||
|
const inferredType = Dish.typeEnum(dishOrInput.constructor.name);
|
||||||
|
this.set(dishOrInput, inferredType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +78,7 @@ class Dish {
|
|||||||
case "big number":
|
case "big number":
|
||||||
return Dish.BIG_NUMBER;
|
return Dish.BIG_NUMBER;
|
||||||
case "json":
|
case "json":
|
||||||
|
case "object": // object constructor name. To allow JSON input in node.
|
||||||
return Dish.JSON;
|
return Dish.JSON;
|
||||||
case "file":
|
case "file":
|
||||||
return Dish.FILE;
|
return Dish.FILE;
|
||||||
@ -99,6 +122,42 @@ class Dish {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the data in the type format specified.
|
||||||
|
*
|
||||||
|
* If running in a browser, get is asynchronous.
|
||||||
|
*
|
||||||
|
* @param {number} type - The data type of value, see Dish enums.
|
||||||
|
* @returns {* | Promise} - (Browser) A promise | (Node) value of dish in given type
|
||||||
|
*/
|
||||||
|
get(type) {
|
||||||
|
if (typeof type === "string") {
|
||||||
|
type = Dish.typeEnum(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type !== type) {
|
||||||
|
|
||||||
|
// Node environment => _translate is sync
|
||||||
|
if (isNodeEnvironment()) {
|
||||||
|
this._translate(type);
|
||||||
|
return this.value;
|
||||||
|
|
||||||
|
// Browser environment => _translate is async
|
||||||
|
} else {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._translate(type)
|
||||||
|
.then(() => {
|
||||||
|
resolve(this.value);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the data value and type and then validates them.
|
* Sets the data value and type and then validates them.
|
||||||
*
|
*
|
||||||
@ -117,124 +176,85 @@ class Dish {
|
|||||||
this.type = type;
|
this.type = type;
|
||||||
|
|
||||||
if (!this.valid()) {
|
if (!this.valid()) {
|
||||||
const sample = Utils.truncate(JSON.stringify(this.value), 13);
|
const sample = Utils.truncate(JSON.stringify(this.value), 25);
|
||||||
throw new DishError(`Data is not a valid ${Dish.enumLookup(type)}: ${sample}`);
|
throw new DishError(`Data is not a valid ${Dish.enumLookup(type)}: ${sample}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the value of the data in the type format specified.
|
* Returns the Dish as the given type, without mutating the original dish.
|
||||||
|
*
|
||||||
|
* If running in a browser, get is asynchronous.
|
||||||
|
*
|
||||||
|
* @Node
|
||||||
*
|
*
|
||||||
* @param {number} type - The data type of value, see Dish enums.
|
* @param {number} type - The data type of value, see Dish enums.
|
||||||
* @param {boolean} [notUTF8=false] - Do not treat strings as UTF8.
|
* @returns {Dish | Promise} - (Browser) A promise | (Node) value of dish in given type
|
||||||
* @returns {*} - The value of the output data.
|
|
||||||
*/
|
*/
|
||||||
async get(type, notUTF8=false) {
|
presentAs(type) {
|
||||||
if (typeof type === "string") {
|
const clone = this.clone();
|
||||||
type = Dish.typeEnum(type);
|
return clone.get(type);
|
||||||
}
|
|
||||||
if (this.type !== type) {
|
|
||||||
await this._translate(type, notUTF8);
|
|
||||||
}
|
|
||||||
return this.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates the data to the given type format.
|
* Detects the MIME type of the current dish
|
||||||
*
|
* @returns {string}
|
||||||
* @param {number} toType - The data type of value, see Dish enums.
|
|
||||||
* @param {boolean} [notUTF8=false] - Do not treat strings as UTF8.
|
|
||||||
*/
|
*/
|
||||||
async _translate(toType, notUTF8=false) {
|
detectDishType() {
|
||||||
log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`);
|
const data = new Uint8Array(this.value.slice(0, 2048)),
|
||||||
const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8;
|
types = detectFileType(data);
|
||||||
|
|
||||||
// Convert data to intermediate byteArray type
|
if (!types.length || !types[0].mime || !(types[0].mime === "text/plain")) {
|
||||||
try {
|
return null;
|
||||||
switch (this.type) {
|
} else {
|
||||||
case Dish.STRING:
|
return types[0].mime;
|
||||||
this.value = this.value ? Utils.strToByteArray(this.value) : [];
|
|
||||||
break;
|
|
||||||
case Dish.NUMBER:
|
|
||||||
this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : [];
|
|
||||||
break;
|
|
||||||
case Dish.HTML:
|
|
||||||
this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : [];
|
|
||||||
break;
|
|
||||||
case Dish.ARRAY_BUFFER:
|
|
||||||
// Array.from() would be nicer here, but it's slightly slower
|
|
||||||
this.value = Array.prototype.slice.call(new Uint8Array(this.value));
|
|
||||||
break;
|
|
||||||
case Dish.BIG_NUMBER:
|
|
||||||
this.value = this.value instanceof BigNumber ? Utils.strToByteArray(this.value.toFixed()) : [];
|
|
||||||
break;
|
|
||||||
case Dish.JSON:
|
|
||||||
this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : [];
|
|
||||||
break;
|
|
||||||
case Dish.FILE:
|
|
||||||
this.value = await Utils.readFile(this.value);
|
|
||||||
this.value = Array.prototype.slice.call(this.value);
|
|
||||||
break;
|
|
||||||
case Dish.LIST_FILE:
|
|
||||||
this.value = await Promise.all(this.value.map(async f => Utils.readFile(f)));
|
|
||||||
this.value = this.value.map(b => Array.prototype.slice.call(b));
|
|
||||||
this.value = [].concat.apply([], this.value);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.type = Dish.BYTE_ARRAY;
|
|
||||||
|
|
||||||
// Convert from byteArray to toType
|
|
||||||
try {
|
|
||||||
switch (toType) {
|
|
||||||
case Dish.STRING:
|
|
||||||
case Dish.HTML:
|
|
||||||
this.value = this.value ? byteArrayToStr(this.value) : "";
|
|
||||||
this.type = Dish.STRING;
|
|
||||||
break;
|
|
||||||
case Dish.NUMBER:
|
|
||||||
this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0;
|
|
||||||
this.type = Dish.NUMBER;
|
|
||||||
break;
|
|
||||||
case Dish.ARRAY_BUFFER:
|
|
||||||
this.value = new Uint8Array(this.value).buffer;
|
|
||||||
this.type = Dish.ARRAY_BUFFER;
|
|
||||||
break;
|
|
||||||
case Dish.BIG_NUMBER:
|
|
||||||
try {
|
|
||||||
this.value = new BigNumber(byteArrayToStr(this.value));
|
|
||||||
} catch (err) {
|
|
||||||
this.value = new BigNumber(NaN);
|
|
||||||
}
|
|
||||||
this.type = Dish.BIG_NUMBER;
|
|
||||||
break;
|
|
||||||
case Dish.JSON:
|
|
||||||
this.value = JSON.parse(byteArrayToStr(this.value));
|
|
||||||
this.type = Dish.JSON;
|
|
||||||
break;
|
|
||||||
case Dish.FILE:
|
|
||||||
this.value = new File(this.value, "unknown");
|
|
||||||
break;
|
|
||||||
case Dish.LIST_FILE:
|
|
||||||
this.value = [new File(this.value, "unknown")];
|
|
||||||
this.type = Dish.LIST_FILE;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the title of the data up to the specified length
|
||||||
|
*
|
||||||
|
* @param {number} maxLength - The maximum title length
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
async getTitle(maxLength) {
|
||||||
|
let title = "";
|
||||||
|
let cloned;
|
||||||
|
|
||||||
|
switch (this.type) {
|
||||||
|
case Dish.FILE:
|
||||||
|
title = this.value.name;
|
||||||
|
break;
|
||||||
|
case Dish.LIST_FILE:
|
||||||
|
title = `${this.value.length} file(s)`;
|
||||||
|
break;
|
||||||
|
case Dish.JSON:
|
||||||
|
title = "application/json";
|
||||||
|
break;
|
||||||
|
case Dish.NUMBER:
|
||||||
|
case Dish.BIG_NUMBER:
|
||||||
|
title = this.value.toString();
|
||||||
|
break;
|
||||||
|
case Dish.ARRAY_BUFFER:
|
||||||
|
case Dish.BYTE_ARRAY:
|
||||||
|
title = this.detectDishType();
|
||||||
|
if (title !== null) break;
|
||||||
|
// fall through if no mime type was detected
|
||||||
|
default:
|
||||||
|
try {
|
||||||
|
cloned = this.clone();
|
||||||
|
cloned.value = cloned.value.slice(0, 256);
|
||||||
|
title = await cloned.get(Dish.STRING);
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`${Dish.enumLookup(this.type)} cannot be sliced. ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return title.slice(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates that the value is the type that has been specified.
|
* Validates that the value is the type that has been specified.
|
||||||
* May have to disable parts of BYTE_ARRAY validation if it effects performance.
|
* May have to disable parts of BYTE_ARRAY validation if it effects performance.
|
||||||
@ -244,7 +264,7 @@ class Dish {
|
|||||||
valid() {
|
valid() {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case Dish.BYTE_ARRAY:
|
case Dish.BYTE_ARRAY:
|
||||||
if (!(this.value instanceof Array)) {
|
if (!(this.value instanceof Uint8Array) && !(this.value instanceof Array)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,7 +285,17 @@ class Dish {
|
|||||||
case Dish.ARRAY_BUFFER:
|
case Dish.ARRAY_BUFFER:
|
||||||
return this.value instanceof ArrayBuffer;
|
return this.value instanceof ArrayBuffer;
|
||||||
case Dish.BIG_NUMBER:
|
case Dish.BIG_NUMBER:
|
||||||
return this.value instanceof BigNumber;
|
if (BigNumber.isBigNumber(this.value)) return true;
|
||||||
|
/*
|
||||||
|
If a BigNumber is passed between WebWorkers it is serialised as a JSON
|
||||||
|
object with a coefficient (c), exponent (e) and sign (s). We detect this
|
||||||
|
and reinitialise it as a BigNumber object.
|
||||||
|
*/
|
||||||
|
if (Object.keys(this.value).sort().equals(["c", "e", "s"])) {
|
||||||
|
this.value = new BigNumber({ s: this.value.s, e: this.value.e, c: this.value.c, _isBigNumber: true});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
case Dish.JSON:
|
case Dish.JSON:
|
||||||
// All values can be serialised in some manner, so we return true in all cases
|
// All values can be serialised in some manner, so we return true in all cases
|
||||||
return true;
|
return true;
|
||||||
@ -372,6 +402,107 @@ class Dish {
|
|||||||
return newDish;
|
return newDish;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates the data to the given type format.
|
||||||
|
*
|
||||||
|
* If running in the browser, _translate is asynchronous.
|
||||||
|
*
|
||||||
|
* @param {number} toType - The data type of value, see Dish enums.
|
||||||
|
* @returns {Promise || undefined}
|
||||||
|
*/
|
||||||
|
_translate(toType) {
|
||||||
|
log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`);
|
||||||
|
|
||||||
|
// Node environment => translate is sync
|
||||||
|
if (isNodeEnvironment()) {
|
||||||
|
this._toArrayBuffer();
|
||||||
|
this.type = Dish.ARRAY_BUFFER;
|
||||||
|
this._fromArrayBuffer(toType);
|
||||||
|
|
||||||
|
// Browser environment => translate is async
|
||||||
|
} else {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._toArrayBuffer()
|
||||||
|
.then(() => this.type = Dish.ARRAY_BUFFER)
|
||||||
|
.then(() => {
|
||||||
|
this._fromArrayBuffer(toType);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this.value to an ArrayBuffer
|
||||||
|
*
|
||||||
|
* If running in a browser, _toByteArray is asynchronous.
|
||||||
|
*
|
||||||
|
* @returns {Promise || undefined}
|
||||||
|
*/
|
||||||
|
_toArrayBuffer() {
|
||||||
|
// Using 'bind' here to allow this.value to be mutated within translation functions
|
||||||
|
const toByteArrayFuncs = {
|
||||||
|
browser: {
|
||||||
|
[Dish.STRING]: () => Promise.resolve(DishString.toArrayBuffer.bind(this)()),
|
||||||
|
[Dish.NUMBER]: () => Promise.resolve(DishNumber.toArrayBuffer.bind(this)()),
|
||||||
|
[Dish.HTML]: () => Promise.resolve(DishHTML.toArrayBuffer.bind(this)()),
|
||||||
|
[Dish.ARRAY_BUFFER]: () => Promise.resolve(),
|
||||||
|
[Dish.BIG_NUMBER]: () => Promise.resolve(DishBigNumber.toArrayBuffer.bind(this)()),
|
||||||
|
[Dish.JSON]: () => Promise.resolve(DishJSON.toArrayBuffer.bind(this)()),
|
||||||
|
[Dish.FILE]: () => DishFile.toArrayBuffer.bind(this)(),
|
||||||
|
[Dish.LIST_FILE]: () => Promise.resolve(DishListFile.toArrayBuffer.bind(this)()),
|
||||||
|
[Dish.BYTE_ARRAY]: () => Promise.resolve(DishByteArray.toArrayBuffer.bind(this)()),
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
[Dish.STRING]: () => DishString.toArrayBuffer.bind(this)(),
|
||||||
|
[Dish.NUMBER]: () => DishNumber.toArrayBuffer.bind(this)(),
|
||||||
|
[Dish.HTML]: () => DishHTML.toArrayBuffer.bind(this)(),
|
||||||
|
[Dish.ARRAY_BUFFER]: () => {},
|
||||||
|
[Dish.BIG_NUMBER]: () => DishBigNumber.toArrayBuffer.bind(this)(),
|
||||||
|
[Dish.JSON]: () => DishJSON.toArrayBuffer.bind(this)(),
|
||||||
|
[Dish.FILE]: () => DishFile.toArrayBuffer.bind(this)(),
|
||||||
|
[Dish.LIST_FILE]: () => DishListFile.toArrayBuffer.bind(this)(),
|
||||||
|
[Dish.BYTE_ARRAY]: () => DishByteArray.toArrayBuffer.bind(this)(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return toByteArrayFuncs[isNodeEnvironment() && "node" || "browser"][this.type]();
|
||||||
|
} catch (err) {
|
||||||
|
throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to ArrayBuffer: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this.value to the given type from ArrayBuffer
|
||||||
|
*
|
||||||
|
* @param {number} toType - the Dish enum to convert to
|
||||||
|
*/
|
||||||
|
_fromArrayBuffer(toType) {
|
||||||
|
|
||||||
|
// Using 'bind' here to allow this.value to be mutated within translation functions
|
||||||
|
const toTypeFunctions = {
|
||||||
|
[Dish.STRING]: () => DishString.fromArrayBuffer.bind(this)(),
|
||||||
|
[Dish.NUMBER]: () => DishNumber.fromArrayBuffer.bind(this)(),
|
||||||
|
[Dish.HTML]: () => DishHTML.fromArrayBuffer.bind(this)(),
|
||||||
|
[Dish.ARRAY_BUFFER]: () => {},
|
||||||
|
[Dish.BIG_NUMBER]: () => DishBigNumber.fromArrayBuffer.bind(this)(),
|
||||||
|
[Dish.JSON]: () => DishJSON.fromArrayBuffer.bind(this)(),
|
||||||
|
[Dish.FILE]: () => DishFile.fromArrayBuffer.bind(this)(),
|
||||||
|
[Dish.LIST_FILE]: () => DishListFile.fromArrayBuffer.bind(this)(),
|
||||||
|
[Dish.BYTE_ARRAY]: () => DishByteArray.fromArrayBuffer.bind(this)(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
toTypeFunctions[toType]();
|
||||||
|
this.type = toType;
|
||||||
|
} catch (err) {
|
||||||
|
throw new DishError(`Error translating from ArrayBuffer to ${Dish.enumLookup(toType)}: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
134
src/core/Ingredient.mjs
Executable file → Normal file
134
src/core/Ingredient.mjs
Executable file → Normal file
@ -4,8 +4,9 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Utils from "./Utils";
|
import Utils from "./Utils.mjs";
|
||||||
import {fromHex} from "./lib/Hex";
|
import { fromHex } from "./lib/Hex.mjs";
|
||||||
|
import OperationError from "./errors/OperationError.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The arguments to operations.
|
* The arguments to operations.
|
||||||
@ -23,9 +24,16 @@ class Ingredient {
|
|||||||
this._value = null;
|
this._value = null;
|
||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
this.hint = "";
|
this.hint = "";
|
||||||
|
this.rows = 0;
|
||||||
this.toggleValues = [];
|
this.toggleValues = [];
|
||||||
this.target = null;
|
this.target = null;
|
||||||
this.defaultIndex = 0;
|
this.defaultIndex = 0;
|
||||||
|
this.maxLength = null;
|
||||||
|
this.min = null;
|
||||||
|
this.max = null;
|
||||||
|
this.step = 1;
|
||||||
|
this.integer = false;
|
||||||
|
this.allowEmpty = true;
|
||||||
|
|
||||||
if (ingredientConfig) {
|
if (ingredientConfig) {
|
||||||
this._parseConfig(ingredientConfig);
|
this._parseConfig(ingredientConfig);
|
||||||
@ -45,9 +53,126 @@ class Ingredient {
|
|||||||
this.defaultValue = ingredientConfig.value;
|
this.defaultValue = ingredientConfig.value;
|
||||||
this.disabled = !!ingredientConfig.disabled;
|
this.disabled = !!ingredientConfig.disabled;
|
||||||
this.hint = ingredientConfig.hint || false;
|
this.hint = ingredientConfig.hint || false;
|
||||||
|
this.rows = ingredientConfig.rows || false;
|
||||||
this.toggleValues = ingredientConfig.toggleValues;
|
this.toggleValues = ingredientConfig.toggleValues;
|
||||||
this.target = typeof ingredientConfig.target !== "undefined" ? ingredientConfig.target : null;
|
this.target = typeof ingredientConfig.target !== "undefined" ? ingredientConfig.target : null;
|
||||||
this.defaultIndex = typeof ingredientConfig.defaultIndex !== "undefined" ? ingredientConfig.defaultIndex : 0;
|
this.defaultIndex = typeof ingredientConfig.defaultIndex !== "undefined" ? ingredientConfig.defaultIndex : 0;
|
||||||
|
this.maxLength = ingredientConfig.maxLength || null;
|
||||||
|
this.min = ingredientConfig.min;
|
||||||
|
this.max = ingredientConfig.max;
|
||||||
|
this.step = ingredientConfig.step;
|
||||||
|
this.integer = typeof ingredientConfig.integer !== "undefined" ? !!ingredientConfig.integer : false;
|
||||||
|
this.allowEmpty = typeof ingredientConfig.allowEmpty !== "undefined" ? !!ingredientConfig.allowEmpty : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the given value against the constraints of this ingredient.
|
||||||
|
*
|
||||||
|
* @param {*} val
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
validate(val) {
|
||||||
|
if (this.disabled) return true;
|
||||||
|
|
||||||
|
let checkVal = val;
|
||||||
|
if (checkVal === null || checkVal === undefined) {
|
||||||
|
checkVal = this.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type === "toggleString" && checkVal && typeof checkVal === "object" && "string" in checkVal) {
|
||||||
|
checkVal = checkVal.string;
|
||||||
|
}
|
||||||
|
if (this.type === "option" && Array.isArray(checkVal)) {
|
||||||
|
checkVal = checkVal[this.defaultIndex ?? 0];
|
||||||
|
}
|
||||||
|
if (this.type === "argSelector" && Array.isArray(checkVal)) {
|
||||||
|
checkVal = checkVal[this.defaultIndex ?? 0]?.name || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. check if empty
|
||||||
|
let isEmpty = false;
|
||||||
|
if (checkVal === null || checkVal === undefined || checkVal === "") {
|
||||||
|
isEmpty = true;
|
||||||
|
} else if (typeof checkVal.length === "number" && checkVal.length === 0) {
|
||||||
|
isEmpty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
let isAllowedOptionEmpty = false;
|
||||||
|
if (this.type === "option" && Array.isArray(this.defaultValue)) {
|
||||||
|
isAllowedOptionEmpty = this.defaultValue.includes("");
|
||||||
|
} else if (this.type === "argSelector" && Array.isArray(this.defaultValue)) {
|
||||||
|
isAllowedOptionEmpty = this.defaultValue.some(opt => opt.name === "");
|
||||||
|
}
|
||||||
|
if (this.allowEmpty === false || ((this.type === "option" || this.type === "argSelector") && !isAllowedOptionEmpty)) {
|
||||||
|
throw new OperationError(`${this.name} cannot be empty.`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. maxLength check
|
||||||
|
if (typeof this.maxLength === "number" && checkVal !== null && checkVal !== undefined) {
|
||||||
|
if (typeof checkVal === "string" && checkVal.length > this.maxLength) {
|
||||||
|
throw new OperationError(`${this.name} length cannot exceed ${this.maxLength}.`);
|
||||||
|
}
|
||||||
|
if (Array.isArray(checkVal) && checkVal.length > this.maxLength) {
|
||||||
|
throw new OperationError(`${this.name} length cannot exceed ${this.maxLength}.`);
|
||||||
|
}
|
||||||
|
if (checkVal instanceof Uint8Array && checkVal.length > this.maxLength) {
|
||||||
|
throw new OperationError(`${this.name} length cannot exceed ${this.maxLength}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. number checks
|
||||||
|
if (this.type === "number") {
|
||||||
|
if (checkVal === null || checkVal === undefined || isNaN(checkVal)) {
|
||||||
|
throw new OperationError(`${this.name} must be a number.`);
|
||||||
|
}
|
||||||
|
if (this.integer && !Number.isInteger(checkVal)) {
|
||||||
|
throw new OperationError(`${this.name} must be an integer.`);
|
||||||
|
}
|
||||||
|
if (typeof this.min === "number" && checkVal < this.min) {
|
||||||
|
throw new OperationError(`${this.name} must be greater than or equal to ${this.min}.`);
|
||||||
|
}
|
||||||
|
if (typeof this.max === "number" && checkVal > this.max) {
|
||||||
|
throw new OperationError(`${this.name} must be less than or equal to ${this.max}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. option checks
|
||||||
|
if (this.type === "option") {
|
||||||
|
if (Array.isArray(this.defaultValue)) {
|
||||||
|
const permittedOptions = this.defaultValue.filter(opt => {
|
||||||
|
if (typeof opt !== "string") return false;
|
||||||
|
return !opt.match(/^\[\/?[a-z0-9 -()^]+\]$/i);
|
||||||
|
});
|
||||||
|
const valStr = (checkVal !== null && checkVal !== undefined) ? String(checkVal).toLowerCase() : "";
|
||||||
|
const matchedOption = permittedOptions.find(opt => opt.toLowerCase() === valStr);
|
||||||
|
if (!matchedOption) {
|
||||||
|
throw new OperationError(`${this.name} must be one of the following: ${permittedOptions.join(", ")}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. argSelector checks
|
||||||
|
if (this.type === "argSelector") {
|
||||||
|
if (Array.isArray(this.defaultValue)) {
|
||||||
|
const permittedOptions = this.defaultValue
|
||||||
|
.map(opt => opt.name)
|
||||||
|
.filter(optName => {
|
||||||
|
if (typeof optName !== "string") return false;
|
||||||
|
return !optName.match(/^\[\/?[a-z0-9 -()^]+\]$/i);
|
||||||
|
});
|
||||||
|
const valStr = (checkVal !== null && checkVal !== undefined) ? String(checkVal).toLowerCase() : "";
|
||||||
|
const matchedOption = permittedOptions.find(opt => opt.toLowerCase() === valStr);
|
||||||
|
if (!matchedOption) {
|
||||||
|
throw new OperationError(`${this.name} must be one of the following: ${permittedOptions.join(", ")}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -105,10 +230,13 @@ class Ingredient {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
case "number":
|
case "number":
|
||||||
|
if (data === null) return data;
|
||||||
number = parseFloat(data);
|
number = parseFloat(data);
|
||||||
if (isNaN(number)) {
|
if (isNaN(number)) {
|
||||||
const sample = Utils.truncate(data.toString(), 10);
|
const sample = Utils.truncate(data.toString(), 10);
|
||||||
throw "Invalid ingredient value. Not a number: " + sample;
|
throw new OperationError(
|
||||||
|
"Invalid ingredient value. Not a number: " + sample,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return number;
|
return number;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -4,8 +4,9 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Dish from "./Dish";
|
import Dish from "./Dish.mjs";
|
||||||
import Ingredient from "./Ingredient";
|
import OperationError from "./errors/OperationError.mjs";
|
||||||
|
import Ingredient from "./Ingredient.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Operation specified by the user to be run.
|
* The Operation specified by the user to be run.
|
||||||
@ -23,6 +24,7 @@ class Operation {
|
|||||||
this._breakpoint = false;
|
this._breakpoint = false;
|
||||||
this._disabled = false;
|
this._disabled = false;
|
||||||
this._flowControl = false;
|
this._flowControl = false;
|
||||||
|
this._manualBake = false;
|
||||||
this._ingList = [];
|
this._ingList = [];
|
||||||
|
|
||||||
// Public fields
|
// Public fields
|
||||||
@ -179,14 +181,38 @@ class Operation {
|
|||||||
|
|
||||||
if (ing.toggleValues) conf.toggleValues = ing.toggleValues;
|
if (ing.toggleValues) conf.toggleValues = ing.toggleValues;
|
||||||
if (ing.hint) conf.hint = ing.hint;
|
if (ing.hint) conf.hint = ing.hint;
|
||||||
|
if (ing.rows) conf.rows = ing.rows;
|
||||||
if (ing.disabled) conf.disabled = ing.disabled;
|
if (ing.disabled) conf.disabled = ing.disabled;
|
||||||
if (ing.target) conf.target = ing.target;
|
if (ing.target) conf.target = ing.target;
|
||||||
if (ing.defaultIndex) conf.defaultIndex = ing.defaultIndex;
|
if (ing.defaultIndex) conf.defaultIndex = ing.defaultIndex;
|
||||||
|
if (ing.maxLength) conf.maxLength = ing.maxLength;
|
||||||
|
if (typeof ing.min === "number") conf.min = ing.min;
|
||||||
|
if (typeof ing.max === "number") conf.max = ing.max;
|
||||||
|
if (ing.step) conf.step = ing.step;
|
||||||
|
if (typeof ing.integer !== "undefined") conf.integer = ing.integer;
|
||||||
|
if (typeof ing.allowEmpty !== "undefined") conf.allowEmpty = ing.allowEmpty;
|
||||||
return conf;
|
return conf;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the operation's ingredients against their defined constraints.
|
||||||
|
*
|
||||||
|
* @param {Object[]} [args] - Optional list of argument values to validate. If not provided, validates the current ingredient values.
|
||||||
|
* @returns {boolean} - True if valid, throws an OperationError if invalid.
|
||||||
|
*/
|
||||||
|
validateIngredients(args) {
|
||||||
|
const values = args || this.ingValues;
|
||||||
|
this._ingList.forEach((ing, i) => {
|
||||||
|
if (i < values.length) {
|
||||||
|
ing.validate(values[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the value of the Operation as it should be displayed in a recipe config.
|
* Returns the value of the Operation as it should be displayed in a recipe config.
|
||||||
*
|
*
|
||||||
@ -217,7 +243,11 @@ class Operation {
|
|||||||
*/
|
*/
|
||||||
set ingValues(ingValues) {
|
set ingValues(ingValues) {
|
||||||
ingValues.forEach((val, i) => {
|
ingValues.forEach((val, i) => {
|
||||||
this._ingList[i].value = val;
|
try {
|
||||||
|
this._ingList[i].value = val;
|
||||||
|
} catch (err) {
|
||||||
|
throw new OperationError(`Failed to set value of ingredient '${this._ingList[i].name}': ${err}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,6 +311,7 @@ class Operation {
|
|||||||
return this._flowControl;
|
return this._flowControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set whether this Operation is a flowcontrol op.
|
* Set whether this Operation is a flowcontrol op.
|
||||||
*
|
*
|
||||||
@ -290,6 +321,26 @@ class Operation {
|
|||||||
this._flowControl = !!value;
|
this._flowControl = !!value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this Operation should not trigger AutoBake.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
get manualBake() {
|
||||||
|
return this._manualBake;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether this Operation should trigger AutoBake.
|
||||||
|
*
|
||||||
|
* @param {boolean} value
|
||||||
|
*/
|
||||||
|
set manualBake(value) {
|
||||||
|
this._manualBake = !!value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Operation;
|
export default Operation;
|
||||||
|
|||||||
@ -4,11 +4,15 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import OpModules from "./config/modules/OpModules";
|
import OperationConfig from "./config/OperationConfig.json" with { type: "json" };
|
||||||
import OperationConfig from "./config/OperationConfig.json";
|
import OperationError from "./errors/OperationError.mjs";
|
||||||
import OperationError from "./errors/OperationError";
|
import Operation from "./Operation.mjs";
|
||||||
import DishError from "./errors/DishError";
|
import DishError from "./errors/DishError.mjs";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
import { isWorkerEnvironment } from "./Utils.mjs";
|
||||||
|
|
||||||
|
// Cache container for modules
|
||||||
|
let modules = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Recipe controls a list of Operations and the Dish they operate on.
|
* The Recipe controls a list of Operations and the Dish they operate on.
|
||||||
@ -36,16 +40,47 @@ class Recipe {
|
|||||||
* @param {Object} recipeConfig
|
* @param {Object} recipeConfig
|
||||||
*/
|
*/
|
||||||
_parseConfig(recipeConfig) {
|
_parseConfig(recipeConfig) {
|
||||||
for (let c = 0; c < recipeConfig.length; c++) {
|
recipeConfig.forEach(c => {
|
||||||
const operationName = recipeConfig[c].op;
|
this.opList.push({
|
||||||
const opConf = OperationConfig[operationName];
|
name: c.op,
|
||||||
const opObj = OpModules[opConf.module][operationName];
|
module: OperationConfig[c.op].module,
|
||||||
const operation = new opObj();
|
ingValues: c.args,
|
||||||
operation.ingValues = recipeConfig[c].args;
|
breakpoint: c.breakpoint,
|
||||||
operation.breakpoint = recipeConfig[c].breakpoint;
|
disabled: c.disabled || c.op === "Comment",
|
||||||
operation.disabled = recipeConfig[c].disabled;
|
});
|
||||||
this.addOperation(operation);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate elements of opList with operation instances.
|
||||||
|
* Dynamic import here removes top-level cyclic dependency issue.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _hydrateOpList() {
|
||||||
|
if (!modules) {
|
||||||
|
// Using Webpack Magic Comments to force the dynamic import to be included in the main chunk
|
||||||
|
// https://webpack.js.org/api/module-methods/
|
||||||
|
modules = await import(/* webpackMode: "eager" */ "./config/modules/OpModules.mjs");
|
||||||
|
modules = modules.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.opList = this.opList.map(o => {
|
||||||
|
if (o instanceof Operation) {
|
||||||
|
return o;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const op = new modules[o.module][o.name]();
|
||||||
|
op.ingValues = o.ingValues;
|
||||||
|
op.breakpoint = o.breakpoint;
|
||||||
|
op.disabled = o.disabled;
|
||||||
|
return op;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to hydrate operation '${o.name}': ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -55,7 +90,10 @@ class Recipe {
|
|||||||
* @returns {Object[]}
|
* @returns {Object[]}
|
||||||
*/
|
*/
|
||||||
get config() {
|
get config() {
|
||||||
return this.opList.map(op => op.config);
|
return this.opList.map(op => ({
|
||||||
|
op: op.name,
|
||||||
|
args: op.ingValues,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -75,7 +113,19 @@ class Recipe {
|
|||||||
* @param {Operation[]} operations
|
* @param {Operation[]} operations
|
||||||
*/
|
*/
|
||||||
addOperations(operations) {
|
addOperations(operations) {
|
||||||
this.opList = this.opList.concat(operations);
|
operations.forEach(o => {
|
||||||
|
if (o instanceof Operation) {
|
||||||
|
this.opList.push(o);
|
||||||
|
} else {
|
||||||
|
this.opList.push({
|
||||||
|
name: o.name,
|
||||||
|
module: o.module,
|
||||||
|
ingValues: o.args,
|
||||||
|
breakpoint: o.breakpoint,
|
||||||
|
disabled: o.disabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -108,7 +158,7 @@ class Recipe {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if there is an Flow Control Operation in this Recipe.
|
* Returns true if there is a Flow Control Operation in this Recipe.
|
||||||
*
|
*
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
@ -137,6 +187,8 @@ class Recipe {
|
|||||||
|
|
||||||
if (startFrom === 0) this.lastRunOp = null;
|
if (startFrom === 0) this.lastRunOp = null;
|
||||||
|
|
||||||
|
await this._hydrateOpList();
|
||||||
|
|
||||||
log.debug(`[*] Executing recipe of ${this.opList.length} operations, starting at ${startFrom}`);
|
log.debug(`[*] Executing recipe of ${this.opList.length} operations, starting at ${startFrom}`);
|
||||||
|
|
||||||
for (let i = startFrom; i < this.opList.length; i++) {
|
for (let i = startFrom; i < this.opList.length; i++) {
|
||||||
@ -153,7 +205,14 @@ class Recipe {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
input = await dish.get(op.inputType);
|
input = await dish.get(op.inputType);
|
||||||
log.debug("Executing operation");
|
log.debug(`Executing operation '${op.name}'`);
|
||||||
|
|
||||||
|
if (isWorkerEnvironment()) {
|
||||||
|
self.sendStatusMessage(`Baking... (${i+1}/${this.opList.length})`);
|
||||||
|
self.sendProgressMessage(i + 1, this.opList.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
op.validateIngredients(op.ingValues);
|
||||||
|
|
||||||
if (op.flowControl) {
|
if (op.flowControl) {
|
||||||
// Package up the current state
|
// Package up the current state
|
||||||
@ -176,16 +235,17 @@ class Recipe {
|
|||||||
}
|
}
|
||||||
this.lastRunOp = op;
|
this.lastRunOp = op;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
log.error(err);
|
||||||
// Return expected errors as output
|
// Return expected errors as output
|
||||||
if (err instanceof OperationError ||
|
if (err instanceof OperationError || err?.type === "OperationError") {
|
||||||
(err.type && err.type === "OperationError")) {
|
|
||||||
// Cannot rely on `err instanceof OperationError` here as extending
|
// Cannot rely on `err instanceof OperationError` here as extending
|
||||||
// native types is not fully supported yet.
|
// native types is not fully supported yet.
|
||||||
dish.set(err.message, "string");
|
dish.set(err.message, "string");
|
||||||
|
this.lastRunOp = null;
|
||||||
return i;
|
return i;
|
||||||
} else if (err instanceof DishError ||
|
} else if (err instanceof DishError || err?.type === "DishError") {
|
||||||
(err.type && err.type === "DishError")) {
|
|
||||||
dish.set(err.message, "string");
|
dish.set(err.message, "string");
|
||||||
|
this.lastRunOp = null;
|
||||||
return i;
|
return i;
|
||||||
} else {
|
} else {
|
||||||
const e = typeof err == "string" ? { message: err } : err;
|
const e = typeof err == "string" ? { message: err } : err;
|
||||||
@ -254,7 +314,8 @@ class Recipe {
|
|||||||
* @returns {function} highlights[].b
|
* @returns {function} highlights[].b
|
||||||
* @returns {Object[]} highlights[].args
|
* @returns {Object[]} highlights[].args
|
||||||
*/
|
*/
|
||||||
generateHighlightList() {
|
async generateHighlightList() {
|
||||||
|
await this._hydrateOpList();
|
||||||
const highlights = [];
|
const highlights = [];
|
||||||
|
|
||||||
for (let i = 0; i < this.opList.length; i++) {
|
for (let i = 0; i < this.opList.length; i++) {
|
||||||
|
|||||||
@ -4,12 +4,13 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// loglevel import required for Node API
|
||||||
|
import log from "loglevel";
|
||||||
import utf8 from "utf8";
|
import utf8 from "utf8";
|
||||||
import {fromBase64, toBase64} from "./lib/Base64";
|
import {fromBase64, toBase64} from "./lib/Base64.mjs";
|
||||||
import {fromHex} from "./lib/Hex";
|
import {fromHex} from "./lib/Hex.mjs";
|
||||||
import {fromDecimal} from "./lib/Decimal";
|
import {fromDecimal} from "./lib/Decimal.mjs";
|
||||||
import {fromBinary} from "./lib/Binary";
|
import {fromBinary} from "./lib/Binary.mjs";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility functions for use in operations, the core framework and the stage.
|
* Utility functions for use in operations, the core framework and the stage.
|
||||||
@ -95,7 +96,7 @@ class Utils {
|
|||||||
const paddedBytes = new Array(numBytes);
|
const paddedBytes = new Array(numBytes);
|
||||||
paddedBytes.fill(padByte);
|
paddedBytes.fill(padByte);
|
||||||
|
|
||||||
Array.prototype.map.call(arr, function(b, i) {
|
[...arr].forEach((b, i) => {
|
||||||
paddedBytes[i] = b;
|
paddedBytes[i] = b;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -171,15 +172,17 @@ class Utils {
|
|||||||
*
|
*
|
||||||
* @param {string} str - The input string to display.
|
* @param {string} str - The input string to display.
|
||||||
* @param {boolean} [preserveWs=false] - Whether or not to print whitespace.
|
* @param {boolean} [preserveWs=false] - Whether or not to print whitespace.
|
||||||
|
* @param {boolean} [onlyAscii=false] - Whether or not to replace non ASCII characters.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
static printable(str, preserveWs=false) {
|
static printable(str, preserveWs=false, onlyAscii=false) {
|
||||||
if (ENVIRONMENT_IS_WEB() && window.app && !window.app.options.treatAsUtf8) {
|
if (onlyAscii) {
|
||||||
str = Utils.byteArrayToChars(Utils.strToByteArray(str));
|
return str.replace(/[^\x20-\x7e]/g, ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-misleading-character-class
|
||||||
const re = /[\0-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g;
|
const re = /[\0-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g;
|
||||||
const wsRe = /[\x09-\x10\x0D\u2028\u2029]/g;
|
const wsRe = /[\x09-\x10\u2028\u2029]/g;
|
||||||
|
|
||||||
str = str.replace(re, ".");
|
str = str.replace(re, ".");
|
||||||
if (!preserveWs) str = str.replace(wsRe, ".");
|
if (!preserveWs) str = str.replace(wsRe, ".");
|
||||||
@ -187,6 +190,21 @@ class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string with whitespace represented as special characters from the
|
||||||
|
* Unicode Private Use Area, which CyberChef will display as control characters.
|
||||||
|
* Private Use Area characters are in the range U+E000..U+F8FF.
|
||||||
|
* https://en.wikipedia.org/wiki/Private_Use_Areas
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static escapeWhitespace(str) {
|
||||||
|
return str.replace(/[\x09-\x10]/g, function(c) {
|
||||||
|
return String.fromCharCode(0xe000 + c.charCodeAt(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a string entered by a user and replace escaped chars with the bytes they represent.
|
* Parse a string entered by a user and replace escaped chars with the bytes they represent.
|
||||||
*
|
*
|
||||||
@ -201,11 +219,21 @@ class Utils {
|
|||||||
* Utils.parseEscapedChars("\\n");
|
* Utils.parseEscapedChars("\\n");
|
||||||
*/
|
*/
|
||||||
static parseEscapedChars(str) {
|
static parseEscapedChars(str) {
|
||||||
return str.replace(/(\\)?\\([bfnrtv0'"]|x[\da-fA-F]{2}|u[\da-fA-F]{4}|u\{[\da-fA-F]{1,6}\})/g, function(m, a, b) {
|
return str.replace(/\\([abfnrtv'"]|[0-3][0-7]{2}|[0-7]{1,2}|x[\da-fA-F]{2}|u[\da-fA-F]{4}|u\{[\da-fA-F]{1,6}\}|\\)/g, function(m, a) {
|
||||||
if (a === "\\") return "\\"+b;
|
switch (a[0]) {
|
||||||
switch (b[0]) {
|
case "\\":
|
||||||
|
return "\\";
|
||||||
case "0":
|
case "0":
|
||||||
return "\0";
|
case "1":
|
||||||
|
case "2":
|
||||||
|
case "3":
|
||||||
|
case "4":
|
||||||
|
case "5":
|
||||||
|
case "6":
|
||||||
|
case "7":
|
||||||
|
return String.fromCharCode(parseInt(a, 8));
|
||||||
|
case "a":
|
||||||
|
return String.fromCharCode(7);
|
||||||
case "b":
|
case "b":
|
||||||
return "\b";
|
return "\b";
|
||||||
case "t":
|
case "t":
|
||||||
@ -223,12 +251,12 @@ class Utils {
|
|||||||
case "'":
|
case "'":
|
||||||
return "'";
|
return "'";
|
||||||
case "x":
|
case "x":
|
||||||
return String.fromCharCode(parseInt(b.substr(1), 16));
|
return String.fromCharCode(parseInt(a.substr(1), 16));
|
||||||
case "u":
|
case "u":
|
||||||
if (b[1] === "{")
|
if (a[1] === "{")
|
||||||
return String.fromCodePoint(parseInt(b.slice(2, -1), 16));
|
return String.fromCodePoint(parseInt(a.slice(2, -1), 16));
|
||||||
else
|
else
|
||||||
return String.fromCharCode(parseInt(b.substr(1), 16));
|
return String.fromCharCode(parseInt(a.substr(1), 16));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -367,6 +395,131 @@ class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a byte array to an integer.
|
||||||
|
*
|
||||||
|
* @param {byteArray} byteArray
|
||||||
|
* @param {string} byteorder - "little" or "big"
|
||||||
|
* @returns {integer}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // returns 67305985
|
||||||
|
* Utils.byteArrayToInt([1, 2, 3, 4], "little");
|
||||||
|
*
|
||||||
|
* // returns 16909060
|
||||||
|
* Utils.byteArrayToInt([1, 2, 3, 4], "big");
|
||||||
|
*/
|
||||||
|
static byteArrayToInt(byteArray, byteorder) {
|
||||||
|
let value = 0;
|
||||||
|
if (byteorder === "big") {
|
||||||
|
for (let i = 0; i < byteArray.length; i++) {
|
||||||
|
value = (value * 256) + byteArray[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = byteArray.length - 1; i >= 0; i--) {
|
||||||
|
value = (value * 256) + byteArray[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an integer to a byte array of {length} bytes.
|
||||||
|
*
|
||||||
|
* @param {integer} value
|
||||||
|
* @param {integer} length
|
||||||
|
* @param {string} byteorder - "little" or "big"
|
||||||
|
* @returns {byteArray}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // returns [5, 255, 109, 1]
|
||||||
|
* Utils.intToByteArray(23985925, 4, "little");
|
||||||
|
*
|
||||||
|
* // returns [1, 109, 255, 5]
|
||||||
|
* Utils.intToByteArray(23985925, 4, "big");
|
||||||
|
*
|
||||||
|
* // returns [0, 0, 0, 0, 1, 109, 255, 5]
|
||||||
|
* Utils.intToByteArray(23985925, 8, "big");
|
||||||
|
*/
|
||||||
|
static intToByteArray(value, length, byteorder) {
|
||||||
|
const arr = new Array(length);
|
||||||
|
if (byteorder === "little") {
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
arr[i] = value & 0xFF;
|
||||||
|
value = value >>> 8;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = length - 1; i >= 0; i--) {
|
||||||
|
arr[i] = value & 0xFF;
|
||||||
|
value = value >>> 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to an ArrayBuffer.
|
||||||
|
* Treats the string as UTF-8 if any values are over 255.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // returns [72,101,108,108,111]
|
||||||
|
* Utils.strToArrayBuffer("Hello");
|
||||||
|
*
|
||||||
|
* // returns [228,189,160,229,165,189]
|
||||||
|
* Utils.strToArrayBuffer("你好");
|
||||||
|
*/
|
||||||
|
static strToArrayBuffer(str) {
|
||||||
|
log.debug(`Converting string[${str?.length}] to array buffer`);
|
||||||
|
if (!str) return new ArrayBuffer;
|
||||||
|
|
||||||
|
const arr = new Uint8Array(str.length);
|
||||||
|
let i = str.length, b;
|
||||||
|
while (i--) {
|
||||||
|
b = str.charCodeAt(i);
|
||||||
|
arr[i] = b;
|
||||||
|
// If any of the bytes are over 255, read as UTF-8
|
||||||
|
if (b > 255) return Utils.strToUtf8ArrayBuffer(str);
|
||||||
|
}
|
||||||
|
return arr.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to a UTF-8 ArrayBuffer.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // returns [72,101,108,108,111]
|
||||||
|
* Utils.strToUtf8ArrayBuffer("Hello");
|
||||||
|
*
|
||||||
|
* // returns [228,189,160,229,165,189]
|
||||||
|
* Utils.strToUtf8ArrayBuffer("你好");
|
||||||
|
*/
|
||||||
|
static strToUtf8ArrayBuffer(str) {
|
||||||
|
log.debug(`Converting string[${str?.length}] to UTF8 array buffer`);
|
||||||
|
if (!str) return new ArrayBuffer;
|
||||||
|
|
||||||
|
const buffer = new TextEncoder("utf-8").encode(str);
|
||||||
|
|
||||||
|
if (str.length !== buffer.length) {
|
||||||
|
if (isWorkerEnvironment() && self && typeof self.setOption === "function") {
|
||||||
|
self.setOption("attemptHighlight", false);
|
||||||
|
} else if (isWebEnvironment()) {
|
||||||
|
window.app.options.attemptHighlight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a string to a byte array.
|
* Converts a string to a byte array.
|
||||||
* Treats the string as UTF-8 if any values are over 255.
|
* Treats the string as UTF-8 if any values are over 255.
|
||||||
@ -382,6 +535,8 @@ class Utils {
|
|||||||
* Utils.strToByteArray("你好");
|
* Utils.strToByteArray("你好");
|
||||||
*/
|
*/
|
||||||
static strToByteArray(str) {
|
static strToByteArray(str) {
|
||||||
|
log.debug(`Converting string[${str?.length}] to byte array`);
|
||||||
|
if (!str) return [];
|
||||||
const byteArray = new Array(str.length);
|
const byteArray = new Array(str.length);
|
||||||
let i = str.length, b;
|
let i = str.length, b;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
@ -408,12 +563,14 @@ class Utils {
|
|||||||
* Utils.strToUtf8ByteArray("你好");
|
* Utils.strToUtf8ByteArray("你好");
|
||||||
*/
|
*/
|
||||||
static strToUtf8ByteArray(str) {
|
static strToUtf8ByteArray(str) {
|
||||||
|
log.debug(`Converting string[${str?.length}] to UTF8 byte array`);
|
||||||
|
if (!str) return [];
|
||||||
const utf8Str = utf8.encode(str);
|
const utf8Str = utf8.encode(str);
|
||||||
|
|
||||||
if (str.length !== utf8Str.length) {
|
if (str.length !== utf8Str.length) {
|
||||||
if (ENVIRONMENT_IS_WORKER()) {
|
if (isWorkerEnvironment()) {
|
||||||
self.setOption("attemptHighlight", false);
|
self.setOption("attemptHighlight", false);
|
||||||
} else if (ENVIRONMENT_IS_WEB()) {
|
} else if (isWebEnvironment()) {
|
||||||
window.app.options.attemptHighlight = false;
|
window.app.options.attemptHighlight = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -436,6 +593,8 @@ class Utils {
|
|||||||
* Utils.strToCharcode("你好");
|
* Utils.strToCharcode("你好");
|
||||||
*/
|
*/
|
||||||
static strToCharcode(str) {
|
static strToCharcode(str) {
|
||||||
|
log.debug(`Converting string[${str?.length}] to charcode`);
|
||||||
|
if (!str) return [];
|
||||||
const charcode = [];
|
const charcode = [];
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
@ -459,7 +618,7 @@ class Utils {
|
|||||||
/**
|
/**
|
||||||
* Attempts to convert a byte array to a UTF-8 string.
|
* Attempts to convert a byte array to a UTF-8 string.
|
||||||
*
|
*
|
||||||
* @param {byteArray} byteArray
|
* @param {byteArray|Uint8Array} byteArray
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@ -470,21 +629,26 @@ class Utils {
|
|||||||
* Utils.byteArrayToUtf8([228,189,160,229,165,189]);
|
* Utils.byteArrayToUtf8([228,189,160,229,165,189]);
|
||||||
*/
|
*/
|
||||||
static byteArrayToUtf8(byteArray) {
|
static byteArrayToUtf8(byteArray) {
|
||||||
const str = Utils.byteArrayToChars(byteArray);
|
log.debug(`Converting byte array[${byteArray?.length}] to UTF8`);
|
||||||
try {
|
if (!byteArray || !byteArray.length) return "";
|
||||||
const utf8Str = utf8.decode(str);
|
if (!(byteArray instanceof Uint8Array))
|
||||||
|
byteArray = new Uint8Array(byteArray);
|
||||||
|
|
||||||
if (str.length !== utf8Str.length) {
|
try {
|
||||||
if (ENVIRONMENT_IS_WORKER()) {
|
const str = new TextDecoder("utf-8", {fatal: true}).decode(byteArray);
|
||||||
|
|
||||||
|
if (str.length !== byteArray.length) {
|
||||||
|
if (isWorkerEnvironment()) {
|
||||||
self.setOption("attemptHighlight", false);
|
self.setOption("attemptHighlight", false);
|
||||||
} else if (ENVIRONMENT_IS_WEB()) {
|
} else if (isWebEnvironment()) {
|
||||||
window.app.options.attemptHighlight = false;
|
window.app.options.attemptHighlight = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return utf8Str;
|
|
||||||
|
return str;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If it fails, treat it as ANSI
|
// If it fails, treat it as ANSI
|
||||||
return str;
|
return Utils.byteArrayToChars(byteArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -503,10 +667,13 @@ class Utils {
|
|||||||
* Utils.byteArrayToChars([20320,22909]);
|
* Utils.byteArrayToChars([20320,22909]);
|
||||||
*/
|
*/
|
||||||
static byteArrayToChars(byteArray) {
|
static byteArrayToChars(byteArray) {
|
||||||
if (!byteArray) return "";
|
log.debug(`Converting byte array[${byteArray?.length}] to chars`);
|
||||||
|
if (!byteArray || !byteArray.length) return "";
|
||||||
let str = "";
|
let str = "";
|
||||||
for (let i = 0; i < byteArray.length;) {
|
// Maxiumum arg length for fromCharCode is 65535, but the stack may already be fairly deep,
|
||||||
str += String.fromCharCode(byteArray[i++]);
|
// so don't get too near it.
|
||||||
|
for (let i = 0; i < byteArray.length; i += 20000) {
|
||||||
|
str += String.fromCharCode(...(byteArray.slice(i, i+20000)));
|
||||||
}
|
}
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
@ -524,8 +691,48 @@ class Utils {
|
|||||||
* Utils.arrayBufferToStr(Uint8Array.from([104,101,108,108,111]).buffer);
|
* Utils.arrayBufferToStr(Uint8Array.from([104,101,108,108,111]).buffer);
|
||||||
*/
|
*/
|
||||||
static arrayBufferToStr(arrayBuffer, utf8=true) {
|
static arrayBufferToStr(arrayBuffer, utf8=true) {
|
||||||
const byteArray = Array.prototype.slice.call(new Uint8Array(arrayBuffer));
|
log.debug(`Converting array buffer[${arrayBuffer?.byteLength}] to str`);
|
||||||
return utf8 ? Utils.byteArrayToUtf8(byteArray) : Utils.byteArrayToChars(byteArray);
|
if (!arrayBuffer || !arrayBuffer.byteLength) return "";
|
||||||
|
const arr = new Uint8Array(arrayBuffer);
|
||||||
|
return utf8 ? Utils.byteArrayToUtf8(arr) : Utils.byteArrayToChars(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the Shannon entropy for a given set of data.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array|ArrayBuffer} input
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static calculateShannonEntropy(data) {
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
data = new Uint8Array(data);
|
||||||
|
}
|
||||||
|
const prob = [],
|
||||||
|
occurrences = new Array(256).fill(0);
|
||||||
|
|
||||||
|
// Count occurrences of each byte in the input
|
||||||
|
let i;
|
||||||
|
for (i = 0; i < data.length; i++) {
|
||||||
|
occurrences[data[i]]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store probability list
|
||||||
|
for (i = 0; i < occurrences.length; i++) {
|
||||||
|
if (occurrences[i] > 0) {
|
||||||
|
prob.push(occurrences[i] / data.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Shannon entropy
|
||||||
|
let entropy = 0,
|
||||||
|
p;
|
||||||
|
|
||||||
|
for (i = 0; i < prob.length; i++) {
|
||||||
|
p = prob[i];
|
||||||
|
entropy += p * Math.log(p) / Math.log(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return -entropy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -603,10 +810,24 @@ class Utils {
|
|||||||
* Utils.stripHtmlTags("<div>Test</div>");
|
* Utils.stripHtmlTags("<div>Test</div>");
|
||||||
*/
|
*/
|
||||||
static stripHtmlTags(htmlStr, removeScriptAndStyle=false) {
|
static stripHtmlTags(htmlStr, removeScriptAndStyle=false) {
|
||||||
if (removeScriptAndStyle) {
|
/**
|
||||||
htmlStr = htmlStr.replace(/<(script|style)[^>]*>.*<\/(script|style)>/gmi, "");
|
* Recursively remove a pattern from a string until there are no more matches.
|
||||||
|
* Avoids incomplete sanitization e.g. "aabcbc".replace(/abc/g, "") === "abc"
|
||||||
|
*
|
||||||
|
* @param {RegExp} pattern
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function recursiveRemove(pattern, str) {
|
||||||
|
const newStr = str.replace(pattern, "");
|
||||||
|
return newStr.length === str.length ? newStr : recursiveRemove(pattern, newStr);
|
||||||
}
|
}
|
||||||
return htmlStr.replace(/<[^>]+>/g, "");
|
|
||||||
|
if (removeScriptAndStyle) {
|
||||||
|
htmlStr = recursiveRemove(/<script[^>]*>(\s|\S)*?<\/script[^>]*>/gi, htmlStr);
|
||||||
|
htmlStr = recursiveRemove(/<style[^>]*>(\s|\S)*?<\/style[^>]*>/gi, htmlStr);
|
||||||
|
}
|
||||||
|
return recursiveRemove(/<[^>]+>/g, htmlStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -614,6 +835,11 @@ class Utils {
|
|||||||
* Escapes HTML tags in a string to stop them being rendered.
|
* Escapes HTML tags in a string to stop them being rendered.
|
||||||
* https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
|
* https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
|
||||||
*
|
*
|
||||||
|
* Null bytes are a special case and are converted to a character from the Unicode
|
||||||
|
* Private Use Area, which CyberChef will display as a control character picture.
|
||||||
|
* This is done due to null bytes not being rendered or stored correctly in HTML
|
||||||
|
* DOM building.
|
||||||
|
*
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
* @returns string
|
* @returns string
|
||||||
*
|
*
|
||||||
@ -628,13 +854,13 @@ class Utils {
|
|||||||
">": ">",
|
">": ">",
|
||||||
'"': """,
|
'"': """,
|
||||||
"'": "'", // ' not recommended because it's not in the HTML spec
|
"'": "'", // ' not recommended because it's not in the HTML spec
|
||||||
"/": "/", // forward slash is included as it helps end an HTML entity
|
"`": "`",
|
||||||
"`": "`"
|
"\u0000": "\ue000"
|
||||||
};
|
};
|
||||||
|
|
||||||
return str.replace(/[&<>"'/`]/g, function (match) {
|
return str ? str.replace(/[&<>"'`\u0000]/g, function (match) {
|
||||||
return HTML_CHARS[match];
|
return HTML_CHARS[match];
|
||||||
});
|
}) : str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -656,15 +882,33 @@ class Utils {
|
|||||||
""": '"',
|
""": '"',
|
||||||
"'": "'",
|
"'": "'",
|
||||||
"/": "/",
|
"/": "/",
|
||||||
"`": "`"
|
"`": "`",
|
||||||
|
"\ue000": "\u0000"
|
||||||
};
|
};
|
||||||
|
|
||||||
return str.replace(/&#?x?[a-z0-9]{2,4};/ig, function (match) {
|
return str.replace(/(&#?x?[a-z0-9]{2,4};|\ue000)/ig, function (match) {
|
||||||
return HTML_CHARS[match] || match;
|
return HTML_CHARS[match] || match;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to its title case equivalent.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @returns string
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // return "A Tiny String"
|
||||||
|
* Utils.toTitleCase("a tIny String");
|
||||||
|
*/
|
||||||
|
static toTitleCase(str) {
|
||||||
|
return str.replace(/\w\S*/g, function(txt) {
|
||||||
|
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes a URI fragment (#) or query (?) using a minimal amount of percent-encoding.
|
* Encodes a URI fragment (#) or query (?) using a minimal amount of percent-encoding.
|
||||||
*
|
*
|
||||||
@ -695,15 +939,15 @@ class Utils {
|
|||||||
"%7E": "~",
|
"%7E": "~",
|
||||||
"%21": "!",
|
"%21": "!",
|
||||||
"%24": "$",
|
"%24": "$",
|
||||||
//"%26": "&",
|
// "%26": "&",
|
||||||
"%27": "'",
|
"%27": "'",
|
||||||
"%28": "(",
|
"%28": "(",
|
||||||
"%29": ")",
|
"%29": ")",
|
||||||
"%2A": "*",
|
"%2A": "*",
|
||||||
//"%2B": "+",
|
// "%2B": "+",
|
||||||
"%2C": ",",
|
"%2C": ",",
|
||||||
"%3B": ";",
|
"%3B": ";",
|
||||||
//"%3D": "=",
|
// "%3D": "=",
|
||||||
"%3A": ":",
|
"%3A": ":",
|
||||||
"%40": "@",
|
"%40": "@",
|
||||||
"%2F": "/",
|
"%2F": "/",
|
||||||
@ -771,16 +1015,17 @@ class Utils {
|
|||||||
|
|
||||||
// Parse bespoke recipe format
|
// Parse bespoke recipe format
|
||||||
recipe = recipe.replace(/\n/g, "");
|
recipe = recipe.replace(/\n/g, "");
|
||||||
|
Utils._validatePrettyRecipe(recipe);
|
||||||
let m, args;
|
let m, args;
|
||||||
const recipeRegex = /([^(]+)\(((?:'[^'\\]*(?:\\.[^'\\]*)*'|[^)/'])*)(\/[^)]+)?\)/g,
|
const recipeRegex = /([^(]+)\(((?:'[^'\\]*(?:\\.[^'\\]*)*'|[^)/'])*)(\/[^)]+)?\)/g,
|
||||||
recipeConfig = [];
|
recipeConfig = [];
|
||||||
|
|
||||||
while ((m = recipeRegex.exec(recipe))) {
|
while ((m = recipeRegex.exec(recipe))) {
|
||||||
// Translate strings in args back to double-quotes
|
// Translate strings in args back to double-quotes
|
||||||
args = m[2]
|
args = m[2] // lgtm [js/incomplete-sanitization]
|
||||||
.replace(/"/g, '\\"') // Escape double quotes
|
.replace(/"/g, '\\"') // Escape double quotes
|
||||||
.replace(/(^|,|{|:)'/g, '$1"') // Replace opening ' with "
|
.replace(/(^|,|{|:)'/g, '$1"') // Replace opening ' with "
|
||||||
.replace(/([^\\]|[^\\]\\\\)'(,|:|}|$)/g, '$1"$2') // Replace closing ' with "
|
.replace(/([^\\]|(?:\\\\)+)'(,|:|}|$)/g, '$1"$2') // Replace closing ' with "
|
||||||
.replace(/\\'/g, "'"); // Unescape single quotes
|
.replace(/\\'/g, "'"); // Unescape single quotes
|
||||||
args = "[" + args + "]";
|
args = "[" + args + "]";
|
||||||
|
|
||||||
@ -796,6 +1041,53 @@ class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a linear structural validation pass over pretty recipe syntax.
|
||||||
|
*
|
||||||
|
* @param {string} recipe
|
||||||
|
* @throws {Error} if the recipe is structurally invalid
|
||||||
|
*/
|
||||||
|
static _validatePrettyRecipe(recipe) {
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < recipe.length) {
|
||||||
|
const openParen = recipe.indexOf("(", i);
|
||||||
|
if (openParen === -1 || openParen === i) {
|
||||||
|
throw new Error("Invalid recipe");
|
||||||
|
}
|
||||||
|
|
||||||
|
i = openParen + 1;
|
||||||
|
let inString = false,
|
||||||
|
escaped = false,
|
||||||
|
foundCloseParen = false;
|
||||||
|
|
||||||
|
for (; i < recipe.length; i++) {
|
||||||
|
const c = recipe[i];
|
||||||
|
|
||||||
|
if (inString) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
} else if (c === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
} else if (c === "'") {
|
||||||
|
inString = false;
|
||||||
|
}
|
||||||
|
} else if (c === "'") {
|
||||||
|
inString = true;
|
||||||
|
} else if (c === ")") {
|
||||||
|
foundCloseParen = true;
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundCloseParen || inString || escaped) {
|
||||||
|
throw new Error("Invalid recipe");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a list of files or directories.
|
* Formats a list of files or directories.
|
||||||
*
|
*
|
||||||
@ -832,8 +1124,9 @@ class Utils {
|
|||||||
const buff = await Utils.readFile(file);
|
const buff = await Utils.readFile(file);
|
||||||
const blob = new Blob(
|
const blob = new Blob(
|
||||||
[buff],
|
[buff],
|
||||||
{type: "octet/stream"}
|
{type: file.type || "octet/stream"}
|
||||||
);
|
);
|
||||||
|
const blobURL = URL.createObjectURL(blob);
|
||||||
|
|
||||||
const html = `<div class='card' style='white-space: normal;'>
|
const html = `<div class='card' style='white-space: normal;'>
|
||||||
<div class='card-header' id='heading${i}'>
|
<div class='card-header' id='heading${i}'>
|
||||||
@ -848,10 +1141,19 @@ class Utils {
|
|||||||
<span class='float-right' style="margin-top: -3px">
|
<span class='float-right' style="margin-top: -3px">
|
||||||
${file.size.toLocaleString()} bytes
|
${file.size.toLocaleString()} bytes
|
||||||
<a title="Download ${Utils.escapeHtml(file.name)}"
|
<a title="Download ${Utils.escapeHtml(file.name)}"
|
||||||
href='${URL.createObjectURL(blob)}'
|
href="${blobURL}"
|
||||||
download='${Utils.escapeHtml(file.name)}'>
|
download="${Utils.escapeHtml(file.name)}"
|
||||||
|
data-toggle="tooltip">
|
||||||
<i class="material-icons" style="vertical-align: bottom">save</i>
|
<i class="material-icons" style="vertical-align: bottom">save</i>
|
||||||
</a>
|
</a>
|
||||||
|
<a title="Move to input"
|
||||||
|
href="#"
|
||||||
|
blob-url="${blobURL}"
|
||||||
|
file-name="${Utils.escapeHtml(file.name)}"
|
||||||
|
class="extract-file"
|
||||||
|
data-toggle="tooltip">
|
||||||
|
<i class="material-icons" style="vertical-align: bottom">open_in_browser</i>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
@ -919,7 +1221,7 @@ class Utils {
|
|||||||
/**
|
/**
|
||||||
* Reads a File and returns the data as a Uint8Array.
|
* Reads a File and returns the data as a Uint8Array.
|
||||||
*
|
*
|
||||||
* @param {File} file
|
* @param {File | for node: array|arrayBuffer|buffer|string} file
|
||||||
* @returns {Uint8Array}
|
* @returns {Uint8Array}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@ -927,33 +1229,57 @@ class Utils {
|
|||||||
* await Utils.readFile(new File(["hello"], "test"))
|
* await Utils.readFile(new File(["hello"], "test"))
|
||||||
*/
|
*/
|
||||||
static readFile(file) {
|
static readFile(file) {
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
const data = new Uint8Array(file.size);
|
|
||||||
let offset = 0;
|
|
||||||
const CHUNK_SIZE = 10485760; // 10MiB
|
|
||||||
|
|
||||||
const seek = function() {
|
if (isNodeEnvironment()) {
|
||||||
if (offset >= file.size) {
|
return Buffer.from(file).buffer;
|
||||||
resolve(data);
|
|
||||||
return;
|
} else {
|
||||||
}
|
return new Promise((resolve, reject) => {
|
||||||
const slice = file.slice(offset, offset + CHUNK_SIZE);
|
const reader = new FileReader();
|
||||||
reader.readAsArrayBuffer(slice);
|
const data = new Uint8Array(file.size);
|
||||||
};
|
let offset = 0;
|
||||||
|
const CHUNK_SIZE = 10485760; // 10MiB
|
||||||
|
|
||||||
|
const seek = function() {
|
||||||
|
if (offset >= file.size) {
|
||||||
|
resolve(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const slice = file.slice(offset, offset + CHUNK_SIZE);
|
||||||
|
reader.readAsArrayBuffer(slice);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onload = function(e) {
|
||||||
|
data.set(new Uint8Array(reader.result), offset);
|
||||||
|
offset += CHUNK_SIZE;
|
||||||
|
seek();
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = function(e) {
|
||||||
|
reject(reader.error.message);
|
||||||
|
};
|
||||||
|
|
||||||
reader.onload = function(e) {
|
|
||||||
data.set(new Uint8Array(reader.result), offset);
|
|
||||||
offset += CHUNK_SIZE;
|
|
||||||
seek();
|
seek();
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reader.onerror = function(e) {
|
/**
|
||||||
reject(reader.error.message);
|
* Synchronously read the raw data from a File object.
|
||||||
};
|
*
|
||||||
|
* Only works in the Node environment
|
||||||
|
*
|
||||||
|
* @param {File} file - a File shim object (see src/node/File.mjs)
|
||||||
|
* @returns {ArrayBuffer} the data from the file in an ArrayBuffer
|
||||||
|
* @throws {TypeError} thrown if the method is called from a browser environment
|
||||||
|
*/
|
||||||
|
static readFileSync(file) {
|
||||||
|
if (!isNodeEnvironment()) {
|
||||||
|
throw new TypeError("Browser environment cannot support readFileSync");
|
||||||
|
}
|
||||||
|
|
||||||
seek();
|
const arrayBuffer = Uint8Array.from(file.data);
|
||||||
});
|
return arrayBuffer.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1013,9 +1339,11 @@ class Utils {
|
|||||||
static charRep(token) {
|
static charRep(token) {
|
||||||
return {
|
return {
|
||||||
"Space": " ",
|
"Space": " ",
|
||||||
|
"Percent": "%",
|
||||||
"Comma": ",",
|
"Comma": ",",
|
||||||
"Semi-colon": ";",
|
"Semi-colon": ";",
|
||||||
"Colon": ":",
|
"Colon": ":",
|
||||||
|
"Tab": "\t",
|
||||||
"Line feed": "\n",
|
"Line feed": "\n",
|
||||||
"CRLF": "\r\n",
|
"CRLF": "\r\n",
|
||||||
"Forward slash": "/",
|
"Forward slash": "/",
|
||||||
@ -1037,6 +1365,7 @@ class Utils {
|
|||||||
static regexRep(token) {
|
static regexRep(token) {
|
||||||
return {
|
return {
|
||||||
"Space": /\s+/g,
|
"Space": /\s+/g,
|
||||||
|
"Percent": /%/g,
|
||||||
"Comma": /,/g,
|
"Comma": /,/g,
|
||||||
"Semi-colon": /;/g,
|
"Semi-colon": /;/g,
|
||||||
"Colon": /:/g,
|
"Colon": /:/g,
|
||||||
@ -1044,12 +1373,61 @@ class Utils {
|
|||||||
"CRLF": /\r\n/g,
|
"CRLF": /\r\n/g,
|
||||||
"Forward slash": /\//g,
|
"Forward slash": /\//g,
|
||||||
"Backslash": /\\/g,
|
"Backslash": /\\/g,
|
||||||
|
"0x with comma": /,?0x/g,
|
||||||
"0x": /0x/g,
|
"0x": /0x/g,
|
||||||
"\\x": /\\x/g,
|
"\\x": /\\x/g,
|
||||||
"None": /\s+/g // Included here to remove whitespace when there shouldn't be any
|
"None": /\s+/g // Included here to remove whitespace when there shouldn't be any
|
||||||
}[token];
|
}[token];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate object in chunks of given size.
|
||||||
|
*
|
||||||
|
* @param {Iterable} iterable
|
||||||
|
* @param {number} chunksize
|
||||||
|
*/
|
||||||
|
static* chunked(iterable, chunksize) {
|
||||||
|
const iterator = iterable[Symbol.iterator]();
|
||||||
|
while (true) {
|
||||||
|
const res = [];
|
||||||
|
for (let i = 0; i < chunksize; i++) {
|
||||||
|
const next = iterator.next();
|
||||||
|
if (next.done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
res.push(next.value);
|
||||||
|
}
|
||||||
|
if (res.length) {
|
||||||
|
yield res;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the code is running in a Node.js environment
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isNodeEnvironment() {
|
||||||
|
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the code is running in a web environment
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isWebEnvironment() {
|
||||||
|
return typeof window === "object";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the code is running in a worker
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isWorkerEnvironment() {
|
||||||
|
return typeof importScripts === "function";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Utils;
|
export default Utils;
|
||||||
@ -1070,7 +1448,7 @@ export default Utils;
|
|||||||
Array.prototype.unique = function() {
|
Array.prototype.unique = function() {
|
||||||
const u = {}, a = [];
|
const u = {}, a = [];
|
||||||
for (let i = 0, l = this.length; i < l; i++) {
|
for (let i = 0, l = this.length; i < l; i++) {
|
||||||
if (u.hasOwnProperty(this[i])) {
|
if (Object.prototype.hasOwnProperty.call(u, this[i])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
a.push(this[i]);
|
a.push(this[i]);
|
||||||
@ -1163,6 +1541,46 @@ String.prototype.count = function(chr) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for self.sendStatusMessage to handle different environments.
|
||||||
|
*
|
||||||
|
* @param {string} msg
|
||||||
|
*/
|
||||||
|
export function sendStatusMessage(msg) {
|
||||||
|
if (isWorkerEnvironment())
|
||||||
|
self.sendStatusMessage(msg);
|
||||||
|
else if (isWebEnvironment())
|
||||||
|
app.alert(msg, 10000);
|
||||||
|
else if (isNodeEnvironment() && !global.TESTING)
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.debug(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const debounceTimeouts = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debouncer to stop functions from being executed multiple times in a
|
||||||
|
* short space of time
|
||||||
|
* https://davidwalsh.name/javascript-debounce-function
|
||||||
|
*
|
||||||
|
* @param {function} func - The function to be executed after the debounce time
|
||||||
|
* @param {number} wait - The time (ms) to wait before executing the function
|
||||||
|
* @param {string} id - Unique ID to reference the timeout for the function
|
||||||
|
* @param {object} scope - The object to bind to the debounced function
|
||||||
|
* @param {array} args - Array of arguments to be passed to func
|
||||||
|
* @returns {function}
|
||||||
|
*/
|
||||||
|
export function debounce(func, wait, id, scope, args) {
|
||||||
|
return function() {
|
||||||
|
const later = function() {
|
||||||
|
func.apply(scope, args);
|
||||||
|
};
|
||||||
|
clearTimeout(debounceTimeouts[id]);
|
||||||
|
debounceTimeouts[id] = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Polyfills
|
* Polyfills
|
||||||
*/
|
*/
|
||||||
@ -1171,14 +1589,14 @@ String.prototype.count = function(chr) {
|
|||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
|
||||||
if (!String.prototype.padStart) {
|
if (!String.prototype.padStart) {
|
||||||
String.prototype.padStart = function padStart(targetLength, padString) {
|
String.prototype.padStart = function padStart(targetLength, padString) {
|
||||||
targetLength = targetLength>>0; //floor if number or convert non-number to 0;
|
targetLength = targetLength>>0; // floor if number or convert non-number to 0;
|
||||||
padString = String((typeof padString !== "undefined" ? padString : " "));
|
padString = String((typeof padString !== "undefined" ? padString : " "));
|
||||||
if (this.length > targetLength) {
|
if (this.length > targetLength) {
|
||||||
return String(this);
|
return String(this);
|
||||||
} else {
|
} else {
|
||||||
targetLength = targetLength-this.length;
|
targetLength = targetLength-this.length;
|
||||||
if (targetLength > padString.length) {
|
if (targetLength > padString.length) {
|
||||||
padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed
|
padString += padString.repeat(targetLength/padString.length); // append to original to ensure we are longer than needed
|
||||||
}
|
}
|
||||||
return padString.slice(0, targetLength) + String(this);
|
return padString.slice(0, targetLength) + String(this);
|
||||||
}
|
}
|
||||||
@ -1190,14 +1608,14 @@ if (!String.prototype.padStart) {
|
|||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd
|
||||||
if (!String.prototype.padEnd) {
|
if (!String.prototype.padEnd) {
|
||||||
String.prototype.padEnd = function padEnd(targetLength, padString) {
|
String.prototype.padEnd = function padEnd(targetLength, padString) {
|
||||||
targetLength = targetLength>>0; //floor if number or convert non-number to 0;
|
targetLength = targetLength>>0; // floor if number or convert non-number to 0;
|
||||||
padString = String((typeof padString !== "undefined" ? padString : " "));
|
padString = String((typeof padString !== "undefined" ? padString : " "));
|
||||||
if (this.length > targetLength) {
|
if (this.length > targetLength) {
|
||||||
return String(this);
|
return String(this);
|
||||||
} else {
|
} else {
|
||||||
targetLength = targetLength-this.length;
|
targetLength = targetLength-this.length;
|
||||||
if (targetLength > padString.length) {
|
if (targetLength > padString.length) {
|
||||||
padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed
|
padString += padString.repeat(targetLength/padString.length); // append to original to ensure we are longer than needed
|
||||||
}
|
}
|
||||||
return String(this) + padString.slice(0, targetLength);
|
return String(this) + padString.slice(0, targetLength);
|
||||||
}
|
}
|
||||||
|
|||||||
274
src/core/config/Categories.json
Executable file → Normal file
274
src/core/config/Categories.json
Executable file → Normal file
@ -14,35 +14,48 @@
|
|||||||
"From Charcode",
|
"From Charcode",
|
||||||
"To Decimal",
|
"To Decimal",
|
||||||
"From Decimal",
|
"From Decimal",
|
||||||
|
"To Float",
|
||||||
|
"From Float",
|
||||||
"To Binary",
|
"To Binary",
|
||||||
"From Binary",
|
"From Binary",
|
||||||
"To Octal",
|
"To Octal",
|
||||||
"From Octal",
|
"From Octal",
|
||||||
|
"To Base32",
|
||||||
|
"From Base32",
|
||||||
|
"To Base45",
|
||||||
|
"From Base45",
|
||||||
|
"To Base58",
|
||||||
|
"From Base58",
|
||||||
|
"To Bech32",
|
||||||
|
"From Bech32",
|
||||||
|
"To Base62",
|
||||||
|
"From Base62",
|
||||||
"To Base64",
|
"To Base64",
|
||||||
"From Base64",
|
"From Base64",
|
||||||
"Show Base64 offsets",
|
"Show Base64 offsets",
|
||||||
"To Base32",
|
"To Base92",
|
||||||
"From Base32",
|
"From Base92",
|
||||||
"To Base58",
|
|
||||||
"From Base58",
|
|
||||||
"To Base62",
|
|
||||||
"From Base62",
|
|
||||||
"To Base85",
|
"To Base85",
|
||||||
"From Base85",
|
"From Base85",
|
||||||
"To Base",
|
"To Base",
|
||||||
"From Base",
|
"From Base",
|
||||||
"To BCD",
|
"To BCD",
|
||||||
"From BCD",
|
"From BCD",
|
||||||
|
"Text-Integer Conversion",
|
||||||
"To HTML Entity",
|
"To HTML Entity",
|
||||||
"From HTML Entity",
|
"From HTML Entity",
|
||||||
"URL Encode",
|
"URL Encode",
|
||||||
"URL Decode",
|
"URL Decode",
|
||||||
"Escape Unicode Characters",
|
"Escape Unicode Characters",
|
||||||
"Unescape Unicode Characters",
|
"Unescape Unicode Characters",
|
||||||
|
"Normalise Unicode",
|
||||||
|
"Escape Smart Characters",
|
||||||
"To Quoted Printable",
|
"To Quoted Printable",
|
||||||
"From Quoted Printable",
|
"From Quoted Printable",
|
||||||
"To Punycode",
|
"To Punycode",
|
||||||
"From Punycode",
|
"From Punycode",
|
||||||
|
"AMF Encode",
|
||||||
|
"AMF Decode",
|
||||||
"To Hex Content",
|
"To Hex Content",
|
||||||
"From Hex Content",
|
"From Hex Content",
|
||||||
"PEM to Hex",
|
"PEM to Hex",
|
||||||
@ -59,7 +72,20 @@
|
|||||||
"From Braille",
|
"From Braille",
|
||||||
"Parse TLV",
|
"Parse TLV",
|
||||||
"CSV to JSON",
|
"CSV to JSON",
|
||||||
"JSON to CSV"
|
"JSON to CSV",
|
||||||
|
"Avro to JSON",
|
||||||
|
"CBOR Encode",
|
||||||
|
"CBOR Decode",
|
||||||
|
"YAML to JSON",
|
||||||
|
"JSON to YAML",
|
||||||
|
"Caret/M-decode",
|
||||||
|
"Rison Encode",
|
||||||
|
"Rison Decode",
|
||||||
|
"To Modhex",
|
||||||
|
"From Modhex",
|
||||||
|
"MIME Decoding",
|
||||||
|
"To COBS",
|
||||||
|
"From COBS"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -73,28 +99,72 @@
|
|||||||
"DES Decrypt",
|
"DES Decrypt",
|
||||||
"Triple DES Encrypt",
|
"Triple DES Encrypt",
|
||||||
"Triple DES Decrypt",
|
"Triple DES Decrypt",
|
||||||
|
"Fernet Encrypt",
|
||||||
|
"Fernet Decrypt",
|
||||||
|
"LS47 Encrypt",
|
||||||
|
"LS47 Decrypt",
|
||||||
"RC2 Encrypt",
|
"RC2 Encrypt",
|
||||||
"RC2 Decrypt",
|
"RC2 Decrypt",
|
||||||
"RC4",
|
"RC4",
|
||||||
"RC4 Drop",
|
"RC4 Drop",
|
||||||
|
"ChaCha",
|
||||||
|
"Salsa20",
|
||||||
|
"XSalsa20",
|
||||||
|
"Rabbit",
|
||||||
|
"SM4 Encrypt",
|
||||||
|
"SM4 Decrypt",
|
||||||
|
"RC6 Encrypt",
|
||||||
|
"RC6 Decrypt",
|
||||||
|
"Ascon Encrypt",
|
||||||
|
"Ascon Decrypt",
|
||||||
|
"PRESENT Encrypt",
|
||||||
|
"PRESENT Decrypt",
|
||||||
|
"Twofish Encrypt",
|
||||||
|
"Twofish Decrypt",
|
||||||
|
"GOST Encrypt",
|
||||||
|
"GOST Decrypt",
|
||||||
|
"GOST Sign",
|
||||||
|
"GOST Verify",
|
||||||
|
"GOST Key Wrap",
|
||||||
|
"GOST Key Unwrap",
|
||||||
|
"ROR13",
|
||||||
"ROT13",
|
"ROT13",
|
||||||
|
"ROT13 Brute Force",
|
||||||
"ROT47",
|
"ROT47",
|
||||||
|
"ROT47 Brute Force",
|
||||||
|
"ROT8000",
|
||||||
"XOR",
|
"XOR",
|
||||||
"XOR Brute Force",
|
"XOR Brute Force",
|
||||||
"Vigenère Encode",
|
"Vigenère Encode",
|
||||||
"Vigenère Decode",
|
"Vigenère Decode",
|
||||||
|
"TEA Encrypt",
|
||||||
|
"TEA Decrypt",
|
||||||
|
"XTEA Encrypt",
|
||||||
|
"XTEA Decrypt",
|
||||||
|
"XXTEA Encrypt",
|
||||||
|
"XXTEA Decrypt",
|
||||||
"To Morse Code",
|
"To Morse Code",
|
||||||
"From Morse Code",
|
"From Morse Code",
|
||||||
|
"Bacon Cipher Encode",
|
||||||
|
"Bacon Cipher Decode",
|
||||||
"Bifid Cipher Encode",
|
"Bifid Cipher Encode",
|
||||||
"Bifid Cipher Decode",
|
"Bifid Cipher Decode",
|
||||||
|
"Caesar Box Cipher",
|
||||||
"Affine Cipher Encode",
|
"Affine Cipher Encode",
|
||||||
"Affine Cipher Decode",
|
"Affine Cipher Decode",
|
||||||
"A1Z26 Cipher Encode",
|
"A1Z26 Cipher Encode",
|
||||||
"A1Z26 Cipher Decode",
|
"A1Z26 Cipher Decode",
|
||||||
|
"Rail Fence Cipher Encode",
|
||||||
|
"Rail Fence Cipher Decode",
|
||||||
"Atbash Cipher",
|
"Atbash Cipher",
|
||||||
|
"CipherSaber2 Encrypt",
|
||||||
|
"CipherSaber2 Decrypt",
|
||||||
|
"Cetacean Cipher Encode",
|
||||||
|
"Cetacean Cipher Decode",
|
||||||
"Substitute",
|
"Substitute",
|
||||||
"Derive PBKDF2 key",
|
"Derive PBKDF2 key",
|
||||||
"Derive EVP key",
|
"Derive EVP key",
|
||||||
|
"Derive HKDF key",
|
||||||
"Bcrypt",
|
"Bcrypt",
|
||||||
"Scrypt",
|
"Scrypt",
|
||||||
"JWT Sign",
|
"JWT Sign",
|
||||||
@ -102,23 +172,56 @@
|
|||||||
"JWT Decode",
|
"JWT Decode",
|
||||||
"Citrix CTX1 Encode",
|
"Citrix CTX1 Encode",
|
||||||
"Citrix CTX1 Decode",
|
"Citrix CTX1 Decode",
|
||||||
"Pseudo-Random Number Generator"
|
"AES Key Wrap",
|
||||||
|
"AES Key Unwrap",
|
||||||
|
"Pseudo-Random Number Generator",
|
||||||
|
"Pseudo-Random Prime Generator",
|
||||||
|
"Enigma",
|
||||||
|
"Bombe",
|
||||||
|
"Multiple Bombe",
|
||||||
|
"Typex",
|
||||||
|
"Lorenz",
|
||||||
|
"Colossus",
|
||||||
|
"SIGABA",
|
||||||
|
"Flask Session Decode",
|
||||||
|
"Flask Session Sign",
|
||||||
|
"Flask Session Verify"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Public Key",
|
"name": "Public Key",
|
||||||
"ops": [
|
"ops": [
|
||||||
"Parse X.509 certificate",
|
"Parse X.509 certificate",
|
||||||
|
"Parse X.509 CRL",
|
||||||
"Parse ASN.1 hex string",
|
"Parse ASN.1 hex string",
|
||||||
"PEM to Hex",
|
"PEM to Hex",
|
||||||
"Hex to PEM",
|
"Hex to PEM",
|
||||||
"Hex to Object Identifier",
|
"Hex to Object Identifier",
|
||||||
"Object Identifier to Hex",
|
"Object Identifier to Hex",
|
||||||
|
"PEM to JWK",
|
||||||
|
"JWK to PEM",
|
||||||
"Generate PGP Key Pair",
|
"Generate PGP Key Pair",
|
||||||
"PGP Encrypt",
|
"PGP Encrypt",
|
||||||
"PGP Decrypt",
|
"PGP Decrypt",
|
||||||
|
"PGP Sign",
|
||||||
|
"PGP Verify",
|
||||||
"PGP Encrypt and Sign",
|
"PGP Encrypt and Sign",
|
||||||
"PGP Decrypt and Verify"
|
"PGP Decrypt and Verify",
|
||||||
|
"Generate RSA Key Pair",
|
||||||
|
"RSA Sign",
|
||||||
|
"RSA Verify",
|
||||||
|
"RSA Encrypt",
|
||||||
|
"RSA Decrypt",
|
||||||
|
"Generate ECDSA Key Pair",
|
||||||
|
"ECDSA Signature Conversion",
|
||||||
|
"ECDSA Sign",
|
||||||
|
"ECDSA Verify",
|
||||||
|
"Parse SSH Host Key",
|
||||||
|
"Parse CSR",
|
||||||
|
"Public Key from Certificate",
|
||||||
|
"Public Key from Private Key",
|
||||||
|
"SM2 Encrypt",
|
||||||
|
"SM2 Decrypt"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -141,6 +244,10 @@
|
|||||||
"Subtract",
|
"Subtract",
|
||||||
"Multiply",
|
"Multiply",
|
||||||
"Divide",
|
"Divide",
|
||||||
|
"MOD",
|
||||||
|
"Extended GCD",
|
||||||
|
"Modular Exponentiation",
|
||||||
|
"Modular Inverse",
|
||||||
"Mean",
|
"Mean",
|
||||||
"Median",
|
"Median",
|
||||||
"Standard Deviation",
|
"Standard Deviation",
|
||||||
@ -148,28 +255,52 @@
|
|||||||
"Bit shift right",
|
"Bit shift right",
|
||||||
"Rotate left",
|
"Rotate left",
|
||||||
"Rotate right",
|
"Rotate right",
|
||||||
"ROT13"
|
"ROR13",
|
||||||
|
"ROT13",
|
||||||
|
"ROT8000"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Networking",
|
"name": "Networking",
|
||||||
"ops": [
|
"ops": [
|
||||||
"HTTP request",
|
"HTTP request",
|
||||||
|
"DNS over HTTPS",
|
||||||
"Strip HTTP headers",
|
"Strip HTTP headers",
|
||||||
"Dechunk HTTP response",
|
"Dechunk HTTP response",
|
||||||
|
"Parse Ethernet frame",
|
||||||
"Parse User Agent",
|
"Parse User Agent",
|
||||||
"Parse IP range",
|
"Parse IP range",
|
||||||
"Parse IPv6 address",
|
"Parse IPv6 address",
|
||||||
|
"IPv6 Transition Addresses",
|
||||||
"Parse IPv4 header",
|
"Parse IPv4 header",
|
||||||
|
"Strip IPv4 header",
|
||||||
|
"Parse TCP",
|
||||||
|
"Strip TCP header",
|
||||||
|
"Parse TLS record",
|
||||||
|
"Parse UDP",
|
||||||
|
"Strip UDP header",
|
||||||
|
"Parse SSH Host Key",
|
||||||
"Parse URI",
|
"Parse URI",
|
||||||
"URL Encode",
|
"URL Encode",
|
||||||
"URL Decode",
|
"URL Decode",
|
||||||
|
"Protobuf Decode",
|
||||||
|
"Protobuf Encode",
|
||||||
|
"VarInt Encode",
|
||||||
|
"VarInt Decode",
|
||||||
|
"JA3 Fingerprint",
|
||||||
|
"JA3S Fingerprint",
|
||||||
|
"JA4 Fingerprint",
|
||||||
|
"JA4Server Fingerprint",
|
||||||
|
"HASSH Client Fingerprint",
|
||||||
|
"HASSH Server Fingerprint",
|
||||||
"Format MAC addresses",
|
"Format MAC addresses",
|
||||||
"Change IP format",
|
"Change IP format",
|
||||||
"Group IP addresses",
|
"Group IP addresses",
|
||||||
"Encode NetBIOS Name",
|
"Encode NetBIOS Name",
|
||||||
"Decode NetBIOS Name",
|
"Decode NetBIOS Name",
|
||||||
"Defang URL"
|
"Defang URL",
|
||||||
|
"Fang URL",
|
||||||
|
"Defang IP Addresses"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -177,8 +308,11 @@
|
|||||||
"ops": [
|
"ops": [
|
||||||
"Encode text",
|
"Encode text",
|
||||||
"Decode text",
|
"Decode text",
|
||||||
|
"Unicode Text Format",
|
||||||
"Remove Diacritics",
|
"Remove Diacritics",
|
||||||
"Unescape Unicode Characters"
|
"Unescape Unicode Characters",
|
||||||
|
"Convert to NATO alphabet",
|
||||||
|
"Convert Leet Speak"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -187,15 +321,20 @@
|
|||||||
"Diff",
|
"Diff",
|
||||||
"Remove whitespace",
|
"Remove whitespace",
|
||||||
"Remove null bytes",
|
"Remove null bytes",
|
||||||
|
"Remove ANSI Escape Codes",
|
||||||
"To Upper case",
|
"To Upper case",
|
||||||
"To Lower case",
|
"To Lower case",
|
||||||
"To Case Insensitive Regex",
|
"Swap case",
|
||||||
"From Case Insensitive Regex",
|
"Alternating Caps",
|
||||||
|
"To Case Insensitive Regex",
|
||||||
|
"From Case Insensitive Regex",
|
||||||
"Add line numbers",
|
"Add line numbers",
|
||||||
"Remove line numbers",
|
"Remove line numbers",
|
||||||
|
"Get All Casings",
|
||||||
"To Table",
|
"To Table",
|
||||||
"Reverse",
|
"Reverse",
|
||||||
"Sort",
|
"Sort",
|
||||||
|
"Shuffle",
|
||||||
"Unique",
|
"Unique",
|
||||||
"Split",
|
"Split",
|
||||||
"Filter",
|
"Filter",
|
||||||
@ -208,20 +347,29 @@
|
|||||||
"Pad lines",
|
"Pad lines",
|
||||||
"Find / Replace",
|
"Find / Replace",
|
||||||
"Regular expression",
|
"Regular expression",
|
||||||
|
"Fuzzy Match",
|
||||||
"Offset checker",
|
"Offset checker",
|
||||||
"Hamming Distance",
|
"Hamming Distance",
|
||||||
|
"Levenshtein Distance",
|
||||||
"Convert distance",
|
"Convert distance",
|
||||||
"Convert area",
|
"Convert area",
|
||||||
"Convert mass",
|
"Convert mass",
|
||||||
"Convert speed",
|
"Convert speed",
|
||||||
"Convert data units",
|
"Convert data units",
|
||||||
|
"Convert co-ordinate format",
|
||||||
|
"Show on map",
|
||||||
"Parse UNIX file permissions",
|
"Parse UNIX file permissions",
|
||||||
|
"Parse ObjectID timestamp",
|
||||||
"Swap endianness",
|
"Swap endianness",
|
||||||
"Parse colour code",
|
"Parse colour code",
|
||||||
"Escape string",
|
"Escape string",
|
||||||
"Unescape string",
|
"Unescape string",
|
||||||
"Pseudo-Random Number Generator",
|
"Pseudo-Random Number Generator",
|
||||||
"Sleep"
|
"Sleep",
|
||||||
|
"File Tree",
|
||||||
|
"Wrap",
|
||||||
|
"Take nth bytes",
|
||||||
|
"Drop nth bytes"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -233,7 +381,9 @@
|
|||||||
"To UNIX Timestamp",
|
"To UNIX Timestamp",
|
||||||
"Windows Filetime to UNIX Timestamp",
|
"Windows Filetime to UNIX Timestamp",
|
||||||
"UNIX Timestamp to Windows Filetime",
|
"UNIX Timestamp to Windows Filetime",
|
||||||
|
"DateTime Delta",
|
||||||
"Extract dates",
|
"Extract dates",
|
||||||
|
"Get Time",
|
||||||
"Sleep"
|
"Sleep"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -248,11 +398,18 @@
|
|||||||
"Extract domains",
|
"Extract domains",
|
||||||
"Extract file paths",
|
"Extract file paths",
|
||||||
"Extract dates",
|
"Extract dates",
|
||||||
|
"Extract hashes",
|
||||||
"Regular expression",
|
"Regular expression",
|
||||||
"XPath expression",
|
"XPath expression",
|
||||||
"JPath expression",
|
"JPath expression",
|
||||||
|
"Jsonata Query",
|
||||||
"CSS selector",
|
"CSS selector",
|
||||||
"Extract EXIF"
|
"Extract EXIF",
|
||||||
|
"Extract ID3",
|
||||||
|
"Extract Audio Metadata",
|
||||||
|
"Extract Files",
|
||||||
|
"RAKE",
|
||||||
|
"Template"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -267,14 +424,23 @@
|
|||||||
"Zip",
|
"Zip",
|
||||||
"Unzip",
|
"Unzip",
|
||||||
"Bzip2 Decompress",
|
"Bzip2 Decompress",
|
||||||
|
"Bzip2 Compress",
|
||||||
"Tar",
|
"Tar",
|
||||||
"Untar"
|
"Untar",
|
||||||
|
"LZString Decompress",
|
||||||
|
"LZString Compress",
|
||||||
|
"LZMA Decompress",
|
||||||
|
"LZMA Compress",
|
||||||
|
"LZ4 Decompress",
|
||||||
|
"LZ4 Compress",
|
||||||
|
"LZNT1 Decompress"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Hashing",
|
"name": "Hashing",
|
||||||
"ops": [
|
"ops": [
|
||||||
"Analyse hash",
|
"Analyse hash",
|
||||||
|
"Generate all checksums",
|
||||||
"Generate all hashes",
|
"Generate all hashes",
|
||||||
"MD2",
|
"MD2",
|
||||||
"MD4",
|
"MD4",
|
||||||
@ -284,31 +450,45 @@
|
|||||||
"SHA1",
|
"SHA1",
|
||||||
"SHA2",
|
"SHA2",
|
||||||
"SHA3",
|
"SHA3",
|
||||||
|
"SM3",
|
||||||
"Keccak",
|
"Keccak",
|
||||||
"Shake",
|
"Shake",
|
||||||
"RIPEMD",
|
"RIPEMD",
|
||||||
"HAS-160",
|
"HAS-160",
|
||||||
"Whirlpool",
|
"Whirlpool",
|
||||||
"Snefru",
|
"Snefru",
|
||||||
|
"BLAKE2b",
|
||||||
|
"BLAKE2s",
|
||||||
|
"BLAKE3",
|
||||||
|
"Ascon Hash",
|
||||||
|
"Ascon MAC",
|
||||||
|
"GOST Hash",
|
||||||
|
"Streebog",
|
||||||
"SSDEEP",
|
"SSDEEP",
|
||||||
"CTPH",
|
"CTPH",
|
||||||
"Compare SSDEEP hashes",
|
"Compare SSDEEP hashes",
|
||||||
"Compare CTPH hashes",
|
"Compare CTPH hashes",
|
||||||
"HMAC",
|
"HMAC",
|
||||||
|
"CMAC",
|
||||||
"Bcrypt",
|
"Bcrypt",
|
||||||
"Bcrypt compare",
|
"Bcrypt compare",
|
||||||
"Bcrypt parse",
|
"Bcrypt parse",
|
||||||
|
"Argon2",
|
||||||
|
"Argon2 compare",
|
||||||
"Scrypt",
|
"Scrypt",
|
||||||
|
"NT Hash",
|
||||||
|
"LM Hash",
|
||||||
|
"MurmurHash3",
|
||||||
"Fletcher-8 Checksum",
|
"Fletcher-8 Checksum",
|
||||||
"Fletcher-16 Checksum",
|
"Fletcher-16 Checksum",
|
||||||
"Fletcher-32 Checksum",
|
"Fletcher-32 Checksum",
|
||||||
"Fletcher-64 Checksum",
|
"Fletcher-64 Checksum",
|
||||||
"Adler-32 Checksum",
|
"Adler-32 Checksum",
|
||||||
"CRC-16 Checksum",
|
"Luhn Checksum",
|
||||||
"CRC-32 Checksum",
|
"CRC Checksum",
|
||||||
"TCP/IP Checksum",
|
"TCP/IP Checksum",
|
||||||
"To Geohash",
|
"XOR Checksum",
|
||||||
"From Geohash"
|
"Parity Bit"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -329,8 +509,10 @@
|
|||||||
"CSS Minify",
|
"CSS Minify",
|
||||||
"XPath expression",
|
"XPath expression",
|
||||||
"JPath expression",
|
"JPath expression",
|
||||||
|
"Jq",
|
||||||
"CSS selector",
|
"CSS selector",
|
||||||
"PHP Deserialize",
|
"PHP Deserialize",
|
||||||
|
"PHP Serialize",
|
||||||
"Microsoft Script Decoder",
|
"Microsoft Script Decoder",
|
||||||
"Strip HTML tags",
|
"Strip HTML tags",
|
||||||
"Diff",
|
"Diff",
|
||||||
@ -340,7 +522,8 @@
|
|||||||
"BSON serialise",
|
"BSON serialise",
|
||||||
"BSON deserialise",
|
"BSON deserialise",
|
||||||
"To MessagePack",
|
"To MessagePack",
|
||||||
"From MessagePack"
|
"From MessagePack",
|
||||||
|
"Render Markdown"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -348,8 +531,16 @@
|
|||||||
"ops": [
|
"ops": [
|
||||||
"Detect File Type",
|
"Detect File Type",
|
||||||
"Scan for Embedded Files",
|
"Scan for Embedded Files",
|
||||||
|
"Extract Files",
|
||||||
|
"YARA Rules",
|
||||||
"Remove EXIF",
|
"Remove EXIF",
|
||||||
"Extract EXIF"
|
"Extract EXIF",
|
||||||
|
"Extract RGBA",
|
||||||
|
"View Bit Plane",
|
||||||
|
"Randomize Colour Palette",
|
||||||
|
"Extract LSB",
|
||||||
|
"ELF Info",
|
||||||
|
"Extract Audio Metadata"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -357,9 +548,33 @@
|
|||||||
"ops": [
|
"ops": [
|
||||||
"Render Image",
|
"Render Image",
|
||||||
"Play Media",
|
"Play Media",
|
||||||
|
"Generate Image",
|
||||||
|
"Optical Character Recognition",
|
||||||
"Remove EXIF",
|
"Remove EXIF",
|
||||||
"Extract EXIF",
|
"Extract EXIF",
|
||||||
"Split Colour Channels"
|
"Split Colour Channels",
|
||||||
|
"Rotate Image",
|
||||||
|
"Resize Image",
|
||||||
|
"Blur Image",
|
||||||
|
"Dither Image",
|
||||||
|
"Invert Image",
|
||||||
|
"Flip Image",
|
||||||
|
"Crop Image",
|
||||||
|
"Image Brightness / Contrast",
|
||||||
|
"Image Opacity",
|
||||||
|
"Image Filter",
|
||||||
|
"Contain Image",
|
||||||
|
"Cover Image",
|
||||||
|
"Image Hue/Saturation/Lightness",
|
||||||
|
"Sharpen Image",
|
||||||
|
"Normalise Image",
|
||||||
|
"Convert Image Format",
|
||||||
|
"Add Text To Image",
|
||||||
|
"Hex Density chart",
|
||||||
|
"Scatter chart",
|
||||||
|
"Series chart",
|
||||||
|
"Heatmap chart",
|
||||||
|
"Render PDF"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -367,18 +582,26 @@
|
|||||||
"ops": [
|
"ops": [
|
||||||
"Entropy",
|
"Entropy",
|
||||||
"Frequency distribution",
|
"Frequency distribution",
|
||||||
|
"Index of Coincidence",
|
||||||
"Chi Square",
|
"Chi Square",
|
||||||
|
"P-list Viewer",
|
||||||
"Disassemble x86",
|
"Disassemble x86",
|
||||||
|
"Disassemble ARM",
|
||||||
"Pseudo-Random Number Generator",
|
"Pseudo-Random Number Generator",
|
||||||
|
"Pseudo-Random Integer Generator",
|
||||||
|
"Generate De Bruijn Sequence",
|
||||||
"Generate UUID",
|
"Generate UUID",
|
||||||
|
"Analyse UUID",
|
||||||
"Generate TOTP",
|
"Generate TOTP",
|
||||||
"Generate HOTP",
|
"Generate HOTP",
|
||||||
"Generate QR Code",
|
"Generate QR Code",
|
||||||
"Parse QR Code",
|
"Parse QR Code",
|
||||||
"Haversine distance",
|
"Haversine distance",
|
||||||
|
"HTML To Text",
|
||||||
"Generate Lorem Ipsum",
|
"Generate Lorem Ipsum",
|
||||||
"Numberwang",
|
"Numberwang",
|
||||||
"XKCD Random Number"
|
"XKCD Random Number",
|
||||||
|
"Automated Validation Test Op"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -386,6 +609,7 @@
|
|||||||
"ops": [
|
"ops": [
|
||||||
"Magic",
|
"Magic",
|
||||||
"Fork",
|
"Fork",
|
||||||
|
"Subsection",
|
||||||
"Merge",
|
"Merge",
|
||||||
"Register",
|
"Register",
|
||||||
"Label",
|
"Label",
|
||||||
|
|||||||
54
src/core/config/scripts/fixCryptoApiImports.mjs
Normal file
54
src/core/config/scripts/fixCryptoApiImports.mjs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* This script updates crypto-api package
|
||||||
|
* It adds .mjs to local imports where its missing
|
||||||
|
*
|
||||||
|
* before:
|
||||||
|
* import foo from "./bar";
|
||||||
|
* after
|
||||||
|
* import foo from "./bar.mjs";
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint no-console: ["off"] */
|
||||||
|
|
||||||
|
import { readdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
// Base directory of crypto-api source
|
||||||
|
const baseDir = join(process.cwd(), "node_modules/crypto-api/src");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively walk a directory, updating import statements
|
||||||
|
* to include ".mjs" if missing
|
||||||
|
*/
|
||||||
|
function walk(dir) {
|
||||||
|
const entries = readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.name === ".git") continue;
|
||||||
|
|
||||||
|
const fullPath = join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(fullPath);
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
const content = readFileSync(fullPath, "utf8");
|
||||||
|
|
||||||
|
// Add .mjs to imports if not present
|
||||||
|
const updated = content.replace(
|
||||||
|
/from "(\.[^"]*)";/g,
|
||||||
|
(match, p1) => {
|
||||||
|
if (p1.endsWith(".mjs")) return match;
|
||||||
|
return `from "${p1}.mjs";`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updated !== content) {
|
||||||
|
writeFileSync(fullPath, updated, "utf8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the walker
|
||||||
|
walk(baseDir);
|
||||||
28
src/core/config/scripts/fixSnackBarMarkup.mjs
Normal file
28
src/core/config/scripts/fixSnackBarMarkup.mjs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* This script updates snackbarjs package
|
||||||
|
* Replaces self-closing div with standard opening div
|
||||||
|
*
|
||||||
|
* before:
|
||||||
|
* <div id=snackbar-container/>
|
||||||
|
* after:
|
||||||
|
* <div id=snackbar-container>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint no-console: ["off"] */
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
// Base directory of snackbarjs source
|
||||||
|
const filePath = join(process.cwd(), "node_modules/snackbarjs/src/snackbar.js");
|
||||||
|
|
||||||
|
const content = readFileSync(filePath, "utf8");
|
||||||
|
|
||||||
|
// Replace self-closing div with standard opening div
|
||||||
|
const updated = content.replace(
|
||||||
|
/<div id=snackbar-container\/>/g,
|
||||||
|
"<div id=snackbar-container>"
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFileSync(filePath, updated, "utf8");
|
||||||
@ -9,12 +9,12 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*eslint no-console: ["off"] */
|
/* eslint no-console: ["off"] */
|
||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import * as Ops from "../../operations/index";
|
import * as Ops from "../../operations/index.mjs";
|
||||||
|
|
||||||
const dir = path.join(process.cwd() + "/src/core/config/");
|
const dir = path.join(process.cwd() + "/src/core/config/");
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
@ -41,14 +41,12 @@ for (const opObj in Ops) {
|
|||||||
inputType: op.inputType,
|
inputType: op.inputType,
|
||||||
outputType: op.presentType,
|
outputType: op.presentType,
|
||||||
flowControl: op.flowControl,
|
flowControl: op.flowControl,
|
||||||
args: op.args
|
manualBake: op.manualBake,
|
||||||
|
args: op.args,
|
||||||
|
checks: op.checks
|
||||||
};
|
};
|
||||||
|
|
||||||
if (op.hasOwnProperty("patterns")) {
|
if (!(op.module in modules))
|
||||||
operationConfig[op.name].patterns = op.patterns;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!modules.hasOwnProperty(op.module))
|
|
||||||
modules[op.module] = {};
|
modules[op.module] = {};
|
||||||
modules[op.module][op.name] = opObj;
|
modules[op.module][op.name] = opObj;
|
||||||
}
|
}
|
||||||
@ -83,7 +81,7 @@ for (const module in modules) {
|
|||||||
|
|
||||||
for (const opName in modules[module]) {
|
for (const opName in modules[module]) {
|
||||||
const objName = modules[module][opName];
|
const objName = modules[module][opName];
|
||||||
code += `import ${objName} from "../../operations/${objName}";\n`;
|
code += `import ${objName} from "../../operations/${objName}.mjs";\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
code += `
|
code += `
|
||||||
@ -123,7 +121,7 @@ let opModulesCode = `/**
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
for (const module in modules) {
|
for (const module in modules) {
|
||||||
opModulesCode += `import ${module}Module from "./${module}";\n`;
|
opModulesCode += `import ${module}Module from "./${module}.mjs";\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
opModulesCode += `
|
opModulesCode += `
|
||||||
|
|||||||
139
src/core/config/scripts/generateHTMLEntities.mjs
Normal file
139
src/core/config/scripts/generateHTMLEntities.mjs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* This script automatically generates src/core/lib/HTMLEntities.mjs, the shared
|
||||||
|
* HTML entity lookup tables used by the "To HTML Entity" and "From HTML Entity"
|
||||||
|
* operations.
|
||||||
|
*
|
||||||
|
* The data is derived from the vendored WHATWG named character reference set
|
||||||
|
* (src/core/vendor/htmlEntities/entity.json, from
|
||||||
|
* https://html.spec.whatwg.org/entities.json) so the
|
||||||
|
* two operations cannot drift apart and every entity is spec-conformant:
|
||||||
|
*
|
||||||
|
* - HTML_ENTITY_REVERSE_LOOKUP (decode) is every single-code-point spec name.
|
||||||
|
* - HTML_ENTITY_LOOKUP (encode) picks one canonical name per code point via a
|
||||||
|
* deterministic tiebreak, overridden by htmlEntityOverrides.mjs where a
|
||||||
|
* specific historical name is preferred.
|
||||||
|
*
|
||||||
|
* @author roberson-io [michaelroberson@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint no-console: ["off"] */
|
||||||
|
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import process from "process";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { HTML_ENTITY_CANONICAL_OVERRIDES } from "./htmlEntityOverrides.mjs";
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(process.cwd(), "src/core/lib"))) {
|
||||||
|
console.log("\nCWD: " + process.cwd());
|
||||||
|
console.log("Error: generateHTMLEntities.mjs should be run from the project root");
|
||||||
|
console.log("Example> node src/core/config/scripts/generateHTMLEntities.mjs");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPEC = JSON.parse(fs.readFileSync(
|
||||||
|
path.join(scriptDir, "..", "..", "vendor", "htmlEntities", "entity.json"), "utf8"));
|
||||||
|
|
||||||
|
// Build code point -> [spec names] for single-code-point, semicolon-terminated
|
||||||
|
// references (the representable subset; multi-code-point entities are skipped).
|
||||||
|
const codePointNames = {};
|
||||||
|
for (const [key, val] of Object.entries(SPEC)) {
|
||||||
|
if (!key.endsWith(";") || val.codepoints.length !== 1) continue;
|
||||||
|
const name = key.slice(1, -1); // strip leading "&" and trailing ";"
|
||||||
|
(codePointNames[val.codepoints[0]] ??= []).push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic canonical-name tiebreak: prefer a lower-case name, then the
|
||||||
|
// shortest, then alphabetical. Overridden per code point where a specific
|
||||||
|
// historical name is preferred.
|
||||||
|
const isAllUpper = s => s === s.toUpperCase() && s !== s.toLowerCase();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose the single canonical entity name for a code point.
|
||||||
|
*
|
||||||
|
* @param {number} codePoint - the Unicode code point being encoded
|
||||||
|
* @param {string[]} names - all valid WHATWG names for that code point
|
||||||
|
* @returns {string} the canonical name to emit when encoding
|
||||||
|
*/
|
||||||
|
function canonicalName(codePoint, names) {
|
||||||
|
const override = HTML_ENTITY_CANONICAL_OVERRIDES[codePoint];
|
||||||
|
if (override !== undefined) {
|
||||||
|
if (!names.includes(override))
|
||||||
|
throw new Error(`Override &${override}; is not a spec name for code point ${codePoint} (spec: ${names})`);
|
||||||
|
return override;
|
||||||
|
}
|
||||||
|
return [...names].sort((a, b) =>
|
||||||
|
(isAllUpper(a) - isAllUpper(b)) ||
|
||||||
|
(a.length - b.length) ||
|
||||||
|
(a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const forward = {}; // code point -> canonical name (encode)
|
||||||
|
const reverse = {}; // name -> code point (decode, every spec name)
|
||||||
|
for (const [cpStr, names] of Object.entries(codePointNames)) {
|
||||||
|
const cp = Number(cpStr);
|
||||||
|
forward[cp] = canonicalName(cp, names);
|
||||||
|
for (const name of names) reverse[name] = cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode-only aliases = spec names that are not the canonical encode name.
|
||||||
|
const aliases = {};
|
||||||
|
for (const [name, cp] of Object.entries(reverse)) {
|
||||||
|
if (forward[cp] !== name) aliases[name] = cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- emit -----------------------------------------------------------------
|
||||||
|
const fwdEntries = Object.keys(forward).map(Number).sort((a, b) => a - b)
|
||||||
|
.map(cp => ` ${cp}: "${forward[cp]}",`).join("\n").replace(/,$/, "");
|
||||||
|
const aliasEntries = Object.keys(aliases).sort()
|
||||||
|
.map(n => ` ${JSON.stringify(n)}: ${aliases[n]},`).join("\n").replace(/,$/, "");
|
||||||
|
|
||||||
|
const code = `/**
|
||||||
|
* THIS FILE IS AUTOMATICALLY GENERATED BY src/core/config/scripts/generateHTMLEntities.mjs
|
||||||
|
*
|
||||||
|
* HTML entity lookup tables shared by the "To HTML Entity" and "From HTML Entity"
|
||||||
|
* operations, derived from the WHATWG named character reference set
|
||||||
|
* (https://html.spec.whatwg.org/entities.json). Do not edit by hand — change the
|
||||||
|
* vendored src/core/vendor/htmlEntities/entity.json or htmlEntityOverrides.mjs
|
||||||
|
* and regenerate.
|
||||||
|
*
|
||||||
|
* @author roberson-io [michaelroberson@gmail.com]
|
||||||
|
* @copyright Crown Copyright ${new Date().getUTCFullYear()}
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical lookup: Unicode code point -> entity name (without "&" and ";"),
|
||||||
|
* used for ENCODING. One canonical name per code point.
|
||||||
|
*/
|
||||||
|
export const HTML_ENTITY_LOOKUP = {
|
||||||
|
${fwdEntries}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy / alias names that only DECODE (spelling variants, deprecated names,
|
||||||
|
* box-drawing aliases, etc.); not used for encoding.
|
||||||
|
*/
|
||||||
|
export const HTML_ENTITY_DECODE_ALIASES = {
|
||||||
|
${aliasEntries}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derived reverse lookup: entity name -> code point, used for DECODING. Built
|
||||||
|
* from the canonical lookup plus the legacy aliases.
|
||||||
|
*/
|
||||||
|
export const HTML_ENTITY_REVERSE_LOOKUP = {...HTML_ENTITY_DECODE_ALIASES};
|
||||||
|
for (const codePoint in HTML_ENTITY_LOOKUP) {
|
||||||
|
HTML_ENTITY_REVERSE_LOOKUP[HTML_ENTITY_LOOKUP[codePoint]] = Number(codePoint);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(process.cwd(), "src/core/lib/HTMLEntities.mjs"), code);
|
||||||
|
console.log(`generateHTMLEntities: ${Object.keys(forward).length} encode names, ` +
|
||||||
|
`${Object.keys(reverse).length} decode names, ${Object.keys(aliases).length} aliases, ` +
|
||||||
|
`${Object.keys(HTML_ENTITY_CANONICAL_OVERRIDES).length} overrides applied.`);
|
||||||
@ -7,7 +7,7 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*eslint no-console: ["off"] */
|
/* eslint no-console: ["off"] */
|
||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@ -30,16 +30,16 @@ fs.readdirSync(path.join(dir, "../operations")).forEach(file => {
|
|||||||
|
|
||||||
// Construct index file
|
// Construct index file
|
||||||
let code = `/**
|
let code = `/**
|
||||||
* THIS FILE IS AUTOMATICALLY GENERATED BY src/core/config/scripts/generateOpsIndex.mjs
|
* THIS FILE IS AUTOMATICALLY GENERATED BY src/core/config/scripts/generateOpsIndex.mjs
|
||||||
*
|
*
|
||||||
* @author n1474335 [n1474335@gmail.com]
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
* @copyright Crown Copyright ${new Date().getUTCFullYear()}
|
* @copyright Crown Copyright ${new Date().getUTCFullYear()}
|
||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
`;
|
`;
|
||||||
|
|
||||||
opObjs.forEach(obj => {
|
opObjs.forEach(obj => {
|
||||||
code += `import ${obj} from "./${obj}";\n`;
|
code += `import ${obj} from "./${obj}.mjs";\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
code += `
|
code += `
|
||||||
@ -58,3 +58,66 @@ fs.writeFileSync(
|
|||||||
code
|
code
|
||||||
);
|
);
|
||||||
console.log("Written operation index.");
|
console.log("Written operation index.");
|
||||||
|
|
||||||
|
// find all test files
|
||||||
|
const testsDir = path.join(process.cwd() + "/tests/operations/tests/");
|
||||||
|
const testObjs = [];
|
||||||
|
fs.readdirSync(testsDir).forEach(file => {
|
||||||
|
if (!file.endsWith(".mjs")) return;
|
||||||
|
testObjs.push(file.split(".mjs")[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Construct test index file
|
||||||
|
code = `/**
|
||||||
|
* THIS FILE IS AUTOMATICALLY GENERATED BY src/core/config/scripts/generateOpsIndex.mjs
|
||||||
|
*
|
||||||
|
* @author john [john19696@protonmail.com]
|
||||||
|
* @author tlwr [toby@toby.codes]
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright ${new Date().getUTCFullYear()}
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
setLongTestFailure,
|
||||||
|
logTestReport,
|
||||||
|
} from "../lib/utils.mjs";
|
||||||
|
|
||||||
|
import "../lib/wasmFetchPolyfill.mjs";
|
||||||
|
|
||||||
|
import TestRegister from "../lib/TestRegister.mjs";
|
||||||
|
`;
|
||||||
|
|
||||||
|
testObjs.forEach(obj => {
|
||||||
|
if (obj !== "SplitColourChannels")
|
||||||
|
code += `import "./tests/${obj}.mjs";\n`;
|
||||||
|
else
|
||||||
|
code += `// Cannot test operations that use the File type yet
|
||||||
|
// import "./tests/SplitColourChannels.mjs";\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
code += `
|
||||||
|
|
||||||
|
const testStatus = {
|
||||||
|
allTestsPassing: true,
|
||||||
|
counts: {
|
||||||
|
total: 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setLongTestFailure();
|
||||||
|
|
||||||
|
const logOpsTestReport = logTestReport.bind(null, testStatus);
|
||||||
|
|
||||||
|
(async function() {
|
||||||
|
const results = await TestRegister.runTests();
|
||||||
|
logOpsTestReport(results);
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Write tests file
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(testsDir, "../index.mjs"),
|
||||||
|
code
|
||||||
|
);
|
||||||
|
console.log("Written operation tests index.");
|
||||||
|
|||||||
86
src/core/config/scripts/htmlEntityOverrides.mjs
Normal file
86
src/core/config/scripts/htmlEntityOverrides.mjs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Canonical entity-name overrides for generateHTMLEntities.mjs.
|
||||||
|
*
|
||||||
|
* The WHATWG named character reference set assigns MANY names to some code
|
||||||
|
* points (e.g. U+2211 is both ∑ and ∑), but does not designate a
|
||||||
|
* canonical one. The generator therefore needs a rule to pick a single name per
|
||||||
|
* code point for ENCODING. It uses a deterministic tiebreak (prefer a
|
||||||
|
* lower-case name, then the shortest, then alphabetical), which reproduces the
|
||||||
|
* historically-emitted name for ~1355 of the ~1414 encodable code points.
|
||||||
|
*
|
||||||
|
* This file pins the canonical name for the code points where the tiebreak
|
||||||
|
* would otherwise change the emitted entity. Every value here is still a valid
|
||||||
|
* WHATWG name for that code point (the generator asserts this) — these are
|
||||||
|
* editorial choices, not correctness fixes, kept so "To HTML Entity" output
|
||||||
|
* stays stable. Trim an entry to let the tiebreak decide instead.
|
||||||
|
*
|
||||||
|
* @author roberson-io [michaelroberson@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constant
|
||||||
|
* @type {Object.<number, string>}
|
||||||
|
*/
|
||||||
|
export const HTML_ENTITY_CANONICAL_OVERRIDES = {
|
||||||
|
124: "verbar",
|
||||||
|
168: "uml",
|
||||||
|
177: "plusmn",
|
||||||
|
189: "frac12",
|
||||||
|
247: "divide",
|
||||||
|
711: "caron",
|
||||||
|
728: "breve",
|
||||||
|
937: "Omega",
|
||||||
|
949: "epsilon",
|
||||||
|
965: "upsilon",
|
||||||
|
977: "thetasym",
|
||||||
|
978: "upsih",
|
||||||
|
981: "straightphi",
|
||||||
|
8208: "hyphen",
|
||||||
|
8214: "Verbar",
|
||||||
|
8230: "hellip",
|
||||||
|
8289: "ApplyFunction",
|
||||||
|
8290: "InvisibleTimes",
|
||||||
|
8291: "InvisibleComma",
|
||||||
|
8459: "hamilt",
|
||||||
|
8461: "quaternions",
|
||||||
|
8463: "planck",
|
||||||
|
8465: "image",
|
||||||
|
8472: "weierp",
|
||||||
|
8474: "rationals",
|
||||||
|
8476: "real",
|
||||||
|
8477: "reals",
|
||||||
|
8484: "integers",
|
||||||
|
8492: "bernou",
|
||||||
|
8499: "phmmat",
|
||||||
|
8500: "order",
|
||||||
|
8501: "alefsym",
|
||||||
|
8518: "DifferentialD",
|
||||||
|
8519: "ExponentialE",
|
||||||
|
8520: "ImaginaryI",
|
||||||
|
8612: "LeftTeeArrow",
|
||||||
|
8613: "UpTeeArrow",
|
||||||
|
8615: "DownTeeArrow",
|
||||||
|
8624: "lsh",
|
||||||
|
8625: "rsh",
|
||||||
|
8660: "hArr",
|
||||||
|
8704: "forall",
|
||||||
|
8711: "nabla",
|
||||||
|
8712: "isin",
|
||||||
|
8721: "sum",
|
||||||
|
8723: "mnplus",
|
||||||
|
8730: "radic",
|
||||||
|
8750: "conint",
|
||||||
|
8768: "wreath",
|
||||||
|
8776: "asymp",
|
||||||
|
8781: "asympeq",
|
||||||
|
8784: "esdot",
|
||||||
|
8788: "colone",
|
||||||
|
8869: "perp",
|
||||||
|
8896: "xwedge",
|
||||||
|
8897: "xvee",
|
||||||
|
8902: "sstarf",
|
||||||
|
10536: "nesear",
|
||||||
|
10537: "seswar"
|
||||||
|
};
|
||||||
203
src/core/config/scripts/newMinorVersion.mjs
Normal file
203
src/core/config/scripts/newMinorVersion.mjs
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* This script updates the CHANGELOG when a new minor version is created.
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2022
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint no-console: ["off"] */
|
||||||
|
/* eslint jsdoc/require-jsdoc: ["off"] */
|
||||||
|
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import process from "process";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
const ignoredAuthors = ["github-advanced-security[bot]", "dependabot[bot]"];
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const dir = path.join(process.cwd() + "/src/core/config/");
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
console.log("\nCWD: " + process.cwd());
|
||||||
|
console.log(
|
||||||
|
"Error: newMinorVersion.mjs should be run from the project root",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"Example> node --experimental-modules src/core/config/scripts/newMinorVersion.mjs",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let changelogData = fs.readFileSync(
|
||||||
|
path.join(process.cwd(), "CHANGELOG.md"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const lastVersion = changelogData.match(
|
||||||
|
/## Details\s+### \[(\d+)\.(\d+)\.(\d+)\]/,
|
||||||
|
);
|
||||||
|
const newVersion = [
|
||||||
|
parseInt(lastVersion[1], 10),
|
||||||
|
parseInt(lastVersion[2], 10) + 1,
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
|
||||||
|
let knownContributors = changelogData.match(/^\[@([^\]]+)\]/gm);
|
||||||
|
knownContributors = knownContributors.map((c) => c.slice(2, -1));
|
||||||
|
|
||||||
|
const date = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
|
const lastVersionSha = execSync(
|
||||||
|
`git rev-list -n 1 v${lastVersion[1]}.${lastVersion[2]}.${lastVersion[3]}`,
|
||||||
|
{
|
||||||
|
encoding: "utf8",
|
||||||
|
},
|
||||||
|
).trim();
|
||||||
|
if (lastVersionSha.length !== 40) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected output from git rev-list: ${lastVersionSha}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const features = [];
|
||||||
|
|
||||||
|
const commits = await (
|
||||||
|
await fetch(`https://api.github.com/repos/gchq/cyberchef/commits`)
|
||||||
|
).json();
|
||||||
|
let foundLast = false;
|
||||||
|
for (const commit of commits) {
|
||||||
|
if (commit.sha === lastVersionSha) {
|
||||||
|
foundLast = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
const feature = {
|
||||||
|
message: "",
|
||||||
|
authors: [],
|
||||||
|
id: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const msgparts = commit.commit.message.split("\n\n");
|
||||||
|
feature.message = msgparts[0];
|
||||||
|
const prIdMatch = feature.message.match(/\(#(\d+)\)$/);
|
||||||
|
if (prIdMatch !== null) {
|
||||||
|
feature.message = feature.message
|
||||||
|
.replace(prIdMatch[0], "")
|
||||||
|
.trim();
|
||||||
|
feature.id = prIdMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ignoredAuthors.includes(commit.author.login)) {
|
||||||
|
feature.authors.push(commit.author.login);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgparts.length > 1) {
|
||||||
|
msgparts[1]
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => line.startsWith("Co-authored-by: "))
|
||||||
|
.forEach((line) => {
|
||||||
|
let coAuthor = line.slice("Co-authored-by: ".length);
|
||||||
|
if (coAuthor.indexOf(">") !== -1) {
|
||||||
|
const email = coAuthor.slice(
|
||||||
|
coAuthor.indexOf("<") + 1,
|
||||||
|
coAuthor.indexOf(">"),
|
||||||
|
);
|
||||||
|
if (email.endsWith("@users.noreply.github.com")) {
|
||||||
|
coAuthor = email.slice(
|
||||||
|
email.indexOf("+") + 1,
|
||||||
|
-"@users.noreply.github.com".length,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Could not get ID of co-author: " +
|
||||||
|
coAuthor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Could not get email of co-author: " + coAuthor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!ignoredAuthors.includes(coAuthor)) {
|
||||||
|
feature.authors.push(coAuthor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
features.push(feature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundLast) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find last version commit: ${lastVersionSha} - need to add paging functionality`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = `### [${newVersion[0]}.${newVersion[1]}.${newVersion[2]}] - ${date}\n`;
|
||||||
|
|
||||||
|
const authors = [];
|
||||||
|
const prIDs = [];
|
||||||
|
const commitIDs = [];
|
||||||
|
|
||||||
|
features.forEach((feature) => {
|
||||||
|
const id =
|
||||||
|
feature.id.length > 10 ? feature.id.slice(0, 7) : "#" + feature.id;
|
||||||
|
message += `- ${feature.message} ${feature.authors.map((a) => `[@${a}]`).join(" ")} | [${id}]\n`;
|
||||||
|
|
||||||
|
feature.authors.forEach((author) => {
|
||||||
|
if (!knownContributors.includes(author)) {
|
||||||
|
knownContributors.push(author);
|
||||||
|
authors.push(`[@${author}]: https://github.com/${author}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (feature.id.length > 10) {
|
||||||
|
commitIDs.push(
|
||||||
|
`[${id}]: https://github.com/gchq/CyberChef/commit/${feature.id}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
prIDs.push(
|
||||||
|
`[#${feature.id}]: https://github.com/gchq/CyberChef/pull/${feature.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Message
|
||||||
|
changelogData = changelogData.replace(
|
||||||
|
/## Details\n\n/,
|
||||||
|
"## Details\n\n" + message + "\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tag
|
||||||
|
const newTag = `[${newVersion[0]}.${newVersion[1]}.${newVersion[2]}]: https://github.com/gchq/CyberChef/releases/tag/v${newVersion[0]}.${newVersion[1]}.${newVersion[2]}\n`;
|
||||||
|
changelogData = changelogData.replace(
|
||||||
|
/\n\n(\[\d+\.\d+\.\d+\]: https)/,
|
||||||
|
"\n\n" + newTag + "$1",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Author
|
||||||
|
authors.forEach((author) => {
|
||||||
|
changelogData = changelogData.replace(
|
||||||
|
/(\n\[@[^\]]+\]: https:\/\/github\.com\/[^\n]+\n)\n/,
|
||||||
|
"$1" + author + "\n\n",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Commit IDs
|
||||||
|
commitIDs.forEach((commitID) => {
|
||||||
|
changelogData = changelogData.replace(
|
||||||
|
/(\n\[[^\].]+\]: https:\/\/github.com\/gchq\/CyberChef\/commit\/[^\n]+\n)\n/,
|
||||||
|
"$1" + commitID + "\n\n",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// PR IDs
|
||||||
|
prIDs.forEach((prID) => {
|
||||||
|
changelogData = changelogData.replace(
|
||||||
|
/(\n\[#[^\]]+\]: https:\/\/github.com\/gchq\/CyberChef\/(?:pull|issues)\/[^\n]+\n)\n*$/,
|
||||||
|
"$1" + prID + "\n\n",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(process.cwd(), "CHANGELOG.md"), changelogData);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
@ -6,14 +6,14 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*eslint no-console: ["off"] */
|
/* eslint no-console: ["off"] */
|
||||||
|
|
||||||
import prompt from "prompt";
|
import prompt from "prompt";
|
||||||
import colors from "colors";
|
import colors from "colors";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import EscapeString from "../../operations/EscapeString";
|
import EscapeString from "../../operations/EscapeString.mjs";
|
||||||
|
|
||||||
|
|
||||||
const dir = path.join(process.cwd() + "/src/core/operations/");
|
const dir = path.join(process.cwd() + "/src/core/operations/");
|
||||||
@ -23,7 +23,7 @@ if (!fs.existsSync(dir)) {
|
|||||||
console.log("Example> node --experimental-modules src/core/config/scripts/newOperation.mjs");
|
console.log("Example> node --experimental-modules src/core/config/scripts/newOperation.mjs");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
const testDir = path.join(process.cwd() + "/tests/operations/tests/");
|
||||||
const ioTypes = ["string", "byteArray", "number", "html", "ArrayBuffer", "BigNumber", "JSON", "File", "List<File>"];
|
const ioTypes = ["string", "byteArray", "number", "html", "ArrayBuffer", "BigNumber", "JSON", "File", "List<File>"];
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -121,8 +121,32 @@ prompt.get(schema, (err, result) => {
|
|||||||
|
|
||||||
const moduleName = result.opName.replace(/\w\S*/g, txt => {
|
const moduleName = result.opName.replace(/\w\S*/g, txt => {
|
||||||
return txt.charAt(0).toUpperCase() + txt.substr(1);
|
return txt.charAt(0).toUpperCase() + txt.substr(1);
|
||||||
}).replace(/[\s-()/./]/g, "");
|
}).replace(/[\s-()./]/g, "");
|
||||||
|
|
||||||
|
const testTemplate = `/**
|
||||||
|
* ${moduleName} tests
|
||||||
|
*
|
||||||
|
* @author ${result.authorName} [${result.authorEmail}]
|
||||||
|
* @copyright Crown Copyright ${(new Date()).getFullYear()}
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import TestRegister from "../../lib/TestRegister.mjs";
|
||||||
|
|
||||||
|
TestRegister.addTests([
|
||||||
|
{
|
||||||
|
name: "${result.opName}: test",
|
||||||
|
input: "Example input",
|
||||||
|
expectedOutput: "Expected output",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "${result.opName}",
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
`;
|
||||||
|
|
||||||
const template = `/**
|
const template = `/**
|
||||||
* @author ${result.authorName} [${result.authorEmail}]
|
* @author ${result.authorName} [${result.authorEmail}]
|
||||||
@ -130,8 +154,8 @@ prompt.get(schema, (err, result) => {
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Operation from "../Operation";
|
import Operation from "../Operation.mjs";
|
||||||
import OperationError from "../errors/OperationError";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ${result.opName} operation
|
* ${result.opName} operation
|
||||||
@ -147,7 +171,7 @@ class ${moduleName} extends Operation {
|
|||||||
this.name = "${result.opName}";
|
this.name = "${result.opName}";
|
||||||
this.module = "${result.module}";
|
this.module = "${result.module}";
|
||||||
this.description = "${(new EscapeString).run(result.description, ["Special chars", "Double"])}";
|
this.description = "${(new EscapeString).run(result.description, ["Special chars", "Double"])}";
|
||||||
this.infoURL = "${result.infoURL}";
|
this.infoURL = "${result.infoURL}"; // Usually a Wikipedia link. Remember to remove localisation (i.e. https://wikipedia.org/etc rather than https://en.wikipedia.org/etc)
|
||||||
this.inputType = "${result.inputType}";
|
this.inputType = "${result.inputType}";
|
||||||
this.outputType = "${result.outputType}";
|
this.outputType = "${result.outputType}";
|
||||||
this.args = [
|
this.args = [
|
||||||
@ -208,7 +232,7 @@ ${result.highlight ? `
|
|||||||
export default ${moduleName};
|
export default ${moduleName};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
//console.log(template);
|
// console.log(template);
|
||||||
|
|
||||||
const filename = path.join(dir, `./${moduleName}.mjs`);
|
const filename = path.join(dir, `./${moduleName}.mjs`);
|
||||||
if (fs.existsSync(filename)) {
|
if (fs.existsSync(filename)) {
|
||||||
@ -218,13 +242,16 @@ export default ${moduleName};
|
|||||||
}
|
}
|
||||||
fs.writeFileSync(filename, template);
|
fs.writeFileSync(filename, template);
|
||||||
|
|
||||||
|
const testFilename = path.join(testDir, `./${moduleName}.mjs`);
|
||||||
|
fs.writeFileSync(testFilename, testTemplate);
|
||||||
|
|
||||||
console.log(`\nOperation template written to ${colors.green(filename)}`);
|
console.log(`\nOperation template written to ${colors.green(filename)}`);
|
||||||
|
console.log(`\nOperation test template written to ${colors.green(testFilename)}`);
|
||||||
console.log(`\nNext steps:
|
console.log(`\nNext steps:
|
||||||
1. Add your operation to ${colors.green("src/core/config/Categories.json")}
|
1. Add your operation to ${colors.green("src/core/config/Categories.json")}
|
||||||
2. Write your operation code.
|
2. Write your operation code in ${colors.green(filename)}
|
||||||
3. Write tests in ${colors.green("tests/operations/tests/")}
|
3. Write your operation test code in ${colors.green(testFilename)}
|
||||||
4. Run ${colors.cyan("npm run lint")} and ${colors.cyan("npm run test")}
|
4. Run ${colors.cyan("npm run lint")} and ${colors.cyan("npm run test")}
|
||||||
5. Submit a Pull Request to get your operation added to the official CyberChef repository.`);
|
5. Submit a Pull Request to get your operation added to the official CyberChef repository.`);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
38
src/core/dishTypes/DishBigNumber.mjs
Normal file
38
src/core/dishTypes/DishBigNumber.mjs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DishType from "./DishType.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
import BigNumber from "bignumber.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* translation methods for BigNumber Dishes
|
||||||
|
*/
|
||||||
|
class DishBigNumber extends DishType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value to a ArrayBuffer
|
||||||
|
* @param {BigNumber} value
|
||||||
|
*/
|
||||||
|
static toArrayBuffer() {
|
||||||
|
DishBigNumber.checkForValue(this.value);
|
||||||
|
this.value = BigNumber.isBigNumber(this.value) ? Utils.strToArrayBuffer(this.value.toFixed()) : new ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value from a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static fromArrayBuffer() {
|
||||||
|
DishBigNumber.checkForValue(this.value);
|
||||||
|
try {
|
||||||
|
this.value = new BigNumber(Utils.arrayBufferToStr(this.value));
|
||||||
|
} catch (err) {
|
||||||
|
this.value = new BigNumber(NaN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DishBigNumber;
|
||||||
31
src/core/dishTypes/DishByteArray.mjs
Normal file
31
src/core/dishTypes/DishByteArray.mjs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DishType from "./DishType.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation methods for ArrayBuffer Dishes
|
||||||
|
*/
|
||||||
|
class DishByteArray extends DishType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value to a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static toArrayBuffer() {
|
||||||
|
DishByteArray.checkForValue(this.value);
|
||||||
|
this.value = new Uint8Array(this.value).buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value from a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static fromArrayBuffer() {
|
||||||
|
DishByteArray.checkForValue(this.value);
|
||||||
|
this.value = Array.prototype.slice.call(new Uint8Array(this.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DishByteArray;
|
||||||
42
src/core/dishTypes/DishFile.mjs
Normal file
42
src/core/dishTypes/DishFile.mjs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DishType from "./DishType.mjs";
|
||||||
|
import Utils, { isNodeEnvironment } from "../Utils.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation methods for file Dishes
|
||||||
|
*/
|
||||||
|
class DishFile extends DishType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value to an ArrayBuffer
|
||||||
|
* @param {File} value
|
||||||
|
*/
|
||||||
|
static toArrayBuffer() {
|
||||||
|
DishFile.checkForValue(this.value);
|
||||||
|
if (isNodeEnvironment()) {
|
||||||
|
this.value = Utils.readFileSync(this.value);
|
||||||
|
} else {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Utils.readFile(this.value)
|
||||||
|
.then(v => this.value = v.buffer)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value from an ArrayBuffer
|
||||||
|
*/
|
||||||
|
static fromArrayBuffer() {
|
||||||
|
DishFile.checkForValue(this.value);
|
||||||
|
this.value = new File(this.value, "unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DishFile;
|
||||||
26
src/core/dishTypes/DishHTML.mjs
Normal file
26
src/core/dishTypes/DishHTML.mjs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DishString from "./DishString.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation methods for HTML Dishes
|
||||||
|
*/
|
||||||
|
class DishHTML extends DishString {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value to a ArrayBuffer
|
||||||
|
* @param {String} value
|
||||||
|
*/
|
||||||
|
static toArrayBuffer() {
|
||||||
|
DishHTML.checkForValue(this.value);
|
||||||
|
this.value = this.value ? Utils.strToArrayBuffer(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : new ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DishHTML;
|
||||||
32
src/core/dishTypes/DishJSON.mjs
Normal file
32
src/core/dishTypes/DishJSON.mjs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DishType from "./DishType.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation methods for JSON dishes
|
||||||
|
*/
|
||||||
|
class DishJSON extends DishType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value to a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static toArrayBuffer() {
|
||||||
|
DishJSON.checkForValue(this.value);
|
||||||
|
this.value = this.value !== undefined ? Utils.strToArrayBuffer(JSON.stringify(this.value, null, 4)) : new ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value from a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static fromArrayBuffer() {
|
||||||
|
DishJSON.checkForValue(this.value);
|
||||||
|
this.value = JSON.parse(Utils.arrayBufferToStr(this.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DishJSON;
|
||||||
80
src/core/dishTypes/DishListFile.mjs
Normal file
80
src/core/dishTypes/DishListFile.mjs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DishType from "./DishType.mjs";
|
||||||
|
import Utils, { isNodeEnvironment } from "../Utils.mjs";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation methods for ListFile Dishes
|
||||||
|
*/
|
||||||
|
class DishListFile extends DishType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value to a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static async toArrayBuffer() {
|
||||||
|
DishListFile.checkForValue(this.value);
|
||||||
|
|
||||||
|
if (isNodeEnvironment()) {
|
||||||
|
this.value = this.value.map(file => Uint8Array.from(file.data));
|
||||||
|
} else {
|
||||||
|
this.value = await DishListFile.concatenateTypedArraysWithTypedElements(...this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value from a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static fromArrayBuffer() {
|
||||||
|
DishListFile.checkForValue(this.value);
|
||||||
|
this.value = [new File(this.value, "unknown")];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenates a list of typed elements together.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array[]} arrays
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
static async concatenateTypedArraysWithTypedElements(...arrays) {
|
||||||
|
let totalLength = 0;
|
||||||
|
for (const arr of arrays) {
|
||||||
|
totalLength += arr.size;
|
||||||
|
}
|
||||||
|
const myArray = new Uint8Array(totalLength);
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
for (const arr of arrays) {
|
||||||
|
const data = await Utils.readFile(arr);
|
||||||
|
myArray.set(data, offset);
|
||||||
|
offset += data.length;
|
||||||
|
}
|
||||||
|
return myArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenates a list of Uint8Arrays together
|
||||||
|
*
|
||||||
|
* @param {Uint8Array[]} arrays
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
static concatenateTypedArrays(...arrays) {
|
||||||
|
let totalLength = 0;
|
||||||
|
for (const arr of arrays) {
|
||||||
|
totalLength += arr.length;
|
||||||
|
}
|
||||||
|
const result = new Uint8Array(totalLength);
|
||||||
|
let offset = 0;
|
||||||
|
for (const arr of arrays) {
|
||||||
|
result.set(arr, offset);
|
||||||
|
offset += arr.length;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DishListFile;
|
||||||
33
src/core/dishTypes/DishNumber.mjs
Normal file
33
src/core/dishTypes/DishNumber.mjs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import DishType from "./DishType.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation methods for number dishes
|
||||||
|
*/
|
||||||
|
class DishNumber extends DishType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value to a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static toArrayBuffer() {
|
||||||
|
DishNumber.checkForValue(this.value);
|
||||||
|
this.value = typeof this.value === "number" ? Utils.strToArrayBuffer(this.value.toString()) : new ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value from a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static fromArrayBuffer() {
|
||||||
|
DishNumber.checkForValue(this.value);
|
||||||
|
this.value = this.value ? parseFloat(Utils.arrayBufferToStr(this.value)) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DishNumber;
|
||||||
33
src/core/dishTypes/DishString.mjs
Normal file
33
src/core/dishTypes/DishString.mjs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import DishType from "./DishType.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation methods for string dishes
|
||||||
|
*/
|
||||||
|
class DishString extends DishType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value to a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static toArrayBuffer() {
|
||||||
|
DishString.checkForValue(this.value);
|
||||||
|
this.value = this.value ? Utils.strToArrayBuffer(this.value) : new ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value from a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static fromArrayBuffer() {
|
||||||
|
DishString.checkForValue(this.value);
|
||||||
|
this.value = this.value ? Utils.arrayBufferToStr(this.value) : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DishString;
|
||||||
38
src/core/dishTypes/DishType.mjs
Normal file
38
src/core/dishTypes/DishType.mjs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for dish translation methods
|
||||||
|
*/
|
||||||
|
class DishType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warn translations don't work without value from bind
|
||||||
|
*/
|
||||||
|
static checkForValue(value) {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error("only use translation methods with .bind");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value to a ArrayBuffer
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
static toArrayBuffer() {
|
||||||
|
throw new Error("toArrayBuffer has not been implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the given value from a ArrayBuffer
|
||||||
|
*/
|
||||||
|
static fromArrayBuffer() {
|
||||||
|
throw new Error("fromArrayBuffer has not been implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DishType;
|
||||||
26
src/core/dishTypes/index.mjs
Normal file
26
src/core/dishTypes/index.mjs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import DishByteArray from "./DishByteArray.mjs";
|
||||||
|
import DishBigNumber from "./DishBigNumber.mjs";
|
||||||
|
import DishFile from "./DishFile.mjs";
|
||||||
|
import DishHTML from "./DishHTML.mjs";
|
||||||
|
import DishJSON from "./DishJSON.mjs";
|
||||||
|
import DishListFile from "./DishListFile.mjs";
|
||||||
|
import DishNumber from "./DishNumber.mjs";
|
||||||
|
import DishString from "./DishString.mjs";
|
||||||
|
|
||||||
|
export {
|
||||||
|
DishByteArray,
|
||||||
|
DishBigNumber,
|
||||||
|
DishFile,
|
||||||
|
DishHTML,
|
||||||
|
DishJSON,
|
||||||
|
DishListFile,
|
||||||
|
DishNumber,
|
||||||
|
DishString,
|
||||||
|
};
|
||||||
25
src/core/errors/ExcludedOperationError.mjs
Normal file
25
src/core/errors/ExcludedOperationError.mjs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Custom error type for handling operation that isn't included in node.js API
|
||||||
|
*
|
||||||
|
* @author d98762625 [d98762625@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2018
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
class ExcludedOperationError extends Error {
|
||||||
|
/**
|
||||||
|
* Standard error constructor. Adds no new behaviour.
|
||||||
|
*
|
||||||
|
* @param args - Standard error args
|
||||||
|
*/
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
this.type = "ExcludedOperationError";
|
||||||
|
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, ExcludedOperationError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExcludedOperationError;
|
||||||
9
src/core/errors/index.mjs
Normal file
9
src/core/errors/index.mjs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import OperationError from "./OperationError.mjs";
|
||||||
|
import DishError from "./DishError.mjs";
|
||||||
|
import ExcludedOperationError from "./ExcludedOperationError.mjs";
|
||||||
|
|
||||||
|
export {
|
||||||
|
OperationError,
|
||||||
|
DishError,
|
||||||
|
ExcludedOperationError,
|
||||||
|
};
|
||||||
@ -5,7 +5,7 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Utils from "../Utils";
|
import Utils from "../Utils.mjs";
|
||||||
import BigNumber from "bignumber.js";
|
import BigNumber from "bignumber.js";
|
||||||
|
|
||||||
|
|
||||||
@ -108,19 +108,35 @@ export function mean(data) {
|
|||||||
* @returns {BigNumber}
|
* @returns {BigNumber}
|
||||||
*/
|
*/
|
||||||
export function median(data) {
|
export function median(data) {
|
||||||
if ((data.length % 2) === 0 && data.length > 0) {
|
if (data.length > 0) {
|
||||||
data.sort(function(a, b){
|
data.sort(function(a, b) {
|
||||||
return a.minus(b);
|
return a.minus(b);
|
||||||
});
|
});
|
||||||
const first = data[Math.floor(data.length / 2)];
|
|
||||||
const second = data[Math.floor(data.length / 2) - 1];
|
if ((data.length % 2) === 0) {
|
||||||
return mean([first, second]);
|
const first = data[Math.floor(data.length / 2)];
|
||||||
} else {
|
const second = data[Math.floor(data.length / 2) - 1];
|
||||||
|
return mean([first, second]);
|
||||||
|
}
|
||||||
|
|
||||||
return data[Math.floor(data.length / 2)];
|
return data[Math.floor(data.length / 2)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes modulo of two numbers and returns the value.
|
||||||
|
*
|
||||||
|
* @param {BigNumber[]} data
|
||||||
|
* @returns {BigNumber}
|
||||||
|
*/
|
||||||
|
export function mod(data) {
|
||||||
|
if (data.length > 0) {
|
||||||
|
return data.reduce((acc, curr) => acc.mod(curr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes standard deviation of a number array and returns the value.
|
* Computes standard deviation of a number array and returns the value.
|
||||||
*
|
*
|
||||||
|
|||||||
103
src/core/lib/AudioBytes.mjs
Normal file
103
src/core/lib/AudioBytes.mjs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Byte-reading and text-decoding utilities for audio metadata parsing.
|
||||||
|
*
|
||||||
|
* @author d0s1nt [d0s1nt@cyberchefaudio]
|
||||||
|
* @copyright Crown Copyright 2025
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @returns {string} 4-byte ASCII at offset, or "" if out of bounds. */
|
||||||
|
export function ascii4(b, off) {
|
||||||
|
if (off + 4 > b.length) return "";
|
||||||
|
return String.fromCharCode(b[off], b[off + 1], b[off + 2], b[off + 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {number} Byte offset of ASCII needle `s`, or -1. */
|
||||||
|
export function indexOfAscii(b, s, start, end) {
|
||||||
|
const limit = Math.max(0, Math.min(end, b.length) - s.length);
|
||||||
|
for (let i = start; i <= limit; i++) {
|
||||||
|
let ok = true;
|
||||||
|
for (let j = 0; j < s.length; j++) {
|
||||||
|
if (b[i + j] !== s.charCodeAt(j)) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ok) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {number} Unsigned 32-bit big-endian read. */
|
||||||
|
export function u32be(bytes, off) {
|
||||||
|
return ((bytes[off] << 24) >>> 0) | (bytes[off + 1] << 16) | (bytes[off + 2] << 8) | bytes[off + 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {number} Unsigned 32-bit little-endian read. */
|
||||||
|
export function u32le(bytes, off) {
|
||||||
|
return (bytes[off] | (bytes[off + 1] << 8) | (bytes[off + 2] << 16) | (bytes[off + 3] << 24)) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {number} Unsigned 16-bit little-endian read. */
|
||||||
|
export function u16le(bytes, off) {
|
||||||
|
return bytes[off] | (bytes[off + 1] << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {BigInt} Unsigned 64-bit little-endian read. */
|
||||||
|
export function u64le(bytes, off) {
|
||||||
|
return BigInt(u32le(bytes, off)) | (BigInt(u32le(bytes, off + 4)) << 32n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {number} Decoded ID3v2 synchsafe integer from four 7-bit bytes. */
|
||||||
|
export function synchsafeToInt(b0, b1, b2, b3) {
|
||||||
|
return ((b0 & 0x7f) << 21) | ((b1 & 0x7f) << 14) | ((b2 & 0x7f) << 7) | (b3 & 0x7f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {string} Decoded UTF-16LE byte range, nulls stripped. */
|
||||||
|
export function decodeUtf16LE(b, off, len) {
|
||||||
|
if (len <= 0 || off + len > b.length) return "";
|
||||||
|
try {
|
||||||
|
return new TextDecoder("utf-16le").decode(b.slice(off, off + len)).replace(/\u0000/g, "").trim();
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {{valueBytes: Uint8Array, next: number}} Bytes until null terminator, UTF-16 aware. */
|
||||||
|
export function readNullTerminated(bytes, start, encoding) {
|
||||||
|
const isUtf16 = encoding === 1 || encoding === 2;
|
||||||
|
if (!isUtf16) {
|
||||||
|
let i = start;
|
||||||
|
while (i < bytes.length && bytes[i] !== 0x00) i++;
|
||||||
|
return { valueBytes: bytes.slice(start, i), next: i + 1 };
|
||||||
|
}
|
||||||
|
let i = start;
|
||||||
|
while (i + 1 < bytes.length && !(bytes[i] === 0x00 && bytes[i + 1] === 0x00)) i += 2;
|
||||||
|
return { valueBytes: bytes.slice(start, i), next: i + 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ID3_ENCODINGS = ["iso-8859-1", "utf-16", "utf-16be", "utf-8"];
|
||||||
|
|
||||||
|
/** @returns {string} Text decoded using ID3v2 encoding byte (0=latin1, 1=utf16, 2=utf16be, 3=utf8). */
|
||||||
|
export function decodeText(bytes, encoding) {
|
||||||
|
if (!bytes || bytes.length === 0) return "";
|
||||||
|
try {
|
||||||
|
return new TextDecoder(ID3_ENCODINGS[encoding] || "utf-16").decode(bytes);
|
||||||
|
} catch {
|
||||||
|
return safeUtf8(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {string} UTF-8 decode with replacement (never throws). */
|
||||||
|
export function safeUtf8(bytes) {
|
||||||
|
try {
|
||||||
|
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {string} ISO-8859-1 decode, nulls stripped, trimmed. */
|
||||||
|
export function decodeLatin1Trim(bytes) {
|
||||||
|
return decodeText(bytes, 0).replace(/\u0000/g, "").trim();
|
||||||
|
}
|
||||||
82
src/core/lib/AudioMetaSchema.mjs
Normal file
82
src/core/lib/AudioMetaSchema.mjs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Report skeleton and container detection for audio metadata extraction.
|
||||||
|
*
|
||||||
|
* @author d0s1nt [d0s1nt@cyberchefaudio]
|
||||||
|
* @copyright Crown Copyright 2025
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
import { ascii4, indexOfAscii } from "./AudioBytes.mjs";
|
||||||
|
|
||||||
|
/** Builds the empty report skeleton ready for a format parser to populate. */
|
||||||
|
export function makeEmptyReport(filename, byteLength, container) {
|
||||||
|
return {
|
||||||
|
schema_version: "audio-meta-1.0",
|
||||||
|
artifact: {
|
||||||
|
filename,
|
||||||
|
byte_length: byteLength,
|
||||||
|
container: { type: container.type, brand: container.brand || null, mime: container.mime || null },
|
||||||
|
},
|
||||||
|
detections: { metadata_systems: [], provenance_systems: [] },
|
||||||
|
tags: {
|
||||||
|
common: {
|
||||||
|
title: null, artist: null, album: null, date: null, track: null,
|
||||||
|
genre: null, comment: null, composer: null, copyright: null, language: null,
|
||||||
|
},
|
||||||
|
raw: {},
|
||||||
|
},
|
||||||
|
embedded: [],
|
||||||
|
provenance: {
|
||||||
|
c2pa: {
|
||||||
|
present: false,
|
||||||
|
embedding: [],
|
||||||
|
manifest_store: { active_manifest_urn: null, instance_id: null, claim_generator: null },
|
||||||
|
assertions: [],
|
||||||
|
signature: {
|
||||||
|
algorithm: null, signing_time: null,
|
||||||
|
certificate: { subject_cn: null, issuer_cn: null, serial_number: null },
|
||||||
|
},
|
||||||
|
validation: { validation_state: "Unknown", reasons: [], details_raw: null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Detects the audio container format from magic bytes. */
|
||||||
|
export function sniffContainer(b) {
|
||||||
|
if (b.length >= 3 && b[0] === 0x49 && b[1] === 0x44 && b[2] === 0x33)
|
||||||
|
return { type: "mp3", mime: "audio/mpeg" };
|
||||||
|
if (b.length >= 2 && b[0] === 0xff && (b[1] & 0xe0) === 0xe0) {
|
||||||
|
if ((b[1] & 0x06) === 0x00) return { type: "aac", mime: "audio/aac" };
|
||||||
|
return { type: "mp3", mime: "audio/mpeg" };
|
||||||
|
}
|
||||||
|
if (b.length >= 8 && b[0] === 0x0b && b[1] === 0x77)
|
||||||
|
return { type: "ac3", mime: "audio/ac3" };
|
||||||
|
if (b.length >= 16 &&
|
||||||
|
b[0] === 0x30 && b[1] === 0x26 && b[2] === 0xb2 && b[3] === 0x75 &&
|
||||||
|
b[4] === 0x8e && b[5] === 0x66 && b[6] === 0xcf && b[7] === 0x11)
|
||||||
|
return { type: "wma", mime: "audio/x-ms-wma" };
|
||||||
|
if (b.length >= 12 && ascii4(b, 0) === "RIFF" && ascii4(b, 8) === "WAVE")
|
||||||
|
return { type: "wav", mime: "audio/wav" };
|
||||||
|
if (b.length >= 12 && ascii4(b, 0) === "BW64" && ascii4(b, 8) === "WAVE")
|
||||||
|
return { type: "bw64", mime: "audio/wav" };
|
||||||
|
if (b.length >= 4 && ascii4(b, 0) === "fLaC")
|
||||||
|
return { type: "flac", mime: "audio/flac" };
|
||||||
|
if (b.length >= 4 && ascii4(b, 0) === "OggS") {
|
||||||
|
const idx = indexOfAscii(b, "OpusHead", 0, Math.min(b.length, 65536));
|
||||||
|
return idx >= 0 ? { type: "opus", mime: "audio/ogg" } : { type: "ogg", mime: "audio/ogg" };
|
||||||
|
}
|
||||||
|
if (b.length >= 12 && ascii4(b, 4) === "ftyp") {
|
||||||
|
const brand = ascii4(b, 8);
|
||||||
|
const isM4A = brand === "M4A " || brand === "M4B " || brand === "M4P ";
|
||||||
|
return { type: isM4A ? "m4a" : "mp4", mime: isM4A ? "audio/mp4" : "video/mp4", brand };
|
||||||
|
}
|
||||||
|
if (b.length >= 12 && ascii4(b, 0) === "FORM") {
|
||||||
|
const formType = ascii4(b, 8);
|
||||||
|
if (formType === "AIFF" || formType === "AIFC") return { type: "aiff", mime: "audio/aiff", brand: formType };
|
||||||
|
}
|
||||||
|
return { type: "unknown", mime: null };
|
||||||
|
}
|
||||||
630
src/core/lib/AudioParsers.mjs
Normal file
630
src/core/lib/AudioParsers.mjs
Normal file
@ -0,0 +1,630 @@
|
|||||||
|
/**
|
||||||
|
* Format-specific audio metadata parsers.
|
||||||
|
*
|
||||||
|
* @author d0s1nt [d0s1nt@cyberchefaudio]
|
||||||
|
* @copyright Crown Copyright 2025
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
import {
|
||||||
|
ascii4, indexOfAscii,
|
||||||
|
u32be, u32le, u16le, u64le, synchsafeToInt,
|
||||||
|
decodeUtf16LE, readNullTerminated, decodeText,
|
||||||
|
safeUtf8, decodeLatin1Trim,
|
||||||
|
} from "./AudioBytes.mjs";
|
||||||
|
|
||||||
|
/** Parses MP3 metadata: ID3v2 frames, ID3v1 footer, APEv2 tags. */
|
||||||
|
export function parseMp3(b, report) {
|
||||||
|
processId3v2(b, report);
|
||||||
|
processId3v1(b, report);
|
||||||
|
|
||||||
|
const ape = parseApeV2BestEffort(b);
|
||||||
|
if (ape) {
|
||||||
|
report.detections.metadata_systems.push("apev2");
|
||||||
|
report.tags.raw.apev2 = ape;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Iterates ID3v2 frames and populates the report. */
|
||||||
|
function processId3v2(b, report) {
|
||||||
|
report.detections.metadata_systems.push("id3v2");
|
||||||
|
|
||||||
|
const id3 = parseId3v2(b);
|
||||||
|
report.tags.raw.id3v2 = id3 ? { header: id3.header, frames: [] } : null;
|
||||||
|
|
||||||
|
if (id3) {
|
||||||
|
for (const f of id3.frames) {
|
||||||
|
const entry = { id: f.id, size: f.size, description: ID3_FRAME_DESCRIPTIONS[f.id] || null };
|
||||||
|
|
||||||
|
if (f.id[0] === "T" && f.id !== "TXXX") {
|
||||||
|
const text = f.data?.length >= 1 ?
|
||||||
|
decodeText(f.data.slice(1), f.data[0]).replace(/\u0000/g, "").trim() :
|
||||||
|
"";
|
||||||
|
entry.decoded = text;
|
||||||
|
if (f.id === "TLEN") {
|
||||||
|
const ms = normalizeTlen(text);
|
||||||
|
if (ms !== null) entry.normalized_ms = ms;
|
||||||
|
}
|
||||||
|
mapCommonId3(report, f.id, text);
|
||||||
|
} else if (f.id === "TXXX") {
|
||||||
|
const txxx = decodeTxxx(f.data);
|
||||||
|
entry.decoded = txxx;
|
||||||
|
if (!report.tags.raw.id3v2.txxx) report.tags.raw.id3v2.txxx = [];
|
||||||
|
report.tags.raw.id3v2.txxx.push(txxx);
|
||||||
|
} else if (f.id === "COMM") {
|
||||||
|
const comm = decodeCommFrame(f.data);
|
||||||
|
entry.decoded = comm;
|
||||||
|
if (comm?.text && !report.tags.common.comment) report.tags.common.comment = comm.text;
|
||||||
|
} else if (f.id === "GEOB") {
|
||||||
|
processGeobFrame(f, entry, report);
|
||||||
|
}
|
||||||
|
|
||||||
|
report.tags.raw.id3v2.frames.push(entry);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
report.detections.metadata_systems = report.detections.metadata_systems.filter((x) => x !== "id3v2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses GEOB frame contents, populates entry, embedded objects, and C2PA provenance. */
|
||||||
|
function processGeobFrame(f, entry, report) {
|
||||||
|
const d = f.data, enc = d[0];
|
||||||
|
let off = 1;
|
||||||
|
const mime = readNullTerminated(d, off, 0);
|
||||||
|
const mimeType = decodeLatin1Trim(mime.valueBytes);
|
||||||
|
off = mime.next;
|
||||||
|
const file = readNullTerminated(d, off, enc);
|
||||||
|
const filename = decodeText(file.valueBytes, enc).replace(/\u0000/g, "").trim();
|
||||||
|
off = file.next;
|
||||||
|
const desc = readNullTerminated(d, off, enc);
|
||||||
|
const description = decodeText(desc.valueBytes, enc).replace(/\u0000/g, "").trim();
|
||||||
|
off = desc.next;
|
||||||
|
const objLen = d.length - off;
|
||||||
|
|
||||||
|
entry.geob = { mimeType, filename, description, object_bytes: objLen };
|
||||||
|
const geobId = `geob_${report.embedded.filter((x) => x.source === "id3v2:GEOB").length}`;
|
||||||
|
report.embedded.push({
|
||||||
|
id: geobId, source: "id3v2:GEOB",
|
||||||
|
content_type: mimeType || null, byte_length: objLen,
|
||||||
|
description: description || null, filename: filename || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mt = (mimeType || "").toLowerCase();
|
||||||
|
if (mt.includes("c2pa") || mt.includes("jumbf") || mt.includes("application/x-c2pa-manifest-store")) {
|
||||||
|
report.provenance.c2pa.present = true;
|
||||||
|
report.provenance.c2pa.embedding.push({
|
||||||
|
carrier: "id3v2:GEOB", content_type: mimeType || null, byte_length: objLen,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Processes the 128-byte ID3v1 footer tag. */
|
||||||
|
function processId3v1(b, report) {
|
||||||
|
const id3v1 = parseId3v1(b);
|
||||||
|
if (!id3v1) return;
|
||||||
|
|
||||||
|
report.detections.metadata_systems.push("id3v1");
|
||||||
|
report.tags.raw.id3v1 = id3v1;
|
||||||
|
mapCommon(report, id3v1, ID3V1_TO_COMMON);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses WAV/BWF/BW64 RIFF chunks: LIST/INFO, bext, iXML, axml, ds64. */
|
||||||
|
export function parseRiffWave(b, report, maxTextBytes) {
|
||||||
|
report.detections.metadata_systems.push("riff_info");
|
||||||
|
|
||||||
|
const chunks = enumerateChunks(b, 12, b.length, 50000);
|
||||||
|
const riff = { chunks: [], info: null, bext: null, ixml: null, axml: null, ds64: null };
|
||||||
|
|
||||||
|
const info = {};
|
||||||
|
for (const c of chunks) {
|
||||||
|
riff.chunks.push({ id: c.id, size: c.size, offset: c.dataOff });
|
||||||
|
processRiffChunk(b, c, riff, info, report, maxTextBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
riff.info = Object.keys(info).length ? info : null;
|
||||||
|
report.tags.raw.riff = riff;
|
||||||
|
if (riff.info) mapCommon(report, riff.info, RIFF_TO_COMMON);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Processes a single RIFF chunk, updating riff state and the report. */
|
||||||
|
function processRiffChunk(b, c, riff, info, report, maxTextBytes) {
|
||||||
|
if (c.id === "ds64") {
|
||||||
|
riff.ds64 = { present: true, size: c.size };
|
||||||
|
if (!report.detections.metadata_systems.includes("bw64_ds64")) report.detections.metadata_systems.push("bw64_ds64");
|
||||||
|
if (report.artifact.container.type === "wav") report.artifact.container.type = "bw64";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.id === "LIST" && ascii4(b, c.dataOff) === "INFO") {
|
||||||
|
for (const s of enumerateChunks(b, c.dataOff + 4, c.dataOff + c.size, 10000))
|
||||||
|
info[s.id] = decodeLatin1Trim(b.slice(s.dataOff, s.dataOff + s.size));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.id === "bext") {
|
||||||
|
if (!report.detections.metadata_systems.includes("bwf_bext")) report.detections.metadata_systems.push("bwf_bext");
|
||||||
|
riff.bext = parseBext(b, c.dataOff, c.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.id === "iXML" || c.id === "axml") {
|
||||||
|
const key = c.id === "iXML" ? "ixml" : "axml";
|
||||||
|
if (!report.detections.metadata_systems.includes(key)) report.detections.metadata_systems.push(key);
|
||||||
|
const payload = b.slice(c.dataOff, c.dataOff + c.size);
|
||||||
|
riff[key] = { xml: safeUtf8(payload.slice(0, Math.min(payload.length, maxTextBytes))), truncated: payload.length > maxTextBytes };
|
||||||
|
report.embedded.push({
|
||||||
|
id: `${key}_0`, source: `riff:${c.id}`, content_type: "application/xml",
|
||||||
|
byte_length: payload.length, description: `${c.id} chunk`, filename: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses FLAC metablocks: STREAMINFO, Vorbis Comment, PICTURE. */
|
||||||
|
export function parseFlac(b, report, maxTextBytes) {
|
||||||
|
report.detections.metadata_systems.push("flac_metablocks");
|
||||||
|
|
||||||
|
const blocks = parseFlacMetaBlocks(b);
|
||||||
|
report.tags.raw.flac = { blocks: [] };
|
||||||
|
|
||||||
|
for (const blk of blocks) {
|
||||||
|
report.tags.raw.flac.blocks.push({ type: blk.typeName, length: blk.length });
|
||||||
|
|
||||||
|
if (blk.typeName === "VORBIS_COMMENT") {
|
||||||
|
if (!report.detections.metadata_systems.includes("vorbis_comments")) report.detections.metadata_systems.push("vorbis_comments");
|
||||||
|
const vc = parseVorbisComment(blk.data);
|
||||||
|
report.tags.raw.vorbis_comments = vc;
|
||||||
|
mapVorbisCommon(report, vc);
|
||||||
|
} else if (blk.typeName === "PICTURE") {
|
||||||
|
const pic = parseFlacPicture(blk.data, maxTextBytes);
|
||||||
|
report.embedded.push({
|
||||||
|
id: `cover_art_${report.embedded.filter((x) => x.id.startsWith("cover_art_")).length}`,
|
||||||
|
source: "flac:PICTURE", content_type: pic.mime || null,
|
||||||
|
byte_length: pic.dataLength, description: pic.description || null, filename: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses OGG/Opus Vorbis comments. */
|
||||||
|
export function parseOgg(b, report) {
|
||||||
|
if (!report.detections.metadata_systems.includes("ogg_opus_tags")) report.detections.metadata_systems.push("ogg_opus_tags");
|
||||||
|
|
||||||
|
const scanEnd = Math.min(b.length, 1024 * 1024);
|
||||||
|
let tags = null;
|
||||||
|
const opusTagsIdx = indexOfAscii(b, "OpusTags", 0, scanEnd);
|
||||||
|
if (opusTagsIdx >= 0) {
|
||||||
|
report.artifact.container.type = "opus";
|
||||||
|
tags = parseVorbisComment(b.slice(opusTagsIdx + 8, scanEnd));
|
||||||
|
} else {
|
||||||
|
const vorbisIdx = indexOfAscii(b, "\x03vorbis", 0, scanEnd);
|
||||||
|
if (vorbisIdx >= 0) tags = parseVorbisComment(b.slice(vorbisIdx + 7, scanEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
report.tags.raw.ogg = { has_opustags: opusTagsIdx >= 0, has_vorbis_comment: !!tags };
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
if (!report.detections.metadata_systems.includes("vorbis_comments")) report.detections.metadata_systems.push("vorbis_comments");
|
||||||
|
report.tags.raw.vorbis_comments = tags;
|
||||||
|
mapVorbisCommon(report, tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Best-effort top-level atom scan for MP4/M4A. */
|
||||||
|
export function parseMp4BestEffort(b, report) {
|
||||||
|
report.detections.metadata_systems.push("mp4_atoms");
|
||||||
|
const atoms = [];
|
||||||
|
|
||||||
|
let off = 0;
|
||||||
|
while (off + 8 <= b.length && atoms.length < 2000) {
|
||||||
|
const size = u32be(b, off);
|
||||||
|
const type = ascii4(b, off + 4);
|
||||||
|
if (size < 8) break;
|
||||||
|
atoms.push({ type, size, offset: off });
|
||||||
|
off += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
report.tags.raw.mp4 = {
|
||||||
|
top_level_atoms: atoms.slice(0, 200),
|
||||||
|
hints: {
|
||||||
|
hasMoov: atoms.some((a) => a.type === "moov"),
|
||||||
|
hasUdta: atoms.some((a) => a.type === "udta"),
|
||||||
|
hasMeta: atoms.some((a) => a.type === "meta"),
|
||||||
|
hasIlst: atoms.some((a) => a.type === "ilst"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Best-effort AIFF/AIFC chunk scanning for NAME, AUTH, ANNO. */
|
||||||
|
export function parseAiffBestEffort(b, report, maxTextBytes) {
|
||||||
|
report.detections.metadata_systems.push("aiff_chunks");
|
||||||
|
let off = 12;
|
||||||
|
const chunks = [];
|
||||||
|
while (off + 8 <= b.length && chunks.length < 2000) {
|
||||||
|
const id = ascii4(b, off);
|
||||||
|
const size = u32be(b, off + 4);
|
||||||
|
const dataOff = off + 8;
|
||||||
|
chunks.push({ id, size, offset: off });
|
||||||
|
|
||||||
|
if (["NAME", "AUTH", "ANNO", "(c) "].includes(id)) {
|
||||||
|
const txt = safeUtf8(b.slice(dataOff, dataOff + Math.min(size, maxTextBytes)));
|
||||||
|
if (!report.tags.raw.aiff) report.tags.raw.aiff = { chunks: [] };
|
||||||
|
report.tags.raw.aiff.chunks.push({ id, value: txt, truncated: size > maxTextBytes });
|
||||||
|
}
|
||||||
|
|
||||||
|
off = dataOff + size + (size % 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!report.tags.raw.aiff) report.tags.raw.aiff = {};
|
||||||
|
report.tags.raw.aiff.chunk_index = chunks.slice(0, 500);
|
||||||
|
|
||||||
|
const nameChunk = report.tags.raw.aiff?.chunks?.find((ch) => ch.id === "NAME")?.value;
|
||||||
|
if (nameChunk) report.tags.common.title = report.tags.common.title || nameChunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AAC_SAMPLE_RATES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
|
||||||
|
const AAC_PROFILES = ["Main", "LC", "SSR", "LTP"];
|
||||||
|
const AAC_CHANNELS = ["defined in AOT", "mono", "stereo", "3.0", "4.0", "5.0", "5.1", "7.1"];
|
||||||
|
|
||||||
|
/** Parses AAC ADTS frame header for audio parameters. */
|
||||||
|
export function parseAacAdts(b, report) {
|
||||||
|
report.detections.metadata_systems.push("adts_header");
|
||||||
|
if (b.length < 7) return;
|
||||||
|
|
||||||
|
const id = (b[1] >> 3) & 0x01;
|
||||||
|
const profile = (b[2] >> 6) & 0x03;
|
||||||
|
const freqIdx = (b[2] >> 2) & 0x0f;
|
||||||
|
const chanCfg = ((b[2] & 0x01) << 2) | ((b[3] >> 6) & 0x03);
|
||||||
|
|
||||||
|
report.tags.raw.aac = {
|
||||||
|
mpeg_version: id === 1 ? "MPEG-2" : "MPEG-4",
|
||||||
|
profile: AAC_PROFILES[profile] || `Profile ${profile}`,
|
||||||
|
sample_rate: AAC_SAMPLE_RATES[freqIdx] || null,
|
||||||
|
sample_rate_index: freqIdx,
|
||||||
|
channel_configuration: chanCfg,
|
||||||
|
channel_description: AAC_CHANNELS[chanCfg] || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const AC3_SAMPLE_RATES = [48000, 44100, 32000];
|
||||||
|
const AC3_BITRATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640];
|
||||||
|
const AC3_ACMODES = [
|
||||||
|
"2.0 (Ch1+Ch2)", "1.0 (C)", "2.0 (L R)", "3.0 (L C R)",
|
||||||
|
"2.1 (L R S)", "3.1 (L C R S)", "2.2 (L R SL SR)", "3.2 (L C R SL SR)",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Parses AC3 (Dolby Digital) bitstream info. */
|
||||||
|
export function parseAc3(b, report) {
|
||||||
|
report.detections.metadata_systems.push("ac3_bsi");
|
||||||
|
if (b.length < 8) return;
|
||||||
|
|
||||||
|
const fscod = (b[4] >> 6) & 0x03;
|
||||||
|
const frmsizecod = b[4] & 0x3f;
|
||||||
|
const bsid = (b[5] >> 3) & 0x1f;
|
||||||
|
const bsmod = b[5] & 0x07;
|
||||||
|
const acmod = (b[6] >> 5) & 0x07;
|
||||||
|
|
||||||
|
report.tags.raw.ac3 = {
|
||||||
|
sample_rate: AC3_SAMPLE_RATES[fscod] || null,
|
||||||
|
fscod,
|
||||||
|
bitrate_kbps: AC3_BITRATES[frmsizecod >> 1] || null,
|
||||||
|
frmsizecod, bsid, bsmod, acmod,
|
||||||
|
channel_layout: AC3_ACMODES[acmod] || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses WMA files (ASF container) for content description metadata. */
|
||||||
|
export function parseWmaAsf(b, report) {
|
||||||
|
report.detections.metadata_systems.push("asf_header");
|
||||||
|
if (b.length < 30) return;
|
||||||
|
|
||||||
|
const headerSize = Number(u64le(b, 16));
|
||||||
|
const numObjects = u32le(b, 24);
|
||||||
|
const headerEnd = Math.min(b.length, headerSize);
|
||||||
|
|
||||||
|
const objects = [];
|
||||||
|
let off = 30;
|
||||||
|
|
||||||
|
for (let i = 0; i < numObjects && off + 24 <= headerEnd; i++) {
|
||||||
|
const guid4 = [b[off], b[off + 1], b[off + 2], b[off + 3]];
|
||||||
|
const objSize = Number(u64le(b, off + 16));
|
||||||
|
if (objSize < 24 || off + objSize > headerEnd) break;
|
||||||
|
|
||||||
|
const dataOff = off + 24;
|
||||||
|
const dataLen = objSize - 24;
|
||||||
|
|
||||||
|
if (guid4[0] === 0x33 && guid4[1] === 0x26 && guid4[2] === 0xb2 && guid4[3] === 0x75 && dataLen >= 10) {
|
||||||
|
const cd = parseAsfContentDescription(b, dataOff);
|
||||||
|
if (!report.detections.metadata_systems.includes("asf_content_desc"))
|
||||||
|
report.detections.metadata_systems.push("asf_content_desc");
|
||||||
|
if (!report.tags.raw.asf) report.tags.raw.asf = {};
|
||||||
|
report.tags.raw.asf.content_description = cd;
|
||||||
|
mapCommon(report, cd, ASF_CD_TO_COMMON);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guid4[0] === 0x40 && guid4[1] === 0xa4 && guid4[2] === 0xd0 && guid4[3] === 0xd2 && dataLen >= 2) {
|
||||||
|
const ext = parseAsfExtContentDescription(b, dataOff, dataOff + dataLen);
|
||||||
|
if (!report.detections.metadata_systems.includes("asf_ext_content_desc"))
|
||||||
|
report.detections.metadata_systems.push("asf_ext_content_desc");
|
||||||
|
if (!report.tags.raw.asf) report.tags.raw.asf = {};
|
||||||
|
report.tags.raw.asf.extended_content = ext;
|
||||||
|
|
||||||
|
const c = report.tags.common;
|
||||||
|
for (const d of ext) {
|
||||||
|
const field = WMA_TO_COMMON[(d.name || "").toUpperCase()];
|
||||||
|
if (field && d.value) c[field] = c[field] || d.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objects.push({ guid_prefix: guid4.map(x => x.toString(16).padStart(2, "0")).join(""), size: objSize });
|
||||||
|
off += objSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!report.tags.raw.asf) report.tags.raw.asf = {};
|
||||||
|
report.tags.raw.asf.header_objects = objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ID3_FRAME_DESCRIPTIONS = {
|
||||||
|
TIT2: "Title/songname/content description", TPE1: "Lead performer(s)/Soloist(s)",
|
||||||
|
TRCK: "Track number/Position in set", TALB: "Album/Movie/Show title",
|
||||||
|
TDRC: "Recording time", TYER: "Year", TCON: "Content type",
|
||||||
|
TPE2: "Band/orchestra/accompaniment", TLEN: "Length (ms)", TCOM: "Composer",
|
||||||
|
COMM: "Comments", APIC: "Attached picture", GEOB: "General encapsulated object",
|
||||||
|
TXXX: "User defined text information frame", UFID: "Unique file identifier", PRIV: "Private frame",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ID3_TO_COMMON = {
|
||||||
|
TIT2: "title", TPE1: "artist", TALB: "album", TDRC: "date", TYER: "date",
|
||||||
|
TRCK: "track", TCON: "genre", COMM: "comment", TCOM: "composer", TCOP: "copyright", TLAN: "language",
|
||||||
|
};
|
||||||
|
const VORBIS_TO_COMMON = {
|
||||||
|
TITLE: "title", ARTIST: "artist", ALBUM: "album", DATE: "date",
|
||||||
|
TRACKNUMBER: "track", GENRE: "genre", COMMENT: "comment", COMPOSER: "composer", LANGUAGE: "language",
|
||||||
|
};
|
||||||
|
const WMA_TO_COMMON = {
|
||||||
|
"WM/ALBUMTITLE": "album", "WM/GENRE": "genre", "WM/YEAR": "date",
|
||||||
|
"WM/TRACKNUMBER": "track", "WM/COMPOSER": "composer", "WM/LANGUAGE": "language",
|
||||||
|
};
|
||||||
|
const ID3V1_TO_COMMON = { title: "title", artist: "artist", album: "album", year: "date", comment: "comment", genre: "genre", track: "track" };
|
||||||
|
const RIFF_TO_COMMON = { INAM: "title", IART: "artist", ICMT: "comment", IGNR: "genre", ICRD: "date", ICOP: "copyright" };
|
||||||
|
const ASF_CD_TO_COMMON = { title: "title", author: "artist", copyright: "copyright", description: "comment" };
|
||||||
|
|
||||||
|
/** Maps source object fields to the common tags layer via a mapping table. */
|
||||||
|
function mapCommon(report, source, mapping) {
|
||||||
|
const c = report.tags.common;
|
||||||
|
for (const [sk, ck] of Object.entries(mapping))
|
||||||
|
c[ck] = c[ck] || source[sk] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Maps an ID3v2 frame value to the common tags layer. */
|
||||||
|
function mapCommonId3(report, frameId, text) {
|
||||||
|
const field = ID3_TO_COMMON[frameId];
|
||||||
|
if (field) report.tags.common[field] = report.tags.common[field] || text || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decodes an ID3v2 COMM (Comments) frame. */
|
||||||
|
function decodeCommFrame(data) {
|
||||||
|
if (!data || data.length < 5) return null;
|
||||||
|
const enc = data[0];
|
||||||
|
const language = String.fromCharCode(data[1], data[2], data[3]);
|
||||||
|
const { valueBytes: descBytes, next } = readNullTerminated(data, 4, enc);
|
||||||
|
const short_description = decodeText(descBytes, enc).replace(/\u0000/g, "").trim() || null;
|
||||||
|
const text = decodeText(data.slice(next), enc).replace(/\u0000/g, "").trim() || null;
|
||||||
|
return { language, short_description, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Normalizes TLEN to integer milliseconds. */
|
||||||
|
function normalizeTlen(s) {
|
||||||
|
if (!s) return null;
|
||||||
|
if (/^\s*\d+\s*$/.test(s)) return parseInt(s.trim(), 10);
|
||||||
|
const f = Number(s);
|
||||||
|
if (Number.isFinite(f) && f > 0 && f < 100000) return Math.round(f * 1000);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses the ID3v2 tag header and frames. */
|
||||||
|
function parseId3v2(mp3) {
|
||||||
|
if (mp3.length < 10 || mp3[0] !== 0x49 || mp3[1] !== 0x44 || mp3[2] !== 0x33) return null;
|
||||||
|
|
||||||
|
const major = mp3[3], minor = mp3[4], flags = mp3[5];
|
||||||
|
const tagSize = synchsafeToInt(mp3[6], mp3[7], mp3[8], mp3[9]);
|
||||||
|
let offset = 10;
|
||||||
|
const end = 10 + tagSize;
|
||||||
|
|
||||||
|
const frames = [];
|
||||||
|
while (offset + 10 <= end) {
|
||||||
|
const id = String.fromCharCode(mp3[offset], mp3[offset + 1], mp3[offset + 2], mp3[offset + 3]);
|
||||||
|
if (!/^[A-Z0-9]{4}$/.test(id)) break;
|
||||||
|
const size = major === 4 ?
|
||||||
|
synchsafeToInt(mp3[offset + 4], mp3[offset + 5], mp3[offset + 6], mp3[offset + 7]) :
|
||||||
|
u32be(mp3, offset + 4);
|
||||||
|
offset += 10;
|
||||||
|
if (size <= 0 || offset + size > mp3.length) break;
|
||||||
|
frames.push({ id, size, data: mp3.slice(offset, offset + size) });
|
||||||
|
offset += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { header: { version: `${major}.${minor}`, flags, tag_size: tagSize }, frames };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses the 128-byte ID3v1 tag at the end of the file. */
|
||||||
|
function parseId3v1(b) {
|
||||||
|
if (b.length < 128) return null;
|
||||||
|
const off = b.length - 128;
|
||||||
|
if (b[off] !== 0x54 || b[off + 1] !== 0x41 || b[off + 2] !== 0x47) return null;
|
||||||
|
|
||||||
|
let track = null;
|
||||||
|
if (b[off + 125] === 0x00 && b[off + 126] !== 0x00) track = String(b[off + 126]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: decodeLatin1Trim(b.slice(off + 3, off + 33)),
|
||||||
|
artist: decodeLatin1Trim(b.slice(off + 33, off + 63)),
|
||||||
|
album: decodeLatin1Trim(b.slice(off + 63, off + 93)),
|
||||||
|
year: decodeLatin1Trim(b.slice(off + 93, off + 97)),
|
||||||
|
comment: decodeLatin1Trim(b.slice(off + 97, off + 127)),
|
||||||
|
track, genre: String(b[off + 127]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decodes an ID3v2 TXXX (user-defined text) frame. */
|
||||||
|
function decodeTxxx(data) {
|
||||||
|
if (!data || data.length < 2) return null;
|
||||||
|
const enc = data[0];
|
||||||
|
const { valueBytes: descBytes, next } = readNullTerminated(data, 1, enc);
|
||||||
|
const desc = decodeText(descBytes, enc).replace(/\u0000/g, "").trim();
|
||||||
|
const val = decodeText(data.slice(next), enc).replace(/\u0000/g, "").trim();
|
||||||
|
return { description: desc || null, value: val || null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Best-effort APEv2 tag parser scanning the last 32 KB. */
|
||||||
|
function parseApeV2BestEffort(b) {
|
||||||
|
const scanStart = Math.max(0, b.length - 32768);
|
||||||
|
const idx = indexOfAscii(b, "APETAGEX", scanStart, b.length);
|
||||||
|
if (idx < 0) return null;
|
||||||
|
if (idx + 32 > b.length) return { present: true, warning: "APETAGEX found but footer truncated." };
|
||||||
|
|
||||||
|
const ver = u32le(b, idx + 8), size = u32le(b, idx + 12);
|
||||||
|
const count = u32le(b, idx + 16), flags = u32le(b, idx + 20);
|
||||||
|
|
||||||
|
const tagStart = idx + 32 - size;
|
||||||
|
if (tagStart < 0 || tagStart >= b.length)
|
||||||
|
return { present: true, version: ver, size, count, flags, warning: "APEv2 bounds invalid (non-standard placement)." };
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
let off = tagStart + 32;
|
||||||
|
const end = Math.min(b.length, idx);
|
||||||
|
while (off + 8 < end && items.length < 5000) {
|
||||||
|
const valueSize = u32le(b, off), itemFlags = u32le(b, off + 4);
|
||||||
|
off += 8;
|
||||||
|
let keyEnd = off;
|
||||||
|
while (keyEnd < end && b[keyEnd] !== 0x00) keyEnd++;
|
||||||
|
const key = decodeLatin1Trim(b.slice(off, keyEnd));
|
||||||
|
off = keyEnd + 1;
|
||||||
|
if (!key || off + valueSize > end) break;
|
||||||
|
const value = safeUtf8(b.slice(off, off + valueSize)).replace(/\u0000/g, "").trim();
|
||||||
|
off += valueSize;
|
||||||
|
items.push({ key, value, flags: itemFlags });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { present: true, version: ver, size, count, flags, items };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enumerates RIFF-style chunks (id + LE32 size) within a byte range, padding to even. */
|
||||||
|
function enumerateChunks(b, start, end, maxCount) {
|
||||||
|
const chunks = [];
|
||||||
|
let off = start;
|
||||||
|
while (off + 8 <= end && chunks.length < maxCount) {
|
||||||
|
const id = ascii4(b, off);
|
||||||
|
const size = u32le(b, off + 4);
|
||||||
|
const dataOff = off + 8;
|
||||||
|
if (dataOff + size > end) break;
|
||||||
|
chunks.push({ id, size, dataOff });
|
||||||
|
off = dataOff + size + (size % 2);
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses a BWF bext chunk. */
|
||||||
|
function parseBext(b, off, size) {
|
||||||
|
const slice = b.slice(off, off + size);
|
||||||
|
const timeRefLow = u32le(slice, 338), timeRefHigh = u32le(slice, 342);
|
||||||
|
return {
|
||||||
|
description: decodeLatin1Trim(slice.slice(0, 256)) || null,
|
||||||
|
originator: decodeLatin1Trim(slice.slice(256, 288)) || null,
|
||||||
|
originator_reference: decodeLatin1Trim(slice.slice(288, 320)) || null,
|
||||||
|
origination_date: decodeLatin1Trim(slice.slice(320, 330)) || null,
|
||||||
|
origination_time: decodeLatin1Trim(slice.slice(330, 338)) || null,
|
||||||
|
time_reference_samples: ((BigInt(timeRefHigh) << 32n) | BigInt(timeRefLow)).toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const FLAC_TYPE_NAMES = { 0: "STREAMINFO", 1: "PADDING", 2: "APPLICATION", 3: "SEEKTABLE", 4: "VORBIS_COMMENT", 5: "CUESHEET", 6: "PICTURE" };
|
||||||
|
|
||||||
|
/** Parses FLAC metadata blocks following the "fLaC" marker. */
|
||||||
|
function parseFlacMetaBlocks(b) {
|
||||||
|
const blocks = [];
|
||||||
|
let off = 4;
|
||||||
|
while (off + 4 <= b.length && blocks.length < 10000) {
|
||||||
|
const header = b[off];
|
||||||
|
const isLast = (header & 0x80) !== 0;
|
||||||
|
const type = header & 0x7f;
|
||||||
|
const len = (b[off + 1] << 16) | (b[off + 2] << 8) | b[off + 3];
|
||||||
|
off += 4;
|
||||||
|
if (off + len > b.length) break;
|
||||||
|
blocks.push({ type, typeName: FLAC_TYPE_NAMES[type] || `TYPE_${type}`, length: len, data: b.slice(off, off + len) });
|
||||||
|
off += len;
|
||||||
|
if (isLast) break;
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses a Vorbis Comment block (used by FLAC and OGG). */
|
||||||
|
function parseVorbisComment(buf) {
|
||||||
|
let off = 0;
|
||||||
|
const vendorLen = u32le(buf, off); off += 4;
|
||||||
|
if (off + vendorLen > buf.length) return { vendor: null, comments: [], warning: "vendor_len out of bounds" };
|
||||||
|
const vendor = safeUtf8(buf.slice(off, off + vendorLen)); off += vendorLen;
|
||||||
|
const count = u32le(buf, off); off += 4;
|
||||||
|
|
||||||
|
const comments = [];
|
||||||
|
for (let i = 0; i < count && off + 4 <= buf.length && comments.length < 20000; i++) {
|
||||||
|
const l = u32le(buf, off); off += 4;
|
||||||
|
if (off + l > buf.length) break;
|
||||||
|
const s = safeUtf8(buf.slice(off, off + l)); off += l;
|
||||||
|
const eq = s.indexOf("=");
|
||||||
|
if (eq > 0) comments.push({ key: s.slice(0, eq).toUpperCase(), value: s.slice(eq + 1) });
|
||||||
|
}
|
||||||
|
return { vendor, comments };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Maps Vorbis Comment fields to the common tags layer. */
|
||||||
|
function mapVorbisCommon(report, vc) {
|
||||||
|
const c = report.tags.common;
|
||||||
|
for (const [vk, ck] of Object.entries(VORBIS_TO_COMMON))
|
||||||
|
c[ck] = c[ck] || vc.comments?.find((x) => x.key === vk)?.value || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses a FLAC PICTURE metadata block (extracts mime, description, data length). */
|
||||||
|
function parseFlacPicture(data, maxTextBytes) {
|
||||||
|
let off = 4;
|
||||||
|
const mimeLen = u32be(data, off); off += 4;
|
||||||
|
const mime = safeUtf8(data.slice(off, off + Math.min(mimeLen, maxTextBytes))); off += mimeLen;
|
||||||
|
const descLen = u32be(data, off); off += 4;
|
||||||
|
const description = safeUtf8(data.slice(off, off + Math.min(descLen, maxTextBytes))); off += descLen + 16;
|
||||||
|
return { mime, description, dataLength: u32be(data, off) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses the ASF Content Description Object fields. */
|
||||||
|
function parseAsfContentDescription(b, off) {
|
||||||
|
const titleLen = u16le(b, off), authorLen = u16le(b, off + 2);
|
||||||
|
const copyrightLen = u16le(b, off + 4), descLen = u16le(b, off + 6), ratingLen = u16le(b, off + 8);
|
||||||
|
let pos = off + 10;
|
||||||
|
const title = decodeUtf16LE(b, pos, titleLen); pos += titleLen;
|
||||||
|
const author = decodeUtf16LE(b, pos, authorLen); pos += authorLen;
|
||||||
|
const copyright = decodeUtf16LE(b, pos, copyrightLen); pos += copyrightLen;
|
||||||
|
const description = decodeUtf16LE(b, pos, descLen); pos += descLen;
|
||||||
|
const rating = decodeUtf16LE(b, pos, ratingLen);
|
||||||
|
return { title, author, copyright, description, rating };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses the ASF Extended Content Description Object descriptors. */
|
||||||
|
function parseAsfExtContentDescription(b, off, end) {
|
||||||
|
const count = u16le(b, off);
|
||||||
|
let pos = off + 2;
|
||||||
|
const descriptors = [];
|
||||||
|
for (let i = 0; i < count && pos + 6 <= end && descriptors.length < 5000; i++) {
|
||||||
|
const nameLen = u16le(b, pos); pos += 2;
|
||||||
|
if (pos + nameLen > end) break;
|
||||||
|
const name = decodeUtf16LE(b, pos, nameLen); pos += nameLen;
|
||||||
|
const valueType = u16le(b, pos); pos += 2;
|
||||||
|
const valueLen = u16le(b, pos); pos += 2;
|
||||||
|
if (pos + valueLen > end) break;
|
||||||
|
let value;
|
||||||
|
if (valueType === 0) value = decodeUtf16LE(b, pos, valueLen);
|
||||||
|
else if (valueType === 3) value = u32le(b, pos);
|
||||||
|
else if (valueType === 5) value = u16le(b, pos);
|
||||||
|
else if (valueType === 2) value = u32le(b, pos) !== 0;
|
||||||
|
else value = `(${valueLen} bytes, type ${valueType})`;
|
||||||
|
pos += valueLen;
|
||||||
|
descriptors.push({ name, value_type: valueType, value });
|
||||||
|
}
|
||||||
|
return descriptors;
|
||||||
|
}
|
||||||
2
src/core/lib/BCD.mjs
Executable file → Normal file
2
src/core/lib/BCD.mjs
Executable file → Normal file
@ -22,7 +22,7 @@ export const ENCODING_SCHEME = [
|
|||||||
/**
|
/**
|
||||||
* Lookup table for the binary value of each digit representation.
|
* Lookup table for the binary value of each digit representation.
|
||||||
*
|
*
|
||||||
* I wrote a very nice algorithm to generate 8 4 2 1 encoding programatically,
|
* I wrote a very nice algorithm to generate 8 4 2 1 encoding programmatically,
|
||||||
* but unfortunately it's much easier (if less elegant) to use lookup tables
|
* but unfortunately it's much easier (if less elegant) to use lookup tables
|
||||||
* when supporting multiple encoding schemes.
|
* when supporting multiple encoding schemes.
|
||||||
*
|
*
|
||||||
|
|||||||
66
src/core/lib/Bacon.mjs
Normal file
66
src/core/lib/Bacon.mjs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Bacon Cipher resources.
|
||||||
|
*
|
||||||
|
* @author Karsten Silkenbäumer [github.com/kassi]
|
||||||
|
* @copyright Karsten Silkenbäumer 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bacon definitions.
|
||||||
|
*/
|
||||||
|
export const BACON_ALPHABETS = {
|
||||||
|
"Standard (I=J and U=V)": {
|
||||||
|
alphabet: "ABCDEFGHIKLMNOPQRSTUWXYZ",
|
||||||
|
codes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 23]
|
||||||
|
},
|
||||||
|
"Complete": {
|
||||||
|
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const BACON_TRANSLATION_01 = "0/1";
|
||||||
|
export const BACON_TRANSLATION_AB = "A/B";
|
||||||
|
export const BACON_TRANSLATION_CASE = "Case";
|
||||||
|
export const BACON_TRANSLATION_AMNZ = "A-M/N-Z first letter";
|
||||||
|
export const BACON_TRANSLATIONS = [
|
||||||
|
BACON_TRANSLATION_01,
|
||||||
|
BACON_TRANSLATION_AB,
|
||||||
|
BACON_TRANSLATION_CASE,
|
||||||
|
BACON_TRANSLATION_AMNZ,
|
||||||
|
];
|
||||||
|
export const BACON_TRANSLATIONS_FOR_ENCODING = [
|
||||||
|
BACON_TRANSLATION_01,
|
||||||
|
BACON_TRANSLATION_AB
|
||||||
|
];
|
||||||
|
export const BACON_CLEARER_MAP = {
|
||||||
|
[BACON_TRANSLATION_01]: /[^01]/g,
|
||||||
|
[BACON_TRANSLATION_AB]: /[^ABab]/g,
|
||||||
|
[BACON_TRANSLATION_CASE]: /[^A-Za-z]/g,
|
||||||
|
};
|
||||||
|
export const BACON_NORMALIZE_MAP = {
|
||||||
|
[BACON_TRANSLATION_AB]: {
|
||||||
|
"A": "0",
|
||||||
|
"B": "1",
|
||||||
|
"a": "0",
|
||||||
|
"b": "1"
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps zeros to ones and ones to zeros.
|
||||||
|
*
|
||||||
|
* @param {string} data
|
||||||
|
* @returns {string}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // returns "11001 01010"
|
||||||
|
* swapZeroAndOne("00110 10101");
|
||||||
|
*/
|
||||||
|
export function swapZeroAndOne(string) {
|
||||||
|
return string.replace(/[01]/g, function (c) {
|
||||||
|
return {
|
||||||
|
"0": "1",
|
||||||
|
"1": "0"
|
||||||
|
}[c];
|
||||||
|
});
|
||||||
|
}
|
||||||
23
src/core/lib/Base32.mjs
Normal file
23
src/core/lib/Base32.mjs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base32 resources.
|
||||||
|
*
|
||||||
|
* @author Peter C-S [petercs@purelymail.com]
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base32 alphabets.
|
||||||
|
*/
|
||||||
|
export const ALPHABET_OPTIONS = [
|
||||||
|
{
|
||||||
|
name: "Standard", // https://www.rfc-editor.org/rfc/rfc4648#section-6
|
||||||
|
value: "A-Z2-7=",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hex Extended", // https://www.rfc-editor.org/rfc/rfc4648#section-7
|
||||||
|
value: "0-9A-V=",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
27
src/core/lib/Base45.mjs
Normal file
27
src/core/lib/Base45.mjs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Base45 resources.
|
||||||
|
*
|
||||||
|
* @author Thomas Weißschuh [thomas@t-8ch.de]
|
||||||
|
* @copyright Crown Copyright 2021
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight to Base45
|
||||||
|
*/
|
||||||
|
export function highlightToBase45(pos, args) {
|
||||||
|
pos[0].start = Math.floor(pos[0].start / 2) * 3;
|
||||||
|
pos[0].end = Math.ceil(pos[0].end / 2) * 3;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight from Base45
|
||||||
|
*/
|
||||||
|
export function highlightFromBase45(pos, args) {
|
||||||
|
pos[0].start = Math.floor(pos[0].start / 3) * 2;
|
||||||
|
pos[0].end = Math.ceil(pos[0].end / 3) * 2;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ALPHABET = "0-9A-Z $%*+\\-./:";
|
||||||
0
src/core/lib/Base58.mjs
Executable file → Normal file
0
src/core/lib/Base58.mjs
Executable file → Normal file
85
src/core/lib/Base64.mjs
Executable file → Normal file
85
src/core/lib/Base64.mjs
Executable file → Normal file
@ -6,13 +6,13 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Utils from "../Utils";
|
import Utils from "../Utils.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base64's the input byte array using the given alphabet, returning a string.
|
* Base64's the input byte array using the given alphabet, returning a string.
|
||||||
*
|
*
|
||||||
* @param {byteArray|Uint8Array|string} data
|
* @param {byteArray|Uint8Array|ArrayBuffer|string} data
|
||||||
* @param {string} [alphabet="A-Za-z0-9+/="]
|
* @param {string} [alphabet="A-Za-z0-9+/="]
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*
|
*
|
||||||
@ -26,10 +26,16 @@ import Utils from "../Utils";
|
|||||||
export function toBase64(data, alphabet="A-Za-z0-9+/=") {
|
export function toBase64(data, alphabet="A-Za-z0-9+/=") {
|
||||||
if (!data) return "";
|
if (!data) return "";
|
||||||
if (typeof data == "string") {
|
if (typeof data == "string") {
|
||||||
data = Utils.strToByteArray(data);
|
data = Utils.strToArrayBuffer(data);
|
||||||
|
}
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
data = new Uint8Array(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
alphabet = Utils.expandAlphRange(alphabet).join("");
|
alphabet = Utils.expandAlphRange(alphabet).join("");
|
||||||
|
if (alphabet.length !== 64 && alphabet.length !== 65) { // Allow for padding
|
||||||
|
throw new OperationError(`Invalid Base64 alphabet length (${alphabet.length}): ${alphabet}`);
|
||||||
|
}
|
||||||
|
|
||||||
let output = "",
|
let output = "",
|
||||||
chr1, chr2, chr3,
|
chr1, chr2, chr3,
|
||||||
@ -63,7 +69,7 @@ export function toBase64(data, alphabet="A-Za-z0-9+/=") {
|
|||||||
/**
|
/**
|
||||||
* UnBase64's the input string using the given alphabet, returning a byte array.
|
* UnBase64's the input string using the given alphabet, returning a byte array.
|
||||||
*
|
*
|
||||||
* @param {byteArray} data
|
* @param {string} data
|
||||||
* @param {string} [alphabet="A-Za-z0-9+/="]
|
* @param {string} [alphabet="A-Za-z0-9+/="]
|
||||||
* @param {string} [returnType="string"] - Either "string" or "byteArray"
|
* @param {string} [returnType="string"] - Either "string" or "byteArray"
|
||||||
* @param {boolean} [removeNonAlphChars=true]
|
* @param {boolean} [removeNonAlphChars=true]
|
||||||
@ -76,7 +82,7 @@ export function toBase64(data, alphabet="A-Za-z0-9+/=") {
|
|||||||
* // returns [72, 101, 108, 108, 111]
|
* // returns [72, 101, 108, 108, 111]
|
||||||
* fromBase64("SGVsbG8=", null, "byteArray");
|
* fromBase64("SGVsbG8=", null, "byteArray");
|
||||||
*/
|
*/
|
||||||
export function fromBase64(data, alphabet="A-Za-z0-9+/=", returnType="string", removeNonAlphChars=true) {
|
export function fromBase64(data, alphabet="A-Za-z0-9+/=", returnType="string", removeNonAlphChars=true, strictMode=false) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return returnType === "string" ? "" : [];
|
return returnType === "string" ? "" : [];
|
||||||
}
|
}
|
||||||
@ -84,36 +90,67 @@ export function fromBase64(data, alphabet="A-Za-z0-9+/=", returnType="string", r
|
|||||||
alphabet = alphabet || "A-Za-z0-9+/=";
|
alphabet = alphabet || "A-Za-z0-9+/=";
|
||||||
alphabet = Utils.expandAlphRange(alphabet).join("");
|
alphabet = Utils.expandAlphRange(alphabet).join("");
|
||||||
|
|
||||||
const output = [];
|
// Confirm alphabet is a valid length
|
||||||
let chr1, chr2, chr3,
|
if (alphabet.length !== 64 && alphabet.length !== 65) { // Allow for padding
|
||||||
enc1, enc2, enc3, enc4,
|
throw new OperationError(`Error: Base64 alphabet should be 64 characters long, or 65 with a padding character. Found ${alphabet.length}: ${alphabet}`);
|
||||||
i = 0;
|
}
|
||||||
|
|
||||||
|
// Remove non-alphabet characters
|
||||||
if (removeNonAlphChars) {
|
if (removeNonAlphChars) {
|
||||||
const re = new RegExp("[^" + alphabet.replace(/[[\]\\\-^$]/g, "\\$&") + "]", "g");
|
const re = new RegExp("[^" + alphabet.replace(/[[\]\\\-^$]/g, "\\$&") + "]", "g");
|
||||||
data = data.replace(re, "");
|
data = data.replace(re, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
while (i < data.length) {
|
if (strictMode) {
|
||||||
enc1 = alphabet.indexOf(data.charAt(i++));
|
// Check for incorrect lengths (even without padding)
|
||||||
enc2 = alphabet.indexOf(data.charAt(i++) || "=");
|
if (data.length % 4 === 1) {
|
||||||
enc3 = alphabet.indexOf(data.charAt(i++) || "=");
|
throw new OperationError(`Error: Invalid Base64 input length (${data.length}). Cannot be 4n+1, even without padding chars.`);
|
||||||
enc4 = alphabet.indexOf(data.charAt(i++) || "=");
|
}
|
||||||
|
|
||||||
enc2 = enc2 === -1 ? 64 : enc2;
|
if (alphabet.length === 65) { // Padding character included
|
||||||
enc3 = enc3 === -1 ? 64 : enc3;
|
const pad = alphabet.charAt(64);
|
||||||
enc4 = enc4 === -1 ? 64 : enc4;
|
const padPos = data.indexOf(pad);
|
||||||
|
if (padPos >= 0) {
|
||||||
|
// Check that the padding character is only used at the end and maximum of twice
|
||||||
|
if (padPos < data.length - 2 || data.charAt(data.length - 1) !== pad) {
|
||||||
|
throw new OperationError(`Error: Base64 padding character (${pad}) not used in the correct place.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that input is padded to the correct length
|
||||||
|
if (data.length % 4 !== 0) {
|
||||||
|
throw new OperationError("Error: Base64 not padded to a multiple of 4.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = [];
|
||||||
|
let chr1, chr2, chr3,
|
||||||
|
enc1, enc2, enc3, enc4,
|
||||||
|
i = 0;
|
||||||
|
|
||||||
|
while (i < data.length) {
|
||||||
|
// Including `|| null` forces empty strings to null so that indexOf returns -1 instead of 0
|
||||||
|
enc1 = alphabet.indexOf(data.charAt(i++) || null);
|
||||||
|
enc2 = alphabet.indexOf(data.charAt(i++) || null);
|
||||||
|
enc3 = alphabet.indexOf(data.charAt(i++) || null);
|
||||||
|
enc4 = alphabet.indexOf(data.charAt(i++) || null);
|
||||||
|
|
||||||
|
if (strictMode && (enc1 < 0 || enc2 < 0 || enc3 < 0 || enc4 < 0)) {
|
||||||
|
throw new OperationError("Error: Base64 input contains non-alphabet char(s)");
|
||||||
|
}
|
||||||
|
|
||||||
chr1 = (enc1 << 2) | (enc2 >> 4);
|
chr1 = (enc1 << 2) | (enc2 >> 4);
|
||||||
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||||
chr3 = ((enc3 & 3) << 6) | enc4;
|
chr3 = ((enc3 & 3) << 6) | enc4;
|
||||||
|
|
||||||
output.push(chr1);
|
if (chr1 >= 0 && chr1 < 256) {
|
||||||
|
output.push(chr1);
|
||||||
if (enc3 !== 64) {
|
}
|
||||||
|
if (chr2 >= 0 && chr2 < 256 && enc3 !== 64) {
|
||||||
output.push(chr2);
|
output.push(chr2);
|
||||||
}
|
}
|
||||||
if (enc4 !== 64) {
|
if (chr3 >= 0 && chr3 < 256 && enc4 !== 64) {
|
||||||
output.push(chr3);
|
output.push(chr3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,4 +176,8 @@ export const ALPHABET_OPTIONS = [
|
|||||||
{name: "BinHex: !-,-0-689@A-NP-VX-Z[`a-fh-mp-r", value: "!-,-0-689@A-NP-VX-Z[`a-fh-mp-r"},
|
{name: "BinHex: !-,-0-689@A-NP-VX-Z[`a-fh-mp-r", value: "!-,-0-689@A-NP-VX-Z[`a-fh-mp-r"},
|
||||||
{name: "ROT13: N-ZA-Mn-za-m0-9+/=", value: "N-ZA-Mn-za-m0-9+/="},
|
{name: "ROT13: N-ZA-Mn-za-m0-9+/=", value: "N-ZA-Mn-za-m0-9+/="},
|
||||||
{name: "UNIX crypt: ./0-9A-Za-z", value: "./0-9A-Za-z"},
|
{name: "UNIX crypt: ./0-9A-Za-z", value: "./0-9A-Za-z"},
|
||||||
|
{name: "Atom128: /128GhIoPQROSTeUbADfgHijKLM+n0pFWXY456xyzB7=39VaqrstJklmNuZvwcdEC", value: "/128GhIoPQROSTeUbADfgHijKLM+n0pFWXY456xyzB7=39VaqrstJklmNuZvwcdEC"},
|
||||||
|
{name: "Megan35: 3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5", value: "3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5"},
|
||||||
|
{name: "Zong22: ZKj9n+yf0wDVX1s/5YbdxSo=ILaUpPBCHg8uvNO4klm6iJGhQ7eFrWczAMEq3RTt2", value: "ZKj9n+yf0wDVX1s/5YbdxSo=ILaUpPBCHg8uvNO4klm6iJGhQ7eFrWczAMEq3RTt2"},
|
||||||
|
{name: "Hazz15: HNO4klm6ij9n+J2hyf0gzA8uvwDEq3X1Q7ZKeFrWcVTts/MRGYbdxSo=ILaUpPBC5", value: "HNO4klm6ij9n+J2hyf0gzA8uvwDEq3X1Q7ZKeFrWcVTts/MRGYbdxSo=ILaUpPBC5"}
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base85 resources.
|
* Base85 resources.
|
||||||
*
|
*
|
||||||
@ -20,7 +22,7 @@ export const ALPHABET_OPTIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv6",
|
name: "IPv6",
|
||||||
value: "0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|~}",
|
value: "0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~",
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -32,13 +34,12 @@ export const ALPHABET_OPTIONS = [
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function alphabetName(alphabet) {
|
export function alphabetName(alphabet) {
|
||||||
alphabet = alphabet.replace("'", "'");
|
alphabet = escape(alphabet);
|
||||||
alphabet = alphabet.replace("\"", """);
|
|
||||||
alphabet = alphabet.replace("\\", "\");
|
|
||||||
let name;
|
let name;
|
||||||
|
|
||||||
ALPHABET_OPTIONS.forEach(function(a) {
|
ALPHABET_OPTIONS.forEach(function(a) {
|
||||||
if (escape(alphabet) === escape(a.value)) name = a.name;
|
const expanded = Utils.expandAlphRange(a.value).join("");
|
||||||
|
if (alphabet === escape(expanded)) name = a.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
44
src/core/lib/Base92.mjs
Normal file
44
src/core/lib/Base92.mjs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Base92 resources.
|
||||||
|
*
|
||||||
|
* @author sg5506844 [sg5506844@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2021
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base92 alphabet char
|
||||||
|
*
|
||||||
|
* @param {number} val
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function base92Chr(val) {
|
||||||
|
if (val < 0 || val >= 91) {
|
||||||
|
throw new OperationError("Invalid value");
|
||||||
|
}
|
||||||
|
if (val === 0)
|
||||||
|
return "!".charCodeAt(0);
|
||||||
|
else if (val <= 61)
|
||||||
|
return "#".charCodeAt(0) + val - 1;
|
||||||
|
else
|
||||||
|
return "a".charCodeAt(0) + val - 62;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base92 alphabet ord
|
||||||
|
*
|
||||||
|
* @param {string} val
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function base92Ord(val) {
|
||||||
|
if (val === "!")
|
||||||
|
return 0;
|
||||||
|
else if ("#" <= val && val <= "_")
|
||||||
|
return val.charCodeAt(0) - "#".charCodeAt(0) + 1;
|
||||||
|
else if ("a" <= val && val <= "}")
|
||||||
|
return val.charCodeAt(0) - "a".charCodeAt(0) + 62;
|
||||||
|
throw new OperationError(`${val} is not a base92 character`);
|
||||||
|
}
|
||||||
|
|
||||||
371
src/core/lib/Bech32.mjs
Normal file
371
src/core/lib/Bech32.mjs
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
/**
|
||||||
|
* Pure JavaScript implementation of Bech32 and Bech32m encoding.
|
||||||
|
*
|
||||||
|
* Bech32 is defined in BIP-0173: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
|
||||||
|
* Bech32m is defined in BIP-0350: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki
|
||||||
|
*
|
||||||
|
* @author Medjedtxm
|
||||||
|
* @copyright Crown Copyright 2025
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
/** Bech32 character set (32 characters, excludes 1, b, i, o) */
|
||||||
|
const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||||
|
|
||||||
|
/** Reverse lookup table for decoding */
|
||||||
|
const CHARSET_REV = {};
|
||||||
|
for (let i = 0; i < CHARSET.length; i++) {
|
||||||
|
CHARSET_REV[CHARSET[i]] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checksum constant for Bech32 (BIP-0173) */
|
||||||
|
const BECH32_CONST = 1;
|
||||||
|
|
||||||
|
/** Checksum constant for Bech32m (BIP-0350) */
|
||||||
|
const BECH32M_CONST = 0x2bc830a3;
|
||||||
|
|
||||||
|
/** Generator polynomial coefficients for checksum */
|
||||||
|
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the polymod checksum
|
||||||
|
* @param {number[]} values - Array of 5-bit values
|
||||||
|
* @returns {number} - Checksum value
|
||||||
|
*/
|
||||||
|
function polymod(values) {
|
||||||
|
let chk = 1;
|
||||||
|
for (const v of values) {
|
||||||
|
const top = chk >> 25;
|
||||||
|
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if ((top >> i) & 1) {
|
||||||
|
chk ^= GENERATOR[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand HRP for checksum computation
|
||||||
|
* @param {string} hrp - Human-readable part (lowercase)
|
||||||
|
* @returns {number[]} - Expanded values
|
||||||
|
*/
|
||||||
|
function hrpExpand(hrp) {
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < hrp.length; i++) {
|
||||||
|
result.push(hrp.charCodeAt(i) >> 5);
|
||||||
|
}
|
||||||
|
result.push(0);
|
||||||
|
for (let i = 0; i < hrp.length; i++) {
|
||||||
|
result.push(hrp.charCodeAt(i) & 31);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify checksum of a Bech32/Bech32m string
|
||||||
|
* @param {string} hrp - Human-readable part (lowercase)
|
||||||
|
* @param {number[]} data - Data including checksum (5-bit values)
|
||||||
|
* @param {string} encoding - "Bech32" or "Bech32m"
|
||||||
|
* @returns {boolean} - True if checksum is valid
|
||||||
|
*/
|
||||||
|
function verifyChecksum(hrp, data, encoding) {
|
||||||
|
const constant = encoding === "Bech32m" ? BECH32M_CONST : BECH32_CONST;
|
||||||
|
return polymod(hrpExpand(hrp).concat(data)) === constant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create checksum for Bech32/Bech32m encoding
|
||||||
|
* @param {string} hrp - Human-readable part (lowercase)
|
||||||
|
* @param {number[]} data - Data values (5-bit)
|
||||||
|
* @param {string} encoding - "Bech32" or "Bech32m"
|
||||||
|
* @returns {number[]} - 6 checksum values
|
||||||
|
*/
|
||||||
|
function createChecksum(hrp, data, encoding) {
|
||||||
|
const constant = encoding === "Bech32m" ? BECH32M_CONST : BECH32_CONST;
|
||||||
|
const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
|
||||||
|
const mod = polymod(values) ^ constant;
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
result.push((mod >> (5 * (5 - i))) & 31);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert 8-bit bytes to 5-bit words
|
||||||
|
* @param {number[]|Uint8Array} data - Input bytes
|
||||||
|
* @returns {number[]} - 5-bit words
|
||||||
|
*/
|
||||||
|
export function toWords(data) {
|
||||||
|
let value = 0;
|
||||||
|
let bits = 0;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
value = (value << 8) | data[i];
|
||||||
|
bits += 8;
|
||||||
|
|
||||||
|
while (bits >= 5) {
|
||||||
|
bits -= 5;
|
||||||
|
result.push((value >> bits) & 31);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad remaining bits
|
||||||
|
if (bits > 0) {
|
||||||
|
result.push((value << (5 - bits)) & 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert 5-bit words to 8-bit bytes
|
||||||
|
* @param {number[]} words - 5-bit words
|
||||||
|
* @returns {number[]} - Output bytes
|
||||||
|
*/
|
||||||
|
export function fromWords(words) {
|
||||||
|
let value = 0;
|
||||||
|
let bits = 0;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
value = (value << 5) | words[i];
|
||||||
|
bits += 5;
|
||||||
|
|
||||||
|
while (bits >= 8) {
|
||||||
|
bits -= 8;
|
||||||
|
result.push((value >> bits) & 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid padding per BIP-0173
|
||||||
|
// Condition 1: Cannot have 5+ bits remaining (would indicate incomplete byte)
|
||||||
|
if (bits >= 5) {
|
||||||
|
throw new OperationError("Invalid padding: too many bits remaining");
|
||||||
|
}
|
||||||
|
// Condition 2: Remaining padding bits must all be zero
|
||||||
|
if (bits > 0) {
|
||||||
|
const paddingValue = (value << (8 - bits)) & 255;
|
||||||
|
if (paddingValue !== 0) {
|
||||||
|
throw new OperationError("Invalid padding: non-zero bits in padding");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode data to Bech32/Bech32m string
|
||||||
|
*
|
||||||
|
* @param {string} hrp - Human-readable part
|
||||||
|
* @param {number[]|Uint8Array} data - Data bytes to encode
|
||||||
|
* @param {string} encoding - "Bech32" or "Bech32m"
|
||||||
|
* @param {boolean} segwit - If true, treat first byte as witness version (for Bitcoin SegWit)
|
||||||
|
* @returns {string} - Encoded Bech32/Bech32m string
|
||||||
|
*/
|
||||||
|
export function encode(hrp, data, encoding = "Bech32", segwit = false) {
|
||||||
|
// Validate HRP
|
||||||
|
if (!hrp || hrp.length === 0) {
|
||||||
|
throw new OperationError("Human-Readable Part (HRP) cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check HRP characters (ASCII 33-126)
|
||||||
|
for (let i = 0; i < hrp.length; i++) {
|
||||||
|
const c = hrp.charCodeAt(i);
|
||||||
|
if (c < 33 || c > 126) {
|
||||||
|
throw new OperationError(`HRP contains invalid character at position ${i}. Only printable ASCII characters (33-126) are allowed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert HRP to lowercase
|
||||||
|
const hrpLower = hrp.toLowerCase();
|
||||||
|
|
||||||
|
let words;
|
||||||
|
if (segwit && data.length >= 2) {
|
||||||
|
// SegWit encoding: first byte is witness version (0-16), rest is witness program
|
||||||
|
const witnessVersion = data[0];
|
||||||
|
if (witnessVersion > 16) {
|
||||||
|
throw new OperationError(`Invalid witness version: ${witnessVersion}. Must be 0-16.`);
|
||||||
|
}
|
||||||
|
const witnessProgram = Array.prototype.slice.call(data, 1);
|
||||||
|
|
||||||
|
// Validate witness program length per BIP-0141
|
||||||
|
if (witnessProgram.length < 2 || witnessProgram.length > 40) {
|
||||||
|
throw new OperationError(`Invalid witness program length: ${witnessProgram.length}. Must be 2-40 bytes.`);
|
||||||
|
}
|
||||||
|
if (witnessVersion === 0 && witnessProgram.length !== 20 && witnessProgram.length !== 32) {
|
||||||
|
throw new OperationError(`Invalid witness program length for v0: ${witnessProgram.length}. Must be 20 or 32 bytes.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Witness version is kept as single 5-bit value, program is converted
|
||||||
|
words = [witnessVersion].concat(toWords(witnessProgram));
|
||||||
|
} else {
|
||||||
|
// Generic encoding: convert all bytes to 5-bit words
|
||||||
|
words = toWords(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create checksum
|
||||||
|
const checksum = createChecksum(hrpLower, words, encoding);
|
||||||
|
|
||||||
|
// Build result string
|
||||||
|
let result = hrpLower + "1";
|
||||||
|
for (const w of words.concat(checksum)) {
|
||||||
|
result += CHARSET[w];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check maximum length (90 characters)
|
||||||
|
if (result.length > 90) {
|
||||||
|
throw new OperationError(`Encoded string exceeds maximum length of 90 characters (got ${result.length}). Consider using smaller input data.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a Bech32/Bech32m string
|
||||||
|
*
|
||||||
|
* @param {string} str - Bech32/Bech32m encoded string
|
||||||
|
* @param {string} encoding - "Bech32", "Bech32m", or "Auto-detect"
|
||||||
|
* @returns {{hrp: string, data: number[]}} - Decoded HRP and data bytes
|
||||||
|
*/
|
||||||
|
export function decode(str, encoding = "Auto-detect") {
|
||||||
|
// Check for empty input
|
||||||
|
if (!str || str.length === 0) {
|
||||||
|
throw new OperationError("Input cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check maximum length
|
||||||
|
if (str.length > 90) {
|
||||||
|
throw new OperationError(`Invalid Bech32 string: exceeds maximum length of 90 characters (got ${str.length}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for mixed case
|
||||||
|
const hasUpper = /[A-Z]/.test(str);
|
||||||
|
const hasLower = /[a-z]/.test(str);
|
||||||
|
if (hasUpper && hasLower) {
|
||||||
|
throw new OperationError("Invalid Bech32 string: mixed case is not allowed. Use all uppercase or all lowercase.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to lowercase for processing
|
||||||
|
str = str.toLowerCase();
|
||||||
|
|
||||||
|
// Find separator (last occurrence of '1')
|
||||||
|
const sepIndex = str.lastIndexOf("1");
|
||||||
|
if (sepIndex === -1) {
|
||||||
|
throw new OperationError("Invalid Bech32 string: no separator '1' found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sepIndex === 0) {
|
||||||
|
throw new OperationError("Invalid Bech32 string: Human-Readable Part (HRP) cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sepIndex + 7 > str.length) {
|
||||||
|
throw new OperationError("Invalid Bech32 string: data part is too short (minimum 6 characters for checksum).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract HRP and data part
|
||||||
|
const hrp = str.substring(0, sepIndex);
|
||||||
|
const dataPart = str.substring(sepIndex + 1);
|
||||||
|
|
||||||
|
// Validate HRP characters
|
||||||
|
for (let i = 0; i < hrp.length; i++) {
|
||||||
|
const c = hrp.charCodeAt(i);
|
||||||
|
if (c < 33 || c > 126) {
|
||||||
|
throw new OperationError(`HRP contains invalid character at position ${i}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode data characters to 5-bit values
|
||||||
|
const data = [];
|
||||||
|
for (let i = 0; i < dataPart.length; i++) {
|
||||||
|
const c = dataPart[i];
|
||||||
|
if (CHARSET_REV[c] === undefined) {
|
||||||
|
throw new OperationError(`Invalid character '${c}' at position ${sepIndex + 1 + i}.`);
|
||||||
|
}
|
||||||
|
data.push(CHARSET_REV[c]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checksum
|
||||||
|
let usedEncoding;
|
||||||
|
if (encoding === "Bech32") {
|
||||||
|
if (!verifyChecksum(hrp, data, "Bech32")) {
|
||||||
|
throw new OperationError("Invalid Bech32 checksum.");
|
||||||
|
}
|
||||||
|
usedEncoding = "Bech32";
|
||||||
|
} else if (encoding === "Bech32m") {
|
||||||
|
if (!verifyChecksum(hrp, data, "Bech32m")) {
|
||||||
|
throw new OperationError("Invalid Bech32m checksum.");
|
||||||
|
}
|
||||||
|
usedEncoding = "Bech32m";
|
||||||
|
} else {
|
||||||
|
// Auto-detect: try Bech32 first, then Bech32m
|
||||||
|
if (verifyChecksum(hrp, data, "Bech32")) {
|
||||||
|
usedEncoding = "Bech32";
|
||||||
|
} else if (verifyChecksum(hrp, data, "Bech32m")) {
|
||||||
|
usedEncoding = "Bech32m";
|
||||||
|
} else {
|
||||||
|
throw new OperationError("Invalid Bech32/Bech32m string: checksum verification failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove checksum (last 6 values)
|
||||||
|
const words = data.slice(0, data.length - 6);
|
||||||
|
|
||||||
|
// Check if this is likely a SegWit address (Bitcoin, Litecoin, etc.)
|
||||||
|
// For SegWit, the first 5-bit word is the witness version (0-16)
|
||||||
|
// and should be extracted separately, not bit-converted with the rest
|
||||||
|
const segwitHrps = ["bc", "tb", "ltc", "tltc", "bcrt"];
|
||||||
|
const couldBeSegWit = segwitHrps.includes(hrp) && words.length > 0 && words[0] <= 16;
|
||||||
|
|
||||||
|
let bytes;
|
||||||
|
let witnessVersion = null;
|
||||||
|
|
||||||
|
if (couldBeSegWit) {
|
||||||
|
// Try SegWit decode first
|
||||||
|
try {
|
||||||
|
witnessVersion = words[0];
|
||||||
|
const programWords = words.slice(1);
|
||||||
|
const programBytes = fromWords(programWords);
|
||||||
|
|
||||||
|
// Validate SegWit witness program length (20 or 32 bytes for v0, 2-40 for others)
|
||||||
|
const validV0 = witnessVersion === 0 && (programBytes.length === 20 || programBytes.length === 32);
|
||||||
|
const validOther = witnessVersion !== 0 && programBytes.length >= 2 && programBytes.length <= 40;
|
||||||
|
|
||||||
|
if (validV0 || validOther) {
|
||||||
|
// Valid SegWit address
|
||||||
|
bytes = [witnessVersion, ...programBytes];
|
||||||
|
} else {
|
||||||
|
// Not valid SegWit, fall back to generic decode
|
||||||
|
witnessVersion = null;
|
||||||
|
bytes = fromWords(words);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// SegWit decode failed, try generic decode
|
||||||
|
witnessVersion = null;
|
||||||
|
try {
|
||||||
|
bytes = fromWords(words);
|
||||||
|
} catch (e2) {
|
||||||
|
throw new OperationError(`Failed to decode data: ${e2.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Generic Bech32: convert all words
|
||||||
|
try {
|
||||||
|
bytes = fromWords(words);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Failed to decode data: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hrp: hrp,
|
||||||
|
data: bytes,
|
||||||
|
encoding: usedEncoding,
|
||||||
|
witnessVersion: witnessVersion
|
||||||
|
};
|
||||||
|
}
|
||||||
72
src/core/lib/BigIntUtils.mjs
Normal file
72
src/core/lib/BigIntUtils.mjs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* @author p-leriche [philip.leriche@cantab.net]
|
||||||
|
* @copyright Crown Copyright 2025
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number theory utilities used by cryptographic operations.
|
||||||
|
*
|
||||||
|
* Currently provides:
|
||||||
|
* - parseBigInt
|
||||||
|
* - Extended Euclidean Algorithm
|
||||||
|
* - Modular Exponentiation
|
||||||
|
*
|
||||||
|
* Additional algorithms may be added as required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parseBigInt helper operation
|
||||||
|
*/
|
||||||
|
export function parseBigInt(value, param) {
|
||||||
|
const v = (value ?? "").trim();
|
||||||
|
if (/^0x[0-9a-f]+$/i.test(v)) return BigInt(v);
|
||||||
|
if (/^[+-]?[0-9]+$/.test(v)) return BigInt(v);
|
||||||
|
throw new OperationError(param + " must be decimal or hex (0x...)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended Euclidean Algorithm
|
||||||
|
*
|
||||||
|
* Returns [g, x, y] such that:
|
||||||
|
* a*x + b*y = g = gcd(a, b)
|
||||||
|
*
|
||||||
|
* (Uses an iterative algorithm to avoid possible stack overflow)
|
||||||
|
*/
|
||||||
|
export function egcd(a, b) {
|
||||||
|
let oldR = a, r = b;
|
||||||
|
let oldS = 1n, s = 0n;
|
||||||
|
let oldT = 0n, t = 1n;
|
||||||
|
|
||||||
|
while (r !== 0n) {
|
||||||
|
const quotient = oldR / r;
|
||||||
|
|
||||||
|
[oldR, r] = [r, oldR - quotient * r];
|
||||||
|
[oldS, s] = [s, oldS - quotient * s];
|
||||||
|
[oldT, t] = [t, oldT - quotient * t];
|
||||||
|
}
|
||||||
|
|
||||||
|
// oldR is the gcd
|
||||||
|
// oldS and oldT are the Bézout coefficients
|
||||||
|
return [oldR, oldS, oldT];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modular exponentiation
|
||||||
|
*/
|
||||||
|
export function modPow(base, exponent, modulus) {
|
||||||
|
let result = 1n;
|
||||||
|
base %= modulus;
|
||||||
|
|
||||||
|
while (exponent > 0n) {
|
||||||
|
if (exponent & 1n) {
|
||||||
|
result = (result * base) % modulus;
|
||||||
|
}
|
||||||
|
base = (base * base) % modulus;
|
||||||
|
exponent >>= 1n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@ -6,39 +6,46 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Utils from "../Utils";
|
import Utils from "../Utils.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a byte array into a binary string.
|
* Convert a byte array into a binary string.
|
||||||
*
|
*
|
||||||
* @param {Uint8Array|byteArray} data
|
* @param {Uint8Array|byteArray|number} data
|
||||||
* @param {string} [delim="Space"]
|
* @param {string} [delim="Space"]
|
||||||
* @param {number} [padding=8]
|
* @param {number} [padding=8]
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // returns "00010000 00100000 00110000"
|
* // returns "00001010 00010100 00011110"
|
||||||
* toBinary([10,20,30]);
|
* toBinary([10,20,30]);
|
||||||
*
|
*
|
||||||
* // returns "00010000 00100000 00110000"
|
* // returns "00001010:00010100:00011110"
|
||||||
* toBinary([10,20,30], ":");
|
* toBinary([10,20,30], "Colon");
|
||||||
|
*
|
||||||
|
* // returns "1010:10100:11110"
|
||||||
|
* toBinary([10,20,30], "Colon", 0);
|
||||||
*/
|
*/
|
||||||
export function toBinary(data, delim="Space", padding=8) {
|
export function toBinary(data, delim="Space", padding=8) {
|
||||||
if (!data) return "";
|
if (data === undefined || data === null)
|
||||||
|
throw new OperationError("Unable to convert to binary: Empty input data enocuntered");
|
||||||
|
|
||||||
delim = Utils.charRep(delim);
|
delim = Utils.charRep(delim);
|
||||||
let output = "";
|
let output = "";
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
if (data.length) { // array
|
||||||
output += data[i].toString(2).padStart(padding, "0") + delim;
|
for (let i = 0; i < data.length; i++) {
|
||||||
}
|
output += data[i].toString(2).padStart(padding, "0");
|
||||||
|
if (i !== data.length - 1) output += delim;
|
||||||
if (delim.length) {
|
}
|
||||||
return output.slice(0, -delim.length);
|
} else if (typeof data === "number") { // Single value
|
||||||
|
return data.toString(2).padStart(padding, "0");
|
||||||
} else {
|
} else {
|
||||||
return output;
|
return "";
|
||||||
}
|
}
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -52,12 +59,15 @@ export function toBinary(data, delim="Space", padding=8) {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // returns [10,20,30]
|
* // returns [10,20,30]
|
||||||
* fromBinary("00010000 00100000 00110000");
|
* fromBinary("00001010 00010100 00011110");
|
||||||
*
|
*
|
||||||
* // returns [10,20,30]
|
* // returns [10,20,30]
|
||||||
* fromBinary("00010000:00100000:00110000", "Colon");
|
* fromBinary("00001010:00010100:00011110", "Colon");
|
||||||
*/
|
*/
|
||||||
export function fromBinary(data, delim="Space", byteLen=8) {
|
export function fromBinary(data, delim="Space", byteLen=8) {
|
||||||
|
if (byteLen < 1 || Math.round(byteLen) !== byteLen)
|
||||||
|
throw new OperationError("Byte length must be a positive integer");
|
||||||
|
|
||||||
const delimRegex = Utils.regexRep(delim);
|
const delimRegex = Utils.regexRep(delim);
|
||||||
data = data.replace(delimRegex, "");
|
data = data.replace(delimRegex, "");
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
/**
|
/**
|
||||||
* Runs bitwise operations across the input data.
|
* Runs bitwise operations across the input data.
|
||||||
*
|
*
|
||||||
* @param {byteArray} input
|
* @param {byteArray|Uint8Array} input
|
||||||
* @param {byteArray} key
|
* @param {byteArray} key
|
||||||
* @param {function} func - The bitwise calculation to carry out
|
* @param {function} func - The bitwise calculation to carry out
|
||||||
* @param {boolean} nullPreserving
|
* @param {boolean} nullPreserving
|
||||||
@ -34,10 +34,10 @@ export function bitOp (input, key, func, nullPreserving, scheme) {
|
|||||||
!(nullPreserving && (o === 0 || o === k))) {
|
!(nullPreserving && (o === 0 || o === k))) {
|
||||||
switch (scheme) {
|
switch (scheme) {
|
||||||
case "Input differential":
|
case "Input differential":
|
||||||
key[i % key.length] = x;
|
key[i % key.length] = o;
|
||||||
break;
|
break;
|
||||||
case "Output differential":
|
case "Output differential":
|
||||||
key[i % key.length] = o;
|
key[i % key.length] = x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
436
src/core/lib/Blowfish.mjs
Normal file
436
src/core/lib/Blowfish.mjs
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
/**
|
||||||
|
Blowfish.js from Dojo Toolkit 1.8.1 (https://github.com/dojo/dojox/tree/1.8/encoding)
|
||||||
|
Extracted by Sladex (xslade@gmail.com)
|
||||||
|
Shoehorned into working with mjs for CyberChef by Matt C (matt@artemisbot.uk)
|
||||||
|
Refactored and implemented modes support by cbeuw (cbeuw.andy@gmail.com)
|
||||||
|
|
||||||
|
@license BSD
|
||||||
|
========================================================================
|
||||||
|
The "New" BSD License:
|
||||||
|
**********************
|
||||||
|
|
||||||
|
Copyright (c) 2005-2016, The Dojo Foundation
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of the Dojo Foundation nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const crypto = {};
|
||||||
|
|
||||||
|
import forge from "node-forge";
|
||||||
|
|
||||||
|
|
||||||
|
/* dojo-release-1.8.1/dojo/_base/lang.js.uncompressed.js */
|
||||||
|
|
||||||
|
const lang = {};
|
||||||
|
lang.isString = function(it) {
|
||||||
|
// summary:
|
||||||
|
// Return true if it is a String
|
||||||
|
// it: anything
|
||||||
|
// Item to test.
|
||||||
|
return (typeof it == "string" || it instanceof String); // Boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* dojo-release-1.8.1/dojo/_base/array.js.uncompressed.js */
|
||||||
|
|
||||||
|
const arrayUtil = {};
|
||||||
|
arrayUtil.map = function(arr, callback, thisObject, Ctr) {
|
||||||
|
// summary:
|
||||||
|
// applies callback to each element of arr and returns
|
||||||
|
// an Array with the results
|
||||||
|
// arr: Array|String
|
||||||
|
// the array to iterate on. If a string, operates on
|
||||||
|
// individual characters.
|
||||||
|
// callback: Function
|
||||||
|
// a function is invoked with three arguments, (item, index,
|
||||||
|
// array), and returns a value
|
||||||
|
// thisObject: Object?
|
||||||
|
// may be used to scope the call to callback
|
||||||
|
// returns: Array
|
||||||
|
// description:
|
||||||
|
// This function corresponds to the JavaScript 1.6 Array.map() method, with one difference: when
|
||||||
|
// run over sparse arrays, this implementation passes the "holes" in the sparse array to
|
||||||
|
// the callback function with a value of undefined. JavaScript 1.6's map skips the holes in the sparse array.
|
||||||
|
// For more details, see:
|
||||||
|
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/map
|
||||||
|
// example:
|
||||||
|
// | // returns [2, 3, 4, 5]
|
||||||
|
// | array.map([1, 2, 3, 4], function(item){ return item+1 });
|
||||||
|
|
||||||
|
// TODO: why do we have a non-standard signature here? do we need "Ctr"?
|
||||||
|
let i = 0;
|
||||||
|
const l = arr && arr.length || 0, out = new (Ctr || Array)(l);
|
||||||
|
if (l && typeof arr == "string") arr = arr.split("");
|
||||||
|
if (thisObject) {
|
||||||
|
for (; i < l; ++i) {
|
||||||
|
out[i] = callback.call(thisObject, arr[i], i, arr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (; i < l; ++i) {
|
||||||
|
out[i] = callback(arr[i], i, arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out; // Array
|
||||||
|
};
|
||||||
|
|
||||||
|
/* dojo-release-1.8.1/dojox/encoding/crypto/Blowfish.js.uncompressed.js */
|
||||||
|
|
||||||
|
/* Blowfish
|
||||||
|
* Created based on the C# implementation by Marcus Hahn (http://www.hotpixel.net/)
|
||||||
|
* Unsigned math based on Paul Johnstone and Peter Wood patches.
|
||||||
|
* 2005-12-08
|
||||||
|
*/
|
||||||
|
const boxes={
|
||||||
|
p: [
|
||||||
|
0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
|
||||||
|
0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
|
||||||
|
0x9216d5d9, 0x8979fb1b
|
||||||
|
],
|
||||||
|
s0: [
|
||||||
|
0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
|
||||||
|
0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
|
||||||
|
0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
|
||||||
|
0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
|
||||||
|
0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440,
|
||||||
|
0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
|
||||||
|
0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677,
|
||||||
|
0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
|
||||||
|
0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
|
||||||
|
0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
|
||||||
|
0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,
|
||||||
|
0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
|
||||||
|
0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d,
|
||||||
|
0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
|
||||||
|
0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
|
||||||
|
0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
|
||||||
|
0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb,
|
||||||
|
0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
|
||||||
|
0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82,
|
||||||
|
0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
|
||||||
|
0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
|
||||||
|
0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
|
||||||
|
0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0,
|
||||||
|
0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
|
||||||
|
0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1,
|
||||||
|
0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
|
||||||
|
0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
|
||||||
|
0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
|
||||||
|
0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5,
|
||||||
|
0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
|
||||||
|
0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915,
|
||||||
|
0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a
|
||||||
|
],
|
||||||
|
s1: [
|
||||||
|
0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266,
|
||||||
|
0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
|
||||||
|
0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
|
||||||
|
0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
|
||||||
|
0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8,
|
||||||
|
0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
|
||||||
|
0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7,
|
||||||
|
0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
|
||||||
|
0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
|
||||||
|
0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
|
||||||
|
0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2,
|
||||||
|
0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
|
||||||
|
0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509,
|
||||||
|
0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
|
||||||
|
0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
|
||||||
|
0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
|
||||||
|
0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28,
|
||||||
|
0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
|
||||||
|
0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf,
|
||||||
|
0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
|
||||||
|
0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
|
||||||
|
0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
|
||||||
|
0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696,
|
||||||
|
0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
|
||||||
|
0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0,
|
||||||
|
0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
|
||||||
|
0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
|
||||||
|
0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
|
||||||
|
0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e,
|
||||||
|
0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
|
||||||
|
0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340,
|
||||||
|
0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7
|
||||||
|
],
|
||||||
|
s2: [
|
||||||
|
0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068,
|
||||||
|
0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
|
||||||
|
0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
|
||||||
|
0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
|
||||||
|
0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6,
|
||||||
|
0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
|
||||||
|
0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,
|
||||||
|
0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
|
||||||
|
0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
|
||||||
|
0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
|
||||||
|
0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564,
|
||||||
|
0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
|
||||||
|
0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728,
|
||||||
|
0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
|
||||||
|
0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
|
||||||
|
0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
|
||||||
|
0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb,
|
||||||
|
0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
|
||||||
|
0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9,
|
||||||
|
0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
|
||||||
|
0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
|
||||||
|
0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
|
||||||
|
0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9,
|
||||||
|
0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
|
||||||
|
0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633,
|
||||||
|
0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
|
||||||
|
0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
|
||||||
|
0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
|
||||||
|
0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76,
|
||||||
|
0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
|
||||||
|
0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c,
|
||||||
|
0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0
|
||||||
|
],
|
||||||
|
s3: [
|
||||||
|
0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe,
|
||||||
|
0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
|
||||||
|
0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
|
||||||
|
0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
|
||||||
|
0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,
|
||||||
|
0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
|
||||||
|
0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51,
|
||||||
|
0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
|
||||||
|
0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
|
||||||
|
0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
|
||||||
|
0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319,
|
||||||
|
0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
|
||||||
|
0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32,
|
||||||
|
0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
|
||||||
|
0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
|
||||||
|
0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
|
||||||
|
0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d,
|
||||||
|
0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
|
||||||
|
0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd,
|
||||||
|
0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
|
||||||
|
0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
|
||||||
|
0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
|
||||||
|
0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442,
|
||||||
|
0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
|
||||||
|
0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d,
|
||||||
|
0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
|
||||||
|
0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
|
||||||
|
0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
|
||||||
|
0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b,
|
||||||
|
0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
|
||||||
|
0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9,
|
||||||
|
0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// fixes based on patch submitted by Peter Wood (#5791)
|
||||||
|
const xor = function(x, y) {
|
||||||
|
return (((x>>0x10)^(y>>0x10))<<0x10)|(((x&0xffff)^(y&0xffff))&0xffff);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const f = function(v, box) {
|
||||||
|
const d=box.s3[v&0xff]; v>>=8;
|
||||||
|
const c=box.s2[v&0xff]; v>>=8;
|
||||||
|
const b=box.s1[v&0xff]; v>>=8;
|
||||||
|
const a=box.s0[v&0xff];
|
||||||
|
|
||||||
|
let r = (((a>>0x10)+(b>>0x10)+(((a&0xffff)+(b&0xffff))>>0x10))<<0x10)|(((a&0xffff)+(b&0xffff))&0xffff);
|
||||||
|
r = (((r>>0x10)^(c>>0x10))<<0x10)|(((r&0xffff)^(c&0xffff))&0xffff);
|
||||||
|
return (((r>>0x10)+(d>>0x10)+(((r&0xffff)+(d&0xffff))>>0x10))<<0x10)|(((r&0xffff)+(d&0xffff))&0xffff);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const eb = function(o, box) {
|
||||||
|
// TODO: see if this can't be made more efficient
|
||||||
|
let l=o.left;
|
||||||
|
let r=o.right;
|
||||||
|
l=xor(l, box.p[0]);
|
||||||
|
r=xor(r, xor(f(l, box), box.p[1]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[2]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[3]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[4]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[5]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[6]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[7]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[8]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[9]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[10]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[11]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[12]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[13]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[14]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[15]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[16]));
|
||||||
|
o.right=l;
|
||||||
|
o.left=xor(r, box.p[17]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const db = function(o, box) {
|
||||||
|
let l=o.left;
|
||||||
|
let r=o.right;
|
||||||
|
l=xor(l, box.p[17]);
|
||||||
|
r=xor(r, xor(f(l, box), box.p[16]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[15]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[14]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[13]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[12]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[11]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[10]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[9]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[8]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[7]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[6]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[5]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[4]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[3]));
|
||||||
|
r=xor(r, xor(f(l, box), box.p[2]));
|
||||||
|
l=xor(l, xor(f(r, box), box.p[1]));
|
||||||
|
o.right=l;
|
||||||
|
o.left=xor(r, box.p[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptBlock=function(inblock, outblock, box) {
|
||||||
|
const o = {};
|
||||||
|
o.left=inblock[0];
|
||||||
|
o.right=inblock[1];
|
||||||
|
eb(o, box);
|
||||||
|
outblock[0] = o.left;
|
||||||
|
outblock[1] = o.right;
|
||||||
|
};
|
||||||
|
|
||||||
|
const decryptBlock=function(inblock, outblock, box) {
|
||||||
|
const o= {};
|
||||||
|
o.left=inblock[0];
|
||||||
|
o.right=inblock[1];
|
||||||
|
db(o, box);
|
||||||
|
outblock[0] = o.left;
|
||||||
|
outblock[1] = o.right;
|
||||||
|
};
|
||||||
|
|
||||||
|
crypto.Blowfish = new function() {
|
||||||
|
this.createCipher=function(key, modeName) {
|
||||||
|
return new forge.cipher.BlockCipher({
|
||||||
|
algorithm: new Blowfish.Algorithm(key, modeName),
|
||||||
|
key: key,
|
||||||
|
decrypt: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.createDecipher=function(key, modeName) {
|
||||||
|
return new forge.cipher.BlockCipher({
|
||||||
|
algorithm: new Blowfish.Algorithm(key, modeName),
|
||||||
|
key: key,
|
||||||
|
decrypt: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}();
|
||||||
|
|
||||||
|
|
||||||
|
crypto.Blowfish.Algorithm=function(key, modeName) {
|
||||||
|
this.initialize({key: key});
|
||||||
|
const _box = this.box;
|
||||||
|
const modeOption = {
|
||||||
|
blockSize: 8,
|
||||||
|
cipher: {
|
||||||
|
encrypt: function(inblock, outblock) {
|
||||||
|
encryptBlock(inblock, outblock, _box);
|
||||||
|
},
|
||||||
|
decrypt: function(inblock, outblock) {
|
||||||
|
decryptBlock(inblock, outblock, _box);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (modeName.toLowerCase()) {
|
||||||
|
case "ecb":
|
||||||
|
this.mode=new forge.cipher.modes.ecb(modeOption);
|
||||||
|
break;
|
||||||
|
case "cbc":
|
||||||
|
this.mode=new forge.cipher.modes.cbc(modeOption);
|
||||||
|
break;
|
||||||
|
case "cfb":
|
||||||
|
this.mode=new forge.cipher.modes.cfb(modeOption);
|
||||||
|
break;
|
||||||
|
case "ofb":
|
||||||
|
this.mode=new forge.cipher.modes.ofb(modeOption);
|
||||||
|
break;
|
||||||
|
case "ctr":
|
||||||
|
this.mode=new forge.cipher.modes.ctr(modeOption);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.mode=new forge.cipher.modes.ecb(modeOption);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
crypto.Blowfish.Algorithm.prototype.initialize=function(options) {
|
||||||
|
const POW8=Math.pow(2, 8);
|
||||||
|
|
||||||
|
let k=options.key;
|
||||||
|
if (lang.isString(k)) {
|
||||||
|
k = arrayUtil.map(k.split(""), function(item) {
|
||||||
|
return item.charCodeAt(0) & 0xff;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// init the boxes
|
||||||
|
let pos=0, data=0;
|
||||||
|
const res={ left: 0, right: 0 };
|
||||||
|
const box = {
|
||||||
|
p: arrayUtil.map(boxes.p.slice(0), function(item) {
|
||||||
|
const l=k.length;
|
||||||
|
for (let j=0; j<4; j++) {
|
||||||
|
data=(data*POW8)|k[pos++ % l];
|
||||||
|
}
|
||||||
|
return (((item>>0x10)^(data>>0x10))<<0x10)|(((item&0xffff)^(data&0xffff))&0xffff);
|
||||||
|
}),
|
||||||
|
s0: boxes.s0.slice(0),
|
||||||
|
s1: boxes.s1.slice(0),
|
||||||
|
s2: boxes.s2.slice(0),
|
||||||
|
s3: boxes.s3.slice(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// encrypt p and the s boxes
|
||||||
|
for (let i=0, l=box.p.length; i<l;) {
|
||||||
|
eb(res, box);
|
||||||
|
box.p[i++]=res.left;
|
||||||
|
box.p[i++]=res.right;
|
||||||
|
}
|
||||||
|
for (let i=0; i<4; i++) {
|
||||||
|
for (let j=0, l=box["s"+i].length; j<l;) {
|
||||||
|
eb(res, box);
|
||||||
|
box["s"+i][j++]=res.left;
|
||||||
|
box["s"+i][j++]=res.right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.box = box;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Blowfish = crypto.Blowfish;
|
||||||
756
src/core/lib/Bombe.mjs
Normal file
756
src/core/lib/Bombe.mjs
Normal file
@ -0,0 +1,756 @@
|
|||||||
|
/**
|
||||||
|
* Emulation of the Bombe machine.
|
||||||
|
*
|
||||||
|
* @author s2224834
|
||||||
|
* @author The National Museum of Computing - Bombe Rebuild Project
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
import {Rotor, Plugboard, a2i, i2a} from "./Enigma.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience/optimisation subclass of Rotor
|
||||||
|
*
|
||||||
|
* This allows creating multiple Rotors which share backing maps, to avoid repeatedly parsing the
|
||||||
|
* rotor spec strings and duplicating the maps in memory.
|
||||||
|
*/
|
||||||
|
class CopyRotor extends Rotor {
|
||||||
|
/**
|
||||||
|
* Return a copy of this Rotor.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
copy() {
|
||||||
|
const clone = {
|
||||||
|
map: this.map,
|
||||||
|
revMap: this.revMap,
|
||||||
|
pos: this.pos,
|
||||||
|
step: this.step,
|
||||||
|
transform: this.transform,
|
||||||
|
revTransform: this.revTransform,
|
||||||
|
};
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node in the menu graph
|
||||||
|
*
|
||||||
|
* A node represents a cipher/plaintext letter.
|
||||||
|
*/
|
||||||
|
class Node {
|
||||||
|
/**
|
||||||
|
* Node constructor.
|
||||||
|
* @param {number} letter - The plain/ciphertext letter this node represents (as a number).
|
||||||
|
*/
|
||||||
|
constructor(letter) {
|
||||||
|
this.letter = letter;
|
||||||
|
this.edges = new Set();
|
||||||
|
this.visited = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edge in the menu graph
|
||||||
|
*
|
||||||
|
* An edge represents an Enigma machine transformation between two letters.
|
||||||
|
*/
|
||||||
|
class Edge {
|
||||||
|
/**
|
||||||
|
* Edge constructor - an Enigma machine mapping between letters
|
||||||
|
* @param {number} pos - The rotor position, relative to the beginning of the crib, at this edge
|
||||||
|
* @param {number} node1 - Letter at one end (as a number)
|
||||||
|
* @param {number} node2 - Letter at the other end
|
||||||
|
*/
|
||||||
|
constructor(pos, node1, node2) {
|
||||||
|
this.pos = pos;
|
||||||
|
this.node1 = node1;
|
||||||
|
this.node2 = node2;
|
||||||
|
node1.edges.add(this);
|
||||||
|
node2.edges.add(this);
|
||||||
|
this.visited = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the node at one end of this edge, return the other end.
|
||||||
|
* @param node {number} - The node we have
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getOther(node) {
|
||||||
|
return this.node1 === node ? this.node2 : this.node1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As all the Bombe's rotors move in step, at any given point the vast majority of the scramblers
|
||||||
|
* in the machine share the majority of their state, which is hosted in this class.
|
||||||
|
*/
|
||||||
|
class SharedScrambler {
|
||||||
|
/**
|
||||||
|
* SharedScrambler constructor.
|
||||||
|
* @param {Object[]} rotors - List of rotors in the shared state _only_.
|
||||||
|
* @param {Object} reflector - The reflector in use.
|
||||||
|
*/
|
||||||
|
constructor(rotors, reflector) {
|
||||||
|
this.lowerCache = new Array(26);
|
||||||
|
this.higherCache = new Array(26);
|
||||||
|
for (let i=0; i<26; i++) {
|
||||||
|
this.higherCache[i] = new Array(26);
|
||||||
|
}
|
||||||
|
this.changeRotors(rotors, reflector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the rotors and reflector in this SharedScrambler.
|
||||||
|
* This takes care of flushing caches as well.
|
||||||
|
* @param {Object[]} rotors - List of rotors in the shared state _only_.
|
||||||
|
* @param {Object} reflector - The reflector in use.
|
||||||
|
*/
|
||||||
|
changeRotors(rotors, reflector) {
|
||||||
|
this.reflector = reflector;
|
||||||
|
this.rotors = rotors;
|
||||||
|
this.rotorsRev = [].concat(rotors).reverse();
|
||||||
|
this.cacheGen();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step the rotors forward.
|
||||||
|
* @param {number} n - How many rotors to step. This includes the rotors which are not part of
|
||||||
|
* the shared state, so should be 2 or more.
|
||||||
|
*/
|
||||||
|
step(n) {
|
||||||
|
for (let i=0; i<n-1; i++) {
|
||||||
|
this.rotors[i].step();
|
||||||
|
}
|
||||||
|
this.cacheGen();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimisation: We pregenerate all routes through the machine with the top rotor removed,
|
||||||
|
* as these rarely change. This saves a lot of lookups. This function generates this route
|
||||||
|
* table.
|
||||||
|
* We also just-in-time cache the full routes through the scramblers, because after stepping
|
||||||
|
* the fast rotor some scramblers will be in states occupied by other scrambles on previous
|
||||||
|
* iterations.
|
||||||
|
*/
|
||||||
|
cacheGen() {
|
||||||
|
for (let i=0; i<26; i++) {
|
||||||
|
this.lowerCache[i] = undefined;
|
||||||
|
for (let j=0; j<26; j++) {
|
||||||
|
this.higherCache[i][j] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i=0; i<26; i++) {
|
||||||
|
if (this.lowerCache[i] !== undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let letter = i;
|
||||||
|
for (const rotor of this.rotors) {
|
||||||
|
letter = rotor.transform(letter);
|
||||||
|
}
|
||||||
|
letter = this.reflector.transform(letter);
|
||||||
|
for (const rotor of this.rotorsRev) {
|
||||||
|
letter = rotor.revTransform(letter);
|
||||||
|
}
|
||||||
|
// By symmetry
|
||||||
|
this.lowerCache[i] = letter;
|
||||||
|
this.lowerCache[letter] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a letter through this (partial) scrambler.
|
||||||
|
* @param {number} i - The letter
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
transform(i) {
|
||||||
|
return this.lowerCache[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrambler.
|
||||||
|
*
|
||||||
|
* This is effectively just an Enigma machine, but it only operates on one character at a time and
|
||||||
|
* the stepping mechanism is different.
|
||||||
|
*/
|
||||||
|
class Scrambler {
|
||||||
|
/** Scrambler constructor.
|
||||||
|
* @param {Object} base - The SharedScrambler whose state this scrambler uses
|
||||||
|
* @param {Object} rotor - The non-shared fast rotor in this scrambler
|
||||||
|
* @param {number} pos - Position offset from start of crib
|
||||||
|
* @param {number} end1 - Letter in menu this scrambler is attached to
|
||||||
|
* @param {number} end2 - Other letter in menu this scrambler is attached to
|
||||||
|
*/
|
||||||
|
constructor(base, rotor, pos, end1, end2) {
|
||||||
|
this.baseScrambler = base;
|
||||||
|
this.initialPos = pos;
|
||||||
|
this.changeRotor(rotor);
|
||||||
|
this.end1 = end1;
|
||||||
|
this.end2 = end2;
|
||||||
|
// For efficiency reasons, we pull the relevant shared cache from the baseScrambler into
|
||||||
|
// this object - this saves us a few pointer dereferences
|
||||||
|
this.cache = this.baseScrambler.higherCache[pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the rotor in this scrambler.
|
||||||
|
* The position is reset automatically.
|
||||||
|
* @param {Object} rotor - New rotor
|
||||||
|
*/
|
||||||
|
changeRotor(rotor) {
|
||||||
|
this.rotor = rotor;
|
||||||
|
this.rotor.pos += this.initialPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step the rotor forward.
|
||||||
|
*
|
||||||
|
* The base SharedScrambler needs to be instructed to step separately.
|
||||||
|
*/
|
||||||
|
step() {
|
||||||
|
// The Bombe steps the slowest rotor on an actual Enigma fastest, for reasons.
|
||||||
|
// ...but for optimisation reasons I'm going to cheat and not do that, as this vastly
|
||||||
|
// simplifies caching the state of the majority of the scramblers. The results are the
|
||||||
|
// same, just in a slightly different order.
|
||||||
|
this.rotor.step();
|
||||||
|
this.cache = this.baseScrambler.higherCache[this.rotor.pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a letter through the scrambler.
|
||||||
|
* @param {number} i - The letter to transform (as a number)
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
transform(i) {
|
||||||
|
let letter = i;
|
||||||
|
const cached = this.cache[i];
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
letter = this.rotor.transform(letter);
|
||||||
|
letter = this.baseScrambler.transform(letter);
|
||||||
|
letter = this.rotor.revTransform(letter);
|
||||||
|
this.cache[i] = letter;
|
||||||
|
this.cache[letter] = i;
|
||||||
|
return letter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given one letter in the menu this scrambler maps to, return the other.
|
||||||
|
* @param end {number} - The node we have
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getOtherEnd(end) {
|
||||||
|
return this.end1 === end ? this.end2 : this.end1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the position this scrambler is set to.
|
||||||
|
* Note that because of Enigma's stepping, you need to set an actual Enigma to the previous
|
||||||
|
* position in order to get it to make a certain set of electrical connections when a button
|
||||||
|
* is pressed - this function *does* take this into account.
|
||||||
|
* However, as with the rest of the Bombe, it does not take stepping into account - the middle
|
||||||
|
* and slow rotors are treated as static.
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
getPos() {
|
||||||
|
let result = "";
|
||||||
|
// Roll back the fast rotor by one step
|
||||||
|
let pos = Utils.mod(this.rotor.pos - 1, 26);
|
||||||
|
result += i2a(pos);
|
||||||
|
for (let i=0; i<this.baseScrambler.rotors.length; i++) {
|
||||||
|
pos = this.baseScrambler.rotors[i].pos;
|
||||||
|
result += i2a(pos);
|
||||||
|
}
|
||||||
|
return result.split("").reverse().join("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bombe simulator class.
|
||||||
|
*/
|
||||||
|
export class BombeMachine {
|
||||||
|
/**
|
||||||
|
* Construct a Bombe.
|
||||||
|
*
|
||||||
|
* Note that there is no handling of offsets here: the crib specified must exactly match the
|
||||||
|
* ciphertext. It will check that the crib is sane (length is vaguely sensible and there's no
|
||||||
|
* matching characters between crib and ciphertext) but cannot check further - if it's wrong
|
||||||
|
* your results will be wrong!
|
||||||
|
*
|
||||||
|
* There is also no handling of rotor stepping - if the target Enigma stepped in the middle of
|
||||||
|
* your crib, you're out of luck. TODO: Allow specifying a step point - this is fairly easy to
|
||||||
|
* configure on a real Bombe, but we're not clear on whether it was ever actually done for
|
||||||
|
* real (there would almost certainly have been better ways of attacking in most situations
|
||||||
|
* than attempting to exhaust options for the stepping point, but in some circumstances, e.g.
|
||||||
|
* via Banburismus, the stepping point might have been known).
|
||||||
|
*
|
||||||
|
* @param {string[]} rotors - list of rotor spec strings (without step points!)
|
||||||
|
* @param {Object} reflector - Reflector object
|
||||||
|
* @param {string} ciphertext - The ciphertext to attack
|
||||||
|
* @param {string} crib - Known plaintext for this ciphertext
|
||||||
|
* @param {boolean} check - Whether to use the checking machine
|
||||||
|
* @param {function} update - Function to call to send status updates (optional)
|
||||||
|
*/
|
||||||
|
constructor(rotors, reflector, ciphertext, crib, check, update=undefined) {
|
||||||
|
if (ciphertext.length < crib.length) {
|
||||||
|
throw new OperationError("Crib overruns supplied ciphertext");
|
||||||
|
}
|
||||||
|
if (crib.length < 2) {
|
||||||
|
// This is the absolute bare minimum to be sane, and even then it's likely too short to
|
||||||
|
// be useful
|
||||||
|
throw new OperationError("Crib is too short");
|
||||||
|
}
|
||||||
|
if (crib.length > 25) {
|
||||||
|
// A crib longer than this will definitely cause the middle rotor to step somewhere
|
||||||
|
// A shorter crib is preferable to reduce this chance, of course
|
||||||
|
throw new OperationError("Crib is too long");
|
||||||
|
}
|
||||||
|
for (let i=0; i<crib.length; i++) {
|
||||||
|
if (ciphertext[i] === crib[i]) {
|
||||||
|
throw new OperationError(`Invalid crib: character ${ciphertext[i]} at pos ${i} in both ciphertext and crib`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.ciphertext = ciphertext;
|
||||||
|
this.crib = crib;
|
||||||
|
this.initRotors(rotors);
|
||||||
|
this.check = check;
|
||||||
|
this.updateFn = update;
|
||||||
|
|
||||||
|
const [mostConnected, edges] = this.makeMenu();
|
||||||
|
|
||||||
|
// This is the bundle of wires corresponding to the 26 letters within each of the 26
|
||||||
|
// possible nodes in the menu
|
||||||
|
this.wires = new Array(26*26);
|
||||||
|
|
||||||
|
// These are the pseudo-Engima devices corresponding to each edge in the menu, and the
|
||||||
|
// nodes in the menu they each connect to
|
||||||
|
this.scramblers = new Array();
|
||||||
|
for (let i=0; i<26; i++) {
|
||||||
|
this.scramblers.push(new Array());
|
||||||
|
}
|
||||||
|
this.sharedScrambler = new SharedScrambler(this.baseRotors.slice(1), reflector);
|
||||||
|
this.allScramblers = new Array();
|
||||||
|
this.indicator = undefined;
|
||||||
|
for (const edge of edges) {
|
||||||
|
const cRotor = this.baseRotors[0].copy();
|
||||||
|
const end1 = a2i(edge.node1.letter);
|
||||||
|
const end2 = a2i(edge.node2.letter);
|
||||||
|
const scrambler = new Scrambler(this.sharedScrambler, cRotor, edge.pos, end1, end2);
|
||||||
|
if (edge.pos === 0) {
|
||||||
|
this.indicator = scrambler;
|
||||||
|
}
|
||||||
|
this.scramblers[end1].push(scrambler);
|
||||||
|
this.scramblers[end2].push(scrambler);
|
||||||
|
this.allScramblers.push(scrambler);
|
||||||
|
}
|
||||||
|
// The Bombe uses a set of rotors to keep track of what settings it's testing. We cheat and
|
||||||
|
// use one of the actual scramblers if there's one in the right position, but if not we'll
|
||||||
|
// just create one.
|
||||||
|
if (this.indicator === undefined) {
|
||||||
|
this.indicator = new Scrambler(this.sharedScrambler, this.baseRotors[0].copy(), 0, undefined, undefined);
|
||||||
|
this.allScramblers.push(this.indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.testRegister = a2i(mostConnected.letter);
|
||||||
|
// This is an arbitrary letter other than the most connected letter
|
||||||
|
for (const edge of mostConnected.edges) {
|
||||||
|
this.testInput = [this.testRegister, a2i(edge.getOther(mostConnected).letter)];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Rotor objects from list of rotor wiring strings.
|
||||||
|
* @param {string[]} rotors - List of rotor wiring strings
|
||||||
|
*/
|
||||||
|
initRotors(rotors) {
|
||||||
|
// This is ordered from the Enigma fast rotor to the slow, so bottom to top for the Bombe
|
||||||
|
this.baseRotors = [];
|
||||||
|
for (const rstr of rotors) {
|
||||||
|
const rotor = new CopyRotor(rstr, "", "A", "A");
|
||||||
|
this.baseRotors.push(rotor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the rotors and reflector in all components of this Bombe.
|
||||||
|
* @param {string[]} rotors - List of rotor wiring strings
|
||||||
|
* @param {Object} reflector - Reflector object
|
||||||
|
*/
|
||||||
|
changeRotors(rotors, reflector) {
|
||||||
|
// At the end of the run, the rotors are all back in the same position they started
|
||||||
|
this.initRotors(rotors);
|
||||||
|
this.sharedScrambler.changeRotors(this.baseRotors.slice(1), reflector);
|
||||||
|
for (const scrambler of this.allScramblers) {
|
||||||
|
scrambler.changeRotor(this.baseRotors[0].copy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we have a way of sending status messages, do so.
|
||||||
|
* @param {...*} msg - Message to send.
|
||||||
|
*/
|
||||||
|
update(...msg) {
|
||||||
|
if (this.updateFn !== undefined) {
|
||||||
|
this.updateFn(...msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursive depth-first search on the menu graph.
|
||||||
|
* This is used to a) isolate unconnected sub-graphs, and b) count the number of loops in each
|
||||||
|
* of those graphs.
|
||||||
|
* @param {Object} node - Node object to start the search from
|
||||||
|
* @returns {[number, number, Object, number, Object[]} - loop count, node count, most connected
|
||||||
|
* node, order of most connected node, list of edges in this sub-graph
|
||||||
|
*/
|
||||||
|
dfs(node) {
|
||||||
|
let loops = 0;
|
||||||
|
let nNodes = 1;
|
||||||
|
let mostConnected = node;
|
||||||
|
let nConnections = mostConnected.edges.size;
|
||||||
|
let edges = new Set();
|
||||||
|
node.visited = true;
|
||||||
|
for (const edge of node.edges) {
|
||||||
|
if (edge.visited) {
|
||||||
|
// Already been here from the other end.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
edge.visited = true;
|
||||||
|
edges.add(edge);
|
||||||
|
const other = edge.getOther(node);
|
||||||
|
if (other.visited) {
|
||||||
|
// We have a loop, record that and continue
|
||||||
|
loops += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// This is a newly visited node
|
||||||
|
const [oLoops, oNNodes, oMostConnected, oNConnections, oEdges] = this.dfs(other);
|
||||||
|
loops += oLoops;
|
||||||
|
nNodes += oNNodes;
|
||||||
|
edges = new Set([...edges, ...oEdges]);
|
||||||
|
if (oNConnections > nConnections) {
|
||||||
|
mostConnected = oMostConnected;
|
||||||
|
nConnections = oNConnections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [loops, nNodes, mostConnected, nConnections, edges];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a menu from the ciphertext and crib.
|
||||||
|
* A menu is just a graph where letters in either the ciphertext or crib (Enigma is symmetric,
|
||||||
|
* so there's no difference mathematically) are nodes and states of the Enigma machine itself
|
||||||
|
* are the edges.
|
||||||
|
* Additionally, we want a single connected graph, and of the subgraphs available, we want the
|
||||||
|
* one with the most loops (since these generate feedback cycles which efficiently close off
|
||||||
|
* disallowed states).
|
||||||
|
* Finally, we want to identify the most connected node in that graph (as it's the best choice
|
||||||
|
* of measurement point).
|
||||||
|
* @returns [Object, Object[]] - the most connected node, and the list of edges in the subgraph
|
||||||
|
*/
|
||||||
|
makeMenu() {
|
||||||
|
// First, we make a graph of all of the mappings given by the crib
|
||||||
|
// Make all nodes first
|
||||||
|
const nodes = new Map();
|
||||||
|
for (const c of this.ciphertext + this.crib) {
|
||||||
|
if (!nodes.has(c)) {
|
||||||
|
const node = new Node(c);
|
||||||
|
nodes.set(c, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Then all edges
|
||||||
|
for (let i=0; i<this.crib.length; i++) {
|
||||||
|
const a = this.crib[i];
|
||||||
|
const b = this.ciphertext[i];
|
||||||
|
new Edge(i, nodes.get(a), nodes.get(b));
|
||||||
|
}
|
||||||
|
// list of [loop_count, node_count, most_connected_node, connections_on_most_connected, edges]
|
||||||
|
const graphs = [];
|
||||||
|
// Then, for each unconnected subgraph, we count the number of loops and nodes
|
||||||
|
for (const start of nodes.keys()) {
|
||||||
|
if (nodes.get(start).visited) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const subgraph = this.dfs(nodes.get(start));
|
||||||
|
graphs.push(subgraph);
|
||||||
|
}
|
||||||
|
// Return the subgraph with the most loops (ties broken by node count)
|
||||||
|
graphs.sort((a, b) => {
|
||||||
|
let result = b[0] - a[0];
|
||||||
|
if (result === 0) {
|
||||||
|
result = b[1] - a[1];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
this.nLoops = graphs[0][0];
|
||||||
|
return [graphs[0][2], graphs[0][4]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bombe electrical simulation. Energise a wire. For all connected wires (both via the diagonal
|
||||||
|
* board and via the scramblers), energise them too, recursively.
|
||||||
|
* @param {number} i - Bombe wire bundle
|
||||||
|
* @param {number} j - Bombe stecker hypothesis wire within bundle
|
||||||
|
*/
|
||||||
|
energise(i, j) {
|
||||||
|
const idx = 26*i + j;
|
||||||
|
if (this.wires[idx]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.wires[idx] = true;
|
||||||
|
// Welchman's diagonal board: if A steckers to B, that implies B steckers to A. Handle
|
||||||
|
// both.
|
||||||
|
const idxPair = 26*j + i;
|
||||||
|
this.wires[idxPair] = true;
|
||||||
|
if (i === this.testRegister || j === this.testRegister) {
|
||||||
|
this.energiseCount++;
|
||||||
|
if (this.energiseCount === 26) {
|
||||||
|
// no point continuing, bail out
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let k=0; k<this.scramblers[i].length; k++) {
|
||||||
|
const scrambler = this.scramblers[i][k];
|
||||||
|
const out = scrambler.transform(j);
|
||||||
|
const other = scrambler.getOtherEnd(i);
|
||||||
|
// Lift the pre-check before the call, to save some function call overhead
|
||||||
|
const otherIdx = 26*other + out;
|
||||||
|
if (!this.wires[otherIdx]) {
|
||||||
|
this.energise(other, out);
|
||||||
|
if (this.energiseCount === 26) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i === j) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let k=0; k<this.scramblers[j].length; k++) {
|
||||||
|
const scrambler = this.scramblers[j][k];
|
||||||
|
const out = scrambler.transform(i);
|
||||||
|
const other = scrambler.getOtherEnd(j);
|
||||||
|
const otherIdx = 26*other + out;
|
||||||
|
if (!this.wires[otherIdx]) {
|
||||||
|
this.energise(other, out);
|
||||||
|
if (this.energiseCount === 26) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trial decryption at the current setting.
|
||||||
|
* Used after we get a stop.
|
||||||
|
* This applies the detected stecker pair if we have one. It does not handle the other
|
||||||
|
* steckering or stepping (which is why we limit it to 26 characters, since it's guaranteed to
|
||||||
|
* be wrong after that anyway).
|
||||||
|
* @param {string} stecker - Known stecker spec string.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
tryDecrypt(stecker) {
|
||||||
|
const fastRotor = this.indicator.rotor;
|
||||||
|
const initialPos = fastRotor.pos;
|
||||||
|
const res = [];
|
||||||
|
const plugboard = new Plugboard(stecker);
|
||||||
|
// The indicator scrambler starts in the right place for the beginning of the ciphertext.
|
||||||
|
for (let i=0; i<Math.min(26, this.ciphertext.length); i++) {
|
||||||
|
const t = this.indicator.transform(plugboard.transform(a2i(this.ciphertext[i])));
|
||||||
|
res.push(i2a(plugboard.transform(t)));
|
||||||
|
this.indicator.step(1);
|
||||||
|
}
|
||||||
|
fastRotor.pos = initialPos;
|
||||||
|
return res.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a steckered pair, in sorted order to allow uniquing.
|
||||||
|
* @param {number} a - A letter
|
||||||
|
* @param {number} b - Its stecker pair
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
formatPair(a, b) {
|
||||||
|
if (a < b) {
|
||||||
|
return `${i2a(a)}${i2a(b)}`;
|
||||||
|
}
|
||||||
|
return `${i2a(b)}${i2a(a)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The checking machine was used to manually verify Bombe stops. Using a device which was
|
||||||
|
* effectively a non-stepping Enigma, the user would walk through each of the links in the
|
||||||
|
* menu at the rotor positions determined by the Bombe. By starting with the stecker pair the
|
||||||
|
* Bombe gives us, we find the stecker pair of each connected letter in the graph, and so on.
|
||||||
|
* If a contradiction is reached, the stop is invalid. If not, we have most (but not
|
||||||
|
* necessarily all) of the plugboard connections.
|
||||||
|
* You will notice that this procedure is exactly the same as what the Bombe itself does, only
|
||||||
|
* we start with an assumed good hypothesis and read out the stecker pair for every letter.
|
||||||
|
* On the real hardware that wasn't practical, but fortunately we're not the real hardware, so
|
||||||
|
* we don't need to implement the manual checking machine procedure.
|
||||||
|
* @param {number} pair - The stecker pair of the test register.
|
||||||
|
* @returns {string} - The empty string for invalid stops, or a plugboard configuration string
|
||||||
|
* containing all known pairs.
|
||||||
|
*/
|
||||||
|
checkingMachine(pair) {
|
||||||
|
if (pair !== this.testInput[1]) {
|
||||||
|
// We have a new hypothesis for this stop - apply the new one.
|
||||||
|
// De-energise the board
|
||||||
|
for (let i=0; i<this.wires.length; i++) {
|
||||||
|
this.wires[i] = false;
|
||||||
|
}
|
||||||
|
this.energiseCount = 0;
|
||||||
|
// Re-energise with the corrected hypothesis
|
||||||
|
this.energise(this.testRegister, pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = new Set();
|
||||||
|
results.add(this.formatPair(this.testRegister, pair));
|
||||||
|
for (let i=0; i<26; i++) {
|
||||||
|
let count = 0;
|
||||||
|
let other;
|
||||||
|
for (let j=0; j<26; j++) {
|
||||||
|
if (this.wires[i*26 + j]) {
|
||||||
|
count++;
|
||||||
|
other = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count > 1) {
|
||||||
|
// This is an invalid stop.
|
||||||
|
return "";
|
||||||
|
} else if (count === 0) {
|
||||||
|
// No information about steckering from this wire
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
results.add(this.formatPair(i, other));
|
||||||
|
}
|
||||||
|
return [...results].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if the Bombe has stopped. If so, process the stop.
|
||||||
|
* @returns {(undefined|string[3])} - Undefined for no stop, or [rotor settings, plugboard settings, decryption preview]
|
||||||
|
*/
|
||||||
|
checkStop() {
|
||||||
|
// Count the energised outputs
|
||||||
|
const count = this.energiseCount;
|
||||||
|
if (count === 26) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// If it's not all of them, we have a stop
|
||||||
|
let steckerPair;
|
||||||
|
// The Bombe tells us one stecker pair as well. The input wire and test register we
|
||||||
|
// started with are hypothesised to be a stecker pair.
|
||||||
|
if (count === 25) {
|
||||||
|
// Our steckering hypothesis is wrong. Correct value is the un-energised wire.
|
||||||
|
for (let j=0; j<26; j++) {
|
||||||
|
if (!this.wires[26*this.testRegister + j]) {
|
||||||
|
steckerPair = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (count === 1) {
|
||||||
|
// This means our hypothesis for the steckering is correct.
|
||||||
|
steckerPair = this.testInput[1];
|
||||||
|
} else {
|
||||||
|
// This was known as a "boxing stop" - we have a stop but not a single hypothesis.
|
||||||
|
// If this happens a lot it implies the menu isn't good enough.
|
||||||
|
// If we have the checking machine enabled, we're going to just check each wire in
|
||||||
|
// turn. If we get 0 or 1 hit, great.
|
||||||
|
// If we get multiple hits, or the checking machine is off, the user will just have to
|
||||||
|
// deal with it.
|
||||||
|
if (!this.check) {
|
||||||
|
// We can't draw any conclusions about the steckering (one could maybe suggest
|
||||||
|
// options in some cases, but too hard to present clearly).
|
||||||
|
return [this.indicator.getPos(), "??", this.tryDecrypt("")];
|
||||||
|
}
|
||||||
|
let stecker = undefined;
|
||||||
|
for (let i = 0; i < 26; i++) {
|
||||||
|
const newStecker = this.checkingMachine(i);
|
||||||
|
if (newStecker !== "") {
|
||||||
|
if (stecker !== undefined) {
|
||||||
|
// Multiple hypotheses can't be ruled out.
|
||||||
|
return [this.indicator.getPos(), "??", this.tryDecrypt("")];
|
||||||
|
}
|
||||||
|
stecker = newStecker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stecker === undefined) {
|
||||||
|
// Checking machine ruled all possibilities out.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// If we got here, there was just one possibility allowed by the checking machine. Success.
|
||||||
|
return [this.indicator.getPos(), stecker, this.tryDecrypt(stecker)];
|
||||||
|
}
|
||||||
|
let stecker;
|
||||||
|
if (this.check) {
|
||||||
|
stecker = this.checkingMachine(steckerPair);
|
||||||
|
if (stecker === "") {
|
||||||
|
// Invalid stop - don't count it, don't return it
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stecker = `${i2a(this.testRegister)}${i2a(steckerPair)}`;
|
||||||
|
}
|
||||||
|
const testDecrypt = this.tryDecrypt(stecker);
|
||||||
|
return [this.indicator.getPos(), stecker, testDecrypt];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Having set up the Bombe, do the actual attack run. This tries every possible rotor setting
|
||||||
|
* and attempts to logically invalidate them. If it can't, it's added to the list of candidate
|
||||||
|
* solutions.
|
||||||
|
* @returns {string[][3]} - list of 3-tuples of candidate rotor setting, plugboard settings, and decryption preview
|
||||||
|
*/
|
||||||
|
run() {
|
||||||
|
let stops = 0;
|
||||||
|
const result = [];
|
||||||
|
// For each possible rotor setting
|
||||||
|
const nChecks = Math.pow(26, this.baseRotors.length);
|
||||||
|
for (let i=1; i<=nChecks; i++) {
|
||||||
|
// Benchmarking suggests this is faster than using .fill()
|
||||||
|
for (let i=0; i<this.wires.length; i++) {
|
||||||
|
this.wires[i] = false;
|
||||||
|
}
|
||||||
|
this.energiseCount = 0;
|
||||||
|
// Energise the test input, follow the current through each scrambler
|
||||||
|
// (and the diagonal board)
|
||||||
|
this.energise(...this.testInput);
|
||||||
|
|
||||||
|
const stop = this.checkStop();
|
||||||
|
if (stop !== undefined) {
|
||||||
|
stops++;
|
||||||
|
result.push(stop);
|
||||||
|
}
|
||||||
|
// Step all the scramblers
|
||||||
|
// This loop counts how many rotors have reached their starting position (meaning the
|
||||||
|
// next one needs to step as well)
|
||||||
|
let n = 1;
|
||||||
|
for (let j=1; j<this.baseRotors.length; j++) {
|
||||||
|
if ((i % Math.pow(26, j)) === 0) {
|
||||||
|
n++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (n > 1) {
|
||||||
|
this.sharedScrambler.step(n);
|
||||||
|
}
|
||||||
|
for (const scrambler of this.allScramblers) {
|
||||||
|
scrambler.step();
|
||||||
|
}
|
||||||
|
// Send status messages at what seems to be a reasonably sensible frequency
|
||||||
|
// (note this won't be triggered on 3-rotor runs - they run fast enough it doesn't seem necessary)
|
||||||
|
if (n > 3) {
|
||||||
|
this.update(this.nLoops, stops, i/nChecks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/core/lib/COBS.mjs
Normal file
86
src/core/lib/COBS.mjs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* @author Imantas Lukenskas [imantas@lukenskas.dev]
|
||||||
|
* @copyright Imantas Lukenskas 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* COBS-encode a byte array
|
||||||
|
* @param {Uint8Array} data
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
|
export function toCobs(data) {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return new Uint8Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = [];
|
||||||
|
data = [0, ...data];
|
||||||
|
|
||||||
|
while (data.length > 0) {
|
||||||
|
const endIndex = data.findIndex((value, index) => value === 0 && index > 0);
|
||||||
|
|
||||||
|
if ((endIndex < 0 || endIndex > 254) && data.length > 254) {
|
||||||
|
output.push(255);
|
||||||
|
output.push(...data.slice(1, 255));
|
||||||
|
data = data.slice(255);
|
||||||
|
if (data.length !== 0) {
|
||||||
|
data = [0, ...data];
|
||||||
|
}
|
||||||
|
} else if (endIndex < 0) {
|
||||||
|
output.push(data.length);
|
||||||
|
output.push(...data.slice(1));
|
||||||
|
data = [];
|
||||||
|
} else {
|
||||||
|
output.push(endIndex);
|
||||||
|
output.push(...data.slice(1, endIndex));
|
||||||
|
data = data.slice(endIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* COBS-decode a byte array
|
||||||
|
* @param {Uint8Array} data
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
|
export function fromCobs(data) {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return new Uint8Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.findIndex((value) => value === 0) >= 0) {
|
||||||
|
throw new OperationError("Could not decode from COBS: payload must not contain a 0x00 byte");
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
while (data.length > 0) {
|
||||||
|
if (data[0] === 0xFF) {
|
||||||
|
output.push(...data.slice(1, 255));
|
||||||
|
data = data.slice(255);
|
||||||
|
} else {
|
||||||
|
const nextZeroIndex = data[0];
|
||||||
|
output.push(...data.slice(1, nextZeroIndex));
|
||||||
|
data = data.slice(nextZeroIndex);
|
||||||
|
|
||||||
|
let blockSize = data[0];
|
||||||
|
while (data.length > 0) {
|
||||||
|
output.push(0, ...data.slice(1, blockSize));
|
||||||
|
data = data.slice(blockSize);
|
||||||
|
|
||||||
|
if (blockSize === 0xFF) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockSize = data[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
0
src/core/lib/CanvasComponents.mjs
Executable file → Normal file
0
src/core/lib/CanvasComponents.mjs
Executable file → Normal file
179
src/core/lib/Charts.mjs
Normal file
179
src/core/lib/Charts.mjs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* @author tlwr [toby@toby.codes]
|
||||||
|
* @author Matt C [me@mitt.dev]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constant
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
export const RECORD_DELIMITER_OPTIONS = ["Line feed", "CRLF"];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constant
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
export const FIELD_DELIMITER_OPTIONS = ["Space", "Comma", "Semi-colon", "Colon", "Tab"];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default from colour
|
||||||
|
*
|
||||||
|
* @constant
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
export const COLOURS = {
|
||||||
|
min: "white",
|
||||||
|
max: "black"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets values from input for a plot.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @param {string} recordDelimiter
|
||||||
|
* @param {string} fieldDelimiter
|
||||||
|
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||||
|
* @param {number} length
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
|
export function getValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded, length) {
|
||||||
|
let headings;
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
input
|
||||||
|
.split(recordDelimiter)
|
||||||
|
.forEach((row, rowIndex) => {
|
||||||
|
const split = row.split(fieldDelimiter);
|
||||||
|
if (split.length !== length) throw new OperationError(`Each row must have length ${length}.`);
|
||||||
|
|
||||||
|
if (columnHeadingsAreIncluded && rowIndex === 0) {
|
||||||
|
headings = split;
|
||||||
|
} else {
|
||||||
|
values.push(split);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { headings, values };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets values from input for a scatter plot.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @param {string} recordDelimiter
|
||||||
|
* @param {string} fieldDelimiter
|
||||||
|
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
|
export function getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
||||||
|
let { headings, values } = getValues(
|
||||||
|
input,
|
||||||
|
recordDelimiter,
|
||||||
|
fieldDelimiter,
|
||||||
|
columnHeadingsAreIncluded,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
if (headings) {
|
||||||
|
headings = {x: headings[0], y: headings[1]};
|
||||||
|
}
|
||||||
|
|
||||||
|
values = values.map(row => {
|
||||||
|
const x = parseFloat(row[0]),
|
||||||
|
y = parseFloat(row[1]);
|
||||||
|
|
||||||
|
if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10.");
|
||||||
|
if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10.");
|
||||||
|
|
||||||
|
return [x, y];
|
||||||
|
});
|
||||||
|
|
||||||
|
return { headings, values };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets values from input for a scatter plot with colour from the third column.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @param {string} recordDelimiter
|
||||||
|
* @param {string} fieldDelimiter
|
||||||
|
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
|
export function getScatterValuesWithColour(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
||||||
|
let { headings, values } = getValues(
|
||||||
|
input,
|
||||||
|
recordDelimiter, fieldDelimiter,
|
||||||
|
columnHeadingsAreIncluded,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
|
if (headings) {
|
||||||
|
headings = {x: headings[0], y: headings[1]};
|
||||||
|
}
|
||||||
|
|
||||||
|
values = values.map(row => {
|
||||||
|
const x = parseFloat(row[0]),
|
||||||
|
y = parseFloat(row[1]),
|
||||||
|
colour = row[2];
|
||||||
|
|
||||||
|
if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10.");
|
||||||
|
if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10.");
|
||||||
|
|
||||||
|
return [x, y, Utils.escapeHtml(colour)];
|
||||||
|
});
|
||||||
|
|
||||||
|
return { headings, values };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets values from input for a time series plot.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @param {string} recordDelimiter
|
||||||
|
* @param {string} fieldDelimiter
|
||||||
|
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
|
export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
||||||
|
const { values } = getValues(
|
||||||
|
input,
|
||||||
|
recordDelimiter, fieldDelimiter,
|
||||||
|
false,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
|
let xValues = new Set();
|
||||||
|
const series = Object.create(null);
|
||||||
|
|
||||||
|
values.forEach(row => {
|
||||||
|
const serie = row[0],
|
||||||
|
xVal = row[1],
|
||||||
|
val = parseFloat(row[2]);
|
||||||
|
|
||||||
|
if (Number.isNaN(val)) throw new OperationError("Values must be numbers in base 10.");
|
||||||
|
|
||||||
|
xValues.add(xVal);
|
||||||
|
if (typeof series[serie] === "undefined") series[serie] = Object.create(null);
|
||||||
|
series[serie][xVal] = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
xValues = new Array(...xValues);
|
||||||
|
|
||||||
|
const seriesList = [];
|
||||||
|
for (const seriesName of Object.keys(series)) {
|
||||||
|
const serie = series[seriesName];
|
||||||
|
seriesList.push({name: seriesName, data: serie});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { xValues, series: seriesList };
|
||||||
|
}
|
||||||
@ -6,22 +6,73 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import cptable from "codepage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Character encoding format mappings.
|
* Character encoding format mappings.
|
||||||
*/
|
*/
|
||||||
export const IO_FORMAT = {
|
export const CHR_ENC_CODE_PAGES = {
|
||||||
"UTF-8 (65001)": 65001,
|
"UTF-8 (65001)": 65001,
|
||||||
"UTF-7 (65000)": 65000,
|
"UTF-7 (65000)": 65000,
|
||||||
"UTF16LE (1200)": 1200,
|
"UTF-16LE (1200)": 1200,
|
||||||
"UTF16BE (1201)": 1201,
|
"UTF-16BE (1201)": 1201,
|
||||||
"UTF16 (1201)": 1201,
|
"UTF-32LE (12000)": 12000,
|
||||||
|
"UTF-32BE (12001)": 12001,
|
||||||
"IBM EBCDIC International (500)": 500,
|
"IBM EBCDIC International (500)": 500,
|
||||||
"IBM EBCDIC US-Canada (37)": 37,
|
"IBM EBCDIC US-Canada (37)": 37,
|
||||||
|
"IBM EBCDIC Multilingual/ROECE (Latin 2) (870)": 870,
|
||||||
|
"IBM EBCDIC Greek Modern (875)": 875,
|
||||||
|
"IBM EBCDIC French (1010)": 1010,
|
||||||
|
"IBM EBCDIC Turkish (Latin 5) (1026)": 1026,
|
||||||
|
"IBM EBCDIC Latin 1/Open System (1047)": 1047,
|
||||||
|
"IBM EBCDIC Lao (1132/1133/1341)": 1132,
|
||||||
|
"IBM EBCDIC US-Canada (037 + Euro symbol) (1140)": 1140,
|
||||||
|
"IBM EBCDIC Germany (20273 + Euro symbol) (1141)": 1141,
|
||||||
|
"IBM EBCDIC Denmark-Norway (20277 + Euro symbol) (1142)": 1142,
|
||||||
|
"IBM EBCDIC Finland-Sweden (20278 + Euro symbol) (1143)": 1143,
|
||||||
|
"IBM EBCDIC Italy (20280 + Euro symbol) (1144)": 1144,
|
||||||
|
"IBM EBCDIC Latin America-Spain (20284 + Euro symbol) (1145)": 1145,
|
||||||
|
"IBM EBCDIC United Kingdom (20285 + Euro symbol) (1146)": 1146,
|
||||||
|
"IBM EBCDIC France (20297 + Euro symbol) (1147)": 1147,
|
||||||
|
"IBM EBCDIC International (500 + Euro symbol) (1148)": 1148,
|
||||||
|
"IBM EBCDIC Icelandic (20871 + Euro symbol) (1149)": 1149,
|
||||||
|
"IBM EBCDIC Germany (20273)": 20273,
|
||||||
|
"IBM EBCDIC Denmark-Norway (20277)": 20277,
|
||||||
|
"IBM EBCDIC Finland-Sweden (20278)": 20278,
|
||||||
|
"IBM EBCDIC Italy (20280)": 20280,
|
||||||
|
"IBM EBCDIC Latin America-Spain (20284)": 20284,
|
||||||
|
"IBM EBCDIC United Kingdom (20285)": 20285,
|
||||||
|
"IBM EBCDIC Japanese Katakana Extended (20290)": 20290,
|
||||||
|
"IBM EBCDIC France (20297)": 20297,
|
||||||
|
"IBM EBCDIC Arabic (20420)": 20420,
|
||||||
|
"IBM EBCDIC Greek (20423)": 20423,
|
||||||
|
"IBM EBCDIC Hebrew (20424)": 20424,
|
||||||
|
"IBM EBCDIC Korean Extended (20833)": 20833,
|
||||||
|
"IBM EBCDIC Thai (20838)": 20838,
|
||||||
|
"IBM EBCDIC Icelandic (20871)": 20871,
|
||||||
|
"IBM EBCDIC Cyrillic Russian (20880)": 20880,
|
||||||
|
"IBM EBCDIC Turkish (20905)": 20905,
|
||||||
|
"IBM EBCDIC Latin 1/Open System (1047 + Euro symbol) (20924)": 20924,
|
||||||
|
"IBM EBCDIC Cyrillic Serbian-Bulgarian (21025)": 21025,
|
||||||
|
"OEM United States (437)": 437,
|
||||||
|
"OEM Greek (formerly 437G); Greek (DOS) (737)": 737,
|
||||||
|
"OEM Baltic; Baltic (DOS) (775)": 775,
|
||||||
|
"OEM Russian; Cyrillic + Euro symbol (808)": 808,
|
||||||
|
"OEM Multilingual Latin 1; Western European (DOS) (850)": 850,
|
||||||
|
"OEM Latin 2; Central European (DOS) (852)": 852,
|
||||||
|
"OEM Cyrillic (primarily Russian) (855)": 855,
|
||||||
|
"OEM Turkish; Turkish (DOS) (857)": 857,
|
||||||
|
"OEM Multilingual Latin 1 + Euro symbol (858)": 858,
|
||||||
|
"OEM Portuguese; Portuguese (DOS) (860)": 860,
|
||||||
|
"OEM Icelandic; Icelandic (DOS) (861)": 861,
|
||||||
|
"OEM Hebrew; Hebrew (DOS) (862)": 862,
|
||||||
|
"OEM French Canadian; French Canadian (DOS) (863)": 863,
|
||||||
|
"OEM Arabic; Arabic (864) (864)": 864,
|
||||||
|
"OEM Nordic; Nordic (DOS) (865)": 865,
|
||||||
|
"OEM Russian; Cyrillic (DOS) (866)": 866,
|
||||||
|
"OEM Modern Greek; Greek, Modern (DOS) (869)": 869,
|
||||||
|
"OEM Cyrillic (primarily Russian) + Euro Symbol (872)": 872,
|
||||||
"Windows-874 Thai (874)": 874,
|
"Windows-874 Thai (874)": 874,
|
||||||
"Japanese Shift-JIS (932)": 932,
|
|
||||||
"Simplified Chinese GBK (936)": 936,
|
|
||||||
"Korean (949)": 949,
|
|
||||||
"Traditional Chinese Big5 (950)": 950,
|
|
||||||
"Windows-1250 Central European (1250)": 1250,
|
"Windows-1250 Central European (1250)": 1250,
|
||||||
"Windows-1251 Cyrillic (1251)": 1251,
|
"Windows-1251 Cyrillic (1251)": 1251,
|
||||||
"Windows-1252 Latin (1252)": 1252,
|
"Windows-1252 Latin (1252)": 1252,
|
||||||
@ -31,10 +82,6 @@ export const IO_FORMAT = {
|
|||||||
"Windows-1256 Arabic (1256)": 1256,
|
"Windows-1256 Arabic (1256)": 1256,
|
||||||
"Windows-1257 Baltic (1257)": 1257,
|
"Windows-1257 Baltic (1257)": 1257,
|
||||||
"Windows-1258 Vietnam (1258)": 1258,
|
"Windows-1258 Vietnam (1258)": 1258,
|
||||||
"US-ASCII (20127)": 20127,
|
|
||||||
"Simplified Chinese GB2312 (20936)": 20936,
|
|
||||||
"KOI8-R Russian Cyrillic (20866)": 20866,
|
|
||||||
"KOI8-U Ukrainian Cyrillic (21866)": 21866,
|
|
||||||
"ISO-8859-1 Latin 1 Western European (28591)": 28591,
|
"ISO-8859-1 Latin 1 Western European (28591)": 28591,
|
||||||
"ISO-8859-2 Latin 2 Central European (28592)": 28592,
|
"ISO-8859-2 Latin 2 Central European (28592)": 28592,
|
||||||
"ISO-8859-3 Latin 3 South European (28593)": 28593,
|
"ISO-8859-3 Latin 3 South European (28593)": 28593,
|
||||||
@ -43,6 +90,7 @@ export const IO_FORMAT = {
|
|||||||
"ISO-8859-6 Latin/Arabic (28596)": 28596,
|
"ISO-8859-6 Latin/Arabic (28596)": 28596,
|
||||||
"ISO-8859-7 Latin/Greek (28597)": 28597,
|
"ISO-8859-7 Latin/Greek (28597)": 28597,
|
||||||
"ISO-8859-8 Latin/Hebrew (28598)": 28598,
|
"ISO-8859-8 Latin/Hebrew (28598)": 28598,
|
||||||
|
"ISO 8859-8 Hebrew (ISO-Logical) (38598)": 38598,
|
||||||
"ISO-8859-9 Latin 5 Turkish (28599)": 28599,
|
"ISO-8859-9 Latin 5 Turkish (28599)": 28599,
|
||||||
"ISO-8859-10 Latin 6 Nordic (28600)": 28600,
|
"ISO-8859-10 Latin 6 Nordic (28600)": 28600,
|
||||||
"ISO-8859-11 Latin/Thai (28601)": 28601,
|
"ISO-8859-11 Latin/Thai (28601)": 28601,
|
||||||
@ -50,9 +98,211 @@ export const IO_FORMAT = {
|
|||||||
"ISO-8859-14 Latin 8 Celtic (28604)": 28604,
|
"ISO-8859-14 Latin 8 Celtic (28604)": 28604,
|
||||||
"ISO-8859-15 Latin 9 (28605)": 28605,
|
"ISO-8859-15 Latin 9 (28605)": 28605,
|
||||||
"ISO-8859-16 Latin 10 (28606)": 28606,
|
"ISO-8859-16 Latin 10 (28606)": 28606,
|
||||||
"ISO-2022 JIS Japanese (50222)": 50222,
|
"ISO 2022 JIS Japanese with no halfwidth Katakana (50220)": 50220,
|
||||||
|
"ISO 2022 JIS Japanese with halfwidth Katakana (50221)": 50221,
|
||||||
|
"ISO 2022 Japanese JIS X 0201-1989 (1 byte Kana-SO/SI) (50222)": 50222,
|
||||||
|
"ISO 2022 Korean (50225)": 50225,
|
||||||
|
"ISO 2022 Simplified Chinese (50227)": 50227,
|
||||||
|
"ISO 6937 Non-Spacing Accent (20269)": 20269,
|
||||||
"EUC Japanese (51932)": 51932,
|
"EUC Japanese (51932)": 51932,
|
||||||
|
"EUC Simplified Chinese (51936)": 51936,
|
||||||
"EUC Korean (51949)": 51949,
|
"EUC Korean (51949)": 51949,
|
||||||
|
"ISCII Devanagari (57002)": 57002,
|
||||||
|
"ISCII Bengali (57003)": 57003,
|
||||||
|
"ISCII Tamil (57004)": 57004,
|
||||||
|
"ISCII Telugu (57005)": 57005,
|
||||||
|
"ISCII Assamese (57006)": 57006,
|
||||||
|
"ISCII Oriya (57007)": 57007,
|
||||||
|
"ISCII Kannada (57008)": 57008,
|
||||||
|
"ISCII Malayalam (57009)": 57009,
|
||||||
|
"ISCII Gujarati (57010)": 57010,
|
||||||
|
"ISCII Punjabi (57011)": 57011,
|
||||||
|
"Japanese Shift-JIS (932)": 932,
|
||||||
|
"Simplified Chinese GBK (936)": 936,
|
||||||
|
"Korean (949)": 949,
|
||||||
|
"Traditional Chinese Big5 (950)": 950,
|
||||||
|
"US-ASCII (7-bit) (20127)": 20127,
|
||||||
|
"Simplified Chinese GB2312 (20936)": 20936,
|
||||||
|
"KOI8-R Russian Cyrillic (20866)": 20866,
|
||||||
|
"KOI8-U Ukrainian Cyrillic (21866)": 21866,
|
||||||
|
"Mazovia (Polish) MS-DOS (620)": 620,
|
||||||
|
"Arabic (ASMO 708) (708)": 708,
|
||||||
|
"Arabic (Transparent ASMO); Arabic (DOS) (720)": 720,
|
||||||
|
"Kamenický (Czech) MS-DOS (895)": 895,
|
||||||
|
"Korean (Johab) (1361)": 1361,
|
||||||
|
"MAC Roman (10000)": 10000,
|
||||||
|
"Japanese (Mac) (10001)": 10001,
|
||||||
|
"MAC Traditional Chinese (Big5) (10002)": 10002,
|
||||||
|
"Korean (Mac) (10003)": 10003,
|
||||||
|
"Arabic (Mac) (10004)": 10004,
|
||||||
|
"Hebrew (Mac) (10005)": 10005,
|
||||||
|
"Greek (Mac) (10006)": 10006,
|
||||||
|
"Cyrillic (Mac) (10007)": 10007,
|
||||||
|
"MAC Simplified Chinese (GB 2312) (10008)": 10008,
|
||||||
|
"Romanian (Mac) (10010)": 10010,
|
||||||
|
"Ukrainian (Mac) (10017)": 10017,
|
||||||
|
"Thai (Mac) (10021)": 10021,
|
||||||
|
"MAC Latin 2 (Central European) (10029)": 10029,
|
||||||
|
"Icelandic (Mac) (10079)": 10079,
|
||||||
|
"Turkish (Mac) (10081)": 10081,
|
||||||
|
"Croatian (Mac) (10082)": 10082,
|
||||||
|
"CNS Taiwan (Chinese Traditional) (20000)": 20000,
|
||||||
|
"TCA Taiwan (20001)": 20001,
|
||||||
|
"ETEN Taiwan (Chinese Traditional) (20002)": 20002,
|
||||||
|
"IBM5550 Taiwan (20003)": 20003,
|
||||||
|
"TeleText Taiwan (20004)": 20004,
|
||||||
|
"Wang Taiwan (20005)": 20005,
|
||||||
|
"Western European IA5 (IRV International Alphabet 5) (20105)": 20105,
|
||||||
|
"IA5 German (7-bit) (20106)": 20106,
|
||||||
|
"IA5 Swedish (7-bit) (20107)": 20107,
|
||||||
|
"IA5 Norwegian (7-bit) (20108)": 20108,
|
||||||
|
"T.61 (20261)": 20261,
|
||||||
|
"Japanese (JIS 0208-1990 and 0212-1990) (20932)": 20932,
|
||||||
|
"Korean Wansung (20949)": 20949,
|
||||||
|
"Extended/Ext Alpha Lowercase (21027)": 21027,
|
||||||
|
"Europa 3 (29001)": 29001,
|
||||||
|
"Atari ST/TT (47451)": 47451,
|
||||||
|
"HZ-GB2312 Simplified Chinese (52936)": 52936,
|
||||||
"Simplified Chinese GB18030 (54936)": 54936,
|
"Simplified Chinese GB18030 (54936)": 54936,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const CHR_ENC_SIMPLE_LOOKUP = {};
|
||||||
|
export const CHR_ENC_SIMPLE_REVERSE_LOOKUP = {};
|
||||||
|
|
||||||
|
for (const name in CHR_ENC_CODE_PAGES) {
|
||||||
|
const simpleName = name.match(/(^.+)\([\d/]+\)$/)[1];
|
||||||
|
|
||||||
|
CHR_ENC_SIMPLE_LOOKUP[simpleName] = CHR_ENC_CODE_PAGES[name];
|
||||||
|
CHR_ENC_SIMPLE_REVERSE_LOOKUP[CHR_ENC_CODE_PAGES[name]] = simpleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the width of the character set for the given codepage.
|
||||||
|
* For example, UTF-8 is a Single Byte Character Set, whereas
|
||||||
|
* UTF-16 is a Double Byte Character Set.
|
||||||
|
*
|
||||||
|
* @param {number} page - The codepage number
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function chrEncWidth(page) {
|
||||||
|
if (typeof page !== "number") return 0;
|
||||||
|
|
||||||
|
// Raw Bytes have a width of 1
|
||||||
|
if (page === 0) return 1;
|
||||||
|
|
||||||
|
const pageStr = page.toString();
|
||||||
|
// Confirm this page is legitimate
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(CHR_ENC_SIMPLE_REVERSE_LOOKUP, pageStr))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Statically defined code pages
|
||||||
|
if (Object.prototype.hasOwnProperty.call(cptable, pageStr))
|
||||||
|
return cptable[pageStr].dec.length > 256 ? 2 : 1;
|
||||||
|
|
||||||
|
// Cached code pages
|
||||||
|
if (cptable.utils.cache.sbcs.includes(pageStr))
|
||||||
|
return 1;
|
||||||
|
if (cptable.utils.cache.dbcs.includes(pageStr))
|
||||||
|
return 2;
|
||||||
|
|
||||||
|
// Dynamically generated code pages
|
||||||
|
if (Object.prototype.hasOwnProperty.call(cptable.utils.magic, pageStr)) {
|
||||||
|
// Generate a single character and measure it
|
||||||
|
const a = cptable.utils.encode(page, "a");
|
||||||
|
return a.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unicode Normalisation Forms
|
||||||
|
*
|
||||||
|
* @author Matthieu [m@tthieu.xyz]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
export const UNICODE_NORMALISATION_FORMS = ["NFD", "NFC", "NFKD", "NFKC"];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects whether the input buffer is valid UTF8.
|
||||||
|
*
|
||||||
|
* @param {ArrayBuffer} data
|
||||||
|
* @returns {number} - 0 = not UTF8, 1 = ASCII, 2 = UTF8
|
||||||
|
*/
|
||||||
|
export function isUTF8(data) {
|
||||||
|
const bytes = new Uint8Array(data);
|
||||||
|
let i = 0;
|
||||||
|
let onlyASCII = true;
|
||||||
|
while (i < bytes.length) {
|
||||||
|
if (( // ASCII
|
||||||
|
bytes[i] === 0x09 ||
|
||||||
|
bytes[i] === 0x0A ||
|
||||||
|
bytes[i] === 0x0D ||
|
||||||
|
(0x20 <= bytes[i] && bytes[i] <= 0x7E)
|
||||||
|
)) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
onlyASCII = false;
|
||||||
|
|
||||||
|
if (( // non-overlong 2-byte
|
||||||
|
(0xC2 <= bytes[i] && bytes[i] <= 0xDF) &&
|
||||||
|
(0x80 <= bytes[i+1] && bytes[i+1] <= 0xBF)
|
||||||
|
)) {
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( // excluding overlongs
|
||||||
|
bytes[i] === 0xE0 &&
|
||||||
|
(0xA0 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
|
||||||
|
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
|
||||||
|
) ||
|
||||||
|
( // straight 3-byte
|
||||||
|
((0xE1 <= bytes[i] && bytes[i] <= 0xEC) ||
|
||||||
|
bytes[i] === 0xEE ||
|
||||||
|
bytes[i] === 0xEF) &&
|
||||||
|
(0x80 <= bytes[i + 1] && bytes[i+1] <= 0xBF) &&
|
||||||
|
(0x80 <= bytes[i+2] && bytes[i+2] <= 0xBF)
|
||||||
|
) ||
|
||||||
|
( // excluding surrogates
|
||||||
|
bytes[i] === 0xED &&
|
||||||
|
(0x80 <= bytes[i+1] && bytes[i+1] <= 0x9F) &&
|
||||||
|
(0x80 <= bytes[i+2] && bytes[i+2] <= 0xBF)
|
||||||
|
)) {
|
||||||
|
i += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( // planes 1-3
|
||||||
|
bytes[i] === 0xF0 &&
|
||||||
|
(0x90 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
|
||||||
|
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
|
||||||
|
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
|
||||||
|
) ||
|
||||||
|
( // planes 4-15
|
||||||
|
(0xF1 <= bytes[i] && bytes[i] <= 0xF3) &&
|
||||||
|
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
|
||||||
|
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
|
||||||
|
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
|
||||||
|
) ||
|
||||||
|
( // plane 16
|
||||||
|
bytes[i] === 0xF4 &&
|
||||||
|
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x8F) &&
|
||||||
|
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
|
||||||
|
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
|
||||||
|
)) {
|
||||||
|
i += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return onlyASCII ? 1 : 2;
|
||||||
|
}
|
||||||
|
|||||||
34
src/core/lib/CipherSaber2.mjs
Normal file
34
src/core/lib/CipherSaber2.mjs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* @author n1073645 [n1073645@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2020
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
export function encode(tempIVP, key, rounds, input) {
|
||||||
|
const ivp = new Uint8Array([...key, ...tempIVP]);
|
||||||
|
const state = new Array(256).fill(0);
|
||||||
|
let j = 0, i = 0;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
// Mixing states based off of IV.
|
||||||
|
for (let i = 0; i < 256; i++)
|
||||||
|
state[i] = i;
|
||||||
|
const ivpLength = ivp.length;
|
||||||
|
for (let r = 0; r < rounds; r ++) {
|
||||||
|
for (let k = 0; k < 256; k++) {
|
||||||
|
j = (j + state[k] + ivp[k % ivpLength]) % 256;
|
||||||
|
[state[k], state[j]] = [state[j], state[k]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
j = 0;
|
||||||
|
i = 0;
|
||||||
|
|
||||||
|
// XOR cipher with key.
|
||||||
|
for (let x = 0; x < input.length; x++) {
|
||||||
|
i = (++i) % 256;
|
||||||
|
j = (j + state[i]) % 256;
|
||||||
|
[state[i], state[j]] = [state[j], state[i]];
|
||||||
|
const n = (state[i] + state[j]) % 256;
|
||||||
|
result.push(state[n] ^ input[x]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@ -3,13 +3,15 @@
|
|||||||
*
|
*
|
||||||
* @author Matt C [matt@artemisbot.uk]
|
* @author Matt C [matt@artemisbot.uk]
|
||||||
* @author n1474335 [n1474335@gmail.com]
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @author Evie H [evie@evie.sh]
|
||||||
*
|
*
|
||||||
* @copyright Crown Copyright 2018
|
* @copyright Crown Copyright 2018
|
||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import OperationError from "../errors/OperationError";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
import CryptoJS from "crypto-js";
|
import CryptoJS from "crypto-js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,6 +32,10 @@ export function affineEncode(input, args) {
|
|||||||
throw new OperationError("The values of a and b can only be integers.");
|
throw new OperationError("The values of a and b can only be integers.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Utils.gcd(a, 26) !== 1) {
|
||||||
|
throw new OperationError("The value of `a` must be coprime to 26.");
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < input.length; i++) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
if (alphabet.indexOf(input[i]) >= 0) {
|
if (alphabet.indexOf(input[i]) >= 0) {
|
||||||
// Uses the affine function ax+b % m = y (where m is length of the alphabet)
|
// Uses the affine function ax+b % m = y (where m is length of the alphabet)
|
||||||
|
|||||||
417
src/core/lib/Colossus.mjs
Normal file
417
src/core/lib/Colossus.mjs
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
/**
|
||||||
|
* Colossus - an emulation of the world's first electronic computer
|
||||||
|
*
|
||||||
|
* @author VirtualColossus [martin@virtualcolossus.co.uk]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
import {INIT_PATTERNS, ITA2_TABLE, ROTOR_SIZES} from "../lib/Lorenz.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colossus simulator class.
|
||||||
|
*/
|
||||||
|
export class ColossusComputer {
|
||||||
|
/**
|
||||||
|
* Construct a Colossus.
|
||||||
|
*
|
||||||
|
* @param {string} ciphertext
|
||||||
|
* @param {string} pattern - named pattern of Chi, Mu and Psi wheels
|
||||||
|
* @param {Object} qbusin - which data inputs are being sent to q bus - each can be null, plain or delta
|
||||||
|
* @param {Object[]} qbusswitches - Q bus calculation switches, multiple rows
|
||||||
|
* @param {Object} control - control switches which specify stepping modes
|
||||||
|
* @param {Object} starts - rotor start positions
|
||||||
|
*/
|
||||||
|
constructor(ciphertext, pattern, qbusin, qbusswitches, control, starts, settotal, limit) {
|
||||||
|
this.ITAlookup = ITA2_TABLE;
|
||||||
|
this.ReverseITAlookup = {};
|
||||||
|
for (const letter in this.ITAlookup) {
|
||||||
|
const code = this.ITAlookup[letter];
|
||||||
|
this.ReverseITAlookup[code] = letter;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initThyratrons(pattern);
|
||||||
|
|
||||||
|
this.ciphertext = ciphertext;
|
||||||
|
this.qbusin = qbusin;
|
||||||
|
this.qbusswitches = qbusswitches;
|
||||||
|
this.control = control;
|
||||||
|
this.starts = starts;
|
||||||
|
this.settotal = settotal;
|
||||||
|
this.limitations = limit;
|
||||||
|
|
||||||
|
|
||||||
|
this.allCounters = [0, 0, 0, 0, 0];
|
||||||
|
|
||||||
|
this.Zbits = [0, 0, 0, 0, 0]; // Z input is the cipher tape
|
||||||
|
this.ZbitsOneBack = [0, 0, 0, 0, 0]; // for delta
|
||||||
|
this.Qbits = [0, 0, 0, 0, 0]; // input generated for placing onto the Q-bus (the logic processor)
|
||||||
|
|
||||||
|
this.Xbits = [0, 0, 0, 0, 0]; // X is the Chi wheel bits
|
||||||
|
this.Xptr = [0, 0, 0, 0, 0]; // pointers to the current X bits (Chi wheels)
|
||||||
|
this.XbitsOneBack = [0, 0, 0, 0, 0]; // the X bits one back (for delta)
|
||||||
|
|
||||||
|
this.Sbits = [0, 0, 0, 0, 0]; // S is the Psi wheel bits
|
||||||
|
this.Sptr = [0, 0, 0, 0, 0]; // pointers to the current S bits (Psi wheels)
|
||||||
|
this.SbitsOneBack = [0, 0, 0, 0, 0]; // the S bits one back (for delta)
|
||||||
|
|
||||||
|
this.Mptr = [0, 0];
|
||||||
|
|
||||||
|
this.rotorPtrs = {};
|
||||||
|
|
||||||
|
this.totalmotor = 0;
|
||||||
|
this.P5Zbit = [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin a run
|
||||||
|
*
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
run() {
|
||||||
|
const result = {
|
||||||
|
printout: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
// loop until our start positions are back to the beginning
|
||||||
|
this.rotorPtrs = {X1: this.starts.X1, X2: this.starts.X2, X3: this.starts.X3, X4: this.starts.X4, X5: this.starts.X5, M61: this.starts.M61, M37: this.starts.M37, S1: this.starts.S1, S2: this.starts.S2, S3: this.starts.S3, S4: this.starts.S4, S5: this.starts.S5};
|
||||||
|
// this.rotorPtrs = this.starts;
|
||||||
|
let runcount = 1;
|
||||||
|
|
||||||
|
const fast = this.control.fast;
|
||||||
|
const slow = this.control.slow;
|
||||||
|
|
||||||
|
// Print Headers
|
||||||
|
result.printout += fast + " " + slow + "\n";
|
||||||
|
|
||||||
|
do {
|
||||||
|
this.allCounters = [0, 0, 0, 0, 0];
|
||||||
|
this.ZbitsOneBack = [0, 0, 0, 0, 0];
|
||||||
|
this.XbitsOneBack = [0, 0, 0, 0, 0];
|
||||||
|
|
||||||
|
// Run full tape loop and process counters
|
||||||
|
this.runTape();
|
||||||
|
|
||||||
|
// Only print result if larger than set total
|
||||||
|
let fastRef = "00";
|
||||||
|
let slowRef = "00";
|
||||||
|
if (fast !== "") fastRef = this.rotorPtrs[fast].toString().padStart(2, "0");
|
||||||
|
if (slow !== "") slowRef = this.rotorPtrs[slow].toString().padStart(2, "0");
|
||||||
|
let printline = "";
|
||||||
|
for (let c=0;c<5;c++) {
|
||||||
|
if (this.allCounters[c] > this.settotal) {
|
||||||
|
printline += String.fromCharCode(c+97) + this.allCounters[c]+" ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (printline !== "") {
|
||||||
|
result.printout += fastRef + " " + slowRef + " : ";
|
||||||
|
result.printout += printline;
|
||||||
|
result.printout += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step fast rotor if required
|
||||||
|
if (fast !== "") {
|
||||||
|
this.rotorPtrs[fast]++;
|
||||||
|
if (this.rotorPtrs[fast] > ROTOR_SIZES[fast]) this.rotorPtrs[fast] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step slow rotor if fast rotor has returned to initial start position
|
||||||
|
if (slow !== "" && this.rotorPtrs[fast] === this.starts[fast]) {
|
||||||
|
this.rotorPtrs[slow]++;
|
||||||
|
if (this.rotorPtrs[slow] > ROTOR_SIZES[slow]) this.rotorPtrs[slow] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
runcount++;
|
||||||
|
} while (JSON.stringify(this.rotorPtrs) !== JSON.stringify(this.starts));
|
||||||
|
|
||||||
|
result.counters = this.allCounters;
|
||||||
|
result.runcount = runcount;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run tape loop
|
||||||
|
*/
|
||||||
|
runTape() {
|
||||||
|
let charZin = "";
|
||||||
|
|
||||||
|
this.Xptr = [this.rotorPtrs.X1, this.rotorPtrs.X2, this.rotorPtrs.X3, this.rotorPtrs.X4, this.rotorPtrs.X5];
|
||||||
|
this.Mptr = [this.rotorPtrs.M37, this.rotorPtrs.M61];
|
||||||
|
this.Sptr = [this.rotorPtrs.S1, this.rotorPtrs.S2, this.rotorPtrs.S3, this.rotorPtrs.S4, this.rotorPtrs.S5];
|
||||||
|
|
||||||
|
// Run full loop of all character on the input tape (Z)
|
||||||
|
for (let i=0; i<this.ciphertext.length; i++) {
|
||||||
|
charZin = this.ciphertext.charAt(i);
|
||||||
|
|
||||||
|
// Firstly, we check what inputs are specified on the Q-bus input switches
|
||||||
|
this.getQbusInputs(charZin);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Pattern conditions on individual impulses. Matching patterns of bits on the Q bus.
|
||||||
|
* This is the top section on Colussus K rack - the Q bus programming switches
|
||||||
|
*/
|
||||||
|
const tmpcnt = this.runQbusProcessingConditional();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Addition of impulses.
|
||||||
|
* This is the bottom section of Colossus K rack.
|
||||||
|
*/
|
||||||
|
this.runQbusProcessingAddition(tmpcnt);
|
||||||
|
|
||||||
|
// Store Z bit impulse 5 two back required for P5 limitation
|
||||||
|
this.P5Zbit[1] = this.P5Zbit[0];
|
||||||
|
this.P5Zbit[0] = this.ITAlookup[charZin].split("")[4];
|
||||||
|
|
||||||
|
// Step rotors
|
||||||
|
this.stepThyratrons();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step thyratron rings to simulate movement of Lorenz rotors
|
||||||
|
* Chi rotors all step one per character
|
||||||
|
* Motor M61 rotor steps one per character, M37 steps dependant on M61 setting
|
||||||
|
* Psi rotors only step dependant on M37 setting + limitation
|
||||||
|
*/
|
||||||
|
stepThyratrons() {
|
||||||
|
let X2bPtr = this.Xptr[1]-1;
|
||||||
|
if (X2bPtr===0) X2bPtr = ROTOR_SIZES.X2;
|
||||||
|
let S1bPtr = this.Sptr[0]-1;
|
||||||
|
if (S1bPtr===0) S1bPtr = ROTOR_SIZES.S1;
|
||||||
|
|
||||||
|
// Get Chi rotor 5 two back to calculate plaintext (Z+Chi+Psi=Plain)
|
||||||
|
let X5bPtr=this.Xptr[4]-1;
|
||||||
|
if (X5bPtr===0) X5bPtr=ROTOR_SIZES.X5;
|
||||||
|
X5bPtr=X5bPtr-1;
|
||||||
|
if (X5bPtr===0) X5bPtr=ROTOR_SIZES.X5;
|
||||||
|
// Get Psi rotor 5 two back to calculate plaintext (Z+Chi+Psi=Plain)
|
||||||
|
let S5bPtr=this.Sptr[4]-1;
|
||||||
|
if (S5bPtr===0) S5bPtr=ROTOR_SIZES.S5;
|
||||||
|
S5bPtr=S5bPtr-1;
|
||||||
|
if (S5bPtr===0) S5bPtr=ROTOR_SIZES.S5;
|
||||||
|
|
||||||
|
const x2sw = this.limitations.X2;
|
||||||
|
const s1sw = this.limitations.S1;
|
||||||
|
const p5sw = this.limitations.P5;
|
||||||
|
|
||||||
|
// Limitation calculations
|
||||||
|
let lim=1;
|
||||||
|
if (x2sw) lim = this.rings.X[2][X2bPtr-1];
|
||||||
|
if (s1sw) lim = lim ^ this.rings.S[1][S1bPtr-1];
|
||||||
|
|
||||||
|
// P5
|
||||||
|
if (p5sw) {
|
||||||
|
let p5lim = this.P5Zbit[1];
|
||||||
|
p5lim = p5lim ^ this.rings.X[5][X5bPtr-1];
|
||||||
|
p5lim = p5lim ^ this.rings.S[5][S5bPtr-1];
|
||||||
|
lim = lim ^ p5lim;
|
||||||
|
}
|
||||||
|
|
||||||
|
const basicmotor = this.rings.M[2][this.Mptr[0]-1];
|
||||||
|
this.totalmotor = basicmotor;
|
||||||
|
|
||||||
|
if (x2sw || s1sw) {
|
||||||
|
if (basicmotor===0 && lim===1) {
|
||||||
|
this.totalmotor = 0;
|
||||||
|
} else {
|
||||||
|
this.totalmotor = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step Chi rotors
|
||||||
|
for (let r=0; r<5; r++) {
|
||||||
|
this.Xptr[r]++;
|
||||||
|
if (this.Xptr[r] > ROTOR_SIZES["X"+(r+1)]) this.Xptr[r] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.totalmotor) {
|
||||||
|
// Step Psi rotors
|
||||||
|
for (let r=0; r<5; r++) {
|
||||||
|
this.Sptr[r]++;
|
||||||
|
if (this.Sptr[r] > ROTOR_SIZES["S"+(r+1)]) this.Sptr[r] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move M37 rotor if M61 set
|
||||||
|
if (this.rings.M[1][this.Mptr[1]-1]===1) this.Mptr[0]++;
|
||||||
|
if (this.Mptr[0] > ROTOR_SIZES.M37) this.Mptr[0]=1;
|
||||||
|
|
||||||
|
// Always move M61 rotor
|
||||||
|
this.Mptr[1]++;
|
||||||
|
if (this.Mptr[1] > ROTOR_SIZES.M61) this.Mptr[1]=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Q bus inputs
|
||||||
|
*/
|
||||||
|
getQbusInputs(charZin) {
|
||||||
|
// Zbits - the bits from the current character from the cipher tape.
|
||||||
|
this.Zbits = this.ITAlookup[charZin].split("");
|
||||||
|
if (this.qbusin.Z === "Z") {
|
||||||
|
// direct Z
|
||||||
|
this.Qbits = this.Zbits;
|
||||||
|
} else if (this.qbusin.Z === "ΔZ") {
|
||||||
|
// delta Z, the Bitwise XOR of this character Zbits + last character Zbits
|
||||||
|
for (let b=0;b<5;b++) {
|
||||||
|
this.Qbits[b] = this.Zbits[b] ^ this.ZbitsOneBack[b];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.ZbitsOneBack = this.Zbits.slice(); // copy value of object, not reference
|
||||||
|
|
||||||
|
// Xbits - the current Chi wheel bits
|
||||||
|
for (let b=0;b<5;b++) {
|
||||||
|
this.Xbits[b] = this.rings.X[b+1][this.Xptr[b]-1];
|
||||||
|
}
|
||||||
|
if (this.qbusin.Chi !== "") {
|
||||||
|
if (this.qbusin.Chi === "Χ") {
|
||||||
|
// direct X added to Qbits
|
||||||
|
for (let b=0;b<5;b++) {
|
||||||
|
this.Qbits[b] = this.Qbits[b] ^ this.Xbits[b];
|
||||||
|
}
|
||||||
|
} else if (this.qbusin.Chi === "ΔΧ") {
|
||||||
|
// delta X
|
||||||
|
for (let b=0;b<5;b++) {
|
||||||
|
this.Qbits[b] = this.Qbits[b] ^ this.Xbits[b];
|
||||||
|
this.Qbits[b] = this.Qbits[b] ^ this.XbitsOneBack[b];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.XbitsOneBack = this.Xbits.slice();
|
||||||
|
|
||||||
|
// Sbits - the current Psi wheel bits
|
||||||
|
for (let b=0;b<5;b++) {
|
||||||
|
this.Sbits[b] = this.rings.S[b+1][this.Sptr[b]-1];
|
||||||
|
}
|
||||||
|
if (this.qbusin.Psi !== "") {
|
||||||
|
if (this.qbusin.Psi === "Ψ") {
|
||||||
|
// direct S added to Qbits
|
||||||
|
for (let b=0;b<5;b++) {
|
||||||
|
this.Qbits[b] = this.Qbits[b] ^ this.Sbits[b];
|
||||||
|
}
|
||||||
|
} else if (this.qbusin.Psi === "ΔΨ") {
|
||||||
|
// delta S
|
||||||
|
for (let b=0;b<5;b++) {
|
||||||
|
this.Qbits[b] = this.Qbits[b] ^ this.Sbits[b];
|
||||||
|
this.Qbits[b] = this.Qbits[b] ^ this.SbitsOneBack[b];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.SbitsOneBack = this.Sbits.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditional impulse Q bus section
|
||||||
|
*/
|
||||||
|
runQbusProcessingConditional() {
|
||||||
|
const cnt = [-1, -1, -1, -1, -1];
|
||||||
|
const numrows = this.qbusswitches.condition.length;
|
||||||
|
|
||||||
|
for (let r=0;r<numrows;r++) {
|
||||||
|
const row = this.qbusswitches.condition[r];
|
||||||
|
if (row.Counter !== "") {
|
||||||
|
let result = true;
|
||||||
|
const cPnt = row.Counter-1;
|
||||||
|
const Qswitch = this.readBusSwitches(row.Qswitches);
|
||||||
|
// Match switches to bit pattern
|
||||||
|
for (let s=0;s<5;s++) {
|
||||||
|
if (Qswitch[s] >= 0 && Qswitch[s] !== parseInt(this.Qbits[s], 10)) result = false;
|
||||||
|
}
|
||||||
|
// Check for NOT switch
|
||||||
|
if (row.Negate) result = !result;
|
||||||
|
|
||||||
|
// AND each row to get final result
|
||||||
|
if (cnt[cPnt] === -1) {
|
||||||
|
cnt[cPnt] = result;
|
||||||
|
} else if (!result) {
|
||||||
|
cnt[cPnt] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negate the whole column, this allows A OR B by doing NOT(NOT A AND NOT B)
|
||||||
|
for (let c=0;c<5;c++) {
|
||||||
|
if (this.qbusswitches.condNegateAll && cnt[c] !== -1) cnt[c] = !cnt[c];
|
||||||
|
}
|
||||||
|
|
||||||
|
return cnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Addition of impulses Q bus section
|
||||||
|
*/
|
||||||
|
runQbusProcessingAddition(cnt) {
|
||||||
|
const row = this.qbusswitches.addition[0];
|
||||||
|
const Qswitch = row.Qswitches.slice();
|
||||||
|
|
||||||
|
// To save making the arguments of this operation any larger, limiting addition counter to first one only
|
||||||
|
// Colossus could actually add into any of the five counters.
|
||||||
|
if (row.C1) {
|
||||||
|
let addition = 0;
|
||||||
|
for (let s=0;s<5;s++) {
|
||||||
|
// XOR addition
|
||||||
|
if (Qswitch[s]) {
|
||||||
|
addition = addition ^ this.Qbits[s];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const equals = (row.Equals===""?-1:(row.Equals==="."?0:1));
|
||||||
|
if (addition === equals) {
|
||||||
|
// AND with conditional rows to get final result
|
||||||
|
if (cnt[0] === -1) cnt[0] = true;
|
||||||
|
} else {
|
||||||
|
cnt[0] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final check, check for addition section negate
|
||||||
|
// then, if any column set, from top to bottom of rack, add to counter.
|
||||||
|
for (let c=0;c<5;c++) {
|
||||||
|
if (this.qbusswitches.addNegateAll && cnt[c] !== -1) cnt[c] = !cnt[c];
|
||||||
|
|
||||||
|
if (this.qbusswitches.totalMotor === "" || (this.qbusswitches.totalMotor === "x" && this.totalmotor === 0) || (this.qbusswitches.totalMotor === "." && this.totalmotor === 1)) {
|
||||||
|
if (cnt[c] === true) this.allCounters[c]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise thyratron rings
|
||||||
|
* These hold the pattern of 1s & 0s for each rotor on banks of thyraton GT1C valves which act as a one-bit store.
|
||||||
|
*/
|
||||||
|
initThyratrons(pattern) {
|
||||||
|
this.rings = {
|
||||||
|
X: {
|
||||||
|
1: INIT_PATTERNS[pattern].X[1].slice().reverse(),
|
||||||
|
2: INIT_PATTERNS[pattern].X[2].slice().reverse(),
|
||||||
|
3: INIT_PATTERNS[pattern].X[3].slice().reverse(),
|
||||||
|
4: INIT_PATTERNS[pattern].X[4].slice().reverse(),
|
||||||
|
5: INIT_PATTERNS[pattern].X[5].slice().reverse()
|
||||||
|
},
|
||||||
|
M: {
|
||||||
|
1: INIT_PATTERNS[pattern].M[1].slice().reverse(),
|
||||||
|
2: INIT_PATTERNS[pattern].M[2].slice().reverse(),
|
||||||
|
},
|
||||||
|
S: {
|
||||||
|
1: INIT_PATTERNS[pattern].S[1].slice().reverse(),
|
||||||
|
2: INIT_PATTERNS[pattern].S[2].slice().reverse(),
|
||||||
|
3: INIT_PATTERNS[pattern].S[3].slice().reverse(),
|
||||||
|
4: INIT_PATTERNS[pattern].S[4].slice().reverse(),
|
||||||
|
5: INIT_PATTERNS[pattern].S[5].slice().reverse()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read argument bus switches X & . and convert to 1 & 0
|
||||||
|
*/
|
||||||
|
readBusSwitches(row) {
|
||||||
|
const output = [-1, -1, -1, -1, -1];
|
||||||
|
for (let c=0;c<5;c++) {
|
||||||
|
if (row[c]===".") output[c] = 0;
|
||||||
|
if (row[c]==="x") output[c] = 1;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
668
src/core/lib/ConvertCoordinates.mjs
Normal file
668
src/core/lib/ConvertCoordinates.mjs
Normal file
@ -0,0 +1,668 @@
|
|||||||
|
/**
|
||||||
|
* Co-ordinate conversion resources.
|
||||||
|
*
|
||||||
|
* @author j433866 [j433866@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import geohash from "ngeohash";
|
||||||
|
/*
|
||||||
|
Currently unable to update to geodesy v2 as we cannot load .js modules into a .mjs file.
|
||||||
|
When we do update, imports will look like this:
|
||||||
|
|
||||||
|
import LatLonEllipsoidal from "geodesy/latlon-ellipsoidal.js";
|
||||||
|
import Mgrs from "geodesy/mgrs.js";
|
||||||
|
import OsGridRef from "geodesy/osgridref.js";
|
||||||
|
import Utm from "geodesy/utm.js";
|
||||||
|
*/
|
||||||
|
import geodesy from "geodesy";
|
||||||
|
const LatLonEllipsoidal = geodesy.LatLonEllipsoidal,
|
||||||
|
Mgrs = geodesy.Mgrs,
|
||||||
|
OsGridRef = geodesy.OsGridRef,
|
||||||
|
Utm = geodesy.Utm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Co-ordinate formats
|
||||||
|
*/
|
||||||
|
export const FORMATS = [
|
||||||
|
"Degrees Minutes Seconds",
|
||||||
|
"Degrees Decimal Minutes",
|
||||||
|
"Decimal Degrees",
|
||||||
|
"Geohash",
|
||||||
|
"Military Grid Reference System",
|
||||||
|
"Ordnance Survey National Grid",
|
||||||
|
"Universal Transverse Mercator"
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats that should be passed to the conversion module as-is
|
||||||
|
*/
|
||||||
|
const NO_CHANGE = [
|
||||||
|
"Geohash",
|
||||||
|
"Military Grid Reference System",
|
||||||
|
"Ordnance Survey National Grid",
|
||||||
|
"Universal Transverse Mercator",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a given latitude and longitude into a different format.
|
||||||
|
*
|
||||||
|
* @param {string} input - Input string to be converted
|
||||||
|
* @param {string} inFormat - Format of the input coordinates
|
||||||
|
* @param {string} inDelim - The delimiter splitting the lat/long of the input
|
||||||
|
* @param {string} outFormat - Format to convert to
|
||||||
|
* @param {string} outDelim - The delimiter to separate the output with
|
||||||
|
* @param {string} includeDir - Whether or not to include the compass direction in the output
|
||||||
|
* @param {number} precision - Precision of the result
|
||||||
|
* @returns {string} A formatted string of the converted co-ordinates
|
||||||
|
*/
|
||||||
|
export function convertCoordinates (input, inFormat, inDelim, outFormat, outDelim, includeDir, precision) {
|
||||||
|
let isPair = false,
|
||||||
|
split,
|
||||||
|
latlon,
|
||||||
|
convLat,
|
||||||
|
convLon,
|
||||||
|
conv,
|
||||||
|
hash,
|
||||||
|
utm,
|
||||||
|
mgrs,
|
||||||
|
osng,
|
||||||
|
splitLat,
|
||||||
|
splitLong,
|
||||||
|
lat,
|
||||||
|
lon;
|
||||||
|
|
||||||
|
// Can't have a precision less than 0!
|
||||||
|
if (precision < 0) {
|
||||||
|
precision = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inDelim === "Auto") {
|
||||||
|
// Try to detect a delimiter in the input.
|
||||||
|
inDelim = findDelim(input);
|
||||||
|
if (inDelim === null) {
|
||||||
|
throw new OperationError("Unable to detect the input delimiter automatically.");
|
||||||
|
}
|
||||||
|
} else if (!inDelim.includes("Direction")) {
|
||||||
|
// Convert the delimiter argument value to the actual character
|
||||||
|
inDelim = realDelim(inDelim);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inFormat === "Auto") {
|
||||||
|
// Try to detect the format of the input data
|
||||||
|
inFormat = findFormat(input, inDelim);
|
||||||
|
if (inFormat === null) {
|
||||||
|
throw new OperationError("Unable to detect the input format automatically.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the output delimiter argument to the real character
|
||||||
|
outDelim = realDelim(outDelim);
|
||||||
|
|
||||||
|
if (!NO_CHANGE.includes(inFormat)) {
|
||||||
|
if (inDelim.includes("Direction")) {
|
||||||
|
// Split on directions
|
||||||
|
split = input.split(/[NnEeSsWw]/g);
|
||||||
|
if (split[0] === "") {
|
||||||
|
// Remove first element if direction preceding
|
||||||
|
split = split.slice(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
split = input.split(inDelim);
|
||||||
|
}
|
||||||
|
// Replace any co-ordinate symbols with spaces so we can split on them later
|
||||||
|
for (let i = 0; i < split.length; i++) {
|
||||||
|
split[i] = split[i].replace(/[°˝´'"]/g, " ");
|
||||||
|
}
|
||||||
|
if (split.length > 1) {
|
||||||
|
isPair = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove any delimiters from the input
|
||||||
|
input = input.replace(inDelim, "");
|
||||||
|
isPair = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversions from the input format into a geodesy latlon object
|
||||||
|
switch (inFormat) {
|
||||||
|
case "Geohash":
|
||||||
|
hash = geohash.decode(input.replace(/[^A-Za-z0-9]/g, ""));
|
||||||
|
latlon = new LatLonEllipsoidal(hash.latitude, hash.longitude);
|
||||||
|
break;
|
||||||
|
case "Military Grid Reference System":
|
||||||
|
utm = Mgrs.parse(input.replace(/[^A-Za-z0-9]/g, "")).toUtm();
|
||||||
|
latlon = utm.toLatLonE();
|
||||||
|
break;
|
||||||
|
case "Ordnance Survey National Grid":
|
||||||
|
osng = OsGridRef.parse(input.replace(/[^A-Za-z0-9]/g, ""));
|
||||||
|
latlon = OsGridRef.osGridToLatLon(osng);
|
||||||
|
break;
|
||||||
|
case "Universal Transverse Mercator":
|
||||||
|
// Geodesy needs a space between the first 2 digits and the next letter
|
||||||
|
if (/^[\d]{2}[A-Za-z]/.test(input)) {
|
||||||
|
input = input.slice(0, 2) + " " + input.slice(2);
|
||||||
|
}
|
||||||
|
utm = Utm.parse(input);
|
||||||
|
latlon = utm.toLatLonE();
|
||||||
|
break;
|
||||||
|
case "Degrees Minutes Seconds":
|
||||||
|
if (isPair) {
|
||||||
|
// Split up the lat/long into degrees / minutes / seconds values
|
||||||
|
splitLat = splitInput(split[0]);
|
||||||
|
splitLong = splitInput(split[1]);
|
||||||
|
|
||||||
|
if (splitLat.length >= 3 && splitLong.length >= 3) {
|
||||||
|
lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2], 10);
|
||||||
|
lon = convDMSToDD(splitLong[0], splitLong[1], splitLong[2], 10);
|
||||||
|
latlon = new LatLonEllipsoidal(lat.degrees, lon.degrees);
|
||||||
|
} else {
|
||||||
|
throw new OperationError("Invalid co-ordinate format for Degrees Minutes Seconds");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not a pair, so only try to convert one set of co-ordinates
|
||||||
|
splitLat = splitInput(split[0]);
|
||||||
|
if (splitLat.length >= 3) {
|
||||||
|
lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2]);
|
||||||
|
latlon = new LatLonEllipsoidal(lat.degrees, lat.degrees);
|
||||||
|
} else {
|
||||||
|
throw new OperationError("Invalid co-ordinate format for Degrees Minutes Seconds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Degrees Decimal Minutes":
|
||||||
|
if (isPair) {
|
||||||
|
splitLat = splitInput(split[0]);
|
||||||
|
splitLong = splitInput(split[1]);
|
||||||
|
if (splitLat.length !== 2 || splitLong.length !== 2) {
|
||||||
|
throw new OperationError("Invalid co-ordinate format for Degrees Decimal Minutes.");
|
||||||
|
}
|
||||||
|
// Convert to decimal degrees, and then convert to a geodesy object
|
||||||
|
lat = convDDMToDD(splitLat[0], splitLat[1], 10);
|
||||||
|
lon = convDDMToDD(splitLong[0], splitLong[1], 10);
|
||||||
|
latlon = new LatLonEllipsoidal(lat.degrees, lon.degrees);
|
||||||
|
} else {
|
||||||
|
// Not a pair, so only try to convert one set of co-ordinates
|
||||||
|
splitLat = splitInput(input);
|
||||||
|
if (splitLat.length !== 2) {
|
||||||
|
throw new OperationError("Invalid co-ordinate format for Degrees Decimal Minutes.");
|
||||||
|
}
|
||||||
|
lat = convDDMToDD(splitLat[0], splitLat[1], 10);
|
||||||
|
latlon = new LatLonEllipsoidal(lat.degrees, lat.degrees);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Decimal Degrees":
|
||||||
|
if (isPair) {
|
||||||
|
splitLat = splitInput(split[0]);
|
||||||
|
splitLong = splitInput(split[1]);
|
||||||
|
if (splitLat.length !== 1 || splitLong.length !== 1) {
|
||||||
|
throw new OperationError("Invalid co-ordinate format for Decimal Degrees.");
|
||||||
|
}
|
||||||
|
latlon = new LatLonEllipsoidal(splitLat[0], splitLong[0]);
|
||||||
|
} else {
|
||||||
|
// Not a pair, so only try to convert one set of co-ordinates
|
||||||
|
splitLat = splitInput(split[0]);
|
||||||
|
if (splitLat.length !== 1) {
|
||||||
|
throw new OperationError("Invalid co-ordinate format for Decimal Degrees.");
|
||||||
|
}
|
||||||
|
latlon = new LatLonEllipsoidal(splitLat[0], splitLat[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new OperationError(`Unknown input format '${inFormat}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything is now a geodesy latlon object
|
||||||
|
// These store the latitude and longitude as decimal
|
||||||
|
if (inFormat.includes("Degrees")) {
|
||||||
|
// If the input string contains directions, we need to check if they're S or W.
|
||||||
|
// If either of the directions are, we should make the decimal value negative
|
||||||
|
const dirs = input.toUpperCase().match(/[NESW]/g);
|
||||||
|
if (dirs && dirs.length >= 1) {
|
||||||
|
// Make positive lat/lon values with S/W directions into negative values
|
||||||
|
if (dirs[0] === "S" || dirs[0] === "W" && latlon.lat > 0) {
|
||||||
|
latlon.lat = -latlon.lat;
|
||||||
|
}
|
||||||
|
if (dirs.length >= 2) {
|
||||||
|
if (dirs[1] === "S" || dirs[1] === "W" && latlon.lon > 0) {
|
||||||
|
latlon.lon = -latlon.lon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the compass directions of the lat and long
|
||||||
|
const [latDir, longDir] = findDirs(latlon.lat + "," + latlon.lon, ",");
|
||||||
|
|
||||||
|
// Output conversions for each output format
|
||||||
|
switch (outFormat) {
|
||||||
|
case "Decimal Degrees":
|
||||||
|
// We could use the built in latlon.toString(),
|
||||||
|
// but this makes adjusting the output harder
|
||||||
|
lat = convDDToDD(latlon.lat, precision);
|
||||||
|
lon = convDDToDD(latlon.lon, precision);
|
||||||
|
convLat = lat.string;
|
||||||
|
convLon = lon.string;
|
||||||
|
break;
|
||||||
|
case "Degrees Decimal Minutes":
|
||||||
|
lat = convDDToDDM(latlon.lat, precision);
|
||||||
|
lon = convDDToDDM(latlon.lon, precision);
|
||||||
|
convLat = lat.string;
|
||||||
|
convLon = lon.string;
|
||||||
|
break;
|
||||||
|
case "Degrees Minutes Seconds":
|
||||||
|
lat = convDDToDMS(latlon.lat, precision);
|
||||||
|
lon = convDDToDMS(latlon.lon, precision);
|
||||||
|
convLat = lat.string;
|
||||||
|
convLon = lon.string;
|
||||||
|
break;
|
||||||
|
case "Geohash":
|
||||||
|
convLat = geohash.encode(latlon.lat, latlon.lon, precision);
|
||||||
|
break;
|
||||||
|
case "Military Grid Reference System":
|
||||||
|
utm = latlon.toUtm();
|
||||||
|
mgrs = utm.toMgrs();
|
||||||
|
// MGRS wants a precision that's an even number between 2 and 10
|
||||||
|
if (precision % 2 !== 0) {
|
||||||
|
precision = precision + 1;
|
||||||
|
}
|
||||||
|
if (precision > 10) {
|
||||||
|
precision = 10;
|
||||||
|
}
|
||||||
|
convLat = mgrs.toString(precision);
|
||||||
|
break;
|
||||||
|
case "Ordnance Survey National Grid":
|
||||||
|
osng = OsGridRef.latLonToOsGrid(latlon);
|
||||||
|
if (osng.toString() === "") {
|
||||||
|
throw new OperationError("Could not convert co-ordinates to OS National Grid. Are the co-ordinates in range?");
|
||||||
|
}
|
||||||
|
// OSNG wants a precision that's an even number between 2 and 10
|
||||||
|
if (precision % 2 !== 0) {
|
||||||
|
precision = precision + 1;
|
||||||
|
}
|
||||||
|
if (precision > 10) {
|
||||||
|
precision = 10;
|
||||||
|
}
|
||||||
|
convLat = osng.toString(precision);
|
||||||
|
break;
|
||||||
|
case "Universal Transverse Mercator":
|
||||||
|
utm = latlon.toUtm();
|
||||||
|
convLat = utm.toString(precision);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convLat === undefined) {
|
||||||
|
throw new OperationError("Error converting co-ordinates.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outFormat.includes("Degrees")) {
|
||||||
|
// Format DD/DDM/DMS for output
|
||||||
|
// If we're outputting a compass direction, remove the negative sign
|
||||||
|
if (latDir === "S" && includeDir !== "None") {
|
||||||
|
convLat = convLat.replace("-", "");
|
||||||
|
}
|
||||||
|
if (longDir === "W" && includeDir !== "None") {
|
||||||
|
convLon = convLon.replace("-", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
let outConv = "";
|
||||||
|
if (includeDir === "Before") {
|
||||||
|
outConv += latDir + " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
outConv += convLat;
|
||||||
|
if (includeDir === "After") {
|
||||||
|
outConv += " " + latDir;
|
||||||
|
}
|
||||||
|
outConv += outDelim;
|
||||||
|
if (isPair) {
|
||||||
|
if (includeDir === "Before") {
|
||||||
|
outConv += longDir + " ";
|
||||||
|
}
|
||||||
|
outConv += convLon;
|
||||||
|
if (includeDir === "After") {
|
||||||
|
outConv += " " + longDir;
|
||||||
|
}
|
||||||
|
outConv += outDelim;
|
||||||
|
}
|
||||||
|
conv = outConv;
|
||||||
|
} else {
|
||||||
|
conv = convLat + outDelim;
|
||||||
|
}
|
||||||
|
|
||||||
|
return conv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split up the input using a space or degrees signs, and sanitise the result
|
||||||
|
*
|
||||||
|
* @param {string} input - The input data to be split
|
||||||
|
* @returns {number[]} An array of the different items in the string, stored as floats
|
||||||
|
*/
|
||||||
|
function splitInput (input) {
|
||||||
|
const split = [];
|
||||||
|
|
||||||
|
input.split(/\s+/).forEach(item => {
|
||||||
|
// Remove any character that isn't a digit, decimal point or negative sign
|
||||||
|
item = item.replace(/[^0-9.-]/g, "");
|
||||||
|
if (item.length > 0) {
|
||||||
|
// Turn the item into a float
|
||||||
|
split.push(parseFloat(item));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return split;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Degrees Minutes Seconds to Decimal Degrees
|
||||||
|
*
|
||||||
|
* @param {number} degrees - The degrees of the input co-ordinates
|
||||||
|
* @param {number} minutes - The minutes of the input co-ordinates
|
||||||
|
* @param {number} seconds - The seconds of the input co-ordinates
|
||||||
|
* @param {number} precision - The precision the result should be rounded to
|
||||||
|
* @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string)
|
||||||
|
*/
|
||||||
|
function convDMSToDD (degrees, minutes, seconds, precision) {
|
||||||
|
const absDegrees = Math.abs(degrees);
|
||||||
|
let conv = absDegrees + (minutes / 60) + (seconds / 3600);
|
||||||
|
let outString = round(conv, precision) + "°";
|
||||||
|
if (isNegativeZero(degrees) || degrees < 0) {
|
||||||
|
conv = -conv;
|
||||||
|
outString = "-" + outString;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"degrees": conv,
|
||||||
|
"string": outString
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Decimal Degrees Minutes to Decimal Degrees
|
||||||
|
*
|
||||||
|
* @param {number} degrees - The input degrees to be converted
|
||||||
|
* @param {number} minutes - The input minutes to be converted
|
||||||
|
* @param {number} precision - The precision which the result should be rounded to
|
||||||
|
* @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string)
|
||||||
|
*/
|
||||||
|
function convDDMToDD (degrees, minutes, precision) {
|
||||||
|
const absDegrees = Math.abs(degrees);
|
||||||
|
let conv = absDegrees + minutes / 60;
|
||||||
|
let outString = round(conv, precision) + "°";
|
||||||
|
if (isNegativeZero(degrees) || degrees < 0) {
|
||||||
|
conv = -conv;
|
||||||
|
outString = "-" + outString;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"degrees": conv,
|
||||||
|
"string": outString
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Decimal Degrees to Decimal Degrees
|
||||||
|
*
|
||||||
|
* Doesn't affect the input, just puts it into an object
|
||||||
|
* @param {number} degrees - The input degrees to be converted
|
||||||
|
* @param {number} precision - The precision which the result should be rounded to
|
||||||
|
* @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string)
|
||||||
|
*/
|
||||||
|
function convDDToDD (degrees, precision) {
|
||||||
|
return {
|
||||||
|
"degrees": degrees,
|
||||||
|
"string": round(degrees, precision) + "°"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Decimal Degrees to Degrees Minutes Seconds
|
||||||
|
*
|
||||||
|
* @param {number} decDegrees - The input data to be converted
|
||||||
|
* @param {number} precision - The precision which the result should be rounded to
|
||||||
|
* @returns {{string: string, degrees: number, minutes: number, seconds: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes, .seconds), and a formatted string version (obj.string)
|
||||||
|
*/
|
||||||
|
function convDDToDMS (decDegrees, precision) {
|
||||||
|
const absDegrees = Math.abs(decDegrees);
|
||||||
|
let degrees = Math.floor(absDegrees);
|
||||||
|
const minutes = Math.floor(60 * (absDegrees - degrees)),
|
||||||
|
seconds = round(3600 * (absDegrees - degrees) - 60 * minutes, precision);
|
||||||
|
let outString = degrees + "° " + minutes + "' " + seconds + "\"";
|
||||||
|
if (isNegativeZero(decDegrees) || decDegrees < 0) {
|
||||||
|
degrees = -degrees;
|
||||||
|
outString = "-" + outString;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"degrees": degrees,
|
||||||
|
"minutes": minutes,
|
||||||
|
"seconds": seconds,
|
||||||
|
"string": outString
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Decimal Degrees to Degrees Decimal Minutes
|
||||||
|
*
|
||||||
|
* @param {number} decDegrees - The input degrees to be converted
|
||||||
|
* @param {number} precision - The precision the input data should be rounded to
|
||||||
|
* @returns {{string: string, degrees: number, minutes: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes), and a formatted string version (obj.string)
|
||||||
|
*/
|
||||||
|
function convDDToDDM (decDegrees, precision) {
|
||||||
|
const absDegrees = Math.abs(decDegrees);
|
||||||
|
let degrees = Math.floor(absDegrees);
|
||||||
|
const minutes = absDegrees - degrees,
|
||||||
|
decMinutes = round(minutes * 60, precision);
|
||||||
|
let outString = degrees + "° " + decMinutes + "'";
|
||||||
|
if (decDegrees < 0 || isNegativeZero(decDegrees)) {
|
||||||
|
degrees = -degrees;
|
||||||
|
outString = "-" + outString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"degrees": degrees,
|
||||||
|
"minutes": decMinutes,
|
||||||
|
"string": outString,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds and returns the compass directions in an input string
|
||||||
|
*
|
||||||
|
* @param {string} input - The input co-ordinates containing the direction
|
||||||
|
* @param {string} delim - The delimiter separating latitide and longitude
|
||||||
|
* @returns {string[]} String array containing the latitude and longitude directions
|
||||||
|
*/
|
||||||
|
export function findDirs(input, delim) {
|
||||||
|
const upperInput = input.toUpperCase();
|
||||||
|
const dirExp = new RegExp(/[NESW]/g);
|
||||||
|
|
||||||
|
const dirs = upperInput.match(dirExp);
|
||||||
|
|
||||||
|
if (dirs) {
|
||||||
|
// If there's actually compass directions
|
||||||
|
// in the input, use these to work out the direction
|
||||||
|
if (dirs.length <= 2 && dirs.length >= 1) {
|
||||||
|
return dirs.length === 2 ? [dirs[0], dirs[1]] : [dirs[0], ""];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing was returned, so guess the directions
|
||||||
|
let lat = upperInput,
|
||||||
|
long,
|
||||||
|
latDir = "",
|
||||||
|
longDir = "";
|
||||||
|
if (!delim.includes("Direction")) {
|
||||||
|
if (upperInput.includes(delim)) {
|
||||||
|
const split = upperInput.split(delim);
|
||||||
|
if (split.length >= 1) {
|
||||||
|
if (split[0] !== "") {
|
||||||
|
lat = split[0];
|
||||||
|
}
|
||||||
|
if (split.length >= 2 && split[1] !== "") {
|
||||||
|
long = split[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const split = upperInput.split(dirExp);
|
||||||
|
if (split.length > 1) {
|
||||||
|
lat = split[0] === "" ? split[1] : split[0];
|
||||||
|
if (split.length > 2 && split[2] !== "") {
|
||||||
|
long = split[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lat) {
|
||||||
|
lat = parseFloat(lat);
|
||||||
|
latDir = lat < 0 ? "S" : "N";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (long) {
|
||||||
|
long = parseFloat(long);
|
||||||
|
longDir = long < 0 ? "W" : "E";
|
||||||
|
}
|
||||||
|
|
||||||
|
return [latDir, longDir];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects the co-ordinate format of the input data
|
||||||
|
*
|
||||||
|
* @param {string} input - The input data whose format we need to detect
|
||||||
|
* @param {string} delim - The delimiter separating the data in input
|
||||||
|
* @returns {string} The input format
|
||||||
|
*/
|
||||||
|
export function findFormat (input, delim) {
|
||||||
|
let testData;
|
||||||
|
const mgrsPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]{1}\s?[A-HJ-NP-Z][A-HJ-NP-V]\s?[0-9\s]+/),
|
||||||
|
osngPattern = new RegExp(/^[A-HJ-Z]{2}\s+[0-9\s]+$/),
|
||||||
|
geohashPattern = new RegExp(/^[0123456789BCDEFGHJKMNPQRSTUVWXYZ]+$/),
|
||||||
|
utmPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]\s[0-9.]+\s?[0-9.]+$/),
|
||||||
|
degPattern = new RegExp(/[°'"]/g);
|
||||||
|
|
||||||
|
input = input.trim();
|
||||||
|
|
||||||
|
if (delim !== null && delim.includes("Direction")) {
|
||||||
|
const split = input.split(/[NnEeSsWw]/);
|
||||||
|
if (split.length > 1) {
|
||||||
|
testData = split[0] === "" ? split[1] : split[0];
|
||||||
|
}
|
||||||
|
} else if (delim !== null && delim !== "") {
|
||||||
|
if (input.includes(delim)) {
|
||||||
|
const split = input.split(delim);
|
||||||
|
if (split.length > 1) {
|
||||||
|
testData = split[0] === "" ? split[1] : split[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
testData = input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test non-degrees formats
|
||||||
|
if (!degPattern.test(input)) {
|
||||||
|
const filteredInput = input.toUpperCase().replace(delim, "");
|
||||||
|
|
||||||
|
if (utmPattern.test(filteredInput)) {
|
||||||
|
return "Universal Transverse Mercator";
|
||||||
|
}
|
||||||
|
if (mgrsPattern.test(filteredInput)) {
|
||||||
|
return "Military Grid Reference System";
|
||||||
|
}
|
||||||
|
if (osngPattern.test(filteredInput)) {
|
||||||
|
return "Ordnance Survey National Grid";
|
||||||
|
}
|
||||||
|
if (geohashPattern.test(filteredInput)) {
|
||||||
|
return "Geohash";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test DMS/DDM/DD formats
|
||||||
|
if (testData !== undefined) {
|
||||||
|
const split = splitInput(testData);
|
||||||
|
switch (split.length) {
|
||||||
|
case 3:
|
||||||
|
return "Degrees Minutes Seconds";
|
||||||
|
case 2:
|
||||||
|
return "Degrees Decimal Minutes";
|
||||||
|
case 1:
|
||||||
|
return "Decimal Degrees";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically find the delimeter type from the given input
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @returns {string} Delimiter type
|
||||||
|
*/
|
||||||
|
export function findDelim (input) {
|
||||||
|
input = input.trim();
|
||||||
|
const delims = [",", ";", ":"];
|
||||||
|
const testDir = input.match(/[NnEeSsWw]/g);
|
||||||
|
if (testDir !== null && testDir.length > 0 && testDir.length < 3) {
|
||||||
|
// Possibly contains a direction
|
||||||
|
const splitInput = input.split(/[NnEeSsWw]/);
|
||||||
|
if (splitInput.length <= 3 && splitInput.length > 0) {
|
||||||
|
// If there's 3 splits (one should be empty), then assume we have directions
|
||||||
|
if (splitInput[0] === "") {
|
||||||
|
return "Direction Preceding";
|
||||||
|
} else if (splitInput[splitInput.length - 1] === "") {
|
||||||
|
return "Direction Following";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through the standard delimiters, and try to find them in the input
|
||||||
|
for (let i = 0; i < delims.length; i++) {
|
||||||
|
const delim = delims[i];
|
||||||
|
if (input.includes(delim)) {
|
||||||
|
const splitInput = input.split(delim);
|
||||||
|
if (splitInput.length <= 3 && splitInput.length > 0) {
|
||||||
|
// Don't want to try and convert more than 2 co-ordinates
|
||||||
|
return delim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the real string for a delimiter name.
|
||||||
|
*
|
||||||
|
* @param {string} delim The delimiter to be matched
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function realDelim (delim) {
|
||||||
|
return {
|
||||||
|
"Auto": "Auto",
|
||||||
|
"Space": " ",
|
||||||
|
"\\n": "\n",
|
||||||
|
"Comma": ",",
|
||||||
|
"Semi-colon": ";",
|
||||||
|
"Colon": ":"
|
||||||
|
}[delim];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a zero is negative
|
||||||
|
*
|
||||||
|
* @param {number} zero
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isNegativeZero(zero) {
|
||||||
|
return zero === 0 && (1/zero < 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds a number to a specified number of decimal places
|
||||||
|
*
|
||||||
|
* @param {number} input - The number to be rounded
|
||||||
|
* @param {precision} precision - The number of decimal places the number should be rounded to
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function round(input, precision) {
|
||||||
|
precision = Math.pow(10, precision);
|
||||||
|
return Math.round(input * precision) / precision;
|
||||||
|
}
|
||||||
9
src/core/lib/Crypt.mjs
Normal file
9
src/core/lib/Crypt.mjs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Crypt resources.
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2021
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const cryptNotice = "WARNING: Cryptographic operations in CyberChef should not be relied upon to provide security in any situation. No guarantee is offered for their correctness. We advise you not to use keys generated from CyberChef in operational contexts.";
|
||||||
@ -6,7 +6,7 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Utils from "../Utils";
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,12 +24,12 @@ import Utils from "../Utils";
|
|||||||
* fromDecimal("10:20:30", "Colon");
|
* fromDecimal("10:20:30", "Colon");
|
||||||
*/
|
*/
|
||||||
export function fromDecimal(data, delim="Auto") {
|
export function fromDecimal(data, delim="Auto") {
|
||||||
delim = Utils.charRep(delim);
|
const delimRegex = delim === "Auto" ? /[^\d-]+/ : Utils.regexRep(delim);
|
||||||
const output = [];
|
let byteStr = data.split(delimRegex);
|
||||||
let byteStr = data.split(delim);
|
|
||||||
if (byteStr[byteStr.length-1] === "")
|
|
||||||
byteStr = byteStr.slice(0, byteStr.length-1);
|
|
||||||
|
|
||||||
|
byteStr = byteStr.filter(str => str !== "");
|
||||||
|
|
||||||
|
const output = [];
|
||||||
for (let i = 0; i < byteStr.length; i++) {
|
for (let i = 0; i < byteStr.length; i++) {
|
||||||
output[i] = parseInt(byteStr[i], 10);
|
output[i] = parseInt(byteStr[i], 10);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export const WORD_DELIM_OPTIONS = ["Line feed", "CRLF", "Forward slash", "Backsl
|
|||||||
export const INPUT_DELIM_OPTIONS = ["Line feed", "CRLF", "Space", "Comma", "Semi-colon", "Colon", "Nothing (separate chars)"];
|
export const INPUT_DELIM_OPTIONS = ["Line feed", "CRLF", "Space", "Comma", "Semi-colon", "Colon", "Nothing (separate chars)"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Armithmetic sequence delimiters
|
* Arithmetic sequence delimiters
|
||||||
*/
|
*/
|
||||||
export const ARITHMETIC_DELIM_OPTIONS = ["Line feed", "Space", "Comma", "Semi-colon", "Colon", "CRLF"];
|
export const ARITHMETIC_DELIM_OPTIONS = ["Line feed", "Space", "Comma", "Semi-colon", "Colon", "CRLF"];
|
||||||
|
|
||||||
@ -72,3 +72,12 @@ export const JOIN_DELIM_OPTIONS = [
|
|||||||
{name: "Nothing (join chars)", value: ""}
|
{name: "Nothing (join chars)", value: ""}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RGBA list delimiters.
|
||||||
|
*/
|
||||||
|
export const RGBA_DELIM_OPTIONS = [
|
||||||
|
{name: "Comma", value: ","},
|
||||||
|
{name: "Space", value: " "},
|
||||||
|
{name: "CRLF", value: "\\r\\n"},
|
||||||
|
{name: "Line Feed", value: "\n"}
|
||||||
|
];
|
||||||
|
|||||||
369
src/core/lib/Enigma.mjs
Normal file
369
src/core/lib/Enigma.mjs
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
/**
|
||||||
|
* Emulation of the Enigma machine.
|
||||||
|
*
|
||||||
|
* @author s2224834
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provided default Enigma rotor set.
|
||||||
|
* These are specified as a list of mappings from the letters A through Z in order, optionally
|
||||||
|
* followed by < and a list of letters at which the rotor steps.
|
||||||
|
*/
|
||||||
|
export const ROTORS = [
|
||||||
|
{name: "I", value: "EKMFLGDQVZNTOWYHXUSPAIBRCJ<R"},
|
||||||
|
{name: "II", value: "AJDKSIRUXBLHWTMCQGZNPYFVOE<F"},
|
||||||
|
{name: "III", value: "BDFHJLCPRTXVZNYEIWGAKMUSQO<W"},
|
||||||
|
{name: "IV", value: "ESOVPZJAYQUIRHXLNFTGKDCMWB<K"},
|
||||||
|
{name: "V", value: "VZBRGITYUPSDNHLXAWMJQOFECK<A"},
|
||||||
|
{name: "VI", value: "JPGVOUMFYQBENHZRDKASXLICTW<AN"},
|
||||||
|
{name: "VII", value: "NZJHGRCXMYSWBOUFAIVLPEKQDT<AN"},
|
||||||
|
{name: "VIII", value: "FKQHTLXOCBJSPDZRAMEWNIUYGV<AN"},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ROTORS_FOURTH = [
|
||||||
|
{name: "Beta", value: "LEYJVCNIXWPBQMDRTAKZGFUHOS"},
|
||||||
|
{name: "Gamma", value: "FSOKANUERHMBTIYCWLQPZXVGJD"},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provided default Enigma reflector set.
|
||||||
|
* These are specified as 13 space-separated transposed pairs covering every letter.
|
||||||
|
*/
|
||||||
|
export const REFLECTORS = [
|
||||||
|
{name: "B", value: "AY BR CU DH EQ FS GL IP JX KN MO TZ VW"},
|
||||||
|
{name: "C", value: "AF BV CP DJ EI GO HY KR LZ MX NW TQ SU"},
|
||||||
|
{name: "B Thin", value: "AE BN CK DQ FU GY HW IJ LO MP RX SZ TV"},
|
||||||
|
{name: "C Thin", value: "AR BD CO EJ FN GT HK IV LM PW QZ SX UY"},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a letter to a number in 0..25.
|
||||||
|
*
|
||||||
|
* @param {char} c
|
||||||
|
* @param {boolean} permissive - Case insensitive; don't throw errors on other chars.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function a2i(c, permissive=false) {
|
||||||
|
const i = Utils.ord(c);
|
||||||
|
if (i >= 65 && i <= 90) {
|
||||||
|
return i - 65;
|
||||||
|
}
|
||||||
|
if (permissive) {
|
||||||
|
// Allow case insensitivity
|
||||||
|
if (i >= 97 && i <= 122) {
|
||||||
|
return i - 97;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
throw new OperationError("a2i called on non-uppercase ASCII character");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a number in 0..25 to a letter.
|
||||||
|
*
|
||||||
|
* @param {number} i
|
||||||
|
* @returns {char}
|
||||||
|
*/
|
||||||
|
export function i2a(i) {
|
||||||
|
if (i >= 0 && i < 26) {
|
||||||
|
return Utils.chr(i+65);
|
||||||
|
}
|
||||||
|
throw new OperationError("i2a called on value outside 0..25");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A rotor in the Enigma machine.
|
||||||
|
*/
|
||||||
|
export class Rotor {
|
||||||
|
/**
|
||||||
|
* Rotor constructor.
|
||||||
|
*
|
||||||
|
* @param {string} wiring - A 26 character string of the wiring order.
|
||||||
|
* @param {string} steps - A 0..26 character string of stepping points.
|
||||||
|
* @param {char} ringSetting - The ring setting.
|
||||||
|
* @param {char} initialPosition - The initial position of the rotor.
|
||||||
|
*/
|
||||||
|
constructor(wiring, steps, ringSetting, initialPosition) {
|
||||||
|
if (!/^[A-Z]{26}$/.test(wiring)) {
|
||||||
|
throw new OperationError("Rotor wiring must be 26 unique uppercase letters");
|
||||||
|
}
|
||||||
|
if (!/^[A-Z]{0,26}$/.test(steps)) {
|
||||||
|
throw new OperationError("Rotor steps must be 0-26 unique uppercase letters");
|
||||||
|
}
|
||||||
|
if (!/^[A-Z]$/.test(ringSetting)) {
|
||||||
|
throw new OperationError("Rotor ring setting must be exactly one uppercase letter");
|
||||||
|
}
|
||||||
|
if (!/^[A-Z]$/.test(initialPosition)) {
|
||||||
|
throw new OperationError("Rotor initial position must be exactly one uppercase letter");
|
||||||
|
}
|
||||||
|
this.map = new Array(26);
|
||||||
|
this.revMap = new Array(26);
|
||||||
|
const uniq = {};
|
||||||
|
for (let i=0; i<LETTERS.length; i++) {
|
||||||
|
const a = a2i(LETTERS[i]);
|
||||||
|
const b = a2i(wiring[i]);
|
||||||
|
this.map[a] = b;
|
||||||
|
this.revMap[b] = a;
|
||||||
|
uniq[b] = true;
|
||||||
|
}
|
||||||
|
if (Object.keys(uniq).length !== LETTERS.length) {
|
||||||
|
throw new OperationError("Rotor wiring must have each letter exactly once");
|
||||||
|
}
|
||||||
|
const rs = a2i(ringSetting);
|
||||||
|
this.steps = new Set();
|
||||||
|
for (const x of steps) {
|
||||||
|
this.steps.add(Utils.mod(a2i(x) - rs, 26));
|
||||||
|
}
|
||||||
|
if (this.steps.size !== steps.length) {
|
||||||
|
// This isn't strictly fatal, but it's probably a mistake
|
||||||
|
throw new OperationError("Rotor steps must be unique");
|
||||||
|
}
|
||||||
|
this.pos = Utils.mod(a2i(initialPosition) - rs, 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step the rotor forward by one.
|
||||||
|
*/
|
||||||
|
step() {
|
||||||
|
this.pos = Utils.mod(this.pos + 1, 26);
|
||||||
|
return this.pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a character through this rotor forwards.
|
||||||
|
*
|
||||||
|
* @param {number} c - The character.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
transform(c) {
|
||||||
|
return Utils.mod(this.map[Utils.mod(c + this.pos, 26)] - this.pos, 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a character through this rotor backwards.
|
||||||
|
*
|
||||||
|
* @param {number} c - The character.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
revTransform(c) {
|
||||||
|
return Utils.mod(this.revMap[Utils.mod(c + this.pos, 26)] - this.pos, 26);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for plugboard and reflector (since these do effectively the same
|
||||||
|
* thing).
|
||||||
|
*/
|
||||||
|
class PairMapBase {
|
||||||
|
/**
|
||||||
|
* PairMapBase constructor.
|
||||||
|
*
|
||||||
|
* @param {string} pairs - A whitespace separated string of letter pairs to swap.
|
||||||
|
* @param {string} [name='PairMapBase'] - For errors, the name of this object.
|
||||||
|
*/
|
||||||
|
constructor(pairs, name="PairMapBase") {
|
||||||
|
// I've chosen to make whitespace significant here to make a) code and
|
||||||
|
// b) inputs easier to read
|
||||||
|
this.pairs = pairs;
|
||||||
|
this.map = {};
|
||||||
|
if (pairs === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pairs.split(/\s+/).forEach(pair => {
|
||||||
|
if (!/^[A-Z]{2}$/.test(pair)) {
|
||||||
|
throw new OperationError(name + " must be a whitespace-separated list of uppercase letter pairs");
|
||||||
|
}
|
||||||
|
const a = a2i(pair[0]), b = a2i(pair[1]);
|
||||||
|
if (a === b) {
|
||||||
|
// self-stecker
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.map, a)) {
|
||||||
|
throw new OperationError(`${name} connects ${pair[0]} more than once`);
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.map, b)) {
|
||||||
|
throw new OperationError(`${name} connects ${pair[1]} more than once`);
|
||||||
|
}
|
||||||
|
this.map[a] = b;
|
||||||
|
this.map[b] = a;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a character through this object.
|
||||||
|
* Returns other characters unchanged.
|
||||||
|
*
|
||||||
|
* @param {number} c - The character.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
transform(c) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(this.map, c)) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
return this.map[c];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for transform, to allow interchangeable use with rotors.
|
||||||
|
*
|
||||||
|
* @param {number} c - The character.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
revTransform(c) {
|
||||||
|
return this.transform(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reflector. PairMapBase but requires that all characters are accounted for.
|
||||||
|
*
|
||||||
|
* Includes a couple of optimisations on that basis.
|
||||||
|
*/
|
||||||
|
export class Reflector extends PairMapBase {
|
||||||
|
/**
|
||||||
|
* Reflector constructor. See PairMapBase.
|
||||||
|
* Additional restriction: every character must be accounted for.
|
||||||
|
*/
|
||||||
|
constructor(pairs) {
|
||||||
|
super(pairs, "Reflector");
|
||||||
|
const s = Object.keys(this.map).length;
|
||||||
|
if (s !== 26) {
|
||||||
|
throw new OperationError("Reflector must have exactly 13 pairs covering every letter");
|
||||||
|
}
|
||||||
|
const optMap = new Array(26);
|
||||||
|
for (const x of Object.keys(this.map)) {
|
||||||
|
optMap[x] = this.map[x];
|
||||||
|
}
|
||||||
|
this.map = optMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a character through this object.
|
||||||
|
*
|
||||||
|
* @param {number} c - The character.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
transform(c) {
|
||||||
|
return this.map[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugboard. Unmodified PairMapBase.
|
||||||
|
*/
|
||||||
|
export class Plugboard extends PairMapBase {
|
||||||
|
/**
|
||||||
|
* Plugboard constructor. See PairMapbase.
|
||||||
|
*/
|
||||||
|
constructor(pairs) {
|
||||||
|
super(pairs, "Plugboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for the Enigma machine itself. Holds rotors, a reflector, and a plugboard.
|
||||||
|
*/
|
||||||
|
export class EnigmaBase {
|
||||||
|
/**
|
||||||
|
* EnigmaBase constructor.
|
||||||
|
*
|
||||||
|
* @param {Object[]} rotors - List of Rotors.
|
||||||
|
* @param {Object} reflector - A Reflector.
|
||||||
|
* @param {Plugboard} plugboard - A Plugboard.
|
||||||
|
*/
|
||||||
|
constructor(rotors, reflector, plugboard) {
|
||||||
|
this.rotors = rotors;
|
||||||
|
this.rotorsRev = [].concat(rotors).reverse();
|
||||||
|
this.reflector = reflector;
|
||||||
|
this.plugboard = plugboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step the rotors forward by one.
|
||||||
|
*
|
||||||
|
* This happens before the output character is generated.
|
||||||
|
*
|
||||||
|
* Note that rotor 4, if it's there, never steps.
|
||||||
|
*
|
||||||
|
* Why is all the logic in EnigmaBase and not a nice neat method on
|
||||||
|
* Rotor that knows when it should advance the next item?
|
||||||
|
* Because the double stepping anomaly is a thing. tl;dr if the left rotor
|
||||||
|
* should step the next time the middle rotor steps, the middle rotor will
|
||||||
|
* immediately step.
|
||||||
|
*/
|
||||||
|
step() {
|
||||||
|
const r0 = this.rotors[0];
|
||||||
|
const r1 = this.rotors[1];
|
||||||
|
r0.step();
|
||||||
|
// The second test here is the double-stepping anomaly
|
||||||
|
if (r0.steps.has(r0.pos) || r1.steps.has(Utils.mod(r1.pos + 1, 26))) {
|
||||||
|
r1.step();
|
||||||
|
if (r1.steps.has(r1.pos)) {
|
||||||
|
const r2 = this.rotors[2];
|
||||||
|
r2.step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt (or decrypt) some data.
|
||||||
|
* Takes an arbitrary string and runs the Engima machine on that data from
|
||||||
|
* *its current state*, and outputs the result. Non-alphabetic characters
|
||||||
|
* are returned unchanged.
|
||||||
|
*
|
||||||
|
* @param {string} input - Data to encrypt.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
crypt(input) {
|
||||||
|
let result = "";
|
||||||
|
for (const c of input) {
|
||||||
|
let letter = a2i(c, true);
|
||||||
|
if (letter === -1) {
|
||||||
|
result += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// First, step the rotors forward.
|
||||||
|
this.step();
|
||||||
|
// Now, run through the plugboard.
|
||||||
|
letter = this.plugboard.transform(letter);
|
||||||
|
// Then through each wheel in sequence, through the reflector, and
|
||||||
|
// backwards through the wheels again.
|
||||||
|
for (const rotor of this.rotors) {
|
||||||
|
letter = rotor.transform(letter);
|
||||||
|
}
|
||||||
|
letter = this.reflector.transform(letter);
|
||||||
|
for (const rotor of this.rotorsRev) {
|
||||||
|
letter = rotor.revTransform(letter);
|
||||||
|
}
|
||||||
|
// Finally, back through the plugboard.
|
||||||
|
letter = this.plugboard.revTransform(letter);
|
||||||
|
result += i2a(letter);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Enigma machine itself. Holds 3-4 rotors, a reflector, and a plugboard.
|
||||||
|
*/
|
||||||
|
export class EnigmaMachine extends EnigmaBase {
|
||||||
|
/**
|
||||||
|
* EnigmaMachine constructor.
|
||||||
|
*
|
||||||
|
* @param {Object[]} rotors - List of Rotors.
|
||||||
|
* @param {Object} reflector - A Reflector.
|
||||||
|
* @param {Plugboard} plugboard - A Plugboard.
|
||||||
|
*/
|
||||||
|
constructor(rotors, reflector, plugboard) {
|
||||||
|
super(rotors, reflector, plugboard);
|
||||||
|
if (rotors.length !== 3 && rotors.length !== 4) {
|
||||||
|
throw new OperationError("Enigma must have 3 or 4 rotors");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,15 +12,15 @@
|
|||||||
*
|
*
|
||||||
* @param {string} input
|
* @param {string} input
|
||||||
* @param {RegExp} searchRegex
|
* @param {RegExp} searchRegex
|
||||||
* @param {RegExp} removeRegex - A regular expression defining results to remove from the
|
* @param {RegExp} [removeRegex=null] - A regular expression defining results to remove from the
|
||||||
* final list
|
* final list
|
||||||
* @param {boolean} includeTotal - Whether or not to include the total number of results
|
* @param {Function} [sortBy=null] - The sorting comparison function to apply
|
||||||
|
* @param {boolean} [unique=false] - Whether to unique the results
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function search (input, searchRegex, removeRegex, includeTotal) {
|
export function search(input, searchRegex, removeRegex=null, sortBy=null, unique=false) {
|
||||||
let output = "",
|
let results = [];
|
||||||
total = 0,
|
let match;
|
||||||
match;
|
|
||||||
|
|
||||||
while ((match = searchRegex.exec(input))) {
|
while ((match = searchRegex.exec(input))) {
|
||||||
// Moves pointer when an empty string is matched (prevents infinite loop)
|
// Moves pointer when an empty string is matched (prevents infinite loop)
|
||||||
@ -30,16 +30,26 @@ export function search (input, searchRegex, removeRegex, includeTotal) {
|
|||||||
|
|
||||||
if (removeRegex && removeRegex.test(match[0]))
|
if (removeRegex && removeRegex.test(match[0]))
|
||||||
continue;
|
continue;
|
||||||
total++;
|
|
||||||
output += match[0] + "\n";
|
results.push(match[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeTotal)
|
if (sortBy) {
|
||||||
output = "Total found: " + total + "\n\n" + output;
|
results = results.sort(sortBy);
|
||||||
|
}
|
||||||
|
|
||||||
return output;
|
if (unique) {
|
||||||
|
results = results.unique();
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email regular expression
|
||||||
|
*/
|
||||||
|
export const EMAIL_REGEX = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\])/ig;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL regular expression
|
* URL regular expression
|
||||||
@ -57,3 +67,9 @@ export const URL_REGEX = new RegExp(protocol + hostname + "(?:" + port + ")?(?:"
|
|||||||
* Domain name regular expression
|
* Domain name regular expression
|
||||||
*/
|
*/
|
||||||
export const DOMAIN_REGEX = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/ig;
|
export const DOMAIN_REGEX = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/ig;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMARC Domain name regular expression
|
||||||
|
*/
|
||||||
|
export const DMARC_DOMAIN_REGEX = /\b((?=[a-z0-9_-]{1,63}\.)(xn--)?[a-z0-9_]+(-[a-z0-9_]+)*\.)+[a-z]{2,63}\b/ig;
|
||||||
|
|||||||
4212
src/core/lib/FileSignatures.mjs
Normal file
4212
src/core/lib/FileSignatures.mjs
Normal file
File diff suppressed because it is too large
Load Diff
267
src/core/lib/FileType.mjs
Normal file
267
src/core/lib/FileType.mjs
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
/**
|
||||||
|
* File type functions
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2018
|
||||||
|
* @license Apache-2.0
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import {FILE_SIGNATURES} from "./FileSignatures.mjs";
|
||||||
|
import {sendStatusMessage} from "../Utils.mjs";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a signature matches a buffer.
|
||||||
|
*
|
||||||
|
* @param {Object|Object[]} sig - A dictionary of offsets with values assigned to them.
|
||||||
|
* These values can be numbers for static checks, arrays of potential valid matches,
|
||||||
|
* or bespoke functions to check the validity of the buffer value at that offset.
|
||||||
|
* @param {Uint8Array} buf
|
||||||
|
* @param {number} [offset=0] Where in the buffer to start searching from
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function signatureMatches(sig, buf, offset=0) {
|
||||||
|
// Using a length check seems to be more performant than `sig instanceof Array`
|
||||||
|
if (sig.length) {
|
||||||
|
// sig is an Array - return true if any of them match
|
||||||
|
// The following `reduce` method is nice, but performance matters here, so we
|
||||||
|
// opt for a faster, if less elegant, for loop.
|
||||||
|
// return sig.reduce((acc, s) => acc || bytesMatch(s, buf, offset), false);
|
||||||
|
for (let i = 0; i < sig.length; i++) {
|
||||||
|
if (bytesMatch(sig[i], buf, offset)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return bytesMatch(sig, buf, offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a set of bytes match the given buffer.
|
||||||
|
*
|
||||||
|
* @param {Object} sig - A dictionary of offsets with values assigned to them.
|
||||||
|
* These values can be numbers for static checks, arrays of potential valid matches,
|
||||||
|
* or bespoke functions to check the validity of the buffer value at that offset.
|
||||||
|
* @param {Uint8Array} buf
|
||||||
|
* @param {number} [offset=0] Where in the buffer to start searching from
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function bytesMatch(sig, buf, offset=0) {
|
||||||
|
for (const sigoffset in sig) {
|
||||||
|
const pos = parseInt(sigoffset, 10) + offset;
|
||||||
|
switch (typeof sig[sigoffset]) {
|
||||||
|
case "number": // Static check
|
||||||
|
if (buf[pos] !== sig[sigoffset])
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
case "object": // Array of options
|
||||||
|
if (sig[sigoffset].indexOf(buf[pos]) < 0)
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
case "function": // More complex calculation
|
||||||
|
if (!sig[sigoffset](buf[pos]))
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unrecognised signature type at offset ${sigoffset}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a buffer, detects magic byte sequences at specific positions and returns the
|
||||||
|
* extension and mime type.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array|ArrayBuffer} buf
|
||||||
|
* @param {string[]} [categories=All] - Which categories of file to look for
|
||||||
|
* @returns {Object[]} types
|
||||||
|
* @returns {string} type.name - Name of file type
|
||||||
|
* @returns {string} type.ext - File extension
|
||||||
|
* @returns {string} type.mime - Mime type
|
||||||
|
* @returns {string} [type.desc] - Description
|
||||||
|
*/
|
||||||
|
export function detectFileType(buf, categories=Object.keys(FILE_SIGNATURES)) {
|
||||||
|
if (buf instanceof ArrayBuffer) {
|
||||||
|
buf = new Uint8Array(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(buf && buf.length > 1)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingFiles = [];
|
||||||
|
const signatures = {};
|
||||||
|
|
||||||
|
for (const cat in FILE_SIGNATURES) {
|
||||||
|
if (categories.includes(cat)) {
|
||||||
|
signatures[cat] = FILE_SIGNATURES[cat];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cat in signatures) {
|
||||||
|
const category = signatures[cat];
|
||||||
|
|
||||||
|
category.forEach(filetype => {
|
||||||
|
if (signatureMatches(filetype.signature, buf)) {
|
||||||
|
matchingFiles.push(filetype);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return matchingFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a buffer, searches for magic byte sequences at all possible positions and returns
|
||||||
|
* the extensions and mime types.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} buf
|
||||||
|
* @param {string[]} [categories=All] - Which categories of file to look for
|
||||||
|
* @returns {Object[]} foundFiles
|
||||||
|
* @returns {number} foundFiles.offset - The position in the buffer at which this file was found
|
||||||
|
* @returns {Object} foundFiles.fileDetails
|
||||||
|
* @returns {string} foundFiles.fileDetails.name - Name of file type
|
||||||
|
* @returns {string} foundFiles.fileDetails.ext - File extension
|
||||||
|
* @returns {string} foundFiles.fileDetails.mime - Mime type
|
||||||
|
* @returns {string} [foundFiles.fileDetails.desc] - Description
|
||||||
|
*/
|
||||||
|
export function scanForFileTypes(buf, categories=Object.keys(FILE_SIGNATURES)) {
|
||||||
|
if (!(buf && buf.length > 1)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundFiles = [];
|
||||||
|
const signatures = {};
|
||||||
|
|
||||||
|
for (const cat in FILE_SIGNATURES) {
|
||||||
|
if (categories.includes(cat)) {
|
||||||
|
signatures[cat] = FILE_SIGNATURES[cat];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cat in signatures) {
|
||||||
|
const category = signatures[cat];
|
||||||
|
|
||||||
|
for (let i = 0; i < category.length; i++) {
|
||||||
|
const filetype = category[i];
|
||||||
|
const sigs = filetype.signature.length ? filetype.signature : [filetype.signature];
|
||||||
|
|
||||||
|
sigs.forEach(sig => {
|
||||||
|
let pos = 0;
|
||||||
|
while ((pos = locatePotentialSig(buf, sig, pos)) >= 0) {
|
||||||
|
if (bytesMatch(sig, buf, pos)) {
|
||||||
|
sendStatusMessage(`Found potential signature for ${filetype.name} at pos ${pos}`);
|
||||||
|
foundFiles.push({
|
||||||
|
offset: pos,
|
||||||
|
fileDetails: filetype
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return found files in order of increasing offset
|
||||||
|
return foundFiles.sort((a, b) => {
|
||||||
|
return a.offset - b.offset;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fastcheck function to quickly scan the buffer for the first byte in a signature.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} buf - The buffer to search
|
||||||
|
* @param {Object} sig - A single signature object (Not an array of signatures)
|
||||||
|
* @param {number} offset - Where to start search from
|
||||||
|
* @returns {number} The position of the match or -1 if one cannot be found.
|
||||||
|
*/
|
||||||
|
function locatePotentialSig(buf, sig, offset) {
|
||||||
|
// Find values for first key and value in sig
|
||||||
|
const k = parseInt(Object.keys(sig)[0], 10);
|
||||||
|
const v = Object.values(sig)[0];
|
||||||
|
switch (typeof v) {
|
||||||
|
case "number":
|
||||||
|
return buf.indexOf(v, offset + k) - k;
|
||||||
|
case "object":
|
||||||
|
for (let i = offset + k; i < buf.length; i++) {
|
||||||
|
if (v.indexOf(buf[i]) >= 0) return i - k;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
case "function":
|
||||||
|
for (let i = offset + k; i < buf.length; i++) {
|
||||||
|
if (v(buf[i])) return i - k;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
default:
|
||||||
|
throw new Error("Unrecognised signature type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects whether the given buffer is a file of the type specified.
|
||||||
|
*
|
||||||
|
* @param {string|RegExp} type
|
||||||
|
* @param {Uint8Array|ArrayBuffer} buf
|
||||||
|
* @returns {string|false} The mime type or false if the type does not match
|
||||||
|
*/
|
||||||
|
export function isType(type, buf) {
|
||||||
|
const types = detectFileType(buf);
|
||||||
|
|
||||||
|
if (!types.length) return false;
|
||||||
|
|
||||||
|
if (typeof type === "string") {
|
||||||
|
return types.reduce((acc, t) => {
|
||||||
|
const mime = t.mime.startsWith(type) ? t.mime : false;
|
||||||
|
return acc || mime;
|
||||||
|
}, false);
|
||||||
|
} else if (type instanceof RegExp) {
|
||||||
|
return types.reduce((acc, t) => {
|
||||||
|
const mime = type.test(t.mime) ? t.mime : false;
|
||||||
|
return acc || mime;
|
||||||
|
}, false);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid type input.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects whether the given buffer contains an image file.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array|ArrayBuffer} buf
|
||||||
|
* @returns {string|false} The mime type or false if the type does not match
|
||||||
|
*/
|
||||||
|
export function isImage(buf) {
|
||||||
|
return isType("image", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to extract a file from a data stream given its offset and extractor function.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @param {Object} fileDetail
|
||||||
|
* @param {string} fileDetail.mime
|
||||||
|
* @param {string} fileDetail.extension
|
||||||
|
* @param {Function} fileDetail.extractor
|
||||||
|
* @param {number} offset
|
||||||
|
* @returns {File}
|
||||||
|
*/
|
||||||
|
export function extractFile(bytes, fileDetail, offset) {
|
||||||
|
if (fileDetail.extractor) {
|
||||||
|
sendStatusMessage(`Attempting to extract ${fileDetail.name} at pos ${offset}...`);
|
||||||
|
const fileData = fileDetail.extractor(bytes, offset);
|
||||||
|
const ext = fileDetail.extension.split(",")[0];
|
||||||
|
return new File([fileData], `extracted_at_0x${offset.toString(16)}.${ext}`, {
|
||||||
|
type: fileDetail.mime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`No extraction algorithm available for "${fileDetail.mime}" files`);
|
||||||
|
}
|
||||||
253
src/core/lib/FuzzyMatch.mjs
Normal file
253
src/core/lib/FuzzyMatch.mjs
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
/**
|
||||||
|
* LICENSE
|
||||||
|
*
|
||||||
|
* This software is dual-licensed to the public domain and under the following
|
||||||
|
* license: you are granted a perpetual, irrevocable license to copy, modify,
|
||||||
|
* publish, and distribute this file as you see fit.
|
||||||
|
*
|
||||||
|
* VERSION
|
||||||
|
* 0.1.0 (2016-03-28) Initial release
|
||||||
|
*
|
||||||
|
* AUTHOR
|
||||||
|
* Forrest Smith
|
||||||
|
*
|
||||||
|
* CONTRIBUTORS
|
||||||
|
* J<EFBFBD>rgen Tjern<EFBFBD> - async helper
|
||||||
|
* Anurag Awasthi - updated to 0.2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DEFAULT_WEIGHTS = {
|
||||||
|
sequentialBonus: 15, // bonus for adjacent matches
|
||||||
|
separatorBonus: 30, // bonus if match occurs after a separator
|
||||||
|
camelBonus: 30, // bonus if match is uppercase and prev is lower
|
||||||
|
firstLetterBonus: 15, // bonus if the first letter is matched
|
||||||
|
|
||||||
|
leadingLetterPenalty: -5, // penalty applied for every letter in str before the first match
|
||||||
|
maxLeadingLetterPenalty: -15, // maximum penalty for leading letters
|
||||||
|
unmatchedLetterPenalty: -1
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does a fuzzy search to find pattern inside a string.
|
||||||
|
* @param {string} pattern pattern to search for
|
||||||
|
* @param {string} str string which is being searched
|
||||||
|
* @param {boolean} global whether to search for all matches or just one
|
||||||
|
* @returns [boolean, number] a boolean which tells if pattern was
|
||||||
|
* found or not and a search score
|
||||||
|
*/
|
||||||
|
export function fuzzyMatch(pattern, str, global=false, weights=DEFAULT_WEIGHTS) {
|
||||||
|
const recursionCount = 0;
|
||||||
|
const recursionLimit = 10;
|
||||||
|
const matches = [];
|
||||||
|
const maxMatches = 256;
|
||||||
|
|
||||||
|
if (!global) {
|
||||||
|
return fuzzyMatchRecursive(
|
||||||
|
pattern,
|
||||||
|
str,
|
||||||
|
0 /* patternCurIndex */,
|
||||||
|
0 /* strCurrIndex */,
|
||||||
|
null /* srcMatches */,
|
||||||
|
matches,
|
||||||
|
maxMatches,
|
||||||
|
0 /* nextMatch */,
|
||||||
|
recursionCount,
|
||||||
|
recursionLimit,
|
||||||
|
weights
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all matches
|
||||||
|
let foundMatch = true,
|
||||||
|
score,
|
||||||
|
idxs,
|
||||||
|
strCurrIndex = 0;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
while (foundMatch) {
|
||||||
|
[foundMatch, score, idxs] = fuzzyMatchRecursive(
|
||||||
|
pattern,
|
||||||
|
str,
|
||||||
|
0 /* patternCurIndex */,
|
||||||
|
strCurrIndex,
|
||||||
|
null /* srcMatches */,
|
||||||
|
matches,
|
||||||
|
maxMatches,
|
||||||
|
0 /* nextMatch */,
|
||||||
|
recursionCount,
|
||||||
|
recursionLimit,
|
||||||
|
weights
|
||||||
|
);
|
||||||
|
if (foundMatch) results.push([foundMatch, score, [...idxs]]);
|
||||||
|
strCurrIndex = idxs[idxs.length - 1] + 1;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursive helper function
|
||||||
|
*/
|
||||||
|
function fuzzyMatchRecursive(
|
||||||
|
pattern,
|
||||||
|
str,
|
||||||
|
patternCurIndex,
|
||||||
|
strCurrIndex,
|
||||||
|
srcMatches,
|
||||||
|
matches,
|
||||||
|
maxMatches,
|
||||||
|
nextMatch,
|
||||||
|
recursionCount,
|
||||||
|
recursionLimit,
|
||||||
|
weights
|
||||||
|
) {
|
||||||
|
let outScore = 0;
|
||||||
|
|
||||||
|
// Return if recursion limit is reached.
|
||||||
|
if (++recursionCount >= recursionLimit) {
|
||||||
|
return [false, outScore, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if we reached ends of strings.
|
||||||
|
if (patternCurIndex === pattern.length || strCurrIndex === str.length) {
|
||||||
|
return [false, outScore, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursion params
|
||||||
|
let recursiveMatch = false;
|
||||||
|
let bestRecursiveMatches = [];
|
||||||
|
let bestRecursiveScore = 0;
|
||||||
|
|
||||||
|
// Loop through pattern and str looking for a match.
|
||||||
|
let firstMatch = true;
|
||||||
|
while (patternCurIndex < pattern.length && strCurrIndex < str.length) {
|
||||||
|
// Match found.
|
||||||
|
if (
|
||||||
|
pattern[patternCurIndex].toLowerCase() === str[strCurrIndex].toLowerCase()
|
||||||
|
) {
|
||||||
|
if (nextMatch >= maxMatches) {
|
||||||
|
return [false, outScore, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstMatch && srcMatches) {
|
||||||
|
matches = [...srcMatches];
|
||||||
|
firstMatch = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [matched, recursiveScore, recursiveMatches] = fuzzyMatchRecursive(
|
||||||
|
pattern,
|
||||||
|
str,
|
||||||
|
patternCurIndex,
|
||||||
|
strCurrIndex + 1,
|
||||||
|
matches,
|
||||||
|
recursiveMatches,
|
||||||
|
maxMatches,
|
||||||
|
nextMatch,
|
||||||
|
recursionCount,
|
||||||
|
recursionLimit,
|
||||||
|
weights
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
// Pick best recursive score.
|
||||||
|
if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
|
||||||
|
bestRecursiveMatches = [...recursiveMatches];
|
||||||
|
bestRecursiveScore = recursiveScore;
|
||||||
|
}
|
||||||
|
recursiveMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches[nextMatch++] = strCurrIndex;
|
||||||
|
++patternCurIndex;
|
||||||
|
}
|
||||||
|
++strCurrIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = patternCurIndex === pattern.length;
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
outScore = 100;
|
||||||
|
|
||||||
|
// Apply leading letter penalty
|
||||||
|
let penalty = weights.leadingLetterPenalty * matches[0];
|
||||||
|
penalty =
|
||||||
|
penalty < weights.maxLeadingLetterPenalty ?
|
||||||
|
weights.maxLeadingLetterPenalty :
|
||||||
|
penalty;
|
||||||
|
outScore += penalty;
|
||||||
|
|
||||||
|
// Apply unmatched penalty
|
||||||
|
const unmatched = str.length - nextMatch;
|
||||||
|
outScore += weights.unmatchedLetterPenalty * unmatched;
|
||||||
|
|
||||||
|
// Apply ordering bonuses
|
||||||
|
for (let i = 0; i < nextMatch; i++) {
|
||||||
|
const currIdx = matches[i];
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
const prevIdx = matches[i - 1];
|
||||||
|
if (currIdx === prevIdx + 1) {
|
||||||
|
outScore += weights.sequentialBonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for bonuses based on neighbor character value.
|
||||||
|
if (currIdx > 0) {
|
||||||
|
// Camel case
|
||||||
|
const neighbor = str[currIdx - 1];
|
||||||
|
const curr = str[currIdx];
|
||||||
|
if (
|
||||||
|
neighbor !== neighbor.toUpperCase() &&
|
||||||
|
curr !== curr.toLowerCase()
|
||||||
|
) {
|
||||||
|
outScore += weights.camelBonus;
|
||||||
|
}
|
||||||
|
const isNeighbourSeparator = neighbor === "_" || neighbor === " ";
|
||||||
|
if (isNeighbourSeparator) {
|
||||||
|
outScore += weights.separatorBonus;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First letter
|
||||||
|
outScore += weights.firstLetterBonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return best result
|
||||||
|
if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) {
|
||||||
|
// Recursive score is better than "this"
|
||||||
|
matches = bestRecursiveMatches;
|
||||||
|
outScore = bestRecursiveScore;
|
||||||
|
return [true, outScore, matches];
|
||||||
|
} else if (matched) {
|
||||||
|
// "this" score is better than recursive
|
||||||
|
return [true, outScore, matches];
|
||||||
|
} else {
|
||||||
|
return [false, outScore, matches];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [false, outScore, matches];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a list of match indexes into a list of match ranges
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @param [number] matches
|
||||||
|
* @returns [[number]]
|
||||||
|
*/
|
||||||
|
export function calcMatchRanges(matches) {
|
||||||
|
const ranges = [];
|
||||||
|
let start = matches[0],
|
||||||
|
curr = start;
|
||||||
|
|
||||||
|
matches.forEach(m => {
|
||||||
|
if (m === curr || m === curr + 1) curr = m;
|
||||||
|
else {
|
||||||
|
ranges.push([start, curr - start + 1]);
|
||||||
|
start = m;
|
||||||
|
curr = m;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ranges.push([start, curr - start + 1]);
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
@ -7,8 +7,8 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Utils from "../Utils";
|
import Utils from "../Utils.mjs";
|
||||||
import CryptoApi from "crypto-api/src/crypto-api";
|
import CryptoApi from "crypto-api/src/crypto-api.mjs";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user