From 6741ba07834cac175e94fb7c54dddf1bf24823c3 Mon Sep 17 00:00:00 2001 From: David Moodie Date: Sun, 4 Jun 2017 17:08:39 +0100 Subject: [PATCH 01/22] Add remove EXIF operation --- package.json | 1 + src/core/config/Categories.js | 1 + src/core/config/OperationConfig.js | 13 ++++++++ src/core/operations/Image.js | 26 ++++++++++++++++ test/tests/operations/Image.js | 49 ++++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+) diff --git a/package.json b/package.json index 4ad37565..01a55b8a 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "lodash": "^4.17.4", "moment": "^2.17.1", "moment-timezone": "^0.5.11", + "piexifjs": "^1.0.3", "sladex-blowfish": "^0.8.1", "sortablejs": "^1.5.1", "split.js": "^1.2.0", diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index 9e6f6157..2470cc0d 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -210,6 +210,7 @@ const Categories = [ "XPath expression", "CSS selector", "Extract EXIF", + "Remove EXIF", ] }, { diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 5fd5a9ee..80009b85 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3388,6 +3388,19 @@ const OperationConfig = { } ] }, + "Remove EXIF": { + description: [ + "Removes EXIF data from an image.", + "

", + "EXIF data is metadata embedded in images (JPEG, JPG, TIFF) and audio files.", + "

", + "EXIF data from photos usually contains information about the image file itself as well as the device used to create it.", + ].join("\n"), + run: Image.removeEXIF, + inputType: "byteArray", + outputType: "byteArray", + args: [], + }, }; export default OperationConfig; diff --git a/src/core/operations/Image.js b/src/core/operations/Image.js index 9ebafaf0..7a0385ba 100644 --- a/src/core/operations/Image.js +++ b/src/core/operations/Image.js @@ -1,4 +1,5 @@ import * as ExifParser from "exif-parser"; +import * as Piexifjs from "piexifjs"; import Utils from "../Utils.js"; import FileType from "./FileType.js"; @@ -43,6 +44,31 @@ const Image = { } }, + /** + * Remove EXIF operation. + * + * Removes EXIF data from a byteArray, representing a JPG. + * + * @author David Moodie [davidmoodie12@gmail.com] + * @param {byteArray} input + * @param {Object[]} args + * @returns {string} + */ + removeEXIF(input, args) { + try { + // Piexifjs seems to work best with base64 input + const base64 = "data:image/jpeg;base64," + Utils.toBase64(input); + const newImageB64 = Piexifjs.remove(base64); + + // Convert the base64 back to byteArray + const newImage = Utils.fromBase64(newImageB64.split(",")[1], null, "byteArray"); + return newImage; + } catch (err) { + // Simply return input if no EXIF data is found + if (err == "Exif not found.") return input; + throw "Could not remove EXIF data from image: " + err; + } + }, /** * @constant diff --git a/test/tests/operations/Image.js b/test/tests/operations/Image.js index c9ec9f84..78522b50 100644 --- a/test/tests/operations/Image.js +++ b/test/tests/operations/Image.js @@ -126,4 +126,53 @@ TestRegister.addTests([ }, ], }, + { + name: "Remove EXIF: hello world text (error)", + input: "hello world", + expectedError: true, + recipeConfig: [ + { + op: "Remove EXIF", + args: [], + }, + ], + }, + { + name: "Remove EXIF: meerkat jpeg (has EXIF)", + input: "ffd8ffe12cd645786966000049492a000800000008000f01020004000000534f4e5910010200060000006e0000001a01050001000000740000001b010500010000007c0000002801030001000000020000003101020011000000840000003201020013000000960000006987040001000000aa000000300200004453432d483546000000010000004600000001000000506963746f6d696f20312e322e33312e3000323031303a30373a30342032333a33313a31330018009a82050001000000d00100009d82050001000000d80100002288030001000000030000002788030001000000c80000000090070004000000303232310390020013000000e00100000490020013000000f401000001920500010000000802000002920500010000001002000004920500010000001802000005920500010000002002000007920300010000000500000008920300010000000a0000000992030001000000100000000a920500010000002802000000a30700010000000300000001a30700010000000100000001a40300010000000000000002a40300010000000100000003a40300010000000100000006a40300010000000000000008a40300010000000000000009a4030001000000000000000aa40300010000000000000000000000010000007d000000250000000a000000323030383a30393a30312031333a32343a343600323030383a30393a30312031333a32343a34360043490d0048e801004b9a390040420f00030000000a0000000300000001000000480000000100000006000301030001000000060000001a010500010000007e0200001b010500010000008602000028010300010000000200000001020400010000008e0200000202040001000000402a00000000000048000000010000004800000001000000ffd8ffee000e41646f626500640000000001ffdb0084000604040405040605050609060506090b080606080b0c0a0a0b0a0a0c100c0c0c0c0c0c100c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c010707070d0c0d18101018140e0e0e14140e0e0e0e14110c0c0c0c0c11110c0c0c0c0c0c110c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cffc0001108009600c803011100021101031101ffdd00040019ffc401a20000000701010101010000000000000000040503020601000708090a0b0100020203010101010100000000000000010002030405060708090a0b1000020103030204020607030402060273010203110400052112314151061361227181143291a10715b14223c152d1e1331662f0247282f12543345392a2b26373c235442793a3b33617546474c3d2e2082683090a181984944546a4b456d355281af2e3f3c4d4e4f465758595a5b5c5d5e5f566768696a6b6c6d6e6f637475767778797a7b7c7d7e7f738485868788898a8b8c8d8e8f82939495969798999a9b9c9d9e9f92a3a4a5a6a7a8a9aaabacadaeafa110002020102030505040506040803036d0100021103042112314105511361220671819132a1b1f014c1d1e1234215526272f1332434438216925325a263b2c20773d235e2448317549308090a18192636451a2764745537f2a3b3c32829d3e3f38494a4b4c4d4e4f465758595a5b5c5d5e5f5465666768696a6b6c6d6e6f6475767778797a7b7c7d7e7f738485868788898a8b8c8d8e8f839495969798999a9b9c9d9e9f92a3a4a5a6a7a8a9aaabacadaeafaffda000c03010002110311003f00068bb671e5ec1500c895b5400786055e0605b5ea299129b545514c16ab82e2b6b80c516b82e295ca315b5c46050b9062825b0a2b8a82adf55b836ef70b133411b057900a852454571a357d11c62ebaa826f51b50f5af862032979ac5708eb0cad546da094f6ff8adbfe346c953ae8e43827c32feee5f47f47fa2b9e4890ee457c303b0b433dcc209a9c09b506d56d83701b9c3c2bc4be1bf59eb6ccb40ff00ddb1e9c8fec9f66cb211b15fe95c5cf5cf9ff3ff00a51412464b1d3e4f85812f68e7a83dd3018f107439316fe19feb6197fbd745752c442c8ed1988f10c3aa7b1ff23fe23950241a29d2eb4e3f44fe8ff728860797271490ee581d9ab92301541da4e24476fde63284bfb58d11a651d0fef546d556f84ed8217f49ff0035d16a3181c94ed4b346fbd67b71461fefc8bfaad7fe0732f04fbf91630f5c2bf8a1fee3fe3aff00ffd00a99c717af541b9c0abc75c8aaa0029810bc0c8b2545c50bc0df155d4c2ad8a57052b7551df0d2b46788756c095a2fe05069b9c6d0a2faa00d451538f35b0ad6be60d46c64f56df70769216dd1d7f95864a13312d5931c6628a672d85bf9821375e5f6105ea0adce8f25036dd5a13fb43db2e38c4b787fa4718669e2353f547f9fff0014904f1dca318a6a823692361423da9d8e5039b7e58472468f229dfe83b6d4b4d8a6d32491b51863a5cdb486a6523a98cff353f63f97098f1721bffba70f067384f873e5fce48dec4f1e5f687ed53a83e0464625cff1624d214daf097971ad7a65b128914472e2952a30128017dc017f685e3dae6021891d6a3bd7df264dfa87f9ceb357a6b143fad8ff00e23fe250a6637aa250804f1ad24a7ed53c464270120e9b248e4dcfd4b6deec5bd15aaf68db78b467dbfc9caa122369391a3d71c468ef044cc11d0c7b3c32a90ac3a6e3a8396bbac9a686685c7aa4a92b251892ae84c335363edfc460eb4f2e4984bb8fd2ff00ffd10a8338e2f5eaa06f8155141c0aa82806e70522da7b98106ec2be18045287975544601456b92114134abfa45a80f1d8f438d23896bdf4bd875c8af129b4b7527d9db088a0cd62add13bb6f87851c4b9ada50b5663538084095a7b169364b60f73688ba85b955fac06052e2dde9bfd93bc7fe5633c46b8a2767132659835f4cbf87f9934b24b6f4c7ab17ef21e95fda53e0c3f8fd9c8c4b660d40c9b7d331fc2a6e4b0fe188e6e5014a96beac12a4d1318e543c91d0d0823b82325747662636c95751d2bcc016df592b65aa00041aa28a249e0b3affc6f97090c9cfd32fe77f39c4962962de1ea8ff33fe252dbed3f54d16f84572a632778e4527830ecc8c3299c4c4d14ce10d4436e63fd8bae1c5f3996e1fd3bb3ff001f4a29ccf8c8077ff2ff00e0b2b323767fd37fc53ac198c7d19072ff004d14bae24961611dec7e993f62e00fddb7d236fbb2e05be3aa303eaf543f9fff0014a77514b1d07246e42a287b1c85efbb990d5e326add6cd2dbc8b3aa6e36643d194f5072709f0972270138d21af81b2bb5beb6158243fbc4f0af63f3c998806ba179ccf78f271ffa78fe3f9ebef204a0bb87e2b79802c3b0aff03954a025b1e6babd38006487d13ff62820cf6a76abdab1fb3dc1febff12caa3220d16ad26b27825b7d3fcd53d4214172b223036f7c940dd848b4ebe06bc72d98dafb975e2329f1c7e9c9eaff0039ffd2297d4d15c2a8a839c707af34179bf97a05df145b5ebde3123a62c6db617463a16dfdb07328b5392d4800b3127ae4e96d171431f004ee461000624a2e4e2c828294cac9dd0b68b415eb8d3605e84a76d8f7c04b1215912bf10c36c486a9f1d1bece56c805682f27b0ba5b9b37f4e551bf7523c1877192848c4d844f1898a298b2daea8a6eb4c516f7e809b8b0ea187ed345fccbfcd1e4e5112de3f57f35d66a34f28ef7b8fa727fbd9a56d12ca098978cabbbc1fc57c46439b769b5b67827e99a902c69e18097654af00b633d273f0d36f0afbfb6188079b0cbc5c3e9e69b58799ededa3fd0faec667d2a43488b1a98fc1a193b7fa87fd8e64c0edc32dc3a59e7a96ffbb9ff003bf9dfd66f56f2ec9a7c02f6ca517fa44dbc7709bb28fe5900e9feb65797098efce2db908ce3f9b947fb34ae2d45d216b690896ce5eb1b74affc6ad98129cb1ec3787f37fe25d4f1981a28196d2e2de3792dd45d5883592d9b7741f2ff008d866463cf190f2652200b8ef1fe6495f4e8c5f2b0d3642cc054db48431fa2b43970c065f4b6e1cf21f41ff3565f42f1c452e6231edc655208f87c457ba9f8b24227e929cf9f8fea1fd197e3fa29758cb2c32358330742498586ea6bd40f66eb9127ab6766e61be19fd32fa575c46622548ac4db53f81cae71120e36af48714abf84fd28195163a249fbcb673553dc1fe0c3230279170f96c5ffd32211af3dfa8e99c800f5d2e6888622d257af86202248dfabd08276c34d56a72bf13c7be00caad6a51fedf6e986d055a092dc13cba0ed8c9785106970bfbad80db2ba21452d96d98a56b4230025b5aa3fa7d6a063694c6f345d5ec638de45564963128f498390a7beddbdd79603ce8b8b1d46394b86f740a0661d7e9c69c85f50053c31a4b71318d8491b157520a3a9a104770460ded4ee374da2b9b3d68ac53b8b5d5c1fdcdcfd98e66f06a7d893fcafb2f969a9794ffdd3aad568b6b1cbfdc7fc750d3412099ad2ed3eada826df17c2b27f46c8f0dedca4d5a5d7981e0c9fe99012218e42ae0ab0eaa7ae45dd8208b0a8ab585926412c2dd636dc7d1844e8b8f9f4b0ca2a415749d4356d11dae347737362dfef4e9b2fc5f0f7a03d73371e5f8879fd4693260363d51466a0ba6ea369fa53455e11c86975647a2b81f677de371fb20fc2d9467c11ab8fd2d5927e20bfe2ff0075ff001e4aa1b978caba315a6c8e36653dc11fad7355931189b0e3d90a5796915c49eb4245adef50cbb4521f114fb0d96e0d4d6c51f615f6be63ba15b0d52ac07c244c390dfc6bff001219b2f16446c6dcec19f1cbd3947f9e965d5b34523440fc517ef2071dd0ee3fe072a27ab899f19c73a1d3e9ff007a8c82fe09d394e9bb0e32f1e95ff3df2b96c5d80d7f890e198b43dcdb888d0fef20907c2c3bff004619331121b3af9c1fffd42c58b91661d00ce40bd792dc3ca36af6ee31b6063688e72ccb55d80c06458d529143ea02c7a75c2cc2a3aab10577f1f0c1c94058ab116a30d8f7c46e83b23ade35896887639191400a8cacc877c8b30a203a8a75070aa3f4ed52e2d53d0957eb1624d4c0cc54a31fda89c7c51bff00abf0b7ed61e2da8ef171751a5865e7b4bf9c8b92c6ceea269e09c705f89e42a15d3dae117fe4f47f07f938401f0fc7d7ff0014e18cb9706d3f547f9c965eda5c5b4aa93af10e2a8e0d51c78ab0d98602087658b2c662e26d41c88854ee0e45b691a34b6f85e19e19f90078a3153bef41cc283f41c8d8e4e29d6e38cb865e93fd24c56f62bc45d375c0d132505a5f91f1c7e01ff9e2ff0088e59c40ed2e7d24e2eab42320e28295d40d677315b6b508709436f74a6a92276f887da4cb7ad486ff00ee9d760d5cf09e13c9d7bf50a0f4a06891bf95f9a9f913956402f61c2ee716632162424a115bc4afc92565237029be40d8dd8fe66f6215923892ebeb30bac170e384f4da3997f9655fd4dfb39918b383cdd4e6c70e2e286c7f9a83d4adbd190cc8b589fe223afd1b771fcd94e5c5c27fa25c59c6f7412ba940e87d489ba8e841fe0d987970b8c546ea28a5840947a908d9251f6e33e1ff0036fd9c8e2cc6068b13f620156e2df8a337a8aa795b4c375207da4dfecffaa73620c6412096c4c2d66f5177b69c7c60ff0029fe287235628f36fd2e6109fa85c25f57e3fa28d7b365857837c2fd509a8f620e4632a2edf51a08ff0001ff0035ffd501c82ab713b1ed9c7bd700d23a1a03d09ef8548d959d8a6c83e123a60456ca45a80f3ef86d695a368d14721f6b011690e9e307651d7a1c4162559632b1835ebd6b903cd905601ca003607be0b56dd0a05e7dc540ee7082c429cce480476e83109a55b396ecdcc5f530df5a0691fa7f68d7b7cb271049a1cd84c4787d5f4b38b2f2dffa2a8d5648ad965359b4f51ea2127a3a8a8f424f1e0dc73698b424c7d66bfa2e8252109de226946ebf2f744a97fd2ce905392c7c14b807fca27f8627b3203f88b991ed29d7d22d45fc8b1c11b369fa899108afa73a0e276ee41ff8d72acbd9408da5c5fd644b5b1c9b64884a6edae34e7f42e151a3ea6094196023a555beda7cd4e6bce1962347fd2ffc4b18e98fd5825fe6a67a5de6837b6bfa2ef58c56b21ac704edcc46c7f6ade71dbfe2b7cb61c321c3f8ff00364e36a32196d9070cd29d56c2ff00406fab4dfe93a6b9adb5d2f415f7ec7db25663b49c3b3050b6d4f4e423eb711656feee688f1a9f0607a1cb31e3c52e6ccea67de8c924f2f4a2b5b841dca80f8cb4986f9ca29fcc4b9a94b71e5d4b79214be75256b1acd191461b8de9b6f968d3438684b883664d5f10dc6ec6a42f0c9eb440153f6d3f64d7e5d8e6b251e13479380762afc9197d6b7358dbeda36e47f92c3f8e533c36a47721cd54996dbb7f7b036e36dfa7ed2e5512607763cb928ac2970af1c3fb557488fda560370bfccad994725ee9ab1411de5fb8b1bbb57d3ef1bd2b886a6da6f14fe53fea1ff85ccb842131bed273f4ba89c8708dcc5fffd62e2d1a76a8ce429ec02ee49455a6e7f562c51317062431fb382904a9ba2b0a8dab884ba4456403baee298dad2a293c4002a460b4220ab98771be42d2136d2b4bb1d42d1520d4238b56a9a595d8f4a3735dbd397752dfe4b71cb23107aeee364cb281dc7a3f9d14bf55d3356d3ee8c5a84124131fe71b37faac3e161feae40dc79b7639c642e25423579691c6a5a47215146e6a76184032200644888b2cdb4893cb3e54b6336af3ac7a83292ce7720569c540cdfe9b1471c7fa4e8b5396594ff45bb9f32f9635711a5b5d24f2ca0b2321dc53dbe9e99394c35460428db5f69d0235c6a13d225709f113c4ef415fa72066032e1279215ff343c9b15d0b54ba018311c56a7a1e950388c94667a20e33d54dfcc9a2ea704b1c4ad3d93539b0a7250687e1af7195e694251a2d98e3381b0a175a4e8f0dcc56d697ea7eb2bce2827d89a0dc123fa66a351a7e11c4370e59d4898ac91e256867d774b431bc5f59b13b3c47f7d111f8f1fa728c7a9ff383893d2c25fddcbfcc9b4961e57d4987a55d3e67ad6d64de073edfcb43fca73263c24d8d9d7f00068ffa54a2f3cb1e63d297d78e332c0bb928dce9feab0fb43fd60b96891fe20c8e107789ff0034a845e608e54f4eeada398aecd55e2e3e63194011e96b333c8b7789672c6b35922a8414922049047c8ee331e62fd32148901561296ac0fce22551beff00f6c7fc3653026268b4722b9f9831bb0f4e47f8a2957ec3fba9fd6b872e1a163e9654a6ea2570ea7d3b91b823e10c7dbf95b31b80c7972470f5085914cae493e8dd21fb7f64337f95fcad97e3c888488363693fffd7284042904743b67216f6055e40c2441d88d8e218ae50cac5fa8aef816955a930544aa9c15482a8d1180508e5e38a04ada32102aa37187609ab56866924251bbe0a5269d2451d0ab50f8644858c933b1f366b16500b39c26a3a69eb65783d4403fc83f6e3ff00627271c846c7d43fa4d13d3449b1e897f3a299e95a6e89ab5e473e8ef369d78ac19ac660668bad498e61461feac9993a7803306361c6cf39c62632a90fe7241f9afe5ed5db50bbbc1207b168d15054171c7ed514d396fbe6ca397864e098dc764abf2a3c9d2b6a4758b90b15880fc5c218d49216a029a7876cbf2512d512427df985a59d574afaae92448e18cad0024315535fc40ca274241b604d178f693e5dbd97524b60a23bb576352bc7663521c9fe5f0cb6796222c63024bd67c9fe49d52c619d6e6506adeb471a7d9236d8fcf304dccb90642229865d45a8cde7d8da69c036d31892db91a8404f6e9b29cb8802041602746de8769eb0b9e16b7c2dfda5ad01f0233512d0891e8ce5adc331eb8ee9a4a616b4922bf9ad660c391e04a3123ba9ecdf4e2745388da71feacdc0cd2c67e9bf8acb0bbbdb3547d36f44b048392db5c1ad40ea0378ff00ac32a8ea278feb1ff12d3b859a845e5ed5ff00777d6a6c2fcf475f86bee0fd96ccd8668cb741024c6afbcbdaad8b97b63f5c8177e49b4aa3dd7bfd197736a31212f59a3950f5046cebd0823bd33172e3bf7b1e6bad2fa3b626daf944ba7cc6aebfc8dfefc43dbdf23872f0ec79318cab6289d5343b9b51ea467eb36cc392b8a160a7715a75f9e5d9f4a63b8e4db49638499784a68c05165ea76ecd4ea3308e3ea10637ef7fffd00170010be9ee0e723c9eb22e347401b623a60660351875aa1dc1ef85079a2ad8c48c4d7e2191218485a8dc5ecccc5a9551d3084c634d5bcfcfed0fa723216ced13c68448ad80ecc4eedba31fde570246cac9cb6246d879312ca3f2fa48135b01a42924a0aa252a0fb9f0cced1c85f9b83ac04c7c93ed6344b5d7219ac6edaa049cd68487001ec46640209ddc3a23931df37dfcba2e976b63a6dba3adb0e1c2bc78af124763fb54cc939a31e6d51c464c2f43f336a53eb0d2cf115423821614353ef41b7f93951cd122f6e26df048dba3255f2ce9d7f787519a2add2303c81f85c0dc0651e1e3944e61946c724fadeede3bc48cafc007150bb8dbc680e571cbea4ca1b24b3f9322bbf3bbdcc6844660f57d4509512b7c14a12a5bc59466466c91aabab7127b452fd634ebbd22e85beab6efe937f73728483c7fc863f6a9fc8f98128189d9c3be13b8b0b224bdf4fd4b5956eedc9a54ecc3d88f1c0353d241bce28917095fbfea529ae450c732342dd40229f10e85586c1b2c12891b38e491cdd1eb37889c2455bc807db8651b8a7707ae60cf07866e3c981994c6c358b69a9159cc6294fd9b4b92684ffc572f6fa72dc7a9e859c660a8ea02c669e97b0345743666145947fc692ae6613637410121b8b6e1c918f38abf04bdb7f1fe5afbe62ce37b8e6d442eb2d4e7b54f459d82447f77de9e2847f2f8618e690aa2d90c9b51576161a9ab4d69fbaba5152a07eedfdbd9b2e204b97a65fcd6c23a87ffd12f0a562a035e39c8757aeb6a542c10d695c348b6d832fc34f6c78934ac901552f5de9915e2535914c6477aef849400a91491315420815f888eb4ef4c36106d381a3d8ddd4e9b732301d6299417fba3f8bfe49e08c78b93892d54a1b4e3b7f3a2b2f346d52de225a0631af5953e351f32bf67fd971c07198f30db8f538e7c8a0e12c45396d95f15f26eaa4c749d4e7d2af16ea2557e26bc4fdd9762cbc06dab2631314f44b4d42df51b48f50b7014c83e21d3891d41ccbe3bddc094083494eb1a569b79306912aeca049434a657395ac7648c794b4622408b346e2abca4228a2bd57e7951f26de328bd3f4bb1d3c3bc4feacb2ecd23b135c78875606dafaf14708c3f7c0d100ee3da995f8b4764f0a1efa695e213dbb8498305a3350353b577197e5c232e3162da2f18954f92c4f306abf57fab6a56c97f675afa7200e07cabd3fd8f1cc38e2cb8fe926bf9b3fde47fe291934f8663d12dfba4ba3f2f699342752d26692c08204a95f563527f6658dbe30bff000b96649ed790708ff54c7eb87f9dfc5175671189db6217ae9f21602ed63319eae94643ee2bb8ff0054e4b0608cb712e28ff3a2dd1c808a9734bf5bd12d804934d2a6702ac8a405703b7f92dfcb99392a3b5d844b4f62c31a78d665e480abf75e8790fd4c330f361af547938262985a6b933c4b697b0a5e47fb21fe1627fc87fd893fe27870e6de8b38e5bd8ae7b64995a5b0769507f796f200b711fb11d1d7dd7334c2d35dc95dc43ccf25346028e00f0fe65ff9a7318e3218108753756ee67b298c5311f1f1a71703c474e43be5f872744d90ff00ffd22949080c3af13be722367ae905d72dc91590d08eb838b7481b2a890b052db62854329008ec702045608d1d7929dfc3254b6b9762283e9c8952adf0ad1f951874a1debed95d8b5e6c8f40f3079c22751691cf7c8365596269683c0494e43fe0f2cf1a71ebfe99c3cfa7c279d459645656bac0ff0073fe5b16329eb7892c7137cfe1657ff8247c075388fd437fe8ff00c75c1390e3fa27c412db8fcbad3e798a691ae461bbdbdd71661f278cfeb4c94714642e12ff004cdd0d791f5453ef2ef95752d1ada5b6b9659a3958346f112ca187c86d5cc9c3c5f498b4cf52272ba416a86381dc90102ee4fbf8644c69b6d8ab6adb33716925249e26953bf615ca886c545d423112b4dfb9077552286bf2e995982da166bf86e61758e391e36d8c82818d7dc6f8c61ba540daac5a618880de838969522aadef4d8e6c0c4986db3839cf522d0713233016f3c91b9e9139afdc0f5ff639872cd387316e1d425cb6475a4fae5acc2e2dca995450f6e4bdd595b6653e193c7aa89626241468371707d4d1cfd4f50a1693469cd60980ea6d98fd9ff8c5957e5f1c8f1633c12fe83120a5f1f9834c9a530ea103e9d76878bb50940dfe52f55c264794c710fe747eaff4a88e523af0a9eaba33381776a55b97fbb633ca3907b91d1b2fc5115e93c516331691dcc120e42542adfb5dc1fa47ed663e6d3f58b44a2bac2e6396758ae2e3eaf2f486edba57b2c8474ff5ff00e0b2cd366fe19158c935b9b69c03fa4613b7d9bf828c0ffad4fe399b38ed6dc3cd2abbb392302746aa56a9731ee0f71c8663ca1ba4c5ffd32755e124849fa33912f5cac5a2921a5006c48a505a721a0a537182f7485cfc8046a6ddf0a2ddb03b1a5705aaba384fb5be0a410985b798b5bb4b7582d2e7d0856b4e11c61b73ddf8f33f7e3640e7b34cb4f091b22cfc5d3ebfabcfbcda8dc313d47aac07dc0819031059470407f084b26b8f5d88798c87fcb62dface4a806e1103905f193171646e0c0ecca6847d2322931bd8bd0ff2dbcc37d717eb612cd2cecc6bcd8d4220ebbf535f7ccbd21919d5ecea35da611a94470a2bf3157d1b3b8643f0d682869bb6cb5cccc9005c7849e7d149a847468e345e43e295b76f6a023318c29bc1b577b5b8b93598991d507c4c766af811c57e8c81d99029859da2470807a7ed329345246c061ab412a66ee4b6be590bfa90d402ae05180a6cc1b3261c9a642d177ba368fa9ca52cd974fd40d196d6427eaf2d77fddb9de36ff272a911c5c279b8797466b8a3c921b97d5f4ab936f7b1392bfeea9766a78a3f47198d974913b869c59ce33ea1c4a91dd5bdf0e319f897e2e1bacaa4771dff00d92e5021c06c7376909e0cb1f3fe6a2e592d75702db593e8dd20e36fabd29d3a2dc01d47fc59993098c9b1da4eb32e3df7ff004c9330d6fcb77a501e15dd93ed432af8ff002b03954b1989b0784b8a41814e6daeb40d6e2e4b27e8fd400fde44c098be629ba8ff0085ff002732239a276973671225e452fd53cb92467948b40c2825520a381d0861f0b7fc4b232d303b844b12034cbbb9d32f543c9208a9c680fdc190ecc3dd72b8ca78f9221e93bb52df0b6bb926b623eaf29acb6bd0a13e0a7f61bc32dc7949e6ccc8479727ffd42466596d8bd7e2e8df46721d5eba97aa30b4471b9184f34c5561642943b138a0af662001db012a22d33fb7d38a533f2fdb2b34b2cd6d6d7aa365b7b8b892061fe50f4fed035ee725c60731c5f371b513aea61fe6f1a71cae789f43cb163c47ed27ab747fe4e93ff0b9299321b461feedc784a27fcacbfdc25afe62bcb79bd2163656cde1f5440dff000e09ca08efdbfd8b923003d652ff00396bf9a35fe4bc2e5235ff008ae1857f5263c16bf968777db2566f32f98aa07d79b7ff00253fe69c8f004fe5b1f77decaff2e757bfbad6592ee6965a46c56a144751eeaa0d733b45000934e1eb30c6205045f9cafa2b995a009536afeac887a1a8201269db2fc991c68c5848bfe6ecc92701c78d1791dc6d53405572156cea940f195ffd22a217350fc89e247ecd6871e101368998358462e2da43242c49915aad4db6d872f96428f4643cd2c9aebeb91cb2ccb20263aa46e18a923607c48a8cb4028e4c87cb7ac5f32c96b2ba7aa2929f5539ab03e15048a6473eabc281911c510e1ea6040b89a4d751924be8cc3711413c1d9412a41f14e5f60ffaa730476c69e7d785c137d77627a97966e50fa96cacea0d53f9d7e4cb9911c98f27d2449a8c1029aadf5b1f4afd1a4886deb05fde01efd9f2271516633486c7746c52fd66c1a2b722ff004f5a911ad4c90b1ebc01fb23c626ff0063970163f9cbc625ee49eef4d92255bcb462d086a2cab50c8e3f65875561fc8d9464c25a658eb70af61e63d4237da758e66fb6928e56d37b3a7ec31fe75c8c32ca277dd9433743f8feb265eb68baa4861914e95a89a73b5987285891d53b80ddb8ff00c0e665821b051f2280bfd2af2cc832a7c1d119bf79130f05906e3e59038fb9062fffd5298ade308c0f47e99c853d7deeb232d11680f4fd9c20a3aae34014d69e3812ab2042a38b57db10a5750f0e95c4a86a2241a9aedd30154645a85e29f858b01b8e5bfe3d72424438f934b8e7cc27565e74b95845bddc1f588c76722407db8ccb22fddc72d19cd6ee29ecf03e891822d6ff00c85a811f5bb0fa9b9fdb843c06bff3ccc917fc2e3c58cf31c3f8fe8a383550e47c46e4f2ef956715b2d66487c05c46b2affc1c457f15c1e144f2923f94271fae09df923ca93d96b1f5c8f5282784a32708096e60eff102071cbf4f88c4d92c33eb61963406e87d7ee4df6b7a9dbc287d08e2559ae29f0a12df0af4dcb53b6195924f46036018bc1e947f0dada9bb1521e595b82b536341b9f1ed938ee105d6f3d9bcbe92c724570e2bc1a80141debf65b89c05907094c7231a290013c36a96dff6bb7f2fd9c1114c8a5a4348222b2845b49c9f8b66e3bd003ff356106b74d222caf9d6e65588fab216288d21a00ecdc885eff463216082d792161447986559de2bc468d9588631b10cbf30763f76687269803c817591d4989a946331fecbfd3232defeee4606caf3d4ff0021b67fb8f5ff0062d950c1889e5c1272e12d364e44e392bcda9dd8a8bcb24bb03af052b27bf4cccc67247612e21fed9ff14c3369384583c4a1696da2debb5e69d34963729f6d88200f6778f7ff00918999d1c86bd40c3fdcb8421d42b5cc77f67ceee789a51320569e3e32dbcc0741281f0b7b306f5172d80eb7c4194a5e4c7f51b4b19a92db37a6d27dbb77af243e1520735fe56fb5fcd95ce01c79c41e497ccee10417aac427f7721ddd076a1fda4f6ca2371361ab96c51b67adea56038349f58b390746f8c11f4fdafa7e25ccd8c848586e8e521fffd62884b00a877e3df390b7ad28a9e389944a3ed0db05b1050cebc89a8a03bd30d36872ac4803826bdc61e881b96c5d56ab4a60b4f0af8837527aef8690aa0480120ed8d215adda22a796c722420a84f2c319a861bf8ed8d81cd953a34ba9d87d5e29253ff152337fc441c3c24f204fb831240e65eb3e44d256d34c7bd76324cb1105e5478e58f9f543cf8d77f6cc9d348efb174d9c478f9525f7d6732f976ee92f17bab9ac9213bf053422bff054cbe5f4a0736377b259da5cada24fe90e20c4c0f21e3e0c37e9804acec9aea837983b299191258fe2695457e0ad2878ff001c8d9b6c0104ecb13b7350a82a5665342caec7c7a32ef846ccb9a9492a8778e451342e94691762c79029cbb7c4a7638ad2cbb8ace082194d4490721242e7b720439f7a7ed64cb00776437d7e97f1aa6a9a726a36ea8384ab486f107f9130da4ff55f30f2cc035270f369f6bb07fa29249e598ee18c9a0de0b96039369f703d0bc51fea9f864f9a6552c008db775f2c5bff00c520e2f31eafa74a60b842eabb3c1382187b03f687df94c606276ff64c0e4947605151eb1e5bba9565134da56a03a4a3fe6aaf165f67ccd8e7007d3fe95946609df628a8f53b9b776749cd9cd5df50b65e56b283d3eb101aa0aff32e1c79632e5b16e9c48ebfe95ab8d634a7262d72c56091a9c752d3be28181fda684d7fe132ebb69240e63fd2b9bcb6973099f4eba4bdb6fd9303026be0637e9f21f1644e2894880291cf61716f234662a57edc26b1b57c78b6d5f96446120d863c0fffd72bb7bb8c2947143fcd9c8bd690b595cb32ab6c771869545e59540461d3a1c0cc056440103f51df15537953951571092c8348d0f4dbbb54b9bef3058e9cadfee86124b381ef1a014ff82cb630891bce31ff00672713267903421297fb94c7ea5f97168b59757d435393fdf76b6eb6ea7fd94d5387f723ae49fc383fdd3571ea0f28c61fd62a6baf7942d36b0f2d89abd24d42e6498ffc8b4e2983c487480ff929233ff629f0731faa75fd48b67cf5aac3fef058e9b62bdbd0b38f97fc13f3c31d4e41b0e187f52293a289faa5397f9ca727e6079e652ab1eaf325762b12c7181ff02ab88cf94ff14becfd4a745840e5f6c9ea3e5b69e4f2a7d6ee6633cf70cccf3b906bc7c4edcb7ea7326aa2eb881c7405283d92cba1c35dd7d3e649ef5deb8c86ca0bcf6e2d25861904919592688fc40024346d5500f8d3e1a656090d9cd27fac178123a17676e0dfcc40fb40d295a83837b6ca58f44f459872474f816848508dc69f4e125900b3d38feacb2280aca38bc6a2a4bc4db487af20c8324189288b9b759ccb0b9502f362e76a2a1aa9f1e5f695b2d1dcd375bb5797b736b398623fbb00710f1923dfe25cc0d4c883cb670f31225610d77aca5e8482ea088fa7bc722b706047f2b7507e9ca4e527c9878c09f50130aa35a8a58843a9c5fa4add471569c52e14760b3a54b7fcf446c98ca0f369998f4e5fd2ff008a42cda0797ef559b4dd4e3b77ff00963bff00dd9f92c9f61bfe17260c4f56a301d364ae4b2d6b4c2c9196109eaa8c2585bfe049189882c4714792eb6d5ecf68ee53ead216f88b7c56ec0f8803927cf271321cfd413e25f30ad25898e6375a44cd693b529c24aa31f0571f0b8ff25f2c8cef92f0750ad1f9df5081bea7afdaaca8363214e54ff5a33423fd68993fd5cb626f932190f22fffd0202c01a5363b31ce469ebc96a3e71cdc0b547ec9f6c2b4bf9c84988ee3b1380326e19f8068a4fa0e14014be3a7daa54e21495552b20ad28463485c5011b1a634ad53e120b570a0af88803e2dc74c8a944699a7cf7faa5b59db426e249dc208aa541a9fda23a28cb31c788d535e59f0c49ba7d037da7c7a7683058a11c2da3e145145d86f41e199d945507498e5c52252fbb2b35988536e7c13e82301dd9311f37c11c002ab16450637047206bdb6f7c8c8328961822416deaf32ace4142bd449fb4483e206544d378dd4657412b492025437114eabca95217bd695c883bb3ad94212e6628d192dc02ab93b80c6aae69ec007cb22c24173098db321955e783d38a1604072180ea3fcae996823ab510ad3dfc6b04ad13bc8b6af49850b3287028d41fb072bca080e26a06d610a9a8595d2543c5282770ca09feb98c780f30e089169ac2cdf711d3de26ff8d4e446289e492a0fa502a78ca69e1227f4c4e15010ff00a32ee2de250c3c636e3f86d80e028e052992e07c332bf03fefc40e3fe1860e0218f0a1238d2266312aa06356542c8a4fbaee9f864b88a88ab4f7114f088ee237650280ecf4f91d9a996c667b948b7fffd18fc75a7c5fecbe59c93d7aa20844c37e408fbbefc507c95a416d5055be2f0c4a624f550a45c8890d3c0e1648880807a557b62c1104afa678af6d8ed80207343815277a7df926c5d185a354febc894351d431ee3241059dfe4f45a51f372cb773b2ddc68c2c6db8b524908ebc94151c57fdf85732f46071dff17f0bafed132e0a1c9e95a9cba8fd6292c27d025b9355761f7e33bb70b1d56c83d23d4faaabce7fd3787148c7f396346afd9d874df2c8f2f344987f9a05c7a8c01a283f19a0ddc529d77151ff000dc7212671a61b1862f2a82160561ea3ee433545280f4ca0b921467a1b98bd71c4ab398b86e58d771b7d38c52796ca363ea1924e1cc382c108a54c7c450ffc178e4c31921a90064170c1a4480052d5f8cf36f4cb765d8ed96061ee4cbcb5f5b8e56fabf099b98f5c27c0dcf71f75373fb3961e4d19792cf30c9a2bcc56ea2b78a7aeef6e794bfecb81ff00892e6264e1bf375f28c4f321049a5da1b532dbeb4aae378ed9e19cb1f62e102ae44c635b90c0c63d0a8c6fab2d0346ce3b303f0ffc3d300df95b59b1e68c8a4d5006ac558ff6e94a7d3434c27887716dc7c5e6a914b72410d0c8a3c432b2fe2572c893dc8bef0a5726d7fdd8233e21a80ffc29c96dd507c902c34863d4a37f9153fc307a18bfffd9ffdb0084000a07070708070a08080a0f0a080a0f120d0a0a0d1214101012101014110c0c0c0c0c0c110c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c010b0c0c15131522181822140e0e0e14140e0e0e0e14110c0c0c0c0c11110c0c0c0c0c0c110c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cffc0001108009600c803011100021101031101ffdd00040019ffc401a20000000701010101010000000000000000040503020601000708090a0b0100020203010101010100000000000000010002030405060708090a0b1000020103030204020607030402060273010203110400052112314151061361227181143291a10715b14223c152d1e1331662f0247282f12543345392a2b26373c235442793a3b33617546474c3d2e2082683090a181984944546a4b456d355281af2e3f3c4d4e4f465758595a5b5c5d5e5f566768696a6b6c6d6e6f637475767778797a7b7c7d7e7f738485868788898a8b8c8d8e8f82939495969798999a9b9c9d9e9f92a3a4a5a6a7a8a9aaabacadaeafa110002020102030505040506040803036d0100021103042112314105511361220671819132a1b1f014c1d1e1234215526272f1332434438216925325a263b2c20773d235e2448317549308090a18192636451a2764745537f2a3b3c32829d3e3f38494a4b4c4d4e4f465758595a5b5c5d5e5f5465666768696a6b6c6d6e6f6475767778797a7b7c7d7e7f738485868788898a8b8c8d8e8f839495969798999a9b9c9d9e9f92a3a4a5a6a7a8a9aaabacadaeafaffda000c03010002110311003f004146684bbe5c06452bc6056c0c50b80c8a57818ab6062b6dd3156c0c55b03155d4c55b51814ba9be155e6293d2694213129e2ce06c09fe6c68d5a3885d2c5dfc29deb8292568215846e6a8768643ff00269ffe65be4a9c412f0a5c27fba97f77fd0fe836cc8bf3f0c0e5a934b1d7738ada91bc881e23738d2daf8ee4484c2c281bec13d2a7f65bfc97c9445ecd393bff00e567f510ca849faa3fc2412d6ec7a83fb51e2458759286fc1fe7609ffbcfc7f1b92674a0762863340c3aa7b1ff008aff00e2190068d15c3a8e1f4cbe8ff70aa41ad5b67ea4d7635c3c3d1cd94481b7ef71285cc48aa641d8fef074d9be16db046fe92eb32c40e4d4352adbd6587a8fe78ffaa57fe032fc72ff004b2447d51fe963ff00a67ff1c7ffd0457340ef970eb81578c0ab80c0ab86055c3155c3156e98ab862adede38ab46441df156beb318c554daf003b0c56d522d4aead9bd487becf19fb2c3f95c64a32312c2711214514d6f16a89eb6947d2b95159b4e7a03eef6edfb6bfe7f065a6025bc7fd2348c9286d2f547fd512e916504a49f2743b1f91f039486d9c44e347e9923ff0047c5776b1c966ced791a52685f73211f69a223f6e9feebfe4c3c3c5cb9ff00bbfc7f31c6c790e33c124bcdbfc2187c43bd3b7b30c8872b8c5d289878bd695c9852556b45dc6280ba51f5a80b2ff7f11049effeb57fcac91ef70f3e2bd87fc92ffaa5ff0010a2643723d40b495451e9de9fcc3fcff932128821d7ca467bff0013514c22146ab5b9fa4a1f6ff2321135b16dc1a830d8fd0aae1594a1a346e080c3a6fdd4e58ec6586392363f892f572b4249565262929d7dbfe365c1d69d2d989fe6bfffd1457340ef978c0abc0c0ab87be34869a58d46e7052549ef1548a0ae48045aff00ad1a03c7ae0a45b4d70fe18136b4bccdd30808325a04c4f5c348b6cc4e16a4ee7010a0a609676e2d9a78145d4141eb72056685a9f17d83bc5fe5e19436b8b8f29caebe99ff00d2bc88568b80e71fc7174af704fecb8ff8dbec3e441678f289edf4cff98b5aa47f0c5bdb883232c8a4aba9aab29a107c4361bae4822d36fad5a6a748b5122def76115fa8f85fc16e57fe667fc432d044f9fa66e3984a1bc7d51fe6212e2daef4fb8114ea518eeac0fc2d4e8f1b8ffaeb2a9031e699463963b372b7d658c93370b8ff00968029c8f8caabfb5ff167fc1e44c8dd97084cc7d33fe1ff004f042caef1304b95e04fd9980f81beef87fe0726da3318fd5eb87faa2d99244a0aab545450f6c8db903342eadd11785c4aabb8d994f420f553928ca8b6ca2242946e2b6d38ba83789b665f9fec9ff5b2446ffd175392e12e2ff959f8ff006d5d3c6bb4f1ef0c82ac3b0aff005c818dec573e202a71feee6a00b43d3e2818fd9ee0ff00cd5ff13c84655b161833cb19dbe95b728a260ea47a374b40dd83ad3fe36e192977ff0035753465c51fa727fbb7ffd204d76a18003ae681df16feb0fd00df145bbd49c9a74c516e226e1b9dfdb145ad6868072249c349b57444e209ed8589577a328a64156fc34df1a6417292bdb638da297aad7718db1a753e2a374c832017a4d25b4cb3db3709146e7a823f9597f6972519107644a02428a2888af019ac944376a0996d3a861fb4d07f3a7f3c1933112de3f57f31c3cb888def7fe0cbff0055507c448098c51c6ed17fc6d1ff0032e41b316a2cf0cbd3916024d3c305b96ab1884c9fbdfb34dbc2bfe57b6102d8ceeb645dbeab1409fa3f534f56c1cfeec13529e0d6f2fecffc636ff6197c4ed52f53af965a3bfeef22ebcd31ed23facdbc9f5ad3e4fb132f55ff00265fe5ff005f213c7c3fd56722328fe6e5ff00a68848ee996330b1125b3f543d2bff001a3e62994a1b0fa1c0e2e128678258519e1027b406af0b6eca3e5ff1bae5b1c80864686e3e9ff5355b55fac822c9cb102a607218ff00b1ad0e583193c99e3c847d27fcc5b708510acc8536a48a6a3e1fe65e5dd0fc58403c8a7264e2e63fae85b7778ddad1983024fa646ea7c47faaff006b237d59e9320df1cbe993722fa6684551b6a7fc6a723216d39f0981afe143baaa7c2df1c2db83dc1fe0f8227a38efffd32e0a396fdba6684077a5511097af5f0c690511e96f538582d73c4d300654d2fc5f6be8c282be368aa6bd076c056954d251fbbe8302869e2256b5a1180166d1e417c40c09454f637b6caa5c2b074120f4d8390a7f6b6fd9ff002d39a62451a6819604d7f121d6ac3f8e2dcbaa29f2c695ca78fc4a68ca6aac0d0823a1046054624b0ea2424ac21bf07f773fd9491bfcba7f7537f97f624cb7eafe8e470b369f6db97fb8ff008e29ba3fa860b85f46ed76f8b657f9ff002be42af6fe261875263e99ff00a7433028c55810c3a83d703b1042f51fbb2aea2488f543d308953564c3198dd7595c5ee9acd369cc66b63fdf5949bededfcd99109fc9d565c12c7fd282bdc2da5d402f74d1c637349ed8f40e3f637de1947ec72f81f2bcb8c558fa18ca5c7bff0017fbbffab8838e52a55d188a7d971b329fe5a7fc4933065020d869e4b2782399bd44220b9ea0aed1b9f11fefa6c9e3cb4aba1d56615b5bea903622415a7cebff00135ccce32473e27271e489db20ff003d07342518c75f8a3f8e27f14ea3fe032bbeae3e48184abf9a888ee6395794abf6b6929d2b913b396353c71a90e252963f4f63f1c4ff0065bc7fe6975c9100b8b28bffd40812a4b0f0cd0bbeb7475435fbf1b6242af2790546c0624a1671f88127e78a57b052411f4e05a580256846d854ec898d44628a7639128a5c412a77c0c8298e40788c2a89b6ba9615f4987ab6b5afa4495e2dfefc8241f14327fa9f6ff6f089743ea8b465c319ff005959a0826432c72fc2bbb48568cbff00317127fd4443fbbff230d0fc7fbf71c4e78f69faa3fce42dc432c32059578f2155606aac3f991c7c2eb80821cd84c48582a6df00a9df22cd5fea676292452f21f655a84d7c3d4081bfd8e0d9a0ea200d1f44913eba4e16cf53aa32ed05d11f1c7fe4c95fef20ff0088659c40ec7fd3b4e6d3898e28ad951ade6483528c371a7a33835574ff005d7edc792ebeafa9c4c79a58f63c9b9feadb7089a353fcafcd7fd8b1c84aaf61c2ec2132458319a924481aaae41ea36df2076478dde1502a2cdebc6c229987194f44957f9264ff008dff00632e865ef706708ddc542ee231b9914551b7a75fa0d3f687f3ff00cdf95ce1c27fa0d247543861c4329e71b751efff001abe63cf1b4ac9911e3024f8e21b2483eda1f0ff009b3ec608643134843012c34527905358241f64d3768ffc8ff8c6d9940890505dea0824e6bbc128f881fe5ff9aa26c157b1e6d987208cbd5fddcbeb44340550056f85baa9dc7b303801a2e7e5d30fe12fffd50d5001a743db342ef69a0cbdf152a8c78ecbd0e05a595a57977c36b4a8a5140a8eb815d2283b0efd0e1085e14851ef91290a946e20740705ab6ca540e5dfa789c6d0b24627a7d03084af85e6f56336fcbeb00d1387daf9648593b7363202b7fa5905be927d102f5e3855cd64b351cd093feec400afd566fe6f45fd3ccd8e9cd7a9d592232bc7f4ac9bcb36352df5f658a9508554b01feb13ff001ae0fca447f13903552ee536f2e88958da5e7243bf0954713feb15ff009a3233d182363fe99075025b4e2829ccb68de8ca14a7530c80c909ed546fef22ff005a3cc430943628184fd588ff00988bb39b4d9e1fa8dc131dbbee90cadc8237f3da5d0ff935265b1e1229a32cb8b698e19a0af2dee74a3e8cdfbdb36358671eff00e57ecb7f938d91b168b315915ddaa91f588cb03f6258cd2a7c194f46c946302cbc592bbb696fbfef9478a80c313871f7ca29f14ac6974a589e35bb652455048845186e3e2a7c3be583146b6971329e7b1cbd494b728dbd48e943f693f64d7e5fb2d984451a2e31d8aa5548f5613543f694ef4ff21c6572c6aa7b825e1ff9e9136e36dfa7ed2640131e68e5c94d635943245fb5f12c67aab01b85fe647cb8cbaa6af92274c92dae2036970dc268f7824f15fe53ff0018cffc266408c6437fa9c8c3964457f31fffd60b545f7cd0bbe6f92d00a6f8a15938924376c696d632823c2b812e750540ee31b452e04d00037c55548631efd7236a8bb3b582e61558ee912ffb5b5c0f4d1b7f87d39f7566ff008adf2c1107facd53998f31e843de5b5e5acc52ee378a53d9fbff00aadf65ff00d8640d8e6ce244b929a866a2a02cec40551b9a9d8623734c8edbb21b26d234388cba84aa978c092dd4815a704033698a0203fa4eb32ce533fd06e6d5347bf08b14cb2bb82ca54ee29fe4f5eff672464180895386e2d63532dd4bf0060bb9d89ad057e9c89932a513e6ed0526102cc0106945af8f881c708920c1636aba7de44e91832da9a7223ed01d7e1af86466624516511289b0a7358d8c72a4105d29f5d794714c37341bf2236ff85cc0cb8a858fa5bfc6121538f12f49352b1528d1996d4ecd19fde4647fc371ff006595c32ff9cd12c313f41ff9273692df46bc61e9ff00a248fd6ddf785e9fcbfcbc4ff2365c29c5a1745053e93aad88f5550bc63af13ce9feac83ed2ffaea993b3d53e1dee1626a8b22f19a14969b1a8a38fa30188236626479174eb03a2c96aa1428a3c752411fea9f886552df63e94101046b1b7242429fbffeba5ff87cae248d8b5f26db90284fc2edf146ebf65bde33ff00124c33875fe14ac601d832fc13762360c7fc9fe47ca784c7faab5dca2c3d46a9fddcea7ed7d904ff0095fc8f96c64c6248363ea7ffd702010a4781cd0bbe2aad50cbe0462ae00825ba8c50a869251576c0ab8a18f6ebe38a2da2c7a8c3c95512467254e0a5269cf1ad0ab50d7052414541abdf5bc5f5770b75667adadc0f513fd81fb717fb06c9899e5f545aa5841dc7a25fd045595a58deceb269ed25a5c0209b69419631bffbaae168dfec66cbb144195869c9290152f525fe72d32f9aeae2e0306b564555151c871fb54434e59942745c6ab083f26686ed79fa4250a96a3951829452484f8550f1f0cb25bb006931f32da7d7ac8416043b54c862ad095535ff0086a6572e6d913b305b1d32e24bb5840093ab135229d773ea13fc9fcb9394c00c44492cdb43f2fde5aa4ab3383f17a8889d3e9f9e631f516db005241325cc9e645692500c3298d61aee1013dbfc94cb0d089620d1b651087137182e843febd4007c0ae609d3f177333a8c721ea08c90c6d03c777241203bfc278b123ba9fe7ff6589d3ca236947fa991c5c86079712db69ae2dc2b595c892371c96098f503ed717ff9ad72b19a50fa870ffb86b5b72ba5df9e17501b4bbec57e127dd7f61f326330774100a5371a65edb31684fd6235deabb4807894ff009a72cab60410865911d4d09dbed0ee0fcb31e714370dc243586e97d4b394d5d7f95bfdfd19edfe5638e75b1403dead79a74d07c4a7d6808a86142c01dc72a7fc4b2793098ee19a0db8c838b9a301412753feabff00366398f722adffd00d2d36e3d0e689de076cca2bb11d30320d2721f0f518a0ab45c1589afc43016245ac927909269503a61480d45272eb80864ad4e8c0e047373293f1d715545af71b6284dfcb2d12ea20331577042ad2a0fbff0093993808b71b3dd2657da7c3a94725adc36dcf98a1a3000f6cb41b71ea92cd72e1f4eb382dace25658471e35e345e24afecb7edf1cb8cc0e6c040963fa7ead772dfb492c6429f850b0a1a9f901f0ff0091903901eee267e191ee4dff00445b5d4e6ee58c99d483507e1603701957af1cae520c85a631ce52e156955028a06e36f1db2027ba98ec80974259fcc4d3229e062f53980bb48dfbba71629cbf99a35cb6721cbf9cd12e484bdb69ac6610dfc4dc4ff752a9a1a7fc54e7edf1ff007d49ff000998d28907671eeb98722dc84e70482e212695e8c3fc961fcd83c6e85b4c011712a724a374914c66b5008a7c43f695c6caf920411b3513dee5be9d5383aadc43fb514837dbf94f5cc6963e0de3c91c48ab6be8a4a476f27a6e7ecdbce4d2bff14cff00b3fecb2c865ef4892cb9fabc92d2e6229703a9145940ff0093770999077e682025b245c6aa4f28ebf0c9daa7f9bf9397f9594485ee3ea604376f77240be99660a87e0a6f4f18c8fe5c23248726719ed4554fd5af41920fddce057881f03fb7f92f93a07fa326443fffd10a010941db342ef5cea4853d31a5b71a8db0daaa2c741cbbd322b6b43028477c36a172321a2d36ef4eb85051df51b79ea6ca67603fdd72282df743fbcff9218816e39ce63f547fce82d9ecaee1425a23c17ac8bf1a8ff5997ec7fcf4e180c08e6d91cb19722a119a8d8d4642edb2a91567772d8ceb3a286e3bd0fdd96427c2c251e2d994c37515dc097708a73ea3a508ea32fbeae298d6c82beb2b49e40ccb566501e8694c84a56a364b868b60438512a30aad5cec057aa7fad912cf88abdad9db5af268dbd491fed3b126b82c3177d60ab0523f7a3651e23da990e3ae49a51b86774f52260b206a518d0353b57a65b3c6271dc35dc41f52d1a8defa5e8de42b756f5af0701c0f97fcdbc331c4270fa4fa7f9b93f7b044b16390f49ddb4d32ce443796123da114122d7d44527f62689ff78abfecb8659296d731c23fd5717ef21ff5520e11810578b67a8170a9c0f565a143feaf2f897fd46c71e312dc1e28ff003db04c72285d434f88716b220cbdd41a0603b7f92ffc9974aa2838ac584a5944ab55146eebd0d47fc45d731e78eb71f4b8c42261d424745b7b98d6e13f6437c249ff008ae41fdd4dff002730e3c9decc4ef62b8c42405ed59a451f6e1701674f665fb32a7f94b992636b4839103fc4bd40a3507fc497fe69ca0c29890a40cd0b196da429211f115fb2e3e5f6792fed65b8e49ba7ffd204ac6847f29cd1077a5b94d5415342305a405e1890396d8a17972011d8e05a5a1558541fa3254ad8dba0c89415cc5168c5803db7a1c8dee94d74dd535c461f5612dd20d943c6d2fdd2ff0078bff2332c139071f262c679fa53a4b68b50ff008ea68df5673d6e52448cfccf17497fe0e39309cb0fe2718c8c3e99712125f2bc124852c3525e5de19c02c3fe7a43ff0054b1108c85c4b6475247d4133d2b47bbd3a2786722449086468eaca08fa36cb217ca9ae59448da85d958d8d68a01a9f9e448a66929bdd8b156690d4d0d2a773f645720432541729c0349fbbaee01d8d7e5f6723c29507b98e58d8223b236c5c50135ff00294f2c44775533084b329407d261252a402a478d3e16cca22e2e3643d7ea5042a4d2191d1fb46db9fa01fb5fec731ce49479b4544f24442fa8c3209a123d451427a120f54746f85d0ff2e4e19c141890aebeb486ba7ffa35dd096d365358a403ed1b476fb3ff0018321e142478a1e89ffb5fe3d6c774326a7652394bb89ace753466009507fcb5fb4b8d9e531c7fd3ff008e289d7f456ded8b301710953cbfdd886b1bfcc8fb2f96406de9f5458cb74ba58dbe2122904f5ee0fd23f6b2ac98ba86b21bb79564915269bd293a45707a57f96561f67fe327fc1e4f0cfa151246cd14a056f63229d2ee2dc1a7f3d3fe36cc8316c41cf03a7ef54d56b559d3707baf35ca8c5487ffd3020717627eecd13be5f5464a537c6a94398d63a771815b6ad14d36c2aed81d8d305aaa2b71ebbe2c4a261d5351b788456f3fa510aeca881bfe0f87a9ff000d878880c258a24d90e9351bd97fbcbb9893d6b2301f703903ba46388e8829248e4243c9c8ff0094d5ff00891c9726c0172b2250ab0520ec41a11f7645245b28f2a6a93cd742d1a57998ef52410a83aeff006be2ff002b2fc26465e4e06a7081b81c288f340f4ede62a7e1edbd376d96b97ca36d512c611ee53e2455151bc8db9f6e208ca886db55304b39ac95770a3e26dc1ff558705ff63913b2844c10848c03d3b904d1491b2af7c54ad33b4370afc8bc75a156028694f85d5fe7fecffd865d16b22d52e2c6c6ee431c0cb69766856ddc9f464aeffb990fc513ff00919135745c79e03563e94ba57bdb398c3731b161feeb9366a7f34727d9917289e00d70c862771c4bd6586e8704dc8df86eb2291fb4bff35a65423c27673632c531e6acef0df7eeb512639d4521d4694e9d12f147ecff00c5d97c6427b7f13873877a008d4348b82a3e027765fb51c8a7fe15d5b2128906c7a64d2418a6114da66a29557faa5d81f1c6d531fe1f122ffc27fc57968c80f3fa990a285bcd2dd7775a06140e08e0dee1d7f76fff0012c89c37c906086b39a5b2b9157709d3634f9738cecebfe52e4019439223b1ddcf70219de480810bff0079074e04f8237fbadbf97270993cd9d81c9fffd42f243c25abbf439a177ab829f41586e461485e854aef8a96c9d80ed809400e27db14a2b4e843191de286e00d96296678587f94be97db5dff0069b2424073f534659575e0ff00378d1aab3807d2d1ad8a8eebea5c9fc266ff0088e489be51835c4c4ff94ff78856d4e58e4f4c5bdac0de1f5740dff25393656dc3103d67fe99b3abea808e33845ff223897f54782ad7c18af6d5b56a81f5b6fb97fe69c8d2f830ee4ebcaf7d773df95b89649288c4542f0ff8255079664e9e2372e3e7801c95b5cb849a43105a981bd4753d0d415562dfe4e59293500c7c5c962591f88e34f8791dc6d56e0a51707364a678c8dfbefeed8d43f226847ec06e2dff0012c14955706d544d0b978c925c354d36daaabcbfd5c1ee48423cbf59491e50fba5551c31534db97f332f25c98084d34bbeb960f048cbea6d21f517929af872183266e08f11f545a32c48dc232e99ee53d39922962eca095a1fe64e43f76dfea36637e7b14baf0b8dba4b75a44cbf1c2a5941aaff003afc9d32d8ca33fa489b03143adf5c43f05d2974ff007e01f18ff5bf664c1c1bb2190a211bd5b5290d2eecd7708b5e7113d7d30df614ff00be1fe1ff007de58058fe72d8297cd6ac8ab7103168eb45916a0ab0fd86fda4917fdf6fff000f954b1b5ca3d42adb6a972adb4ab1cadf69641ca097da58ff00dd6fff00162e08e431e698e4ef45f3d3ef58c6ca6c2f36e56f28e51313dd3f69797ecf0ff80cc8b0596c50d7367716e47a8bf0f442dfbc8c8f04947c4bfeae44c1487fffd5049120520fed74cd0bbe5aa4a5623d3b61b43668003f7e04af60081c4d710abb7e3d3150d275a9edd312aaeb7130e87901b8e5bff6e104b54f0c25cc23e1d7a6f4fd19e332a56a43307afb70b959d3fe078658329ead074a07d265055171e5aba3fbfb5fab37668c345bff00cf26960ff828f0f140f4470e68f5f11b6d2f48937b6d45d07fc5c8241ff23202b4ff0064991e089e457f3528fd514c7cbda34b6f7ff595bc8648f895e309e5c81fe6e4070cb7140c4f3639351198a0a7a8cdf5ad46f618d7f76a8ab24d4d9096f857a7c5cf8fece277b3fc2c46c93c5c176820370370f248dc54d3ad07c47f9b0852e8a4819f82aba4ce3ecb100151fb5cbfbb7e0d894b83f0726828013c76049a1fdbfd91fb1f6300494290cc232240a2de527e2d9b8efc457e8fdbc3cb74aadbdc309a458fe37e4514bec033372f87bffb1c4ee2984a361486a6dea324ea55949078310cbec47d9ff85cd64b0d1e5193863311b111c9f8fe7a222b89dc836d71cffc96d9bee6fb5fec1b2bf0b1df2e0937c4e19f596292a3dddc0a8b8b65b8a75e20ac9ff0b9911e38f29717fc358e4c15b8f5a943169f704dd59c8f6b327da720803da49a1dd7fe7bc7993191fe21c1fee1c7e1ee54985cdbf2b8951984aa14cc9c6486503ecace07c127f92cafeb2659115d78a2991f24b2ea1b6928f0b712ff006a16af253e15217d54fe493edff3e42510d5200a1a42c144372090a3e063bba8f6fe78bfc9ca85c4d860af05fdddafc25fd5b77eb5f8811fecbedffb2f8d332232b161b04cbfffd6031d47153bd33436ef4ab48a8407ee303105498549a8a57b61661c15147204fb8c2bcdc26ea302697257ae142f0180241db02aa4450835eb80a14a5923435e4057c4d30a5a42d291e8ab48dff1582dff0010ae1e1250481cd9b7972c961b56ba625e5084739119245e5d636f538f2f8b2ec479ecebf201c5c90b7104834ab8a49c5a79eaee4feca9e2c397d0dc72c3c9039a5770f6f6f32c0b2fa60806320f214ebe0fd7ece0b4d28349c88e6cab226ed22d0fc15e343c3e7fb591b64021d8842792855ea25534e4acc7c7ecbc7f1e1e4c963b80ccb20f52265ddc6c58f2529cff006793237c2d8aacb84b78e14735e71060f1b1edcb9090ff00b1fdbc9310534bab817400bdb45bc8828e2e0fa772a3fe2b9d7e197fd4972894c5d49c79e2dad2e3a4accc5b4b9fd7602ad6930f4ae947fa9f626ff5a3c89c40f2714c1463d56f6d5cc5282c17668a50430f60df6972a11ae4c7888d9596fb499dc49ea4b61783a4a3fe6baf075ff8c99903250e5fe919095f3565ba961667594daca4ef796eb5b792bff2d76a7947f17f3c7861901e5cd9c85352ded993e9ea76a2263bade597c51303fb4d6e797fc93cb2c16b3b736ce94268ccb6732dcc1d8c441dfc1a193a7faabf1e44c014f08297496f244e50a509fb711ac6d5f1e126dcbfd5c8f8647263c2ff00ffd70714cb4e2c287c7342ef08688624a83b1dc6492a6cee0056fbf032a54550072eddf155ace2b403154c2cec6d668565b9d52dad14ff00badb9c928ff5a28d47fc4f2c1189e72e16896420d08ce48af43cb10afc77d7778dfcb6f0ac2bff000571cb0feec759cffd830e2ca7946305a350d161ff0079b480fe0f773bcbf7c49c23c1c71e911fe7fad3e1e43ce5fe922dfe9ebd8ffde5b7b3b5f030db203ff0527a988cd21cb861fd48a9d38ea672ff00396bf98fcc52502ea128af5081100ff8045c7c599ea57f2f8c74ff0074ccb49323e8debcd2196491896918d6bc7c5bf6f7cb79071081c5b297a024d3a2a9aaf00f53debf157091b24318785e38dc321579233520026b1b5517e85f838e42e992044a5a354a162edc5bf9881b3a9a53972e5819ad242fa4cc2aacb5414242853c69fecbf6b092a16f051007501594717402a79c44d263fceaf1affc3e1082ab3442432c4c540b9d8b9da8a9f60ff373fb6af930d76eb8b896194c687e0005032123fe0d331b2920b44f9a8cf7e974162b88a3609ba386e2411fcac7ecff00c1657c67faac7c4dfd438d7fd7d5d047789f5d8945019852651e0b7517267ff9ed149931901e6d72ae9cbfa6a0fa76997009b3bd485bfe59ef3f767e4b2ff76fff00099214587084235bea365554e4233d42912447fe00b2e020140b8f25d0df5bec92afa0e4fc45be28181fe651f1c792163fa49e25ef6bc24f5ec2436f29e9c5eaa7d9255f8245ff0021f2519772f0f72a2f982e633f56d5610ea362c52b4ff5a13c78ff00ad6ef0ff00a99602912e85ffd02c247d07639a2a77d6e4e492712d51d8fb62abb93d781dc7638a5c9271aa3fd18a80b969d69be2abc10e2b4c690d9507be34aea6d4270a1b4a0ebbe04aa5b5b4973770c10c6667958288ebc6b5fe66fd95c9c059a613342de9d716cb69a6c76ab4e30a70a0141b0ecbfcb99193675b0366d0d391241e9aedcb8afd046259247ae46b180a09640383822a0d7f676ff2b22424243c07a3ea722acc415a6c43fed1f8bf994640ecd8163b28919dea579536eabca9c9957f6b953960b674a719632712956e202b13b80c6b1cbb7f2fc2b264c302e2243095e61a58824709068c4305a725ff002fece4ac302a92dca2c6ec8eccb03d24ea5806038bfc3feea6c8ceda328dad456ea09d6a1d2407af200ffcdd949e1ee71812d1b681ba253de36ff8d5b0080292a66cea3690d3c1d7fa63e1ad297d5264de350478a1a7f4c1e19470ac904c3e1903713fefc50dff001218f090b4a2aa884940aa18fc416aa0fcd3e28ffe170d9452a492a4b184951881b0e8f4ff0057ecb532624921ffd12c4ad37fa7e59a277cb97d3120ee298a151c4551c4efe18a45a97c153cfefc29568e95e9518b154a8e1b0f91c5429537eb4c5936a050d4e255cb5af88c21059179212cceb8b24f2309954fd5a1a3519c83bf25f87e04e5fdee5f82affa4e26aaf87c9975d3dcfaa03c7fbaab72351d3efc079b44690f63ea7a0ad37fbd3c6813fca25a8dfc9f0e4c72f34148b5712f3201d81f88d06ec38f1fb5d392ff00c3fa780b20912024c8010b1023d46dc866057ec86fb35ca8b7053937993d51420b98c2f526bf12ff00c4b10bd165bf2e4fc79730582914a94a0f8bfe0f2410548888151390ceb10a13fb5f1c9e8f3e8abb1c9b145693eba487d1e323721eaf0f85b9fc43dfe1e3f137fbaf24d53e4b7547d3da42b7090a4d5dda124c9fecf81ff89ae532a71881d4c50eb670984c916a0aa46e90b472927dbd454555c8d0ea58103a158ad7a281958f8107e1ff00929c700f26255d1af00354aa7ed5294fa78b53137fd1670e2fe92f47988358dc0f10415ff86e39304adf7ac9bd11f6f81f10680ffc2e1d9050a7ea27c54ff93538fa50ff00ffd9", + expectedOutput: "Found 0 tags.\n", + recipeConfig: [ + { + op: "From Hex", + args: ["None"] + }, + { + op: "Remove EXIF", + args: [], + }, + { + op: "Extract EXIF", + args: [], + }, + ], + }, + { + name: "Extract EXIF: avatar jpeg (has no EXIF)", + input: "ffd8ffdb008400080606070605080707070909080a0c140d0c0b0b0c1912130f141d1a1f1e1d1a1c1c20242e2720222c231c1c2837292c30313434341f27393d38323c2e333432010909090c0b0c180d0d1832211c213232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232ffc00011080058005803012200021101031101ffc401a20000010501010101010100000000000000000102030405060708090a0b100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9fa0100030101010101010101010000000000000102030405060708090a0b1100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffda000c03010002110311003f00f39d2237f3223f398e3765047cb8c95ee0f1d6b73c66037852c2786479049772073d7e652ca09ef9da1067bedef597e15b382ffcd6b89248e08d1a57208c95040c73c67381fad6f6a176ff00f08d491adbdb5bc12cd3b24580f85405b71cf392463230319ae09de334cf16ab71aa9f6397b2bb824d34690d641e6ba789d67de4ec55dc08db8e739ffc77a77ad6f14c7e4e8f3a12c156ed157e5c06468f7af18e0ae48fc4d72d2cc069ab2c4e1255941054004647b723fce2afbea4b35bc534c91ce63456944a5fe7cf1cfcdea2b4941b69f6379d36e4a48c270c08638e4e79c1a78cb49c138dfcae00ae96d239b5dba8ec2db4eb37915c6c2a879382704e7a7afa558d6f4fb7d1960b696dedbce89dbed3322ee5dc73b541ce7181d7dcfb55f3f4ea5fb7d795ad4e6e20de747f7940751b94e0823dfd7393f8557946f676ddb882492ddfd4d74497da4b3208e21bd54b380a4ab10383c9fef13dbd2b2afedada1b62f1caccce0328ddca8c904118f507d288b77d8709b6f556330f2d96ce3a015bda8d9a4766080b1b9c30dc39202038f6ebfad610daad8dd93d2ba0d5ee20480c250b398c6370230c55727f43c7b83454bdd58755be68a464a320310192cbcb7bf3c7d2ad79c3fe790ffbec5500c1ee03e554f650381cf4ab7e637aaffdf34e511ca26ef871e38f4c2b72ec90dcc7246c40e87820f1fed0157ee58c1e1f8e1701fcfb7bb73701b2782c17278e4e31f8d67787525bab3b6b5891371903296c839049233d8607a7f2a97c617d6443450cafbc41e4f979cedc480e4e3f88e1f3e84d6325cd3b1c938f355e5f3ff339b1103a0ceed8c89230b93cf20f6ff3d2b4edec74a78205b9bc811c4232584ae33d7b0c743eb546403fe11e90855199a2048ebf75bfcfe745a45e749045bbefe130173d87f3ade5b5cea95da7adb5fd0f45d3751d3f4ad16eff00b25650af84fb43a005c842c4a8c7ca33dbdfd79a5b1d60ae9aaef70ce59fe645c640c77eb8f5ed54b50b29345f0d258c36acf700abf9dbb2a176307e339c92c3f2ac981e5b8d26178623b3cdd8caa48f9b1d40fcff004af1e718d6bcaf7573c274a15ef34ee9b23d62cac6fa39aeac24115c451ab02bf2994606e040ee3afe159fae5d68973046d60eff00687b687cc528c01942fef0e48ee467d39ae834ad2ede7d3cdc4c678eecec4822519121dca1b771c0c67b8fc78ae28c6ff69103e4183cc420f452303f9d77e167bc2f7b1e9611ef0bdec5265c3b1381c9ad2d5a4f308da98071839cf1b4553995448e064904d68eaa85d62906d0b21200ce4f0060febfa57549ea8ed93f7a2674118f35493d4f738ef5a1e5c7ea2a8daa05ba0092464638f715b388ff00baff0097ff005aa66f514e5a973c2d7690de69ee085114d997cb3c84dc324e781c1c0fcbbd67f8b12d12ee21668c525432f98c319dc49c019ec49ebcd47a6ca122910a9632945dc075f99491fa51ae4cb75696770a71b435bb26dc7cc9b4023eaa53f106a52b4ccd46d56e432b29f0dedd8438953e6f6c356cf83a289b534bc755cc4b84c91f78a63fa1ac09a50da394c6312263fef8357bc3b7820bd87cc70a98663ce390b91fd696222dd29242c44252a334ba9deea69fdadaed858c7ff1eed3624562402157241f6e2ae8f045b2ea4f7104b2a83f398a33f2ff00fab9e94786d22bbf10ea17330c24312c28ad93b778dcdd47a103f3adbbb92ce0b416af6eec8b242d12a9daa02306c75f415f3739384a34a32b7f57ff0023e5a729539468c25cbdfe7aff0091c668ccb040efd337b2246a4fdd507a7f21f8572dacc022d55e751b7ed11173df2df2e7fc7f1aded5266b5d64c31102d64b93329c1ea71b81ff003deb06eaf61943b48c15a3b726204fdf62ca08c7d39fcabd3c2425ed1545d51ebe0e12f69ed57da4624aaf966e0f3cfeb52dd2954419184381f2f3d05432c81d9f039dd525e3670dcfcc7238ed815eb3bdd1ec6ba1108df7a32fca4fcd9f6ab3be7ff9eaf4e28c90db3908721bd39edf851bff00d95ffbed6a1b6c4e4c8d09f29b24001b8c724d47733a3e9f1c25b748269246c0e06768fcfe53fa540642c140e0f5a85f00c9c9232467f1ab51d4d1435279189d373dbcdc0ffbe6b5bc3da59d46e115c95b645dccfbb1db381ee6b24465f4c8c2ae59a7238ea785c0fd6bd074cb78b4ed36ce348b6489bdda403efb6d1819faf4ed58e2eafb38596ece6c6d6f654da8eecdfb4bcd1747b4bab97b88fcc91cccf1abee21b0011d4ff7455af0f78874bf107da229191368f944bfc5e983eb9ae07c4897579abb2b4437f92dd0e78cb1e4f6ea063f0ad34b77b6d2e196097c989275dcc0fcb8cf07df91d3fc6bc6960a125cf277933c196029b5ed24ef264fabe971b6a0f34d231637281610782bb179cfae4f35c36b9118278d55711053b4eece7393f875aeb2d01934fb58be4dd34b82fdf3c6093ebcd50f12e9d35a68c84aab2994124804ed2a0a9cf6aedc34dc26a127e477e12aba7523093f238f0c431ee2ac5c1dc839c76e9ec2aae3939e2accdf740230700f1f415eabdd1ed496a89d9898d12304b1183c74e9f9537ecf71fdc6ff00bee9f02949c646723af07d2aef3ea3fef81fe358b958c5cb976326509bd4c671c66a13cc9202d9249008e879a9a28fcc915482d8c74f4f4fd29b2c47cc28000ccc7a718e6b54eda1b276d0d9d374c59921921bdda613e7b0d9feace40e4fe031f515d6a5fdc5c4762218484077361b710c1704e3fc79fc2b8101ed8c76e18b1620b2a92703d0d7576d7d7b234652297ca8711ab20c053c33738ea1413ea33cd7257829ee79f8aa7cf66f52cebe5a59b0a479e20e7202600c7279e7e503afb7b55b8b4d17761299278d48995941e8c4f619e9c8ed9eb5951debb089dd415bd690c8ef92480aa38c7beeff0022b46db599ed34d82285329f69655ce0b1c8c8cfbe4e7a0e9dfb72ba524924724a9ce315189b6be1f96c7c356b7df6a19494c8230be8aa7f9f1f8573b7c50e9f3aa157064390e3214f039ad0bbd4ef648058cb18110060f32318071cf5efc05ebea69935846da7cb6f05d7cdbbcc65765f987ca4120fa8dbd339352e9fbe9a308c1a92933ce6f23733190a0505413b5703a75a7dc6d65520a9278f93d38c1ae8d3435d56e21820956dd6e6421372ee0ad8395c819eddbd6b2752d352c228e179775cabb2b2e38e081c1eff00e7d89f4a3513b23db8568c9a8f51212f13a606ed8a7273db231f5ea2adfdaffd95fcab3e26de09dc49200faf200a93ca7f43fe7f1a9924dea4ca2ba9bda07832e6f744fedb9e4f2ac62c8760a4b37254e077e78fc863ad564d08ac575a82c53f90a5b0cc46460f53ff00d6f41eb5e91e1eff009230bff5d07fe94d611ff9136f3fedb7f4acab4dc76ea7155c4ce327eb6fc4c4b1d19934d6d42e1dff0079033e32b9298c76c93ce39c63247d6a16d1ee12de44123652356711b10199c16230072428231ffeaaddff0099661ffb0537fe8694f8fef5cffd744ffd024acf99dd99fb69ddbfeb4665da5919e7b7d309002c6b3b4c80b1562776d23db38ff0a8a08becd10bcf3e5024b860d2956dc0a052ac08e33b891dfaf6ad2d0ffe461baffaf71ffb2d55b9ff009142dffebea6ff00da7426f99af41a9be7b7a7e25f8352fece85a2f3ae64dea920287a000aed6f4ce0023dbad56b6be83c889bcd966b52de5323aab051d53195279c9c71fe150c9d67ff00ae6bff00a35ea869bff2094ffaf8b7ff00d0452b68d87b34937e65775bb8c491492b8b4998491859182ab60a838fa8c671e9cd634d1bca249b73b4a8db999c92d9cf27fc6ba3bfff008f3b3ffae51ffe8c35889d6fbfdd6ffd0ab7a6fa9d9464dab95ac7f7bbe4567f300195c71d6ae66e7fb8ff0098ff001aaba37df9ff000ad7aaa9a4ac6951da563fffd9", + expectedOutput: "Found 0 tags.\n", + recipeConfig: [ + { + op: "From Hex", + args: ["None"] + }, + { + op: "Remove EXIF", + args: [], + }, + { + op: "Extract EXIF", + args: [], + }, + ], + }, ]); From 6cf64d794ff57724cc0839d192f783f611b1cc91 Mon Sep 17 00:00:00 2001 From: David Moodie Date: Sun, 4 Jun 2017 17:23:53 +0100 Subject: [PATCH 02/22] Change == to === --- src/core/operations/Image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/Image.js b/src/core/operations/Image.js index 7a0385ba..6ddfad9d 100644 --- a/src/core/operations/Image.js +++ b/src/core/operations/Image.js @@ -65,7 +65,7 @@ const Image = { return newImage; } catch (err) { // Simply return input if no EXIF data is found - if (err == "Exif not found.") return input; + if (err === "Exif not found.") return input; throw "Could not remove EXIF data from image: " + err; } }, From 3bfe22c0f70fb0fb9b3d46893eea7fa31f04a2fd Mon Sep 17 00:00:00 2001 From: David Moodie Date: Wed, 7 Jun 2017 19:49:44 +0100 Subject: [PATCH 03/22] Remove piexifjs dep and extract removeEXIF to lib module --- package.json | 1 - src/core/lib/remove-exif.js | 163 +++++++++++++++++++++++++++++++++++ src/core/operations/Image.js | 10 +-- 3 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 src/core/lib/remove-exif.js diff --git a/package.json b/package.json index 01a55b8a..4ad37565 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "lodash": "^4.17.4", "moment": "^2.17.1", "moment-timezone": "^0.5.11", - "piexifjs": "^1.0.3", "sladex-blowfish": "^0.8.1", "sortablejs": "^1.5.1", "split.js": "^1.2.0", diff --git a/src/core/lib/remove-exif.js b/src/core/lib/remove-exif.js new file mode 100644 index 00000000..1e158221 --- /dev/null +++ b/src/core/lib/remove-exif.js @@ -0,0 +1,163 @@ +/* piexifjs +The MIT License (MIT) +Copyright (c) 2014, 2015 hMatoba(https://github.com/hMatoba) +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. +*/ + +function bin2String(array) { + return String.fromCharCode.apply(String, array); +} + +function string2Bin(str) { + var result = []; + for (var i = 0; i < str.length; i++) { + result.push(str.charCodeAt(i)); + } + return result; +} + +// Param jpeg should be a binaryArray +function removeEXIF(jpeg) { + // Convert binaryArray to char string + jpeg = bin2String(jpeg); + if (jpeg.slice(0, 2) != "\xff\xd8") { + throw ("Given data is not jpeg."); + } + + var segments = splitIntoSegments(jpeg); + if (segments[1].slice(0, 2) == "\xff\xe1" && + segments[1].slice(4, 10) == "Exif\x00\x00") { + segments = [segments[0]].concat(segments.slice(2)); + } else if (segments[2].slice(0, 2) == "\xff\xe1" && + segments[2].slice(4, 10) == "Exif\x00\x00") { + segments = segments.slice(0, 2).concat(segments.slice(3)); + } else { + throw ("Exif not found."); + } + + var new_data = segments.join(""); + + // Convert back to binaryArray + new_data = string2Bin(new_data); + + return new_data; +}; + +function splitIntoSegments(data) { + if (data.slice(0, 2) != "\xff\xd8") { + throw ("Given data isn't JPEG."); + } + + var head = 2; + var segments = ["\xff\xd8"]; + while (true) { + if (data.slice(head, head + 2) == "\xff\xda") { + segments.push(data.slice(head)); + break; + } else { + var length = unpack(">H", data.slice(head + 2, head + 4))[0]; + var endPoint = head + length + 2; + segments.push(data.slice(head, endPoint)); + head = endPoint; + } + + if (head >= data.length) { + throw ("Wrong JPEG data."); + } + } + return segments; +} + +function unpack(mark, str) { + if (typeof(str) != "string") { + throw ("'unpack' error. Got invalid type argument."); + } + var l = 0; + for (var markPointer = 1; markPointer < mark.length; markPointer++) { + if (mark[markPointer].toLowerCase() == "b") { + l += 1; + } else if (mark[markPointer].toLowerCase() == "h") { + l += 2; + } else if (mark[markPointer].toLowerCase() == "l") { + l += 4; + } else { + throw ("'unpack' error. Got invalid mark."); + } + } + + if (l != str.length) { + throw ("'unpack' error. Mismatch between symbol and string length. " + l + ":" + str.length); + } + + var littleEndian; + if (mark[0] == "<") { + littleEndian = true; + } else if (mark[0] == ">") { + littleEndian = false; + } else { + throw ("'unpack' error."); + } + var unpacked = []; + var strPointer = 0; + var p = 1; + var val = null; + var c = null; + var length = null; + var sliced = ""; + + while (c = mark[p]) { + if (c.toLowerCase() == "b") { + length = 1; + sliced = str.slice(strPointer, strPointer + length); + val = sliced.charCodeAt(0); + if ((c == "b") && (val >= 0x80)) { + val -= 0x100; + } + } else if (c == "H") { + length = 2; + sliced = str.slice(strPointer, strPointer + length); + if (littleEndian) { + sliced = sliced.split("").reverse().join(""); + } + val = sliced.charCodeAt(0) * 0x100 + + sliced.charCodeAt(1); + } else if (c.toLowerCase() == "l") { + length = 4; + sliced = str.slice(strPointer, strPointer + length); + if (littleEndian) { + sliced = sliced.split("").reverse().join(""); + } + val = sliced.charCodeAt(0) * 0x1000000 + + sliced.charCodeAt(1) * 0x10000 + + sliced.charCodeAt(2) * 0x100 + + sliced.charCodeAt(3); + if ((c == "l") && (val >= 0x80000000)) { + val -= 0x100000000; + } + } else { + throw ("'unpack' error. " + c); + } + + unpacked.push(val); + strPointer += length; + p += 1; + } + + return unpacked; +} + +export default removeEXIF; diff --git a/src/core/operations/Image.js b/src/core/operations/Image.js index 6ddfad9d..7bf375dc 100644 --- a/src/core/operations/Image.js +++ b/src/core/operations/Image.js @@ -1,5 +1,5 @@ import * as ExifParser from "exif-parser"; -import * as Piexifjs from "piexifjs"; +import removeEXIF from "../lib/remove-exif.js"; import Utils from "../Utils.js"; import FileType from "./FileType.js"; @@ -56,13 +56,7 @@ const Image = { */ removeEXIF(input, args) { try { - // Piexifjs seems to work best with base64 input - const base64 = "data:image/jpeg;base64," + Utils.toBase64(input); - const newImageB64 = Piexifjs.remove(base64); - - // Convert the base64 back to byteArray - const newImage = Utils.fromBase64(newImageB64.split(",")[1], null, "byteArray"); - return newImage; + return removeEXIF(input); } catch (err) { // Simply return input if no EXIF data is found if (err === "Exif not found.") return input; From 096891295461d6fd5e18800e702a94c1802b0a2c Mon Sep 17 00:00:00 2001 From: David Moodie Date: Wed, 7 Jun 2017 19:51:09 +0100 Subject: [PATCH 04/22] Only reference JPEGs in removeEXIF description to be more clear --- src/core/config/OperationConfig.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 80009b85..6041fc89 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3390,9 +3390,9 @@ const OperationConfig = { }, "Remove EXIF": { description: [ - "Removes EXIF data from an image.", + "Removes EXIF data from a JPEG image.", "

", - "EXIF data is metadata embedded in images (JPEG, JPG, TIFF) and audio files.", + "EXIF data is metadata embedded in JPEGs.", "

", "EXIF data from photos usually contains information about the image file itself as well as the device used to create it.", ].join("\n"), From cbcd45cd70ffa57e31e1f19416b6225bf86d8abc Mon Sep 17 00:00:00 2001 From: David Moodie Date: Wed, 7 Jun 2017 20:01:45 +0100 Subject: [PATCH 05/22] Do nothing if input is empty for removeEXIF operation --- src/core/operations/Image.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/operations/Image.js b/src/core/operations/Image.js index 7bf375dc..336e5931 100644 --- a/src/core/operations/Image.js +++ b/src/core/operations/Image.js @@ -55,6 +55,9 @@ const Image = { * @returns {string} */ removeEXIF(input, args) { + // Do nothing if input is empty + if (input.length === 0) return input; + try { return removeEXIF(input); } catch (err) { From e4a91b53970f1ca0292ae4a2446bd6d1a4772c9f Mon Sep 17 00:00:00 2001 From: David Moodie Date: Wed, 7 Jun 2017 21:41:02 +0100 Subject: [PATCH 06/22] Use functions from utils --- src/core/lib/remove-exif.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/core/lib/remove-exif.js b/src/core/lib/remove-exif.js index 1e158221..5dbb5bb0 100644 --- a/src/core/lib/remove-exif.js +++ b/src/core/lib/remove-exif.js @@ -18,22 +18,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -function bin2String(array) { - return String.fromCharCode.apply(String, array); -} - -function string2Bin(str) { - var result = []; - for (var i = 0; i < str.length; i++) { - result.push(str.charCodeAt(i)); - } - return result; -} +import Utils from "../Utils.js"; // Param jpeg should be a binaryArray function removeEXIF(jpeg) { // Convert binaryArray to char string - jpeg = bin2String(jpeg); + jpeg = Utils.byteArrayToChars(jpeg); if (jpeg.slice(0, 2) != "\xff\xd8") { throw ("Given data is not jpeg."); } @@ -52,7 +42,7 @@ function removeEXIF(jpeg) { var new_data = segments.join(""); // Convert back to binaryArray - new_data = string2Bin(new_data); + new_data = Utils.strToCharcode(new_data); return new_data; }; From a5f1c430a3ef510150d87409ebdf6ff12a732fe0 Mon Sep 17 00:00:00 2001 From: toby Date: Wed, 7 Jun 2017 22:42:37 -0400 Subject: [PATCH 07/22] Add "HTTP request" operation --- src/core/config/OperationConfig.js | 33 +++++++++++++++++ src/core/operations/HTTP.js | 57 ++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 5fd5a9ee..ddb67d28 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3388,6 +3388,39 @@ const OperationConfig = { } ] }, + "HTTP request": { + description: [ + "Makes a HTTP request and returns the response body.", + "

", + "This operation supports different HTTP verbs like GET, POST, PUT, etc.", + "

", + "You can add headers line by line in the format Key: Value", + "

", + "This operation will throw an error for any status code that is not 200, unless the 'Ignore status code' option is checked.", + ].join("\n"), + run: HTTP.runHTTPRequest, + inputType: "string", + outputType: "string", + args: [ + { + name: "Method", + type: "option", + value: HTTP.METHODS, + }, { + name: "URL", + type: "string", + value: "", + }, { + name: "Headers", + type: "text", + value: "", + }, { + name: "Ignore status code", + type: "boolean", + value: false, + }, + ] + }, }; export default OperationConfig; diff --git a/src/core/operations/HTTP.js b/src/core/operations/HTTP.js index 28c41ad5..e80c0fb3 100755 --- a/src/core/operations/HTTP.js +++ b/src/core/operations/HTTP.js @@ -11,6 +11,14 @@ import {UAS_parser as UAParser} from "../lib/uas_parser.js"; * @namespace */ const HTTP = { + /** + * @constant + * @default + */ + METHODS: [ + "GET", "POST", "HEAD", + "PUT", "PATCH", "DELETE", + ], /** * Strip HTTP headers operation. @@ -51,6 +59,55 @@ const HTTP = { "Device Type: " + ua.deviceType + "\n"; }, + + /** + * HTTP request operation. + * + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + runHTTPRequest(input, args) { + const method = args[0], + url = args[1], + headersText = args[2], + ignoreStatusCode = args[3]; + + if (url.length === 0) return ""; + + let headers = new Headers(); + headersText.split(/\r?\n/).forEach(line => { + line = line.trim(); + + if (line.length === 0) return; + + let split = line.split(":"); + if (split.length !== 2) throw `Could not parse header in line: ${line}`; + + headers.set(split[0].trim(), split[1].trim()); + }); + + let config = { + method, + headers, + mode: "cors", + cache: "no-cache", + }; + + if (method !== "GET" && method !== "HEAD") { + config.body = input; + } + + return fetch(url, config) + .then(r => { + if (ignoreStatusCode || r.status === 200) { + return r.text(); + } else { + throw `HTTP response code was ${r.status}.`; + } + }); + }, + }; export default HTTP; From 369d213da504d2628e38c06305a2aa20cc7d774e Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 8 Jun 2017 11:09:31 +0000 Subject: [PATCH 08/22] Tidying 'Remove EXIF' --- src/core/config/Categories.js | 3 ++- src/core/config/OperationConfig.js | 8 +++----- src/core/operations/Image.js | 6 ++++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index 2470cc0d..3bddf092 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -210,7 +210,6 @@ const Categories = [ "XPath expression", "CSS selector", "Extract EXIF", - "Remove EXIF", ] }, { @@ -289,6 +288,8 @@ const Categories = [ "Scan for Embedded Files", "Generate UUID", "Render Image", + "Remove EXIF", + "Extract EXIF", "Numberwang", ] }, diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 6041fc89..c7101bb4 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3370,7 +3370,7 @@ const OperationConfig = { "

", "EXIF data from photos usually contains information about the image file itself as well as the device used to create it.", ].join("\n"), - run: Image.runEXIF, + run: Image.runExtractEXIF, inputType: "byteArray", outputType: "string", args: [], @@ -3392,11 +3392,9 @@ const OperationConfig = { description: [ "Removes EXIF data from a JPEG image.", "

", - "EXIF data is metadata embedded in JPEGs.", - "

", - "EXIF data from photos usually contains information about the image file itself as well as the device used to create it.", + "EXIF data embedded in photos usually contains information about the image file itself as well as the device used to create it.", ].join("\n"), - run: Image.removeEXIF, + run: Image.runRemoveEXIF, inputType: "byteArray", outputType: "byteArray", args: [], diff --git a/src/core/operations/Image.js b/src/core/operations/Image.js index 336e5931..2dd72df5 100644 --- a/src/core/operations/Image.js +++ b/src/core/operations/Image.js @@ -24,7 +24,7 @@ const Image = { * @param {Object[]} args * @returns {string} */ - runEXIF(input, args) { + runExtractEXIF(input, args) { try { const bytes = Uint8Array.from(input); const parser = ExifParser.create(bytes.buffer); @@ -44,6 +44,7 @@ const Image = { } }, + /** * Remove EXIF operation. * @@ -54,7 +55,7 @@ const Image = { * @param {Object[]} args * @returns {string} */ - removeEXIF(input, args) { + runRemoveEXIF(input, args) { // Do nothing if input is empty if (input.length === 0) return input; @@ -67,6 +68,7 @@ const Image = { } }, + /** * @constant * @default From 45a9da5b306433aeebae14f30e6958fe039a5061 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 8 Jun 2017 11:10:35 +0000 Subject: [PATCH 09/22] 5.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05bfa9a6..d46f9a94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "5.7.3", + "version": "5.8.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From cbab995c6dd0381f559a624619ab1ce2284e4e55 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 8 Jun 2017 15:03:55 +0000 Subject: [PATCH 10/22] Added error handling and CORS support --- src/core/config/Categories.js | 3 ++ src/core/config/OperationConfig.js | 34 +++++++++++++++---- src/core/operations/HTTP.js | 54 +++++++++++++++++++++++++----- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index 9e6f6157..ea095649 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -126,6 +126,7 @@ const Categories = [ { name: "Networking", ops: [ + "HTTP request", "Strip HTTP headers", "Parse User Agent", "Parse IP range", @@ -288,6 +289,8 @@ const Categories = [ "Scan for Embedded Files", "Generate UUID", "Render Image", + "Remove EXIF", + "Extract EXIF", "Numberwang", ] }, diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index ddb67d28..a22bc9e2 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3370,7 +3370,7 @@ const OperationConfig = { "

", "EXIF data from photos usually contains information about the image file itself as well as the device used to create it.", ].join("\n"), - run: Image.runEXIF, + run: Image.runExtractEXIF, inputType: "byteArray", outputType: "string", args: [], @@ -3388,9 +3388,20 @@ const OperationConfig = { } ] }, + "Remove EXIF": { + description: [ + "Removes EXIF data from a JPEG image.", + "

", + "EXIF data embedded in photos usually contains information about the image file itself as well as the device used to create it.", + ].join("\n"), + run: Image.runRemoveEXIF, + inputType: "byteArray", + outputType: "byteArray", + args: [] + }, "HTTP request": { description: [ - "Makes a HTTP request and returns the response body.", + "Makes an HTTP request and returns the response.", "

", "This operation supports different HTTP verbs like GET, POST, PUT, etc.", "

", @@ -3401,24 +3412,33 @@ const OperationConfig = { run: HTTP.runHTTPRequest, inputType: "string", outputType: "string", + manualBake: true, args: [ { name: "Method", type: "option", value: HTTP.METHODS, - }, { + }, + { name: "URL", type: "string", value: "", - }, { + }, + { name: "Headers", type: "text", value: "", - }, { - name: "Ignore status code", + }, + { + name: "Mode", + type: "option", + value: HTTP.MODE, + }, + { + name: "Show response metadata", type: "boolean", value: false, - }, + } ] }, }; diff --git a/src/core/operations/HTTP.js b/src/core/operations/HTTP.js index e80c0fb3..fd90f455 100755 --- a/src/core/operations/HTTP.js +++ b/src/core/operations/HTTP.js @@ -11,6 +11,7 @@ import {UAS_parser as UAParser} from "../lib/uas_parser.js"; * @namespace */ const HTTP = { + /** * @constant * @default @@ -18,8 +19,10 @@ const HTTP = { METHODS: [ "GET", "POST", "HEAD", "PUT", "PATCH", "DELETE", + "CONNECT", "TRACE", "OPTIONS" ], + /** * Strip HTTP headers operation. * @@ -60,9 +63,31 @@ const HTTP = { }, + /** + * @constant + * @default + */ + MODE: [ + "Cross-Origin Resource Sharing", + "No CORS (limited to HEAD, GET or POST)", + ], + + /** + * Lookup table for HTTP modes + * + * @private + * @constant + */ + _modeLookup: { + "Cross-Origin Resource Sharing": "cors", + "No CORS (limited to HEAD, GET or POST)": "no-cors", + }, + /** * HTTP request operation. * + * @author tlwr [toby@toby.codes] + * @author n1474335 [n1474335@gmail.com] * @param {string} input * @param {Object[]} args * @returns {string} @@ -71,7 +96,8 @@ const HTTP = { const method = args[0], url = args[1], headersText = args[2], - ignoreStatusCode = args[3]; + mode = args[3], + showResponseMetadata = args[4]; if (url.length === 0) return ""; @@ -88,9 +114,9 @@ const HTTP = { }); let config = { - method, - headers, - mode: "cors", + method: method, + headers: headers, + mode: HTTP._modeLookup[mode], cache: "no-cache", }; @@ -100,11 +126,23 @@ const HTTP = { return fetch(url, config) .then(r => { - if (ignoreStatusCode || r.status === 200) { - return r.text(); - } else { - throw `HTTP response code was ${r.status}.`; + if (showResponseMetadata) { + let headers = ""; + for (let pair of r.headers.entries()) { + headers += " " + pair[0] + ": " + pair[1] + "\n"; + } + return r.text().then(b => { + return "####\n Status: " + r.status + " " + r.statusText + + "\n Exposed headers:\n" + headers + "####\n\n" + b; + }); } + return r.text(); + }) + .catch(e => { + return e.toString() + + "\n\nThis error could be caused by one of the following:\n" + + " - An invalid URL\n" + + " - Making a cross-origin request to a server which does not support CORS\n"; }); }, From 127364e8a4b2735c7a4c1081db96b1e44426a4e4 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Jun 2017 14:53:15 +0000 Subject: [PATCH 11/22] Added error handling for non-CORS requests. --- src/core/config/OperationConfig.js | 2 +- src/core/operations/HTTP.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index a22bc9e2..0d6778fc 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -3407,7 +3407,7 @@ const OperationConfig = { "

", "You can add headers line by line in the format Key: Value", "

", - "This operation will throw an error for any status code that is not 200, unless the 'Ignore status code' option is checked.", + "The status code of the response, along with a limited selection of exposed headers, can be viewed by checking the 'Show response metadata' option. Only a limited set of response headers are exposed by the browser for security reasons.", ].join("\n"), run: HTTP.runHTTPRequest, inputType: "string", diff --git a/src/core/operations/HTTP.js b/src/core/operations/HTTP.js index fd90f455..c02c3628 100755 --- a/src/core/operations/HTTP.js +++ b/src/core/operations/HTTP.js @@ -126,6 +126,10 @@ const HTTP = { return fetch(url, config) .then(r => { + if (r.status === 0 && r.type === "opaque") { + return "Error: Null response. Try setting the connection mode to CORS."; + } + if (showResponseMetadata) { let headers = ""; for (let pair of r.headers.entries()) { From 3affce8f98bcc1e9a45cb5bf58d34e297c49ba92 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Jun 2017 14:54:27 +0000 Subject: [PATCH 12/22] 5.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d46f9a94..aebe5fbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "5.8.0", + "version": "5.9.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From fef446687aa3426767a2d73abefdd75af48f7e31 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Jun 2017 15:21:39 +0000 Subject: [PATCH 13/22] Loading messages won't repeat as often and cycle more slowly --- src/web/html/index.html | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/web/html/index.html b/src/web/html/index.html index 4c596be9..6bf88185 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -35,7 +35,7 @@ // Load theme before the preloader is shown document.querySelector(":root").className = JSON.parse(localStorage.getItem("options")).theme; - // Cycle loading messages + // Define loading messages const loadingMsgs = [ "Proving P = NP...", "Computing 6 x 9...", @@ -49,15 +49,28 @@ "Navigating neural network...", "Importing machine learning..." ]; + + // Shuffle array using Durstenfeld algorithm + for (let i = loadingMsgs.length - 1; i > 0; --i) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = loadingMsgs[i]; + loadingMsgs[i] = loadingMsgs[j]; + loadingMsgs[j] = temp; + } + + // Show next loading message then move it to the end of the array function changeLoadingMsg() { + const msg = loadingMsgs.shift(); try { const el = document.getElementById("preloader-msg"); el.className = "loading"; // Causes CSS transition on first message - el.innerHTML = loadingMsgs[Math.floor(Math.random()*loadingMsgs.length)]; - } catch (err) {} + el.innerHTML = msg; + } catch (err) {} // Ignore errors if DOM not yet ready + loadingMsgs.push(msg); } + changeLoadingMsg(); - window.loadingMsgsInt = setInterval(changeLoadingMsg, (Math.random()*500) + 500); + window.loadingMsgsInt = setInterval(changeLoadingMsg, (Math.random() * 1000) + 1000); From 69e12b10675d4e9ba192a7af1ea36326e3e06186 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Jun 2017 15:21:45 +0000 Subject: [PATCH 14/22] 5.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aebe5fbe..da157a35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "5.9.0", + "version": "5.9.1", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From e7f5b1718419749a188e00819fb4d980f1151073 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Jun 2017 15:36:15 +0000 Subject: [PATCH 15/22] Manual bake now triggers when recipes are loaded from the URL. Fixes #93. --- src/web/App.js | 37 +++++++++++++++++++------------------ src/web/RecipeWaiter.js | 3 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/web/App.js b/src/web/App.js index 2b3f3638..7b02c350 100755 --- a/src/web/App.js +++ b/src/web/App.js @@ -21,21 +21,22 @@ import Split from "split.js"; * @param {Object} options - Default setting for app options. */ const App = function(categories, operations, defaultFavourites, defaultOptions) { - this.categories = categories; - this.operations = operations; - this.dfavourites = defaultFavourites; - this.doptions = defaultOptions; - this.options = Utils.extend({}, defaultOptions); + this.categories = categories; + this.operations = operations; + this.dfavourites = defaultFavourites; + this.doptions = defaultOptions; + this.options = Utils.extend({}, defaultOptions); - this.chef = new Chef(); - this.manager = new Manager(this); + this.chef = new Chef(); + this.manager = new Manager(this); - this.baking = false; - this.autoBake_ = false; - this.progress = 0; - this.ingId = 0; + this.baking = false; + this.autoBake_ = false; + this.autoBakePause = false; + this.progress = 0; + this.ingId = 0; - window.chef = this.chef; + window.chef = this.chef; }; @@ -166,7 +167,7 @@ App.prototype.bake = async function(step) { * Runs Auto Bake if it is set. */ App.prototype.autoBake = function() { - if (this.autoBake_) { + if (this.autoBake_ && !this.autoBakePause) { this.bake(); } }; @@ -413,9 +414,9 @@ App.prototype.loadURIParams = function() { return b; })(window.location.search.substr(1).split("&")); - // Turn off auto-bake while loading - const autoBakeVal = this.autoBake_; - this.autoBake_ = false; + // Pause auto-bake while loading but don't modify `this.autoBake_` + // otherwise `manualBake` cannot trigger. + this.autoBakePause = true; // Read in recipe from query string if (this.queryString.recipe) { @@ -451,8 +452,8 @@ App.prototype.loadURIParams = function() { } catch (err) {} } - // Restore auto-bake state - this.autoBake_ = autoBakeVal; + // Unpause auto-bake + this.autoBakePause = false; this.autoBake(); }; diff --git a/src/web/RecipeWaiter.js b/src/web/RecipeWaiter.js index b9cf23c5..63233127 100755 --- a/src/web/RecipeWaiter.js +++ b/src/web/RecipeWaiter.js @@ -354,8 +354,7 @@ RecipeWaiter.prototype.buildRecipeOperation = function(el) { el.classList.add("flow-control-op"); } - // Disable auto-bake if this is a manual op - this should be moved to the 'operationadd' - // handler after event restructuring + // Disable auto-bake if this is a manual op if (op.manualBake && this.app.autoBake_) { this.manager.controls.setAutoBake(false); this.app.alert("Auto-Bake is disabled by default when using this operation.", "info", 5000); From 2c2a0eb7d934bab03ca42ac50442995707d27ae6 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Jun 2017 15:36:53 +0000 Subject: [PATCH 16/22] 5.9.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index da157a35..0fb68f9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "5.9.1", + "version": "5.9.2", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 3eacc325a34ed65c3f49bf6bcae76f07589db060 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 13 Jun 2017 15:33:37 +0000 Subject: [PATCH 17/22] Improved descriptions for timestamp operations. --- src/core/config/OperationConfig.js | 16 ++++++++-------- src/core/operations/DateTime.js | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 9115f0de..585bf600 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -2231,7 +2231,7 @@ const OperationConfig = { ] }, "From UNIX Timestamp": { - description: "Converts a UNIX timestamp to a datetime string.

e.g. 978346800 becomes Mon 1 January 2001 11:00:00 UTC", + description: "Converts a UNIX timestamp to a datetime string.

e.g. 978346800 becomes Mon 1 January 2001 11:00:00 UTC

A UNIX timestamp is a 32-bit value representing the number of seconds since January 1, 1970 UTC (the UNIX epoch).", run: DateTime.runFromUnixTimestamp, inputType: "number", outputType: "string", @@ -2244,7 +2244,7 @@ const OperationConfig = { ] }, "To UNIX Timestamp": { - description: "Parses a datetime string in UTC and returns the corresponding UNIX timestamp.

e.g. Mon 1 January 2001 11:00:00 becomes 978346800", + description: "Parses a datetime string in UTC and returns the corresponding UNIX timestamp.

e.g. Mon 1 January 2001 11:00:00 becomes 978346800

A UNIX timestamp is a 32-bit value representing the number of seconds since January 1, 1970 UTC (the UNIX epoch).", run: DateTime.runToUnixTimestamp, inputType: "string", outputType: "number", @@ -2262,27 +2262,27 @@ const OperationConfig = { ] }, "Windows Filetime to UNIX Timestamp":{ - description: "Converts a Windows Filetime timestamp to a datetime format", + description: "Converts a Windows Filetime value to a UNIX timestamp.

A Windows Filetime is a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 UTC.

A UNIX timestamp is a 32-bit value representing the number of seconds since January 1, 1970 UTC (the UNIX epoch).

This operation also supports UNIX timestamps in milliseconds, microseconds and nanoseconds.", run: DateTime.runFromFiletimeToUnix, inputType: "string", outputType: "string", args: [ { - name: "Output Units", - type: "Option", + name: "Output units", + type: "option", value: DateTime.UNITS } ] }, "UNIX Timestamp to Windows Filetime":{ - description: "Parses a datetime string in UTC and returns the corresponding Windows Filetime timestamp", + description: "Converts a UNIX timestamp to a Windows Filetime value.

A Windows Filetime is a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 UTC.

A UNIX timestamp is a 32-bit value representing the number of seconds since January 1, 1970 UTC (the UNIX epoch).

This operation also supports UNIX timestamps in milliseconds, microseconds and nanoseconds.", run: DateTime.runToFiletimeFromUnix, inputType: "string", outputType: "string", args: [ { - name: "Input Units", - type: "Option", + name: "Input units", + type: "option", value: DateTime.UNITS } ] diff --git a/src/core/operations/DateTime.js b/src/core/operations/DateTime.js index a7a3c483..5262ecc4 100755 --- a/src/core/operations/DateTime.js +++ b/src/core/operations/DateTime.js @@ -81,11 +81,11 @@ const DateTime = { /** - * Converts a Windows FILETIME to Unix Epoch time. + * Windows Filetime to Unix Timestamp operation. * * @author bwhitn [brian.m.whitney@outlook.com] * @param {string} input - * @param {Object[]} args (not used) + * @param {Object[]} args * @returns {string} */ runFromFiletimeToUnix: function(input, args) { @@ -107,11 +107,11 @@ const DateTime = { /** - * Converts a Unix Epoch time to Windows FILETIME. + * Unix Timestamp to Windows Filetime operation. * * @author bwhitn [brian.m.whitney@outlook.com] * @param {string} input - * @param {Object[]} args (not used) + * @param {Object[]} args * @returns {string} */ runToFiletimeFromUnix: function(input, args) { From fbd6ead6b7492e286ab9f74147252bf4125241d9 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 13 Jun 2017 15:34:36 +0000 Subject: [PATCH 18/22] 5.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0fb68f9c..aa3cf683 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "5.9.2", + "version": "5.10.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From eda17d1671bac85f7faae871ca6d4ce783ba85ba Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 13 Jun 2017 16:30:55 +0000 Subject: [PATCH 19/22] Added mixed content note to 'HTTP request' error message. --- src/core/operations/HTTP.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/operations/HTTP.js b/src/core/operations/HTTP.js index c02c3628..f12b151f 100755 --- a/src/core/operations/HTTP.js +++ b/src/core/operations/HTTP.js @@ -146,6 +146,7 @@ const HTTP = { return e.toString() + "\n\nThis error could be caused by one of the following:\n" + " - An invalid URL\n" + + " - Making a request to an insecure resource (HTTP) from a secure source (HTTPS)\n" + " - Making a cross-origin request to a server which does not support CORS\n"; }); }, From 3faef2c9c9c81cc0bcfd33d075408612feb8dc52 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 13 Jun 2017 16:31:00 +0000 Subject: [PATCH 20/22] 5.10.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa3cf683..45e2dd79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "5.10.0", + "version": "5.10.1", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 04aac03d6e224f3f768cd5461757df19f437f680 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 15 Jun 2017 14:21:30 +0000 Subject: [PATCH 21/22] Fixed global matching for simple strings in 'Find / Replace' operation. Closes #25. --- src/core/Utils.js | 16 ++++++++++++++++ src/core/config/OperationConfig.js | 2 +- src/core/operations/StrUtils.js | 12 +++++++----- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/core/Utils.js b/src/core/Utils.js index 9b0d2a30..9011880f 100755 --- a/src/core/Utils.js +++ b/src/core/Utils.js @@ -259,6 +259,22 @@ const Utils = { }, + /** + * Escape a string containing regex control characters so that it can be safely + * used in a regex without causing unintended behaviours. + * + * @param {string} str + * @returns {string} + * + * @example + * // returns "\[example\]" + * Utils.escapeRegex("[example]"); + */ + escapeRegex: function(str) { + return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); + }, + + /** * Expand an alphabet range string into a list of the characters in that range. * diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 585bf600..0397b50e 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -1919,7 +1919,7 @@ const OperationConfig = { args: [] }, "Find / Replace": { - description: "Replaces all occurrences of the first string with the second.

The three match options are only relevant to regex search strings.", + description: "Replaces all occurrences of the first string with the second.

Includes support for regular expressions (regex), simple strings and extended strings (which support \\n, \\r, \\t, \\b, \\f and escaped hex bytes using \\x notation, e.g. \\x00 for a null byte).", run: StrUtils.runFindReplace, manualBake: true, inputType: "string", diff --git a/src/core/operations/StrUtils.js b/src/core/operations/StrUtils.js index 78a0e838..093de39b 100755 --- a/src/core/operations/StrUtils.js +++ b/src/core/operations/StrUtils.js @@ -227,14 +227,16 @@ const StrUtils = { if (type === "Regex") { find = new RegExp(find, modifiers); - } else if (type.indexOf("Extended") === 0) { + return input.replace(find, replace); + } + + if (type.indexOf("Extended") === 0) { find = Utils.parseEscapedChars(find); } - return input.replace(find, replace, modifiers); - // Non-standard addition of flags in the third argument. This will work in Firefox but - // probably nowhere else. The purpose is to allow global matching when the `find` parameter - // is just a string. + find = new RegExp(Utils.escapeRegex(find), modifiers); + + return input.replace(find, replace); }, From 61951e76aca67e57835b9eaa6667040157d1c170 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 15 Jun 2017 14:25:03 +0000 Subject: [PATCH 22/22] 5.10.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 45e2dd79..9c266899 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "5.10.1", + "version": "5.10.2", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef",