mirror of
https://github.com/gchq/CyberChef
synced 2025-12-05 23:53:27 +00:00
Compare commits
219 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b3efa746e | ||
|
|
90ddc2bfa7 | ||
|
|
8e3425ed6d | ||
|
|
c1bb42fe65 | ||
|
|
b99f73919f | ||
|
|
978bf75765 | ||
|
|
432d5b43a1 | ||
|
|
3412372d1e | ||
|
|
c2e130f369 | ||
|
|
e8880f068f | ||
|
|
45c1c23e09 | ||
|
|
e10d4bf45c | ||
|
|
369b640408 | ||
|
|
d2d30bf668 | ||
|
|
dcb59edb79 | ||
|
|
84d31c1d59 | ||
|
|
58d41f4458 | ||
|
|
360effb839 | ||
|
|
d923c99975 | ||
|
|
2b538061e9 | ||
|
|
0c9db5afe9 | ||
|
|
1031429550 | ||
|
|
4a7ea469d4 | ||
|
|
833c1cd98f | ||
|
|
662922be6f | ||
|
|
370ae323f6 | ||
|
|
514eef50de | ||
|
|
ec1fd7b923 | ||
|
|
737ce99398 | ||
|
|
4f1a897e18 | ||
|
|
588a8b2a3a | ||
|
|
f281a32a4e | ||
|
|
d09e6089ca | ||
|
|
7b6062a4a2 | ||
|
|
7975fadfe9 | ||
|
|
9fa7edffbf | ||
|
|
24a47445f6 | ||
|
|
68278267e1 | ||
|
|
0d86a7e427 | ||
|
|
9f4aa0a123 | ||
|
|
da838e266e | ||
|
|
fd160e87e8 | ||
|
|
0dd4304902 | ||
|
|
a0b94bba4e | ||
|
|
74c2a2b5cb | ||
|
|
b691c30677 | ||
|
|
01acefe4cf | ||
|
|
1a2c5a95c7 | ||
|
|
eb8725a0db | ||
|
|
57e1061063 | ||
|
|
91f4681a3c | ||
|
|
8148c1a8a8 | ||
|
|
ce61bcc078 | ||
|
|
49e2b05a11 | ||
|
|
481f2a4717 | ||
|
|
c01c076561 | ||
|
|
2391e08ac1 | ||
|
|
a7a2fe243a | ||
|
|
b5c655dd70 | ||
|
|
d84a61d108 | ||
|
|
13abbd2c5d | ||
|
|
d22eac9f35 | ||
|
|
d71aa8d7e2 | ||
|
|
7a4e0301d2 | ||
|
|
42826e542d | ||
|
|
42d1c9403c | ||
|
|
ab43635583 | ||
|
|
b8ecd83bfd | ||
|
|
0db0ced1ab | ||
|
|
3d20833d42 | ||
|
|
7d09ba5669 | ||
|
|
310ff30278 | ||
|
|
821bc9405c | ||
|
|
d54d66cffc | ||
|
|
d2b4c40357 | ||
|
|
f48af97ddc | ||
|
|
f1264d6310 | ||
|
|
4dc5a1499a | ||
|
|
58a8af20a6 | ||
|
|
75a58f465c | ||
|
|
613cbaa556 | ||
|
|
0d0a634255 | ||
|
|
105090db60 | ||
|
|
3e9c75f735 | ||
|
|
d42075072b | ||
|
|
6a099f0813 | ||
|
|
9af5e40071 | ||
|
|
4bf2a29070 | ||
|
|
069d0e48c1 | ||
|
|
c8cb2692dd | ||
|
|
74a22bcf9c | ||
|
|
8b44927cb6 | ||
|
|
3209c94622 | ||
|
|
6f8a5ea1be | ||
|
|
69837837b0 | ||
|
|
03d8bf2836 | ||
|
|
eca2c142f3 | ||
|
|
715f7bbbc2 | ||
|
|
291ebd5c12 | ||
|
|
ba04cac7ac | ||
|
|
acb8c0b5af | ||
|
|
0c14bacea7 | ||
|
|
4cabb849f3 | ||
|
|
445a85798b | ||
|
|
55775f48e9 | ||
|
|
4bd923dc06 | ||
|
|
439654ed7f | ||
|
|
69797e58cb | ||
|
|
d1961ca3fa | ||
|
|
2e9b1e079c | ||
|
|
3dfaaf4c25 | ||
|
|
fcc39a0397 | ||
|
|
0602f457ce | ||
|
|
d00b0f4c0e | ||
|
|
5e68959c03 | ||
|
|
ad4451a757 | ||
|
|
4d8127a7d9 | ||
|
|
cd2c8078c8 | ||
|
|
ee360521bb | ||
|
|
04b0b8c723 | ||
|
|
b3ac8d0835 | ||
|
|
1a88a0164c | ||
|
|
8b77ad7748 | ||
|
|
8d1f668fc5 | ||
|
|
68fbbb64db | ||
|
|
8bba4b2973 | ||
|
|
2307325af8 | ||
|
|
4e57b4be88 | ||
|
|
abdd70c6fa | ||
|
|
c077b22410 | ||
|
|
9787ab04cd | ||
|
|
79d3c90026 | ||
|
|
c2068b343b | ||
|
|
9e63e40dab | ||
|
|
6424839731 | ||
|
|
863a525625 | ||
|
|
f82a727e24 | ||
|
|
995fcab071 | ||
|
|
c5270d75a1 | ||
|
|
324c409ff1 | ||
|
|
1db8e6dddc | ||
|
|
c49a770c59 | ||
|
|
dd9ba4d250 | ||
|
|
0e601d5b5f | ||
|
|
ebb632e888 | ||
|
|
4db2335107 | ||
|
|
26a2fb6662 | ||
|
|
4c1521a98e | ||
|
|
df8abb099c | ||
|
|
fe1332f18e | ||
|
|
cb9ab7a2c9 | ||
|
|
3a6b2875d5 | ||
|
|
766de7e6fa | ||
|
|
13439e100e | ||
|
|
5ac469b174 | ||
|
|
8ac5b48493 | ||
|
|
1a827ef44f | ||
|
|
0f0e346a02 | ||
|
|
2a6db47aeb | ||
|
|
19b7957523 | ||
|
|
0d2cb02f97 | ||
|
|
7d8d80ca2c | ||
|
|
0449c46b38 | ||
|
|
cd0c86e0d6 | ||
|
|
a56f92cdee | ||
|
|
c82971f8db | ||
|
|
cb2c376c63 | ||
|
|
bc00fa0694 | ||
|
|
c86007da71 | ||
|
|
1bf513ca74 | ||
|
|
29411c903f | ||
|
|
017dde364c | ||
|
|
c123d7370a | ||
|
|
76f1e5e8f3 | ||
|
|
4e466c7886 | ||
|
|
d469fb9c58 | ||
|
|
4c285bce57 | ||
|
|
050ab03448 | ||
|
|
40acf751a8 | ||
|
|
126ad585c0 | ||
|
|
1d04b649e0 | ||
|
|
b750006cf0 | ||
|
|
3c16b839b6 | ||
|
|
32aea6b86c | ||
|
|
688c2d0df5 | ||
|
|
ede75530d0 | ||
|
|
3ae225ac59 | ||
|
|
fd07b89028 | ||
|
|
0cea56dc62 | ||
|
|
bb44268c30 | ||
|
|
19b3dcf1c2 | ||
|
|
71e0a4e0ce | ||
|
|
7f2e879e24 | ||
|
|
840e44deac | ||
|
|
f7707faece | ||
|
|
b631e3fef6 | ||
|
|
b0fb9db4b8 | ||
|
|
c7e9115994 | ||
|
|
f2d115ee4d | ||
|
|
0198f05112 | ||
|
|
729307336e | ||
|
|
f4f9b5c91c | ||
|
|
f355fe3447 | ||
|
|
321718d43a | ||
|
|
a1b161493c | ||
|
|
5acee80463 | ||
|
|
e6932401ad | ||
|
|
7a4eff0f5c | ||
|
|
8b533e9893 | ||
|
|
02b92c7977 | ||
|
|
454ef0076b | ||
|
|
18693d2471 | ||
|
|
5a9583c970 | ||
|
|
0046f7e3d7 | ||
|
|
8d3836cb16 | ||
|
|
9829491c4c | ||
|
|
e6fb0be1d0 | ||
|
|
d02124550b | ||
|
|
6aa9d2b492 |
@@ -9,6 +9,6 @@ trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[{package.json,.travis.yml}]
|
||||
[{package.json,.travis.yml,nightwatch.json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
"sourceType": "module",
|
||||
"allowImportExportEverywhere": true
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
@@ -87,10 +89,20 @@
|
||||
"no-var": "error",
|
||||
"prefer-const": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": "tests/**/*",
|
||||
"rules": {
|
||||
"no-unused-expressions": "off",
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globals": {
|
||||
"$": false,
|
||||
"jQuery": false,
|
||||
"log": false,
|
||||
"app": false,
|
||||
|
||||
"COMPILE_TIME": false,
|
||||
"COMPILE_MSG": false,
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,4 +9,5 @@ docs/*
|
||||
src/core/config/modules/*
|
||||
src/core/config/OperationConfig.json
|
||||
src/core/operations/index.mjs
|
||||
tests/browser/output/*
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
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
|
||||
|
||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -2,6 +2,30 @@
|
||||
All major and minor version changes will be documented in this file. Details of patch-level version changes can be found in [commit messages](https://github.com/gchq/CyberChef/commits/master).
|
||||
|
||||
|
||||
### [8.24.0] - 2019-02-08
|
||||
- 'DNS over HTTPS' operation added [@h345983745] | [#489]
|
||||
|
||||
### [8.23.1] - 2019-01-18
|
||||
- 'Convert co-ordinate format' operation added [@j433866] | [#476]
|
||||
|
||||
### [8.23.0] - 2019-01-18
|
||||
- 'YARA Rules' operation added [@artemisbot] | [#468]
|
||||
|
||||
### [8.22.0] - 2019-01-10
|
||||
- 'Subsection' operation added [@j433866] | [#467]
|
||||
|
||||
### [8.21.0] - 2019-01-10
|
||||
- 'To Case Insensitive Regex' and 'From Case Insensitive Regex' operations added [@masq] | [#461]
|
||||
|
||||
### [8.20.0] - 2019-01-09
|
||||
- 'Generate Lorem Ipsum' operation added [@klaxon1] | [#455]
|
||||
|
||||
### [8.19.0] - 2018-12-30
|
||||
- UI test suite added to confirm that the app loads correctly in a reasonable time and that various operations from each module can be run [@n1474335] | [#458]
|
||||
|
||||
### [8.18.0] - 2018-12-26
|
||||
- 'Split Colour Channels' operation added [@artemisbot] | [#449]
|
||||
|
||||
### [8.17.0] - 2018-12-25
|
||||
- 'Generate QR Code' and 'Parse QR Code' operations added [@j433866] | [#448]
|
||||
|
||||
@@ -82,6 +106,14 @@ All major and minor version changes will be documented in this file. Details of
|
||||
|
||||
|
||||
|
||||
[8.24.0]: https://github.com/gchq/CyberChef/releases/tag/v8.24.0
|
||||
[8.23.1]: https://github.com/gchq/CyberChef/releases/tag/v8.23.1
|
||||
[8.23.0]: https://github.com/gchq/CyberChef/releases/tag/v8.23.0
|
||||
[8.22.0]: https://github.com/gchq/CyberChef/releases/tag/v8.22.0
|
||||
[8.21.0]: https://github.com/gchq/CyberChef/releases/tag/v8.21.0
|
||||
[8.20.0]: https://github.com/gchq/CyberChef/releases/tag/v8.20.0
|
||||
[8.19.0]: https://github.com/gchq/CyberChef/releases/tag/v8.19.0
|
||||
[8.18.0]: https://github.com/gchq/CyberChef/releases/tag/v8.18.0
|
||||
[8.17.0]: https://github.com/gchq/CyberChef/releases/tag/v8.17.0
|
||||
[8.16.0]: https://github.com/gchq/CyberChef/releases/tag/v8.16.0
|
||||
[8.15.0]: https://github.com/gchq/CyberChef/releases/tag/v8.15.0
|
||||
@@ -109,6 +141,7 @@ All major and minor version changes will be documented in this file. Details of
|
||||
[@d98762625]: https://github.com/d98762625
|
||||
[@j433866]: https://github.com/j433866
|
||||
[@GCHQ77703]: https://github.com/GCHQ77703
|
||||
[@h345983745]: https://github.com/h345983745
|
||||
[@artemisbot]: https://github.com/artemisbot
|
||||
[@picapi]: https://github.com/picapi
|
||||
[@Dachande663]: https://github.com/Dachande663
|
||||
@@ -122,6 +155,7 @@ All major and minor version changes will be documented in this file. Details of
|
||||
[@tcode2k16]: https://github.com/tcode2k16
|
||||
[@Cynser]: https://github.com/Cynser
|
||||
[@anthony-arnold]: https://github.com/anthony-arnold
|
||||
[@masq]: https://github.com/masq
|
||||
|
||||
[#95]: https://github.com/gchq/CyberChef/pull/299
|
||||
[#173]: https://github.com/gchq/CyberChef/pull/173
|
||||
@@ -150,3 +184,11 @@ All major and minor version changes will be documented in this file. Details of
|
||||
[#443]: https://github.com/gchq/CyberChef/pull/443
|
||||
[#446]: https://github.com/gchq/CyberChef/pull/446
|
||||
[#448]: https://github.com/gchq/CyberChef/pull/448
|
||||
[#449]: https://github.com/gchq/CyberChef/pull/449
|
||||
[#455]: https://github.com/gchq/CyberChef/pull/455
|
||||
[#458]: https://github.com/gchq/CyberChef/pull/458
|
||||
[#461]: https://github.com/gchq/CyberChef/pull/461
|
||||
[#467]: https://github.com/gchq/CyberChef/pull/467
|
||||
[#468]: https://github.com/gchq/CyberChef/pull/468
|
||||
[#476]: https://github.com/gchq/CyberChef/pull/476
|
||||
[#489]: https://github.com/gchq/CyberChef/pull/489
|
||||
|
||||
51
Gruntfile.js
51
Gruntfile.js
@@ -30,8 +30,12 @@ module.exports = function (grunt) {
|
||||
["clean:node", "clean:config", "exec:generateConfig", "webpack:node", "chmod:build"]);
|
||||
|
||||
grunt.registerTask("test",
|
||||
"A task which runs all the tests in test/tests.",
|
||||
["exec:generateConfig", "exec:tests"]);
|
||||
"A task which runs all the operation tests in the tests directory.",
|
||||
["exec:generateConfig", "exec:opTests"]);
|
||||
|
||||
grunt.registerTask("testui",
|
||||
"A task which runs all the UI tests in the tests directory. The prod task must already have been run.",
|
||||
["connect:prod", "exec:browserTests"]);
|
||||
|
||||
grunt.registerTask("docs",
|
||||
"Compiles documentation in the /docs directory.",
|
||||
@@ -67,6 +71,7 @@ module.exports = function (grunt) {
|
||||
grunt.loadNpmTasks("grunt-exec");
|
||||
grunt.loadNpmTasks("grunt-accessibility");
|
||||
grunt.loadNpmTasks("grunt-concurrent");
|
||||
grunt.loadNpmTasks("grunt-contrib-connect");
|
||||
|
||||
|
||||
// Project configuration
|
||||
@@ -144,11 +149,11 @@ module.exports = function (grunt) {
|
||||
options: {
|
||||
configFile: "./.eslintrc.json"
|
||||
},
|
||||
configs: ["*.js"],
|
||||
configs: ["*.{js,mjs}"],
|
||||
core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*", "!src/core/operations/legacy/**/*"],
|
||||
web: ["src/web/**/*.{js,mjs}"],
|
||||
node: ["src/node/**/*.{js,mjs}"],
|
||||
tests: ["test/**/*.{js,mjs}"],
|
||||
tests: ["tests/**/*.{js,mjs}"],
|
||||
},
|
||||
jsdoc: {
|
||||
options: {
|
||||
@@ -189,7 +194,8 @@ module.exports = function (grunt) {
|
||||
sitemap: "./src/web/static/sitemap.js"
|
||||
}, moduleEntryPoints),
|
||||
output: {
|
||||
path: __dirname + "/build/prod"
|
||||
path: __dirname + "/build/prod",
|
||||
globalObject: "this"
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -246,19 +252,6 @@ module.exports = function (grunt) {
|
||||
}),
|
||||
]
|
||||
},
|
||||
tests: {
|
||||
mode: "development",
|
||||
target: "node",
|
||||
entry: "./test/index.mjs",
|
||||
externals: [NodeExternals()],
|
||||
output: {
|
||||
filename: "index.js",
|
||||
path: __dirname + "/build/test"
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin(BUILD_CONSTANTS)
|
||||
]
|
||||
},
|
||||
node: {
|
||||
mode: "production",
|
||||
target: "node",
|
||||
@@ -307,6 +300,9 @@ module.exports = function (grunt) {
|
||||
"./config/modules/OpModules": "./config/modules/Default"
|
||||
}
|
||||
},
|
||||
output: {
|
||||
globalObject: "this",
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin(BUILD_CONSTANTS),
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -320,6 +316,14 @@ module.exports = function (grunt) {
|
||||
}
|
||||
}
|
||||
},
|
||||
connect: {
|
||||
prod: {
|
||||
options: {
|
||||
port: 8000,
|
||||
base: "build/prod/"
|
||||
}
|
||||
}
|
||||
},
|
||||
copy: {
|
||||
ghPages: {
|
||||
options: {
|
||||
@@ -391,16 +395,17 @@ module.exports = function (grunt) {
|
||||
generateConfig: {
|
||||
command: [
|
||||
"echo '\n--- Regenerating config files. ---'",
|
||||
"mkdir -p src/core/config/modules",
|
||||
"echo 'export default {};\n' > src/core/config/modules/OpModules.mjs",
|
||||
"echo '[]\n' > src/core/config/OperationConfig.json",
|
||||
"echo [] > src/core/config/OperationConfig.json",
|
||||
"node --experimental-modules --no-warnings --no-deprecation src/core/config/scripts/generateOpsIndex.mjs",
|
||||
"node --experimental-modules --no-warnings --no-deprecation src/core/config/scripts/generateConfig.mjs",
|
||||
"echo '--- Config scripts finished. ---\n'"
|
||||
].join(";")
|
||||
},
|
||||
tests: {
|
||||
command: "node --experimental-modules --no-warnings --no-deprecation test/index.mjs"
|
||||
opTests: {
|
||||
command: "node --experimental-modules --no-warnings --no-deprecation tests/operations/index.mjs"
|
||||
},
|
||||
browserTests: {
|
||||
command: "./node_modules/.bin/nightwatch --env prod,inline"
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ module.exports = function(api) {
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"babel-plugin-syntax-dynamic-import",
|
||||
["babel-plugin-transform-builtin-extend", {
|
||||
"globals": ["Error"]
|
||||
}]
|
||||
|
||||
34
nightwatch.json
Normal file
34
nightwatch.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"src_folders": ["tests/browser"],
|
||||
"output_folder": "tests/browser/output",
|
||||
|
||||
"test_settings": {
|
||||
|
||||
"default": {
|
||||
"launch_url": "http://localhost:8080",
|
||||
"webdriver": {
|
||||
"start_process": true,
|
||||
"server_path": "./node_modules/.bin/chromedriver",
|
||||
"port": 9515,
|
||||
"log_path": false
|
||||
},
|
||||
"desiredCapabilities": {
|
||||
"browserName": "chrome"
|
||||
}
|
||||
},
|
||||
|
||||
"dev": {
|
||||
"launch_url": "http://localhost:8080"
|
||||
},
|
||||
|
||||
"prod": {
|
||||
"launch_url": "http://localhost:8000/index.html"
|
||||
},
|
||||
|
||||
"inline": {
|
||||
"launch_url": "http://localhost:8000/cyberchef.htm"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
2462
package-lock.json
generated
2462
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
58
package.json
58
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cyberchef",
|
||||
"version": "8.17.2",
|
||||
"version": "8.26.2",
|
||||
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
|
||||
"author": "n1474335 <n1474335@gmail.com>",
|
||||
"homepage": "https://gchq.github.io/CyberChef",
|
||||
@@ -30,22 +30,25 @@
|
||||
"main": "build/node/CyberChef.js",
|
||||
"bugs": "https://github.com/gchq/CyberChef/issues",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.5",
|
||||
"@babel/preset-env": "^7.1.5",
|
||||
"autoprefixer": "^9.3.1",
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/preset-env": "^7.2.3",
|
||||
"autoprefixer": "^9.4.3",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.0.4",
|
||||
"bootstrap": "^4.1.3",
|
||||
"colors": "^1.3.2",
|
||||
"css-loader": "^1.0.1",
|
||||
"eslint": "^5.8.0",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"bootstrap": "^4.2.1",
|
||||
"chromedriver": "^2.45.0",
|
||||
"colors": "^1.3.3",
|
||||
"css-loader": "^2.1.0",
|
||||
"eslint": "^5.12.1",
|
||||
"exports-loader": "^0.7.0",
|
||||
"extract-text-webpack-plugin": "^4.0.0-alpha0",
|
||||
"file-loader": "^2.0.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"grunt": "^1.0.3",
|
||||
"grunt-accessibility": "~6.0.0",
|
||||
"grunt-chmod": "~1.1.1",
|
||||
"grunt-concurrent": "^2.3.1",
|
||||
"grunt-contrib-clean": "~2.0.0",
|
||||
"grunt-contrib-connect": "^2.0.0",
|
||||
"grunt-contrib-copy": "~1.0.0",
|
||||
"grunt-contrib-watch": "^1.1.0",
|
||||
"grunt-eslint": "^21.0.0",
|
||||
@@ -56,7 +59,9 @@
|
||||
"imports-loader": "^0.8.0",
|
||||
"ink-docstrap": "^1.3.2",
|
||||
"jsdoc-babel": "^0.5.0",
|
||||
"node-sass": "^4.10.0",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"nightwatch": "^1.0.18",
|
||||
"node-sass": "^4.11.0",
|
||||
"postcss-css-variables": "^0.11.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
@@ -66,9 +71,9 @@
|
||||
"style-loader": "^0.23.1",
|
||||
"url-loader": "^1.1.2",
|
||||
"web-resource-inliner": "^4.2.1",
|
||||
"webpack": "^4.25.1",
|
||||
"webpack": "^4.28.3",
|
||||
"webpack-bundle-analyzer": "^3.0.3",
|
||||
"webpack-dev-server": "^3.1.10",
|
||||
"webpack-dev-server": "^3.1.14",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"worker-loader": "^2.0.0"
|
||||
},
|
||||
@@ -77,10 +82,10 @@
|
||||
"babel-plugin-transform-builtin-extend": "1.1.2",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bignumber.js": "^8.0.1",
|
||||
"bignumber.js": "^8.0.2",
|
||||
"bootstrap-colorpicker": "^2.5.3",
|
||||
"bootstrap-material-design": "^4.1.1",
|
||||
"bson": "^3.0.2",
|
||||
"bson": "^4.0.1",
|
||||
"chi-squared": "^1.1.0",
|
||||
"crypto-api": "^0.8.3",
|
||||
"crypto-js": "^3.1.9-1",
|
||||
@@ -91,48 +96,51 @@
|
||||
"esmangle": "^1.0.1",
|
||||
"esprima": "^4.0.1",
|
||||
"exif-parser": "^0.1.12",
|
||||
"file-saver": "^2.0.0-rc.4",
|
||||
"file-saver": "^2.0.0",
|
||||
"geodesy": "^1.1.3",
|
||||
"highlight.js": "^9.13.1",
|
||||
"jimp": "^0.6.0",
|
||||
"jquery": "^3.3.1",
|
||||
"js-crc": "^0.2.0",
|
||||
"js-sha3": "^0.8.0",
|
||||
"jsesc": "^2.5.1",
|
||||
"jsesc": "^2.5.2",
|
||||
"jsonpath": "^1.0.0",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"jsonwebtoken": "^8.4.0",
|
||||
"jsqr": "^1.1.1",
|
||||
"jsrsasign": "8.0.12",
|
||||
"kbpgp": "^2.0.82",
|
||||
"libyara-wasm": "0.0.12",
|
||||
"lodash": "^4.17.11",
|
||||
"loglevel": "^1.6.1",
|
||||
"loglevel-message-prefix": "^3.0.0",
|
||||
"moment": "^2.22.2",
|
||||
"moment": "^2.23.0",
|
||||
"moment-timezone": "^0.5.23",
|
||||
"ngeohash": "^0.6.0",
|
||||
"ngeohash": "^0.6.3",
|
||||
"node-forge": "^0.7.6",
|
||||
"node-md6": "^0.1.0",
|
||||
"notepack.io": "^2.1.3",
|
||||
"notepack.io": "^2.2.0",
|
||||
"nwmatcher": "^1.4.4",
|
||||
"otp": "^0.1.3",
|
||||
"popper.js": "^1.14.4",
|
||||
"popper.js": "^1.14.6",
|
||||
"qr-image": "^3.2.0",
|
||||
"scryptsy": "^2.0.0",
|
||||
"snackbarjs": "^1.1.0",
|
||||
"sortablejs": "^1.7.0",
|
||||
"split.js": "^1.5.9",
|
||||
"sortablejs": "^1.8.0-rc1",
|
||||
"split.js": "^1.5.10",
|
||||
"ssdeep.js": "0.0.2",
|
||||
"ua-parser-js": "^0.7.19",
|
||||
"utf8": "^3.0.0",
|
||||
"vkbeautify": "^0.99.3",
|
||||
"xmldom": "^0.1.27",
|
||||
"xpath": "0.0.27",
|
||||
"xregexp": "^4.2.0",
|
||||
"xregexp": "^4.2.4",
|
||||
"zlibjs": "^0.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "grunt dev",
|
||||
"build": "grunt prod",
|
||||
"test": "grunt test",
|
||||
"testui": "grunt testui",
|
||||
"docs": "grunt docs",
|
||||
"lint": "grunt lint",
|
||||
"newop": "node --experimental-modules src/core/config/scripts/newOperation.mjs"
|
||||
|
||||
@@ -157,9 +157,9 @@ class Chef {
|
||||
* @param {number} pos.end - The end offset.
|
||||
* @returns {Object}
|
||||
*/
|
||||
calculateHighlights(recipeConfig, direction, pos) {
|
||||
async calculateHighlights(recipeConfig, direction, pos) {
|
||||
const recipe = new Recipe(recipeConfig);
|
||||
const highlights = recipe.generateHighlightList();
|
||||
const highlights = await recipe.generateHighlightList();
|
||||
|
||||
if (!highlights) return false;
|
||||
|
||||
|
||||
@@ -157,8 +157,8 @@ async function getDishAs(data) {
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
*/
|
||||
function calculateHighlights(recipeConfig, direction, pos) {
|
||||
pos = self.chef.calculateHighlights(recipeConfig, direction, pos);
|
||||
async function calculateHighlights(recipeConfig, direction, pos) {
|
||||
pos = await self.chef.calculateHighlights(recipeConfig, direction, pos);
|
||||
|
||||
self.postMessage({
|
||||
action: "highlightsCalculated",
|
||||
|
||||
@@ -168,7 +168,7 @@ class Dish {
|
||||
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()) : [];
|
||||
this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : [];
|
||||
break;
|
||||
case Dish.JSON:
|
||||
this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : [];
|
||||
@@ -265,7 +265,7 @@ class Dish {
|
||||
case Dish.ARRAY_BUFFER:
|
||||
return this.value instanceof ArrayBuffer;
|
||||
case Dish.BIG_NUMBER:
|
||||
return this.value instanceof BigNumber;
|
||||
return BigNumber.isBigNumber(this.value);
|
||||
case Dish.JSON:
|
||||
// All values can be serialised in some manner, so we return true in all cases
|
||||
return true;
|
||||
|
||||
@@ -23,9 +23,13 @@ class Ingredient {
|
||||
this._value = null;
|
||||
this.disabled = false;
|
||||
this.hint = "";
|
||||
this.rows = 0;
|
||||
this.toggleValues = [];
|
||||
this.target = null;
|
||||
this.defaultIndex = 0;
|
||||
this.min = null;
|
||||
this.max = null;
|
||||
this.step = 1;
|
||||
|
||||
if (ingredientConfig) {
|
||||
this._parseConfig(ingredientConfig);
|
||||
@@ -45,9 +49,13 @@ class Ingredient {
|
||||
this.defaultValue = ingredientConfig.value;
|
||||
this.disabled = !!ingredientConfig.disabled;
|
||||
this.hint = ingredientConfig.hint || false;
|
||||
this.rows = ingredientConfig.rows || false;
|
||||
this.toggleValues = ingredientConfig.toggleValues;
|
||||
this.target = typeof ingredientConfig.target !== "undefined" ? ingredientConfig.target : null;
|
||||
this.defaultIndex = typeof ingredientConfig.defaultIndex !== "undefined" ? ingredientConfig.defaultIndex : 0;
|
||||
this.min = ingredientConfig.min;
|
||||
this.max = ingredientConfig.max;
|
||||
this.step = ingredientConfig.step;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +103,7 @@ class Ingredient {
|
||||
case "binaryString":
|
||||
case "binaryShortString":
|
||||
case "editableOption":
|
||||
case "editableOptionShort":
|
||||
return Utils.parseEscapedChars(data);
|
||||
case "byteArray":
|
||||
if (typeof data == "string") {
|
||||
|
||||
@@ -23,6 +23,7 @@ class Operation {
|
||||
this._breakpoint = false;
|
||||
this._disabled = false;
|
||||
this._flowControl = false;
|
||||
this._manualBake = false;
|
||||
this._ingList = [];
|
||||
|
||||
// Public fields
|
||||
@@ -179,9 +180,13 @@ class Operation {
|
||||
|
||||
if (ing.toggleValues) conf.toggleValues = ing.toggleValues;
|
||||
if (ing.hint) conf.hint = ing.hint;
|
||||
if (ing.rows) conf.rows = ing.rows;
|
||||
if (ing.disabled) conf.disabled = ing.disabled;
|
||||
if (ing.target) conf.target = ing.target;
|
||||
if (ing.defaultIndex) conf.defaultIndex = ing.defaultIndex;
|
||||
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;
|
||||
return conf;
|
||||
});
|
||||
}
|
||||
@@ -281,6 +286,7 @@ class Operation {
|
||||
return this._flowControl;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set whether this Operation is a flowcontrol op.
|
||||
*
|
||||
@@ -290,6 +296,26 @@ class Operation {
|
||||
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;
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import OpModules from "./config/modules/OpModules";
|
||||
import OperationConfig from "./config/OperationConfig.json";
|
||||
import OperationError from "./errors/OperationError";
|
||||
import Operation from "./Operation";
|
||||
import DishError from "./errors/DishError";
|
||||
import log from "loglevel";
|
||||
|
||||
// Cache container for modules
|
||||
let modules = null;
|
||||
|
||||
/**
|
||||
* The Recipe controls a list of Operations and the Dish they operate on.
|
||||
*/
|
||||
@@ -36,16 +39,43 @@ class Recipe {
|
||||
* @param {Object} recipeConfig
|
||||
*/
|
||||
_parseConfig(recipeConfig) {
|
||||
for (let c = 0; c < recipeConfig.length; c++) {
|
||||
const operationName = recipeConfig[c].op;
|
||||
const opConf = OperationConfig[operationName];
|
||||
const opObj = OpModules[opConf.module][operationName];
|
||||
const operation = new opObj();
|
||||
operation.ingValues = recipeConfig[c].args;
|
||||
operation.breakpoint = recipeConfig[c].breakpoint;
|
||||
operation.disabled = recipeConfig[c].disabled;
|
||||
this.addOperation(operation);
|
||||
recipeConfig.forEach(c => {
|
||||
this.opList.push({
|
||||
name: c.op,
|
||||
module: OperationConfig[c.op].module,
|
||||
ingValues: c.args,
|
||||
breakpoint: c.breakpoint,
|
||||
disabled: c.disabled,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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");
|
||||
modules = modules.default;
|
||||
}
|
||||
|
||||
this.opList = this.opList.map(o => {
|
||||
if (o instanceof Operation) {
|
||||
return o;
|
||||
} else {
|
||||
const op = new modules[o.module][o.name]();
|
||||
op.ingValues = o.ingValues;
|
||||
op.breakpoint = o.breakpoint;
|
||||
op.disabled = o.disabled;
|
||||
return op;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +85,10 @@ class Recipe {
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
get config() {
|
||||
return this.opList.map(op => op.config);
|
||||
return this.opList.map(op => ({
|
||||
op: op.name,
|
||||
args: op.ingValues,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +108,19 @@ class Recipe {
|
||||
* @param {Operation[]} 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 +153,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}
|
||||
*/
|
||||
@@ -137,6 +182,8 @@ class Recipe {
|
||||
|
||||
if (startFrom === 0) this.lastRunOp = null;
|
||||
|
||||
await this._hydrateOpList();
|
||||
|
||||
log.debug(`[*] Executing recipe of ${this.opList.length} operations, starting at ${startFrom}`);
|
||||
|
||||
for (let i = startFrom; i < this.opList.length; i++) {
|
||||
@@ -254,7 +301,8 @@ class Recipe {
|
||||
* @returns {function} highlights[].b
|
||||
* @returns {Object[]} highlights[].args
|
||||
*/
|
||||
generateHighlightList() {
|
||||
async generateHighlightList() {
|
||||
await this._hydrateOpList();
|
||||
const highlights = [];
|
||||
|
||||
for (let i = 0; i < this.opList.length; i++) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import utf8 from "utf8";
|
||||
import {fromBase64} from "./lib/Base64";
|
||||
import {fromBase64, toBase64} from "./lib/Base64";
|
||||
import {fromHex} from "./lib/Hex";
|
||||
import {fromDecimal} from "./lib/Decimal";
|
||||
import {fromBinary} from "./lib/Binary";
|
||||
@@ -817,12 +817,24 @@ class Utils {
|
||||
return html;
|
||||
};
|
||||
|
||||
const formatContent = function (buff, type) {
|
||||
if (type.startsWith("image")) {
|
||||
let dataURI = "data:";
|
||||
dataURI += type + ";";
|
||||
dataURI += "base64," + toBase64(buff);
|
||||
return "<img style='max-width: 100%;' src='" + dataURI + "'>";
|
||||
} else {
|
||||
return `<pre>${Utils.escapeHtml(Utils.arrayBufferToStr(buff.buffer))}</pre>`;
|
||||
}
|
||||
};
|
||||
|
||||
const formatFile = async function(file, i) {
|
||||
const buff = await Utils.readFile(file);
|
||||
const blob = new Blob(
|
||||
[buff],
|
||||
{type: "octet/stream"}
|
||||
{type: file.type || "octet/stream"}
|
||||
);
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
|
||||
const html = `<div class='card' style='white-space: normal;'>
|
||||
<div class='card-header' id='heading${i}'>
|
||||
@@ -837,16 +849,25 @@ class Utils {
|
||||
<span class='float-right' style="margin-top: -3px">
|
||||
${file.size.toLocaleString()} bytes
|
||||
<a title="Download ${Utils.escapeHtml(file.name)}"
|
||||
href='${URL.createObjectURL(blob)}'
|
||||
download='${Utils.escapeHtml(file.name)}'>
|
||||
href="${blobURL}"
|
||||
download="${Utils.escapeHtml(file.name)}"
|
||||
data-toggle="tooltip">
|
||||
<i class="material-icons" style="vertical-align: bottom">save</i>
|
||||
</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>
|
||||
</h6>
|
||||
</div>
|
||||
<div id='collapse${i}' class='collapse' aria-labelledby='heading${i}' data-parent="#files">
|
||||
<div class='card-body'>
|
||||
<pre>${Utils.escapeHtml(Utils.arrayBufferToStr(buff.buffer))}</pre>
|
||||
${formatContent(buff, file.type)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -1152,6 +1173,21 @@ String.prototype.count = function(chr) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for self.sendStatusMessage to handle different environments.
|
||||
*
|
||||
* @param {string} msg
|
||||
*/
|
||||
export function sendStatusMessage(msg) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage(msg);
|
||||
else if (ENVIRONMENT_IS_WEB())
|
||||
app.alert(msg, 10000);
|
||||
else if (ENVIRONMENT_IS_NODE())
|
||||
log.debug(msg);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Polyfills
|
||||
*/
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
"name": "Networking",
|
||||
"ops": [
|
||||
"HTTP request",
|
||||
"DNS over HTTPS",
|
||||
"Strip HTTP headers",
|
||||
"Dechunk HTTP response",
|
||||
"Parse User Agent",
|
||||
@@ -189,6 +190,8 @@
|
||||
"Remove null bytes",
|
||||
"To Upper case",
|
||||
"To Lower case",
|
||||
"To Case Insensitive Regex",
|
||||
"From Case Insensitive Regex",
|
||||
"Add line numbers",
|
||||
"Remove line numbers",
|
||||
"To Table",
|
||||
@@ -213,6 +216,7 @@
|
||||
"Convert mass",
|
||||
"Convert speed",
|
||||
"Convert data units",
|
||||
"Convert co-ordinate format",
|
||||
"Parse UNIX file permissions",
|
||||
"Swap endianness",
|
||||
"Parse colour code",
|
||||
@@ -250,7 +254,8 @@
|
||||
"XPath expression",
|
||||
"JPath expression",
|
||||
"CSS selector",
|
||||
"Extract EXIF"
|
||||
"Extract EXIF",
|
||||
"Extract Files"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -304,9 +309,7 @@
|
||||
"Adler-32 Checksum",
|
||||
"CRC-16 Checksum",
|
||||
"CRC-32 Checksum",
|
||||
"TCP/IP Checksum",
|
||||
"To Geohash",
|
||||
"From Geohash"
|
||||
"TCP/IP Checksum"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -346,10 +349,32 @@
|
||||
"ops": [
|
||||
"Detect File Type",
|
||||
"Scan for Embedded Files",
|
||||
"Extract Files",
|
||||
"Remove EXIF",
|
||||
"Extract EXIF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Multimedia",
|
||||
"ops": [
|
||||
"Render Image",
|
||||
"Play Media",
|
||||
"Remove EXIF",
|
||||
"Extract EXIF",
|
||||
"Render Image",
|
||||
"Play Media"
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -366,6 +391,7 @@
|
||||
"Generate QR Code",
|
||||
"Parse QR Code",
|
||||
"Haversine distance",
|
||||
"Generate Lorem Ipsum",
|
||||
"Numberwang",
|
||||
"XKCD Random Number"
|
||||
]
|
||||
@@ -375,6 +401,7 @@
|
||||
"ops": [
|
||||
"Magic",
|
||||
"Fork",
|
||||
"Subsection",
|
||||
"Merge",
|
||||
"Register",
|
||||
"Label",
|
||||
|
||||
@@ -41,6 +41,7 @@ for (const opObj in Ops) {
|
||||
inputType: op.inputType,
|
||||
outputType: op.presentType,
|
||||
flowControl: op.flowControl,
|
||||
manualBake: op.manualBake,
|
||||
args: op.args
|
||||
};
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ export default ${moduleName};
|
||||
console.log(`\nNext steps:
|
||||
1. Add your operation to ${colors.green("src/core/config/Categories.json")}
|
||||
2. Write your operation code.
|
||||
3. Write tests in ${colors.green("test/tests/operations/")}
|
||||
3. Write tests in ${colors.green("tests/operations/tests/")}
|
||||
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.`);
|
||||
|
||||
|
||||
0
src/core/lib/BCD.mjs
Executable file → Normal file
0
src/core/lib/BCD.mjs
Executable file → Normal file
0
src/core/lib/Base58.mjs
Executable file → Normal file
0
src/core/lib/Base58.mjs
Executable file → Normal file
0
src/core/lib/Base64.mjs
Executable file → Normal file
0
src/core/lib/Base64.mjs
Executable file → Normal file
0
src/core/lib/CanvasComponents.mjs
Executable file → Normal file
0
src/core/lib/CanvasComponents.mjs
Executable file → Normal file
655
src/core/lib/ConvertCoordinates.mjs
Normal file
655
src/core/lib/ConvertCoordinates.mjs
Normal file
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* Co-ordinate conversion resources.
|
||||
*
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import geohash from "ngeohash";
|
||||
import geodesy from "geodesy";
|
||||
import OperationError from "../errors/OperationError";
|
||||
|
||||
/**
|
||||
* 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 geodesy.LatLonEllipsoidal(hash.latitude, hash.longitude);
|
||||
break;
|
||||
case "Military Grid Reference System":
|
||||
utm = geodesy.Mgrs.parse(input.replace(/[^A-Za-z0-9]/g, "")).toUtm();
|
||||
latlon = utm.toLatLonE();
|
||||
break;
|
||||
case "Ordnance Survey National Grid":
|
||||
osng = geodesy.OsGridRef.parse(input.replace(/[^A-Za-z0-9]/g, ""));
|
||||
latlon = geodesy.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 = geodesy.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 geodesy.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 geodesy.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 geodesy.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 geodesy.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 geodesy.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 geodesy.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 = geodesy.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;
|
||||
}
|
||||
1727
src/core/lib/FileSignatures.mjs
Normal file
1727
src/core/lib/FileSignatures.mjs
Normal file
File diff suppressed because it is too large
Load Diff
263
src/core/lib/FileType.mjs
Normal file
263
src/core/lib/FileType.mjs
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* File type functions
|
||||
*
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*
|
||||
*/
|
||||
import {FILE_SIGNATURES} from "./FileSignatures";
|
||||
import {sendStatusMessage} from "../Utils";
|
||||
|
||||
|
||||
/**
|
||||
* 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} 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 && 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
|
||||
* @returs {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} 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 && 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} 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`);
|
||||
}
|
||||
230
src/core/lib/LoremIpsum.mjs
Normal file
230
src/core/lib/LoremIpsum.mjs
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Lorem Ipsum generator.
|
||||
*
|
||||
* @author Klaxon [klaxon@veyr.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate lorem ipsum paragraphs.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string}
|
||||
*/
|
||||
export function GenerateParagraphs(length=3) {
|
||||
const paragraphs = [];
|
||||
while (paragraphs.length < length) {
|
||||
const paragraphLength = getRandomLength(PARAGRAPH_LENGTH_MEAN, PARAGRAPH_LENGTH_STD_DEV);
|
||||
const sentences = [];
|
||||
while (sentences.length < paragraphLength) {
|
||||
const sentenceLength = getRandomLength(SENTENCE_LENGTH_MEAN, SENTENCE_LENGTH_STD_DEV);
|
||||
const sentence = getWords(sentenceLength);
|
||||
sentences.push(formatSentence(sentence));
|
||||
}
|
||||
paragraphs.push(formatParagraph(sentences));
|
||||
}
|
||||
paragraphs[paragraphs.length-1] = paragraphs[paragraphs.length-1].slice(0, -2);
|
||||
paragraphs[0] = replaceStart(paragraphs[0]);
|
||||
return paragraphs.join("");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate lorem ipsum sentences.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string}
|
||||
*/
|
||||
export function GenerateSentences(length=3) {
|
||||
const sentences = [];
|
||||
while (sentences.length < length) {
|
||||
const sentenceLength = getRandomLength(SENTENCE_LENGTH_MEAN, SENTENCE_LENGTH_STD_DEV);
|
||||
const sentence = getWords(sentenceLength);
|
||||
sentences.push(formatSentence(sentence));
|
||||
}
|
||||
const paragraphs = sentencesToParagraphs(sentences);
|
||||
return paragraphs.join("");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate lorem ipsum words.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string}
|
||||
*/
|
||||
export function GenerateWords(length=3) {
|
||||
const words = getWords(length);
|
||||
const sentences = wordsToSentences(words);
|
||||
const paragraphs = sentencesToParagraphs(sentences);
|
||||
return paragraphs.join("");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate lorem ipsum bytes.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string}
|
||||
*/
|
||||
export function GenerateBytes(length=3) {
|
||||
const str = GenerateWords(length/3);
|
||||
return str.slice(0, length);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get array of randomly selected words from the lorem ipsum wordList.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
function getWords(length=3) {
|
||||
const words = [];
|
||||
let word;
|
||||
let previousWord;
|
||||
while (words.length < length){
|
||||
do {
|
||||
word = wordList[Math.floor(Math.random() * wordList.length)];
|
||||
} while (previousWord === word);
|
||||
words.push(word);
|
||||
previousWord = word;
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert an array of words into an array of sentences
|
||||
*
|
||||
* @param {string[]} words
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
function wordsToSentences(words) {
|
||||
const sentences = [];
|
||||
while (words.length > 0) {
|
||||
const sentenceLength = getRandomLength(SENTENCE_LENGTH_MEAN, SENTENCE_LENGTH_STD_DEV);
|
||||
if (sentenceLength <= words.length) {
|
||||
sentences.push(formatSentence(words.splice(0, sentenceLength)));
|
||||
} else {
|
||||
sentences.push(formatSentence(words.splice(0, words.length)));
|
||||
}
|
||||
}
|
||||
return sentences;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert an array of sentences into an array of paragraphs
|
||||
*
|
||||
* @param {string[]} sentences
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
function sentencesToParagraphs(sentences) {
|
||||
const paragraphs = [];
|
||||
while (sentences.length > 0) {
|
||||
const paragraphLength = getRandomLength(PARAGRAPH_LENGTH_MEAN, PARAGRAPH_LENGTH_STD_DEV);
|
||||
paragraphs.push(formatParagraph(sentences.splice(0, paragraphLength)));
|
||||
}
|
||||
paragraphs[paragraphs.length-1] = paragraphs[paragraphs.length-1].slice(0, -1);
|
||||
paragraphs[0] = replaceStart(paragraphs[0]);
|
||||
return paragraphs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format an array of words into a sentence.
|
||||
*
|
||||
* @param {string[]} words
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
function formatSentence(words) {
|
||||
// 0.35 chance of a comma being added randomly to the sentence.
|
||||
if (Math.random() < PROBABILITY_OF_A_COMMA) {
|
||||
const pos = Math.round(Math.random()*(words.length-1));
|
||||
words[pos] +=",";
|
||||
}
|
||||
let sentence = words.join(" ");
|
||||
sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
||||
sentence += ".";
|
||||
return sentence;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format an array of sentences into a paragraph.
|
||||
*
|
||||
* @param {string[]} sentences
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
function formatParagraph(sentences) {
|
||||
let paragraph = sentences.join(" ");
|
||||
paragraph += "\n\n";
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a random number based on a mean and standard deviation.
|
||||
*
|
||||
* @param {number} mean
|
||||
* @param {number} stdDev
|
||||
* @returns {number}
|
||||
* @private
|
||||
*/
|
||||
function getRandomLength(mean, stdDev) {
|
||||
let length;
|
||||
do {
|
||||
length = Math.round((Math.random()*2-1)+(Math.random()*2-1)+(Math.random()*2-1)*stdDev+mean);
|
||||
} while (length <= 0);
|
||||
return length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replace first 5 words with "Lorem ipsum dolor sit amet"
|
||||
*
|
||||
* @param {string[]} str
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
function replaceStart(str) {
|
||||
let words = str.split(" ");
|
||||
if (words.length > 5) {
|
||||
words.splice(0, 5, "Lorem", "ipsum", "dolor", "sit", "amet");
|
||||
return words.join(" ");
|
||||
} else {
|
||||
const lorem = ["Lorem", "ipsum", "dolor", "sit", "amet"];
|
||||
words = lorem.slice(0, words.length);
|
||||
str = words.join(" ");
|
||||
str += ".";
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const SENTENCE_LENGTH_MEAN = 15;
|
||||
const SENTENCE_LENGTH_STD_DEV = 9;
|
||||
const PARAGRAPH_LENGTH_MEAN = 5;
|
||||
const PARAGRAPH_LENGTH_STD_DEV = 2;
|
||||
const PROBABILITY_OF_A_COMMA = 0.35;
|
||||
|
||||
const wordList = [
|
||||
"ad", "adipisicing", "aliqua", "aliquip", "amet", "anim",
|
||||
"aute", "cillum", "commodo", "consectetur", "consequat", "culpa",
|
||||
"cupidatat", "deserunt", "do", "dolor", "dolore", "duis",
|
||||
"ea", "eiusmod", "elit", "enim", "esse", "est",
|
||||
"et", "eu", "ex", "excepteur", "exercitation", "fugiat",
|
||||
"id", "in", "incididunt", "ipsum", "irure", "labore",
|
||||
"laboris", "laborum", "Lorem", "magna", "minim", "mollit",
|
||||
"nisi", "non", "nostrud", "nulla", "occaecat", "officia",
|
||||
"pariatur", "proident", "qui", "quis", "reprehenderit", "sint",
|
||||
"sit", "sunt", "tempor", "ullamco", "ut", "velit",
|
||||
"veniam", "voluptate",
|
||||
];
|
||||
@@ -2,6 +2,7 @@ import OperationConfig from "../config/OperationConfig.json";
|
||||
import Utils from "../Utils";
|
||||
import Recipe from "../Recipe";
|
||||
import Dish from "../Dish";
|
||||
import {detectFileType} from "./FileType";
|
||||
import chiSquared from "chi-squared";
|
||||
|
||||
/**
|
||||
@@ -92,7 +93,14 @@ class Magic {
|
||||
* @returns {string} [type.desc] - Description
|
||||
*/
|
||||
detectFileType() {
|
||||
return Magic.magicFileType(this.inputBuffer);
|
||||
const fileType = detectFileType(this.inputBuffer);
|
||||
|
||||
if (!fileType.length) return null;
|
||||
return {
|
||||
ext: fileType[0].extension,
|
||||
mime: fileType[0].mime,
|
||||
desc: fileType[0].description
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -785,452 +793,9 @@ class Magic {
|
||||
}[code];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a buffer, detects magic byte sequences at specific positions and returns the
|
||||
* extension and mime type.
|
||||
*
|
||||
* @param {Uint8Array} buf
|
||||
* @returns {Object} type
|
||||
* @returns {string} type.ext - File extension
|
||||
* @returns {string} type.mime - Mime type
|
||||
* @returns {string} [type.desc] - Description
|
||||
*/
|
||||
static magicFileType(buf) {
|
||||
if (!(buf && buf.length > 1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (buf[0] === 0xFF && buf[1] === 0xD8 && buf[2] === 0xFF) {
|
||||
return {
|
||||
ext: "jpg",
|
||||
mime: "image/jpeg"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4E && buf[3] === 0x47) {
|
||||
return {
|
||||
ext: "png",
|
||||
mime: "image/png"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) {
|
||||
return {
|
||||
ext: "gif",
|
||||
mime: "image/gif"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50) {
|
||||
return {
|
||||
ext: "webp",
|
||||
mime: "image/webp"
|
||||
};
|
||||
}
|
||||
|
||||
// needs to be before `tif` check
|
||||
if (((buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0x2A && buf[3] === 0x0) || (buf[0] === 0x4D && buf[1] === 0x4D && buf[2] === 0x0 && buf[3] === 0x2A)) && buf[8] === 0x43 && buf[9] === 0x52) {
|
||||
return {
|
||||
ext: "cr2",
|
||||
mime: "image/x-canon-cr2"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0x2A && buf[3] === 0x0) || (buf[0] === 0x4D && buf[1] === 0x4D && buf[2] === 0x0 && buf[3] === 0x2A)) {
|
||||
return {
|
||||
ext: "tif",
|
||||
mime: "image/tiff"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x42 && buf[1] === 0x4D) {
|
||||
return {
|
||||
ext: "bmp",
|
||||
mime: "image/bmp"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0xBC) {
|
||||
return {
|
||||
ext: "jxr",
|
||||
mime: "image/vnd.ms-photo"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x38 && buf[1] === 0x42 && buf[2] === 0x50 && buf[3] === 0x53) {
|
||||
return {
|
||||
ext: "psd",
|
||||
mime: "image/vnd.adobe.photoshop"
|
||||
};
|
||||
}
|
||||
|
||||
// needs to be before `zip` check
|
||||
if (buf[0] === 0x50 && buf[1] === 0x4B && buf[2] === 0x3 && buf[3] === 0x4 && buf[30] === 0x6D && buf[31] === 0x69 && buf[32] === 0x6D && buf[33] === 0x65 && buf[34] === 0x74 && buf[35] === 0x79 && buf[36] === 0x70 && buf[37] === 0x65 && buf[38] === 0x61 && buf[39] === 0x70 && buf[40] === 0x70 && buf[41] === 0x6C && buf[42] === 0x69 && buf[43] === 0x63 && buf[44] === 0x61 && buf[45] === 0x74 && buf[46] === 0x69 && buf[47] === 0x6F && buf[48] === 0x6E && buf[49] === 0x2F && buf[50] === 0x65 && buf[51] === 0x70 && buf[52] === 0x75 && buf[53] === 0x62 && buf[54] === 0x2B && buf[55] === 0x7A && buf[56] === 0x69 && buf[57] === 0x70) {
|
||||
return {
|
||||
ext: "epub",
|
||||
mime: "application/epub+zip"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x50 && buf[1] === 0x4B && (buf[2] === 0x3 || buf[2] === 0x5 || buf[2] === 0x7) && (buf[3] === 0x4 || buf[3] === 0x6 || buf[3] === 0x8)) {
|
||||
return {
|
||||
ext: "zip",
|
||||
mime: "application/zip"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[257] === 0x75 && buf[258] === 0x73 && buf[259] === 0x74 && buf[260] === 0x61 && buf[261] === 0x72) {
|
||||
return {
|
||||
ext: "tar",
|
||||
mime: "application/x-tar"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x52 && buf[1] === 0x61 && buf[2] === 0x72 && buf[3] === 0x21 && buf[4] === 0x1A && buf[5] === 0x7 && (buf[6] === 0x0 || buf[6] === 0x1)) {
|
||||
return {
|
||||
ext: "rar",
|
||||
mime: "application/x-rar-compressed"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x1F && buf[1] === 0x8B && buf[2] === 0x8) {
|
||||
return {
|
||||
ext: "gz",
|
||||
mime: "application/gzip"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x42 && buf[1] === 0x5A && buf[2] === 0x68) {
|
||||
return {
|
||||
ext: "bz2",
|
||||
mime: "application/x-bzip2"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x37 && buf[1] === 0x7A && buf[2] === 0xBC && buf[3] === 0xAF && buf[4] === 0x27 && buf[5] === 0x1C) {
|
||||
return {
|
||||
ext: "7z",
|
||||
mime: "application/x-7z-compressed"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x78 && buf[1] === 0x01) {
|
||||
return {
|
||||
ext: "dmg, zlib",
|
||||
mime: "application/x-apple-diskimage, application/x-deflate"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && (buf[3] === 0x18 || buf[3] === 0x20) && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70) || (buf[0] === 0x33 && buf[1] === 0x67 && buf[2] === 0x70 && buf[3] === 0x35) || (buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && buf[3] === 0x1C && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70 && buf[8] === 0x6D && buf[9] === 0x70 && buf[10] === 0x34 && buf[11] === 0x32 && buf[16] === 0x6D && buf[17] === 0x70 && buf[18] === 0x34 && buf[19] === 0x31 && buf[20] === 0x6D && buf[21] === 0x70 && buf[22] === 0x34 && buf[23] === 0x32 && buf[24] === 0x69 && buf[25] === 0x73 && buf[26] === 0x6F && buf[27] === 0x6D)) {
|
||||
return {
|
||||
ext: "mp4",
|
||||
mime: "video/mp4"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && buf[3] === 0x1C && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70 && buf[8] === 0x4D && buf[9] === 0x34 && buf[10] === 0x56)) {
|
||||
return {
|
||||
ext: "m4v",
|
||||
mime: "video/x-m4v"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x4D && buf[1] === 0x54 && buf[2] === 0x68 && buf[3] === 0x64) {
|
||||
return {
|
||||
ext: "mid",
|
||||
mime: "audio/midi"
|
||||
};
|
||||
}
|
||||
|
||||
// needs to be before the `webm` check
|
||||
if (buf[31] === 0x6D && buf[32] === 0x61 && buf[33] === 0x74 && buf[34] === 0x72 && buf[35] === 0x6f && buf[36] === 0x73 && buf[37] === 0x6B && buf[38] === 0x61) {
|
||||
return {
|
||||
ext: "mkv",
|
||||
mime: "video/x-matroska"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x1A && buf[1] === 0x45 && buf[2] === 0xDF && buf[3] === 0xA3) {
|
||||
return {
|
||||
ext: "webm",
|
||||
mime: "video/webm"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && buf[3] === 0x14 && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70) {
|
||||
return {
|
||||
ext: "mov",
|
||||
mime: "video/quicktime"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf[8] === 0x41 && buf[9] === 0x56 && buf[10] === 0x49) {
|
||||
return {
|
||||
ext: "avi",
|
||||
mime: "video/x-msvideo"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x30 && buf[1] === 0x26 && buf[2] === 0xB2 && buf[3] === 0x75 && buf[4] === 0x8E && buf[5] === 0x66 && buf[6] === 0xCF && buf[7] === 0x11 && buf[8] === 0xA6 && buf[9] === 0xD9) {
|
||||
return {
|
||||
ext: "wmv",
|
||||
mime: "video/x-ms-wmv"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x1 && buf[3].toString(16)[0] === "b") {
|
||||
return {
|
||||
ext: "mpg",
|
||||
mime: "video/mpeg"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33) || (buf[0] === 0xFF && buf[1] === 0xfb)) {
|
||||
return {
|
||||
ext: "mp3",
|
||||
mime: "audio/mpeg"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70 && buf[8] === 0x4D && buf[9] === 0x34 && buf[10] === 0x41) || (buf[0] === 0x4D && buf[1] === 0x34 && buf[2] === 0x41 && buf[3] === 0x20)) {
|
||||
return {
|
||||
ext: "m4a",
|
||||
mime: "audio/m4a"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x4F && buf[1] === 0x67 && buf[2] === 0x67 && buf[3] === 0x53) {
|
||||
return {
|
||||
ext: "ogg",
|
||||
mime: "audio/ogg"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x66 && buf[1] === 0x4C && buf[2] === 0x61 && buf[3] === 0x43) {
|
||||
return {
|
||||
ext: "flac",
|
||||
mime: "audio/x-flac"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf[8] === 0x57 && buf[9] === 0x41 && buf[10] === 0x56 && buf[11] === 0x45) {
|
||||
return {
|
||||
ext: "wav",
|
||||
mime: "audio/x-wav"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x23 && buf[1] === 0x21 && buf[2] === 0x41 && buf[3] === 0x4D && buf[4] === 0x52 && buf[5] === 0x0A) {
|
||||
return {
|
||||
ext: "amr",
|
||||
mime: "audio/amr"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x25 && buf[1] === 0x50 && buf[2] === 0x44 && buf[3] === 0x46) {
|
||||
return {
|
||||
ext: "pdf",
|
||||
mime: "application/pdf"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x4D && buf[1] === 0x5A) {
|
||||
return {
|
||||
ext: "exe",
|
||||
mime: "application/x-msdownload"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0] === 0x43 || buf[0] === 0x46) && buf[1] === 0x57 && buf[2] === 0x53) {
|
||||
return {
|
||||
ext: "swf",
|
||||
mime: "application/x-shockwave-flash"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x7B && buf[1] === 0x5C && buf[2] === 0x72 && buf[3] === 0x74 && buf[4] === 0x66) {
|
||||
return {
|
||||
ext: "rtf",
|
||||
mime: "application/rtf"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x77 && buf[1] === 0x4F && buf[2] === 0x46 && buf[3] === 0x46 && buf[4] === 0x00 && buf[5] === 0x01 && buf[6] === 0x00 && buf[7] === 0x00) {
|
||||
return {
|
||||
ext: "woff",
|
||||
mime: "application/font-woff"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x77 && buf[1] === 0x4F && buf[2] === 0x46 && buf[3] === 0x32 && buf[4] === 0x00 && buf[5] === 0x01 && buf[6] === 0x00 && buf[7] === 0x00) {
|
||||
return {
|
||||
ext: "woff2",
|
||||
mime: "application/font-woff"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[34] === 0x4C && buf[35] === 0x50 && ((buf[8] === 0x02 && buf[9] === 0x00 && buf[10] === 0x01) || (buf[8] === 0x01 && buf[9] === 0x00 && buf[10] === 0x00) || (buf[8] === 0x02 && buf[9] === 0x00 && buf[10] === 0x02))) {
|
||||
return {
|
||||
ext: "eot",
|
||||
mime: "application/octet-stream"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x00 && buf[1] === 0x01 && buf[2] === 0x00 && buf[3] === 0x00 && buf[4] === 0x00) {
|
||||
return {
|
||||
ext: "ttf",
|
||||
mime: "application/font-sfnt"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x4F && buf[1] === 0x54 && buf[2] === 0x54 && buf[3] === 0x4F && buf[4] === 0x00) {
|
||||
return {
|
||||
ext: "otf",
|
||||
mime: "application/font-sfnt"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0x01 && buf[3] === 0x00) {
|
||||
return {
|
||||
ext: "ico",
|
||||
mime: "image/x-icon"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x46 && buf[1] === 0x4C && buf[2] === 0x56 && buf[3] === 0x01) {
|
||||
return {
|
||||
ext: "flv",
|
||||
mime: "video/x-flv"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x25 && buf[1] === 0x21) {
|
||||
return {
|
||||
ext: "ps",
|
||||
mime: "application/postscript"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0xFD && buf[1] === 0x37 && buf[2] === 0x7A && buf[3] === 0x58 && buf[4] === 0x5A && buf[5] === 0x00) {
|
||||
return {
|
||||
ext: "xz",
|
||||
mime: "application/x-xz"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x53 && buf[1] === 0x51 && buf[2] === 0x4C && buf[3] === 0x69) {
|
||||
return {
|
||||
ext: "sqlite",
|
||||
mime: "application/x-sqlite3"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Added by n1474335 [n1474335@gmail.com] from here on
|
||||
*
|
||||
*/
|
||||
if ((buf[0] === 0x1F && buf[1] === 0x9D) || (buf[0] === 0x1F && buf[1] === 0xA0)) {
|
||||
return {
|
||||
ext: "z, tar.z",
|
||||
mime: "application/x-gtar"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x7F && buf[1] === 0x45 && buf[2] === 0x4C && buf[3] === 0x46) {
|
||||
return {
|
||||
ext: "none, axf, bin, elf, o, prx, puff, so",
|
||||
mime: "application/x-executable",
|
||||
desc: "Executable and Linkable Format file. No standard file extension."
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0xCA && buf[1] === 0xFE && buf[2] === 0xBA && buf[3] === 0xBE) {
|
||||
return {
|
||||
ext: "class",
|
||||
mime: "application/java-vm"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
|
||||
return {
|
||||
ext: "txt",
|
||||
mime: "text/plain",
|
||||
desc: "UTF-8 encoded Unicode byte order mark detected, commonly but not exclusively seen in text files."
|
||||
};
|
||||
}
|
||||
|
||||
// Must be before Little-endian UTF-16 BOM
|
||||
if (buf[0] === 0xFF && buf[1] === 0xFE && buf[2] === 0x00 && buf[3] === 0x00) {
|
||||
return {
|
||||
ext: "UTF32LE",
|
||||
mime: "charset/utf32le",
|
||||
desc: "Little-endian UTF-32 encoded Unicode byte order mark detected."
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0xFF && buf[1] === 0xFE) {
|
||||
return {
|
||||
ext: "UTF16LE",
|
||||
mime: "charset/utf16le",
|
||||
desc: "Little-endian UTF-16 encoded Unicode byte order mark detected."
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0x8001] === 0x43 && buf[0x8002] === 0x44 && buf[0x8003] === 0x30 && buf[0x8004] === 0x30 && buf[0x8005] === 0x31) ||
|
||||
(buf[0x8801] === 0x43 && buf[0x8802] === 0x44 && buf[0x8803] === 0x30 && buf[0x8804] === 0x30 && buf[0x8805] === 0x31) ||
|
||||
(buf[0x9001] === 0x43 && buf[0x9002] === 0x44 && buf[0x9003] === 0x30 && buf[0x9004] === 0x30 && buf[0x9005] === 0x31)) {
|
||||
return {
|
||||
ext: "iso",
|
||||
mime: "application/octet-stream",
|
||||
desc: "ISO 9660 CD/DVD image file"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0xD0 && buf[1] === 0xCF && buf[2] === 0x11 && buf[3] === 0xE0 && buf[4] === 0xA1 && buf[5] === 0xB1 && buf[6] === 0x1A && buf[7] === 0xE1) {
|
||||
return {
|
||||
ext: "doc, xls, ppt",
|
||||
mime: "application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint",
|
||||
desc: "Microsoft Office documents"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x64 && buf[1] === 0x65 && buf[2] === 0x78 && buf[3] === 0x0A && buf[4] === 0x30 && buf[5] === 0x33 && buf[6] === 0x35 && buf[7] === 0x00) {
|
||||
return {
|
||||
ext: "dex",
|
||||
mime: "application/octet-stream",
|
||||
desc: "Dalvik Executable (Android)"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x4B && buf[1] === 0x44 && buf[2] === 0x4D) {
|
||||
return {
|
||||
ext: "vmdk",
|
||||
mime: "application/vmdk, application/x-virtualbox-vmdk"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x43 && buf[1] === 0x72 && buf[2] === 0x32 && buf[3] === 0x34) {
|
||||
return {
|
||||
ext: "crx",
|
||||
mime: "application/crx",
|
||||
desc: "Google Chrome extension or packaged app"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x78 && (buf[1] === 0x01 || buf[1] === 0x9C || buf[1] === 0xDA || buf[1] === 0x5e)) {
|
||||
return {
|
||||
ext: "zlib",
|
||||
mime: "application/x-deflate"
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Byte frequencies of various languages generated from Wikipedia dumps taken in late 2017 and early 2018.
|
||||
* The Chi-Squared test cannot accept expected values of 0, so 0.0001 has been used to account for bytes
|
||||
|
||||
263
src/core/lib/Stream.mjs
Normal file
263
src/core/lib/Stream.mjs
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Stream class for parsing binary protocols.
|
||||
*
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @author tlwr [toby@toby.codes]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Stream can be used to traverse a binary blob, interpreting sections of it
|
||||
* as various data types.
|
||||
*/
|
||||
export default class Stream {
|
||||
|
||||
/**
|
||||
* Stream constructor.
|
||||
*
|
||||
* @param {Uint8Array} input
|
||||
*/
|
||||
constructor(input) {
|
||||
this.bytes = input;
|
||||
this.length = this.bytes.length;
|
||||
this.position = 0;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a number of bytes from the current position.
|
||||
*
|
||||
* @param {number} numBytes
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
getBytes(numBytes) {
|
||||
if (this.position > this.length) return undefined;
|
||||
|
||||
const newPosition = this.position + numBytes;
|
||||
const bytes = this.bytes.slice(this.position, newPosition);
|
||||
this.position = newPosition;
|
||||
this.bitPos = 0;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpret the following bytes as a string, stopping at the next null byte or
|
||||
* the supplied limit.
|
||||
*
|
||||
* @param {number} numBytes
|
||||
* @returns {string}
|
||||
*/
|
||||
readString(numBytes) {
|
||||
if (this.position > this.length) return undefined;
|
||||
|
||||
let result = "";
|
||||
for (let i = this.position; i < this.position + numBytes; i++) {
|
||||
const currentByte = this.bytes[i];
|
||||
if (currentByte === 0) break;
|
||||
result += String.fromCharCode(currentByte);
|
||||
}
|
||||
this.position += numBytes;
|
||||
this.bitPos = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpret the following bytes as an integer in big or little endian.
|
||||
*
|
||||
* @param {number} numBytes
|
||||
* @param {string} [endianness="be"]
|
||||
* @returns {number}
|
||||
*/
|
||||
readInt(numBytes, endianness="be") {
|
||||
if (this.position > this.length) return undefined;
|
||||
|
||||
let val = 0;
|
||||
if (endianness === "be") {
|
||||
for (let i = this.position; i < this.position + numBytes; i++) {
|
||||
val = val << 8;
|
||||
val |= this.bytes[i];
|
||||
}
|
||||
} else {
|
||||
for (let i = this.position + numBytes - 1; i >= this.position; i--) {
|
||||
val = val << 8;
|
||||
val |= this.bytes[i];
|
||||
}
|
||||
}
|
||||
this.position += numBytes;
|
||||
this.bitPos = 0;
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a number of bits from the buffer.
|
||||
*
|
||||
* @TODO Add endianness
|
||||
*
|
||||
* @param {number} numBits
|
||||
* @returns {number}
|
||||
*/
|
||||
readBits(numBits) {
|
||||
if (this.position > this.length) return undefined;
|
||||
|
||||
let bitBuf = 0,
|
||||
bitBufLen = 0;
|
||||
|
||||
// Add remaining bits from current byte
|
||||
bitBuf = (this.bytes[this.position++] & bitMask(this.bitPos)) >>> this.bitPos;
|
||||
bitBufLen = 8 - this.bitPos;
|
||||
this.bitPos = 0;
|
||||
|
||||
// Not enough bits yet
|
||||
while (bitBufLen < numBits) {
|
||||
bitBuf |= this.bytes[this.position++] << bitBufLen;
|
||||
bitBufLen += 8;
|
||||
}
|
||||
|
||||
// Reverse back to numBits
|
||||
if (bitBufLen > numBits) {
|
||||
const excess = bitBufLen - numBits;
|
||||
bitBuf &= (1 << numBits) - 1;
|
||||
bitBufLen -= excess;
|
||||
this.position--;
|
||||
this.bitPos = 8 - excess;
|
||||
}
|
||||
|
||||
return bitBuf;
|
||||
|
||||
/**
|
||||
* Calculates the bit mask based on the current bit position.
|
||||
*
|
||||
* @param {number} bitPos
|
||||
* @returns {number} The bit mask
|
||||
*/
|
||||
function bitMask(bitPos) {
|
||||
return 256 - (1 << bitPos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the stream until we reach the specified byte or sequence of bytes.
|
||||
*
|
||||
* @param {number|List<number>} val
|
||||
*/
|
||||
continueUntil(val) {
|
||||
if (this.position > this.length) return;
|
||||
|
||||
this.bitPos = 0;
|
||||
|
||||
if (typeof val === "number") {
|
||||
while (++this.position < this.length && this.bytes[this.position] !== val) {
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// val is an array
|
||||
let found = false;
|
||||
while (!found && this.position < this.length) {
|
||||
while (++this.position < this.length && this.bytes[this.position] !== val[0]) {
|
||||
continue;
|
||||
}
|
||||
found = true;
|
||||
for (let i = 1; i < val.length; i++) {
|
||||
if (this.position + i > this.length || this.bytes[this.position + i] !== val[i])
|
||||
found = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the next byte if it matches the supplied value.
|
||||
*
|
||||
* @param {number} val
|
||||
*/
|
||||
consumeIf(val) {
|
||||
if (this.bytes[this.position] === val) {
|
||||
this.position++;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forwards through the stream by the specified number of bytes.
|
||||
*
|
||||
* @param {number} numBytes
|
||||
*/
|
||||
moveForwardsBy(numBytes) {
|
||||
const pos = this.position + numBytes;
|
||||
if (pos < 0 || pos > this.length)
|
||||
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
|
||||
this.position = pos;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move backwards through the stream by the specified number of bytes.
|
||||
*
|
||||
* @param {number} numBytes
|
||||
*/
|
||||
moveBackwardsBy(numBytes) {
|
||||
const pos = this.position - numBytes;
|
||||
if (pos < 0 || pos > this.length)
|
||||
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
|
||||
this.position = pos;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move backwards through the strem by the specified number of bits.
|
||||
*
|
||||
* @param {number} numBits
|
||||
*/
|
||||
moveBackwardsByBits(numBits) {
|
||||
if (numBits <= this.bitPos) {
|
||||
this.bitPos -= numBits;
|
||||
} else {
|
||||
if (this.bitPos > 0) {
|
||||
numBits -= this.bitPos;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
|
||||
while (numBits > 0) {
|
||||
this.moveBackwardsBy(1);
|
||||
this.bitPos = 8;
|
||||
this.moveBackwardsByBits(numBits);
|
||||
numBits -= 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to a specified position in the stream.
|
||||
*
|
||||
* @param {number} pos
|
||||
*/
|
||||
moveTo(pos) {
|
||||
if (pos < 0 || pos > this.length)
|
||||
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
|
||||
this.position = pos;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are more bytes left in the stream.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasMore() {
|
||||
return this.position < this.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a slice of the stream up to the current position.
|
||||
*
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
carve() {
|
||||
if (this.bitPos > 0) this.position++;
|
||||
return this.bytes.slice(0, this.position);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import bsonjs from "bson";
|
||||
import bson from "bson";
|
||||
import OperationError from "../errors/OperationError";
|
||||
|
||||
/**
|
||||
@@ -36,8 +36,6 @@ class BSONDeserialise extends Operation {
|
||||
run(input, args) {
|
||||
if (!input.byteLength) return "";
|
||||
|
||||
const bson = new bsonjs();
|
||||
|
||||
try {
|
||||
const data = bson.deserialize(new Buffer(input));
|
||||
return JSON.stringify(data, null, 2);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import bsonjs from "bson";
|
||||
import bson from "bson";
|
||||
import OperationError from "../errors/OperationError";
|
||||
|
||||
/**
|
||||
@@ -36,8 +36,6 @@ class BSONSerialise extends Operation {
|
||||
run(input, args) {
|
||||
if (!input) return new ArrayBuffer();
|
||||
|
||||
const bson = new bsonjs();
|
||||
|
||||
try {
|
||||
const data = JSON.parse(input);
|
||||
return bson.serialize(data).buffer;
|
||||
|
||||
102
src/core/operations/BlurImage.mjs
Normal file
102
src/core/operations/BlurImage.mjs
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Blur Image operation
|
||||
*/
|
||||
class BlurImage extends Operation {
|
||||
|
||||
/**
|
||||
* BlurImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Blur Image";
|
||||
this.module = "Image";
|
||||
this.description = "Applies a blur effect to the image.<br><br>Gaussian blur is much slower than fast blur, but produces better results.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Gaussian_blur";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Amount",
|
||||
type: "number",
|
||||
value: 5,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
type: "option",
|
||||
value: ["Fast", "Gaussian"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [blurAmount, blurType] = args;
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
switch (blurType){
|
||||
case "Fast":
|
||||
image.blur(blurAmount);
|
||||
break;
|
||||
case "Gaussian":
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Gaussian blurring image. This may take a while...");
|
||||
image.gaussian(blurAmount);
|
||||
break;
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error blurring image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the blurred image using HTML for web apps
|
||||
*
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BlurImage;
|
||||
143
src/core/operations/ContainImage.mjs
Normal file
143
src/core/operations/ContainImage.mjs
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Contain Image operation
|
||||
*/
|
||||
class ContainImage extends Operation {
|
||||
|
||||
/**
|
||||
* ContainImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Contain Image";
|
||||
this.module = "Image";
|
||||
this.description = "Scales an image to the specified width and height, maintaining the aspect ratio. The image may be letterboxed.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Width",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Height",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Horizontal align",
|
||||
type: "option",
|
||||
value: [
|
||||
"Left",
|
||||
"Center",
|
||||
"Right"
|
||||
],
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "Vertical align",
|
||||
type: "option",
|
||||
value: [
|
||||
"Top",
|
||||
"Middle",
|
||||
"Bottom"
|
||||
],
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "Resizing algorithm",
|
||||
type: "option",
|
||||
value: [
|
||||
"Nearest Neighbour",
|
||||
"Bilinear",
|
||||
"Bicubic",
|
||||
"Hermite",
|
||||
"Bezier"
|
||||
],
|
||||
defaultIndex: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [width, height, hAlign, vAlign, alg] = args;
|
||||
|
||||
const resizeMap = {
|
||||
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
|
||||
"Bilinear": jimp.RESIZE_BILINEAR,
|
||||
"Bicubic": jimp.RESIZE_BICUBIC,
|
||||
"Hermite": jimp.RESIZE_HERMITE,
|
||||
"Bezier": jimp.RESIZE_BEZIER
|
||||
};
|
||||
|
||||
const alignMap = {
|
||||
"Left": jimp.HORIZONTAL_ALIGN_LEFT,
|
||||
"Center": jimp.HORIZONTAL_ALIGN_CENTER,
|
||||
"Right": jimp.HORIZONTAL_ALIGN_RIGHT,
|
||||
"Top": jimp.VERTICAL_ALIGN_TOP,
|
||||
"Middle": jimp.VERTICAL_ALIGN_MIDDLE,
|
||||
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM
|
||||
};
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Containing image...");
|
||||
image.contain(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error containing image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the contained image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ContainImage;
|
||||
95
src/core/operations/ConvertCoordinateFormat.mjs
Normal file
95
src/core/operations/ConvertCoordinateFormat.mjs
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import {FORMATS, convertCoordinates} from "../lib/ConvertCoordinates";
|
||||
|
||||
/**
|
||||
* Convert co-ordinate format operation
|
||||
*/
|
||||
class ConvertCoordinateFormat extends Operation {
|
||||
|
||||
/**
|
||||
* ConvertCoordinateFormat constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Convert co-ordinate format";
|
||||
this.module = "Hashing";
|
||||
this.description = "Converts geographical coordinates between different formats.<br><br>Supported formats:<ul><li>Degrees Minutes Seconds (DMS)</li><li>Degrees Decimal Minutes (DDM)</li><li>Decimal Degrees (DD)</li><li>Geohash</li><li>Military Grid Reference System (MGRS)</li><li>Ordnance Survey National Grid (OSNG)</li><li>Universal Transverse Mercator (UTM)</li></ul><br>The operation can try to detect the input co-ordinate format and delimiter automatically, but this may not always work correctly.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Geographic_coordinate_conversion";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Input Format",
|
||||
"type": "option",
|
||||
"value": ["Auto"].concat(FORMATS)
|
||||
},
|
||||
{
|
||||
"name": "Input Delimiter",
|
||||
"type": "option",
|
||||
"value": [
|
||||
"Auto",
|
||||
"Direction Preceding",
|
||||
"Direction Following",
|
||||
"\\n",
|
||||
"Comma",
|
||||
"Semi-colon",
|
||||
"Colon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Output Format",
|
||||
"type": "option",
|
||||
"value": FORMATS
|
||||
},
|
||||
{
|
||||
"name": "Output Delimiter",
|
||||
"type": "option",
|
||||
"value": [
|
||||
"Space",
|
||||
"\\n",
|
||||
"Comma",
|
||||
"Semi-colon",
|
||||
"Colon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Include Compass Directions",
|
||||
"type": "option",
|
||||
"value": [
|
||||
"None",
|
||||
"Before",
|
||||
"After"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Precision",
|
||||
"type": "number",
|
||||
"value": 3
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
if (input.replace(/[\s+]/g, "") !== "") {
|
||||
const [inFormat, inDelim, outFormat, outDelim, incDirection, precision] = args;
|
||||
const result = convertCoordinates(input, inFormat, inDelim, outFormat, outDelim, incDirection, precision);
|
||||
return result;
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ConvertCoordinateFormat;
|
||||
143
src/core/operations/CoverImage.mjs
Normal file
143
src/core/operations/CoverImage.mjs
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Cover Image operation
|
||||
*/
|
||||
class CoverImage extends Operation {
|
||||
|
||||
/**
|
||||
* CoverImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Cover Image";
|
||||
this.module = "Image";
|
||||
this.description = "Scales the image to the given width and height, keeping the aspect ratio. The image may be clipped.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Width",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Height",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Horizontal align",
|
||||
type: "option",
|
||||
value: [
|
||||
"Left",
|
||||
"Center",
|
||||
"Right"
|
||||
],
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "Vertical align",
|
||||
type: "option",
|
||||
value: [
|
||||
"Top",
|
||||
"Middle",
|
||||
"Bottom"
|
||||
],
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "Resizing algorithm",
|
||||
type: "option",
|
||||
value: [
|
||||
"Nearest Neighbour",
|
||||
"Bilinear",
|
||||
"Bicubic",
|
||||
"Hermite",
|
||||
"Bezier"
|
||||
],
|
||||
defaultIndex: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [width, height, hAlign, vAlign, alg] = args;
|
||||
|
||||
const resizeMap = {
|
||||
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
|
||||
"Bilinear": jimp.RESIZE_BILINEAR,
|
||||
"Bicubic": jimp.RESIZE_BICUBIC,
|
||||
"Hermite": jimp.RESIZE_HERMITE,
|
||||
"Bezier": jimp.RESIZE_BEZIER
|
||||
};
|
||||
|
||||
const alignMap = {
|
||||
"Left": jimp.HORIZONTAL_ALIGN_LEFT,
|
||||
"Center": jimp.HORIZONTAL_ALIGN_CENTER,
|
||||
"Right": jimp.HORIZONTAL_ALIGN_RIGHT,
|
||||
"Top": jimp.VERTICAL_ALIGN_TOP,
|
||||
"Middle": jimp.VERTICAL_ALIGN_MIDDLE,
|
||||
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM
|
||||
};
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Covering image...");
|
||||
image.cover(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error covering image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the covered image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CoverImage;
|
||||
144
src/core/operations/CropImage.mjs
Normal file
144
src/core/operations/CropImage.mjs
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Crop Image operation
|
||||
*/
|
||||
class CropImage extends Operation {
|
||||
|
||||
/**
|
||||
* CropImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Crop Image";
|
||||
this.module = "Image";
|
||||
this.description = "Crops an image to the specified region, or automatically crops edges.<br><br><b><u>Autocrop</u></b><br>Automatically crops same-colour borders from the image.<br><br><u>Autocrop tolerance</u><br>A percentage value for the tolerance of colour difference between pixels.<br><br><u>Only autocrop frames</u><br>Only crop real frames (all sides must have the same border)<br><br><u>Symmetric autocrop</u><br>Force autocrop to be symmetric (top/bottom and left/right are cropped by the same amount)<br><br><u>Autocrop keep border</u><br>The number of pixels of border to leave around the image.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Cropping_(image)";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "X Position",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: 0
|
||||
},
|
||||
{
|
||||
name: "Y Position",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: 0
|
||||
},
|
||||
{
|
||||
name: "Width",
|
||||
type: "number",
|
||||
value: 10,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Height",
|
||||
type: "number",
|
||||
value: 10,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Autocrop",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Autocrop tolerance (%)",
|
||||
type: "number",
|
||||
value: 0.02,
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 0.01
|
||||
},
|
||||
{
|
||||
name: "Only autocrop frames",
|
||||
type: "boolean",
|
||||
value: true
|
||||
},
|
||||
{
|
||||
name: "Symmetric autocrop",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Autocrop keep border (px)",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: 0
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [xPos, yPos, width, height, autocrop, autoTolerance, autoFrames, autoSymmetric, autoBorder] = args;
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Cropping image...");
|
||||
if (autocrop) {
|
||||
image.autocrop({
|
||||
tolerance: (autoTolerance / 100),
|
||||
cropOnlyFrames: autoFrames,
|
||||
cropSymmetric: autoSymmetric,
|
||||
leaveBorder: autoBorder
|
||||
});
|
||||
} else {
|
||||
image.crop(xPos, yPos, width, height);
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error cropping image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the cropped image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CropImage;
|
||||
125
src/core/operations/DNSOverHTTPS.mjs
Normal file
125
src/core/operations/DNSOverHTTPS.mjs
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @author h345983745
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
|
||||
/**
|
||||
* DNS over HTTPS operation
|
||||
*/
|
||||
class DNSOverHTTPS extends Operation {
|
||||
|
||||
/**
|
||||
* DNSOverHTTPS constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "DNS over HTTPS";
|
||||
this.module = "Default";
|
||||
this.description = [
|
||||
"Takes a single domain name and performs a DNS lookup using DNS over HTTPS.",
|
||||
"<br><br>",
|
||||
"By default, <a href='https://developers.cloudflare.com/1.1.1.1/dns-over-https/'>Cloudflare</a> and <a href='https://developers.google.com/speed/public-dns/docs/dns-over-https'>Google</a> DNS over HTTPS services are supported.",
|
||||
"<br><br>",
|
||||
"Can be used with any service that supports the GET parameters <code>name</code> and <code>type</code>."
|
||||
].join("\n");
|
||||
this.infoURL = "https://wikipedia.org/wiki/DNS_over_HTTPS";
|
||||
this.inputType = "string";
|
||||
this.outputType = "JSON";
|
||||
this.manualBake = true;
|
||||
this.args = [
|
||||
{
|
||||
name: "Resolver",
|
||||
type: "editableOption",
|
||||
value: [
|
||||
{
|
||||
name: "Google",
|
||||
value: "https://dns.google.com/resolve"
|
||||
},
|
||||
{
|
||||
name: "Cloudflare",
|
||||
value: "https://cloudflare-dns.com/dns-query"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Request Type",
|
||||
type: "option",
|
||||
value: [
|
||||
"A",
|
||||
"AAAA",
|
||||
"TXT",
|
||||
"MX",
|
||||
"DNSKEY",
|
||||
"NS"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Answer Data Only",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Validate DNSSEC",
|
||||
type: "boolean",
|
||||
value: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {JSON}
|
||||
*/
|
||||
run(input, args) {
|
||||
const [resolver, requestType, justAnswer, DNSSEC] = args;
|
||||
let url = URL;
|
||||
try {
|
||||
url = new URL(resolver);
|
||||
} catch (error) {
|
||||
throw new OperationError(error.toString() +
|
||||
"\n\nThis error could be caused by one of the following:\n" +
|
||||
" - An invalid Resolver URL\n");
|
||||
}
|
||||
const params = {name: input, type: requestType, cd: DNSSEC};
|
||||
|
||||
url.search = new URLSearchParams(params);
|
||||
|
||||
return fetch(url, {headers: {"accept": "application/dns-json"}}).then(response => {
|
||||
return response.json();
|
||||
}).then(data => {
|
||||
if (justAnswer) {
|
||||
return extractData(data.Answer);
|
||||
}
|
||||
return data;
|
||||
}).catch(e => {
|
||||
throw new OperationError(`Error making request to ${url}\n${e.toString()}`);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an array of just data from a DNS Answer section
|
||||
*
|
||||
* @private
|
||||
* @param {JSON} data
|
||||
* @returns {JSON}
|
||||
*/
|
||||
function extractData(data) {
|
||||
if (typeof(data) == "undefined"){
|
||||
return [];
|
||||
} else {
|
||||
const dataValues = [];
|
||||
data.forEach(element => {
|
||||
dataValues.push(element.data);
|
||||
});
|
||||
return dataValues;
|
||||
}
|
||||
}
|
||||
|
||||
export default DNSOverHTTPS;
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Magic from "../lib/Magic";
|
||||
import {detectFileType} from "../lib/FileType";
|
||||
import {FILE_SIGNATURES} from "../lib/FileSignatures";
|
||||
|
||||
/**
|
||||
* Detect File Type operation
|
||||
@@ -24,7 +25,13 @@ class DetectFileType extends Operation {
|
||||
this.infoURL = "https://wikipedia.org/wiki/List_of_file_signatures";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
this.args = Object.keys(FILE_SIGNATURES).map(cat => {
|
||||
return {
|
||||
name: cat,
|
||||
type: "boolean",
|
||||
value: true
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,17 +41,27 @@ class DetectFileType extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const data = new Uint8Array(input),
|
||||
type = Magic.magicFileType(data);
|
||||
categories = [];
|
||||
|
||||
if (!type) {
|
||||
args.forEach((cat, i) => {
|
||||
if (cat) categories.push(Object.keys(FILE_SIGNATURES)[i]);
|
||||
});
|
||||
|
||||
const types = detectFileType(data, categories);
|
||||
|
||||
if (!types.length) {
|
||||
return "Unknown file type. Have you tried checking the entropy of this data to determine whether it might be encrypted or compressed?";
|
||||
} else {
|
||||
let output = "File extension: " + type.ext + "\n" +
|
||||
"MIME type: " + type.mime;
|
||||
let output = "";
|
||||
|
||||
if (type.desc && type.desc.length) {
|
||||
output += "\nDescription: " + type.desc;
|
||||
}
|
||||
types.forEach(type => {
|
||||
output += "File extension: " + type.extension + "\n" +
|
||||
"MIME type: " + type.mime + "\n";
|
||||
|
||||
if (type.description && type.description.length) {
|
||||
output += "\nDescription: " + type.description + "\n";
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
79
src/core/operations/DitherImage.mjs
Normal file
79
src/core/operations/DitherImage.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Image Dither operation
|
||||
*/
|
||||
class DitherImage extends Operation {
|
||||
|
||||
/**
|
||||
* DitherImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Dither Image";
|
||||
this.module = "Image";
|
||||
this.description = "Apply a dither effect to an image.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Dither";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Applying dither to image...");
|
||||
image.dither565();
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error applying dither to image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the dithered image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DitherImage;
|
||||
@@ -43,7 +43,7 @@ class Divide extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const val = div(createNumArray(input, args[0]));
|
||||
return val instanceof BigNumber ? val : new BigNumber(NaN);
|
||||
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
100
src/core/operations/ExtractFiles.mjs
Normal file
100
src/core/operations/ExtractFiles.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import {scanForFileTypes, extractFile} from "../lib/FileType";
|
||||
import {FILE_SIGNATURES} from "../lib/FileSignatures";
|
||||
|
||||
/**
|
||||
* Extract Files operation
|
||||
*/
|
||||
class ExtractFiles extends Operation {
|
||||
|
||||
/**
|
||||
* ExtractFiles constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Extract Files";
|
||||
this.module = "Default";
|
||||
this.description = "TODO";
|
||||
this.infoURL = "https://forensicswiki.org/wiki/File_Carving";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "List<File>";
|
||||
this.presentType = "html";
|
||||
this.args = Object.keys(FILE_SIGNATURES).map(cat => {
|
||||
return {
|
||||
name: cat,
|
||||
type: "boolean",
|
||||
value: cat === "Miscellaneous" ? false : true
|
||||
};
|
||||
}).concat([
|
||||
{
|
||||
name: "Ignore failed extractions",
|
||||
type: "boolean",
|
||||
value: "true"
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} input
|
||||
* @param {Object[]} args
|
||||
* @returns {List<File>}
|
||||
*/
|
||||
run(input, args) {
|
||||
const bytes = new Uint8Array(input),
|
||||
categories = [],
|
||||
ignoreFailedExtractions = args.pop(1);
|
||||
|
||||
args.forEach((cat, i) => {
|
||||
if (cat) categories.push(Object.keys(FILE_SIGNATURES)[i]);
|
||||
});
|
||||
|
||||
// Scan for embedded files
|
||||
const detectedFiles = scanForFileTypes(bytes, categories);
|
||||
|
||||
// Extract each file that we support
|
||||
const files = [];
|
||||
const errors = [];
|
||||
detectedFiles.forEach(detectedFile => {
|
||||
try {
|
||||
files.push(extractFile(bytes, detectedFile.fileDetails, detectedFile.offset));
|
||||
} catch (err) {
|
||||
if (!ignoreFailedExtractions && err.message.indexOf("No extraction algorithm available") < 0) {
|
||||
errors.push(
|
||||
`Error while attempting to extract ${detectedFile.fileDetails.name} ` +
|
||||
`at offset ${detectedFile.offset}:\n` +
|
||||
`${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
throw new OperationError(errors.join("\n\n"));
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays the files in HTML for web apps.
|
||||
*
|
||||
* @param {File[]} files
|
||||
* @returns {html}
|
||||
*/
|
||||
async present(files) {
|
||||
return await Utils.displayFilesAsHTML(files);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ExtractFiles;
|
||||
94
src/core/operations/FlipImage.mjs
Normal file
94
src/core/operations/FlipImage.mjs
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Flip Image operation
|
||||
*/
|
||||
class FlipImage extends Operation {
|
||||
|
||||
/**
|
||||
* FlipImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Flip Image";
|
||||
this.module = "Image";
|
||||
this.description = "Flips an image along its X or Y axis.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Axis",
|
||||
type: "option",
|
||||
value: ["Horizontal", "Vertical"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [flipAxis] = args;
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid input file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Flipping image...");
|
||||
switch (flipAxis){
|
||||
case "Horizontal":
|
||||
image.flip(true, false);
|
||||
break;
|
||||
case "Vertical":
|
||||
image.flip(false, true);
|
||||
break;
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error flipping image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the flipped image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FlipImage;
|
||||
@@ -89,7 +89,7 @@ class Fork extends Operation {
|
||||
// Run recipe over each tranche
|
||||
for (i = 0; i < inputs.length; i++) {
|
||||
// Baseline ing values for each tranche so that registers are reset
|
||||
subOpList.forEach((op, i) => {
|
||||
recipe.opList.forEach((op, i) => {
|
||||
op.ingValues = JSON.parse(JSON.stringify(ingValues[i]));
|
||||
});
|
||||
|
||||
|
||||
39
src/core/operations/FromCaseInsensitiveRegex.mjs
Normal file
39
src/core/operations/FromCaseInsensitiveRegex.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @author masq [github.cyberchef@masq.cc]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
|
||||
/**
|
||||
* From Case Insensitive Regex operation
|
||||
*/
|
||||
class FromCaseInsensitiveRegex extends Operation {
|
||||
|
||||
/**
|
||||
* FromCaseInsensitiveRegex constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "From Case Insensitive Regex";
|
||||
this.module = "Default";
|
||||
this.description = "Converts a case-insensitive regex string to a case sensitive regex string (no guarantee on it being the proper original casing) in case the i flag wasn't available at the time but now is, or you need it to be case-sensitive again.<br><br>e.g. <code>[mM][oO][zZ][iI][lL][lL][aA]/[0-9].[0-9] .*</code> becomes <code>Mozilla/[0-9].[0-9] .*</code>";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Regular_expression";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
return input.replace(/\[[a-z]{2}\]/ig, m => m[1].toUpperCase() === m[2].toUpperCase() ? m[1] : m);
|
||||
}
|
||||
}
|
||||
|
||||
export default FromCaseInsensitiveRegex;
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* @author gchq77703 []
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import geohash from "ngeohash";
|
||||
|
||||
/**
|
||||
* From Geohash operation
|
||||
*/
|
||||
class FromGeohash extends Operation {
|
||||
|
||||
/**
|
||||
* FromGeohash constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "From Geohash";
|
||||
this.module = "Crypto";
|
||||
this.description = "Converts Geohash strings into Lat/Long coordinates. For example, <code>ww8p1r4t8</code> becomes <code>37.8324,112.5584</code>.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Geohash";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
return input.split("\n").map(line => {
|
||||
const coords = geohash.decode(line);
|
||||
return [coords.latitude, coords.longitude].join(",");
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FromGeohash;
|
||||
70
src/core/operations/GenerateLoremIpsum.mjs
Normal file
70
src/core/operations/GenerateLoremIpsum.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @author klaxon [klaxon@veyr.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { GenerateParagraphs, GenerateSentences, GenerateWords, GenerateBytes } from "../lib/LoremIpsum";
|
||||
|
||||
/**
|
||||
* Generate Lorem Ipsum operation
|
||||
*/
|
||||
class GenerateLoremIpsum extends Operation {
|
||||
|
||||
/**
|
||||
* GenerateLoremIpsum constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Generate Lorem Ipsum";
|
||||
this.module = "Default";
|
||||
this.description = "Generate varying length lorem ipsum placeholder text.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Lorem_ipsum";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Length",
|
||||
"type": "number",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"name": "Length in",
|
||||
"type": "option",
|
||||
"value": ["Paragraphs", "Sentences", "Words", "Bytes"]
|
||||
}
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const [length, lengthType] = args;
|
||||
if (length < 1){
|
||||
throw new OperationError("Length must be greater than 0");
|
||||
}
|
||||
switch (lengthType) {
|
||||
case "Paragraphs":
|
||||
return GenerateParagraphs(length);
|
||||
case "Sentences":
|
||||
return GenerateSentences(length);
|
||||
case "Words":
|
||||
return GenerateWords(length);
|
||||
case "Bytes":
|
||||
return GenerateBytes(length);
|
||||
default:
|
||||
throw new OperationError("Invalid length type");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default GenerateLoremIpsum;
|
||||
@@ -8,7 +8,7 @@ import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import qr from "qr-image";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import Magic from "../lib/Magic";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import Utils from "../Utils";
|
||||
|
||||
/**
|
||||
@@ -100,9 +100,9 @@ class GenerateQRCode extends Operation {
|
||||
|
||||
if (format === "PNG") {
|
||||
let dataURI = "data:";
|
||||
const type = Magic.magicFileType(data);
|
||||
if (type && type.mime.indexOf("image") === 0){
|
||||
dataURI += type.mime + ";";
|
||||
const mime = isImage(data);
|
||||
if (mime){
|
||||
dataURI += mime + ";";
|
||||
} else {
|
||||
throw new OperationError("Invalid PNG file generated by QR image");
|
||||
}
|
||||
|
||||
103
src/core/operations/ImageBrightnessContrast.mjs
Normal file
103
src/core/operations/ImageBrightnessContrast.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Image Brightness / Contrast operation
|
||||
*/
|
||||
class ImageBrightnessContrast extends Operation {
|
||||
|
||||
/**
|
||||
* ImageBrightnessContrast constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Image Brightness / Contrast";
|
||||
this.module = "Image";
|
||||
this.description = "Adjust the brightness or contrast of an image.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Brightness",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: -100,
|
||||
max: 100
|
||||
},
|
||||
{
|
||||
name: "Contrast",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: -100,
|
||||
max: 100
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [brightness, contrast] = args;
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (brightness !== 0) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image brightness...");
|
||||
image.brightness(brightness / 100);
|
||||
}
|
||||
if (contrast !== 0) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image contrast...");
|
||||
image.contrast(contrast / 100);
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error adjusting image brightness or contrast. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ImageBrightnessContrast;
|
||||
94
src/core/operations/ImageFilter.mjs
Normal file
94
src/core/operations/ImageFilter.mjs
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Image Filter operation
|
||||
*/
|
||||
class ImageFilter extends Operation {
|
||||
|
||||
/**
|
||||
* ImageFilter constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Image Filter";
|
||||
this.module = "Image";
|
||||
this.description = "Applies a greyscale or sepia filter to an image.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Filter type",
|
||||
type: "option",
|
||||
value: [
|
||||
"Greyscale",
|
||||
"Sepia"
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [filterType] = args;
|
||||
if (!isImage(input)){
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Applying " + filterType.toLowerCase() + " filter to image...");
|
||||
if (filterType === "Greyscale") {
|
||||
image.greyscale();
|
||||
} else {
|
||||
image.sepia();
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error applying filter to image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the blurred image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ImageFilter;
|
||||
129
src/core/operations/ImageHueSaturationLightness.mjs
Normal file
129
src/core/operations/ImageHueSaturationLightness.mjs
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Image Hue/Saturation/Lightness operation
|
||||
*/
|
||||
class ImageHueSaturationLightness extends Operation {
|
||||
|
||||
/**
|
||||
* ImageHueSaturationLightness constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Image Hue/Saturation/Lightness";
|
||||
this.module = "Image";
|
||||
this.description = "Adjusts the hue / saturation / lightness (HSL) values of an image.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Hue",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: -360,
|
||||
max: 360
|
||||
},
|
||||
{
|
||||
name: "Saturation",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: -100,
|
||||
max: 100
|
||||
},
|
||||
{
|
||||
name: "Lightness",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: -100,
|
||||
max: 100
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [hue, saturation, lightness] = args;
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (hue !== 0) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image hue...");
|
||||
image.colour([
|
||||
{
|
||||
apply: "hue",
|
||||
params: [hue]
|
||||
}
|
||||
]);
|
||||
}
|
||||
if (saturation !== 0) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image saturation...");
|
||||
image.colour([
|
||||
{
|
||||
apply: "saturate",
|
||||
params: [saturation]
|
||||
}
|
||||
]);
|
||||
}
|
||||
if (lightness !== 0) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image lightness...");
|
||||
image.colour([
|
||||
{
|
||||
apply: "lighten",
|
||||
params: [lightness]
|
||||
}
|
||||
]);
|
||||
}
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error adjusting image hue / saturation / lightness. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageHueSaturationLightness;
|
||||
89
src/core/operations/ImageOpacity.mjs
Normal file
89
src/core/operations/ImageOpacity.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Image Opacity operation
|
||||
*/
|
||||
class ImageOpacity extends Operation {
|
||||
|
||||
/**
|
||||
* ImageOpacity constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Image Opacity";
|
||||
this.module = "Image";
|
||||
this.description = "Adjust the opacity of an image.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Opacity (%)",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 0,
|
||||
max: 100
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [opacity] = args;
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image opacity...");
|
||||
image.opacity(opacity / 100);
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error changing image opacity. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ImageOpacity;
|
||||
79
src/core/operations/InvertImage.mjs
Normal file
79
src/core/operations/InvertImage.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Invert Image operation
|
||||
*/
|
||||
class InvertImage extends Operation {
|
||||
|
||||
/**
|
||||
* InvertImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Invert Image";
|
||||
this.module = "Image";
|
||||
this.description = "Invert the colours of an image.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid input file format.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Inverting image...");
|
||||
image.invert();
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error inverting image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the inverted image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default InvertImage;
|
||||
@@ -43,7 +43,7 @@ class Mean extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const val = mean(createNumArray(input, args[0]));
|
||||
return val instanceof BigNumber ? val : new BigNumber(NaN);
|
||||
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class Median extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const val = median(createNumArray(input, args[0]));
|
||||
return val instanceof BigNumber ? val : new BigNumber(NaN);
|
||||
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class Multiply extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const val = multi(createNumArray(input, args[0]));
|
||||
return val instanceof BigNumber ? val : new BigNumber(NaN);
|
||||
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
70
src/core/operations/NormaliseImage.mjs
Normal file
70
src/core/operations/NormaliseImage.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Normalise Image operation
|
||||
*/
|
||||
class NormaliseImage extends Operation {
|
||||
|
||||
/**
|
||||
* NormaliseImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Normalise Image";
|
||||
this.module = "Image";
|
||||
this.description = "Normalise the image colours.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType= "html";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
const image = await jimp.read(Buffer.from(input));
|
||||
|
||||
image.normalize();
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the normalised image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default NormaliseImage;
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Magic from "../lib/Magic";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import jsqr from "jsqr";
|
||||
import jimp from "jimp";
|
||||
|
||||
@@ -42,64 +42,61 @@ class ParseQRCode extends Operation {
|
||||
* @returns {string}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const type = Magic.magicFileType(input);
|
||||
const [normalise] = args;
|
||||
|
||||
// Make sure that the input is an image
|
||||
if (type && type.mime.indexOf("image") === 0) {
|
||||
let image = input;
|
||||
if (!isImage(input)) throw new OperationError("Invalid file type.");
|
||||
|
||||
if (normalise) {
|
||||
// Process the image to be easier to read by jsqr
|
||||
// Disables the alpha channel
|
||||
// Sets the image default background to white
|
||||
// Normalises the image colours
|
||||
// Makes the image greyscale
|
||||
// Converts image to a JPEG
|
||||
image = await new Promise((resolve, reject) => {
|
||||
jimp.read(Buffer.from(input))
|
||||
.then(image => {
|
||||
image
|
||||
.rgba(false)
|
||||
.background(0xFFFFFFFF)
|
||||
.normalize()
|
||||
.greyscale()
|
||||
.getBuffer(jimp.MIME_JPEG, (error, result) => {
|
||||
resolve(result);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
reject(new OperationError("Error reading the image file."));
|
||||
});
|
||||
});
|
||||
}
|
||||
let image = input;
|
||||
|
||||
if (image instanceof OperationError) {
|
||||
throw image;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
jimp.read(Buffer.from(image))
|
||||
if (normalise) {
|
||||
// Process the image to be easier to read by jsqr
|
||||
// Disables the alpha channel
|
||||
// Sets the image default background to white
|
||||
// Normalises the image colours
|
||||
// Makes the image greyscale
|
||||
// Converts image to a JPEG
|
||||
image = await new Promise((resolve, reject) => {
|
||||
jimp.read(Buffer.from(input))
|
||||
.then(image => {
|
||||
if (image.bitmap != null) {
|
||||
const qrData = jsqr(image.bitmap.data, image.getWidth(), image.getHeight());
|
||||
if (qrData != null) {
|
||||
resolve(qrData.data);
|
||||
} else {
|
||||
reject(new OperationError("Couldn't read a QR code from the image."));
|
||||
}
|
||||
} else {
|
||||
reject(new OperationError("Error reading the image file."));
|
||||
}
|
||||
image
|
||||
.rgba(false)
|
||||
.background(0xFFFFFFFF)
|
||||
.normalize()
|
||||
.greyscale()
|
||||
.getBuffer(jimp.MIME_JPEG, (error, result) => {
|
||||
resolve(result);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
reject(new OperationError("Error reading the image file."));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
if (image instanceof OperationError) {
|
||||
throw image;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
jimp.read(Buffer.from(image))
|
||||
.then(image => {
|
||||
if (image.bitmap != null) {
|
||||
const qrData = jsqr(image.bitmap.data, image.getWidth(), image.getHeight());
|
||||
if (qrData != null) {
|
||||
resolve(qrData.data);
|
||||
} else {
|
||||
reject(new OperationError("Couldn't read a QR code from the image."));
|
||||
}
|
||||
} else {
|
||||
reject(new OperationError("Error reading the image file."));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
reject(new OperationError("Error reading the image file."));
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -181,8 +181,8 @@ class ParseX509Certificate extends Operation {
|
||||
Serial number: ${new r.BigInteger(sn, 16).toString()} (0x${sn})
|
||||
Algorithm ID: ${cert.getSignatureAlgorithmField()}
|
||||
Validity
|
||||
Not Before: ${nbDate} (dd-mm-yy hh:mm:ss) (${cert.getNotBefore()})
|
||||
Not After: ${naDate} (dd-mm-yy hh:mm:ss) (${cert.getNotAfter()})
|
||||
Not Before: ${nbDate} (dd-mm-yyyy hh:mm:ss) (${cert.getNotBefore()})
|
||||
Not After: ${naDate} (dd-mm-yyyy hh:mm:ss) (${cert.getNotAfter()})
|
||||
Issuer
|
||||
${issuerStr}
|
||||
Subject
|
||||
@@ -206,12 +206,15 @@ ${extensions}`;
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatDate (dateStr) {
|
||||
return dateStr[4] + dateStr[5] + "/" +
|
||||
dateStr[2] + dateStr[3] + "/" +
|
||||
dateStr[0] + dateStr[1] + " " +
|
||||
dateStr[6] + dateStr[7] + ":" +
|
||||
if (dateStr.length === 13) { // UTC Time
|
||||
dateStr = (dateStr[0] < "5" ? "20" : "19") + dateStr;
|
||||
}
|
||||
return dateStr[6] + dateStr[7] + "/" +
|
||||
dateStr[4] + dateStr[5] + "/" +
|
||||
dateStr[0] + dateStr[1] + dateStr[2] + dateStr[3] + " " +
|
||||
dateStr[8] + dateStr[9] + ":" +
|
||||
dateStr[10] + dateStr[11];
|
||||
dateStr[10] + dateStr[11] + ":" +
|
||||
dateStr[12] + dateStr[13];
|
||||
}
|
||||
|
||||
export default ParseX509Certificate;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { fromHex } from "../lib/Hex";
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import Magic from "../lib/Magic";
|
||||
import { isType, detectFileType } from "../lib/FileType";
|
||||
|
||||
/**
|
||||
* PlayMedia operation
|
||||
@@ -66,8 +66,7 @@ class PlayMedia extends Operation {
|
||||
|
||||
|
||||
// Determine file type
|
||||
const type = Magic.magicFileType(input);
|
||||
if (!(type && /^audio|video/.test(type.mime))) {
|
||||
if (!isType(/^(audio|video)/, input)) {
|
||||
throw new OperationError("Invalid or unrecognised file type");
|
||||
}
|
||||
|
||||
@@ -84,15 +83,15 @@ class PlayMedia extends Operation {
|
||||
async present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = Magic.magicFileType(data);
|
||||
const matches = /^audio|video/.exec(type.mime);
|
||||
const types = detectFileType(data);
|
||||
const matches = /^audio|video/.exec(types[0].mime);
|
||||
if (!matches) {
|
||||
throw new OperationError("Invalid file type");
|
||||
}
|
||||
const dataURI = `data:${type.mime};base64,${toBase64(data)}`;
|
||||
const dataURI = `data:${types[0].mime};base64,${toBase64(data)}`;
|
||||
const element = matches[0];
|
||||
|
||||
let html = `<${element} src='${dataURI}' type='${type.mime}' controls>`;
|
||||
let html = `<${element} src='${dataURI}' type='${types[0].mime}' controls>`;
|
||||
html += "<p>Unsupported media type.</p>";
|
||||
html += `</${element}>`;
|
||||
return html;
|
||||
|
||||
@@ -228,40 +228,29 @@ function regexList (input, regex, displayTotal, matches, captureGroups) {
|
||||
function regexHighlight (input, regex, displayTotal) {
|
||||
let output = "",
|
||||
title = "",
|
||||
m,
|
||||
hl = 1,
|
||||
i = 0,
|
||||
total = 0;
|
||||
|
||||
while ((m = regex.exec(input))) {
|
||||
// Moves pointer when an empty string is matched (prevents infinite loop)
|
||||
if (m.index === regex.lastIndex) {
|
||||
regex.lastIndex++;
|
||||
}
|
||||
output = input.replace(regex, (match, ...args) => {
|
||||
args.pop(); // Throw away full string
|
||||
const offset = args.pop(),
|
||||
groups = args;
|
||||
|
||||
// Add up to match
|
||||
output += Utils.escapeHtml(input.slice(i, m.index));
|
||||
|
||||
title = `Offset: ${m.index}\n`;
|
||||
if (m.length > 1) {
|
||||
title = `Offset: ${offset}\n`;
|
||||
if (groups.length) {
|
||||
title += "Groups:\n";
|
||||
for (let n = 1; n < m.length; ++n) {
|
||||
title += `\t${n}: ${m[n]}\n`;
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
title += `\t${i+1}: ${Utils.escapeHtml(groups[i] || "")}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add match with highlighting
|
||||
output += "<span class='hl"+hl+"' title='"+title+"'>" + Utils.escapeHtml(m[0]) + "</span>";
|
||||
|
||||
// Switch highlight
|
||||
hl = hl === 1 ? 2 : 1;
|
||||
|
||||
i = regex.lastIndex;
|
||||
total++;
|
||||
}
|
||||
|
||||
// Add all after final match
|
||||
output += Utils.escapeHtml(input.slice(i, input.length));
|
||||
return `<span class='hl${hl}' title='${title}'>${Utils.escapeHtml(match)}</span>`;
|
||||
});
|
||||
|
||||
if (displayTotal)
|
||||
output = "Total found: " + total + "\n\n" + output;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { fromHex } from "../lib/Hex";
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import Magic from "../lib/Magic";
|
||||
import {isImage} from "../lib/FileType";
|
||||
|
||||
/**
|
||||
* Render Image operation
|
||||
@@ -72,8 +72,7 @@ class RenderImage extends Operation {
|
||||
}
|
||||
|
||||
// Determine file type
|
||||
const type = Magic.magicFileType(input);
|
||||
if (!(type && type.mime.indexOf("image") === 0)) {
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type");
|
||||
}
|
||||
|
||||
@@ -92,9 +91,9 @@ class RenderImage extends Operation {
|
||||
let dataURI = "data:";
|
||||
|
||||
// Determine file type
|
||||
const type = Magic.magicFileType(data);
|
||||
if (type && type.mime.indexOf("image") === 0) {
|
||||
dataURI += type.mime + ";";
|
||||
const mime = isImage(data);
|
||||
if (mime) {
|
||||
dataURI += mime + ";";
|
||||
} else {
|
||||
throw new OperationError("Invalid file type");
|
||||
}
|
||||
|
||||
138
src/core/operations/ResizeImage.mjs
Normal file
138
src/core/operations/ResizeImage.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Resize Image operation
|
||||
*/
|
||||
class ResizeImage extends Operation {
|
||||
|
||||
/**
|
||||
* ResizeImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Resize Image";
|
||||
this.module = "Image";
|
||||
this.description = "Resizes an image to the specified width and height values.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Image_scaling";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Width",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Height",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Unit type",
|
||||
type: "option",
|
||||
value: ["Pixels", "Percent"]
|
||||
},
|
||||
{
|
||||
name: "Maintain aspect ratio",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Resizing algorithm",
|
||||
type: "option",
|
||||
value: [
|
||||
"Nearest Neighbour",
|
||||
"Bilinear",
|
||||
"Bicubic",
|
||||
"Hermite",
|
||||
"Bezier"
|
||||
],
|
||||
defaultIndex: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
let width = args[0],
|
||||
height = args[1];
|
||||
const unit = args[2],
|
||||
aspect = args[3],
|
||||
resizeAlg = args[4];
|
||||
|
||||
const resizeMap = {
|
||||
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
|
||||
"Bilinear": jimp.RESIZE_BILINEAR,
|
||||
"Bicubic": jimp.RESIZE_BICUBIC,
|
||||
"Hermite": jimp.RESIZE_HERMITE,
|
||||
"Bezier": jimp.RESIZE_BEZIER
|
||||
};
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (unit === "Percent") {
|
||||
width = image.getWidth() * (width / 100);
|
||||
height = image.getHeight() * (height / 100);
|
||||
}
|
||||
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Resizing image...");
|
||||
if (aspect) {
|
||||
image.scaleToFit(width, height, resizeMap[resizeAlg]);
|
||||
} else {
|
||||
image.resize(width, height, resizeMap[resizeAlg]);
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error resizing image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the resized image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ResizeImage;
|
||||
87
src/core/operations/RotateImage.mjs
Normal file
87
src/core/operations/RotateImage.mjs
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Rotate Image operation
|
||||
*/
|
||||
class RotateImage extends Operation {
|
||||
|
||||
/**
|
||||
* RotateImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Rotate Image";
|
||||
this.module = "Image";
|
||||
this.description = "Rotates an image by the specified number of degrees.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Rotation amount (degrees)",
|
||||
type: "number",
|
||||
value: 90
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [degrees] = args;
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Rotating image...");
|
||||
image.rotate(degrees);
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error rotating image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the rotated image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default RotateImage;
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Utils from "../Utils";
|
||||
import Magic from "../lib/Magic";
|
||||
import {scanForFileTypes} from "../lib/FileType";
|
||||
import {FILE_SIGNATURES} from "../lib/FileSignatures";
|
||||
|
||||
/**
|
||||
* Scan for Embedded Files operation
|
||||
@@ -25,13 +26,13 @@ class ScanForEmbeddedFiles extends Operation {
|
||||
this.infoURL = "https://wikipedia.org/wiki/List_of_file_signatures";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Ignore common byte sequences",
|
||||
"type": "boolean",
|
||||
"value": true
|
||||
}
|
||||
];
|
||||
this.args = Object.keys(FILE_SIGNATURES).map(cat => {
|
||||
return {
|
||||
name: cat,
|
||||
type: "boolean",
|
||||
value: cat === "Miscellaneous" ? false : true
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,43 +42,33 @@ class ScanForEmbeddedFiles extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
let output = "Scanning data for 'magic bytes' which may indicate embedded files. The following results may be false positives and should not be treat as reliable. Any suffiently long file is likely to contain these magic bytes coincidentally.\n",
|
||||
type,
|
||||
numFound = 0,
|
||||
numCommonFound = 0;
|
||||
const ignoreCommon = args[0],
|
||||
commonExts = ["ico", "ttf", ""],
|
||||
numFound = 0;
|
||||
const categories = [],
|
||||
data = new Uint8Array(input);
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
type = Magic.magicFileType(data.slice(i));
|
||||
if (type) {
|
||||
if (ignoreCommon && commonExts.indexOf(type.ext) > -1) {
|
||||
numCommonFound++;
|
||||
continue;
|
||||
}
|
||||
numFound++;
|
||||
output += "\nOffset " + i + " (0x" + Utils.hex(i) + "):\n" +
|
||||
" File extension: " + type.ext + "\n" +
|
||||
" MIME type: " + type.mime + "\n";
|
||||
args.forEach((cat, i) => {
|
||||
if (cat) categories.push(Object.keys(FILE_SIGNATURES)[i]);
|
||||
});
|
||||
|
||||
if (type.desc && type.desc.length) {
|
||||
output += " Description: " + type.desc + "\n";
|
||||
const types = scanForFileTypes(data, categories);
|
||||
|
||||
if (types.length) {
|
||||
types.forEach(type => {
|
||||
numFound++;
|
||||
output += "\nOffset " + type.offset + " (0x" + Utils.hex(type.offset) + "):\n" +
|
||||
" File extension: " + type.fileDetails.extension + "\n" +
|
||||
" MIME type: " + type.fileDetails.mime + "\n";
|
||||
|
||||
if (type.fileDetails.description && type.fileDetails.description.length) {
|
||||
output += " Description: " + type.fileDetails.description + "\n";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (numFound === 0) {
|
||||
output += "\nNo embedded files were found.";
|
||||
}
|
||||
|
||||
if (numCommonFound > 0) {
|
||||
output += "\n\n" + numCommonFound;
|
||||
output += numCommonFound === 1 ?
|
||||
" file type was detected that has a common byte sequence. This is likely to be a false positive." :
|
||||
" file types were detected that have common byte sequences. These are likely to be false positives.";
|
||||
output += " Run this operation with the 'Ignore common byte sequences' option unchecked to see details.";
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,12 @@ class Split extends Operation {
|
||||
this.args = [
|
||||
{
|
||||
"name": "Split delimiter",
|
||||
"type": "editableOption",
|
||||
"type": "editableOptionShort",
|
||||
"value": SPLIT_DELIM_OPTIONS
|
||||
},
|
||||
{
|
||||
"name": "Join delimiter",
|
||||
"type": "editableOption",
|
||||
"type": "editableOptionShort",
|
||||
"value": JOIN_DELIM_OPTIONS
|
||||
}
|
||||
];
|
||||
|
||||
102
src/core/operations/SplitColourChannels.mjs
Normal file
102
src/core/operations/SplitColourChannels.mjs
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @author Matt C [matt@artemisbot.uk]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import {isImage} from "../lib/FileType";
|
||||
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Split Colour Channels operation
|
||||
*/
|
||||
class SplitColourChannels extends Operation {
|
||||
|
||||
/**
|
||||
* SplitColourChannels constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Split Colour Channels";
|
||||
this.module = "Image";
|
||||
this.description = "Splits the given image into its red, green and blue colour channels.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Channel_(digital_image)";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "List<File>";
|
||||
this.presentType = "html";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {List<File>}
|
||||
*/
|
||||
async run(input, args) {
|
||||
// Make sure that the input is an image
|
||||
if (!isImage(input)) throw new OperationError("Invalid file type.");
|
||||
|
||||
const parsedImage = await jimp.read(Buffer.from(input));
|
||||
|
||||
const red = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const split = parsedImage
|
||||
.clone()
|
||||
.color([
|
||||
{apply: "blue", params: [-255]},
|
||||
{apply: "green", params: [-255]}
|
||||
])
|
||||
.getBufferAsync(jimp.MIME_PNG);
|
||||
resolve(new File([new Uint8Array((await split).values())], "red.png", {type: "image/png"}));
|
||||
} catch (err) {
|
||||
reject(new OperationError(`Could not split red channel: ${err}`));
|
||||
}
|
||||
});
|
||||
|
||||
const green = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const split = parsedImage.clone()
|
||||
.color([
|
||||
{apply: "red", params: [-255]},
|
||||
{apply: "blue", params: [-255]},
|
||||
]).getBufferAsync(jimp.MIME_PNG);
|
||||
resolve(new File([new Uint8Array((await split).values())], "green.png", {type: "image/png"}));
|
||||
} catch (err) {
|
||||
reject(new OperationError(`Could not split green channel: ${err}`));
|
||||
}
|
||||
});
|
||||
|
||||
const blue = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const split = parsedImage
|
||||
.color([
|
||||
{apply: "red", params: [-255]},
|
||||
{apply: "green", params: [-255]},
|
||||
]).getBufferAsync(jimp.MIME_PNG);
|
||||
resolve(new File([new Uint8Array((await split).values())], "blue.png", {type: "image/png"}));
|
||||
} catch (err) {
|
||||
reject(new OperationError(`Could not split blue channel: ${err}`));
|
||||
}
|
||||
});
|
||||
|
||||
return await Promise.all([red, green, blue]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the files in HTML for web apps.
|
||||
*
|
||||
* @param {File[]} files
|
||||
* @returns {html}
|
||||
*/
|
||||
async present(files) {
|
||||
return await Utils.displayFilesAsHTML(files);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SplitColourChannels;
|
||||
@@ -44,7 +44,7 @@ class StandardDeviation extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const val = stdDev(createNumArray(input, args[0]));
|
||||
return val instanceof BigNumber ? val : new BigNumber(NaN);
|
||||
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
|
||||
|
||||
}
|
||||
|
||||
|
||||
153
src/core/operations/Subsection.mjs
Normal file
153
src/core/operations/Subsection.mjs
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import XRegExp from "xregexp";
|
||||
import Operation from "../Operation";
|
||||
import Recipe from "../Recipe";
|
||||
import Dish from "../Dish";
|
||||
|
||||
/**
|
||||
* Subsection operation
|
||||
*/
|
||||
class Subsection extends Operation {
|
||||
|
||||
/**
|
||||
* Subsection constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Subsection";
|
||||
this.flowControl = true;
|
||||
this.module = "Regex";
|
||||
this.description = "Select a part of the input data using a regular expression (regex), and run all subsequent operations on each match separately.<br><br>You can use up to one capture group, where the recipe will only be run on the data in the capture group. If there's more than one capture group, only the first one will be operated on.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Section (regex)",
|
||||
"type": "string",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": "Case sensitive matching",
|
||||
"type": "boolean",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "Global matching",
|
||||
"type": "boolean",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "Ignore errors",
|
||||
"type": "boolean",
|
||||
"value": false
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} state - The current state of the recipe.
|
||||
* @param {number} state.progress - The current position in the recipe.
|
||||
* @param {Dish} state.dish - The Dish being operated on
|
||||
* @param {Operation[]} state.opList - The list of operations in the recipe
|
||||
* @returns {Object} - The updated state of the recipe
|
||||
*/
|
||||
async run(state) {
|
||||
const opList = state.opList,
|
||||
inputType = opList[state.progress].inputType,
|
||||
outputType = opList[state.progress].outputType,
|
||||
input = await state.dish.get(inputType),
|
||||
ings = opList[state.progress].ingValues,
|
||||
[section, caseSensitive, global, ignoreErrors] = ings,
|
||||
subOpList = [];
|
||||
|
||||
if (input && section !== "") {
|
||||
// Create subOpList for each tranche to operate on
|
||||
// all remaining operations unless we encounter a Merge
|
||||
for (let i = state.progress + 1; i < opList.length; i++) {
|
||||
if (opList[i].name === "Merge" && !opList[i].disabled) {
|
||||
break;
|
||||
} else {
|
||||
subOpList.push(opList[i]);
|
||||
}
|
||||
}
|
||||
|
||||
let flags = "",
|
||||
inOffset = 0,
|
||||
output = "",
|
||||
m,
|
||||
progress = 0;
|
||||
|
||||
if (!caseSensitive) flags += "i";
|
||||
if (global) flags += "g";
|
||||
|
||||
const regex = new XRegExp(section, flags),
|
||||
recipe = new Recipe();
|
||||
|
||||
recipe.addOperations(subOpList);
|
||||
state.forkOffset += state.progress + 1;
|
||||
|
||||
// Take a deep(ish) copy of the ingredient values
|
||||
const ingValues = subOpList.map(op => JSON.parse(JSON.stringify(op.ingValues)));
|
||||
let matched = false;
|
||||
|
||||
// Run recipe over each match
|
||||
while ((m = regex.exec(input))) {
|
||||
matched = true;
|
||||
// Add up to match
|
||||
let matchStr = m[0];
|
||||
|
||||
if (m.length === 1) { // No capture groups
|
||||
output += input.slice(inOffset, m.index);
|
||||
inOffset = m.index + m[0].length;
|
||||
} else if (m.length >= 2) {
|
||||
matchStr = m[1];
|
||||
|
||||
// Need to add some of the matched string that isn't in the capture group
|
||||
output += input.slice(inOffset, m.index + m[0].indexOf(m[1]));
|
||||
// Set i to be after the end of the first capture group
|
||||
inOffset = m.index + m[0].indexOf(m[1]) + m[1].length;
|
||||
}
|
||||
|
||||
// Baseline ing values for each tranche so that registers are reset
|
||||
recipe.opList.forEach((op, i) => {
|
||||
op.ingValues = JSON.parse(JSON.stringify(ingValues[i]));
|
||||
});
|
||||
|
||||
const dish = new Dish();
|
||||
dish.set(matchStr, inputType);
|
||||
|
||||
try {
|
||||
progress = await recipe.execute(dish, 0, state);
|
||||
} catch (err) {
|
||||
if (!ignoreErrors) {
|
||||
throw err;
|
||||
}
|
||||
progress = err.progress + 1;
|
||||
}
|
||||
output += await dish.get(outputType);
|
||||
if (!regex.global) break;
|
||||
}
|
||||
|
||||
// If no matches were found, advance progress to after a Merge op
|
||||
// Otherwise, the operations below Subsection will be run on all the input data
|
||||
if (!matched) {
|
||||
state.progress += subOpList.length + 1;
|
||||
}
|
||||
|
||||
output += input.slice(inOffset);
|
||||
state.progress += progress;
|
||||
state.dish.set(output, outputType);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Subsection;
|
||||
@@ -44,7 +44,7 @@ class Subtract extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const val = sub(createNumArray(input, args[0]));
|
||||
return val instanceof BigNumber ? val : new BigNumber(NaN);
|
||||
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class Sum extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const val = sum(createNumArray(input, args[0]));
|
||||
return val instanceof BigNumber ? val : new BigNumber(NaN);
|
||||
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class ToBase64 extends Operation {
|
||||
|
||||
this.name = "To Base64";
|
||||
this.module = "Default";
|
||||
this.description = "Base64 is a notation for encoding arbitrary byte data using a restricted set of symbols that can be conveniently used by humans and processed by computers.<br><br>This operation decodes data from an ASCII Base64 string back into its raw format.<br><br>e.g. <code>aGVsbG8=</code> becomes <code>hello</code>";
|
||||
this.description = "Base64 is a notation for encoding arbitrary byte data using a restricted set of symbols that can be conveniently used by humans and processed by computers.<br><br>This operation encodes raw data into an ASCII Base64 string.<br><br>e.g. <code>hello</code> becomes <code>aGVsbG8=</code>";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Base64";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
|
||||
39
src/core/operations/ToCaseInsensitiveRegex.mjs
Normal file
39
src/core/operations/ToCaseInsensitiveRegex.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @author masq [github.cyberchef@masq.cc]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
|
||||
/**
|
||||
* To Case Insensitive Regex operation
|
||||
*/
|
||||
class ToCaseInsensitiveRegex extends Operation {
|
||||
|
||||
/**
|
||||
* ToCaseInsensitiveRegex constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "To Case Insensitive Regex";
|
||||
this.module = "Default";
|
||||
this.description = "Converts a case-sensitive regex string into a case-insensitive regex string in case the i flag is unavailable to you.<br><br>e.g. <code>Mozilla/[0-9].[0-9] .*</code> becomes <code>[mM][oO][zZ][iI][lL][lL][aA]/[0-9].[0-9] .*</code>";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Regular_expression";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
return input.replace(/[a-z]/ig, m => `[${m.toLowerCase()}${m.toUpperCase()}]`);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToCaseInsensitiveRegex;
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* @author gchq77703 []
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import geohash from "ngeohash";
|
||||
|
||||
/**
|
||||
* To Geohash operation
|
||||
*/
|
||||
class ToGeohash extends Operation {
|
||||
|
||||
/**
|
||||
* ToGeohash constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "To Geohash";
|
||||
this.module = "Crypto";
|
||||
this.description = "Converts Lat/Long coordinates into a Geohash string. For example, <code>37.8324,112.5584</code> becomes <code>ww8p1r4t8</code>.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Geohash";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
name: "Precision",
|
||||
type: "number",
|
||||
value: 9
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const [precision] = args;
|
||||
|
||||
return input.split("\n").map(line => {
|
||||
line = line.replace(/ /g, "");
|
||||
if (line === "") return "";
|
||||
return geohash.encode(...line.split(",").map(num => parseFloat(num)), precision);
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ToGeohash;
|
||||
@@ -38,7 +38,9 @@ class ToMessagePack extends Operation {
|
||||
if (ENVIRONMENT_IS_WORKER()) {
|
||||
return notepack.encode(input);
|
||||
} else {
|
||||
return notepack.encode(input).buffer;
|
||||
const res = notepack.encode(input);
|
||||
// Safely convert from Node Buffer to ArrayBuffer using the correct view of the data
|
||||
return (new Uint8Array(res)).buffer;
|
||||
}
|
||||
} catch (err) {
|
||||
throw new OperationError(`Could not encode JSON to MessagePack: ${err}`);
|
||||
|
||||
@@ -57,7 +57,7 @@ class ToTable extends Operation {
|
||||
const [cellDelims, rowDelims, firstRowHeader, format] = args;
|
||||
|
||||
// Process the input into a nested array of elements.
|
||||
const tableData = Utils.parseCSV(input, cellDelims.split(""), rowDelims.split(""));
|
||||
const tableData = Utils.parseCSV(Utils.escapeHtml(input), cellDelims.split(""), rowDelims.split(""));
|
||||
|
||||
if (!tableData.length) return "";
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Utils from "../Utils";
|
||||
import Stream from "../lib/Stream";
|
||||
|
||||
/**
|
||||
* Untar operation
|
||||
@@ -41,38 +42,6 @@ class Untar extends Operation {
|
||||
* @returns {List<File>}
|
||||
*/
|
||||
run(input, args) {
|
||||
const Stream = function(input) {
|
||||
this.bytes = input;
|
||||
this.position = 0;
|
||||
};
|
||||
|
||||
Stream.prototype.getBytes = function(bytesToGet) {
|
||||
const newPosition = this.position + bytesToGet;
|
||||
const bytes = this.bytes.slice(this.position, newPosition);
|
||||
this.position = newPosition;
|
||||
return bytes;
|
||||
};
|
||||
|
||||
Stream.prototype.readString = function(numBytes) {
|
||||
let result = "";
|
||||
for (let i = this.position; i < this.position + numBytes; i++) {
|
||||
const currentByte = this.bytes[i];
|
||||
if (currentByte === 0) break;
|
||||
result += String.fromCharCode(currentByte);
|
||||
}
|
||||
this.position += numBytes;
|
||||
return result;
|
||||
};
|
||||
|
||||
Stream.prototype.readInt = function(numBytes, base) {
|
||||
const string = this.readString(numBytes);
|
||||
return parseInt(string, base);
|
||||
};
|
||||
|
||||
Stream.prototype.hasMore = function() {
|
||||
return this.position < this.bytes.length;
|
||||
};
|
||||
|
||||
const stream = new Stream(input),
|
||||
files = [];
|
||||
|
||||
@@ -85,7 +54,7 @@ class Untar extends Operation {
|
||||
ownerUID: stream.readString(8),
|
||||
ownerGID: stream.readString(8),
|
||||
size: parseInt(stream.readString(12), 8), // Octal
|
||||
lastModTime: new Date(1000 * stream.readInt(12, 8)), // Octal
|
||||
lastModTime: new Date(1000 * parseInt(stream.readString(12), 8)), // Octal
|
||||
checksum: stream.readString(8),
|
||||
type: stream.readString(1),
|
||||
linkedFileName: stream.readString(100),
|
||||
|
||||
@@ -57,7 +57,7 @@ class XPathExpression extends Operation {
|
||||
|
||||
let nodes;
|
||||
try {
|
||||
nodes = xpath.select(query, doc);
|
||||
nodes = xpath.parse(query).select({ node: doc, allowAnyNamespaceForNoPrefix: true });
|
||||
} catch (err) {
|
||||
throw new OperationError(`Invalid XPath. Details:\n${err.message}.`);
|
||||
}
|
||||
|
||||
122
src/core/operations/YARARules.mjs
Normal file
122
src/core/operations/YARARules.mjs
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @author Matt C [matt@artemisbot.uk]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Yara from "libyara-wasm";
|
||||
|
||||
/**
|
||||
* YARA Rules operation
|
||||
*/
|
||||
class YARARules extends Operation {
|
||||
|
||||
/**
|
||||
* YARARules constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "YARA Rules";
|
||||
this.module = "Yara";
|
||||
this.description = "YARA is a tool developed at VirusTotal, primarily aimed at helping malware researchers to identify and classify malware samples. It matches based on rules specified by the user containing textual or binary patterns and a boolean expression. For help on writing rules, see the <a href='https://yara.readthedocs.io/en/latest/writingrules.html'>YARA documentation.</a>";
|
||||
this.infoURL = "https://wikipedia.org/wiki/YARA";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
name: "Rules",
|
||||
type: "text",
|
||||
value: "",
|
||||
rows: 5
|
||||
},
|
||||
{
|
||||
name: "Show strings",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Show string lengths",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Show metadata",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Show counts",
|
||||
type: "boolean",
|
||||
value: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Instantiating YARA...");
|
||||
const [rules, showStrings, showLengths, showMeta, showCounts] = args;
|
||||
return new Promise((resolve, reject) => {
|
||||
Yara().then(yara => {
|
||||
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Converting data for YARA.");
|
||||
let matchString = "";
|
||||
|
||||
const inpArr = new Uint8Array(input); // Turns out embind knows that JS uint8array <==> C++ std::string
|
||||
|
||||
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Running YARA matching.");
|
||||
|
||||
const resp = yara.run(inpArr, rules);
|
||||
|
||||
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Processing data.");
|
||||
|
||||
if (resp.compileErrors.size() > 0) {
|
||||
for (let i = 0; i < resp.compileErrors.size(); i++) {
|
||||
const compileError = resp.compileErrors.get(i);
|
||||
if (!compileError.warning) {
|
||||
reject(new OperationError(`Error on line ${compileError.lineNumber}: ${compileError.message}`));
|
||||
} else {
|
||||
matchString += `Warning on line ${compileError.lineNumber}: ${compileError.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
const matchedRules = resp.matchedRules;
|
||||
for (let i = 0; i < matchedRules.size(); i++) {
|
||||
const rule = matchedRules.get(i);
|
||||
const matches = rule.resolvedMatches;
|
||||
let meta = "";
|
||||
if (showMeta && rule.metadata.size() > 0) {
|
||||
meta += " [";
|
||||
for (let j = 0; j < rule.metadata.size(); j++) {
|
||||
meta += `${rule.metadata.get(j).identifier}: ${rule.metadata.get(j).data}, `;
|
||||
}
|
||||
meta = meta.slice(0, -2) + "]";
|
||||
}
|
||||
const countString = showCounts ? `${matches.size()} time${matches.size() > 1 ? "s" : ""}` : "";
|
||||
if (matches.size() === 0 || !(showStrings || showLengths)) {
|
||||
matchString += `Input matches rule "${rule.ruleName}"${meta}${countString.length > 0 ? ` ${countString}`: ""}.\n`;
|
||||
} else {
|
||||
matchString += `Rule "${rule.ruleName}"${meta} matches (${countString}):\n`;
|
||||
for (let j = 0; j < matches.size(); j++) {
|
||||
const match = matches.get(j);
|
||||
if (showStrings || showLengths) {
|
||||
matchString += `Pos ${match.location}, ${showLengths ? `length ${match.matchLength}, ` : ""}identifier ${match.stringIdentifier}${showStrings ? `, data: "${match.data}"` : ""}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve(matchString);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default YARARules;
|
||||
97
src/core/vendor/DisassembleX86-64.mjs
vendored
97
src/core/vendor/DisassembleX86-64.mjs
vendored
@@ -1,3 +1,31 @@
|
||||
/*-------------------------------------------------------------------------------------------------------------------------
|
||||
Created by Damian Recoskie (https://github.com/Recoskie/X86-64-Disassembler-JS)
|
||||
& exported for CyberChef by Matt [me@mitt.dev]
|
||||
---------------------------------------------------------------------------------------------------------------------------
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Damian Recoskie
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-------------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
/*-------------------------------------------------------------------------------------------------------------------------
|
||||
Binary byte code array.
|
||||
---------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -3525,7 +3553,7 @@ export function LoadBinCode( HexStr )
|
||||
|
||||
var len = HexStr.length;
|
||||
|
||||
for( var i = 0, el = 0, Sing = 0, int32 = 0; i < len; i += 8 )
|
||||
for( var i = 0, el = 0, Sign = 0, int32 = 0; i < len; i += 8 )
|
||||
{
|
||||
//It is faster to read 8 hex digits at a time if possible.
|
||||
|
||||
@@ -3541,22 +3569,22 @@ export function LoadBinCode( HexStr )
|
||||
|
||||
//The variable sing corrects the unusable sing bits during the 4 byte rotation algorithm.
|
||||
|
||||
Sing = int32;
|
||||
Sign = int32;
|
||||
|
||||
//Remove the Sing bit value if active for when the number is changed to int32 during rotation.
|
||||
//Remove the Sign bit value if active for when the number is changed to int32 during rotation.
|
||||
|
||||
int32 ^= int32 & 0x80000000;
|
||||
|
||||
//Rotate the 32 bit int so that each number is put in order in the BinCode array. Add the Sing Bit positions back though each rotation.
|
||||
//Rotate the 32 bit int so that each number is put in order in the BinCode array. Add the Sign Bit positions back though each rotation.
|
||||
|
||||
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
|
||||
BinCode[el++] = ( ( ( Sing >> 24 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
BinCode[el++] = ( ( ( Sign >> 24 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
|
||||
BinCode[el++] = ( ( ( Sing >> 16 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
BinCode[el++] = ( ( ( Sign >> 16 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
|
||||
BinCode[el++] = ( ( ( Sing >> 8 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
BinCode[el++] = ( ( ( Sign >> 8 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
|
||||
BinCode[el++] = ( ( Sing & 0x80 ) | int32 ) & 0xFF;
|
||||
BinCode[el++] = ( ( Sign & 0x80 ) | int32 ) & 0xFF;
|
||||
}
|
||||
|
||||
//Remove elements past the Number of bytes in HexStr because int 32 is always 4 bytes it is possible to end in an uneven number.
|
||||
@@ -3585,7 +3613,6 @@ function NextByte()
|
||||
if ( CodePos < BinCode.length ) //If not out of bounds.
|
||||
{
|
||||
//Convert current byte to String, and pad.
|
||||
var t;
|
||||
|
||||
( ( t = BinCode[CodePos++].toString(16) ).length === 1) && ( t = "0" + t );
|
||||
|
||||
@@ -3947,11 +3974,11 @@ function DecodeImmediate( type, BySize, SizeSetting )
|
||||
|
||||
var Pad32 = 0, Pad64 = 0;
|
||||
|
||||
//*Initialize the Sing value that is only set for Negative, or Positive Relative displacements.
|
||||
//*Initialize the Sign value that is only set for Negative, or Positive Relative displacements.
|
||||
|
||||
var Sing = 0;
|
||||
var Sign = 0;
|
||||
|
||||
//*Initialize the Sing Extend variable size as 0 Some Immediate numbers Sing extend.
|
||||
//*Initialize the Sign Extend variable size as 0 Some Immediate numbers Sign extend.
|
||||
|
||||
var Extend = 0;
|
||||
|
||||
@@ -4017,21 +4044,33 @@ function DecodeImmediate( type, BySize, SizeSetting )
|
||||
|
||||
Pad32 = ( Math.min( BitMode, 1 ) << 2 ) + 4; Pad64 = Math.max( Math.min( BitMode, 2 ), 1 ) << 3;
|
||||
|
||||
//Add the 32 bit section to V32.
|
||||
//Carry bit to 64 bit section.
|
||||
|
||||
var C64 = 0;
|
||||
|
||||
//Relative size.
|
||||
|
||||
var n = Math.min( 0x100000000, Math.pow( 2, 4 << ( S + 1 ) ) );
|
||||
|
||||
//Sign bit adjust.
|
||||
|
||||
if( V32 >= ( n >> 1 ) ) { V32 -= n; }
|
||||
|
||||
//Add position.
|
||||
|
||||
V32 += Pos32;
|
||||
|
||||
//Remove carry bit and add it to C64.
|
||||
|
||||
var C64 = 0; V32 += Pos32;
|
||||
( C64 = ( ( V32 ) >= 0x100000000 ) ) && ( V32 -= 0x100000000 );
|
||||
|
||||
//Do not carry to 64 if address is 32, and below.
|
||||
|
||||
if ( S <= 2 ) { C64 = false; }
|
||||
|
||||
//If bit mode is 16 bits only the first 16 bits are used, or if Size Attribute is 16 bit.
|
||||
//Add the 64 bit position plus carry.
|
||||
|
||||
( BitMode <= 0 || SizeAttrSelect <= 0 ) && ( V32 &= 0xFFFF );
|
||||
|
||||
//Adjust the 32 bit relative address section if it was not cropped to 16 bit's.
|
||||
|
||||
( C64 = ( ( V32 ) > 0xFFFFFFFF ) ) && ( V32 -= 0x100000000 );
|
||||
|
||||
//Add the 64 bit address section if in 64 bit mode, or higher.
|
||||
|
||||
( BitMode >= 2 ) && ( ( V64 += Pos64 + C64 ) > 0xFFFFFFFF ) && ( V64 -= 0x100000000 );
|
||||
( ( V64 += Pos64 + C64 ) > 0xFFFFFFFF ) && ( V64 -= 0x100000000 );
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -4052,9 +4091,9 @@ function DecodeImmediate( type, BySize, SizeSetting )
|
||||
|
||||
var Center = 2 * ( 1 << ( n << 3 ) - 2 );
|
||||
|
||||
//By default the Sing is Positive.
|
||||
//By default the Sign is Positive.
|
||||
|
||||
Sing = 1;
|
||||
Sign = 1;
|
||||
|
||||
/*-------------------------------------------------------------------------------------------------------------------------
|
||||
Calculate the VSIB displacement size if it is a VSIB Disp8.
|
||||
@@ -4074,9 +4113,9 @@ function DecodeImmediate( type, BySize, SizeSetting )
|
||||
|
||||
V32 = Center * 2 - V32;
|
||||
|
||||
//The Sing is negative.
|
||||
//The Sign is negative.
|
||||
|
||||
Sing = 2;
|
||||
Sign = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4110,7 +4149,7 @@ function DecodeImmediate( type, BySize, SizeSetting )
|
||||
|
||||
//*Return the Imm.
|
||||
|
||||
return ( ( Sing > 0 ? ( Sing > 1 ? "-" : "+" ) : "" ) + Imm.toUpperCase() );
|
||||
return ( ( Sign > 0 ? ( Sign > 1 ? "-" : "+" ) : "" ) + Imm.toUpperCase() );
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -238,12 +238,18 @@ class App {
|
||||
|
||||
/**
|
||||
* Sets up the adjustable splitter to allow the user to resize areas of the page.
|
||||
*
|
||||
* @param {boolean} [minimise=false] - Set this flag if attempting to minimuse frames to 0 width
|
||||
*/
|
||||
initialiseSplitter() {
|
||||
initialiseSplitter(minimise=false) {
|
||||
if (this.columnSplitter) this.columnSplitter.destroy();
|
||||
if (this.ioSplitter) this.ioSplitter.destroy();
|
||||
|
||||
this.columnSplitter = Split(["#operations", "#recipe", "#IO"], {
|
||||
sizes: [20, 30, 50],
|
||||
minSize: [240, 370, 450],
|
||||
minSize: minimise ? [0, 0, 0] : [240, 370, 450],
|
||||
gutterSize: 4,
|
||||
expandToMin: false,
|
||||
onDrag: function() {
|
||||
this.manager.recipe.adjustWidth();
|
||||
}.bind(this)
|
||||
@@ -251,7 +257,8 @@ class App {
|
||||
|
||||
this.ioSplitter = Split(["#input", "#output"], {
|
||||
direction: "vertical",
|
||||
gutterSize: 4
|
||||
gutterSize: 4,
|
||||
minSize: minimise ? [0, 0] : [100, 100]
|
||||
});
|
||||
|
||||
this.resetLayout();
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Utils from "../core/Utils";
|
||||
|
||||
/**
|
||||
* Object to handle the creation of operation ingredients.
|
||||
*/
|
||||
@@ -25,10 +27,14 @@ class HTMLIngredient {
|
||||
this.value = config.value;
|
||||
this.disabled = config.disabled || false;
|
||||
this.hint = config.hint || false;
|
||||
this.rows = config.rows || false;
|
||||
this.target = config.target;
|
||||
this.defaultIndex = config.defaultIndex || 0;
|
||||
this.toggleValues = config.toggleValues;
|
||||
this.id = "ing-" + this.app.nextIngId();
|
||||
this.min = (typeof config.min === "number") ? config.min : "";
|
||||
this.max = (typeof config.max === "number") ? config.max : "";
|
||||
this.step = config.step || 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +106,9 @@ class HTMLIngredient {
|
||||
id="${this.id}"
|
||||
arg-name="${this.name}"
|
||||
value="${this.value}"
|
||||
min="${this.min}"
|
||||
max="${this.max}"
|
||||
step="${this.step}"
|
||||
${this.disabled ? "disabled" : ""}>
|
||||
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
|
||||
</div>`;
|
||||
@@ -155,7 +164,7 @@ class HTMLIngredient {
|
||||
} else if ((m = this.value[i].name.match(/\[\/([a-z0-9 -()^]+)\]/i))) {
|
||||
html += "</optgroup>";
|
||||
} else {
|
||||
html += `<option populate-value="${this.value[i].value}">${this.value[i].name}</option>`;
|
||||
html += `<option populate-value="${Utils.escapeHtml(this.value[i].value)}">${this.value[i].name}</option>`;
|
||||
}
|
||||
}
|
||||
html += `</select>
|
||||
@@ -165,6 +174,35 @@ class HTMLIngredient {
|
||||
this.manager.addDynamicListener("#" + this.id, "change", this.populateOptionChange, this);
|
||||
break;
|
||||
case "editableOption":
|
||||
html += `<div class="form-group input-group">
|
||||
<label for="${this.id}" class="bmd-label-floating">${this.name}</label>
|
||||
<input type="text"
|
||||
class="form-control arg"
|
||||
id="${this.id}"
|
||||
arg-name="${this.name}"
|
||||
value="${this.value[this.defaultIndex].value}"
|
||||
${this.disabled ? "disabled" : ""}>
|
||||
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
|
||||
<div class="input-group-append">
|
||||
<button type="button"
|
||||
class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
|
||||
data-toggle="dropdown"
|
||||
data-boundary="scrollParent"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu editable-option-menu">`;
|
||||
for (i = 0; i < this.value.length; i++) {
|
||||
html += `<a class="dropdown-item" href="#" value="${this.value[i].value}">${this.value[i].name}</a>`;
|
||||
}
|
||||
html += `</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
this.manager.addDynamicListener(".editable-option-menu a", "click", this.editableOptionClick, this);
|
||||
break;
|
||||
case "editableOptionShort":
|
||||
html += `<div class="form-group input-group inline">
|
||||
<label for="${this.id}" class="bmd-label-floating inline">${this.name}</label>
|
||||
<input type="text"
|
||||
@@ -200,6 +238,7 @@ class HTMLIngredient {
|
||||
class="form-control arg"
|
||||
id="${this.id}"
|
||||
arg-name="${this.name}"
|
||||
rows="${this.rows ? this.rows : 3}"
|
||||
${this.disabled ? "disabled" : ""}>${this.value}</textarea>
|
||||
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
|
||||
</div>`;
|
||||
|
||||
@@ -243,14 +243,22 @@ class InputWaiter {
|
||||
}
|
||||
|
||||
if (file) {
|
||||
this.closeFile();
|
||||
this.loaderWorker = new LoaderWorker();
|
||||
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
|
||||
this.loaderWorker.postMessage({"file": file});
|
||||
this.set(file);
|
||||
this.loadFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for open input button events
|
||||
* Loads the opened data into the input textarea
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
inputOpen(e) {
|
||||
e.preventDefault();
|
||||
const file = e.srcElement.files[0];
|
||||
this.loadFile(file);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for messages sent back by the LoaderWorker.
|
||||
@@ -306,6 +314,22 @@ class InputWaiter {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads a file into the input.
|
||||
*
|
||||
* @param {File} file
|
||||
*/
|
||||
loadFile(file) {
|
||||
if (file) {
|
||||
this.closeFile();
|
||||
this.loaderWorker = new LoaderWorker();
|
||||
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
|
||||
this.loaderWorker.postMessage({"file": file});
|
||||
this.set(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for clear IO events.
|
||||
* Resets the input, output and info areas.
|
||||
|
||||
@@ -137,12 +137,16 @@ class Manager {
|
||||
this.addDynamicListener("#rec-list li.operation > div", "dblclick", this.recipe.operationChildDblclick, this.recipe);
|
||||
this.addDynamicListener("#rec-list .dropdown-menu.toggle-dropdown a", "click", this.recipe.dropdownToggleClick, this.recipe);
|
||||
this.addDynamicListener("#rec-list", "operationremove", this.recipe.opRemove.bind(this.recipe));
|
||||
this.addDynamicListener("textarea.arg", "dragover", this.recipe.textArgDragover, this.recipe);
|
||||
this.addDynamicListener("textarea.arg", "dragleave", this.recipe.textArgDragLeave, this.recipe);
|
||||
this.addDynamicListener("textarea.arg", "drop", this.recipe.textArgDrop, this.recipe);
|
||||
|
||||
// Input
|
||||
this.addMultiEventListener("#input-text", "keyup", this.input.inputChange, this.input);
|
||||
this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input);
|
||||
document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app));
|
||||
document.getElementById("clr-io").addEventListener("click", this.input.clearIoClick.bind(this.input));
|
||||
this.addListeners("#open-file", "change", this.input.inputOpen, this.input);
|
||||
this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input);
|
||||
this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input);
|
||||
this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input);
|
||||
@@ -169,6 +173,7 @@ class Manager {
|
||||
this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output);
|
||||
this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
|
||||
document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
|
||||
this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output);
|
||||
|
||||
// Options
|
||||
document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options));
|
||||
|
||||
@@ -319,6 +319,7 @@ class OutputWaiter {
|
||||
const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode;
|
||||
|
||||
if (el.getAttribute("data-original-title").indexOf("Maximise") === 0) {
|
||||
this.app.initialiseSplitter(true);
|
||||
this.app.columnSplitter.collapse(0);
|
||||
this.app.columnSplitter.collapse(1);
|
||||
this.app.ioSplitter.collapse(0);
|
||||
@@ -328,6 +329,7 @@ class OutputWaiter {
|
||||
} else {
|
||||
$(el).attr("data-original-title", "Maximise output pane");
|
||||
el.querySelector("i").innerHTML = "fullscreen";
|
||||
this.app.initialiseSplitter(false);
|
||||
this.app.resetLayout();
|
||||
}
|
||||
}
|
||||
@@ -476,7 +478,7 @@ class OutputWaiter {
|
||||
*/
|
||||
showMagicButton(opSequence, result, recipeConfig) {
|
||||
const magicButton = document.getElementById("magic");
|
||||
magicButton.setAttribute("data-original-title", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.truncate(result, 30)}"</span>`);
|
||||
magicButton.setAttribute("data-original-title", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.escapeHtml(Utils.truncate(result), 30)}"</span>`);
|
||||
magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, "");
|
||||
magicButton.classList.remove("hidden");
|
||||
}
|
||||
@@ -492,6 +494,24 @@ class OutputWaiter {
|
||||
magicButton.setAttribute("data-original-title", "Magic!");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for extract file events.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
async extractFileClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const el = e.target.nodeName === "I" ? e.target.parentNode : e.target;
|
||||
const blobURL = el.getAttribute("blob-url");
|
||||
const fileName = el.getAttribute("file-name");
|
||||
|
||||
const blob = await fetch(blobURL).then(r => r.blob());
|
||||
this.manager.input.loadFile(new File([blob], fileName, {type: blob.type}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default OutputWaiter;
|
||||
|
||||
@@ -376,6 +376,7 @@ class RecipeWaiter {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds the specified operation to the recipe.
|
||||
*
|
||||
@@ -454,6 +455,75 @@ class RecipeWaiter {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for text argument dragover events.
|
||||
* Gives the user a visual cue to show that items can be dropped here.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
textArgDragover (e) {
|
||||
// This will be set if we're dragging an operation
|
||||
if (e.dataTransfer.effectAllowed === "move")
|
||||
return false;
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.target.closest("textarea.arg").classList.add("dropping-file");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for text argument dragleave events.
|
||||
* Removes the visual cue.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
textArgDragLeave (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.target.classList.remove("dropping-file");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for text argument drop events.
|
||||
* Loads the dragged data into the argument textarea.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
textArgDrop(e) {
|
||||
// This will be set if we're dragging an operation
|
||||
if (e.dataTransfer.effectAllowed === "move")
|
||||
return false;
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const targ = e.target;
|
||||
const file = e.dataTransfer.files[0];
|
||||
const text = e.dataTransfer.getData("Text");
|
||||
|
||||
targ.classList.remove("dropping-file");
|
||||
|
||||
if (text) {
|
||||
targ.value = text;
|
||||
return;
|
||||
}
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
const self = this;
|
||||
reader.onload = function (e) {
|
||||
targ.value = e.target.result;
|
||||
// Trigger floating label move
|
||||
const changeEvent = new Event("change");
|
||||
targ.dispatchEvent(changeEvent);
|
||||
window.dispatchEvent(self.manager.statechange);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets register values.
|
||||
*
|
||||
@@ -479,6 +549,7 @@ class RecipeWaiter {
|
||||
op.insertAdjacentHTML("beforeend", registerListEl);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adjusts the number of ingredient columns as the width of the recipe changes.
|
||||
*/
|
||||
@@ -490,20 +561,25 @@ class RecipeWaiter {
|
||||
this.ingredientChildRuleID = null;
|
||||
|
||||
// Find relevant rules in the stylesheet
|
||||
for (const i in document.styleSheets[0].cssRules) {
|
||||
if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients") {
|
||||
this.ingredientRuleID = i;
|
||||
}
|
||||
if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients > div") {
|
||||
this.ingredientChildRuleID = i;
|
||||
// try/catch for chrome 64+ CORS error on cssRules.
|
||||
try {
|
||||
for (const i in document.styleSheets[0].cssRules) {
|
||||
if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients") {
|
||||
this.ingredientRuleID = i;
|
||||
}
|
||||
if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients > div") {
|
||||
this.ingredientChildRuleID = i;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.ingredientRuleID || !this.ingredientChildRuleID) return;
|
||||
|
||||
const ingredientRule = document.styleSheets[0].cssRules[this.ingredientRuleID],
|
||||
ingredientChildRule = document.styleSheets[0].cssRules[this.ingredientChildRuleID];
|
||||
const ingredientRule = document.styleSheets[0].cssRules[this.ingredientRuleID];
|
||||
const ingredientChildRule = document.styleSheets[0].cssRules[this.ingredientChildRuleID];
|
||||
|
||||
if (recList.clientWidth < 450) {
|
||||
ingredientRule.style.gridTemplateColumns = "auto auto";
|
||||
|
||||
@@ -225,6 +225,10 @@
|
||||
<div class="title no-select">
|
||||
<label for="input-text">Input</label>
|
||||
<span class="float-right">
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-open-file" data-toggle="tooltip" title="Open file as input" onclick="document.getElementById('open-file').click();">
|
||||
<i class="material-icons">input</i>
|
||||
<input type="file" id="open-file" style="display: none">
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="clr-io" data-toggle="tooltip" title="Clear input and output">
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
@@ -267,7 +271,7 @@
|
||||
<i class="material-icons">content_copy</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="switch" data-toggle="tooltip" title="Move output to input">
|
||||
<i class="material-icons">loop</i>
|
||||
<i class="material-icons">open_in_browser</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="undo-switch" data-toggle="tooltip" title="Undo" disabled="disabled">
|
||||
<i class="material-icons">undo</i>
|
||||
|
||||
@@ -91,3 +91,7 @@
|
||||
padding-right: 6px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
#files .card-header .float-right a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
width: 100%;
|
||||
height: var(--controls-height);
|
||||
bottom: 0;
|
||||
padding: 10px;
|
||||
padding: 0;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--primary-border-colour);
|
||||
background-color: var(--secondary-background-colour);
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
|
||||
.dropping-file {
|
||||
border: 5px dashed var(--drop-file-border-colour) !important;
|
||||
margin: -5px;
|
||||
}
|
||||
|
||||
#stale-indicator {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* To Geohash tests
|
||||
*
|
||||
* @author gchq77703
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../TestRegister";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
name: "From Geohash",
|
||||
input: "ww8p1r4t8",
|
||||
expectedOutput: "37.83238649368286,112.55838632583618",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Geohash",
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Geohash",
|
||||
input: "ww8p1r",
|
||||
expectedOutput: "37.83416748046875,112.5604248046875",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Geohash",
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Geohash",
|
||||
input: "ww8",
|
||||
expectedOutput: "37.265625,113.203125",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Geohash",
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Geohash",
|
||||
input: "w",
|
||||
expectedOutput: "22.5,112.5",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Geohash",
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* To Geohash tests
|
||||
*
|
||||
* @author gchq77703
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../TestRegister";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
name: "To Geohash",
|
||||
input: "37.8324,112.5584",
|
||||
expectedOutput: "ww8p1r4t8",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "To Geohash",
|
||||
args: [9],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Geohash",
|
||||
input: "37.9324,-112.2584",
|
||||
expectedOutput: "9w8pv3ruj",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "To Geohash",
|
||||
args: [9],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Geohash",
|
||||
input: "37.8324,112.5584",
|
||||
expectedOutput: "ww8",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "To Geohash",
|
||||
args: [3],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Geohash",
|
||||
input: "37.9324,-112.2584",
|
||||
expectedOutput: "9w8pv3rujxy5b99",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "To Geohash",
|
||||
args: [15],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
182
tests/browser/nightwatch.js
Normal file
182
tests/browser/nightwatch.js
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Tests to ensure that the app loads correctly in a reasonable time and that operations can be run.
|
||||
*
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
module.exports = {
|
||||
before: browser => {
|
||||
browser
|
||||
.resizeWindow(1280, 800)
|
||||
.url(browser.launchUrl);
|
||||
},
|
||||
|
||||
"Loading screen": browser => {
|
||||
// Check that the loading screen appears and then disappears within a reasonable time
|
||||
browser
|
||||
.waitForElementVisible("#preloader", 300)
|
||||
.waitForElementNotPresent("#preloader", 10000);
|
||||
},
|
||||
|
||||
"App loaded": browser => {
|
||||
browser.useCss();
|
||||
// Check that various important elements are loaded
|
||||
browser.expect.element("#operations").to.be.visible;
|
||||
browser.expect.element("#recipe").to.be.visible;
|
||||
browser.expect.element("#input").to.be.present;
|
||||
browser.expect.element("#output").to.be.present;
|
||||
browser.expect.element(".op-list").to.be.present;
|
||||
browser.expect.element("#rec-list").to.be.visible;
|
||||
browser.expect.element("#controls").to.be.visible;
|
||||
browser.expect.element("#input-text").to.be.visible;
|
||||
browser.expect.element("#output-text").to.be.visible;
|
||||
},
|
||||
|
||||
"Operations loaded": browser => {
|
||||
browser.useXpath();
|
||||
// Check that an operation in every category has been populated
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='To Base64']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='To Binary']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='AES Decrypt']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='PEM to Hex']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Power Set']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Parse IP range']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Remove Diacritics']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Sort']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='To UNIX Timestamp']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Extract dates']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Gzip']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Keccak']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='JSON Beautify']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Detect File Type']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Play Media']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Disassemble x86']").to.be.present;
|
||||
browser.expect.element("//li[contains(@class, 'operation') and text()='Register']").to.be.present;
|
||||
},
|
||||
|
||||
"Recipe can be run": browser => {
|
||||
const toHex = "//li[contains(@class, 'operation') and text()='To Hex']";
|
||||
const op = "#rec-list .operation .op-title";
|
||||
|
||||
// Check that operation is visible
|
||||
browser
|
||||
.useXpath()
|
||||
.expect.element(toHex).to.be.visible;
|
||||
|
||||
// Add it to the recipe by double clicking
|
||||
browser
|
||||
.useXpath()
|
||||
.moveToElement(toHex, 10, 10)
|
||||
.useCss()
|
||||
.waitForElementVisible(".popover-body", 1000)
|
||||
.doubleClick();
|
||||
|
||||
// Confirm that it has been added to the recipe
|
||||
browser
|
||||
.useCss()
|
||||
.waitForElementVisible(op)
|
||||
.expect.element(op).text.to.contain("To Hex");
|
||||
|
||||
// Enter input
|
||||
browser
|
||||
.useCss()
|
||||
.setValue("#input-text", "Don't Panic.")
|
||||
.click("#bake");
|
||||
|
||||
// Check output
|
||||
browser
|
||||
.useCss()
|
||||
.waitForElementNotVisible("#stale-indicator", 1000)
|
||||
.expect.element("#output-text").to.have.value.that.equals("44 6f 6e 27 74 20 50 61 6e 69 63 2e");
|
||||
|
||||
// Clear recipe
|
||||
browser
|
||||
.useCss()
|
||||
.moveToElement(op, 10, 10)
|
||||
.waitForElementNotPresent(".popover-body", 1000)
|
||||
.click("#clr-recipe")
|
||||
.waitForElementNotPresent(op);
|
||||
},
|
||||
|
||||
"Test every module": browser => {
|
||||
browser.useCss();
|
||||
|
||||
// BSON
|
||||
loadOp("BSON deserialise", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// Ciphers
|
||||
loadOp("AES Encrypt", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// Code
|
||||
loadOp("XPath expression", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// Compression
|
||||
loadOp("Gzip", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// Crypto
|
||||
loadOp("MD5", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// Default
|
||||
loadOp("Fork", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// Diff
|
||||
loadOp("Diff", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// Encodings
|
||||
loadOp("Encode text", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// Image
|
||||
loadOp("Extract EXIF", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// PGP
|
||||
loadOp("PGP Encrypt", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// PublicKey
|
||||
loadOp("Hex to PEM", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// Regex
|
||||
loadOp("Strings", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// Shellcode
|
||||
loadOp("Disassemble x86", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// URL
|
||||
loadOp("URL Encode", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
|
||||
// UserAgent
|
||||
loadOp("Parse User Agent", browser)
|
||||
.waitForElementNotVisible("#output-loader", 5000);
|
||||
},
|
||||
|
||||
after: browser => {
|
||||
browser.end();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the current recipe and loads a new operation.
|
||||
*
|
||||
* @param {string} opName
|
||||
* @param {Browser} browser
|
||||
*/
|
||||
function loadOp(opName, browser) {
|
||||
return browser
|
||||
.useCss()
|
||||
.click("#clr-recipe")
|
||||
.urlHash("op=" + opName);
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
* @copyright Crown Copyright 2017
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import Chef from "../src/core/Chef";
|
||||
import Chef from "../../src/core/Chef";
|
||||
|
||||
(function() {
|
||||
/**
|
||||
@@ -24,64 +24,68 @@ global.ENVIRONMENT_IS_WEB = function() {
|
||||
};
|
||||
|
||||
import TestRegister from "./TestRegister";
|
||||
import "./tests/operations/BCD";
|
||||
import "./tests/operations/BSON";
|
||||
import "./tests/operations/Base58";
|
||||
import "./tests/operations/Base64";
|
||||
import "./tests/operations/Base62";
|
||||
import "./tests/operations/BitwiseOp";
|
||||
import "./tests/operations/ByteRepr";
|
||||
import "./tests/operations/CartesianProduct";
|
||||
import "./tests/operations/CharEnc";
|
||||
import "./tests/operations/Checksum";
|
||||
import "./tests/operations/Ciphers";
|
||||
import "./tests/operations/Code";
|
||||
import "./tests/operations/Comment";
|
||||
import "./tests/operations/Compress";
|
||||
import "./tests/operations/ConditionalJump";
|
||||
import "./tests/operations/Crypt";
|
||||
import "./tests/operations/CSV";
|
||||
import "./tests/operations/DateTime";
|
||||
import "./tests/operations/ExtractEmailAddresses";
|
||||
import "./tests/operations/Fork";
|
||||
import "./tests/operations/FromDecimal";
|
||||
import "./tests/operations/FromGeohash";
|
||||
import "./tests/operations/Hash";
|
||||
import "./tests/operations/HaversineDistance";
|
||||
import "./tests/operations/Hexdump";
|
||||
import "./tests/operations/Image";
|
||||
import "./tests/operations/Jump";
|
||||
import "./tests/operations/JSONBeautify";
|
||||
import "./tests/operations/JSONMinify";
|
||||
import "./tests/operations/JWTDecode";
|
||||
import "./tests/operations/JWTSign";
|
||||
import "./tests/operations/JWTVerify";
|
||||
import "./tests/operations/MS";
|
||||
import "./tests/operations/Magic";
|
||||
import "./tests/operations/MorseCode";
|
||||
import "./tests/operations/NetBIOS";
|
||||
import "./tests/operations/OTP";
|
||||
import "./tests/operations/PGP";
|
||||
import "./tests/operations/PHP";
|
||||
import "./tests/operations/ParseIPRange";
|
||||
import "./tests/operations/ParseQRCode";
|
||||
import "./tests/operations/PowerSet";
|
||||
import "./tests/operations/Regex";
|
||||
import "./tests/operations/Register";
|
||||
import "./tests/operations/RemoveDiacritics";
|
||||
import "./tests/operations/Rotate";
|
||||
import "./tests/operations/SeqUtils";
|
||||
import "./tests/operations/SetDifference";
|
||||
import "./tests/operations/SetIntersection";
|
||||
import "./tests/operations/SetUnion";
|
||||
import "./tests/operations/StrUtils";
|
||||
import "./tests/operations/SymmetricDifference";
|
||||
import "./tests/operations/TextEncodingBruteForce";
|
||||
import "./tests/operations/ToGeohash";
|
||||
import "./tests/operations/TranslateDateTimeFormat";
|
||||
import "./tests/operations/Magic";
|
||||
import "./tests/operations/ParseTLV";
|
||||
import "./tests/operations/Media";
|
||||
import "./tests/BCD";
|
||||
import "./tests/BSON";
|
||||
import "./tests/Base58";
|
||||
import "./tests/Base64";
|
||||
import "./tests/Base62";
|
||||
import "./tests/BitwiseOp";
|
||||
import "./tests/ByteRepr";
|
||||
import "./tests/CartesianProduct";
|
||||
import "./tests/CharEnc";
|
||||
import "./tests/Checksum";
|
||||
import "./tests/Ciphers";
|
||||
import "./tests/Code";
|
||||
import "./tests/Comment";
|
||||
import "./tests/Compress";
|
||||
import "./tests/ConditionalJump";
|
||||
import "./tests/Crypt";
|
||||
import "./tests/CSV";
|
||||
import "./tests/DateTime";
|
||||
import "./tests/ExtractEmailAddresses";
|
||||
import "./tests/Fork";
|
||||
import "./tests/FromDecimal";
|
||||
import "./tests/Hash";
|
||||
import "./tests/HaversineDistance";
|
||||
import "./tests/Hexdump";
|
||||
import "./tests/Image";
|
||||
import "./tests/Jump";
|
||||
import "./tests/JSONBeautify";
|
||||
import "./tests/JSONMinify";
|
||||
import "./tests/JWTDecode";
|
||||
import "./tests/JWTSign";
|
||||
import "./tests/JWTVerify";
|
||||
import "./tests/MS";
|
||||
import "./tests/Magic";
|
||||
import "./tests/MorseCode";
|
||||
import "./tests/NetBIOS";
|
||||
import "./tests/OTP";
|
||||
import "./tests/PGP";
|
||||
import "./tests/PHP";
|
||||
import "./tests/ParseIPRange";
|
||||
import "./tests/ParseQRCode";
|
||||
import "./tests/PowerSet";
|
||||
import "./tests/Regex";
|
||||
import "./tests/Register";
|
||||
import "./tests/RemoveDiacritics";
|
||||
import "./tests/Rotate";
|
||||
import "./tests/SeqUtils";
|
||||
import "./tests/SetDifference";
|
||||
import "./tests/SetIntersection";
|
||||
import "./tests/SetUnion";
|
||||
import "./tests/StrUtils";
|
||||
import "./tests/SymmetricDifference";
|
||||
import "./tests/TextEncodingBruteForce";
|
||||
import "./tests/TranslateDateTimeFormat";
|
||||
import "./tests/Magic";
|
||||
import "./tests/ParseTLV";
|
||||
import "./tests/Media";
|
||||
import "./tests/ToFromInsensitiveRegex";
|
||||
import "./tests/YARA.mjs";
|
||||
import "./tests/ConvertCoordinateFormat";
|
||||
|
||||
// Cannot test operations that use the File type yet
|
||||
//import "./tests/SplitColourChannels";
|
||||
|
||||
let allTestsPassing = true;
|
||||
const testStatusCounts = {
|
||||
@@ -5,7 +5,7 @@
|
||||
* @copyright Crown Copyright 2017
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../TestRegister";
|
||||
import TestRegister from "../TestRegister";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
@@ -6,7 +6,7 @@
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../TestRegister";
|
||||
import TestRegister from "../TestRegister";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
@@ -6,7 +6,7 @@
|
||||
* @copyright Crown Copyright 2017
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../TestRegister";
|
||||
import TestRegister from "../TestRegister";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
@@ -7,7 +7,7 @@
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import TestRegister from "../../TestRegister";
|
||||
import TestRegister from "../TestRegister";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
@@ -6,7 +6,7 @@
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../TestRegister";
|
||||
import TestRegister from "../TestRegister";
|
||||
|
||||
const ALL_BYTES = [
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user